Compare commits

..

216 Commits

Author SHA1 Message Date
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
122 changed files with 6271 additions and 3277 deletions

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include README.txt
include release_notes.txt
include requirements.txt
recursive-include scripts *.py *.txt

View File

@ -1,25 +1,60 @@
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
VERSION_STR := $(shell grep version $(TAGGER_BASE)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
TAGGER_BASE ?:= $(HOME)/Dropbox/tagger/comictagger
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
VERSION_STR := $(shell grep version $(TAGGER_SRC)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
PASSWORD := $(shell cat $(TAGGER_BASE)/project_password.txt)
UPLOAD_TOOL := $(TAGGER_BASE)/google/googlecode_upload.py
all: clean
clean:
rm -f *~ *.pyc *.pyo
rm -f logdict*.log
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
make -C windows clean
rm -rf build
zip:
cd release; \
rm -rf *zip comictagger-src-$(VERSION_STR) ; \
svn export 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)
pydist:
mkdir -p release
rm -f release/*.zip
python setup.py sdist --formats=zip #,gztar
mv dist/comictagger-$(VERSION_STR).zip release
@echo When satisfied with release, do this:
@echo make svn_tag
remove_test_install:
sudo rm -rf /usr/local/bin/comictagger.py
sudo rm -rf /usr/local/lib/python2.7/dist-packages/comictagger*
#deb:
# fpm -s python -t deb \
# -n 'comictagger' \
# --category 'utilities' \
# --maintainer 'comictagger@gmail.com' \
# --after-install debian_scripts/after_install.sh \
# --before-remove debian_scripts/before_remove.sh \
# -d 'python >= 2.6' \
# -d 'python < 2.8' \
# -d 'python-imaging' \
# -d 'python-bs4' \
# --deb-suggests 'rar' \
# --deb-suggests 'unrar-free' \
# --python-install-bin /usr/share/comictagger \
# --python-install-lib /usr/share/comictagger \
# setup.py
#
# # For now, don't require PyQt, since command-line is available without it
# #-d 'python-qt4 >= 4.8'
upload:
$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Source" -l Featured,Type-Source -u beville -w $(PASSWORD) "release/comictagger-$(VERSION_STR).zip"
$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Mac OS X" -l Featured,Type-Archive -u beville -w $(PASSWORD) "release/ComicTagger-$(VERSION_STR).dmg"
$(UPLOAD_TOOL) -p comictagger -s "ComicTagger $(VERSION_STR) Windows" -l Featured,Type-Installer -u beville -w $(PASSWORD) "release/ComicTagger v$(VERSION_STR).exe"
python setup.py register
svn_tag:
svn copy https://comictagger.googlecode.com/svn/trunk \
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"

31
README.txt Normal file
View File

@ -0,0 +1,31 @@
ComicTagger is a multi-platform app for writing metadata to comic archives, written in Python and PyQt.
Features:
* Runs on Mac OSX, Microsoft Windows, and Linux systems
* Communicates with an online database (Comic Vine) for acquiring metadata
* Uses image processing to automatically match a given archive with the correct issue data
* Batch processing in the GUI for tagging hundreds or more comics at a time
* Reads and writes multiple tagging schemes ( ComicBookLover and ComicRack, with more planned).
* Reads and writes RAR and Zip archives (external tools needed for writing RAR)
* Command line interface (CLI) on all platforms (including Windows), which supports batch operations, and which can be used in native scripts for complex operations. For example, to recusrively scrape and tag all archives in a folder
comictagger.py -R -s -o -f -t cr -v -i --nooverwrite /path/to/comics/
For details, screenshots, release notes, and more, visit http://code.google.com/p/comictagger/
Requires:
* python 2.6 or 2.7
* configparser
* python imaging (PIL) >= 1.1.6
* beautifulsoup > 4.1
Optional requirement (for GUI):
* pyqt4
Install and run:
* ComicTagger can be run directly from this directory, using the launcher script "comictagger.py"
* To install on your system use: "python setup.py install". Take note in the output where comictagger.py goes!

View File

@ -1,161 +0,0 @@
<?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>831</width>
<height>506</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Match</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="labelCover">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>300</height>
</size>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="twList">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</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>Series</string>
</property>
</column>
<column>
<property name="text">
<string>Publisher</string>
</property>
</column>
<column>
<property name="text">
<string>Date</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QLabel" name="labelThumbnail">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>300</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>
</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

@ -1,507 +1,5 @@
#!/usr/bin/python
#!/usr/bin/env python
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
import locale
filename_encoding = sys.getfilesystemencoding()
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 filerenamer import FileRenamer
from cbltransformer import CBLTransformer
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 )
except ComicVineTalkerException:
print "Network error while getting issue details. Save aborted"
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!"
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 u" {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:
f = f.decode(filename_encoding, 'replace')
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 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 u"{0}: Tag copy seemed to fail!".format( filename )
else:
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
else:
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
else:
print u"{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 u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
return
if batch_mode:
print u"Processing {0}: ".format(filename)
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
# now, search online
if opts.search_online:
if opts.issue_id is not None:
# we were given the actual ID to search with
try:
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
except ComicVineTalkerException:
print "Network error while getting issue details. Save aborted"
return None
if cv_md is None:
print "No match for ID {0} was found.".format(opts.issue_id)
return None
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!"
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 = u"{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"
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 )
new_name = renamer.determineName( filename, ext=new_ext )
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 u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
#-----------------------------
def main():
# try to make stdout encodings happy for unicode
if platform.system() == "Darwin":
preferred_encoding = "utf-8"
else:
preferred_encoding = locale.getpreferredencoding()
sys.stdout = codecs.getwriter(preferred_encoding)(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.file_list, 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()
if __name__ == '__main__':
ctmain()

View File

@ -120,7 +120,9 @@ class RarFileImplementation(object):
if len(accum)==2:
data = {}
data['index'] = i
data['filename'] = accum[0].strip()
#!!!ATB - changed this because it was choking when a folder or file started with a space.
#!!! now, just strip off the first char in the string
data['filename'] = accum[0].rstrip()[1:]
info = re_spaces.split(accum[1].strip())
data['size'] = int(info[0])
attr = info[5]

View File

View File

@ -26,7 +26,10 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
from imagefetcher import ImageFetcher
from settings import ComicTaggerSettings
from options import MetaDataStyle
from comicarchive import MetaDataStyle
from coverimagewidget import CoverImageWidget
from comicvinetalker import ComicVineTalker
import utils
class AutoTagMatchWindow(QtGui.QDialog):
@ -35,11 +38,28 @@ class AutoTagMatchWindow(QtGui.QDialog):
def __init__(self, parent, match_set_list, style, fetch_func):
super(AutoTagMatchWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagmatchwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('matchselectionwindow.ui' ), self)
self.skipButton = QtGui.QPushButton(self.tr("Skip"))
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
gridlayout.addWidget( self.altCoverWidget )
gridlayout.setContentsMargins(0,0,0,0)
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.ArchiveMode )
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
gridlayout.addWidget( self.archiveCoverWidget )
gridlayout.setContentsMargins(0,0,0,0)
utils.reduceWidgetFontSize( self.twList )
utils.reduceWidgetFontSize( self.teDescription, 1 )
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.skipButton = QtGui.QPushButton(self.tr("Skip to Next"))
self.buttonBox.addButton(self.skipButton, QtGui.QDialogButtonBox.ActionRole)
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Next")
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Write Tags")
self.match_set_list = match_set_list
self.style = style
@ -57,18 +77,18 @@ class AutoTagMatchWindow(QtGui.QDialog):
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.skipButton.setDisabled(True)
self.buttonBox.button(QtGui.QDialogButtonBox.Cancel).setDisabled(True)
#self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept")
self.skipButton.setText(self.tr("Skip"))
self.setCoverImage()
self.populateTable()
self.twList.resizeColumnsToContents()
self.current_row = 0
self.twList.selectRow( 0 )
path = self.current_match_set.ca.path
self.setWindowTitle( u"Select correct match ({0} of {1}): {2}".format(
self.setWindowTitle( u"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] ))
@ -87,6 +107,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
item_text = match['series']
item = QtGui.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)
@ -99,19 +120,35 @@ class AutoTagMatchWindow(QtGui.QDialog):
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
item_text = ""
month_str = u""
year_str = u"????"
if match['month'] is not None:
item_text = u"{0}/".format(match['month'])
month_str = u"-{0:02d}".format(int(match['month']))
if match['year'] is not None:
item_text += u"{0}".format(match['year'])
else:
item_text += u"????"
year_str = u"{0}".format(match['year'])
item_text = year_str + month_str
item = QtGui.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 = QtGui.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 ):
@ -123,34 +160,22 @@ class AutoTagMatchWindow(QtGui.QDialog):
return
if prev is not None and prev.row() == curr.row():
return
self.current_row = curr.row()
# list selection was changed, update the the issue cover
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.cover_fetcher = ImageFetcher( )
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
self.cover_fetcher.fetch( self.current_match_set.matches[self.current_row]['img_url'] )
# called when the image is done loading
def coverFetchComplete( self, image_data, issue_id ):
img = QtGui.QImage()
img.loadFromData( image_data )
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
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
cover_idx = ca.readMetadata(self.style).getCoverPageIndexList()[0]
image_data = ca.getPage( cover_idx )
self.labelCover.setScaledContents(True)
if image_data is not None:
img = QtGui.QImage()
img.loadFromData( image_data )
self.labelCover.setPixmap(QtGui.QPixmap(img))
else:
self.labelCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.archiveCoverWidget.setArchive(ca)
def currentMatch( self ):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data( QtCore.Qt.UserRole ).toPyObject()[0]
return match
def accept(self):
self.saveMatch()
@ -184,7 +209,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
def saveMatch( self ):
match = self.current_match_set.matches[self.current_row]
match = self.currentMatch()
ca = self.current_match_set.ca
md = ca.readMetadata( self.style )

View File

@ -22,7 +22,8 @@ import sys
from PyQt4 import QtCore, QtGui, uic
import os
from settings import ComicTaggerSettings
from coverimagewidget import CoverImageWidget
import utils
class AutoTagProgressWindow(QtGui.QDialog):
@ -30,36 +31,37 @@ class AutoTagProgressWindow(QtGui.QDialog):
def __init__(self, parent):
super(AutoTagProgressWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagprogresswindow.ui' ), self)
self.lblTest.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.lblArchive.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.isdone = False
uic.loadUi(ComicTaggerSettings.getUIFile('autotagprogresswindow.ui' ), self)
# we can't specify relative font sizes in the UI designer, so
# make font for scroll window a smidge smaller
f = self.textEdit.font()
if f.pointSize() > 10:
f.setPointSize( f.pointSize() - 2 )
self.textEdit.setFont( f )
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.DataMode, False )
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
gridlayout.addWidget( self.archiveCoverWidget )
gridlayout.setContentsMargins(0,0,0,0)
self.testCoverWidget = CoverImageWidget( self.testCoverContainer, CoverImageWidget.DataMode, False )
gridlayout = QtGui.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)
utils.reduceWidgetFontSize( self.textEdit )
def setArchiveImage( self, img_data):
self.setCoverImage( img_data, self.lblArchive )
self.setCoverImage( img_data, self.archiveCoverWidget)
def setTestImage( self, img_data):
self.setCoverImage( img_data, self.lblTest )
self.setCoverImage( img_data, self.testCoverWidget)
def setCoverImage( self, img_data , label):
if img_data is not None:
img = QtGui.QImage()
img.loadFromData( img_data )
label.setPixmap(QtGui.QPixmap(img))
label.setScaledContents(True)
else:
label.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
label.setScaledContents(True)
def setCoverImage( self, img_data , widget):
widget.setImageData( img_data )
QtCore.QCoreApplication.processEvents()
QtCore.QCoreApplication.processEvents()
def reject(self):
QtGui.QDialog.reject(self)
self.isdone = True

View File

@ -32,9 +32,12 @@ class AutoTagStartWindow(QtGui.QDialog):
def __init__( self, parent, settings, msg ):
super(AutoTagStartWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagstartwindow.ui' ), self)
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 )
@ -58,7 +61,7 @@ class AutoTagStartWindow(QtGui.QDialog):
ssTip = (
"""<html>
The <b>series search string</b> specifies the search string to be used for all selected archives.
Use this only when trying to match archives with hard-to-parse filenames. All archives selected
Use this when trying to match archives with hard-to-parse or incorrect filenames. All archives selected
should be from the same series.
</html>"""
)

539
comictaggerlib/cli.py Normal file
View File

@ -0,0 +1,539 @@
#!/usr/bin/python
"""
Comic tagger 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 sys
import signal
import os
import traceback
import time
from pprint import pprint
import json
import platform
import locale
filename_encoding = sys.getfilesystemencoding()
from settings import ComicTaggerSettings
from options import Options
from comicarchive import ComicArchive, MetaDataStyle
from issueidentifier import IssueIdentifier
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from filerenamer import FileRenamer
from cbltransformer import CBLTransformer
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.lowConfidenceMatches = []
self.writeFailures = []
self.fetchDataFailures = []
#-----------------------------
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 )
except ComicVineTalkerException:
print >> sys.stderr, "Network error while getting issue details. Save aborted"
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 >> sys.stderr,"The tag save seemed to fail!"
return False
else:
print >> sys.stderr,"Save complete."
else:
if opts.terse:
print >> sys.stderr,"dry-run option was set, so nothing was written"
else:
print >> sys.stderr,"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 display_match_set_for_choice( label, match_set, opts, settings ):
print u"{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 u" {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(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 )
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings)
md.overlay( cv_md )
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:"
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 len( match_results.fetchDataFailures ) > 0:
print "\nNetwork Data Fetch Failures:"
print "------------------"
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:"
print "------------------"
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:"
print "------------------"
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 >> sys.stderr,"You must specify at least one filename. Use the -h option for more info"
return
match_results = OnlineMatchResults()
for f in opts.file_list:
if type(f) == str:
f = f.decode(filename_encoding, 'replace')
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, settings)
if not os.path.lexists( filename ):
print >> sys.stderr,"Cannot find "+ filename
return
if not ca.seemsToBeAComicArchive():
print >> sys.stderr,"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 >> sys.stderr,"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 = u"{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 u"{0}: Tag removal seemed to fail!".format( filename )
else:
print u"{0}: Removed {1} tags.".format( filename, style_name )
else:
print u"{0}: dry-run. {1} tags not removed".format( filename, style_name )
else:
print u"{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 u"{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
return
if opts.copy_source == opts.data_style:
print u"{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 u"{0}: Tag copy seemed to fail!".format( filename )
else:
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
else:
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
else:
print u"{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 u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
return
if batch_mode:
print u"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:
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
except ComicVineTalkerException:
print >> sys.stderr,"Network error while getting issue details. Save aborted"
match_results.fetchDataFailures.append(filename)
return
if cv_md is None:
print >> sys.stderr,"No match for ID {0} was found.".format(opts.issue_id)
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 >> sys.stderr,"No metadata given to search online with!"
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.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 >> sys.stderr,"Online search: Multiple low confidence matches. Save aborted"
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
return
else:
print >> sys.stderr,"Online search: Multiple good matches. Save aborted"
match_results.multipleMatches.append(MultipleMatch(filename,matches))
return
if low_confidence and opts.abortOnLowConfidence:
print >> sys.stderr,"Online search: Low confidence match. Save aborted"
match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches))
return
if not found_match:
print >> sys.stderr,"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:
match_results.fetchDataFailures.append(filename)
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 = u"{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 >> sys.stderr, msg_hdr + "Can't rename without series name"
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 )
new_name = renamer.determineName( filename, ext=new_ext )
if new_name == os.path.basename(filename):
print >> sys.stderr, 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 u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
elif opts.export_to_zip:
msg_hdr = ""
if batch_mode:
msg_hdr = u"{0}: ".format(filename)
if not ca.isRar():
print >> sys.stderr, msg_hdr + "Archive is not a RAR."
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 >> sys.stderr, msg_hdr + "Error deleting original RAR after export"
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 + u"Dry-run: Would try to create {0}".format(os.path.split(new_file)[1])
if opts.delete_rar_after_export:
msg += u" and delete orginal."
print msg
return
msg = msg_hdr
if export_success:
msg += u"Archive exported successfully to: {0}".format( os.path.split(new_file)[1] )
if opts.delete_rar_after_export and delete_success:
msg += u" (Original deleted) "
else:
msg += u"Archive failed to export!"
print msg

View File

@ -25,6 +25,8 @@ import sys
import tempfile
import subprocess
import platform
import locale
if platform.system() == "Windows":
import _subprocess
import time
@ -40,7 +42,6 @@ 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
@ -48,6 +49,12 @@ from genericmetadata import GenericMetadata, PageType
from filenameparser import FileNameParser
from settings import ComicTaggerSettings
class MetaDataStyle:
CBI = 0
CIX = 1
COMET = 2
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
class ZipArchiver:
def __init__( self, path ):
@ -68,12 +75,12 @@ class ZipArchiver:
try:
data = zf.read( archive_file )
except zipfile.BadZipfile as e:
print "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
zf.close()
raise IOError
except Exception as e:
zf.close()
print "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
raise IOError
finally:
zf.close()
@ -112,7 +119,7 @@ class ZipArchiver:
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 )
#print ">> sys.stderr, 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) )
@ -215,7 +222,7 @@ class ZipArchiver:
if not self.writeZipComment( self.path, comment ):
return False
except Exception as e:
print "Error while copying to {0}: {1}".format(self.path, e)
print >> sys.stderr, "Error while copying to {0}: {1}".format(self.path, e)
return False
else:
return True
@ -227,9 +234,9 @@ class ZipArchiver:
class RarArchiver:
devnull = None
def __init__( self, path ):
def __init__( self, path, settings ):
self.path = path
self.rar_exe_path = None
self.settings = settings
if RarArchiver.devnull is None:
RarArchiver.devnull = open(os.devnull, "w")
@ -252,7 +259,7 @@ class RarArchiver:
def setArchiveComment( self, comment ):
if self.rar_exe_path is not None:
if self.settings.rar_exe_path is not None:
try:
# write comment to temp file
tmp_fd, tmp_name = tempfile.mkstemp()
@ -263,7 +270,7 @@ class RarArchiver:
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],
subprocess.call([self.settings.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
@ -294,22 +301,22 @@ class RarArchiver:
entries = rarc.read_files( archive_file )
if entries[0][0].size != len(entries[0][1]):
print "readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
print >> sys.stderr, "readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
entries[0][0].size,len(entries[0][1]), self.path, archive_file, tries)
continue
except (OSError, IOError) as e:
print "readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
print >> sys.stderr, "readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
time.sleep(1)
except Exception as e:
print "Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
print >> sys.stderr, "Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
break
else:
#Success"
#entries is a list of of tuples: ( rarinfo, filedata)
if tries > 1:
print "Attempted read_files() {0} times".format(tries)
print >> sys.stderr, "Attempted read_files() {0} times".format(tries)
if (len(entries) == 1):
return entries[0][1]
else:
@ -321,7 +328,7 @@ class RarArchiver:
def writeArchiveFile( self, archive_file, data ):
if self.rar_exe_path is not None:
if self.settings.rar_exe_path is not None:
try:
tmp_folder = tempfile.mkdtemp()
@ -336,7 +343,7 @@ class RarArchiver:
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],
subprocess.call([self.settings.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
@ -352,10 +359,10 @@ class RarArchiver:
return False
def removeArchiveFile( self, archive_file ):
if self.rar_exe_path is not None:
if self.settings.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],
subprocess.call([self.settings.rar_exe_path, 'd','-c-', self.path, archive_file],
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
@ -385,7 +392,7 @@ class RarArchiver:
namelist.append( item.filename )
except (OSError, IOError) as e:
print "getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
print >> sys.stderr, "getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
time.sleep(1)
else:
@ -403,7 +410,7 @@ class RarArchiver:
rarc = UnRAR2.RarFile( self.path )
except (OSError, IOError) as e:
print "getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
print >> sys.stderr, "getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
time.sleep(1)
else:
@ -503,20 +510,21 @@ class ComicArchive:
class ArchiveType:
Zip, Rar, Folder, Unknown = range(4)
def __init__( self, path ):
def __init__( self, path, settings ):
self.path = path
self.ci_xml_filename = 'ComicInfo.xml'
self.comet_default_filename = 'CoMet.xml'
self.resetCache()
self.settings = settings
if self.zipTest():
if self.rarTest():
self.archive_type = self.ArchiveType.Rar
self.archiver = RarArchiver( self.path, settings )
elif 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 )
@ -525,9 +533,9 @@ class ComicArchive:
self.archiver = UnknownArchiver( self.path )
if ComicArchive.logo_data is None:
fname = os.path.join(ComicTaggerSettings.baseDir(), 'graphics','nocover.png' )
fname = ComicTaggerSettings.getGraphic('nocover.png')
with open(fname, 'rb') as fd:
ComicArchive.logo_data = fd.read()
ComicArchive.logo_data = fd.read()
# Clears the cached data
def resetCache( self ):
@ -548,10 +556,6 @@ class ComicArchive:
def rename( self, path ):
self.path = path
self.archiver.path = 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 )
@ -578,7 +582,7 @@ class ComicArchive:
if self.archive_type == self.ArchiveType.Unknown :
return False
elif check_rar_status and self.isRar() and self.archiver.rar_exe_path is None:
elif check_rar_status and self.isRar() and self.settings.rar_exe_path is None:
return False
elif not os.access(self.path, os.W_OK):
@ -603,9 +607,9 @@ class ComicArchive:
ext = os.path.splitext(self.path)[1].lower()
if (
( self.isZip() or self.isRar() or self.isFolder() )
( self.isZip() or self.isRar() ) #or self.isFolder() )
and
( self.getNumberOfPages() > 2)
( self.getNumberOfPages() > 0)
):
return True
@ -666,13 +670,16 @@ class ComicArchive:
try:
image_data = self.archiver.readArchiveFile( filename )
except IOError:
print "Error reading in page. Substituting logo page."
print >> sys.stderr, "Error reading in page. Substituting logo page."
image_data = ComicArchive.logo_data
return image_data
def getPageName( self, index ):
if index is None:
return None
page_list = self.getPageNameList()
num_pages = len( page_list )
@ -681,6 +688,56 @@ class ComicArchive:
return page_list[index]
def getScannerPageIndex( self ):
scanner_page_index = None
#make a guess at the scanner page
name_list = self.getPageNameList()
count = self.getNumberOfPages()
#too few pages to really know
if count < 5:
return None
# count the length of every filename, and count occurences
length_buckets = dict()
for name in name_list:
fname = os.path.split(name)[1]
length = len(fname)
if length_buckets.has_key( length ):
length_buckets[ length ] += 1
else:
length_buckets[ length ] = 1
# sort by most common
sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True)
# statistical mode occurence is first
mode_length = sorted_buckets[0][0]
# we are only going to consider the final image file:
final_name = os.path.split(name_list[count-1])[1]
common_length_list = list()
for name in name_list:
if len(os.path.split(name)[1]) == mode_length:
common_length_list.append( os.path.split(name)[1] )
prefix = os.path.commonprefix(common_length_list)
if mode_length <= 7 and prefix == "":
#probably all numbers
if len(final_name) > mode_length:
scanner_page_index = count-1
# see if the last page doesn't start with the same prefix as most others
elif not final_name.startswith(prefix):
scanner_page_index = count-1
return scanner_page_index
def getPageNameList( self , sort_list=True):
if self.page_list is None:
@ -689,12 +746,19 @@ class ComicArchive:
# 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())
def keyfunc(k):
#hack to account for some weird scanner ID pages
basename=os.path.split(k)[1]
if basename < '0':
k = os.path.join(os.path.split(k)[0], "z" + basename)
return k.lower()
files.sort(key=keyfunc)
# make a sub-list of image files
self.page_list = []
for name in files:
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] and os.path.basename(name)[0] != "." ):
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif" ] and os.path.basename(name)[0] != "." ):
self.page_list.append(name)
return self.page_list
@ -851,13 +915,13 @@ class ComicArchive:
def readRawCoMet( self ):
if not self.hasCoMet():
print self.path, "doesn't have CoMet data!"
print >> sys.stderr, self.path, "doesn't have CoMet data!"
return None
try:
raw_comet = self.archiver.readArchiveFile( self.comet_filename )
except IOError:
print "Error reading in raw CoMet!"
print >> sys.stderr, "Error reading in raw CoMet!"
raw_comet = ""
return raw_comet
@ -908,7 +972,7 @@ class ComicArchive:
data = self.archiver.readArchiveFile( n )
except:
data = ""
print "Error reading in Comet XML for validation!"
print >> sys.stderr, "Error reading in Comet XML for validation!"
if CoMet().validateString( data ):
# since we found it, save it!
self.comet_filename = n
@ -963,6 +1027,9 @@ class ComicArchive:
metadata.year = fnp.year
if fnp.issue_count != "":
metadata.issueCount = fnp.issue_count
if self.settings.parse_scan_info:
if fnp.remainder != "":
metadata.scanInfo = fnp.remainder
metadata.isEmpty = False

View File

@ -89,36 +89,21 @@ class ComicInfoXml:
if md_entry is not None:
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
assign( 'Title', md.title )
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( 'StoryArc', md.storyArc )
assign( 'SeriesGroup', md.seriesGroup )
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"
assign( 'Day', md.day )
# need to specially process the credits, since they are structured differently than CIX
credit_writer_list = list()
@ -181,7 +166,23 @@ class ComicInfoXml:
if len( credit_editor_list ) > 0:
node = ET.SubElement(root, 'Editor')
node.text = utils.listToString( credit_editor_list )
assign( 'Publisher', md.publisher )
assign( 'Imprint', md.imprint )
assign( 'Genre', md.genre )
assign( 'Web', md.webLink )
assign( 'PageCount', md.pageCount )
assign( 'LanguageISO', md.language )
assign( 'Format', md.format )
assign( 'AgeRating', md.maturityRating )
if md.blackAndWhite is not None and md.blackAndWhite:
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
assign( 'Manga', md.manga )
assign( 'Characters', md.characters )
assign( 'Teams', md.teams )
assign( 'Locations', md.locations )
assign( 'ScanInformation', md.scanInfo )
# loop and add the page entries under pages node
if len( md.pages ) > 0:
pages_node = ET.SubElement(root, 'Pages')
@ -229,6 +230,7 @@ class ComicInfoXml:
md.notes = xlate( 'Notes' )
md.year = xlate( 'Year' )
md.month = xlate( 'Month' )
md.day = xlate( 'Day' )
md.publisher = xlate( 'Publisher' )
md.imprint = xlate( 'Imprint' )
md.genre = xlate( 'Genre' )
@ -258,12 +260,14 @@ class ComicInfoXml:
n.tag == 'Letterer' or
n.tag == 'Editor'
):
for name in n.text.split(','):
metadata.addCredit( name.strip(), n.tag )
if n.text is not None:
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" )
if n.text is not None:
for name in n.text.split(','):
metadata.addCredit( name.strip(), "Cover" )
# parse page data now
pages_node = root.find( "Pages" )

View File

@ -27,6 +27,7 @@ import datetime
import ctversion
from settings import ComicTaggerSettings
import utils
class ComicVineCacher:
@ -97,17 +98,23 @@ class ComicVineCacher:
"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," +
"image_url TEXT," +
"image_hash TEXT," +
"thumb_image_url TEXT," +
"thumb_image_hash TEXT," +
"publish_month TEXT," +
"publish_year 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 ) )"
)
@ -149,7 +156,7 @@ class ComicVineCacher:
url,
record['description'])
)
def get_search_results( self, search_term ):
results = list()
@ -184,7 +191,52 @@ class ComicVineCacher:
return results
def add_alt_covers( self, issue_id, url_list ):
con = lite.connect( self.db_file )
with con:
con.text_factory = unicode
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 = unicode
# 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 )
@ -209,18 +261,34 @@ class ComicVineCacher:
}
self.upsert( cur, "volumes", "id", cv_volume_record['id'], data)
# now add in issues
for issue in cv_volume_record['issues']:
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": cv_volume_record['id'],
"name": issue['name'],
"issue_number": issue['issue_number'],
"timestamp": timestamp
"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 ):
@ -234,10 +302,6 @@ class ComicVineCacher:
# 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) ] )
# 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 Issues WHERE timestamp < ?", [ str(a_month_ago) ] )
# fetch
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [ volume_id ] )
@ -257,25 +321,51 @@ class ComicVineCacher:
result['count_of_issues'] = row[3]
result['start_year'] = row[4]
result['issues'] = list()
return result
cur.execute("SELECT id,name,issue_number,image_url,image_hash FROM Issues WHERE volume_id = ?", [ volume_id ] )
def get_volume_issues_info( self, volume_id ):
result = None
con = lite.connect( self.db_file )
with con:
cur = con.cursor()
con.text_factory = unicode
# 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['image_url'] = row[3]
record['image_hash'] = row[4]
result['issues'].append(record)
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)
return result
if len(results) == 0:
return None
return results
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, publish_month, publish_year ):
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url ):
con = lite.connect( self.db_file )
@ -285,10 +375,10 @@ class ComicVineCacher:
timestamp = datetime.datetime.now()
data = {
"image_url": image_url,
"thumb_image_url": thumb_image_url,
"publish_month": publish_month,
"publish_year": publish_year,
"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)
@ -302,13 +392,23 @@ class ComicVineCacher:
cur = con.cursor()
con.text_factory = unicode
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year FROM Issues WHERE id=?", [ issue_id ])
cur.execute("SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?", [ issue_id ])
row = cur.fetchone()
if row[0] is None :
return None, None, None, None
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:
return row[0],row[1],row[2],row[3]
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):

View File

@ -0,0 +1,668 @@
"""
A python class to manage communication with Comic Vine's REST API
"""
"""
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 pprint import pprint
import urllib2, urllib
import math
import re
import time
import datetime
import ctversion
import sys
from bs4 import BeautifulSoup
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
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
import utils
from settings import ComicTaggerSettings
from comicvinecacher import ComicVineCacher
from genericmetadata import GenericMetadata
from issuestring import IssueString
class CVTypeID:
Volume = "4050"
Issue = "4000"
class ComicVineTalkerException(Exception):
pass
class ComicVineTalker(QObject):
logo_url = "http://static.comicvine.com/bundles/comicvinesite/images/logo.png"
def __init__(self, api_key=""):
QObject.__init__(self)
self.api_base_url = "http://www.comicvine.com/api"
# key that is registered to comictagger
self.api_key = '27431e6787042105bd3e47e169a624521f89f3a4'
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 >> sys.stderr, text
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 = parts[0]
if len(parts) > 1:
month = parts[1]
if len(parts) > 2:
day = parts[2]
return day, month, year
def testKey( self ):
test_url = self.api_base_url + "/issue/1/?api_key=" + self.api_key + "&format=json&field_list=name"
resp = urllib2.urlopen( test_url )
content = resp.read()
cv_response = json.loads( content )
# Bogus request, but if the key is wrong, you get error 100: "Invalid API Key"
return cv_response[ 'status_code' ] != 100
def getUrlContent( self, url ):
# connect to server:
# if there is a 500 error, try a few more times before giving up
# any other error, just bail
#print "ATB---", url
for tries in range(3):
try:
resp = urllib2.urlopen( url )
return resp.read()
except urllib2.HTTPError as e:
if e.getcode() == 500:
self.writeLog( "Try #{0}: ".format(tries+1) )
time.sleep(1)
self.writeLog( str(e) + "\n" )
if e.getcode() != 500:
break
except Exception as e:
self.writeLog( str(e) + "\n" )
raise ComicVineTalkerException("Network Error!")
raise ComicVineTalkerException("Error on Comic Vine server")
def searchForSeries( self, series_name , callback=None, refresh_cache=False ):
# remove cruft from the search string
series_name = utils.removearticles( 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
original_series_name = series_name
# We need to make the series name into an "AND"ed query list
query_word_list = series_name.split()
and_list = ['AND'] * (len(query_word_list)-1)
and_list.append('')
# zipper up the two lists
query_list = zip(query_word_list, and_list)
# flatten the list
query_list = [ item for sublist in query_list for item in sublist]
# convert back to a string
query_string = " ".join( query_list ).strip()
#print "Query string = ", query_string
query_string = urllib.quote_plus(query_string.encode("utf-8"))
search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + query_string + "&field_list=name,id,start_year,publisher,image,description,count_of_issues"
content = self.getUrlContent(search_url + "&page=1")
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
return None
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']
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
content = self.getUrlContent(search_url + "&page="+str(page))
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
return None
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 )
#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( original_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) + "/?api_key=" + self.api_key + "&field_list=name,id,start_year,publisher,count_of_issues&format=json"
content = self.getUrlContent(volume_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
return None
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
#---------------------------------
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + "&filter=volume:" + str(series_id) + "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
content = self.getUrlContent(issues_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
return None
#------------------------------------
limit = cv_response['limit']
current_result_count = cv_response['number_of_page_results']
total_result_count = cv_response['number_of_total_results']
#print "ATB total_result_count", total_result_count
#print "ATB 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 "ATB 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']
#print issues_url+ "&offset="+str(offset)
content = self.getUrlContent(issues_url + "&offset="+str(offset))
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
return None
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 = "volume:"
for vid in volume_id_list:
volume_filter += str(vid) + "|"
year_filter = ""
if year is not None and str(year).isdigit():
year_filter = ",cover_date:{0}-1-1|{1}-1-1".format(year, int(year)+1)
issue_number = urllib.quote_plus(unicode(issue_number).encode("utf-8"))
filter = "&filter=" + volume_filter + year_filter + ",issue_number:" + issue_number
issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + filter + "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
content = self.getUrlContent(issues_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
return None
#------------------------------------
limit = cv_response['limit']
current_result_count = cv_response['number_of_page_results']
total_result_count = cv_response['number_of_total_results']
#print "ATB total_result_count", total_result_count
#print "ATB 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 "ATB 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']
#print issues_url+ "&offset="+str(offset)
content = self.getUrlContent(issues_url + "&offset="+str(offset))
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
return None
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']) + "/?api_key=" + self.api_key + "&format=json"
content = self.getUrlContent(issue_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
return None
issue_results = cv_response['results']
else:
return None
# now, map the comicvine 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) + "/?api_key=" + self.api_key + "&format=json"
content = self.getUrlContent(issue_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
return None
issue_results = cv_response['results']
volume_results = self.fetchVolumeData( issue_results['volume']['id'] )
# now, map the comicvine 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 comicvine data to generic metadata
metadata = GenericMetadata()
metadata.series = issue_results['volume']['name']
num_s = IssueString(issue_results['issue_number']).asString()
metadata.issue = num_s
metadata.title = issue_results['name']
metadata.publisher = volume_results['publisher']['name']
metadata.day, metadata.month, metadata.year = self.parseDateStr( issue_results['cover_date'] )
#metadata.issueCount = volume_results['count_of_issues']
metadata.comments = self.cleanup_html(issue_results['description'])
if settings.use_series_start_as_volume:
metadata.volume = 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 person.has_key('role'):
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):
if string is None:
return ""
# 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")
# now strip all other tags
p = re.compile(r'<[^<]*?>')
newstring = p.sub('',string)
newstring = newstring.replace('&nbsp;',' ')
newstring = newstring.replace('&amp;','&')
newstring = newstring.strip()
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) + "/?api_key=" + self.api_key + "&format=json&field_list=image,cover_date,site_detail_url"
content = self.getUrlContent(issue_url)
details = dict()
details['image_url'] = None
details['thumb_image_url'] = None
details['cover_date'] = None
details['site_detail_url'] = None
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
return details
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
resp = urllib2.urlopen( issue_page_url )
content = resp.read()
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 )
alt_cover_url_list = []
# Using knowledge of the layout of the ComicVine issue page here:
# look for the divs that are in the classes 'content-pod' and 'alt-cover'
div_list = soup.find_all( 'div')
covers_found = 0
for d in div_list:
if d.has_key('class'):
c = d['class']
if 'imgboxart' in c and 'issue-cover' in c:
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(str(data))
except:
print >> sys.stderr, "Comic Vine query failed to get JSON data"
print >> sys.stderr, str(data)
return
if cv_response[ 'status_code' ] != 1:
print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] )
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,312 @@
"""
A PyQt4 widget display cover images from either local archive, or from ComicVine
(TODO: This should be re-factored using subclasses!)
"""
"""
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 os
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from settings import ComicTaggerSettings
from genericmetadata import GenericMetadata, PageType
from comicarchive import MetaDataStyle
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from imagefetcher import ImageFetcher
from pageloader import PageLoader
from imagepopup import ImagePopup
import utils
# helper func to allow a label to be clickable
def clickable(widget):
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)
utils.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 = QImage()
img.loadFromData( 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)
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

@ -33,7 +33,7 @@ class CreditEditorWindow(QtGui.QDialog):
def __init__(self, parent, mode, role, name, primary ):
super(CreditEditorWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'crediteditorwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('crediteditorwindow.ui' ), self)
self.mode = mode

View File

@ -1,3 +1,3 @@
# This file should contan only these comments, and the line below.
# Used by packaging makefiles and app
version="1.0.3-beta"
version="1.1.10-beta"

View File

@ -36,9 +36,12 @@ class ExportWindow(QtGui.QDialog):
def __init__( self, parent, settings, msg ):
super(ExportWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'exportwindow.ui' ), self)
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 )

View File

@ -0,0 +1,272 @@
"""
Functions for parsing comic info from filename
This should probably be re-written, but, well, it mostly works!
"""
"""
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.
"""
# Some portions of this code were modified from pyComicMetaThis project
# http://code.google.com/p/pycomicmetathis/
import re
import os
from urllib import unquote
class FileNameParser:
def repl(self, m):
return ' ' * len(m.group())
def fixSpaces( self, string, remove_dashes=True ):
if remove_dashes:
placeholders = ['[-_]',' +']
else:
placeholders = ['[_]',' +']
for ph in placeholders:
string = re.sub(ph, self.repl, string )
return string #.strip()
def getIssueCount( self,filename, issue_end ):
count = ""
filename = filename[issue_end:]
# replace any name seperators with spaces
tmpstr = self.fixSpaces(filename)
found = False
match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE)
if match:
count = match.group()
found = True
if not found:
match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE)
if match:
count = match.group()
found = True
count = count.lstrip("0")
return count
def getIssueNumber( self, filename ):
# Returns a tuple of issue number string, and start and end indexs in the filename
# (The indexes will be used to split the string up for further parsing)
found = False
issue = ''
start = 0
end = 0
# first, look for multiple "--", this means it's formatted differently from most:
if "--" in filename:
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
filename = re.sub("--.*", self.repl, filename)
elif "__" in filename:
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
filename = re.sub("__.*", self.repl, filename)
filename = filename.replace("+", " ")
# replace parenthetical phrases with spaces
filename = re.sub( "\(.*?\)", self.repl, filename)
filename = re.sub( "\[.*?\]", self.repl, filename)
# replace any name seperators with spaces
filename = self.fixSpaces(filename)
# remove any "of NN" phrase with spaces (problem: this could break on some titles)
filename = re.sub( "of [\d]+", self.repl, filename)
#print u"[{0}]".format(filename)
# we should now have a cleaned up filename version with all the words in
# the same positions as original filename
# make a list of each word and its position
word_list = list()
for m in re.finditer("\S+", filename):
word_list.append( (m.group(0), m.start(), m.end()) )
# remove the first word, since it can't be the issue number
if len(word_list) > 1:
word_list = word_list[1:]
else:
#only one word?? just bail.
return issue, start, end
# Now try to search for the likely issue number word in the list
# first look for a word with "#" followed by digits with optional sufix
# this is almost certainly the issue number
for w in reversed(word_list):
if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
found = True
break
# same as above but w/o a '#', and only look at the last word in the list
if not found:
w = word_list[-1]
if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
found = True
# now try to look for a # followed by any characters
if not found:
for w in reversed(word_list):
if re.match("#\S+", w[0]):
found = True
break
if found:
issue = w[0]
start = w[1]
end = w[2]
if issue[0] == '#':
issue = issue[1:]
return issue, start, end
def getSeriesName(self, filename, issue_start ):
# use the issue number string index to split the filename string
if issue_start != 0:
filename = filename[:issue_start]
# in case there is no issue number, remove some obvious stuff
if "--" in filename:
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
filename = re.sub("--.*", self.repl, filename)
elif "__" in filename:
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
filename = re.sub("__.*", self.repl, filename)
filename = filename.replace("+", " ")
tmpstr = self.fixSpaces(filename, remove_dashes=False)
series = tmpstr
volume = ""
#save the last word
last_word = series.split()[-1]
# remove any parenthetical phrases
series = re.sub( "\(.*?\)", "", series)
# search for volume number
match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series)
if match:
series = match.group(1)
volume = match.group(3)
# if a volume wasn't found, see if the last word is a year in parentheses
# since that's a common way to designate the volume
if volume == "":
#match either (YEAR), (YEAR-), or (YEAR-YEAR2)
match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word)
if match:
volume = match.group(2)
series = series.strip()
# if we don't have an issue number (issue_start==0), look
# for hints i.e. "TPB", "one-shot", "OS", "OGN", etc that might
# be removed to help search online
if issue_start == 0:
one_shot_words = [ "tpb", "os", "one-shot", "ogn", "gn" ]
last_word = series.split()[-1]
if last_word.lower() in one_shot_words:
series = series.rsplit(' ', 1)[0]
return series, volume.strip()
def getYear( self,filename, issue_end):
filename = filename[issue_end:]
year = ""
# look for four digit number with "(" ")" or "--" around it
match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename)
if match:
year = match.group()
# remove non-numerics
year = re.sub("[^0-9]", "", year)
return year
def getRemainder( self, filename, year, count, issue_end ):
#make a guess at where the the non-interesting stuff begins
remainder = ""
if "--" in filename:
remainder = filename.split("--",1)[1]
elif "__" in filename:
remainder = filename.split("__",1)[1]
elif issue_end != 0:
remainder = filename[issue_end:]
remainder = self.fixSpaces(remainder, remove_dashes=False)
if year != "":
remainder = remainder.replace(year,"",1)
if count != "":
remainder = remainder.replace("of "+count,"",1)
remainder = remainder.replace("()","")
return remainder.strip()
def parseFilename( self, filename ):
# remove the path
filename = os.path.basename(filename)
# remove the extension
filename = os.path.splitext(filename)[0]
#url decode, just in case
filename = unquote(filename)
# sometimes archives get messed up names from too many decodings
# often url encodings will break and leave "_28" and "_29" in place
# of "(" and ")" see if there are a number of these, and replace them
if filename.count("_28") > 1 and filename.count("_29") > 1:
filename = filename.replace("_28", "(")
filename = filename.replace("_29", ")")
self.issue, issue_start, issue_end = self.getIssueNumber(filename)
self.series, self.volume = self.getSeriesName(filename, issue_start)
self.year = self.getYear(filename, issue_end)
self.issue_count = self.getIssueCount(filename, issue_end)
self.remainder = self.getRemainder( filename, self.year, self.issue_count, issue_end )
if self.issue != "":
# strip off leading zeros
self.issue = self.issue.lstrip("0")
if self.issue == "":
self.issue = "0"
if self.issue[0] == ".":
self.issue = "0" + self.issue

View File

@ -21,6 +21,7 @@ limitations under the License.
import os
import re
import datetime
import utils
from issuestring import IssueString
class FileRenamer:
@ -71,6 +72,7 @@ class FileRenamer:
md = self.metdata
new_name = self.template
preferred_encoding = utils.get_actual_preferred_encoding()
#print u"{0}".format(md)
@ -93,8 +95,21 @@ class FileRenamer:
if (type(md.month) == str and md.month.isdigit()) or type(md.month) == int:
if int(md.month) in range(1,13):
dt = datetime.datetime( 1970, int(md.month), 1, 0, 0)
month_name = dt.strftime("%B")
month_name = dt.strftime(u"%B".encode(preferred_encoding)).decode(preferred_encoding)
new_name = self.replaceToken( new_name, month_name, '%month_name%')
new_name = self.replaceToken( new_name, md.genre, '%genre%')
new_name = self.replaceToken( new_name, md.language, '%language_code%')
new_name = self.replaceToken( new_name, md.criticalRating , '%criticalrating%')
new_name = self.replaceToken( new_name, md.alternateSeries, '%alternateseries%')
new_name = self.replaceToken( new_name, md.alternateNumber, '%alternatenumber%')
new_name = self.replaceToken( new_name, md.alternateCount, '%alternatecount%')
new_name = self.replaceToken( new_name, md.imprint, '%imprint%')
new_name = self.replaceToken( new_name, md.format, '%format%')
new_name = self.replaceToken( new_name, md.maturityRating, '%maturityrating%')
new_name = self.replaceToken( new_name, md.storyArc, '%storyarc%')
new_name = self.replaceToken( new_name, md.seriesGroup, '%seriesgroup%')
new_name = self.replaceToken( new_name, md.scanInfo, '%scaninfo%')
if self.smart_cleanup:
@ -103,12 +118,20 @@ class FileRenamer:
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name )
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name )
# remove remove duplicate -, _,
new_name = re.sub("[-_]+\s+", "- ", new_name )
new_name = re.sub("(\s-)+", " -", new_name )
# remove duplicate spaces
new_name = u" ".join(new_name.split())
# remove remove duplicate -, _,
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name )
new_name = re.sub("(\s--)+", " --", new_name )
new_name = re.sub("(\s-)+", " -", new_name )
# remove dash or double dash at end of line
new_name = re.sub("[-]{1,2}\s*$", "", new_name )
# remove duplicate spaces (again!)
new_name = u" ".join(new_name.split())
if ext is None:
ext = os.path.splitext( filename )[1]
@ -117,7 +140,9 @@ class FileRenamer:
# some tweaks to keep various filesystems happy
new_name = new_name.replace("/", "-")
new_name = new_name.replace(":", "-")
new_name = new_name.replace(" :", " -")
new_name = new_name.replace(": ", " - ")
new_name = new_name.replace(":", "-")
new_name = new_name.replace("?", "")
return new_name

View File

@ -29,8 +29,9 @@ from PyQt4.QtCore import pyqtSignal
from settings import ComicTaggerSettings
from comicarchive import ComicArchive
from comicarchive import MetaDataStyle
from genericmetadata import GenericMetadata, PageType
from options import MetaDataStyle
import utils
class FileTableWidget( QTableWidget ):
@ -70,14 +71,12 @@ class FileSelectionList(QWidget):
def __init__(self, parent , settings ):
super(FileSelectionList, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui' ), self)
self.settings = settings
#self.twList = FileTableWidget( self )
#gridlayout = QGridLayout( self )
#gridlayout.addWidget( self.twList )
utils.reduceWidgetFontSize( self.twList )
#self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
self.currentItem = None
@ -99,6 +98,14 @@ class FileSelectionList(QWidget):
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 )
@ -155,31 +162,23 @@ class FileSelectionList(QWidget):
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 = []
for p in pathlist:
# if path is a folder, walk it recursivly, and all files underneath
if type(p) == str:
#make sure string is unicode
filename_encoding = sys.getfilesystemencoding()
p = p.decode(filename_encoding, 'replace')
if os.path.isdir( unicode(p)):
for root,dirs,files in os.walk( unicode(p) ):
for f in files:
filelist.append(os.path.join(root,unicode(f)))
else:
filelist.append(unicode(p))
filelist = utils.get_recursive_filelist( pathlist )
# we now have a list of files to add
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
progdialog.setWindowTitle( "Adding Files" )
progdialog.setWindowModality(Qt.WindowModal)
#progdialog.setWindowModality(Qt.WindowModal)
progdialog.setWindowModality(Qt.ApplicationModal)
progdialog.show()
firstAdded = None
@ -190,6 +189,7 @@ class FileSelectionList(QWidget):
break
progdialog.setValue(idx)
progdialog.setLabelText(f)
utils.centerWindowOnParent( progdialog )
QCoreApplication.processEvents()
row = self.addPathItem( f )
if firstAdded is None and row is not None:
@ -198,7 +198,13 @@ class FileSelectionList(QWidget):
progdialog.close()
if firstAdded is not None:
self.twList.selectRow(firstAdded)
else:
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
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 comic archives were found."))
self.twList.setSortingEnabled(True)
# Adjust column size
@ -230,12 +236,9 @@ class FileSelectionList(QWidget):
if self.isListDupe(path):
return None
ca = ComicArchive( path )
if self.settings.rar_exe_path != "":
ca.setExternalRarProgram( self.settings.rar_exe_path )
ca = ComicArchive( path, self.settings )
if ca.seemsToBeAComicArchive() :
row = self.twList.rowCount()
self.twList.insertRow( row )

View File

@ -63,6 +63,7 @@ class GenericMetadata:
self.publisher = None
self.month = None
self.year = None
self.day = None
self.issueCount = None
self.volume = None
self.genre = None
@ -125,6 +126,7 @@ class GenericMetadata:
assign( "issueCount", new_md.issueCount )
assign( "title", new_md.title )
assign( "publisher", new_md.publisher )
assign( "day", new_md.day )
assign( "month", new_md.month )
assign( "year", new_md.year )
assign( "volume", new_md.volume )
@ -256,8 +258,9 @@ class GenericMetadata:
add_attr_string( "issueCount" )
add_attr_string( "title" )
add_attr_string( "publisher" )
add_attr_string( "month" )
add_attr_string( "year" )
add_attr_string( "month" )
add_attr_string( "day" )
add_attr_string( "volume" )
add_attr_string( "volumeCount" )
add_attr_string( "genre" )

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

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 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

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 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

@ -1,4 +1,22 @@
"""
A pthyon 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 StringIO
import sys

View File

@ -0,0 +1,86 @@
"""
A PyQT4 widget to display a popup image
"""
"""
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
from PyQt4 import QtCore, QtGui, uic
import os
from settings import ComicTaggerSettings
class ImagePopup(QtGui.QDialog):
def __init__(self, parent, image_pixmap):
super(ImagePopup, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui' ), self)
QtGui.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 = QtGui.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
self.desktopBg = QtGui.QPixmap.grabWindow(QtGui.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())
self.setMask(self.clientBgPixmap.mask())
self.applyImagePixmap()
self.showFullScreen()
self.raise_( )
QtGui.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)
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()

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ limitations under the License.
import sys
import os
import re
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
@ -29,6 +30,15 @@ from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from imagefetcher import ImageFetcher
from settings import ComicTaggerSettings
from issuestring import IssueString
from coverimagewidget import CoverImageWidget
import utils
class IssueNumberTableWidgetItem(QtGui.QTableWidgetItem):
def __lt__(self, other):
selfStr = self.data(QtCore.Qt.DisplayRole).toString()
otherStr = other.data(QtCore.Qt.DisplayRole).toString()
return (IssueString(selfStr).asFloat() <
IssueString(otherStr).asFloat())
class IssueSelectionWindow(QtGui.QDialog):
@ -37,8 +47,20 @@ class IssueSelectionWindow(QtGui.QDialog):
def __init__(self, parent, settings, series_id, issue_number):
super(IssueSelectionWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'issueselectionwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('issueselectionwindow.ui' ), self)
self.coverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.AltCoverMode )
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
gridlayout.addWidget( self.coverWidget )
gridlayout.setContentsMargins(0,0,0,0)
utils.reduceWidgetFontSize( self.twList )
utils.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
@ -72,6 +94,7 @@ class IssueSelectionWindow(QtGui.QDialog):
try:
comicVine = ComicVineTalker( )
volume_data = comicVine.fetchVolumeData( self.series_id )
self.issue_list = comicVine.fetchIssuesByVolume( self.series_id )
except ComicVineTalkerException:
QtGui.QApplication.restoreOverrideCursor()
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to list issues!"))
@ -79,8 +102,6 @@ class IssueSelectionWindow(QtGui.QDialog):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
self.issue_list = volume_data['issues']
self.twList.setSortingEnabled(False)
@ -89,20 +110,35 @@ class IssueSelectionWindow(QtGui.QDialog):
self.twList.insertRow(row)
item_text = record['issue_number']
item = QtGui.QTableWidgetItem(item_text)
item = IssueNumberTableWidgetItem(item_text)
item.setData( QtCore.Qt.ToolTipRole, item_text )
item.setData( QtCore.Qt.UserRole ,record['id'])
item.setData(QtCore.Qt.DisplayRole, float(item_text))
item.setData(QtCore.Qt.DisplayRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = record['name']
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 = QtGui.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 = QtGui.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() == IssueString(self.issue_number).asString():
if IssueString(record['issue_number']).asString().lower() == IssueString(self.issue_number).asString().lower():
self.initial_id = record['id']
row += 1
@ -121,35 +157,18 @@ class IssueSelectionWindow(QtGui.QDialog):
return
if prev is not None and prev.row() == curr.row():
return
self.issue_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
# list selection was changed, update the the issue cover
for record in self.issue_list:
if record['id'] == self.issue_id:
if record['id'] == self.issue_id:
self.issue_number = record['issue_number']
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.cv = ComicVineTalker( )
self.cv.urlFetchComplete.connect( self.urlFetchComplete )
self.cv.asyncFetchIssueCoverURLs( int(self.issue_id) )
self.coverWidget.setIssueID( int(self.issue_id) )
if record['description'] is None:
self.teDescription.setText ( "" )
else:
self.teDescription.setText ( record['description'] )
break
# called when the cover URL has been fetched
def urlFetchComplete( self, image_url, thumb_url, issue_id ):
self.cover_fetcher = ImageFetcher( )
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
self.cover_fetcher.fetch( str(image_url), user_data=issue_id )
# called when the image is done loading
def coverFetchComplete( self, image_data, issue_id ):
if self.issue_id == issue_id:
img = QtGui.QImage()
img.loadFromData( image_data )
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))

View File

@ -8,6 +8,7 @@ e.g.:
"0"
"-1"
"5AU"
"100-2"
"""
@ -34,31 +35,53 @@ import re
class IssueString:
def __init__(self, text):
if text is None:
self.num = None
self.suffix = ""
return
self.text = str(text)
#strip out non float-y stuff
tmp_num_str = re.sub('[^0-9.-]',"", self.text )
# break up the issue number string into 2 parts: the numeric and suffix string.
# ( assumes that the numeric portion is always first )
self.num = None
self.suffix = ""
if tmp_num_str == "":
self.num = None
self.suffix = self.text
if text is None:
return
text = unicode(text)
#skip the minus sign if it's first
if text[0] == '-':
start = 1
else:
if tmp_num_str.count(".") > 1:
#make sure it's a valid float or int.
parts = tmp_num_str.split('.')
self.num = float( parts[0] + '.' + parts[1] )
else:
self.num = float( tmp_num_str )
self.suffix = ""
parts = self.text.split(tmp_num_str)
if len( parts ) > 1 :
self.suffix = parts[1]
start = 0
# walk through the string, look for split point (the first non-numeric)
decimal_count = 0
for idx in range( start, len(text) ):
if text[idx] not in "0123456789.":
break
# special case: also split on second "."
if text[idx] == ".":
decimal_count += 1
if decimal_count > 1:
break
else:
idx = len(text)
# move trailing numeric decimal to suffix
# (only if there is other junk after )
if text[idx-1] == "." and len(text) != idx:
idx = idx -1
# if there is no numeric after the minus, make the minus part of the suffix
if idx == 1 and start == 1:
idx = 0
part1 = text[0:idx]
part2 = text[idx:len(text)]
if part1 != "":
self.num = float( part1 )
self.suffix = part2
#print "num: {0} suf: {1}".format(self.num, self.suffix)
def asString( self, pad = 0 ):
#return the float, left side zero-padded, with suffix attached

View File

@ -30,9 +30,11 @@ class LogWindow(QtGui.QDialog):
def __init__(self, parent):
super(LogWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'logwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('logwindow.ui' ), self)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
def setText( self, text ):
self.textEdit.setPlainText( text )

79
comictaggerlib/main.py Executable file
View File

@ -0,0 +1,79 @@
"""
A python app to (automatically) 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 platform
import utils
import cli
from settings import ComicTaggerSettings
from options import Options
try:
qt_available = True
from PyQt4 import QtCore, QtGui
from taggerwindow import TaggerWindow
except ImportError as e:
qt_available = False
#---------------------------------------
def ctmain():
utils.fix_output_encoding()
settings = ComicTaggerSettings()
opts = Options()
opts.parseCmdLineArgs()
signal.signal(signal.SIGINT, signal.SIG_DFL)
if not qt_available and not opts.no_gui:
opts.no_gui = True
print >> sys.stderr, "PyQt4 is not available. ComicTagger is limited to command-line mode."
if opts.no_gui:
cli.cli_mode( opts, settings )
else:
app = QtGui.QApplication(sys.argv)
if platform.system() != "Linux":
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
splash = QtGui.QSplashScreen(img)
splash.show()
splash.raise_()
app.processEvents()
try:
tagger_window = TaggerWindow( opts.file_list, 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() )

View File

@ -26,26 +26,56 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
from imagefetcher import ImageFetcher
from settings import ComicTaggerSettings
from comicarchive import MetaDataStyle
from coverimagewidget import CoverImageWidget
from comicvinetalker import ComicVineTalker
import utils
class MatchSelectionWindow(QtGui.QDialog):
volume_id = 0
def __init__(self, parent, matches):
def __init__(self, parent, matches, comic_archive):
super(MatchSelectionWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'matchselectionwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('matchselectionwindow.ui' ), self)
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
gridlayout.addWidget( self.altCoverWidget )
gridlayout.setContentsMargins(0,0,0,0)
self.archiveCoverWidget = CoverImageWidget( self.archiveCoverContainer, CoverImageWidget.ArchiveMode )
gridlayout = QtGui.QGridLayout( self.archiveCoverContainer )
gridlayout.addWidget( self.archiveCoverWidget )
gridlayout.setContentsMargins(0,0,0,0)
utils.reduceWidgetFontSize( self.twList )
utils.reduceWidgetFontSize( self.teDescription, 1 )
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.matches = matches
self.populateTable( )
self.twList.resizeColumnsToContents()
self.comic_archive = comic_archive
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
self.current_row = 0
self.twList.selectRow( 0 )
self.updateData()
def updateData( self):
self.setCoverImage()
self.populateTable()
self.twList.resizeColumnsToContents()
self.twList.selectRow( 0 )
path = self.comic_archive.path
self.setWindowTitle( u"Select correct match: {0}".format(
os.path.split(path)[1] ))
def populateTable( self ):
while self.twList.rowCount() > 0:
@ -60,15 +90,10 @@ class MatchSelectionWindow(QtGui.QDialog):
item_text = match['series']
item = QtGui.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)
"""
item_text = u"{0}".format(match['issue_number'])
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
"""
if match['publisher'] is not None:
item_text = u"{0}".format(match['publisher'])
else:
@ -78,19 +103,35 @@ class MatchSelectionWindow(QtGui.QDialog):
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
item_text = ""
month_str = u""
year_str = u"????"
if match['month'] is not None:
item_text = u"{0}/".format(match['month'])
month_str = u"-{0:02d}".format(int(match['month']))
if match['year'] is not None:
item_text += u"{0}".format(match['year'])
else:
item_text += u"????"
year_str = u"{0}".format(match['year'])
item_text = year_str + month_str
item = QtGui.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 = QtGui.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 ):
@ -102,19 +143,18 @@ class MatchSelectionWindow(QtGui.QDialog):
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 ).toPyObject()[0]
return match
self.current_row = curr.row()
# list selection was changed, update the the issue cover
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.cover_fetcher = ImageFetcher( )
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
self.cover_fetcher.fetch( self.matches[self.current_row]['img_url'] )
# called when the image is done loading
def coverFetchComplete( self, image_data, issue_id ):
img = QtGui.QImage()
img.loadFromData( image_data )
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))

View File

@ -22,22 +22,17 @@ import sys
import getopt
import platform
import os
import traceback
import ctversion
import utils
try:
import argparse
except:
pass
from genericmetadata import GenericMetadata
class Enum(set):
def __getattr__(self, name):
if name in self:
return name
raise AttributeError
class MetaDataStyle:
CBI = 0
CIX = 1
COMET = 2
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
from comicarchive import MetaDataStyle
from versionchecker import VersionChecker
class Options:
help_text = """
@ -57,6 +52,7 @@ If no options are given, {0} will run in windowed mode
-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 ( relevent for -s or -c )
-1, --assume-issue-one Assume issue number is 1 if not found ( relevent for -s )
-n, --dryrun Don't actually modify file (only relevent for -d, -s, or -r)
-t, --type=TYPE Specify TYPE as either "CR", "CBL", or "COMET" (as either
ComicRack, ComicBookLover, or CoMet style tags, respectivly)
@ -78,12 +74,21 @@ If no options are given, {0} will run in windowed mode
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
--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
-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
"""
-h, --help Display this message
For more help visit the wiki at: http://code.google.com/p/comictagger/
"""
def __init__(self):
@ -96,6 +101,9 @@ If no options are given, {0} will run in windowed mode
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
@ -107,6 +115,10 @@ If no options are given, {0} will run in windowed mode
self.no_overwrite = False
self.interactive = False
self.issue_id = None
self.recursive = False
self.run_script = False
self.script = None
self.assume_issue_is_one_if_not_set = False
self.file_list = []
def display_msg_and_quit( self, msg, code, show_help=False ):
@ -168,6 +180,41 @@ If no options are given, {0} will run in windowed mode
#print md
return md
def launch_script(self, scriptfile):
# we were given a script. special case for the args:
# 1. ignore everthing 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:
@ -176,23 +223,38 @@ If no options are given, {0} will run in windowed mode
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:vonsrc:i",
"hpdt:fm:vonsrc:ieRS:1",
[ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose",
"online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite",
"interactive", "nosummary", "version", "id=" ])
"interactive", "nosummary", "version", "id=" , "recursive", "script=",
"export-to-zip", "delete-rar", "abort-on-conflict", "assume-issue-one" ] )
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"):
@ -219,6 +281,12 @@ If no options are given, {0} will run in windowed mode
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 == "--id":
@ -231,11 +299,18 @@ If no options are given, {0} will run in windowed mode
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 == "--version":
print "ComicTagger {0}: Copyright (c) 2012-2013 Anthony Beville".format(ctversion.version)
print "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)"
print "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)"
new_version = VersionChecker().getLatestVersion("", False)
if new_version is not None and new_version != ctversion.version:
print "----------------------------------------"
print "New version available online: {0}".format(new_version)
print "----------------------------------------"
sys.exit(0)
if o in ("-t", "--type"):
if a.lower() == "cr":
@ -246,20 +321,25 @@ If no options are given, {0} will run in windowed mode
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:
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:
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 count > 1:
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, export, 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
@ -267,13 +347,14 @@ If no options are given, {0} will run in windowed mode
self.file_list = []
for item in args:
self.file_list.extend(glob.glob(item))
self.filename = self.file_list[0]
if len(self.file_list) > 0:
self.filename = self.file_list[0]
else:
self.filename = args[0]
self.file_list = args
if self.no_gui and self.filename is None:
self.display_msg_and_quit( "Command requires a filename!", 1 )
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 )
@ -287,3 +368,5 @@ If no options are given, {0} will run in windowed mode
#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

@ -18,27 +18,43 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
import platform
import sys
from PyQt4 import QtCore, QtGui, uic
import os
from settings import ComicTaggerSettings
from coverimagewidget import CoverImageWidget
class PageBrowserWindow(QtGui.QDialog):
def __init__(self, parent, metadata):
super(PageBrowserWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagebrowser.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui' ), self)
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
gridlayout = QtGui.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.lblPage.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.lblPage.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Ignored)
self.comic_archive = None
self.current_pixmap = None
self.page_count = 0
self.current_page_num = 0
self.metadata = metadata
self.buttonBox.button(QtGui.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()
@ -46,72 +62,49 @@ class PageBrowserWindow(QtGui.QDialog):
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 ):
archive_page_index = self.metadata.getArchivePageIndex( self.current_page_num )
image_data = self.comic_archive.getPage( archive_page_index )
if image_data is not None:
self.setCurrentPixmap( image_data )
self.setDisplayPixmap( 0, 0)
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 ) )
if self.current_page_num + 1 < self.page_count:
self.btnNext.setEnabled( True )
else:
self.btnNext.setEnabled( False )
if self.current_page_num - 1 >= 0:
self.btnPrev.setEnabled( True )
else:
self.btnPrev.setEnabled( False )
def setCurrentPixmap( self, image_data ):
if image_data is not None:
img = QtGui.QImage()
img.loadFromData( image_data )
self.current_pixmap = QtGui.QPixmap(QtGui.QPixmap(img))
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()
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.lblPage.height() + delta_h
new_w = self.lblPage.width() + delta_w
if new_h < 0:
new_h = 0;
if new_w < 0:
new_w = 0;
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, QtCore.Qt.KeepAspectRatio)
self.lblPage.setPixmap( scaled_pixmap )
#QtCore.QCoreApplication.processEvents()

View File

@ -26,8 +26,10 @@ from PyQt4 import uic
from settings import ComicTaggerSettings
from genericmetadata import GenericMetadata, PageType
from options import MetaDataStyle
from comicarchive import MetaDataStyle
from pageloader import PageLoader
from coverimagewidget import CoverImageWidget
def itemMoveEvents( widget ):
@ -76,11 +78,13 @@ class PageListEditor(QWidget):
def __init__(self, parent ):
super(PageListEditor, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagelisteditor.ui' ), self )
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui' ), self)
self.comic_archive = None
self.pages_list = None
self.page_loader = None
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()
@ -107,8 +111,10 @@ class PageListEditor(QWidget):
self.first_front_page = None
def resetPage( self ):
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
self.setDisplayPixmap( 0, 0)
self.pageWidget.clear()
self.comboBox.setDisabled(True)
self.comic_archive = None
self.pages_list = None
def moveCurrentUp( self ):
row = self.listWidget.currentRow()
@ -157,18 +163,8 @@ class PageListEditor(QWidget):
#idx = int(str (self.listWidget.item( row ).text()))
idx = int(self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]['Image'])
if self.page_loader is not None:
self.page_loader.abandoned = True
if self.comic_archive is not None:
self.page_loader = PageLoader( self.comic_archive, idx )
self.page_loader.loadComplete.connect( self.actualChangePageImage )
self.page_loader.start()
def actualChangePageImage( self, img ):
self.page_loader = None
self.current_pixmap = QPixmap(img)
self.setDisplayPixmap( 0, 0)
self.pageWidget.setArchive( self.comic_archive, idx )
def getFirstFrontCover( self ):
frontCover = 0
@ -203,44 +199,13 @@ class PageListEditor(QWidget):
# 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 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()
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
frame_w = new_w
frame_h = new_h
new_h -= 4
new_w -= 4
if new_h < 0:
new_h = 0;
if new_w < 0:
new_w = 0;
# scale the pixmap to fit in the frame
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio)
self.label.setPixmap( scaled_pixmap )
# ,pve and resize the label to be centered in the fame
img_w = scaled_pixmap.width()
img_h = scaled_pixmap.height()
self.label.resize( img_w, img_h )
self.label.move( (frame_w - img_w)/2, (frame_h - img_h)/2 )
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 )
@ -256,7 +221,7 @@ class PageListEditor(QWidget):
self.listWidget.setCurrentRow ( 0 )
def listEntryText(self, page_dict):
text = page_dict['Image']
text = str(int(page_dict['Image']) + 1)
if 'Type' in page_dict:
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
return text
@ -278,9 +243,9 @@ class PageListEditor(QWidget):
# depending on the current data style, certain fields are disabled
inactive_color = QColor(255, 170, 150)
active_palette = self.label.palette()
active_palette = self.comboBox.palette()
inactive_palette3 = self.label.palette()
inactive_palette3 = self.comboBox.palette()
inactive_palette3.setColor(QPalette.Base, inactive_color)
@ -302,8 +267,7 @@ class PageListEditor(QWidget):
elif data_style == MetaDataStyle.CoMet:
pass
def showEvent( self, event ):
# make sure to adjust the size and pos of the pixmap based on frame size
self.setDisplayPixmap( 0,0 )
# make sure combo is disabled when no list
if self.comic_archive is None:
self.comboBox.setEnabled( False )

View File

@ -22,7 +22,7 @@ import sys
from PyQt4 import QtCore, QtGui, uic
import os
from settings import ComicTaggerSettings
import utils
class IDProgressWindow(QtGui.QDialog):
@ -30,14 +30,13 @@ class IDProgressWindow(QtGui.QDialog):
def __init__(self, parent):
super(IDProgressWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'progresswindow.ui' ), self)
# we can't specify relative font sizes in the UI designer, so
# make font for scroll window a smidge smaller
f = self.textEdit.font()
if f.pointSize() > 10:
f.setPointSize( f.pointSize() - 2 )
self.textEdit.setFont( f )
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui' ), self)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
utils.reduceWidgetFontSize( self.textEdit )

View File

@ -23,7 +23,7 @@ from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
from settingswindow import SettingsWindow
from filerenamer import FileRenamer
from options import MetaDataStyle
from comicarchive import MetaDataStyle
import os
import utils
@ -33,9 +33,13 @@ class RenameWindow(QtGui.QDialog):
def __init__( self, parent, comic_archive_list, data_style, settings ):
super(RenameWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
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

View File

@ -21,8 +21,10 @@ limitations under the License.
#import sys
import os
import sys
import ConfigParser
import configparser
import platform
import codecs
import uuid
import utils
@ -35,22 +37,42 @@ class ComicTaggerSettings:
else:
return os.path.join( os.path.expanduser('~') , '.ComicTagger')
frozen_win_exe_path = None
@staticmethod
def baseDir():
if platform.system() == "Darwin" and getattr(sys, 'frozen', None):
return sys._MEIPASS
if getattr(sys, 'frozen', None):
if platform.system() == "Darwin":
return sys._MEIPASS
else: # Windows
#Preserve this value, in case sys.argv gets changed importing a plugin script
if ComicTaggerSettings.frozen_win_exe_path is None:
ComicTaggerSettings.frozen_win_exe_path = os.path.dirname( os.path.abspath( sys.argv[0] ) )
return ComicTaggerSettings.frozen_win_exe_path
else:
#print "ATB basename", os.path.dirname( os.path.abspath( sys.argv[0] ) )
return os.path.dirname( os.path.abspath( sys.argv[0] ) )
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_exe_path = ""
self.allow_cbi_in_rar = True
self.check_for_new_version = True
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 = ""
@ -60,14 +82,21 @@ class ComicTaggerSettings:
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, Scholastic Book Services, Editorial Televisa"
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa"
# 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
#filename parsing settings
self.parse_scan_info = True
# Comic Vine settings
self.use_series_start_as_volume = False
@ -95,7 +124,7 @@ class ComicTaggerSettings:
self.folder = ""
self.setDefaultValues()
self.config = ConfigParser.RawConfigParser()
self.config = configparser.RawConfigParser()
self.folder = ComicTaggerSettings.getSettingsFolder()
if not os.path.exists( self.folder ):
@ -131,18 +160,35 @@ class ComicTaggerSettings:
self.unrar_exe_path = utils.which("unrar")
if self.unrar_exe_path != "":
self.save()
# make sure unrar/rar program is now in the path for the UnRAR class to use
utils.addtopath(os.path.dirname(self.unrar_exe_path))
utils.addtopath(os.path.dirname(self.rar_exe_path))
def reset( self ):
os.unlink( self.settings_file )
self.__init__()
def load(self):
self.config.read( self.settings_file )
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' )
self.unrar_exe_path = self.config.get( 'settings', 'unrar_exe_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'):
@ -161,17 +207,28 @@ class ComicTaggerSettings:
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('comicvine', 'use_series_start_as_volume'):
self.use_series_start_as_volume = self.config.getboolean( 'comicvine', 'use_series_start_as_volume' )
@ -205,13 +262,16 @@ class ComicTaggerSettings:
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_exe_path', self.unrar_exe_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 )
@ -221,6 +281,8 @@ class ComicTaggerSettings:
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' )
@ -233,7 +295,14 @@ class ComicTaggerSettings:
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 )
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' )
@ -259,6 +328,8 @@ class ComicTaggerSettings:
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 )
with open( self.settings_file, 'wb') as configfile:
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

@ -54,10 +54,12 @@ class SettingsWindow(QtGui.QDialog):
def __init__(self, parent, settings ):
super(SettingsWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'settingswindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui' ), self)
self.settings = settings
self.setWindowFlags(self.windowFlags() &
~QtCore.Qt.WindowContextHelpButtonHint )
self.settings = settings
self.name = "Settings"
if platform.system() == "Windows":
@ -117,6 +119,12 @@ class SettingsWindow(QtGui.QDialog):
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)
@ -151,18 +159,23 @@ class SettingsWindow(QtGui.QDialog):
self.settings.rar_exe_path = str(self.leRarExePath.text())
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
# make sure unrar program is now in the path for the UnRAR class
# make sure unrar/rar program is now in the path for the UnRAR class
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
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.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
@ -182,7 +195,6 @@ class SettingsWindow(QtGui.QDialog):
self.settings.save()
QtGui.QDialog.accept(self)
def selectRar( self ):
self.selectFile( self.leRarExePath, "RAR" )
@ -223,5 +235,5 @@ class SettingsWindow(QtGui.QDialog):
control.setText( str(fileList[0]) )
def showRenameTab( self ):
self.tabWidget.setCurrentIndex(4)
self.tabWidget.setCurrentIndex(5)

View File

@ -1,6 +1,6 @@
# coding=utf-8
"""
The main window of the comictagger app
The main window of the ComicTagger app
"""
"""
@ -32,7 +32,7 @@ import webbrowser
import re
from volumeselectionwindow import VolumeSelectionWindow
from options import MetaDataStyle
from comicarchive import MetaDataStyle
from comicinfoxml import ComicInfoXml
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
@ -54,6 +54,9 @@ from issueidentifier import IssueIdentifier
from autotagstartwindow import AutoTagStartWindow
from autotagprogresswindow import AutoTagProgressWindow
from autotagmatchwindow import AutoTagMatchWindow
from coverimagewidget import CoverImageWidget
from versionchecker import VersionChecker
import utils
import ctversion
@ -62,6 +65,7 @@ class OnlineMatchResults():
self.goodMatches = []
self.noMatches = []
self.multipleMatches = []
self.lowConfidenceMatches = []
self.writeFailures = []
self.fetchDataFailures = []
@ -69,31 +73,6 @@ class MultipleMatch():
def __init__( self, ca, match_list):
self.ca = ca
self.matches = match_list
# this reads the environment and inits the right locale
locale.setlocale(locale.LC_ALL, "")
# helper func to allow a label to be clickable
def clickable(widget):
class Filter(QtCore.QObject):
dblclicked = pyqtSignal()
def eventFilter(self, obj, event):
if obj == widget:
if event.type() == QtCore.QEvent.MouseButtonDblClick:
self.dblclicked.emit()
return True
return False
filter = Filter(widget)
widget.installEventFilter(filter)
return filter.dblclicked
class TaggerWindow( QtGui.QMainWindow):
@ -103,9 +82,14 @@ class TaggerWindow( QtGui.QMainWindow):
def __init__(self, file_list, settings, parent = None):
super(TaggerWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'taggerwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('taggerwindow.ui' ), self)
self.settings = settings
self.archiveCoverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.ArchiveMode )
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
gridlayout.addWidget( self.archiveCoverWidget )
gridlayout.setContentsMargins(0,0,0,0)
self.pageListEditor = PageListEditor( self.tabPages )
gridlayout = QtGui.QGridLayout( self.tabPages )
gridlayout.addWidget( self.pageListEditor )
@ -117,11 +101,8 @@ class TaggerWindow( QtGui.QMainWindow):
self.fileSelectionList.selectionChanged.connect( self.fileListSelectionChanged )
self.fileSelectionList.listCleared.connect( self.fileListCleared )
# ATB: Disable the list...
#self.splitter.setSizes([100,0])
#self.splitter.setHandleWidth(0)
#self.splitter.handle(1).setDisabled(True)
self.fileSelectionList.setSorting(self.settings.last_filelist_sorted_column,
self.settings.last_filelist_sorted_order)
# we can't specify relative font sizes in the UI designer, so
# walk through all the lablels in the main form, and make them
@ -133,14 +114,11 @@ class TaggerWindow( QtGui.QMainWindow):
f.setPointSize( f.pointSize() - 2 )
f.setItalic( True )
child.setFont( f )
self.scrollAreaWidgetContents.adjustSize()
self.setWindowIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/app.png' )))
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.setWindowIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('app.png')))
self.save_data_style = settings.last_selected_save_data_style
self.load_data_style = settings.last_selected_load_data_style
@ -149,6 +127,7 @@ class TaggerWindow( QtGui.QMainWindow):
self.statusBar()
self.populateComboBoxes()
self.page_browser = None
self.resetApp()
# set up some basic field validators
@ -167,7 +146,13 @@ class TaggerWindow( QtGui.QMainWindow):
#TODO set up an RE validator for issueNum that allows
# for all sorts of wacky things
# tweak some control fonts
utils.reduceWidgetFontSize( self.lblFilename, 1 )
utils.reduceWidgetFontSize( self.lblArchiveType )
utils.reduceWidgetFontSize( self.lblTagList )
utils.reduceWidgetFontSize( self.lblPageCount )
#make sure some editable comboboxes don't take drop actions
self.cbFormat.lineEdit().setAcceptDrops(False)
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
@ -179,13 +164,11 @@ class TaggerWindow( QtGui.QMainWindow):
self.btnAddCredit.clicked.connect(self.addCredit)
self.btnRemoveCredit.clicked.connect(self.removeCredit)
self.twCredits.cellDoubleClicked.connect(self.editCredit)
clickable(self.lblCover).connect(self.showPageBrowser)
self.connectDirtyFlagSignals()
self.pageListEditor.modified.connect(self.setDirtyFlag)
self.pageListEditor.modified.connect(self.setDirtyFlag)
self.pageListEditor.firstFrontCoverChanged.connect( self.frontCoverChanged )
self.pageListEditor.listOrderChanged.connect( self.pageListOrderChanged )
self.tabWidget.currentChanged.connect( self.tabChanged )
self.updateStyleTweaks()
@ -223,26 +206,41 @@ class TaggerWindow( QtGui.QMainWindow):
"""
)
self.settings.show_disclaimer = not checked
if self.settings.ask_about_usage_stats:
reply = QtGui.QMessageBox.question(self,
self.tr("Anonymous Stats"),
self.tr(
"Is it okay if ComicTagger occasionally sends some anonymous usage statistics? Nothing nefarious, "
"just trying to get a better idea of how the app is being used.\n\nThanks for your support!"
),
QtGui.QMessageBox.Yes|QtGui.QMessageBox.Default, QtGui.QMessageBox.No )
if reply == QtGui.QMessageBox.Yes:
self.settings.send_usage_stats = True
self.settings.ask_about_usage_stats = False
if self.settings.check_for_new_version:
self.checkLatestVersionOnline()
def sigint_handler(self, *args):
# defer the actual close in the app loop thread
QtCore.QTimer.singleShot(200, self.close)
def resetApp( self ):
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.archiveCoverWidget.clear()
self.comic_archive = None
self.dirtyFlag = False
self.clearForm()
self.pageListEditor.resetPage()
if self.page_browser is not None:
self.page_browser.reset()
self.updateAppTitle()
self.updateMenus()
self.updateInfoBox()
self.droppedFile = None
self.page_browser = None
self.page_loader = None
@ -313,6 +311,7 @@ class TaggerWindow( QtGui.QMainWindow):
self.actionRename.setStatusTip( 'Rename archive based on tags' )
self.actionRename.triggered.connect( self.renameArchive )
self.actionSettings.setShortcut( 'Ctrl+Shift+S' )
self.actionSettings.setStatusTip( 'Configure ComicTagger' )
self.actionSettings.triggered.connect( self.showSettings )
@ -332,7 +331,7 @@ class TaggerWindow( QtGui.QMainWindow):
self.actionApplyCBLTransform.setStatusTip( 'Modify tags specifically for CBL format' )
self.actionApplyCBLTransform.triggered.connect( self.applyCBLTransform )
#self.actionClearEntryForm.setShortcut( 'Ctrl+C' )
self.actionClearEntryForm.setShortcut( 'Ctrl+Shift+C' )
self.actionClearEntryForm.setStatusTip( 'Clear all the data on the screen' )
self.actionClearEntryForm.triggered.connect( self.clearForm )
@ -349,16 +348,16 @@ class TaggerWindow( QtGui.QMainWindow):
self.actionComicTaggerForum.triggered.connect( self.showForum )
# ToolBar
self.actionLoad.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/open.png')) )
self.actionLoadFolder.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/longbox.png')) )
self.actionWrite_Tags.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/save.png')) )
self.actionParse_Filename.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/parse.png')) )
self.actionSearchOnline.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/search.png')) )
self.actionAutoIdentify.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/auto.png')) )
self.actionAutoTag.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/autotag.png')) )
self.actionClearEntryForm.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/clear.png')) )
self.actionPageBrowser.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/browse.png') ))
self.actionLoad.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('open.png')))
self.actionLoadFolder.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('longbox.png')))
self.actionWrite_Tags.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('save.png')))
self.actionParse_Filename.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('parse.png')))
self.actionSearchOnline.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('search.png')))
self.actionAutoIdentify.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('auto.png')))
self.actionAutoTag.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('autotag.png')))
self.actionClearEntryForm.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('clear.png')))
self.actionPageBrowser.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('browse.png')))
self.toolBar.addAction( self.actionLoad )
self.toolBar.addAction( self.actionLoadFolder )
@ -394,7 +393,7 @@ class TaggerWindow( QtGui.QMainWindow):
progdialog = QtGui.QProgressDialog("", "Cancel", 0, rar_count, self)
progdialog.setWindowTitle( "Exporting as ZIP" )
progdialog.setWindowModality(QtCore.Qt.WindowModal)
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
progdialog.show()
prog_idx = 0
@ -412,6 +411,7 @@ class TaggerWindow( QtGui.QMainWindow):
progdialog.setValue(prog_idx)
prog_idx += 1
progdialog.setLabelText( ca.path )
utils.centerWindowOnParent( progdialog )
QtCore.QCoreApplication.processEvents()
original_path = os.path.abspath( ca.path )
@ -470,7 +470,7 @@ class TaggerWindow( QtGui.QMainWindow):
msgBox = QtGui.QMessageBox()
msgBox.setWindowTitle( self.tr("About " + self.appName ) )
msgBox.setTextFormat( QtCore.Qt.RichText )
msgBox.setIconPixmap( QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/about.png' )) )
msgBox.setIconPixmap( QtGui.QPixmap(ComicTaggerSettings.getGraphic('about.png')) )
msgBox.setText( "<br><br><br>"
+ self.appName + " v" + self.version + "<br>"
+ "(c)2012 Anthony Beville<br><br>"
@ -518,21 +518,11 @@ class TaggerWindow( QtGui.QMainWindow):
self.clearDirtyFlag() # also updates the app title
self.updateInfoBox()
self.updateMenus()
self.updateAppTitle()
def updateCoverImage( self ):
if self.page_loader is not None:
self.page_loader.abandoned = True
cover_idx = self.metadata.getCoverPageIndexList()[0]
self.page_loader = PageLoader( self.comic_archive, cover_idx )
self.page_loader.loadComplete.connect( self.actualUpdateCoverImage )
self.page_loader.start()
def actualUpdateCoverImage( self, img ):
self.page_loader = None
self.lblCover.setPixmap(QtGui.QPixmap(img))
self.lblCover.setScaledContents(True)
self.archiveCoverWidget.setArchive( self.comic_archive, cover_idx)
def updateMenus( self ):
@ -589,7 +579,7 @@ class TaggerWindow( QtGui.QMainWindow):
filename = os.path.basename( ca.path )
filename = os.path.splitext(filename)[0]
filename = FileNameParser().fixSpaces(filename)
filename = FileNameParser().fixSpaces(filename, False)
self.lblFilename.setText( filename )
@ -926,7 +916,7 @@ class TaggerWindow( QtGui.QMainWindow):
def queryOnline(self, autoselect=False):
issue_number = str(self.leIssueNum.text()).strip()
issue_number = unicode(self.leIssueNum.text()).strip()
if autoselect and issue_number == "":
QtGui.QMessageBox.information(self,"Automatic Identify Search", "Can't auto-identify without an issue number (yet!)")
@ -937,14 +927,17 @@ class TaggerWindow( QtGui.QMainWindow):
else:
QtGui.QMessageBox.information(self, self.tr("Online Search"), self.tr("Need to enter a series name to search."))
return
year = str(self.lePubYear.text()).strip()
if year == "":
year = None
issue_count = str(self.leIssueCount.text()).strip()
if issue_count == "":
issue_count = None
cover_index_list = self.metadata.getCoverPageIndexList()
selector = VolumeSelectionWindow( self, series_name, issue_number, year, cover_index_list, self.comic_archive, self.settings, autoselect )
selector = VolumeSelectionWindow( self, series_name, issue_number, year, issue_count, cover_index_list, self.comic_archive, self.settings, autoselect )
title = "Search: '" + series_name + "' - "
selector.setWindowTitle( title + "Select Series")
@ -982,25 +975,6 @@ class TaggerWindow( QtGui.QMainWindow):
def commitMetadata(self):
if ( self.metadata is not None and self.comic_archive is not None):
if self.comic_archive.isRar() and self.save_data_style == MetaDataStyle.CBI:
if self.settings.ask_about_cbi_in_rar:
checked = OptionalMessageDialog.msg( self, "RAR and ComicBookLover",
"""
You are about to write a CBL tag block to a RAR archive!
While technically possible, no known reader can read those tags from RAR
yet.<br><br>
If you would like this feature in the ComicBookLover apps, please go to their
forums and add your voice to a feature request!
<a href=http://forums.comicbooklover.com/categories/ipad-features>
http://forums.comicbooklover.com/categories/ipad-features</a><br>
<a href=http://forums.comicbooklover.com/categories/mac-features>
http://forums.comicbooklover.com/categories/mac-features</a>
""",
)
self.settings.ask_about_cbi_in_rar = not checked
reply = QtGui.QMessageBox.question(self,
self.tr("Save Tags"),
self.tr("Are you sure you wish to save " + MetaDataStyle.name[self.save_data_style] + " tags to this archive?"),
@ -1408,7 +1382,7 @@ class TaggerWindow( QtGui.QMainWindow):
if reply == QtGui.QMessageBox.Yes:
progdialog = QtGui.QProgressDialog("", "Cancel", 0, has_md_count, self)
progdialog.setWindowTitle( "Removing Tags" )
progdialog.setWindowModality(QtCore.Qt.WindowModal)
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
progdialog.show()
prog_idx = 0
@ -1422,6 +1396,7 @@ class TaggerWindow( QtGui.QMainWindow):
progdialog.setValue(prog_idx)
prog_idx += 1
progdialog.setLabelText( ca.path )
utils.centerWindowOnParent( progdialog )
QtCore.QCoreApplication.processEvents()
if ca.hasMetadata( style ) and ca.isWritable():
@ -1445,6 +1420,7 @@ class TaggerWindow( QtGui.QMainWindow):
dlg = LogWindow( self )
dlg.setText( summary )
dlg.setWindowTitle( "Tag Remove Summary" )
#dlg.adjustSize()
dlg.exec_()
def copyTags( self ):
@ -1483,7 +1459,7 @@ class TaggerWindow( QtGui.QMainWindow):
if reply == QtGui.QMessageBox.Yes:
progdialog = QtGui.QProgressDialog("", "Cancel", 0, has_src_count, self)
progdialog.setWindowTitle( "Copying Tags" )
progdialog.setWindowModality(QtCore.Qt.WindowModal)
progdialog.setWindowModality(QtCore.Qt.ApplicationModal)
progdialog.show()
prog_idx = 0
@ -1497,6 +1473,7 @@ class TaggerWindow( QtGui.QMainWindow):
progdialog.setValue(prog_idx)
prog_idx += 1
progdialog.setLabelText( ca.path )
utils.centerWindowOnParent( progdialog )
QtCore.QCoreApplication.processEvents()
if ca.hasMetadata( src_style ) and ca.isWritable():
@ -1594,6 +1571,7 @@ class TaggerWindow( QtGui.QMainWindow):
found_match = False
choices = False
low_confidence = False
no_match = False
if result == ii.ResultNoMatches:
pass
@ -1611,18 +1589,22 @@ class TaggerWindow( QtGui.QMainWindow):
choices = True
if choices:
self.autoTagLog( "Online search: Multiple matches. Save aborted\n" )
match_results.multipleMatches.append(MultipleMatch(ca,matches))
if low_confidence:
self.autoTagLog( "Online search: Multiple low-confidence matches. Save aborted\n" )
match_results.lowConfidenceMatches.append(MultipleMatch(ca,matches))
else:
self.autoTagLog( "Online search: Multiple matches. Save aborted\n" )
match_results.multipleMatches.append(MultipleMatch(ca,matches))
elif low_confidence and not dlg.autoSaveOnLow:
self.autoTagLog( "Online search: Low confidence match. Save aborted\n" )
match_results.noMatches.append(ca.path)
match_results.lowConfidenceMatches.append(MultipleMatch(ca,matches))
elif not found_match:
self.autoTagLog( "Online search: No match found. Save aborted\n" )
match_results.noMatches.append(ca.path)
else:
# a single match!
if low_confidence:
self.autoTagLog( "Online search: Low confidence match, but saving anyways, as incdicated...\n" )
self.autoTagLog( "Online search: Low confidence match, but saving anyways, as indicated...\n" )
# now get the particular issue data
cv_md = self.actualIssueDataFetch( matches[0] )
@ -1691,6 +1673,7 @@ class TaggerWindow( QtGui.QMainWindow):
self.atprogdialog.progressBar.setValue( prog_idx )
prog_idx += 1
self.atprogdialog.label.setText( ca.path )
utils.centerWindowOnParent( self.atprogdialog )
QtCore.QCoreApplication.processEvents()
if ca.isWritable():
@ -1713,6 +1696,8 @@ class TaggerWindow( QtGui.QMainWindow):
if len ( match_results.multipleMatches ) > 0:
summary += u"Archives with multiple matches: {0}\n".format( len(match_results.multipleMatches))
if len ( match_results.lowConfidenceMatches ) > 0:
summary += u"Archives with one or more low-confidence matches: {0}\n".format( len(match_results.lowConfidenceMatches))
if len ( match_results.noMatches ) > 0:
summary += u"Archives with no matches: {0}\n".format( len(match_results.noMatches))
if len ( match_results.fetchDataFailures ) > 0:
@ -1721,15 +1706,17 @@ class TaggerWindow( QtGui.QMainWindow):
summary += u"Archives that failed due to file writing errors: {0}\n".format( len(match_results.writeFailures))
self.autoTagLog( summary )
if len ( match_results.multipleMatches ) > 0:
summary += u"\n\nDo you want to manually select the ones with multiple matches now?"
sum_selectable = len ( match_results.multipleMatches ) + len(match_results.lowConfidenceMatches)
if sum_selectable > 0:
summary += u"\n\nDo you want to manually select the ones with multiple matches and/or low-confidence matches now?"
reply = QtGui.QMessageBox.question(self,
self.tr(u"Auto-Tag Summary"),
self.tr(summary),
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
match_results.multipleMatches.extend( match_results.lowConfidenceMatches )
if reply == QtGui.QMessageBox.Yes:
matchdlg = AutoTagMatchWindow( self, match_results.multipleMatches, style, self.actualIssueDataFetch)
matchdlg.setModal( True )
@ -1766,6 +1753,7 @@ class TaggerWindow( QtGui.QMainWindow):
self.settings.last_main_window_y = self.y()
self.settings.last_form_side_width = self.splitter.sizes()[0]
self.settings.last_list_side_width = self.splitter.sizes()[1]
self.settings.last_filelist_sorted_column, self.settings.last_filelist_sorted_order = self.fileSelectionList.getSorting()
self.settings.save()
@ -1867,3 +1855,24 @@ class TaggerWindow( QtGui.QMainWindow):
def resizeEvent( self, ev ):
self.splitterMovedEvent( 0, 0)
def tabChanged( self, idx ):
if idx == 0:
self.splitterMovedEvent( 0, 0)
def checkLatestVersionOnline( self ):
self.versionChecker = VersionChecker()
self.versionChecker.versionRequestComplete.connect( self.versionCheckComplete )
self.versionChecker.asyncGetLatestVersion( self.settings.install_id, self.settings.send_usage_stats )
def versionCheckComplete( self, new_version ):
if ( new_version != self.version and
new_version != self.settings.dont_notify_about_this_version):
website = "http://code.google.com/p/comictagger"
checked = OptionalMessageDialog.msg( self, "New version available!",
"New version ({0}) available!<br>(You are currently running {1})<br><br>".format( new_version, self.version) +
"Visit <a href='{0}'>{0}</a> for more info.<br><br>".format(website),
QtCore.Qt.Unchecked,
"Don't tell me about this version again")
if checked:
self.settings.dont_notify_about_this_version = new_version

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

@ -21,6 +21,44 @@
</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">
@ -66,50 +104,6 @@
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="lblArchive">
<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>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="lblTest">
<property name="minimumSize">
<size>
<width>110</width>
<height>165</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>110</width>
<height>165</height>
</size>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

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,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

@ -98,32 +98,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

@ -35,7 +35,7 @@
<string/>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<item row="1" column="0">
<widget class="QPushButton" name="btnResetSettings">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
@ -48,7 +48,7 @@
</property>
</widget>
</item>
<item row="0" column="1">
<item row="1" column="1">
<widget class="QLabel" name="lblDefaultSettings">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@ -64,7 +64,7 @@
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QPushButton" name="btnClearCache">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
@ -77,7 +77,7 @@
</property>
</widget>
</item>
<item row="1" column="1">
<item row="2" column="1">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@ -93,6 +93,13 @@
</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>
@ -307,6 +314,24 @@
</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>
@ -449,7 +474,20 @@
<item row="1" column="1">
<widget class="QLineEdit" name="leRenameTemplate">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The template for the new filename. Accepts the following variables:&lt;/p&gt;&lt;p&gt;%series%&lt;br/&gt;%issue%&lt;br/&gt;%volume%&lt;br/&gt;%issuecount%&lt;br/&gt;%year%&lt;br/&gt;%month%&lt;br/&gt;%month_name%&lt;br/&gt;%publisher%&lt;br/&gt;%title%&lt;/p&gt;&lt;p&gt;Examples:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% %issue% (%year%)&lt;/span&gt;&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% #%issue% - %title%&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The template for the new filename. Accepts the following variables:&lt;/p&gt;&lt;p&gt;%series%&lt;br/&gt;%issue%&lt;br/&gt;%volume%&lt;br/&gt;%issuecount%&lt;br/&gt;%year%&lt;br/&gt;%month%&lt;br/&gt;%month_name%&lt;br/&gt;%publisher%&lt;br/&gt;%title%&lt;br/&gt;
%genre%&lt;br/&gt;
%language_code%&lt;br/&gt;
%criticalrating%&lt;br/&gt;
%alternateseries%&lt;br/&gt;
%alternatenumber%&lt;br/&gt;
%alternatecount%&lt;br/&gt;
%imprint%&lt;br/&gt;
%format%&lt;br/&gt;
%maturityrating%&lt;br/&gt;
%storyarc%&lt;br/&gt;
%seriesgroup%&lt;br/&gt;
%scaninfo%
&lt;/p&gt;&lt;p&gt;Examples:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% %issue% (%year%)&lt;/span&gt;&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% #%issue% - %title%&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>1096</width>
<height>571</height>
<height>621</height>
</rect>
</property>
<property name="sizePolicy">
@ -91,23 +91,26 @@
</property>
<property name="minimumSize">
<size>
<width>220</width>
<width>230</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>220</width>
<width>230</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
<enum>QFrame::Sunken</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="margin">
<number>6</number>
</property>
<item>
<widget class="QLabel" name="lblFilename">
<property name="sizePolicy">
@ -202,7 +205,7 @@
</widget>
</item>
<item>
<widget class="QLabel" name="lblCover">
<widget class="QWidget" name="coverImageContainer" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
@ -211,31 +214,16 @@
</property>
<property name="minimumSize">
<size>
<width>220</width>
<height>330</height>
<width>230</width>
<height>380</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>220</width>
<height>330</height>
<width>230</width>
<height>380</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>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
@ -1107,7 +1095,7 @@
<x>0</x>
<y>0</y>
<width>1096</width>
<height>21</height>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuComicTagger">

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>

View File

@ -19,9 +19,63 @@ 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 os
import re
import platform
import locale
import codecs
class UtilsVars:
already_fixed_encoding = False
def get_actual_preferred_encoding():
preferred_encoding = locale.getpreferredencoding()
if getattr(sys, 'frozen', None) and platform.system() == "Darwin":
preferred_encoding = "utf-8"
return preferred_encoding
def fix_output_encoding( ):
if not UtilsVars.already_fixed_encoding:
# this reads the environment and inits the right locale
locale.setlocale(locale.LC_ALL, "")
# try to make stdout/stderr encodings happy for unicode printing
preferred_encoding = get_actual_preferred_encoding()
sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout)
sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr)
UtilsVars.already_fixed_encoding = True
def get_recursive_filelist( pathlist ):
"""
Get a recursive list of of all files under all path items in the list
"""
filename_encoding = sys.getfilesystemencoding()
filelist = []
for p in pathlist:
# if path is a folder, walk it recursivly, and all files underneath
if type(p) == str:
#make sure string is unicode
p = p.decode(filename_encoding) #, 'replace')
elif type(p) != unicode:
#it's probably a QString
p = unicode(p)
if os.path.isdir( p ):
for root,dirs,files in os.walk( p ):
for f in files:
if type(f) == str:
#make sure string is unicode
f = f.decode(filename_encoding, 'replace')
elif type(f) != unicode:
#it's probably a QString
f = unicode(f)
filelist.append(os.path.join(root,f))
else:
filelist.append(p)
return filelist
def listToString( l ):
string = ""
if l is not None:
@ -31,10 +85,16 @@ def listToString( l ):
string += item
return string
def addtopath( dir ):
# TODO only add if not there already
if dir is not None and dir != "":
os.environ['PATH'] = dir + os.pathsep + os.environ['PATH']
def addtopath( dirname ):
if dirname is not None and dirname != "":
# verify that path doesn't already contain the given dirname
tmpdirname = re.escape(dirname)
pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep)
match = re.search(pattern, os.environ['PATH'])
if not match:
os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH']
# returns executable path, if it exists
def which(program):
@ -66,9 +126,13 @@ def removearticles( text ):
# now get rid of some other junk
newText = newText.replace(":", "")
newText = newText.replace(".", "")
newText = newText.replace(",", "")
newText = newText.replace("-", " ")
# since the CV api changed, searches for series names with periods
# now explicity require the period to be in the search key,
# so the line below is removed (for now)
#newText = newText.replace(".", "")
return newText
@ -520,3 +584,49 @@ def getLanguageFromISO( iso ):
else:
return lang_dict[ iso ]
try:
from PyQt4 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 implemention 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 calulated as screenwidth - windowwidth /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 calulated as screenwidth - windowwidth /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())

View File

@ -0,0 +1,91 @@
"""
Version checker
"""
"""
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 sys
import os
import platform
import urllib,urllib2
import ctversion
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
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
class VersionChecker(QObject):
def getRequestUrl( self, uuid, use_stats ):
base_url = "http://comictagger1.appspot.com/latest"
args = ""
if use_stats:
if platform.system() == "Windows":
plat = "win"
elif platform.system() == "Linux":
plat = "lin"
elif platform.system() == "Darwin":
plat = "mac"
else:
plat = "other"
args = "?uuid={0}&platform={1}&version={2}".format(uuid, plat, ctversion.version)
if not getattr(sys, 'frozen', None):
args += "&src=T"
return base_url+args
def getLatestVersion( self, uuid, use_stats=True):
try:
resp = urllib2.urlopen( self.getRequestUrl(uuid, use_stats ))
new_version = resp.read()
except Exception as e:
return None
if new_version is None or new_version == "":
return None
return new_version.strip()
versionRequestComplete = pyqtSignal( str )
def asyncGetLatestVersion( self, uuid, use_stats ):
url = self.getRequestUrl( uuid, use_stats )
self.nam = QNetworkAccessManager()
self.nam.finished.connect( self.asyncGetLatestVersionComplete )
self.nam.get(QNetworkRequest(QUrl(str(url))))
def asyncGetLatestVersionComplete( self, reply ):
# read in the response
new_version = str(reply.readAll())
if new_version is None or new_version == "":
return
self.versionRequestComplete.emit( new_version.strip() )

View File

@ -34,6 +34,8 @@ from imagefetcher import ImageFetcher
from progresswindow import IDProgressWindow
from settings import ComicTaggerSettings
from matchselectionwindow import MatchSelectionWindow
from coverimagewidget import CoverImageWidget
import utils
class SearchThread( QtCore.QThread):
@ -85,15 +87,28 @@ class IdentifyThread( QtCore.QThread):
class VolumeSelectionWindow(QtGui.QDialog):
def __init__(self, parent, series_name, issue_number, year, cover_index_list, comic_archive, settings, autoselect=False):
def __init__(self, parent, series_name, issue_number, year, issue_count, cover_index_list, comic_archive, settings, autoselect=False):
super(VolumeSelectionWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'volumeselectionwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('volumeselectionwindow.ui' ), self)
self.imageWidget = CoverImageWidget( self.imageContainer, CoverImageWidget.URLMode )
gridlayout = QtGui.QGridLayout( self.imageContainer )
gridlayout.addWidget( self.imageWidget )
gridlayout.setContentsMargins(0,0,0,0)
utils.reduceWidgetFontSize( self.teDetails, 1 )
utils.reduceWidgetFontSize( self.twList )
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.settings = settings
self.series_name = series_name
self.issue_number = issue_number
self.year = year
self.issue_count = issue_count
self.volume_id = 0
self.comic_archive = comic_archive
self.immediate_autoselect = autoselect
@ -108,7 +123,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.btnAutoSelect.clicked.connect(self.autoSelect)
self.updateButtons()
self.performQuery()
self.performQuery()
self.twList.selectRow(0)
def updateButtons( self ):
@ -147,10 +162,11 @@ class VolumeSelectionWindow(QtGui.QDialog):
md.series = self.series_name
md.issue = self.issue_number
md.year = self.year
md.issueCount = self.issue_count
self.ii.setAdditionalMetadata( md )
self.ii.onlyUseAdditionalMetaData = True
print self.cover_index_list
self.ii.cover_page_index = int(self.cover_index_list[0])
self.id_thread = IdentifyThread( self.ii )
@ -180,54 +196,43 @@ class VolumeSelectionWindow(QtGui.QDialog):
result = self.ii.search_result
match_index = 0
found_match = False
found_match = None
choices = False
if result == self.ii.ResultNoMatches:
QtGui.QMessageBox.information(self,"Auto-Select Result", " No matches found :-(")
elif result == self.ii.ResultFoundMatchButBadCoverScore:
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but cover doesn't seem the same. Verify before commiting!")
found_match = True
found_match = matches[0]
elif result == self.ii.ResultFoundMatchButNotFirstPage :
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but not with the first page of the archive.")
found_match = True
found_match = matches[0]
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually.")
choices = True
elif result == self.ii.ResultOneGoodMatch:
found_match = True
found_match = matches[0]
elif result == self.ii.ResultMultipleGoodMatches:
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found multiple likely matches. Please select.")
choices = True
if choices:
selector = MatchSelectionWindow( self, matches )
selector = MatchSelectionWindow( self, matches, self.comic_archive )
selector.setModal(True)
title = self.series_name
title += " #" + self.issue_number
if self.year is not None:
title += " (" + str(self.year) + ")"
title += " - "
selector.setWindowTitle( title + "Select Match")
selector.exec_()
if selector.result():
#we should now have a list index
found_match = True
match_index = selector.current_row
found_match = selector.currentMatch()
if found_match:
if found_match is not None:
self.iddialog.accept()
self.volume_id = matches[match_index]['volume_id']
self.issue_number = matches[match_index]['issue_number']
self.volume_id = found_match['volume_id']
self.issue_number = found_match['issue_number']
self.selectByID()
self.showIssues()
def showIssues( self ):
selector = IssueSelectionWindow( self, self.settings, self.volume_id, self.issue_number )
selector.setModal(True)
title = ""
for record in self.cv_search_results:
if record['id'] == self.volume_id:
@ -237,6 +242,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
break
selector.setWindowTitle( title + "Select Issue")
selector.setModal( True )
selector.exec_()
if selector.result():
#we should now have a volume ID
@ -365,23 +371,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
# list selection was changed, update the info on the volume
for record in self.cv_search_results:
if record['id'] == self.volume_id:
self.teDetails.setText ( record['description'] )
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
url = record['image']['super_url']
self.fetcher = ImageFetcher( )
self.fetcher.fetchComplete.connect(self.finishRequest)
self.fetcher.fetch( url, user_data=record['id'] )
def finishRequest(self, image_data, user_data):
# called when the image is done loading
img = QtGui.QImage()
img.loadFromData( image_data )
self.setCover( img )
def setCover( self, img ):
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
if record['description'] is None:
self.teDetails.setText ( "" )
else:
self.teDetails.setText ( record['description'] )
self.imageWidget.setURL( record['image']['super_url'] )
break

View File

@ -1,403 +0,0 @@
"""
A python class to manage communication with Comic Vine's REST API
"""
"""
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 pprint import pprint
import urllib2, urllib
import math
import re
import datetime
import ctversion
import sys
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
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
import utils
from settings import ComicTaggerSettings
from comicvinecacher import ComicVineCacher
from genericmetadata import GenericMetadata
from issuestring import IssueString
class ComicVineTalkerException(Exception):
pass
class ComicVineTalker(QObject):
def __init__(self, api_key=""):
QObject.__init__(self)
# key that is registered to comictagger
self.api_key = '27431e6787042105bd3e47e169a624521f89f3a4'
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()
else:
self.log_func( text )
def testKey( self ):
test_url = "http://api.comicvine.com/issue/1/?api_key=" + self.api_key + "&format=json&field_list=name"
resp = urllib2.urlopen( test_url )
content = resp.read()
cv_response = json.loads( content )
# Bogus request, but if the key is wrong, you get error 100: "Invalid API Key"
return cv_response[ 'status_code' ] != 100
def getUrlContent( self, url ):
try:
resp = urllib2.urlopen( url )
return resp.read()
except Exception as e:
self.writeLog( str(e) )
raise ComicVineTalkerException("Network Error!")
def searchForSeries( self, series_name , callback=None, refresh_cache=False ):
# remove cruft from the search string
series_name = utils.removearticles( 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
original_series_name = series_name
series_name = urllib.quote_plus(series_name.encode("utf-8"))
#series_name = urllib.quote_plus(unicode(series_name))
search_url = "http://api.comicvine.com/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues&sort=start_year"
content = self.getUrlContent(search_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
return None
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']
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'])
offset = 0
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))
offset += limit
content = self.getUrlContent(search_url + "&offset="+str(offset))
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] ))
return None
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 )
#for record in search_results:
# print( "{0}: {1} ({2})".format(record['id'], smart_str(record['name']) , record['start_year'] ) )
# print( "{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year'] ) )
#print "{0}: {1} ({2})".format(search_results['results'][0]['id'], smart_str(search_results['results'][0]['name']) , search_results['results'][0]['start_year'] )
# cache these search results
cvc.add_search_results( original_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 = "http://api.comicvine.com/volume/" + str(series_id) + "/?api_key=" + self.api_key + "&format=json"
content = self.getUrlContent(volume_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
return None
volume_results = cv_response['results']
cvc.add_volume_info( volume_results )
return volume_results
def fetchIssueData( self, series_id, issue_number, settings ):
volume_results = self.fetchVolumeData( series_id )
found = False
for record in volume_results['issues']:
if IssueString(issue_number).asFloat() is None:
issue_number = 1
if float(record['issue_number']) == IssueString(issue_number).asFloat():
found = True
break
if (found):
issue_url = "http://api.comicvine.com/issue/" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
content = self.getUrlContent(issue_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
return None
issue_results = cv_response['results']
else:
return None
# now, map the comicvine data to generic metadata
return self.mapCVDataToMetadata( volume_results, issue_results, settings )
def fetchIssueDataByIssueID( self, issue_id, settings ):
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
content = self.getUrlContent(issue_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
return None
issue_results = cv_response['results']
volume_results = self.fetchVolumeData( issue_results['volume']['id'] )
# now, map the comicvine 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 comicvine data to generic metadata
metadata = GenericMetadata()
metadata.series = issue_results['volume']['name']
num_s = IssueString(issue_results['issue_number']).asString()
metadata.issue = num_s
metadata.title = issue_results['name']
metadata.publisher = volume_results['publisher']['name']
metadata.month = issue_results['publish_month']
metadata.year = issue_results['publish_year']
#metadata.issueCount = volume_results['count_of_issues']
metadata.comments = self.cleanup_html(issue_results['description'])
if settings.use_series_start_as_volume:
metadata.volume = 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:
for role in person['roles']:
# can we determine 'primary' from CV??
role_name = role['role'].title()
metadata.addCredit( person['name'], role['role'].title(), 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 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")
# now strip all other tags
p = re.compile(r'<[^<]*?>')
newstring = p.sub('',string)
newstring = newstring.replace('&nbsp;',' ')
newstring = newstring.replace('&amp;','&')
newstring = newstring.strip()
return newstring
def fetchIssueDate( self, issue_id ):
image_url, thumb_url, month,year = self.fetchIssueSelectDetails( issue_id )
return month, year
def fetchIssueCoverURLs( self, issue_id ):
image_url, thumb_url, month,year = self.fetchIssueSelectDetails( issue_id )
return image_url, thumb_url
def fetchIssueSelectDetails( self, issue_id ):
cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails( issue_id )
if cached_image_url is not None:
return cached_image_url,cached_thumb_url, cached_month, cached_year
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year"
content = self.getUrlContent(issue_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
return None, None,None,None
image_url = cv_response['results']['image']['super_url']
thumb_url = cv_response['results']['image']['thumb_url']
year = cv_response['results']['publish_year']
month = cv_response['results']['publish_month']
if image_url is not None:
self.cacheIssueSelectDetails( issue_id, image_url,thumb_url, month, year )
return image_url,thumb_url,month,year
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, month, year ):
cvc = ComicVineCacher( )
cvc.add_issue_select_details( issue_id, image_url, thumb_url, month, year )
#---------------------------------------------------------------------------
urlFetchComplete = pyqtSignal( str , str, int)
def asyncFetchIssueCoverURLs( self, issue_id ):
self.issue_id = issue_id
cached_image_url,cached_thumb_url,month,year = self.fetchCachedIssueSelectDetails( issue_id )
if cached_image_url is not None:
self.urlFetchComplete.emit( cached_image_url,cached_thumb_url, self.issue_id )
return
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year"
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()
cv_response = json.loads(str(data))
if cv_response[ 'status_code' ] != 1:
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
return
image_url = cv_response['results']['image']['super_url']
thumb_url = cv_response['results']['image']['thumb_url']
year = cv_response['results']['publish_year']
month = cv_response['results']['publish_month']
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, month, year )
self.urlFetchComplete.emit( image_url, thumb_url, self.issue_id )

1
current_version.txt Normal file
View File

@ -0,0 +1 @@
1.1.7-beta

View File

@ -1,232 +0,0 @@
"""
Functions for parsing comic info from filename
This should probably be re-written, but, well, it mostly works!
"""
"""
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.
"""
# Some portions of this code were modified from pyComicMetaThis project
# http://code.google.com/p/pycomicmetathis/
import re
import os
from urllib import unquote
class FileNameParser:
def fixSpaces( self, string ):
placeholders = ['[-_]',' +']
for ph in placeholders:
string = re.sub(ph, ' ', string )
return string.strip()
# check for silly .1 or .5 style issue strings
# allow up to 5 chars total
def isPointIssue( self, word ):
ret = False
try:
float(word)
if (len(word) < 5 and not word.isdigit()):
ret = True
except ValueError:
pass
return ret
def getIssueCount( self,filename ):
count = ""
# replace any name seperators with spaces
tmpstr = self.fixSpaces(filename)
found = False
match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE)
if match:
count = match.group()
found = True
if not found:
match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE)
if match:
count = match.group()
found = True
count = count.lstrip("0")
return count
def getIssueNumber( self, filename ):
found = False
issue = ''
# first, look for multiple "--", this mean's it's formatted differently from most:
if "--" in filename:
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
filename = filename.split("--")[0]
elif "___" in filename:
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
filename = filename.split("__")[0]
filename = filename.replace("+", " ")
# remove parenthetical phrases
filename = re.sub( "\(.*\)", "", filename)
filename = re.sub( "\[.*\]", "", filename)
# guess based on position
# replace any name seperators with spaces
tmpstr = self.fixSpaces(filename)
word_list = tmpstr.split(' ')
#before we search, remove any kind of likely "of X" phrase
for i in range(0, len(word_list)-2):
if ( word_list[i].isdigit() and
word_list[i+1] == "of" and
word_list[i+2].isdigit() ):
word_list[i+1] ="XXX"
word_list[i+2] ="XXX"
# first look for the last "#" followed by a digit in the filename. this is almost certainly the issue number
#issnum = re.search('#\d+', filename)
matchlist = re.findall("#\d+", filename)
if len(matchlist) > 0:
#get the last item
issue = matchlist[ len(matchlist) - 1]
issue = issue[1:]
found = True
# assume the last number in the filename that is under 4 digits is the issue number
if not found:
for word in reversed(word_list):
if len(word) > 0 and word[0] == "#":
word = word[1:]
if (
(word.isdigit() and len(word) < 4) or
(self.isPointIssue(word))
):
issue = word
found = True
#print 'Assuming issue number is ' + str(issue) + ' based on the position.'
break
if not found:
# try a regex
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]|\d+\.\d|\d+)', filename)
if issnum:
issue = issnum.group()
found = True
#print 'Got the issue using regex. Issue is ' + issue
return issue.strip()
def getSeriesName(self, filename, issue ):
# use the issue number string to split the filename string
# assume first element of list is the series name, plus cruft
#!!! this could fail in the case of small numerics in the series name!!!
# TODO: we really should pass in the *INDEX* of the issue, that makes
# finding it easier
filename = filename.replace("+", " ")
tmpstr = self.fixSpaces(filename)
#remove pound signs. this might mess up the series name if there is a# in it.
tmpstr = tmpstr.replace("#", " ")
if issue != "":
# assume that issue substr has at least one space before it
issue_str = " " + str(issue)
series = tmpstr.split(issue_str)[0]
else:
# no issue to work off of
#!!! TODO we should look for the year, and split from that
# and if that doesn't exist, remove parenthetical phrases
series = tmpstr
series = re.sub( "\(.*\)", "", tmpstr)
volume = ""
series = series.rstrip("#")
# search for volume number
match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series)
if match:
series = match.group(1)
volume = match.group(3)
return series.strip(), volume.strip()
def getYear( self,filename):
year = ""
# look for four digit number with "(" ")" or "--" around it
match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename)
if match:
year = match.group()
# remove non-numerics
year = re.sub("[^0-9]", "", year)
return year
def parseFilename( self, filename ):
# remove the path
filename = os.path.basename(filename)
# remove the extension
filename = os.path.splitext(filename)[0]
#url decode, just in case
filename = unquote(filename)
# sometimes archives get messed up names from too many decodings
# often url encodings will break and leave "_28" and "_29" in place
# of "(" and ")" see if there are a number of these, and replace them
if filename.count("_28") > 1 and filename.count("_29") > 1:
filename = filename.replace("_28", "(")
filename = filename.replace("_29", ")")
# ----HACK
# remove the first word that word is a 3 digit number.
# some story arcs collection packs do this, but it's ugly
# this will probably break something, i.e. "100 bullets"
#word = filename.split(' ')[0]
#if len(word) == 3 and word[0] =='0' and word.isdigit():
# filename = filename[4:]
# ----HACK -
self.issue = self.getIssueNumber(filename)
self.series, self.volume = self.getSeriesName(filename, self.issue)
self.year = self.getYear(filename)
self.issue_count = self.getIssueCount(filename)
if self.issue != "":
# strip off leading zeros
self.issue = self.issue.lstrip("0")
if self.issue == "":
self.issue = "0"
if self.issue[0] == ".":
self.issue = "0" + self.issue

11
google/gadgets/social.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="mygaget" />
<Content type="html">
<![CDATA[
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
<iframe allowtransparency="true" frameborder="0" scrolling="no" src="http://www.facebook.com/plugins/likebox.php?href=http%3A%2F%2Fwww.facebook.com%2Fpages%2FComictagger/139615369550787&amp;width=292&amp;colorscheme=light&amp;show_faces =false&amp;border_color&amp;stream=false&amp;header=false&amp;height=62" style="background-color: white; border-bottom-style: none; border-color: initial; border-left-style: none; border-right-style: none; border-top-style: none; border-width: initial; color: #333333; font-family: Verdana; font-size: 12px; height: 62px; line-height: 19px; overflow-x: hidden; overflow-y: hidden; text-align: -webkit-auto; width: 292px;"></iframe>
]]>
</Content>
</Module>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="mygaget" />
<Content type="html">
<![CDATA[
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
]]>
</Content>
</Module>

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