Compare commits

...

81 Commits

Author SHA1 Message Date
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
37 changed files with 4193 additions and 1399 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)"

89
cbltransformer.py Normal file
View File

@ -0,0 +1,89 @@
"""
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 and self.metadata.notes not in self.metadata.comments:
self.metadata.comments += "\n\n" + self.metadata.notes
if self.settings.copy_weblink_to_comments:
if self.metadata.webLink is not None and self.metadata.webLink not in self.metadata.comments:
self.metadata.comments += "\n\n" + 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
@ -92,7 +100,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,6 +187,26 @@ 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 )
zout.writestr( fname, data )
zout.close()
#preserve the old comment
comment = otherArchive.getArchiveComment()
if comment is not None:
if not self.writeZipComment( self.path, comment ):
return False
except:
return False
else:
return True
#------------------------------------------
# RAR implementation
@ -216,8 +243,10 @@ 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)
@ -249,13 +278,17 @@ 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)
@ -385,7 +418,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 +436,14 @@ 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.comet_filename = None
self.page_count = None
self.page_list = None
def setExternalRarProgram( self, rar_exe_path ):
if self.isRar():
self.archiver.rar_exe_path = rar_exe_path
@ -470,15 +513,23 @@ 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 )
self.resetCache()
return retcode
def hasMetadata( self, style ):
@ -486,20 +537,22 @@ 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()
self.resetCache()
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 +576,38 @@ 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()
md = GenericMetadata()
else:
md = ComicBookInfo().metadataFromString( raw_cbi )
return ComicBookInfo().metadataFromString( raw_cbi )
md.setDefaultPageList( self.getNumberOfPages() )
return md
def readRawCBI( self ):
if ( not self.hasCBI() ):
@ -555,8 +615,20 @@ 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 ):
self.applyArchiveInfoToMetadata( metadata )
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
return self.archiver.setArchiveComment( cbi_string )
@ -566,13 +638,23 @@ class ComicArchive:
def readCIX( self ):
raw_cix = self.readRawCIX()
if raw_cix is None:
return GenericMetadata()
md = GenericMetadata()
else:
md = ComicInfoXml().metadataFromString( raw_cix )
#validate the existing page list (make sure count is correct)
if len ( md.pages ) != 0 :
if len ( md.pages ) != self.getNumberOfPages():
# pages array doesn't match the actual number of images we're seeing
# in the archive, so discard the data
md.pages = []
return ComicInfoXml().metadataFromString( raw_cix )
if len( md.pages ) == 0:
md.setDefaultPageList( self.getNumberOfPages() )
return 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,6 +662,7 @@ 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 )
else:
@ -590,22 +673,117 @@ class ComicArchive:
return self.archiver.removeArchiveFile( self.ci_xml_filename )
def hasCIX(self):
if not self.seemsToBeAComicArchive():
return False
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
return True
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 ):
raw_comet = self.readRawCoMet()
if raw_comet is None:
md = GenericMetadata()
else:
md = CoMet().metadataFromString( raw_comet )
md.setDefaultPageList( self.getNumberOfPages() )
#use the coverImage value from the comet_data to mark the cover in this struct
# walk through list of images in file, and find the matching one for md.coverImage
# need to remove the existing one in the default
if md.coverImage is not None:
cover_idx = 0
for idx,f in enumerate(self.getPageNameList()):
if md.coverImage == f:
cover_idx = idx
break
if cover_idx != 0:
del (md.pages[0]['Type'] )
md.pages[ cover_idx ]['Type'] = PageType.FrontCover
return md
def readRawCoMet( self ):
if not self.hasCoMet():
print self.path, "doesn't have CoMet data!"
return None
return self.archiver.readArchiveFile( self.comet_filename )
def writeCoMet(self, metadata):
if metadata is not None:
if not self.hasCoMet():
self.comet_filename = self.comet_default_filename
self.applyArchiveInfoToMetadata( metadata )
# Set the coverImage value, if it's not the first page
cover_idx = int(metadata.getCoverPageIndexList()[0])
if cover_idx != 0:
metadata.coverImage = self.getPageName( cover_idx )
comet_string = CoMet().stringFromMetadata( metadata )
return self.archiver.writeArchiveFile( self.comet_filename, comet_string )
else:
return False
def hasCBI(self):
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
if not self.seemsToBeAComicArchive():
def removeCoMet( self ):
if self.hasCoMet():
retcode = self.archiver.removeArchiveFile( self.comet_filename )
self.comet_filename = None
return retcode
return True
def hasCoMet(self):
if not self.seemsToBeAComicArchive():
return False
#Use the existence of self.comet_filename as a cue that the tag block exists
if self.comet_filename is None:
#TODO look at all xml files in root, and search for CoMet data, get first
for n in self.archiver.getArchiveFilenameList():
if ( os.path.dirname(n) == "" and
os.path.splitext(n)[1].lower() == '.xml'):
# read in XML file, and validate it
data = self.archiver.readArchiveFile( n )
if CoMet().validateString( data ):
# since we found it, save it!
self.comet_filename = n
return True
# if we made it through the loop, no CoMet here...
return False
else:
return True
comment = self.archiver.getArchiveComment()
return ComicBookInfo().validateString( comment )
def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False):
md.pageCount = self.getNumberOfPages()
if calc_page_sizes:
for p in md.pages:
idx = int( p['Image'] )
if pil_available:
if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p:
data = self.getPage( idx )
im = Image.open(StringIO.StringIO(data))
w,h = im.size
p['ImageSize'] = str(len(data))
p['ImageHeight'] = str(h)
p['ImageWidth'] = str(w)
else:
if 'ImageSize' not in p:
data = self.getPage( idx )
p['ImageSize'] = str(len(data))
def metadataFromFilename( self ):
metadata = GenericMetadata()
@ -627,3 +805,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:
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
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"
new_ext = ".cbz"
elif ca.isRar():
new_name += ".cbr"
new_ext = ".cbr"
else:
new_ext = None # default
renamer = FileRenamer( md )
renamer.setTemplate( settings.rename_template )
renamer.setIssueZeroPadding( settings.rename_issue_number_padding )
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
new_name = renamer.determineName( filename, ext=new_ext )
if new_name == os.path.basename(filename):
print "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()
@ -349,7 +487,10 @@ def main():
app.processEvents()
try:
tagger_window = TaggerWindow( opts.filename, settings )
fname = None
if opts.filename is not None:
fname = opts.filename.decode(filename_encoding, 'replace')
tagger_window = TaggerWindow( fname, settings )
tagger_window.show()
if platform.system() != "Linux":
@ -361,8 +502,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

@ -43,6 +43,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 +93,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,7 +178,7 @@ 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 )
@ -198,16 +201,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,7 +238,9 @@ class ComicVineTalker(QObject):
metadata.year = issue_results['publish_year']
#metadata.issueCount = volume_results['count_of_issues']
metadata.comments = self.cleanup_html(issue_results['description'])
if settings.use_series_start_as_volume:
metadata.volume = volume_results['start_year']
metadata.notes = "Tagged with ComicTagger app using info from Comic Vine."
#metadata.notes += issue_results['site_detail_url']
@ -227,7 +251,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 +272,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="0.9.5-beta"

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

111
filerenamer.py Normal file
View File

@ -0,0 +1,111 @@
"""
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
return new_name

