From f642b4674222b8c69f4fd9ec961a5d0a7dd1be95 Mon Sep 17 00:00:00 2001 From: "beville@gmail.com" Date: Fri, 2 Nov 2012 20:54:17 +0000 Subject: [PATCH] Initial checking git-svn-id: http://comictagger.googlecode.com/svn/trunk@2 6c5673fe-1810-88d6-992b-cd32ca31540c --- comicarchive.py | 284 +++++++++++++++ comicbookinfo.py | 118 ++++++ comicinfoxml.py | 248 +++++++++++++ comicvinetalker.py | 186 ++++++++++ filenameparser.py | 124 +++++++ genericmetadata.py | 96 +++++ issueselectionwindow.py | 118 ++++++ issueselectionwindow.ui | 147 ++++++++ options.py | 65 ++++ tagger.py | 88 +++++ taggerwindow.py | 480 ++++++++++++++++++++++++ taggerwindow.ui | 764 +++++++++++++++++++++++++++++++++++++++ test.ui | 311 ++++++++++++++++ utils.py | 450 +++++++++++++++++++++++ volumeselectionwindow.py | 135 +++++++ volumeselectionwindow.ui | 214 +++++++++++ 16 files changed, 3828 insertions(+) create mode 100644 comicarchive.py create mode 100644 comicbookinfo.py create mode 100644 comicinfoxml.py create mode 100644 comicvinetalker.py create mode 100644 filenameparser.py create mode 100644 genericmetadata.py create mode 100644 issueselectionwindow.py create mode 100644 issueselectionwindow.ui create mode 100644 options.py create mode 100755 tagger.py create mode 100644 taggerwindow.py create mode 100644 taggerwindow.ui create mode 100644 test.ui create mode 100644 utils.py create mode 100644 volumeselectionwindow.py create mode 100644 volumeselectionwindow.ui 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 + + + + +