Compare commits
33 Commits
0.9.0-beta
...
0.9.1-beta
Author | SHA1 | Date | |
---|---|---|---|
8af7651a50 | |||
1e3d8ccad3 | |||
c367b8806b | |||
d3ea8d1b2c | |||
c5f1542874 | |||
ab5d8599ac | |||
a2d0068522 | |||
c6c5728cb3 | |||
e6f63beee2 | |||
72af8f8564 | |||
5390a92b98 | |||
c814436899 | |||
dbec1999dc | |||
a970ed0e36 | |||
6d8d90d5b7 | |||
117d8d8998 | |||
3689317518 | |||
c845c786e4 | |||
9ccdc60c19 | |||
aec0477170 | |||
134dcbaba3 | |||
f040f8dc74 | |||
948acf9b23 | |||
3c2f4fa662 | |||
f99d466bae | |||
a773ab6539 | |||
ff2fca44f4 | |||
97fe437bb4 | |||
32aabb100b | |||
b385be4338 | |||
deeeef90a6 | |||
121889ed1b | |||
d300f51c7f |
6
Makefile
6
Makefile
@ -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
260
comet.py
Normal 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 )
|
||||
|
109
comicarchive.py
109
comicarchive.py
@ -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 )
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
"""
|
||||
|
||||
"""
|
||||
|
346
comictagger.py
346
comictagger.py
@ -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)
|
||||
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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() == "":
|
||||
|
@ -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>
|
||||
|
@ -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"
|
@ -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
|
||||
|
@ -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" )
|
||||
|
@ -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'])
|
||||
|
@ -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
90
issuestring.py
Normal 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 )
|
||||
|
||||
|
62
options.py
62
options.py
@ -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 )
|
||||
|
||||
|
@ -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
|
15
settings.py
15
settings.py
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
215
taggerwindow.py
215
taggerwindow.py
@ -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/")
|
||||
|
||||
|
@ -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/>
|
||||
|
62
todo.txt
62
todo.txt
@ -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
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user