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 + + + + + + + + + + 0 + 0 + 943 + 28 + + + + + File + + + + + + + + + Help + + + + + + Tags + + + + + + + + + + + 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 + + + + + + + + 0 + 0 + 943 + 25 + + + + + File + + + + + Help + + + + + + + + + + + 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 + + + + +