Compare commits

..

107 Commits

Author SHA1 Message Date
79a9cf1b40 Release 1.0.1
Fixed stupid bug where unicode can't be printed to OS X console

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

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

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

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

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

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

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

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

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

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

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

git-svn-id: http://comictagger.googlecode.com/svn/trunk@302 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:29:32 +00:00
3f305c6788 Made sure to reset the cache on a tag block delete
git-svn-id: http://comictagger.googlecode.com/svn/trunk@301 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 23:20:59 +00:00
9e68516dac Work on multi-file processing
git-svn-id: http://comictagger.googlecode.com/svn/trunk@300 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-16 22:46:22 +00:00
8f45994b9a Added a CLI option for searching by CV issue ID, that can be used when being called by Mylar
git-svn-id: http://comictagger.googlecode.com/svn/trunk@299 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-12 01:48:34 +00:00
4ea56c0bd0 Updated version and release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@296 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 21:20:36 +00:00
5445417404 Tweaked setting window UI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@295 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 21:20:20 +00:00
db6423aea9 Tweaked CBL tranform to save notes and weblink to comments
git-svn-id: http://comictagger.googlecode.com/svn/trunk@293 6c5673fe-1810-88d6-992b-cd32ca31540c
2013-01-07 20:15:17 +00:00
aa62a3e8ff gracefully handle no search results
git-svn-id: http://comictagger.googlecode.com/svn/trunk@292 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-31 00:43:22 +00:00
cd1733a975 Added a cache version file to manage clearing old one on upgrade
git-svn-id: http://comictagger.googlecode.com/svn/trunk@291 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 23:32:37 +00:00
c81319402d A few more unicode fixes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@290 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 05:31:00 +00:00
8a8e53d9c9 A lot of unicode related fixes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@289 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-30 05:06:12 +00:00
7614e95084 Handle case of no numeric portion of issue number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@288 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-22 01:19:14 +00:00
bd9f314496 Some tweaks to issue number finder
git-svn-id: http://comictagger.googlecode.com/svn/trunk@287 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-22 01:18:18 +00:00
bebd09d3f6 release notes update
git-svn-id: http://comictagger.googlecode.com/svn/trunk@284 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 06:00:31 +00:00
8a5430c83e updated todo
git-svn-id: http://comictagger.googlecode.com/svn/trunk@278 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:07:14 +00:00
93be1b42f4 Neatened up the new settings tabs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@277 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:06:44 +00:00
01be389fad New version
git-svn-id: http://comictagger.googlecode.com/svn/trunk@276 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:06:15 +00:00
ca9aaf9279 Don't always show full help
git-svn-id: http://comictagger.googlecode.com/svn/trunk@275 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 05:05:51 +00:00
ee9175087e Implemented file-renaming in GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@273 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-19 01:37:55 +00:00
94c5882175 Fixed printing of primary flag on CLI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@272 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-18 23:21:56 +00:00
ff74b3e5bc Added menu options to rename and apply CBL transform
git-svn-id: http://comictagger.googlecode.com/svn/trunk@271 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-18 23:14:00 +00:00
0017903a4f Got CBL transformer working
git-svn-id: http://comictagger.googlecode.com/svn/trunk@270 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 21:19:21 +00:00
3d98118fa9 Added option set CV series start year as volume
git-svn-id: http://comictagger.googlecode.com/svn/trunk@269 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 18:44:33 +00:00
faf0b5d437 New settings
git-svn-id: http://comictagger.googlecode.com/svn/trunk@268 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-17 18:19:32 +00:00
e14c9dfe19 fixed encoding error
git-svn-id: http://comictagger.googlecode.com/svn/trunk@267 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:23:48 +00:00
4343f3f08d Gracefully deal with bad image data
git-svn-id: http://comictagger.googlecode.com/svn/trunk@266 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:09:26 +00:00
4a94bf4d6f Ignore image files that begin with ".". They're probably cruft.
git-svn-id: http://comictagger.googlecode.com/svn/trunk@265 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-16 18:08:59 +00:00
a602c42f0e new file renamer class
git-svn-id: http://comictagger.googlecode.com/svn/trunk@263 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-15 05:54:12 +00:00
1efdc0e623 Set ctrl+A for menu auto-select
git-svn-id: http://comictagger.googlecode.com/svn/trunk@262 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-14 23:18:44 +00:00
152040964e exit properly on a version command
git-svn-id: http://comictagger.googlecode.com/svn/trunk@253 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-12 23:23:32 +00:00
584f78bc3c Added a link to Applications folder in the Mac DMG
git-svn-id: http://comictagger.googlecode.com/svn/trunk@252 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-12 23:23:08 +00:00
3f1868222d Updated version and release notes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@251 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-12 01:14:25 +00:00
45b94ce1fd removed debug print statement
git-svn-id: http://comictagger.googlecode.com/svn/trunk@250 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-11 17:59:20 +00:00
7289f6915a tweaked the page position a bit
git-svn-id: http://comictagger.googlecode.com/svn/trunk@249 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-11 17:59:02 +00:00
a5d39a88c8 Made sure the penciler isn't set a primary if more than artist already exists
git-svn-id: http://comictagger.googlecode.com/svn/trunk@248 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-11 17:57:59 +00:00
2acf2f60f3 Explicitly set the working folder for rar exe commands
git-svn-id: http://comictagger.googlecode.com/svn/trunk@247 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-11 17:57:26 +00:00
f6ff6c3b73 Added license link to about box
git-svn-id: http://comictagger.googlecode.com/svn/trunk@246 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-11 17:56:52 +00:00
6b88fb7e58 file globbing for windows command line
git-svn-id: http://comictagger.googlecode.com/svn/trunk@244 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-10 20:16:17 +00:00
3364e437c6 Better resizing in page list editor
git-svn-id: http://comictagger.googlecode.com/svn/trunk@243 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-10 20:05:14 +00:00
1e5f40121c Some resizing work for the pagelisteditor
git-svn-id: http://comictagger.googlecode.com/svn/trunk@242 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-10 19:35:00 +00:00
2a347522e4 Added style tweak based on metadata type
git-svn-id: http://comictagger.googlecode.com/svn/trunk@241 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-10 07:48:17 +00:00
7f1ce793e3 Page list editor work
git-svn-id: http://comictagger.googlecode.com/svn/trunk@240 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-10 06:39:54 +00:00
f7cb6e9d2b removed rouge print statement
git-svn-id: http://comictagger.googlecode.com/svn/trunk@239 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-10 06:39:31 +00:00
487c8a5bf4 Page list management work
git-svn-id: http://comictagger.googlecode.com/svn/trunk@238 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-08 19:57:51 +00:00
5b8f73528b Fixed a type casting bug in comet dates
git-svn-id: http://comictagger.googlecode.com/svn/trunk@237 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-07 01:53:59 +00:00
8af7651a50 Release notes update for 0.9.1-beta
git-svn-id: http://comictagger.googlecode.com/svn/trunk@234 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-06 23:01:18 +00:00
1e3d8ccad3 New release version 0.9.1
git-svn-id: http://comictagger.googlecode.com/svn/trunk@232 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-06 20:33:05 +00:00
c367b8806b Added Export as ZIP to GUI
Enhanced menu enabling/disabling based on state

git-svn-id: http://comictagger.googlecode.com/svn/trunk@231 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-06 20:32:24 +00:00
d3ea8d1b2c Make sure text is a string
git-svn-id: http://comictagger.googlecode.com/svn/trunk@230 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-06 20:30:55 +00:00
c5f1542874 First cut at zip export
git-svn-id: http://comictagger.googlecode.com/svn/trunk@229 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-06 05:45:53 +00:00
ab5d8599ac Added interactive CLI session after batch saving
git-svn-id: http://comictagger.googlecode.com/svn/trunk@228 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-06 04:46:01 +00:00
a2d0068522 only look at 3 pages if no good match
git-svn-id: http://comictagger.googlecode.com/svn/trunk@227 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-06 04:44:27 +00:00
c6c5728cb3 Added missed credit to comet
git-svn-id: http://comictagger.googlecode.com/svn/trunk@226 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-06 04:44:01 +00:00
e6f63beee2 Updated todo
git-svn-id: http://comictagger.googlecode.com/svn/trunk@225 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 22:17:56 +00:00
72af8f8564 Better CoMet support
git-svn-id: http://comictagger.googlecode.com/svn/trunk@224 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 22:17:30 +00:00
5390a92b98 Update file header comment
git-svn-id: http://comictagger.googlecode.com/svn/trunk@223 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 22:16:19 +00:00
c814436899 Make sure to check writable on copy operation
git-svn-id: http://comictagger.googlecode.com/svn/trunk@222 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 22:15:43 +00:00
dbec1999dc Fixed parsing bugs
Tweaked text

git-svn-id: http://comictagger.googlecode.com/svn/trunk@221 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 22:15:06 +00:00
a970ed0e36 Added tag copy copy to CLI
Added --nooverwrite option for save and copy on CLI

git-svn-id: http://comictagger.googlecode.com/svn/trunk@220 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 19:28:16 +00:00
6d8d90d5b7 Added some help menu items to direct to web URLs
git-svn-id: http://comictagger.googlecode.com/svn/trunk@208 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 07:34:53 +00:00
117d8d8998 Added "assume lone credit is primary" to the UI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@207 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 00:51:30 +00:00
3689317518 When CBI is read in, make sure the credits and tags are at least empty lists
git-svn-id: http://comictagger.googlecode.com/svn/trunk@206 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-05 00:20:06 +00:00
c845c786e4 Added option and code for assume that a lone writer or artist credit from CV is a 'primary'
git-svn-id: http://comictagger.googlecode.com/svn/trunk@205 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 20:36:56 +00:00
9ccdc60c19 Added support for CBI credit primary flag in GUI
git-svn-id: http://comictagger.googlecode.com/svn/trunk@204 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 19:46:54 +00:00
aec0477170 Cleaned up comments
git-svn-id: http://comictagger.googlecode.com/svn/trunk@203 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 19:45:42 +00:00
134dcbaba3 handle the case of "of XX" without parentheses
git-svn-id: http://comictagger.googlecode.com/svn/trunk@202 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 05:28:06 +00:00
f040f8dc74 Added a terse mode for only printing the page count and tags block types
git-svn-id: http://comictagger.googlecode.com/svn/trunk@201 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 05:27:32 +00:00
948acf9b23 parse out parthetical phrases when no issue number
git-svn-id: http://comictagger.googlecode.com/svn/trunk@200 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 04:02:53 +00:00
3c2f4fa662 work on CLI mode for better output when batch processing
git-svn-id: http://comictagger.googlecode.com/svn/trunk@199 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 04:02:16 +00:00
f99d466bae Some examples in comment
git-svn-id: http://comictagger.googlecode.com/svn/trunk@198 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 04:00:58 +00:00
a773ab6539 fixed cut and paste error
git-svn-id: http://comictagger.googlecode.com/svn/trunk@197 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 02:51:10 +00:00
ff2fca44f4 Added special case of mangled URL encodings in filename
git-svn-id: http://comictagger.googlecode.com/svn/trunk@196 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 02:50:25 +00:00
97fe437bb4 Changed issue selection window to compare with IssueString class
git-svn-id: http://comictagger.googlecode.com/svn/trunk@195 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 02:27:31 +00:00
32aabb100b Renaming now can use filename, or specified metadata
Added an issuestring parser for complex issue numbers with suffixes

git-svn-id: http://comictagger.googlecode.com/svn/trunk@194 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 01:16:58 +00:00
b385be4338 some more CoMet stuff
git-svn-id: http://comictagger.googlecode.com/svn/trunk@193 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-04 01:15:12 +00:00
deeeef90a6 First cut at CoMet support
git-svn-id: http://comictagger.googlecode.com/svn/trunk@192 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-12-02 20:17:39 +00:00
121889ed1b Fixed a exception when selecting a non-existent issue from a volume
git-svn-id: http://comictagger.googlecode.com/svn/trunk@187 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-11-30 17:55:28 +00:00
d300f51c7f Added svn tag target for doing releases
git-svn-id: http://comictagger.googlecode.com/svn/trunk@185 6c5673fe-1810-88d6-992b-cd32ca31540c
2012-11-30 08:01:35 +00:00
49 changed files with 6387 additions and 1711 deletions

View File

@ -16,5 +16,9 @@ zip:
rm -rf comictagger-src-$(VERSION_STR)
@echo When satisfied with release, do this:
@echo svn fpoooo $(VERSION_STR)
@echo make svn_tag
svn_tag:
svn copy https://comictagger.googlecode.com/svn/trunk \
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"

202
autotagmatchwindow.py Normal file
View File