217
fileselectionlist.py Normal file
View File

@ -0,0 +1,217 @@
# coding=utf-8
"""
A PyQt4 widget for managing list of 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, path, ca, cix_md, cbi_md ):
self.path = path
self.cix_md = cix_md
self.cbi_md = cbi_md
self.ca = ca
class FileSelectionList(QWidget):
selectionChanged = pyqtSignal(QVariant)
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.setAcceptDrops(True)
self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
def dragEnterEvent(self, event):
self.droppedFiles = None
if event.mimeData().hasUrls():
# walk through the URL list and build a file list
for url in event.mimeData().urls():
if url.isValid() and url.scheme() == "file":
if self.droppedFiles is None:
self.droppedFiles = []
self.droppedFiles.append(url.toLocalFile())
if self.droppedFiles is not None:
event.accept()
def dropEvent(self, event):
self.addPathList( self.droppedFiles)
event.accept()
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)
self.twList.setSortingEnabled(False)
for idx,f in enumerate(filelist):
QCoreApplication.processEvents()
if progdialog.wasCanceled():
break
progdialog.setValue(idx)
self.addPathItem( f )
progdialog.close()
self.twList.setSortingEnabled(True)
#Maybe set a max size??
self.twList.resizeColumnsToContents()
def isListDupe( self, path ):
r = 0
while r < self.twList.rowCount():
fi = self.twList.item(r, 0).data( Qt.UserRole ).toPyObject()
if fi.path == path:
return True
r = r + 1
return False
def addPathItem( self, path):
path = unicode( path )
#print "processing", path
if self.isListDupe(path):
return
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 )
cix_md = None
cbi_md = None
has_cix = ca.hasCIX()
if has_cix:
cix_md = ca.readCIX()
has_cbi = ca.hasCBI()
if has_cbi:
cbi_md = ca.readCBI()
fi = FileInfo( path, ca, cix_md, cbi_md )
item_text = os.path.split(path)[1]
item = QTableWidgetItem(item_text)
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
item.setData( Qt.UserRole , fi )
item.setData( Qt.ToolTipRole ,item_text)
self.twList.setItem(row, 0, item)
item_text = os.path.split(path)[0]
item = QTableWidgetItem(item_text)
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
item.setData( Qt.ToolTipRole ,item_text)
self.twList.setItem(row, 1, item)
# 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 )
item = FileTableWidgetItem()
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
item.setTextAlignment(Qt.AlignHCenter)
if has_cix:
item.setCheckState(Qt.Checked)
item.setData(Qt.UserRole, True)
else:
item.setData(Qt.UserRole, False)
self.twList.setItem(row, 2, item)
item = FileTableWidgetItem()
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
item.setTextAlignment(Qt.AlignHCenter)
if has_cbi:
item.setCheckState(Qt.Checked)
item.setData(Qt.UserRole, True)
else:
item.setData(Qt.UserRole, False)
self.twList.setItem(row, 3, item)
def itemSelectionChangedCB( self ):
idx = self.twList.currentRow()
fi = self.twList.item(idx, 0).data( Qt.UserRole ).toPyObject()
#if fi.cix_md is not None:
# print u"{0}".format(fi.cix_md)
self.selectionChanged.emit( QVariant(fi))

69
fileselectionlist.ui Normal file
View File

@ -0,0 +1,69 @@
<?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="selectionBehavior">
<enum>QAbstractItemView::SelectRows</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>
</column>
<column>
<property name="text">
<string>Path</string>
</property>
</column>
<column>
<property name="text">
<string>CR</string>
</property>
<property name="toolTip">
<string/>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>CBL</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

View File

@ -17,11 +17,16 @@ 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")

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
@ -72,7 +73,8 @@ class IssueIdentifier:
self.output_function = IssueIdentifier.defaultWriteOutput
self.callback = None
self.search_result = self.ResultNoMatches
self.cover_page_index = 0
def setScoreMinThreshold( self, thresh ):
self.min_score_thresh = thresh
@ -105,11 +107,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))
@ -217,7 +221,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
@ -253,7 +257,7 @@ class IssueIdentifier:
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 +277,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
@ -315,7 +319,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 +332,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
@ -368,7 +367,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
@ -424,7 +423,7 @@ class IssueIdentifier:
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'])

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

95
issuestring.py Normal file
View File

@ -0,0 +1,95 @@
"""
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):
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 )

