Compare commits

...

99 Commits

Author SHA1 Message Date
719c711484 Language tweak
git-svn-id: http://comictagger.googlecode.com/svn/trunk@668 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 18:03:08 +00:00
afbbc9d00c git-svn-id: http://comictagger.googlecode.com/svn/trunk@667 6c5673fe-1810-88d6-992b-cd32ca31540c 2014-03-23 17:48:59 +00:00
b8e0a45fc8 bumped version and release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@665 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 17:31:00 +00:00
b7360dd33e Updated copyright dates
git-svn-id: http://comictagger.googlecode.com/svn/trunk@664 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 17:30:23 +00:00
d9f1956426 handle a crash bug when file starts with --
git-svn-id: http://comictagger.googlecode.com/svn/trunk@663 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-23 16:56:04 +00:00
b5c7f36410 New pyunrar version to handle rar tools 5.x
git-svn-id: http://comictagger.googlecode.com/svn/trunk@662 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:43:03 +00:00
0b0663d935 Update copyright date
git-svn-id: http://comictagger.googlecode.com/svn/trunk@661 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:42:07 +00:00
eee1f65436 handle corner case of non-numeric issue ending in "."
git-svn-id: http://comictagger.googlecode.com/svn/trunk@660 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:41:38 +00:00
9a8d4149f2 fixed spelling error
git-svn-id: http://comictagger.googlecode.com/svn/trunk@659 6c5673fe-1810-88d6-992b-cd32ca31540c
2014-03-22 21:40:01 +00:00
b02a205668 Make sure all error print outs are unicode
Catch error when zipfile list fails

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

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

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


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

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

View File

@ -1,4 +1,4 @@
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
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)

View File

@ -1,4 +1,4 @@
ComicTagger is a multi-platform app for writing metadata to comic archives, written in Python and PyQt.
ComicTagger is a multi-platform app for writing metadata to digital comics, written in Python and PyQt.
Features:

View File