@ -0,0 +1,202 @@
"""
A PyQT4 dialog to select from automated issue matches
"""
"""
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 os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
from imagefetcher import ImageFetcher
from settings import ComicTaggerSettings
class AutoTagMatchWindow(QtGui.QDialog):
volume_id = 0
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)
self.skipButton = QtGui.QPushButton(self.tr("Skip"))
self.buttonBox.addButton(self.skipButton, QtGui.QDialogButtonBox.ActionRole)
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setText("Accept and Next")
self.match_set_list = match_set_list
self.style = style
self.fetch_func = fetch_func
self.current_match_set_idx = 0
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
self.skipButton.clicked.connect(self.skipToNext)
self.updateData()
def updateData( self):
self.current_match_set = self.match_set_list[ self.current_match_set_idx ]
if self.current_match_set_idx + 1 == len( self.match_set_list ):
self.skipButton.setDisabled(True)
self.setCoverImage()
self.populateTable()
self.twList.resizeColumnsToContents()
self.current_row = 0
self.twList.selectRow( 0 )
path = self.current_match_set.ca.path
self.setWindowTitle( "Select correct match ({0} of {1}): {2}".format(
self.current_match_set_idx+1,
len( self.match_set_list ),
os.path.split(path)[1] ))
def populateTable( self ):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
self.twList.setSortingEnabled(False)
row = 0
for match in self.current_match_set.matches:
self.twList.insertRow(row)
item_text = match['series']
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
if match['publisher'] is not None:
item_text = u"{0}".format(match['publisher'])
else:
item_text = u"Unknown"
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
item_text = ""
if match['month'] is not None:
item_text = u"{0}/".format(match['month'])
if match['year'] is not None:
item_text += u"{0}".format(match['year'])
else:
item_text += u"????"
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
row += 1
def cellDoubleClicked( self, r, c ):
self.accept()
def currentItemChanged( self, curr, prev ):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.current_row = curr.row()
# list selection was changed, update the the issue cover
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.cover_fetcher = ImageFetcher( )
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
self.cover_fetcher.fetch( self.current_match_set.matches[self.current_row]['img_url'] )
# called when the image is done loading
def coverFetchComplete( self, image_data, issue_id ):
img = QtGui.QImage()
img.loadFromData( image_data )
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
def setCoverImage( self ):
ca = self.current_match_set.ca
cover_idx = ca.readMetadata(self.style).getCoverPageIndexList()[0]
image_data = ca.getPage( cover_idx )
self.labelCover.setScaledContents(True)
if image_data is not None:
img = QtGui.QImage()
img.loadFromData( image_data )
self.labelCover.setPixmap(QtGui.QPixmap(img))
else:
self.labelCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
def accept(self):
self.saveMatch()
self.current_match_set_idx += 1
if self.current_match_set_idx == len( self.match_set_list ):
# no more items
QtGui.QDialog.accept(self)
else:
self.updateData()
def skipToNext( self ):
self.current_match_set_idx += 1
if self.current_match_set_idx == len( self.match_set_list ):
# no more items
QtGui.QDialog.reject(self)
else:
self.updateData()
def reject(self):
reply = QtGui.QMessageBox.question(self,
self.tr("Cancel Matching"),
self.tr("Are you sure you wish to cancel the matching process?"),
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
if reply == QtGui.QMessageBox.No:
return
QtGui.QDialog.reject(self)
def saveMatch( self ):
match = self.current_match_set.matches[self.current_row]
ca = self.current_match_set.ca
md = ca.readMetadata( self.style )
if md.isEmpty:
md = ca.metadataFromFilename()
# now get the particular issue data
cv_md = self.fetch_func( match )
if cv_md is None:
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to get issue details!"))
return
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
md.overlay( cv_md )
success = ca.writeMetadata( md, self.style )
QtGui.QApplication.restoreOverrideCursor()
if not success:
QtGui.QMessageBox.warning(self, self.tr("Write Error"), self.tr("Saving the tags to the archive seemed to fail!"))

161
autotagmatchwindow.ui Normal file
View File

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogMatchSelect</class>
<widget class="QDialog" name="dialogMatchSelect">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>831</width>
<height>506</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Match</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="labelCover">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>300</height>
</size>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="twList">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="rowCount">
<number>0</number>
</property>
<property name="columnCount">
<number>3</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Series</string>
</property>
</column>
<column>
<property name="text">
<string>Publisher</string>
</property>
</column>
<column>
<property name="text">
<string>Date</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QLabel" name="labelThumbnail">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>300</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogMatchSelect</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogMatchSelect</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

67
autotagprogresswindow.py Normal file
View File

@ -0,0 +1,67 @@
"""
A PyQT4 dialog to show ID log and progress
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
from PyQt4 import QtCore, QtGui, uic
import os
from settings import ComicTaggerSettings
class AutoTagProgressWindow(QtGui.QDialog):
def __init__(self, parent):
super(AutoTagProgressWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagprogresswindow.ui' ), self)
self.lblTest.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.lblArchive.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
self.isdone = False
# we can't specify relative font sizes in the UI designer, so
# make font for scroll window a smidge smaller
f = self.textEdit.font()
if f.pointSize() > 10:
f.setPointSize( f.pointSize() - 2 )
self.textEdit.setFont( f )
def setArchiveImage( self, img_data):
self.setCoverImage( img_data, self.lblArchive )
def setTestImage( self, img_data):
self.setCoverImage( img_data, self.lblTest )
def setCoverImage( self, img_data , label):
if img_data is not None:
img = QtGui.QImage()
img.loadFromData( img_data )
label.setPixmap(QtGui.QPixmap(img))
label.setScaledContents(True)
else:
label.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
label.setScaledContents(True)
QtCore.QCoreApplication.processEvents()
QtCore.QCoreApplication.processEvents()
def reject(self):
QtGui.QDialog.reject(self)
self.isdone = True

150
autotagprogresswindow.ui Normal file
View File

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

55
autotagstartwindow.py Normal file
View File

@ -0,0 +1,55 @@
"""
A PyQT4 dialog to confirm and set options for auto-tag
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
from settingswindow import SettingsWindow
from filerenamer import FileRenamer
import os
import utils
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)
self.label.setText( msg )
self.settings = settings
self.cbxSaveOnLowConfidence.setCheckState( QtCore.Qt.Unchecked )
self.cbxDontUseYear.setCheckState( QtCore.Qt.Unchecked )
self.cbxAssumeIssueOne.setCheckState( QtCore.Qt.Unchecked )
self.autoSaveOnLow = False
self.dontUseYear = False
self.assumeIssueOne = False
def accept( self ):
QtGui.QDialog.accept(self)
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
self.dontUseYear = self.cbxDontUseYear.isChecked()
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()

124
autotagstartwindow.ui Normal file
View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogExport</class>
<widget class="QDialog" name="dialogExport">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>524</width>
<height>248</height>
</rect>
</property>
<property name="windowTitle">
<string>Auto-Tag</string>
</property>
<property name="modal">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="1">
<widget class="QCheckBox" name="cbxDontUseYear">
<property name="text">
<string>Don't use publication year in indentification process</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="cbxSaveOnLowConfidence">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Save on low confidence match</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="cbxAssumeIssueOne">
<property name="text">
<string>If no issue number, assume &quot;1&quot;</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogExport</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>346</x>
<y>187</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>104</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dialogExport</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>346</x>
<y>187</y>
</hint>
<hint type="destinationlabel">
<x>277</x>
<y>104</y>
</hint>
</hints>
</connection>
</connections>
</ui>

99
cbltransformer.py Normal file
View File

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

260
comet.py Normal file
View File

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

View File

@ -29,6 +29,13 @@ if platform.system() == "Windows":
import _subprocess
import time
import StringIO
try:
import Image
pil_available = True
except ImportError:
pil_available = False
sys.path.insert(0, os.path.abspath(".") )
import UnRAR2
from UnRAR2.rar_exceptions import *
@ -36,7 +43,8 @@ from UnRAR2.rar_exceptions import *
from options import Options, MetaDataStyle
from comicinfoxml import ComicInfoXml
from comicbookinfo import ComicBookInfo
from genericmetadata import GenericMetadata
from comet import CoMet
from genericmetadata import GenericMetadata, PageType
from filenameparser import FileNameParser
@ -55,9 +63,16 @@ class ZipArchiver:
return self.writeZipComment( self.path, comment )
def readArchiveFile( self, archive_file ):
data = ""
zf = zipfile.ZipFile( self.path, 'r' )
data = zf.read( archive_file )
zf.close()
try:
data = zf.read( archive_file )
except zipfile.BadZipfile:
print "bad zipfile: {0} :: {1}".format(self.path, archive_file)
except Exception:
print "bad zipfile: {0} :: {1}".format(self.path, archive_file)
finally:
zf.close()
return data
def removeArchiveFile( self, archive_file ):
@ -92,7 +107,6 @@ class ZipArchiver:
# zip helper func
def rebuildZipFile( self, exclude_list ):
# TODO: use tempfile.mkstemp
# this recompresses the zip archive, without the files in the exclude_list
#print "Rebuilding zip {0} without {1}".format( self.path, exclude_list )
@ -180,16 +194,41 @@ class ZipArchiver:
return False
else:
return True
def copyFromArchive( self, otherArchive ):
# Replace the current zip with one copied from another archive
try:
zout = zipfile.ZipFile (self.path, 'w')
for fname in otherArchive.getArchiveFilenameList():
data = otherArchive.readArchiveFile( fname )
if data is not None:
zout.writestr( fname, data )
zout.close()
#preserve the old comment
comment = otherArchive.getArchiveComment()
if comment is not None:
if not self.writeZipComment( self.path, comment ):
return False
except Exception as e:
print "Error while copying to {0}: {1}".format(self.path, e)
return False
else:
return True
#------------------------------------------
# RAR implementation
class RarArchiver:
devnull = None
def __init__( self, path ):
self.path = path
self.rar_exe_path = None
self.devnull = open(os.devnull, "w")
if RarArchiver.devnull is None:
RarArchiver.devnull = open(os.devnull, "w")
# windows only, keeps the cmd.exe from popping up
if platform.system() == "Windows":
@ -199,11 +238,12 @@ class RarArchiver:
self.startupinfo = None
def __del__(self):
self.devnull.close()
#RarArchiver.devnull.close()
pass
def getArchiveComment( self ):
rarc = UnRAR2.RarFile( self.path )
rarc = self.getRARObj()
return rarc.comment
def setArchiveComment( self, comment ):
@ -216,10 +256,12 @@ class RarArchiver:
f.write( comment )
f.close()
working_dir = os.path.dirname( os.path.abspath( self.path ) )
# use external program to write comment to Rar archive
subprocess.call([self.rar_exe_path, 'c', '-c-', '-z' + tmp_name, self.path],
subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
startupinfo=self.startupinfo,
stdout=self.devnull)
stdout=RarArchiver.devnull)
if platform.system() == "Darwin":
time.sleep(1)
@ -234,14 +276,38 @@ class RarArchiver:
def readArchiveFile( self, archive_file ):
entries = UnRAR2.RarFile( self.path ).read_files( archive_file )
# Make sure to escape brackets, since some funky stuff is going on
# underneath with "fnmatch"
archive_file = archive_file.replace("[", '[[]')
entries = []
#entries is a list of of tuples: ( rarinfo, filedata)
if (len(entries) == 1):
return entries[0][1]
else:
return ""
rarc = self.getRARObj()
tries = 0
while tries < 10:
try:
tries = tries+1
entries = rarc.read_files( archive_file )
except (OSError, IOError) as e:
print e, "in readArchiveFile! try %s" % tries
time.sleep(1)
except Exception as e:
print "Unexpected exception in readArchiveFile! {0}".format( e )
break
else:
#Success"
#entries is a list of of tuples: ( rarinfo, filedata)
if (len(entries) == 1):
return entries[0][1]
else:
return None
return None
def writeArchiveFile( self, archive_file, data ):
if self.rar_exe_path is not None:
@ -249,15 +315,19 @@ class RarArchiver:
tmp_folder = tempfile.mkdtemp()
tmp_file = os.path.join( tmp_folder, archive_file )
working_dir = os.path.dirname( os.path.abspath( self.path ) )
# TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt"
# will need to create the subfolder above, I guess...
f = open(tmp_file, 'w')
f.write( data )
f.close()
# use external program to write file to Rar archive
subprocess.call([self.rar_exe_path, 'a', '-c-', '-ep', self.path, tmp_file],
subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
startupinfo=self.startupinfo,
stdout=self.devnull)
stdout=RarArchiver.devnull)
if platform.system() == "Darwin":
time.sleep(1)
@ -276,7 +346,7 @@ class RarArchiver:
# use external program to remove file from Rar archive
subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file],
startupinfo=self.startupinfo,
stdout=self.devnull)
stdout=RarArchiver.devnull)
if platform.system() == "Darwin":
time.sleep(1)
@ -289,10 +359,44 @@ class RarArchiver:
def getArchiveFilenameList( self ):
rarc = UnRAR2.RarFile( self.path )
rarc = self.getRARObj()
#namelist = [ item.filename for item in rarc.infolist() ]
#return namelist
return [ item.filename for item in rarc.infolist() ]
tries = 0
while tries < 10:
try:
tries = tries+1
namelist = [ item.filename for item in rarc.infolist() ]
except (OSError, IOError) as e:
print e, "in getArchiveFilenameList! try %s" % tries
time.sleep(1)
else:
#Success"
return namelist
raise e
def getRARObj( self ):
tries = 0
while tries < 10:
try:
tries = tries+1
rarc = UnRAR2.RarFile( self.path )
except (OSError, IOError) as e:
print e, "in getRARObj! try %s" % tries
time.sleep(1)
else:
#Success"
return rarc
raise e
#------------------------------------------
# Folder implementation
class FolderArchiver:
@ -385,7 +489,9 @@ class ComicArchive:
def __init__( self, path ):
self.path = path
self.ci_xml_filename = 'ComicInfo.xml'
self.comet_default_filename = 'CoMet.xml'
self.resetCache()
if self.zipTest():
self.archive_type = self.ArchiveType.Zip
self.archiver = ZipArchiver( self.path )
@ -401,6 +507,22 @@ class ComicArchive:
self.archive_type = self.ArchiveType.Unknown
self.archiver = UnknownArchiver( self.path )
# Clears the cached data
def resetCache( self ):
self.has_cix = None
self.has_cbi = None
self.has_comet = None
self.comet_filename = None
self.page_count = None
self.page_list = None
self.cix_md = None
self.cbi_md = None
self.comet_md = None
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
@ -426,11 +548,11 @@ class ComicArchive:
def isFolder( self ):
return self.archive_type == self.ArchiveType.Folder
def isWritable( self ):
def isWritable( self, check_rar_status=True ):
if self.archive_type == self.ArchiveType.Unknown :
return False
elif self.isRar() and self.archiver.rar_exe_path is None:
elif check_rar_status and self.isRar() and self.archiver.rar_exe_path is None:
return False
elif not os.access(self.path, os.W_OK):
@ -470,15 +592,22 @@ class ComicArchive:
return self.readCIX()
elif style == MetaDataStyle.CBI:
return self.readCBI()
elif style == MetaDataStyle.COMET:
return self.readCoMet()
else:
return GenericMetadata()
def writeMetadata( self, metadata, style ):
retcode = None
if style == MetaDataStyle.CIX:
return self.writeCIX( metadata )
retcode = self.writeCIX( metadata )
elif style == MetaDataStyle.CBI:
return self.writeCBI( metadata )
retcode = self.writeCBI( metadata )
elif style == MetaDataStyle.COMET:
retcode = self.writeCoMet( metadata )
return retcode
def hasMetadata( self, style ):
@ -486,20 +615,21 @@ class ComicArchive:
return self.hasCIX()
elif style == MetaDataStyle.CBI:
return self.hasCBI()
elif style == MetaDataStyle.COMET:
return self.hasCoMet()
else:
return False
def removeMetadata( self, style ):
retcode = True
if style == MetaDataStyle.CIX:
return self.removeCIX()
retcode = self.removeCIX()
elif style == MetaDataStyle.CBI:
return self.removeCBI()
retcode = self.removeCBI()
elif style == MetaDataStyle.COMET:
retcode = self.removeCoMet()
return retcode
def getCoverPage(self):
# assume first page is the cover (for now)
return self.getPage( 0 )
def getPage( self, index ):
image_data = None
@ -523,31 +653,39 @@ class ComicArchive:
def getPageNameList( self , sort_list=True):
# get the list file names in the archive, and sort
files = self.archiver.getArchiveFilenameList()
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
if sort_list:
files.sort(key=lambda x: x.lower())
# make a sub-list of image files
page_list = []
for name in files:
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
page_list.append(name)
if self.page_list is None:
# get the list file names in the archive, and sort
files = self.archiver.getArchiveFilenameList()
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
if sort_list:
files.sort(key=lambda x: x.lower())
# make a sub-list of image files
self.page_list = []
for name in files:
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] and os.path.basename(name)[0] != "." ):
self.page_list.append(name)
return page_list
return self.page_list
def getNumberOfPages( self ):
return len( self.getPageNameList( sort_list=False ) )
if self.page_count is None:
self.page_count = len( self.getPageNameList( ) )
return self.page_count
def readCBI( self ):
raw_cbi = self.readRawCBI()
if raw_cbi is None:
return GenericMetadata()
return ComicBookInfo().metadataFromString( raw_cbi )
if self.cbi_md is None:
raw_cbi = self.readRawCBI()
if raw_cbi is None:
self.cbi_md = GenericMetadata()
else:
self.cbi_md = ComicBookInfo().metadataFromString( raw_cbi )
self.cbi_md.setDefaultPageList( self.getNumberOfPages() )
return self.cbi_md
def readRawCBI( self ):
if ( not self.hasCBI() ):
@ -555,24 +693,65 @@ class ComicArchive:
return self.archiver.getArchiveComment()
def hasCBI(self):
if self.has_cbi is None:
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
if not self.seemsToBeAComicArchive():
self.has_cbi = False
else:
comment = self.archiver.getArchiveComment()
self.has_cbi = ComicBookInfo().validateString( comment )
return self.has_cbi
def writeCBI( self, metadata ):
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
return self.archiver.setArchiveComment( cbi_string )
if metadata is not None:
self.applyArchiveInfoToMetadata( metadata )
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
write_success = self.archiver.setArchiveComment( cbi_string )
if write_success:
self.has_cbi = True
self.cbi_md = metadata
else:
self.resetCache()
return write_success
else:
return False
def removeCBI( self ):
return self.archiver.setArchiveComment( "" )
if self.hasCBI():
write_success = self.archiver.setArchiveComment( "" )
if write_success:
self.has_cbi = False
self.cbi_md = None
else:
self.resetCache()
return write_success
return True
def readCIX( self ):
raw_cix = self.readRawCIX()
if raw_cix is None:
return GenericMetadata()
if self.cix_md is None:
raw_cix = self.readRawCIX()
if raw_cix is None:
self.cix_md = GenericMetadata()
else:
self.cix_md = ComicInfoXml().metadataFromString( raw_cix )
#validate the existing page list (make sure count is correct)
if len ( self.cix_md.pages ) != 0 :
if len ( self.cix_md.pages ) != self.getNumberOfPages():
# pages array doesn't match the actual number of images we're seeing
# in the archive, so discard the data
self.cix_md.pages = []
if len( self.cix_md.pages ) == 0:
self.cix_md.setDefaultPageList( self.getNumberOfPages() )
return ComicInfoXml().metadataFromString( raw_cix )
return self.cix_md
def readRawCIX( self ):
if not self.hasCIX():
print self.path, "doesn't has ComicInfo.xml data!"
return None
return self.archiver.readArchiveFile( self.ci_xml_filename )
@ -580,32 +759,156 @@ class ComicArchive:
def writeCIX(self, metadata):
if metadata is not None:
self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True )
cix_string = ComicInfoXml().stringFromMetadata( metadata )
return self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string )
write_success = self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string )
if write_success:
self.has_cix = True
self.cix_md = metadata
else:
self.resetCache()
return write_success
else:
return False
def removeCIX( self ):
return self.archiver.removeArchiveFile( self.ci_xml_filename )
if self.hasCIX():
write_success = self.archiver.removeArchiveFile( self.ci_xml_filename )
if write_success:
self.has_cix = False
self.cix_md = None
else:
self.resetCache()
return write_success
return True
def hasCIX(self):
if not self.seemsToBeAComicArchive():
return False
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
return True
if self.has_cix is None:
if not self.seemsToBeAComicArchive():
self.has_cix = False
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
self.has_cix = True
else:
self.has_cix = False
return self.has_cix
def readCoMet( self ):
if self.comet_md is None:
raw_comet = self.readRawCoMet()
if raw_comet is None:
self.comet_md = GenericMetadata()
else:
self.comet_md = CoMet().metadataFromString( raw_comet )
self.comet_md.setDefaultPageList( self.getNumberOfPages() )
#use the coverImage value from the comet_data to mark the cover in this struct
# walk through list of images in file, and find the matching one for md.coverImage
# need to remove the existing one in the default
if self.comet_md.coverImage is not None:
cover_idx = 0
for idx,f in enumerate(self.getPageNameList()):
if self.comet_md.coverImage == f:
cover_idx = idx
break
if cover_idx != 0:
del (self.comet_md.pages[0]['Type'] )
self.comet_md.pages[ cover_idx ]['Type'] = PageType.FrontCover
return self.comet_md
def readRawCoMet( self ):
if not self.hasCoMet():
print self.path, "doesn't have CoMet data!"
return None
return self.archiver.readArchiveFile( self.comet_filename )
def writeCoMet(self, metadata):
if metadata is not None:
if not self.hasCoMet():
self.comet_filename = self.comet_default_filename
self.applyArchiveInfoToMetadata( metadata )
# Set the coverImage value, if it's not the first page
cover_idx = int(metadata.getCoverPageIndexList()[0])
if cover_idx != 0:
metadata.coverImage = self.getPageName( cover_idx )
comet_string = CoMet().stringFromMetadata( metadata )
write_success = self.archiver.writeArchiveFile( self.comet_filename, comet_string )
if write_success:
self.has_comet = True
self.comet_md = metadata
else:
self.resetCache()
return write_success
else:
return False
def removeCoMet( self ):
if self.hasCoMet():
write_success = self.archiver.removeArchiveFile( self.comet_filename )
if write_success:
self.has_comet = False
self.comet_md = None
else:
self.resetCache()
return write_success
return True
def hasCoMet(self):
if self.has_comet is None:
self.has_comet = False
if not self.seemsToBeAComicArchive():
return self.has_comet
#look at all xml files in root, and search for CoMet data, get first
for n in self.archiver.getArchiveFilenameList():
if ( os.path.dirname(n) == "" and
os.path.splitext(n)[1].lower() == '.xml'):
# read in XML file, and validate it
data = self.archiver.readArchiveFile( n )
if CoMet().validateString( data ):
# since we found it, save it!
self.comet_filename = n
self.has_comet = True
break
return self.has_comet
def hasCBI(self):
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
if not self.seemsToBeAComicArchive():
return False
def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False):
md.pageCount = self.getNumberOfPages()
if calc_page_sizes:
for p in md.pages:
idx = int( p['Image'] )
if pil_available:
if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p:
data = self.getPage( idx )
if data is not None:
try:
im = Image.open(StringIO.StringIO(data))
w,h = im.size
p['ImageSize'] = str(len(data))
p['ImageHeight'] = str(h)
p['ImageWidth'] = str(w)
except IOError:
p['ImageSize'] = str(len(data))
else:
if 'ImageSize' not in p:
data = self.getPage( idx )
p['ImageSize'] = str(len(data))
comment = self.archiver.getArchiveComment()
return ComicBookInfo().validateString( comment )
def metadataFromFilename( self ):
metadata = GenericMetadata()
@ -627,3 +930,12 @@ class ComicArchive:
metadata.isEmpty = False
return metadata
def exportAsZip( self, zipfilename ):
if self.archive_type == self.ArchiveType.Zip:
# nothing to do, we're already a zip
return True
zip_archiver = ZipArchiver( zipfilename )
return zip_archiver.copyFromArchive( self.archiver )

View File

@ -1,5 +1,5 @@
"""
A python class to encapsulate the ComicBookInfo data and file handling
A python class to encapsulate the ComicBookInfo data
"""
"""
@ -25,6 +25,7 @@ import zipfile
from genericmetadata import GenericMetadata
import utils
import ctversion
class ComicBookInfo:
@ -62,6 +63,12 @@ class ComicBookInfo:
metadata.criticalRating = xlate( 'rating' )
metadata.tags = xlate( 'tags' )
# make sure credits and tags are at least empty lists and not None
if metadata.credits is None:
metadata.credits = []
if metadata.tags is None:
metadata.tags = []
#need to massage the language string to be ISO
if metadata.language is not None:
# reverse look-up
@ -96,7 +103,7 @@ class ComicBookInfo:
# Create the dictionary that we will convert to JSON text
cbi = dict()
cbi_container = {'appID' : 'ComicTagger/0.1',
cbi_container = {'appID' : 'ComicTagger/' + ctversion.version,
'lastModified' : str(datetime.now()),
'ComicBookInfo/1.0' : cbi }
@ -104,18 +111,28 @@ class ComicBookInfo:
def assign( cbi_entry, md_entry):
if md_entry is not None:
cbi[cbi_entry] = md_entry
#helper func
def toInt(s):
i = None
if type(s) == str or type(s) == int:
try:
i = int(s)
except ValueError:
pass
return i
assign( 'series', metadata.series )
assign( 'title', metadata.title )
assign( 'issue', metadata.issue )
assign( 'publisher', metadata.publisher )
assign( 'publicationMonth', metadata.month )
assign( 'publicationYear', metadata.year )
assign( 'numberOfIssues', metadata.issueCount )
assign( 'publicationMonth', toInt(metadata.month) )
assign( 'publicationYear', toInt(metadata.year) )
assign( 'numberOfIssues', toInt(metadata.issueCount) )
assign( 'comments', metadata.comments )
assign( 'genre', metadata.genre )
assign( 'volume', metadata.volume )
assign( 'numberOfVolumes', metadata.volumeCount )
assign( 'volume', toInt(metadata.volume) )
assign( 'numberOfVolumes', toInt(metadata.volumeCount) )
assign( 'language', utils.getLanguageFromISO(metadata.language) )
assign( 'country', metadata.country )
assign( 'rating', metadata.criticalRating )

View File

@ -1,5 +1,5 @@
"""
A python class to encapsulate ComicRack's ComicInfo.xml data and file handling
A python class to encapsulate ComicRack's ComicInfo.xml data
"""
"""
@ -270,6 +270,7 @@ class ComicInfoXml:
if pages_node is not None:
for page in pages_node:
metadata.pages.append( page.attrib )
#print page.attrib
metadata.isEmpty = False

View File

@ -28,7 +28,10 @@ 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
@ -43,24 +46,151 @@ from comicarchive import ComicArchive
from issueidentifier import IssueIdentifier
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from filerenamer import FileRenamer
from cbltransformer import CBLTransformer
import utils
import codecs
class MultipleMatch():
def __init__( self, filename, match_list):
self.filename = filename
self.matches = match_list
class OnlineMatchResults():
def __init__(self):
self.goodMatches = []
self.noMatches = []
self.multipleMatches = []
self.writeFailures = []
#-----------------------------
def actual_issue_data_fetch( match, settings ):
# now get the particular issue data
try:
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings )
except ComicVineTalkerException:
print "Network error while getting issue details. Save aborted"
return None
if settings.apply_cbl_transform_on_cv_import:
cv_md = CBLTransformer( cv_md, settings ).apply()
return cv_md
def actual_metadata_save( ca, opts, md ):
if not opts.dryrun:
# write out the new data
if not ca.writeMetadata( md, opts.data_style ):
print "The tag save seemed to fail!"
return False
else:
print "Save complete."
else:
if opts.terse:
print "dry-run option was set, so nothing was written"
else:
print "dry-run option was set, so nothing was written, but here is the final set of tags:"
print u"{0}".format(md)
return True
def post_process_matches( match_results, opts, settings ):
# now go through the match results
if opts.show_save_summary:
if len( match_results.goodMatches ) > 0:
print "\nSuccessful matches:"
print "------------------"
for f in match_results.goodMatches:
print f
if len( match_results.noMatches ) > 0:
print "\nNo matches:"
print "------------------"
for f in match_results.noMatches:
print f
if len( match_results.writeFailures ) > 0:
print "\nFile Write Failures:"
print "------------------"
for f in match_results.writeFailures:
print f
if not opts.show_save_summary and not opts.interactive:
#jusr quit if we're not interactive or showing the summary
return
if len( match_results.multipleMatches ) > 0:
print "\nMultiple matches:"
print "------------------"
for mm in match_results.multipleMatches:
print mm.filename
for (counter,m) in enumerate(mm.matches):
print u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
m['series'],
m['issue_number'],
m['publisher'],
m['month'],
m['year'],
m['issue_title'])
if opts.interactive:
while True:
i = raw_input("Choose a match #, or 's' to skip: ")
if (i.isdigit() and int(i) in range(len(mm.matches))) or i == 's':
break
if i != 's':
# save the data!
# we know at this point, that the file is all good to go
ca = ComicArchive( mm.filename )
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
cv_md = actual_issue_data_fetch(mm.matches[int(i)], settings)
md.overlay( cv_md )
actual_metadata_save( ca, opts, md )
print
def cli_mode( opts, settings ):
if len( opts.file_list ) < 1:
print "You must specify at least one filename. Use the -h option for more info"
return
for f in opts.file_list:
if len( opts.file_list ) > 1:
print "Processing: ", f
process_file_cli( f, opts, settings )
def process_file_cli( filename, opts, settings ):
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 )
@ -70,246 +200,254 @@ def process_file_cli( filename, opts, settings ):
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.save_tags or opts.rename_file ):
if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ):
print "This archive is not writable for that tag type"
return
cix = False
cbi = False
if ca.hasCIX(): cix = True
if ca.hasCBI(): cbi = True
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 ca.isZip(): brief = "ZIP archive "
elif ca.isRar(): brief = "RAR archive "
elif ca.isFolder(): brief = "Folder archive "
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 (cbi or cix):
if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ):
brief += "none "
else:
if cbi: brief += "CBL "
if cix: brief += "CR "
if has[ MetaDataStyle.CBI ]: brief += "CBL "
if has[ MetaDataStyle.CIX ]: brief += "CR "
if has[ MetaDataStyle.COMET ]: brief += "CoMet "
brief += "]"
print brief
print
if opts.terse:
return
print
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
if cix:
if has[ MetaDataStyle.CIX ]:
print "------ComicRack tags--------"
if opts.raw:
print u"{0}".format(ca.readRawCIX())
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 cbi:
if has[ MetaDataStyle.CBI ]:
print "------ComicBookLover tags--------"
if opts.raw:
pprint(json.loads(ca.readRawCBI()))
else:
print u"{0}".format(ca.readCBI())
elif opts.delete_tags:
if opts.data_style == MetaDataStyle.CIX:
if cix:
if not opts.dryrun:
if not ca.removeCIX():
print "Tag removal seemed to fail!"
else:
print "Removed ComicRack tags."
else:
print "dry-run. ComicRack tags not removed"
else:
print "This archive doesn't have ComicRack tags."
if opts.data_style == MetaDataStyle.CBI:
if cbi:
if not opts.dryrun:
if not ca.removeCBI():
print "Tag removal seemed to fail!"
else:
print "Removed ComicBookLover tags."
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 "dry-run. ComicBookLover tags not removed"
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 "This archive doesn't have ComicBookLover tags."
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:
# OK we're gonna do a save of some new data
md = GenericMetadata()
# First read in existing data, if it's there
if opts.data_style == MetaDataStyle.CIX and cix:
md = ca.readCIX()
elif opts.data_style == MetaDataStyle.CBI and cbi:
md = ca.readCBI()
# now, overlay the new data onto the old, in order
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 opts.parse_filename:
md.overlay( ca.metadataFromFilename() )
if opts.metadata is not None:
md.overlay( opts.metadata )
if batch_mode:
print u"Processing {0}: ".format(filename)
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
# finally, search online
# now, search online
if opts.search_online:
ii = IssueIdentifier( ca, settings )
if md is None or md.isEmpty:
print "No metadata given to search online with!"
return
def myoutput( text ):
if opts.verbose:
IssueIdentifier.defaultWriteOutput( text )
if opts.issue_id is not None:
# we were given the actual ID to search with
try:
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
except ComicVineTalkerException:
print "Network error while getting issue details. Save aborted"
return None
# use our overlayed MD struct to search
ii.setAdditionalMetadata( md )
ii.onlyUseAdditionalMetaData = True
ii.setOutputFunction( myoutput )
matches = ii.search()
result = ii.search_result
found_match = False
choices = False
low_confidence = False
if result == ii.ResultNoMatches:
pass
elif result == ii.ResultFoundMatchButBadCoverScore:
low_confidence = True
found_match = True
elif result == ii.ResultFoundMatchButNotFirstPage :
found_match = True
elif result == ii.ResultMultipleMatchesWithBadImageScores:
low_confidence = True
choices = True
elif result == ii.ResultOneGoodMatch:
found_match = True
elif result == ii.ResultMultipleGoodMatches:
choices = True
if choices:
print "Online search: Multiple matches. Save aborted"
return
if low_confidence and opts.abortOnLowConfidence:
print "Online search: Low confidence match. Save aborted"
return
if not found_match:
print "Online search: No match found. Save aborted"
return
# we got here, so we have a single match
# now get the particular issue data
try:
cv_md = ComicVineTalker().fetchIssueData( matches[0]['volume_id'], matches[0]['issue_number'] )
except ComicVineTalkerException:
print "Network error while getting issue details. Save aborted"
return
if cv_md is None:
print "No match for ID {0} was found.".format(opts.issue_id)
return None
md.overlay( cv_md )
# ok, done building our metadata. time to save
#HACK
#opts.dryrun = True
#HACK
if not opts.dryrun:
# write out the new data
if not ca.writeMetadata( md, opts.data_style ):
print "The tag save seemed to fail!"
if settings.apply_cbl_transform_on_cv_import:
cv_md = CBLTransformer( cv_md, settings ).apply()
else:
print "Save complete."
ii = IssueIdentifier( ca, settings )
if md is None or md.isEmpty:
print "No metadata given to search online with!"
return
def myoutput( text ):
if opts.verbose:
IssueIdentifier.defaultWriteOutput( text )
# use our overlayed MD struct to search
ii.setAdditionalMetadata( md )
ii.onlyUseAdditionalMetaData = True
ii.setOutputFunction( myoutput )
ii.cover_page_index = md.getCoverPageIndexList()[0]
matches = ii.search()
result = ii.search_result
found_match = False
choices = False
low_confidence = False
if result == ii.ResultNoMatches:
pass
elif result == ii.ResultFoundMatchButBadCoverScore:
low_confidence = True
found_match = True
elif result == ii.ResultFoundMatchButNotFirstPage :
found_match = True
elif result == ii.ResultMultipleMatchesWithBadImageScores:
low_confidence = True
choices = True
elif result == ii.ResultOneGoodMatch:
found_match = True
elif result == ii.ResultMultipleGoodMatches:
choices = True
if choices:
print "Online search: Multiple matches. Save aborted"
match_results.multipleMatches.append(MultipleMatch(filename,matches))
return
if low_confidence and opts.abortOnLowConfidence:
print "Online search: Low confidence match. Save aborted"
match_results.noMatches.append(filename)
return
if not found_match:
print "Online search: No match found. Save aborted"
match_results.noMatches.append(filename)
return
# we got here, so we have a single match
# now get the particular issue data
cv_md = actual_issue_data_fetch(matches[0], settings)
if cv_md is None:
return
md.overlay( cv_md )
# ok, done building our metadata. time to save
if not actual_metadata_save( ca, opts, md ):
match_results.writeFailures.append(filename)
else:
print "dry-run option was set, so nothing was written, but here is the final set of tags:"
print u"{0}".format(md)
match_results.goodMatches.append(filename)
elif opts.rename_file:
md = GenericMetadata()
# First read in existing data, if it's there
if opts.data_style == MetaDataStyle.CIX and cix:
md = ca.readCIX()
elif opts.data_style == MetaDataStyle.CBI and cbi:
md = ca.readCBI()
msg_hdr = ""
if batch_mode:
msg_hdr = u"{0}: ".format(filename)
if md.isEmpty:
print "Comic archive contains no tags!"
if opts.data_style == MetaDataStyle.CIX:
if cix:
md = ca.readCIX()
else:
print "Comic archive contains no ComicRack tags!"
if opts.data_style == MetaDataStyle.CBI:
if cbi:
md = ca.readCBI()
else:
print "Comic archive contains no ComicBookLover tags!"
# TODO move this to ComicArchive, or maybe another class???
new_name = ""
if md.series is not None:
new_name += "{0}".format( md.series )
if opts.data_style is not None:
use_tags = has[ opts.data_style ]
else:
print "Can't rename without series name"
return
use_tags = False
if md.volume is not None:
new_name += " v{0}".format( md.volume )
if md.issue is not None:
new_name += " #{:03d}".format( int(md.issue) )
else:
print "Can't rename without issue number"
md = create_local_metadata( opts, ca, use_tags )
if md.series is None:
print msg_hdr + "Can't rename without series name"
return
new_ext = None # default
if settings.rename_extension_based_on_archive:
if ca.isZip():
new_ext = ".cbz"
elif ca.isRar():
new_ext = ".cbr"
renamer = FileRenamer( md )
renamer.setTemplate( settings.rename_template )
renamer.setIssueZeroPadding( settings.rename_issue_number_padding )
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
if md.issueCount is not None:
new_name += " (of {0})".format( md.issueCount )
if md.year is not None:
new_name += " ({0})".format( md.year )
if ca.isZip():
new_name += ".cbz"
elif ca.isRar():
new_name += ".cbr"
new_name = renamer.determineName( filename, ext=new_ext )
if new_name == os.path.basename(filename):
print "Filename is already good!"
print msg_hdr + "Filename is already good!"
return
folder = os.path.dirname( os.path.abspath( filename ) )
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
#HACK
#opts.dryrun = True
#HACK
suffix = ""
if not opts.dryrun:
# rename the file
os.rename( filename, new_abs_path )
else:
print "dry-run option was set, so nothing was changed, but here is the proposed filename:"
print "'{0}'".format(new_abs_path)
suffix = " (dry-run, no change)"
print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
@ -319,7 +457,7 @@ def process_file_cli( filename, opts, settings ):
def main():
# try to make stdout encodings happy for unicode
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
opts = Options()
opts.parseCmdLineArgs()
@ -348,8 +486,8 @@ def main():
splash.raise_()
app.processEvents()
try:
tagger_window = TaggerWindow( opts.filename, settings )
try:
tagger_window = TaggerWindow( opts.file_list, settings )
tagger_window.show()
if platform.system() != "Linux":
@ -361,8 +499,7 @@ def main():
if __name__ == "__main__":
main()
main()

View File

@ -25,6 +25,7 @@ import sys
import os
import datetime
import ctversion
from settings import ComicTaggerSettings
class ComicVineCacher:
@ -32,15 +33,38 @@ class ComicVineCacher:
def __init__(self ):
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
self.db_file = os.path.join( self.settings_folder, "cv_cache.db")
self.version_file = os.path.join( self.settings_folder, "cache_version.txt")
#verify that cache is from same version as this one
data = ""
try:
with open( self.version_file, 'rb' ) as f:
data = f.read()
f.close()
except:
pass
if data != ctversion.version:
self.clearCache()
if not os.path.exists( self.db_file ):
self.create_cache_db()
def clearCache( self ):
os.unlink( self.db_file )
try:
os.unlink( self.db_file )
except:
pass
try:
os.unlink( self.version_file )
except:
pass
def create_cache_db( self ):
#create the version file
with open( self.version_file, 'w' ) as f:
f.write( ctversion.version )
# this will wipe out any existing version
open( self.db_file, 'w').close()
@ -68,6 +92,7 @@ class ComicVineCacher:
"name TEXT," +
"publisher TEXT," +
"count_of_issues INT," +
"start_year INT," +
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (id) )"
)
@ -92,7 +117,7 @@ class ComicVineCacher:
con = lite.connect( self.db_file )
with con:
con.text_factory = unicode
cur = con.cursor()
# remove all previous entries with this search term
@ -130,6 +155,7 @@ class ComicVineCacher:
results = list()
con = lite.connect( self.db_file )
with con:
con.text_factory = unicode
cur = con.cursor()
@ -178,6 +204,7 @@ class ComicVineCacher:
"name": cv_volume_record['name'],
"publisher": pub_name,
"count_of_issues": cv_volume_record['count_of_issues'],
"start_year": cv_volume_record['start_year'],
"timestamp": timestamp
}
self.upsert( cur, "volumes", "id", cv_volume_record['id'], data)
@ -202,6 +229,7 @@ class ComicVineCacher:
con = lite.connect( self.db_file )
with con:
cur = con.cursor()
con.text_factory = unicode
# purge stale volume info
a_week_ago = datetime.datetime.today()-datetime.timedelta(days=7)
@ -212,7 +240,7 @@ class ComicVineCacher:
cur.execute( "DELETE FROM Issues WHERE timestamp < ?", [ str(a_month_ago) ] )
# fetch
cur.execute("SELECT id,name,publisher,count_of_issues FROM Volumes WHERE id = ?", [ volume_id ] )
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [ volume_id ] )
row = cur.fetchone()
@ -227,6 +255,7 @@ class ComicVineCacher:
result['publisher'] = dict()
result['publisher']['name'] = row[2]
result['count_of_issues'] = row[3]
result['start_year'] = row[4]
result['issues'] = list()
cur.execute("SELECT id,name,issue_number,image_url,image_hash FROM Issues WHERE volume_id = ?", [ volume_id ] )
@ -252,6 +281,7 @@ class ComicVineCacher:
with con:
cur = con.cursor()
con.text_factory = unicode
timestamp = datetime.datetime.now()
data = {
@ -270,6 +300,7 @@ class ComicVineCacher:
con = lite.connect( self.db_file )
with con:
cur = con.cursor()
con.text_factory = unicode
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year FROM Issues WHERE id=?", [ issue_id ])
row = cur.fetchone()

View File

@ -24,6 +24,8 @@ from pprint import pprint
import urllib2, urllib
import math
import re
import datetime
import ctversion
try:
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
@ -43,6 +45,8 @@ import utils
from settings import ComicTaggerSettings
from comicvinecacher import ComicVineCacher
from genericmetadata import GenericMetadata
from issuestring import IssueString
class ComicVineTalkerException(Exception):
pass
@ -91,7 +95,8 @@ class ComicVineTalker(QObject):
original_series_name = series_name
series_name = urllib.quote_plus(str(series_name))
series_name = urllib.quote_plus(series_name.encode("utf-8"))
#series_name = urllib.quote_plus(unicode(series_name))
search_url = "http://api.comicvine.com/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues&sort=start_year"
content = self.getUrlContent(search_url)
@ -175,12 +180,14 @@ class ComicVineTalker(QObject):
return volume_results
def fetchIssueData( self, series_id, issue_number ):
def fetchIssueData( self, series_id, issue_number, settings ):
volume_results = self.fetchVolumeData( series_id )
found = False
for record in volume_results['issues']:
for record in volume_results['issues']:
if IssueString(issue_number).asFloat() is None:
issue_number = 1
if float(record['issue_number']) == float(issue_number):
found = True
break
@ -198,16 +205,35 @@ class ComicVineTalker(QObject):
else:
return None
# now, map the comicvine data to generic metadata
return self.mapCVDataToMetadata( volume_results, issue_results, settings )
def fetchIssueDataByIssueID( self, issue_id, settings ):
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
content = self.getUrlContent(issue_url)
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
return None
issue_results = cv_response['results']
volume_results = self.fetchVolumeData( issue_results['volume']['id'] )
# now, map the comicvine data to generic metadata
md = self.mapCVDataToMetadata( volume_results, issue_results, settings )
md.isEmpty = False
return md
def mapCVDataToMetadata(self, volume_results, issue_results, settings ):
# now, map the comicvine data to generic metadata
metadata = GenericMetadata()
metadata.series = issue_results['volume']['name']
# format the issue number string nicely, since it's usually something like "2.00"
num_f = float(issue_results['issue_number'])
num_s = str( int(math.floor(num_f)) )
if math.floor(num_f) != num_f:
num_s = str( num_f )
num_s = IssueString(issue_results['issue_number']).asString()
metadata.issue = num_s
metadata.title = issue_results['name']
@ -216,8 +242,13 @@ class ComicVineTalker(QObject):
metadata.year = issue_results['publish_year']
#metadata.issueCount = volume_results['count_of_issues']
metadata.comments = self.cleanup_html(issue_results['description'])
metadata.notes = "Tagged with ComicTagger app using info from Comic Vine."
if settings.use_series_start_as_volume:
metadata.volume = volume_results['start_year']
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
ctversion.version,
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
issue_results['id'])
#metadata.notes += issue_results['site_detail_url']
metadata.webLink = issue_results['site_detail_url']
@ -227,7 +258,7 @@ class ComicVineTalker(QObject):
for role in person['roles']:
# can we determine 'primary' from CV??
role_name = role['role'].title()
metadata.addCredit( person['name'], role['role'].title(), False )
metadata.addCredit( person['name'], role['role'].title(), False )
character_credits = issue_results['character_credits']
character_list = list()
@ -248,11 +279,12 @@ class ComicVineTalker(QObject):
metadata.locations = utils.listToString( location_list )
story_arc_credits = issue_results['story_arc_credits']
for arc in story_arc_credits:
metadata.storyArc = arc['name']
#just use the first one, if at all
break
arc_list = []
for arc in story_arc_credits:
arc_list.append(arc['name'])
if len(arc_list) > 0:
metadata.storyArc = utils.listToString(arc_list)
return metadata
def cleanup_html( self, string):

View File

@ -30,7 +30,7 @@ class CreditEditorWindow(QtGui.QDialog):
ModeNew = 1
def __init__(self, parent, mode, role, name ):
def __init__(self, parent, mode, role, name, primary ):
super(CreditEditorWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'crediteditorwindow.ui' ), self)
@ -64,10 +64,33 @@ class CreditEditorWindow(QtGui.QDialog):
self.cbRole.setEditText( role )
else:
self.cbRole.setCurrentIndex( i )
if primary:
self.cbPrimary.setCheckState( QtCore.Qt.Checked )
self.cbRole.currentIndexChanged.connect(self.roleChanged)
self.cbRole.editTextChanged.connect(self.roleChanged)
self.updatePrimaryButton()
def updatePrimaryButton( self ):
enabled =self.currentRoleCanBePrimary()
self.cbPrimary.setEnabled( enabled )
def currentRoleCanBePrimary( self ):
role = self.cbRole.currentText()
if str(role).lower() == "writer" or str(role).lower() == "artist":
return True
else:
return False
def roleChanged( self, s ):
self.updatePrimaryButton()
def getCredits( self ):
return self.cbRole.currentText(), self.leName.text()
primary = self.currentRoleCanBePrimary() and self.cbPrimary.isChecked()
return self.cbRole.currentText(), self.leName.text(), primary
def accept( self ):
if self.cbRole.currentText() == "" or self.leName.text() == "":

View File

@ -66,6 +66,13 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="cbPrimary">
<property name="text">
<string>Primary</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>

View File

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

62
exportwindow.py Normal file
View File

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

154
exportwindow.ui Normal file
View File

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

View File

@ -80,8 +80,11 @@ class FileNameParser:
# first, look for multiple "--", this mean's it's formatted differently from most:
if "--" in filename:
# the pattern seems to be that anything to left of the first "--" is the series name follow
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
filename = filename.split("--")[0]
elif "___" in filename:
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
filename = filename.split("__")[0]
# guess based on position
@ -89,8 +92,19 @@ class FileNameParser:
tmpstr = self.fixSpaces(filename)
word_list = tmpstr.split(' ')
#before we search, remove any kind of likely "of X" phrase
for i in range(0, len(word_list)-2):
if ( word_list[i].isdigit() and
word_list[i+1] == "of" and
word_list[i+2].isdigit() ):
word_list[i+1] ="XXX"
word_list[i+2] ="XXX"
# assume the last number in the filename that is under 4 digits is the issue number
for word in reversed(word_list):
for word in reversed(word_list):
if word[0] == "#":
word = word[1:]
if (
(word.isdigit() and len(word) < 4) or
(self.isPointIssue(word))
@ -126,14 +140,15 @@ class FileNameParser:
tmpstr = tmpstr.replace("#", " ")
if issue != "":
# assume that issue substr has at least on space before it
# assume that issue substr has at least one space before it
issue_str = " " + str(issue)
series = tmpstr.split(issue_str)[0]
else:
# no issue to work off of
#!!! TODO we should look for the year, and split from that
# and if that doesn't exist, remove parenthetical words
# and if that doesn't exist, remove parenthetical phrases
series = tmpstr
series = re.sub( "\(.*\)", "", tmpstr)
volume = ""
@ -161,7 +176,7 @@ class FileNameParser:
return year
def parseFilename( self, filename ):
# remove the path
filename = os.path.basename(filename)
@ -171,6 +186,13 @@ class FileNameParser:
#url decode, just in case
filename = unquote(filename)
# sometimes archives get messed up names from too many decodings
# often url encodings will break and leave "_28" and "_29" in place
# of "(" and ")" see if there are a number of these, and replace them
if filename.count("_28") > 1 and filename.count("_29") > 1:
filename = filename.replace("_28", "(")
filename = filename.replace("_29", ")")
# ----HACK
# remove the first word that word is a 3 digit number.
# some story arcs collection packs do this, but it's ugly
@ -190,3 +212,6 @@ class FileNameParser:
self.issue = self.issue.lstrip("0")
if self.issue == "":
self.issue = "0"
if self.issue[0] == ".":
self.issue = "0" + self.issue

115
filerenamer.py Normal file
View File

@ -0,0 +1,115 @@
"""
Functions for renaming files based on metadata
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
import re
from issuestring import IssueString
class FileRenamer:
def __init__( self, metadata ):
self.setMetadata( metadata )
self.setTemplate( "%series% v%volume% #%issue% (of %issuecount%) (%year%)" )
self.smart_cleanup = True
self.issue_zero_padding = 3
def setMetadata( self, metadata ):
self.metdata = metadata
def setIssueZeroPadding( self, count ):
self.issue_zero_padding = count
def setSmartCleanup( self, on ):
self.smart_cleanup = on
def setTemplate( self, template ):
self.template = template
def replaceToken( self, text, value, token ):
#helper func
def isToken( word ):
return (word[0] == "%" and word[-1:] == "%")
if value is not None:
return text.replace( token, unicode(value) )
else:
if self.smart_cleanup:
# smart cleanup means we want to remove anything appended to token if it's empty
# (e.g "#%issue%" or "v%volume%" )
# (TODO: This could fail if there is more than one token appended together, I guess)
text_list = text.split()
#special case for issuecount, remove preceding non-token word, as in "...(of %issuecount%)..."
if token == '%issuecount%':
for idx,word in enumerate( text_list ):
if token in word and not isToken(text_list[idx -1]) :
text_list[idx -1] = ""
text_list = [ x for x in text_list if token not in x ]
return " ".join( text_list )
else:
return text.replace( token, "" )
def determineName( self, filename, ext=None ):
md = self.metdata
new_name = self.template
#print u"{0}".format(md)
new_name = self.replaceToken( new_name, md.series, '%series%')
new_name = self.replaceToken( new_name, md.volume, '%volume%')
if md.issue is not None:
issue_str = u"{0}".format( IssueString(md.issue).asString(pad=self.issue_zero_padding) )
else:
issue_str = None
new_name = self.replaceToken( new_name, issue_str, '%issue%')
new_name = self.replaceToken( new_name, md.issueCount, '%issuecount%')
new_name = self.replaceToken( new_name, md.year, '%year%')
new_name = self.replaceToken( new_name, md.publisher, '%publisher%')
new_name = self.replaceToken( new_name, md.title, '%title%')
if self.smart_cleanup:
# remove empty braces,brackets, parentheses
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name )
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name )
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name )
# remove remove duplicate -, _,
new_name = re.sub("[-_]+\s+", "- ", new_name )
new_name = re.sub("(\s-)+", " -", new_name )
# remove duplicate spaces
new_name = u" ".join(new_name.split())
if ext is None:
ext = os.path.splitext( filename )[1]
new_name += ext
# some tweaks to keep various filesystems happy
new_name = new_name.replace("/", "-")
new_name = new_name.replace(":", "-")
return new_name