303
pagelisteditor.py Normal file
View File

@ -0,0 +1,303 @@
"""
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.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
self.setDisplayPixmap( 0, 0)
# 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 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.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.listWidget.setCurrentRow ( 0 )
self.first_front_page = self.getFirstFrontCover()
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

@ -1,4 +1,57 @@
30-Nov-2012
0.9.0-beta
---------------------------------
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

79
renamewindow.py Normal file
View File

@ -0,0 +1,79 @@
"""
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
import os
import utils
class RenameWindow(QtGui.QDialog):
def __init__( self, parent, comic_archive, metadata, settings ):
super(RenameWindow, self).__init__(parent)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
self.settings = settings
self.metadata = metadata
self.comic_archive = comic_archive
self.new_name = None
self.btnSettings.clicked.connect( self.modifySettings )
self.configRenamer()
self.doPreview()
def configRenamer( self ):
self.renamer = FileRenamer( self.metadata )
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.new_name = self.renamer.determineName( self.comic_archive.path )
preview = u"\"{0}\" ==> \"{1}\"".format( self.comic_archive.path, self.new_name )
self.textEdit.setPlainText( preview )
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 ):
QtGui.QDialog.accept(self)
if self.new_name == os.path.basename( self.comic_archive.path ):
#print msg_hdr + "Filename is already good!"
return
folder = os.path.dirname( os.path.abspath( self.comic_archive.path ) )
new_abs_path = utils.unique_file( os.path.join( folder, self.new_name ) )
os.rename( self.comic_archive.path, new_abs_path )
self.new_name = new_abs_path

106
renamewindow.ui Normal file
View File

@ -0,0 +1,106 @@
<?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>556</width>
<height>210</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="QTextEdit" name="textEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</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 class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>140</x>
<y>10</y>
<width>133</width>
<height>29</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout"/>
</widget>
</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

@ -65,7 +65,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
def __init__(self):
self.settings_file = ""
@ -136,13 +156,41 @@ class ComicTaggerSettings:
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' )
def save( self ):
if not self.config.has_section( 'settings' ):
@ -173,8 +221,30 @@ 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 )
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,32 @@ 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 )
def accept( self ):
# Copy values from form to settings and save
@ -121,12 +152,29 @@ 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.save()
QtGui.QDialog.accept(self)
@ -170,5 +218,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,192 @@
</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/CLI Operations</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>242</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>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>

View File

