Compare commits

...

97 Commits

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

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

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

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

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

git-svn-id: http://comictagger.googlecode.com/svn/trunk@468 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-08 01:14:04 +00:00
078b3cef3c more python packaging tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@464 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 23:09:00 +00:00
22ef0250ca python packaging tweaks
git-svn-id: http://comictagger.googlecode.com/svn/trunk@463 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 22:38:27 +00:00
cc53162dcc Added a readme.txt for the source distrubution
git-svn-id: http://comictagger.googlecode.com/svn/trunk@462 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 21:34:00 +00:00
fa309cfcef Got mac build working with new structure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@461 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 17:35:25 +00:00
4d57b0cf79 God deb built using fpm!
git-svn-id: http://comictagger.googlecode.com/svn/trunk@460 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 07:04:23 +00:00
6ea5d28609 More distutil fun
git-svn-id: http://comictagger.googlecode.com/svn/trunk@459 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 05:21:07 +00:00
9d56a2ce9a Got frozen windows build working again
git-svn-id: http://comictagger.googlecode.com/svn/trunk@458 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 04:50:10 +00:00
811759478a Function dist install on linux
git-svn-id: http://comictagger.googlecode.com/svn/trunk@457 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 04:19:20 +00:00
28e2d93314 Name conflict with launcher script
git-svn-id: http://comictagger.googlecode.com/svn/trunk@456 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 04:00:20 +00:00
93b3117699 MOre cleanup
git-svn-id: http://comictagger.googlecode.com/svn/trunk@455 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 02:40:29 +00:00
10e6a1019e First cut at a dist-package build
git-svn-id: http://comictagger.googlecode.com/svn/trunk@454 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 02:39:36 +00:00
2024555780 restructure - done, I think
git-svn-id: http://comictagger.googlecode.com/svn/trunk@453 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 01:20:05 +00:00
e15c3fa3e6 restructure - almost there!
git-svn-id: http://comictagger.googlecode.com/svn/trunk@452 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 01:12:49 +00:00
8aa6403f51 Restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@451 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-07 01:01:39 +00:00
fb5fca1dc4 restructre
git-svn-id: http://comictagger.googlecode.com/svn/trunk@450 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:29:52 +00:00
75d5b1a695 Restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@449 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:17:38 +00:00
e56d9bddbf restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@448 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:10:48 +00:00
7d9aa70dc0 restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@447 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:05:46 +00:00
6d72ed2a69 restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@446 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:05:24 +00:00
9b584f78a0 restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@445 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:04:40 +00:00
dfe0e74f9c Refactored code for restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@444 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 22:03:53 +00:00
a11c08a2ee Restructure
git-svn-id: http://comictagger.googlecode.com/svn/trunk@443 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 21:59:43 +00:00
9159204883 Added missing file header
git-svn-id: http://comictagger.googlecode.com/svn/trunk@442 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 20:42:57 +00:00
605e27ce99 Deleted cruft file
git-svn-id: http://comictagger.googlecode.com/svn/trunk@441 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 20:35:24 +00:00
2dc08b36ea Added use google of upload tool to makefile
git-svn-id: http://comictagger.googlecode.com/svn/trunk@440 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 19:13:07 +00:00
60dae4f1fb Keep the google project file upload utility in a handy place
git-svn-id: http://comictagger.googlecode.com/svn/trunk@439 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-02-06 19:08:32 +00:00
110 changed files with 2327 additions and 1245 deletions

4
MANIFEST.in Normal file
View File

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

View File

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

31
README.txt Normal file
View File

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

View File

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

View File

View File