397
fileselectionlist.py Normal file
View File

@ -0,0 +1,397 @@
# coding=utf-8
"""
A PyQt4 widget for managing list of comic archive files
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from PyQt4.QtCore import pyqtSignal
from settings import ComicTaggerSettings
from comicarchive import ComicArchive
from genericmetadata import GenericMetadata, PageType
from options import MetaDataStyle
class FileTableWidget( QTableWidget ):
def __init__(self, parent ):
super(FileTableWidget, self).__init__(parent)
self.setColumnCount(5)
self.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
self.horizontalHeader().setStretchLastSection( True )
class FileTableWidgetItem(QTableWidgetItem):
def __lt__(self, other):
return (self.data(Qt.UserRole).toBool() <
other.data(Qt.UserRole).toBool())
class FileInfo( ):
def __init__(self, ca ):
self.ca = ca
class FileSelectionList(QWidget):
selectionChanged = pyqtSignal(QVariant)
listCleared = pyqtSignal()
fileColNum = 0
CRFlagColNum = 1
CBLFlagColNum = 2
typeColNum = 3
readonlyColNum = 4
folderColNum = 5
dataColNum = fileColNum
def __init__(self, parent , settings ):
super(FileSelectionList, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
self.settings = settings
#self.twList = FileTableWidget( self )
#gridlayout = QGridLayout( self )
#gridlayout.addWidget( self.twList )
#self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
self.currentItem = None
self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.modifiedFlag = False
selectAllAction = QAction("Select All", self)
removeAction = QAction("Remove Selected Items", self)
self.separator = QAction("",self)
self.separator.setSeparator(True)
selectAllAction.setShortcut( 'Ctrl+A' )
removeAction.setShortcut( 'Ctrl+X' )
selectAllAction.triggered.connect(self.selectAll)
removeAction.triggered.connect(self.removeSelection)
self.addAction(selectAllAction)
self.addAction(removeAction)
self.addAction(self.separator)
def addAppAction( self, action ):
self.insertAction( None , action )
def setModifiedFlag( self, modified ):
self.modifiedFlag = modified
def selectAll( self ):
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), True )
def deselectAll( self ):
self.twList.setRangeSelected( QTableWidgetSelectionRange ( 0, 0, self.twList.rowCount()-1, 5 ), False )
def removeArchiveList( self, ca_list ):
self.twList.setSortingEnabled(False)
for ca in ca_list:
for row in range(self.twList.rowCount()):
row_ca = self.getArchiveByRow( row )
if row_ca == ca:
self.twList.removeRow(row)
break
self.twList.setSortingEnabled(True)
def getArchiveByRow( self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data( Qt.UserRole ).toPyObject()
return fi.ca
def getCurrentArchive( self ):
return self.getArchiveByRow( self.twList.currentRow() )
def removeSelection( self ):
row_list = []
for item in self.twList.selectedItems():
if item.column() == 0:
row_list.append(item.row())
if len(row_list) == 0:
return
if self.twList.currentRow() in row_list:
if not self.modifiedFlagVerification( "Remove Archive",
"If you close this archive, data in the form will be lost. Are you sure?"):
return
row_list.sort()
row_list.reverse()
self.twList.currentItemChanged.disconnect( self.currentItemChangedCB )
self.twList.setSortingEnabled(False)
for i in row_list:
self.twList.removeRow(i)
self.twList.setSortingEnabled(True)
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
if self.twList.rowCount() > 0:
self.twList.selectRow(0)
else:
self.listCleared.emit()
def addPathList( self, pathlist ):
filelist = []
for p in pathlist:
# if path is a folder, walk it recursivly, and all files underneath
if 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))
# we now have a list of files to add
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
progdialog.setWindowTitle( "Adding Files" )
progdialog.setWindowModality(Qt.WindowModal)
progdialog.show()
firstAdded = None
self.twList.setSortingEnabled(False)
for idx,f in enumerate(filelist):
QCoreApplication.processEvents()
if progdialog.wasCanceled():
break
progdialog.setValue(idx)
progdialog.setLabelText(f)
QCoreApplication.processEvents()
row = self.addPathItem( f )
if firstAdded is None and row is not None:
firstAdded = row
progdialog.close()
if firstAdded is not None:
self.twList.selectRow(firstAdded)
self.twList.setSortingEnabled(True)
# Adjust column size
self.twList.resizeColumnsToContents()
self.twList.setColumnWidth(FileSelectionList.CRFlagColNum, 35)
self.twList.setColumnWidth(FileSelectionList.CBLFlagColNum, 35)
self.twList.setColumnWidth(FileSelectionList.readonlyColNum, 35)
self.twList.setColumnWidth(FileSelectionList.typeColNum, 45)
if self.twList.columnWidth(FileSelectionList.fileColNum) > 250:
self.twList.setColumnWidth(FileSelectionList.fileColNum, 250)
if self.twList.columnWidth(FileSelectionList.folderColNum ) > 200:
self.twList.setColumnWidth(FileSelectionList.folderColNum, 200)
def isListDupe( self, path ):
r = 0
while r < self.twList.rowCount():
ca = self.getArchiveByRow( r )
if ca.path == path:
return True
r = r + 1
return False
def addPathItem( self, path):
path = unicode( path )
path = os.path.abspath( path )
#print "processing", path
if self.isListDupe(path):
return None
ca = ComicArchive( path )
if self.settings.rar_exe_path != "":
ca.setExternalRarProgram( self.settings.rar_exe_path )
if ca.seemsToBeAComicArchive() :
row = self.twList.rowCount()
self.twList.insertRow( row )
fi = FileInfo( ca )
filename_item = QTableWidgetItem()
folder_item = QTableWidgetItem()
cix_item = FileTableWidgetItem()
cbi_item = FileTableWidgetItem()
readonly_item = FileTableWidgetItem()
type_item = QTableWidgetItem()
filename_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
filename_item.setData( Qt.UserRole , fi )
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
folder_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.folderColNum, folder_item)
type_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
cix_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
cix_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
cbi_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
cbi_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
readonly_item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
readonly_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.readonlyColNum, readonly_item)
self.updateRow( row )
return row
def updateRow( self, row ):
fi = self.twList.item( row, FileSelectionList.dataColNum ).data( Qt.UserRole ).toPyObject()
filename_item = self.twList.item( row, FileSelectionList.fileColNum )
folder_item = self.twList.item( row, FileSelectionList.folderColNum )
cix_item = self.twList.item( row, FileSelectionList.CRFlagColNum )
cbi_item = self.twList.item( row, FileSelectionList.CBLFlagColNum )
type_item = self.twList.item( row, FileSelectionList.typeColNum )
readonly_item = self.twList.item( row, FileSelectionList.readonlyColNum )
item_text = os.path.split(fi.ca.path)[0]
folder_item.setText( item_text )
folder_item.setData( Qt.ToolTipRole, item_text )
item_text = os.path.split(fi.ca.path)[1]
filename_item.setText( item_text )
filename_item.setData( Qt.ToolTipRole, item_text )
if fi.ca.isZip():
item_text = "ZIP"
elif fi.ca.isRar():
item_text = "RAR"
else:
item_text = ""
type_item.setText( item_text )
type_item.setData( Qt.ToolTipRole, item_text )
if fi.ca.hasCIX():
cix_item.setCheckState(Qt.Checked)
cix_item.setData(Qt.UserRole, True)
else:
cix_item.setData(Qt.UserRole, False)
cix_item.setCheckState(Qt.Unchecked)
if fi.ca.hasCBI():
cbi_item.setCheckState(Qt.Checked)
cbi_item.setData(Qt.UserRole, True)
else:
cbi_item.setData(Qt.UserRole, False)
cbi_item.setCheckState(Qt.Unchecked)
if not fi.ca.isWritable():
readonly_item.setCheckState(Qt.Checked)
readonly_item.setData(Qt.UserRole, True)
else:
readonly_item.setData(Qt.UserRole, False)
readonly_item.setCheckState(Qt.Unchecked)
# Reading these will force them into the ComicArchive's cache
fi.ca.readCIX()
fi.ca.hasCBI()
def getSelectedArchiveList( self ):
ca_list = []
for r in range( self.twList.rowCount() ):
item = self.twList.item(r, FileSelectionList.dataColNum)
if self.twList.isItemSelected(item):
fi = item.data( Qt.UserRole ).toPyObject()
ca_list.append(fi.ca)
return ca_list
def updateCurrentRow( self ):
self.updateRow( self.twList.currentRow() )
def updateSelectedRows( self ):
self.twList.setSortingEnabled(False)
for r in range( self.twList.rowCount() ):
item = self.twList.item(r, FileSelectionList.dataColNum)
if self.twList.isItemSelected(item):
self.updateRow( r )
self.twList.setSortingEnabled(True)
def currentItemChangedCB( self, curr, prev ):
new_idx = curr.row()
old_idx = -1
if prev is not None:
old_idx = prev.row()
#print "old {0} new {1}".format(old_idx, new_idx)
if old_idx == new_idx:
return
# don't allow change if modified
if prev is not None and new_idx != old_idx:
if not self.modifiedFlagVerification( "Change Archive",
"If you change archives now, data in the form will be lost. Are you sure?"):
self.twList.currentItemChanged.disconnect( self.currentItemChangedCB )
self.twList.setCurrentItem( prev )
self.twList.currentItemChanged.connect( self.currentItemChangedCB )
# Need to defer this revert selection, for some reason
QTimer.singleShot(1, self.revertSelection)
return
fi = self.twList.item( new_idx, FileSelectionList.dataColNum ).data( Qt.UserRole ).toPyObject()
self.selectionChanged.emit( QVariant(fi))
def revertSelection( self ):
self.twList.selectRow( self.twList.currentRow() )
def modifiedFlagVerification( self, title, desc):
if self.modifiedFlag:
reply = QMessageBox.question(self,
self.tr(title),
self.tr(desc),
QMessageBox.Yes, QMessageBox.No )
if reply != QMessageBox.Yes:
return False
return True
# Attempt to use a special checkbox widget in the cell.
# Couldn't figure out how to disable it with "enabled" colors
#w = QWidget()
#cb = QCheckBox(w)
#cb.setCheckState(Qt.Checked)
#layout = QHBoxLayout()
#layout.addWidget( cb )
#layout.setAlignment(Qt.AlignHCenter)
#layout.setMargin(2)
#w.setLayout(layout)
#self.twList.setCellWidget( row, 2, w )

112
fileselectionlist.ui Normal file
View File

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

View File

@ -32,7 +32,6 @@ class PageType:
Roundup = "Roundup"
Story = "Story"
Advertisment = "Advertisment"
Story = "Story"
Editorial = "Editorial"
Letters = "Letters"
Preview = "Preview"
@ -93,11 +92,19 @@ class GenericMetadata:
self.characters = None
self.teams = None
self.locations = None
self.credits = list()
self.tags = list()
self.pages = list()
# Some CoMet-only items
self.price = None
self.isVersionOf = None
self.rights = None
self.identifier = None
self.lastMark = None
self.coverImage = None
def overlay( self, new_md ):
# Overlay a metadata object on this one
# that is, when the new object has non-None
@ -125,24 +132,30 @@ class GenericMetadata:
assign( "genre", new_md.genre )
assign( "language", new_md.language )
assign( "country", new_md.country )
assign( "alternateSeries", new_md.criticalRating )
assign( "criticalRating", new_md.criticalRating )
assign( "alternateSeries", new_md.alternateSeries )
assign( "alternateNumber", new_md.alternateNumber )
assign( "alternateCount", new_md.alternateCount )
assign( "imprint", new_md.imprint )
assign( "webLink", new_md.webLink )
assign( "webLink", new_md.webLink )
assign( "format", new_md.format )
assign( "manga", new_md.manga )
assign( "blackAndWhite", new_md.blackAndWhite )
assign( "maturityRating", new_md.maturityRating )
assign( "scanInfo", new_md.scanInfo )
assign( "scanInfo", new_md.scanInfo )
assign( "storyArc", new_md.storyArc )
assign( "seriesGroup", new_md.seriesGroup )
assign( "scanInfo", new_md.scanInfo )
assign( "characters", new_md.characters )
assign( "teams", new_md.teams )
assign( "locations", new_md.locations )
assign( "comments", new_md.comments )
assign( "notes", new_md.notes )
assign( "price", new_md.price )
assign( "isVersionOf", new_md.isVersionOf )
assign( "rights", new_md.rights )
assign( "identifier", new_md.identifier )
assign( "lastMark", new_md.lastMark )
self.overlayCredits( new_md.credits )
# TODO
@ -153,7 +166,8 @@ class GenericMetadata:
# For now, go the easy route, where any overlay
# value wipes out the whole list
if len(new_md.tags) > 0:
assign( "tags", new_md.tags )
assign( "tags", new_md.tags )
if len(new_md.pages) > 0:
assign( "pages", new_md.pages )
@ -173,7 +187,35 @@ class GenericMetadata:
# otherwise, add it!
else:
self.addCredit( c['person'], c['role'], primary )
def setDefaultPageList( self, count ):
# generate a default page list, with the first page marked as the cover
for i in range(count):
page_dict = dict()
page_dict['Image'] = str(i)
if i == 0:
page_dict['Type'] = PageType.FrontCover
self.pages.append( page_dict )
def getArchivePageIndex( self, pagenum ):
# convert the displayed page number to the page index of the file in the archive
if pagenum < len( self.pages ):
return int( self.pages[pagenum]['Image'] )
else:
return 0
def getCoverPageIndexList( self ):
# return a list of archive page indices of cover pages
coverlist = []
for p in self.pages:
if 'Type' in p and p['Type'] == PageType.FrontCover:
coverlist.append( int(p['Image']))
if len(coverlist) == 0:
coverlist.append( 0 )
return coverlist
def addCredit( self, person, role, primary = False ):
credit = dict()
@ -229,6 +271,13 @@ class GenericMetadata:
add_attr_string( "webLink" )
add_attr_string( "format" )
add_attr_string( "manga" )
add_attr_string( "price" )
add_attr_string( "isVersionOf" )
add_attr_string( "rights" )
add_attr_string( "identifier" )
add_attr_string( "lastMark" )
if self.blackAndWhite:
add_attr_string( "blackAndWhite" )
add_attr_string( "maturityRating" )
@ -246,7 +295,7 @@ class GenericMetadata:
for c in self.credits:
primary = ""
if c.has_key('primary') and c['primary']:
primary == " [P]"
primary = " [P]"
add_string( "credit", c['role']+": "+c['person'] + primary)
# find the longest field name

BIN
graphics/autotag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
graphics/longbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,5 +1,6 @@
import StringIO
import sys
try:
import Image
@ -17,14 +18,25 @@ class ImageHasher(object):
if path is None and data is None:
raise IOError
elif path is not None:
self.image = Image.open(path)
else:
self.image = Image.open(StringIO.StringIO(data))
try:
if path is not None:
self.image = Image.open(path)
else:
self.image = Image.open(StringIO.StringIO(data))
except:
print "Image data seems corrupted!"
# just generate a bogus image
self.image = Image.new( "L", (1,1))
def average_hash(self):
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
try:
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
except Exception as e:
sys.exc_clear()
print "average_hash error:", e
return long(0)
pixels = list(image.getdata())
avg = sum(pixels) / len(pixels)

View File

@ -34,6 +34,7 @@ from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from imagehasher import ImageHasher
from imagefetcher import ImageFetcher, ImageFetcherException
from issuestring import IssueString
import utils
@ -71,8 +72,10 @@ class IssueIdentifier:
self.additional_metadata = GenericMetadata()
self.output_function = IssueIdentifier.defaultWriteOutput
self.callback = None
self.coverUrlCallback = None
self.search_result = self.ResultNoMatches
self.cover_page_index = 0
def setScoreMinThreshold( self, thresh ):
self.min_score_thresh = thresh
@ -95,7 +98,7 @@ class IssueIdentifier:
def setOutputFunction( self, func ):
self.output_function = func
pass
def calculateHash( self, image_data ):
if self.image_hasher == '3':
return ImageHasher( data=image_data ).dct_average_hash()
@ -105,11 +108,13 @@ class IssueIdentifier:
return ImageHasher( data=image_data ).average_hash()
def getAspectRatio( self, image_data ):
try:
im = Image.open(StringIO.StringIO(image_data))
w,h = im.size
return float(h)/float(w)
except:
return 1.5
im = Image.open(StringIO.StringIO(image_data))
w,h = im.size
return float(h)/float(w)
def cropCover( self, image_data ):
im = Image.open(StringIO.StringIO(image_data))
@ -126,6 +131,9 @@ class IssueIdentifier:
def setProgressCallback( self, cb_func ):
self.callback = cb_func
def setCoverURLCallback( self, cb_func ):
self.coverUrlCallback = cb_func
def getSearchKeys( self ):
@ -194,7 +202,7 @@ class IssueIdentifier:
@staticmethod
def defaultWriteOutput( text ):
sys.stdout.write(text)
sys.stdout.write(text.encode( errors='replace') )
sys.stdout.flush()
def log_msg( self, msg , newline=True ):
@ -217,7 +225,7 @@ class IssueIdentifier:
self.log_msg( "Sorry, but "+ opts.filename + " is not a comic archive!")
return self.match_list
cover_image_data = ca.getCoverPage()
cover_image_data = ca.getPage( self.cover_page_index )
cover_hash = self.calculateHash( cover_image_data )
#check the apect ratio
@ -248,12 +256,12 @@ class IssueIdentifier:
if keys['month'] is not None:
self.log_msg( "\tMonth : " + str(keys['month']) )
self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
comicVine = ComicVineTalker( )
#self.log_msg( ( "Searching for " + keys['series'] + "...")
self.log_msg( "Searching for {0} #{1} ...".format( keys['series'], keys['issue_number']) )
self.log_msg( u"Searching for {0} #{1} ...".format( keys['series'], keys['issue_number']) )
try:
cv_search_results = comicVine.searchForSeries( keys['series'] )
except ComicVineTalkerException:
@ -273,7 +281,7 @@ class IssueIdentifier:
date_approved = True
# remove any series that starts after the issue year
if keys['year'] is not None and keys['year'].isdigit():
if keys['year'] is not None and str(keys['year']).isdigit():
if int(keys['year']) < item['start_year']:
date_approved = False
@ -302,7 +310,6 @@ class IssueIdentifier:
if self.callback is not None:
self.callback( 0, len(series_shortlist))
# now sort the list by name length
series_shortlist.sort(key=lambda x: len(x['name']), reverse=False)
@ -315,7 +322,7 @@ class IssueIdentifier:
counter += 1
self.callback( counter, len(series_shortlist))
self.log_msg( "Fetching info for ID: {0} {1} ({2}) ...".format(
self.log_msg( u"Fetching info for ID: {0} {1} ({2}) ...".format(
series['id'],
series['name'],
series['start_year']), newline=False )
@ -328,13 +335,8 @@ class IssueIdentifier:
issue_list = cv_series_results['issues']
for issue in issue_list:
num_s = IssueString(issue['issue_number']).asString()
# format the issue number string nicely, since it's usually something like "2.00"
num_f = float(issue['issue_number'])
num_s = str( int(math.floor(num_f)) )
if math.floor(num_f) != num_f:
num_s = str( num_f )
# look for a matching issue number
if num_s == keys['issue_number']:
# found a matching issue number! now get the issue data
@ -359,6 +361,9 @@ class IssueIdentifier:
self.match_list = []
return self.match_list
if self.coverUrlCallback is not None:
self.coverUrlCallback( url_image_data )
url_image_hash = self.calculateHash( url_image_data )
score = ImageHasher.hamming_distance(cover_hash, url_image_hash)
@ -368,7 +373,7 @@ class IssueIdentifier:
score = min( score, score2 )
match = dict()
match['series'] = "{0} ({1})".format(series['name'], series['start_year'])
match['series'] = u"{0} ({1})".format(series['name'], series['start_year'])
match['distance'] = score
match['issue_number'] = num_s
match['url_image_hash'] = url_image_hash
@ -388,7 +393,6 @@ class IssueIdentifier:
break
self.log_msg( "" )
if len(self.match_list) == 0:
self.log_msg( ":-( no matches!" )
@ -420,20 +424,20 @@ class IssueIdentifier:
if len(self.match_list) == 1:
self.search_result = self.ResultOneGoodMatch
if best_score > self.min_score_thresh:
self.log_msg( "!!!! Very weak score for the cover. Maybe it's not the cover?" )
self.log_msg( "Very weak score for the cover. Maybe it's not the cover..." )
self.log_msg( "Comparing to some other archive pages now..." )
found = False
for i in range( min(5, ca.getNumberOfPages())):
for i in range( min(3, ca.getNumberOfPages())):
image_data = ca.getPage(i)
page_hash = self.calculateHash( image_data )
distance = ImageHasher.hamming_distance(page_hash, self.match_list[0]['url_image_hash'])
if distance <= self.strong_score_thresh:
self.log_msg( "Found a great match d={0} on page {1}!".format(distance, i+1) )
self.log_msg( "Found a great match (distance = {0}) on page {1}!".format(distance, i+1) )
found = True
break
elif distance < self.min_score_thresh:
self.log_msg( "Found a good match d={0} on page {1}".format(distance, i) )
self.log_msg( "Found a good match (distance = {0}) on page {1}".format(distance, i) )
found = True
self.log_msg( ".", newline=False )
self.log_msg( "" )
@ -441,7 +445,9 @@ class IssueIdentifier:
self.log_msg( "No matching pages in the issue. Bummer" )
self.search_result = self.ResultFoundMatchButBadCoverScore
self.log_msg( u"--------------------------------------------------")
print_match(self.match_list[0])
self.log_msg( u"--------------------------------------------------")
return self.match_list
elif best_score > self.min_score_thresh and len(self.match_list) > 1:
@ -456,7 +462,9 @@ class IssueIdentifier:
self.match_list.remove(item)
if len(self.match_list) == 1:
self.log_msg( u"--------------------------------------------------")
print_match(self.match_list[0])
self.log_msg( u"--------------------------------------------------")
self.search_result = self.ResultOneGoodMatch
elif len(self.match_list) == 0:
@ -466,8 +474,10 @@ class IssueIdentifier:
print
self.log_msg( "More than one likley candiate." )
self.search_result = self.ResultMultipleGoodMatches
self.log_msg( u"--------------------------------------------------")
for item in self.match_list:
print_match(item)
self.log_msg( u"--------------------------------------------------")
return self.match_list

View File

@ -28,6 +28,7 @@ from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from imagefetcher import ImageFetcher
from settings import ComicTaggerSettings
from issuestring import IssueString
class IssueSelectionWindow(QtGui.QDialog):
@ -94,12 +95,12 @@ class IssueSelectionWindow(QtGui.QDialog):
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = u"{0}".format(record['name'])
item_text = record['name']
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
if float(record['issue_number']) == float(self.issue_number):
if IssueString(record['issue_number']).asString() == IssueString(self.issue_number).asString():
self.initial_id = record['id']
row += 1

101
issuestring.py Normal file
View File

@ -0,0 +1,101 @@
"""
Class for handling the odd permutations of an 'issue number' that the comics industry throws at us
e.g.:
"12"
"12.1"
"0"
"-1"
"5AU"
"""
"""
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 utils
import math
import re
class IssueString:
def __init__(self, text):
if text is None:
self.num = None
self.suffix = ""
return
self.text = str(text)
#strip out non float-y stuff
tmp_num_str = re.sub('[^0-9.-]',"", self.text )
if tmp_num_str == "":
self.num = None
self.suffix = self.text
else:
if tmp_num_str.count(".") > 1:
#make sure it's a valid float or int.
parts = tmp_num_str.split('.')
self.num = float( parts[0] + '.' + parts[1] )
else:
self.num = float( tmp_num_str )
self.suffix = ""
parts = self.text.split(tmp_num_str)
if len( parts ) > 1 :
self.suffix = parts[1]
def asString( self, pad = 0 ):
#return the float, left side zero-padded, with suffix attached
if self.num is None:
return self.suffix
negative = self.num < 0
num_f = abs(self.num)
num_int = int( num_f )
num_s = str( num_int )
if float( num_int ) != num_f:
num_s = str( num_f )
num_s += self.suffix
# create padding
padding = ""
l = len( str(num_int))
if l < pad :
padding = "0" * (pad - l)
num_s = padding + num_s
if negative:
num_s = "-" + num_s
return num_s
def asFloat( self ):
#return the float, with no suffix
return self.num
def asInt( self ):
#return the int version of the float
if self.num is None:
return None
return int( self.num )

