Compare commits

..

437 Commits

Author SHA1 Message Date
4608b97e23 v1.2.0
Separate comicapi into it's own package
Add support for tar files
Insert standard gitignore
Use suggested _version from setuptools-scm
Cleanup setup.py

Fix formatting in the rename template help
2021-08-05 22:42:13 -07:00
d18e94cd4e Update gitignore 2021-08-02 11:33:59 -07:00
feb206c165 Remove pyinstaller from requirements
pyinstaller is only needed for a specific deploy scenario
2021-07-11 05:54:05 +00:00
31e567fe1f Update current_version.txt 2021-07-02 03:42:52 +00:00
cb2031f2e7 Merge remote-tracking branch 'origin/un-cffi' 2021-07-01 19:20:38 -07:00
4ae1632aba Update usage of filetype
Fix an index out of range error when there are no duplicates to display
Exit immediately if there are no duplicates found
2021-03-07 19:59:39 -08:00
737ddd2486 Fix remove_ads.py 2021-03-07 19:41:54 -08:00
f31abe2df9 Add a shortcut for marking a page as an ad 2021-03-07 19:40:25 -08:00
fbc1137e0f Revert "Changed: use unrar-cffi for cbr handling (#151)"
This reverts commit 83d2557af3.
2020-08-14 22:15:52 -07:00
be698a17d6 Remove setuptools_scm 2020-07-26 19:28:45 -07:00
29be759c9c Implement isort and black 2020-07-06 16:11:15 -07:00
39052b58c6 Remove PyPDF2 2020-06-29 19:07:14 -07:00
83d2557af3 Changed: use unrar-cffi for cbr handling (#151) 2020-06-02 21:26:11 -07:00
a3d0b3372b fix broken drag & drop on macOS (#142) 2020-06-02 21:10:38 -07:00
9b097c80eb Increase comicvine search results per request to max (#164) 2020-06-02 21:09:32 -07:00
bbf9e6e38f Fix an issue with reducing the returned search results 2020-06-02 20:21:05 -07:00
6ab03cc3d3 Update natsort usage 2020-06-02 20:09:32 -07:00
8b227afbb2 Add a literal search option
In order to bypass processing of search results and search terms
2020-06-02 20:09:02 -07:00
3a3d67cbd0 update requirements.txt 2020-06-02 20:05:31 -07:00
676ecaf547 Update comic publisher imprints 2020-06-02 20:03:48 -07:00
931df0109d duplicate script and fixes 2020-05-30 15:30:27 -07:00
475540560a Merge branch 'seriesSearch' 2020-02-13 00:30:21 -08:00
7aa4e1c4ed Improve searchForSeries
Refactor removearticles to only remove articles
Add normalization on the search string and the series name results

Searching now only compares ASCII a-z and 0-9 and all other characters
are replaced with single space, this is done to both the search string
and the result. This fixes an with names that are separated by a
hyphen (-) in the filename but in the Comic Vine name are separated by a
slash (/) and other similar issues.
2020-02-13 00:27:08 -08:00
f5e88d07bb Fix errors
Libraries updated and these are no longer needed
2020-02-13 00:04:44 -08:00
f634783d26 Merge branch 'requests' 2020-02-12 23:44:14 -08:00
ebfbbacc16 Add requests to requirements.txt 2020-02-12 23:30:04 -08:00
76b524c767 Merge branch 'Renaming' 2019-09-23 17:48:19 -07:00
fbb234a527 Add template error checking and other small issues
Error checking has been added in CLI mode, the settings window and when
running a rename operation from the GUI.
The Template Help window is now non-modal
Discrepancies between the tool tip and help window have been resolved
2019-09-23 17:47:59 -07:00
ce021d82cf Change filename parsing to default to the issue number
e.g. 123.cbr parses with series: 123, issue number: 123
2019-09-11 14:45:55 -07:00
cd4097f0c0 Fix seriesYear handling 2019-09-11 14:45:50 -07:00
cb9db19073 Fix requirements
pip doesn't recognize my PyQt5 install (Void Linux) and pip complains
on the git+https for pyinstaller
2019-09-11 14:45:45 -07:00
abd8019bf9 Add seriesYear attribute
Attribute is only serialized in ComicRack style metadata
2019-09-11 14:45:40 -07:00
c596062f55 Merge branch 'requests' into temp 2019-09-11 14:43:52 -07:00
c26f33e3d5 Merge branch 'duplicateFinder' into temp 2019-09-11 14:43:28 -07:00
37e6e81894 Merge branch 'Renaming' into temp 2019-09-11 14:43:21 -07:00
f73324f003 Merge branch 'AutoImprint' into temp 2019-09-11 14:43:05 -07:00
f0f8a061b5 Add publisher and imprint handling
Imprint handling has been added to utils and uses a subclassed dict to
return tuples for imprint matching may not be the best idea but it works
for now.

Add settings option auto_imprint
Add cli flag -a, --auto-import
2019-09-11 14:42:29 -07:00
944c0b9b2e Move to python requests module
requests is much simpler and fixes all ssl errors.
Comic Vine now requires a unique useragent string
2019-09-10 14:52:59 -07:00
8752e26476 Merge branch 'PageListEditorExtendedSelection' 2019-09-05 15:10:43 -07:00
146e7b21c6 Merge branch 'IssueString' 2019-09-05 15:10:39 -07:00
60e962b903 Merge branch 'FixLanguageSort' 2019-09-05 15:10:33 -07:00
0747a6b0ef Improve file renaming
Moves to Python format strings for renaming, handles directory
structures, moving of files to a destination directory, sanitizes
file paths with pathvalidate and takes a different approach to
smart filename cleanup using the Python string.Formatter class

Moving to Python format strings means we can point to python
documentation for syntax and all we have to do is document the
properties and types that are attached to the GenericMetadata class.

Switching to pathvalidate allows comictagger to more simply handle both
directories and symbols in filenames.

The only changes to the string.Formatter class is:
1. format_field returns
an empty string if the value is none or an empty string regardless of
the format specifier.
2. _vformat drops the previous literal text if the field value
is an empty string and lstrips the following literal text of closing
special characters.
2019-09-05 14:40:17 -07:00
51132c061b Cleanup metadata handling
Mainly corrects for consistency in most situations
CoMet is not touched as there is no support in the gui and has an odd requirements on attributes
2019-09-05 14:40:14 -07:00
3b01a9b58b convert duplicate finder script to Python 3, provide JSON output and call a script to actually handle the duplicate comics 2019-09-05 13:58:46 -07:00
7f79c2b024 Allow extended selection in the page list editor 2019-08-16 13:45:00 -07:00
0069d7fdc5 issue string parsing now strips off (# of #) (e.g. 1 of 45) 2019-08-16 13:43:43 -07:00
3606cbd0f2 Sort language correctly 2019-08-16 13:22:15 -07:00
2891209b4e bump version 2019-02-04 20:27:37 +01:00
5b87e19d3e Limit Comic Vine search result queries (#119)
* Tweaked search string based on new comic vine search behavior
Placated Beaufitul Soup by passing the parser

* Limit search results fetching after recent Comic Vine changes.
Also, minor debug comment tweaks.
2019-02-04 20:16:44 +01:00
tlc
674e24fc41 Enable Zip64 (#96) 2018-09-20 00:09:24 +02:00
91f82fd6d3 Python3 and QT5 upgrade (#109)
* Tweaked search string based on new comic vine search behavior
Placated Beaufitul Soup by passing the parser

* First cut at porting to Python 3 and PyQt5

* remove debug print

* tweaked progress dialog handling for issues on ubuntu gui

* Handle bad key more gracefullu

* More integration of unrarlib into settings and rest of app

* Better handling of "personal" unrar lib setting

* PEP 440-compliant version string

* Tuned linux rar help strings

* Got setup working again
* Attempts to build unrar on install
* Some minimal desktop integration on various platforms

* Fix wrong shortfile

* More setup.py enhancements
* Use proper temp file
* Added comment block at top

* Comment out desktop integration attempt for now

* Updated some links and info

* Fixed the html a bit

* Repaired some images that caused libpng to complain

* update readme re:  py3qt5 branch changes

* another note

* #108 feat: try to simplify windows build using only pip and python3

* #108 feat: fix python location on appveyor (try 1)

* #108 feat: use venv (try 2)

* #108 feat: use venv (try 3)

* #108 feat: update to latest pyinstaller develop branch

* #108 feat: update to latest pyinstaller develop branch (again)

* #108: add ssl libraries for windows packaging

* #108: refresh env in win build to pick the right mingw

* #108: change order of win build script operations

* #113: fix subprocess usage in pyinstaller package

* bump version
2018-09-19 22:05:39 +02:00
cf43513d52 feat: add appveyor configuration 2018-01-17 13:35:10 -08:00
a7288a94cc #98 Multiplatform pyinstaller dist (#99)
Multiplatform pyinstaller dist (#98)
2018-01-14 16:41:27 +01:00
d0918c92e4 #87 Update comic vine url and ssl config (#93)
* #87 fix ssl comicvine communication

* handle missing libunrar. update macos makefile. remove version check window. bump version.

* update release notes

* #87 fix ssl context in several places. update comicvine api url.

* fix drag and drop issues on macOS

* bump version to 1.1.16-beta-rc2

* use PNG conversion for Windows build
2017-12-21 15:19:45 +01:00
4ff2061568 Merge pull request #74 from Alkpone/master
Bugs in move2folder.py script
2015-03-22 10:49:21 +01:00
08c402149b Prevent error when no file has been detected
Script raised an unhandled exception:  local variable 'fmt_str' referenced before assignment
Traceback (most recent call last):
  File "/volume1/@appstore/comictagger/comictaggerlib/options.py", line 233, in launch_script
    script.main()
  File "/volume1/@appstore/comictagger/scripts/move2folder.py", line 90, in main
    print >> sys.stderr, fmt_str.format("")
UnboundLocalError: local variable 'fmt_str' referenced before assignment
2015-03-21 14:32:55 +01:00
184dbf0684 Prevent error when running the script
Script raised an unhandled exception:  coercing to Unicode: need string or buffer, NoneType found
Traceback (most recent call last):
  File "/root/comictagger/comictaggerlib/options.py", line 233, in launch_script
    script.main()
  File "scripts/move2folder.py", line 80, in main
    ca = ComicArchive(filename, settings.rar_exe_path)
  File "/root/comictagger/comicapi/comicarchive.py", line 648, in __init__
    with open(fname, 'rb') as fd:
TypeError: coercing to Unicode: need string or buffer, NoneType found
2015-03-21 14:17:05 +01:00
ed0050ba05 fixed typo 2015-03-06 11:26:47 +01:00
68030a1024 updated to unrar 0.3 2015-03-01 16:14:01 +01:00
983ad1fcf4 Merge branch 'fcanc-master' 2015-03-01 15:44:11 +01:00
d959ac0401 Huge code cleanup
- `autopep8 -aa` for general cleanup;
- Changed order of imports, they should be ordered into 3 groups:
1. standard library imports;
2. 3rd party packages;
3. project imports.
- I commented various imports that were reported as unused by my IDE.
If everything goes fine we can consider to delete them;
- The Apache license disclaimers are now comments since triple-quotes
should be used only for docstrings;
- Fix - `utils.centerWindowOnParent` did not resolve, changed to
`centerWindowOnParent`
2015-02-22 03:30:32 +01:00
2a550db02a Merge pull request #1 from davide-romanini/master
Merge davide-romanini commits
2015-02-18 20:44:28 +01:00
6369fa5fda updated readme 2015-02-16 16:34:38 +01:00
d5a13a4206 various fixes after merging comicstream-integr 2015-02-16 16:19:38 +01:00
b2532ce03a Merge branch 'comicstream-integr' 2015-02-16 16:18:00 +01:00
79a67d8c29 Merge pull request #71 from branch 'fcanc-master' 2015-02-16 14:51:57 +01:00
d9bd38674c added new dependencies to requirements.txt. with new unrar needs UNRAR_LIB_PATH to be set to start 2015-02-16 14:27:13 +01:00
a0154aaaae Merge commit '17f74cf2968a4e0aa01d7309afe7e1407b8abef2' into comicstream-integr 2015-02-16 14:09:21 +01:00
17f74cf296 Squashed 'comicapi/' changes from b7d2458..18f87d3
18f87d3 using comicapi subtree classes

git-subtree-dir: comicapi
git-subtree-split: 18f87d35b1b2cf5e135fad353419eda11209a6be
2015-02-16 14:09:21 +01:00
3f112cd578 Merge commit 'f6439049d8d8b5a4709f1b78afbfd289d00e8c25' as 'comicapi' 2015-02-16 13:27:21 +01:00
f6439049d8 Squashed 'comicapi/' content from commit b7d2458
git-subtree-dir: comicapi
git-subtree-split: b7d2458b80467a47be1d1d58b31ffcac62c2743c
2015-02-16 13:27:21 +01:00
2fe818872c removed splitted comicapi 2015-02-16 13:25:35 +01:00
a419969b85 autopep8 -aa
—aggressive, level 2
2015-02-15 12:55:04 +01:00
ee52448f17 autopep8 -a
—aggressive, level 1
2015-02-15 12:44:09 +01:00
79103990fa autopep8
automatically formats Python code to conform to the PEP 8 style guide —
default usage (whitespace changes only)
2015-02-15 11:44:00 +01:00
22dbafbc00 Code cleanup, round 1
Some formatting cleanup, plus print modernization, & typos correction.
2015-02-14 00:08:07 +01:00
0df283778c Indentation
Replaced tabs with spaces, and removed some trailing spaces.
2015-02-12 23:57:46 +01:00
a6282b5449 Move2folder script
Added a script to organize comics in a folder tree by Publisher/Series
(Volume).
2015-02-12 19:15:17 +01:00
5574280ad6 Filename parser tweaks
Fixes the Scan Info tag being left blank when the filename doesn’t
provide an issue number.
2015-02-12 19:09:33 +01:00
19b907b742 refactor (continue) 2015-02-11 19:45:45 +01:00
a9ff8f37b0 refactor core comicarchive classes in its own package comicapi 2015-02-11 19:45:02 +01:00
0769111f8c #70 added support for the day field on the gui 2015-02-09 21:50:02 +01:00
cf6ae8b5ae aligned with comicstreamer updates
refactor qt specific functions in utils.py in new ui.qtutils module
2015-02-02 17:20:48 +01:00
1d6846ced3 gitignore
changed to README.md for github.
2015-01-23 17:42:22 +01:00
d516d80093 Removed unused FileTableWidget, and explicitly set the column count. This fixes a problem on ArchLinux systems
git-svn-id: http://comictagger.googlecode.com/svn/trunk@744 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-07-06 18:19:50 +00:00
bf9ab71fd9 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@737 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-14 03:56:46 +00:00
33b00ad323 Text tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@736 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-14 03:56:32 +00:00
301ff084f1 fixes for webp, api key handling, and CV rate limit
git-svn-id: http://comictagger.googlecode.com/svn/trunk@734 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-13 06:26:44 +00:00
0c146bb245 minor fix
git-svn-id: http://comictagger.googlecode.com/svn/trunk@733 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-13 06:26:13 +00:00
08cc4a1acb Use pip-installed pyinstaller
git-svn-id: http://comictagger.googlecode.com/svn/trunk@732 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-06-13 06:25:35 +00:00
f97a1653d9 dos-ified release_notes file
git-svn-id: http://comictagger.googlecode.com/svn/trunk@728 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-18 15:44:38 +00:00
d9dbab301a prep for release
git-svn-id: http://comictagger.googlecode.com/svn/trunk@727 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-18 15:42:05 +00:00
3d93197101 Added warning when rar is tried be loaded, and unrar tool isn't known
Fixed a bug when erroneous message is show when file is attempted to be reloaded

git-svn-id: http://comictagger.googlecode.com/svn/trunk@726 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-12 06:08:07 +00:00
752a1d8923 actual version bump
git-svn-id: http://comictagger.googlecode.com/svn/trunk@714 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 04:04:40 +00:00
68002daffa bumped version number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@713 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 04:02:13 +00:00
ad5062c582 Persist some auto-tag options
git-svn-id: http://comictagger.googlecode.com/svn/trunk@712 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 03:21:24 +00:00
2680468f34 New CBL transform to copy story arcs to generic tags
git-svn-id: http://comictagger.googlecode.com/svn/trunk@711 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 02:06:44 +00:00
6156fc296a Added settings option to auto-clear form when importing from CV
added settings option to remove html tables from CV summary

git-svn-id: http://comictagger.googlecode.com/svn/trunk@710 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 01:52:14 +00:00
0feed294d4 Avoid an exception condition
git-svn-id: http://comictagger.googlecode.com/svn/trunk@709 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-09 01:50:40 +00:00
e57736b955 Decouple comicarchive from settings
Enforce single instance of GUI app

git-svn-id: http://comictagger.googlecode.com/svn/trunk@708 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:13:04 +00:00
70fcdc0129 Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@707 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:12:05 +00:00
9a64195ebd Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@706 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:10:18 +00:00
b0f229f851 Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@705 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:09:03 +00:00
877a5ccd85 Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@704 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:08:22 +00:00
c0f2e2f771 Decouple comicarchive from settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@703 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-08 07:07:39 +00:00
0adfc9beb3 properly decode the user settings path
git-svn-id: http://comictagger.googlecode.com/svn/trunk@702 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:46:56 +00:00
d0bc41d7ee Allow user to specify the GUI start up tag style on the command line
git-svn-id: http://comictagger.googlecode.com/svn/trunk@701 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:44:47 +00:00
fa46a065a4 fixed some spelling errors
git-svn-id: http://comictagger.googlecode.com/svn/trunk@700 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:43:21 +00:00
8fcd5ba7d6 try to parse table HTML in the comment field
git-svn-id: http://comictagger.googlecode.com/svn/trunk@699 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:42:11 +00:00
759cdc6b40 use the requirements in the setup
git-svn-id: http://comictagger.googlecode.com/svn/trunk@698 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-04-06 19:40:22 +00:00
1405d9ff0e more process tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@692 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 22:28:50 +00:00
d8fcbbad0a Upload the zip package to pypi index site also
git-svn-id: http://comictagger.googlecode.com/svn/trunk@691 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 22:28:03 +00:00
3eca25db34 changed build checklist
git-svn-id: http://comictagger.googlecode.com/svn/trunk@688 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 21:39:16 +00:00
c8a5a89369 changed download URL to point at google drive site
git-svn-id: http://comictagger.googlecode.com/svn/trunk@687 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 21:38:55 +00:00
ff578ea819 bumped version to 1.1.12
git-svn-id: http://comictagger.googlecode.com/svn/trunk@686 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 21:38:22 +00:00
1c730c25d5 removed auto-upload to google code site
git-svn-id: http://comictagger.googlecode.com/svn/trunk@685 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 21:38:02 +00:00
35b7b39b86 Don't choke when the version string server fails.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@683 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 20:59:35 +00:00
719c711484 Language tweak
git-svn-id: http://comictagger.googlecode.com/svn/trunk@668 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 18:03:08 +00:00
afbbc9d00c git-svn-id: http://comictagger.googlecode.com/svn/trunk@667 6c5673fe-1810-88d6-992b-cd32ca31540c 2014-03-23 17:48:59 +00:00
b8e0a45fc8 bumped version and release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@665 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 17:31:00 +00:00
b7360dd33e Updated copyright dates
git-svn-id: http://comictagger.googlecode.com/svn/trunk@664 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 17:30:23 +00:00
d9f1956426 handle a crash bug when file starts with --
git-svn-id: http://comictagger.googlecode.com/svn/trunk@663 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 16:56:04 +00:00
b5c7f36410 New pyunrar version to handle rar tools 5.x
git-svn-id: http://comictagger.googlecode.com/svn/trunk@662 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:43:03 +00:00
0b0663d935 Update copyright date
git-svn-id: http://comictagger.googlecode.com/svn/trunk@661 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:42:07 +00:00
eee1f65436 handle corner case of non-numeric issue ending in "."
git-svn-id: http://comictagger.googlecode.com/svn/trunk@660 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:41:38 +00:00
9a8d4149f2 fixed spelling error
git-svn-id: http://comictagger.googlecode.com/svn/trunk@659 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:40:01 +00:00
b02a205668 Make sure all error print outs are unicode
Catch error when zipfile list fails

git-svn-id: http://comictagger.googlecode.com/svn/trunk@658 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:38:36 +00:00
57284dfbed fixed typo in makefile
git-svn-id: http://comictagger.googlecode.com/svn/trunk@657 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:37:19 +00:00
afcbde7fc6 update todo
git-svn-id: http://comictagger.googlecode.com/svn/trunk@651 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-01-31 04:45:15 +00:00
151fac5bf1 updated release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@650 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-01-31 04:45:06 +00:00
57c1efdab9 makefile TAGGER_BASE can be set in the environment
git-svn-id: http://comictagger.googlecode.com/svn/trunk@649 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-01-31 04:43:22 +00:00
6b272cef87 When searching for a title, convert the string to a list of words separated by "ANDS", and then back to a string
git-svn-id: http://comictagger.googlecode.com/svn/trunk@648 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-01-31 04:40:58 +00:00
1cdc732739 Added a message when not able to open selected folder or file
git-svn-id: http://comictagger.googlecode.com/svn/trunk@647 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-01-31 04:39:47 +00:00
d1b00d162d Allow any size archive to be considered a comic
git-svn-id: http://comictagger.googlecode.com/svn/trunk@646 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-01-31 04:37:13 +00:00
3dd3980bc1 update todo file
git-svn-id: http://comictagger.googlecode.com/svn/trunk@645 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-08-18 18:01:01 +00:00
cbf475eb26 removed filtering out of period (".")
git-svn-id: http://comictagger.googlecode.com/svn/trunk@644 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-08-18 18:00:04 +00:00
ac8b575659 bumped version number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@643 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-08-18 17:56:38 +00:00
ac8ef286a4 Perform the rar test first, since some rars can be falsly identified as zips, somehow...
git-svn-id: http://comictagger.googlecode.com/svn/trunk@641 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-07-23 17:06:35 +00:00
f567dc37be Handle case of None value credit tags in XML
git-svn-id: http://comictagger.googlecode.com/svn/trunk@640 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-07-08 23:32:24 +00:00
15c5fc5258 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@637 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-09 01:31:26 +00:00
cc985b52a5 Do the limited series check/elimination after cover matching
git-svn-id: http://comictagger.googlecode.com/svn/trunk@636 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-08 02:39:06 +00:00
910b0386be Remove tooltip if not expandable
git-svn-id: http://comictagger.googlecode.com/svn/trunk@635 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-08 02:38:06 +00:00
0fece23405 Allow rename w/smart cleanup to have "--"
git-svn-id: http://comictagger.googlecode.com/svn/trunk@634 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-06 22:30:32 +00:00
eee320e0c7 bumped version number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@632 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-06 21:07:00 +00:00
accabf8e21 Added keyboard shortcut for form clear
git-svn-id: http://comictagger.googlecode.com/svn/trunk@631 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-06 21:06:48 +00:00
acc253d35c todo update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@630 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-06 18:36:30 +00:00
ede0154efe issueCount now gets passed to issueidentifier.
a possible technique for eliminating potential volumes is coded, but commented out for now

git-svn-id: http://comictagger.googlecode.com/svn/trunk@629 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-06 18:24:57 +00:00
5b805b1428 auto-tag progress window now uses coverimagewidget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@628 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-06 18:22:14 +00:00
2e6b2a89db Added a raw image data mode for the coverimagewidget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@627 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-06 18:21:34 +00:00
c028bb4ddc Make sure to catch all non-numeric characters after a # for the issue number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@626 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-04 01:48:42 +00:00
b70beb5684 more file name parser enhancements
git-svn-id: http://comictagger.googlecode.com/svn/trunk@625 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-04 01:22:39 +00:00
128af4521b better filename parsing
git-svn-id: http://comictagger.googlecode.com/svn/trunk@623 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-02 16:31:50 +00:00
43cf7a80c8 remove print
git-svn-id: http://comictagger.googlecode.com/svn/trunk@622 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-01 22:33:05 +00:00
3223ed190c Make sure form is updated when removing top item from list
git-svn-id: http://comictagger.googlecode.com/svn/trunk@621 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-01 22:32:20 +00:00
9e2817c037 deal with CV bug (wrong result set count) when not specifying page=1
git-svn-id: http://comictagger.googlecode.com/svn/trunk@620 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-01 22:31:25 +00:00
6e7bd10fb9 deal with pagination bug on comicvine side reporting wrong result set size when not specifiying page=1
git-svn-id: http://comictagger.googlecode.com/svn/trunk@619 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-05-01 22:30:30 +00:00
c099205779 Reworked the issue string parsing
git-svn-id: http://comictagger.googlecode.com/svn/trunk@618 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-30 18:05:10 +00:00
47d8da0e80 removed extra line
git-svn-id: http://comictagger.googlecode.com/svn/trunk@615 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-22 02:37:13 +00:00
0f7e88e58c bump to 1.1.8-beta
git-svn-id: http://comictagger.googlecode.com/svn/trunk@614 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-22 00:49:20 +00:00
65902a15b1 add-on script for renaming files based on transform list
git-svn-id: http://comictagger.googlecode.com/svn/trunk@613 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-21 06:55:32 +00:00
a68b2babeb some reworking so scripts get passed all options after scriptname
git-svn-id: http://comictagger.googlecode.com/svn/trunk@612 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-21 06:53:44 +00:00
4098802e43 sleep 1 sec before retrying after http 500 error
git-svn-id: http://comictagger.googlecode.com/svn/trunk@611 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-21 06:51:43 +00:00
9c14258e9f verify need to check version in GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@610 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-17 18:10:08 +00:00
33bdbe8be8 verify need to check version in CLI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@609 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-17 18:09:43 +00:00
a76864c109 be a little smarted in colon replacement in renaming
git-svn-id: http://comictagger.googlecode.com/svn/trunk@608 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-17 18:09:10 +00:00
cb68d07751 Added special handling of HTTP 500 error that Comic Vine seems to give occasionally.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@607 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-17 18:08:39 +00:00
8e9fccdbbc removed line feed from prints
git-svn-id: http://comictagger.googlecode.com/svn/trunk@600 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-13 05:30:53 +00:00
39990fc2b4 Updated todo and release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@599 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 17:56:15 +00:00
e8c315d834 parse scan info by default
git-svn-id: http://comictagger.googlecode.com/svn/trunk@598 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 17:55:38 +00:00
f8a06a8746 Make sure there is a default image URL if none exists
git-svn-id: http://comictagger.googlecode.com/svn/trunk@597 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 17:53:35 +00:00
9415087da7 removed debug print
git-svn-id: http://comictagger.googlecode.com/svn/trunk@596 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 17:52:43 +00:00
9aee5c32eb Made the description font a little smaller
git-svn-id: http://comictagger.googlecode.com/svn/trunk@595 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 17:52:23 +00:00
fcdb4a3889 cli option to assume issue number 1 if not found/parsed
git-svn-id: http://comictagger.googlecode.com/svn/trunk@594 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 06:11:25 +00:00
534a326258 Remember filelist sorting
git-svn-id: http://comictagger.googlecode.com/svn/trunk@593 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 06:10:50 +00:00
0390ff5919 Added option to parse scan info from filename
git-svn-id: http://comictagger.googlecode.com/svn/trunk@592 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 04:49:08 +00:00
b800ae1751 Added issue description to the match and issue selection dialogs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@591 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 01:56:24 +00:00
a2c17982d3 Fixed the resizing with the splitter
git-svn-id: http://comictagger.googlecode.com/svn/trunk@590 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 01:55:59 +00:00
0347befae6 bumped version number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@589 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-12 01:54:59 +00:00
af54b79790 Added cover date to issue selection dialog
git-svn-id: http://comictagger.googlecode.com/svn/trunk@588 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-11 01:57:19 +00:00
dd04ae98a0 Remove optimization for eliminating one-shots from consideratoion (not need with new CV search method)
git-svn-id: http://comictagger.googlecode.com/svn/trunk@587 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-11 01:32:07 +00:00
31b76fba92 Make sure out data is set in the case of pages that don't need to be resized
git-svn-id: http://comictagger.googlecode.com/svn/trunk@586 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-10 20:46:59 +00:00
9f4a4b0eb0 More version checking stuff
git-svn-id: http://comictagger.googlecode.com/svn/trunk@585 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-06 19:31:00 +00:00
575a23c6bf More version checking stuff
git-svn-id: http://comictagger.googlecode.com/svn/trunk@584 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-06 19:30:01 +00:00
5d84f09359 Check online for new version
Use non-deprecated "read_file" for configparser

git-svn-id: http://comictagger.googlecode.com/svn/trunk@583 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-05 19:48:49 +00:00
3072583482 Normalize issue number for search
git-svn-id: http://comictagger.googlecode.com/svn/trunk@582 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-05 19:43:45 +00:00
8d867cf78a This file will be checked by the app to see if it should update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@581 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-04 19:42:10 +00:00
36c79b5a2a Twitter and facebook buttons
git-svn-id: http://comictagger.googlecode.com/svn/trunk@580 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-04 19:18:55 +00:00
dfdaf731b4 updated release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@576 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-03 17:37:23 +00:00
67bff8586c Make sure start_year test is with all ints
git-svn-id: http://comictagger.googlecode.com/svn/trunk@575 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-03 00:34:55 +00:00
9e4cbea6e4 Made sure some prints are unicode
git-svn-id: http://comictagger.googlecode.com/svn/trunk@574 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-03 00:33:03 +00:00
d150b2ce54 made Auto-ID use the info already fetched from the 'issues' query for the image and page URLs (rather than use the cache or fetch again)
git-svn-id: http://comictagger.googlecode.com/svn/trunk@573 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-02 22:37:28 +00:00
a20949cc4d got rid of debug print
git-svn-id: http://comictagger.googlecode.com/svn/trunk@572 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-02 22:33:13 +00:00
e3fceb20a2 merged all the cover_date parsing into one function in CV talker
git-svn-id: http://comictagger.googlecode.com/svn/trunk@571 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-02 20:47:18 +00:00
f4e00d9ef3 bumped version
git-svn-id: http://comictagger.googlecode.com/svn/trunk@570 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-02 19:59:35 +00:00
1980bd5988 Added search across issues by volume id, issue number, and date for much faster matching
git-svn-id: http://comictagger.googlecode.com/svn/trunk@569 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-02 19:58:23 +00:00
db54affc74 Handle None cover_date
git-svn-id: http://comictagger.googlecode.com/svn/trunk@568 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-02 19:57:50 +00:00
0edb9444ef Nice twitter button for code page
git-svn-id: http://comictagger.googlecode.com/svn/trunk@567 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-02 16:42:49 +00:00
b22c25f53f Remove parsing of title. We're back to how it was before, except now we get 'none' instead of empty string.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@566 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-04-02 14:11:00 +00:00
76e6666a79 Tweaks for dealing with unicode issue "number"
Updated release_notes


git-svn-id: http://comictagger.googlecode.com/svn/trunk@563 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-30 16:31:56 +00:00
a804a10e0e use unicode in case of weird things like "1/2" symbol
git-svn-id: http://comictagger.googlecode.com/svn/trunk@562 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-30 06:26:41 +00:00
fe413b12c1 Use issues filtered query to get issue list instead of deprecated volume.issues
git-svn-id: http://comictagger.googlecode.com/svn/trunk@561 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-30 06:25:04 +00:00
e38dc2f063 CV API changes: use cover_date instead of publish_month/year for issues, roles are now a list
bumped version

git-svn-id: http://comictagger.googlecode.com/svn/trunk@560 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-29 23:09:41 +00:00
5e5418090b Added resource types for comicvine requests
git-svn-id: http://comictagger.googlecode.com/svn/trunk@557 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-28 19:04:30 +00:00
56c1f8582a todo update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@554 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 19:35:41 +00:00
00f8c0a280 removed typo
git-svn-id: http://comictagger.googlecode.com/svn/trunk@553 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 19:25:42 +00:00
1d915eb155 make sure issue number comparisons are case-normalized in case of alpha appendage
git-svn-id: http://comictagger.googlecode.com/svn/trunk@552 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 19:21:20 +00:00
b7b8060ef2 Fixed filename parsing to find "AU" issues
git-svn-id: http://comictagger.googlecode.com/svn/trunk@551 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 19:20:10 +00:00
2d190b076a Bumped version and notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@550 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 18:17:20 +00:00
cd92b1afea cleanup
git-svn-id: http://comictagger.googlecode.com/svn/trunk@549 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 17:58:05 +00:00
4d21a001d6 Fix the way sorting is done by issues
git-svn-id: http://comictagger.googlecode.com/svn/trunk@548 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 17:57:05 +00:00
4af59d2315 Handle changes to the ComicVine API and result sets
git-svn-id: http://comictagger.googlecode.com/svn/trunk@547 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 17:56:30 +00:00
c9c98b6c11 Handle if volume description is None
git-svn-id: http://comictagger.googlecode.com/svn/trunk@546 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-27 17:55:02 +00:00
1ff43db2ce Add-on for reducing page sizes in comics
git-svn-id: http://comictagger.googlecode.com/svn/trunk@545 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-24 17:45:10 +00:00
822f6b4729 0.1 issue gets special consideration a "first" issue.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@544 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-11 23:19:50 +00:00
44a8dc6815 Fixed flawed RE assumption when parsing issue number with # in front. Now properly handle issues with decimal point
git-svn-id: http://comictagger.googlecode.com/svn/trunk@543 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-11 23:18:07 +00:00
a35576895c Removed warning about writing CBI to RAR since CBL supports it now. Yay!
git-svn-id: http://comictagger.googlecode.com/svn/trunk@542 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-03-11 23:16:22 +00:00
631662b30c added configparser to requirements notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@534 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-25 07:10:28 +00:00
cbe3f5a2dc some commented out lines for building on 64-bit snow leopard
git-svn-id: http://comictagger.googlecode.com/svn/trunk@533 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-25 07:01:42 +00:00
73f8bd426b added experimental function to look for scanner page
tweaked image sorting to push some files that begin with "-" to end

git-svn-id: http://comictagger.googlecode.com/svn/trunk@532 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-25 07:00:22 +00:00
0642604480 cropcover now creates a PNG instead of JPEG in case of palletized image
git-svn-id: http://comictagger.googlecode.com/svn/trunk@531 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-25 06:55:44 +00:00
1d95f5076e bumped version, release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@530 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-25 06:54:52 +00:00
53b0c2e8f9 Use backported "configparser" module instead of stock "ConfigParser" to better handle unicode
git-svn-id: http://comictagger.googlecode.com/svn/trunk@529 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-24 19:08:14 +00:00
f59f5fe981 use env command to launch python
git-svn-id: http://comictagger.googlecode.com/svn/trunk@528 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-24 19:04:43 +00:00
67545d8a13 Fixed issue where month_name creation was failing by not decoding date string from system
git-svn-id: http://comictagger.googlecode.com/svn/trunk@527 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-24 18:56:29 +00:00
ab3e3b40c4 Added a script to thin out fat binaries
git-svn-id: http://comictagger.googlecode.com/svn/trunk@526 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-21 22:40:00 +00:00
188024c2db Make sure the mac bundle Info.plist has the version string set
git-svn-id: http://comictagger.googlecode.com/svn/trunk@524 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-19 22:15:24 +00:00
324b56a623 text cleanup
git-svn-id: http://comictagger.googlecode.com/svn/trunk@523 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-19 18:03:40 +00:00
782d424392 added support for retaining the new CR "day" field in the metadata
git-svn-id: http://comictagger.googlecode.com/svn/trunk@522 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-19 01:50:53 +00:00
cf63bfda9d added pypi project update to the upload target
git-svn-id: http://comictagger.googlecode.com/svn/trunk@521 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-19 01:23:09 +00:00
903d4c647c rearranged XML output to more closely match the order of ComicRack output
git-svn-id: http://comictagger.googlecode.com/svn/trunk@520 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-19 01:22:37 +00:00
407b83fe90 fixed date for release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@515 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-15 02:37:04 +00:00
27edc80d2b a few makefile tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@514 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-15 01:05:35 +00:00
01f48f8b91 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@513 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-15 01:02:52 +00:00
527e690170 removed cruft from script printouts
git-svn-id: http://comictagger.googlecode.com/svn/trunk@512 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-15 01:02:35 +00:00
d100572aa4 add import of script into the try-catch
git-svn-id: http://comictagger.googlecode.com/svn/trunk@511 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-15 01:00:59 +00:00
42640c4ad5 reduced size of filename font in infobox
git-svn-id: http://comictagger.googlecode.com/svn/trunk@510 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-15 00:25:37 +00:00
a61972e503 wrapped a try-catch around script execution
git-svn-id: http://comictagger.googlecode.com/svn/trunk@509 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-15 00:25:06 +00:00
464e147223 back to "ctmain"
git-svn-id: http://comictagger.googlecode.com/svn/trunk@508 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-15 00:23:54 +00:00
8759784561 Script fixes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@507 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 21:07:36 +00:00
ee5b4a689e updated readme
git-svn-id: http://comictagger.googlecode.com/svn/trunk@506 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 20:15:18 +00:00
71ccf1eea8 Script updates
git-svn-id: http://comictagger.googlecode.com/svn/trunk@505 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 20:06:42 +00:00
a9ee7c463b Added script folder to manifest
git-svn-id: http://comictagger.googlecode.com/svn/trunk@504 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 19:09:35 +00:00
6f683a71c7 added readme for script folder
git-svn-id: http://comictagger.googlecode.com/svn/trunk@503 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 19:09:13 +00:00
24b192b22c Tweaked the UI box
git-svn-id: http://comictagger.googlecode.com/svn/trunk@502 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 18:42:20 +00:00
b6b1a4737f got rid of debug print
git-svn-id: http://comictagger.googlecode.com/svn/trunk@501 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 18:41:33 +00:00
00202cc865 more scripts
git-svn-id: http://comictagger.googlecode.com/svn/trunk@499 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 06:36:28 +00:00
235524b06d added script options to help
git-svn-id: http://comictagger.googlecode.com/svn/trunk@498 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 06:30:53 +00:00
8a7f822970 restored accidentally lost dirty flag signals
git-svn-id: http://comictagger.googlecode.com/svn/trunk@495 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 05:38:25 +00:00
ff3f048bb4 Filename parsing preserves dashes in series name
git-svn-id: http://comictagger.googlecode.com/svn/trunk@494 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-14 05:37:00 +00:00
abda202f32 removed local setting, and now import tag style from comicarchive
git-svn-id: http://comictagger.googlecode.com/svn/trunk@493 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-13 21:55:56 +00:00
2d4ac84de0 app main entry point is now called "main"
git-svn-id: http://comictagger.googlecode.com/svn/trunk@492 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-13 21:55:00 +00:00
86732e7827 Moved tag style class to comicarchive module
git-svn-id: http://comictagger.googlecode.com/svn/trunk@491 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-13 21:54:07 +00:00
693b5b1978 Moved tagstyle class to comicarchive module
git-svn-id: http://comictagger.googlecode.com/svn/trunk@490 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-13 21:53:15 +00:00
e3d3ecfd31 fix basedir setting for frozen windows
git-svn-id: http://comictagger.googlecode.com/svn/trunk@489 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-13 21:52:02 +00:00
ce6b81ab73 Reworked the encoding in the recursive filelist creation
Changed the "fix encoding" fuction to only load once, and set the local from env

git-svn-id: http://comictagger.googlecode.com/svn/trunk@488 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-13 21:50:47 +00:00
501365b5a3 Removed mention of depricated folder archives
git-svn-id: http://comictagger.googlecode.com/svn/trunk@487 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-12 19:22:21 +00:00
c6741d4392 Renamed test script to inventory
git-svn-id: http://comictagger.googlecode.com/svn/trunk@486 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-12 18:36:49 +00:00
42feae53dd caught some small errors exposed by calling via script
git-svn-id: http://comictagger.googlecode.com/svn/trunk@485 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-12 18:29:29 +00:00
c65695b8dc updated test script
git-svn-id: http://comictagger.googlecode.com/svn/trunk@484 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-12 04:53:36 +00:00
4da71e262b bumped version
git-svn-id: http://comictagger.googlecode.com/svn/trunk@483 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-12 04:53:02 +00:00
c519fd33d5 fixed a bug in windows filename arg list processing
git-svn-id: http://comictagger.googlecode.com/svn/trunk@482 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-12 04:52:51 +00:00
07ef0211b9 windows mkdir version doesn't do -p
git-svn-id: http://comictagger.googlecode.com/svn/trunk@481 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-12 04:52:04 +00:00
c45b56a5b6 moved some code into utils:
recursive file list
	output encoding config

git-svn-id: http://comictagger.googlecode.com/svn/trunk@479 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-11 17:29:55 +00:00
6f27fc7669 added recursive flag for CLI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@478 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-11 17:29:08 +00:00
4530ac017c disable support for "folder" archives
git-svn-id: http://comictagger.googlecode.com/svn/trunk@477 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-11 17:28:18 +00:00
400fe6efa3 text cleanup
git-svn-id: http://comictagger.googlecode.com/svn/trunk@476 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-11 17:27:54 +00:00
ac7a12d18d Experimental script using lib
git-svn-id: http://comictagger.googlecode.com/svn/trunk@475 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-09 00:15:42 +00:00
c2ff11fab7 make sure the release folder always exists during build process
git-svn-id: http://comictagger.googlecode.com/svn/trunk@474 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-08 22:14:18 +00:00
34019ff338 clean out generated release.nsh file
git-svn-id: http://comictagger.googlecode.com/svn/trunk@473 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-08 22:07:38 +00:00
176bc43888 rar_exe_path is maintained only in the settings now.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@472 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-08 22:05:28 +00:00
2e290c4c74 Used RE to make sure duplicate doesn't get added to path
git-svn-id: http://comictagger.googlecode.com/svn/trunk@471 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-08 22:04:56 +00:00
74a374d46b typos and and text fixes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@470 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-08 22:01:58 +00:00
58f5f10c78 abandoned debian build for now.
cleaned up the windows makefile a bit

git-svn-id: http://comictagger.googlecode.com/svn/trunk@469 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-08 05:38:14 +00:00
7d8ed954a9 Changed text regarding PIL requirement
make now uses the python dist package to make the source zip

git-svn-id: http://comictagger.googlecode.com/svn/trunk@468 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-08 01:14:04 +00:00
078b3cef3c more python packaging tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@464 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 23:09:00 +00:00
22ef0250ca python packaging tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@463 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 22:38:27 +00:00
cc53162dcc Added a readme.txt for the source distrubution
git-svn-id: http://comictagger.googlecode.com/svn/trunk@462 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 21:34:00 +00:00
fa309cfcef Got mac build working with new structure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@461 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 17:35:25 +00:00
4d57b0cf79 God deb built using fpm!
git-svn-id: http://comictagger.googlecode.com/svn/trunk@460 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 07:04:23 +00:00
6ea5d28609 More distutil fun
git-svn-id: http://comictagger.googlecode.com/svn/trunk@459 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 05:21:07 +00:00
9d56a2ce9a Got frozen windows build working again
git-svn-id: http://comictagger.googlecode.com/svn/trunk@458 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 04:50:10 +00:00
811759478a Function dist install on linux
git-svn-id: http://comictagger.googlecode.com/svn/trunk@457 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 04:19:20 +00:00
28e2d93314 Name conflict with launcher script
git-svn-id: http://comictagger.googlecode.com/svn/trunk@456 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 04:00:20 +00:00
93b3117699 MOre cleanup
git-svn-id: http://comictagger.googlecode.com/svn/trunk@455 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 02:40:29 +00:00
10e6a1019e First cut at a dist-package build
git-svn-id: http://comictagger.googlecode.com/svn/trunk@454 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 02:39:36 +00:00
2024555780 restructure - done, I think
git-svn-id: http://comictagger.googlecode.com/svn/trunk@453 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 01:20:05 +00:00
e15c3fa3e6 restructure - almost there!
git-svn-id: http://comictagger.googlecode.com/svn/trunk@452 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 01:12:49 +00:00
8aa6403f51 Restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@451 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 01:01:39 +00:00
fb5fca1dc4 restructre
git-svn-id: http://comictagger.googlecode.com/svn/trunk@450 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:29:52 +00:00
75d5b1a695 Restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@449 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:17:38 +00:00
e56d9bddbf restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@448 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:10:48 +00:00
7d9aa70dc0 restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@447 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:05:46 +00:00
6d72ed2a69 restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@446 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:05:24 +00:00
9b584f78a0 restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@445 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:04:40 +00:00
dfe0e74f9c Refactored code for restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@444 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:03:53 +00:00
a11c08a2ee Restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@443 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 21:59:43 +00:00
9159204883 Added missing file header
git-svn-id: http://comictagger.googlecode.com/svn/trunk@442 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 20:42:57 +00:00
605e27ce99 Deleted cruft file
git-svn-id: http://comictagger.googlecode.com/svn/trunk@441 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 20:35:24 +00:00
2dc08b36ea Added use google of upload tool to makefile
git-svn-id: http://comictagger.googlecode.com/svn/trunk@440 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 19:13:07 +00:00
60dae4f1fb Keep the google project file upload utility in a handy place
git-svn-id: http://comictagger.googlecode.com/svn/trunk@439 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 19:08:32 +00:00
85728d33bb New filename template variables
git-svn-id: http://comictagger.googlecode.com/svn/trunk@435 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 05:59:52 +00:00
2ade08aa89 Bumped version
git-svn-id: http://comictagger.googlecode.com/svn/trunk@434 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 05:59:08 +00:00
50909962d3 Implemented export to zip on command line
git-svn-id: http://comictagger.googlecode.com/svn/trunk@430 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 00:14:37 +00:00
cc02023730 Fixed an issue in rar directory reading when the first char in the path is a space.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@429 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 22:48:12 +00:00
5bdc40b9f5 Make sure to change codec for stderror too
git-svn-id: http://comictagger.googlecode.com/svn/trunk@428 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 22:47:45 +00:00
4f3e63db07 Make a lot of print statements go to stderr
git-svn-id: http://comictagger.googlecode.com/svn/trunk@427 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 22:27:35 +00:00
b8893b853f Release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@426 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 06:42:24 +00:00
6da6f38673 Text tweak
git-svn-id: http://comictagger.googlecode.com/svn/trunk@425 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 06:37:21 +00:00
369dcbb5a1 Tweaked the pagebrowser layout
Added arrow icons for some buttons

git-svn-id: http://comictagger.googlecode.com/svn/trunk@424 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 06:37:04 +00:00
ec010f29e8 Center progress dialogs on update to keep from drifting due to growth
git-svn-id: http://comictagger.googlecode.com/svn/trunk@423 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 05:14:26 +00:00
22867bc9e6 Addded popup screen image
git-svn-id: http://comictagger.googlecode.com/svn/trunk@422 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 04:50:26 +00:00
dde1913e07 Font tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@421 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 04:49:44 +00:00
5b5842a5f8 tweaked the dialogs window flags to enable maximize on some, and remove the help button on others
git-svn-id: http://comictagger.googlecode.com/svn/trunk@420 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 03:51:50 +00:00
fbf086886f Made selection list font a little smaller
git-svn-id: http://comictagger.googlecode.com/svn/trunk@419 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 03:51:05 +00:00
c1ff6c4b26 Fixed form resizing bug
git-svn-id: http://comictagger.googlecode.com/svn/trunk@418 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 03:50:03 +00:00
99b110d052 PageListEditor now uses CoverImageWidget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@417 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-05 00:00:18 +00:00
3df498eed4 Page list editor displays 1-based list
git-svn-id: http://comictagger.googlecode.com/svn/trunk@416 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 22:20:04 +00:00
b5ab2a6ac9 Updated page browser to use coverimagewidget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@415 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 22:19:31 +00:00
5c91960f04 Added option to not show controls in widget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@414 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 22:19:08 +00:00
3b52fd3213 Main window now uses the CoverImageWidget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@413 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 21:05:31 +00:00
9366457b88 MatchSelectionWindow now uses CoverImageWidget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@412 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 19:54:04 +00:00
1cb7ef66db Added tool tip about double-clicking
git-svn-id: http://comictagger.googlecode.com/svn/trunk@411 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 19:53:32 +00:00
ee6a05deae UI tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@410 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 19:53:03 +00:00
c978883584 Volume selection widget now uses CoverImageWidget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@409 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 19:11:57 +00:00
9b5508ecba Added URL (singe image) mode.
Tweakd resize logic

git-svn-id: http://comictagger.googlecode.com/svn/trunk@408 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 19:11:28 +00:00
8e1c6fae7c Mac OS X acts weird with modality settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@407 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 19:10:53 +00:00
59e662f5a7 Fixed window modality of issue selection window
git-svn-id: http://comictagger.googlecode.com/svn/trunk@406 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 17:25:39 +00:00
6486d97ee3 Added modal image quick popup
git-svn-id: http://comictagger.googlecode.com/svn/trunk@405 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 17:24:48 +00:00
8c088440c5 updated coverimagewidget to manage background loading of alt cover URLs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@404 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 05:15:23 +00:00
320ee1c5d1 Updated todo
git-svn-id: http://comictagger.googlecode.com/svn/trunk@403 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 05:14:29 +00:00
e123720354 Issue selection dialog now uses the coverimagewidget
git-svn-id: http://comictagger.googlecode.com/svn/trunk@402 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 05:13:23 +00:00
d39d4e79ad Added async version of the alt cover URL fetcher
git-svn-id: http://comictagger.googlecode.com/svn/trunk@401 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 05:09:48 +00:00
8d7eeece30 No need to pre-fetch now, since the cover widget manages this itself
git-svn-id: http://comictagger.googlecode.com/svn/trunk@400 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-04 05:08:22 +00:00
3b64e1a3ec Added a new default publisher blacklist item
git-svn-id: http://comictagger.googlecode.com/svn/trunk@399 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-03 18:16:16 +00:00
81ae9bd635 Change the post auto-tag dialog to also show low-confidence single matches
git-svn-id: http://comictagger.googlecode.com/svn/trunk@398 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-03 18:15:48 +00:00
27846772e9 Reworked the post auto-tag selection dialog:
New display image widgets
  Sorting
  Added issue title

git-svn-id: http://comictagger.googlecode.com/svn/trunk@397 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-03 18:14:16 +00:00
baf697b919 New widget for managing the loading and displaying of archive pages and covers from Comic Vine
git-svn-id: http://comictagger.googlecode.com/svn/trunk@396 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-03 18:13:18 +00:00
59ede8d446 Clean up the strings from the alt cover URL list
git-svn-id: http://comictagger.googlecode.com/svn/trunk@395 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-03 18:12:10 +00:00
8b748a3343 Made the alt cover threshold more stringent
git-svn-id: http://comictagger.googlecode.com/svn/trunk@394 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-03 18:10:59 +00:00
75471aaddc Added caching of the alt cover image URL list
git-svn-id: http://comictagger.googlecode.com/svn/trunk@393 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-02 18:41:06 +00:00
7225f261f1 Tuned the cover score thresholds a bit
Fixed a "one-shot" bug where sometimes there is a zero issue but not a "1"

git-svn-id: http://comictagger.googlecode.com/svn/trunk@392 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-02 18:40:40 +00:00
c466264d43 UI tweaks for auto tag match window
git-svn-id: http://comictagger.googlecode.com/svn/trunk@391 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-02 18:39:39 +00:00
14e801b717 Added support for alternate covers from comicvine
git-svn-id: http://comictagger.googlecode.com/svn/trunk@390 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-02 06:03:58 +00:00
af4b467814 Added support for gif in archive
git-svn-id: http://comictagger.googlecode.com/svn/trunk@389 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-02 06:02:25 +00:00
1b3feaa167 made zip building use export instead of checkout
git-svn-id: http://comictagger.googlecode.com/svn/trunk@386 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-01 03:48:53 +00:00
2526fa0ca8 Made retry count for fail rar reads 7
git-svn-id: http://comictagger.googlecode.com/svn/trunk@385 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-01 03:46:16 +00:00
a878d36dcf GUI tweak
git-svn-id: http://comictagger.googlecode.com/svn/trunk@384 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-31 20:36:57 +00:00
90de6433b6 GUI goodness, better adjusted forms and dialogs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@383 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-31 05:12:12 +00:00
d9abc364f1 Version bump
git-svn-id: http://comictagger.googlecode.com/svn/trunk@382 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-31 01:14:21 +00:00
e542b6df1f Added more options to auto tag start:
Use specific search string
 Change length tolerance

git-svn-id: http://comictagger.googlecode.com/svn/trunk@381 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-31 01:14:03 +00:00
a7a6b085f1 Added more options to auto tag start:
Use specific search string
 Change length tolerance

git-svn-id: http://comictagger.googlecode.com/svn/trunk@380 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-31 01:06:19 +00:00
0078f76e8c Use an RE to look for #issue before anything else
git-svn-id: http://comictagger.googlecode.com/svn/trunk@379 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-31 01:05:16 +00:00
1a01cb60d9 added autotag options to remove after success, and ignore leading digits
git-svn-id: http://comictagger.googlecode.com/svn/trunk@375 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 18:40:53 +00:00
0f81ce4c24 make sure filenames are unicode
git-svn-id: http://comictagger.googlecode.com/svn/trunk@374 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 18:40:03 +00:00
c4ef4137d0 removed dead comment line
git-svn-id: http://comictagger.googlecode.com/svn/trunk@373 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 18:39:33 +00:00
cdc6d71356 parser tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@372 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 18:39:13 +00:00
2357a6378e resized window
git-svn-id: http://comictagger.googlecode.com/svn/trunk@371 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 18:38:48 +00:00
9503d0fef4 Add some new options
git-svn-id: http://comictagger.googlecode.com/svn/trunk@370 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 18:38:30 +00:00
c46dda4540 Added tooltip strigs to credit table
Explicitly refresh the archive cache after modify operation
added summary after zip export, tag copy and tag remove

git-svn-id: http://comictagger.googlecode.com/svn/trunk@369 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 02:19:18 +00:00
894c23f64f Added tooltip strigs to table
git-svn-id: http://comictagger.googlecode.com/svn/trunk@368 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 02:17:55 +00:00
9360fa954c Added tooltip strigs to table
git-svn-id: http://comictagger.googlecode.com/svn/trunk@367 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 02:17:40 +00:00
74408e56fd make sure to decode strings straight from filesystem
git-svn-id: http://comictagger.googlecode.com/svn/trunk@366 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 02:17:16 +00:00
dd8e54fa6b another fix for badly formatted issue string
git-svn-id: http://comictagger.googlecode.com/svn/trunk@365 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 02:16:38 +00:00
b378840878 Rar fix for zero length entries
make sure archive object clears cache on any write operation

git-svn-id: http://comictagger.googlecode.com/svn/trunk@364 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 02:16:10 +00:00
c0a6406dc9 Unicode fix.
Added tooltip strigs to table

git-svn-id: http://comictagger.googlecode.com/svn/trunk@363 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-30 02:15:16 +00:00
df3544e734 Make sure the page list is populated even if it's not in the existing metadata
git-svn-id: http://comictagger.googlecode.com/svn/trunk@362 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-28 20:59:08 +00:00
d40de5b67e added copyright/license info to CLI version output
git-svn-id: http://comictagger.googlecode.com/svn/trunk@361 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-28 20:58:26 +00:00
25b63dfc65 Also check for unicode when converting to int value for output
git-svn-id: http://comictagger.googlecode.com/svn/trunk@360 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-28 17:38:59 +00:00
70f50c8595 reduced rar failure retry max to 5
git-svn-id: http://comictagger.googlecode.com/svn/trunk@359 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-28 17:37:18 +00:00
e9aba4e119 DId a premature tag. Interim version string bump
git-svn-id: http://comictagger.googlecode.com/svn/trunk@355 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 17:22:43 +00:00
53aca0ee08 version bump
git-svn-id: http://comictagger.googlecode.com/svn/trunk@354 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 17:21:36 +00:00
9aa41823b4 more vebose output for zip errors
git-svn-id: http://comictagger.googlecode.com/svn/trunk@353 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 17:20:59 +00:00
8d4a336b50 Fixed logic bug for low confidence save
git-svn-id: http://comictagger.googlecode.com/svn/trunk@352 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 17:20:20 +00:00
3c96e68fde Remove "?" from renamer output
git-svn-id: http://comictagger.googlecode.com/svn/trunk@351 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 17:19:42 +00:00
93f316b820 more robust dealing with read errors in rar archives
more logging in auto-tag process

git-svn-id: http://comictagger.googlecode.com/svn/trunk@349 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 06:17:45 +00:00
ccde71f9d0 fixed mac text encoding at always utf-8
git-svn-id: http://comictagger.googlecode.com/svn/trunk@348 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 06:14:51 +00:00
7186c6792a rename takes month variables
git-svn-id: http://comictagger.googlecode.com/svn/trunk@347 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 06:14:11 +00:00
e8961ed299 make clean at root does all below
git-svn-id: http://comictagger.googlecode.com/svn/trunk@346 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 06:13:33 +00:00
1f050436d3 better volume number parsing
fixed case of more or less no filename

git-svn-id: http://comictagger.googlecode.com/svn/trunk@345 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-25 06:13:09 +00:00
79a9cf1b40 Release 1.0.1
Fixed stupid bug where unicode can't be printed to OS X console

git-svn-id: http://comictagger.googlecode.com/svn/trunk@341 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-24 04:22:37 +00:00
6e7d7bcc47 Fixed an issue with export
git-svn-id: http://comictagger.googlecode.com/svn/trunk@334 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-24 01:33:21 +00:00
d96690c351 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@333 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 22:15:00 +00:00
c44c240eef UI tweaks
New icons
bumped version number

git-svn-id: http://comictagger.googlecode.com/svn/trunk@332 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 22:05:38 +00:00
ddc225c2be New auto-tag icon
git-svn-id: http://comictagger.googlecode.com/svn/trunk@331 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 19:43:05 +00:00
d9cdb14aa6 Form re-worked
Preserve splitter location in settings

git-svn-id: http://comictagger.googlecode.com/svn/trunk@330 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 18:55:10 +00:00
cab525675d Fixed a rename window bug where window was going away too soon
git-svn-id: http://comictagger.googlecode.com/svn/trunk@329 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 18:54:44 +00:00
e9321b741e Added more cursor feedback when saving
git-svn-id: http://comictagger.googlecode.com/svn/trunk@328 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 06:00:59 +00:00
4143ca3314 Added a dialog for manually matching after auto-tag
git-svn-id: http://comictagger.googlecode.com/svn/trunk@327 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 05:25:50 +00:00
667c21bbed More exception handling for corrupt archives
git-svn-id: http://comictagger.googlecode.com/svn/trunk@326 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 03:04:55 +00:00
37048b99fc Assorted fixes and enhancements
git-svn-id: http://comictagger.googlecode.com/svn/trunk@325 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 01:25:17 +00:00
e839b008c6 Handle exception when resizing corrupt image data
git-svn-id: http://comictagger.googlecode.com/svn/trunk@324 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 01:24:33 +00:00
ba3673a4c0 Better exception handling
Fixed a horrible memory leak

git-svn-id: http://comictagger.googlecode.com/svn/trunk@323 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-23 01:23:57 +00:00
221923607a Auto-Tag progress window added
More auto-tag and other stuff

git-svn-id: http://comictagger.googlecode.com/svn/trunk@322 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-22 04:09:08 +00:00
b712226b1e Export confirm window
AutoTag confirm window
Other fixes and such

git-svn-id: http://comictagger.googlecode.com/svn/trunk@321 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-22 00:19:59 +00:00
b8e8c6433a Options dialog for export to zip
Fixed progress dialogs

git-svn-id: http://comictagger.googlecode.com/svn/trunk@320 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-21 06:39:44 +00:00
be3b0fe92c Tweaked the comictagger note text
Fixed a transformer bug

git-svn-id: http://comictagger.googlecode.com/svn/trunk@319 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-21 01:45:51 +00:00
d26441306a Added setting for rename to use the archive type
git-svn-id: http://comictagger.googlecode.com/svn/trunk@318 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-21 01:07:45 +00:00
f2b1db5479 Added batch tag copy
Other fixed for batch stuff

git-svn-id: http://comictagger.googlecode.com/svn/trunk@317 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-20 20:43:48 +00:00
0cd10f3f75 Handle case of non-exisiting issue string
git-svn-id: http://comictagger.googlecode.com/svn/trunk@316 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-20 20:42:06 +00:00
97dc36b8fb Implemented batch export to zip
more multi-file/batch enhancements

git-svn-id: http://comictagger.googlecode.com/svn/trunk@315 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-20 08:36:21 +00:00
d58e033689 Implemented batch tag remove
more multi-file/batch enhancements

git-svn-id: http://comictagger.googlecode.com/svn/trunk@314 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-19 06:15:33 +00:00
c3d5d44788 Implemented batch rename in GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@312 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-19 03:12:25 +00:00
2bf9b9ed7c Completed initial multi-file management, before implementing batch features
git-svn-id: http://comictagger.googlecode.com/svn/trunk@311 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-18 21:12:23 +00:00
cfca394bcb More work on managing mutiple files in the GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@310 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-18 00:52:42 +00:00
7a7adc1c3f Implemented context menu for file list
git-svn-id: http://comictagger.googlecode.com/svn/trunk@309 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-17 06:19:06 +00:00
41f730a558 Version update for 0.9.5
git-svn-id: http://comictagger.googlecode.com/svn/trunk@305 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-17 00:05:09 +00:00
550b84361c Create a list of story arcs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@304 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:59:09 +00:00
fb4248fda2 Fixed some typos
git-svn-id: http://comictagger.googlecode.com/svn/trunk@303 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:58:54 +00:00
9626c3fd77 Use the CT version in JSON
Make sure certain fields are ints

git-svn-id: http://comictagger.googlecode.com/svn/trunk@302 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:29:32 +00:00
3f305c6788 Made sure to reset the cache on a tag block delete
git-svn-id: http://comictagger.googlecode.com/svn/trunk@301 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:20:59 +00:00
9e68516dac Work on multi-file processing
git-svn-id: http://comictagger.googlecode.com/svn/trunk@300 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 22:46:22 +00:00
8f45994b9a Added a CLI option for searching by CV issue ID, that can be used when being called by Mylar
git-svn-id: http://comictagger.googlecode.com/svn/trunk@299 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-12 01:48:34 +00:00
4ea56c0bd0 Updated version and release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@296 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 21:20:36 +00:00
5445417404 Tweaked setting window UI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@295 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 21:20:20 +00:00
db6423aea9 Tweaked CBL tranform to save notes and weblink to comments
git-svn-id: http://comictagger.googlecode.com/svn/trunk@293 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 20:15:17 +00:00
aa62a3e8ff gracefully handle no search results
git-svn-id: http://comictagger.googlecode.com/svn/trunk@292 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-31 00:43:22 +00:00
cd1733a975 Added a cache version file to manage clearing old one on upgrade
git-svn-id: http://comictagger.googlecode.com/svn/trunk@291 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 23:32:37 +00:00
c81319402d A few more unicode fixes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@290 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 05:31:00 +00:00
8a8e53d9c9 A lot of unicode related fixes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@289 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 05:06:12 +00:00
7614e95084 Handle case of no numeric portion of issue number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@288 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-22 01:19:14 +00:00
bd9f314496 Some tweaks to issue number finder
git-svn-id: http://comictagger.googlecode.com/svn/trunk@287 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-22 01:18:18 +00:00
bebd09d3f6 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@284 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 06:00:31 +00:00
8a5430c83e updated todo
git-svn-id: http://comictagger.googlecode.com/svn/trunk@278 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:07:14 +00:00
93be1b42f4 Neatened up the new settings tabs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@277 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:06:44 +00:00
01be389fad New version
git-svn-id: http://comictagger.googlecode.com/svn/trunk@276 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:06:15 +00:00
ca9aaf9279 Don't always show full help
git-svn-id: http://comictagger.googlecode.com/svn/trunk@275 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:05:51 +00:00
ee9175087e Implemented file-renaming in GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@273 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 01:37:55 +00:00
94c5882175 Fixed printing of primary flag on CLI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@272 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-18 23:21:56 +00:00
ff74b3e5bc Added menu options to rename and apply CBL transform
git-svn-id: http://comictagger.googlecode.com/svn/trunk@271 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-18 23:14:00 +00:00
0017903a4f Got CBL transformer working
git-svn-id: http://comictagger.googlecode.com/svn/trunk@270 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 21:19:21 +00:00
3d98118fa9 Added option set CV series start year as volume
git-svn-id: http://comictagger.googlecode.com/svn/trunk@269 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 18:44:33 +00:00
faf0b5d437 New settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@268 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 18:19:32 +00:00
e14c9dfe19 fixed encoding error
git-svn-id: http://comictagger.googlecode.com/svn/trunk@267 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:23:48 +00:00
4343f3f08d Gracefully deal with bad image data
git-svn-id: http://comictagger.googlecode.com/svn/trunk@266 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:09:26 +00:00
4a94bf4d6f Ignore image files that begin with ".". They're probably cruft.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@265 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:08:59 +00:00
a602c42f0e new file renamer class
git-svn-id: http://comictagger.googlecode.com/svn/trunk@263 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-15 05:54:12 +00:00
1efdc0e623 Set ctrl+A for menu auto-select
git-svn-id: http://comictagger.googlecode.com/svn/trunk@262 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-14 23:18:44 +00:00
173 changed files with 17313 additions and 12386 deletions

154
.gitignore vendored Normal file
View File

@ -0,0 +1,154 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion
*.iml
## Directory-based project format:
.idea/
### Other editors
.*.swp
nbproject/
.vscode
comictaggerlib/_version.py
*.exe
*.zip
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

View File

@ -1,24 +1,46 @@
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
VERSION_STR := $(shell grep version $(TAGGER_BASE)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
VERSION_STR := $(shell python -c 'import comictaggerlib._version; print( comictaggerlib._version.version)')
all: clean
ifeq ($(OS),Windows_NT)
APP_NAME=comictagger.exe
FINAL_NAME=ComicTagger-$(VERSION_STR).exe
ICON_PATH="windows/app.ico"
else ifeq ($(shell uname -s),Darwin)
APP_NAME=ComicTagger.app
FINAL_NAME=ComicTagger-$(VERSION_STR).app
ICON_PATH="mac/app.icns"
else
APP_NAME=comictagger
FINAL_NAME=ComicTagger-$(VERSION_STR)
ICON_PATH="windows/app.ico"
endif
.PHONY: all clean pydist upload dist
all: clean dist
clean:
rm -f *~ *.pyc *.pyo
rm -f logdict*.log
zip:
cd release; \
rm -rf *zip comictagger-src-$(VERSION_STR) ; \
svn checkout https://comictagger.googlecode.com/svn/trunk/ comictagger-src-$(VERSION_STR); \
zip -r comictagger-src-$(VERSION_STR).zip comictagger-src-$(VERSION_STR); \
rm -rf comictagger-src-$(VERSION_STR)
@echo When satisfied with release, do this:
@echo make svn_tag
svn_tag:
svn copy https://comictagger.googlecode.com/svn/trunk \
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
rm -rf *~ *.pyc *.pyo
rm -rf scripts/*.pyc
cd comictaggerlib; rm -f *~ *.pyc *.pyo
rm -rf dist MANIFEST
rm -rf *.deb
rm -rf logdict*.log
$(MAKE) -C mac clean
rm -rf build
rm -rf comictaggerlib/ui/__pycache__
pydist:
make clean
mkdir -p piprelease
rm -f comictagger-$(VERSION_STR).zip
python setup.py sdist --formats=zip #,gztar
mv dist/comictagger-$(VERSION_STR).zip piprelease
rm -rf comictagger.egg-info dist
upload:
python setup.py register
python setup.py sdist --formats=zip upload
dist:
pyinstaller.exe --name="comictagger" --windowed --add-data 'comictaggerlib/ui/*.ui;ui' --add-data 'comictaggerlib/graphics;graphics' -i windows/app.ico --version-file file_version_info.py comictagger.py
mv dist/$(APP_NAME) dist/$(FINAL_NAME)

16
README.md Normal file
View File

@ -0,0 +1,16 @@
A fork from https://github.com/comictagger/comictagger
Changes:
- switched to rarfile, makes dependencies simpler and I had issues using unrar-cffi with python<6.7
- Move to Python requests module, requests is much simpler and fixes all ssl errors.
- Moved to using Python format strings and use pathvalidate to handle filenames, supports directory structures
- Issue string parsing now strips off (# of #) (e.g. 1 of 45)
- Add publisher and imprint handling, currently hardcoded
Notes:
- I did some testing with the pyinstaller build, and it worked on both platforms. I did encounter two problems:
- Mac build showed the wrong widget set. I found a solution here that seemed to work: https://stackoverflow.com/questions/48626999/packaging-with-pyinstaller-pyqt5-setstyle-ignored
- Windows build had problems grabbing images from ComicVine using SSL. It think that some libraries are missing from the monolithic exe, but I couldn't figure out how to fix the problem.
- In setup.py you can also find the remains of an attempt to do some desktop integration from a pip install. It does work, but can cause problems with wheel installs, and I don't know if it's worth the bother. I kept the commented-out code in place, just in case.
With Python 3, it's much easier to get the app working from scratch on a new distro, as all of the dependencies are available as wheels, including PyQt5, so just a simple "pip install comictagger.zip" is all that's needed.

View File

@ -1,18 +0,0 @@
The unrar.dll library is freeware. This means:
1. All copyrights to RAR and the unrar.dll are exclusively
owned by the author - Alexander Roshal.
2. The unrar.dll library may be used in any software to handle RAR
archives without limitations free of charge.
3. THE RAR ARCHIVER AND THE UNRAR.DLL LIBRARY ARE DISTRIBUTED "AS IS".
NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT
YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS,
DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING
OR MISUSING THIS SOFTWARE.
Thank you for your interest in RAR and unrar.dll.
Alexander L. Roshal

Binary file not shown.

View File

@ -1,140 +0,0 @@
#ifndef _UNRAR_DLL_
#define _UNRAR_DLL_
#define ERAR_END_ARCHIVE 10
#define ERAR_NO_MEMORY 11
#define ERAR_BAD_DATA 12
#define ERAR_BAD_ARCHIVE 13
#define ERAR_UNKNOWN_FORMAT 14
#define ERAR_EOPEN 15
#define ERAR_ECREATE 16
#define ERAR_ECLOSE 17
#define ERAR_EREAD 18
#define ERAR_EWRITE 19
#define ERAR_SMALL_BUF 20
#define ERAR_UNKNOWN 21
#define ERAR_MISSING_PASSWORD 22
#define RAR_OM_LIST 0
#define RAR_OM_EXTRACT 1
#define RAR_OM_LIST_INCSPLIT 2
#define RAR_SKIP 0
#define RAR_TEST 1
#define RAR_EXTRACT 2
#define RAR_VOL_ASK 0
#define RAR_VOL_NOTIFY 1
#define RAR_DLL_VERSION 4
#ifdef _UNIX
#define CALLBACK
#define PASCAL
#define LONG long
#define HANDLE void *
#define LPARAM long
#define UINT unsigned int
#endif
struct RARHeaderData
{
char ArcName[260];
char FileName[260];
unsigned int Flags;
unsigned int PackSize;
unsigned int UnpSize;
unsigned int HostOS;
unsigned int FileCRC;
unsigned int FileTime;
unsigned int UnpVer;
unsigned int Method;
unsigned int FileAttr;
char *CmtBuf;
unsigned int CmtBufSize;
unsigned int CmtSize;
unsigned int CmtState;
};
struct RARHeaderDataEx
{
char ArcName[1024];
wchar_t ArcNameW[1024];
char FileName[1024];
wchar_t FileNameW[1024];
unsigned int Flags;
unsigned int PackSize;
unsigned int PackSizeHigh;
unsigned int UnpSize;
unsigned int UnpSizeHigh;
unsigned int HostOS;
unsigned int FileCRC;
unsigned int FileTime;
unsigned int UnpVer;
unsigned int Method;
unsigned int FileAttr;
char *CmtBuf;
unsigned int CmtBufSize;
unsigned int CmtSize;
unsigned int CmtState;
unsigned int Reserved[1024];
};
struct RAROpenArchiveData
{
char *ArcName;
unsigned int OpenMode;
unsigned int OpenResult;
char *CmtBuf;
unsigned int CmtBufSize;
unsigned int CmtSize;
unsigned int CmtState;
};
struct RAROpenArchiveDataEx
{
char *ArcName;
wchar_t *ArcNameW;
unsigned int OpenMode;
unsigned int OpenResult;
char *CmtBuf;
unsigned int CmtBufSize;
unsigned int CmtSize;
unsigned int CmtState;
unsigned int Flags;
unsigned int Reserved[32];
};
enum UNRARCALLBACK_MESSAGES {
UCM_CHANGEVOLUME,UCM_PROCESSDATA,UCM_NEEDPASSWORD
};
typedef int (CALLBACK *UNRARCALLBACK)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2);
typedef int (PASCAL *CHANGEVOLPROC)(char *ArcName,int Mode);
typedef int (PASCAL *PROCESSDATAPROC)(unsigned char *Addr,int Size);
#ifdef __cplusplus
extern "C" {
#endif
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData);
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData);
int PASCAL RARCloseArchive(HANDLE hArcData);
int PASCAL RARReadHeader(HANDLE hArcData,struct RARHeaderData *HeaderData);
int PASCAL RARReadHeaderEx(HANDLE hArcData,struct RARHeaderDataEx *HeaderData);
int PASCAL RARProcessFile(HANDLE hArcData,int Operation,char *DestPath,char *DestName);
int PASCAL RARProcessFileW(HANDLE hArcData,int Operation,wchar_t *DestPath,wchar_t *DestName);
void PASCAL RARSetCallback(HANDLE hArcData,UNRARCALLBACK Callback,LPARAM UserData);
void PASCAL RARSetChangeVolProc(HANDLE hArcData,CHANGEVOLPROC ChangeVolProc);
void PASCAL RARSetProcessDataProc(HANDLE hArcData,PROCESSDATAPROC ProcessDataProc);
void PASCAL RARSetPassword(HANDLE hArcData,char *Password);
int PASCAL RARGetDllVersion();
#ifdef __cplusplus
}
#endif
#endif

Binary file not shown.

View File

@ -1,606 +0,0 @@
UnRAR.dll Manual
~~~~~~~~~~~~~~~~
UnRAR.dll is a 32-bit Windows dynamic-link library which provides
file extraction from RAR archives.
Exported functions
====================================================================
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData)
====================================================================
Description
~~~~~~~~~~~
Open RAR archive and allocate memory structures
Parameters
~~~~~~~~~~
ArchiveData Points to RAROpenArchiveData structure
struct RAROpenArchiveData
{
char *ArcName;
UINT OpenMode;
UINT OpenResult;
char *CmtBuf;
UINT CmtBufSize;
UINT CmtSize;
UINT CmtState;
};
Structure fields:
ArcName
Input parameter which should point to zero terminated string
containing the archive name.
OpenMode
Input parameter.
Possible values
RAR_OM_LIST
Open archive for reading file headers only.
RAR_OM_EXTRACT
Open archive for testing and extracting files.
RAR_OM_LIST_INCSPLIT
Open archive for reading file headers only. If you open an archive
in such mode, RARReadHeader[Ex] will return all file headers,
including those with "file continued from previous volume" flag.
In case of RAR_OM_LIST such headers are automatically skipped.
So if you process RAR volumes in RAR_OM_LIST_INCSPLIT mode, you will
get several file header records for same file if file is split between
volumes. For such files only the last file header record will contain
the correct file CRC and if you wish to get the correct packed size,
you need to sum up packed sizes of all parts.
OpenResult
Output parameter.
Possible values
0 Success
ERAR_NO_MEMORY Not enough memory to initialize data structures
ERAR_BAD_DATA Archive header broken
ERAR_BAD_ARCHIVE File is not valid RAR archive
ERAR_UNKNOWN_FORMAT Unknown encryption used for archive headers
ERAR_EOPEN File open error
CmtBuf
Input parameter which should point to the buffer for archive
comments. Maximum comment size is limited to 64Kb. Comment text is
zero terminated. If the comment text is larger than the buffer
size, the comment text will be truncated. If CmtBuf is set to
NULL, comments will not be read.
CmtBufSize
Input parameter which should contain size of buffer for archive
comments.
CmtSize
Output parameter containing size of comments actually read into the
buffer, cannot exceed CmtBufSize.
CmtState
Output parameter.
Possible values
0 comments not present
1 Comments read completely
ERAR_NO_MEMORY Not enough memory to extract comments
ERAR_BAD_DATA Broken comment
ERAR_UNKNOWN_FORMAT Unknown comment format
ERAR_SMALL_BUF Buffer too small, comments not completely read
Return values
~~~~~~~~~~~~~
Archive handle or NULL in case of error
========================================================================
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData)
========================================================================
Description
~~~~~~~~~~~
Similar to RAROpenArchive, but uses RAROpenArchiveDataEx structure
allowing to specify Unicode archive name and returning information
about archive flags.
Parameters
~~~~~~~~~~
ArchiveData Points to RAROpenArchiveDataEx structure
struct RAROpenArchiveDataEx
{
char *ArcName;
wchar_t *ArcNameW;
unsigned int OpenMode;
unsigned int OpenResult;
char *CmtBuf;
unsigned int CmtBufSize;
unsigned int CmtSize;
unsigned int CmtState;
unsigned int Flags;
unsigned int Reserved[32];
};
Structure fields:
ArcNameW
Input parameter which should point to zero terminated Unicode string
containing the archive name or NULL if Unicode name is not specified.
Flags
Output parameter. Combination of bit flags.
Possible values
0x0001 - Volume attribute (archive volume)
0x0002 - Archive comment present
0x0004 - Archive lock attribute
0x0008 - Solid attribute (solid archive)
0x0010 - New volume naming scheme ('volname.partN.rar')
0x0020 - Authenticity information present
0x0040 - Recovery record present
0x0080 - Block headers are encrypted
0x0100 - First volume (set only by RAR 3.0 and later)
Reserved[32]
Reserved for future use. Must be zero.
Information on other structure fields and function return values
is available above, in RAROpenArchive function description.
====================================================================
int PASCAL RARCloseArchive(HANDLE hArcData)
====================================================================
Description
~~~~~~~~~~~
Close RAR archive and release allocated memory. It must be called when
archive processing is finished, even if the archive processing was stopped
due to an error.
Parameters
~~~~~~~~~~
hArcData
This parameter should contain the archive handle obtained from the
RAROpenArchive function call.
Return values
~~~~~~~~~~~~~
0 Success
ERAR_ECLOSE Archive close error
====================================================================
int PASCAL RARReadHeader(HANDLE hArcData,
struct RARHeaderData *HeaderData)
====================================================================
Description
~~~~~~~~~~~
Read header of file in archive.
Parameters
~~~~~~~~~~
hArcData
This parameter should contain the archive handle obtained from the
RAROpenArchive function call.
HeaderData
It should point to RARHeaderData structure:
struct RARHeaderData
{
char ArcName[260];
char FileName[260];
UINT Flags;
UINT PackSize;
UINT UnpSize;
UINT HostOS;
UINT FileCRC;
UINT FileTime;
UINT UnpVer;
UINT Method;
UINT FileAttr;
char *CmtBuf;
UINT CmtBufSize;
UINT CmtSize;
UINT CmtState;
};
Structure fields:
ArcName
Output parameter which contains a zero terminated string of the
current archive name. May be used to determine the current volume
name.
FileName
Output parameter which contains a zero terminated string of the
file name in OEM (DOS) encoding.
Flags
Output parameter which contains file flags:
0x01 - file continued from previous volume
0x02 - file continued on next volume
0x04 - file encrypted with password
0x08 - file comment present
0x10 - compression of previous files is used (solid flag)
bits 7 6 5
0 0 0 - dictionary size 64 Kb
0 0 1 - dictionary size 128 Kb
0 1 0 - dictionary size 256 Kb
0 1 1 - dictionary size 512 Kb
1 0 0 - dictionary size 1024 Kb
1 0 1 - dictionary size 2048 KB
1 1 0 - dictionary size 4096 KB
1 1 1 - file is directory
Other bits are reserved.
PackSize
Output parameter means packed file size or size of the
file part if file was split between volumes.
UnpSize
Output parameter - unpacked file size.
HostOS
Output parameter - operating system used for archiving:
0 - MS DOS;
1 - OS/2.
2 - Win32
3 - Unix
FileCRC
Output parameter which contains unpacked file CRC. In case of file parts
split between volumes only the last part contains the correct CRC
and it is accessible only in RAR_OM_LIST_INCSPLIT listing mode.
FileTime
Output parameter - contains date and time in standard MS DOS format.
UnpVer
Output parameter - RAR version needed to extract file.
It is encoded as 10 * Major version + minor version.
Method
Output parameter - packing method.
FileAttr
Output parameter - file attributes.
CmtBuf
File comments support is not implemented in the new DLL version yet.
Now CmtState is always 0.
/*
* Input parameter which should point to the buffer for file
* comments. Maximum comment size is limited to 64Kb. Comment text is
* a zero terminated string in OEM encoding. If the comment text is
* larger than the buffer size, the comment text will be truncated.
* If CmtBuf is set to NULL, comments will not be read.
*/
CmtBufSize
Input parameter which should contain size of buffer for archive
comments.
CmtSize
Output parameter containing size of comments actually read into the
buffer, should not exceed CmtBufSize.
CmtState
Output parameter.
Possible values
0 Absent comments
1 Comments read completely
ERAR_NO_MEMORY Not enough memory to extract comments
ERAR_BAD_DATA Broken comment
ERAR_UNKNOWN_FORMAT Unknown comment format
ERAR_SMALL_BUF Buffer too small, comments not completely read
Return values
~~~~~~~~~~~~~
0 Success
ERAR_END_ARCHIVE End of archive
ERAR_BAD_DATA File header broken
====================================================================
int PASCAL RARReadHeaderEx(HANDLE hArcData,
struct RARHeaderDataEx *HeaderData)
====================================================================
Description
~~~~~~~~~~~
Similar to RARReadHeader, but uses RARHeaderDataEx structure,
containing information about Unicode file names and 64 bit file sizes.
struct RARHeaderDataEx
{
char ArcName[1024];
wchar_t ArcNameW[1024];
char FileName[1024];
wchar_t FileNameW[1024];
unsigned int Flags;
unsigned int PackSize;
unsigned int PackSizeHigh;
unsigned int UnpSize;
unsigned int UnpSizeHigh;
unsigned int HostOS;
unsigned int FileCRC;
unsigned int FileTime;
unsigned int UnpVer;
unsigned int Method;
unsigned int FileAttr;
char *CmtBuf;
unsigned int CmtBufSize;
unsigned int CmtSize;
unsigned int CmtState;
unsigned int Reserved[1024];
};
====================================================================
int PASCAL RARProcessFile(HANDLE hArcData,
int Operation,
char *DestPath,
char *DestName)
====================================================================
Description
~~~~~~~~~~~
Performs action and moves the current position in the archive to
the next file. Extract or test the current file from the archive
opened in RAR_OM_EXTRACT mode. If the mode RAR_OM_LIST is set,
then a call to this function will simply skip the archive position
to the next file.
Parameters
~~~~~~~~~~
hArcData
This parameter should contain the archive handle obtained from the
RAROpenArchive function call.
Operation
File operation.
Possible values
RAR_SKIP Move to the next file in the archive. If the
archive is solid and RAR_OM_EXTRACT mode was set
when the archive was opened, the current file will
be processed - the operation will be performed
slower than a simple seek.
RAR_TEST Test the current file and move to the next file in
the archive. If the archive was opened with
RAR_OM_LIST mode, the operation is equal to
RAR_SKIP.
RAR_EXTRACT Extract the current file and move to the next file.
If the archive was opened with RAR_OM_LIST mode,
the operation is equal to RAR_SKIP.
DestPath
This parameter should point to a zero terminated string containing the
destination directory to which to extract files to. If DestPath is equal
to NULL, it means extract to the current directory. This parameter has
meaning only if DestName is NULL.
DestName
This parameter should point to a string containing the full path and name
to assign to extracted file or it can be NULL to use the default name.
If DestName is defined (not NULL), it overrides both the original file
name saved in the archive and path specigied in DestPath setting.
Both DestPath and DestName must be in OEM encoding. If necessary,
use CharToOem to convert text to OEM before passing to this function.
Return values
~~~~~~~~~~~~~
0 Success
ERAR_BAD_DATA File CRC error
ERAR_BAD_ARCHIVE Volume is not valid RAR archive
ERAR_UNKNOWN_FORMAT Unknown archive format
ERAR_EOPEN Volume open error
ERAR_ECREATE File create error
ERAR_ECLOSE File close error
ERAR_EREAD Read error
ERAR_EWRITE Write error
Note: if you wish to cancel extraction, return -1 when processing
UCM_PROCESSDATA callback message.
====================================================================
int PASCAL RARProcessFileW(HANDLE hArcData,
int Operation,
wchar_t *DestPath,
wchar_t *DestName)
====================================================================
Description
~~~~~~~~~~~
Unicode version of RARProcessFile. It uses Unicode DestPath
and DestName parameters, other parameters and return values
are the same as in RARProcessFile.
====================================================================
void PASCAL RARSetCallback(HANDLE hArcData,
int PASCAL (*CallbackProc)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2),
LPARAM UserData);
====================================================================
Description
~~~~~~~~~~~
Set a user-defined callback function to process Unrar events.
Parameters
~~~~~~~~~~
hArcData
This parameter should contain the archive handle obtained from the
RAROpenArchive function call.
CallbackProc
It should point to a user-defined callback function.
The function will be passed four parameters:
msg Type of event. Described below.
UserData User defined value passed to RARSetCallback.
P1 and P2 Event dependent parameters. Described below.
Possible events
UCM_CHANGEVOLUME Process volume change.
P1 Points to the zero terminated name
of the next volume.
P2 The function call mode:
RAR_VOL_ASK Required volume is absent. The function should
prompt user and return a positive value
to retry or return -1 value to terminate
operation. The function may also specify a new
volume name, placing it to the address specified
by P1 parameter.
RAR_VOL_NOTIFY Required volume is successfully opened.
This is a notification call and volume name
modification is not allowed. The function should
return a positive value to continue or -1
to terminate operation.
UCM_PROCESSDATA Process unpacked data. It may be used to read
a file while it is being extracted or tested
without actual extracting file to disk.
Return a positive value to continue process
or -1 to cancel the archive operation
P1 Address pointing to the unpacked data.
Function may refer to the data but must not
change it.
P2 Size of the unpacked data. It is guaranteed
only that the size will not exceed the maximum
dictionary size (4 Mb in RAR 3.0).
UCM_NEEDPASSWORD DLL needs a password to process archive.
This message must be processed if you wish
to be able to handle archives with encrypted
file names. It can be also used as replacement
of RARSetPassword function even for usual
encrypted files with non-encrypted names.
P1 Address pointing to the buffer for a password.
You need to copy a password here.
P2 Size of the password buffer.
UserData
User data passed to callback function.
Other functions of UnRAR.dll should not be called from the callback
function.
Return values
~~~~~~~~~~~~~
None
====================================================================
void PASCAL RARSetChangeVolProc(HANDLE hArcData,
int PASCAL (*ChangeVolProc)(char *ArcName,int Mode));
====================================================================
Obsoleted, use RARSetCallback instead.
====================================================================
void PASCAL RARSetProcessDataProc(HANDLE hArcData,
int PASCAL (*ProcessDataProc)(unsigned char *Addr,int Size))
====================================================================
Obsoleted, use RARSetCallback instead.
====================================================================
void PASCAL RARSetPassword(HANDLE hArcData,
char *Password);
====================================================================
Description
~~~~~~~~~~~
Set a password to decrypt files.
Parameters
~~~~~~~~~~
hArcData
This parameter should contain the archive handle obtained from the
RAROpenArchive function call.
Password
It should point to a string containing a zero terminated password.
Return values
~~~~~~~~~~~~~
None
====================================================================
void PASCAL RARGetDllVersion();
====================================================================
Description
~~~~~~~~~~~
Returns API version.
Parameters
~~~~~~~~~~
None.
Return values
~~~~~~~~~~~~~
Returns an integer value denoting UnRAR.dll API version, which is also
defined in unrar.h as RAR_DLL_VERSION. API version number is incremented
only in case of noticeable changes in UnRAR.dll API. Do not confuse it
with version of UnRAR.dll stored in DLL resources, which is incremented
with every DLL rebuild.
If RARGetDllVersion() returns a value lower than UnRAR.dll which your
application was designed for, it may indicate that DLL version is too old
and it will fail to provide all necessary functions to your application.
This function is absent in old versions of UnRAR.dll, so it is safer
to use LoadLibrary and GetProcAddress to access this function.

View File

@ -1,80 +0,0 @@
List of unrar.dll API changes. We do not include performance and reliability
improvements into this list, but this library and RAR/UnRAR tools share
the same source code. So the latest version of unrar.dll usually contains
same decompression algorithm changes as the latest UnRAR version.
============================================================================
-- 18 January 2008
all LONG parameters of CallbackProc function were changed
to LPARAM type for 64 bit mode compatibility.
-- 12 December 2007
Added new RAR_OM_LIST_INCSPLIT open mode for function RAROpenArchive.
-- 14 August 2007
Added NoCrypt\unrar_nocrypt.dll without decryption code for those
applications where presence of encryption or decryption code is not
allowed because of legal restrictions.
-- 14 December 2006
Added ERAR_MISSING_PASSWORD error type. This error is returned
if empty password is specified for encrypted file.
-- 12 June 2003
Added RARProcessFileW function, Unicode version of RARProcessFile
-- 9 August 2002
Added RAROpenArchiveEx function allowing to specify Unicode archive
name and get archive flags.
-- 24 January 2002
Added RARReadHeaderEx function allowing to read Unicode file names
and 64 bit file sizes.
-- 23 January 2002
Added ERAR_UNKNOWN error type (it is used for all errors which
do not have special ERAR code yet) and UCM_NEEDPASSWORD callback
message.
Unrar.dll automatically opens all next volumes not only when extracting,
but also in RAR_OM_LIST mode.
-- 27 November 2001
RARSetChangeVolProc and RARSetProcessDataProc are replaced by
the single callback function installed with RARSetCallback.
Unlike old style callbacks, the new function accepts the user defined
parameter. Unrar.dll still supports RARSetChangeVolProc and
RARSetProcessDataProc for compatibility purposes, but if you write
a new application, better use RARSetCallback.
File comments support is not implemented in the new DLL version yet.
Now CmtState is always 0.
-- 13 August 2001
Added RARGetDllVersion function, so you may distinguish old unrar.dll,
which used C style callback functions and the new one with PASCAL callbacks.
-- 10 May 2001
Callback functions in RARSetChangeVolProc and RARSetProcessDataProc
use PASCAL style call convention now.

View File

@ -1 +0,0 @@
This is x64 version of unrar.dll.

Binary file not shown.

Binary file not shown.

View File

@ -1,177 +0,0 @@
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll.
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,
stable and foolproof.
Notice that it has INCOMPATIBLE interface.
It enables reading and unpacking of archives created with the
RAR/WinRAR archivers. There is a low-level interface which is very
similar to the C interface provided by UnRAR. There is also a
higher level interface which makes some common operations easier.
"""
__version__ = '0.99.2'
try:
WindowsError
in_windows = True
except NameError:
in_windows = False
if in_windows:
from windows import RarFileImplementation
else:
from unix import RarFileImplementation
import fnmatch, time, weakref
class RarInfo(object):
"""Represents a file header in an archive. Don't instantiate directly.
Use only to obtain information about file.
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT.
USE METHODS OF RarFile CLASS INSTEAD.
Properties:
index - index of file within the archive
filename - name of the file in the archive including path (if any)
datetime - file date/time as a struct_time suitable for time.strftime
isdir - True if the file is a directory
size - size in bytes of the uncompressed file
comment - comment associated with the file
Note - this is not currently intended to be a Python file-like object.
"""
def __init__(self, rarfile, data):
self.rarfile = weakref.proxy(rarfile)
self.index = data['index']
self.filename = data['filename']
self.isdir = data['isdir']
self.size = data['size']
self.datetime = data['datetime']
self.comment = data['comment']
def __str__(self):
try :
arcName = self.rarfile.archiveName
except ReferenceError:
arcName = "[ARCHIVE_NO_LONGER_LOADED]"
return '<RarInfo "%s" in "%s">' % (self.filename, arcName)
class RarFile(RarFileImplementation):
def __init__(self, archiveName, password=None):
"""Instantiate the archive.
archiveName is the name of the RAR file.
password is used to decrypt the files in the archive.
Properties:
comment - comment associated with the archive
>>> print RarFile('test.rar').comment
This is a test.
"""
self.archiveName = archiveName
RarFileImplementation.init(self, password)
def __del__(self):
self.destruct()
def infoiter(self):
"""Iterate over all the files in the archive, generating RarInfos.
>>> import os
>>> for fileInArchive in RarFile('test.rar').infoiter():
... print os.path.split(fileInArchive.filename)[-1],
... print fileInArchive.isdir,
... print fileInArchive.size,
... print fileInArchive.comment,
... print tuple(fileInArchive.datetime)[0:5],
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime)
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47
"""
for params in RarFileImplementation.infoiter(self):
yield RarInfo(self, params)
def infolist(self):
"""Return a list of RarInfos, descripting the contents of the archive."""
return list(self.infoiter())
def read_files(self, condition='*'):
"""Read specific files from archive into memory.
If "condition" is a list of numbers, then return files which have those positions in infolist.
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
and returns boolean True (extract) or False (skip).
If "condition" is omitted, all files are returned.
Returns list of tuples (RarInfo info, str contents)
"""
checker = condition2checker(condition)
return RarFileImplementation.read_files(self, checker)
def extract(self, condition='*', path='.', withSubpath=True, overwrite=True):
"""Extract specific files from archive to disk.
If "condition" is a list of numbers, then extract files which have those positions in infolist.
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
and returns either boolean True (extract) or boolean False (skip).
DEPRECATED: If "condition" callback returns string (only supported for Windows) -
that string will be used as a new name to save the file under.
If "condition" is omitted, all files are extracted.
"path" is a directory to extract to
"withSubpath" flag denotes whether files are extracted with their full path in the archive.
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.
Returns list of RarInfos for extracted files."""
checker = condition2checker(condition)
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite)
def condition2checker(condition):
"""Converts different condition types to callback"""
if type(condition) in [str, unicode]:
def smatcher(info):
return fnmatch.fnmatch(info.filename, condition)
return smatcher
elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]:
def imatcher(info):
return info.index in condition
return imatcher
elif callable(condition):
return condition
else:
raise TypeError

View File

@ -1,30 +0,0 @@
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Low level interface - see UnRARDLL\UNRARDLL.TXT
class ArchiveHeaderBroken(Exception): pass
class InvalidRARArchive(Exception): pass
class FileOpenError(Exception): pass
class IncorrectRARPassword(Exception): pass
class InvalidRARArchiveUsage(Exception): pass

View File

@ -1,139 +0,0 @@
import os, sys
import UnRAR2
from UnRAR2.rar_exceptions import *
def cleanup(dir='test'):
for path, dirs, files in os.walk(dir):
for fn in files:
os.remove(os.path.join(path, fn))
for dir in dirs:
os.removedirs(os.path.join(path, dir))
# reuse RarArchive object, en
cleanup()
rarc = UnRAR2.RarFile('test.rar')
rarc.infolist()
for info in rarc.infoiter():
saveinfo = info
assert (str(info)=="""<RarInfo "test" in "test.rar">""")
break
rarc.extract()
assert os.path.exists('test'+os.sep+'test.txt')
assert os.path.exists('test'+os.sep+'this.py')
del rarc
assert (str(saveinfo)=="""<RarInfo "test" in "[ARCHIVE_NO_LONGER_LOADED]">""")
cleanup()
# extract all the files in test.rar
cleanup()
UnRAR2.RarFile('test.rar').extract()
assert os.path.exists('test'+os.sep+'test.txt')
assert os.path.exists('test'+os.sep+'this.py')
cleanup()
# extract all the files in test.rar matching the wildcard *.txt
cleanup()
UnRAR2.RarFile('test.rar').extract('*.txt')
assert os.path.exists('test'+os.sep+'test.txt')
assert not os.path.exists('test'+os.sep+'this.py')
cleanup()
# check the name and size of each file, extracting small ones
cleanup()
archive = UnRAR2.RarFile('test.rar')
assert archive.comment == 'This is a test.'
archive.extract(lambda rarinfo: rarinfo.size <= 1024)
for rarinfo in archive.infoiter():
if rarinfo.size <= 1024 and not rarinfo.isdir:
assert rarinfo.size == os.stat(rarinfo.filename).st_size
assert file('test'+os.sep+'test.txt', 'rt').read() == 'This is only a test.'
assert not os.path.exists('test'+os.sep+'this.py')
cleanup()
# extract this.py, overriding it's destination
cleanup('test2')
archive = UnRAR2.RarFile('test.rar')
archive.extract('*.py', 'test2', False)
assert os.path.exists('test2'+os.sep+'this.py')
cleanup('test2')
# extract test.txt to memory
cleanup()
archive = UnRAR2.RarFile('test.rar')
entries = UnRAR2.RarFile('test.rar').read_files('*test.txt')
assert len(entries)==1
assert entries[0][0].filename.endswith('test.txt')
assert entries[0][1]=='This is only a test.'
# extract all the files in test.rar with overwriting
cleanup()
fo = open('test'+os.sep+'test.txt',"wt")
fo.write("blah")
fo.close()
UnRAR2.RarFile('test.rar').extract('*.txt')
assert open('test'+os.sep+'test.txt',"rt").read()!="blah"
cleanup()
# extract all the files in test.rar without overwriting
cleanup()
fo = open('test'+os.sep+'test.txt',"wt")
fo.write("blahblah")
fo.close()
UnRAR2.RarFile('test.rar').extract('*.txt', overwrite = False)
assert open('test'+os.sep+'test.txt',"rt").read()=="blahblah"
cleanup()
# list big file in an archive
list(UnRAR2.RarFile('test_nulls.rar').infoiter())
# extract files from an archive with protected files
cleanup()
UnRAR2.RarFile('test_protected_files.rar', password="protected").extract()
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
cleanup()
errored = False
try:
UnRAR2.RarFile('test_protected_files.rar', password="proteqted").extract()
except IncorrectRARPassword:
errored = True
assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
assert errored
cleanup()
# extract files from an archive with protected headers
cleanup()
UnRAR2.RarFile('test_protected_headers.rar', password="secret").extract()
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
cleanup()
errored = False
try:
UnRAR2.RarFile('test_protected_headers.rar', password="seqret").extract()
except IncorrectRARPassword:
errored = True
assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
assert errored
cleanup()
# make sure docstring examples are working
import doctest
doctest.testmod(UnRAR2)
# update documentation
import pydoc
pydoc.writedoc(UnRAR2)
# cleanup
try:
os.remove('__init__.pyc')
except:
pass

View File

@ -1,175 +0,0 @@
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Unix version uses unrar command line executable
import subprocess
import gc
import os, os.path
import time, re
from rar_exceptions import *
class UnpackerNotInstalled(Exception): pass
rar_executable_cached = None
def call_unrar(params):
"Calls rar/unrar command line executable, returns stdout pipe"
global rar_executable_cached
if rar_executable_cached is None:
for command in ('unrar', 'rar'):
try:
subprocess.Popen([command], stdout=subprocess.PIPE)
rar_executable_cached = command
break
except OSError:
pass
if rar_executable_cached is None:
raise UnpackerNotInstalled("No suitable RAR unpacker installed")
assert type(params) == list, "params must be list"
args = [rar_executable_cached] + params
try:
gc.disable() # See http://bugs.python.org/issue1336
return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
finally:
gc.enable()
class RarFileImplementation(object):
def init(self, password=None):
self.password = password
stdoutdata, stderrdata = self.call('v', []).communicate()
for line in stderrdata.splitlines():
if line.strip().startswith("Cannot open"):
raise FileOpenError
if line.find("CRC failed")>=0:
raise IncorrectRARPassword
accum = []
source = iter(stdoutdata.splitlines())
line = ''
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
line = source.next()
while not line.startswith('Pathname/Comment'):
accum.append(line.rstrip('\n'))
line = source.next()
if len(accum):
accum[0] = accum[0][9:]
self.comment = '\n'.join(accum[:-1])
else:
self.comment = None
def escaped_password(self):
return '-' if self.password == None else self.password
def call(self, cmd, options=[], files=[]):
options2 = options + ['p'+self.escaped_password()]
soptions = ['-'+x for x in options2]
return call_unrar([cmd]+soptions+['--',self.archiveName]+files)
def infoiter(self):
stdoutdata, stderrdata = self.call('v', ['c-']).communicate()
for line in stderrdata.splitlines():
if line.strip().startswith("Cannot open"):
raise FileOpenError
accum = []
source = iter(stdoutdata.splitlines())
line = ''
while not line.startswith('--------------'):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
if line.find("CRC failed")>=0:
raise IncorrectRARPassword
line = source.next()
line = source.next()
i = 0
re_spaces = re.compile(r"\s+")
while not line.startswith('--------------'):
accum.append(line)
if len(accum)==2:
data = {}
data['index'] = i
data['filename'] = accum[0].strip()
info = re_spaces.split(accum[1].strip())
data['size'] = int(info[0])
attr = info[5]
data['isdir'] = 'd' in attr.lower()
data['datetime'] = time.strptime(info[3]+" "+info[4], '%d-%m-%y %H:%M')
data['comment'] = None
yield data
accum = []
i += 1
line = source.next()
def read_files(self, checker):
res = []
for info in self.infoiter():
checkres = checker(info)
if checkres==True and not info.isdir:
pipe = self.call('p', ['inul'], [info.filename]).stdout
res.append((info, pipe.read()))
return res
def extract(self, checker, path, withSubpath, overwrite):
res = []
command = 'x'
if not withSubpath:
command = 'e'
options = []
if overwrite:
options.append('o+')
else:
options.append('o-')
if not path.endswith(os.sep):
path += os.sep
names = []
for info in self.infoiter():
checkres = checker(info)
if type(checkres) in [str, unicode]:
raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows")
if checkres==True and not info.isdir:
names.append(info.filename)
res.append(info)
names.append(path)
proc = self.call(command, options, names)
stdoutdata, stderrdata = proc.communicate()
if stderrdata.find("CRC failed")>=0:
raise IncorrectRARPassword
return res
def destruct(self):
pass

View File

@ -1,309 +0,0 @@
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Low level interface - see UnRARDLL\UNRARDLL.TXT
from __future__ import generators
import ctypes, ctypes.wintypes
import os, os.path, sys
import Queue
import time
from rar_exceptions import *
ERAR_END_ARCHIVE = 10
ERAR_NO_MEMORY = 11
ERAR_BAD_DATA = 12
ERAR_BAD_ARCHIVE = 13
ERAR_UNKNOWN_FORMAT = 14
ERAR_EOPEN = 15
ERAR_ECREATE = 16
ERAR_ECLOSE = 17
ERAR_EREAD = 18
ERAR_EWRITE = 19
ERAR_SMALL_BUF = 20
ERAR_UNKNOWN = 21
RAR_OM_LIST = 0
RAR_OM_EXTRACT = 1
RAR_SKIP = 0
RAR_TEST = 1
RAR_EXTRACT = 2
RAR_VOL_ASK = 0
RAR_VOL_NOTIFY = 1
RAR_DLL_VERSION = 3
# enum UNRARCALLBACK_MESSAGES
UCM_CHANGEVOLUME = 0
UCM_PROCESSDATA = 1
UCM_NEEDPASSWORD = 2
architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8
dll_name = "unrar.dll"
if architecture_bits == 64:
dll_name = "x64\\unrar64.dll"
try:
unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name))
except WindowsError:
unrar = ctypes.WinDLL(dll_name)
class RAROpenArchiveDataEx(ctypes.Structure):
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
self.CmtBuf = ctypes.c_buffer(64*1024)
ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
_fields_ = [
('ArcName', ctypes.c_char_p),
('ArcNameW', ctypes.c_wchar_p),
('OpenMode', ctypes.c_uint),
('OpenResult', ctypes.c_uint),
('_CmtBuf', ctypes.c_voidp),
('CmtBufSize', ctypes.c_uint),
('CmtSize', ctypes.c_uint),
('CmtState', ctypes.c_uint),
('Flags', ctypes.c_uint),
('Reserved', ctypes.c_uint*32),
]
class RARHeaderDataEx(ctypes.Structure):
def __init__(self):
self.CmtBuf = ctypes.c_buffer(64*1024)
ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
_fields_ = [
('ArcName', ctypes.c_char*1024),
('ArcNameW', ctypes.c_wchar*1024),
('FileName', ctypes.c_char*1024),
('FileNameW', ctypes.c_wchar*1024),
('Flags', ctypes.c_uint),
('PackSize', ctypes.c_uint),
('PackSizeHigh', ctypes.c_uint),
('UnpSize', ctypes.c_uint),
('UnpSizeHigh', ctypes.c_uint),
('HostOS', ctypes.c_uint),
('FileCRC', ctypes.c_uint),
('FileTime', ctypes.c_uint),
('UnpVer', ctypes.c_uint),
('Method', ctypes.c_uint),
('FileAttr', ctypes.c_uint),
('_CmtBuf', ctypes.c_voidp),
('CmtBufSize', ctypes.c_uint),
('CmtSize', ctypes.c_uint),
('CmtState', ctypes.c_uint),
('Reserved', ctypes.c_uint*1024),
]
def DosDateTimeToTimeTuple(dosDateTime):
"""Convert an MS-DOS format date time to a Python time tuple.
"""
dosDate = dosDateTime >> 16
dosTime = dosDateTime & 0xffff
day = dosDate & 0x1f
month = (dosDate >> 5) & 0xf
year = 1980 + (dosDate >> 9)
second = 2*(dosTime & 0x1f)
minute = (dosTime >> 5) & 0x3f
hour = dosTime >> 11
return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1)))
def _wrap(restype, function, argtypes):
result = function
result.argtypes = argtypes
result.restype = restype
return result
RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, [])
RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)])
RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)])
_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p])
def RARSetPassword(*args, **kwargs):
_RARSetPassword(*args, **kwargs)
RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p])
RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE])
UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long)
RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long])
RARExceptions = {
ERAR_NO_MEMORY : MemoryError,
ERAR_BAD_DATA : ArchiveHeaderBroken,
ERAR_BAD_ARCHIVE : InvalidRARArchive,
ERAR_EOPEN : FileOpenError,
}
class PassiveReader:
"""Used for reading files to memory"""
def __init__(self, usercallback = None):
self.buf = []
self.ucb = usercallback
def _callback(self, msg, UserData, P1, P2):
if msg == UCM_PROCESSDATA:
data = (ctypes.c_char*P2).from_address(P1).raw
if self.ucb!=None:
self.ucb(data)
else:
self.buf.append(data)
return 1
def get_result(self):
return ''.join(self.buf)
class RarInfoIterator(object):
def __init__(self, arc):
self.arc = arc
self.index = 0
self.headerData = RARHeaderDataEx()
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
if self.res==ERAR_BAD_DATA:
raise IncorrectRARPassword
self.arc.lockStatus = "locked"
self.arc.needskip = False
def __iter__(self):
return self
def next(self):
if self.index>0:
if self.arc.needskip:
RARProcessFile(self.arc._handle, RAR_SKIP, None, None)
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
if self.res:
raise StopIteration
self.arc.needskip = True
data = {}
data['index'] = self.index
data['filename'] = self.headerData.FileName
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
if self.headerData.CmtState == 1:
data['comment'] = self.headerData.CmtBuf.value
else:
data['comment'] = None
self.index += 1
return data
def __del__(self):
self.arc.lockStatus = "finished"
def generate_password_provider(password):
def password_provider_callback(msg, UserData, P1, P2):
if msg == UCM_NEEDPASSWORD and password!=None:
(ctypes.c_char*P2).from_address(P1).value = password
return 1
return password_provider_callback
class RarFileImplementation(object):
def init(self, password=None):
self.password = password
archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT)
self._handle = RAROpenArchiveEx(ctypes.byref(archiveData))
self.c_callback = UNRARCALLBACK(generate_password_provider(self.password))
RARSetCallback(self._handle, self.c_callback, 1)
if archiveData.OpenResult != 0:
raise RARExceptions[archiveData.OpenResult]
if archiveData.CmtState == 1:
self.comment = archiveData.CmtBuf.value
else:
self.comment = None
if password:
RARSetPassword(self._handle, password)
self.lockStatus = "ready"
def destruct(self):
if self._handle and RARCloseArchive:
RARCloseArchive(self._handle)
def make_sure_ready(self):
if self.lockStatus == "locked":
raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one")
if self.lockStatus == "finished":
self.destruct()
self.init(self.password)
def infoiter(self):
self.make_sure_ready()
return RarInfoIterator(self)
def read_files(self, checker):
res = []
for info in self.infoiter():
if checker(info) and not info.isdir:
reader = PassiveReader()
c_callback = UNRARCALLBACK(reader._callback)
RARSetCallback(self._handle, c_callback, 1)
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
if tmpres==ERAR_BAD_DATA:
raise IncorrectRARPassword
self.needskip = False
res.append((info, reader.get_result()))
return res
def extract(self, checker, path, withSubpath, overwrite):
res = []
for info in self.infoiter():
checkres = checker(info)
if checkres!=False and not info.isdir:
if checkres==True:
fn = info.filename
if not withSubpath:
fn = os.path.split(fn)[-1]
target = os.path.join(path, fn)
else:
raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows"
target = checkres
if overwrite or (not os.path.exists(target)):
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)
if tmpres==ERAR_BAD_DATA:
raise IncorrectRARPassword
self.needskip = False
res.append(info)
return res

6
appveyor.yml Normal file
View File

@ -0,0 +1,6 @@
version: 1.0.{build}
build_script:
- cmd: powershell -exec bypass -File windows\fullbuild.ps1
artifacts:
- path: dist\*.exe
name: ComicTagger

260
comet.py
View File

@ -1,260 +0,0 @@
"""
A python class to encapsulate CoMet data
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from datetime import datetime
import zipfile
from pprint import pprint
import xml.etree.ElementTree as ET
from genericmetadata import GenericMetadata
import utils
class CoMet:
writer_synonyms = ['writer', 'plotter', 'scripter']
penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ]
inker_synonyms = [ 'inker', 'artist', 'finishes' ]
colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ]
letterer_synonyms = [ 'letterer']
cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ]
editor_synonyms = [ 'editor']
def metadataFromString( self, string ):
tree = ET.ElementTree(ET.fromstring( string ))
return self.convertXMLToMetadata( tree )
def stringFromMetadata( self, metadata ):
header = '<?xml version="1.0" encoding="UTF-8"?>\n'
tree = self.convertMetadataToXML( self, metadata )
return header + ET.tostring(tree.getroot())
def indent( self, elem, level=0 ):
# for making the XML output readable
i = "\n" + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self.indent( elem, level+1 )
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def convertMetadataToXML( self, filename, metadata ):
#shorthand for the metadata
md = metadata
# build a tree structure
root = ET.Element("comet")
root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/"
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
root.attrib['xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
#helper func
def assign( comet_entry, md_entry):
if md_entry is not None:
ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry)
# title is manditory
if md.title is None:
md.title = ""
assign( 'title', md.title )
assign( 'series', md.series )
assign( 'issue', md.issue ) #must be int??
assign( 'volume', md.volume )
assign( 'description', md.comments )
assign( 'publisher', md.publisher )
assign( 'pages', md.pageCount )
assign( 'format', md.format )
assign( 'language', md.language )
assign( 'rating', md.maturityRating )
assign( 'price', md.price )
assign( 'isVersionOf', md.isVersionOf )
assign( 'rights', md.rights )
assign( 'identifier', md.identifier )
assign( 'lastMark', md.lastMark )
assign( 'genre', md.genre ) # TODO repeatable
if md.characters is not None:
char_list = [ c.strip() for c in md.characters.split(',') ]
for c in char_list:
assign( 'character', c )
if md.manga is not None and md.manga == "YesAndRightToLeft":
assign( 'readingDirection', "rtl")
date_str = ""
if md.year is not None:
date_str = str(md.year).zfill(4)
if md.month is not None:
date_str += "-" + str(md.month).zfill(2)
assign( 'date', date_str )
assign( 'coverImage', md.coverImage )
# need to specially process the credits, since they are structured differently than CIX
credit_writer_list = list()
credit_penciller_list = list()
credit_inker_list = list()
credit_colorist_list = list()
credit_letterer_list = list()
credit_cover_list = list()
credit_editor_list = list()
# loop thru credits, and build a list for each role that CoMet supports
for credit in metadata.credits:
if credit['role'].lower() in set( self.writer_synonyms ):
ET.SubElement(root, 'writer').text = u"{0}".format(credit['person'])
if credit['role'].lower() in set( self.penciller_synonyms ):
ET.SubElement(root, 'penciller').text = u"{0}".format(credit['person'])
if credit['role'].lower() in set( self.inker_synonyms ):
ET.SubElement(root, 'inker').text = u"{0}".format(credit['person'])
if credit['role'].lower() in set( self.colorist_synonyms ):
ET.SubElement(root, 'colorist').text = u"{0}".format(credit['person'])
if credit['role'].lower() in set( self.letterer_synonyms ):
ET.SubElement(root, 'letterer').text = u"{0}".format(credit['person'])
if credit['role'].lower() in set( self.cover_synonyms ):
ET.SubElement(root, 'coverDesigner').text = u"{0}".format(credit['person'])
if credit['role'].lower() in set( self.editor_synonyms ):
ET.SubElement(root, 'editor').text = u"{0}".format(credit['person'])
# self pretty-print
self.indent(root)
# wrap it in an ElementTree instance, and save as XML
tree = ET.ElementTree(root)
return tree
def convertXMLToMetadata( self, tree ):
root = tree.getroot()
if root.tag != 'comet':
raise 1
return None
metadata = GenericMetadata()
md = metadata
# Helper function
def xlate( tag ):
node = root.find( tag )
if node is not None:
return node.text
else:
return None
md.series = xlate( 'series' )
md.title = xlate( 'title' )
md.issue = xlate( 'issue' )
md.volume = xlate( 'volume' )
md.comments = xlate( 'description' )
md.publisher = xlate( 'publisher' )
md.language = xlate( 'language' )
md.format = xlate( 'format' )
md.pageCount = xlate( 'pages' )
md.maturityRating = xlate( 'rating' )
md.price = xlate( 'price' )
md.isVersionOf = xlate( 'isVersionOf' )
md.rights = xlate( 'rights' )
md.identifier = xlate( 'identifier' )
md.lastMark = xlate( 'lastMark' )
md.genre = xlate( 'genre' ) # TODO - repeatable field
date = xlate( 'date' )
if date is not None:
parts = date.split('-')
if len( parts) > 0:
md.year = parts[0]
if len( parts) > 1:
md.month = parts[1]
md.coverImage = xlate( 'coverImage' )
readingDirection = xlate( 'readingDirection' )
if readingDirection is not None and readingDirection == "rtl":
md.manga = "YesAndRightToLeft"
# loop for character tags
char_list = []
for n in root:
if n.tag == 'character':
char_list.append(n.text.strip())
md.characters = utils.listToString( char_list )
# Now extract the credit info
for n in root:
if ( n.tag == 'writer' or
n.tag == 'penciller' or
n.tag == 'inker' or
n.tag == 'colorist' or
n.tag == 'letterer' or
n.tag == 'editor'
):
metadata.addCredit( n.text.strip(), n.tag.title() )
if n.tag == 'coverDesigner':
metadata.addCredit( n.text.strip(), "Cover" )
metadata.isEmpty = False
return metadata
#verify that the string actually contains CoMet data in XML format
def validateString( self, string ):
try:
tree = ET.ElementTree(ET.fromstring( string ))
root = tree.getroot()
if root.tag != 'comet':
raise Exception
except:
return False
return True
def writeToExternalFile( self, filename, metadata ):
tree = self.convertMetadataToXML( self, metadata )
#ET.dump(tree)
tree.write(filename, encoding='utf-8')
def readFromExternalFile( self, filename ):
tree = ET.parse( filename )
return self.convertXMLToMetadata( tree )

View File

@ -1,792 +0,0 @@
"""
A python class to represent a single comic, be it file or folder of images
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import zipfile
import os
import struct
import sys
import tempfile
import subprocess
import platform
if platform.system() == "Windows":
import _subprocess
import time
import StringIO
try:
import Image
pil_available = True
except ImportError:
pil_available = False
sys.path.insert(0, os.path.abspath(".") )
import UnRAR2
from UnRAR2.rar_exceptions import *
from options import Options, MetaDataStyle
from comicinfoxml import ComicInfoXml
from comicbookinfo import ComicBookInfo
from comet import CoMet
from genericmetadata import GenericMetadata, PageType
from filenameparser import FileNameParser
class ZipArchiver:
def __init__( self, path ):
self.path = path
def getArchiveComment( self ):
zf = zipfile.ZipFile( self.path, 'r' )
comment = zf.comment
zf.close()
return comment
def setArchiveComment( self, comment ):
return self.writeZipComment( self.path, comment )
def readArchiveFile( self, archive_file ):
zf = zipfile.ZipFile( self.path, 'r' )
data = zf.read( archive_file )
zf.close()
return data
def removeArchiveFile( self, archive_file ):
try:
self.rebuildZipFile( [ archive_file ] )
except:
return False
else:
return True
def writeArchiveFile( self, archive_file, data ):
# At the moment, no other option but to rebuild the whole
# zip archive w/o the indicated file. Very sucky, but maybe
# another solution can be found
try:
self.rebuildZipFile( [ archive_file ] )
#now just add the archive file as a new one
zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED )
zf.writestr( archive_file, data )
zf.close()
return True
except:
return False
def getArchiveFilenameList( self ):
zf = zipfile.ZipFile( self.path, 'r' )
namelist = zf.namelist()
zf.close()
return namelist
# zip helper func
def rebuildZipFile( self, exclude_list ):
# this recompresses the zip archive, without the files in the exclude_list
#print "Rebuilding zip {0} without {1}".format( self.path, exclude_list )
# generate temp file
tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) )
os.close( tmp_fd )
zin = zipfile.ZipFile (self.path, 'r')
zout = zipfile.ZipFile (tmp_name, 'w')
for item in zin.infolist():
buffer = zin.read(item.filename)
if ( item.filename not in exclude_list ):
zout.writestr(item, buffer)
#preserve the old comment
zout.comment = zin.comment
zout.close()
zin.close()
# replace with the new file
os.remove( self.path )
os.rename( tmp_name, self.path )
def writeZipComment( self, filename, comment ):
"""
This is a custom function for writing a comment to a zip file,
since the built-in one doesn't seem to work on Windows and Mac OS/X
Fortunately, the zip comment is at the end of the file, and it's
easy to manipulate. See this website for more info:
see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure
"""
#get file size
statinfo = os.stat(filename)
file_length = statinfo.st_size
try:
fo = open(filename, "r+b")
#the starting position, relative to EOF
pos = -4
found = False
value = bytearray()
# walk backwards to find the "End of Central Directory" record
while ( not found ) and ( -pos != file_length ):
# seek, relative to EOF
fo.seek( pos, 2)
value = fo.read( 4 )
#look for the end of central directory signature
if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]):
found = True
else:
# not found, step back another byte
pos = pos - 1
#print pos,"{1} int: {0:x}".format(bytearray(value)[0], value)
if found:
# now skip forward 20 bytes to the comment length word
pos += 20
fo.seek( pos, 2)
# Pack the length of the comment string
format = "H" # one 2-byte integer
comment_length = struct.pack(format, len(comment)) # pack integer in a binary string
# write out the length
fo.write( comment_length )
fo.seek( pos+2, 2)
# write out the comment itself
fo.write( comment )
fo.truncate()
fo.close()
else:
raise Exception('Failed to write comment to zip file!')
except:
return False
else:
return True
def copyFromArchive( self, otherArchive ):
# Replace the current zip with one copied from another archive
try:
zout = zipfile.ZipFile (self.path, 'w')
for fname in otherArchive.getArchiveFilenameList():
data = otherArchive.readArchiveFile( fname )
zout.writestr( fname, data )
zout.close()
#preserve the old comment
comment = otherArchive.getArchiveComment()
if comment is not None:
if not self.writeZipComment( self.path, comment ):
return False
except:
return False
else:
return True
#------------------------------------------
# RAR implementation
class RarArchiver:
def __init__( self, path ):
self.path = path
self.rar_exe_path = None
self.devnull = open(os.devnull, "w")
# windows only, keeps the cmd.exe from popping up
if platform.system() == "Windows":
self.startupinfo = subprocess.STARTUPINFO()
self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW
else:
self.startupinfo = None
def __del__(self):
self.devnull.close()
def getArchiveComment( self ):
rarc = UnRAR2.RarFile( self.path )
return rarc.comment
def setArchiveComment( self, comment ):
if self.rar_exe_path is not None:
try:
# write comment to temp file
tmp_fd, tmp_name = tempfile.mkstemp()
f = os.fdopen(tmp_fd, 'w+b')
f.write( comment )
f.close()
working_dir = os.path.dirname( os.path.abspath( self.path ) )
# use external program to write comment to Rar archive
subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
startupinfo=self.startupinfo,
stdout=self.devnull)
if platform.system() == "Darwin":
time.sleep(1)
os.remove( tmp_name)
except:
return False
else:
return True
else:
return False
def readArchiveFile( self, archive_file ):
entries = UnRAR2.RarFile( self.path ).read_files( archive_file )
#entries is a list of of tuples: ( rarinfo, filedata)
if (len(entries) == 1):
return entries[0][1]
else:
return ""
def writeArchiveFile( self, archive_file, data ):
if self.rar_exe_path is not None:
try:
tmp_folder = tempfile.mkdtemp()
tmp_file = os.path.join( tmp_folder, archive_file )
working_dir = os.path.dirname( os.path.abspath( self.path ) )
# TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt"
# will need to create the subfolder above, I guess...
f = open(tmp_file, 'w')
f.write( data )
f.close()
# use external program to write file to Rar archive
subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
startupinfo=self.startupinfo,
stdout=self.devnull)
if platform.system() == "Darwin":
time.sleep(1)
os.remove( tmp_file)
os.rmdir( tmp_folder)
except:
return False
else:
return True
else:
return False
def removeArchiveFile( self, archive_file ):
if self.rar_exe_path is not None:
try:
# use external program to remove file from Rar archive
subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file],
startupinfo=self.startupinfo,
stdout=self.devnull)
if platform.system() == "Darwin":
time.sleep(1)
except:
return False
else:
return True
else:
return False
def getArchiveFilenameList( self ):
rarc = UnRAR2.RarFile( self.path )
return [ item.filename for item in rarc.infolist() ]
#------------------------------------------
# Folder implementation
class FolderArchiver:
def __init__( self, path ):
self.path = path
self.comment_file_name = "ComicTaggerFolderComment.txt"
def getArchiveComment( self ):
return self.readArchiveFile( self.comment_file_name )
def setArchiveComment( self, comment ):
return self.writeArchiveFile( self.comment_file_name, comment )
def readArchiveFile( self, archive_file ):
data = ""
fname = os.path.join( self.path, archive_file )
try:
with open( fname, 'rb' ) as f:
data = f.read()
f.close()
except IOError as e:
pass
return data
def writeArchiveFile( self, archive_file, data ):
fname = os.path.join( self.path, archive_file )
try:
with open(fname, 'w+') as f:
f.write( data )
f.close()
except:
return False
else:
return True
def removeArchiveFile( self, archive_file ):
fname = os.path.join( self.path, archive_file )
try:
os.remove( fname )
except:
return False
else:
return True
def getArchiveFilenameList( self ):
return self.listFiles( self.path )
def listFiles( self, folder ):
itemlist = list()
for item in os.listdir( folder ):
itemlist.append( item )
if os.path.isdir( item ):
itemlist.extend( self.listFiles( os.path.join( folder, item ) ))
return itemlist
#------------------------------------------
# Unknown implementation
class UnknownArchiver:
def __init__( self, path ):
self.path = path
def getArchiveComment( self ):
return ""
def setArchiveComment( self, comment ):
return False
def readArchiveFilen( self ):
return ""
def writeArchiveFile( self, archive_file, data ):
return False
def removeArchiveFile( self, archive_file ):
return False
def getArchiveFilenameList( self ):
return []
#------------------------------------------------------------------
class ComicArchive:
class ArchiveType:
Zip, Rar, Folder, Unknown = range(4)
def __init__( self, path ):
self.path = path
self.ci_xml_filename = 'ComicInfo.xml'
self.comet_default_filename = 'CoMet.xml'
self.comet_filename = None
if self.zipTest():
self.archive_type = self.ArchiveType.Zip
self.archiver = ZipArchiver( self.path )
elif self.rarTest():
self.archive_type = self.ArchiveType.Rar
self.archiver = RarArchiver( self.path )
elif os.path.isdir( self.path ):
self.archive_type = self.ArchiveType.Folder
self.archiver = FolderArchiver( self.path )
else:
self.archive_type = self.ArchiveType.Unknown
self.archiver = UnknownArchiver( self.path )
def setExternalRarProgram( self, rar_exe_path ):
if self.isRar():
self.archiver.rar_exe_path = rar_exe_path
def zipTest( self ):
return zipfile.is_zipfile( self.path )
def rarTest( self ):
try:
rarc = UnRAR2.RarFile( self.path )
except: # InvalidRARArchive:
return False
else:
return True
def isZip( self ):
return self.archive_type == self.ArchiveType.Zip
def isRar( self ):
return self.archive_type == self.ArchiveType.Rar
def isFolder( self ):
return self.archive_type == self.ArchiveType.Folder
def isWritable( self ):
if self.archive_type == self.ArchiveType.Unknown :
return False
elif self.isRar() and self.archiver.rar_exe_path is None:
return False
elif not os.access(self.path, os.W_OK):
return False
elif ((self.archive_type != self.ArchiveType.Folder) and
(not os.access( os.path.dirname( os.path.abspath(self.path)), os.W_OK ))):
return False
return True
def isWritableForStyle( self, data_style ):
if self.isRar() and data_style == MetaDataStyle.CBI:
return False
return self.isWritable()
def seemsToBeAComicArchive( self ):
# Do we even care about extensions??
ext = os.path.splitext(self.path)[1].lower()
if (
( self.isZip() or self.isRar() or self.isFolder() )
and
( self.getNumberOfPages() > 2)
):
return True
else:
return False
def readMetadata( self, style ):
if style == MetaDataStyle.CIX:
return self.readCIX()
elif style == MetaDataStyle.CBI:
return self.readCBI()
elif style == MetaDataStyle.COMET:
return self.readCoMet()
else:
return GenericMetadata()
def writeMetadata( self, metadata, style ):
if style == MetaDataStyle.CIX:
return self.writeCIX( metadata )
elif style == MetaDataStyle.CBI:
return self.writeCBI( metadata )
elif style == MetaDataStyle.COMET:
return self.writeCoMet( metadata )
def hasMetadata( self, style ):
if style == MetaDataStyle.CIX:
return self.hasCIX()
elif style == MetaDataStyle.CBI:
return self.hasCBI()
elif style == MetaDataStyle.COMET:
return self.hasCoMet()
else:
return False
def removeMetadata( self, style ):
if style == MetaDataStyle.CIX:
return self.removeCIX()
elif style == MetaDataStyle.CBI:
return self.removeCBI()
elif style == MetaDataStyle.COMET:
return self.removeCoMet()
def getPage( self, index ):
image_data = None
filename = self.getPageName( index )
if filename is not None:
image_data = self.archiver.readArchiveFile( filename )
return image_data
def getPageName( self, index ):
page_list = self.getPageNameList()
num_pages = len( page_list )
if num_pages == 0 or index >= num_pages:
return None
return page_list[index]
def getPageNameList( self , sort_list=True):
# get the list file names in the archive, and sort
files = self.archiver.getArchiveFilenameList()
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
if sort_list:
files.sort(key=lambda x: x.lower())
# make a sub-list of image files
page_list = []
for name in files:
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
page_list.append(name)
return page_list
def getNumberOfPages( self ):
return len( self.getPageNameList( sort_list=False ) )
def readCBI( self ):
raw_cbi = self.readRawCBI()
if raw_cbi is None:
md =GenericMetadata()
else:
md = ComicBookInfo().metadataFromString( raw_cbi )
md.setDefaultPageList( self.getNumberOfPages() )
return md
def readRawCBI( self ):
if ( not self.hasCBI() ):
return None
return self.archiver.getArchiveComment()
def hasCBI(self):
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
if not self.seemsToBeAComicArchive():
return False
comment = self.archiver.getArchiveComment()
return ComicBookInfo().validateString( comment )
def writeCBI( self, metadata ):
self.applyArchiveInfoToMetadata( metadata )
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
return self.archiver.setArchiveComment( cbi_string )
def removeCBI( self ):
return self.archiver.setArchiveComment( "" )
def readCIX( self ):
raw_cix = self.readRawCIX()
if raw_cix is None:
md = GenericMetadata()
else:
md = ComicInfoXml().metadataFromString( raw_cix )
#validate the existing page list (make sure count is correct)
if len ( md.pages ) != 0 :
if len ( md.pages ) != self.getNumberOfPages():
# pages array doesn't match the actual number of images we're seeing
# in the archive, so discard the data
md.pages = []
if len( md.pages ) == 0:
md.setDefaultPageList( self.getNumberOfPages() )
return md
def readRawCIX( self ):
if not self.hasCIX():
print self.path, "doesn't have ComicInfo.xml data!"
return None
return self.archiver.readArchiveFile( self.ci_xml_filename )
def writeCIX(self, metadata):
if metadata is not None:
self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True )
cix_string = ComicInfoXml().stringFromMetadata( metadata )
return self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string )
else:
return False
def removeCIX( self ):
return self.archiver.removeArchiveFile( self.ci_xml_filename )
def hasCIX(self):
if not self.seemsToBeAComicArchive():
return False
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
return True
else:
return False
def readCoMet( self ):
raw_comet = self.readRawCoMet()
if raw_comet is None:
md = GenericMetadata()
else:
md = CoMet().metadataFromString( raw_comet )
md.setDefaultPageList( self.getNumberOfPages() )
#use the coverImage value from the comet_data to mark the cover in this struct
# walk through list of images in file, and find the matching one for md.coverImage
# need to remove the existing one in the default
if md.coverImage is not None:
cover_idx = 0
for idx,f in enumerate(self.getPageNameList()):
if md.coverImage == f:
cover_idx = idx
break
if cover_idx != 0:
del (md.pages[0]['Type'] )
md.pages[ cover_idx ]['Type'] = PageType.FrontCover
return md
def readRawCoMet( self ):
if not self.hasCoMet():
print self.path, "doesn't have CoMet data!"
return None
return self.archiver.readArchiveFile( self.comet_filename )
def writeCoMet(self, metadata):
if metadata is not None:
if not self.hasCoMet():
self.comet_filename = self.comet_default_filename
self.applyArchiveInfoToMetadata( metadata )
# Set the coverImage value, if it's not the first page
cover_idx = int(metadata.getCoverPageIndexList()[0])
if cover_idx != 0:
metadata.coverImage = self.getPageName( cover_idx )
comet_string = CoMet().stringFromMetadata( metadata )
return self.archiver.writeArchiveFile( self.comet_filename, comet_string )
else:
return False
def removeCoMet( self ):
if self.hasCoMet():
retcode = self.archiver.removeArchiveFile( self.comet_filename )
self.comet_filename = None
return retcode
return True
def hasCoMet(self):
if not self.seemsToBeAComicArchive():
return False
#Use the existence of self.comet_filename as a cue that the tag block exists
if self.comet_filename is None:
#TODO look at all xml files in root, and search for CoMet data, get first
for n in self.archiver.getArchiveFilenameList():
if ( os.path.dirname(n) == "" and
os.path.splitext(n)[1].lower() == '.xml'):
# read in XML file, and validate it
data = self.archiver.readArchiveFile( n )
if CoMet().validateString( data ):
# since we found it, save it!
self.comet_filename = n
return True
# if we made it through the loop, no CoMet here...
return False
else:
return True
def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False):
md.pageCount = self.getNumberOfPages()
if calc_page_sizes:
for p in md.pages:
idx = int( p['Image'] )
if pil_available:
if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p:
data = self.getPage( idx )
im = Image.open(StringIO.StringIO(data))
w,h = im.size
p['ImageSize'] = str(len(data))
p['ImageHeight'] = str(h)
p['ImageWidth'] = str(w)
else:
if 'ImageSize' not in p:
data = self.getPage( idx )
p['ImageSize'] = str(len(data))
def metadataFromFilename( self ):
metadata = GenericMetadata()
fnp = FileNameParser()
fnp.parseFilename( self.path )
if fnp.issue != "":
metadata.issue = fnp.issue
if fnp.series != "":
metadata.series = fnp.series
if fnp.volume != "":
metadata.volume = fnp.volume
if fnp.year != "":
metadata.year = fnp.year
if fnp.issue_count != "":
metadata.issueCount = fnp.issue_count
metadata.isEmpty = False
return metadata
def exportAsZip( self, zipfilename ):
if self.archive_type == self.ArchiveType.Zip:
# nothing to do, we're already a zip
return True
zip_archiver = ZipArchiver( zipfilename )
return zip_archiver.copyFromArchive( self.archiver )

View File

@ -1,141 +0,0 @@
"""
A python class to encapsulate the ComicBookInfo data
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import json
from datetime import datetime
import zipfile
from genericmetadata import GenericMetadata
import utils
class ComicBookInfo:
def metadataFromString( self, string ):
cbi_container = json.loads( unicode(string, 'utf-8') )
metadata = GenericMetadata()
cbi = cbi_container[ 'ComicBookInfo/1.0' ]
#helper func
# If item is not in CBI, return None
def xlate( cbi_entry):
if cbi_entry in cbi:
return cbi[cbi_entry]
else:
return None
metadata.series = xlate( 'series' )
metadata.title = xlate( 'title' )
metadata.issue = xlate( 'issue' )
metadata.publisher = xlate( 'publisher' )
metadata.month = xlate( 'publicationMonth' )
metadata.year = xlate( 'publicationYear' )
metadata.issueCount = xlate( 'numberOfIssues' )
metadata.comments = xlate( 'comments' )
metadata.credits = xlate( 'credits' )
metadata.genre = xlate( 'genre' )
metadata.volume = xlate( 'volume' )
metadata.volumeCount = xlate( 'numberOfVolumes' )
metadata.language = xlate( 'language' )
metadata.country = xlate( 'country' )
metadata.criticalRating = xlate( 'rating' )
metadata.tags = xlate( 'tags' )
# make sure credits and tags are at least empty lists and not None
if metadata.credits is None:
metadata.credits = []
if metadata.tags is None:
metadata.tags = []
#need to massage the language string to be ISO
if metadata.language is not None:
# reverse look-up
pattern = metadata.language
metadata.language = None
for key in utils.getLanguageDict():
if utils.getLanguageDict()[ key ] == pattern.encode('utf-8'):
metadata.language = key
break
metadata.isEmpty = False
return metadata
def stringFromMetadata( self, metadata ):
cbi_container = self.createJSONDictionary( metadata )
return json.dumps( cbi_container )
#verify that the string actually contains CBI data in JSON format
def validateString( self, string ):
try:
cbi_container = json.loads( string )
except:
return False
return ( 'ComicBookInfo/1.0' in cbi_container )
def createJSONDictionary( self, metadata ):
# Create the dictionary that we will convert to JSON text
cbi = dict()
cbi_container = {'appID' : 'ComicTagger/0.1',
'lastModified' : str(datetime.now()),
'ComicBookInfo/1.0' : cbi }
#helper func
def assign( cbi_entry, md_entry):
if md_entry is not None:
cbi[cbi_entry] = md_entry
assign( 'series', metadata.series )
assign( 'title', metadata.title )
assign( 'issue', metadata.issue )
assign( 'publisher', metadata.publisher )
assign( 'publicationMonth', metadata.month )
assign( 'publicationYear', metadata.year )
assign( 'numberOfIssues', metadata.issueCount )
assign( 'comments', metadata.comments )
assign( 'genre', metadata.genre )
assign( 'volume', metadata.volume )
assign( 'numberOfVolumes', metadata.volumeCount )
assign( 'language', utils.getLanguageFromISO(metadata.language) )
assign( 'country', metadata.country )
assign( 'rating', metadata.criticalRating )
assign( 'credits', metadata.credits )
assign( 'tags', metadata.tags )
return cbi_container
def writeToExternalFile( self, filename, metadata ):
cbi_container = self.createJSONDictionary(metadata)
f = open(filename, 'w')
f.write(json.dumps(cbi_container, indent=4))
f.close

View File

@ -1,289 +0,0 @@
"""
A python class to encapsulate ComicRack's ComicInfo.xml data
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from datetime import datetime
import zipfile
from pprint import pprint
import xml.etree.ElementTree as ET
from genericmetadata import GenericMetadata
import utils
class ComicInfoXml:
writer_synonyms = ['writer', 'plotter', 'scripter']
penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ]
inker_synonyms = [ 'inker', 'artist', 'finishes' ]
colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ]
letterer_synonyms = [ 'letterer']
cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ]
editor_synonyms = [ 'editor']
def getParseableCredits( self ):
parsable_credits = []
parsable_credits.extend( self.writer_synonyms )
parsable_credits.extend( self.penciller_synonyms )
parsable_credits.extend( self.inker_synonyms )
parsable_credits.extend( self.colorist_synonyms )
parsable_credits.extend( self.letterer_synonyms )
parsable_credits.extend( self.cover_synonyms )
parsable_credits.extend( self.editor_synonyms )
return parsable_credits
def metadataFromString( self, string ):
tree = ET.ElementTree(ET.fromstring( string ))
return self.convertXMLToMetadata( tree )
def stringFromMetadata( self, metadata ):
header = '<?xml version="1.0"?>\n'
tree = self.convertMetadataToXML( self, metadata )
return header + ET.tostring(tree.getroot())
def indent( self, elem, level=0 ):
# for making the XML output readable
i = "\n" + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self.indent( elem, level+1 )
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def convertMetadataToXML( self, filename, metadata ):
#shorthand for the metadata
md = metadata
# build a tree structure
root = ET.Element("ComicInfo")
root.attrib['xmlns:xsi']="http://www.w3.org/2001/XMLSchema-instance"
root.attrib['xmlns:xsd']="http://www.w3.org/2001/XMLSchema"
#helper func
def assign( cix_entry, md_entry):
if md_entry is not None:
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
assign( 'Series', md.series )
assign( 'Number', md.issue )
assign( 'Title', md.title )
assign( 'Count', md.issueCount )
assign( 'Volume', md.volume )
assign( 'AlternateSeries', md.alternateSeries )
assign( 'AlternateNumber', md.alternateNumber )
assign( 'AlternateCount', md.alternateCount )
assign( 'Summary', md.comments )
assign( 'Notes', md.notes )
assign( 'Year', md.year )
assign( 'Month', md.month )
assign( 'Publisher', md.publisher )
assign( 'Imprint', md.imprint )
assign( 'Genre', md.genre )
assign( 'Web', md.webLink )
assign( 'PageCount', md.pageCount )
assign( 'Format', md.format )
assign( 'LanguageISO', md.language )
assign( 'Manga', md.manga )
assign( 'Characters', md.characters )
assign( 'Teams', md.teams )
assign( 'Locations', md.locations )
assign( 'ScanInformation', md.scanInfo )
assign( 'StoryArc', md.storyArc )
assign( 'SeriesGroup', md.seriesGroup )
assign( 'AgeRating', md.maturityRating )
if md.blackAndWhite is not None and md.blackAndWhite:
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
# need to specially process the credits, since they are structured differently than CIX
credit_writer_list = list()
credit_penciller_list = list()
credit_inker_list = list()
credit_colorist_list = list()
credit_letterer_list = list()
credit_cover_list = list()
credit_editor_list = list()
# first, loop thru credits, and build a list for each role that CIX supports
for credit in metadata.credits:
if credit['role'].lower() in set( self.writer_synonyms ):
credit_writer_list.append(credit['person'].replace(",",""))
if credit['role'].lower() in set( self.penciller_synonyms ):
credit_penciller_list.append(credit['person'].replace(",",""))
if credit['role'].lower() in set( self.inker_synonyms ):
credit_inker_list.append(credit['person'].replace(",",""))
if credit['role'].lower() in set( self.colorist_synonyms ):
credit_colorist_list.append(credit['person'].replace(",",""))
if credit['role'].lower() in set( self.letterer_synonyms ):
credit_letterer_list.append(credit['person'].replace(",",""))
if credit['role'].lower() in set( self.cover_synonyms ):
credit_cover_list.append(credit['person'].replace(",",""))
if credit['role'].lower() in set( self.editor_synonyms ):
credit_editor_list.append(credit['person'].replace(",",""))
# second, convert each list to string, and add to XML struct
if len( credit_writer_list ) > 0:
node = ET.SubElement(root, 'Writer')
node.text = utils.listToString( credit_writer_list )
if len( credit_penciller_list ) > 0:
node = ET.SubElement(root, 'Penciller')
node.text = utils.listToString( credit_penciller_list )
if len( credit_inker_list ) > 0:
node = ET.SubElement(root, 'Inker')
node.text = utils.listToString( credit_inker_list )
if len( credit_colorist_list ) > 0:
node = ET.SubElement(root, 'Colorist')
node.text = utils.listToString( credit_colorist_list )
if len( credit_letterer_list ) > 0:
node = ET.SubElement(root, 'Letterer')
node.text = utils.listToString( credit_letterer_list )
if len( credit_cover_list ) > 0:
node = ET.SubElement(root, 'CoverArtist')
node.text = utils.listToString( credit_cover_list )
if len( credit_editor_list ) > 0:
node = ET.SubElement(root, 'Editor')
node.text = utils.listToString( credit_editor_list )
# loop and add the page entries under pages node
if len( md.pages ) > 0:
pages_node = ET.SubElement(root, 'Pages')
for page_dict in md.pages:
page_node = ET.SubElement(pages_node, 'Page')
page_node.attrib = page_dict
# self pretty-print
self.indent(root)
# wrap it in an ElementTree instance, and save as XML
tree = ET.ElementTree(root)
return tree
def convertXMLToMetadata( self, tree ):
root = tree.getroot()
if root.tag != 'ComicInfo':
raise 1
return None
metadata = GenericMetadata()
md = metadata
# Helper function
def xlate( tag ):
node = root.find( tag )
if node is not None:
return node.text
else:
return None
md.series = xlate( 'Series' )
md.title = xlate( 'Title' )
md.issue = xlate( 'Number' )
md.issueCount = xlate( 'Count' )
md.volume = xlate( 'Volume' )
md.alternateSeries = xlate( 'AlternateSeries' )
md.alternateNumber = xlate( 'AlternateNumber' )
md.alternateCount = xlate( 'AlternateCount' )
md.comments = xlate( 'Summary' )
md.notes = xlate( 'Notes' )
md.year = xlate( 'Year' )
md.month = xlate( 'Month' )
md.publisher = xlate( 'Publisher' )
md.imprint = xlate( 'Imprint' )
md.genre = xlate( 'Genre' )
md.webLink = xlate( 'Web' )
md.language = xlate( 'LanguageISO' )
md.format = xlate( 'Format' )
md.manga = xlate( 'Manga' )
md.characters = xlate( 'Characters' )
md.teams = xlate( 'Teams' )
md.locations = xlate( 'Locations' )
md.pageCount = xlate( 'PageCount' )
md.scanInfo = xlate( 'ScanInformation' )
md.storyArc = xlate( 'StoryArc' )
md.seriesGroup = xlate( 'SeriesGroup' )
md.maturityRating = xlate( 'AgeRating' )
tmp = xlate( 'BlackAndWhite' )
md.blackAndWhite = False
if tmp is not None and tmp.lower() in [ "yes", "true", "1" ]:
md.blackAndWhite = True
# Now extract the credit info
for n in root:
if ( n.tag == 'Writer' or
n.tag == 'Penciller' or
n.tag == 'Inker' or
n.tag == 'Colorist' or
n.tag == 'Letterer' or
n.tag == 'Editor'
):
for name in n.text.split(','):
metadata.addCredit( name.strip(), n.tag )
if n.tag == 'CoverArtist':
for name in n.text.split(','):
metadata.addCredit( name.strip(), "Cover" )
# parse page data now
pages_node = root.find( "Pages" )
if pages_node is not None:
for page in pages_node:
metadata.pages.append( page.attrib )
#print page.attrib
metadata.isEmpty = False
return metadata
def writeToExternalFile( self, filename, metadata ):
tree = self.convertMetadataToXML( self, metadata )
#ET.dump(tree)
tree.write(filename, encoding='utf-8')
def readFromExternalFile( self, filename ):
tree = ET.parse( filename )
return self.convertXMLToMetadata( tree )

View File

@ -1,489 +1,5 @@
#!/usr/bin/python
#!/usr/bin/env python3
from comictaggerlib.main import ctmain
"""
A python script to tag comic archives
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
import signal
import os
import traceback
import time
from pprint import pprint
import json
import platform
try:
qt_available = True
from PyQt4 import QtCore, QtGui
from taggerwindow import TaggerWindow
except ImportError as e:
qt_available = False
from settings import ComicTaggerSettings
from options import Options, MetaDataStyle
from comicarchive import ComicArchive
from issueidentifier import IssueIdentifier
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from issuestring import IssueString
import utils
import codecs
class MultipleMatch():
def __init__( self, filename, match_list):
self.filename = filename
self.matches = match_list
class OnlineMatchResults():
def __init__(self):
self.goodMatches = []
self.noMatches = []
self.multipleMatches = []
self.writeFailures = []
#-----------------------------
def actual_issue_data_fetch( match, settings ):
# now get the particular issue data
try:
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings.assume_lone_credit_is_primary )
except ComicVineTalkerException:
print "Network error while getting issue details. Save aborted"
return None
return cv_md
def actual_metadata_save( ca, opts, md ):
if not opts.dryrun:
# write out the new data
if not ca.writeMetadata( md, opts.data_style ):
print "The tag save seemed to fail!"
return False
else:
print "Save complete."
else:
if opts.terse:
print "dry-run option was set, so nothing was written"
else:
print "dry-run option was set, so nothing was written, but here is the final set of tags:"
print u"{0}".format(md)
return True
def post_process_matches( match_results, opts, settings ):
# now go through the match results
if opts.show_save_summary:
if len( match_results.goodMatches ) > 0:
print "\nSuccessful matches:"
print "------------------"
for f in match_results.goodMatches:
print f
if len( match_results.noMatches ) > 0:
print "\nNo matches:"
print "------------------"
for f in match_results.noMatches:
print f
if len( match_results.writeFailures ) > 0:
print "\nFile Write Failures:"
print "------------------"
for f in match_results.writeFailures:
print f
if not opts.show_save_summary and not opts.interactive:
#jusr quit if we're not interactive or showing the summary
return
if len( match_results.multipleMatches ) > 0:
print "\nMultiple matches:"
print "------------------"
for mm in match_results.multipleMatches:
print mm.filename
for (counter,m) in enumerate(mm.matches):
print " {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
m['series'],
m['issue_number'],
m['publisher'],
m['month'],
m['year'],
m['issue_title'])
if opts.interactive:
while True:
i = raw_input("Choose a match #, or 's' to skip: ")
if (i.isdigit() and int(i) in range(len(mm.matches))) or i == 's':
break
if i != 's':
# save the data!
# we know at this point, that the file is all good to go
ca = ComicArchive( mm.filename )
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
cv_md = actual_issue_data_fetch(mm.matches[int(i)], settings)
md.overlay( cv_md )
actual_metadata_save( ca, opts, md )
print
def cli_mode( opts, settings ):
if len( opts.file_list ) < 1:
print "You must specify at least one filename. Use the -h option for more info"
return
match_results = OnlineMatchResults()
for f in opts.file_list:
process_file_cli( f, opts, settings, match_results )
sys.stdout.flush()
post_process_matches( match_results, opts, settings )
def create_local_metadata( opts, ca, has_desired_tags ):
md = GenericMetadata()
md.setDefaultPageList( ca.getNumberOfPages() )
if has_desired_tags:
md = ca.readMetadata( opts.data_style )
# now, overlay the parsed filename info
if opts.parse_filename:
md.overlay( ca.metadataFromFilename() )
# finally, use explicit stuff
if opts.metadata is not None:
md.overlay( opts.metadata )
return md
def process_file_cli( filename, opts, settings, match_results ):
batch_mode = len( opts.file_list ) > 1
ca = ComicArchive(filename)
if settings.rar_exe_path != "":
ca.setExternalRarProgram( settings.rar_exe_path )
if not ca.seemsToBeAComicArchive():
print "Sorry, but "+ filename + " is not a comic archive!"
return
#if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ):
if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ):
print "This archive is not writable for that tag type"
return
has = [ False, False, False ]
if ca.hasCIX(): has[ MetaDataStyle.CIX ] = True
if ca.hasCBI(): has[ MetaDataStyle.CBI ] = True
if ca.hasCoMet(): has[ MetaDataStyle.COMET ] = True
if opts.print_tags:
if opts.data_style is None:
page_count = ca.getNumberOfPages()
brief = ""
if batch_mode:
brief = "{0}: ".format(filename)
if ca.isZip(): brief += "ZIP archive "
elif ca.isRar(): brief += "RAR archive "
elif ca.isFolder(): brief += "Folder archive "
brief += "({0: >3} pages)".format(page_count)
brief += " tags:[ "
if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ):
brief += "none "
else:
if has[ MetaDataStyle.CBI ]: brief += "CBL "
if has[ MetaDataStyle.CIX ]: brief += "CR "
if has[ MetaDataStyle.COMET ]: brief += "CoMet "
brief += "]"
print brief
if opts.terse:
return
print
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
if has[ MetaDataStyle.CIX ]:
print "------ComicRack tags--------"
if opts.raw:
print u"{0}".format(unicode(ca.readRawCIX(), errors='ignore'))
else:
print u"{0}".format(ca.readCIX())
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
if has[ MetaDataStyle.CBI ]:
print "------ComicBookLover tags--------"
if opts.raw:
pprint(json.loads(ca.readRawCBI()))
else:
print u"{0}".format(ca.readCBI())
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
if has[ MetaDataStyle.COMET ]:
print "------CoMet tags--------"
if opts.raw:
print u"{0}".format(ca.readRawCoMet())
else:
print u"{0}".format(ca.readCoMet())
elif opts.delete_tags:
style_name = MetaDataStyle.name[ opts.data_style ]
if has[ opts.data_style ]:
if not opts.dryrun:
if not ca.removeMetadata( opts.data_style ):
print "{0}: Tag removal seemed to fail!".format( filename )
else:
print "{0}: Removed {1} tags.".format( filename, style_name )
else:
print "{0}: dry-run. {1} tags not removed".format( filename, style_name )
else:
print "{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name )
elif opts.copy_tags:
dst_style_name = MetaDataStyle.name[ opts.data_style ]
if opts.no_overwrite and has[ opts.data_style ]:
print "{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
return
if opts.copy_source == opts.data_style:
print "{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name)
return
src_style_name = MetaDataStyle.name[ opts.copy_source ]
if has[ opts.copy_source ]:
if not opts.dryrun:
md = ca.readMetadata( opts.copy_source )
if not ca.writeMetadata( md, opts.data_style ):
print "{0}: Tag copy seemed to fail!".format( filename )
else:
print "{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
else:
print "{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
else:
print "{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
elif opts.save_tags:
if opts.no_overwrite and has[ opts.data_style ]:
print "{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
return
if batch_mode:
print "Processing {0}: ".format(filename)
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
# now, search online
if opts.search_online:
ii = IssueIdentifier( ca, settings )
if md is None or md.isEmpty:
print "No metadata given to search online with!"
return
def myoutput( text ):
if opts.verbose:
IssueIdentifier.defaultWriteOutput( text )
# use our overlayed MD struct to search
ii.setAdditionalMetadata( md )
ii.onlyUseAdditionalMetaData = True
ii.setOutputFunction( myoutput )
ii.cover_page_index = md.getCoverPageIndexList()[0]
matches = ii.search()
result = ii.search_result
found_match = False
choices = False
low_confidence = False
if result == ii.ResultNoMatches:
pass
elif result == ii.ResultFoundMatchButBadCoverScore:
low_confidence = True
found_match = True
elif result == ii.ResultFoundMatchButNotFirstPage :
found_match = True
elif result == ii.ResultMultipleMatchesWithBadImageScores:
low_confidence = True
choices = True
elif result == ii.ResultOneGoodMatch:
found_match = True
elif result == ii.ResultMultipleGoodMatches:
choices = True
if choices:
print "Online search: Multiple matches. Save aborted"
match_results.multipleMatches.append(MultipleMatch(filename,matches))
return
if low_confidence and opts.abortOnLowConfidence:
print "Online search: Low confidence match. Save aborted"
match_results.noMatches.append(filename)
return
if not found_match:
print "Online search: No match found. Save aborted"
match_results.noMatches.append(filename)
return
# we got here, so we have a single match
# now get the particular issue data
cv_md = actual_issue_data_fetch(matches[0], settings)
if cv_md is None:
return
md.overlay( cv_md )
# ok, done building our metadata. time to save
if not actual_metadata_save( ca, opts, md ):
match_results.writeFailures.append(filename)
else:
match_results.goodMatches.append(filename)
elif opts.rename_file:
msg_hdr = ""
if batch_mode:
msg_hdr = "{0}: ".format(filename)
if opts.data_style is not None:
use_tags = has[ opts.data_style ]
else:
use_tags = False
md = create_local_metadata( opts, ca, use_tags )
# TODO move this to ComicArchive, or maybe another class???
new_name = ""
if md.series is not None:
new_name += "{0}".format( md.series )
else:
print msg_hdr + "Can't rename without series name"
return
if md.volume is not None:
new_name += " v{0}".format( md.volume )
if md.issue is not None:
new_name += " #{0}".format( IssueString(md.issue).asString(pad=3) )
#else:
# print msg_hdr + "Can't rename without issue number"
# return
if md.issueCount is not None:
new_name += " (of {0})".format( md.issueCount )
if md.year is not None:
new_name += " ({0})".format( md.year )
if ca.isZip():
new_name += ".cbz"
elif ca.isRar():
new_name += ".cbr"
if new_name == os.path.basename(filename):
print msg_hdr + "Filename is already good!"
return
folder = os.path.dirname( os.path.abspath( filename ) )
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
suffix = ""
if not opts.dryrun:
# rename the file
os.rename( filename, new_abs_path )
else:
suffix = " (dry-run, no change)"
print "renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
#-----------------------------
def main():
# try to make stdout encodings happy for unicode
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
opts = Options()
opts.parseCmdLineArgs()
settings = ComicTaggerSettings()
# make sure unrar program is in the path for the UnRAR class
utils.addtopath(os.path.dirname(settings.unrar_exe_path))
signal.signal(signal.SIGINT, signal.SIG_DFL)
if not qt_available and not opts.no_gui:
opts.no_gui = True
print "QT is not available."
if opts.no_gui:
cli_mode( opts, settings )
else:
app = QtGui.QApplication(sys.argv)
if platform.system() != "Linux":
img = QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/tags.png' ))
splash = QtGui.QSplashScreen(img)
splash.show()
splash.raise_()
app.processEvents()
try:
tagger_window = TaggerWindow( opts.filename, settings )
tagger_window.show()
if platform.system() != "Linux":
splash.finish( tagger_window )
sys.exit(app.exec_())
except Exception, e:
QtGui.QMessageBox.critical(QtGui.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc() )
if __name__ == "__main__":
main()
ctmain()

View File

@ -0,0 +1,17 @@
{
"build_systems":
[
{
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"name": "Anaconda Python Builder",
"selector": "source.python",
"shell_cmd": "\"python3\" -u \"$file\""
}
],
"folders":
[
{
"path": "."
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
from ._version import version as __version__

View File

@ -0,0 +1,226 @@
"""A PyQT4 dialog to select from automated issue matches"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
from .comicarchive import MetaDataStyle
from .coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
class AutoTagMatchWindow(QtWidgets.QDialog):
volume_id = 0
def __init__(self, parent, match_set_list, style, fetch_func):
super(AutoTagMatchWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("matchselectionwindow.ui"), self)
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
gridlayout.addWidget(self.altCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.skipButton = QtWidgets.QPushButton(self.tr("Skip to Next"))
self.buttonBox.addButton(self.skipButton, QtWidgets.QDialogButtonBox.ActionRole)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText("Accept and Write Tags")
self.match_set_list = match_set_list
self.style = style
self.fetch_func = fetch_func
self.current_match_set_idx = 0
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
self.skipButton.clicked.connect(self.skipToNext)
self.updateData()
def updateData(self):
self.current_match_set = self.match_set_list[self.current_match_set_idx]
if self.current_match_set_idx + 1 == len(self.match_set_list):
self.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setDisabled(True)
# self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText("Accept")
self.skipButton.setText(self.tr("Skip"))
self.setCoverImage()
self.populateTable()
self.twList.resizeColumnsToContents()
self.twList.selectRow(0)
path = self.current_match_set.ca.path
self.setWindowTitle(
"Select correct match or skip ({0} of {1}): {2}".format(self.current_match_set_idx + 1, len(self.match_set_list), os.path.split(path)[1])
)
def populateTable(self):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
self.twList.setSortingEnabled(False)
row = 0
for match in self.current_match_set.matches:
self.twList.insertRow(row)
item_text = match["series"]
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, (match,))
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
if match["publisher"] is not None:
item_text = "{0}".format(match["publisher"])
else:
item_text = "Unknown"
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
month_str = ""
year_str = "????"
if match["month"] is not None:
month_str = "-{0:02d}".format(int(match["month"]))
if match["year"] is not None:
year_str = "{0}".format(match["year"])
item_text = year_str + month_str
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
item_text = match["issue_title"]
if item_text is None:
item_text = ""
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
row += 1
self.twList.resizeColumnsToContents()
self.twList.setSortingEnabled(True)
self.twList.sortItems(2, QtCore.Qt.AscendingOrder)
self.twList.selectRow(0)
self.twList.resizeColumnsToContents()
self.twList.horizontalHeader().setStretchLastSection(True)
def cellDoubleClicked(self, r, c):
self.accept()
def currentItemChanged(self, curr, prev):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.altCoverWidget.setIssueID(self.currentMatch()["issue_id"])
if self.currentMatch()["description"] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(self.currentMatch()["description"])
def setCoverImage(self):
ca = self.current_match_set.ca
self.archiveCoverWidget.setArchive(ca)
def currentMatch(self):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data(QtCore.Qt.UserRole)[0]
return match
def accept(self):
self.saveMatch()
self.current_match_set_idx += 1
if self.current_match_set_idx == len(self.match_set_list):
# no more items
QtWidgets.QDialog.accept(self)
else:
self.updateData()
def skipToNext(self):
self.current_match_set_idx += 1
if self.current_match_set_idx == len(self.match_set_list):
# no more items
QtWidgets.QDialog.reject(self)
else:
self.updateData()
def reject(self):
reply = QtWidgets.QMessageBox.question(
self,
self.tr("Cancel Matching"),
self.tr("Are you sure you wish to cancel the matching process?"),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No,
)
if reply == QtWidgets.QMessageBox.No:
return
QtWidgets.QDialog.reject(self)
def saveMatch(self):
match = self.currentMatch()
ca = self.current_match_set.ca
md = ca.readMetadata(self.style)
if md.isEmpty:
md = ca.metadataFromFilename()
# now get the particular issue data
cv_md = self.fetch_func(match)
if cv_md is None:
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to Comic Vine to get issue details!"))
return
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
md.overlay(cv_md)
success = ca.writeMetadata(md, self.style)
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
QtWidgets.QApplication.restoreOverrideCursor()
if not success:
QtWidgets.QMessageBox.warning(self, self.tr("Write Error"), self.tr("Saving the tags to the archive seemed to fail!"))

View File

@ -0,0 +1,60 @@
"""A PyQT4 dialog to show ID log and progress"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
from .coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
class AutoTagProgressWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(AutoTagProgressWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("autotagprogresswindow.ui"), self)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.testCoverWidget = CoverImageWidget(self.testCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtWidgets.QGridLayout(self.testCoverContainer)
gridlayout.addWidget(self.testCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.isdone = False
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
reduceWidgetFontSize(self.textEdit)
def setArchiveImage(self, img_data):
self.setCoverImage(img_data, self.archiveCoverWidget)
def setTestImage(self, img_data):
self.setCoverImage(img_data, self.testCoverWidget)
def setCoverImage(self, img_data, widget):
widget.setImageData(img_data)
QtCore.QCoreApplication.processEvents()
QtCore.QCoreApplication.processEvents()
def reject(self):
QtWidgets.QDialog.reject(self)
self.isdone = True

View File

@ -0,0 +1,114 @@
"""A PyQT4 dialog to confirm and set options for auto-tag"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from .settings import ComicTaggerSettings
class AutoTagStartWindow(QtWidgets.QDialog):
def __init__(self, parent, settings, msg):
super(AutoTagStartWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("autotagstartwindow.ui"), self)
self.label.setText(msg)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Unchecked)
self.cbxDontUseYear.setCheckState(QtCore.Qt.Unchecked)
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Unchecked)
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(QtCore.Qt.Unchecked)
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Unchecked)
self.cbxSpecifySearchString.setCheckState(QtCore.Qt.Unchecked)
self.cbxAutoImprint.setCheckState(QtCore.Qt.Unchecked)
self.leNameLengthMatchTolerance.setText(str(self.settings.id_length_delta_thresh))
self.leSearchString.setEnabled(False)
if self.settings.save_on_low_confidence:
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Checked)
if self.settings.dont_use_year_when_identifying:
self.cbxDontUseYear.setCheckState(QtCore.Qt.Checked)
if self.settings.assume_1_if_no_issue_num:
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Checked)
if self.settings.ignore_leading_numbers_in_filename:
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(QtCore.Qt.Checked)
if self.settings.remove_archive_after_successful_match:
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Checked)
if self.settings.wait_and_retry_on_rate_limit:
self.cbxWaitForRateLimit.setCheckState(QtCore.Qt.Checked)
if self.settings.auto_imprint:
self.cbxAutoImprint.setCheckState(QtCore.Qt.Checked)
nlmtTip = """ <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
search matches that are too long compared to your series name search. The higher
it is, the more likely to have a good match, but each search will take longer and
use more bandwidth. Too low, and only the very closest lexical matches will be
explored.</html>"""
self.leNameLengthMatchTolerance.setToolTip(nlmtTip)
ssTip = """<html>
The <b>series search string</b> specifies the search string to be used for all selected archives.
Use this when trying to match archives with hard-to-parse or incorrect filenames. All archives selected
should be from the same series.
</html>"""
self.leSearchString.setToolTip(ssTip)
self.cbxSpecifySearchString.setToolTip(ssTip)
validator = QtGui.QIntValidator(0, 99, self)
self.leNameLengthMatchTolerance.setValidator(validator)
self.cbxSpecifySearchString.stateChanged.connect(self.searchStringToggle)
self.autoSaveOnLow = False
self.dontUseYear = False
self.assumeIssueOne = False
self.ignoreLeadingDigitsInFilename = False
self.removeAfterSuccess = False
self.waitAndRetryOnRateLimit = False
self.searchString = None
self.nameLengthMatchTolerance = self.settings.id_length_delta_thresh
def searchStringToggle(self):
enable = self.cbxSpecifySearchString.isChecked()
self.leSearchString.setEnabled(enable)
def accept(self):
QtWidgets.QDialog.accept(self)
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
self.dontUseYear = self.cbxDontUseYear.isChecked()
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
self.ignoreLeadingDigitsInFilename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
self.removeAfterSuccess = self.cbxRemoveAfterSuccess.isChecked()
self.nameLengthMatchTolerance = int(self.leNameLengthMatchTolerance.text())
self.waitAndRetryOnRateLimit = self.cbxWaitForRateLimit.isChecked()
# persist some settings
self.settings.save_on_low_confidence = self.autoSaveOnLow
self.settings.dont_use_year_when_identifying = self.dontUseYear
self.settings.assume_1_if_no_issue_num = self.assumeIssueOne
self.settings.ignore_leading_numbers_in_filename = self.ignoreLeadingDigitsInFilename
self.settings.remove_archive_after_successful_match = self.removeAfterSuccess
self.settings.wait_and_retry_on_rate_limit = self.waitAndRetryOnRateLimit
if self.cbxSpecifySearchString.isChecked():
self.searchString = str(self.leSearchString.text())
if len(self.searchString) == 0:
self.searchString = None

View File

@ -0,0 +1,93 @@
"""A class to manage modifying metadata specifically for CBL/CBI"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class CBLTransformer:
def __init__(self, metadata, settings):
self.metadata = metadata
self.settings = settings
def apply(self):
# helper funcs
def append_to_tags_if_unique(item):
if item.lower() not in (tag.lower() for tag in self.metadata.tags):
self.metadata.tags.append(item)
def add_string_list_to_tags(str_list):
if str_list is not None and str_list != "":
items = [s.strip() for s in str_list.split(",")]
for item in items:
append_to_tags_if_unique(item)
if self.settings.assume_lone_credit_is_primary:
# helper
def setLonePrimary(role_list):
lone_credit = None
count = 0
for c in self.metadata.credits:
if c["role"].lower() in role_list:
count += 1
lone_credit = c
if count > 1:
lone_credit = None
break
if lone_credit is not None:
lone_credit["primary"] = True
return lone_credit, count
# need to loop three times, once for 'writer', 'artist', and then
# 'penciler' if no artist
setLonePrimary(["writer"])
c, count = setLonePrimary(["artist"])
if c is None and count == 0:
c, count = setLonePrimary(["penciler", "penciller"])
if c is not None:
c["primary"] = False
self.metadata.addCredit(c["person"], "Artist", True)
if self.settings.copy_characters_to_tags:
add_string_list_to_tags(self.metadata.characters)
if self.settings.copy_teams_to_tags:
add_string_list_to_tags(self.metadata.teams)
if self.settings.copy_locations_to_tags:
add_string_list_to_tags(self.metadata.locations)
if self.settings.copy_storyarcs_to_tags:
add_string_list_to_tags(self.metadata.storyArc)
if self.settings.copy_notes_to_comments:
if self.metadata.notes is not None:
if self.metadata.comments is None:
self.metadata.comments = ""
else:
self.metadata.comments += "\n\n"
if self.metadata.notes not in self.metadata.comments:
self.metadata.comments += self.metadata.notes
if self.settings.copy_weblink_to_comments:
if self.metadata.webLink is not None:
if self.metadata.comments is None:
self.metadata.comments = ""
else:
self.metadata.comments += "\n\n"
if self.metadata.webLink not in self.metadata.comments:
self.metadata.comments += self.metadata.webLink
return self.metadata

554
comictaggerlib/cli.py Normal file
View File

@ -0,0 +1,554 @@
#!/usr/bin/python
"""ComicTagger CLI functions"""
# Copyright 2013 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
import sys
from pprint import pprint
from . import utils
from .cbltransformer import CBLTransformer
from .comicarchive import ComicArchive, MetaDataStyle
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .filerenamer import FileRenamer
from .genericmetadata import GenericMetadata
from .issueidentifier import IssueIdentifier
from .options import Options
from .settings import ComicTaggerSettings
filename_encoding = sys.getfilesystemencoding()
class MultipleMatch:
def __init__(self, filename, match_list):
self.filename = filename
self.matches = match_list
class OnlineMatchResults:
def __init__(self):
self.goodMatches = []
self.noMatches = []
self.multipleMatches = []
self.lowConfidenceMatches = []
self.writeFailures = []
self.fetchDataFailures = []
def actual_issue_data_fetch(match, settings, opts):
# now get the particular issue data
try:
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
cv_md = comicVine.fetchIssueData(match["volume_id"], match["issue_number"], settings)
except ComicVineTalkerException:
print("Network error while getting issue details. Save aborted", file=sys.stderr)
return None
if settings.apply_cbl_transform_on_cv_import:
cv_md = CBLTransformer(cv_md, settings).apply()
return cv_md
def actual_metadata_save(ca, opts, md):
if not opts.dryrun:
# write out the new data
if not ca.writeMetadata(md, opts.data_style):
print("The tag save seemed to fail!", file=sys.stderr)
return False
else:
print("Save complete.", file=sys.stderr)
else:
if opts.terse:
print("dry-run option was set, so nothing was written", file=sys.stderr)
else:
print("dry-run option was set, so nothing was written, but here is the final set of tags:", file=sys.stderr)
print("{0}".format(md))
return True
def display_match_set_for_choice(label, match_set, opts, settings):
print("{0} -- {1}:".format(match_set.filename, label))
# sort match list by year
match_set.matches.sort(key=lambda k: k["year"])
for (counter, m) in enumerate(match_set.matches):
counter += 1
print(
" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(
counter, m["series"], m["issue_number"], m["publisher"], m["month"], m["year"], m["issue_title"]
)
)
if opts.interactive:
while True:
i = input("Choose a match #, or 's' to skip: ")
if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s":
break
if i != "s":
i = int(i) - 1
# save the data!
# we know at this point, that the file is all good to go
ca = ComicArchive(match_set.filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic("nocover.png"))
md = create_local_metadata(opts, ca, ca.hasMetadata(opts.data_style))
cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings, opts)
md.overlay(cv_md)
if settings.auto_imprint:
md.fixPublisher()
actual_metadata_save(ca, opts, md)
def post_process_matches(match_results, opts, settings):
# now go through the match results
if opts.show_save_summary:
if len(match_results.goodMatches) > 0:
print("\nSuccessful matches:\n------------------")
for f in match_results.goodMatches:
print(f)
if len(match_results.noMatches) > 0:
print("\nNo matches:\n------------------")
for f in match_results.noMatches:
print(f)
if len(match_results.writeFailures) > 0:
print("\nFile Write Failures:\n------------------")
for f in match_results.writeFailures:
print(f)
if len(match_results.fetchDataFailures) > 0:
print("\nNetwork Data Fetch Failures:\n------------------")
for f in match_results.fetchDataFailures:
print(f)
if not opts.show_save_summary and not opts.interactive:
# just quit if we're not interactive or showing the summary
return
if len(match_results.multipleMatches) > 0:
print("\nArchives with multiple high-confidence matches:\n------------------")
for match_set in match_results.multipleMatches:
display_match_set_for_choice("Multiple high-confidence matches", match_set, opts, settings)
if len(match_results.lowConfidenceMatches) > 0:
print("\nArchives with low-confidence matches:\n------------------")
for match_set in match_results.lowConfidenceMatches:
if len(match_set.matches) == 1:
label = "Single low-confidence match"
else:
label = "Multiple low-confidence matches"
display_match_set_for_choice(label, match_set, opts, settings)
def cli_mode(opts, settings):
if len(opts.file_list) < 1:
print("You must specify at least one filename. Use the -h option for more info", file=sys.stderr)
return
match_results = OnlineMatchResults()
for f in opts.file_list:
if isinstance(f, str):
pass
process_file_cli(f, opts, settings, match_results)
sys.stdout.flush()
post_process_matches(match_results, opts, settings)
def create_local_metadata(opts, ca, has_desired_tags):
md = GenericMetadata()
md.setDefaultPageList(ca.getNumberOfPages())
if has_desired_tags:
md = ca.readMetadata(opts.data_style)
# now, overlay the parsed filename info
if opts.parse_filename:
md.overlay(ca.metadataFromFilename())
# finally, use explicit stuff
if opts.metadata is not None:
md.overlay(opts.metadata)
return md
def process_file_cli(filename, opts, settings, match_results):
batch_mode = len(opts.file_list) > 1
settings.auto_imprint = opts.auto_imprint
ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic("nocover.png"))
if not os.path.lexists(filename):
print("Cannot find " + filename, file=sys.stderr)
return
if not ca.seemsToBeAComicArchive():
print("Sorry, but " + filename + " is not a comic archive!", file=sys.stderr)
return
# if not ca.isWritableForStyle(opts.data_style) and (opts.delete_tags or
# opts.save_tags or opts.rename_file):
if not ca.isWritable() and (opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file):
print("This archive is not writable for that tag type", file=sys.stderr)
return
has = [False, False, False]
if ca.hasCIX():
has[MetaDataStyle.CIX] = True
if ca.hasCBI():
has[MetaDataStyle.CBI] = True
if ca.hasCoMet():
has[MetaDataStyle.COMET] = True
if opts.print_tags:
if opts.data_style is None:
page_count = ca.getNumberOfPages()
brief = ""
if batch_mode:
brief = "{0}: ".format(filename)
if ca.isZip():
brief += "ZIP archive "
elif ca.isRar():
brief += "RAR archive "
elif ca.isFolder():
brief += "Folder archive "
brief += "({0: >3} pages)".format(page_count)
brief += " tags:[ "
if not (has[MetaDataStyle.CBI] or has[MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
brief += "none "
else:
if has[MetaDataStyle.CBI]:
brief += "CBL "
if has[MetaDataStyle.CIX]:
brief += "CR "
if has[MetaDataStyle.COMET]:
brief += "CoMet "
brief += "]"
print(brief)
if opts.terse:
return
print()
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
if has[MetaDataStyle.CIX]:
print("--------- ComicRack tags ---------")
if opts.raw:
print("{0}".format(str(ca.readRawCIX(), errors="ignore")))
else:
print("{0}".format(ca.readCIX()))
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
if has[MetaDataStyle.CBI]:
print("------- ComicBookLover tags -------")
if opts.raw:
pprint(json.loads(ca.readRawCBI()))
else:
print("{0}".format(ca.readCBI()))
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
if has[MetaDataStyle.COMET]:
print("----------- CoMet tags -----------")
if opts.raw:
print("{0}".format(ca.readRawCoMet()))
else:
print("{0}".format(ca.readCoMet()))
elif opts.delete_tags:
style_name = MetaDataStyle.name[opts.data_style]
if has[opts.data_style]:
if not opts.dryrun:
if not ca.removeMetadata(opts.data_style):
print("{0}: Tag removal seemed to fail!".format(filename))
else:
print("{0}: Removed {1} tags.".format(filename, style_name))
else:
print("{0}: dry-run. {1} tags not removed".format(filename, style_name))
else:
print("{0}: This archive doesn't have {1} tags to remove.".format(filename, style_name))
elif opts.copy_tags:
dst_style_name = MetaDataStyle.name[opts.data_style]
if opts.no_overwrite and has[opts.data_style]:
print("{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name))
return
if opts.copy_source == opts.data_style:
print("{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name))
return
src_style_name = MetaDataStyle.name[opts.copy_source]
if has[opts.copy_source]:
if not opts.dryrun:
md = ca.readMetadata(opts.copy_source)
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
md = CBLTransformer(md, settings).apply()
if not ca.writeMetadata(md, opts.data_style):
print("{0}: Tag copy seemed to fail!".format(filename))
else:
print("{0}: Copied {1} tags to {2} .".format(filename, src_style_name, dst_style_name))
else:
print("{0}: dry-run. {1} tags not copied".format(filename, src_style_name))
else:
print("{0}: This archive doesn't have {1} tags to copy.".format(filename, src_style_name))
elif opts.save_tags:
if opts.no_overwrite and has[opts.data_style]:
print("{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[opts.data_style]))
return
if batch_mode:
print("Processing {0}...".format(filename))
md = create_local_metadata(opts, ca, has[opts.data_style])
if md.issue is None or md.issue == "":
if opts.assume_issue_is_one_if_not_set:
md.issue = "1"
# now, search online
if opts.search_online:
if opts.issue_id is not None:
# we were given the actual ID to search with
try:
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
cv_md = comicVine.fetchIssueDataByIssueID(opts.issue_id, settings)
except ComicVineTalkerException:
print("Network error while getting issue details. Save aborted", file=sys.stderr)
match_results.fetchDataFailures.append(filename)
return
if cv_md is None:
print("No match for ID {0} was found.".format(opts.issue_id), file=sys.stderr)
match_results.noMatches.append(filename)
return
if settings.apply_cbl_transform_on_cv_import:
cv_md = CBLTransformer(cv_md, settings).apply()
else:
ii = IssueIdentifier(ca, settings)
if md is None or md.isEmpty:
print("No metadata given to search online with!", file=sys.stderr)
match_results.noMatches.append(filename)
return
def myoutput(text):
if opts.verbose:
IssueIdentifier.defaultWriteOutput(text)
# use our overlayed MD struct to search
ii.setAdditionalMetadata(md)
ii.onlyUseAdditionalMetaData = True
ii.waitAndRetryOnRateLimit = opts.wait_and_retry_on_rate_limit
ii.setOutputFunction(myoutput)
ii.cover_page_index = md.getCoverPageIndexList()[0]
matches = ii.search()
result = ii.search_result
found_match = False
choices = False
low_confidence = False
if result == ii.ResultNoMatches:
pass
elif result == ii.ResultFoundMatchButBadCoverScore:
low_confidence = True
found_match = True
elif result == ii.ResultFoundMatchButNotFirstPage:
found_match = True
elif result == ii.ResultMultipleMatchesWithBadImageScores:
low_confidence = True
choices = True
elif result == ii.ResultOneGoodMatch:
found_match = True
elif result == ii.ResultMultipleGoodMatches:
choices = True
if choices:
if low_confidence:
print("Online search: Multiple low confidence matches. Save aborted", file=sys.stderr)
match_results.lowConfidenceMatches.append(MultipleMatch(filename, matches))
return
else:
print("Online search: Multiple good matches. Save aborted", file=sys.stderr)
match_results.multipleMatches.append(MultipleMatch(filename, matches))
return
if low_confidence and opts.abortOnLowConfidence:
print("Online search: Low confidence match. Save aborted", file=sys.stderr)
match_results.lowConfidenceMatches.append(MultipleMatch(filename, matches))
return
if not found_match:
print("Online search: No match found. Save aborted", file=sys.stderr)
match_results.noMatches.append(filename)
return
# we got here, so we have a single match
# now get the particular issue data
cv_md = actual_issue_data_fetch(matches[0], settings, opts)
if cv_md is None:
match_results.fetchDataFailures.append(filename)
return
md.overlay(cv_md)
if settings.auto_imprint:
md.fixPublisher()
# ok, done building our metadata. time to save
if not actual_metadata_save(ca, opts, md):
match_results.writeFailures.append(filename)
else:
match_results.goodMatches.append(filename)
elif opts.rename_file:
msg_hdr = ""
if batch_mode:
msg_hdr = "{0}: ".format(filename)
if opts.data_style is not None:
use_tags = has[opts.data_style]
else:
use_tags = False
md = create_local_metadata(opts, ca, use_tags)
if md.series is None:
print(msg_hdr + "Can't rename without series name", file=sys.stderr)
return
new_ext = None # default
if settings.rename_extension_based_on_archive:
if ca.isZip():
new_ext = ".cbz"
elif ca.isRar():
new_ext = ".cbr"
renamer = FileRenamer(md)
renamer.setTemplate(settings.rename_template)
renamer.setIssueZeroPadding(settings.rename_issue_number_padding)
renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup)
renamer.move = settings.rename_move_dir
try:
new_name = renamer.determineName(filename, ext=new_ext)
except Exception as e:
print(
msg_hdr + "Invalid format string!\nYour rename template is invalid!\n\n"
"{}\n\nPlease consult the template help in the settings "
"and the documentation on the format at "
"https://docs.python.org/3/library/string.html#format-string-syntax",
file=sys.stderr,
)
return
folder = os.path.dirname(os.path.abspath(filename))
if settings.rename_move_dir and len(settings.rename_dir.strip()) > 3:
folder = settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
if os.path.join(folder, new_name) == os.path.abspath(filename):
print(msg_hdr + "Filename is already good!", file=sys.stderr)
return
suffix = ""
if not opts.dryrun:
# rename the file
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(filename, new_abs_path)
else:
suffix = " (dry-run, no change)"
print("renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix))
elif opts.export_to_zip:
msg_hdr = ""
if batch_mode:
msg_hdr = "{0}: ".format(filename)
if not ca.isRar():
print(msg_hdr + "Archive is not a RAR.", file=sys.stderr)
return
rar_file = os.path.abspath(os.path.abspath(filename))
new_file = os.path.splitext(rar_file)[0] + ".cbz"
if opts.abort_export_on_conflict and os.path.lexists(new_file):
print(msg_hdr + "{0} already exists in the that folder.".format(os.path.split(new_file)[1]))
return
new_file = utils.unique_file(os.path.join(new_file))
delete_success = False
export_success = False
if not opts.dryrun:
if ca.exportAsZip(new_file):
export_success = True
if opts.delete_rar_after_export:
try:
os.unlink(rar_file)
except:
print(msg_hdr + "Error deleting original RAR after export", file=sys.stderr)
delete_success = False
else:
delete_success = True
else:
# last export failed, so remove the zip, if it exists
if os.path.lexists(new_file):
os.remove(new_file)
else:
msg = msg_hdr + "Dry-run: Would try to create {0}".format(os.path.split(new_file)[1])
if opts.delete_rar_after_export:
msg += " and delete orginal."
print(msg)
return
msg = msg_hdr
if export_success:
msg += "Archive exported successfully to: {0}".format(os.path.split(new_file)[1])
if opts.delete_rar_after_export and delete_success:
msg += " (Original deleted) "
else:
msg += "Archive failed to export!"
print(msg)

1
comictaggerlib/comet.py Normal file
View File

@ -0,0 +1 @@
from comicapi.comet import *

View File

@ -0,0 +1 @@
from comicapi.comicarchive import *

View File

@ -0,0 +1 @@
from comicapi.comicbookinfo import *

View File

@ -0,0 +1 @@
from comicapi.comicinfoxml import *

View File

@ -0,0 +1,442 @@
"""A python class to manage caching of data from Comic Vine"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import os
import sqlite3 as lite
from . import _version, utils
from .settings import ComicTaggerSettings
class ComicVineCacher:
def __init__(self):
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
self.db_file = os.path.join(self.settings_folder, "cv_cache.db")
self.version_file = os.path.join(self.settings_folder, "cache_version.txt")
# verify that cache is from same version as this one
data = ""
try:
with open(self.version_file, "rb") as f:
data = f.read().decode("utf-8")
f.close()
except:
pass
if data != _version.version:
self.clearCache()
if not os.path.exists(self.db_file):
self.create_cache_db()
def clearCache(self):
try:
os.unlink(self.db_file)
except:
pass
try:
os.unlink(self.version_file)
except:
pass
def create_cache_db(self):
# create the version file
with open(self.version_file, "w") as f:
f.write(_version.version)
# this will wipe out any existing version
open(self.db_file, "w").close()
con = lite.connect(self.db_file)
# create tables
with con:
cur = con.cursor()
# name,id,start_year,publisher,image,description,count_of_issues
cur.execute(
"CREATE TABLE VolumeSearchCache("
+ "search_term TEXT,"
+ "id INT,"
+ "name TEXT,"
+ "start_year INT,"
+ "publisher TEXT,"
+ "count_of_issues INT,"
+ "image_url TEXT,"
+ "description TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime'))) "
)
cur.execute(
"CREATE TABLE Volumes("
+ "id INT,"
+ "name TEXT,"
+ "publisher TEXT,"
+ "count_of_issues INT,"
+ "start_year INT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (id))"
)
cur.execute(
"CREATE TABLE AltCovers("
+ "issue_id INT,"
+ "url_list TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (issue_id))"
)
cur.execute(
"CREATE TABLE Issues("
+ "id INT,"
+ "volume_id INT,"
+ "name TEXT,"
+ "issue_number TEXT,"
+ "super_url TEXT,"
+ "thumb_url TEXT,"
+ "cover_date TEXT,"
+ "site_detail_url TEXT,"
+ "description TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (id))"
)
def add_search_results(self, search_term, cv_search_results):
con = lite.connect(self.db_file)
with con:
con.text_factory = str
cur = con.cursor()
# remove all previous entries with this search term
cur.execute("DELETE FROM VolumeSearchCache WHERE search_term = ?", [search_term.lower()])
# now add in new results
for record in cv_search_results:
timestamp = datetime.datetime.now()
if record["publisher"] is None:
pub_name = ""
else:
pub_name = record["publisher"]["name"]
if record["image"] is None:
url = ""
else:
url = record["image"]["super_url"]
cur.execute(
"INSERT INTO VolumeSearchCache "
+ "(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) "
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
(
search_term.lower(),
record["id"],
record["name"],
record["start_year"],
pub_name,
record["count_of_issues"],
url,
record["description"],
),
)
def get_search_results(self, search_term):
results = list()
con = lite.connect(self.db_file)
with con:
con.text_factory = str
cur = con.cursor()
# purge stale search results
a_day_ago = datetime.datetime.today() - datetime.timedelta(days=1)
cur.execute("DELETE FROM VolumeSearchCache WHERE timestamp < ?", [str(a_day_ago)])
# fetch
cur.execute("SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
rows = cur.fetchall()
# now process the results
for record in rows:
result = dict()
result["id"] = record[1]
result["name"] = record[2]
result["start_year"] = record[3]
result["publisher"] = dict()
result["publisher"]["name"] = record[4]
result["count_of_issues"] = record[5]
result["image"] = dict()
result["image"]["super_url"] = record[6]
result["description"] = record[7]
results.append(result)
return results
def add_alt_covers(self, issue_id, url_list):
con = lite.connect(self.db_file)
with con:
con.text_factory = str
cur = con.cursor()
# remove all previous entries with this search term
cur.execute("DELETE FROM AltCovers WHERE issue_id = ?", [issue_id])
url_list_str = utils.listToString(url_list)
# now add in new record
cur.execute("INSERT INTO AltCovers " + "(issue_id, url_list) " + "VALUES(?, ?)", (issue_id, url_list_str))
def get_alt_covers(self, issue_id):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = str
# purge stale issue info - probably issue data won't change
# much....
a_month_ago = datetime.datetime.today() - datetime.timedelta(days=30)
cur.execute("DELETE FROM AltCovers WHERE timestamp < ?", [str(a_month_ago)])
cur.execute("SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
row = cur.fetchone()
if row is None:
return None
else:
url_list_str = row[0]
if len(url_list_str) == 0:
return []
raw_list = url_list_str.split(",")
url_list = []
for item in raw_list:
url_list.append(str(item).strip())
return url_list
def add_volume_info(self, cv_volume_record):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
timestamp = datetime.datetime.now()
if cv_volume_record["publisher"] is None:
pub_name = ""
else:
pub_name = cv_volume_record["publisher"]["name"]
data = {
"name": cv_volume_record["name"],
"publisher": pub_name,
"count_of_issues": cv_volume_record["count_of_issues"],
"start_year": cv_volume_record["start_year"],
"timestamp": timestamp,
}
self.upsert(cur, "volumes", "id", cv_volume_record["id"], data)
def add_volume_issues_info(self, volume_id, cv_volume_issues):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
timestamp = datetime.datetime.now()
# add in issues
for issue in cv_volume_issues:
data = {
"volume_id": volume_id,
"name": issue["name"],
"issue_number": issue["issue_number"],
"site_detail_url": issue["site_detail_url"],
"cover_date": issue["cover_date"],
"super_url": issue["image"]["super_url"],
"thumb_url": issue["image"]["thumb_url"],
"description": issue["description"],
"timestamp": timestamp,
}
self.upsert(cur, "issues", "id", issue["id"], data)
def get_volume_info(self, volume_id):
result = None
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = str
# purge stale volume info
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
cur.execute("DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
# fetch
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [volume_id])
row = cur.fetchone()
if row is None:
return result
result = dict()
# since ID is primary key, there is only one row
result["id"] = row[0]
result["name"] = row[1]
result["publisher"] = dict()
result["publisher"]["name"] = row[2]
result["count_of_issues"] = row[3]
result["start_year"] = row[4]
result["issues"] = list()
return result
def get_volume_issues_info(self, volume_id):
result = None
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = str
# purge stale issue info - probably issue data won't change
# much....
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
cur.execute("DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
# fetch
results = list()
cur.execute(
"SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description FROM Issues WHERE volume_id = ?", [volume_id]
)
rows = cur.fetchall()
# now process the results
for row in rows:
record = dict()
record["id"] = row[0]
record["name"] = row[1]
record["issue_number"] = row[2]
record["site_detail_url"] = row[3]
record["cover_date"] = row[4]
record["image"] = dict()
record["image"]["super_url"] = row[5]
record["image"]["thumb_url"] = row[6]
record["description"] = row[7]
results.append(record)
if len(results) == 0:
return None
return results
def add_issue_select_details(self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = str
timestamp = datetime.datetime.now()
data = {
"super_url": image_url,
"thumb_url": thumb_image_url,
"cover_date": cover_date,
"site_detail_url": site_detail_url,
"timestamp": timestamp,
}
self.upsert(cur, "issues", "id", issue_id, data)
def get_issue_select_details(self, issue_id):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
con.text_factory = str
cur.execute("SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?", [issue_id])
row = cur.fetchone()
details = dict()
if row is None or row[0] is None:
details["image_url"] = None
details["thumb_image_url"] = None
details["cover_date"] = None
details["site_detail_url"] = None
else:
details["image_url"] = row[0]
details["thumb_image_url"] = row[1]
details["cover_date"] = row[2]
details["site_detail_url"] = row[3]
return details
def upsert(self, cur, tablename, pkname, pkval, data):
"""This does an insert if the given PK doesn't exist, and an
update it if does
TODO: look into checking if UPDATE is needed
TODO: should the cursor be created here, and not up the stack?
"""
ins_count = len(data) + 1
keys = ""
vals = list()
ins_slots = ""
set_slots = ""
for key in data:
if keys != "":
keys += ", "
if ins_slots != "":
ins_slots += ", "
if set_slots != "":
set_slots += ", "
keys += key
vals.append(data[key])
ins_slots += "?"
set_slots += key + " = ?"
keys += ", " + pkname
vals.append(pkval)
ins_slots += ", ?"
condition = pkname + " = ?"
sql_ins = "INSERT OR IGNORE INTO " + tablename + " (" + keys + ") " + " VALUES (" + ins_slots + ")"
cur.execute(sql_ins, vals)
sql_upd = "UPDATE " + tablename + " SET " + set_slots + " WHERE " + condition
cur.execute(sql_upd, vals)

View File

@ -0,0 +1,883 @@
"""A python class to manage communication with Comic Vine's REST API"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import json
import re
import ssl
import sys
import time
import unicodedata
import requests
from bs4 import BeautifulSoup
from . import _version, utils
from .comicvinecacher import ComicVineCacher
from .genericmetadata import GenericMetadata
from .issuestring import IssueString
try:
from PyQt5.QtCore import QByteArray, QObject, QUrl, pyqtSignal
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
except ImportError:
# No Qt, so define a few dummy QObjects to help us compile
class QObject:
def __init__(self, *args):
pass
class pyqtSignal:
def __init__(self, *args):
pass
def emit(a, b, c):
pass
# from settings import ComicTaggerSettings
class CVTypeID:
Volume = "4050"
Issue = "4000"
class ComicVineTalkerException(Exception):
Unknown = -1
Network = -2
InvalidKey = 100
RateLimit = 107
def __init__(self, code=-1, desc=""):
self.desc = desc
self.code = code
def __str__(self):
if self.code == ComicVineTalkerException.Unknown or self.code == ComicVineTalkerException.Network:
return self.desc
else:
return "CV error #{0}: [{1}]. \n".format(self.code, self.desc)
class ComicVineTalker(QObject):
logo_url = "http://static.comicvine.com/bundles/comicvinesite/images/logo.png"
api_key = ""
@staticmethod
def getRateLimitMessage():
if ComicVineTalker.api_key == "":
return "Comic Vine rate limit exceeded. You should configue your own Comic Vine API key."
else:
return "Comic Vine rate limit exceeded. Please wait a bit."
def __init__(self):
QObject.__init__(self)
self.api_base_url = "https://comicvine.gamespot.com/api"
self.wait_for_rate_limit = False
# key that is registered to comictagger
default_api_key = "27431e6787042105bd3e47e169a624521f89f3a4"
if ComicVineTalker.api_key == "":
self.api_key = default_api_key
else:
self.api_key = ComicVineTalker.api_key
self.log_func = None
def setLogFunc(self, log_func):
self.log_func = log_func
def writeLog(self, text):
if self.log_func is None:
# sys.stdout.write(text.encode(errors='replace'))
# sys.stdout.flush()
print(text, file=sys.stderr)
else:
self.log_func(text)
def parseDateStr(self, date_str):
day = None
month = None
year = None
if date_str is not None:
parts = date_str.split("-")
year = utils.xlate(parts[0], True)
if len(parts) > 1:
month = utils.xlate(parts[1], True)
if len(parts) > 2:
day = utils.xlate(parts[2], True)
return day, month, year
def testKey(self, key):
try:
test_url = self.api_base_url + "/issue/1/?api_key=" + key + "&format=json&field_list=name"
cv_response = requests.get(test_url, headers={"user-agent": "comictagger/" + _version.version}).json()
# Bogus request, but if the key is wrong, you get error 100: "Invalid
# API Key"
return cv_response["status_code"] != 100
except:
return False
"""
Get the contect from the CV server. If we're in "wait mode" and status code is a rate limit error
sleep for a bit and retry.
"""
def getCVContent(self, url, params):
total_time_waited = 0
limit_wait_time = 1
counter = 0
wait_times = [1, 2, 3, 4]
while True:
cv_response = self.getUrlContent(url, params)
if self.wait_for_rate_limit and cv_response["status_code"] == ComicVineTalkerException.RateLimit:
self.writeLog("Rate limit encountered. Waiting for {0} minutes\n".format(limit_wait_time))
time.sleep(limit_wait_time * 60)
total_time_waited += limit_wait_time
limit_wait_time = wait_times[counter]
if counter < 3:
counter += 1
# don't wait much more than 20 minutes
if total_time_waited < 20:
continue
if cv_response["status_code"] != 1:
self.writeLog("Comic Vine query failed with error #{0}: [{1}]. \n".format(cv_response["status_code"], cv_response["error"]))
raise ComicVineTalkerException(cv_response["status_code"], cv_response["error"])
else:
# it's all good
break
return cv_response
def getUrlContent(self, url, params):
# connect to server:
# if there is a 500 error, try a few more times before giving up
# any other error, just bail
# print("---", url)
for tries in range(3):
try:
resp = requests.get(url, params=params, headers={"user-agent": "comictagger/" + _version.version})
if resp.status_code == 200:
return resp.json()
if resp.status_code == 500:
self.writeLog("Try #{0}: ".format(tries + 1))
time.sleep(1)
self.writeLog(str(resp.status_code) + "\n")
else:
break
except requests.exceptions.RequestException as e:
self.writeLog(str(e) + "\n")
raise ComicVineTalkerException(ComicVineTalkerException.Network, "Network Error!")
raise ComicVineTalkerException(ComicVineTalkerException.Unknown, "Error on Comic Vine server")
def literalSearchForSeries(self, series_name, callback=None):
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 12 not 1/2
search_series_name = unicodedata.normalize("NFKD", series_name).encode("ascii", "ignore").decode("ascii")
params = {
"api_key": self.api_key,
"format": "json",
"resources": "volume",
"query": search_series_name,
"field_list": "volume,name,id,start_year,publisher,image,description,count_of_issues",
"page": 1,
"limit": 100,
}
cv_response = self.getCVContent(self.api_base_url + "/search", params)
search_results = list()
# see http://api.comicvine.com/documentation/#handling_responses
limit = cv_response["limit"]
current_result_count = cv_response["number_of_page_results"]
total_result_count = cv_response["number_of_total_results"]
# 8 Dec 2018 - Comic Vine changed query results again. Terms are now
# ORed together, and we get thousands of results. Good news is the
# results are sorted by relevance, so we can be smart about halting
# the search.
# 1. Don't fetch more than some sane amount of pages.
max_results = 50
total_result_count = min(total_result_count, max_results)
if callback is None:
self.writeLog("Found {0} of {1} results\n".format(cv_response["number_of_page_results"], cv_response["number_of_total_results"]))
search_results.extend(cv_response["results"])
page = 1
if callback is not None:
callback(current_result_count, total_result_count)
# see if we need to keep asking for more pages...
while current_result_count < total_result_count:
if callback is None:
self.writeLog("getting another page of results {0} of {1}...\n".format(current_result_count, total_result_count))
page += 1
params["page"] = page
cv_response = self.getCVContent(self.api_base_url + "/search", params)
search_results.extend(cv_response["results"])
current_result_count += cv_response["number_of_page_results"]
if callback is not None:
callback(current_result_count, total_result_count)
return search_results
def searchForSeries(self, series_name, callback=None, refresh_cache=False):
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 12 not 1/2
search_series_name = unicodedata.normalize("NFKD", series_name).encode("ascii", "ignore").decode("ascii")
# comicvine ignores punctuation and accents
search_series_name = re.sub(r"[^A-Za-z0-9]+", " ", search_series_name)
# remove extra space and articles and all lower case
search_series_name = utils.removearticles(search_series_name).lower().strip()
# before we search online, look in our cache, since we might have
# done this same search recently
cvc = ComicVineCacher()
if not refresh_cache:
cached_search_results = cvc.get_search_results(series_name)
if len(cached_search_results) > 0:
return cached_search_results
params = {
"api_key": self.api_key,
"format": "json",
"resources": "volume",
"query": search_series_name,
"field_list": "volume,name,id,start_year,publisher,image,description,count_of_issues",
"page": 1,
"limit": 100,
}
cv_response = self.getCVContent(self.api_base_url + "/search", params)
search_results = list()
# see http://api.comicvine.com/documentation/#handling_responses
limit = cv_response["limit"]
current_result_count = cv_response["number_of_page_results"]
total_result_count = cv_response["number_of_total_results"]
# 8 Dec 2018 - Comic Vine changed query results again. Terms are now
# ORed together, and we get thousands of results. Good news is the
# results are sorted by relevance, so we can be smart about halting
# the search.
# 1. Don't fetch more than some sane amount of pages.
max_results = 500
# 2. Halt when not all of our search terms are present in a result
# 3. Halt when the results contain more (plus threshold) words than
# our search
result_word_count_max = len(search_series_name.split()) + 3
total_result_count = min(total_result_count, max_results)
if callback is None:
self.writeLog("Found {0} of {1} results\n".format(cv_response["number_of_page_results"], cv_response["number_of_total_results"]))
search_results.extend(cv_response["results"])
page = 1
if callback is not None:
callback(current_result_count, total_result_count)
# see if we need to keep asking for more pages...
stop_searching = False
while current_result_count < total_result_count:
last_result = search_results[-1]["name"]
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 12 not 1/2
last_result = unicodedata.normalize("NFKD", last_result).encode("ascii", "ignore").decode("ascii")
# comicvine ignores punctuation and accents
last_result = re.sub(r"[^A-Za-z0-9]+", " ", last_result)
# remove extra space and articles and all lower case
last_result = utils.removearticles(last_result).lower().strip()
# See if the last result's name has all the of the search terms.
# if not, break out of this, loop, we're done.
for term in search_series_name.split():
if term not in last_result.lower():
# print("Term '{}' not in last result. Halting search result fetching".format(term))
stop_searching = True
break
# Also, stop searching when the word count of last results is too much longer
# than our search terms list
if len(last_result.split()) > result_word_count_max:
print(
"Last result '{}' is too long: max word count: {}; Search terms {}. Halting search result fetching".format(
last_result, result_word_count_max, search_series_name.split()
),
file=sys.stderr,
)
stop_searching = True
if stop_searching:
break
if callback is None:
self.writeLog("getting another page of results {0} of {1}...\n".format(current_result_count, total_result_count))
page += 1
params["page"] = page
cv_response = self.getCVContent(self.api_base_url + "/search", params)
search_results.extend(cv_response["results"])
current_result_count += cv_response["number_of_page_results"]
if callback is not None:
callback(current_result_count, total_result_count)
# Remove any search results that don't contain all the search terms
# (iterate backwards for easy removal)
for i in range(len(search_results) - 1, -1, -1):
record = search_results[i]
for term in search_series_name.split():
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 12 not 1/2
recordName = unicodedata.normalize("NFKD", record["name"]).encode("ascii", "ignore").decode("ascii")
# comicvine ignores punctuation and accents
recordName = re.sub(r"[^A-Za-z0-9]+", " ", recordName)
# remove extra space and articles and all lower case
recordName = utils.removearticles(recordName).lower().strip()
if term not in recordName:
del search_results[i]
break
# for record in search_results:
# print(u"{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year']))
# print(record)
# record['count_of_issues'] = record['count_of_isssues']
# print(u"{0}: {1} ({2})".format(search_results['results'][0]['id'], search_results['results'][0]['name'] , search_results['results'][0]['start_year']))
# cache these search results
cvc.add_search_results(series_name, search_results)
return search_results
def fetchVolumeData(self, series_id):
# before we search online, look in our cache, since we might already
# have this info
cvc = ComicVineCacher()
cached_volume_result = cvc.get_volume_info(series_id)
if cached_volume_result is not None:
return cached_volume_result
volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + str(series_id)
params = {"api_key": self.api_key, "format": "json", "field_list": "name,id,start_year,publisher,count_of_issues"}
cv_response = self.getCVContent(volume_url, params)
volume_results = cv_response["results"]
cvc.add_volume_info(volume_results)
return volume_results
def fetchIssuesByVolume(self, series_id):
# before we search online, look in our cache, since we might already
# have this info
cvc = ComicVineCacher()
cached_volume_issues_result = cvc.get_volume_issues_info(series_id)
if cached_volume_issues_result is not None:
return cached_volume_issues_result
params = {
"api_key": self.api_key,
"filter": "volume:" + str(series_id),
"format": "json",
"field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description",
}
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
# ------------------------------------
limit = cv_response["limit"]
current_result_count = cv_response["number_of_page_results"]
total_result_count = cv_response["number_of_total_results"]
# print("total_result_count", total_result_count)
# print("Found {0} of {1} results".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
volume_issues_result = cv_response["results"]
page = 1
offset = 0
# see if we need to keep asking for more pages...
while current_result_count < total_result_count:
# print("getting another page of issue results {0} of {1}...".format(current_result_count, total_result_count))
page += 1
offset += cv_response["number_of_page_results"]
params["offset"] = offset
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
volume_issues_result.extend(cv_response["results"])
current_result_count += cv_response["number_of_page_results"]
self.repairUrls(volume_issues_result)
cvc.add_volume_issues_info(series_id, volume_issues_result)
return volume_issues_result
def fetchIssuesByVolumeIssueNumAndYear(self, volume_id_list, issue_number, year):
volume_filter = ""
for vid in volume_id_list:
volume_filter += str(vid) + "|"
filter = "volume:{},issue_number:{}".format(volume_filter, issue_number)
intYear = utils.xlate(year, True)
if intYear is not None:
filter += ",cover_date:{}-1-1|{}-1-1".format(intYear, intYear + 1)
params = {
"api_key": self.api_key,
"format": "json",
"field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description",
"filter": filter,
}
cv_response = self.getCVContent(self.api_base_url + "/issues", params)
# ------------------------------------
limit = cv_response["limit"]
current_result_count = cv_response["number_of_page_results"]
total_result_count = cv_response["number_of_total_results"]
# print("total_result_count", total_result_count)
# print("Found {0} of {1} results\n".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
filtered_issues_result = cv_response["results"]
page = 1
offset = 0
# see if we need to keep asking for more pages...
while current_result_count < total_result_count:
# print("getting another page of issue results {0} of {1}...\n".format(current_result_count, total_result_count))
page += 1
offset += cv_response["number_of_page_results"]
params["offset"] = offset
cv_response = self.getCVContent(self.api_base_url + "/issues/", params)
filtered_issues_result.extend(cv_response["results"])
current_result_count += cv_response["number_of_page_results"]
self.repairUrls(filtered_issues_result)
return filtered_issues_result
def fetchIssueData(self, series_id, issue_number, settings):
volume_results = self.fetchVolumeData(series_id)
issues_list_results = self.fetchIssuesByVolume(series_id)
found = False
for record in issues_list_results:
if IssueString(issue_number).asString() is None:
issue_number = 1
if IssueString(record["issue_number"]).asString().lower() == IssueString(issue_number).asString().lower():
found = True
break
if found:
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(record["id"])
params = {"api_key": self.api_key, "format": "json"}
cv_response = self.getCVContent(issue_url, params)
issue_results = cv_response["results"]
else:
return None
# Now, map the Comic Vine data to generic metadata
return self.mapCVDataToMetadata(volume_results, issue_results, settings)
def fetchIssueDataByIssueID(self, issue_id, settings):
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id)
params = {"api_key": self.api_key, "format": "json"}
cv_response = self.getCVContent(issue_url, params)
issue_results = cv_response["results"]
volume_results = self.fetchVolumeData(issue_results["volume"]["id"])
# Now, map the Comic Vine data to generic metadata
md = self.mapCVDataToMetadata(volume_results, issue_results, settings)
md.isEmpty = False
return md
def mapCVDataToMetadata(self, volume_results, issue_results, settings):
# Now, map the Comic Vine data to generic metadata
metadata = GenericMetadata()
metadata.series = utils.xlate(issue_results["volume"]["name"])
metadata.issue = IssueString(issue_results["issue_number"]).asString()
metadata.title = utils.xlate(issue_results["name"])
if volume_results["publisher"] is not None:
metadata.publisher = utils.xlate(volume_results["publisher"]["name"])
metadata.day, metadata.month, metadata.year = self.parseDateStr(issue_results["cover_date"])
metadata.seriesYear = utils.xlate(volume_results["start_year"])
metadata.issueCount = utils.xlate(volume_results["count_of_issues"])
metadata.comments = self.cleanup_html(issue_results["description"], settings.remove_html_tables)
if settings.use_series_start_as_volume:
metadata.volume = utils.xlate(volume_results["start_year"])
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
ctversion.version, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), issue_results["id"]
)
# metadata.notes += issue_results['site_detail_url']
metadata.webLink = issue_results["site_detail_url"]
person_credits = issue_results["person_credits"]
for person in person_credits:
if "role" in person:
roles = person["role"].split(",")
for role in roles:
# can we determine 'primary' from CV??
metadata.addCredit(person["name"], role.title().strip(), False)
character_credits = issue_results["character_credits"]
character_list = list()
for character in character_credits:
character_list.append(character["name"])
metadata.characters = utils.listToString(character_list)
team_credits = issue_results["team_credits"]
team_list = list()
for team in team_credits:
team_list.append(team["name"])
metadata.teams = utils.listToString(team_list)
location_credits = issue_results["location_credits"]
location_list = list()
for location in location_credits:
location_list.append(location["name"])
metadata.locations = utils.listToString(location_list)
story_arc_credits = issue_results["story_arc_credits"]
arc_list = []
for arc in story_arc_credits:
arc_list.append(arc["name"])
if len(arc_list) > 0:
metadata.storyArc = utils.listToString(arc_list)
return metadata
def cleanup_html(self, string, remove_html_tables):
"""
converter = html2text.HTML2Text()
#converter.emphasis_mark = '*'
#converter.ignore_links = True
converter.body_width = 0
print(html2text.html2text(string))
return string
#return converter.handle(string)
"""
if string is None:
return ""
# find any tables
soup = BeautifulSoup(string, "html.parser")
tables = soup.findAll("table")
# remove all newlines first
string = string.replace("\n", "")
# put in our own
string = string.replace("<br>", "\n")
string = string.replace("</p>", "\n\n")
string = string.replace("<h4>", "*")
string = string.replace("</h4>", "*\n")
# remove the tables
p = re.compile(r"<table[^<]*?>.*?<\/table>")
if remove_html_tables:
string = p.sub("", string)
string = string.replace("*List of covers and their creators:*", "")
else:
string = p.sub("{}", string)
# now strip all other tags
p = re.compile(r"<[^<]*?>")
newstring = p.sub("", string)
newstring = newstring.replace("&nbsp;", " ")
newstring = newstring.replace("&amp;", "&")
newstring = newstring.strip()
if not remove_html_tables:
# now rebuild the tables into text from BSoup
try:
table_strings = []
for table in tables:
rows = []
hdrs = []
col_widths = []
for hdr in table.findAll("th"):
item = hdr.string.strip()
hdrs.append(item)
col_widths.append(len(item))
rows.append(hdrs)
for row in table.findAll("tr"):
cols = []
col = row.findAll("td")
i = 0
for c in col:
item = c.string.strip()
cols.append(item)
if len(item) > col_widths[i]:
col_widths[i] = len(item)
i += 1
if len(cols) != 0:
rows.append(cols)
# now we have the data, make it into text
fmtstr = ""
for w in col_widths:
fmtstr += " {{:{}}}|".format(w + 1)
width = sum(col_widths) + len(col_widths) * 2
print("width=", width)
table_text = ""
counter = 0
for row in rows:
table_text += fmtstr.format(*row) + "\n"
if counter == 0 and len(hdrs) != 0:
table_text += "-" * width + "\n"
counter += 1
table_strings.append(table_text)
newstring = newstring.format(*table_strings)
except:
# we caught an error rebuilding the table.
# just bail and remove the formatting
print("table parse error")
newstring.replace("{}", "")
return newstring
def fetchIssueDate(self, issue_id):
details = self.fetchIssueSelectDetails(issue_id)
day, month, year = self.parseDateStr(details["cover_date"])
return month, year
def fetchIssueCoverURLs(self, issue_id):
details = self.fetchIssueSelectDetails(issue_id)
return details["image_url"], details["thumb_image_url"]
def fetchIssuePageURL(self, issue_id):
details = self.fetchIssueSelectDetails(issue_id)
return details["site_detail_url"]
def fetchIssueSelectDetails(self, issue_id):
# cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails(issue_id)
cached_details = self.fetchCachedIssueSelectDetails(issue_id)
if cached_details["image_url"] is not None:
return cached_details
issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id)
params = {"api_key": self.api_key, "format": "json", "field_list": "image,cover_date,site_detail_url"}
cv_response = self.getCVContent(issue_url, params)
details = dict()
details["image_url"] = None
details["thumb_image_url"] = None
details["cover_date"] = None
details["site_detail_url"] = None
details["image_url"] = cv_response["results"]["image"]["super_url"]
details["thumb_image_url"] = cv_response["results"]["image"]["thumb_url"]
details["cover_date"] = cv_response["results"]["cover_date"]
details["site_detail_url"] = cv_response["results"]["site_detail_url"]
if details["image_url"] is not None:
self.cacheIssueSelectDetails(
issue_id, details["image_url"], details["thumb_image_url"], details["cover_date"], details["site_detail_url"]
)
# print(details['site_detail_url'])
return details
def fetchCachedIssueSelectDetails(self, issue_id):
# before we search online, look in our cache, since we might already
# have this info
cvc = ComicVineCacher()
return cvc.get_issue_select_details(issue_id)
def cacheIssueSelectDetails(self, issue_id, image_url, thumb_url, cover_date, page_url):
cvc = ComicVineCacher()
cvc.add_issue_select_details(issue_id, image_url, thumb_url, cover_date, page_url)
def fetchAlternateCoverURLs(self, issue_id, issue_page_url):
url_list = self.fetchCachedAlternateCoverURLs(issue_id)
if url_list is not None:
return url_list
# scrape the CV issue page URL to get the alternate cover URLs
content = requests.get(issue_page_url, headers={"user-agent": "comictagger/" + _version.version}).text
alt_cover_url_list = self.parseOutAltCoverUrls(content)
# cache this alt cover URL list
self.cacheAlternateCoverURLs(issue_id, alt_cover_url_list)
return alt_cover_url_list
def parseOutAltCoverUrls(self, page_html):
soup = BeautifulSoup(page_html, "html.parser")
alt_cover_url_list = []
# Using knowledge of the layout of the Comic Vine issue page here:
# look for the divs that are in the classes 'imgboxart' and
# 'issue-cover'
div_list = soup.find_all("div")
covers_found = 0
for d in div_list:
if "class" in d.attrs:
c = d["class"]
if "imgboxart" in c and "issue-cover" in c and d.img["src"].startswith("http"):
covers_found += 1
if covers_found != 1:
alt_cover_url_list.append(d.img["src"])
return alt_cover_url_list
def fetchCachedAlternateCoverURLs(self, issue_id):
# before we search online, look in our cache, since we might already
# have this info
cvc = ComicVineCacher()
url_list = cvc.get_alt_covers(issue_id)
if url_list is not None:
return url_list
else:
return None
def cacheAlternateCoverURLs(self, issue_id, url_list):
cvc = ComicVineCacher()
cvc.add_alt_covers(issue_id, url_list)
# -------------------------------------------------------------------------
urlFetchComplete = pyqtSignal(str, str, int)
def asyncFetchIssueCoverURLs(self, issue_id):
self.issue_id = issue_id
details = self.fetchCachedIssueSelectDetails(issue_id)
if details["image_url"] is not None:
self.urlFetchComplete.emit(details["image_url"], details["thumb_image_url"], self.issue_id)
return
issue_url = (
self.api_base_url
+ "/issue/"
+ CVTypeID.Issue
+ "-"
+ str(issue_id)
+ "/?api_key="
+ self.api_key
+ "&format=json&field_list=image,cover_date,site_detail_url"
)
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.asyncFetchIssueCoverURLComplete)
self.nam.get(QNetworkRequest(QUrl(issue_url)))
def asyncFetchIssueCoverURLComplete(self, reply):
# read in the response
data = reply.readAll()
try:
cv_response = json.loads(bytes(data))
except Exception as e:
print("Comic Vine query failed to get JSON data", file=sys.stderr)
print(str(data), file=sys.stderr)
return
if cv_response["status_code"] != 1:
print("Comic Vine query failed with error: [{0}]. ".format(cv_response["error"]), file=sys.stderr)
return
image_url = cv_response["results"]["image"]["super_url"]
thumb_url = cv_response["results"]["image"]["thumb_url"]
cover_date = cv_response["results"]["cover_date"]
page_url = cv_response["results"]["site_detail_url"]
self.cacheIssueSelectDetails(self.issue_id, image_url, thumb_url, cover_date, page_url)
self.urlFetchComplete.emit(image_url, thumb_url, self.issue_id)
altUrlListFetchComplete = pyqtSignal(list, int)
def asyncFetchAlternateCoverURLs(self, issue_id, issue_page_url):
# This async version requires the issue page url to be provided!
self.issue_id = issue_id
url_list = self.fetchCachedAlternateCoverURLs(issue_id)
if url_list is not None:
self.altUrlListFetchComplete.emit(url_list, int(self.issue_id))
return
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.asyncFetchAlternateCoverURLsComplete)
self.nam.get(QNetworkRequest(QUrl(str(issue_page_url))))
def asyncFetchAlternateCoverURLsComplete(self, reply):
# read in the response
html = str(reply.readAll())
alt_cover_url_list = self.parseOutAltCoverUrls(html)
# cache this alt cover URL list
self.cacheAlternateCoverURLs(self.issue_id, alt_cover_url_list)
self.altUrlListFetchComplete.emit(alt_cover_url_list, int(self.issue_id))
def repairUrls(self, issue_list):
# make sure there are URLs for the image fields
for issue in issue_list:
if issue["image"] is None:
issue["image"] = dict()
issue["image"]["super_url"] = ComicVineTalker.logo_url
issue["image"]["thumb_url"] = ComicVineTalker.logo_url

View File

@ -0,0 +1,308 @@
"""A PyQt5 widget to display cover images
Display cover images from either a local archive, or from Comic Vine.
TODO: This should be re-factored using subclasses!
"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from comictaggerlib.ui.qtutils import getQImageFromData, reduceWidgetFontSize
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .imagefetcher import ImageFetcher
from .imagepopup import ImagePopup
from .pageloader import PageLoader
from .settings import ComicTaggerSettings
def clickable(widget):
"""# Allow a label to be clickable"""
class Filter(QObject):
dblclicked = pyqtSignal()
def eventFilter(self, obj, event):
if obj == widget:
if event.type() == QEvent.MouseButtonDblClick:
self.dblclicked.emit()
return True
return False
filter = Filter(widget)
widget.installEventFilter(filter)
return filter.dblclicked
class CoverImageWidget(QWidget):
ArchiveMode = 0
AltCoverMode = 1
URLMode = 1
DataMode = 3
def __init__(self, parent, mode, expand_on_click=True):
super(CoverImageWidget, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("coverimagewidget.ui"), self)
reduceWidgetFontSize(self.label)
self.mode = mode
self.comicVine = ComicVineTalker()
self.page_loader = None
self.showControls = True
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic("left.png")))
self.btnRight.setIcon(QIcon(ComicTaggerSettings.getGraphic("right.png")))
self.btnLeft.clicked.connect(self.decrementImage)
self.btnRight.clicked.connect(self.incrementImage)
self.resetWidget()
if expand_on_click:
clickable(self.lblImage).connect(self.showPopup)
else:
self.lblImage.setToolTip("")
self.updateContent()
def resetWidget(self):
self.comic_archive = None
self.issue_id = None
self.comicVine = None
self.cover_fetcher = None
self.url_list = []
if self.page_loader is not None:
self.page_loader.abandoned = True
self.page_loader = None
self.imageIndex = -1
self.imageCount = 1
self.imageData = None
def clear(self):
self.resetWidget()
self.updateContent()
def incrementImage(self):
self.imageIndex += 1
if self.imageIndex == self.imageCount:
self.imageIndex = 0
self.updateContent()
def decrementImage(self):
self.imageIndex -= 1
if self.imageIndex == -1:
self.imageIndex = self.imageCount - 1
self.updateContent()
def setArchive(self, ca, page=0):
if self.mode == CoverImageWidget.ArchiveMode:
self.resetWidget()
self.comic_archive = ca
self.imageIndex = page
self.imageCount = ca.getNumberOfPages()
self.updateContent()
def setURL(self, url):
if self.mode == CoverImageWidget.URLMode:
self.resetWidget()
self.updateContent()
self.url_list = [url]
self.imageIndex = 0
self.imageCount = 1
self.updateContent()
def setIssueID(self, issue_id):
if self.mode == CoverImageWidget.AltCoverMode:
self.resetWidget()
self.updateContent()
self.issue_id = issue_id
self.comicVine = ComicVineTalker()
self.comicVine.urlFetchComplete.connect(self.primaryUrlFetchComplete)
self.comicVine.asyncFetchIssueCoverURLs(int(self.issue_id))
def setImageData(self, image_data):
if self.mode == CoverImageWidget.DataMode:
self.resetWidget()
if image_data is None:
self.imageIndex = -1
else:
self.imageIndex = 0
self.imageData = image_data
self.updateContent()
def primaryUrlFetchComplete(self, primary_url, thumb_url, issue_id):
self.url_list.append(str(primary_url))
self.imageIndex = 0
self.imageCount = len(self.url_list)
self.updateContent()
# defer the alt cover search
QTimer.singleShot(1, self.startAltCoverSearch)
def startAltCoverSearch(self):
# now we need to get the list of alt cover URLs
self.label.setText("Searching for alt. covers...")
# page URL should already be cached, so no need to defer
self.comicVine = ComicVineTalker()
issue_page_url = self.comicVine.fetchIssuePageURL(self.issue_id)
self.comicVine.altUrlListFetchComplete.connect(self.altCoverUrlListFetchComplete)
self.comicVine.asyncFetchAlternateCoverURLs(int(self.issue_id), issue_page_url)
def altCoverUrlListFetchComplete(self, url_list, issue_id):
if len(url_list) > 0:
self.url_list.extend(url_list)
self.imageCount = len(self.url_list)
self.updateControls()
def setPage(self, pagenum):
if self.mode == CoverImageWidget.ArchiveMode:
self.imageIndex = pagenum
self.updateContent()
def updateContent(self):
self.updateImage()
self.updateControls()
def updateImage(self):
if self.imageIndex == -1:
self.loadDefault()
elif self.mode in [CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode]:
self.loadURL()
elif self.mode == CoverImageWidget.DataMode:
self.coverRemoteFetchComplete(self.imageData, 0)
else:
self.loadPage()
def updateControls(self):
if not self.showControls or self.mode == CoverImageWidget.DataMode:
self.btnLeft.hide()
self.btnRight.hide()
self.label.hide()
return
if self.imageIndex == -1 or self.imageCount == 1:
self.btnLeft.setEnabled(False)
self.btnRight.setEnabled(False)
self.btnLeft.hide()
self.btnRight.hide()
else:
self.btnLeft.setEnabled(True)
self.btnRight.setEnabled(True)
self.btnLeft.show()
self.btnRight.show()
if self.imageIndex == -1 or self.imageCount == 1:
self.label.setText("")
elif self.mode == CoverImageWidget.AltCoverMode:
self.label.setText("Cover {0} (of {1})".format(self.imageIndex + 1, self.imageCount))
else:
self.label.setText("Page {0} (of {1})".format(self.imageIndex + 1, self.imageCount))
def loadURL(self):
self.loadDefault()
self.cover_fetcher = ImageFetcher()
self.cover_fetcher.fetchComplete.connect(self.coverRemoteFetchComplete)
self.cover_fetcher.fetch(self.url_list[self.imageIndex])
# print("ATB cover fetch started...")
# called when the image is done loading from internet
def coverRemoteFetchComplete(self, image_data, issue_id):
img = getQImageFromData(image_data)
self.current_pixmap = QPixmap(img)
self.setDisplayPixmap(0, 0)
# print("ATB cover fetch complete!")
def loadPage(self):
if self.comic_archive is not None:
if self.page_loader is not None:
self.page_loader.abandoned = True
self.page_loader = PageLoader(self.comic_archive, self.imageIndex)
self.page_loader.loadComplete.connect(self.pageLoadComplete)
self.page_loader.start()
def pageLoadComplete(self, img):
self.current_pixmap = QPixmap(img)
self.setDisplayPixmap(0, 0)
self.page_loader = None
def loadDefault(self):
self.current_pixmap = QPixmap(ComicTaggerSettings.getGraphic("nocover.png"))
# print("loadDefault called")
self.setDisplayPixmap(0, 0)
def resizeEvent(self, resize_event):
if self.current_pixmap is not None:
delta_w = resize_event.size().width() - resize_event.oldSize().width()
delta_h = resize_event.size().height() - resize_event.oldSize().height()
# print "ATB resizeEvent deltas", resize_event.size().width(),
# resize_event.size().height()
self.setDisplayPixmap(delta_w, delta_h)
def setDisplayPixmap(self, delta_w, delta_h):
"""The deltas let us know what the new width and height of the label will be"""
# new_h = self.frame.height() + delta_h
# new_w = self.frame.width() + delta_w
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
# print "ATB self.frame", self.frame.width(), self.frame.height()
# print "ATB self.", self.width(), self.height()
# frame_w = new_w
# frame_h = new_h
new_h = self.frame.height()
new_w = self.frame.width()
frame_w = self.frame.width()
frame_h = self.frame.height()
new_h -= 4
new_w -= 4
if new_h < 0:
new_h = 0
if new_w < 0:
new_w = 0
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
# print "ATB self.frame", frame_w, frame_h
# print "ATB new size", new_w, new_h
# scale the pixmap to fit in the frame
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.lblImage.setPixmap(scaled_pixmap)
# move and resize the label to be centered in the fame
img_w = scaled_pixmap.width()
img_h = scaled_pixmap.height()
self.lblImage.resize(img_w, img_h)
self.lblImage.move((frame_w - img_w) / 2, (frame_h - img_h) / 2)
def showPopup(self):
self.popup = ImagePopup(self, self.current_pixmap)

View File

@ -0,0 +1,92 @@
"""A PyQT4 dialog to edit credits"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from .settings import ComicTaggerSettings
class CreditEditorWindow(QtWidgets.QDialog):
ModeEdit = 0
ModeNew = 1
def __init__(self, parent, mode, role, name, primary):
super(CreditEditorWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("crediteditorwindow.ui"), self)
self.mode = mode
if self.mode == self.ModeEdit:
self.setWindowTitle("Edit Credit")
else:
self.setWindowTitle("New Credit")
# Add the entries to the role combobox
self.cbRole.addItem("")
self.cbRole.addItem("Writer")
self.cbRole.addItem("Artist")
self.cbRole.addItem("Penciller")
self.cbRole.addItem("Inker")
self.cbRole.addItem("Colorist")
self.cbRole.addItem("Letterer")
self.cbRole.addItem("Cover Artist")
self.cbRole.addItem("Editor")
self.cbRole.addItem("Other")
self.cbRole.addItem("Plotter")
self.cbRole.addItem("Scripter")
self.leName.setText(name)
if role is not None and role != "":
i = self.cbRole.findText(role)
if i == -1:
self.cbRole.setEditText(role)
else:
self.cbRole.setCurrentIndex(i)
if primary:
self.cbPrimary.setCheckState(QtCore.Qt.Checked)
self.cbRole.currentIndexChanged.connect(self.roleChanged)
self.cbRole.editTextChanged.connect(self.roleChanged)
self.updatePrimaryButton()
def updatePrimaryButton(self):
enabled = self.currentRoleCanBePrimary()
self.cbPrimary.setEnabled(enabled)
def currentRoleCanBePrimary(self):
role = self.cbRole.currentText()
if str(role).lower() == "writer" or str(role).lower() == "artist":
return True
else:
return False
def roleChanged(self, s):
self.updatePrimaryButton()
def getCredits(self):
primary = self.currentRoleCanBePrimary() and self.cbPrimary.isChecked()
return self.cbRole.currentText(), self.leName.text(), primary
def accept(self):
if self.cbRole.currentText() == "" or self.leName.text() == "":
QtWidgets.QMessageBox.warning(self, self.tr("Whoops"), self.tr("You need to enter both role and name for a credit."))
else:
QtWidgets.QDialog.accept(self)

View File

@ -0,0 +1,57 @@
"""A PyQT4 dialog to confirm and set options for export to zip"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from .settings import ComicTaggerSettings
class ExportConflictOpts:
dontCreate = 1
overwrite = 2
createUnique = 3
class ExportWindow(QtWidgets.QDialog):
def __init__(self, parent, settings, msg):
super(ExportWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("exportwindow.ui"), self)
self.label.setText(msg)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.cbxDeleteOriginal.setCheckState(QtCore.Qt.Unchecked)
self.cbxAddToList.setCheckState(QtCore.Qt.Checked)
self.radioDontCreate.setChecked(True)
self.deleteOriginal = False
self.addToList = True
self.fileConflictBehavior = ExportConflictOpts.dontCreate
def accept(self):
QtWidgets.QDialog.accept(self)
self.deleteOriginal = self.cbxDeleteOriginal.isChecked()
self.addToList = self.cbxAddToList.isChecked()
if self.radioDontCreate.isChecked():
self.fileConflictBehavior = ExportConflictOpts.dontCreate
elif self.radioCreateNew.isChecked():
self.fileConflictBehavior = ExportConflictOpts.createUnique
# else:
# self.fileConflictBehavior = ExportConflictOpts.overwrite

View File

@ -0,0 +1 @@
from comicapi.filenameparser import *

View File

@ -0,0 +1,144 @@
"""Functions for renaming files based on metadata"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import re
import string
import sys
from pathvalidate import sanitize_filepath
from . import utils
from .issuestring import IssueString
class MetadataFormatter(string.Formatter):
def __init__(self, smart_cleanup=False):
super().__init__()
self.smart_cleanup = smart_cleanup
def format_field(self, value, format_spec):
if value is None or value == "":
return ""
return super().format_field(value, format_spec)
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth, auto_arg_index=0):
if recursion_depth < 0:
raise ValueError("Max string recursion exceeded")
result = []
lstrip = False
for literal_text, field_name, format_spec, conversion in self.parse(format_string):
# output the literal text
if literal_text:
if lstrip:
result.append(literal_text.lstrip("-_)}]#"))
else:
result.append(literal_text)
lstrip = False
# if there's a field, output it
if field_name is not None:
# this is some markup, find the object and do
# the formatting
# handle arg indexing when empty field_names are given.
if field_name == "":
if auto_arg_index is False:
raise ValueError("cannot switch from manual field specification to automatic field numbering")
field_name = str(auto_arg_index)
auto_arg_index += 1
elif field_name.isdigit():
if auto_arg_index:
raise ValueError("cannot switch from manual field specification to automatic field numbering")
# disable auto arg incrementing, if it gets
# used later on, then an exception will be raised
auto_arg_index = False
# given the field_name, find the object it references
# and the argument it came from
obj, arg_used = self.get_field(field_name, args, kwargs)
used_args.add(arg_used)
# do any conversion on the resulting object
obj = self.convert_field(obj, conversion)
# expand the format spec, if needed
format_spec, auto_arg_index = self._vformat(format_spec, args, kwargs, used_args, recursion_depth - 1, auto_arg_index=auto_arg_index)
# format the object and append to the result
fmtObj = self.format_field(obj, format_spec)
if fmtObj == "" and len(result) > 0 and self.smart_cleanup:
lstrip = True
result.pop()
result.append(fmtObj)
return "".join(result), auto_arg_index
class FileRenamer:
def __init__(self, metadata):
self.setMetadata(metadata)
self.setTemplate("{publisher}/{series}/{series} v{volume} #{issue} (of {issueCount}) ({year})")
self.smart_cleanup = True
self.issue_zero_padding = 3
self.move = False
def setMetadata(self, metadata):
self.metdata = metadata
def setIssueZeroPadding(self, count):
self.issue_zero_padding = count
def setSmartCleanup(self, on):
self.smart_cleanup = on
def setTemplate(self, template):
self.template = template
def determineName(self, filename, ext=None):
class Default(dict):
def __missing__(self, key):
return "{" + key + "}"
md = self.metdata
# padding for issue
md.issue = IssueString(md.issue).asString(pad=self.issue_zero_padding)
template = self.template
pathComponents = template.split(os.sep)
new_name = ""
fmt = MetadataFormatter(self.smart_cleanup)
for Component in pathComponents:
new_name = os.path.join(new_name, fmt.vformat(Component, args=[], kwargs=Default(vars(md))).replace("/", "-"))
if ext is None or ext == "":
ext = os.path.splitext(filename)[1]
new_name += ext
# some tweaks to keep various filesystems happy
new_name = new_name.replace(": ", " - ")
new_name = new_name.replace(":", "-")
# remove padding
md.issue = IssueString(md.issue).asString()
if self.move:
return sanitize_filepath(new_name.strip())
else:
return os.path.basename(sanitize_filepath(new_name.strip()))

View File

@ -0,0 +1,434 @@
# coding=utf-8
"""A PyQt5 widget for managing list of comic archive files"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import platform
import sys
from PyQt5 import uic
from PyQt5.QtCore import *
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from comictaggerlib.ui.qtutils import centerWindowOnParent, reduceWidgetFontSize
from . import utils
from .comicarchive import ComicArchive
from .optionalmsgdialog import OptionalMessageDialog
from .settings import ComicTaggerSettings
class FileTableWidgetItem(QTableWidgetItem):
def __lt__(self, other):
# return (self.data(Qt.UserRole).toBool() <
# other.data(Qt.UserRole).toBool())
return self.data(Qt.UserRole) < other.data(Qt.UserRole)
class FileInfo:
def __init__(self, ca):
self.ca = ca
class FileSelectionList(QWidget):
selectionChanged = pyqtSignal(QVariant)
listCleared = pyqtSignal()
fileColNum = 0
CRFlagColNum = 1
CBLFlagColNum = 2
typeColNum = 3
readonlyColNum = 4
folderColNum = 5
dataColNum = fileColNum
def __init__(self, parent, settings):
super(FileSelectionList, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("fileselectionlist.ui"), self)
self.settings = settings
reduceWidgetFontSize(self.twList)
self.twList.setColumnCount(6)
# self.twlist.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
# self.twList.horizontalHeader().setStretchLastSection(True)
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
self.currentItem = None
self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.modifiedFlag = False
selectAllAction = QAction("Select All", self)
removeAction = QAction("Remove Selected Items", self)
self.separator = QAction("", self)
self.separator.setSeparator(True)
selectAllAction.setShortcut("Ctrl+A")
removeAction.setShortcut("Ctrl+X")
selectAllAction.triggered.connect(self.selectAll)
removeAction.triggered.connect(self.removeSelection)
self.addAction(selectAllAction)
self.addAction(removeAction)
self.addAction(self.separator)
def getSorting(self):
col = self.twList.horizontalHeader().sortIndicatorSection()
order = self.twList.horizontalHeader().sortIndicatorOrder()
return col, order
def setSorting(self, col, order):
col = self.twList.horizontalHeader().setSortIndicator(col, order)
def addAppAction(self, action):
self.insertAction(None, action)
def setModifiedFlag(self, modified):
self.modifiedFlag = modified
def selectAll(self):
self.twList.setRangeSelected(QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), True)
def deselectAll(self):
self.twList.setRangeSelected(QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), False)
def removeArchiveList(self, ca_list):
self.twList.setSortingEnabled(False)
for ca in ca_list:
for row in range(self.twList.rowCount()):
row_ca = self.getArchiveByRow(row)
if row_ca == ca:
self.twList.removeRow(row)
break
self.twList.setSortingEnabled(True)
def getArchiveByRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(Qt.UserRole)
return fi.ca
def getCurrentArchive(self):
return self.getArchiveByRow(self.twList.currentRow())
def removeSelection(self):
row_list = []
for item in self.twList.selectedItems():
if item.column() == 0:
row_list.append(item.row())
if len(row_list) == 0:
return
if self.twList.currentRow() in row_list:
if not self.modifiedFlagVerification("Remove Archive", "If you close this archive, data in the form will be lost. Are you sure?"):
return
row_list.sort()
row_list.reverse()
self.twList.currentItemChanged.disconnect(self.currentItemChangedCB)
self.twList.setSortingEnabled(False)
for i in row_list:
self.twList.removeRow(i)
self.twList.setSortingEnabled(True)
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
if self.twList.rowCount() > 0:
# since on a removal, we select row 0, make sure callback occurs if
# we're already there
if self.twList.currentRow() == 0:
self.currentItemChangedCB(self.twList.currentItem(), None)
self.twList.selectRow(0)
else:
self.listCleared.emit()
def addPathList(self, pathlist):
filelist = utils.get_recursive_filelist(pathlist)
# we now have a list of files to add
# Prog dialog on Linux flakes out for small range, so scale up
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), parent=self)
progdialog.setWindowTitle("Adding Files")
progdialog.setWindowModality(Qt.ApplicationModal)
progdialog.setMinimumDuration(300)
centerWindowOnParent(progdialog)
# QCoreApplication.processEvents()
# progdialog.show()
QCoreApplication.processEvents()
firstAdded = None
self.twList.setSortingEnabled(False)
for idx, f in enumerate(filelist):
QCoreApplication.processEvents()
if progdialog.wasCanceled():
break
progdialog.setValue(idx + 1)
progdialog.setLabelText(f)
centerWindowOnParent(progdialog)
QCoreApplication.processEvents()
row = self.addPathItem(f)
if firstAdded is None and row is not None:
firstAdded = row
progdialog.hide()
QCoreApplication.processEvents()
if self.settings.show_no_unrar_warning and self.settings.unrar_lib_path == "" and not ComicTaggerSettings.haveOwnUnrarLib():
for f in filelist:
ext = os.path.splitext(f)[1].lower()
if ext == ".rar" or ext == ".cbr":
checked = OptionalMessageDialog.msg(
self,
"No UnRAR Ability",
"""
It looks like you've tried to open at least one CBR or RAR file.<br><br>
In order for ComicTagger to read this kind of file, you will have to configure
the location of the unrar library in the settings. Until then, ComicTagger
will not be able read these kind of files. See the "RAR Tools" tab in the
settings/preferences for more info.
""",
)
self.settings.show_no_unrar_warning = not checked
break
if firstAdded is not None:
self.twList.selectRow(firstAdded)
else:
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
ext = os.path.splitext(pathlist[0])[1].lower()
if ext == ".rar" or ext == ".cbr" and self.settings.unrar_lib_path == "":
QMessageBox.information(
self,
self.tr("File Open"),
self.tr("Selected file seems to be a rar file, " "and can't be read until the unrar library is configured."),
)
else:
QMessageBox.information(self, self.tr("File Open"), self.tr("Selected file doesn't seem to be a comic archive."))
else:
QMessageBox.information(self, self.tr("File/Folder Open"), self.tr("No readable comic archives were found."))
self.twList.setSortingEnabled(True)
# Adjust column size
self.twList.resizeColumnsToContents()
self.twList.setColumnWidth(FileSelectionList.CRFlagColNum, 35)
self.twList.setColumnWidth(FileSelectionList.CBLFlagColNum, 35)
self.twList.setColumnWidth(FileSelectionList.readonlyColNum, 35)
self.twList.setColumnWidth(FileSelectionList.typeColNum, 45)
if self.twList.columnWidth(FileSelectionList.fileColNum) > 250:
self.twList.setColumnWidth(FileSelectionList.fileColNum, 250)
if self.twList.columnWidth(FileSelectionList.folderColNum) > 200:
self.twList.setColumnWidth(FileSelectionList.folderColNum, 200)
def isListDupe(self, path):
r = 0
while r < self.twList.rowCount():
ca = self.getArchiveByRow(r)
if ca.path == path:
return True
r = r + 1
return False
def getCurrentListRow(self, path):
r = 0
while r < self.twList.rowCount():
ca = self.getArchiveByRow(r)
if ca.path == path:
return r
r = r + 1
return -1
def addPathItem(self, path):
path = str(path)
path = os.path.abspath(path)
# print "processing", path
if self.isListDupe(path):
return self.getCurrentListRow(path)
ca = ComicArchive(path, self.settings.rar_exe_path, ComicTaggerSettings.getGraphic("nocover.png"))
if ca.seemsToBeAComicArchive():
row = self.twList.rowCount()
self.twList.insertRow(row)
fi = FileInfo(ca)
filename_item = QTableWidgetItem()
folder_item = QTableWidgetItem()
cix_item = FileTableWidgetItem()
cbi_item = FileTableWidgetItem()
readonly_item = FileTableWidgetItem()
type_item = QTableWidgetItem()
filename_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
filename_item.setData(Qt.UserRole, fi)
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
folder_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.folderColNum, folder_item)
type_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
cix_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
cix_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
cbi_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
cbi_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
readonly_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
readonly_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.readonlyColNum, readonly_item)
self.updateRow(row)
return row
def updateRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(Qt.UserRole) # .toPyObject()
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
cix_item = self.twList.item(row, FileSelectionList.CRFlagColNum)
cbi_item = self.twList.item(row, FileSelectionList.CBLFlagColNum)
type_item = self.twList.item(row, FileSelectionList.typeColNum)
readonly_item = self.twList.item(row, FileSelectionList.readonlyColNum)
item_text = os.path.split(fi.ca.path)[0]
folder_item.setText(item_text)
folder_item.setData(Qt.ToolTipRole, item_text)
item_text = os.path.split(fi.ca.path)[1]
filename_item.setText(item_text)
filename_item.setData(Qt.ToolTipRole, item_text)
if fi.ca.isZip():
item_text = "ZIP"
elif fi.ca.isRar():
item_text = "RAR"
elif fi.ca.isTar():
item_text = "TAR"
else:
item_text = ""
type_item.setText(item_text)
type_item.setData(Qt.ToolTipRole, item_text)
if fi.ca.hasCIX():
cix_item.setCheckState(Qt.Checked)
cix_item.setData(Qt.UserRole, True)
else:
cix_item.setData(Qt.UserRole, False)
cix_item.setCheckState(Qt.Unchecked)
if fi.ca.hasCBI():
cbi_item.setCheckState(Qt.Checked)
cbi_item.setData(Qt.UserRole, True)
else:
cbi_item.setData(Qt.UserRole, False)
cbi_item.setCheckState(Qt.Unchecked)
if not fi.ca.isWritable():
readonly_item.setCheckState(Qt.Checked)
readonly_item.setData(Qt.UserRole, True)
else:
readonly_item.setData(Qt.UserRole, False)
readonly_item.setCheckState(Qt.Unchecked)
# Reading these will force them into the ComicArchive's cache
fi.ca.readCIX()
fi.ca.hasCBI()
def getSelectedArchiveList(self):
ca_list = []
for r in range(self.twList.rowCount()):
item = self.twList.item(r, FileSelectionList.dataColNum)
if item.isSelected():
fi = item.data(Qt.UserRole)
ca_list.append(fi.ca)
return ca_list
def updateCurrentRow(self):
self.updateRow(self.twList.currentRow())
def updateSelectedRows(self):
self.twList.setSortingEnabled(False)
for r in range(self.twList.rowCount()):
item = self.twList.item(r, FileSelectionList.dataColNum)
if item.isSelected():
self.updateRow(r)
self.twList.setSortingEnabled(True)
def currentItemChangedCB(self, curr, prev):
new_idx = curr.row()
old_idx = -1
if prev is not None:
old_idx = prev.row()
# print("old {0} new {1}".format(old_idx, new_idx))
if old_idx == new_idx:
return
# don't allow change if modified
if prev is not None and new_idx != old_idx:
if not self.modifiedFlagVerification("Change Archive", "If you change archives now, data in the form will be lost. Are you sure?"):
self.twList.currentItemChanged.disconnect(self.currentItemChangedCB)
self.twList.setCurrentItem(prev)
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
# Need to defer this revert selection, for some reason
QTimer.singleShot(1, self.revertSelection)
return
fi = self.twList.item(new_idx, FileSelectionList.dataColNum).data(Qt.UserRole) # .toPyObject()
self.selectionChanged.emit(QVariant(fi))
def revertSelection(self):
self.twList.selectRow(self.twList.currentRow())
def modifiedFlagVerification(self, title, desc):
if self.modifiedFlag:
reply = QMessageBox.question(self, self.tr(title), self.tr(desc), QMessageBox.Yes, QMessageBox.No)
if reply != QMessageBox.Yes:
return False
return True
# Attempt to use a special checkbox widget in the cell.
# Couldn't figure out how to disable it with "enabled" colors
# w = QWidget()
# cb = QCheckBox(w)
# cb.setCheckState(Qt.Checked)
# layout = QHBoxLayout()
# layout.addWidget(cb)
# layout.setAlignment(Qt.AlignHCenter)
# layout.setMargin(2)
# w.setLayout(layout)
# self.twList.setCellWidget(row, 2, w)

View File

@ -0,0 +1 @@
from comicapi.genericmetadata import *

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,180 @@
"""A class to manage fetching and caching of images by URL"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import os
import shutil
import sqlite3 as lite
import tempfile
import requests
from . import _version
from .settings import ComicTaggerSettings
try:
from PyQt5 import QtGui
from PyQt5.QtCore import QByteArray, QObject, QUrl, pyqtSignal
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
except ImportError:
# No Qt, so define a few dummy QObjects to help us compile
class QObject:
def __init__(self, *args):
pass
class QByteArray:
pass
class pyqtSignal:
def __init__(self, *args):
pass
def emit(a, b, c):
pass
class ImageFetcherException(Exception):
pass
class ImageFetcher(QObject):
fetchComplete = pyqtSignal(QByteArray, int)
def __init__(self):
QObject.__init__(self)
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
self.db_file = os.path.join(self.settings_folder, "image_url_cache.db")
self.cache_folder = os.path.join(self.settings_folder, "image_cache")
if not os.path.exists(self.db_file):
self.create_image_db()
def clearCache(self):
os.unlink(self.db_file)
if os.path.isdir(self.cache_folder):
shutil.rmtree(self.cache_folder)
def fetch(self, url, user_data=None, blocking=False):
"""
If called with blocking=True, this will block until the image is
fetched.
If called with blocking=False, this will run the fetch in the
background, and emit a signal when done
"""
self.user_data = user_data
self.fetched_url = url
# first look in the DB
image_data = self.get_image_from_cache(url)
if blocking:
if image_data is None:
try:
print(url)
image_data = requests.get(url, headers={"user-agent": "comictagger/" + _version.version}).content
except Exception as e:
print(e)
raise ImageFetcherException("Network Error!")
# save the image to the cache
self.add_image_to_cache(self.fetched_url, image_data)
return image_data
else:
# if we found it, just emit the signal asap
if image_data is not None:
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
return
# didn't find it. look online
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.finishRequest)
self.nam.get(QNetworkRequest(QUrl(url)))
# we'll get called back when done...
def finishRequest(self, reply):
# read in the image data
image_data = reply.readAll()
# save the image to the cache
self.add_image_to_cache(self.fetched_url, image_data)
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
def create_image_db(self):
# this will wipe out any existing version
open(self.db_file, "w").close()
# wipe any existing image cache folder too
if os.path.isdir(self.cache_folder):
shutil.rmtree(self.cache_folder)
os.makedirs(self.cache_folder)
con = lite.connect(self.db_file)
# create tables
with con:
cur = con.cursor()
cur.execute("CREATE TABLE Images(" + "url TEXT," + "filename TEXT," + "timestamp TEXT," + "PRIMARY KEY (url))")
def add_image_to_cache(self, url, image_data):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
timestamp = datetime.datetime.now()
tmp_fd, filename = tempfile.mkstemp(dir=self.cache_folder, prefix="img")
f = os.fdopen(tmp_fd, "w+b")
f.write(image_data)
f.close()
cur.execute("INSERT or REPLACE INTO Images VALUES(?, ?, ?)", (url, filename, timestamp))
def get_image_from_cache(self, url):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
cur.execute("SELECT filename FROM Images WHERE url=?", [url])
row = cur.fetchone()
if row is None:
return None
else:
filename = row[0]
image_data = None
try:
with open(filename, "rb") as f:
image_data = f.read()
f.close()
except IOError as e:
pass
return image_data

190
comictaggerlib/imagehasher.py Executable file
View File

@ -0,0 +1,190 @@
"""A class to manage creating image content hashes, and calculate hamming distances"""
# Copyright 2013 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import io
import sys
from functools import reduce
try:
from PIL import Image, WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False
class ImageHasher(object):
def __init__(self, path=None, data=None, width=8, height=8):
# self.hash_size = size
self.width = width
self.height = height
if path is None and data is None:
raise IOError
else:
try:
if path is not None:
self.image = Image.open(path)
else:
self.image = Image.open(io.BytesIO(data))
except Exception as e:
print("Image data seems corrupted! [{}]".format(e))
# just generate a bogus image
self.image = Image.new("L", (1, 1))
def average_hash(self):
try:
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
except Exception as e:
print("average_hash error:", e)
return int(0)
pixels = list(image.getdata())
avg = sum(pixels) / len(pixels)
def compare_value_to_avg(i):
return 1 if i > avg else 0
bitlist = list(map(compare_value_to_avg, pixels))
# build up an int value from the bit list, one bit at a time
def set_bit(x, idx_val):
(idx, val) = idx_val
return x | (val << idx)
result = reduce(set_bit, enumerate(bitlist), 0)
# print("{0:016x}".format(result))
return result
def average_hash2(self):
pass
"""
# Got this one from somewhere on the net. Not a clue how the 'convolve2d'
# works!
from numpy import array
from scipy.signal import convolve2d
im = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert('L')
in_data = array((im.getdata())).reshape(self.width, self.height)
filt = array([[0,1,0],[1,-4,1],[0,1,0]])
filt_data = convolve2d(in_data,filt,mode='same',boundary='symm').flatten()
result = reduce(lambda x, (y, z): x | (z << y),
enumerate(map(lambda i: 0 if i < 0 else 1, filt_data)),
0)
#print("{0:016x}".format(result))
return result
"""
def dct_average_hash(self):
pass
"""
# Algorithm source: http://syntaxcandy.blogspot.com/2012/08/perceptual-hash.html
1. Reduce size. Like Average Hash, pHash starts with a small image.
However, the image is larger than 8x8; 32x32 is a good size. This
is really done to simplify the DCT computation and not because it
is needed to reduce the high frequencies.
2. Reduce color. The image is reduced to a grayscale just to further
simplify the number of computations.
3. Compute the DCT. The DCT separates the image into a collection of
frequencies and scalars. While JPEG uses an 8x8 DCT, this algorithm
uses a 32x32 DCT.
4. Reduce the DCT. This is the magic step. While the DCT is 32x32,
just keep the top-left 8x8. Those represent the lowest frequencies in
the picture.
5. Compute the average value. Like the Average Hash, compute the mean DCT
value (using only the 8x8 DCT low-frequency values and excluding the first
term since the DC coefficient can be significantly different from the other
values and will throw off the average). Thanks to David Starkweather for the
added information about pHash. He wrote: "the dct hash is based on the low 2D
DCT coefficients starting at the second from lowest, leaving out the first DC
term. This excludes completely flat image information (i.e. solid colors) from
being included in the hash description."
6. Further reduce the DCT. This is the magic step. Set the 64 hash bits to 0 or
1 depending on whether each of the 64 DCT values is above or below the average
value. The result doesn't tell us the actual low frequencies; it just tells us
the very-rough relative scale of the frequencies to the mean. The result will not
vary as long as the overall structure of the image remains the same; this can
survive gamma and color histogram adjustments without a problem.
7. Construct the hash. Set the 64 bits into a 64-bit integer. The order does not
matter, just as long as you are consistent.
"""
"""
import numpy
import scipy.fftpack
numpy.set_printoptions(threshold=10000, linewidth=200, precision=2, suppress=True)
# Step 1,2
im = self.image.resize((32, 32), Image.ANTIALIAS).convert("L")
in_data = numpy.asarray(im)
# Step 3
dct = scipy.fftpack.dct(in_data.astype(float))
# Step 4
# Just skip the top and left rows when slicing, as suggested somewhere else...
lofreq_dct = dct[1:9, 1:9].flatten()
# Step 5
avg = (lofreq_dct.sum()) / (lofreq_dct.size)
median = numpy.median(lofreq_dct)
thresh = avg
# Step 6
def compare_value_to_thresh(i):
return (1 if i > thresh else 0)
bitlist = map(compare_value_to_thresh, lofreq_dct)
#Step 7
def set_bit(x, (idx, val)):
return (x | (val << idx))
result = reduce(set_bit, enumerate(bitlist), long(0))
#print("{0:016x}".format(result))
return result
"""
# accepts 2 hashes (longs or hex strings) and returns the hamming distance
@staticmethod
def hamming_distance(h1, h2):
if isinstance(h1, int) or isinstance(h1, int):
n1 = h1
n2 = h2
else:
# convert hex strings to ints
n1 = int(h1, 16)
n2 = int(h2, 16)
# xor the two numbers
n = n1 ^ n2
# count up the 1's in the binary string
return sum(b == "1" for b in bin(n)[2:])

View File

@ -0,0 +1,80 @@
"""A PyQT4 widget to display a popup image"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from .settings import ComicTaggerSettings
class ImagePopup(QtWidgets.QDialog):
def __init__(self, parent, image_pixmap):
super(ImagePopup, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("imagepopup.ui"), self)
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
# self.setWindowModality(QtCore.Qt.WindowModal)
self.setWindowFlags(QtCore.Qt.Popup)
self.setWindowState(QtCore.Qt.WindowFullScreen)
self.imagePixmap = image_pixmap
screen_size = QtWidgets.QDesktopWidget().screenGeometry()
self.resize(screen_size.width(), screen_size.height())
self.move(0, 0)
# This is a total hack. Uses a snapshot of the desktop, and overlays a
# translucent screen over it. Probably can do it better by setting opacity of a
# widget
screen = QtWidgets.QApplication.primaryScreen()
self.desktopBg = screen.grabWindow(QtWidgets.QApplication.desktop().winId(), 0, 0, screen_size.width(), screen_size.height())
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic("popup_bg.png"))
self.clientBgPixmap = bg.scaled(screen_size.width(), screen_size.height(), QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
self.setMask(self.clientBgPixmap.mask())
self.applyImagePixmap()
self.showFullScreen()
self.raise_()
QtWidgets.QApplication.restoreOverrideCursor()
def paintEvent(self, event):
self.painter = QtGui.QPainter(self)
self.painter.setRenderHint(QtGui.QPainter.Antialiasing)
self.painter.drawPixmap(0, 0, self.desktopBg)
self.painter.drawPixmap(0, 0, self.clientBgPixmap)
self.painter.end()
def applyImagePixmap(self):
win_h = self.height()
win_w = self.width()
if self.imagePixmap.width() > win_w or self.imagePixmap.height() > win_h:
# scale the pixmap to fit in the frame
display_pixmap = self.imagePixmap.scaled(win_w, win_h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
self.lblImage.setPixmap(display_pixmap)
else:
display_pixmap = self.imagePixmap
self.lblImage.setPixmap(display_pixmap)
# move and resize the label to be centered in the fame
img_w = display_pixmap.width()
img_h = display_pixmap.height()
self.lblImage.resize(img_w, img_h)
self.lblImage.move((win_w - img_w) / 2, (win_h - img_h) / 2)
def mousePressEvent(self, event):
self.close()

View File

@ -0,0 +1,645 @@
"""A class to automatically identify a comic archive"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import io
import sys
from . import utils
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .genericmetadata import GenericMetadata
from .imagefetcher import ImageFetcher, ImageFetcherException
from .imagehasher import ImageHasher
from .issuestring import IssueString
try:
from PIL import Image, WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False
class IssueIdentifierNetworkError(Exception):
pass
class IssueIdentifierCancelled(Exception):
pass
class IssueIdentifier:
ResultNoMatches = 0
ResultFoundMatchButBadCoverScore = 1
ResultFoundMatchButNotFirstPage = 2
ResultMultipleMatchesWithBadImageScores = 3
ResultOneGoodMatch = 4
ResultMultipleGoodMatches = 5
def __init__(self, comic_archive, settings):
self.comic_archive = comic_archive
self.image_hasher = 1
self.onlyUseAdditionalMetaData = False
# a decent hamming score, good enough to call it a match
self.min_score_thresh = 16
# for alternate covers, be more stringent, since we're a bit more
# scattershot in comparisons
self.min_alternate_score_thresh = 12
# the min distance a hamming score must be to separate itself from
# closest neighbor
self.min_score_distance = 4
# a very strong hamming score, almost certainly the same image
self.strong_score_thresh = 8
# used to eliminate series names that are too long based on our search
# string
self.length_delta_thresh = settings.id_length_delta_thresh
# used to eliminate unlikely publishers
self.publisher_blacklist = [s.strip().lower() for s in settings.id_publisher_blacklist.split(",")]
self.additional_metadata = GenericMetadata()
self.output_function = IssueIdentifier.defaultWriteOutput
self.callback = None
self.coverUrlCallback = None
self.search_result = self.ResultNoMatches
self.cover_page_index = 0
self.cancel = False
self.waitAndRetryOnRateLimit = False
def setScoreMinThreshold(self, thresh):
self.min_score_thresh = thresh
def setScoreMinDistance(self, distance):
self.min_score_distance = distance
def setAdditionalMetadata(self, md):
self.additional_metadata = md
def setNameLengthDeltaThreshold(self, delta):
self.length_delta_thresh = delta
def setPublisherBlackList(self, blacklist):
self.publisher_blacklist = blacklist
def setHasherAlgorithm(self, algo):
self.image_hasher = algo
pass
def setOutputFunction(self, func):
self.output_function = func
pass
def calculateHash(self, image_data):
if self.image_hasher == "3":
return ImageHasher(data=image_data).dct_average_hash()
elif self.image_hasher == "2":
return ImageHasher(data=image_data).average_hash2()
else:
return ImageHasher(data=image_data).average_hash()
def getAspectRatio(self, image_data):
try:
im = Image.open(io.StringIO(image_data))
w, h = im.size
return float(h) / float(w)
except:
return 1.5
def cropCover(self, image_data):
im = Image.open(io.StringIO(image_data))
w, h = im.size
try:
cropped_im = im.crop((int(w / 2), 0, w, h))
except Exception as e:
print("cropCover() error:", e)
return None
output = io.StringIO()
cropped_im.save(output, format="PNG")
cropped_image_data = output.getvalue()
output.close()
return cropped_image_data
def setProgressCallback(self, cb_func):
self.callback = cb_func
def setCoverURLCallback(self, cb_func):
self.coverUrlCallback = cb_func
def getSearchKeys(self):
ca = self.comic_archive
search_keys = dict()
search_keys["series"] = None
search_keys["issue_number"] = None
search_keys["month"] = None
search_keys["year"] = None
search_keys["issue_count"] = None
if ca is None:
return
if self.onlyUseAdditionalMetaData:
search_keys["series"] = self.additional_metadata.series
search_keys["issue_number"] = self.additional_metadata.issue
search_keys["year"] = self.additional_metadata.year
search_keys["month"] = self.additional_metadata.month
search_keys["issue_count"] = self.additional_metadata.issueCount
return search_keys
# see if the archive has any useful meta data for searching with
if ca.hasCIX():
internal_metadata = ca.readCIX()
elif ca.hasCBI():
internal_metadata = ca.readCBI()
else:
internal_metadata = ca.readCBI()
# try to get some metadata from filename
md_from_filename = ca.metadataFromFilename()
# preference order:
# 1. Additional metadata
# 1. Internal metadata
# 1. Filename metadata
if self.additional_metadata.series is not None:
search_keys["series"] = self.additional_metadata.series
elif internal_metadata.series is not None:
search_keys["series"] = internal_metadata.series
else:
search_keys["series"] = md_from_filename.series
if self.additional_metadata.issue is not None:
search_keys["issue_number"] = self.additional_metadata.issue
elif internal_metadata.issue is not None:
search_keys["issue_number"] = internal_metadata.issue
else:
search_keys["issue_number"] = md_from_filename.issue
if self.additional_metadata.year is not None:
search_keys["year"] = self.additional_metadata.year
elif internal_metadata.year is not None:
search_keys["year"] = internal_metadata.year
else:
search_keys["year"] = md_from_filename.year
if self.additional_metadata.month is not None:
search_keys["month"] = self.additional_metadata.month
elif internal_metadata.month is not None:
search_keys["month"] = internal_metadata.month
else:
search_keys["month"] = md_from_filename.month
if self.additional_metadata.issueCount is not None:
search_keys["issue_count"] = self.additional_metadata.issueCount
elif internal_metadata.issueCount is not None:
search_keys["issue_count"] = internal_metadata.issueCount
else:
search_keys["issue_count"] = md_from_filename.issueCount
return search_keys
@staticmethod
def defaultWriteOutput(text):
sys.stdout.write(text)
sys.stdout.flush()
def log_msg(self, msg, newline=True):
self.output_function(msg)
if newline:
self.output_function("\n")
def getIssueCoverMatchScore(
self, comicVine, issue_id, primary_img_url, primary_thumb_url, page_url, localCoverHashList, useRemoteAlternates=False, useLog=True
):
# localHashes is a list of pre-calculated hashs.
# useRemoteAlternates - indicates to use alternate covers from CV
try:
url_image_data = ImageFetcher().fetch(primary_thumb_url, blocking=True)
except ImageFetcherException:
self.log_msg("Network issue while fetching cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError
if self.cancel:
raise IssueIdentifierCancelled
# alert the GUI, if needed
if self.coverUrlCallback is not None:
self.coverUrlCallback(url_image_data)
remote_cover_list = []
item = dict()
item["url"] = primary_img_url
item["hash"] = self.calculateHash(url_image_data)
remote_cover_list.append(item)
if self.cancel:
raise IssueIdentifierCancelled
if useRemoteAlternates:
alt_img_url_list = comicVine.fetchAlternateCoverURLs(issue_id, page_url)
for alt_url in alt_img_url_list:
try:
alt_url_image_data = ImageFetcher().fetch(alt_url, blocking=True)
except ImageFetcherException:
self.log_msg("Network issue while fetching alt. cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError
if self.cancel:
raise IssueIdentifierCancelled
# alert the GUI, if needed
if self.coverUrlCallback is not None:
self.coverUrlCallback(alt_url_image_data)
item = dict()
item["url"] = alt_url
item["hash"] = self.calculateHash(alt_url_image_data)
remote_cover_list.append(item)
if self.cancel:
raise IssueIdentifierCancelled
if useLog and useRemoteAlternates:
self.log_msg("[{0} alt. covers]".format(len(remote_cover_list) - 1), False)
if useLog:
self.log_msg("[ ", False)
score_list = []
done = False
for local_cover_hash in localCoverHashList:
for remote_cover_item in remote_cover_list:
score = ImageHasher.hamming_distance(local_cover_hash, remote_cover_item["hash"])
score_item = dict()
score_item["score"] = score
score_item["url"] = remote_cover_item["url"]
score_item["hash"] = remote_cover_item["hash"]
score_list.append(score_item)
if useLog:
self.log_msg("{0}".format(score), False)
if score <= self.strong_score_thresh:
# such a good score, we can quit now, since for sure we
# have a winner
done = True
break
if done:
break
if useLog:
self.log_msg(" ]", False)
best_score_item = min(score_list, key=lambda x: x["score"])
return best_score_item
# def validate(self, issue_id):
# create hash list
# score = self.getIssueMatchScore(issue_id, hash_list, useRemoteAlternates = True)
# if score < 20:
# return True
# else:
# return False
def search(self):
ca = self.comic_archive
self.match_list = []
self.cancel = False
self.search_result = self.ResultNoMatches
if not pil_available:
self.log_msg("Python Imaging Library (PIL) is not available and is needed for issue identification.")
return self.match_list
if not ca.seemsToBeAComicArchive():
self.log_msg("Sorry, but " + opts.filename + " is not a comic archive!")
return self.match_list
cover_image_data = ca.getPage(self.cover_page_index)
cover_hash = self.calculateHash(cover_image_data)
# check the aspect ratio
# if it's wider than it is high, it's probably a two page spread
# if so, crop it and calculate a second hash
narrow_cover_hash = None
aspect_ratio = self.getAspectRatio(cover_image_data)
if aspect_ratio < 1.0:
right_side_image_data = self.cropCover(cover_image_data)
if right_side_image_data is not None:
narrow_cover_hash = self.calculateHash(right_side_image_data)
# self.log_msg("Cover hash = {0:016x}".format(cover_hash))
keys = self.getSearchKeys()
# normalize the issue number
keys["issue_number"] = IssueString(keys["issue_number"]).asString()
# we need, at minimum, a series and issue number
if keys["series"] is None or keys["issue_number"] is None:
self.log_msg("Not enough info for a search!")
return []
self.log_msg("Going to search for:")
self.log_msg("\tSeries: " + keys["series"])
self.log_msg("\tIssue: " + keys["issue_number"])
if keys["issue_count"] is not None:
self.log_msg("\tCount: " + str(keys["issue_count"]))
if keys["year"] is not None:
self.log_msg("\tYear: " + str(keys["year"]))
if keys["month"] is not None:
self.log_msg("\tMonth: " + str(keys["month"]))
# self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = self.waitAndRetryOnRateLimit
comicVine.setLogFunc(self.output_function)
# self.log_msg(("Searching for " + keys['series'] + "...")
self.log_msg("Searching for {0} #{1} ...".format(keys["series"], keys["issue_number"]))
try:
cv_search_results = comicVine.searchForSeries(keys["series"])
except ComicVineTalkerException:
self.log_msg("Network issue while searching for series. Aborting...")
return []
# self.log_msg("Found " + str(len(cv_search_results)) + " initial results")
if self.cancel:
return []
if cv_search_results is None:
return []
series_second_round_list = []
# self.log_msg("Removing results with too long names, banned publishers, or future start dates")
for item in cv_search_results:
length_approved = False
publisher_approved = True
date_approved = True
# remove any series that starts after the issue year
if keys["year"] is not None and str(keys["year"]).isdigit() and item["start_year"] is not None and str(item["start_year"]).isdigit():
if int(keys["year"]) < int(item["start_year"]):
date_approved = False
# assume that our search name is close to the actual name, say
# within ,e.g. 5 chars
shortened_key = utils.removearticles(keys["series"])
shortened_item_name = utils.removearticles(item["name"])
if len(shortened_item_name) < (len(shortened_key) + self.length_delta_thresh):
length_approved = True
# remove any series from publishers on the blacklist
if item["publisher"] is not None:
publisher = item["publisher"]["name"]
if publisher is not None and publisher.lower() in self.publisher_blacklist:
publisher_approved = False
if length_approved and publisher_approved and date_approved:
series_second_round_list.append(item)
self.log_msg("Searching in " + str(len(series_second_round_list)) + " series")
if self.callback is not None:
self.callback(0, len(series_second_round_list))
# now sort the list by name length
series_second_round_list.sort(key=lambda x: len(x["name"]), reverse=False)
# build a list of volume IDs
volume_id_list = list()
for series in series_second_round_list:
volume_id_list.append(series["id"])
try:
issue_list = comicVine.fetchIssuesByVolumeIssueNumAndYear(volume_id_list, keys["issue_number"], keys["year"])
except ComicVineTalkerException:
self.log_msg("Network issue while searching for series details. Aborting...")
return []
if issue_list is None:
return []
shortlist = list()
# now re-associate the issues and volumes
for issue in issue_list:
for series in series_second_round_list:
if series["id"] == issue["volume"]["id"]:
shortlist.append((series, issue))
break
if keys["year"] is None:
self.log_msg("Found {0} series that have an issue #{1}".format(len(shortlist), keys["issue_number"]))
else:
self.log_msg("Found {0} series that have an issue #{1} from {2}".format(len(shortlist), keys["issue_number"], keys["year"]))
# now we have a shortlist of volumes with the desired issue number
# Do first round of cover matching
counter = len(shortlist)
for series, issue in shortlist:
if self.callback is not None:
self.callback(counter, len(shortlist) * 3)
counter += 1
self.log_msg("Examining covers for ID: {0} {1} ({2}) ...".format(series["id"], series["name"], series["start_year"]), newline=False)
# parse out the cover date
day, month, year = comicVine.parseDateStr(issue["cover_date"])
# Now check the cover match against the primary image
hash_list = [cover_hash]
if narrow_cover_hash is not None:
hash_list.append(narrow_cover_hash)
try:
image_url = issue["image"]["super_url"]
thumb_url = issue["image"]["thumb_url"]
page_url = issue["site_detail_url"]
score_item = self.getIssueCoverMatchScore(
comicVine, issue["id"], image_url, thumb_url, page_url, hash_list, useRemoteAlternates=False
)
except:
self.match_list = []
return self.match_list
match = dict()
match["series"] = "{0} ({1})".format(series["name"], series["start_year"])
match["distance"] = score_item["score"]
match["issue_number"] = keys["issue_number"]
match["cv_issue_count"] = series["count_of_issues"]
match["url_image_hash"] = score_item["hash"]
match["issue_title"] = issue["name"]
match["issue_id"] = issue["id"]
match["volume_id"] = series["id"]
match["month"] = month
match["year"] = year
match["publisher"] = None
if series["publisher"] is not None:
match["publisher"] = series["publisher"]["name"]
match["image_url"] = image_url
match["thumb_url"] = thumb_url
match["page_url"] = page_url
match["description"] = issue["description"]
self.match_list.append(match)
self.log_msg(" --> {0}".format(match["distance"]), newline=False)
self.log_msg("")
if len(self.match_list) == 0:
self.log_msg(":-(no matches!")
self.search_result = self.ResultNoMatches
return self.match_list
# sort list by image match scores
self.match_list.sort(key=lambda k: k["distance"])
l = []
for i in self.match_list:
l.append(i["distance"])
self.log_msg("Compared to covers in {0} issue(s):".format(len(self.match_list)), newline=False)
self.log_msg(str(l))
def print_match(item):
self.log_msg(
"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
item["series"], item["issue_number"], item["issue_title"], item["month"], item["year"], item["distance"]
)
)
best_score = self.match_list[0]["distance"]
if best_score >= self.min_score_thresh:
# we have 1 or more low-confidence matches (all bad cover scores)
# look at a few more pages in the archive, and also alternate
# covers online
self.log_msg("Very weak scores for the cover. Analyzing alternate pages and covers...")
hash_list = [cover_hash]
if narrow_cover_hash is not None:
hash_list.append(narrow_cover_hash)
for i in range(1, min(3, ca.getNumberOfPages())):
image_data = ca.getPage(i)
page_hash = self.calculateHash(image_data)
hash_list.append(page_hash)
second_match_list = []
counter = 2 * len(self.match_list)
for m in self.match_list:
if self.callback is not None:
self.callback(counter, len(self.match_list) * 3)
counter += 1
self.log_msg("Examining alternate covers for ID: {0} {1} ...".format(m["volume_id"], m["series"]), newline=False)
try:
score_item = self.getIssueCoverMatchScore(
comicVine, m["issue_id"], m["image_url"], m["thumb_url"], m["page_url"], hash_list, useRemoteAlternates=True
)
except:
self.match_list = []
return self.match_list
self.log_msg("--->{0}".format(score_item["score"]))
self.log_msg("")
if score_item["score"] < self.min_alternate_score_thresh:
second_match_list.append(m)
m["distance"] = score_item["score"]
if len(second_match_list) == 0:
if len(self.match_list) == 1:
self.log_msg("No matching pages in the issue.")
self.log_msg("--------------------------------------------------------------------------")
print_match(self.match_list[0])
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.ResultFoundMatchButBadCoverScore
else:
self.log_msg("--------------------------------------------------------------------------")
self.log_msg("Multiple bad cover matches! Need to use other info...")
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.ResultMultipleMatchesWithBadImageScores
return self.match_list
else:
# We did good, found something!
self.log_msg("Success in secondary/alternate cover matching!")
self.match_list = second_match_list
# sort new list by image match scores
self.match_list.sort(key=lambda k: k["distance"])
best_score = self.match_list[0]["distance"]
self.log_msg("[Second round cover matching: best score = {0}]".format(best_score))
# now drop down into the rest of the processing
if self.callback is not None:
self.callback(99, 100)
# now pare down list, remove any item more than specified distant from
# the top scores
for item in reversed(self.match_list):
if item["distance"] > best_score + self.min_score_distance:
self.match_list.remove(item)
# One more test for the case choosing limited series first issue vs a trade with the same cover:
# if we have a given issue count > 1 and the volume from CV has
# count==1, remove it from match list
if len(self.match_list) >= 2 and keys["issue_count"] is not None and keys["issue_count"] != 1:
new_list = list()
for match in self.match_list:
if match["cv_issue_count"] != 1:
new_list.append(match)
else:
self.log_msg("Removing volume {0} [{1}] from consideration (only 1 issue)".format(match["series"], match["volume_id"]))
if len(new_list) > 0:
self.match_list = new_list
if len(self.match_list) == 1:
self.log_msg("--------------------------------------------------------------------------")
print_match(self.match_list[0])
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.ResultOneGoodMatch
elif len(self.match_list) == 0:
self.log_msg("--------------------------------------------------------------------------")
self.log_msg("No matches found :(")
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.ResultNoMatches
else:
# we've got multiple good matches:
self.log_msg("More than one likely candidate.")
self.search_result = self.ResultMultipleGoodMatches
self.log_msg("--------------------------------------------------------------------------")
for item in self.match_list:
print_match(item)
self.log_msg("--------------------------------------------------------------------------")
return self.match_list

View File

@ -0,0 +1,167 @@
"""A PyQT4 dialog to select specific issue from list"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .coverimagewidget import CoverImageWidget
from .issuestring import IssueString
from .settings import ComicTaggerSettings
class IssueNumberTableWidgetItem(QtWidgets.QTableWidgetItem):
def __lt__(self, other):
selfStr = self.data(QtCore.Qt.DisplayRole)
otherStr = other.data(QtCore.Qt.DisplayRole)
return IssueString(selfStr).asFloat() < IssueString(otherStr).asFloat()
class IssueSelectionWindow(QtWidgets.QDialog):
volume_id = 0
def __init__(self, parent, settings, series_id, issue_number):
super(IssueSelectionWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("issueselectionwindow.ui"), self)
self.coverWidget = CoverImageWidget(self.coverImageContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
gridlayout.addWidget(self.coverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.series_id = series_id
self.settings = settings
self.url_fetch_thread = None
if issue_number is None or issue_number == "":
self.issue_number = 1
else:
self.issue_number = issue_number
self.initial_id = None
self.performQuery()
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
# now that the list has been sorted, find the initial record, and
# select it
if self.initial_id is None:
self.twList.selectRow(0)
else:
for r in range(0, self.twList.rowCount()):
issue_id = self.twList.item(r, 0).data(QtCore.Qt.UserRole)
if issue_id == self.initial_id:
self.twList.selectRow(r)
break
def performQuery(self):
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
try:
comicVine = ComicVineTalker()
volume_data = comicVine.fetchVolumeData(self.series_id)
self.issue_list = comicVine.fetchIssuesByVolume(self.series_id)
except ComicVineTalkerException as e:
QtWidgets.QApplication.restoreOverrideCursor()
if e.code == ComicVineTalkerException.RateLimit:
QtWidgets.QMessageBox.critical(self, self.tr("Comic Vine Error"), ComicVineTalker.getRateLimitMessage())
else:
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to Comic Vine to list issues!"))
return
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
self.twList.setSortingEnabled(False)
row = 0
for record in self.issue_list:
self.twList.insertRow(row)
item_text = record["issue_number"]
item = IssueNumberTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, record["id"])
item.setData(QtCore.Qt.DisplayRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = record["cover_date"]
if item_text is None:
item_text = ""
# remove the day of "YYYY-MM-DD"
parts = item_text.split("-")
if len(parts) > 1:
item_text = parts[0] + "-" + parts[1]
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
item_text = record["name"]
if item_text is None:
item_text = ""
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
if IssueString(record["issue_number"]).asString().lower() == IssueString(self.issue_number).asString().lower():
self.initial_id = record["id"]
row += 1
self.twList.setSortingEnabled(True)
self.twList.sortItems(0, QtCore.Qt.AscendingOrder)
QtWidgets.QApplication.restoreOverrideCursor()
def cellDoubleClicked(self, r, c):
self.accept()
def currentItemChanged(self, curr, prev):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.issue_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
# list selection was changed, update the the issue cover
for record in self.issue_list:
if record["id"] == self.issue_id:
self.issue_number = record["issue_number"]
self.coverWidget.setIssueID(int(self.issue_id))
if record["description"] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(record["description"])
break

View File

@ -0,0 +1 @@
from comicapi.issuestring import *

View File

@ -0,0 +1,35 @@
"""A PyQT4 dialog to a text file or log"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from .settings import ComicTaggerSettings
class LogWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(LogWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("logwindow.ui"), self)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
def setText(self, text):
try:
text = text.decode()
except:
pass
self.textEdit.setPlainText(text)

103
comictaggerlib/main.py Executable file
View File

@ -0,0 +1,103 @@
"""A python app to (automatically) tag comic archives"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import platform
import signal
import sys
import traceback
from . import cli, utils
from .comicvinetalker import ComicVineTalker
from .options import Options
from .settings import ComicTaggerSettings
# Need to load setting before anything else
SETTINGS = ComicTaggerSettings()
try:
qt_available = True
from PyQt5 import QtCore, QtGui, QtWidgets
from .taggerwindow import TaggerWindow
except ImportError as e:
qt_available = False
def ctmain():
opts = Options()
opts.parseCmdLineArgs()
# manage the CV API key
if opts.cv_api_key:
if opts.cv_api_key != SETTINGS.cv_api_key:
SETTINGS.cv_api_key = opts.cv_api_key
SETTINGS.save()
if opts.only_set_key:
print("Key set")
return
ComicVineTalker.api_key = SETTINGS.cv_api_key
signal.signal(signal.SIGINT, signal.SIG_DFL)
if not qt_available and not opts.no_gui:
opts.no_gui = True
print("PyQt5 is not available. ComicTagger is limited to command-line mode.", file=sys.stderr)
if opts.no_gui:
cli.cli_mode(opts, SETTINGS)
else:
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
# if platform.system() == "Darwin":
# QtWidgets.QApplication.setStyle("macintosh")
# else:
# QtWidgets.QApplication.setStyle("Fusion")
app = QtWidgets.QApplication(sys.argv)
if platform.system() == "Darwin":
# Set the MacOS dock icon
app.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.getGraphic("app.png")))
if platform.system() == "Windows":
# For pure python, tell windows that we're not python,
# so we can have our own taskbar icon
import ctypes
myappid = u"comictagger" # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
if platform.system() != "Linux":
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic("tags.png"))
splash = QtWidgets.QSplashScreen(img)
splash.show()
splash.raise_()
app.processEvents()
try:
tagger_window = TaggerWindow(opts.file_list, SETTINGS, opts=opts)
tagger_window.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.getGraphic("app.png")))
tagger_window.show()
if platform.system() != "Linux":
splash.finish(tagger_window)
sys.exit(app.exec_())
except Exception as e:
QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc())

View File

@ -0,0 +1,148 @@
"""A PyQT4 dialog to select from automated issue matches"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
from .coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
class MatchSelectionWindow(QtWidgets.QDialog):
volume_id = 0
def __init__(self, parent, matches, comic_archive):
super(MatchSelectionWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("matchselectionwindow.ui"), self)
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
gridlayout.addWidget(self.altCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.matches = matches
self.comic_archive = comic_archive
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
self.updateData()
def updateData(self):
self.setCoverImage()
self.populateTable()
self.twList.resizeColumnsToContents()
self.twList.selectRow(0)
path = self.comic_archive.path
self.setWindowTitle("Select correct match: {0}".format(os.path.split(path)[1]))
def populateTable(self):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
self.twList.setSortingEnabled(False)
row = 0
for match in self.matches:
self.twList.insertRow(row)
item_text = match["series"]
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, (match,))
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
if match["publisher"] is not None:
item_text = "{0}".format(match["publisher"])
else:
item_text = "Unknown"
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
month_str = ""
year_str = "????"
if match["month"] is not None:
month_str = "-{0:02d}".format(int(match["month"]))
if match["year"] is not None:
year_str = "{0}".format(match["year"])
item_text = year_str + month_str
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
item_text = match["issue_title"]
if item_text is None:
item_text = ""
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
row += 1
self.twList.resizeColumnsToContents()
self.twList.setSortingEnabled(True)
self.twList.sortItems(2, QtCore.Qt.AscendingOrder)
self.twList.selectRow(0)
self.twList.resizeColumnsToContents()
self.twList.horizontalHeader().setStretchLastSection(True)
def cellDoubleClicked(self, r, c):
self.accept()
def currentItemChanged(self, curr, prev):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.altCoverWidget.setIssueID(self.currentMatch()["issue_id"])
if self.currentMatch()["description"] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(self.currentMatch()["description"])
def setCoverImage(self):
self.archiveCoverWidget.setArchive(self.comic_archive)
def currentMatch(self):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data(QtCore.Qt.UserRole)[0]
return match

View File

@ -0,0 +1,97 @@
"""A PyQt5 dialog to show a message and let the user check a box
Example usage:
checked = OptionalMessageDialog.msg(self, "Disclaimer",
"This is beta software, and you are using it at your own risk!",
)
said_yes, checked = OptionalMessageDialog.question(self, "Question",
"Are you sure you wish to do this?",
)
"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
StyleMessage = 0
StyleQuestion = 1
class OptionalMessageDialog(QDialog):
def __init__(self, parent, style, title, msg, check_state=Qt.Unchecked, check_text=None):
QDialog.__init__(self, parent)
self.setWindowTitle(title)
self.was_accepted = False
l = QVBoxLayout(self)
self.theLabel = QLabel(msg)
self.theLabel.setWordWrap(True)
self.theLabel.setTextFormat(Qt.RichText)
self.theLabel.setOpenExternalLinks(True)
self.theLabel.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
l.addWidget(self.theLabel)
l.insertSpacing(-1, 10)
if check_text is None:
if style == StyleQuestion:
check_text = "Remember this answer"
else:
check_text = "Don't show this message again"
self.theCheckBox = QCheckBox(check_text)
self.theCheckBox.setCheckState(check_state)
l.addWidget(self.theCheckBox)
btnbox_style = QDialogButtonBox.Ok
if style == StyleQuestion:
btnbox_style = QDialogButtonBox.Yes | QDialogButtonBox.No
self.theButtonBox = QDialogButtonBox(btnbox_style, parent=self, accepted=self.accept, rejected=self.reject)
l.addWidget(self.theButtonBox)
def accept(self):
self.was_accepted = True
QDialog.accept(self)
def reject(self):
self.was_accepted = False
QDialog.reject(self)
@staticmethod
def msg(parent, title, msg, check_state=Qt.Unchecked, check_text=None):
d = OptionalMessageDialog(parent, StyleMessage, title, msg, check_state=check_state, check_text=check_text)
d.exec_()
return d.theCheckBox.isChecked()
@staticmethod
def question(parent, title, msg, check_state=Qt.Unchecked, check_text=None):
d = OptionalMessageDialog(parent, StyleQuestion, title, msg, check_state=check_state, check_text=check_text)
d.exec_()
return d.was_accepted, d.theCheckBox.isChecked()

437
comictaggerlib/options.py Normal file
View File

@ -0,0 +1,437 @@
"""CLI options class for ComicTagger app"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import getopt
import os
import platform
import sys
import traceback
from . import _version, utils
from .comicarchive import MetaDataStyle
from .genericmetadata import GenericMetadata
from .versionchecker import VersionChecker
try:
import argparse
except ImportError:
pass
class Options:
help_text = """Usage: {0} [option] ... [file [files ...]]
A utility for reading and writing metadata to comic archives.
If no options are given, {0} will run in windowed mode.
-p, --print Print out tag info from file. Specify type
(via -t) to get only info of that tag type.
--raw With -p, will print out the raw tag block(s)
from the file.
-d, --delete Deletes the tag block of specified type (via
-t).
-c, --copy=SOURCE Copy the specified source tag block to
destination style specified via -t
(potentially lossy operation).
-s, --save Save out tags as specified type (via -t).
Must specify also at least -o, -p, or -m.
--nooverwrite Don't modify tag block if it already exists
(relevant for -s or -c).
-1, --assume-issue-one Assume issue number is 1 if not found
(relevant for -s).
-n, --dryrun Don't actually modify file (only relevant for
-d, -s, or -r).
-t, --type=TYPE Specify TYPE as either "CR", "CBL", or
"COMET" (as either ComicRack, ComicBookLover,
or CoMet style tags, respectively).
-f, --parsefilename Parse the filename to get some info,
specifically series name, issue number,
volume, and publication year.
-i, --interactive Interactively query the user when there are
multiple matches for an online search.
--nosummary Suppress the default summary after a save
operation.
-o, --online Search online and attempt to identify file
using existing metadata and images in archive.
May be used in conjunction with -f and -m.
--id=ID Use the issue ID when searching online.
Overrides all other metadata.
-m, --metadata=LIST Explicitly define, as a list, some tags to be
used. e.g.:
"series=Plastic Man, publisher=Quality Comics"
"series=Kickers^, Inc., issue=1, year=1986"
Name-Value pairs are comma separated. Use a
"^" to escape an "=" or a ",", as shown in
the example above. Some names that can be
used: series, issue, issueCount, year,
publisher, title
-r, --rename Rename the file based on specified tag style.
--noabort Don't abort save operation when online match
is of low confidence.
-e, --export-to-zip Export RAR archive to Zip format.
--delete-rar Delete original RAR archive after successful
export to Zip.
--abort-on-conflict Don't export to zip if intended new filename
exists (otherwise, creates a new unique
filename).
-S, --script=FILE Run an "add-on" python script that uses the
ComicTagger library for custom processing.
Script arguments can follow the script name.
-R, --recursive Recursively include files in sub-folders.
--cv-api-key=KEY Use the given Comic Vine API Key (persisted
in settings).
--only-set-cv-key Only set the Comic Vine API key and quit.
-w, --wait-on-cv-rate-limit When encountering a Comic Vine rate limit
error, wait and retry query.
-v, --verbose Be noisy when doing what it does.
--terse Don't say much (for print mode).
--version Display version.
-h, --help Display this message.
For more help visit the wiki at: http://code.google.com/p/comictagger/
"""
def __init__(self):
self.data_style = None
self.no_gui = False
self.filename = None
self.verbose = False
self.terse = False
self.auto_imprint = False
self.metadata = None
self.print_tags = False
self.copy_tags = False
self.delete_tags = False
self.export_to_zip = False
self.abort_export_on_conflict = False
self.delete_rar_after_export = False
self.search_online = False
self.dryrun = False
self.abortOnLowConfidence = True
self.save_tags = False
self.parse_filename = False
self.show_save_summary = True
self.raw = False
self.cv_api_key = None
self.only_set_key = False
self.rename_file = False
self.no_overwrite = False
self.interactive = False
self.issue_id = None
self.recursive = False
self.run_script = False
self.script = None
self.wait_and_retry_on_rate_limit = False
self.assume_issue_is_one_if_not_set = False
self.file_list = []
def display_msg_and_quit(self, msg, code, show_help=False):
appname = os.path.basename(sys.argv[0])
if msg is not None:
print(msg)
if show_help:
print((self.help_text.format(appname)))
else:
print("For more help, run with '--help'")
sys.exit(code)
def parseMetadataFromString(self, mdstr):
"""The metadata string is a comma separated list of name-value pairs
The names match the attributes of the internal metadata struct (for now)
The caret is the special "escape character", since it's not common in
natural language text
example = "series=Kickers^, Inc. ,issue=1, year=1986"
"""
escaped_comma = "^,"
escaped_equals = "^="
replacement_token = "<_~_>"
md = GenericMetadata()
# First, replace escaped commas with with a unique token (to be changed
# back later)
mdstr = mdstr.replace(escaped_comma, replacement_token)
tmp_list = mdstr.split(",")
md_list = []
for item in tmp_list:
item = item.replace(replacement_token, ",")
md_list.append(item)
# Now build a nice dict from the list
md_dict = dict()
for item in md_list:
# Make sure to fix any escaped equal signs
i = item.replace(escaped_equals, replacement_token)
key, value = i.split("=")
value = value.replace(replacement_token, "=").strip()
key = key.strip()
if key.lower() == "credit":
cred_attribs = value.split(":")
role = cred_attribs[0]
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
primary = cred_attribs[2] if len(cred_attribs) > 2 else None
md.addCredit(person.strip(), role.strip(), True if primary is not None else False)
else:
md_dict[key] = value
# Map the dict to the metadata object
for key in md_dict:
if not hasattr(md, key):
print("Warning: '{0}' is not a valid tag name".format(key))
else:
md.isEmpty = False
setattr(md, key, md_dict[key])
# print(md)
return md
def launch_script(self, scriptfile):
# we were given a script. special case for the args:
# 1. ignore everything before the -S,
# 2. pass all the ones that follow (including script name) to the
# script
script_args = list()
for idx, arg in enumerate(sys.argv):
if arg in ["-S", "--script"]:
# found script!
script_args = sys.argv[idx + 1 :]
break
sys.argv = script_args
if not os.path.exists(scriptfile):
print("Can't find {0}".format(scriptfile))
else:
# I *think* this makes sense:
# assume the base name of the file is the module name
# add the folder of the given file to the python path
# import module
dirname = os.path.dirname(scriptfile)
module_name = os.path.splitext(os.path.basename(scriptfile))[0]
sys.path = [dirname] + sys.path
try:
script = __import__(module_name)
# Determine if the entry point exists before trying to run it
if "main" in dir(script):
script.main()
else:
print('Can\'t find entry point "main()" in module "{0}"'.format(module_name))
except Exception as e:
print("Script raised an unhandled exception: ", e)
print((traceback.format_exc()))
sys.exit(0)
def parseCmdLineArgs(self):
if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1:
# remove the PSN ("process serial number") argument from OS/X
input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a]
else:
input_args = sys.argv[1:]
# first check if we're launching a script:
for n in range(len(input_args)):
if input_args[n] in ["-S", "--script"] and n + 1 < len(input_args):
# insert a "--" which will cause getopt to ignore the remaining args
# so they will be passed to the script
input_args.insert(n + 2, "--")
break
# parse command line options
try:
opts, args = getopt.getopt(
input_args,
"hpdt:fm:vownsrc:ieRS:1",
[
"help",
"print",
"delete",
"type=",
"copy=",
"parsefilename",
"metadata=",
"verbose",
"online",
"dryrun",
"save",
"rename",
"raw",
"noabort",
"terse",
"nooverwrite",
"interactive",
"nosummary",
"version",
"id=",
"recursive",
"script=",
"export-to-zip",
"delete-rar",
"abort-on-conflict",
"assume-issue-one",
"cv-api-key=",
"only-set-cv-key",
"wait-on-cv-rate-limit",
],
)
except getopt.GetoptError as err:
self.display_msg_and_quit(str(err), 2)
# process options
for o, a in opts:
if o in ("-h", "--help"):
self.display_msg_and_quit(None, 0, show_help=True)
if o in ("-v", "--verbose"):
self.verbose = True
if o in ("-S", "--script"):
self.run_script = True
self.script = a
if o in ("-R", "--recursive"):
self.recursive = True
if o in ("-p", "--print"):
self.print_tags = True
if o in ("-d", "--delete"):
self.delete_tags = True
if o in ("-i", "--interactive"):
self.interactive = True
if o in ("-a", "--auto-imprint"):
self.auto_imprint = True
if o in ("-c", "--copy"):
self.copy_tags = True
if a.lower() == "cr":
self.copy_source = MetaDataStyle.CIX
elif a.lower() == "cbl":
self.copy_source = MetaDataStyle.CBI
elif a.lower() == "comet":
self.copy_source = MetaDataStyle.COMET
else:
self.display_msg_and_quit("Invalid copy tag source type", 1)
if o in ("-o", "--online"):
self.search_online = True
if o in ("-n", "--dryrun"):
self.dryrun = True
if o in ("-m", "--metadata"):
self.metadata = self.parseMetadataFromString(a)
if o in ("-s", "--save"):
self.save_tags = True
if o in ("-r", "--rename"):
self.rename_file = True
if o in ("-e", "--export_to_zip"):
self.export_to_zip = True
if o == "--delete-rar":
self.delete_rar_after_export = True
if o == "--abort-on-conflict":
self.abort_export_on_conflict = True
if o in ("-f", "--parsefilename"):
self.parse_filename = True
if o in ("-w", "--wait-on-cv-rate-limit"):
self.wait_and_retry_on_rate_limit = True
if o == "--id":
self.issue_id = a
if o == "--raw":
self.raw = True
if o == "--noabort":
self.abortOnLowConfidence = False
if o == "--terse":
self.terse = True
if o == "--nosummary":
self.show_save_summary = False
if o in ("-1", "--assume-issue-one"):
self.assume_issue_is_one_if_not_set = True
if o == "--nooverwrite":
self.no_overwrite = True
if o == "--cv-api-key":
self.cv_api_key = a
if o == "--only-set-cv-key":
self.only_set_key = True
if o == "--version":
print("ComicTagger {0}: Copyright (c) 2012-2014 Anthony Beville".format(_version.version))
print("Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
sys.exit(0)
if o in ("-t", "--type"):
if a.lower() == "cr":
self.data_style = MetaDataStyle.CIX
elif a.lower() == "cbl":
self.data_style = MetaDataStyle.CBI
elif a.lower() == "comet":
self.data_style = MetaDataStyle.COMET
else:
self.display_msg_and_quit("Invalid tag type", 1)
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip or self.only_set_key:
self.no_gui = True
count = 0
if self.run_script:
count += 1
if self.print_tags:
count += 1
if self.delete_tags:
count += 1
if self.save_tags:
count += 1
if self.copy_tags:
count += 1
if self.rename_file:
count += 1
if self.export_to_zip:
count += 1
if self.only_set_key:
count += 1
if count > 1:
self.display_msg_and_quit("Must choose only one action of print, delete, save, copy, rename, export, set key, or run script", 1)
if self.script is not None:
self.launch_script(self.script)
if len(args) > 0:
if platform.system() == "Windows":
# no globbing on windows shell, so do it for them
import glob
self.file_list = []
for item in args:
self.file_list.extend(glob.glob(item))
if len(self.file_list) > 0:
self.filename = self.file_list[0]
else:
self.filename = args[0]
self.file_list = args
if self.only_set_key and self.cv_api_key is None:
self.display_msg_and_quit("Key not given!", 1)
if (self.only_set_key == False) and self.no_gui and (self.filename is None):
self.display_msg_and_quit("Command requires at least one filename!", 1)
if self.delete_tags and self.data_style is None:
self.display_msg_and_quit("Please specify the type to delete with -t", 1)
if self.save_tags and self.data_style is None:
self.display_msg_and_quit("Please specify the type to save with -t", 1)
if self.copy_tags and self.data_style is None:
self.display_msg_and_quit("Please specify the type to copy to with -t", 1)
# if self.rename_file and self.data_style is None:
# self.display_msg_and_quit("Please specify the type to use for renaming with -t", 1)
if self.recursive:
self.file_list = utils.get_recursive_filelist(self.file_list)

View File

@ -0,0 +1,104 @@
"""A PyQT4 dialog to show pages of a comic archive"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import platform
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from .coverimagewidget import CoverImageWidget
from .settings import ComicTaggerSettings
class PageBrowserWindow(QtWidgets.QDialog):
def __init__(self, parent, metadata):
super(PageBrowserWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("pagebrowser.ui"), self)
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.pageWidget.showControls = False
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.comic_archive = None
self.page_count = 0
self.current_page_num = 0
self.metadata = metadata
self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).setDefault(True)
if platform.system() == "Darwin":
self.btnPrev.setText("<<")
self.btnNext.setText(">>")
else:
self.btnPrev.setIcon(QtGui.QIcon(ComicTaggerSettings.getGraphic("left.png")))
self.btnNext.setIcon(QtGui.QIcon(ComicTaggerSettings.getGraphic("right.png")))
self.btnNext.clicked.connect(self.nextPage)
self.btnPrev.clicked.connect(self.prevPage)
self.show()
self.btnNext.setEnabled(False)
self.btnPrev.setEnabled(False)
def reset(self):
self.comic_archive = None
self.page_count = 0
self.current_page_num = 0
self.metadata = None
self.btnNext.setEnabled(False)
self.btnPrev.setEnabled(False)
self.pageWidget.clear()
def setComicArchive(self, ca):
self.comic_archive = ca
self.page_count = ca.getNumberOfPages()
self.current_page_num = 0
self.pageWidget.setArchive(self.comic_archive)
self.setPage()
if self.page_count > 1:
self.btnNext.setEnabled(True)
self.btnPrev.setEnabled(True)
def nextPage(self):
if self.current_page_num + 1 < self.page_count:
self.current_page_num += 1
else:
self.current_page_num = 0
self.setPage()
def prevPage(self):
if self.current_page_num - 1 >= 0:
self.current_page_num -= 1
else:
self.current_page_num = self.page_count - 1
self.setPage()
def setPage(self):
if self.metadata is not None:
archive_page_index = self.metadata.getArchivePageIndex(self.current_page_num)
else:
archive_page_index = self.current_page_num
self.pageWidget.setPage(archive_page_index)
self.setWindowTitle("Page Browser - Page {0} (of {1}) ".format(self.current_page_num + 1, self.page_count))

View File

@ -0,0 +1,327 @@
"""A PyQt5 widget for editing the page list info"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
from operator import attrgetter, itemgetter
from PyQt5 import uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from .comicarchive import MetaDataStyle
from .coverimagewidget import CoverImageWidget
from .genericmetadata import GenericMetadata, PageType
from .settings import ComicTaggerSettings
def itemMoveEvents(widget):
class Filter(QObject):
mysignal = pyqtSignal(str)
def eventFilter(self, obj, event):
if obj == widget:
# print(event.type())
if event.type() == QEvent.ChildRemoved:
# print("ChildRemoved")
self.mysignal.emit("finish")
if event.type() == QEvent.ChildAdded:
# print("ChildAdded")
self.mysignal.emit("start")
return True
return False
filter = Filter(widget)
widget.installEventFilter(filter)
return filter.mysignal
class PageListEditor(QWidget):
firstFrontCoverChanged = pyqtSignal(int)
listOrderChanged = pyqtSignal()
modified = pyqtSignal()
pageTypeNames = {
PageType.FrontCover: "Front Cover",
PageType.InnerCover: "Inner Cover",
PageType.Advertisement: "Advertisement",
PageType.Roundup: "Roundup",
PageType.Story: "Story",
PageType.Editorial: "Editorial",
PageType.Letters: "Letters",
PageType.Preview: "Preview",
PageType.BackCover: "Back Cover",
PageType.Other: "Other",
PageType.Deleted: "Deleted",
}
def __init__(self, parent):
super(PageListEditor, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("pagelisteditor.ui"), self)
self.setEnabled(True)
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.pageWidget.showControls = False
self.resetPage()
# Add the entries to the manga combobox
self.comboBox.addItem("", "")
self.comboBox.addItem(self.pageTypeNames[PageType.FrontCover], PageType.FrontCover)
self.comboBox.addItem(self.pageTypeNames[PageType.InnerCover], PageType.InnerCover)
self.comboBox.addItem(self.pageTypeNames[PageType.Advertisement], PageType.Advertisement)
self.comboBox.addItem(self.pageTypeNames[PageType.Roundup], PageType.Roundup)
self.comboBox.addItem(self.pageTypeNames[PageType.Story], PageType.Story)
self.comboBox.addItem(self.pageTypeNames[PageType.Editorial], PageType.Editorial)
self.comboBox.addItem(self.pageTypeNames[PageType.Letters], PageType.Letters)
self.comboBox.addItem(self.pageTypeNames[PageType.Preview], PageType.Preview)
self.comboBox.addItem(self.pageTypeNames[PageType.BackCover], PageType.BackCover)
self.comboBox.addItem(self.pageTypeNames[PageType.Other], PageType.Other)
self.comboBox.addItem(self.pageTypeNames[PageType.Deleted], PageType.Deleted)
self.listWidget.itemSelectionChanged.connect(self.changePage)
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
self.comboBox.activated.connect(self.changePageType)
self.btnUp.clicked.connect(self.moveCurrentUp)
self.btnDown.clicked.connect(self.moveCurrentDown)
self.pre_move_row = -1
self.first_front_page = None
def toggleAd(self):
ad = self.comboBox.findData(PageType.Advertisement)
if self.comboBox.currentIndex() == ad:
self.comboBox.setCurrentIndex(0)
self.changePageType(0)
else:
self.comboBox.setCurrentIndex(ad)
self.changePageType(ad)
def resetPage(self):
self.pageWidget.clear()
self.comboBox.setDisabled(True)
self.comic_archive = None
self.pages_list = None
def getNewIndexes(self, movement):
selection = self.listWidget.selectionModel().selectedRows()
selection.sort(reverse=movement > 0)
current = 0
newindexes = []
oldindexes = []
for x in selection:
current = x.row()
oldindexes.append(current)
if current + movement >= 0 and current + movement <= self.listWidget.count() - 1:
if len(newindexes) < 1 or current + movement != newindexes[-1]:
current += movement
else:
prev = current
newindexes.append(current)
oldindexes.sort()
newindexes.sort()
return list(zip(newindexes, oldindexes))
def SetSelection(self, indexes):
selectionRanges = []
first = 0
for i, selection in enumerate(indexes):
if i == 0:
first = selection[0]
continue
if selection != indexes[i - 1][0] + 1:
selectionRanges.append((first, indexes[i - 1][0]))
first = selection[0]
selectionRanges.append((first, indexes[-1][0]))
selection = QItemSelection()
for x in selectionRanges:
selection.merge(
QItemSelection(self.listWidget.model().index(x[0], 0), self.listWidget.model().index(x[1], 0)), QItemSelectionModel.Select
)
self.listWidget.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect)
return selectionRanges
def moveCurrentUp(self):
row = self.listWidget.currentRow()
selection = self.getNewIndexes(-1)
for sel in selection:
item = self.listWidget.takeItem(sel[1])
self.listWidget.insertItem(sel[0], item)
if row > 0:
self.listWidget.setCurrentRow(row - 1)
self.SetSelection(selection)
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def moveCurrentDown(self):
row = self.listWidget.currentRow()
selection = self.getNewIndexes(1)
selection.sort(reverse=True)
for sel in selection:
item = self.listWidget.takeItem(sel[1])
self.listWidget.insertItem(sel[0], item)
if row < self.listWidget.count() - 1:
self.listWidget.setCurrentRow(row + 1)
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.SetSelection(selection)
self.modified.emit()
def itemMoveEvent(self, s):
# print "move event: ", s, self.listWidget.currentRow()
if s == "start":
self.pre_move_row = self.listWidget.currentRow()
if s == "finish":
if self.pre_move_row != self.listWidget.currentRow():
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def changePageType(self, i):
new_type = self.comboBox.itemData(i)
if self.getCurrentPageType() != new_type:
self.setCurrentPageType(new_type)
self.emitFrontCoverChange()
self.modified.emit()
def changePage(self):
row = self.listWidget.currentRow()
pagetype = self.getCurrentPageType()
i = self.comboBox.findData(pagetype)
self.comboBox.setCurrentIndex(i)
# idx = int(str (self.listWidget.item(row).text()))
idx = int(self.listWidget.item(row).data(Qt.UserRole)[0]["Image"])
if self.comic_archive is not None:
self.pageWidget.setArchive(self.comic_archive, idx)
def getFirstFrontCover(self):
frontCover = 0
for i in range(self.listWidget.count()):
item = self.listWidget.item(i)
page_dict = item.data(Qt.UserRole)[0] # .toPyObject()[0]
if "Type" in page_dict and page_dict["Type"] == PageType.FrontCover:
frontCover = int(page_dict["Image"])
break
return frontCover
def getCurrentPageType(self):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] # .toPyObject()[0]
if "Type" in page_dict:
return page_dict["Type"]
else:
return ""
def setCurrentPageType(self, t):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] # .toPyObject()[0]
if t == "":
if "Type" in page_dict:
del page_dict["Type"]
else:
page_dict["Type"] = str(t)
item = self.listWidget.item(row)
# wrap the dict in a tuple to keep from being converted to QStrings
item.setData(Qt.UserRole, (page_dict,))
item.setText(self.listEntryText(page_dict))
def setData(self, comic_archive, pages_list):
self.comic_archive = comic_archive
self.pages_list = pages_list
if pages_list is not None and len(pages_list) > 0:
self.comboBox.setDisabled(False)
self.listWidget.itemSelectionChanged.disconnect(self.changePage)
self.listWidget.clear()
for p in pages_list:
item = QListWidgetItem(self.listEntryText(p))
# wrap the dict in a tuple to keep from being converted to QStrings
item.setData(Qt.UserRole, (p,))
self.listWidget.addItem(item)
self.first_front_page = self.getFirstFrontCover()
self.listWidget.itemSelectionChanged.connect(self.changePage)
self.listWidget.setCurrentRow(0)
def listEntryText(self, page_dict):
text = str(int(page_dict["Image"]) + 1)
if "Type" in page_dict:
text += " (" + self.pageTypeNames[page_dict["Type"]] + ")"
return text
def getPageList(self):
page_list = []
for i in range(self.listWidget.count()):
item = self.listWidget.item(i)
page_list.append(item.data(Qt.UserRole)[0]) # .toPyObject()[0]
return page_list
def emitFrontCoverChange(self):
if self.first_front_page != self.getFirstFrontCover():
self.first_front_page = self.getFirstFrontCover()
self.firstFrontCoverChanged.emit(self.first_front_page)
def setMetadataStyle(self, data_style):
# depending on the current data style, certain fields are disabled
inactive_color = QColor(255, 170, 150)
active_palette = self.comboBox.palette()
inactive_palette3 = self.comboBox.palette()
inactive_palette3.setColor(QPalette.Base, inactive_color)
if data_style == MetaDataStyle.CIX:
self.btnUp.setEnabled(True)
self.btnDown.setEnabled(True)
self.comboBox.setEnabled(True)
self.listWidget.setEnabled(True)
self.listWidget.setPalette(active_palette)
elif data_style == MetaDataStyle.CBI:
self.btnUp.setEnabled(False)
self.btnDown.setEnabled(False)
self.comboBox.setEnabled(False)
self.listWidget.setEnabled(False)
self.listWidget.setPalette(inactive_palette3)
elif data_style == MetaDataStyle.CoMet:
pass
# make sure combo is disabled when no list
if self.comic_archive is None:
self.comboBox.setEnabled(False)

View File

@ -0,0 +1,68 @@
"""A PyQT4 class to load a page image from a ComicArchive in a background thread"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal
from comictaggerlib.ui.qtutils import getQImageFromData
class PageLoader(QtCore.QThread):
"""
This class holds onto a reference of each instance in a list since
problems occur if the ref count goes to zero and the GC tries to reap
the object while the thread is going.
If the client class wants to stop the thread, they should mark it as
"abandoned", and no signals will be issued.
"""
loadComplete = pyqtSignal(QtGui.QImage)
instanceList = []
mutex = QtCore.QMutex()
# Remove all finished threads from the list
@staticmethod
def reapInstances():
for obj in reversed(PageLoader.instanceList):
if obj.isFinished():
PageLoader.instanceList.remove(obj)
def __init__(self, ca, page_num):
QtCore.QThread.__init__(self)
self.ca = ca
self.page_num = page_num
self.abandoned = False
# remove any old instances, and then add ourself
PageLoader.mutex.lock()
PageLoader.reapInstances()
PageLoader.instanceList.append(self)
PageLoader.mutex.unlock()
def run(self):
image_data = self.ca.getPage(self.page_num)
if self.abandoned:
return
if image_data is not None:
img = getQImageFromData(image_data)
if self.abandoned:
return
self.loadComplete.emit(img)

View File

@ -0,0 +1,33 @@
"""A PyQT5 dialog to show ID log and progress"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
from .settings import ComicTaggerSettings
class IDProgressWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(IDProgressWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("progresswindow.ui"), self)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
reduceWidgetFontSize(self.textEdit)

View File

@ -0,0 +1,178 @@
"""A PyQT4 dialog to confirm rename"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui.qtutils import centerWindowOnParent
from . import utils
from .comicarchive import MetaDataStyle
from .filerenamer import FileRenamer
from .settings import ComicTaggerSettings
from .settingswindow import SettingsWindow
class RenameWindow(QtWidgets.QDialog):
def __init__(self, parent, comic_archive_list, data_style, settings):
super(RenameWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("renamewindow.ui"), self)
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint)
self.settings = settings
self.comic_archive_list = comic_archive_list
self.data_style = data_style
self.btnSettings.clicked.connect(self.modifySettings)
self.configRenamer()
self.doPreview()
def configRenamer(self):
self.renamer = FileRenamer(None)
self.renamer.setTemplate(self.settings.rename_template)
self.renamer.setIssueZeroPadding(self.settings.rename_issue_number_padding)
self.renamer.setSmartCleanup(self.settings.rename_use_smart_string_cleanup)
def doPreview(self):
self.rename_list = []
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
self.twList.setSortingEnabled(False)
for ca in self.comic_archive_list:
new_ext = None # default
if self.settings.rename_extension_based_on_archive:
if ca.isZip():
new_ext = ".cbz"
elif ca.isRar():
new_ext = ".cbr"
md = ca.readMetadata(self.data_style)
if md.isEmpty:
md = ca.metadataFromFilename(self.settings.parse_scan_info)
self.renamer.setMetadata(md)
self.renamer.move = self.settings.rename_move_dir
try:
new_name = self.renamer.determineName(ca.path, ext=new_ext)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
"Your rename template is invalid!"
"<br/><br/>{}<br/><br/>"
"Please consult the template help in the "
"settings and the documentation on the format at "
"<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
"https://docs.python.org/3/library/string.html#format-string-syntax</a>".format(e),
)
return
row = self.twList.rowCount()
self.twList.insertRow(row)
folder_item = QtWidgets.QTableWidgetItem()
old_name_item = QtWidgets.QTableWidgetItem()
new_name_item = QtWidgets.QTableWidgetItem()
item_text = os.path.split(ca.path)[0]
folder_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, folder_item)
folder_item.setText(item_text)
folder_item.setData(QtCore.Qt.ToolTipRole, item_text)
item_text = os.path.split(ca.path)[1]
old_name_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, old_name_item)
old_name_item.setText(item_text)
old_name_item.setData(QtCore.Qt.ToolTipRole, item_text)
new_name_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, new_name_item)
new_name_item.setText(new_name)
new_name_item.setData(QtCore.Qt.ToolTipRole, new_name)
dict_item = dict()
dict_item["archive"] = ca
dict_item["new_name"] = new_name
self.rename_list.append(dict_item)
# Adjust column sizes
self.twList.setVisible(False)
self.twList.resizeColumnsToContents()
self.twList.setVisible(True)
if self.twList.columnWidth(0) > 200:
self.twList.setColumnWidth(0, 200)
self.twList.setSortingEnabled(True)
def modifySettings(self):
settingswin = SettingsWindow(self, self.settings)
settingswin.setModal(True)
settingswin.showRenameTab()
settingswin.exec_()
if settingswin.result():
self.configRenamer()
self.doPreview()
def accept(self):
progdialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
progdialog.setWindowTitle("Renaming Archives")
progdialog.setWindowModality(QtCore.Qt.WindowModal)
progdialog.setMinimumDuration(100)
centerWindowOnParent(progdialog)
# progdialog.show()
QtCore.QCoreApplication.processEvents()
for idx, item in enumerate(self.rename_list):
QtCore.QCoreApplication.processEvents()
if progdialog.wasCanceled():
break
idx += 1
progdialog.setValue(idx)
progdialog.setLabelText(item["new_name"])
centerWindowOnParent(progdialog)
QtCore.QCoreApplication.processEvents()
folder = os.path.dirname(os.path.abspath(item["archive"].path))
if self.settings.rename_move_dir and len(self.settings.rename_dir.strip()) > 3:
folder = self.settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, item["new_name"]))
if os.path.join(folder, item["new_name"]) == item["archive"].path:
print(item["new_name"], "Filename is already good!")
continue
if not item["archive"].isWritable(check_rar_status=False):
continue
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(item["archive"].path, new_abs_path)
item["archive"].rename(new_abs_path)
progdialog.hide()
QtCore.QCoreApplication.processEvents()
QtWidgets.QDialog.accept(self)

426
comictaggerlib/settings.py Normal file
View File

@ -0,0 +1,426 @@
"""Settings class for ComicTagger app"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import codecs
import configparser
import os
import platform
import sys
import uuid
from . import utils
class ComicTaggerSettings:
@staticmethod
def getSettingsFolder():
filename_encoding = sys.getfilesystemencoding()
if platform.system() == "Windows":
folder = os.path.join(os.environ["APPDATA"], "ComicTagger")
else:
folder = os.path.join(os.path.expanduser("~"), ".ComicTagger")
if folder is not None:
folder = folder
return folder
@staticmethod
def defaultLibunrarPath():
return ComicTaggerSettings.baseDir() + "/libunrar.so"
@staticmethod
def haveOwnUnrarLib():
return os.path.exists(ComicTaggerSettings.defaultLibunrarPath())
@staticmethod
def baseDir():
if getattr(sys, "frozen", None):
return sys._MEIPASS
else:
return os.path.dirname(os.path.abspath(__file__))
@staticmethod
def getGraphic(filename):
graphic_folder = os.path.join(ComicTaggerSettings.baseDir(), "graphics")
return os.path.join(graphic_folder, filename)
@staticmethod
def getUIFile(filename):
ui_folder = os.path.join(ComicTaggerSettings.baseDir(), "ui")
return os.path.join(ui_folder, filename)
def setDefaultValues(self):
# General Settings
self.rar_exe_path = ""
self.unrar_lib_path = ""
self.allow_cbi_in_rar = True
self.check_for_new_version = False
self.send_usage_stats = False
# automatic settings
self.install_id = uuid.uuid4().hex
self.last_selected_save_data_style = 0
self.last_selected_load_data_style = 0
self.last_opened_folder = ""
self.last_main_window_width = 0
self.last_main_window_height = 0
self.last_main_window_x = 0
self.last_main_window_y = 0
self.last_form_side_width = -1
self.last_list_side_width = -1
self.last_filelist_sorted_column = -1
self.last_filelist_sorted_order = 0
# identifier settings
self.id_length_delta_thresh = 5
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
# Show/ask dialog flags
self.ask_about_cbi_in_rar = True
self.show_disclaimer = True
self.dont_notify_about_this_version = ""
self.ask_about_usage_stats = True
self.show_no_unrar_warning = True
# filename parsing settings
self.parse_scan_info = True
# Comic Vine settings
self.use_series_start_as_volume = False
self.clear_form_before_populating_from_cv = False
self.remove_html_tables = False
self.cv_api_key = ""
self.auto_imprint = False
# CBL Tranform settings
self.assume_lone_credit_is_primary = False
self.copy_characters_to_tags = False
self.copy_teams_to_tags = False
self.copy_locations_to_tags = False
self.copy_storyarcs_to_tags = False
self.copy_notes_to_comments = False
self.copy_weblink_to_comments = False
self.apply_cbl_transform_on_cv_import = False
self.apply_cbl_transform_on_bulk_operation = False
# Rename settings
self.rename_template = "{publisher}/{series}/{series} #{issue} - {title} ({year})"
self.rename_issue_number_padding = 3
self.rename_use_smart_string_cleanup = True
self.rename_extension_based_on_archive = True
self.rename_dir = ""
self.rename_move_dir = False
# Auto-tag stickies
self.save_on_low_confidence = False
self.dont_use_year_when_identifying = False
self.assume_1_if_no_issue_num = False
self.ignore_leading_numbers_in_filename = False
self.remove_archive_after_successful_match = False
self.wait_and_retry_on_rate_limit = False
def __init__(self):
self.settings_file = ""
self.folder = ""
self.setDefaultValues()
self.config = configparser.RawConfigParser()
self.folder = ComicTaggerSettings.getSettingsFolder()
if not os.path.exists(self.folder):
os.makedirs(self.folder)
self.settings_file = os.path.join(self.folder, "settings")
# if config file doesn't exist, write one out
if not os.path.exists(self.settings_file):
self.save()
else:
self.load()
# take a crack at finding rar exe, if not set already
if self.rar_exe_path == "":
if platform.system() == "Windows":
# look in some likely places for Windows machines
if os.path.exists("C:\Program Files\WinRAR\Rar.exe"):
self.rar_exe_path = "C:\Program Files\WinRAR\Rar.exe"
elif os.path.exists("C:\Program Files (x86)\WinRAR\Rar.exe"):
self.rar_exe_path = "C:\Program Files (x86)\WinRAR\Rar.exe"
else:
# see if it's in the path of unix user
if utils.which("rar") is not None:
self.rar_exe_path = utils.which("rar")
if self.rar_exe_path != "":
self.save()
if self.rar_exe_path != "":
# make sure rar program is now in the path for the rar class
utils.addtopath(os.path.dirname(self.rar_exe_path))
if self.haveOwnUnrarLib():
# We have a 'personal' copy of the unrar lib in the basedir, so
# don't search and change the setting
# NOTE: a manual edit of the settings file overrides this below
os.environ["UNRAR_LIB_PATH"] = self.defaultLibunrarPath()
elif self.unrar_lib_path == "":
# Priority is for unrar lib search is:
# 1. explicit setting in settings file
# 2. UNRAR_LIB_PATH in environment
# 3. check some likely platform specific places
if "UNRAR_LIB_PATH" in os.environ:
self.unrar_lib_path = os.environ["UNRAR_LIB_PATH"]
else:
# look in some platform specific places:
if platform.system() == "Windows":
# Default location for the RARLab DLL installer
if platform.architecture()[0] == "64bit" and os.path.exists("C:\\Program Files (x86)\\UnrarDLL\\x64\\UnRAR64.dll"):
self.unrar_lib_path = "C:\\Program Files (x86)\\UnrarDLL\\x64\\UnRAR64.dll"
elif platform.architecture()[0] == "32bit" and os.path.exists("C:\\Program Files\\UnrarDLL\\UnRAR.dll"):
self.unrar_lib_path = "C:\\Program Files\\UnrarDLL\\UnRAR.dll"
elif platform.system() == "Darwin":
# Look for the brew unrar library
if os.path.exists("/usr/local/lib/libunrar.dylib"):
self.unrar_lib_path = "/usr/local/lib/libunrar.dylib"
elif platform.system() == "Linux":
if os.path.exists("/usr/local/lib/libunrar.so"):
self.unrar_lib_path = "/usr/local/lib/libunrar.so"
elif os.path.exists("/usr/lib/libunrar.so"):
self.unrar_lib_path = "/usr/lib/libunrar.so"
if self.unrar_lib_path != "":
self.save()
if self.unrar_lib_path != "":
# This needs to occur before the unrar module is loaded for the first time
os.environ["UNRAR_LIB_PATH"] = self.unrar_lib_path
def reset(self):
os.unlink(self.settings_file)
self.__init__()
def load(self):
def readline_generator(f):
line = f.readline()
while line:
yield line
line = f.readline()
# self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
self.config.read_file(readline_generator(codecs.open(self.settings_file, "r", "utf8")))
self.rar_exe_path = self.config.get("settings", "rar_exe_path")
if self.config.has_option("settings", "unrar_lib_path"):
self.unrar_lib_path = self.config.get("settings", "unrar_lib_path")
if self.config.has_option("settings", "check_for_new_version"):
self.check_for_new_version = self.config.getboolean("settings", "check_for_new_version")
if self.config.has_option("settings", "send_usage_stats"):
self.send_usage_stats = self.config.getboolean("settings", "send_usage_stats")
if self.config.has_option("auto", "install_id"):
self.install_id = self.config.get("auto", "install_id")
if self.config.has_option("auto", "last_selected_load_data_style"):
self.last_selected_load_data_style = self.config.getint("auto", "last_selected_load_data_style")
if self.config.has_option("auto", "last_selected_save_data_style"):
self.last_selected_save_data_style = self.config.getint("auto", "last_selected_save_data_style")
if self.config.has_option("auto", "last_opened_folder"):
self.last_opened_folder = self.config.get("auto", "last_opened_folder")
if self.config.has_option("auto", "last_main_window_width"):
self.last_main_window_width = self.config.getint("auto", "last_main_window_width")
if self.config.has_option("auto", "last_main_window_height"):
self.last_main_window_height = self.config.getint("auto", "last_main_window_height")
if self.config.has_option("auto", "last_main_window_x"):
self.last_main_window_x = self.config.getint("auto", "last_main_window_x")
if self.config.has_option("auto", "last_main_window_y"):
self.last_main_window_y = self.config.getint("auto", "last_main_window_y")
if self.config.has_option("auto", "last_form_side_width"):
self.last_form_side_width = self.config.getint("auto", "last_form_side_width")
if self.config.has_option("auto", "last_list_side_width"):
self.last_list_side_width = self.config.getint("auto", "last_list_side_width")
if self.config.has_option("auto", "last_filelist_sorted_column"):
self.last_filelist_sorted_column = self.config.getint("auto", "last_filelist_sorted_column")
if self.config.has_option("auto", "last_filelist_sorted_order"):
self.last_filelist_sorted_order = self.config.getint("auto", "last_filelist_sorted_order")
if self.config.has_option("identifier", "id_length_delta_thresh"):
self.id_length_delta_thresh = self.config.getint("identifier", "id_length_delta_thresh")
if self.config.has_option("identifier", "id_publisher_blacklist"):
self.id_publisher_blacklist = self.config.get("identifier", "id_publisher_blacklist")
if self.config.has_option("filenameparser", "parse_scan_info"):
self.parse_scan_info = self.config.getboolean("filenameparser", "parse_scan_info")
if self.config.has_option("dialogflags", "ask_about_cbi_in_rar"):
self.ask_about_cbi_in_rar = self.config.getboolean("dialogflags", "ask_about_cbi_in_rar")
if self.config.has_option("dialogflags", "show_disclaimer"):
self.show_disclaimer = self.config.getboolean("dialogflags", "show_disclaimer")
if self.config.has_option("dialogflags", "dont_notify_about_this_version"):
self.dont_notify_about_this_version = self.config.get("dialogflags", "dont_notify_about_this_version")
if self.config.has_option("dialogflags", "ask_about_usage_stats"):
self.ask_about_usage_stats = self.config.getboolean("dialogflags", "ask_about_usage_stats")
if self.config.has_option("dialogflags", "show_no_unrar_warning"):
self.show_no_unrar_warning = self.config.getboolean("dialogflags", "show_no_unrar_warning")
if self.config.has_option("comicvine", "use_series_start_as_volume"):
self.use_series_start_as_volume = self.config.getboolean("comicvine", "use_series_start_as_volume")
if self.config.has_option("comicvine", "clear_form_before_populating_from_cv"):
self.clear_form_before_populating_from_cv = self.config.getboolean("comicvine", "clear_form_before_populating_from_cv")
if self.config.has_option("comicvine", "remove_html_tables"):
self.remove_html_tables = self.config.getboolean("comicvine", "remove_html_tables")
if self.config.has_option("comicvine", "cv_api_key"):
self.cv_api_key = self.config.get("comicvine", "cv_api_key")
if self.config.has_option("cbl_transform", "assume_lone_credit_is_primary"):
self.assume_lone_credit_is_primary = self.config.getboolean("cbl_transform", "assume_lone_credit_is_primary")
if self.config.has_option("cbl_transform", "copy_characters_to_tags"):
self.copy_characters_to_tags = self.config.getboolean("cbl_transform", "copy_characters_to_tags")
if self.config.has_option("cbl_transform", "copy_teams_to_tags"):
self.copy_teams_to_tags = self.config.getboolean("cbl_transform", "copy_teams_to_tags")
if self.config.has_option("cbl_transform", "copy_locations_to_tags"):
self.copy_locations_to_tags = self.config.getboolean("cbl_transform", "copy_locations_to_tags")
if self.config.has_option("cbl_transform", "copy_notes_to_comments"):
self.copy_notes_to_comments = self.config.getboolean("cbl_transform", "copy_notes_to_comments")
if self.config.has_option("cbl_transform", "copy_storyarcs_to_tags"):
self.copy_storyarcs_to_tags = self.config.getboolean("cbl_transform", "copy_storyarcs_to_tags")
if self.config.has_option("cbl_transform", "copy_weblink_to_comments"):
self.copy_weblink_to_comments = self.config.getboolean("cbl_transform", "copy_weblink_to_comments")
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_cv_import"):
self.apply_cbl_transform_on_cv_import = self.config.getboolean("cbl_transform", "apply_cbl_transform_on_cv_import")
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_bulk_operation"):
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean("cbl_transform", "apply_cbl_transform_on_bulk_operation")
if self.config.has_option("rename", "rename_template"):
self.rename_template = self.config.get("rename", "rename_template")
if self.config.has_option("rename", "rename_issue_number_padding"):
self.rename_issue_number_padding = self.config.getint("rename", "rename_issue_number_padding")
if self.config.has_option("rename", "rename_use_smart_string_cleanup"):
self.rename_use_smart_string_cleanup = self.config.getboolean("rename", "rename_use_smart_string_cleanup")
if self.config.has_option("rename", "rename_extension_based_on_archive"):
self.rename_extension_based_on_archive = self.config.getboolean("rename", "rename_extension_based_on_archive")
if self.config.has_option("rename", "rename_dir"):
self.rename_dir = self.config.get("rename", "rename_dir")
if self.config.has_option("rename", "rename_move_dir"):
self.rename_move_dir = self.config.getboolean("rename", "rename_move_dir")
if self.config.has_option("autotag", "save_on_low_confidence"):
self.save_on_low_confidence = self.config.getboolean("autotag", "save_on_low_confidence")
if self.config.has_option("autotag", "dont_use_year_when_identifying"):
self.dont_use_year_when_identifying = self.config.getboolean("autotag", "dont_use_year_when_identifying")
if self.config.has_option("autotag", "assume_1_if_no_issue_num"):
self.assume_1_if_no_issue_num = self.config.getboolean("autotag", "assume_1_if_no_issue_num")
if self.config.has_option("autotag", "ignore_leading_numbers_in_filename"):
self.ignore_leading_numbers_in_filename = self.config.getboolean("autotag", "ignore_leading_numbers_in_filename")
if self.config.has_option("autotag", "remove_archive_after_successful_match"):
self.remove_archive_after_successful_match = self.config.getboolean("autotag", "remove_archive_after_successful_match")
if self.config.has_option("autotag", "wait_and_retry_on_rate_limit"):
self.wait_and_retry_on_rate_limit = self.config.getboolean("autotag", "wait_and_retry_on_rate_limit")
if self.config.has_option("autotag", "auto_imprint"):
self.auto_imprint = self.config.getboolean("autotag", "auto_imprint")
def save(self):
if not self.config.has_section("settings"):
self.config.add_section("settings")
self.config.set("settings", "check_for_new_version", self.check_for_new_version)
self.config.set("settings", "rar_exe_path", self.rar_exe_path)
self.config.set("settings", "unrar_lib_path", self.unrar_lib_path)
self.config.set("settings", "send_usage_stats", self.send_usage_stats)
if not self.config.has_section("auto"):
self.config.add_section("auto")
self.config.set("auto", "install_id", self.install_id)
self.config.set("auto", "last_selected_load_data_style", self.last_selected_load_data_style)
self.config.set("auto", "last_selected_save_data_style", self.last_selected_save_data_style)
self.config.set("auto", "last_opened_folder", self.last_opened_folder)
self.config.set("auto", "last_main_window_width", self.last_main_window_width)
self.config.set("auto", "last_main_window_height", self.last_main_window_height)
self.config.set("auto", "last_main_window_x", self.last_main_window_x)
self.config.set("auto", "last_main_window_y", self.last_main_window_y)
self.config.set("auto", "last_form_side_width", self.last_form_side_width)
self.config.set("auto", "last_list_side_width", self.last_list_side_width)
self.config.set("auto", "last_filelist_sorted_column", self.last_filelist_sorted_column)
self.config.set("auto", "last_filelist_sorted_order", self.last_filelist_sorted_order)
if not self.config.has_section("identifier"):
self.config.add_section("identifier")
self.config.set("identifier", "id_length_delta_thresh", self.id_length_delta_thresh)
self.config.set("identifier", "id_publisher_blacklist", self.id_publisher_blacklist)
if not self.config.has_section("dialogflags"):
self.config.add_section("dialogflags")
self.config.set("dialogflags", "ask_about_cbi_in_rar", self.ask_about_cbi_in_rar)
self.config.set("dialogflags", "show_disclaimer", self.show_disclaimer)
self.config.set("dialogflags", "dont_notify_about_this_version", self.dont_notify_about_this_version)
self.config.set("dialogflags", "ask_about_usage_stats", self.ask_about_usage_stats)
self.config.set("dialogflags", "show_no_unrar_warning", self.show_no_unrar_warning)
if not self.config.has_section("filenameparser"):
self.config.add_section("filenameparser")
self.config.set("filenameparser", "parse_scan_info", self.parse_scan_info)
if not self.config.has_section("comicvine"):
self.config.add_section("comicvine")
self.config.set("comicvine", "use_series_start_as_volume", self.use_series_start_as_volume)
self.config.set("comicvine", "clear_form_before_populating_from_cv", self.clear_form_before_populating_from_cv)
self.config.set("comicvine", "remove_html_tables", self.remove_html_tables)
self.config.set("comicvine", "cv_api_key", self.cv_api_key)
if not self.config.has_section("cbl_transform"):
self.config.add_section("cbl_transform")
self.config.set("cbl_transform", "assume_lone_credit_is_primary", self.assume_lone_credit_is_primary)
self.config.set("cbl_transform", "copy_characters_to_tags", self.copy_characters_to_tags)
self.config.set("cbl_transform", "copy_teams_to_tags", self.copy_teams_to_tags)
self.config.set("cbl_transform", "copy_locations_to_tags", self.copy_locations_to_tags)
self.config.set("cbl_transform", "copy_storyarcs_to_tags", self.copy_storyarcs_to_tags)
self.config.set("cbl_transform", "copy_notes_to_comments", self.copy_notes_to_comments)
self.config.set("cbl_transform", "copy_weblink_to_comments", self.copy_weblink_to_comments)
self.config.set("cbl_transform", "apply_cbl_transform_on_cv_import", self.apply_cbl_transform_on_cv_import)
self.config.set("cbl_transform", "apply_cbl_transform_on_bulk_operation", self.apply_cbl_transform_on_bulk_operation)
if not self.config.has_section("rename"):
self.config.add_section("rename")
self.config.set("rename", "rename_template", self.rename_template)
self.config.set("rename", "rename_issue_number_padding", self.rename_issue_number_padding)
self.config.set("rename", "rename_use_smart_string_cleanup", self.rename_use_smart_string_cleanup)
self.config.set("rename", "rename_extension_based_on_archive", self.rename_extension_based_on_archive)
self.config.set("rename", "rename_dir", self.rename_dir)
self.config.set("rename", "rename_move_dir", self.rename_move_dir)
if not self.config.has_section("autotag"):
self.config.add_section("autotag")
self.config.set("autotag", "save_on_low_confidence", self.save_on_low_confidence)
self.config.set("autotag", "dont_use_year_when_identifying", self.dont_use_year_when_identifying)
self.config.set("autotag", "assume_1_if_no_issue_num", self.assume_1_if_no_issue_num)
self.config.set("autotag", "ignore_leading_numbers_in_filename", self.ignore_leading_numbers_in_filename)
self.config.set("autotag", "remove_archive_after_successful_match", self.remove_archive_after_successful_match)
self.config.set("autotag", "wait_and_retry_on_rate_limit", self.wait_and_retry_on_rate_limit)
self.config.set("autotag", "auto_imprint", self.auto_imprint)
with codecs.open(self.settings_file, "wb", "utf8") as configfile:
self.config.write(configfile)
# make sure the basedir is cached, in case we're on Windows running a
# script from frozen binary
ComicTaggerSettings.baseDir()

View File

@ -0,0 +1,398 @@
"""A PyQT4 dialog to enter app settings"""
# Copyright 2012-2014 Anthony Beville
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import platform
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from . import utils
from .comicvinecacher import ComicVineCacher
from .comicvinetalker import ComicVineTalker
from .filerenamer import FileRenamer
from .genericmetadata import GenericMetadata
from .imagefetcher import ImageFetcher
from .settings import ComicTaggerSettings
windowsRarHelp = """
<html><head/><body><p>To write to CBR/RAR archives,
you will need to have the tools from
<span style=" text-decoration: underline; color:#0000ff;">
<a href="http://www.win-rar.com/download.html">WINRar</a></span>
installed. (ComicTagger only uses the command-line rar tool,
which is free to use.)</p></body></html>
"""
linuxRarHelp = """
<html><head/><body><p>To write to CBR/RAR archives,
you will need to have the shareware rar tool from RARLab installed.
Your package manager should have rar (e.g. "apt-get install rar"). If not, download it
<span style=" text-decoration: underline; color:#0000ff;">
<a href="https://www.rarlab.com/download.htm">here</a></span>,
and install in your path. </p></body></html>
"""
macRarHelp = """
<html><head/><body><p>To write to CBR/RAR archives,
you will need the rar tool. The easiest way to get this is
to install <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://brew.sh/">homebrew</a></span>.
</p>Once homebrew is installed, run: <b>brew install caskroom/cask/rar</b></body></html>
"""
windowsUnrarHelp = """
<html><head/><body><p>To read CBR/RAR archives,
you will need to have the unrar DLL from
<span style=" text-decoration: underline; color:#0000ff;">
<a href="https://www.rarlab.com/rar_add.htm">
RARLab</a></span> installed. </p></body></html>
"""
linuxUnrarHelp = """
<html><head/><body><p>To read CBR/RAR archives,
you will need to have the unrar library from RARLab installed.
Look <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://github.com/beville/libunrar-binaries/releases">here</a></span>
for pre-compiled binaries, or <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://www.rarlab.com/rar_add.htm">here</a></span>
for the UnRAR source (which is easy to compile on Linux). </p></body></html>
"""
macUnrarHelp = """
<html><head/><body><p>To read CBR/RAR archives,
you will need the unrar library. The easiest way to get this is
to install <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://brew.sh/homebrew">homebrew</a></span>.
</p>Once homebrew is installed, run: <b>brew install unrar</b></body></html>
"""
class SettingsWindow(QtWidgets.QDialog):
def __init__(self, parent, settings):
super(SettingsWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("settingswindow.ui"), self)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.name = "Settings"
self.priorUnrarLibPath = self.settings.unrar_lib_path
if self.settings.haveOwnUnrarLib():
# We have our own unrarlib, so no need for this GUI
self.grpBoxUnrar.hide()
if platform.system() == "Windows":
self.lblRarHelp.setText(windowsRarHelp)
self.lblUnrarHelp.setText(windowsUnrarHelp)
elif platform.system() == "Linux":
self.lblRarHelp.setText(linuxRarHelp)
self.lblUnrarHelp.setText(linuxUnrarHelp)
elif platform.system() == "Darwin":
# Mac file dialog hides "/usr" and others, so allow user to type
self.leUnrarLibPath.setReadOnly(False)
self.leRarExePath.setReadOnly(False)
self.lblRarHelp.setText(macRarHelp)
self.lblUnrarHelp.setText(macUnrarHelp)
self.name = "Preferences"
self.setWindowTitle("ComicTagger " + self.name)
self.lblDefaultSettings.setText("Revert to default " + self.name.lower())
self.btnResetSettings.setText("Default " + self.name)
nldtTip = """<html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
search matches that are too long compared to your series name search. The higher
it is, the more likely to have a good match, but each search will take longer and
use more bandwidth. Too low, and only the very closest lexical matches will be
explored.</html>"""
self.leNameLengthDeltaThresh.setToolTip(nldtTip)
pblTip = """<html>
The <b>Publisher Blacklist</b> is for eliminating automatic matches to certain publishers
that you know are incorrect. Useful for avoiding international re-prints with same
covers or series names. Enter publisher names separated by commas.
</html>"""
self.tePublisherBlacklist.setToolTip(pblTip)
validator = QtGui.QIntValidator(1, 4, self)
self.leIssueNumPadding.setValidator(validator)
validator = QtGui.QIntValidator(0, 99, self)
self.leNameLengthDeltaThresh.setValidator(validator)
self.settingsToForm()
self.btnBrowseRar.clicked.connect(self.selectRar)
self.btnBrowseUnrar.clicked.connect(self.selectUnrar)
self.btnClearCache.clicked.connect(self.clearCache)
self.btnResetSettings.clicked.connect(self.resetSettings)
self.btnTestKey.clicked.connect(self.testAPIKey)
self.btnTemplateHelp.clicked.connect(self.showTemplateHelp)
def configRenamer(self):
md = GenericMetadata()
md.isEmpty = False
md.tagOrigin = "testing"
md.series = "series name"
md.issue = "1"
md.title = "issue title"
md.publisher = "lordwelch"
md.month = 4
md.year = 1998
md.day = 4
md.issueCount = 1
md.volume = 256
md.genre = "test"
md.language = "en" # 2 letter iso code
md.comments = "This is definitly not something you want to read." # use same way as Summary in CIX
md.volumeCount = 4096
md.criticalRating = "Worst Comic Ever"
md.country = "US"
md.alternateSeries = "None"
md.alternateNumber = 4.4
md.alternateCount = 4444
md.imprint = "Welch Publishing"
md.notes = "This doesn't actually exist"
md.webLink = "https://example.com/series name/1"
md.format = "Box Set"
md.manga = "Yes"
md.blackAndWhite = False
md.pageCount = 4
md.maturityRating = "Everyone"
md.storyArc = "None of your buisness"
md.seriesGroup = "Advertures of buisness"
md.scanInfo = "(lordwelch)"
md.characters = "lordwelch, Welch"
md.teams = "None"
md.locations = "Earth, 444 B.C."
md.credits = [dict({"role": "Everything", "person": "lordwelch", "primary": True})]
md.tags = ["testing", "not testing", "fake"]
md.pages = [dict({"Image": "0", "Type": "Front Cover"}), dict({"Image": "1", "Type": "Story"})]
# Some CoMet-only items
md.price = 0.00
md.isVersionOf = "SERIES #1"
md.rights = "None"
md.identifier = "LW4444-Comic"
md.lastMark = "0"
md.coverImage = "https://example.com/series name/1/cover"
self.renamer = FileRenamer(md)
self.renamer.setTemplate(str(self.leRenameTemplate.text()))
self.renamer.setIssueZeroPadding(self.settings.rename_issue_number_padding)
self.renamer.setSmartCleanup(self.settings.rename_use_smart_string_cleanup)
def settingsToForm(self):
# Copy values from settings to form
self.leRarExePath.setText(self.settings.rar_exe_path)
self.leUnrarLibPath.setText(self.settings.unrar_lib_path)
self.leNameLengthDeltaThresh.setText(str(self.settings.id_length_delta_thresh))
self.tePublisherBlacklist.setPlainText(self.settings.id_publisher_blacklist)
if self.settings.check_for_new_version:
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
if self.settings.parse_scan_info:
self.cbxParseScanInfo.setCheckState(QtCore.Qt.Checked)
if self.settings.use_series_start_as_volume:
self.cbxUseSeriesStartAsVolume.setCheckState(QtCore.Qt.Checked)
if self.settings.clear_form_before_populating_from_cv:
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.Checked)
if self.settings.remove_html_tables:
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.Checked)
self.leKey.setText(str(self.settings.cv_api_key))
if self.settings.assume_lone_credit_is_primary:
self.cbxAssumeLoneCreditIsPrimary.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_characters_to_tags:
self.cbxCopyCharactersToTags.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_teams_to_tags:
self.cbxCopyTeamsToTags.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_locations_to_tags:
self.cbxCopyLocationsToTags.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_storyarcs_to_tags:
self.cbxCopyStoryArcsToTags.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_notes_to_comments:
self.cbxCopyNotesToComments.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_weblink_to_comments:
self.cbxCopyWebLinkToComments.setCheckState(QtCore.Qt.Checked)
if self.settings.apply_cbl_transform_on_cv_import:
self.cbxApplyCBLTransformOnCVIMport.setCheckState(QtCore.Qt.Checked)
if self.settings.apply_cbl_transform_on_bulk_operation:
self.cbxApplyCBLTransformOnBatchOperation.setCheckState(QtCore.Qt.Checked)
self.leRenameTemplate.setText(self.settings.rename_template)
self.leIssueNumPadding.setText(str(self.settings.rename_issue_number_padding))
if self.settings.rename_use_smart_string_cleanup:
self.cbxSmartCleanup.setCheckState(QtCore.Qt.Checked)
if self.settings.rename_extension_based_on_archive:
self.cbxChangeExtension.setCheckState(QtCore.Qt.Checked)
if self.settings.rename_move_dir:
self.cbxMoveFiles.setCheckState(QtCore.Qt.Checked)
self.leDirectory.setText(self.settings.rename_dir)
def accept(self):
self.configRenamer()
try:
new_name = self.renamer.determineName("test.cbz")
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
"Your rename template is invalid!"
"<br/><br/>{}<br/><br/>"
"Please consult the template help in the "
"settings and the documentation on the format at "
"<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
"https://docs.python.org/3/library/string.html#format-string-syntax</a>".format(e),
)
return
# Copy values from form to settings and save
self.settings.rar_exe_path = str(self.leRarExePath.text())
# Don't accept the form info if we have our own unrar lib
if not self.settings.haveOwnUnrarLib():
self.settings.unrar_lib_path = str(self.leUnrarLibPath.text())
# make sure rar program is now in the path for the rar class
if self.settings.rar_exe_path:
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
if self.settings.unrar_lib_path:
os.environ["UNRAR_LIB_PATH"] = self.settings.unrar_lib_path
# This doesn't do anything... we need to restart!
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
self.leNameLengthDeltaThresh.setText("0")
if not str(self.leIssueNumPadding.text()).isdigit():
self.leIssueNumPadding.setText("0")
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
self.settings.id_publisher_blacklist = str(self.tePublisherBlacklist.toPlainText())
self.settings.parse_scan_info = self.cbxParseScanInfo.isChecked()
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
self.settings.cv_api_key = str(self.leKey.text())
ComicVineTalker.api_key = self.settings.cv_api_key.strip()
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
self.settings.copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
self.settings.rename_template = str(self.leRenameTemplate.text())
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
self.settings.rename_dir = self.leDirectory.text()
self.settings.save()
QtWidgets.QDialog.accept(self)
if self.priorUnrarLibPath != self.settings.unrar_lib_path:
QtWidgets.QMessageBox.information(self, "UnRar Library Change", "ComicTagger will need to be restarted for changes to take effect.")
def selectRar(self):
self.selectFile(self.leRarExePath, "RAR")
def selectUnrar(self):
self.selectFile(self.leUnrarLibPath, "UnRAR")
def clearCache(self):
ImageFetcher().clearCache()
ComicVineCacher().clearCache()
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
def testAPIKey(self):
if ComicVineTalker().testKey(str(self.leKey.text()).strip()):
QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!")
else:
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
def resetSettings(self):
self.settings.reset()
self.settingsToForm()
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
def selectFile(self, control, name):
dialog = QtWidgets.QFileDialog(self)
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
if platform.system() == "Windows":
if name == "RAR":
filter = self.tr("Rar Program (Rar.exe)")
else:
filter = self.tr("Libraries (*.dll)")
dialog.setNameFilter(filter)
else:
# QtCore.QDir.Executable | QtCore.QDir.Files)
dialog.setFilter(QtCore.QDir.Files)
pass
dialog.setDirectory(os.path.dirname(str(control.text())))
if name == "RAR":
dialog.setWindowTitle("Find " + name + " program")
else:
dialog.setWindowTitle("Find " + name + " library")
if dialog.exec_():
fileList = dialog.selectedFiles()
control.setText(str(fileList[0]))
def showRenameTab(self):
self.tabWidget.setCurrentIndex(5)
def showTemplateHelp(self):
TemplateHelpWin = TemplateHelpWindow(self)
TemplateHelpWin.setModal(False)
TemplateHelpWin.show()
class TemplateHelpWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(TemplateHelpWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile("TemplateHelp.ui"), self)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>702</width>
<height>452</height>
</rect>
</property>
<property name="windowTitle">
<string>Template Help</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<item>
<widget class="QTextBrowser" name="textBrowser">
<property name="html">
<string>&lt;html&gt;
&lt;head/&gt;
&lt;body&gt;
&lt;h1 style=&quot;text-align: center&quot;&gt;Template help&lt;/h1&gt;
&lt;p&gt;The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the
&lt;a href=&quot;https://docs.python.org/3/library/string.html#format-string-syntax&quot;&gt;Python 3 documentation&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;Accepts the following variables:
{isEmpty}&#009;&#009;(boolean)
{tagOrigin}&#009;&#009;(string)
{series}&#009;&#009;(string)
{issue}&#009;&#009;(string)
{title}&#009;&#009;(string)
{publisher}&#009;&#009;(string)
{month}&#009;&#009;(integer)
{year}&#009;&#009;(integer)
{day}&#009;&#009;(integer)
{issueCount}&#009;(integer)
{volume}&#009;&#009;(integer)
{genre}&#009;&#009;(string)
{language}&#009;&#009;(string)
{comments}&#009;&#009;(string)
{volumeCount}&#009;(integer)
{criticalRating}&#009;(string)
{country}&#009;&#009;(string)
{alternateSeries}&#009;(string)
{alternateNumber}&#009;(string)
{alternateCount}&#009;(integer)
{imprint}&#009;&#009;(string)
{notes}&#009;&#009;(string)
{webLink}&#009;&#009;(string)
{format}&#009;&#009;(string)
{manga}&#009;&#009;(string)
{blackAndWhite}&#009;(boolean)
{pageCount}&#009;&#009;(integer)
{maturityRating}&#009;(string)
{storyArc}&#009;&#009;(string)
{seriesGroup}&#009;(string)
{scanInfo}&#009;&#009;(string)
{characters}&#009;(string)
{teams}&#009;&#009;(string)
{locations}&#009;&#009;(string)
{credits}&#009;&#009;(list of dict({&apos;role&apos;: &apos;str&apos;, &apos;person&apos;: &apos;str&apos;, &apos;primary&apos;: boolean}))
{tags}&#009;&#009;(list of str)
{pages}&#009;&#009;(list of dict({&apos;Image&apos;: &apos;str(int)&apos;, &apos;Type&apos;: &apos;str&apos;}))
CoMet-only items:
{price}&#009;&#009;(float)
{isVersionOf}&#009;(string)
{rights}&#009;&#009;(string)
{identifier}&#009;(string)
{lastMark}&#009;&#009;(string)
{coverImage}&#009;(string)
Examples:
{series} {issue} ({year})
Spider-Geddon 1 (2018)
{series} #{issue} - {title}
Spider-Geddon #1 - New Players; Check In
&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogMatchSelect</class>
<widget class="QDialog" name="dialogMatchSelect">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>943</width>
<height>467</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Match</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QWidget" name="archiveCoverContainer" native="true">
<property name="minimumSize">
<size>
<width>200</width>
<height>350</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>350</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>7</verstretch>
</sizepolicy>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="rowCount">
<number>0</number>
</property>
<property name="columnCount">
<number>4</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Series</string>
</property>
</column>
<column>
<property name="text">
<string>Publisher</string>
</property>
</column>
<column>
<property name="text">
<string>Date</string>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
</property>
</column>
</widget>
<widget class="QTextEdit" name="teDescription">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>3</verstretch>
</sizepolicy>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QWidget" name="altCoverContainer" native="true">
<property name="minimumSize">
<size>
<width>200</width>
<height>350</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>350</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogMatchSelect</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogMatchSelect</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogIssueSelect</class>
<widget class="QDialog" name="dialogIssueSelect">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>413</height>
</rect>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Issue Identification Progress</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QWidget" name="archiveCoverContainer">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>110</width>
<height>165</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>110</width>
<height>165</height>
</size>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QWidget" name="testCoverContainer">
<property name="minimumSize">
<size>
<width>110</width>
<height>165</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>110</width>
<height>165</height>
</size>
</property>
</widget>
</item>
<item row="0" column="2">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="textEdit">
<property name="font">
<font>
<family>Courier</family>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogIssueSelect</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogIssueSelect</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,249 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogExport</class>
<widget class="QDialog" name="dialogExport">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>519</width>
<height>378</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Auto-Tag</string>
</property>
<property name="modal">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="gridLayout">
<item row="7" column="0">
<widget class="QCheckBox" name="cbxSpecifySearchString">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Specify series search string for all selected archives:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Save on low confidence match</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="cbxIgnoreLeadingDigitsInFilename">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Ignore leading (sequence) numbers in filename</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="cbxRemoveAfterSuccess">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Remove archives from list after successful tagging</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="cbxWaitForRateLimit">
<property name="text">
<string>Wait and retry when Comic Vine rate limit is exceeded (experimental)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="cbxDontUseYear">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Don't use publication year in indentification process</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="cbxAssumeIssueOne">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>If no issue number, assume &quot;1&quot;</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxAutoImprint">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Checks the publisher against a list of imprints.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Auto Imprint</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLineEdit" name="leNameLengthMatchTolerance">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLineEdit" name="leSearchString">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Adjust Name Length Match Tolerance:</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogExport</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>346</x>
<y>187</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>104</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogExport</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>346</x>
<y>187</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>104</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>coverImageWidget</class>
<widget class="QWidget" name="coverImageWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>292</width>
<height>353</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>0</number>
</property>
<property name="verticalSpacing">
<number>4</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>2</number>
</property>
<item>
<widget class="QPushButton" name="btnLeft">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnRight">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<widget class="QLabel" name="lblImage">
<property name="geometry">
<rect>
<x>60</x>
<y>50</y>
<width>91</width>
<height>61</height>
</rect>
</property>
<property name="toolTip">
<string>Double-click to expand</string>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogExport</class>
<widget class="QDialog" name="dialogExport">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>533</width>
<height>202</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Export to Zip Archive</string>
</property>
<property name="modal">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="cbxAddToList">
<property name="text">
<string>Add New Archive to ComicTagger list</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="cbxDeleteOriginal">
<property name="text">
<string>Delete Original RAR (Not recommended)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>When Filename already exists:</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QRadioButton" name="radioDontCreate">
<property name="text">
<string>Don't Export</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="radioCreateNew">
<property name="text">
<string>Create New Archive With Unique Name (Number appended)</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogExport</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>346</x>
<y>187</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>104</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogExport</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>346</x>
<y>187</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>104</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>pageListEditor</class>
<widget class="QWidget" name="pageListEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>527</width>
<height>323</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTableWidget" name="twList">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>61</number>
</attribute>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>36</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>File</string>
</property>
<property name="toolTip">
<string>File Name</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>CR</string>
</property>
<property name="toolTip">
<string>Has ComicRack Tags</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>CBL</string>
</property>
<property name="toolTip">
<string>Has ComicBookLover Tags</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
<property name="toolTip">
<string>Archive Type</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>R/O</string>
</property>
<property name="toolTip">
<string>Read-Only</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Folder</string>
</property>
<property name="toolTip">
<string>File Location</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QDialog" name="Form">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>817</width>
<height>455</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="windowOpacity">
<double>1.000000000000000</double>
</property>
<widget class="QLabel" name="lblImage">
<property name="geometry">
<rect>
<x>300</x>
<y>120</y>
<width>66</width>
<height>17</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogIssueSelect</class>
<widget class="QDialog" name="dialogIssueSelect">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>872</width>
<height>550</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Issue</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>7</verstretch>
</sizepolicy>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="rowCount">
<number>0</number>
</property>
<property name="columnCount">
<number>3</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Issue</string>
</property>
</column>
<column>
<property name="text">
<string>Date</string>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
</widget>
<widget class="QTextEdit" name="teDescription">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>3</verstretch>
</sizepolicy>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QWidget" name="coverImageContainer" native="true">
<property name="minimumSize">
<size>
<width>300</width>
<height>450</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>450</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogIssueSelect</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogIssueSelect</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogMatchSelect</class>
<widget class="QDialog" name="dialogMatchSelect">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>943</width>
<height>467</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Match</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QWidget" name="archiveCoverContainer" native="true">
<property name="minimumSize">
<size>
<width>200</width>
<height>350</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>350</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>7</verstretch>
</sizepolicy>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="rowCount">
<number>0</number>
</property>
<property name="columnCount">
<number>4</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Series</string>
</property>
</column>
<column>
<property name="text">
<string>Publisher</string>
</property>
</column>
<column>
<property name="text">
<string>Date</string>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
</property>
</column>
</widget>
<widget class="QTextEdit" name="teDescription">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>3</verstretch>
</sizepolicy>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QWidget" name="altCoverContainer" native="true">
<property name="minimumSize">
<size>
<width>200</width>
<height>350</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>350</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogMatchSelect</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogMatchSelect</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>429</width>
<height>637</height>
<width>369</width>
<height>582</height>
</rect>
</property>
<property name="sizePolicy">
@ -20,10 +20,31 @@
<string>Page Browser</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<property name="horizontalSpacing">
<number>0</number>
</property>
<property name="verticalSpacing">
<number>2</number>
</property>
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="lblPage">
<widget class="QWidget" name="pageContainer" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
@ -36,37 +57,47 @@
<height>300</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>20</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="topMargin">
<number>4</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnPrev">
<property name="text">
<string>&lt;&lt;</string>
<string/>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@ -81,10 +112,26 @@
<item>
<widget class="QPushButton" name="btnNext">
<property name="text">
<string>&gt;&gt;</string>
<string/>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>

View File

@ -37,6 +37,9 @@
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
</item>
<item>
@ -98,32 +101,13 @@
</layout>
</item>
<item>
<widget class="QFrame" name="frame">
<widget class="QWidget" name="pageContainer" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>90</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>151</width>
<height>141</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
</item>
</layout>

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>556</width>
<width>650</width>
<height>287</height>
</rect>
</property>
@ -28,7 +28,11 @@
</item>
<item>
<widget class="QTextEdit" name="textEdit">
<property name="readOnly">
<property name="font">
<font>
<family>Courier</family>
</font>
</property> <property name="readOnly">
<bool>true</bool>
</property>
</widget>

View File

@ -0,0 +1,87 @@
"""Some utilities for the GUI"""
# import StringIO
# from PIL import Image
from comictaggerlib.settings import ComicTaggerSettings
try:
from PyQt5 import QtGui
qt_available = True
except ImportError:
qt_available = False
if qt_available:
def reduceWidgetFontSize(widget, delta=2):
f = widget.font()
if f.pointSize() > 10:
f.setPointSize(f.pointSize() - delta)
widget.setFont(f)
def centerWindowOnScreen(window):
"""Center the window on screen.
This implementation will handle the window
being resized or the screen resolution changing.
"""
# Get the current screens' dimensions...
screen = QtGui.QDesktopWidget().screenGeometry()
# ... and get this windows' dimensions
mysize = window.geometry()
# The horizontal position is calculated as screen width - window width
# / 2
hpos = (screen.width() - window.width()) / 2
# And vertical position the same, but with the height dimensions
vpos = (screen.height() - window.height()) / 2
# And the move call repositions the window
window.move(hpos, vpos)
def centerWindowOnParent(window):
top_level = window
while top_level.parent() is not None:
top_level = top_level.parent()
# Get the current screens' dimensions...
main_window_size = top_level.geometry()
# ... and get this windows' dimensions
mysize = window.geometry()
# The horizontal position is calculated as screen width - window width
# /2
hpos = (main_window_size.width() - window.width()) / 2
# And vertical position the same, but with the height dimensions
vpos = (main_window_size.height() - window.height()) / 2
# And the move call repositions the window
window.move(hpos + main_window_size.left(), vpos + main_window_size.top())
try:
import io
from PIL import Image, WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False
def getQImageFromData(image_data):
img = QtGui.QImage()
success = img.loadFromData(image_data)
if not success:
try:
if pil_available:
# Qt doesn't understand the format, but maybe PIL does
# so try to convert the image data to uncompressed tiff
# format
im = Image.open(io.StringIO(image_data))
output = io.StringIO()
im.save(output, format="PNG")
success = img.loadFromData(output.getvalue())
except Exception as e:
pass
# if still nothing, go with default image
if not success:
img.load(ComicTaggerSettings.getGraphic("nocover.png"))
return img

View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogRename</class>
<widget class="QDialog" name="dialogRename">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>801</width>
<height>360</height>
</rect>
</property>
<property name="windowTitle">
<string>Archive Rename</string>
</property>
<property name="modal">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string> Preview:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QTableWidget" name="twList">
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<column>
<property name="text">
<string>Folder</string>
</property>
</column>
<column>
<property name="text">
<string>Old Name</string>
</property>
</column>
<column>
<property name="text">
<string>New Name</string>
</property>
</column>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="btnSettings">
<property name="text">
<string>Rename Settings</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogRename</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>346</x>
<y>187</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>104</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogRename</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>346</x>
<y>187</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>104</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,863 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogCreditEditor</class>
<widget class="QDialog" name="dialogCreditEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>702</width>
<height>432</height>
</rect>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>General</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="grpBoxBtns">
<property name="title">
<string/>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="1" column="0">
<widget class="QPushButton" name="btnResetSettings">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Default Settings</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="lblDefaultSettings">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Revert to default settings</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="btnClearCache">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Clear Cache</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>If you need to free up the disk space, or the responses seems out of date, clear the online cache.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="cbxCheckForNewVersion">
<property name="text">
<string>Check for new version on startup</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Identifier</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These settings are for the automatic issue identifier which searches online for matches. They will not affect &amp;quot;manual&amp;quot; searching.&lt;/p&gt;&lt;p&gt;Hover the mouse over an entry field for more info.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>Default Name Length Match Tolerance:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leNameLengthDeltaThresh">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Publisher Blacklist:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPlainTextEdit" name="tePublisherBlacklist">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_6">
<attribute name="title">
<string>Filename Parser</string>
</attribute>
<widget class="QCheckBox" name="cbxParseScanInfo">
<property name="geometry">
<rect>
<x>30</x>
<y>30</y>
<width>421</width>
<height>25</height>
</rect>
</property>
<property name="text">
<string>Parse Scan Info From Filename (Experimental)</string>
</property>
</widget>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Comic Vine</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="grpBoxCVTop">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
<property name="text">
<string>Use Series Start Date as Volume</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbxClearFormBeforePopulating">
<property name="text">
<string>Clear Form Before Importing Comic Vine data</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbxRemoveHtmlTables">
<property name="text">
<string>Remove HTML tables from CV summary field</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="grpBoxKey">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="1" column="1">
<widget class="QLineEdit" name="leKey">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="readOnly">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Comic Vine API Key</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="lblKeyHelp">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;A personal API key from &lt;a href=&quot;http://www.comicvine.com/api/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Comic Vine&lt;/span&gt;&lt;/a&gt; is recommended in order to search for tag data. Login (or create a new account) there to get your key, and enter it below.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnTestKey">
<property name="text">
<string>Tesk Key</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_4">
<attribute name="title">
<string>CBL</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QCheckBox" name="cbxApplyCBLTransformOnCVIMport">
<property name="text">
<string>Apply CBL Transforms on ComicVine Import</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="cbxApplyCBLTransformOnBatchOperation">
<property name="text">
<string>Apply CBL Transforms on Batch Copy Operations to CBL Tags</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>CBL Transforms</string>
</property>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>11</x>
<y>21</y>
<width>251</width>
<height>199</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_7">
<item row="3" column="0">
<widget class="QCheckBox" name="cbxCopyLocationsToTags">
<property name="text">
<string>Copy Locations to Generic Tags</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="cbxAssumeLoneCreditIsPrimary">
<property name="text">
<string>Assume Lone Credit Is Primary</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="cbxCopyCharactersToTags">
<property name="text">
<string>Copy Characters to Generic Tags</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="cbxCopyTeamsToTags">
<property name="text">
<string>Copy Teams to Generic Tags</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="cbxCopyNotesToComments">
<property name="text">
<string>Copy Notes to Comments</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxCopyWebLinkToComments">
<property name="text">
<string>Copy Web Link to Comments</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="cbxCopyStoryArcsToTags">
<property name="text">
<string>Copy Story Arcs to Generic Tags</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_5">
<attribute name="title">
<string>Rename</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="lblTemplate">
<property name="text">
<string>Template:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leRenameTemplate">
<property name="toolTip">
<string>&lt;pre&gt;The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
Accepts the following variables:
{isEmpty} (boolean)
{tagOrigin} (string)
{series} (string)
{issue} (string)
{title} (string)
{publisher} (string)
{month} (integer)
{year} (integer)
{day} (integer)
{issueCount} (integer)
{volume} (integer)
{genre} (string)
{language} (string)
{comments} (string)
{volumeCount} (integer)
{criticalRating} (string)
{country} (string)
{alternateSeries} (string)
{alternateNumber} (string)
{alternateCount} (integer)
{imprint} (string)
{notes} (string)
{webLink} (string)
{format} (string)
{manga} (string)
{blackAndWhite} (boolean)
{pageCount} (integer)
{maturityRating} (string)
{storyArc} (string)
{seriesGroup} (string)
{scanInfo} (string)
{characters} (string)
{teams} (string)
{locations} (string)
{credits} (list of dict({'role': 'str', 'person': 'str', 'primary': boolean}))
{tags} (list of str)
{pages} (list of dict({'Image': 'str(int)', 'Type': 'str'}))
CoMet-only items:
{price} (float)
{isVersionOf} (string)
{rights} (string)
{identifier} (string)
{lastMark} (string)
{coverImage} (string)
Examples:
{series} {issue} ({year})
{series} #{issue} - {title}
&lt;/pre&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="btnTemplateHelp">
<property name="text">
<string>Template Help</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblPadding">
<property name="text">
<string>Issue # Zero Padding</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="leIssueNumPadding">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Issue # Zero Padding&lt;/span&gt; dictates if the issue number should be padded on left with zeros. A value of 2, for example, means that the number will always be at least two digits.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="cbxSmartCleanup">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&amp;quot;Smart Text Cleanup&amp;quot; &lt;/span&gt;will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Use Smart Text Cleanup (Experimental)</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="cbxChangeExtension">
<property name="text">
<string>Change Extension Based On Archive Type</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxMoveFiles">
<property name="toolTip">
<string>If checked moves files to specified folder</string>
</property>
<property name="text">
<string>Move files when renaming</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="lblDirectory">
<property name="text">
<string>Destination Directory:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="leDirectory"/>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_7">
<attribute name="title">
<string>RAR Tools</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="grpBoxUnrar">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="lblUnrar">
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>UnRAR library</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leUnrarLibPath">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnBrowseUnrar">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="lblUnrarHelp">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In order to read CBR/RAR archives, you will need to have the unrar library from &lt;a href=&quot;www.win-rar.com/download.html&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;WinRAR&lt;/span&gt;&lt;/a&gt; installed. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="grpBoxRar">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>RAR program</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leRarExePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnBrowseRar">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QLabel" name="lblRarHelp">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In order to write to CBR/RAR archives, you will need to have the shareware tools from &lt;a href=&quot;www.win-rar.com/download.html&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;WinRAR&lt;/span&gt;&lt;/a&gt; installed. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogCreditEditor</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogCreditEditor</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>801</width>
<height>470</height>
<width>849</width>
<height>476</height>
</rect>
</property>
<property name="windowTitle">
@ -20,7 +20,7 @@
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="labelThumbnail">
<widget class="QWidget" name="imageContainer" native="true">
<property name="minimumSize">
<size>
<width>300</width>
@ -33,18 +33,6 @@
<height>450</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
@ -54,24 +42,22 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
<verstretch>7</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>250</height>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
@ -119,22 +105,17 @@
</widget>
<widget class="QTextEdit" name="teDetails">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
<verstretch>3</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>200</height>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
@ -149,7 +130,7 @@
<item>
<widget class="QPushButton" name="btnAutoSelect">
<property name="text">
<string>Auto-Select</string>
<string>Auto-Identify</string>
</property>
</widget>
</item>

Some files were not shown because too many files have changed in this diff Show More