@ -26,7 +26,7 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
from imagefetcher import ImageFetcher
from settings import ComicTaggerSettings
from options import MetaDataStyle
from comicarchive import MetaDataStyle
from coverimagewidget import CoverImageWidget
from comicvinetalker import ComicVineTalker
import utils
@ -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(os.path.join(ComicTaggerSettings.baseDir(), 'autotagmatchwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('autotagmatchwindow.ui' ), self)
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
gridlayout = QtGui.QGridLayout( self.altCoverContainer )

View File

@ -30,9 +30,9 @@ class AutoTagProgressWindow(QtGui.QDialog):
def __init__(self, parent):
super(AutoTagProgressWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagprogresswindow.ui' ), self)
self.lblTest.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.lblArchive.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
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.isdone = False
self.setWindowFlags(self.windowFlags() |
@ -54,7 +54,7 @@ class AutoTagProgressWindow(QtGui.QDialog):
label.setPixmap(QtGui.QPixmap(img))
label.setScaledContents(True)
else:
label.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
label.setPixmap(QtGui.QPixmap(ComicTaggerSettings.getGraphic('nocover.png')))
label.setScaledContents(True)
QtCore.QCoreApplication.processEvents()
QtCore.QCoreApplication.processEvents()

View File

@ -32,7 +32,7 @@ class AutoTagStartWindow(QtGui.QDialog):
def __init__( self, parent, settings, msg ):
super(AutoTagStartWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagstartwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('autotagstartwindow.ui' ), self)
self.label.setText( msg )
self.setWindowFlags(self.windowFlags() &

536
comictaggerlib/cli.py Normal file
View File

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

View File

@ -25,6 +25,8 @@ import sys
import tempfile
import subprocess
import platform
import locale
if platform.system() == "Windows":
import _subprocess
import time
@ -40,7 +42,6 @@ sys.path.insert(0, os.path.abspath(".") )
import UnRAR2
from UnRAR2.rar_exceptions import *
from options import Options, MetaDataStyle
from comicinfoxml import ComicInfoXml
from comicbookinfo import ComicBookInfo
from comet import CoMet
@ -48,6 +49,12 @@ from genericmetadata import GenericMetadata, PageType
from filenameparser import FileNameParser
from settings import ComicTaggerSettings
class MetaDataStyle:
CBI = 0
CIX = 1
COMET = 2
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
class ZipArchiver:
def __init__( self, path ):
@ -227,9 +234,9 @@ class ZipArchiver:
class RarArchiver:
devnull = None
def __init__( self, path ):
def __init__( self, path, settings ):
self.path = path
self.rar_exe_path = None
self.settings = settings
if RarArchiver.devnull is None:
RarArchiver.devnull = open(os.devnull, "w")
@ -252,7 +259,7 @@ class RarArchiver:
def setArchiveComment( self, comment ):
if self.rar_exe_path is not None:
if self.settings.rar_exe_path is not None:
try:
# write comment to temp file
tmp_fd, tmp_name = tempfile.mkstemp()
@ -263,7 +270,7 @@ class RarArchiver:
working_dir = os.path.dirname( os.path.abspath( self.path ) )
# use external program to write comment to Rar archive
subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
subprocess.call([self.settings.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
@ -321,7 +328,7 @@ class RarArchiver:
def writeArchiveFile( self, archive_file, data ):
if self.rar_exe_path is not None:
if self.settings.rar_exe_path is not None:
try:
tmp_folder = tempfile.mkdtemp()
@ -336,7 +343,7 @@ class RarArchiver:
f.close()
# use external program to write file to Rar archive
subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
subprocess.call([self.settings.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
@ -352,10 +359,10 @@ class RarArchiver:
return False
def removeArchiveFile( self, archive_file ):
if self.rar_exe_path is not None:
if self.settings.rar_exe_path is not None:
try:
# use external program to remove file from Rar archive
subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file],
subprocess.call([self.settings.rar_exe_path, 'd','-c-', self.path, archive_file],
startupinfo=self.startupinfo,
stdout=RarArchiver.devnull)
@ -503,11 +510,12 @@ class ComicArchive:
class ArchiveType:
Zip, Rar, Folder, Unknown = range(4)
def __init__( self, path ):
def __init__( self, path, settings ):
self.path = path
self.ci_xml_filename = 'ComicInfo.xml'
self.comet_default_filename = 'CoMet.xml'
self.resetCache()
self.settings = settings
if self.zipTest():
self.archive_type = self.ArchiveType.Zip
@ -515,7 +523,7 @@ class ComicArchive:
elif self.rarTest():
self.archive_type = self.ArchiveType.Rar
self.archiver = RarArchiver( self.path )
self.archiver = RarArchiver( self.path, settings )
elif os.path.isdir( self.path ):
self.archive_type = self.ArchiveType.Folder
@ -525,9 +533,9 @@ class ComicArchive:
self.archiver = UnknownArchiver( self.path )
if ComicArchive.logo_data is None:
fname = os.path.join(ComicTaggerSettings.baseDir(), 'graphics','nocover.png' )
fname = ComicTaggerSettings.getGraphic('nocover.png')
with open(fname, 'rb') as fd:
ComicArchive.logo_data = fd.read()
ComicArchive.logo_data = fd.read()
# Clears the cached data
def resetCache( self ):
@ -548,10 +556,6 @@ class ComicArchive:
def rename( self, path ):
self.path = path
self.archiver.path = path
def setExternalRarProgram( self, rar_exe_path ):
if self.isRar():
self.archiver.rar_exe_path = rar_exe_path
def zipTest( self ):
return zipfile.is_zipfile( self.path )
@ -578,7 +582,7 @@ class ComicArchive:
if self.archive_type == self.ArchiveType.Unknown :
return False
elif check_rar_status and self.isRar() and self.archiver.rar_exe_path is None:
elif check_rar_status and self.isRar() and self.settings.rar_exe_path is None:
return False
elif not os.access(self.path, os.W_OK):
@ -603,7 +607,7 @@ class ComicArchive:
ext = os.path.splitext(self.path)[1].lower()
if (
( self.isZip() or self.isRar() or self.isFolder() )
( self.isZip() or self.isRar() ) #or self.isFolder() )
and
( self.getNumberOfPages() > 2)
@ -673,6 +677,9 @@ class ComicArchive:
def getPageName( self, index ):
if index is None:
return None
page_list = self.getPageNameList()
num_pages = len( page_list )
@ -681,6 +688,56 @@ class ComicArchive:
return page_list[index]
def getScannerPageIndex( self ):
scanner_page_index = None
#make a guess at the scanner page
name_list = self.getPageNameList()
count = self.getNumberOfPages()
#too few pages to really know
if count < 5:
return None
# count the length of every filename, and count occurences
length_buckets = dict()
for name in name_list:
fname = os.path.split(name)[1]
length = len(fname)
if length_buckets.has_key( length ):
length_buckets[ length ] += 1
else:
length_buckets[ length ] = 1
# sort by most common
sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True)
# statistical mode occurence is first
mode_length = sorted_buckets[0][0]
# we are only going to consider the final image file:
final_name = os.path.split(name_list[count-1])[1]
common_length_list = list()
for name in name_list:
if len(os.path.split(name)[1]) == mode_length:
common_length_list.append( os.path.split(name)[1] )
prefix = os.path.commonprefix(common_length_list)
if mode_length <= 7 and prefix == "":
#probably all numbers
if len(final_name) > mode_length:
scanner_page_index = count-1
# see if the last page doesn't start with the same prefix as most others
elif not final_name.startswith(prefix):
scanner_page_index = count-1
return scanner_page_index
def getPageNameList( self , sort_list=True):
if self.page_list is None:
@ -689,7 +746,14 @@ class ComicArchive:
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
if sort_list:
files.sort(key=lambda x: x.lower())
def keyfunc(k):
#hack to account for some weird scanner ID pages
basename=os.path.split(k)[1]
if basename < '0':
k = os.path.join(os.path.split(k)[0], "z" + basename)
return k.lower()
files.sort(key=keyfunc)
# make a sub-list of image files
self.page_list = []

View File

@ -89,36 +89,21 @@ class ComicInfoXml:
if md_entry is not None:
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
assign( 'Title', md.title )
assign( 'Series', md.series )
assign( 'Number', md.issue )
assign( 'Title', md.title )
assign( 'Count', md.issueCount )
assign( 'Volume', md.volume )
assign( 'AlternateSeries', md.alternateSeries )
assign( 'AlternateNumber', md.alternateNumber )
assign( 'StoryArc', md.storyArc )
assign( 'SeriesGroup', md.seriesGroup )
assign( 'AlternateCount', md.alternateCount )
assign( 'Summary', md.comments )
assign( 'Notes', md.notes )
assign( 'Year', md.year )
assign( 'Month', md.month )
assign( 'Publisher', md.publisher )
assign( 'Imprint', md.imprint )
assign( 'Genre', md.genre )
assign( 'Web', md.webLink )
assign( 'PageCount', md.pageCount )
assign( 'Format', md.format )
assign( 'LanguageISO', md.language )
assign( 'Manga', md.manga )
assign( 'Characters', md.characters )
assign( 'Teams', md.teams )
assign( 'Locations', md.locations )
assign( 'ScanInformation', md.scanInfo )
assign( 'StoryArc', md.storyArc )
assign( 'SeriesGroup', md.seriesGroup )
assign( 'AgeRating', md.maturityRating )
if md.blackAndWhite is not None and md.blackAndWhite:
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
assign( 'Day', md.day )
# need to specially process the credits, since they are structured differently than CIX
credit_writer_list = list()
@ -181,7 +166,23 @@ class ComicInfoXml:
if len( credit_editor_list ) > 0:
node = ET.SubElement(root, 'Editor')
node.text = utils.listToString( credit_editor_list )
assign( 'Publisher', md.publisher )
assign( 'Imprint', md.imprint )
assign( 'Genre', md.genre )
assign( 'Web', md.webLink )
assign( 'PageCount', md.pageCount )
assign( 'LanguageISO', md.language )
assign( 'Format', md.format )
assign( 'AgeRating', md.maturityRating )
if md.blackAndWhite is not None and md.blackAndWhite:
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
assign( 'Manga', md.manga )
assign( 'Characters', md.characters )
assign( 'Teams', md.teams )
assign( 'Locations', md.locations )
assign( 'ScanInformation', md.scanInfo )
# loop and add the page entries under pages node
if len( md.pages ) > 0:
pages_node = ET.SubElement(root, 'Pages')
@ -229,6 +230,7 @@ class ComicInfoXml:
md.notes = xlate( 'Notes' )
md.year = xlate( 'Year' )
md.month = xlate( 'Month' )
md.day = xlate( 'Day' )
md.publisher = xlate( 'Publisher' )
md.imprint = xlate( 'Imprint' )
md.genre = xlate( 'Genre' )

View File

@ -361,7 +361,7 @@ class ComicVineCacher:
row = cur.fetchone()
details = dict()
if row[0] is None :
if row is None or row[0] is None :
details['image_url'] = None
details['thumb_image_url'] = None
details['publish_month'] = None

View File

@ -58,6 +58,8 @@ class ComicVineTalker(QObject):
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'
@ -76,7 +78,7 @@ class ComicVineTalker(QObject):
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()
@ -111,7 +113,7 @@ class ComicVineTalker(QObject):
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"
search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues&sort=start_year"
content = self.getUrlContent(search_url)
@ -132,7 +134,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 +143,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 +160,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 +181,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/" + str(series_id) + "/?api_key=" + self.api_key + "&format=json"
content = self.getUrlContent(volume_url)
cv_response = json.loads(content)
@ -202,12 +205,12 @@ class ComicVineTalker(QObject):
for record in volume_results['issues']:
if IssueString(issue_number).asFloat() is None:
issue_number = 1
if float(record['issue_number']) == IssueString(issue_number).asFloat():
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/" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
content = self.getUrlContent(issue_url)
cv_response = json.loads(content)
@ -224,7 +227,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/" + 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:
@ -251,6 +254,11 @@ class ComicVineTalker(QObject):
metadata.issue = num_s
metadata.title = issue_results['name']
# ComicVine gives redundant info in the title. Strip out the
# volume name and issue number
metadata.title = re.sub( ".* #.+ - ", "", metadata.title )
metadata.publisher = volume_results['publisher']['name']
metadata.month = issue_results['publish_month']
metadata.year = issue_results['publish_year']
@ -268,11 +276,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('roles'):
for role in person['roles']:
# can we determine 'primary' from CV??
role_name = role['role'].title()
metadata.addCredit( person['name'], role['role'].title(), False )
character_credits = issue_results['character_credits']
character_list = list()
@ -343,7 +352,7 @@ 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/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year,site_detail_url"
content = self.getUrlContent(issue_url)
@ -412,11 +421,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 +458,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 = "http://www.comicvine.com/api/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,publish_month,publish_year,site_detail_url"
self.nam = QNetworkAccessManager()
self.nam.finished.connect( self.asyncFetchIssueCoverURLComplete )
self.nam.get(QNetworkRequest(QUrl(issue_url)))
@ -455,7 +467,14 @@ 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

View File

@ -26,7 +26,7 @@ from PyQt4 import uic
from settings import ComicTaggerSettings
from genericmetadata import GenericMetadata, PageType
from options import MetaDataStyle
from comicarchive import MetaDataStyle
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from imagefetcher import ImageFetcher
from pageloader import PageLoader
@ -63,7 +63,7 @@ class CoverImageWidget(QWidget):
def __init__(self, parent, mode ):
super(CoverImageWidget, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'coverimagewidget.ui' ), self )
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui' ), self)
utils.reduceWidgetFontSize( self.label )
@ -72,8 +72,8 @@ class CoverImageWidget(QWidget):
self.page_loader = None
self.showControls = True
self.btnLeft.setIcon(QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/left.png' )))
self.btnRight.setIcon(QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/right.png' )))
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png')))
self.btnRight.setIcon(QIcon(ComicTaggerSettings.getGraphic('right.png')))
self.btnLeft.clicked.connect( self.decrementImage )
self.btnRight.clicked.connect( self.incrementImage )
@ -236,7 +236,7 @@ class CoverImageWidget(QWidget):
self.page_loader = None
def loadDefault( self ):
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
self.current_pixmap = QPixmap(ComicTaggerSettings.getGraphic('nocover.png'))
#print "loadDefault called"
self.setDisplayPixmap( 0, 0)

View File

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

View File

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

View File

@ -36,7 +36,7 @@ class ExportWindow(QtGui.QDialog):
def __init__( self, parent, settings, msg ):
super(ExportWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'exportwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('exportwindow.ui' ), self)
self.label.setText( msg )
self.setWindowFlags(self.windowFlags() &

View File

@ -30,8 +30,11 @@ import os
from urllib import unquote
class FileNameParser:
def fixSpaces( self, string ):
placeholders = ['[-_]',' +']
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()
@ -109,14 +112,12 @@ class FileNameParser:
# 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)
matchlist = re.findall("#[-+]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", filename)
if len(matchlist) > 0:
#get the last item
issue = matchlist[ len(matchlist) - 1]
issue = issue[1:]
issue = matchlist[ len(matchlist) - 1][0]
found = True
# assume the last number in the filename that is under 4 digits is the issue number
if not found:
for word in reversed(word_list):
@ -133,7 +134,7 @@ class FileNameParser:
if not found:
# try a regex
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]|\d+\.\d|\d+)', filename)
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]+|\d+\.\d|\d+)', filename)
if issnum:
issue = issnum.group()
found = True
@ -151,7 +152,7 @@ class FileNameParser:
# finding it easier
filename = filename.replace("+", " ")
tmpstr = self.fixSpaces(filename)
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("#", " ")

View File

@ -21,6 +21,7 @@ limitations under the License.
import os
import re
import datetime
import utils
from issuestring import IssueString
class FileRenamer:
@ -71,6 +72,7 @@ class FileRenamer:
md = self.metdata
new_name = self.template
preferred_encoding = utils.get_actual_preferred_encoding()
#print u"{0}".format(md)
@ -93,7 +95,7 @@ class FileRenamer:
if (type(md.month) == str and md.month.isdigit()) or type(md.month) == int:
if int(md.month) in range(1,13):
dt = datetime.datetime( 1970, int(md.month), 1, 0, 0)
month_name = dt.strftime("%B")
month_name = dt.strftime(u"%B".encode(preferred_encoding)).decode(preferred_encoding)
new_name = self.replaceToken( new_name, month_name, '%month_name%')
new_name = self.replaceToken( new_name, md.genre, '%genre%')

View File

@ -29,8 +29,8 @@ from PyQt4.QtCore import pyqtSignal
from settings import ComicTaggerSettings
from comicarchive import ComicArchive
from comicarchive import MetaDataStyle
from genericmetadata import GenericMetadata, PageType
from options import MetaDataStyle
import utils
class FileTableWidget( QTableWidget ):
@ -71,7 +71,7 @@ class FileSelectionList(QWidget):
def __init__(self, parent , settings ):
super(FileSelectionList, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui' ), self)
self.settings = settings
@ -159,20 +159,8 @@ class FileSelectionList(QWidget):
self.listCleared.emit()
def addPathList( self, pathlist ):
filelist = []
for p in pathlist:
# if path is a folder, walk it recursivly, and all files underneath
if type(p) == str:
#make sure string is unicode
filename_encoding = sys.getfilesystemencoding()
p = p.decode(filename_encoding, 'replace')
if os.path.isdir( unicode(p)):
for root,dirs,files in os.walk( unicode(p) ):
for f in files:
filelist.append(os.path.join(root,unicode(f)))
else:
filelist.append(unicode(p))
filelist = utils.get_recursive_filelist( pathlist )
# we now have a list of files to add
@ -231,12 +219,9 @@ class FileSelectionList(QWidget):
if self.isListDupe(path):
return None
ca = ComicArchive( path )
if self.settings.rar_exe_path != "":
ca.setExternalRarProgram( self.settings.rar_exe_path )
ca = ComicArchive( path, self.settings )
if ca.seemsToBeAComicArchive() :
row = self.twList.rowCount()
self.twList.insertRow( row )

View File

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

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 362 B

After

Width:  |  Height:  |  Size: 362 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,4 +1,22 @@
"""
A pthyon class to manage creating image content hashes, and calculate hamming distances
"""
"""
Copyright 2013 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import StringIO
import sys

View File

@ -29,7 +29,7 @@ class ImagePopup(QtGui.QDialog):
def __init__(self, parent, image_pixmap):
super(ImagePopup, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'imagepopup.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui' ), self)
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
@ -48,7 +48,7 @@ class ImagePopup(QtGui.QDialog):
# widget
self.desktopBg = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop ().winId(),
0,0, screen_size.width(), screen_size.height())
bg = QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/popup_bg.png' ))
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic('popup_bg.png'))
self.clientBgPixmap = bg.scaled(screen_size.width(), screen_size.height())
self.setMask(self.clientBgPixmap.mask())

View File

@ -82,6 +82,7 @@ class IssueIdentifier:
self.coverUrlCallback = None
self.search_result = self.ResultNoMatches
self.cover_page_index = 0
self.cancel = False
def setScoreMinThreshold( self, thresh ):
self.min_score_thresh = thresh
@ -135,7 +136,7 @@ class IssueIdentifier:
return None
output = StringIO.StringIO()
cropped_im.save(output, format="JPEG")
cropped_im.save(output, format="PNG")
cropped_image_data = output.getvalue()
output.close()
@ -223,7 +224,7 @@ class IssueIdentifier:
if newline:
self.output_function("\n")
def getIssueCoverMatchScore( self, comicVine, issue_id, localCoverHashList, useRemoteAlternates = False , use_log=True):
def getIssueCoverMatchScore( self, comicVine, issue_id, localCoverHashList, useRemoteAlternates = False , useLog=True):
# localHashes is a list of pre-calculated hashs.
# useRemoteAlternates - indicates to use alternate covers from CV
@ -279,9 +280,9 @@ class IssueIdentifier:
if self.cancel == True:
raise IssueIdentifierCancelled
if use_log and useRemoteAlternates:
if useLog and useRemoteAlternates:
self.log_msg( "[{0} alt. covers]".format(len(remote_cover_list)-1), False )
if use_log:
if useLog:
self.log_msg( "[ ", False )
score_list = []
@ -294,7 +295,7 @@ class IssueIdentifier:
score_item['url'] = remote_cover_item['url']
score_item['hash'] = remote_cover_item['hash']
score_list.append( score_item )
if use_log:
if useLog:
self.log_msg( "{0} ".format(score), False )
if score <= self.strong_score_thresh:
@ -304,7 +305,7 @@ class IssueIdentifier:
if done:
break
if use_log:
if useLog:
self.log_msg( " ]", False )
best_score_item = min(score_list, key=lambda x:x['score'])
@ -348,7 +349,6 @@ class IssueIdentifier:
right_side_image_data = self.cropCover( cover_image_data )
if right_side_image_data is not None:
narrow_cover_hash = self.calculateHash( right_side_image_data )
self.log_msg(unicode(str(narrow_cover_hash)))
#self.log_msg( "Cover hash = {0:016x}".format(cover_hash) )
@ -414,7 +414,7 @@ class IssueIdentifier:
series_second_round_list.append(item)
# if we don't think it's an issue number 1, remove any series' that are one-shots
if keys['issue_number'] not in [ '1', '0' ]:
if keys['issue_number'] not in [ '1', '0', '0.1' ]:
#self.log_msg( "Removing one-shots" )
series_second_round_list[:] = [x for x in series_second_round_list if not x['count_of_issues'] == 1]
@ -450,7 +450,7 @@ class IssueIdentifier:
num_s = IssueString(issue['issue_number']).asString()
# look for a matching issue number
if num_s == keys['issue_number']:
if num_s.lower() == keys['issue_number'].lower():
# now, if we have an issue year key given, reject this one if not a match
month, year = comicVine.fetchIssueDate( issue['id'] )

View File

@ -32,6 +32,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
@ -39,7 +46,7 @@ class IssueSelectionWindow(QtGui.QDialog):
def __init__(self, parent, settings, series_id, issue_number):
super(IssueSelectionWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'issueselectionwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('issueselectionwindow.ui' ), self)
self.coverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.AltCoverMode )
gridlayout = QtGui.QGridLayout( self.coverImageContainer )
@ -102,10 +109,10 @@ 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)
@ -115,7 +122,7 @@ class IssueSelectionWindow(QtGui.QDialog):
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, 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

View File

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

79
comictaggerlib/main.py Executable file
View File

@ -0,0 +1,79 @@
"""
A python app to (automatically) tag comic archives
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
import signal
import os
import traceback
import platform
import utils
import cli
from settings import ComicTaggerSettings
from options import Options
try:
qt_available = True
from PyQt4 import QtCore, QtGui
from taggerwindow import TaggerWindow
except ImportError as e:
qt_available = False
#---------------------------------------
def ctmain():
utils.fix_output_encoding()
settings = ComicTaggerSettings()
opts = Options()
opts.parseCmdLineArgs()
signal.signal(signal.SIGINT, signal.SIG_DFL)
if not qt_available and not opts.no_gui:
opts.no_gui = True
print >> sys.stderr, "PyQt4 is not available. ComicTagger is limited to command-line mode."
if opts.no_gui:
cli.cli_mode( opts, settings )
else:
app = QtGui.QApplication(sys.argv)
if platform.system() != "Linux":
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
splash = QtGui.QSplashScreen(img)
splash.show()
splash.raise_()
app.processEvents()
try:
tagger_window = TaggerWindow( opts.file_list, settings )
tagger_window.show()
if platform.system() != "Linux":
splash.finish( tagger_window )
sys.exit(app.exec_())
except Exception, e:
QtGui.QMessageBox.critical(QtGui.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc() )

View File

@ -26,7 +26,7 @@ from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
from imagefetcher import ImageFetcher
from settings import ComicTaggerSettings
from options import MetaDataStyle
from comicarchive import MetaDataStyle
from coverimagewidget import CoverImageWidget
from comicvinetalker import ComicVineTalker
import utils
@ -38,7 +38,7 @@ class MatchSelectionWindow(QtGui.QDialog):
def __init__(self, parent, matches, comic_archive):
super(MatchSelectionWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'matchselectionwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('matchselectionwindow.ui' ), self)
self.altCoverWidget = CoverImageWidget( self.altCoverContainer, CoverImageWidget.AltCoverMode )
gridlayout = QtGui.QGridLayout( self.altCoverContainer )

View File

@ -22,22 +22,11 @@ import sys
import getopt
import platform
import os
import traceback
import ctversion
import utils
from genericmetadata import GenericMetadata
class Enum(set):
def __getattr__(self, name):
if name in self:
return name
raise AttributeError
class MetaDataStyle:
CBI = 0
CIX = 1
COMET = 2
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
from comicarchive import MetaDataStyle
class Options:
help_text = """
@ -82,11 +71,16 @@ If no options are given, {0} will run in windowed mode
-e, --export-to-zip Export RAR archive to Zip format
--delete-rar Delete original RAR archive after successful export to Zip
--abort-on-conflict Don't export to zip if intended new filename exists (Otherwise, creates
a new unique filename)
a new unique filename)
-S, --script=FILE Run an "add-on" python script that uses the comictagger library for custom
processing. Script arguments can follow the script name
-R, --recursive Recursively include files in sub-folders
-v, --verbose Be noisy when doing what it does
--terse Don't say much (for print mode)
--version Display version
-h, --help Display this message
-h, --help Display this message
For more help visit the wiki at: http://code.google.com/p/comictagger/
"""
@ -114,6 +108,9 @@ If no options are given, {0} will run in windowed mode
self.no_overwrite = False
self.interactive = False
self.issue_id = None
self.recursive = False
self.run_script = False
self.script = None
self.file_list = []
def display_msg_and_quit( self, msg, code, show_help=False ):
@ -186,10 +183,10 @@ If no options are given, {0} will run in windowed mode
# parse command line options
try:
opts, args = getopt.getopt( input_args,
"hpdt:fm:vonsrc:ie",
"hpdt:fm:vonsrc:ieRS:",
[ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose",
"online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite",
"interactive", "nosummary", "version", "id="
"interactive", "nosummary", "version", "id=" , "recursive", "script=",
"export-to-zip", "delete-rar", "abort-on-conflict" ] )
except getopt.GetoptError as err:
@ -201,6 +198,11 @@ If no options are given, {0} will run in windowed mode
self.display_msg_and_quit( None, 0, show_help=True )
if o in ("-v", "--verbose"):
self.verbose = True
if o in ("-S", "--script"):
self.run_script = True
self.script = a
if o in ("-R", "--recursive"):
self.recursive = True
if o in ("-p", "--print"):
self.print_tags = True
if o in ("-d", "--delete"):
@ -260,11 +262,12 @@ If no options are given, {0} will run in windowed mode
self.data_style = MetaDataStyle.COMET
else:
self.display_msg_and_quit( "Invalid tag type", 1 )
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip:
self.no_gui = True
count = 0
if self.run_script: count += 1
if self.print_tags: count += 1
if self.delete_tags: count += 1
if self.save_tags: count += 1
@ -273,7 +276,42 @@ If no options are given, {0} will run in windowed mode
if self.export_to_zip: count +=1
if count > 1:
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, or export", 1 )
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, export, or run script", 1 )
if self.script is not None:
# 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)
if len(args) > 0:
if platform.system() == "Windows":
@ -282,7 +320,8 @@ If no options are given, {0} will run in windowed mode
self.file_list = []
for item in args:
self.file_list.extend(glob.glob(item))
self.filename = self.file_list[0]
if len(self.file_list) > 0:
self.filename = self.file_list[0]
else:
self.filename = args[0]
self.file_list = args
@ -302,3 +341,5 @@ If no options are given, {0} will run in windowed mode
#if self.rename_file and self.data_style is None:
# self.display_msg_and_quit( "Please specify the type to use for renaming with -t", 1 )
if self.recursive:
self.file_list = utils.get_recursive_filelist( self.file_list )

View File

@ -30,7 +30,7 @@ class PageBrowserWindow(QtGui.QDialog):
def __init__(self, parent, metadata):
super(PageBrowserWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagebrowser.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui' ), self)
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
gridlayout = QtGui.QGridLayout( self.pageContainer )
@ -52,8 +52,8 @@ class PageBrowserWindow(QtGui.QDialog):
self.btnPrev.setText("<<")
self.btnNext.setText(">>")
else:
self.btnPrev.setIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/left.png' )))
self.btnNext.setIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/right.png' )))
self.btnPrev.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('left.png' )))
self.btnNext.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('right.png')))
self.btnNext.clicked.connect( self.nextPage )
self.btnPrev.clicked.connect( self.prevPage )

View File

@ -26,7 +26,7 @@ from PyQt4 import uic
from settings import ComicTaggerSettings
from genericmetadata import GenericMetadata, PageType
from options import MetaDataStyle
from comicarchive import MetaDataStyle
from pageloader import PageLoader
from coverimagewidget import CoverImageWidget
@ -78,7 +78,7 @@ class PageListEditor(QWidget):
def __init__(self, parent ):
super(PageListEditor, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagelisteditor.ui' ), self )
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui' ), self)
self.pageWidget = CoverImageWidget( self.pageContainer, CoverImageWidget.ArchiveMode )
gridlayout = QGridLayout( self.pageContainer )

View File

@ -30,7 +30,7 @@ class IDProgressWindow(QtGui.QDialog):
def __init__(self, parent):
super(IDProgressWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'progresswindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui' ), self)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |

View File

@ -23,7 +23,7 @@ from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
from settingswindow import SettingsWindow
from filerenamer import FileRenamer
from options import MetaDataStyle
from comicarchive import MetaDataStyle
import os
import utils
@ -33,7 +33,7 @@ class RenameWindow(QtGui.QDialog):
def __init__( self, parent, comic_archive_list, data_style, settings ):
super(RenameWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('renamewindow.ui' ), self)
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
self.setWindowFlags(self.windowFlags() |

View File

@ -21,8 +21,9 @@ limitations under the License.
#import sys
import os
import sys
import ConfigParser
import configparser
import platform
import codecs
import utils
@ -35,14 +36,31 @@ class ComicTaggerSettings:
else:
return os.path.join( os.path.expanduser('~') , '.ComicTagger')
frozen_win_exe_path = None
@staticmethod
def baseDir():
if platform.system() == "Darwin" and getattr(sys, 'frozen', None):
return sys._MEIPASS
if getattr(sys, 'frozen', None):
if platform.system() == "Darwin":
return sys._MEIPASS
else: # Windows
#Preserve this value, in case sys.argv gets changed importing a plugin script
if ComicTaggerSettings.frozen_win_exe_path is None:
ComicTaggerSettings.frozen_win_exe_path = os.path.dirname( os.path.abspath( sys.argv[0] ) )
return ComicTaggerSettings.frozen_win_exe_path
else:
#print "ATB basename", os.path.dirname( os.path.abspath( sys.argv[0] ) )
return os.path.dirname( os.path.abspath( sys.argv[0] ) )
return os.path.dirname( os.path.abspath( __file__) )
@staticmethod
def getGraphic( filename ):
graphic_folder = os.path.join(ComicTaggerSettings.baseDir(), 'graphics')
return os.path.join( graphic_folder, filename )
@staticmethod
def getUIFile( filename ):
ui_folder = os.path.join(ComicTaggerSettings.baseDir(), 'ui')
return os.path.join( ui_folder, filename )
def setDefaultValues( self ):
# General Settings
@ -95,7 +113,7 @@ class ComicTaggerSettings:
self.folder = ""
self.setDefaultValues()
self.config = ConfigParser.RawConfigParser()
self.config = configparser.RawConfigParser()
self.folder = ComicTaggerSettings.getSettingsFolder()
if not os.path.exists( self.folder ):
@ -131,6 +149,10 @@ class ComicTaggerSettings:
self.unrar_exe_path = utils.which("unrar")
if self.unrar_exe_path != "":
self.save()
# make sure unrar/rar program is now in the path for the UnRAR class to use
utils.addtopath(os.path.dirname(self.unrar_exe_path))
utils.addtopath(os.path.dirname(self.rar_exe_path))
def reset( self ):
os.unlink( self.settings_file )
@ -138,7 +160,7 @@ class ComicTaggerSettings:
def load(self):
self.config.read( self.settings_file )
self.config.readfp(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' )
@ -259,6 +281,8 @@ class ComicTaggerSettings:
self.config.set( 'rename', 'rename_use_smart_string_cleanup', self.rename_use_smart_string_cleanup )
self.config.set( 'rename', 'rename_extension_based_on_archive', self.rename_extension_based_on_archive )
with open( self.settings_file, 'wb') as configfile:
with codecs.open( self.settings_file, 'wb', 'utf8') as configfile:
self.config.write(configfile)
#make sure the basedir is cached, in case we're on windows running a script from frozen binary
ComicTaggerSettings.baseDir()

View File

@ -54,7 +54,7 @@ class SettingsWindow(QtGui.QDialog):
def __init__(self, parent, settings ):
super(SettingsWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'settingswindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui' ), self)
self.setWindowFlags(self.windowFlags() &
~QtCore.Qt.WindowContextHelpButtonHint )
@ -153,8 +153,9 @@ class SettingsWindow(QtGui.QDialog):
self.settings.rar_exe_path = str(self.leRarExePath.text())
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
# make sure unrar program is now in the path for the UnRAR class
# make sure unrar/rar program is now in the path for the UnRAR class
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
self.leNameLengthDeltaThresh.setText("0")

View File

@ -32,7 +32,7 @@ import webbrowser
import re
from volumeselectionwindow import VolumeSelectionWindow
from options import MetaDataStyle
from comicarchive import MetaDataStyle
from comicinfoxml import ComicInfoXml
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
@ -72,9 +72,6 @@ class MultipleMatch():
def __init__( self, ca, match_list):
self.ca = ca
self.matches = match_list
# this reads the environment and inits the right locale
locale.setlocale(locale.LC_ALL, "")
class TaggerWindow( QtGui.QMainWindow):
@ -84,7 +81,7 @@ class TaggerWindow( QtGui.QMainWindow):
def __init__(self, file_list, settings, parent = None):
super(TaggerWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'taggerwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('taggerwindow.ui' ), self)
self.settings = settings
self.archiveCoverWidget = CoverImageWidget( self.coverImageContainer, CoverImageWidget.ArchiveMode )
@ -103,11 +100,6 @@ class TaggerWindow( QtGui.QMainWindow):
self.fileSelectionList.selectionChanged.connect( self.fileListSelectionChanged )
self.fileSelectionList.listCleared.connect( self.fileListCleared )
# ATB: Disable the list...
#self.splitter.setSizes([100,0])
#self.splitter.setHandleWidth(0)
#self.splitter.handle(1).setDisabled(True)
# we can't specify relative font sizes in the UI designer, so
# walk through all the lablels in the main form, and make them
@ -122,8 +114,8 @@ class TaggerWindow( QtGui.QMainWindow):
self.scrollAreaWidgetContents.adjustSize()
self.setWindowIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/app.png' )))
self.setWindowIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('app.png')))
self.save_data_style = settings.last_selected_save_data_style
self.load_data_style = settings.last_selected_load_data_style
@ -151,7 +143,13 @@ class TaggerWindow( QtGui.QMainWindow):
#TODO set up an RE validator for issueNum that allows
# for all sorts of wacky things
# tweak some control fonts
utils.reduceWidgetFontSize( self.lblFilename, 1 )
utils.reduceWidgetFontSize( self.lblArchiveType )
utils.reduceWidgetFontSize( self.lblTagList )
utils.reduceWidgetFontSize( self.lblPageCount )
#make sure some editable comboboxes don't take drop actions
self.cbFormat.lineEdit().setAcceptDrops(False)
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
@ -163,6 +161,8 @@ class TaggerWindow( QtGui.QMainWindow):
self.btnAddCredit.clicked.connect(self.addCredit)
self.btnRemoveCredit.clicked.connect(self.removeCredit)
self.twCredits.cellDoubleClicked.connect(self.editCredit)
self.connectDirtyFlagSignals()
self.pageListEditor.modified.connect(self.setDirtyFlag)
self.pageListEditor.firstFrontCoverChanged.connect( self.frontCoverChanged )
self.pageListEditor.listOrderChanged.connect( self.pageListOrderChanged )
self.tabWidget.currentChanged.connect( self.tabChanged )
@ -329,16 +329,16 @@ class TaggerWindow( QtGui.QMainWindow):
self.actionComicTaggerForum.triggered.connect( self.showForum )
# ToolBar
self.actionLoad.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/open.png')) )
self.actionLoadFolder.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/longbox.png')) )
self.actionWrite_Tags.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/save.png')) )
self.actionParse_Filename.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/parse.png')) )
self.actionSearchOnline.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/search.png')) )
self.actionAutoIdentify.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/auto.png')) )
self.actionAutoTag.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/autotag.png')) )
self.actionClearEntryForm.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/clear.png')) )
self.actionPageBrowser.setIcon( QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(),'graphics/browse.png') ))
self.actionLoad.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('open.png')))
self.actionLoadFolder.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('longbox.png')))
self.actionWrite_Tags.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('save.png')))
self.actionParse_Filename.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('parse.png')))
self.actionSearchOnline.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('search.png')))
self.actionAutoIdentify.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('auto.png')))
self.actionAutoTag.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('autotag.png')))
self.actionClearEntryForm.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('clear.png')))
self.actionPageBrowser.setIcon(QtGui.QIcon( ComicTaggerSettings.getGraphic('browse.png')))
self.toolBar.addAction( self.actionLoad )
self.toolBar.addAction( self.actionLoadFolder )
@ -451,7 +451,7 @@ class TaggerWindow( QtGui.QMainWindow):
msgBox = QtGui.QMessageBox()
msgBox.setWindowTitle( self.tr("About " + self.appName ) )
msgBox.setTextFormat( QtCore.Qt.RichText )
msgBox.setIconPixmap( QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/about.png' )) )
msgBox.setIconPixmap( QtGui.QPixmap(ComicTaggerSettings.getGraphic('about.png')) )
msgBox.setText( "<br><br><br>"
+ self.appName + " v" + self.version + "<br>"
+ "(c)2012 Anthony Beville<br><br>"
@ -560,7 +560,7 @@ class TaggerWindow( QtGui.QMainWindow):
filename = os.path.basename( ca.path )
filename = os.path.splitext(filename)[0]
filename = FileNameParser().fixSpaces(filename)
filename = FileNameParser().fixSpaces(filename, False)
self.lblFilename.setText( filename )
@ -953,25 +953,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?"),

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>1096</width>
<height>575</height>
<height>621</height>
</rect>
</property>
<property name="sizePolicy">
@ -91,23 +91,26 @@
</property>
<property name="minimumSize">
<size>
<width>220</width>
<width>230</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>220</width>
<width>230</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
<enum>QFrame::Sunken</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="margin">
<number>6</number>
</property>
<item>
<widget class="QLabel" name="lblFilename">
<property name="sizePolicy">
@ -1092,7 +1095,7 @@
<x>0</x>
<y>0</y>
<width>1096</width>
<height>21</height>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuComicTagger">

View File

@ -19,9 +19,63 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
import os
import re
import platform
import locale
import codecs
class UtilsVars:
already_fixed_encoding = False
def get_actual_preferred_encoding():
preferred_encoding = locale.getpreferredencoding()
if getattr(sys, 'frozen', None) and platform.system() == "Darwin":
preferred_encoding = "utf-8"
return preferred_encoding
def fix_output_encoding( ):
if not UtilsVars.already_fixed_encoding:
# this reads the environment and inits the right locale
locale.setlocale(locale.LC_ALL, "")
# try to make stdout/stderr encodings happy for unicode printing
preferred_encoding = get_actual_preferred_encoding()
sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout)
sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr)
UtilsVars.already_fixed_encoding = True
def get_recursive_filelist( pathlist ):
"""
Get a recursive list of of all files under all path items in the list
"""
filename_encoding = sys.getfilesystemencoding()
filelist = []
for p in pathlist:
# if path is a folder, walk it recursivly, and all files underneath
if type(p) == str:
#make sure string is unicode
p = p.decode(filename_encoding) #, 'replace')
elif type(p) != unicode:
#it's probably a QString
p = unicode(p)
if os.path.isdir( p ):
for root,dirs,files in os.walk( p ):
for f in files:
if type(f) == str:
#make sure string is unicode
f = f.decode(filename_encoding, 'replace')
elif type(f) != unicode:
#it's probably a QString
f = unicode(f)
filelist.append(os.path.join(root,f))
else:
filelist.append(p)
return filelist
def listToString( l ):
string = ""
if l is not None:
@ -31,10 +85,16 @@ def listToString( l ):
string += item
return string
def addtopath( dir ):
# TODO only add if not there already
if dir is not None and dir != "":
os.environ['PATH'] = dir + os.pathsep + os.environ['PATH']
def addtopath( dirname ):
if dirname is not None and dirname != "":
# verify that path doesn't already contain the given dirname
tmpdirname = re.escape(dirname)
pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep)
match = re.search(pattern, os.environ['PATH'])
if not match:
os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH']
# returns executable path, if it exists
def which(program):

