Compare commits

...

33 Commits

Author SHA1 Message Date
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
23 changed files with 1148 additions and 237 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)"

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 = md.year.zfill(4)
if md.month is not None:
date_str += "-" + md.month.zfill(2)
assign( 'date', date_str )
#assign( 'coverImage', md.??? ) #TODO Need to use pages list, eventually...
# 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]
coverImage = xlate( 'coverImage' ) # TODO - do something with this!
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

@ -36,6 +36,7 @@ from UnRAR2.rar_exceptions import *
from options import Options, MetaDataStyle
from comicinfoxml import ComicInfoXml
from comicbookinfo import ComicBookInfo
from comet import CoMet
from genericmetadata import GenericMetadata
from filenameparser import FileNameParser
@ -92,7 +93,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 +180,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
@ -385,6 +405,8 @@ class ComicArchive:
def __init__( self, path ):
self.path = path
self.ci_xml_filename = 'ComicInfo.xml'
self.comet_default_filename = 'CoMet.xml'
self.comet_filename = None
if self.zipTest():
self.archive_type = self.ArchiveType.Zip
@ -470,6 +492,8 @@ class ComicArchive:
return self.readCIX()
elif style == MetaDataStyle.CBI:
return self.readCBI()
elif style == MetaDataStyle.COMET:
return self.readCoMet()
else:
return GenericMetadata()
@ -479,6 +503,8 @@ class ComicArchive:
return self.writeCIX( metadata )
elif style == MetaDataStyle.CBI:
return self.writeCBI( metadata )
elif style == MetaDataStyle.COMET:
return self.writeCoMet( metadata )
def hasMetadata( self, style ):
@ -486,6 +512,8 @@ class ComicArchive:
return self.hasCIX()
elif style == MetaDataStyle.CBI:
return self.hasCBI()
elif style == MetaDataStyle.COMET:
return self.hasCoMet()
else:
return False
@ -494,6 +522,8 @@ class ComicArchive:
return self.removeCIX()
elif style == MetaDataStyle.CBI:
return self.removeCBI()
elif style == MetaDataStyle.COMET:
return self.removeCoMet()
def getCoverPage(self):
@ -555,7 +585,14 @@ class ComicArchive:
return self.archiver.getArchiveComment()
def hasCBI(self):
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
if not self.seemsToBeAComicArchive():
return False
comment = self.archiver.getArchiveComment()
return ComicBookInfo().validateString( comment )
def writeCBI( self, metadata ):
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
return self.archiver.setArchiveComment( cbi_string )
@ -572,7 +609,7 @@ class ComicArchive:
def readRawCIX( self ):
if not self.hasCIX():
print self.path, "doesn't has ComicInfo.xml data!"
print self.path, "doesn't have ComicInfo.xml data!"
return None
return self.archiver.readArchiveFile( self.ci_xml_filename )
@ -580,6 +617,7 @@ class ComicArchive:
def writeCIX(self, metadata):
if metadata is not None:
metadata.pageCount = self.getNumberOfPages()
cix_string = ComicInfoXml().stringFromMetadata( metadata )
return self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string )
else:
@ -597,14 +635,62 @@ class ComicArchive:
else:
return False
def hasCBI(self):
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
if not self.seemsToBeAComicArchive():
def readCoMet( self ):
raw_comet = self.readRawCoMet()
if raw_comet is None:
return GenericMetadata()
return CoMet().metadataFromString( raw_comet )
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
metadata.pageCount = self.getNumberOfPages()
comet_string = CoMet().stringFromMetadata( metadata )
return self.archiver.writeArchiveFile( self.comet_filename, comet_string )
else:
return False
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 metadataFromFilename( self ):
@ -627,3 +713,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
"""
"""
@ -62,6 +62,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

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

View File