@ -33,7 +33,7 @@ similar to the C interface provided by UnRAR. There is also a
higher level interface which makes some common operations easier.
"""
__version__ = '0.99.2'
__version__ = '0.99.3'
try:
WindowsError

View File

@ -12,13 +12,11 @@ def cleanup(dir='test'):
os.removedirs(os.path.join(path, dir))
# reuse RarArchive object, en
# basic test
cleanup()
rarc = UnRAR2.RarFile('test.rar')
rarc.infolist()
assert rarc.comment == "This is a test."
for info in rarc.infoiter():
saveinfo = info
assert (str(info)=="""<RarInfo "test" in "test.rar">""")
@ -98,7 +96,8 @@ list(UnRAR2.RarFile('test_nulls.rar').infoiter())
# extract files from an archive with protected files
cleanup()
UnRAR2.RarFile('test_protected_files.rar', password="protected").extract()
rarc = UnRAR2.RarFile('test_protected_files.rar', password="protected")
rarc.extract()
assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt')
cleanup()
errored = False

View File

@ -33,6 +33,7 @@ from rar_exceptions import *
class UnpackerNotInstalled(Exception): pass
rar_executable_cached = None
rar_executable_version = None
def call_unrar(params):
"Calls rar/unrar command line executable, returns stdout pipe"
@ -59,10 +60,10 @@ def call_unrar(params):
class RarFileImplementation(object):
def init(self, password=None):
global rar_executable_version
self.password = password
stdoutdata, stderrdata = self.call('v', []).communicate()
for line in stderrdata.splitlines():
@ -73,18 +74,42 @@ class RarFileImplementation(object):
accum = []
source = iter(stdoutdata.splitlines())
line = ''
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
while not (line.startswith('UNRAR')):
line = source.next()
while not line.startswith('Pathname/Comment'):
accum.append(line.rstrip('\n'))
signature = line
# The code below is mighty flaky
# and will probably crash on localized versions of RAR
# but I see no safe way to rewrite it using a CLI tool
if signature.startswith("UNRAR 4"):
rar_executable_version = 4
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
line = source.next()
while not line.startswith('Pathname/Comment'):
accum.append(line.rstrip('\n'))
line = source.next()
if len(accum):
accum[0] = accum[0][9:] # strip out "Comment:" part
self.comment = '\n'.join(accum[:-1])
else:
self.comment = None
elif signature.startswith("UNRAR 5"):
rar_executable_version = 5
line = source.next()
if len(accum):
accum[0] = accum[0][9:]
self.comment = '\n'.join(accum[:-1])
while not line.startswith('Archive:'):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
accum.append(line.rstrip('\n'))
line = source.next()
if len(accum):
self.comment = '\n'.join(accum[:-1]).strip()
else:
self.comment = None
else:
self.comment = None
raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: "
+ signature.split(" ")[1])
def escaped_password(self):
return '-' if self.password == None else self.password
@ -97,7 +122,8 @@ class RarFileImplementation(object):
def infoiter(self):
stdoutdata, stderrdata = self.call('v', ['c-']).communicate()
command = "v" if rar_executable_version == 4 else "l"
stdoutdata, stderrdata = self.call(command, ['c-']).communicate()
for line in stderrdata.splitlines():
if line.strip().startswith("Cannot open"):
@ -106,33 +132,48 @@ class RarFileImplementation(object):
accum = []
source = iter(stdoutdata.splitlines())
line = ''
while not line.startswith('--------------'):
while not line.startswith('-----------'):
if line.strip().endswith('is not RAR archive'):
raise InvalidRARArchive
if line.find("CRC failed")>=0:
if line.startswith("CRC failed") or line.startswith("Checksum error"):
raise IncorrectRARPassword
line = source.next()
line = source.next()
i = 0
re_spaces = re.compile(r"\s+")
while not line.startswith('--------------'):
accum.append(line)
if len(accum)==2:
if rar_executable_version == 4:
while not line.startswith('-----------'):
accum.append(line)
if len(accum)==2:
data = {}
data['index'] = i
# asterisks mark password-encrypted files
data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files
fields = re_spaces.split(accum[1].strip())
data['size'] = int(fields[0])
attr = fields[5]
data['isdir'] = 'd' in attr.lower()
data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M')
data['comment'] = None
yield data
accum = []
i += 1
line = source.next()
elif rar_executable_version == 5:
while not line.startswith('-----------'):
fields = line.strip().lstrip("*").split()
data = {}
data['index'] = i
#!!!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]
data['filename'] = " ".join(fields[4:])
data['size'] = int(fields[1])
attr = fields[0]
data['isdir'] = 'd' in attr.lower()
data['datetime'] = time.strptime(info[3]+" "+info[4], '%d-%m-%y %H:%M')
data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M')
data['comment'] = None
yield data
accum = []
i += 1
line = source.next()
line = source.next()
def read_files(self, checker):
res = []
@ -153,7 +194,7 @@ class RarFileImplementation(object):
if overwrite:
options.append('o+')
else:
options.append('o-')
options.append('o-')
if not path.endswith(os.sep):
path += os.sep
names = []
@ -167,7 +208,7 @@ class RarFileImplementation(object):
names.append(path)
proc = self.call(command, options, names)
stdoutdata, stderrdata = proc.communicate()
if stderrdata.find("CRC failed")>=0:
if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0:
raise IncorrectRARPassword
return res

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to select from automated issue matches
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -38,7 +38,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
def __init__(self, parent, match_set_list, style, fetch_func):
super(AutoTagMatchWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('autotagmatchwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('matchselectionwindow.ui' ), self)
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
gridlayout = QtGui.QGridLayout( self.altCoverContainer )
@ -51,6 +51,7 @@ class AutoTagMatchWindow(QtGui.QDialog):
gridlayout.setContentsMargins(0,0,0,0)
utils.reduceWidgetFontSize( self.twList )
utils.reduceWidgetFontSize( self.teDescription, 1 )
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
@ -132,7 +133,9 @@ class AutoTagMatchWindow(QtGui.QDialog):
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
item_text = match['issue_title']
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)
@ -159,6 +162,10 @@ class AutoTagMatchWindow(QtGui.QDialog):
return
self.altCoverWidget.setIssueID( self.currentMatch()['issue_id'] )
if self.currentMatch()['description'] is None:
self.teDescription.setText ( "" )
else:
self.teDescription.setText ( self.currentMatch()['description'] )
def setCoverImage( self ):
ca = self.current_match_set.ca

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to show ID log and progress
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import sys
from PyQt4 import QtCore, QtGui, uic
import os
from settings import ComicTaggerSettings
from coverimagewidget import CoverImageWidget
import utils
class AutoTagProgressWindow(QtGui.QDialog):
@ -31,8 +32,17 @@ class AutoTagProgressWindow(QtGui.QDialog):
super(AutoTagProgressWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('autotagprogresswindow.ui' ), self)
self.lblTest.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
self.lblArchive.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
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() |
@ -42,23 +52,16 @@ class AutoTagProgressWindow(QtGui.QDialog):
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(ComicTaggerSettings.getGraphic('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

@ -3,7 +3,7 @@ A PyQT4 dialog to confirm and set options for auto-tag
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ Class to manage modifying metadata specifically for CBL/CBI
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -92,7 +92,7 @@ def actual_metadata_save( ca, opts, md ):
return True
def display_match_set_for_choice( label, match_set, opts, settings ):
print "{0} -- {1}:".format(match_set.filename, label )
print u"{0} -- {1}:".format(match_set.filename, label )
# sort match list by year
match_set.matches.sort(key=lambda k: k['year'])
@ -292,21 +292,21 @@ def process_file_cli( filename, opts, settings, match_results ):
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 )
print u"{0}: Tag removal seemed to fail!".format( filename )
else:
print "{0}: Removed {1} tags.".format( filename, style_name )
print u"{0}: Removed {1} tags.".format( filename, style_name )
else:
print "{0}: dry-run. {1} tags not removed".format( filename, style_name )
print u"{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 )
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 "{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
print u"{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)
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 ]
@ -337,7 +337,10 @@ def process_file_cli( filename, opts, settings, match_results ):
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:

View File

@ -3,7 +3,7 @@ A python class to encapsulate CoMet data
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A python class to represent a single comic, be it file or folder of images
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -72,15 +72,16 @@ class ZipArchiver:
def readArchiveFile( self, archive_file ):
data = ""
zf = zipfile.ZipFile( self.path, 'r' )
try:
data = zf.read( archive_file )
except zipfile.BadZipfile as e:
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
zf.close()
raise IOError
except Exception as e:
zf.close()
print >> sys.stderr, "bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file)
raise IOError
finally:
zf.close()
@ -109,12 +110,16 @@ class ZipArchiver:
except:
return False
def getArchiveFilenameList( self ):
zf = zipfile.ZipFile( self.path, 'r' )
namelist = zf.namelist()
zf.close()
return namelist
def getArchiveFilenameList( self ):
try:
zf = zipfile.ZipFile( self.path, 'r' )
namelist = zf.namelist()
zf.close()
return namelist
except Exception as e:
print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(e, self.path)
return []
# zip helper func
def rebuildZipFile( self, exclude_list ):
@ -222,7 +227,7 @@ class ZipArchiver:
if not self.writeZipComment( self.path, comment ):
return False
except Exception as e:
print >> sys.stderr, "Error while copying to {0}: {1}".format(self.path, e)
print >> sys.stderr, u"Error while copying to {0}: {1}".format(self.path, e)
return False
else:
return True
@ -301,22 +306,22 @@ class RarArchiver:
entries = rarc.read_files( archive_file )
if entries[0][0].size != len(entries[0][1]):
print >> sys.stderr, "readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
print >> sys.stderr, u"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 >> sys.stderr, "readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
time.sleep(1)
except Exception as e:
print >> sys.stderr, "Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries)
print >> sys.stderr, u"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 >> sys.stderr, "Attempted read_files() {0} times".format(tries)
print >> sys.stderr, u"Attempted read_files() {0} times".format(tries)
if (len(entries) == 1):
return entries[0][1]
else:
@ -392,7 +397,7 @@ class RarArchiver:
namelist.append( item.filename )
except (OSError, IOError) as e:
print >> sys.stderr, "getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
time.sleep(1)
else:
@ -410,7 +415,7 @@ class RarArchiver:
rarc = UnRAR2.RarFile( self.path )
except (OSError, IOError) as e:
print >> sys.stderr, "getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries)
time.sleep(1)
else:
@ -512,19 +517,20 @@ class ComicArchive:
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():
self.archive_type = self.ArchiveType.Zip
self.archiver = ZipArchiver( self.path )
elif self.rarTest():
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 os.path.isdir( self.path ):
self.archive_type = self.ArchiveType.Folder
self.archiver = FolderArchiver( self.path )
@ -609,7 +615,7 @@ class ComicArchive:
if (
( self.isZip() or self.isRar() ) #or self.isFolder() )
and
( self.getNumberOfPages() > 2)
( self.getNumberOfPages() > 0)
):
return True
@ -670,7 +676,7 @@ class ComicArchive:
try:
image_data = self.archiver.readArchiveFile( filename )
except IOError:
print >> sys.stderr, "Error reading in page. Substituting logo page."
print >> sys.stderr, u"Error reading in page. Substituting logo page."
image_data = ComicArchive.logo_data
return image_data
@ -921,7 +927,7 @@ class ComicArchive:
try:
raw_comet = self.archiver.readArchiveFile( self.comet_filename )
except IOError:
print >> sys.stderr, "Error reading in raw CoMet!"
print >> sys.stderr, u"Error reading in raw CoMet!"
raw_comet = ""
return raw_comet
@ -972,7 +978,7 @@ class ComicArchive:
data = self.archiver.readArchiveFile( n )
except:
data = ""
print >> sys.stderr, "Error reading in Comet XML for validation!"
print >> sys.stderr, u"Error reading in Comet XML for validation!"
if CoMet().validateString( data ):
# since we found it, save it!
self.comet_filename = n
@ -1027,6 +1033,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

@ -3,7 +3,7 @@ A python class to encapsulate the ComicBookInfo data
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A python class to encapsulate ComicRack's ComicInfo.xml data
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -260,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

@ -3,7 +3,7 @@ A python class to manage caching of data from Comic Vine
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -110,13 +110,11 @@ class ComicVineCacher:
"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 ) )"
)
@ -263,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 ):
@ -288,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 ] )
@ -311,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, site_detail_url ):
def add_issue_select_details( self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url ):
con = lite.connect( self.db_file )
@ -339,10 +375,9 @@ 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
}
@ -357,23 +392,21 @@ class ComicVineCacher:
cur = con.cursor()
con.text_factory = unicode
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year,site_detail_url 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()
details = dict()
if row is None or row[0] is None :
details['image_url'] = None
details['thumb_image_url'] = None
details['publish_month'] = None
details['publish_year'] = None
details['cover_date'] = None
details['site_detail_url'] = None
else:
details['image_url'] = row[0]
details['thumb_image_url'] = row[1]
details['publish_month'] = row[2]
details['publish_year'] = row[3]
details['site_detail_url'] = row[4]
details['cover_date'] = row[2]
details['site_detail_url'] = row[3]
return details

View File

@ -3,7 +3,7 @@ A python class to manage communication with Comic Vine's REST API
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ from pprint import pprint
import urllib2, urllib
import math
import re
import time
import datetime
import ctversion
import sys
@ -49,15 +50,22 @@ 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'
@ -74,9 +82,22 @@ class ComicVineTalker(QObject):
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 = "http://api.comicvine.com/issue/1/?api_key=" + self.api_key + "&format=json&field_list=name"
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()
@ -86,12 +107,28 @@ class ComicVineTalker(QObject):
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!")
# 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 ):
@ -108,12 +145,23 @@ class ComicVineTalker(QObject):
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"
# 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"))
content = self.getUrlContent(search_url)
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)
@ -132,7 +180,7 @@ class ComicVineTalker(QObject):
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
page = 1
if callback is not None:
callback( current_result_count, total_result_count )
@ -141,8 +189,9 @@ class ComicVineTalker(QObject):
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))
page += 1
content = self.getUrlContent(search_url + "&page="+str(page))
cv_response = json.loads(content)
@ -157,10 +206,10 @@ class ComicVineTalker(QObject):
#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'] )
# #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 )
@ -178,7 +227,7 @@ class ComicVineTalker(QObject):
return cached_volume_result
volume_url = "http://api.comicvine.com/volume/" + str(series_id) + "/?api_key=" + self.api_key + "&format=json"
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)
@ -192,22 +241,130 @@ class ComicVineTalker(QObject):
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 volume_results['issues']:
if IssueString(issue_number).asFloat() is None:
for record in issues_list_results:
if IssueString(issue_number).asString() is None:
issue_number = 1
if float(record['issue_number']) == IssueString(issue_number).asFloat():
if IssueString(record['issue_number']).asString().lower() == IssueString(issue_number).asString().lower():
found = True
break
if (found):
issue_url = "http://api.comicvine.com/issue/" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
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)
@ -224,7 +381,7 @@ class ComicVineTalker(QObject):
def fetchIssueDataByIssueID( self, issue_id, settings ):
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
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:
@ -248,12 +405,12 @@ class ComicVineTalker(QObject):
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.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:
@ -268,11 +425,12 @@ class ComicVineTalker(QObject):
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 )
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()
@ -303,6 +461,8 @@ class ComicVineTalker(QObject):
def cleanup_html( self, string):
if string is None:
return ""
# remove all newlines first
string = string.replace("\n", "")
@ -320,14 +480,13 @@ class ComicVineTalker(QObject):
newstring = newstring.replace('&amp;','&')
newstring = newstring.strip()
return newstring
def fetchIssueDate( self, issue_id ):
details = self.fetchIssueSelectDetails( issue_id )
return details['publish_month'], details['publish_year']
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']
@ -343,15 +502,14 @@ class ComicVineTalker(QObject):
if cached_details['image_url'] is not None:
return cached_details
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year,site_detail_url"
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['publish_month'] = None
details['publish_year'] = None
details['cover_date'] = None
details['site_detail_url'] = None
cv_response = json.loads(content)
@ -361,16 +519,14 @@ class ComicVineTalker(QObject):
details['image_url'] = cv_response['results']['image']['super_url']
details['thumb_image_url'] = cv_response['results']['image']['thumb_url']
details['publish_year'] = cv_response['results']['publish_year']
details['publish_month'] = cv_response['results']['publish_month']
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['publish_month'],
details['publish_year'],
details['cover_date'],
details['site_detail_url'] )
#print details['site_detail_url']
return details
@ -382,18 +538,16 @@ class ComicVineTalker(QObject):
cvc = ComicVineCacher( )
return cvc.get_issue_select_details( issue_id )
def cacheIssueSelectDetails( self, issue_id, image_url, thumb_url, month, year, page_url ):
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, month, year, page_url )
cvc.add_issue_select_details( issue_id, image_url, thumb_url, cover_date, page_url )
def fetchAlternateCoverURLs(self, issue_id):
def fetchAlternateCoverURLs(self, issue_id, issue_page_url):
url_list = self.fetchCachedAlternateCoverURLs( issue_id )
if url_list is not None:
return url_list
issue_page_url = self.fetchIssuePageURL( issue_id )
# scrape the CV issue page URL to get the alternate cover URLs
resp = urllib2.urlopen( issue_page_url )
content = resp.read()
@ -412,11 +566,14 @@ class ComicVineTalker(QObject):
# 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 'content-pod' in c and 'alt-cover' in c:
alt_cover_url_list.append( d.img['src'] )
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
@ -446,7 +603,7 @@ class ComicVineTalker(QObject):
self.urlFetchComplete.emit( details['image_url'],details['thumb_image_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,site_detail_url"
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)))
@ -455,18 +612,24 @@ class ComicVineTalker(QObject):
# read in the response
data = reply.readAll()
cv_response = json.loads(str(data))
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']
year = cv_response['results']['publish_year']
month = cv_response['results']['publish_month']
cover_date = cv_response['results']['cover_date']
page_url = cv_response['results']['site_detail_url']
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, month, year, page_url )
self.cacheIssueSelectDetails( self.issue_id, image_url, thumb_url, cover_date, page_url )
self.urlFetchComplete.emit( image_url, thumb_url, self.issue_id )
@ -495,3 +658,11 @@ class ComicVineTalker(QObject):
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

@ -1,9 +1,11 @@
"""
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
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -59,8 +61,9 @@ class CoverImageWidget(QWidget):
ArchiveMode = 0
AltCoverMode = 1
URLMode = 1
DataMode = 3
def __init__(self, parent, mode ):
def __init__(self, parent, mode, expand_on_click = True ):
super(CoverImageWidget, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui' ), self)
@ -78,7 +81,10 @@ class CoverImageWidget(QWidget):
self.btnLeft.clicked.connect( self.decrementImage )
self.btnRight.clicked.connect( self.incrementImage )
self.resetWidget()
clickable(self.lblImage).connect(self.showPopup)
if expand_on_click:
clickable(self.lblImage).connect(self.showPopup)
else:
self.lblImage.setToolTip( "" )
self.updateContent()
@ -93,11 +99,12 @@ class CoverImageWidget(QWidget):
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:
@ -138,7 +145,19 @@ class CoverImageWidget(QWidget):
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
@ -179,11 +198,13 @@ class CoverImageWidget(QWidget):
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:
if not self.showControls or self.mode == CoverImageWidget.DataMode:
self.btnLeft.hide()
self.btnRight.hide()
self.label.hide()
@ -288,3 +309,4 @@ class CoverImageWidget(QWidget):
def showPopup( self ):
self.popup = ImagePopup(self, self.current_pixmap)

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to edit credits
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

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.1.3-beta"
version="1.1.11-beta"

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to confirm and set options for export to zip
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -6,7 +6,7 @@ This should probably be re-written, but, well, it mostly works!
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -30,31 +30,25 @@ 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, ' ', 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
string = re.sub(ph, self.repl, string )
return string #.strip()
def getIssueCount( self,filename ):
def getIssueCount( self,filename, issue_end ):
count = ""
filename = filename[issue_end:]
# replace any name seperators with spaces
tmpstr = self.fixSpaces(filename)
found = False
@ -74,115 +68,150 @@ class FileNameParser:
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 mean's it's formatted differently from most:
# 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 = filename.split("--")[0]
elif "___" in filename:
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 = filename.split("__")[0]
filename = re.sub("__.*", self.repl, filename)
filename = filename.replace("+", " ")
# remove parenthetical phrases
filename = re.sub( "\(.*\)", "", filename)
filename = re.sub( "\[.*\]", "", filename)
# guess based on position
# replace parenthetical phrases with spaces
filename = re.sub( "\(.*?\)", self.repl, filename)
filename = re.sub( "\[.*?\]", self.repl, filename)
# 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
filename = self.fixSpaces(filename)
# 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
# remove any "of NN" phrase with spaces (problem: this could break on some titles)
filename = re.sub( "of [\d]+", self.repl, filename)
if not found:
# try a regex
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]|\d+\.\d|\d+)', filename)
if issnum:
issue = issnum.group()
#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
#print 'Got the issue using regex. Issue is ' + issue
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
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
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)
#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)
series = tmpstr
volume = ""
#save the last word
try:
last_word = series.split()[-1]
except:
last_word = ""
series = series.rstrip("#")
# 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)
return series.strip(), volume.strip()
# 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)
def getYear( self,filename):
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" ]
try:
last_word = series.split()[-1]
if last_word.lower() in one_shot_words:
series = series.rsplit(' ', 1)[0]
except:
pass
return series, volume.strip()
def getYear( self,filename, issue_end):
filename = filename[issue_end:]
year = ""
# look for four digit number with "(" ")" or "--" around it
@ -193,6 +222,28 @@ class FileNameParser:
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
@ -210,20 +261,12 @@ class FileNameParser:
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)
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
@ -232,4 +275,3 @@ class FileNameParser:
self.issue = "0"
if self.issue[0] == ".":
self.issue = "0" + self.issue