View File

@ -90,7 +90,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
def __init__(self, parent, series_name, issue_number, year, cover_index_list, comic_archive, settings, autoselect=False):
super(VolumeSelectionWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'volumeselectionwindow.ui' ), self)
uic.loadUi(ComicTaggerSettings.getUIFile('volumeselectionwindow.ui' ), self)
self.imageWidget = CoverImageWidget( self.imageContainer, CoverImageWidget.URLMode )
gridlayout = QtGui.QGridLayout( self.imageContainer )
@ -369,7 +369,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

256
google/googlecode_upload.py Executable file
View File

@ -0,0 +1,256 @@
#!/usr/bin/env python
#
# Copyright 2006, 2007 Google Inc. All Rights Reserved.
# Author: danderson@google.com (David Anderson)
#
# Script for uploading files to a Google Code project.
#
# This is intended to be both a useful script for people who want to
# streamline project uploads and a reference implementation for
# uploading files to Google Code projects.
#
# To upload a file to Google Code, you need to provide a path to the
# file on your local machine, a small summary of what the file is, a
# project name, and a valid account that is a member or owner of that
# project. You can optionally provide a list of labels that apply to
# the file. The file will be uploaded under the same name that it has
# in your local filesystem (that is, the "basename" or last path
# component). Run the script with '--help' to get the exact syntax
# and available options.
#
# Note that the upload script requests that you enter your
# googlecode.com password. This is NOT your Gmail account password!
# This is the password you use on googlecode.com for committing to
# Subversion and uploading files. You can find your password by going
# to http://code.google.com/hosting/settings when logged in with your
# Gmail account. If you have already committed to your project's
# Subversion repository, the script will automatically retrieve your
# credentials from there (unless disabled, see the output of '--help'
# for details).
#
# If you are looking at this script as a reference for implementing
# your own Google Code file uploader, then you should take a look at
# the upload() function, which is the meat of the uploader. You
# basically need to build a multipart/form-data POST request with the
# right fields and send it to https://PROJECT.googlecode.com/files .
# Authenticate the request using HTTP Basic authentication, as is
# shown below.
#
# Licensed under the terms of the Apache Software License 2.0:
# http://www.apache.org/licenses/LICENSE-2.0
#
# Questions, comments, feature requests and patches are most welcome.
# Please direct all of these to the Google Code users group:
# http://groups.google.com/group/google-code-hosting
"""Google Code file uploader script.
"""
__author__ = 'danderson@google.com (David Anderson)'
import httplib
import os.path
import optparse
import getpass
import base64
import sys
def upload(file, project_name, user_name, password, summary, labels=None):
"""Upload a file to a Google Code project's file server.
Args:
file: The local path to the file.
project_name: The name of your project on Google Code.
user_name: Your Google account name.
password: The googlecode.com password for your account.
Note that this is NOT your global Google Account password!
summary: A small description for the file.
labels: an optional list of label strings with which to tag the file.
Returns: a tuple:
http_status: 201 if the upload succeeded, something else if an
error occured.
http_reason: The human-readable string associated with http_status
file_url: If the upload succeeded, the URL of the file on Google
Code, None otherwise.
"""
# The login is the user part of user@gmail.com. If the login provided
# is in the full user@domain form, strip it down.
if user_name.endswith('@gmail.com'):
user_name = user_name[:user_name.index('@gmail.com')]
form_fields = [('summary', summary)]
if labels is not None:
form_fields.extend([('label', l.strip()) for l in labels])
content_type, body = encode_upload_request(form_fields, file)
upload_host = '%s.googlecode.com' % project_name
upload_uri = '/files'
auth_token = base64.b64encode('%s:%s'% (user_name, password))
headers = {
'Authorization': 'Basic %s' % auth_token,
'User-Agent': 'Googlecode.com uploader v0.9.4',
'Content-Type': content_type,
}
server = httplib.HTTPSConnection(upload_host)
server.request('POST', upload_uri, body, headers)
resp = server.getresponse()
server.close()
if resp.status == 201:
location = resp.getheader('Location', None)
else:
location = None
return resp.status, resp.reason, location
def encode_upload_request(fields, file_path):
"""Encode the given fields and file into a multipart form body.
fields is a sequence of (name, value) pairs. file is the path of
the file to upload. The file will be uploaded to Google Code with
the same file name.
Returns: (content_type, body) ready for httplib.HTTP instance
"""
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
CRLF = '\r\n'
body = []
# Add the metadata about the upload first
for key, value in fields:
body.extend(
['--' + BOUNDARY,
'Content-Disposition: form-data; name="%s"' % key,
'',
value,
])
# Now add the file itself
file_name = os.path.basename(file_path)
f = open(file_path, 'rb')
file_content = f.read()
f.close()
body.extend(
['--' + BOUNDARY,
'Content-Disposition: form-data; name="filename"; filename="%s"'
% file_name,
# The upload server determines the mime-type, no need to set it.
'Content-Type: application/octet-stream',
'',
file_content,
])
# Finalize the form body
body.extend(['--' + BOUNDARY + '--', ''])
return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
def upload_find_auth(file_path, project_name, summary, labels=None,
user_name=None, password=None, tries=3):
"""Find credentials and upload a file to a Google Code project's file server.
file_path, project_name, summary, and labels are passed as-is to upload.
Args:
file_path: The local path to the file.
project_name: The name of your project on Google Code.
summary: A small description for the file.
labels: an optional list of label strings with which to tag the file.
config_dir: Path to Subversion configuration directory, 'none', or None.
user_name: Your Google account name.
tries: How many attempts to make.
"""
if user_name is None or password is None:
from netrc import netrc
authenticators = netrc().authenticators("code.google.com")
if authenticators:
if user_name is None:
user_name = authenticators[0]
if password is None:
password = authenticators[2]
while tries > 0:
if user_name is None:
# Read username if not specified or loaded from svn config, or on
# subsequent tries.
sys.stdout.write('Please enter your googlecode.com username: ')
sys.stdout.flush()
user_name = sys.stdin.readline().rstrip()
if password is None:
# Read password if not loaded from svn config, or on subsequent tries.
print 'Please enter your googlecode.com password.'
print '** Note that this is NOT your Gmail account password! **'
print 'It is the password you use to access Subversion repositories,'
print 'and can be found here: http://code.google.com/hosting/settings'
password = getpass.getpass()
status, reason, url = upload(file_path, project_name, user_name, password,
summary, labels)
# Returns 403 Forbidden instead of 401 Unauthorized for bad
# credentials as of 2007-07-17.
if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
# Rest for another try.
user_name = password = None
tries = tries - 1
else:
# We're done.
break
return status, reason, url
def main():
parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
'-p PROJECT [options] FILE')
parser.add_option('-s', '--summary', dest='summary',
help='Short description of the file')
parser.add_option('-p', '--project', dest='project',
help='Google Code project name')
parser.add_option('-u', '--user', dest='user',
help='Your Google Code username')
parser.add_option('-w', '--password', dest='password',
help='Your Google Code password')
parser.add_option('-l', '--labels', dest='labels',
help='An optional list of comma-separated labels to attach '
'to the file')
options, args = parser.parse_args()
if not options.summary:
parser.error('File summary is missing.')
elif not options.project:
parser.error('Project name is missing.')
elif len(args) < 1:
parser.error('File to upload not provided.')
elif len(args) > 1:
parser.error('Only one file may be specified.')
file_path = args[0]
if options.labels:
labels = options.labels.split(',')
else:
labels = None
status, reason, url = upload_find_auth(file_path, options.project,
options.summary, labels,
options.user, options.password)
if url:
print 'The file was uploaded successfully.'
print 'URL: %s' % url
return 0
else:
print 'An error occurred. Your file was not uploaded.'
print 'Google Code upload server said: %s (%s)' % (reason, status)
return 1
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,10 +1,10 @@
#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_SRC := $(TAGGER_BASE)/comictaggerlib
APP_NAME := ComicTagger
VERSION_STR := $(shell grep version $(TAGGER_BASE)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
VERSION_STR := $(shell grep version $(TAGGER_SRC)/ctversion.py| cut -d= -f2 | sed 's/\"//g')
MAC_BASE := $(TAGGER_BASE)/mac
DIST_DIR := $(MAC_BASE)/dist
@ -17,10 +17,20 @@ all: clean dist diskimage
dist:
$(PYINSTALLER_CMD) $(TAGGER_BASE)/comictagger.py -o $(MAC_BASE) -w -n $(APP_NAME) -s
cp $(TAGGER_BASE)/*.ui $(APP_BUNDLE)/Contents/MacOS
cp -a $(TAGGER_BASE)/graphics $(APP_BUNDLE)/Contents/MacOS
cp -a $(TAGGER_SRC)/ui $(APP_BUNDLE)/Contents/MacOS
cp -a $(TAGGER_SRC)/graphics $(APP_BUNDLE)/Contents/MacOS
cp $(MAC_BASE)/app.icns $(APP_BUNDLE)/Contents/Resources/icon-windowed.icns
# fix the version string in the Info.plist
sed -i -e 's/0\.0\.0/$(VERSION_STR)/' $(MAC_BASE)/dist/ComicTagger.app/Contents/Info.plist
# strip out PPC/x64
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/accessible
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/bearer
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/codecs
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/graphicssystems
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/iconengines
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/imageformats
clean:
rm -rf $(DIST_DIR) $(MAC_BASE)/build
rm -f $(MAC_BASE)/*.spec
@ -60,5 +70,6 @@ diskimage:
rm -f raw-$(DMG_FILE)
#move finished product to release folder
mkdir -p $(TAGGER_BASE)/release
mv $(DMG_FILE) $(TAGGER_BASE)/release

21
mac/make_thin.sh Executable file
View File

@ -0,0 +1,21 @@
rm -rf thin
BINFOLDER=$1
LIST=`cd $BINFOLDER; ls Qt* *.so *.dylib Python 2>/dev/null`
for FILE in $LIST
do
ISFAT=`lipo -info $BINFOLDER/$FILE|grep -v Non-fat`
if [ "$ISFAT" != "" ]
then
echo "Fat Binary: $FILE"
mkdir -p thin
lipo -thin i386 -output thin/$FILE $BINFOLDER/$FILE
fi
done
if [ -d thin ]
then
mv thin/* $BINFOLDER
else
echo No files to lipo
fi
rm -rf thin

View File

@ -1,3 +1,32 @@
---------------------------------
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
---------------------------------
Bug Fixes:
* Fixed a bug when renaming on non-English systems
* Fixed issue when saving settings on non-English systems
* Fixed a bug when comic contains non-RGB images
* Fixed a rare crash when comic image is not-RGB format
* Fixed sequence order of ComicInfo.xml items
Note:
New requirement for users of the python package: "configparser"
---------------------------------
1.1.2-beta - 14-Feb-2013
---------------------------------
Changes:
* Source is now packaged using Python distutils
* Recursive mode for CLI
* Run custom add-on scripts from CLI
* Minor UI tweaks
* Misc bug fixes
---------------------------------
1.1.0-beta - 06-Feb-2013
---------------------------------
@ -8,7 +37,7 @@ Changes:
double-clicked for embiggened view
* Export-to-zip in CLI (very handy in scripts!)
* More rename template variables
* Misc GUI & CLI Tweaks
* Misc GUI & CLI Tweaks
---------------------------------
1.0.3-beta - 31-Jan-2013

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
configparser
beautifulsoup4 >= 4.1
PIL >= 1.1.6

30
scripts/README.txt Normal file
View File

@ -0,0 +1,30 @@
This folder contains a set of example scripts that be used to extend the
capabilities of the ComicTagger app. They can be run either directly through
the python interpreter, or via the ComicTagger app.
To run via python directly, install ComicTagger source on your system using
the setup.py file.
To run via the ComicTagger app, invoke:
# comictagger.py -S script.py [script args]
(This will work also for binary distributions on Mac and Windows. No need for
an extra python install.)
The script must have an entry point function called "main()" to be invoked
via the app.
-----------------------------------------------------------------------------
This feature is UNSUPPORTED, and is for the convenience of development-minded
users of ComicTagger. The comictaggerlib module will remain largely
undocumented, and it will up to the crafty script developer to look through
the code to discern APIs and such.
That said, if there are questions, please post in the forums, and hopefully we
can get your add-on scripts working!
http://comictagger.forumotion.com/

84
scripts/find_dupes.py Executable file
View File

@ -0,0 +1,84 @@
#!/usr/bin/python
"""
find all duplicate comics
"""
import sys
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
from comictaggerlib.issuestring import *
import comictaggerlib.utils
def main():
utils.fix_output_encoding()
settings = ComicTaggerSettings()
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 find all comics with metadata
print >> sys.stderr, "reading in all comics..."
comic_list = []
max_name_len = 2
for filename in filelist:
ca = ComicArchive(filename, settings )
if ca.seemsToBeAComicArchive() and ca.hasMetadata( style ):
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()
comic_list.append((filename, ca.readMetadata( style )))
print >> sys.stderr, fmt_str.format( "" ) + "\r",
print "-----------------------------------------------"
print "Found {0} comics with {1} tags".format( len(comic_list), MetaDataStyle.name[style])
print "-----------------------------------------------"
#sort the list by series+issue+year, to put all the dupes together
def makeKey(x):
return "<" + unicode(x[1].series) + u" #" + unicode( x[1].issue ) + u" - " + unicode( x[1].year ) + ">"
comic_list.sort(key=makeKey, reverse=False)
# look for duplicate blocks
dupe_set_list = list()
dupe_set = list()
prev_key = ""
for filename, md in comic_list:
print >> sys.stderr, fmt_str.format( filename ) + "\r",
sys.stderr.flush()
new_key = makeKey((filename, md))
#if the new key same as the last, add to to dupe set
if new_key == prev_key:
dupe_set.append(filename)
#else we're on a new potential block
else:
# only add if the dupe list has 2 or more
if len (dupe_set) > 1:
dupe_set_list.append( dupe_set )
dupe_set = list()
dupe_set.append(filename)
prev_key = new_key
print >> sys.stderr, fmt_str.format( "" ) + "\r",
print "Found {0} duplicate sets".format( len(dupe_set_list))
for dupe_set in dupe_set_list:
ca = ComicArchive(dupe_set[0], settings )
md = ca.readMetadata( style )
print "{0} #{1} ({2})".format( md.series, md.issue, md.year )
for filename in dupe_set:
print "------------->{0}".format( filename )
if __name__ == '__main__':
main()

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