@ -43,24 +43,143 @@ from comicarchive import ComicArchive
from issueidentifier import IssueIdentifier
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
from issuestring import IssueString
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.assume_lone_credit_is_primary )
except ComicVineTalkerException:
print "Network error while getting issue details. Save aborted"
return None
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 " {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:
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()
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,41 +189,50 @@ 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())
@ -112,60 +240,70 @@ def process_file_cli( filename, opts, settings ):
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 not ca.writeMetadata( md, opts.data_style ):
print "{0}: Tag copy seemed to fail!".format( filename )
else:
print "{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
else:
print "{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
else:
print "{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 "{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 "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 )
@ -207,80 +345,62 @@ def process_file_cli( filename, opts, settings ):
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
try:
cv_md = ComicVineTalker().fetchIssueData( matches[0]['volume_id'], matches[0]['issue_number'] )
except ComicVineTalkerException:
print "Network error while getting issue details. Save aborted"
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
#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!"
else:
print "Save complete."
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 = "{0}: ".format(filename)
if md.isEmpty:
print "Comic archive contains no tags!"
if opts.data_style is not None:
use_tags = has[ opts.data_style ]
else:
use_tags = False
md = create_local_metadata( opts, ca, use_tags )
if 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 )
else:
print "Can't rename without series name"
print msg_hdr + "Can't rename without series name"
return
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"
return
new_name += " #{0}".format( IssueString(md.issue).asString(pad=3) )
#else:
# print msg_hdr + "Can't rename without issue number"
# return
if md.issueCount is not None:
new_name += " (of {0})".format( md.issueCount )
@ -294,22 +414,20 @@ def process_file_cli( filename, opts, settings ):
new_name += ".cbr"
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 "renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)

View File

@ -43,6 +43,7 @@ import utils
from settings import ComicTaggerSettings
from comicvinecacher import ComicVineCacher
from genericmetadata import GenericMetadata
from issuestring import IssueString
class ComicVineTalkerException(Exception):
pass
@ -175,7 +176,7 @@ class ComicVineTalker(QObject):
return volume_results
def fetchIssueData( self, series_id, issue_number ):
def fetchIssueData( self, series_id, issue_number, assumeLoneCreditIsPrimary = False ):
volume_results = self.fetchVolumeData( series_id )
@ -203,11 +204,7 @@ class ComicVineTalker(QObject):
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']
@ -228,6 +225,30 @@ class ComicVineTalker(QObject):
# can we determine 'primary' from CV??
role_name = role['role'].title()
metadata.addCredit( person['name'], role['role'].title(), False )
if assumeLoneCreditIsPrimary:
def setLonePrimary( role ):
lone_credit = None
count = 0
for c in metadata.credits:
if c['role'].lower() == role:
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
#need to loop three times, once for 'writer', 'artist', and then 'penciler' if no artist
setLonePrimary( 'writer' )
if setLonePrimary( 'artist' ) is None:
c = setLonePrimary( 'penciler' )
if c is not None:
c['primary'] = False
metadata.addCredit( c['person'], 'Artist', True )
character_credits = issue_results['character_credits']
character_list = list()

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.1-beta"

View File

@ -89,6 +89,15 @@ 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):
if (
@ -132,8 +141,9 @@ class FileNameParser:
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 = ""
@ -171,6 +181,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

View File

@ -93,11 +93,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
def overlay( self, new_md ):
# Overlay a metadata object on this one
# that is, when the new object has non-None
@ -130,7 +138,7 @@ class GenericMetadata:
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 )
@ -143,6 +151,12 @@ class GenericMetadata:
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
@ -229,6 +243,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" )

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
@ -328,13 +329,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
@ -424,7 +420,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):
@ -99,7 +100,7 @@ class IssueSelectionWindow(QtGui.QDialog):
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

90
issuestring.py Normal file
View File

