diff --git a/comicarchive.py b/comicarchive.py
new file mode 100644
index 0000000..2977034
--- /dev/null
+++ b/comicarchive.py
@@ -0,0 +1,284 @@
+"""
+A python class to represent a single comic, be it file or folder of images
+"""
+
+import zipfile
+import os
+import struct
+
+from options import Options, MetaDataStyle
+from comicinfoxml import ComicInfoXml
+from comicbookinfo import ComicBookInfo
+from genericmetadata import GenericMetadata
+from filenameparser import FileNameParser
+
+
+# This is a custom function for writing a comment to a zip file,
+# since the built-in one doesn't seem to work on Windows and Mac OS/X
+
+# Fortunately, the zip comment is at the end of the file, and it's
+# easy to manipulate. See this website for more info:
+# see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure
+
+def writeZipComment( filename, comment ):
+
+ #get file size
+ statinfo = os.stat(filename)
+ file_length = statinfo.st_size
+
+ fo = open(filename, "r+b")
+
+ #the starting position, relative to EOF
+ pos = -4
+
+ found = False
+ value = bytearray()
+
+ # walk backwards to find the "End of Central Directory" record
+ while ( not found ) and ( -pos != file_length ):
+ # seek, relative to EOF
+ fo.seek( pos, 2)
+
+ value = fo.read( 4 )
+
+ #look for the end of central directory signature
+ if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]):
+ found = True
+ else:
+ # not found, step back another byte
+ pos = pos - 1
+ #print pos,"{1} int: {0:x}".format(bytearray(value)[0], value)
+
+ if found:
+
+ # now skip forward 20 bytes to the comment length word
+ pos += 20
+ fo.seek( pos, 2)
+
+ # Pack the length of the comment string
+ format = "H" # one 2-byte integer
+ comment_length = struct.pack(format, len(comment)) # pack integer in a binary string
+
+ # write out the length
+ fo.write( comment_length )
+ fo.seek( pos+2, 2)
+
+ # write out the comment itself
+ fo.write( comment )
+ fo.truncate()
+ fo.close()
+
+ else:
+ raise Exception('Failed to write comment to zip file!')
+
+#------------------------------------------
+
+
+class ComicArchive:
+
+ def __init__( self, path ):
+ self.path = path
+ self.ci_xml_filename = 'ComicInfo.xml'
+
+ def isZip( self ):
+ return zipfile.is_zipfile( self.path )
+
+ def isFolder( self ):
+ return False
+
+ def isNonWritableArchive( self ):
+ # TODO check for rar, maybe others
+ # also check permissions
+ return False
+
+ def seemsToBeAComicArchive( self ):
+ # TODO this will need to be fleshed out to support RAR and Folder
+
+ ext = os.path.splitext(self.path)[1].lower()
+
+ if (
+ ( self.isZip() ) and
+ ( ext in [ '.zip', '.cbz' ] ) and
+ ( self.getNumberOfPages() > 3)
+ ):
+ return True
+ else:
+ return False
+
+ def readMetadata( self, style ):
+
+ if style == MetaDataStyle.CIX:
+ return self.readCIX()
+ elif style == MetaDataStyle.CBI:
+ return self.readCBI()
+ else:
+ return GenericMetadata()
+
+ def writeMetadata( self, metadata, style ):
+
+ if style == MetaDataStyle.CIX:
+ self.writeCIX( metadata )
+ elif style == MetaDataStyle.CBI:
+ self.writeCBI( metadata )
+
+ def hasMetadata( self, syle ):
+
+ if style == MetaDataStyle.CIX:
+ return self.hasCIX()
+ elif style == MetaDataStyle.CBI:
+ return self.hasCBI()
+ else:
+ return False
+
+ def clearMetadata( self, style ):
+ return
+
+ def getCoverPage(self):
+
+ if self.getNumberOfPages() == 0:
+ return None
+
+ zf = zipfile.ZipFile (self.path, 'r')
+
+ # get the list file names in the archive, and sort
+ files = zf.namelist()
+ files.sort()
+
+ # find the first image file, assume it's the cover
+ for name in files:
+ if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
+ break
+
+ image_data = zf.read( name )
+ zf.close()
+
+ return image_data
+
+ def getNumberOfPages(self):
+
+ count = 0
+
+ if self.isZip():
+ zf = zipfile.ZipFile (self.path, 'r')
+ for item in zf.infolist():
+ if ( item.filename[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
+ count += 1
+ zf.close()
+
+ return count
+
+ def readCBI( self ):
+
+ if ( not self.hasCBI() ):
+ print self.path, " isn't a zip or doesn't has CBI data!"
+ return GenericMetadata()
+
+ zf = zipfile.ZipFile( self.path, "r" )
+ cbi_string = zf.comment
+ zf.close()
+
+ metadata = ComicBookInfo().metadataFromString( cbi_string )
+ return metadata
+
+ def writeCBI( self, metadata ):
+
+ cbi_string = ComicBookInfo().stringFromMetadata( metadata )
+ writeZipComment( self.path, cbi_string )
+
+ def readCIX( self ):
+
+ # !!!ATB TODO add support for folders
+
+ if (not self.isZip()) or ( not self.hasCIX()):
+ print self.path, " isn't a zip or doesn't has ComicInfo.xml data!"
+ return GenericMetadata()
+
+ zf = zipfile.ZipFile( self.path, 'r' )
+ cix_string = zf.read( self.ci_xml_filename )
+ zf.close()
+
+ metadata = ComicInfoXml().metadataFromString( cix_string )
+ return metadata
+
+ def writeCIX(self, metadata):
+
+ # !!!ATB TODO add support for folders
+ if (not self.isZip()):
+ print self.path, "isn't a zip archive!"
+ return
+
+ cix_string = ComicInfoXml().stringFromMetadata( metadata )
+
+ # check if an XML file already exists in archive
+ if not self.hasCIX():
+
+ #simple case: just add the new archive file
+ zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED )
+ zf.writestr( self.ci_xml_filename, cix_string )
+ zf.close()
+
+ else:
+ # If we need to replace it, well, at the moment, no other option
+ # but to rebuild the whole zip again.
+ # very sucky, but maybe another solution can be found
+
+ print "{0} already exists in {1}. Rebuilding it...".format( self.ci_xml_filename, self.path)
+ zin = zipfile.ZipFile (self.path, 'r')
+ zout = zipfile.ZipFile ('tmpnew.zip', 'w')
+ for item in zin.infolist():
+ buffer = zin.read(item.filename)
+ if ( item.filename != self.ci_xml_filename ):
+ zout.writestr(item, buffer)
+
+ # now write out the new xml file
+ zout.writestr( self.ci_xml_filename, cix_string )
+
+ #preserve the old comment
+ zout.comment = zin.comment
+
+ zout.close()
+ zin.close()
+
+ # replace with the new file
+ os.remove( self.path )
+ os.rename( 'tmpnew.zip', self.path )
+
+ def hasCIX(self):
+
+ has = False
+
+ zf = zipfile.ZipFile( self.path, 'r' )
+ if self.ci_xml_filename in zf.namelist():
+ has = True
+ zf.close()
+
+ return has
+
+ def hasCBI(self):
+ if (not self.isZip() ):
+ return False
+ zf = zipfile.ZipFile( self.path, 'r' )
+ comment = zf.comment
+ zf.close()
+
+ return ComicBookInfo().validateString( comment )
+
+ def metadataFromFilename( self ):
+
+ metadata = GenericMetadata()
+
+ fnp = FileNameParser()
+ fnp.parseFilename( self.path )
+
+ if fnp.issue != "":
+ metadata.issueNumber = fnp.issue
+ if fnp.series != "":
+ metadata.series = fnp.series
+ if fnp.volume != "":
+ metadata.volumeNumber = fnp.volume
+ if fnp.year != "":
+ metadata.publicationYear = fnp.year
+
+ metadata.isEmpty = False
+
+ return metadata
\ No newline at end of file
diff --git a/comicbookinfo.py b/comicbookinfo.py
new file mode 100644
index 0000000..d706817
--- /dev/null
+++ b/comicbookinfo.py
@@ -0,0 +1,118 @@
+"""
+A python class to encapsulate the ComicBookInfo data and file handling
+"""
+
+import json
+from datetime import datetime
+import zipfile
+
+from genericmetadata import GenericMetadata
+import utils
+
+class ComicBookInfo:
+
+
+ def metadataFromString( self, string ):
+
+ cbi_container = json.loads( unicode(string, 'utf-8') )
+
+ metadata = GenericMetadata()
+
+ cbi = cbi_container[ 'ComicBookInfo/1.0' ]
+
+ #helper func
+ # If item is not in CBI, return None
+ def xlate( cbi_entry):
+ if cbi_entry in cbi:
+ return cbi[cbi_entry]
+ else:
+ return None
+
+ metadata.series = xlate( 'series' )
+ metadata.title = xlate( 'title' )
+ metadata.issueNumber = xlate( 'issue' )
+ metadata.publisher = xlate( 'publisher' )
+ metadata.publicationMonth = xlate( 'publicationMonth' )
+ metadata.publicationYear = xlate( 'publicationYear' )
+ metadata.issueCount = xlate( 'numberOfIssues' )
+ metadata.comments = xlate( 'comments' )
+ metadata.credits = xlate( 'credits' )
+ metadata.genre = xlate( 'genre' )
+ metadata.volumeNumber = xlate( 'volume' )
+ metadata.volumeCount = xlate( 'numberOfVolumes' )
+ metadata.language = xlate( 'language' )
+ metadata.country = xlate( 'country' )
+ metadata.criticalRating = xlate( 'rating' )
+ metadata.tags = xlate( 'tags' )
+
+ #need to massage the language string to be ISO
+ if metadata.language is not None:
+ # reverse look-up
+ pattern = metadata.language
+ metadata.language = None
+ for key in utils.getLanguageDict():
+ if utils.getLanguageDict()[ key ] == pattern.encode('utf-8'):
+ metadata.language = key
+ break
+
+ metadata.isEmpty = False
+
+ return metadata
+
+ def stringFromMetadata( self, metadata ):
+
+ cbi_container = self.createJSONDictionary( metadata )
+ return json.dumps( cbi_container )
+
+ #verify that the string actually contains CBI data in JSON format
+ def validateString( self, string ):
+
+ try:
+ cbi_container = json.loads( string )
+ except:
+ return False
+
+ return ( 'ComicBookInfo/1.0' in cbi_container )
+
+
+ def createJSONDictionary( self, metadata ):
+
+ # Create the dictionary that we will convert to JSON text
+ cbi = dict()
+ cbi_container = {'appID' : 'ComicTagger/0.1',
+ 'lastModified' : str(datetime.now()),
+ 'ComicBookInfo/1.0' : cbi }
+
+ #helper func
+ def assign( cbi_entry, md_entry):
+ if md_entry is not None:
+ cbi[cbi_entry] = md_entry
+
+ assign( 'series', metadata.series )
+ assign( 'title', metadata.title )
+ assign( 'issue', metadata.issueNumber )
+ assign( 'publisher', metadata.publisher )
+ assign( 'publicationMonth', metadata.publicationMonth )
+ assign( 'publicationYear', metadata.publicationYear )
+ assign( 'numberOfIssues', metadata.issueCount )
+ assign( 'comments', metadata.comments )
+ assign( 'genre', metadata.genre )
+ assign( 'volume', metadata.volumeNumber )
+ assign( 'numberOfVolumes', metadata.volumeCount )
+ assign( 'language', utils.getLanguageFromISO(metadata.language) )
+ assign( 'country', metadata.country )
+ assign( 'rating', metadata.criticalRating )
+ assign( 'credits', metadata.credits )
+ assign( 'tags', metadata.tags )
+
+ return cbi_container
+
+
+ def writeToExternalFile( self, filename, metadata ):
+
+ cbi_container = self.createJSONDictionary(metadata)
+
+ f = open(filename, 'w')
+ f.write(json.dumps(cbi_container, indent=4))
+ f.close
+
diff --git a/comicinfoxml.py b/comicinfoxml.py
new file mode 100644
index 0000000..ceeeb6e
--- /dev/null
+++ b/comicinfoxml.py
@@ -0,0 +1,248 @@
+"""
+A python class to encapsulate ComicRack's ComicInfo.xml data and file handling
+"""
+
+from datetime import datetime
+import zipfile
+from pprint import pprint
+import xml.etree.ElementTree as ET
+from genericmetadata import GenericMetadata
+
+
+class ComicInfoXml:
+
+ def metadataFromString( self, string ):
+
+ tree = ET.ElementTree(ET.fromstring( string ))
+ return self.convertXMLToMetadata( tree )
+
+ def stringFromMetadata( self, metadata ):
+
+ tree = self.convertMetadataToXML( self, metadata )
+ return 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("ComicInfo")
+
+
+ #helper func
+ def assign( cix_entry, md_entry):
+ if md_entry is not None:
+ ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
+
+ assign( 'Series', md.series )
+ assign( 'Number', md.issueNumber )
+ assign( 'Title', md.title )
+ assign( 'Count', md.issueCount )
+ assign( 'Volume', md.volumeNumber )
+ assign( 'AlternateSeries', md.alternateSeries )
+ assign( 'AlternateNumber', md.alternateNumber )
+ assign( 'AlternateCount', md.alternateCount )
+ assign( 'Summary', md.comments )
+ assign( 'Notes', md.notes )
+ assign( 'Year', md.publicationYear )
+ assign( 'Month', md.publicationMonth )
+ assign( 'Publisher', md.publisher )
+ assign( 'Imprint', md.imprint )
+ assign( 'Genre', md.genre )
+ assign( 'Web', md.webLink )
+ assign( 'PageCount', md.pageCount )
+ assign( 'Format', md.format )
+ assign( 'LanguageISO', md.language )
+ assign( 'Manga', md.manga )
+ assign( 'Characters', md.characters )
+ assign( 'Teams', md.teams )
+ assign( 'Locations', md.locations )
+ assign( 'ScanInformation', md.scanInfo )
+ assign( 'StoryArc', md.storyArc )
+ assign( 'SeriesGroup', md.seriesGroup )
+ assign( 'AgeRating', md.maturityRating )
+
+ if md.blackAndWhite is not None and md.blackAndWhite:
+ ET.SubElement(root, 'BlackAndWhite').text = "Yes"
+
+ # need to specially process the credits, since they are structured differently than CIX
+ credit_writer = None
+ credit_penciller = None
+ credit_inker = None
+ credit_colorist = None
+ credit_letterer = None
+ credit_cover = None
+ credit_editor = None
+
+ for credit in metadata.credits:
+ if credit['role'].title() in set( ['Writer', 'Plotter'] ):
+ if credit_writer == None:
+ credit_writer = ET.SubElement(root, 'Writer')
+ credit_writer.text = ""
+ if len(credit_writer.text) > 0:
+ credit_writer.text += ", "
+ credit_writer.text += credit['person']
+
+ if credit['role'].title() in set( [ 'Inker', 'Artist', 'Finishes' ] ):
+ if credit_inker == None:
+ credit_inker = ET.SubElement(root, 'Inker')
+ credit_inker.text = ""
+ if len(credit_inker.text) > 0:
+ credit_inker.text += ", "
+ credit_inker.text += credit['person']
+
+ if credit['role'].title() in set( [ 'Artist', 'Penciller', 'Penciler', 'Breakdowns' ] ):
+ if credit_penciller == None:
+ credit_penciller = ET.SubElement(root, 'Penciller')
+ credit_penciller.text = ""
+ if len(credit_penciller.text) > 0:
+ credit_penciller.text += ", "
+ credit_penciller.text += credit['person']
+
+ if credit['role'].title() in set( [ 'Colorist', 'Colourist' ]):
+ if credit_colorist == None:
+ credit_colorist = ET.SubElement(root, 'Colorist')
+ credit_colorist.text = ""
+ if len(credit_colorist.text) > 0:
+ credit_colorist.text += ", "
+ credit_colorist.text += credit['person']
+
+ if credit['role'].title() == 'Letterer':
+ if credit_letterer == None:
+ credit_letterer = ET.SubElement(root, 'Letterer')
+ credit_letterer.text = ""
+ if len(credit_letterer.text) > 0:
+ credit_letterer.text += ", "
+ credit_letterer.text += credit['person']
+
+ if credit['role'].title() in set( [ 'Cover', 'Covers', 'CoverArtist', 'Cover Artist' ] ):
+ if credit_cover == None:
+ credit_cover = ET.SubElement(root, 'CoverArtist')
+ credit_cover.text = ""
+ if len(credit_cover.text) > 0:
+ credit_cover.text += ", "
+ credit_cover.text += credit['person']
+
+ if credit['role'].title() in set( [ 'Editor'] ):
+ if credit_editor == None:
+ credit_editor = ET.SubElement(root, 'Editor')
+ credit_editor.text = ""
+ if len(credit_editor.text) > 0:
+ credit_editor.text += ", "
+ credit_editor.text += credit['person']
+
+ # !!!ATB todo: loop and add the page entries under pages node
+ #pages = ET.SubElement(root, 'Pages')
+
+ # 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 != 'ComicInfo':
+ 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.issueNumber = xlate( 'Number' )
+ md.issueCount = xlate( 'Count' )
+ md.volumeNumber = xlate( 'Volume' )
+ md.alternateSeries = xlate( 'AlternateSeries' )
+ md.alternateNumber = xlate( 'AlternateNumber' )
+ md.alternateCount = xlate( 'AlternateCount' )
+ md.comments = xlate( 'Summary' )
+ md.notes = xlate( 'Notes' )
+ md.publicationYear = xlate( 'Year' )
+ md.publicationMonth = xlate( 'Month' )
+ md.publisher = xlate( 'Publisher' )
+ md.imprint = xlate( 'Imprint' )
+ md.genre = xlate( 'Genre' )
+ md.webLink = xlate( 'Web' )
+ md.language = xlate( 'LanguageISO' )
+ md.format = xlate( 'Format' )
+ md.manga = xlate( 'Manga' )
+ md.characters = xlate( 'Characters' )
+ md.teams = xlate( 'Teams' )
+ md.locations = xlate( 'Locations' )
+ md.pageCount = xlate( 'PageCount' )
+ md.scanInfo = xlate( 'ScanInformation' )
+ md.storyArc = xlate( 'StoryArc' )
+ md.seriesGroup = xlate( 'SeriesGroup' )
+ md.maturityRating = xlate( 'AgeRating' )
+
+ tmp = xlate( 'BlackAndWhite' )
+ md.blackAndWhite = False
+ if tmp is not None and tmp.lower() in [ "yes", "true", "1" ]:
+ md.blackAndWhite = True
+ # 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'
+ ):
+ for name in n.text.split(','):
+ metadata.addCredit( name.strip(), n.tag )
+
+ if n.tag == 'CoverArtist':
+ for name in n.text.split(','):
+ metadata.addCredit( name.strip(), "Cover" )
+
+ #!!! ATB parse page data now
+
+
+
+ metadata.isEmpty = False
+
+ return metadata
+
+ 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 )
+
diff --git a/comicvinetalker.py b/comicvinetalker.py
new file mode 100644
index 0000000..9852330
--- /dev/null
+++ b/comicvinetalker.py
@@ -0,0 +1,186 @@
+import json
+from pprint import pprint
+import urllib2, urllib
+import math
+import re
+
+import utils
+
+from genericmetadata import GenericMetadata
+
+class ComicVineTalker:
+
+ api_key = '67ac5baeba16a2f56acf7ff136a1d591324c2253'
+
+ def searchForSeries( self, series_name ):
+
+ series_name = urllib.quote_plus(str(series_name))
+ search_url = "http://api.comicvine.com/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues&sort=start_year"
+
+ resp = urllib2.urlopen(search_url)
+ content = resp.read()
+
+ cv_response = json.loads(content)
+
+ if cv_response[ 'status_code' ] != 1:
+ print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
+ return None
+
+ search_results = list()
+
+ # see http://api.comicvine.com/documentation/#handling_responses
+
+ limit = cv_response['limit']
+ current_result_count = cv_response['number_of_page_results']
+ total_result_count = cv_response['number_of_total_results']
+
+ print ("Found {0} of {1} results".format( cv_response['number_of_page_results'], cv_response['number_of_total_results']))
+ search_results.extend( cv_response['results'])
+ offset = 0
+
+ # see if we need to keep asking for more pages...
+ while ( current_result_count < total_result_count ):
+ print ("getting another page of results...")
+ offset += limit
+ resp = urllib2.urlopen( search_url + "&offset="+str(offset) )
+ content = resp.read()
+
+ cv_response = json.loads(content)
+
+ if cv_response[ 'status_code' ] != 1:
+ print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
+ return None
+ search_results.extend( cv_response['results'])
+ current_result_count += cv_response['number_of_page_results']
+
+
+ #for record in search_results:
+ # print( "{0}: {1} ({2})".format(record['id'], smart_str(record['name']) , record['start_year'] ) )
+ # print( "{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year'] ) )
+
+ #print "{0}: {1} ({2})".format(search_results['results'][0]['id'], smart_str(search_results['results'][0]['name']) , search_results['results'][0]['start_year'] )
+
+ return search_results
+
+ def fetchVolumeData( self, series_id ):
+
+ volume_url = "http://api.comicvine.com/volume/" + str(series_id) + "/?api_key=" + self.api_key + "&format=json"
+ #print "search_url = : ", volume_url
+
+ resp = urllib2.urlopen(volume_url)
+ content = resp.read()
+
+ cv_response = json.loads(content)
+
+ if cv_response[ 'status_code' ] != 1:
+ print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
+ return None
+
+ volume_results = cv_response['results']
+
+ return volume_results
+
+
+ def fetchIssueData( self, series_id, issue_number ):
+
+ volume_results = self.fetchVolumeData( series_id )
+
+ found = False
+ for record in volume_results['issues']:
+ if float(record['issue_number']) == float(issue_number):
+ found = True
+ break
+
+ if (found):
+ issue_url = "http://api.comicvine.com/issue/" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
+ resp = urllib2.urlopen(issue_url)
+ content = resp.read()
+ cv_response = json.loads(content)
+ if cv_response[ 'status_code' ] != 1:
+ print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
+ return None
+ issue_results = cv_response['results']
+
+ else:
+ return None
+
+ # now, map the comicvine data to generic metadata
+ metadata = GenericMetadata()
+
+ metadata.series = issue_results['volume']['name']
+
+ # format the issue number string nicely, since it's usually something like "2.00"
+ num_f = float(issue_results['issue_number'])
+ num_s = str( int(math.floor(num_f)) )
+ if math.floor(num_f) != num_f:
+ num_s = str( num_f )
+
+ metadata.issueNumber = num_s
+ metadata.title = issue_results['name']
+ metadata.publisher = volume_results['publisher']['name']
+ metadata.publicationMonth = issue_results['publish_month']
+ metadata.publicationYear = issue_results['publish_year']
+ metadata.issueCount = volume_results['count_of_issues']
+ metadata.comments = self.cleanup_html(issue_results['description'])
+
+ metadata.notes = "Tagged with ComicTagger using info from Comic Vine:\n"
+ metadata.notes += issue_results['site_detail_url']
+
+ metadata.webLink = issue_results['site_detail_url']
+
+ person_credits = issue_results['person_credits']
+ for person in person_credits:
+ for role in person['roles']:
+ # can we determine 'primary' from CV??
+ role_name = role['role'].title()
+ metadata.addCredit( person['name'], role['role'].title(), False )
+
+ character_credits = issue_results['character_credits']
+ character_list = list()
+ for character in character_credits:
+ character_list.append( character['name'] )
+ metadata.characters = utils.listToString( character_list )
+
+ team_credits = issue_results['team_credits']
+ team_list = list()
+ for team in team_credits:
+ team_list.append( team['name'] )
+ metadata.teams = utils.listToString( team_list )
+
+ location_credits = issue_results['location_credits']
+ location_list = list()
+ for location in location_credits:
+ location_list.append( location['name'] )
+ metadata.locations = utils.listToString( location_list )
+
+ story_arc_credits = issue_results['story_arc_credits']
+ for arc in story_arc_credits:
+ metadata.storyArc = arc['name']
+ #just use the first one, if at all
+ break
+
+ return metadata
+
+ def cleanup_html( self, string):
+ p = re.compile(r'<[^<]*?>')
+
+ newstring = p.sub('',string)
+
+ newstring = newstring.replace(' ',' ')
+ newstring = newstring.replace('&','&')
+
+ return newstring
+
+ def fetchIssueCoverURL( self, issue_id ):
+
+ issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image"
+ resp = urllib2.urlopen(issue_url)
+ content = resp.read()
+ cv_response = json.loads(content)
+ if cv_response[ 'status_code' ] != 1:
+ print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
+ return None
+
+ return cv_response['results']['image']['super_url']
+
+
diff --git a/filenameparser.py b/filenameparser.py
new file mode 100644
index 0000000..2687767
--- /dev/null
+++ b/filenameparser.py
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+
+# This is horrible, and needs to be re-written, but, well, it mostly works!
+
+import re
+import os
+from urllib import unquote
+
+class FileNameParser:
+ def fixSpaces( self, string ):
+ placeholders = ['[-_]',' +']
+ for ph in placeholders:
+ string = re.sub(ph, ' ', string )
+ return string.strip()
+
+ # check for silly .1 or .5 style issue strings
+ # allow up to 5 chars total
+ def isPointIssue( self, word ):
+ ret = False
+ try:
+ float(word)
+ if (len(word) < 5 and not word.isdigit()):
+ ret = True
+ except ValueError:
+ pass
+ return ret
+
+ def getIssueNumber( self,filename ):
+
+ found = False
+ issue = ''
+ # guess based on position
+
+ # replace any name seperators with spaces
+ tmpstr = self.fixSpaces(filename)
+ word_list = tmpstr.split(' ')
+
+ # assume the last number in the filename that is under 4 digits is the issue number
+ for word in reversed(word_list):
+ if (
+ (word.isdigit() and len(word) < 4) or
+ (self.isPointIssue(word))
+ ):
+ issue = word
+ found = True
+ #print 'Assuming issue number is ' + str(issue) + ' based on the position.'
+ break
+
+ if not found:
+ # try a regex
+ issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]|\d+\.\d|\d+)', filename)
+ if issnum:
+ issue = issnum.group()
+ found = True
+ #print 'Got the issue using regex. Issue is ' + issue
+
+ return issue.strip()
+
+ def getSeriesName(self, filename, issue ):
+
+ # use the issue number string to split the filename string
+ # assume first element of list is the series name, plus cruft
+
+ #!!! this could fail in the case of small numerics in the series name!!!
+
+ tmpstr = self.fixSpaces(filename)
+ if issue != "":
+ series = tmpstr.split(issue)[0]
+ else:
+ # no issue to work off of
+ #!!! TODO we should look for the year, and split from that
+ # and if that doesn't exist, remove parenthetical words
+ series = tmpstr
+
+ volume = ""
+
+ series = series.rstrip("#")
+
+ # search for volume number
+ match = re.search('(?<=v)(\d+)\s*$', series)
+ if match:
+ volume = match.group()
+ series = series.split("v"+volume)[0]
+ volume = volume.lstrip("0")
+
+ return series.strip(), volume.strip()
+
+ def getYear( self,filename):
+
+ year = ""
+ # look for four digit number with "(" ")" or "--" around it
+ match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename)
+ if match:
+ year = match.group()
+ # remove non-numerics
+ year = re.sub("[^0-9]", "", year)
+ return year
+
+ self.issue = ""
+ self.series = ""
+ self.volume = ""
+ self.year = ""
+
+
+ def parseFilename( self, filename ):
+
+ # remove the path
+ filename = os.path.basename(filename)
+
+ # remove the extension
+ filename = os.path.splitext(filename)[0]
+
+ #url decvocde, just in case
+ filename = unquote(filename)
+
+ self.issue = self.getIssueNumber(filename)
+ self.series, self.volume = self.getSeriesName(filename, self.issue)
+ self.year = self.getYear(filename)
+
+ if self.issue != "":
+ # strip off leading zeros
+ self.issue = self.issue.lstrip("0")
+ if self.issue == "":
+ self.issue = "0"
diff --git a/genericmetadata.py b/genericmetadata.py
new file mode 100644
index 0000000..56f81fb
--- /dev/null
+++ b/genericmetadata.py
@@ -0,0 +1,96 @@
+"""
+ A python class for internal metadata storage
+
+ The goal of this class is to handle ALL the data that might come from various
+ tagging schemes and databases, such as ComicVine or GCD. This makes conversion
+ possible, however lossy it might be
+
+
+"""
+from sets import Set
+
+
+# These page info classes are exactly the same as the CIX scheme, since it's unique
+class PageType:
+ FrontCover = "FrontCover"
+ InnerCover = "InnerCover"
+ Roundup = "Roundup"
+ Story = "Story"
+ Advertisment = "Advertisment"
+ Story = "Story"
+ Editorial = "Editorial"
+ Letters = "Letters"
+ Preview = "Preview"
+ BackCover = "BackCover"
+ Other = "Other"
+ Deleted = "Deleted"
+
+class PageInfo:
+ SeqNum = 0
+ Type = PageType.FrontCover
+ DoublePage = False
+ ImageSize = 0
+ Key = ""
+ ImageWidth = 0
+ ImageHeight = 0
+
+
+class GenericMetadata:
+
+ def __init__(self):
+
+ self.isEmpty = True
+ self.tagOrigin = None
+
+ self.series = None
+ self.issueNumber = None
+ self.title = None
+ self.publisher = None
+ self.publicationMonth = None
+ self.publicationYear = None
+ self.issueCount = None
+ self.volumeNumber = None
+ self.genre = None
+ self.language = None # 2 letter iso code
+ self.comments = None # use same way as Summary in CIX
+
+ self.volumeCount = None
+ self.criticalRating = None
+ self.country = None
+
+ self.alternateSeries = None
+ self.alternateNumber = None
+ self.alternateCount = None
+ self.imprint = None
+ self.notes = None
+ self.webLink = None
+ self.format = None
+ self.manga = None
+ self.blackAndWhite = None
+ self.pageCount = None
+ self.maturityRating = None
+
+ self.storyArc = None
+ self.seriesGroup = None
+ self.scanInfo = None
+
+ self.characters = None
+ self.teams = None
+ self.locations = None
+
+ self.credits = list()
+ self.tags = list()
+ self.pages = list()
+
+
+ def addCredit( self, person, role, primary = False ):
+
+ credit = dict()
+ credit['person'] = person
+ credit['role'] = role
+ if primary:
+ credit['primary'] = primary
+
+ self.credits.append(credit)
+
+
\ No newline at end of file
diff --git a/issueselectionwindow.py b/issueselectionwindow.py
new file mode 100644
index 0000000..9451cda
--- /dev/null
+++ b/issueselectionwindow.py
@@ -0,0 +1,118 @@
+import sys
+from PyQt4 import QtCore, QtGui, uic
+
+from PyQt4.QtCore import QUrl
+from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
+
+from comicvinetalker import *
+
+class IssueSelectionWindow(QtGui.QDialog):
+
+ volume_id = 0
+
+ def __init__(self, parent, series_id, issue_number):
+ super(IssueSelectionWindow, self).__init__(parent)
+
+ uic.loadUi('issueselectionwindow.ui', self)
+
+ self.series_id = series_id
+
+ if issue_number is None or issue_number == "":
+ self.issue_number = 1
+ else:
+ self.issue_number = issue_number
+
+ self.initial_id = None
+ self.performQuery()
+
+ self.twList.resizeColumnsToContents()
+ self.twList.currentItemChanged.connect(self.currentItemChanged)
+ self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
+
+ #now that the list has been sorted, find the initial record, and select it
+ if self.initial_id is None:
+ self.twList.selectRow( 0 )
+ else:
+ for r in range(0, self.twList.rowCount()):
+ issue_id, b = self.twList.item( r, 0 ).data( QtCore.Qt.UserRole ).toInt()
+ if (issue_id == self.initial_id):
+ self.twList.selectRow( r )
+ break
+
+
+ def performQuery( self ):
+
+ while self.twList.rowCount() > 0:
+ self.twList.removeRow(0)
+
+ comicVine = ComicVineTalker()
+ volume_data = comicVine.fetchVolumeData( self.series_id )
+ self.issue_list = volume_data['issues']
+
+ self.twList.setSortingEnabled(False)
+
+ row = 0
+ for record in self.issue_list:
+ self.twList.insertRow(row)
+
+ item_text = record['issue_number']
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setData( QtCore.Qt.UserRole ,record['id'])
+ item.setData(QtCore.Qt.DisplayRole, float(item_text))
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 0, item)
+
+ item_text = u"{0}".format(record['name'])
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 1, item)
+
+ record['url'] = None
+
+ if float(record['issue_number']) == float(self.issue_number):
+ self.initial_id = record['id']
+
+ row += 1
+
+ #TODO look for given issue in list, and select that one
+
+ self.twList.setSortingEnabled(True)
+ self.twList.sortItems( 0 , QtCore.Qt.AscendingOrder )
+
+ def cellDoubleClicked( self, r, c ):
+ self.accept()
+
+ def currentItemChanged( self, curr, prev ):
+
+ if curr is None:
+ return
+ if prev is not None and prev.row() == curr.row():
+ return
+
+
+ self.issue_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
+
+ # list selection was changed, update the the issue cover
+ for record in self.issue_list:
+ if record['id'] == self.issue_id:
+
+ self.issue_number = record['issue_number']
+
+ # We don't yet have an image URL for this issue. Make a request for URL, and hold onto it
+ # TODO: this should be reworked... too much UI latency, maybe chain the NAMs??
+ if record['url'] == None:
+ record['url'] = ComicVineTalker().fetchIssueCoverURL( self.issue_id )
+
+ self.labelThumbnail.setText("loading...")
+ self.nam = QNetworkAccessManager()
+
+ self.nam.finished.connect(self.finishedImageRequest)
+ self.nam.get(QNetworkRequest(QUrl(record['url'])))
+ break
+
+ # called when the image is done loading
+ def finishedImageRequest(self, reply):
+ img = QtGui.QImage()
+ img.loadFromData(reply.readAll())
+ self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
+
\ No newline at end of file
diff --git a/issueselectionwindow.ui b/issueselectionwindow.ui
new file mode 100644
index 0000000..30fbce2
--- /dev/null
+++ b/issueselectionwindow.ui
@@ -0,0 +1,147 @@
+
+
+ dialogIssueSelect
+
+
+
+ 0
+ 0
+ 843
+ 454
+
+
+
+ Select Issue
+
+
+
+
+ 10
+ 20
+ 571
+ 392
+
+
+
+
+
+
+
+ 589
+ 9
+ 241
+ 391
+
+
+
+ QFrame::Panel
+
+
+ QFrame::Sunken
+
+
+
+
+
+ true
+
+
+
+
+
+ 10
+ 10
+ 569
+ 390
+
+
+
+
+ 9
+
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ 0
+
+
+ 2
+
+
+ true
+
+
+ false
+
+
+
+ Issue
+
+
+
+
+ Title
+
+
+ AlignHCenter|AlignVCenter|AlignCenter
+
+
+
+
+
+
+ 260
+ 420
+ 569
+ 27
+
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ dialogIssueSelect
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ dialogIssueSelect
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/options.py b/options.py
new file mode 100644
index 0000000..40e0957
--- /dev/null
+++ b/options.py
@@ -0,0 +1,65 @@
+import sys
+import getopt
+
+
+class Enum(set):
+ def __getattr__(self, name):
+ if name in self:
+ return name
+ raise AttributeError
+
+class MetaDataStyle:
+ CBI = 0
+ CIX = 1
+
+
+class Options:
+
+ def __init__(self):
+ self.data_style = MetaDataStyle.CBI
+ self.no_gui = False
+
+ # Some defaults for testing
+ self.series_name = '' #'Watchmen'
+ self.issue_number = '' #'1'
+ self.filename = '' # "Watchmen #01.cbz"
+
+ def parseCmdLineArgs(self):
+
+ # parse command line options
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "cht:s:i:vf:", ["cli", "help", "type=", "series=", "issue=", "verbose", "file" ])
+ except (getopt.error, msg):
+ print( msg )
+ print( "for help use --help" )
+ sys.exit(2)
+ # process options
+ for o, a in opts:
+ if o in ("-h", "--help"):
+ print( __doc__ )
+ sys.exit(0)
+ if o in ("-v", "--verbose"):
+ print( "Verbose output!" )
+ if o in ("-c", "--cli"):
+ self.no_gui = True
+ if o in ("-s", "--series"):
+ self.series_name = a
+ if o in ("-i", "--issue"):
+ self.issue_number = a
+ if o in ("-f", "--file"):
+ self.filename = a
+ if o in ("-t", "--type"):
+ if a == "cr":
+ self.data_style = MetaDataStyle.CIX
+ elif a == "cbl":
+ self.data_style = MetaDataStyle.CBI
+ else:
+ print( __doc__ )
+ sys.exit(0)
+
+ # process arguments
+ for arg in args:
+ process(arg) # process() is defined elsewhere
+
+ return opts
+
\ No newline at end of file
diff --git a/tagger.py b/tagger.py
new file mode 100755
index 0000000..033ee2f
--- /dev/null
+++ b/tagger.py
@@ -0,0 +1,88 @@
+#!/usr/bin/python
+
+"""
+A python script to tag CBZ files
+"""
+
+import sys
+import getopt
+import json
+import xml
+from pprint import pprint
+from PyQt4 import QtCore, QtGui
+import signal
+
+from taggerwindow import TaggerWindow
+from options import Options, MetaDataStyle
+from comicarchive import ComicArchive
+
+from comicvinetalker import ComicVineTalker
+from comicinfoxml import ComicInfoXml
+from comicbookinfo import ComicBookInfo
+
+#-----------------------------
+def cliProcedure( opts ):
+
+ comicVine = ComicVineTalker()
+
+ cv_search_results = comicVine.searchForSeries( opts.series_name )
+
+ #error checking here: did we get any results?
+
+ # we will eventualy want user interaction to choose the appropriate result, but for now, assume the first one
+ series_id = cv_search_results[0]['id']
+
+ print( "-->Auto-selecting volume ID:", cv_search_results[0]['id'] )
+ print(" ")
+
+ # now get the particular issue data
+ metadata = comicVine.fetchIssueData( series_id, opts.issue_number )
+
+ #pprint( cv_volume_data, indent=4 )
+
+ ca = ComicArchive(opts.filename)
+ ca.writeMetadata( metadata, opts.data_style )
+
+ #debugging
+ ComicBookInfo().writeToExternalFile( "test.json" )
+ ComicBookInfo().writeToExternalFile( "test.xml" )
+
+#-----------------------------
+
+def main():
+ opts = Options()
+ opts.parseCmdLineArgs()
+
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+ #ca = ComicArchive( opts.filename )
+
+ #metadata = ca.readMetadata( MetaDataStyle.CBI )
+ #ca.writeMetadata( metadata, MetaDataStyle.CIX )
+
+ #ComicInfoXml().writeToExternalFile( "test.xml", metadata )
+ #ComicBookInfo().writeToExternalFile("test.json", metadata)
+
+ #quit()
+
+
+
+
+ if opts.no_gui:
+
+ cliProcedure( opts )
+
+ else:
+
+ app = QtGui.QApplication(sys.argv)
+ tagger_window = TaggerWindow( opts )
+ tagger_window.show()
+ sys.exit(app.exec_())
+
+if __name__ == "__main__":
+ main()
+
+
+
+
+
diff --git a/taggerwindow.py b/taggerwindow.py
new file mode 100644
index 0000000..1dc5ccc
--- /dev/null
+++ b/taggerwindow.py
@@ -0,0 +1,480 @@
+
+from PyQt4 import QtCore, QtGui, uic
+
+from volumeselectionwindow import VolumeSelectionWindow
+from options import Options, MetaDataStyle
+from genericmetadata import GenericMetadata
+from comicvinetalker import ComicVineTalker
+from comicarchive import ComicArchive
+import utils
+import locale
+# this reads the environment and inits the right locale
+locale.setlocale(locale.LC_ALL, "")
+
+
+import os
+class TaggerWindow( QtGui.QMainWindow):
+
+ appName = "ComicTagger"
+
+ def __init__(self, opts , parent = None):
+ super(TaggerWindow, self).__init__(parent)
+
+ uic.loadUi('taggerwindow.ui', self)
+ self.setWindowIcon(QtGui.QIcon('app.png'))
+ self.center()
+ self.raise_()
+
+ self.opts = opts
+ self.data_style = opts.data_style
+
+ #set up a default metadata object
+ self.metadata = GenericMetadata()
+ self.comic_archive = None
+
+ self.configMenus()
+ self.statusBar()
+ self.updateAppTitle()
+ self.setAcceptDrops(True)
+ self.droppedFile=None
+
+
+ self.populateComboBoxes()
+
+ # hook up the callbacks
+ self.cbDataStyle.currentIndexChanged.connect(self.setDataStyle)
+
+ self.updateStyleTweaks()
+
+
+ self.openArchive( opts.filename )
+
+ # fill in some explicit metadata stuff from our options
+ # this overrides what we just read in
+ if self.metadata.series is None:
+ self.metadata.series = opts.series_name
+ if self.metadata.issueNumber is None:
+ self.metadata.issueNumber = opts.issue_number
+
+
+ def updateAppTitle( self ):
+ if self.comic_archive is None:
+ self.setWindowTitle( self.appName )
+ else:
+ self.setWindowTitle( self.appName + " - " + self.comic_archive.path)
+
+ def configMenus( self):
+
+ # File Menu
+ self.actionExit.setShortcut( 'Ctrl+Q' )
+ self.actionExit.setStatusTip( 'Exit application' )
+ self.actionExit.triggered.connect( QtGui.qApp.quit )
+
+ self.actionLoad.setShortcut( 'Ctrl+O' )
+ self.actionLoad.setStatusTip( 'Load comic archive' )
+ self.actionLoad.triggered.connect( self.selectFile )
+
+ self.actionWrite_Tags.setShortcut( 'Ctrl+S' )
+ self.actionWrite_Tags.setStatusTip( 'Save tags to comic archive' )
+ self.actionWrite_Tags.triggered.connect( self.commitMetadata )
+
+ #self.actionRepackage.setShortcut( )
+ self.actionRepackage.setStatusTip( 'Re-create archive as CBZ' )
+ self.actionRepackage.triggered.connect( self.repackageArchive )
+
+ # Tag Menu
+ self.actionParse_Filename.setShortcut( 'Ctrl+F' )
+ self.actionParse_Filename.setStatusTip( 'Try to extract tags from filename' )
+ self.actionParse_Filename.triggered.connect( self.useFilename )
+
+ self.actionQuery_Online.setShortcut( 'Ctrl+W' )
+ self.actionQuery_Online.setStatusTip( 'Search online for tags' )
+ self.actionQuery_Online.triggered.connect( self.queryOnline )
+
+ # Help Menu
+ self.actionAbout.setShortcut( 'Ctrl+A' )
+ self.actionAbout.setStatusTip( 'Show the ' + self.appName + ' info' )
+ self.actionAbout.triggered.connect( self.aboutApp )
+
+ # ToolBar
+
+ self.toolBar.addAction( self.actionLoad )
+ self.toolBar.addAction( self.actionWrite_Tags )
+ self.toolBar.addAction( self.actionParse_Filename )
+ self.toolBar.addAction( self.actionQuery_Online )
+
+
+ def repackageArchive( self ):
+ QtGui.QMessageBox.information(self, self.tr("Repackage Comic Archive"), self.tr("TBD"))
+
+ def aboutApp( self ):
+ QtGui.QMessageBox.information(self, self.tr("About " + self.appName ), self.tr(self.appName + " 0.1"))
+
+
+ def dragEnterEvent(self, event):
+ self.droppedFile=None
+ if event.mimeData().hasUrls():
+ url=event.mimeData().urls()[0]
+ if url.isValid():
+ if url.scheme()=="file":
+ self.droppedFile=url.toLocalFile()
+ event.accept()
+
+ def dropEvent(self, event):
+ #print self.droppedFile # displays the file name
+ self.openArchive( str(self.droppedFile) )
+
+ def openArchive( self, path ):
+
+ if path is None or path == "":
+ return
+
+ ca = ComicArchive( path )
+
+ if ca is not None and ca.seemsToBeAComicArchive():
+
+ self.comic_archive = ca
+
+ self.metadata = self.comic_archive.readMetadata( self.data_style )
+
+ if self.metadata.isEmpty:
+ self.metadata = self.comic_archive.metadataFromFilename( )
+
+ image_data = self.comic_archive.getCoverPage()
+ if not image_data is None:
+ img = QtGui.QImage()
+ img.loadFromData( image_data )
+ self.lblCover.setPixmap(QtGui.QPixmap(img))
+ self.lblCover.setScaledContents(True)
+
+ #!!!ATB should I clear the form???
+ self.metadataToForm()
+ self.updateAppTitle()
+ self.updateInfoBox()
+
+
+ else:
+ QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("That file doesn't appear to be a comic archive!"))
+
+ def updateInfoBox( self ):
+
+ ca = self.comic_archive
+ info_text = os.path.basename( ca.path ) + "\n"
+ info_text += str(ca.getNumberOfPages()) + " pages \n"
+ if ca.hasCIX():
+ info_text += "* ComicRack tags\n"
+ if ca.hasCBI():
+ info_text += "* ComicBookLover tags\n"
+
+ self.lblArchiveInfo.setText( info_text )
+
+ def metadataToForm( self ):
+ # copy the the metadata object into to the form
+
+ #helper func
+ def assignText( field, value):
+ if value is not None:
+ field.setText( u"{0}".format(value) )
+
+ md = self.metadata
+
+ assignText( self.leSeries, md.series )
+ assignText( self.leIssueNum, md.issueNumber )
+ assignText( self.leIssueCount, md.issueCount )
+ assignText( self.leVolumeNum, md.volumeNumber )
+ assignText( self.leVolumeCount, md.volumeCount )
+ assignText( self.leTitle, md.title )
+ assignText( self.lePublisher, md.publisher )
+ assignText( self.lePubMonth, md.publicationMonth )
+ assignText( self.lePubYear, md.publicationYear )
+ assignText( self.leGenre, md.genre )
+ assignText( self.leImprint, md.imprint )
+ assignText( self.teComments, md.comments )
+ assignText( self.teNotes, md.notes )
+ assignText( self.leCriticalRating, md.criticalRating )
+ assignText( self.leMaturityRating, md.maturityRating )
+ assignText( self.leStoryArc, md.storyArc )
+ assignText( self.leScanInfo, md.scanInfo )
+ assignText( self.leSeriesGroup, md.seriesGroup )
+ assignText( self.leAltSeries, md.alternateSeries )
+ assignText( self.leAltIssueNum, md.alternateNumber )
+ assignText( self.leAltIssueCount, md.alternateCount )
+ assignText( self.leWebLink, md.webLink )
+ assignText( self.teCharacters, md.characters )
+ assignText( self.teTeams, md.teams )
+ assignText( self.teLocations, md.locations )
+ assignText( self.leFormat, md.format )
+
+ if md.language is not None:
+ i = self.cbLanguage.findData( md.language )
+ self.cbLanguage.setCurrentIndex( i )
+
+ if md.country is not None:
+ i = self.cbCountry.findText( md.country )
+ self.cbCountry.setCurrentIndex( i )
+
+ if md.manga is not None:
+ i = self.cbManga.findData( md.manga )
+ self.cbManga.setCurrentIndex( i )
+
+ if md.blackAndWhite is not None and md.blackAndWhite:
+ self.cbBW.setChecked( True )
+
+ assignText( self.teTags, utils.listToString( md.tags ) )
+
+ # !!! Should we clear the credits table or just avoid duplicates?
+ while self.twCredits.rowCount() > 0:
+ self.twCredits.removeRow(0)
+
+ if md.credits is not None and len(md.credits) != 0:
+
+ self.twCredits.setSortingEnabled( False )
+
+ row = 0
+ for credit in md.credits:
+
+ # before we add the credit, see if the role-person pair already exists:
+ r = 0
+ while r < self.twCredits.rowCount():
+ if ( self.twCredits.item(r, 0).text() == credit['role'].title() and
+ self.twCredits.item(r, 1).text() == credit['person'] ):
+ break
+ r = r + 1
+
+ # if we didn't make it through the table, it's there alread, so continue without adding
+ if ( r != self.twCredits.rowCount() ):
+ continue
+
+ self.twCredits.insertRow(row)
+
+ item_text = credit['role'].title()
+ item = QtGui.QTableWidgetItem(item_text)
+ #item.setData( QtCore.Qt.UserRole ,record['id'])
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twCredits.setItem(row, 0, item)
+
+ item_text = credit['person']
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twCredits.setItem(row, 1, item)
+
+ row += 1
+
+ self.twCredits.setSortingEnabled( True )
+
+
+ def formToMetadata( self ):
+
+ #helper func
+ def xlate( data, type_str):
+ s = u"{0}".format(data).strip()
+ if s == "":
+ return None
+ elif type_str == "str":
+ return s
+ else:
+ return int(s)
+
+ # copy the data from the form into the metadata
+ md = self.metadata
+ md.series = xlate( self.leSeries.text(), "str" )
+ md.issueNumber = xlate( self.leIssueNum.text(), "str" )
+ md.issueCount = xlate( self.leIssueCount.text(), "int" )
+ md.volumeNumber = xlate( self.leVolumeNum.text(), "int" )
+ md.volumeCount = xlate( self.leVolumeCount.text(), "int" )
+ md.title = xlate( self.leTitle.text(), "str" )
+ md.publisher = xlate( self.lePublisher.text(), "str" )
+ md.publicationMonth = xlate( self.lePubMonth.text(), "int" )
+ md.publicationYear = xlate( self.lePubYear.text(), "int" )
+ md.genre = xlate( self.leGenre.text(), "str" )
+ md.imprint = xlate( self.leImprint.text(), "str" )
+ md.comments = xlate( self.teComments.toPlainText(), "str" )
+ md.notes = xlate( self.teNotes.toPlainText(), "str" )
+ md.criticalRating = xlate( self.leCriticalRating.text(), "int" )
+ md.maturityRating = xlate( self.leMaturityRating.text(), "str" )
+
+ md.storyArc = xlate( self.leStoryArc.text(), "str" )
+ md.scanInfo = xlate( self.leScanInfo.text(), "str" )
+ md.seriesGroup = xlate( self.leSeriesGroup.text(), "str" )
+ md.alternateSeries = xlate( self.leAltSeries.text(), "str" )
+ md.alternateNumber = xlate( self.leAltIssueNum.text(), "int" )
+ md.alternateCount = xlate( self.leAltIssueCount.text(), "int" )
+ md.webLink = xlate( self.leWebLink.text(), "str" )
+ md.characters = xlate( self.teCharacters.toPlainText(), "str" )
+ md.teams = xlate( self.teTeams.toPlainText(), "str" )
+ md.locations = xlate( self.teLocations.toPlainText(), "str" )
+
+ md.format = xlate( self.leFormat.text(), "str" )
+ md.country = xlate( self.cbCountry.currentText(), "str" )
+
+ langiso = self.cbLanguage.itemData(self.cbLanguage.currentIndex()).toString()
+ md.language = xlate( langiso, "str" )
+
+ manga_code = self.cbManga.itemData(self.cbManga.currentIndex()).toString()
+ md.manga = xlate( manga_code, "str" )
+
+ # Make a list from the coma delimited tags string
+ tmp = xlate( self.teTags.toPlainText(), "str" )
+ if tmp != None:
+ def striplist(l):
+ return([x.strip() for x in l])
+
+ md.tags = striplist(tmp.split( "," ))
+
+ if ( self.cbBW.isChecked() ):
+ md.blackAndWhite = True
+ else:
+ md.blackAndWhite = False
+
+
+ def useFilename( self ):
+ self.metadata = self.comic_archive.metadataFromFilename( )
+ self.metadataToForm()
+
+
+ def selectFile( self ):
+ path = str(QtGui.QFileDialog.getOpenFileName())
+ self.openArchive( path )
+
+ def queryOnline(self):
+
+ if str(self.leSeries.text()).strip() != "":
+ series_name = str(self.leSeries.text()).strip()
+ else:
+ QtGui.QMessageBox.information(self, self.tr("Whoops"), self.tr("Need to enter a series name to query."))
+ return
+
+ issue_number = str(self.leIssueNum.text()).strip()
+
+ selector = VolumeSelectionWindow( self, series_name, issue_number )
+ selector.setModal(True)
+ selector.exec_()
+
+ if selector.result():
+ #we should now have a volume ID
+
+ comicVine = ComicVineTalker()
+ self.metadata = comicVine.fetchIssueData( selector.volume_id, selector.issue_number )
+
+ # Now push the right data into the edit controls
+ self.metadataToForm()
+ #!!!ATB should I clear the form???
+
+ def commitMetadata(self):
+
+ if (not self.metadata is None and not self.comic_archive is None):
+ self.formToMetadata()
+ self.comic_archive.writeMetadata( self.metadata, self.data_style )
+ self.updateInfoBox()
+
+ QtGui.QMessageBox.information(self, self.tr("Yeah!"), self.tr("File written."))
+
+
+ else:
+ QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("No data to commit!"))
+
+
+ def setDataStyle(self, s):
+ self.data_style, b = self.cbDataStyle.itemData(s).toInt()
+ self.updateStyleTweaks()
+
+
+ def updateStyleTweaks( self ):
+
+ # depending on the current data style, certain fields are disabled
+
+ inactive_color = QtGui.QColor(255, 170, 150)
+ active_palette = self.leSeries.palette()
+
+ inactive_palette1 = self.leSeries.palette()
+ inactive_palette1.setColor(QtGui.QPalette.Base, inactive_color)
+
+ inactive_palette2 = self.leSeries.palette()
+
+ inactive_palette3 = self.leSeries.palette()
+ inactive_palette3.setColor(QtGui.QPalette.Base, inactive_color)
+
+ inactive_palette3.setColor(QtGui.QPalette.Base, inactive_color)
+
+ #helper func
+ def enableWidget( item, enable ):
+ inactive_palette3.setColor(item.backgroundRole(), inactive_color)
+ inactive_palette2.setColor(item.backgroundRole(), inactive_color)
+ inactive_palette3.setColor(item.foregroundRole(), inactive_color)
+
+ if enable:
+ item.setPalette(active_palette)
+ if type(item) == QtGui.QCheckBox:
+ item.setEnabled( True )
+ elif type(item) == QtGui.QComboBox:
+ item.setEnabled( True )
+ else:
+ item.setReadOnly( False )
+ else:
+ if type(item) == QtGui.QCheckBox:
+ item.setPalette(inactive_palette2)
+ item.setEnabled( False )
+ elif type(item) == QtGui.QComboBox:
+ item.setPalette(inactive_palette3)
+ item.setEnabled( False )
+ else:
+ item.setReadOnly( True )
+ item.setPalette(inactive_palette1)
+
+
+ cbi_only = [ self.leVolumeCount, self.cbCountry, self.leCriticalRating, self.teTags ]
+ cix_only = [
+ self.leImprint, self.teNotes, self.cbBW, self.cbManga,
+ self.leStoryArc, self.leScanInfo, self.leSeriesGroup,
+ self.leAltSeries, self.leAltIssueNum, self.leAltIssueCount,
+ self.leWebLink, self.teCharacters, self.teTeams,
+ self.teLocations, self.leMaturityRating, self.leFormat
+ ]
+
+ if self.data_style == MetaDataStyle.CIX:
+ for item in cix_only:
+ enableWidget( item, True )
+ for item in cbi_only:
+ enableWidget(item, False )
+
+ if self.data_style == MetaDataStyle.CBI:
+ for item in cbi_only:
+ enableWidget( item, True )
+ for item in cix_only:
+ enableWidget(item, False )
+
+
+
+ def center(self):
+ screen = QtGui.QDesktopWidget().screenGeometry()
+ size = self.geometry()
+ self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)
+
+ def populateComboBoxes( self ):
+
+ # Add the entries to the tag style combobox
+ self.cbDataStyle.addItem( "ComicBookLover", MetaDataStyle.CBI )
+ self.cbDataStyle.addItem( "ComicRack", MetaDataStyle.CIX )
+
+ # select the current style
+ if ( self.data_style == MetaDataStyle.CBI ):
+ self.cbDataStyle.setCurrentIndex ( 0 )
+ elif ( self.data_style == MetaDataStyle.CIX ):
+ self.cbDataStyle.setCurrentIndex ( 1 )
+
+ # Add the entries to the country combobox
+ self.cbCountry.addItem( "", "" )
+ for c in utils.countries:
+ self.cbCountry.addItem( c[1], c[0] )
+
+ # Add the entries to the language combobox
+ self.cbLanguage.addItem( "", "" )
+ lang_dict = utils.getLanguageDict()
+ for key in sorted(lang_dict, cmp=locale.strcoll, key=lang_dict.get):
+ self.cbLanguage.addItem( lang_dict[key], key )
+
+ # Add the entries to the manga combobox
+ self.cbManga.addItem( "", "" )
+ self.cbManga.addItem( "Yes", "Yes" )
+ self.cbManga.addItem( "Yes (Right to Left)", "YesAndRightToLeft" )
+ self.cbManga.addItem( "No", "No" )
diff --git a/taggerwindow.ui b/taggerwindow.ui
new file mode 100644
index 0000000..ce0485e
--- /dev/null
+++ b/taggerwindow.ui
@@ -0,0 +1,764 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 943
+ 507
+
+
+
+ ComicTagger
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+ 20
+ 150
+ 211
+ 291
+
+
+
+ QFrame::Panel
+
+
+ QFrame::Sunken
+
+
+
+
+
+
+
+
+ 20
+ 20
+ 211
+ 32
+
+
+
+ -
+
+
+ Tag Style:
+
+
+
+ -
+
+
+
+
+
+
+
+ 250
+ 10
+ 671
+ 431
+
+
+
+
+ QLayout::SetDefaultConstraint
+
+ -
+
+
+ 0
+
+
+
+ Details
+
+
+
+
+ 10
+ 0
+ 371
+ 284
+
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+
-
+
+
+ Series
+
+
+
+ -
+
+
+ -
+
+
+ Title
+
+
+
+ -
+
+
+ -
+
+
+ Publisher
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ Series Group
+
+
+
+ -
+
+
+ -
+
+
+ Imprint
+
+
+
+ -
+
+
+ -
+
+
+ Story Arc
+
+
+
+ -
+
+
+ -
+
+
+ Genre
+
+
+
+ -
+
+
+ Format
+
+
+
+ -
+
+
+
+
+
+
+
+ 390
+ 0
+ 151
+ 140
+
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+ -
+
+
+ Issue
+
+
+
+ -
+
+
+ -
+
+
+ # Issues
+
+
+
+ -
+
+
+ -
+
+
+ Volume
+
+
+
+ -
+
+
+ -
+
+
+ # Volumes
+
+
+
+ -
+
+
+
+
+
+
+
+ 390
+ 140
+ 112
+ 71
+
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+ -
+
+
+ -
+
+
+ Month
+
+
+
+ -
+
+
+ -
+
+
+ Year
+
+
+
+
+
+
+
+
+ 390
+ 210
+ 251
+ 81
+
+
+
+ -
+
+
+ -
+
+
+ Language
+
+
+
+ -
+
+
+ -
+
+
+ Country
+
+
+
+
+
+
+
+
+ 550
+ 0
+ 103
+ 120
+
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+
+ QFormLayout::WrapAllRows
+
+ -
+
+
+ Maturity Rating
+
+
+
+ -
+
+
+ -
+
+
+ Critical Rating
+
+
+
+ -
+
+
+
+
+
+
+
+ 10
+ 290
+ 371
+ 111
+
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+ -
+
+
+ Alt. Series
+
+
+
+ -
+
+
+ -
+
+
+ Alt. Issue
+
+
+
+ -
+
+
+ -
+
+
+ Alt. # Issues
+
+
+
+ -
+
+
+
+
+
+
+
+ 390
+ 300
+ 211
+ 71
+
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Manga
+
+
+
+ -
+
+
+ true
+
+
+ Black && White
+
+
+
+
+
+
+
+
+ Credits
+
+
+
+
+ 30
+ 350
+ 481
+ 32
+
+
+
+
+ QFormLayout::ExpandingFieldsGrow
+
+ -
+
+
+ Scan Info
+
+
+
+ -
+
+
+
+
+
+
+
+ 30
+ 10
+ 481
+ 321
+
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ 0
+
+
+ 2
+
+
+ true
+
+
+ false
+
+
+
+ Credit
+
+
+
+
+ Name
+
+
+
+
+
+
+ 520
+ 20
+ 121
+ 30
+
+
+
+ Add Credit
+
+
+
+
+
+ 520
+ 60
+ 121
+ 30
+
+
+
+ Remove Credit
+
+
+
+
+
+ 520
+ 100
+ 121
+ 30
+
+
+
+ Edit Credit
+
+
+
+
+
+ Notes
+
+
+
+
+ 30
+ 10
+ 591
+ 371
+
+
+
+
+ QFormLayout::ExpandingFieldsGrow
+
+ -
+
+
+ Comments
+
+
+
+ -
+
+
+ Notes
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ Web
+
+
+
+
+
+
+
+
+ Other
+
+
+
+
+ 20
+ 17
+ 581
+ 371
+
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+ -
+
+
+ Teams
+
+
+
+ -
+
+
+ Characters
+
+
+
+ -
+
+
+ Locations
+
+
+
+ -
+
+
+ Other Tags
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+ Pages
+
+
+
+
+
+
+
+
+
+ 20
+ 60
+ 211
+ 81
+
+
+
+ QFrame::Panel
+
+
+ QFrame::Sunken
+
+
+
+
+
+
+
+
+
+ toolBar
+
+
+ false
+
+
+ false
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+ Open
+
+
+
+
+ Save Tags
+
+
+
+
+ Repackage
+
+
+
+
+ Exit
+
+
+
+
+ About ComicTagger
+
+
+
+
+ Parse Filename
+
+
+
+
+ Query Online
+
+
+
+
+
+
+
diff --git a/test.ui b/test.ui
new file mode 100644
index 0000000..f7eaa49
--- /dev/null
+++ b/test.ui
@@ -0,0 +1,311 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 943
+ 565
+
+
+
+ ComicTagger
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+ 50
+ 20
+ 251
+ 60
+
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+ -
+
+
+ Tag Style:
+
+
+
+ -
+
+
+
+
+
+
+
+ 590
+ 170
+ 181
+ 27
+
+
+
+ Query Online
+
+
+
+
+
+ 570
+ 20
+ 111
+ 27
+
+
+
+ Save Tags
+
+
+
+
+
+ 590
+ 260
+ 181
+ 51
+
+
+
+ QFrame::Panel
+
+
+ QFrame::Sunken
+
+
+
+
+
+
+
+
+ 40
+ 80
+ 461
+ 411
+
+
+
+ 0
+
+
+
+
+
+ 130
+ 380
+ 66
+ 17
+
+
+
+ CR Entry
+
+
+
+
+
+
+
+ 30
+ 10
+ 361
+ 351
+
+
+
+ 1
+
+
+
+ Tab 1
+
+
+
+
+ 50
+ 20
+ 261
+ 291
+
+
+
+ -
+
+
+ Issue
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ Series
+
+
+
+ -
+
+
+ Publication Year
+
+
+
+ -
+
+
+ -
+
+
+ Publication Month
+
+
+
+ -
+
+
+
+
+
+
+
+ Tab 2
+
+
+
+
+ 40
+ 10
+ 261
+ 291
+
+
+
+ -
+
+
+ Notes
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ Comments
+
+
+
+ -
+
+
+ Tags
+
+
+
+ -
+
+
+ -
+
+
+ Credits
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+ 120
+ 370
+ 66
+ 17
+
+
+
+ CBL Entry
+
+
+
+
+
+
+
+ 620
+ 90
+ 113
+ 27
+
+
+
+
+
+
+ 585
+ 371
+ 221
+ 91
+
+
+
+
+
+
+
+
+
+
+
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..c89b01d
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,450 @@
+# coding=utf-8
+
+def listToString( l ):
+ string = ""
+ if l is not None:
+ for item in l:
+ if len(string) > 0:
+ string += ", "
+ string += item
+ return string
+
+
+
+# -o- coding: utf-8 -o-
+# ISO639 python dict
+# oficial list in http://www.loc.gov/standards/iso639-2/php/code_list.php
+
+lang_dict = {
+ 'ab': 'Abkhaz',
+ 'aa': 'Afar',
+ 'af': 'Afrikaans',
+ 'ak': 'Akan',
+ 'sq': 'Albanian',
+ 'am': 'Amharic',
+ 'ar': 'Arabic',
+ 'an': 'Aragonese',
+ 'hy': 'Armenian',
+ 'as': 'Assamese',
+ 'av': 'Avaric',
+ 'ae': 'Avestan',
+ 'ay': 'Aymara',
+ 'az': 'Azerbaijani',
+ 'bm': 'Bambara',
+ 'ba': 'Bashkir',
+ 'eu': 'Basque',
+ 'be': 'Belarusian',
+ 'bn': 'Bengali',
+ 'bh': 'Bihari',
+ 'bi': 'Bislama',
+ 'bs': 'Bosnian',
+ 'br': 'Breton',
+ 'bg': 'Bulgarian',
+ 'my': 'Burmese',
+ 'ca': 'Catalan; Valencian',
+ 'ch': 'Chamorro',
+ 'ce': 'Chechen',
+ 'ny': 'Chichewa; Chewa; Nyanja',
+ 'zh': 'Chinese',
+ 'cv': 'Chuvash',
+ 'kw': 'Cornish',
+ 'co': 'Corsican',
+ 'cr': 'Cree',
+ 'hr': 'Croatian',
+ 'cs': 'Czech',
+ 'da': 'Danish',
+ 'dv': 'Divehi; Maldivian;',
+ 'nl': 'Dutch',
+ 'dz': 'Dzongkha',
+ 'en': 'English',
+ 'eo': 'Esperanto',
+ 'et': 'Estonian',
+ 'ee': 'Ewe',
+ 'fo': 'Faroese',
+ 'fj': 'Fijian',
+ 'fi': 'Finnish',
+ 'fr': 'French',
+ 'ff': 'Fula',
+ 'gl': 'Galician',
+ 'ka': 'Georgian',
+ 'de': 'German',
+ 'el': 'Greek, Modern',
+ 'gn': 'Guaraní',
+ 'gu': 'Gujarati',
+ 'ht': 'Haitian',
+ 'ha': 'Hausa',
+ 'he': 'Hebrew (modern)',
+ 'hz': 'Herero',
+ 'hi': 'Hindi',
+ 'ho': 'Hiri Motu',
+ 'hu': 'Hungarian',
+ 'ia': 'Interlingua',
+ 'id': 'Indonesian',
+ 'ie': 'Interlingue',
+ 'ga': 'Irish',
+ 'ig': 'Igbo',
+ 'ik': 'Inupiaq',
+ 'io': 'Ido',
+ 'is': 'Icelandic',
+ 'it': 'Italian',
+ 'iu': 'Inuktitut',
+ 'ja': 'Japanese',
+ 'jv': 'Javanese',
+ 'kl': 'Kalaallisut',
+ 'kn': 'Kannada',
+ 'kr': 'Kanuri',
+ 'ks': 'Kashmiri',
+ 'kk': 'Kazakh',
+ 'km': 'Khmer',
+ 'ki': 'Kikuyu, Gikuyu',
+ 'rw': 'Kinyarwanda',
+ 'ky': 'Kirghiz, Kyrgyz',
+ 'kv': 'Komi',
+ 'kg': 'Kongo',
+ 'ko': 'Korean',
+ 'ku': 'Kurdish',
+ 'kj': 'Kwanyama, Kuanyama',
+ 'la': 'Latin',
+ 'lb': 'Luxembourgish',
+ 'lg': 'Luganda',
+ 'li': 'Limburgish',
+ 'ln': 'Lingala',
+ 'lo': 'Lao',
+ 'lt': 'Lithuanian',
+ 'lu': 'Luba-Katanga',
+ 'lv': 'Latvian',
+ 'gv': 'Manx',
+ 'mk': 'Macedonian',
+ 'mg': 'Malagasy',
+ 'ms': 'Malay',
+ 'ml': 'Malayalam',
+ 'mt': 'Maltese',
+ 'mi': 'Māori',
+ 'mr': 'Marathi (Marāṭhī)',
+ 'mh': 'Marshallese',
+ 'mn': 'Mongolian',
+ 'na': 'Nauru',
+ 'nv': 'Navajo, Navaho',
+ 'nb': 'Norwegian Bokmål',
+ 'nd': 'North Ndebele',
+ 'ne': 'Nepali',
+ 'ng': 'Ndonga',
+ 'nn': 'Norwegian Nynorsk',
+ 'no': 'Norwegian',
+ 'ii': 'Nuosu',
+ 'nr': 'South Ndebele',
+ 'oc': 'Occitan',
+ 'oj': 'Ojibwe, Ojibwa',
+ 'cu': 'Old Church Slavonic',
+ 'om': 'Oromo',
+ 'or': 'Oriya',
+ 'os': 'Ossetian, Ossetic',
+ 'pa': 'Panjabi, Punjabi',
+ 'pi': 'Pāli',
+ 'fa': 'Persian',
+ 'pl': 'Polish',
+ 'ps': 'Pashto, Pushto',
+ 'pt': 'Portuguese',
+ 'qu': 'Quechua',
+ 'rm': 'Romansh',
+ 'rn': 'Kirundi',
+ 'ro': 'Romanian, Moldavan',
+ 'ru': 'Russian',
+ 'sa': 'Sanskrit (Saṁskṛta)',
+ 'sc': 'Sardinian',
+ 'sd': 'Sindhi',
+ 'se': 'Northern Sami',
+ 'sm': 'Samoan',
+ 'sg': 'Sango',
+ 'sr': 'Serbian',
+ 'gd': 'Scottish Gaelic',
+ 'sn': 'Shona',
+ 'si': 'Sinhala, Sinhalese',
+ 'sk': 'Slovak',
+ 'sl': 'Slovene',
+ 'so': 'Somali',
+ 'st': 'Southern Sotho',
+ 'es': 'Spanish; Castilian',
+ 'su': 'Sundanese',
+ 'sw': 'Swahili',
+ 'ss': 'Swati',
+ 'sv': 'Swedish',
+ 'ta': 'Tamil',
+ 'te': 'Telugu',
+ 'tg': 'Tajik',
+ 'th': 'Thai',
+ 'ti': 'Tigrinya',
+ 'bo': 'Tibetan',
+ 'tk': 'Turkmen',
+ 'tl': 'Tagalog',
+ 'tn': 'Tswana',
+ 'to': 'Tonga',
+ 'tr': 'Turkish',
+ 'ts': 'Tsonga',
+ 'tt': 'Tatar',
+ 'tw': 'Twi',
+ 'ty': 'Tahitian',
+ 'ug': 'Uighur, Uyghur',
+ 'uk': 'Ukrainian',
+ 'ur': 'Urdu',
+ 'uz': 'Uzbek',
+ 've': 'Venda',
+ 'vi': 'Vietnamese',
+ 'vo': 'Volapük',
+ 'wa': 'Walloon',
+ 'cy': 'Welsh',
+ 'wo': 'Wolof',
+ 'fy': 'Western Frisian',
+ 'xh': 'Xhosa',
+ 'yi': 'Yiddish',
+ 'yo': 'Yoruba',
+ 'za': 'Zhuang, Chuang',
+ 'zu': 'Zulu',
+}
+
+
+countries = [
+ ('AF', 'Afghanistan'),
+ ('AL', 'Albania'),
+ ('DZ', 'Algeria'),
+ ('AS', 'American Samoa'),
+ ('AD', 'Andorra'),
+ ('AO', 'Angola'),
+ ('AI', 'Anguilla'),
+ ('AQ', 'Antarctica'),
+ ('AG', 'Antigua And Barbuda'),
+ ('AR', 'Argentina'),
+ ('AM', 'Armenia'),
+ ('AW', 'Aruba'),
+ ('AU', 'Australia'),
+ ('AT', 'Austria'),
+ ('AZ', 'Azerbaijan'),
+ ('BS', 'Bahamas'),
+ ('BH', 'Bahrain'),
+ ('BD', 'Bangladesh'),
+ ('BB', 'Barbados'),
+ ('BY', 'Belarus'),
+ ('BE', 'Belgium'),
+ ('BZ', 'Belize'),
+ ('BJ', 'Benin'),
+ ('BM', 'Bermuda'),
+ ('BT', 'Bhutan'),
+ ('BO', 'Bolivia'),
+ ('BA', 'Bosnia And Herzegowina'),
+ ('BW', 'Botswana'),
+ ('BV', 'Bouvet Island'),
+ ('BR', 'Brazil'),
+ ('BN', 'Brunei Darussalam'),
+ ('BG', 'Bulgaria'),
+ ('BF', 'Burkina Faso'),
+ ('BI', 'Burundi'),
+ ('KH', 'Cambodia'),
+ ('CM', 'Cameroon'),
+ ('CA', 'Canada'),
+ ('CV', 'Cape Verde'),
+ ('KY', 'Cayman Islands'),
+ ('CF', 'Central African Rep'),
+ ('TD', 'Chad'),
+ ('CL', 'Chile'),
+ ('CN', 'China'),
+ ('CX', 'Christmas Island'),
+ ('CC', 'Cocos Islands'),
+ ('CO', 'Colombia'),
+ ('KM', 'Comoros'),
+ ('CG', 'Congo'),
+ ('CK', 'Cook Islands'),
+ ('CR', 'Costa Rica'),
+ ('CI', 'Cote D`ivoire'),
+ ('HR', 'Croatia'),
+ ('CU', 'Cuba'),
+ ('CY', 'Cyprus'),
+ ('CZ', 'Czech Republic'),
+ ('DK', 'Denmark'),
+ ('DJ', 'Djibouti'),
+ ('DM', 'Dominica'),
+ ('DO', 'Dominican Republic'),
+ ('TP', 'East Timor'),
+ ('EC', 'Ecuador'),
+ ('EG', 'Egypt'),
+ ('SV', 'El Salvador'),
+ ('GQ', 'Equatorial Guinea'),
+ ('ER', 'Eritrea'),
+ ('EE', 'Estonia'),
+ ('ET', 'Ethiopia'),
+ ('FK', 'Falkland Islands (Malvinas)'),
+ ('FO', 'Faroe Islands'),
+ ('FJ', 'Fiji'),
+ ('FI', 'Finland'),
+ ('FR', 'France'),
+ ('GF', 'French Guiana'),
+ ('PF', 'French Polynesia'),
+ ('TF', 'French S. Territories'),
+ ('GA', 'Gabon'),
+ ('GM', 'Gambia'),
+ ('GE', 'Georgia'),
+ ('DE', 'Germany'),
+ ('GH', 'Ghana'),
+ ('GI', 'Gibraltar'),
+ ('GR', 'Greece'),
+ ('GL', 'Greenland'),
+ ('GD', 'Grenada'),
+ ('GP', 'Guadeloupe'),
+ ('GU', 'Guam'),
+ ('GT', 'Guatemala'),
+ ('GN', 'Guinea'),
+ ('GW', 'Guinea-bissau'),
+ ('GY', 'Guyana'),
+ ('HT', 'Haiti'),
+ ('HN', 'Honduras'),
+ ('HK', 'Hong Kong'),
+ ('HU', 'Hungary'),
+ ('IS', 'Iceland'),
+ ('IN', 'India'),
+ ('ID', 'Indonesia'),
+ ('IR', 'Iran'),
+ ('IQ', 'Iraq'),
+ ('IE', 'Ireland'),
+ ('IL', 'Israel'),
+ ('IT', 'Italy'),
+ ('JM', 'Jamaica'),
+ ('JP', 'Japan'),
+ ('JO', 'Jordan'),
+ ('KZ', 'Kazakhstan'),
+ ('KE', 'Kenya'),
+ ('KI', 'Kiribati'),
+ ('KP', 'Korea (North)'),
+ ('KR', 'Korea (South)'),
+ ('KW', 'Kuwait'),
+ ('KG', 'Kyrgyzstan'),
+ ('LA', 'Laos'),
+ ('LV', 'Latvia'),
+ ('LB', 'Lebanon'),
+ ('LS', 'Lesotho'),
+ ('LR', 'Liberia'),
+ ('LY', 'Libya'),
+ ('LI', 'Liechtenstein'),
+ ('LT', 'Lithuania'),
+ ('LU', 'Luxembourg'),
+ ('MO', 'Macau'),
+ ('MK', 'Macedonia'),
+ ('MG', 'Madagascar'),
+ ('MW', 'Malawi'),
+ ('MY', 'Malaysia'),
+ ('MV', 'Maldives'),
+ ('ML', 'Mali'),
+ ('MT', 'Malta'),
+ ('MH', 'Marshall Islands'),
+ ('MQ', 'Martinique'),
+ ('MR', 'Mauritania'),
+ ('MU', 'Mauritius'),
+ ('YT', 'Mayotte'),
+ ('MX', 'Mexico'),
+ ('FM', 'Micronesia'),
+ ('MD', 'Moldova'),
+ ('MC', 'Monaco'),
+ ('MN', 'Mongolia'),
+ ('MS', 'Montserrat'),
+ ('MA', 'Morocco'),
+ ('MZ', 'Mozambique'),
+ ('MM', 'Myanmar'),
+ ('NA', 'Namibia'),
+ ('NR', 'Nauru'),
+ ('NP', 'Nepal'),
+ ('NL', 'Netherlands'),
+ ('AN', 'Netherlands Antilles'),
+ ('NC', 'New Caledonia'),
+ ('NZ', 'New Zealand'),
+ ('NI', 'Nicaragua'),
+ ('NE', 'Niger'),
+ ('NG', 'Nigeria'),
+ ('NU', 'Niue'),
+ ('NF', 'Norfolk Island'),
+ ('MP', 'Northern Mariana Islands'),
+ ('NO', 'Norway'),
+ ('OM', 'Oman'),
+ ('PK', 'Pakistan'),
+ ('PW', 'Palau'),
+ ('PA', 'Panama'),
+ ('PG', 'Papua New Guinea'),
+ ('PY', 'Paraguay'),
+ ('PE', 'Peru'),
+ ('PH', 'Philippines'),
+ ('PN', 'Pitcairn'),
+ ('PL', 'Poland'),
+ ('PT', 'Portugal'),
+ ('PR', 'Puerto Rico'),
+ ('QA', 'Qatar'),
+ ('RE', 'Reunion'),
+ ('RO', 'Romania'),
+ ('RU', 'Russian Federation'),
+ ('RW', 'Rwanda'),
+ ('KN', 'Saint Kitts And Nevis'),
+ ('LC', 'Saint Lucia'),
+ ('VC', 'St Vincent/Grenadines'),
+ ('WS', 'Samoa'),
+ ('SM', 'San Marino'),
+ ('ST', 'Sao Tome'),
+ ('SA', 'Saudi Arabia'),
+ ('SN', 'Senegal'),
+ ('SC', 'Seychelles'),
+ ('SL', 'Sierra Leone'),
+ ('SG', 'Singapore'),
+ ('SK', 'Slovakia'),
+ ('SI', 'Slovenia'),
+ ('SB', 'Solomon Islands'),
+ ('SO', 'Somalia'),
+ ('ZA', 'South Africa'),
+ ('ES', 'Spain'),
+ ('LK', 'Sri Lanka'),
+ ('SH', 'St. Helena'),
+ ('PM', 'St.Pierre'),
+ ('SD', 'Sudan'),
+ ('SR', 'Suriname'),
+ ('SZ', 'Swaziland'),
+ ('SE', 'Sweden'),
+ ('CH', 'Switzerland'),
+ ('SY', 'Syrian Arab Republic'),
+ ('TW', 'Taiwan'),
+ ('TJ', 'Tajikistan'),
+ ('TZ', 'Tanzania'),
+ ('TH', 'Thailand'),
+ ('TG', 'Togo'),
+ ('TK', 'Tokelau'),
+ ('TO', 'Tonga'),
+ ('TT', 'Trinidad And Tobago'),
+ ('TN', 'Tunisia'),
+ ('TR', 'Turkey'),
+ ('TM', 'Turkmenistan'),
+ ('TV', 'Tuvalu'),
+ ('UG', 'Uganda'),
+ ('UA', 'Ukraine'),
+ ('AE', 'United Arab Emirates'),
+ ('UK', 'United Kingdom'),
+ ('US', 'United States'),
+ ('UY', 'Uruguay'),
+ ('UZ', 'Uzbekistan'),
+ ('VU', 'Vanuatu'),
+ ('VA', 'Vatican City State'),
+ ('VE', 'Venezuela'),
+ ('VN', 'Viet Nam'),
+ ('VG', 'Virgin Islands (British)'),
+ ('VI', 'Virgin Islands (U.S.)'),
+ ('EH', 'Western Sahara'),
+ ('YE', 'Yemen'),
+ ('YU', 'Yugoslavia'),
+ ('ZR', 'Zaire'),
+ ('ZM', 'Zambia'),
+ ('ZW', 'Zimbabwe')
+]
+
+
+
+def getLanguageDict():
+ return lang_dict
+
+def getLanguageFromISO( iso ):
+ if iso == None:
+ return None
+ else:
+ return lang_dict[ iso ]
+
diff --git a/volumeselectionwindow.py b/volumeselectionwindow.py
new file mode 100644
index 0000000..ad737e1
--- /dev/null
+++ b/volumeselectionwindow.py
@@ -0,0 +1,135 @@
+import sys
+from PyQt4 import QtCore, QtGui, uic
+
+from PyQt4.QtCore import QUrl
+from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
+
+from comicvinetalker import ComicVineTalker
+from issueselectionwindow import IssueSelectionWindow
+
+class VolumeSelectionWindow(QtGui.QDialog):
+
+ volume_id = 0
+
+ def __init__(self, parent, series_name, issue_number):
+ super(VolumeSelectionWindow, self).__init__(parent)
+
+ uic.loadUi('volumeselectionwindow.ui', self)
+
+ self.series_name = series_name
+ self.issue_number = issue_number
+
+ self.performQuery()
+
+ self.twList.resizeColumnsToContents()
+ self.twList.currentItemChanged.connect(self.currentItemChanged)
+ self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
+ self.btnRequery.clicked.connect(self.requery)
+ self.btnIssues.clicked.connect(self.showIssues)
+
+ self.twList.selectRow(0)
+
+ def requery( self ):
+ self.performQuery()
+ self.twList.selectRow(0)
+
+ def showIssues( self ):
+ selector = IssueSelectionWindow( self, self.volume_id, self.issue_number )
+ selector.setModal(True)
+ selector.exec_()
+ if selector.result():
+ #we should now have a volume ID
+ self.issue_number = selector.issue_number
+ self.accept()
+ return
+
+ def performQuery( self ):
+
+ while self.twList.rowCount() > 0:
+ self.twList.removeRow(0)
+
+ comicVine = ComicVineTalker()
+ self.cv_search_results = comicVine.searchForSeries( self.series_name )
+
+ self.twList.setSortingEnabled(False)
+
+ row = 0
+ for record in self.cv_search_results:
+ self.twList.insertRow(row)
+
+ item_text = record['name']
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setData( QtCore.Qt.UserRole ,record['id'])
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 0, item)
+
+ item_text = str(record['start_year'])
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 1, item)
+
+ item_text = record['count_of_issues']
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
+ item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 2, item)
+
+ if record['publisher'] is not None:
+ item_text = record['publisher']['name']
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 3, item)
+
+ record['cover_image'] = None
+
+ row += 1
+
+ self.twList.setSortingEnabled(True)
+ self.twList.sortItems( 2 , QtCore.Qt.DescendingOrder )
+
+
+ def cellDoubleClicked( self, r, c ):
+ self.showIssues()
+
+ def currentItemChanged( self, curr, prev ):
+
+ if curr is None:
+ return
+ if prev is not None and prev.row() == curr.row():
+ return
+
+
+ self.volume_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
+
+ # list selection was changed, update the info on the volume
+ for record in self.cv_search_results:
+ if record['id'] == self.volume_id:
+
+ self.teDetails.setText ( record['description'] )
+
+ if record['cover_image'] == None:
+ url = record['image']['super_url']
+ self.labelThumbnail.setText("loading...")
+ self.nam = QNetworkAccessManager()
+
+ self.nam.finished.connect(self.finishRequest)
+ self.nam.get(QNetworkRequest(QUrl(url)))
+ self.pending_cover_record = record
+ else:
+ self.setCover(record['cover_image'])
+
+ # called when the image is done loading
+ def finishRequest(self, reply):
+ img = QtGui.QImage()
+ img.loadFromData(reply.readAll())
+
+ self.pending_cover_record['cover_image'] = img
+ self.pending_cover_record = None
+
+ self.setCover( img )
+
+ def setCover( self, img ):
+ self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
+
+
+
diff --git a/volumeselectionwindow.ui b/volumeselectionwindow.ui
new file mode 100644
index 0000000..e744b1a
--- /dev/null
+++ b/volumeselectionwindow.ui
@@ -0,0 +1,214 @@
+
+
+ SelectSeries
+
+
+
+ 0
+ 0
+ 900
+ 480
+
+
+
+ Select Series
+
+
+ false
+
+
+
+
+ 20
+ 30
+ 241
+ 391
+
+
+
+ QFrame::Panel
+
+
+ QFrame::Sunken
+
+
+
+
+
+ true
+
+
+
+
+
+ 272
+ 430
+ 611
+ 29
+
+
+
+ -
+
+
+ Show Issues
+
+
+
+ -
+
+
+ Re-Query
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ 270
+ 33
+ 611
+ 391
+
+
+
+ Qt::Vertical
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 250
+
+
+
+
+ 9
+
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ 0
+
+
+ 4
+
+
+ true
+
+
+ false
+
+
+
+ Series
+
+
+
+
+ Year
+
+
+ AlignHCenter|AlignVCenter|AlignCenter
+
+
+
+
+ Issues
+
+
+ AlignHCenter|AlignVCenter|AlignCenter
+
+
+
+
+ Publisher
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 200
+
+
+
+
+ 9
+
+
+
+ true
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ SelectSeries
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ SelectSeries
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+