View File

@ -33,6 +33,7 @@ diskimage:
rm -rf $(STAGING)
mkdir $(STAGING)
cp $(TAGGER_BASE)/release_notes.txt $(STAGING)
ln -s /Applications $(STAGING)/Applications
cp -a $(APP_BUNDLE) $(STAGING)
cp $(MAC_BASE)/volume.icns $(STAGING)/.VolumeIcon.icns
SetFile -c icnC $(STAGING)/.VolumeIcon.icns

View File

@ -23,6 +23,7 @@ import getopt
import platform
import os
import ctversion
from genericmetadata import GenericMetadata
class Enum(set):
@ -34,7 +35,8 @@ class Enum(set):
class MetaDataStyle:
CBI = 0
CIX = 1
name = [ 'ComicBookLover', 'ComicRack' ]
COMET = 2
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
class Options:
@ -50,17 +52,24 @@ If no options are given, {0} will run in windowed mode
--raw With -p, will print out the raw tag block(s)
from the file
-d, --delete Deletes the tag block of specified type (via -t)
-c, --copy=SOURCE Copy the specified source tag block to destination style
specified via via -t (potentially lossy operation)
-s, --save Save out tags as specified type (via -t)
Must specify also at least -o, -p, or -m
--nooverwrite Don't modify tag block if it already exists ( relevent for -s or -c )
-n, --dryrun Don't actually modify file (only relevent for -d, -s, or -r)
-t, --type=TYPE Specify TYPE as either "CR" or "CBL", (as either
ComicRack or ComicBookLover style tags, respectivly)
-t, --type=TYPE Specify TYPE as either "CR", "CBL", or "COMET" (as either
ComicRack, ComicBookLover, or CoMet style tags, respectivly)
-f, --parsefilename Parse the filename to get some info, specifically
series name, issue number, volume, and publication
year
-i, --interactive Interactively query the user when there are multiple matches for
an online search
--nosummary Suppress the default summary after a save operation
-o, --online Search online and attempt to identify file using
existing metadata and images in archive. May be used
in conjuntion with -f and -m
--id=ID Use the issue ID when searching online. Overrides all other metadata
-m, --metadata=LIST Explicity define, as a list, some tags to be used
e.g. "series=Plastic Man , publisher=Quality Comics"
"series=Kickers^, Inc., issue=1, year=1986"
@ -71,6 +80,8 @@ If no options are given, {0} will run in windowed mode
-r, --rename Rename the file based on specified tag style.
--noabort Don't abort save operation when online match is of low confidence
-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
"""
@ -80,23 +91,32 @@ If no options are given, {0} will run in windowed mode
self.no_gui = False
self.filename = None
self.verbose = False
self.terse = False
self.metadata = None
self.print_tags = False
self.copy_tags = False
self.delete_tags = False
self.search_online = False
self.dryrun = False
self.abortOnLowConfidence = True
self.save_tags = False
self.parse_filename = False
self.show_save_summary = True
self.raw = False
self.rename_file = False
self.no_overwrite = False
self.interactive = False
self.issue_id = None
self.file_list = []
def display_help_and_quit( self, msg, code ):
def display_msg_and_quit( self, msg, code, show_help=False ):
appname = os.path.basename(sys.argv[0])
if msg is not None:
print( msg )
print self.help_text.format(appname)
if show_help:
print self.help_text.format(appname)
else:
print "For more help, run with '--help'"
sys.exit(code)
def parseMetadataFromString( self, mdstr ):
@ -159,23 +179,36 @@ If no options are given, {0} will run in windowed mode
# parse command line options
try:
opts, args = getopt.getopt( input_args,
"hpdt:fm:vonsr",
[ "help", "print", "delete", "type=", "parsefilename", "metadata=", "verbose",
"online", "dryrun", "save", "rename" , "raw", "noabort" ])
"hpdt:fm:vonsrc:i",
[ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose",
"online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite",
"interactive", "nosummary", "version", "id=" ])
except getopt.GetoptError as err:
self.display_help_and_quit( str(err), 2 )
self.display_msg_and_quit( str(err), 2 )
# process options
for o, a in opts:
if o in ("-h", "--help"):
self.display_help_and_quit( None, 0 )
self.display_msg_and_quit( None, 0, show_help=True )
if o in ("-v", "--verbose"):
self.verbose = True
if o in ("-p", "--print"):
self.print_tags = True
if o in ("-d", "--delete"):
self.delete_tags = True
if o in ("-i", "--interactive"):
self.interactive = True
if o in ("-c", "--copy"):
self.copy_tags = True
if a.lower() == "cr":
self.copy_source = MetaDataStyle.CIX
elif a.lower() == "cbl":
self.copy_source = MetaDataStyle.CBI
elif a.lower() == "comet":
self.copy_source = MetaDataStyle.COMET
else:
self.display_msg_and_quit( "Invalid copy tag source type", 1 )
if o in ("-o", "--online"):
self.search_online = True
if o in ("-n", "--dryrun"):
@ -188,43 +221,68 @@ If no options are given, {0} will run in windowed mode
self.rename_file = True
if o in ("-f", "--parsefilename"):
self.parse_filename = True
if o in ("--raw"):
if o == "--id":
self.issue_id = a
if o == "--raw":
self.raw = True
if o in ("--noabort"):
if o == "--noabort":
self.abortOnLowConfidence = False
if o == "--terse":
self.terse = True
if o == "--nosummary":
self.show_save_summary = False
if o == "--nooverwrite":
self.no_overwrite = True
if o == "--version":
print "ComicTagger version: ", ctversion.version
sys.exit(0)
if o in ("-t", "--type"):
if a.lower() == "cr":
self.data_style = MetaDataStyle.CIX
elif a.lower() == "cbl":
self.data_style = MetaDataStyle.CBI
elif a.lower() == "comet":
self.data_style = MetaDataStyle.COMET
else:
self.display_help_and_quit( "Invalid tag type", 1 )
self.display_msg_and_quit( "Invalid tag type", 1 )
if self.print_tags or self.delete_tags or self.save_tags or self.rename_file:
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file:
self.no_gui = True
count = 0
if self.print_tags: count += 1
if self.delete_tags: count += 1
if self.save_tags: count += 1
if self.copy_tags: count += 1
if self.rename_file: count += 1
if count > 1:
self.display_help_and_quit( "Must choose only one action of print, delete, save, or rename", 1 )
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
if len(args) > 0:
self.filename = args[0]
self.file_list = args
if platform.system() == "Windows":
# no globbing on windows shell, so do it for them
import glob
self.file_list = []
for item in args:
self.file_list.extend(glob.glob(item))
self.filename = self.file_list[0]
else:
self.filename = args[0]
self.file_list = args
if self.no_gui and self.filename is None:
self.display_help_and_quit( "Command requires a filename!", 1 )
self.display_msg_and_quit( "Command requires a filename!", 1 )
if self.delete_tags and self.data_style is None:
self.display_help_and_quit( "Please specify the type to delete with -t", 1 )
self.display_msg_and_quit( "Please specify the type to delete with -t", 1 )
if self.save_tags and self.data_style is None:
self.display_help_and_quit( "Please specify the type to save with -t", 1 )
self.display_msg_and_quit( "Please specify the type to save with -t", 1 )
if self.copy_tags and self.data_style is None:
self.display_msg_and_quit( "Please specify the type to copy to with -t", 1 )
if self.rename_file and self.data_style is None:
self.display_help_and_quit( "Please specify the type to use for renaming with -t", 1 )
#if self.rename_file and self.data_style is None:
# self.display_msg_and_quit( "Please specify the type to use for renaming with -t", 1 )

View File

@ -26,7 +26,7 @@ from settings import ComicTaggerSettings
class PageBrowserWindow(QtGui.QDialog):
def __init__(self, parent):
def __init__(self, parent, metadata):
super(PageBrowserWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagebrowser.ui' ), self)
@ -37,6 +37,7 @@ class PageBrowserWindow(QtGui.QDialog):
self.current_pixmap = None
self.page_count = 0
self.current_page_num = 0
self.metadata = metadata
self.btnNext.clicked.connect( self.nextPage )
self.btnPrev.clicked.connect( self.prevPage )
@ -66,7 +67,8 @@ class PageBrowserWindow(QtGui.QDialog):
self.setPage()
def setPage( self ):
image_data = self.comic_archive.getPage( self.current_page_num )
archive_page_index = self.metadata.getArchivePageIndex( self.current_page_num )
image_data = self.comic_archive.getPage( archive_page_index )
if image_data is not None:
self.setCurrentPixmap( image_data )

309
pagelisteditor.py Normal file
View File

@ -0,0 +1,309 @@
"""
A PyQt4 widget for editing the page list info
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from settings import ComicTaggerSettings
from genericmetadata import GenericMetadata, PageType
from options import MetaDataStyle
from pageloader import PageLoader
def itemMoveEvents( widget ):
class Filter(QObject):
mysignal = pyqtSignal( str )
def eventFilter(self, obj, event):
if obj == widget:
#print event.type()
if event.type() == QEvent.ChildRemoved:
#print "ChildRemoved"
self.mysignal.emit("finish")
if event.type() == QEvent.ChildAdded:
#print "ChildAdded"
self.mysignal.emit("start")
return True
return False
filter = Filter( widget )
widget.installEventFilter( filter )
return filter.mysignal
class PageListEditor(QWidget):
firstFrontCoverChanged = pyqtSignal( int )
listOrderChanged = pyqtSignal( )
modified = pyqtSignal( )
pageTypeNames = {
PageType.FrontCover: "Front Cover",
PageType.InnerCover: "Inner Cover",
PageType.Advertisment: "Advertisment",
PageType.Roundup: "Roundup",
PageType.Story: "Story",
PageType.Editorial: "Editorial",
PageType.Letters: "Letters",
PageType.Preview: "Preview",
PageType.BackCover: "Back Cover",
PageType.Other: "Other",
PageType.Deleted: "Deleted",
}
def __init__(self, parent ):
super(PageListEditor, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagelisteditor.ui' ), self )
self.comic_archive = None
self.pages_list = None
self.page_loader = None
self.resetPage()
# Add the entries to the manga combobox
self.comboBox.addItem( "", "" )
self.comboBox.addItem( self.pageTypeNames[ PageType.FrontCover], PageType.FrontCover )
self.comboBox.addItem( self.pageTypeNames[ PageType.InnerCover], PageType.InnerCover )
self.comboBox.addItem( self.pageTypeNames[ PageType.Advertisment], PageType.Advertisment )
self.comboBox.addItem( self.pageTypeNames[ PageType.Roundup], PageType.Roundup )
self.comboBox.addItem( self.pageTypeNames[ PageType.Story], PageType.Story )
self.comboBox.addItem( self.pageTypeNames[ PageType.Editorial], PageType.Editorial )
self.comboBox.addItem( self.pageTypeNames[ PageType.Letters], PageType.Letters )
self.comboBox.addItem( self.pageTypeNames[ PageType.Preview], PageType.Preview )
self.comboBox.addItem( self.pageTypeNames[ PageType.BackCover], PageType.BackCover )
self.comboBox.addItem( self.pageTypeNames[ PageType.Other], PageType.Other )
self.comboBox.addItem( self.pageTypeNames[ PageType.Deleted], PageType.Deleted )
self.listWidget.itemSelectionChanged.connect( self.changePage )
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
self.comboBox.activated.connect( self.changePageType )
self.btnUp.clicked.connect( self.moveCurrentUp )
self.btnDown.clicked.connect( self.moveCurrentDown )
self.pre_move_row = -1
self.first_front_page = None
def resetPage( self ):
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
self.setDisplayPixmap( 0, 0)
def moveCurrentUp( self ):
row = self.listWidget.currentRow()
if row > 0:
item = self.listWidget.takeItem ( row )
self.listWidget.insertItem( row-1, item )
self.listWidget.setCurrentRow( row-1 )
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def moveCurrentDown( self ):
row = self.listWidget.currentRow()
if row < self.listWidget.count()-1:
item = self.listWidget.takeItem ( row )
self.listWidget.insertItem( row+1, item )
self.listWidget.setCurrentRow( row+1 )
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def itemMoveEvent(self, s):
#print "move event: ", s, self.listWidget.currentRow()
if s == "start":
self.pre_move_row = self.listWidget.currentRow()
if s == "finish":
if self.pre_move_row != self.listWidget.currentRow():
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def changePageType( self , i):
new_type = self.comboBox.itemData(i).toString()
if self.getCurrentPageType() != new_type:
self.setCurrentPageType( new_type )
self.emitFrontCoverChange()
self.modified.emit()
def changePage( self ):
row = self.listWidget.currentRow()
pagetype = self.getCurrentPageType()
i = self.comboBox.findData( pagetype )
self.comboBox.setCurrentIndex( i )
#idx = int(str (self.listWidget.item( row ).text()))
idx = int(self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]['Image'])
if self.page_loader is not None:
self.page_loader.abandoned = True
if self.comic_archive is not None:
self.page_loader = PageLoader( self.comic_archive, idx )
self.page_loader.loadComplete.connect( self.actualChangePageImage )
self.page_loader.start()
def actualChangePageImage( self, img ):
self.page_loader = None
self.current_pixmap = QPixmap(img)
self.setDisplayPixmap( 0, 0)
def getFirstFrontCover( self ):
frontCover = 0
for i in range( self.listWidget.count() ):
item = self.listWidget.item( i )
page_dict = item.data(Qt.UserRole).toPyObject()[0]
if 'Type' in page_dict and page_dict['Type'] == PageType.FrontCover:
frontCover = int(page_dict['Image'])
break
return frontCover
def getCurrentPageType( self ):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]
if 'Type' in page_dict:
return page_dict['Type']
else:
return ""
def setCurrentPageType( self, t ):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]
if t == "":
if 'Type' in page_dict:
del(page_dict['Type'])
else:
page_dict['Type'] = str(t)
item = self.listWidget.item( row )
# wrap the dict in a tuple to keep from being converted to QStrings
item.setData(Qt.UserRole, (page_dict,) )
item.setText( self.listEntryText( page_dict ) )
def resizeEvent( self, resize_event ):
if self.current_pixmap is not None:
delta_w = resize_event.size().width() - resize_event.oldSize().width()
delta_h = resize_event.size().height() - resize_event.oldSize().height()
self.setDisplayPixmap( delta_w , delta_h )
def setDisplayPixmap( self, delta_w , delta_h ):
# the deltas let us know what the new width and height of the label will be
new_h = self.frame.height() + delta_h
new_w = self.frame.width() + delta_w
frame_w = new_w
frame_h = new_h
new_h -= 4
new_w -= 4
if new_h < 0:
new_h = 0;
if new_w < 0:
new_w = 0;
# scale the pixmap to fit in the frame
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio)
self.label.setPixmap( scaled_pixmap )
# ,pve and resize the label to be centered in the fame
img_w = scaled_pixmap.width()
img_h = scaled_pixmap.height()
self.label.resize( img_w, img_h )
self.label.move( (frame_w - img_w)/2, (frame_h - img_h)/2 )
def setData( self, comic_archive, pages_list ):
self.comic_archive = comic_archive
self.pages_list = pages_list
self.listWidget.itemSelectionChanged.disconnect( self.changePage )
self.listWidget.clear()
for p in pages_list:
item = QListWidgetItem( self.listEntryText( p ) )
# wrap the dict in a tuple to keep from being converted to QStrings
item.setData(Qt.UserRole, (p, ))
self.listWidget.addItem( item )
self.first_front_page = self.getFirstFrontCover()
self.listWidget.itemSelectionChanged.connect( self.changePage )
self.listWidget.setCurrentRow ( 0 )
def listEntryText(self, page_dict):
text = page_dict['Image']
if 'Type' in page_dict:
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
return text
def getPageList( self ):
page_list = []
for i in range( self.listWidget.count() ):
item = self.listWidget.item( i )
page_list.append( item.data(Qt.UserRole).toPyObject()[0] )
return page_list
def emitFrontCoverChange( self ):
if self.first_front_page != self.getFirstFrontCover():
self.first_front_page = self.getFirstFrontCover()
self.firstFrontCoverChanged.emit( self.first_front_page )
def setMetadataStyle( self, data_style ):
# depending on the current data style, certain fields are disabled
inactive_color = QColor(255, 170, 150)
active_palette = self.label.palette()
inactive_palette3 = self.label.palette()
inactive_palette3.setColor(QPalette.Base, inactive_color)
if data_style == MetaDataStyle.CIX:
self.btnUp.setEnabled( True )
self.btnDown.setEnabled( True )
self.comboBox.setEnabled( True )
self.listWidget.setEnabled( True )
self.listWidget.setPalette(active_palette)
elif data_style == MetaDataStyle.CBI:
self.btnUp.setEnabled( False )
self.btnDown.setEnabled( False )
self.comboBox.setEnabled( False )
self.listWidget.setEnabled( False )
self.listWidget.setPalette(inactive_palette3)
elif data_style == MetaDataStyle.CoMet:
pass
def showEvent( self, event ):
# make sure to adjust the size and pos of the pixmap based on frame size
self.setDisplayPixmap( 0,0 )