View File

@ -3,7 +3,7 @@ Functions for renaming files based on metadata
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -118,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]
@ -132,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

@ -4,7 +4,7 @@ A PyQt4 widget for managing list of comic archive files
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -98,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 )
@ -154,6 +162,9 @@ 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()
@ -187,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

View File

@ -8,7 +8,7 @@
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -31,7 +31,7 @@ class PageType:
InnerCover = "InnerCover"
Roundup = "Roundup"
Story = "Story"
Advertisment = "Advertisment"
Advertisment = "Advertisement"
Editorial = "Editorial"
Letters = "Letters"
Preview = "Preview"

View File

@ -3,7 +3,7 @@ A python class to manage fetching and caching of images by URL
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A PyQT4 widget to display a popup image
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to select specific issue from list
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -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
@ -32,6 +33,13 @@ 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):
volume_id = 0
@ -47,6 +55,7 @@ class IssueSelectionWindow(QtGui.QDialog):
gridlayout.setContentsMargins(0,0,0,0)
utils.reduceWidgetFontSize( self.twList )
utils.reduceWidgetFontSize( self.teDescription, 1 )
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
@ -85,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!"))
@ -92,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)
@ -102,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
@ -142,5 +165,10 @@ class IssueSelectionWindow(QtGui.QDialog):
if record['id'] == self.issue_id:
self.issue_number = record['issue_number']
self.coverWidget.setIssueID( int(self.issue_id) )
if record['description'] is None:
self.teDescription.setText ( "" )
else:
self.teDescription.setText ( record['description'] )
break