@ -0,0 +1,90 @@
"""
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
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
return int( self.num )

View File

@ -34,7 +34,8 @@ class Enum(set):
class MetaDataStyle:
CBI = 0
CIX = 1
name = [ 'ComicBookLover', 'ComicRack' ]
COMET = 2
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
class Options:
@ -50,14 +51,20 @@ 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
@ -71,6 +78,7 @@ 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)
-h, --help Display this message
"""
@ -80,16 +88,21 @@ 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.file_list = []
def display_help_and_quit( self, msg, code ):
@ -159,9 +172,10 @@ If no options are given, {0} will run in windowed mode
# parse command line options
try:
opts, args = getopt.getopt( input_args,
"hpdt:fm: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" ])
except getopt.GetoptError as err:
self.display_help_and_quit( str(err), 2 )
@ -176,6 +190,18 @@ If no options are given, {0} will run in windowed mode
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_help_and_quit( "Invalid copy tag source type", 1 )
if o in ("-o", "--online"):
self.search_online = True
if o in ("-n", "--dryrun"):
@ -188,29 +214,38 @@ 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 == "--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 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 )
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_help_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
if len(args) > 0:
self.filename = args[0]
@ -224,7 +259,10 @@ If no options are given, {0} will run in windowed mode
if self.save_tags and self.data_style is None:
self.display_help_and_quit( "Please specify the type to save with -t", 1 )
if self.copy_tags and self.data_style is None:
self.display_help_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_help_and_quit( "Please specify the type to use for renaming with -t", 1 )

View File

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

View File

@ -65,6 +65,9 @@ class ComicTaggerSettings:
# Show/ask dialog flags
self.ask_about_cbi_in_rar = True
self.show_disclaimer = True
# Comic Vine settings
self.assume_lone_credit_is_primary = False
def __init__(self):
@ -142,6 +145,10 @@ class ComicTaggerSettings:
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', 'assume_lone_credit_is_primary'):
self.assume_lone_credit_is_primary = self.config.getboolean( 'comicvine', 'assume_lone_credit_is_primary' )
def save( self ):
@ -173,8 +180,14 @@ class ComicTaggerSettings:
self.config.set( 'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar )
self.config.set( 'dialogflags', 'show_disclaimer', self.show_disclaimer )
if not self.config.has_section( 'comicvine' ):
self.config.add_section( 'comicvine' )
self.config.set( 'comicvine', 'assume_lone_credit_is_primary', self.assume_lone_credit_is_primary )
with open( self.settings_file, 'wb') as configfile:
self.config.write(configfile)

View File

@ -111,6 +111,9 @@ class SettingsWindow(QtGui.QDialog):
self.leNameLengthDeltaThresh.setText( str(self.settings.id_length_delta_thresh) )
self.tePublisherBlacklist.setPlainText( self.settings.id_publisher_blacklist )
if self.settings.assume_lone_credit_is_primary:
self.cbxAssumeLoneCreditIsPrimary.setCheckState( QtCore.Qt.Checked)
def accept( self ):
# Copy values from form to settings and save
@ -126,6 +129,7 @@ class SettingsWindow(QtGui.QDialog):
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
self.settings.id_publisher_blacklist = str(self.tePublisherBlacklist.toPlainText())
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
self.settings.save()
QtGui.QDialog.accept(self)

View File

@ -301,6 +301,24 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Comic Vine</string>
</attribute>
<widget class="QCheckBox" name="cbxAssumeLoneCreditIsPrimary">
<property name="geometry">
<rect>
<x>50</x>
<y>30</y>
<width>241</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string>Assume Lone Credit Is Primary</string>
</property>
</widget>
</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
@ -104,7 +105,7 @@ class TaggerWindow( QtGui.QMainWindow):
self.statusBar()
self.updateAppTitle()
self.setAcceptDrops(True)
self.updateSaveMenu()
self.updateMenus()
self.droppedFile = None
self.page_browser = None
@ -197,6 +198,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 +224,10 @@ 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.actionRepackage.setShortcut( )
self.actionSettings.setStatusTip( 'Configure ComicTagger' )
self.actionSettings.triggered.connect( self.showSettings )
@ -251,6 +255,9 @@ class TaggerWindow( QtGui.QMainWindow):
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,8 +278,48 @@ 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"
@ -377,26 +424,57 @@ class TaggerWindow( QtGui.QMainWindow):
self.metadataToForm()
self.clearDirtyFlag() # also updates the app title
self.updateInfoBox()
self.updateSaveMenu()
self.updateMenus()
#self.updatePagesInfo()
else:
QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("That file doesn't appear to be a comic archive!"))
def updateSaveMenu( self ):
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 )
# 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 )
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 )
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 )
def updateInfoBox( self ):
ca = self.comic_archive
@ -615,20 +693,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,9 +782,9 @@ 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 = str(self.twCredits.item(row, 1).text())
name = str(self.twCredits.item(row, 2).text())
primary_flag = self.twCredits.item( row, 0 ).text() != ""
md.addCredit( name, role, bool(primary_flag) )
row += 1
@ -786,16 +867,19 @@ 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.assume_lone_credit_is_primary )
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:
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 +920,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 +932,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 +945,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 +958,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
@ -951,27 +1039,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 +1100,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 +1108,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 +1248,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 +1272,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 ):
@ -1222,5 +1341,13 @@ class TaggerWindow( QtGui.QMainWindow):
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/")

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>959</width>
<height>539</height>
<height>541</height>
</rect>
</property>
<property name="sizePolicy">
@ -748,14 +748,25 @@
<number>0</number>
</property>
<property name="columnCount">
<number>2</number>
<number>3</number>
</property>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>2</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Primary</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Credit</string>
@ -1042,6 +1053,7 @@
<property name="title">
<string>Remove</string>
</property>
<addaction name="actionRemoveAuto"/>
<addaction name="actionRemoveCBLTags"/>
<addaction name="actionRemoveCRTags"/>
</widget>
@ -1073,6 +1085,10 @@
<property name="title">
<string>Help</string>
</property>
<addaction name="actionWiki"/>
<addaction name="actionReportBug"/>
<addaction name="actionComicTaggerForum"/>
<addaction name="separator"/>
<addaction name="actionAbout"/>
</widget>
<widget class="QMenu" name="menuTags">
@ -1131,7 +1147,7 @@
</action>
<action name="actionRepackage">
<property name="text">
<string>Repackage</string>
<string>Export as Zip Archive</string>
</property>
</action>
<action name="actionExit">
@ -1227,6 +1243,26 @@
<string>View Raw ComicBookLover Tags</string>
</property>
</action>
<action name="actionReportBug">
<property name="text">
<string>Report Bug...</string>
</property>
</action>
<action name="actionComicTaggerForum">
<property name="text">
<string>ComicTagger Forum...</string>
</property>
</action>
<action name="actionWiki">
<property name="text">
<string>Online Docs...</string>
</property>
</action>
<action name="actionRemoveAuto">
<property name="text">
<string>Remove Selected Tag Style</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>