135
pagelisteditor.ui Normal file
View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>pageListEditor</class>
<widget class="QWidget" name="pageListEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>527</width>
<height>323</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="listWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="btnUp">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>^</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnDown">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>v</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Page Type:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox"/>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>90</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>151</width>
<height>141</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

77
pageloader.py Normal file
View File

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

View File

@ -32,7 +32,12 @@ class IDProgressWindow(QtGui.QDialog):
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'progresswindow.ui' ), self)
# we can't specify relative font sizes in the UI designer, so
# make font for scroll window a smidge smaller
f = self.textEdit.font()
if f.pointSize() > 10:
f.setPointSize( f.pointSize() - 2 )
self.textEdit.setFont( f )

View File

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

View File

@ -1,4 +1,78 @@
30-Nov-2012
0.9.0-beta
---------------------------------
1.0.1-beta - 24-Jan-2013
---------------------------------
Bug Fix:
Fixed an issue where unicode strings can't be printed to OS Console
---------------------------------
1.0.0-beta - 23-Jan-2013
---------------------------------
Version 1! New multi-file processing in GUI!
GUI Changes:
Open multiple files and/or folders via drag/drop or file dialog
File management list for easy viewing and selection
Batch tag remove
Batch export as zip
Batch rename
Batch tag copy
Batch auto-tag (automatic identification and save!)
---------------------------------
0.9.5-beta - 16-Jan-2013
---------------------------------
Changes:
Added CLI option to search by comicvine issue ID
Some image loading optimizations
Bug Fix: Some CBL fields that should have been ints were written as strings
---------------------------------
0.9.4-beta - 7-Jan-2013
---------------------------------
Changes:
Better handling of non-ascii characters in filenames and data
Add CBL Transform to copy Web Link and Notes to comments
Minor bug fixes
---------------------------------
0.9.3-beta - 19-Dec-2012
---------------------------------
Changes:
File rename in GUI
Setting for file rename
Option to use series start year as volume
Added "CBL Transform" to handle primary credits copying data into the generic tags field
Bug Fix: unicode characters in credits caused crash
Bug Fix: bad or non-image data in file caused crash
Note:
The user should clear the cache and delete the existing settings when first running this version.
---------------------------------
0.9.2-beta - 13-Dec-2012
---------------------------------
Page List/Type editing in GUI
File globbing for windows CLI (i.e. use of wildcards like '*.cbz')
Fixed RAR writing bug on windows
Minor bug and crash fixes
---------------------------------
0.9.1-beta - 07-Dec-2012
---------------------------------
Export as ZIP Archive
Added help menu option for websites
Added Primary Credit Flag editing
Menu enhancements
CLI Enhancements:
Interactive selection of matches
Tag copy
Better output
CoMet support
Minor bug and crash fixes
---------------------------------
0.9.0-beta - 30-Nov-2012
---------------------------------
Initial beta release