View File

@ -8,11 +8,12 @@ e.g.:
"0"
"-1"
"5AU"
"100-2"
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -34,31 +35,57 @@ 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
# if it's still not numeric at start skip it
if text[start].isdigit() or text[start] == ".":
# 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
else:
self.suffix = text
#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

@ -3,7 +3,7 @@ A PyQT4 dialog to a text file or log
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A python app to (automatically) tag comic archives
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to select from automated issue matches
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -51,6 +51,7 @@ class MatchSelectionWindow(QtGui.QDialog):
gridlayout.setContentsMargins(0,0,0,0)
utils.reduceWidgetFontSize( self.twList )
utils.reduceWidgetFontSize( self.teDescription, 1 )
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
@ -116,6 +117,8 @@ class MatchSelectionWindow(QtGui.QDialog):
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)
@ -142,7 +145,11 @@ class MatchSelectionWindow(QtGui.QDialog):
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)

View File

@ -3,7 +3,7 @@ A PyQt4 dialog to show a message and let the user check a box
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ CLI options class for comictagger app
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -25,8 +25,14 @@ import os
import traceback
import ctversion
import utils
try:
import argparse
except:
pass
from genericmetadata import GenericMetadata
from comicarchive import MetaDataStyle
from versionchecker import VersionChecker
class Options:
help_text = """
@ -46,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)
@ -111,6 +118,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
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 ):
@ -172,6 +180,41 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
#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:
@ -180,14 +223,23 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
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:ieRS:",
"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=" , "recursive", "script=",
"export-to-zip", "delete-rar", "abort-on-conflict" ] )
"export-to-zip", "delete-rar", "abort-on-conflict", "assume-issue-one" ] )
except getopt.GetoptError as err:
self.display_msg_and_quit( str(err), 2 )
@ -247,11 +299,18 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
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 "ComicTagger {0}: Copyright (c) 2012-2014 Anthony Beville".format(ctversion.version)
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":
@ -279,40 +338,8 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/
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:
# 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(self.script):
print "Can't find {0}".format( self.script )
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(self.script)
module_name = os.path.splitext(os.path.basename(self.script))[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)
self.launch_script( self.script )
if len(args) > 0:
if platform.system() == "Windows":
# no globbing on windows shell, so do it for them

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to show pages of a comic archive
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A PyQt4 widget for editing the page list info
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A PyQT4 class to load a page image from a ComicArchive in a background thread
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to show ID log and progress
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to confirm rename
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@ Settings class for comictagger app
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ import sys
import configparser
import platform
import codecs
import uuid
import utils
@ -67,8 +68,11 @@ class ComicTaggerSettings:
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 = ""
@ -78,6 +82,8 @@ 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
@ -86,6 +92,11 @@ class ComicTaggerSettings:
# 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
@ -159,12 +170,25 @@ class ComicTaggerSettings:
self.__init__()
def load(self):
self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
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'):
@ -183,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' )
@ -227,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 )
@ -243,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' )
@ -255,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' )

View File

@ -3,7 +3,7 @@ A PyQT4 dialog to enter app settings
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -119,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)
@ -162,10 +168,14 @@ class SettingsWindow(QtGui.QDialog):
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()
@ -185,7 +195,6 @@ class SettingsWindow(QtGui.QDialog):
self.settings.save()
QtGui.QDialog.accept(self)
def selectRar( self ):
self.selectFile( self.leRarExePath, "RAR" )
@ -226,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

@ -4,7 +4,7 @@ The main window of the ComicTagger app
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -55,6 +55,7 @@ from autotagstartwindow import AutoTagStartWindow
from autotagprogresswindow import AutoTagProgressWindow
from autotagmatchwindow import AutoTagMatchWindow
from coverimagewidget import CoverImageWidget
from versionchecker import VersionChecker
import utils
import ctversion
@ -100,6 +101,8 @@ class TaggerWindow( QtGui.QMainWindow):
self.fileSelectionList.selectionChanged.connect( self.fileListSelectionChanged )
self.fileSelectionList.listCleared.connect( self.fileListCleared )
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
@ -203,7 +206,23 @@ 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)
@ -312,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 )
@ -454,7 +473,7 @@ class TaggerWindow( QtGui.QMainWindow):
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>"
+ "(c)2014 Anthony Beville<br><br>"
+ "<a href='{0}'>{0}</a><br><br>".format(website)
+ "<a href='mailto:{0}'>{0}</a><br><br>".format(email)
+ "License: <a href='{0}'>{1}</a>".format(license_link, license_name) )
@ -897,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!)")
@ -908,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")
@ -953,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?"),
@ -1750,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()
@ -1854,4 +1858,21 @@ class TaggerWindow( QtGui.QMainWindow):
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

@ -6,21 +6,18 @@
<rect>
<x>0</x>
<y>0</y>
<width>907</width>
<height>507</height>
<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="1">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="archiveCoverContainer" native="true">
<property name="minimumSize">
@ -38,45 +35,73 @@
</widget>
</item>
<item>
<widget class="QTableWidget" name="twList">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<property name="orientation">
<enum>Qt::Vertical</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">
<property name="childrenCollapsible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Series</string>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>7</verstretch>
</sizepolicy>
</property>
</column>
<column>
<property name="text">
<string>Publisher</string>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
</column>
<column>
<property name="text">
<string>Date</string>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
<property name="rowCount">
<number>0</number>
</property>
</column>
<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>

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

@ -6,51 +6,90 @@
<rect>
<x>0</x>
<y>0</y>
<width>657</width>
<height>400</height>
<width>872</width>
<height>550</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Issue</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<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="QTableWidget" name="twList">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="rowCount">
<number>0</number>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<property name="childrenCollapsible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Issue</string>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>7</verstretch>
</sizepolicy>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</column>
<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>

View File

@ -6,21 +6,18 @@
<rect>
<x>0</x>
<y>0</y>
<width>907</width>
<height>507</height>
<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="1">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="archiveCoverContainer" native="true">
<property name="minimumSize">
@ -38,45 +35,73 @@
</widget>
</item>
<item>
<widget class="QTableWidget" name="twList">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<property name="orientation">
<enum>Qt::Vertical</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">
<property name="childrenCollapsible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Series</string>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>7</verstretch>
</sizepolicy>
</property>
</column>
<column>
<property name="text">
<string>Publisher</string>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
</column>
<column>
<property name="text">
<string>Date</string>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
<property name="rowCount">
<number>0</number>
</property>
</column>
<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>

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>

View File

@ -42,17 +42,20 @@
<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="selectionMode">
@ -102,15 +105,15 @@
</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="readOnly">

View File

@ -5,7 +5,7 @@ Some generic utilities
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -126,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

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

@ -3,7 +3,7 @@ A PyQT4 dialog to select specific series/volume from list
"""
"""
Copyright 2012 Anthony Beville
Copyright 2012-2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -87,7 +87,7 @@ 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(ComicTaggerSettings.getUIFile('volumeselectionwindow.ui' ), self)
@ -108,6 +108,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
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
@ -161,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 )
@ -369,7 +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'] )
if record['description'] is None:
self.teDetails.setText ( "" )
else:
self.teDetails.setText ( record['description'] )
self.imageWidget.setURL( record['image']['super_url'] )
break

1
current_version.txt Normal file
View File

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

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>

View File

@ -1,6 +1,6 @@
#PYINSTALLER_CMD := VERSIONER_PYTHON_PREFER_32_BIT=yes arch -i386 python $(HOME)/pyinstaller-2.0/pyinstaller.py
PYINSTALLER_CMD := python $(HOME)/pyinstaller-2.0/pyinstaller.py
TAGGER_BASE := $(HOME)/Dropbox/tagger/comictagger
TAGGER_BASE ?= $(HOME)/Dropbox/tagger/comictagger
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
APP_NAME := ComicTagger

View File

@ -1,3 +1,60 @@
---------------------------------
1.1.11-beta - 23-Mar-2014
---------------------------------
* Updated unrar library to hand Rar tools 5.0 and greater
* Other misc bug fixes
---------------------------------
1.1.10-beta - 30-Jan-2014
---------------------------------
* Updated series query to match changes on Comic Vine side
* Added a message when not able to open a file or folder
* Fixed an issue where series names with periods would fail on search
* Other misc bug fixes
---------------------------------
1.1.9-beta - 8-May-2013
---------------------------------
* Filename parser and identification enhancements
* Misc bug fixes
---------------------------------
1.1.8-beta - 21-Apr-2013
---------------------------------
* Handle occasional error 500 from Comic Vine by retrying a few times
* Nicer handling of colon (":") in file rename
* Fixed command-line option parsing issue for add-on scripts
* Misc bug fixes
---------------------------------
1.1.7-beta - 12-Apr-2013
---------------------------------
* Added description and cover date to issue selection dialogs
* Added notification of new version
* Added setting to attempt to parse scan info from file name
* Last sorted column in the file list is now remembered
* Added CLI option ('-1') to assume issue #1 if not found/parsed
* Misc bug fixes
---------------------------------
1.1.6-beta - 3-Apr-2013
---------------------------------
* More ComicVine API-related fixes
* More efficient automated search using new CV API issue filters
* Minor bug fixes
---------------------------------
1.1.5-beta - 30-Mar-2013
---------------------------------
* More updates for handling changes to ComicVine API and result sets
* Even better handling of non-numeric issue "numbers" ("½", "X")
---------------------------------
1.1.4-beta - 27-Mar-2013
---------------------------------
* Updated to match the changes to the ComicVine API and result sets
* Better handling of weird issue numbers ("0.1", "6au")
---------------------------------
1.1.3-beta - 25-Feb-2013
---------------------------------

151
scripts/name_fixer.py Executable file
View File

@ -0,0 +1,151 @@
#!/usr/bin/python
"""
fix the comic file names using a list of transforms
"""
"""
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 re
import argparse
import json
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
from comictaggerlib.filerenamer import *
import comictaggerlib.utils
def parse_args():
input_args = sys.argv[1:]
parser = argparse.ArgumentParser(description='a script to rename comic files')
parser.add_argument('-t', '--transforms', metavar='xformfile', help="the file with transforms")
parser.add_argument('-n', '--noconfirm', action='store_true', help="don't confirm before rename")
parser.add_argument('paths', metavar='PATH', type=str, nargs='+', help='path to look for comic files')
parsed_args = parser.parse_args(input_args)
return parsed_args
def caclulate_rename(ca, md, settings):
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( "%series% V%volume% #%issue% (of %issuecount%) (%year%) %scaninfo%" )
renamer.setIssueZeroPadding( 0 )
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
return renamer.determineName( ca.path, ext=new_ext )
def perform_rename(filelist):
for old_name,new_name in filelist:
folder = os.path.dirname( os.path.abspath( old_name ) )
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
os.rename( old_name, new_abs_path )
print u"renamed '{0}' -> '{1}'".format(os.path.basename(old_name), new_name)
def main():
default_xform_list = [
[ "^2000AD$", "2000 AD" ],
[ "^G\.{0,1}I\.{0,1}Joe$", "G.I. Joe" ],
]
utils.fix_output_encoding()
settings = ComicTaggerSettings()
style = MetaDataStyle.CIX
parsed_args = parse_args()
#parsed_args.noconfirm
if parsed_args.transforms is not None:
print "Reading in transforms from:", parsed_args.transforms
json_data=open(parsed_args.transforms).read()
data = json.loads(json_data)
xform_list = data['xforms']
else:
xform_list = default_xform_list
#pprint( xform_list, indent=4)
filelist = utils.get_recursive_filelist( parsed_args.paths )
#first find all comics
print "reading in all comics..."
comic_list = []
max_name_len = 2
fmt_str = ""
for filename in filelist:
ca = ComicArchive(filename, settings )
# do we care if it already has metadata?
if ca.seemsToBeAComicArchive() and not ca.hasMetadata( style ):
comic_list.append(ca)
max_name_len = max ( max_name_len, len(filename))
fmt_str = u"{{0:{0}}}".format(max_name_len)
print >> sys.stderr, fmt_str.format( filename ) + "\r",
sys.stderr.flush()
print >> sys.stderr, fmt_str.format( "" )
print "Found {0} comics.".format( len(comic_list))
modify_list = list()
# walk through the comic list fix the filenames
for ca in comic_list:
# 1. parse the filename into a MD object
md = ca.metadataFromFilename()
# 2. walk thru list of transforms
if md.series is not None and md.series != "":
for pattern, replacement in xform_list:
# apply each transform
new_series = re.sub( pattern, replacement, md.series )
if new_series != md.series:
md.series = new_series
new_name = caclulate_rename(ca, md, settings)
#found a match. add to proposed list, and bail on this file
modify_list.append( (ca.path, new_name ) )
break
print "{0} filenames to modify".format(len(modify_list))
if len(modify_list) > 0:
if parsed_args.noconfirm:
print "Not confirming before rename"
else:
for old_name, new_name in modify_list:
print u"'{0}' -> '{1}'".format(os.path.basename(old_name), new_name)
i = raw_input("Do you want to proceed with rename? [y/N] ")
if i.lower() not in ('y', 'yes'):
print "exiting without rename."
sys.exit(0)
perform_rename(modify_list)
if __name__ == '__main__':
main()