View File

@ -1,53 +1,38 @@
-----------------------------------------------------
Config Mgmt
-----------------------------------------------------
Release Process
Optionally, make screen shots, upload to wiki
Update release notes and wiki
Update ctversion.py
Build packages
Make exe on Windows
Make dmg on Mac
Make zip on Mac or Linux
Tag the repository
Upload packages
-----------------------------------------------------
Features
-----------------------------------------------------
CLI batch save, keep log of failures
CLI batch save, interactive at end
TaggerWindow entry fields
Special tabbed Dialog needed for:
Pages Info - maybe a custom painted widget
-----------------------------------------------------
Bugs
-----------------------------------------------------
Auto-select failure when year is off by one. Maybe check with a wider radius??
-----------------------------------------------------
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
-- figure out how to add CBI "tags"
-- delete CBI "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
@ -77,10 +62,27 @@ Settings
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
Rework how series name is separated from issue
Support marvel's "AU" issues...
Mostly done, gotta wait and see what CV does
-----------------------------------------------------
Config Mgmt check list
-----------------------------------------------------
Release Process
Optionally, make screen shots, upload to wiki
Update release notes and wiki
Update ctversion.py
Build packages
Make exe on Windows
Make dmg on Mac
Make zip on Mac or Linux
Tag the repository
Upload packages
----------------------------------------------