153
renamewindow.py Normal file
View File

@ -0,0 +1,153 @@
"""
A PyQT4 dialog to confirm rename
"""
"""
Copyright 2012 Anthony Beville
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
from settingswindow import SettingsWindow
from filerenamer import FileRenamer
from options import MetaDataStyle
import os
import utils
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)
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
self.settings = settings
self.comic_archive_list = comic_archive_list
self.data_style = data_style
self.btnSettings.clicked.connect( self.modifySettings )
self.configRenamer()
self.doPreview()
def configRenamer( self ):
self.renamer = FileRenamer( None )
self.renamer.setTemplate( self.settings.rename_template )
self.renamer.setIssueZeroPadding( self.settings.rename_issue_number_padding )
self.renamer.setSmartCleanup( self.settings.rename_use_smart_string_cleanup )
def doPreview( self ):
self.rename_list = []
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
self.twList.setSortingEnabled(False)
for ca in self.comic_archive_list:
new_ext = None # default
if self.settings.rename_extension_based_on_archive:
if ca.isZip():
new_ext = ".cbz"
elif ca.isRar():
new_ext = ".cbr"
md = ca.readMetadata(self.data_style)
if md.isEmpty:
md = ca.metadataFromFilename()
self.renamer.setMetadata( md )
new_name = self.renamer.determineName( ca.path, ext=new_ext )
row = self.twList.rowCount()
self.twList.insertRow( row )
folder_item = QtGui.QTableWidgetItem()
old_name_item = QtGui.QTableWidgetItem()
new_name_item = QtGui.QTableWidgetItem()
item_text = os.path.split(ca.path)[0]
folder_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, folder_item)
folder_item.setText( item_text )
folder_item.setData( QtCore.Qt.ToolTipRole, item_text )
item_text = os.path.split(ca.path)[1]
old_name_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, old_name_item)
old_name_item.setText( item_text )
old_name_item.setData( QtCore.Qt.ToolTipRole, item_text )
new_name_item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, new_name_item)
new_name_item.setText( new_name )
new_name_item.setData( QtCore.Qt.ToolTipRole, new_name )
dict_item = dict()
dict_item['archive'] = ca
dict_item['new_name'] = new_name
self.rename_list.append( dict_item)
# Adjust column sizes
self.twList.setVisible( False )
self.twList.resizeColumnsToContents()
self.twList.setVisible( True )
if self.twList.columnWidth(0) > 200:
self.twList.setColumnWidth(0, 200)
self.twList.setSortingEnabled(True)
def modifySettings( self ):
settingswin = SettingsWindow( self, self.settings )
settingswin.setModal(True)
settingswin.showRenameTab()
settingswin.exec_()
if settingswin.result():
self.configRenamer()
self.doPreview()
def accept( self ):
progdialog = QtGui.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
progdialog.setWindowTitle( "Renaming Archives" )
progdialog.setWindowModality(QtCore.Qt.WindowModal)
progdialog.show()
for idx,item in enumerate(self.rename_list):
QtCore.QCoreApplication.processEvents()
if progdialog.wasCanceled():
break
progdialog.setValue(idx)
idx += 1
progdialog.setLabelText( item['new_name'] )
if item['new_name'] == os.path.basename( item['archive'].path ):
print item['new_name'] , "Filename is already good!"
continue
if not item['archive'].isWritable(check_rar_status=False):
continue
folder = os.path.dirname( os.path.abspath( item['archive'].path ) )
new_abs_path = utils.unique_file( os.path.join( folder, item['new_name'] ) )
os.rename( item['archive'].path, new_abs_path)
item['archive'].rename( new_abs_path )
progdialog.close()
QtGui.QDialog.accept(self)

116
renamewindow.ui Normal file
View File

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

View File

@ -51,12 +51,15 @@ class ComicTaggerSettings:
self.allow_cbi_in_rar = True
# automatic settings
self.last_selected_data_style = 0
self.last_selected_save_data_style = 0
self.last_selected_load_data_style = 0
self.last_opened_folder = ""
self.last_main_window_width = 0
self.last_main_window_height = 0
self.last_main_window_x = 0
self.last_main_window_y = 0
self.last_form_side_width = -1
self.last_list_side_width = -1
# identifier settings
self.id_length_delta_thresh = 5
@ -65,7 +68,27 @@ class ComicTaggerSettings:
# Show/ask dialog flags
self.ask_about_cbi_in_rar = True
self.show_disclaimer = True
# Comic Vine settings
self.use_series_start_as_volume = False
# CBL Tranform settings
self.assume_lone_credit_is_primary = False
self.copy_characters_to_tags = False
self.copy_teams_to_tags = False
self.copy_locations_to_tags = False
self.copy_notes_to_comments = False
self.copy_weblink_to_comments = False
self.apply_cbl_transform_on_cv_import = False
self.apply_cbl_transform_on_bulk_operation = False
# Rename settings
self.rename_template = "%series% #%issue% (%year%)"
self.rename_issue_number_padding = 3
self.rename_use_smart_string_cleanup = True
self.rename_extension_based_on_archive = True
def __init__(self):
self.settings_file = ""
@ -119,9 +142,11 @@ class ComicTaggerSettings:
self.rar_exe_path = self.config.get( 'settings', 'rar_exe_path' )
self.unrar_exe_path = self.config.get( 'settings', 'unrar_exe_path' )
if self.config.has_option('auto', 'last_selected_data_style'):
self.last_selected_data_style = self.config.getint( 'auto', 'last_selected_data_style' )
if self.config.has_option('auto', 'last_selected_load_data_style'):
self.last_selected_load_data_style = self.config.getint( 'auto', 'last_selected_load_data_style' )
if self.config.has_option('auto', 'last_selected_save_data_style'):
self.last_selected_save_data_style = self.config.getint( 'auto', 'last_selected_save_data_style' )
if self.config.has_option('auto', 'last_opened_folder'):
self.last_opened_folder = self.config.get( 'auto', 'last_opened_folder' )
if self.config.has_option('auto', 'last_main_window_width'):
@ -132,17 +157,50 @@ class ComicTaggerSettings:
self.last_main_window_x = self.config.getint( 'auto', 'last_main_window_x' )
if self.config.has_option('auto', 'last_main_window_y'):
self.last_main_window_y = self.config.getint( 'auto', 'last_main_window_y' )
if self.config.has_option('auto', 'last_form_side_width'):
self.last_form_side_width = self.config.getint( 'auto', 'last_form_side_width' )
if self.config.has_option('auto', 'last_list_side_width'):
self.last_list_side_width = self.config.getint( 'auto', 'last_list_side_width' )
if self.config.has_option('identifier', 'id_length_delta_thresh'):
self.id_length_delta_thresh = self.config.getint( 'identifier', 'id_length_delta_thresh' )
if self.config.has_option('identifier', 'id_publisher_blacklist'):
self.id_publisher_blacklist = self.config.get( 'identifier', 'id_publisher_blacklist' )
self.id_publisher_blacklist = self.config.get( 'identifier', 'id_publisher_blacklist' )
if self.config.has_option('dialogflags', 'ask_about_cbi_in_rar'):
self.ask_about_cbi_in_rar = self.config.getboolean( 'dialogflags', 'ask_about_cbi_in_rar' )
if self.config.has_option('dialogflags', 'show_disclaimer'):
self.show_disclaimer = self.config.getboolean( 'dialogflags', 'show_disclaimer' )
if self.config.has_option('comicvine', 'use_series_start_as_volume'):
self.use_series_start_as_volume = self.config.getboolean( 'comicvine', 'use_series_start_as_volume' )
if self.config.has_option('cbl_transform', 'assume_lone_credit_is_primary'):
self.assume_lone_credit_is_primary = self.config.getboolean( 'cbl_transform', 'assume_lone_credit_is_primary' )
if self.config.has_option('cbl_transform', 'copy_characters_to_tags'):
self.copy_characters_to_tags = self.config.getboolean( 'cbl_transform', 'copy_characters_to_tags' )
if self.config.has_option('cbl_transform', 'copy_teams_to_tags'):
self.copy_teams_to_tags = self.config.getboolean( 'cbl_transform', 'copy_teams_to_tags' )
if self.config.has_option('cbl_transform', 'copy_locations_to_tags'):
self.copy_locations_to_tags = self.config.getboolean( 'cbl_transform', 'copy_locations_to_tags' )
if self.config.has_option('cbl_transform', 'copy_notes_to_comments'):
self.copy_notes_to_comments = self.config.getboolean( 'cbl_transform', 'copy_notes_to_comments' )
if self.config.has_option('cbl_transform', 'copy_weblink_to_comments'):
self.copy_weblink_to_comments = self.config.getboolean( 'cbl_transform', 'copy_weblink_to_comments' )
if self.config.has_option('cbl_transform', 'apply_cbl_transform_on_cv_import'):
self.apply_cbl_transform_on_cv_import = self.config.getboolean( 'cbl_transform', 'apply_cbl_transform_on_cv_import' )
if self.config.has_option('cbl_transform', 'apply_cbl_transform_on_bulk_operation'):
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean( 'cbl_transform', 'apply_cbl_transform_on_bulk_operation' )
if self.config.has_option('rename', 'rename_template'):
self.rename_template = self.config.get( 'rename', 'rename_template' )
if self.config.has_option('rename', 'rename_issue_number_padding'):
self.rename_issue_number_padding = self.config.getint( 'rename', 'rename_issue_number_padding' )
if self.config.has_option('rename', 'rename_use_smart_string_cleanup'):
self.rename_use_smart_string_cleanup = self.config.getboolean( 'rename', 'rename_use_smart_string_cleanup' )
if self.config.has_option('rename', 'rename_extension_based_on_archive'):
self.rename_extension_based_on_archive = self.config.getboolean( 'rename', 'rename_extension_based_on_archive' )
def save( self ):
if not self.config.has_section( 'settings' ):
@ -154,12 +212,15 @@ class ComicTaggerSettings:
if not self.config.has_section( 'auto' ):
self.config.add_section( 'auto' )
self.config.set( 'auto', 'last_selected_data_style', self.last_selected_data_style )
self.config.set( 'auto', 'last_selected_load_data_style', self.last_selected_load_data_style )
self.config.set( 'auto', 'last_selected_save_data_style', self.last_selected_save_data_style )
self.config.set( 'auto', 'last_opened_folder', self.last_opened_folder )
self.config.set( 'auto', 'last_main_window_width', self.last_main_window_width )
self.config.set( 'auto', 'last_main_window_height', self.last_main_window_height )
self.config.set( 'auto', 'last_main_window_x', self.last_main_window_x )
self.config.set( 'auto', 'last_main_window_y', self.last_main_window_y )
self.config.set( 'auto', 'last_form_side_width', self.last_form_side_width )
self.config.set( 'auto', 'last_list_side_width', self.last_list_side_width )
if not self.config.has_section( 'identifier' ):
self.config.add_section( 'identifier' )
@ -173,8 +234,31 @@ class ComicTaggerSettings:
self.config.set( 'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar )
self.config.set( 'dialogflags', 'show_disclaimer', self.show_disclaimer )
if not self.config.has_section( 'comicvine' ):
self.config.add_section( 'comicvine' )
self.config.set( 'comicvine', 'use_series_start_as_volume', self.use_series_start_as_volume )
if not self.config.has_section( 'cbl_transform' ):
self.config.add_section( 'cbl_transform' )
self.config.set( 'cbl_transform', 'assume_lone_credit_is_primary', self.assume_lone_credit_is_primary )
self.config.set( 'cbl_transform', 'copy_characters_to_tags', self.copy_characters_to_tags )
self.config.set( 'cbl_transform', 'copy_teams_to_tags', self.copy_teams_to_tags )
self.config.set( 'cbl_transform', 'copy_locations_to_tags', self.copy_locations_to_tags )
self.config.set( 'cbl_transform', 'copy_notes_to_comments', self.copy_notes_to_comments )
self.config.set( 'cbl_transform', 'copy_weblink_to_comments', self.copy_weblink_to_comments )
self.config.set( 'cbl_transform', 'apply_cbl_transform_on_cv_import', self.apply_cbl_transform_on_cv_import )
self.config.set( 'cbl_transform', 'apply_cbl_transform_on_bulk_operation', self.apply_cbl_transform_on_bulk_operation )
if not self.config.has_section( 'rename' ):
self.config.add_section( 'rename' )
self.config.set( 'rename', 'rename_template', self.rename_template )
self.config.set( 'rename', 'rename_issue_number_padding', self.rename_issue_number_padding )
self.config.set( 'rename', 'rename_use_smart_string_cleanup', self.rename_use_smart_string_cleanup )
self.config.set( 'rename', 'rename_extension_based_on_archive', self.rename_extension_based_on_archive )
with open( self.settings_file, 'wb') as configfile:
self.config.write(configfile)

View File

@ -96,6 +96,12 @@ class SettingsWindow(QtGui.QDialog):
)
self.tePublisherBlacklist.setToolTip(pblTip)
validator = QtGui.QIntValidator(1, 4, self)
self.leIssueNumPadding.setValidator(validator)
validator = QtGui.QIntValidator(0, 99, self)
self.leNameLengthDeltaThresh.setValidator(validator)
self.settingsToForm()
self.btnBrowseRar.clicked.connect(self.selectRar)
@ -110,7 +116,35 @@ class SettingsWindow(QtGui.QDialog):
self.leUnrarExePath.setText( self.settings.unrar_exe_path )
self.leNameLengthDeltaThresh.setText( str(self.settings.id_length_delta_thresh) )
self.tePublisherBlacklist.setPlainText( self.settings.id_publisher_blacklist )
if self.settings.use_series_start_as_volume:
self.cbxUseSeriesStartAsVolume.setCheckState( QtCore.Qt.Checked)
if self.settings.assume_lone_credit_is_primary:
self.cbxAssumeLoneCreditIsPrimary.setCheckState( QtCore.Qt.Checked)
if self.settings.copy_characters_to_tags:
self.cbxCopyCharactersToTags.setCheckState( QtCore.Qt.Checked)
if self.settings.copy_teams_to_tags:
self.cbxCopyTeamsToTags.setCheckState( QtCore.Qt.Checked)
if self.settings.copy_locations_to_tags:
self.cbxCopyLocationsToTags.setCheckState( QtCore.Qt.Checked)
if self.settings.copy_notes_to_comments:
self.cbxCopyNotesToComments.setCheckState( QtCore.Qt.Checked)
if self.settings.copy_weblink_to_comments:
self.cbxCopyWebLinkToComments.setCheckState( QtCore.Qt.Checked)
if self.settings.apply_cbl_transform_on_cv_import:
self.cbxApplyCBLTransformOnCVIMport.setCheckState( QtCore.Qt.Checked)
if self.settings.apply_cbl_transform_on_bulk_operation:
self.cbxApplyCBLTransformOnBatchOperation.setCheckState( QtCore.Qt.Checked)
self.leRenameTemplate.setText( self.settings.rename_template )
self.leIssueNumPadding.setText( str(self.settings.rename_issue_number_padding) )
if self.settings.rename_use_smart_string_cleanup:
self.cbxSmartCleanup.setCheckState( QtCore.Qt.Checked )
if self.settings.rename_extension_based_on_archive:
self.cbxChangeExtension.setCheckState( QtCore.Qt.Checked )
def accept( self ):
# Copy values from form to settings and save
@ -121,12 +155,30 @@ class SettingsWindow(QtGui.QDialog):
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
QtGui.QMessageBox.information(self,"Settings", "The Name Length Delta Threshold must be a number!")
return
self.leNameLengthDeltaThresh.setText("0")
if not str(self.leIssueNumPadding.text()).isdigit():
self.leIssueNumPadding.setText("0")
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
self.settings.id_publisher_blacklist = str(self.tePublisherBlacklist.toPlainText())
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
self.settings.rename_template = str(self.leRenameTemplate.text())
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.settings.save()
QtGui.QDialog.accept(self)
@ -170,5 +222,6 @@ class SettingsWindow(QtGui.QDialog):
fileList = dialog.selectedFiles()
control.setText( str(fileList[0]) )
def showRenameTab( self ):
self.tabWidget.setCurrentIndex(4)

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>416</height>
<width>674</width>
<height>428</height>
</rect>
</property>
<property name="windowTitle">
@ -301,6 +301,199 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Comic Vine</string>
</attribute>
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
<property name="geometry">
<rect>
<x>30</x>
<y>30</y>
<width>240</width>
<height>25</height>
</rect>
</property>
<property name="text">
<string>Use Series Start Date as Volume</string>
</property>
</widget>
</widget>
<widget class="QWidget" name="tab_4">
<attribute name="title">
<string>CBL</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0">
<widget class="QCheckBox" name="cbxApplyCBLTransformOnCVIMport">
<property name="text">
<string>Apply CBL Transforms on ComicVine Import</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="cbxApplyCBLTransformOnBatchOperation">
<property name="text">
<string>Apply CBL Transforms on Batch Copy Operations to CBL Tags</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>CBL Transforms</string>
</property>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>11</x>
<y>21</y>
<width>246</width>
<height>182</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QCheckBox" name="cbxAssumeLoneCreditIsPrimary">
<property name="text">
<string>Assume Lone Credit Is Primary</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="cbxCopyCharactersToTags">
<property name="text">
<string>Copy Characters to Generic Tags</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="cbxCopyTeamsToTags">
<property name="text">
<string>Copy Teams to Generic Tags</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="cbxCopyLocationsToTags">
<property name="text">
<string>Copy Locations to Generic Tags</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="cbxCopyNotesToComments">
<property name="text">
<string>Copy Notes to Comments</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="cbxCopyWebLinkToComments">
<property name="text">
<string>Copy Web Link to Comments</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_5">
<attribute name="title">
<string>Rename</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Template:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leRenameTemplate">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The template for the new filename. Accepts the following variables:&lt;/p&gt;&lt;p&gt;%series%&lt;br/&gt;%issue%&lt;br/&gt;%volume%&lt;br/&gt;%issuecount%&lt;br/&gt;%year%&lt;br/&gt;%publisher%&lt;br/&gt;%title%&lt;/p&gt;&lt;p&gt;Examples:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% %issue% (%year%)&lt;/span&gt;&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% #%issue% - %title%&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Issue # Zero Padding</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="leIssueNumPadding">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Issue # Zero Padding&lt;/span&gt; dictates if the issue number should be padded on left with zeros. A value of 2, for example, means that the number will always be at least two digits.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="cbxSmartCleanup">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&amp;quot;Smart Text Cleanup&amp;quot; &lt;/span&gt;will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Use Smart Text Cleanup (Experimental)</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="cbxChangeExtension">
<property name="text">
<string>Change Extension Based On Archive Type</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

150
todo.txt
View File

@ -1,5 +1,71 @@
-----------------------------------------------------
Config Mgmt
Features
-----------------------------------------------------
Multi-file:
Batch Functions:
Auto-Tag
Interactive dialog at end
Manually change cover image on left??
Rename
check-box for rows?
manual edit the preview?
-----------------------------------------------------
Bugs
spider-man 678 .... ascii print problem. grrrr
-----------------------------------------------------
RAR Password -- childrens crusade 3
-----------------------------------------------------
Big Future Features
-----------------------------------------------------
Scrape alternate Covers from ComicVine issue pages
GCD scraper or DB reader
Auto search:
Searching w/o issue #
Form Mode: Single vs Batch
-----------------------------------------------------
Small(er) Future Feature
-----------------------------------------------------
Parse out the rest of the scan info from filename
Style sheets for windows/mac/linux
CLI
explicit metadata settings option format
-- figure out how to add CBI "tags"
-- delete CBI "tags"
-- set primary credit flags
-- set frontcover and others?
Archive function to detect tag blocks out of sync
Settings
Add setting to dis-allow writing CBI to RAR
Google App engine to store hashes
Content Hashes, Image hashes, who knows?
Filename parsing:
Rework how series name is separated from issue
Support marvel's "AU" issues...
Mostly done, gotta wait and see what CV does
Internal GenericMetadata - Make Characters, Genre into lists?
-----------------------------------------------------
Config Mgmt check list
-----------------------------------------------------
Release Process
@ -12,88 +78,8 @@ Release Process
Make zip on Mac or Linux
Tag the repository
Upload packages
Announce on Forum and Main Page
-----------------------------------------------------
Features
-----------------------------------------------------
-----------------------------------------------------
Bugs
-----------------------------------------------------
-----------------------------------------------------
Future
-----------------------------------------------------
Add license info to About Dialog
Add CoMet Support
Support marvel's "AU" issues...
Style sheets for windows/mac/linux
File rename
-Dialog??
formatting with missing pieces.
TaggerWindow entry fields
Special tabbed Dialog needed for:
Pages Info - maybe a custom painted widget
CLI
explicit metadata settings option format
-- figure out how to add tags
-- delete tags
write a log for multiple file processing
override abort on low confidence flag
interactive for choices option?
--- or defer choices to end, by keeping special log of match option for each file
Scrape alternate Covers from ComicVine issue pages
GCD scraper or DB reader
Overlay tags, maybe more, on online search
GUI to handle mutliple files or folders
Auto search:
Searching w/o issue #?
Wizard for converting between tag styles
App option to covert RAR to ZIP
Archive function to detect tag blocks out of sync
app tour?
Settings
Add setting to dis-allow writing CBI to RAR
Overwrite or overlay
Google App engine to store hashes
Content Hashes, Image hashes, who knows?
Support primary credit flag editing
Filename parsing:
Rework how series name is separated from issue
----------------------------------------------
COMIC RACK Questions
Missing from XML as enterable in ComicRack:
Main Character or Team
Review
User Rating
Some that seem library only:
"Series Complete"
Tags
Proposed Values
Community Rating

View File

@ -85,7 +85,7 @@ class IdentifyThread( QtCore.QThread):
class VolumeSelectionWindow(QtGui.QDialog):
def __init__(self, parent, series_name, issue_number, year, comic_archive, settings, autoselect=False):
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)
@ -97,7 +97,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.volume_id = 0
self.comic_archive = comic_archive
self.immediate_autoselect = autoselect
self.cover_index_list = cover_index_list
self.cv_search_results = None
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
@ -105,9 +107,21 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.btnIssues.clicked.connect(self.showIssues)
self.btnAutoSelect.clicked.connect(self.autoSelect)
self.updateButtons()
self.performQuery()
self.twList.selectRow(0)
def updateButtons( self ):
if self.cv_search_results is not None and len(self.cv_search_results) > 0:
enabled = True
else:
enabled = False
self.btnRequery.setEnabled( enabled )
self.btnIssues.setEnabled( enabled )
self.btnAutoSelect.setEnabled( enabled )
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled( enabled )
def requery( self, ):
self.performQuery( refresh=True )
self.twList.selectRow(0)
@ -136,6 +150,8 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.ii.setAdditionalMetadata( md )
self.ii.onlyUseAdditionalMetaData = True
print self.cover_index_list
self.ii.cover_page_index = int(self.cover_index_list[0])
self.id_thread = IdentifyThread( self.ii )
self.id_thread.identifyComplete.connect( self.identifyComplete )
@ -147,7 +163,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.iddialog.exec_()
def logIDOutput( self, text ):
print text,
print unicode(text),
self.iddialog.textEdit.ensureCursorVisible()
self.iddialog.textEdit.insertPlainText(text)
@ -216,7 +232,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
for record in self.cv_search_results:
if record['id'] == self.volume_id:
title = record['name']
title += " (" + str(record['start_year']) + ")"
title += " (" + unicode(record['start_year']) + ")"
title += " - "
break
@ -274,8 +290,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to search for series!"))
return
self.cv_search_results = self.search_thread.cv_search_results
self.cv_search_results = self.search_thread.cv_search_results
self.updateButtons()
self.twList.setSortingEnabled(False)
while self.twList.rowCount() > 0:
@ -285,8 +302,8 @@ class VolumeSelectionWindow(QtGui.QDialog):
for record in self.cv_search_results:
self.twList.insertRow(row)
item_text = record['name']
item = QtGui.QTableWidgetItem(item_text)
item_text = record['name']
item = QtGui.QTableWidgetItem( item_text )
item.setData( QtCore.Qt.UserRole ,record['id'])
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
@ -316,7 +333,11 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.twList.selectRow(0)
self.twList.resizeColumnsToContents()
if self.immediate_autoselect:
if len( self.cv_search_results ) == 0:
QtCore.QCoreApplication.processEvents()
QtGui.QMessageBox.information(self,"Search Result", "No matches found!")
if self.immediate_autoselect and len( self.cv_search_results ) > 0:
# defer the immediate autoselect so this dialog has time to pop up
QtCore.QCoreApplication.processEvents()
QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)