@ -28,6 +28,7 @@ import platform
import os
import pprint
import json
import webbrowser
from volumeselectionwindow import VolumeSelectionWindow
from options import MetaDataStyle
@ -42,6 +43,11 @@ from pagebrowser import PageBrowserWindow
from filenameparser import FileNameParser
from logwindow import LogWindow
from optionalmsgdialog import OptionalMessageDialog
from pagelisteditor import PageListEditor
from fileselectionlist import FileSelectionList
from cbltransformer import CBLTransformer
from renamewindow import RenameWindow
from pageloader import PageLoader
import utils
import ctversion
@ -87,13 +93,32 @@ class TaggerWindow( QtGui.QMainWindow):
#signal.signal(signal.SIGINT, self.sigint_handler)
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'taggerwindow.ui' ), self)
self.settings = settings
self.pageListEditor = PageListEditor( self.tabPages )
gridlayout = QtGui.QGridLayout( self.tabPages )
gridlayout.addWidget( self.pageListEditor )
#---------------------------
self.fileSelectionList = FileSelectionList( self.widgetListHolder, self.settings )
gridlayout = QtGui.QGridLayout( self.widgetListHolder )
gridlayout.addWidget( self.fileSelectionList )
self.fileSelectionList.selectionChanged.connect( self.fileListSelectionChanged )
# ATB: Disable the list for now...
self.splitter.setSizes([100,0])
self.splitter.setHandleWidth(0)
self.splitter.handle(1).setDisabled(True)
#---------------------------
self.setWindowIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/app.png' )))
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
#print platform.system(), platform.release()
self.dirtyFlag = False
self.settings = settings
self.data_style = settings.last_selected_data_style
#set up a default metadata object
@ -104,10 +129,11 @@ class TaggerWindow( QtGui.QMainWindow):
self.statusBar()
self.updateAppTitle()
self.setAcceptDrops(True)
self.updateSaveMenu()
self.updateMenus()
self.droppedFile = None
self.page_browser = None
self.page_loader = None
self.populateComboBoxes()
@ -135,7 +161,12 @@ class TaggerWindow( QtGui.QMainWindow):
self.btnRemoveCredit.clicked.connect(self.removeCredit)
self.twCredits.cellDoubleClicked.connect(self.editCredit)
clickable(self.lblCover).connect(self.showPageBrowser)
self.connectDirtyFlagSignals()
self.pageListEditor.modified.connect(self.setDirtyFlag)
self.pageListEditor.firstFrontCoverChanged.connect( self.frontCoverChanged )
self.pageListEditor.listOrderChanged.connect( self.pageListOrderChanged )
self.updateStyleTweaks()
@ -197,6 +228,10 @@ class TaggerWindow( QtGui.QMainWindow):
self.actionWrite_Tags.setStatusTip( 'Save tags to comic archive' )
self.actionWrite_Tags.triggered.connect( self.commitMetadata )
self.actionRemoveAuto.setShortcut( 'Ctrl+D' )
self.actionRemoveAuto.setStatusTip( 'Remove selected style tags from archive' )
self.actionRemoveAuto.triggered.connect( self.removeAuto )
self.actionRemoveCBLTags.setStatusTip( 'Remove ComicBookLover tags from comic archive' )
self.actionRemoveCBLTags.triggered.connect( self.removeCBLTags )
@ -219,11 +254,14 @@ class TaggerWindow( QtGui.QMainWindow):
self.actionViewRawCBLTags.setStatusTip( 'View raw ComicBookLover tag block from file' )
self.actionViewRawCBLTags.triggered.connect( self.viewRawCBLTags )
#self.actionRepackage.setShortcut( )
self.actionRepackage.setShortcut( 'Ctrl+E' )
self.actionRepackage.setStatusTip( 'Re-create archive as CBZ' )
self.actionRepackage.triggered.connect( self.repackageArchive )
self.actionRename.setShortcut( 'Ctrl+N' )
self.actionRename.setStatusTip( 'Rename archive based on tags' )
self.actionRename.triggered.connect( self.renameArchive )
#self.actionRepackage.setShortcut( )
self.actionSettings.setStatusTip( 'Configure ComicTagger' )
self.actionSettings.triggered.connect( self.showSettings )
@ -236,8 +274,13 @@ class TaggerWindow( QtGui.QMainWindow):
self.actionSearchOnline.setStatusTip( 'Search online for tags' )
self.actionSearchOnline.triggered.connect( self.queryOnline )
self.actionAutoSearch.setShortcut( 'Ctrl+A' )
self.actionAutoSearch.triggered.connect( self.autoSelectSearch )
self.actionApplyCBLTransform.setShortcut( 'Ctrl+L' )
self.actionApplyCBLTransform.setStatusTip( 'Modify tags specifically for CBL format' )
self.actionApplyCBLTransform.triggered.connect( self.applyCBLTransform )
#self.actionClearEntryForm.setShortcut( 'Ctrl+C' )
self.actionClearEntryForm.setStatusTip( 'Clear all the data on the screen' )
self.actionClearEntryForm.triggered.connect( self.clearForm )
@ -248,9 +291,11 @@ class TaggerWindow( QtGui.QMainWindow):
self.actionPageBrowser.triggered.connect( self.showPageBrowser )
# Help Menu
self.actionAbout.setShortcut( 'Ctrl+A' )
self.actionAbout.setStatusTip( 'Show the ' + self.appName + ' info' )
self.actionAbout.triggered.connect( self.aboutApp )
self.actionWiki.triggered.connect( self.showWiki )
self.actionReportBug.triggered.connect( self.reportBug )
self.actionComicTaggerForum.triggered.connect( self.showForum )
# ToolBar
@ -271,12 +316,54 @@ class TaggerWindow( QtGui.QMainWindow):
self.toolBar.addAction( self.actionPageBrowser )
def repackageArchive( self ):
QtGui.QMessageBox.information(self, self.tr("Repackage Comic Archive"), self.tr("TBD"))
if self.comic_archive is not None:
if self.comic_archive.isZip():
QtGui.QMessageBox.information(self, self.tr("Export as Zip Archive"), self.tr("It's already a Zip Archive!"))
return
if not self.dirtyFlagVerification( "Export as Zip Archive",
"If you export the archive to Zip format, the data in the form won't be in the new " +
"archive unless you save it first. Do you want to continue with the export?"):
return
export_name = os.path.splitext(self.comic_archive.path)[0] + ".cbz"
#export_name = utils.unique_file( export_name )
dialog = QtGui.QFileDialog(self)
dialog.setFileMode(QtGui.QFileDialog.AnyFile)
if self.settings.last_opened_folder is not None:
dialog.setDirectory( self.settings.last_opened_folder )
filters = [
"Comic archive files (*.cbz *.zip)",
"Any files (*)"
]
dialog.setNameFilters(filters)
dialog.selectFile( export_name )
if (dialog.exec_()):
fileList = dialog.selectedFiles()
if os.path.lexists( fileList[0] ):
reply = QtGui.QMessageBox.question(self,
self.tr("Export as Zip Archive"),
self.tr(fileList[0] + " already exisits. Are you sure you want to overwrite it?"),
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
if reply == QtGui.QMessageBox.No:
return
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
retcode = self.comic_archive.exportAsZip( str(fileList[0]) )
QtGui.QApplication.restoreOverrideCursor()
if not retcode:
QtGui.QMessageBox.information(self, self.tr("Export as Zip Archive"), self.tr("An error occure while exporting."))
def aboutApp( self ):
website = "http://code.google.com/p/comictagger"
email = "comictagger@gmail.com"
license_link = "http://www.apache.org/licenses/LICENSE-2.0"
license_name = "Apache License 2.0"
msgBox = QtGui.QMessageBox()
msgBox.setWindowTitle( self.tr("About " + self.appName ) )
@ -286,14 +373,12 @@ class TaggerWindow( QtGui.QMainWindow):
+ self.appName + " v" + self.version + "<br>"
+ "(c)2012 Anthony Beville<br><br>"
+ "<a href='{0}'>{0}</a><br><br>".format(website)
+ "<a href='mailto:{0}'>{0}</a>".format(email) )
+ "<a href='mailto:{0}'>{0}</a><br><br>".format(email)
+ "License: <a href='{0}'>{1}</a>".format(license_link, license_name) )
msgBox.setStandardButtons( QtGui.QMessageBox.Ok )
msgBox.exec_()
def dragEnterEvent(self, event):
self.droppedFile=None
if event.mimeData().hasUrls():
@ -302,12 +387,12 @@ class TaggerWindow( QtGui.QMainWindow):
if url.scheme()=="file":
self.droppedFile=url.toLocalFile()
event.accept()
def dropEvent(self, event):
if self.dirtyFlagVerification( "Open Archive",
"If you open a new archive now, data in the form will be lost. Are you sure?"):
self.openArchive( str(self.droppedFile) )
self.openArchive( unicode(self.droppedFile))
def openArchive( self, path, explicit_style=None, clear_form=True ):
if path is None or path == "":
@ -335,6 +420,7 @@ class TaggerWindow( QtGui.QMainWindow):
# no style indicated, so try to choose
if hasNeither:
self.metadata = self.comic_archive.metadataFromFilename( )
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
else:
if hasCBI and not hasCIX:
self.data_style = MetaDataStyle.CBI
@ -361,42 +447,93 @@ class TaggerWindow( QtGui.QMainWindow):
else:
return
if self.metadata.isEmpty:
self.metadata = self.comic_archive.metadataFromFilename( )
image_data = self.comic_archive.getCoverPage()
if not image_data is None:
img = QtGui.QImage()
img.loadFromData( image_data )
self.lblCover.setPixmap(QtGui.QPixmap(img))
self.lblCover.setScaledContents(True)
if self.page_browser is not None:
self.page_browser.setComicArchive( self.comic_archive )
self.metadataToForm()
self.clearDirtyFlag() # also updates the app title
self.updateInfoBox()
self.updateSaveMenu()
#self.updatePagesInfo()
self.loadCurrentArchive()
else:
QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("That file doesn't appear to be a comic archive!"))
def updateSaveMenu( self ):
def loadCurrentArchive( self ):
if self.metadata.isEmpty:
self.metadata = self.comic_archive.metadataFromFilename( )
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
if ( self.comic_archive is not None and
self.comic_archive.isWritable( )
):
self.actionRemoveCRTags.setEnabled( True )
self.actionRemoveCBLTags.setEnabled( True )
self.actionWrite_Tags.setEnabled( True )
else:
self.actionRemoveCRTags.setEnabled( False )
self.actionRemoveCBLTags.setEnabled( False )
self.actionWrite_Tags.setEnabled( False )
self.updateCoverImage()
if self.page_browser is not None:
self.page_browser.setComicArchive( self.comic_archive )
self.page_browser.metadata = self.metadata
self.metadataToForm()
self.pageListEditor.setData( self.comic_archive, self.metadata.pages )
self.clearDirtyFlag() # also updates the app title
self.updateInfoBox()
self.updateMenus()
def updateCoverImage( self ):
if self.page_loader is not None:
self.page_loader.abandoned = True
cover_idx = self.metadata.getCoverPageIndexList()[0]
self.page_loader = PageLoader( self.comic_archive, cover_idx )
self.page_loader.loadComplete.connect( self.actualUpdateCoverImage )
self.page_loader.start()
def actualUpdateCoverImage( self, img ):
self.page_loader = None
self.lblCover.setPixmap(QtGui.QPixmap(img))
self.lblCover.setScaledContents(True)
def updateMenus( self ):
# First just disable all the questionable items
self.actionRemoveAuto.setEnabled( False )
self.actionRemoveCRTags.setEnabled( False )
self.actionRemoveCBLTags.setEnabled( False )
self.actionWrite_Tags.setEnabled( False )
self.actionRepackage.setEnabled(False)
self.actionViewRawCBLTags.setEnabled( False )
self.actionViewRawCRTags.setEnabled( False )
self.actionReloadCRTags.setEnabled( False )
self.actionReloadCBLTags.setEnabled( False )
self.actionReloadAuto.setEnabled( False )
self.actionParse_Filename.setEnabled( False )
self.actionAutoSearch.setEnabled( False )
self.actionRename.setEnabled( False )
self.actionApplyCBLTransform.setEnabled( False )
# now, selectively re-enable
if self.comic_archive is not None :
has_cix = self.comic_archive.hasCIX()
has_cbi = self.comic_archive.hasCBI()
self.actionParse_Filename.setEnabled( True )
self.actionAutoSearch.setEnabled( True )
self.actionRename.setEnabled( True )
self.actionApplyCBLTransform.setEnabled( True )
if not self.comic_archive.isZip():
self.actionRepackage.setEnabled(True)
if has_cix or has_cbi:
self.actionReloadAuto.setEnabled( True )
if has_cix:
self.actionReloadCRTags.setEnabled( True )
self.actionViewRawCRTags.setEnabled( True )
if has_cbi:
self.actionReloadCBLTags.setEnabled( True )
self.actionViewRawCBLTags.setEnabled( True )
if self.comic_archive.isWritable():
self.actionWrite_Tags.setEnabled( True )
if has_cix or has_cbi:
self.actionRemoveAuto.setEnabled( True )
if has_cix:
self.actionRemoveCRTags.setEnabled( True )
if has_cbi:
self.actionRemoveCBLTags.setEnabled( True )
def updateInfoBox( self ):
ca = self.comic_archive
@ -428,39 +565,6 @@ class TaggerWindow( QtGui.QMainWindow):
tag_info += u"• ComicBookLover tags"
self.lblTagList.setText( tag_info )
def updatePagesInfo( self ):
#tablemodel = PageTableModel( self.comic_archive, self )
#self.tableView.setModel(tablemodel)
listmodel = PageListModel( self.comic_archive, self )
self.listView.setModel(listmodel)
#self.listView.setDragDropMode(self.InternalMove)
#listmodel.installEventFilter(self)
self.listView.setDragEnabled(True)
self.listView.setAcceptDrops(True)
self.listView.setDropIndicatorShown(True)
#listmodel.rowsMoved.connect( self.rowsMoved )
#listmodel.rowsRemoved.connect( self.rowsRemoved )
#listmodel.beginMoveRows.connect( self.beginMoveRows )
#def rowsMoved( self, b, c, d):
# print "rowsMoved"
#def rowsRemoved( self,b, c, d):
# print "rowsRemoved"
"""
def eventFilter(self, sender, event):
if (event.type() == QtCore.QEvent.ChildRemoved):
print "QEvent::ChildRemoved"
return False # don't actually interrupt anything
"""
def setDirtyFlag( self, param1=None, param2=None, param3=None ):
if not self.dirtyFlag:
@ -489,20 +593,24 @@ class TaggerWindow( QtGui.QMainWindow):
# recursive call on chillun
for child in widget.children():
self.connectChildDirtyFlagSignals( child )
if child != self.pageListEditor:
self.connectChildDirtyFlagSignals( child )
def clearForm( self ):
# get a minty fresh metadata object
self.metadata = GenericMetadata()
if self.comic_archive is not None:
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
# recursivly clear the tab form
self.clearChildren( self.tabWidget )
# clear the dirty flag, since there is nothing in there now to lose
self.clearDirtyFlag()
self.pageListEditor.setData( self.comic_archive, self.metadata.pages )
def clearChildren (self, widget ):
@ -528,7 +636,7 @@ class TaggerWindow( QtGui.QMainWindow):
#helper func
def assignText( field, value):
if value is not None:
field.setText( u"{0}".format(value) )
field.setText( unicode(value) )
md = self.metadata
@ -615,20 +723,23 @@ class TaggerWindow( QtGui.QMainWindow):
item_text = role
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twCredits.setItem(row, 0, item)
self.twCredits.setItem(row, 1, item)
item_text = name
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twCredits.setItem(row, 1, item)
# for now, jusr preserve the primary flag
item.setData( QtCore.Qt.UserRole, primary_flag)
self.twCredits.setItem(row, 2, item)
item = QtGui.QTableWidgetItem("")
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twCredits.setItem(row, 0, item)
self.updateCreditPrimaryFlag( row, primary_flag )
def isDupeCredit( self, role, name ):
r = 0
while r < self.twCredits.rowCount():
if ( self.twCredits.item(r, 0).text() == role and
self.twCredits.item(r, 1).text() == name ):
if ( self.twCredits.item(r, 1).text() == role and
self.twCredits.item(r, 2).text() == name ):
return True
r = r + 1
@ -701,18 +812,23 @@ class TaggerWindow( QtGui.QMainWindow):
md.credits = list()
row = 0
while row < self.twCredits.rowCount():
role = str(self.twCredits.item(row, 0).text())
name = str(self.twCredits.item(row, 1).text())
primary_flag = self.twCredits.item( row, 1 ).data( QtCore.Qt.UserRole ).toBool()
role = u"{0}".format(self.twCredits.item(row, 1).text())
name = u"{0}".format(self.twCredits.item(row, 2).text())
primary_flag = self.twCredits.item( row, 0 ).text() != ""
md.addCredit( name, role, bool(primary_flag) )
row += 1
md.pages = self.pageListEditor.getPageList()
def useFilename( self ):
if self.comic_archive is not None:
self.metadata = self.comic_archive.metadataFromFilename( )
self.metadataToForm()
#copy the form onto metadata object
self.formToMetadata()
new_metadata = self.comic_archive.metadataFromFilename( )
if new_metadata is not None:
self.metadata.overlay( new_metadata )
self.metadataToForm()
def selectFile( self ):
@ -739,7 +855,7 @@ class TaggerWindow( QtGui.QMainWindow):
fileList = dialog.selectedFiles()
if self.dirtyFlagVerification( "Open Archive",
"If you open a new archive now, data in the form will be lost. Are you sure?"):
self.openArchive( str(fileList[0]) )
self.openArchive( unicode(fileList[0]) )
def autoSelectSearch(self):
@ -758,8 +874,8 @@ class TaggerWindow( QtGui.QMainWindow):
QtGui.QMessageBox.information(self,"Automatic Online Search", "Can't auto-select without an issue number (yet!)")
return
if str(self.leSeries.text()).strip() != "":
series_name = str(self.leSeries.text()).strip()
if unicode(self.leSeries.text()).strip() != "":
series_name = unicode(self.leSeries.text()).strip()
else:
QtGui.QMessageBox.information(self, self.tr("Online Search"), self.tr("Need to enter a series name to search."))
return
@ -769,7 +885,8 @@ class TaggerWindow( QtGui.QMainWindow):
if year == "":
year = None
selector = VolumeSelectionWindow( self, series_name, issue_number, year, self.comic_archive, self.settings, autoselect )
cover_index_list = self.metadata.getCoverPageIndexList()
selector = VolumeSelectionWindow( self, series_name, issue_number, year, cover_index_list, self.comic_archive, self.settings, autoselect )
title = "Search: '" + series_name + "' - "
selector.setWindowTitle( title + "Select Series")
@ -786,16 +903,23 @@ class TaggerWindow( QtGui.QMainWindow):
try:
comicVine = ComicVineTalker( )
new_metadata = comicVine.fetchIssueData( selector.volume_id, selector.issue_number )
new_metadata = comicVine.fetchIssueData( selector.volume_id, selector.issue_number, self.settings )
except ComicVineTalkerException:
QtGui.QApplication.restoreOverrideCursor()
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to get issue details!"))
else:
self.metadata.overlay( new_metadata )
# Now push the new combined data into the edit controls
self.metadataToForm()
finally:
QtGui.QApplication.restoreOverrideCursor()
if new_metadata is not None:
if self.settings.apply_cbl_transform_on_cv_import:
new_metadata = CBLTransformer( new_metadata, self.settings ).apply()
self.metadata.overlay( new_metadata )
# Now push the new combined data into the edit controls
self.metadataToForm()
else:
QtGui.QMessageBox.critical(self, self.tr("Search"), self.tr("Could not find an issue {0} for that series".format(selector.issue_number)))
def commitMetadata(self):
@ -836,6 +960,7 @@ class TaggerWindow( QtGui.QMainWindow):
else:
self.clearDirtyFlag()
self.updateInfoBox()
self.updateMenus()
#QtGui.QMessageBox.information(self, self.tr("Yeah!"), self.tr("File written."))
else:
@ -847,7 +972,7 @@ class TaggerWindow( QtGui.QMainWindow):
self.settings.last_selected_data_style = self.data_style
self.updateStyleTweaks()
self.updateSaveMenu()
self.updateMenus()
def updateCreditColors( self ):
inactive_color = QtGui.QColor(255, 170, 150)
@ -860,10 +985,12 @@ class TaggerWindow( QtGui.QMainWindow):
#loop over credit table, mark selected rows
r = 0
while r < self.twCredits.rowCount():
if str(self.twCredits.item(r, 0).text()).lower() not in cix_credits:
self.twCredits.item(r, 0).setBackgroundColor( inactive_color )
if str(self.twCredits.item(r, 1).text()).lower() not in cix_credits:
self.twCredits.item(r, 1).setBackgroundColor( inactive_color )
else:
self.twCredits.item(r, 0).setBackgroundColor( active_color )
self.twCredits.item(r, 1).setBackgroundColor( active_color )
# turn off entire primary column
self.twCredits.item(r, 0).setBackgroundColor( inactive_color )
r = r + 1
if self.data_style == MetaDataStyle.CBI:
@ -871,6 +998,7 @@ class TaggerWindow( QtGui.QMainWindow):
r = 0
while r < self.twCredits.rowCount():
self.twCredits.item(r, 0).setBackgroundColor( active_color )
self.twCredits.item(r, 1).setBackgroundColor( active_color )
r = r + 1
@ -941,6 +1069,7 @@ class TaggerWindow( QtGui.QMainWindow):
enableWidget(item, False )
self.updateCreditColors()
self.pageListEditor.setMetadataStyle( self.data_style )
def cellDoubleClicked( self, r, c ):
self.editCredit()
@ -951,27 +1080,53 @@ class TaggerWindow( QtGui.QMainWindow):
def editCredit( self ):
if ( self.twCredits.currentRow() > -1 ):
self.modifyCredits( "edit" )
def updateCreditPrimaryFlag( self, row, primary ):
# if we're clearing a flagm do it and quit
if not primary:
self.twCredits.item(row, 0).setText( "" )
return
# otherwise, we need to check for, and clear, other primaries with same role
role = str(self.twCredits.item(row, 1).text())
r = 0
while r < self.twCredits.rowCount():
if ( self.twCredits.item(r, 0).text() != "" and
str(self.twCredits.item(r, 1).text()).lower() == role.lower() ):
self.twCredits.item(r, 0).setText( "" )
r = r + 1
# Now set our new primary
self.twCredits.item(row, 0).setText( "Yes" )
def modifyCredits( self , action ):
if action == "edit":
row = self.twCredits.currentRow()
role = self.twCredits.item( row, 0 ).text()
name = self.twCredits.item( row, 1 ).text()
role = self.twCredits.item( row, 1 ).text()
name = self.twCredits.item( row, 2 ).text()
primary = self.twCredits.item( row, 0 ).text() != ""
else:
role = ""
name = ""
primary = False
editor = CreditEditorWindow( self, CreditEditorWindow.ModeEdit, role, name )
editor = CreditEditorWindow( self, CreditEditorWindow.ModeEdit, role, name, primary )
editor.setModal(True)
editor.exec_()
if editor.result():
new_role, new_name = editor.getCredits()
new_role, new_name, new_primary = editor.getCredits()
if new_name == name and new_role == role:
if new_name == name and new_role == role and new_primary == primary:
#nothing has changed, just quit
return
# name and role is the same, but primary flag changed
if new_name == name and new_role == role:
self.updateCreditPrimaryFlag( row, new_primary )
return
# check for dupes
ok_to_mod = True
if self.isDupeCredit( new_role, new_name):
@ -986,6 +1141,7 @@ class TaggerWindow( QtGui.QMainWindow):
if action == "edit":
# just remove the row that would be same
self.twCredits.removeRow( row )
# TODO -- need to find the row of the dupe, and possible change the primary flag
ok_to_mod = False
@ -993,12 +1149,13 @@ class TaggerWindow( QtGui.QMainWindow):
if ok_to_mod:
#modify it
if action == "edit":
self.twCredits.item(row, 0).setText( new_role )
self.twCredits.item(row, 1).setText( new_name )
self.twCredits.item(row, 1).setText( new_role )
self.twCredits.item(row, 2).setText( new_name )
self.updateCreditPrimaryFlag( row, new_primary )
else:
# add new entry
row = self.twCredits.rowCount()
self.addNewCreditEntry( row, new_role, new_name)
self.addNewCreditEntry( row, new_role, new_name, new_primary)
self.updateCreditColors()
self.setDirtyFlag()
@ -1132,6 +1289,8 @@ class TaggerWindow( QtGui.QMainWindow):
self.cbFormat.addItem("Year 1")
self.cbFormat.addItem("Year One")
def removeAuto( self ):
self.removeTags( self.data_style )
def removeCBLTags( self ):
self.removeTags( MetaDataStyle.CBI )
@ -1154,6 +1313,7 @@ class TaggerWindow( QtGui.QMainWindow):
QtGui.QMessageBox.warning(self, self.tr("Remove failed"), self.tr("The tag removal operation seemed to fail!"))
else:
self.updateInfoBox()
self.updateMenus()
def reloadAuto( self ):
@ -1199,7 +1359,7 @@ class TaggerWindow( QtGui.QMainWindow):
def showPageBrowser( self ):
if self.page_browser is None:
self.page_browser = PageBrowserWindow( self )
self.page_browser = PageBrowserWindow( self, self.metadata )
if self.comic_archive is not None:
self.page_browser.setComicArchive( self.comic_archive )
self.page_browser.finished.connect(self.pageBrowserClosed)
@ -1218,9 +1378,55 @@ class TaggerWindow( QtGui.QMainWindow):
if self.comic_archive is not None and self.comic_archive.hasCBI():
dlg = LogWindow( self )
text = pprint.pformat( json.loads(self.comic_archive.readRawCBI()), indent=4 )
print text
dlg.setText(text )
dlg.setWindowTitle( "Raw ComicBookLover Tag View" )
dlg.exec_()
def showWiki( self ):
webbrowser.open("http://code.google.com/p/comictagger/wiki/Home?tm=6")
def reportBug( self ):
webbrowser.open("http://code.google.com/p/comictagger/issues/list")
def showForum( self ):
webbrowser.open("http://comictagger.forumotion.com/")
def frontCoverChanged( self, int ):
self.metadata.pages = self.pageListEditor.getPageList()
self.updateCoverImage()
def pageListOrderChanged( self ):
self.metadata.pages = self.pageListEditor.getPageList()
def applyCBLTransform(self):
self.formToMetadata()
self.metadata = CBLTransformer( self.metadata, self.settings ).apply()
self.metadataToForm()
def renameArchive(self):
if self.comic_archive is not None:
self.formToMetadata()
dlg = RenameWindow( self, self.comic_archive, self.metadata, self.settings )
dlg.setModal( True )
if dlg.exec_():
#reopen the archive, since the filename changed
print dlg.new_name
self.comic_archive = None
self.openArchive( dlg.new_name )
def fileListSelectionChanged( self, qvarFI ):
fi = qvarFI.toPyObject()
#if fi.cix_md is not None:
# print u"{0}".format(fi.cix_md)
self.comic_archive = None
self.clearForm()
self.comic_archive = fi.ca
if self.data_style == MetaDataStyle.CIX:
self.metadata = fi.cix_md
else:
self.metadata = fi.cbi_md
if self.metadata is None:
self.metadata = GenericMetadata()
self.loadCurrentArchive()

File diff suppressed because it is too large Load Diff

147
todo.txt
View File

@ -1,5 +1,81 @@
-----------------------------------------------------
Config Mgmt
Features
-----------------------------------------------------
Multi-file:
Does the main UI need to have "View/Read Tag Style" and "Write Tag style" concept?
Edit functions on list: select, select all, delete,
Batch Functions:
Auto-Select
Start/Options Dialog
Progress Dialog - maybe reuse
Interactive dialog at end
Rename
Start dialog with preview
maybe table with checkboxes?
Copy Block
Verify overwrites
Turn off drop accept for edit lines/boxes
Drop on app goes to list and selects it
Accept multiple files on file open dialog
Warn on moving selection list away from modified form
ComicArchive: cache each metadata block? Need to make sure cache is cleared on file modify
-----------------------------------------------------
Bugs
-----------------------------------------------------
-----------------------------------------------------
Big Future Features
-----------------------------------------------------
GUI to handle mutliple files or folders
Scrape alternate Covers from ComicVine issue pages
GCD scraper or DB reader
pyComicMetaThis CBI features
Auto search:
Searching w/o issue #
-----------------------------------------------------
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
Overwrite or overlay
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,75 +88,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
----------------------------------------------

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)