189
scripts/shrink.py Normal file
View File

@ -0,0 +1,189 @@
#!/usr/bin/python
"""
Reduce the image size of pages in the comic archive
"""
"""
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 tempfile
import zipfile
import shutil
import Image
import comictaggerlib.utils
from comictaggerlib.settings import *
from comictaggerlib.comicarchive import *
subfolder_name = "ORIGINALS"
max_height = 2000
def main():
utils.fix_output_encoding()
settings = ComicTaggerSettings()
# this can only work with files with ComicRack tags
style = MetaDataStyle.CIX
if len(sys.argv) < 2:
print >> sys.stderr, "usage: {0} comic_folder ".format(sys.argv[0])
return
filelist = utils.get_recursive_filelist( sys.argv[1:] )
#first make a list of all comic archive files
comics_list = []
max_name_len = 2
fmt_str = u"{{0:{0}}}".format(max_name_len)
for filename in filelist:
ca = ComicArchive(filename, settings )
if (ca.seemsToBeAComicArchive()):
# Check the images in the file, see if we need to reduce any
for idx in range(ca.getNumberOfPages()):
in_data = ca.getPage( idx )
if in_data is not None:
try:
im = Image.open(StringIO.StringIO(in_data))
w,h = im.size
if h > max_height:
comics_list.append( ca )
max_name_len = max ( max_name_len, len(filename))
fmt_str = u"{{0:{0}}}".format(max_name_len)
print >> sys.stderr, fmt_str.format( filename ) + "\r",
sys.stderr.flush()
break
except IOError:
#doesn't appear to be an image
pass
print >> sys.stderr, fmt_str.format( "" ) + "\r",
print "-----------------------------------------------"
print "Found {0} comics with over-large pages".format( len(comics_list))
print "-----------------------------------------------"
for item in comics_list:
print item.path
#now actually process those files with over-large pages
for ca in comics_list:
filename = ca.path
curr_folder = os.path.dirname( filename )
curr_subfolder = os.path.join( curr_folder, subfolder_name )
#skip any of our generated subfolders...
if os.path.basename(curr_folder) == subfolder_name:
continue
sys.stdout.write("Processing: " + filename)
# verify that we can write to current folder
if not os.access(filename, os.W_OK):
print "Can't move: {0}: skipped!".format(filename)
continue
if not os.path.exists( curr_subfolder ) and not os.access(curr_folder, os.W_OK):
print "Can't create subfolder here: {0}: skipped!".format(filename)
continue
if not os.path.exists( curr_subfolder ):
os.mkdir( curr_subfolder )
if not os.access(curr_subfolder, os.W_OK):
print "Can't write to the subfolder here: {0}: skipped!".format(filename)
continue
# generate a new file with temp name
tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(filename) )
os.close( tmp_fd )
cix_md = None
if ca.hasCIX():
cix_md = ca.readCIX()
try:
zout = zipfile.ZipFile (tmp_name, 'w')
# Check the images in the file, see if we want to reduce them
page_count = ca.getNumberOfPages()
for idx in range(ca.getNumberOfPages()):
name = ca.getPageName( idx )
in_data = ca.getPage( idx )
out_data = in_data
if in_data is not None:
try:
im = Image.open(StringIO.StringIO(in_data))
w,h = im.size
if h > max_height:
#resize the image
hpercent = (max_height/float(h))
wsize = int((float(w)*float(hpercent)))
size=(wsize, max_height)
im = im.resize(size, Image.ANTIALIAS)
output = StringIO.StringIO()
im.save(output, format="JPEG", quality=85)
out_data = output.getvalue()
output.close()
except IOError:
#doesn't appear to be an image
pass
else:
#page is empty?? nothing to write
out_data = ""
sys.stdout.write('.')
sys.stdout.flush()
#write out the new resized image
zout.writestr(name, out_data)
#preserve the old comment
comment = ca.archiver.getArchiveComment()
if comment is not None:
zout.comment = ca.archiver.getArchiveComment()
except Exception as e:
print "Failure creating new archive: {0}!".format(filename)
print e, sys.exc_info()[0]
zout.close()
os.unlink( tmp_name )
else:
zout.close()
# Success! Now move the files
shutil.move( filename, curr_subfolder )
os.rename( tmp_name, filename )
# TODO: We might have converted a rar to a zip, and should probably change
# the extension, as needed.
print "Done!".format(filename)
# Create a new archive object for the new file, and write the old CIX data, w/o page info
if cix_md is not None:
ca = ComicArchive( filename, settings )
cix_md.pages = []
ca.writeCIX( cix_md )
if __name__ == '__main__':
main()

8
scripts/xforms Normal file
View File

@ -0,0 +1,8 @@
{
"_comment": "This file contains JSON data. Any backslashes should be escaped with another backslash",
"xforms":
[
[ "^2000AD$", "2000 AD" ],
[ "^G\\.{0,1}I\\.{0,1}Joe$", "G.I. Joe" ]
]
}

View File

@ -4,24 +4,47 @@ Features
Rename dialog:
check-box for rows?
manual edit the preview?
Maybe replace configparser -- seems to be causing all sorts of problems
Feature Requests:
Move CBR to other folder after conversion to ZIP
Pre-process series name before identification
(using a list of regex transforms)
(GC #28) Save auto-tag options
(GC #24) Multiple options for -t i.e. "-t cr,cbl"
(GC #18 ) Option for handling colon in rename
(GC #31 ) Specify CV Series ID for auto-tag
Re-org - move to new folder based on template
repair incorrect extension
Denied Requests (for now):
Auto-rename on auto-tag
Re-zip (to remove compression)
Selective fields on CLI print (use -m option. Maybe internally remove all but specified fields in MD object before print )
Docs:
Auto-Tagging Tips:
Multiple Passes with different options
Multiple Passes with different options
-----------------------------------------------------
Bugs
-----------------------------------------------------
Zip flakes out when filename differs from index (or whatever) i.e "\" vs "/". Python issue
-----------------------------------------------------
Big Future Features
-----------------------------------------------------
Support for ACBF metatdata in CBZ
GCD scraper or DB reader
Batch Edit
(GC #29) Batch Edit
Form Mode: Single vs Batch
-----------------------------------------------------
@ -48,10 +71,7 @@ Google App engine to store hashes
Filename parsing:
Rework how series name is separated from issue
Support marvel's "AU" issues...
Mostly done, gotta wait and see what CV does
Internal GenericMetadata - Make Characters, Genre into lists?
-----------------------------------------------------
@ -68,7 +88,9 @@ Release Process
Make zip on Mac or Linux
Tag the repository
Upload packages
Announce on Forum and Main Page
Announce on Forum and Main Page and Twitter
MacUpdate
Update appspot value
----------------------------------------------

View File

@ -2,7 +2,7 @@
# rm, cp, grep, cut, cat
HOMEPATH ?= $(HOME)
TAGGER_BASE:= $(HOMEPATH)/Dropbox/tagger/comictagger
TAGGER_BASE?= $(HOMEPATH)/Dropbox/tagger/comictagger
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
DIST_DIR := $(TAGGER_BASE)\windows\dist
NSIS_CMD := "C:\Program Files (x86)\NSIS\makensis.exe"

View File

@ -1,4 +1,4 @@
ComicTagger - Copyright 2012 Anthony Beville
ComicTagger - Copyright 2014 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.