From 301ff084f1b246a854bc116300f6f4025123c09e Mon Sep 17 00:00:00 2001 From: beville Date: Fri, 13 Jun 2014 06:26:44 +0000 Subject: [PATCH] fixes for webp, api key handling, and CV rate limit git-svn-id: http://comictagger.googlecode.com/svn/trunk@734 6c5673fe-1810-88d6-992b-cd32ca31540c --- comictaggerlib/autotagstartwindow.py | 7 +- comictaggerlib/cli.py | 15 +- comictaggerlib/comicarchive.py | 2 +- comictaggerlib/comicvinetalker.py | 154 ++++++------ comictaggerlib/coverimagewidget.py | 3 +- comictaggerlib/ctversion.py | 2 +- comictaggerlib/imagehasher.py | 3 +- comictaggerlib/issueidentifier.py | 13 +- comictaggerlib/issueselectionwindow.py | 11 +- comictaggerlib/main.py | 14 +- comictaggerlib/options.py | 116 +++++---- comictaggerlib/pageloader.py | 4 +- comictaggerlib/settings.py | 9 +- comictaggerlib/settingswindow.py | 12 +- comictaggerlib/taggerwindow.py | 15 +- comictaggerlib/ui/autotagstartwindow.ui | 317 ++++++++++++------------ comictaggerlib/ui/settingswindow.ui | 199 ++++++++++++--- comictaggerlib/utils.py | 35 ++- comictaggerlib/volumeselectionwindow.py | 12 +- 19 files changed, 593 insertions(+), 350 deletions(-) diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index d23658a..72a9257 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -58,7 +58,9 @@ class AutoTagStartWindow(QtGui.QDialog): if self.settings.ignore_leading_numbers_in_filename: self.cbxIgnoreLeadingDigitsInFilename.setCheckState( QtCore.Qt.Checked) if self.settings.remove_archive_after_successful_match: - self.cbxRemoveAfterSuccess.setCheckState( QtCore.Qt.Checked) + self.cbxRemoveAfterSuccess.setCheckState( QtCore.Qt.Checked) + if self.settings.wait_and_retry_on_rate_limit: + self.cbxWaitForRateLimit.setCheckState( QtCore.Qt.Checked) nlmtTip = ( """ The Name Length Match Tolerance is for eliminating automatic @@ -90,6 +92,7 @@ class AutoTagStartWindow(QtGui.QDialog): self.assumeIssueOne = False self.ignoreLeadingDigitsInFilename = False self.removeAfterSuccess = False + self.waitAndRetryOnRateLimit = False self.searchString = None self.nameLengthMatchTolerance = self.settings.id_length_delta_thresh @@ -107,6 +110,7 @@ class AutoTagStartWindow(QtGui.QDialog): self.ignoreLeadingDigitsInFilename = self.cbxIgnoreLeadingDigitsInFilename.isChecked() self.removeAfterSuccess = self.cbxRemoveAfterSuccess.isChecked() self.nameLengthMatchTolerance = int(self.leNameLengthMatchTolerance.text()) + self.waitAndRetryOnRateLimit = self.cbxWaitForRateLimit.isChecked() #persist some settings self.settings.save_on_low_confidence = self.autoSaveOnLow @@ -114,6 +118,7 @@ class AutoTagStartWindow(QtGui.QDialog): self.settings.assume_1_if_no_issue_num = self.assumeIssueOne self.settings.ignore_leading_numbers_in_filename = self.ignoreLeadingDigitsInFilename self.settings.remove_archive_after_successful_match = self.removeAfterSuccess + self.settings.wait_and_retry_on_rate_limit = self.waitAndRetryOnRateLimit if self.cbxSpecifySearchString.isChecked(): self.searchString = unicode(self.leSearchString.text()) diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index ee10725..34b9a5d 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -60,11 +60,13 @@ class OnlineMatchResults(): #----------------------------- -def actual_issue_data_fetch( match, settings ): +def actual_issue_data_fetch( match, settings, opts ): # now get the particular issue data try: - cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings ) + comicVine = ComicVineTalker() + comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit + cv_md = comicVine.fetchIssueData( match['volume_id'], match['issue_number'], settings ) except ComicVineTalkerException: print >> sys.stderr, "Network error while getting issue details. Save aborted" return None @@ -117,7 +119,7 @@ def display_match_set_for_choice( label, match_set, opts, settings ): # we know at this point, that the file is all good to go ca = ComicArchive( match_set.filename, settings.rar_exe_path ) md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) ) - cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings) + cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings, opts) md.overlay( cv_md ) actual_metadata_save( ca, opts, md ) @@ -346,7 +348,9 @@ def process_file_cli( filename, opts, settings, match_results ): if opts.issue_id is not None: # we were given the actual ID to search with try: - cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings ) + comicVine = ComicVineTalker() + comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit + cv_md = comicVine.fetchIssueDataByIssueID( opts.issue_id, settings ) except ComicVineTalkerException: print >> sys.stderr,"Network error while getting issue details. Save aborted" match_results.fetchDataFailures.append(filename) @@ -374,6 +378,7 @@ def process_file_cli( filename, opts, settings, match_results ): # use our overlayed MD struct to search ii.setAdditionalMetadata( md ) ii.onlyUseAdditionalMetaData = True + ii.waitAndRetryOnRateLimit = opts.wait_and_retry_on_rate_limit ii.setOutputFunction( myoutput ) ii.cover_page_index = md.getCoverPageIndexList()[0] matches = ii.search() @@ -421,7 +426,7 @@ def process_file_cli( filename, opts, settings, match_results ): # we got here, so we have a single match # now get the particular issue data - cv_md = actual_issue_data_fetch(matches[0], settings) + cv_md = actual_issue_data_fetch(matches[0], settings, opts) if cv_md is None: match_results.fetchDataFailures.append(filename) return diff --git a/comictaggerlib/comicarchive.py b/comictaggerlib/comicarchive.py index 4988af4..d5667ef 100644 --- a/comictaggerlib/comicarchive.py +++ b/comictaggerlib/comicarchive.py @@ -764,7 +764,7 @@ class ComicArchive: # make a sub-list of image files self.page_list = [] for name in files: - if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif" ] and os.path.basename(name)[0] != "." ): + if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif", "webp" ] and os.path.basename(name)[0] != "." ): self.page_list.append(name) return self.page_list diff --git a/comictaggerlib/comicvinetalker.py b/comictaggerlib/comicvinetalker.py index cc15794..0b68ba9 100644 --- a/comictaggerlib/comicvinetalker.py +++ b/comictaggerlib/comicvinetalker.py @@ -55,19 +55,50 @@ class CVTypeID: Issue = "4000" class ComicVineTalkerException(Exception): - pass + Unknown = -1 + Network = -2 + InvalidKey = 100 + RateLimit = 107 + + def __init__(self, code=-1, desc=""): + self.desc = desc + self.code = code + + def __str__(self): + if (self.code == ComicVineTalkerException.Unknown or + self.code == ComicVineTalkerException.Network): + return self.desc + else: + return "CV error #{0}: [{1}]. \n".format( self.code, self.desc ) + + class ComicVineTalker(QObject): logo_url = "http://static.comicvine.com/bundles/comicvinesite/images/logo.png" + api_key = "" - def __init__(self, api_key=""): + @staticmethod + def getRateLimitMessage(): + if ComicVineTalker.api_key == "": + return "Comic Vine rate limit exceeded. You should configue your own Comic Vine API key." + else: + return "Comic Vine rate limit exceeded. Please wait a bit." + + + def __init__(self): QObject.__init__(self) self.api_base_url = "http://www.comicvine.com/api" + self.wait_for_rate_limit = False # key that is registered to comictagger - self.api_key = '27431e6787042105bd3e47e169a624521f89f3a4' + default_api_key = '27431e6787042105bd3e47e169a624521f89f3a4' + + if ComicVineTalker.api_key == "": + self.api_key = default_api_key + else: + self.api_key = ComicVineTalker.api_key self.log_func = None @@ -95,9 +126,9 @@ class ComicVineTalker(QObject): day = parts[2] return day, month, year - def testKey( self ): + def testKey( self, key ): - test_url = self.api_base_url + "/issue/1/?api_key=" + self.api_key + "&format=json&field_list=name" + test_url = self.api_base_url + "/issue/1/?api_key=" + key + "&format=json&field_list=name" resp = urllib2.urlopen( test_url ) content = resp.read() @@ -106,6 +137,36 @@ class ComicVineTalker(QObject): # Bogus request, but if the key is wrong, you get error 100: "Invalid API Key" return cv_response[ 'status_code' ] != 100 + """ + Get the contect from the CV server. If we're in "wait mode" and status code is a rate limit error + sleep for a bit and retry. + """ + def getCVContent(self, url): + total_time_waited = 0 + limit_wait_time = 1 + counter = 0 + wait_times = [1,2,3,4] + while True: + content = self.getUrlContent(url) + cv_response = json.loads(content) + if self.wait_for_rate_limit and cv_response[ 'status_code' ] == ComicVineTalkerException.RateLimit: + self.writeLog( "Rate limit encountered. Waiting for {0} minutes\n".format(limit_wait_time)) + time.sleep(limit_wait_time * 60) + total_time_waited += limit_wait_time + limit_wait_time = wait_times[counter] + if counter < 3: + counter += 1 + # don't wait much more than 20 minutes + if total_time_waited < 20: + continue + if cv_response[ 'status_code' ] != 1: + self.writeLog( "Comic Vine query failed with error #{0}: [{1}]. \n".format( cv_response[ 'status_code' ], cv_response[ 'error' ] )) + raise ComicVineTalkerException(cv_response[ 'status_code' ], cv_response[ 'error' ] ) + else: + # it's all good + break + return cv_response + def getUrlContent( self, url ): # connect to server: # if there is a 500 error, try a few more times before giving up @@ -126,9 +187,9 @@ class ComicVineTalker(QObject): except Exception as e: self.writeLog( str(e) + "\n" ) - raise ComicVineTalkerException("Network Error!") + raise ComicVineTalkerException(ComicVineTalkerException.Network, "Network Error!") - raise ComicVineTalkerException("Error on Comic Vine server") + raise ComicVineTalkerException(ComicVineTalkerException.Unknown, "Error on Comic Vine server") def searchForSeries( self, series_name , callback=None, refresh_cache=False ): @@ -161,14 +222,8 @@ class ComicVineTalker(QObject): query_string = urllib.quote_plus(query_string.encode("utf-8")) search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + query_string + "&field_list=name,id,start_year,publisher,image,description,count_of_issues" - content = self.getUrlContent(search_url + "&page=1") - - cv_response = json.loads(content) - - if cv_response[ 'status_code' ] != 1: - self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] )) - return None - + cv_response = self.getCVContent(search_url + "&page=1") + search_results = list() # see http://api.comicvine.com/documentation/#handling_responses @@ -190,14 +245,9 @@ class ComicVineTalker(QObject): if callback is None: self.writeLog("getting another page of results {0} of {1}...\n".format( current_result_count, total_result_count)) page += 1 + + cv_response = self.getCVContent(search_url + "&page="+str(page)) - content = self.getUrlContent(search_url + "&page="+str(page)) - - cv_response = json.loads(content) - - if cv_response[ 'status_code' ] != 1: - self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] )) - return None search_results.extend( cv_response['results']) current_result_count += cv_response['number_of_page_results'] @@ -229,12 +279,7 @@ class ComicVineTalker(QObject): volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + str(series_id) + "/?api_key=" + self.api_key + "&field_list=name,id,start_year,publisher,count_of_issues&format=json" - content = self.getUrlContent(volume_url) - cv_response = json.loads(content) - - if cv_response[ 'status_code' ] != 1: - print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ) - return None + cv_response = self.getCVContent(volume_url) volume_results = cv_response['results'] @@ -254,12 +299,8 @@ class ComicVineTalker(QObject): #--------------------------------- issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + "&filter=volume:" + str(series_id) + "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json" - content = self.getUrlContent(issues_url) - cv_response = json.loads(content) - - if cv_response[ 'status_code' ] != 1: - print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ) - return None + cv_response = self.getCVContent(issues_url) + #------------------------------------ limit = cv_response['limit'] @@ -279,12 +320,8 @@ class ComicVineTalker(QObject): offset += cv_response['number_of_page_results'] #print issues_url+ "&offset="+str(offset) - content = self.getUrlContent(issues_url + "&offset="+str(offset)) - cv_response = json.loads(content) - - if cv_response[ 'status_code' ] != 1: - self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] )) - return None + cv_response = self.getCVContent(issues_url + "&offset="+str(offset)) + volume_issues_result.extend( cv_response['results']) current_result_count += cv_response['number_of_page_results'] @@ -310,12 +347,8 @@ class ComicVineTalker(QObject): issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + filter + "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json" - content = self.getUrlContent(issues_url) - cv_response = json.loads(content) - - if cv_response[ 'status_code' ] != 1: - print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ) - return None + cv_response = self.getCVContent(issues_url) + #------------------------------------ limit = cv_response['limit'] @@ -335,12 +368,8 @@ class ComicVineTalker(QObject): offset += cv_response['number_of_page_results'] #print issues_url+ "&offset="+str(offset) - content = self.getUrlContent(issues_url + "&offset="+str(offset)) - cv_response = json.loads(content) - - if cv_response[ 'status_code' ] != 1: - self.writeLog( "Comic Vine query failed with error: [{0}]. \n".format( cv_response[ 'error' ] )) - return None + cv_response = self.getCVContent(issues_url + "&offset="+str(offset)) + filtered_issues_result.extend( cv_response['results']) current_result_count += cv_response['number_of_page_results'] @@ -366,11 +395,7 @@ class ComicVineTalker(QObject): if (found): issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json" - content = self.getUrlContent(issue_url) - cv_response = json.loads(content) - if cv_response[ 'status_code' ] != 1: - print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ) - return None + cv_response = self.getCVContent(issue_url) issue_results = cv_response['results'] else: @@ -382,11 +407,7 @@ class ComicVineTalker(QObject): def fetchIssueDataByIssueID( self, issue_id, settings ): issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json" - content = self.getUrlContent(issue_url) - cv_response = json.loads(content) - if cv_response[ 'status_code' ] != 1: - print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ) - return None + cv_response = self.getCVContent(issue_url) issue_results = cv_response['results'] @@ -577,19 +598,14 @@ class ComicVineTalker(QObject): issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image,cover_date,site_detail_url" - content = self.getUrlContent(issue_url) - details = dict() details['image_url'] = None details['thumb_image_url'] = None details['cover_date'] = None details['site_detail_url'] = None - cv_response = json.loads(content) - if cv_response[ 'status_code' ] != 1: - print >> sys.stderr, "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ) - return details - + cv_response = self.getCVContent(issue_url) + details['image_url'] = cv_response['results']['image']['super_url'] details['thumb_image_url'] = cv_response['results']['image']['thumb_url'] details['cover_date'] = cv_response['results']['cover_date'] diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py index 55154cf..c109696 100644 --- a/comictaggerlib/coverimagewidget.py +++ b/comictaggerlib/coverimagewidget.py @@ -237,8 +237,7 @@ class CoverImageWidget(QWidget): # called when the image is done loading from internet def coverRemoteFetchComplete( self, image_data, issue_id ): - img = QImage() - img.loadFromData( image_data ) + img = utils.getQImageFromData( image_data) self.current_pixmap = QPixmap(img) self.setDisplayPixmap( 0, 0) #print "ATB cover fetch complete!" diff --git a/comictaggerlib/ctversion.py b/comictaggerlib/ctversion.py index a0b2511..8d86cf7 100644 --- a/comictaggerlib/ctversion.py +++ b/comictaggerlib/ctversion.py @@ -1,3 +1,3 @@ # This file should contan only these comments, and the line below. # Used by packaging makefiles and app -version="1.1.14-beta" +version="1.1.15-beta" diff --git a/comictaggerlib/imagehasher.py b/comictaggerlib/imagehasher.py index fb26397..3441d8e 100755 --- a/comictaggerlib/imagehasher.py +++ b/comictaggerlib/imagehasher.py @@ -21,7 +21,8 @@ import StringIO import sys try: - import Image + from PIL import Image + from PIL import WebPImagePlugin pil_available = True except ImportError: pil_available = False diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index 3ac17c2..6cf351d 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -23,7 +23,8 @@ import math import urllib2, urllib import StringIO try: - import Image + from PIL import Image + from PIL import WebPImagePlugin pil_available = True except ImportError: pil_available = False @@ -83,6 +84,7 @@ class IssueIdentifier: self.search_result = self.ResultNoMatches self.cover_page_index = 0 self.cancel = False + self.waitAndRetryOnRateLimit = False def setScoreMinThreshold( self, thresh ): self.min_score_thresh = thresh @@ -377,8 +379,9 @@ class IssueIdentifier: self.log_msg( "\tMonth : " + str(keys['month']) ) #self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist)) - comicVine = ComicVineTalker( ) + comicVine.wait_for_rate_limit = self.waitAndRetryOnRateLimit + comicVine.setLogFunc( self.output_function ) #self.log_msg( ( "Searching for " + keys['series'] + "...") @@ -393,6 +396,9 @@ class IssueIdentifier: if self.cancel == True: return [] + if cv_search_results == None: + return [] + series_second_round_list = [] #self.log_msg( "Removing results with too long names, banned publishers, or future start dates" ) @@ -443,6 +449,9 @@ class IssueIdentifier: self.log_msg( "Network issue while searching for series details. Aborting...") return [] + if issue_list is None: + return [] + shortlist = list() #now re-associate the issues and volumes for issue in issue_list: diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index 6549392..2272f61 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -92,12 +92,15 @@ class IssueSelectionWindow(QtGui.QDialog): QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) try: - comicVine = ComicVineTalker( ) + comicVine = ComicVineTalker() volume_data = comicVine.fetchVolumeData( self.series_id ) self.issue_list = comicVine.fetchIssuesByVolume( self.series_id ) - except ComicVineTalkerException: - QtGui.QApplication.restoreOverrideCursor() - QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to list issues!")) + except ComicVineTalkerException as e: + QtGui.QApplication.restoreOverrideCursor() + if e.code == ComicVineTalkerException.RateLimit: + QtGui.QMessageBox.critical(self, self.tr("Comic Vine Error"), ComicVineTalker.getRateLimitMessage()) + else: + QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to list issues!")) return while self.twList.rowCount() > 0: diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 883603b..f3c9cdf 100755 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -28,6 +28,7 @@ import utils import cli from settings import ComicTaggerSettings from options import Options +from comicvinetalker import ComicVineTalker try: qt_available = True @@ -40,9 +41,20 @@ except ImportError as e: def ctmain(): utils.fix_output_encoding() settings = ComicTaggerSettings() - + opts = Options() opts.parseCmdLineArgs() + + # manage the CV API key + if opts.cv_api_key: + if opts.cv_api_key != settings.cv_api_key: + settings.cv_api_key = opts.cv_api_key + settings.save() + if opts.only_set_key: + print "Key set" + return + + ComicVineTalker.api_key = settings.cv_api_key signal.signal(signal.SIGINT, signal.SIG_DFL) diff --git a/comictaggerlib/options.py b/comictaggerlib/options.py index 7a252a3..63e6e6b 100644 --- a/comictaggerlib/options.py +++ b/comictaggerlib/options.py @@ -42,50 +42,53 @@ A utility for reading and writing metadata to comic archives. If no options are given, {0} will run in windowed mode - -p, --print Print out tag info from file. Specify type - (via -t) to get only info of that tag type - --raw With -p, will print out the raw tag block(s) - from the file - -d, --delete Deletes the tag block of specified type (via -t) - -c, --copy=SOURCE Copy the specified source tag block to destination style - specified via via -t (potentially lossy operation) - -s, --save Save out tags as specified type (via -t) - Must specify also at least -o, -p, or -m - --nooverwrite Don't modify tag block if it already exists ( relevent for -s or -c ) - -1, --assume-issue-one Assume issue number is 1 if not found ( relevent for -s ) - -n, --dryrun Don't actually modify file (only relevent for -d, -s, or -r) - -t, --type=TYPE Specify TYPE as either "CR", "CBL", or "COMET" (as either - ComicRack, ComicBookLover, or CoMet style tags, respectivly) - -f, --parsefilename Parse the filename to get some info, specifically - series name, issue number, volume, and publication - year - -i, --interactive Interactively query the user when there are multiple matches for - an online search - --nosummary Suppress the default summary after a save operation - -o, --online Search online and attempt to identify file using - existing metadata and images in archive. May be used - in conjuntion with -f and -m - --id=ID Use the issue ID when searching online. Overrides all other metadata - -m, --metadata=LIST Explicity define, as a list, some tags to be used - e.g. "series=Plastic Man , publisher=Quality Comics" - "series=Kickers^, Inc., issue=1, year=1986" - Name-Value pairs are comma separated. Use a "^" to - escape an "=" or a ",", as shown in the example above - Some names that can be used: - series, issue, issueCount, year, publisher, title - -r, --rename Rename the file based on specified tag style. - --noabort Don't abort save operation when online match is of low confidence - -e, --export-to-zip Export RAR archive to Zip format - --delete-rar Delete original RAR archive after successful export to Zip - --abort-on-conflict Don't export to zip if intended new filename exists (Otherwise, creates - a new unique filename) - -S, --script=FILE Run an "add-on" python script that uses the comictagger library for custom - processing. Script arguments can follow the script name - -R, --recursive Recursively include files in sub-folders - -v, --verbose Be noisy when doing what it does - --terse Don't say much (for print mode) - --version Display version - -h, --help Display this message + -p, --print Print out tag info from file. Specify type + (via -t) to get only info of that tag type + --raw With -p, will print out the raw tag block(s) + from the file + -d, --delete Deletes the tag block of specified type (via -t) + -c, --copy=SOURCE Copy the specified source tag block to destination style + specified via via -t (potentially lossy operation) + -s, --save Save out tags as specified type (via -t) + Must specify also at least -o, -p, or -m + --nooverwrite Don't modify tag block if it already exists ( relevent for -s or -c ) + -1, --assume-issue-one Assume issue number is 1 if not found ( relevent for -s ) + -n, --dryrun Don't actually modify file (only relevent for -d, -s, or -r) + -t, --type=TYPE Specify TYPE as either "CR", "CBL", or "COMET" (as either + ComicRack, ComicBookLover, or CoMet style tags, respectivly) + -f, --parsefilename Parse the filename to get some info, specifically + series name, issue number, volume, and publication + year + -i, --interactive Interactively query the user when there are multiple matches for + an online search + --nosummary Suppress the default summary after a save operation + -o, --online Search online and attempt to identify file using + existing metadata and images in archive. May be used + in conjuntion with -f and -m + --id=ID Use the issue ID when searching online. Overrides all other metadata + -m, --metadata=LIST Explicity define, as a list, some tags to be used + e.g. "series=Plastic Man , publisher=Quality Comics" + "series=Kickers^, Inc., issue=1, year=1986" + Name-Value pairs are comma separated. Use a "^" to + escape an "=" or a ",", as shown in the example above + Some names that can be used: + series, issue, issueCount, year, publisher, title + -r, --rename Rename the file based on specified tag style. + --noabort Don't abort save operation when online match is of low confidence + -e, --export-to-zip Export RAR archive to Zip format + --delete-rar Delete original RAR archive after successful export to Zip + --abort-on-conflict Don't export to zip if intended new filename exists (Otherwise, creates + a new unique filename) + -S, --script=FILE Run an "add-on" python script that uses the comictagger library for custom + processing. Script arguments can follow the script name + -R, --recursive Recursively include files in sub-folders + --cv-api-key=KEY Use the given Comic Vine API Key (persisted in settings) + --only-set-cv-key Only set the Comiv Vine API key and quit + -w, --wait-on-cv-rate-limit When encountering a Comic Vine rate limit error, wait and retry query + -v, --verbose Be noisy when doing what it does + --terse Don't say much (for print mode) + --version Display version + -h, --help Display this message For more help visit the wiki at: http://code.google.com/p/comictagger/ """ @@ -111,6 +114,8 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/ self.parse_filename = False self.show_save_summary = True self.raw = False + self.cv_api_key = None + self.only_set_key = False self.rename_file = False self.no_overwrite = False self.interactive = False @@ -118,8 +123,10 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/ self.recursive = False self.run_script = False self.script = None + self.wait_and_retry_on_rate_limit = False self.assume_issue_is_one_if_not_set = False self.file_list = [] + def display_msg_and_quit( self, msg, code, show_help=False ): appname = os.path.basename(sys.argv[0]) @@ -235,11 +242,12 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/ # parse command line options try: opts, args = getopt.getopt( input_args, - "hpdt:fm:vonsrc:ieRS:1", + "hpdt:fm:vownsrc:ieRS:1", [ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose", "online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite", "interactive", "nosummary", "version", "id=" , "recursive", "script=", - "export-to-zip", "delete-rar", "abort-on-conflict", "assume-issue-one" ] ) + "export-to-zip", "delete-rar", "abort-on-conflict", "assume-issue-one", + "cv-api-key=", "only-set-cv-key", "wait-on-cv-rate-limit" ] ) except getopt.GetoptError as err: self.display_msg_and_quit( str(err), 2 ) @@ -289,6 +297,8 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/ self.abort_export_on_conflict = True if o in ("-f", "--parsefilename"): self.parse_filename = True + if o in ("-w", "--wait-on-cv-rate-limit"): + self.wait_and_retry_on_rate_limit = True if o == "--id": self.issue_id = a if o == "--raw": @@ -303,6 +313,10 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/ self.assume_issue_is_one_if_not_set = True if o == "--nooverwrite": self.no_overwrite = True + if o == "--cv-api-key": + self.cv_api_key = a + if o == "--only-set-cv-key": + self.only_set_key = True if o == "--version": print "ComicTagger {0}: Copyright (c) 2012-2014 Anthony Beville".format(ctversion.version) print "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)" @@ -322,7 +336,7 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/ else: self.display_msg_and_quit( "Invalid tag type", 1 ) - if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip: + if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip or self.only_set_key: self.no_gui = True count = 0 @@ -333,9 +347,10 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/ if self.copy_tags: count += 1 if self.rename_file: count += 1 if self.export_to_zip: count +=1 + if self.only_set_key: count +=1 if count > 1: - self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, export, or run script", 1 ) + self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, rename, export, set key, or run script", 1 ) if self.script is not None: self.launch_script( self.script ) @@ -352,8 +367,11 @@ For more help visit the wiki at: http://code.google.com/p/comictagger/ else: self.filename = args[0] self.file_list = args + + if self.only_set_key and self.cv_api_key == None: + self.display_msg_and_quit( "Key not given!", 1 ) - if self.no_gui and self.filename is None: + if (self.only_set_key == False) and self.no_gui and (self.filename is None): self.display_msg_and_quit( "Command requires at least one filename!", 1 ) if self.delete_tags and self.data_style is None: diff --git a/comictaggerlib/pageloader.py b/comictaggerlib/pageloader.py index 0bb0c26..182911a 100644 --- a/comictaggerlib/pageloader.py +++ b/comictaggerlib/pageloader.py @@ -22,6 +22,7 @@ from PyQt4 import QtCore, QtGui, uic from PyQt4.QtCore import pyqtSignal from comicarchive import ComicArchive +import utils """ This class holds onto a reference of each instance in a list @@ -66,8 +67,7 @@ class PageLoader( QtCore.QThread ): return if image_data is not None: - img = QtGui.QImage() - img.loadFromData( image_data ) + img = utils.getQImageFromData( image_data) if self.abandoned: return diff --git a/comictaggerlib/settings.py b/comictaggerlib/settings.py index 590ebff..cef49e4 100644 --- a/comictaggerlib/settings.py +++ b/comictaggerlib/settings.py @@ -107,6 +107,7 @@ class ComicTaggerSettings: self.use_series_start_as_volume = False self.clear_form_before_populating_from_cv = False self.remove_html_tables = False + self.cv_api_key = "" # CBL Tranform settings @@ -132,6 +133,7 @@ class ComicTaggerSettings: self.assume_1_if_no_issue_num = False self.ignore_leading_numbers_in_filename = False self.remove_archive_after_successful_match = False + self.wait_and_retry_on_rate_limit = False def __init__(self): @@ -252,7 +254,8 @@ class ComicTaggerSettings: self.clear_form_before_populating_from_cv = self.config.getboolean( 'comicvine', 'clear_form_before_populating_from_cv' ) if self.config.has_option('comicvine', 'remove_html_tables'): self.remove_html_tables = self.config.getboolean( 'comicvine', 'remove_html_tables' ) - + if self.config.has_option('comicvine', 'cv_api_key'): + self.cv_api_key = self.config.get( 'comicvine', 'cv_api_key' ) if self.config.has_option('cbl_transform', 'assume_lone_credit_is_primary'): self.assume_lone_credit_is_primary = self.config.getboolean( 'cbl_transform', 'assume_lone_credit_is_primary' ) @@ -292,6 +295,8 @@ class ComicTaggerSettings: self.ignore_leading_numbers_in_filename = self.config.getboolean( 'autotag', 'ignore_leading_numbers_in_filename' ) if self.config.has_option('autotag', 'remove_archive_after_successful_match'): self.remove_archive_after_successful_match = self.config.getboolean( 'autotag', 'remove_archive_after_successful_match' ) + if self.config.has_option('autotag', 'wait_and_retry_on_rate_limit'): + self.wait_and_retry_on_rate_limit = self.config.getboolean( 'autotag', 'wait_and_retry_on_rate_limit' ) def save( self ): @@ -345,6 +350,7 @@ class ComicTaggerSettings: self.config.set( 'comicvine', 'use_series_start_as_volume', self.use_series_start_as_volume ) self.config.set( 'comicvine', 'clear_form_before_populating_from_cv', self.clear_form_before_populating_from_cv ) self.config.set( 'comicvine', 'remove_html_tables', self.remove_html_tables ) + self.config.set( 'comicvine', 'cv_api_key', self.cv_api_key ) if not self.config.has_section( 'cbl_transform' ): self.config.add_section( 'cbl_transform' ) @@ -374,6 +380,7 @@ class ComicTaggerSettings: self.config.set( 'autotag', 'assume_1_if_no_issue_num', self.assume_1_if_no_issue_num ) self.config.set( 'autotag', 'ignore_leading_numbers_in_filename', self.ignore_leading_numbers_in_filename ) self.config.set( 'autotag', 'remove_archive_after_successful_match', self.remove_archive_after_successful_match ) + self.config.set( 'autotag', 'wait_and_retry_on_rate_limit', self.wait_and_retry_on_rate_limit ) with codecs.open( self.settings_file, 'wb', 'utf8') as configfile: self.config.write(configfile) diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 176edbb..727d592 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -24,6 +24,7 @@ from PyQt4 import QtCore, QtGui, uic from settings import ComicTaggerSettings from comicvinecacher import ComicVineCacher +from comicvinetalker import ComicVineTalker from imagefetcher import ImageFetcher import utils @@ -110,6 +111,7 @@ class SettingsWindow(QtGui.QDialog): self.btnBrowseUnrar.clicked.connect(self.selectUnrar) self.btnClearCache.clicked.connect(self.clearCache) self.btnResetSettings.clicked.connect(self.resetSettings) + self.btnTestKey.clicked.connect(self.testAPIKey) def settingsToForm( self ): @@ -131,6 +133,7 @@ class SettingsWindow(QtGui.QDialog): self.cbxClearFormBeforePopulating.setCheckState( QtCore.Qt.Checked) if self.settings.remove_html_tables: self.cbxRemoveHtmlTables.setCheckState( QtCore.Qt.Checked) + self.leKey.setText( str(self.settings.cv_api_key) ) if self.settings.assume_lone_credit_is_primary: self.cbxAssumeLoneCreditIsPrimary.setCheckState( QtCore.Qt.Checked) @@ -185,7 +188,8 @@ class SettingsWindow(QtGui.QDialog): self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked() self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked() self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked() - + self.settings.cv_api_key = unicode(self.leKey.text()) + ComicVineTalker.api_key = self.settings.cv_api_key self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked() self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked() self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked() @@ -215,6 +219,12 @@ class SettingsWindow(QtGui.QDialog): ComicVineCacher( ).clearCache() QtGui.QMessageBox.information(self, self.name, "Cache has been cleared.") + def testAPIKey( self ): + if ComicVineTalker().testKey( unicode(self.leKey.text()) ): + QtGui.QMessageBox.information(self, "API Key Test", "Key is valid!") + else: + QtGui.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.") + def resetSettings( self ): self.settings.reset() diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 4680b3d..84c761f 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -992,11 +992,14 @@ class TaggerWindow( QtGui.QMainWindow): self.formToMetadata() try: - comicVine = ComicVineTalker( ) + comicVine = ComicVineTalker() new_metadata = comicVine.fetchIssueData( selector.volume_id, selector.issue_number, self.settings ) - except ComicVineTalkerException: + except ComicVineTalkerException as e: QtGui.QApplication.restoreOverrideCursor() - QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to get issue details!")) + if e.code == ComicVineTalkerException.RateLimit: + QtGui.QMessageBox.critical(self, self.tr("Comic Vine Error"), ComicVineTalker.getRateLimitMessage()) + else: + QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to get issue details.!")) else: QtGui.QApplication.restoreOverrideCursor() if new_metadata is not None: @@ -1554,7 +1557,9 @@ class TaggerWindow( QtGui.QMainWindow): QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) try: - cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], self.settings ) + comicVine = ComicVineTalker( ) + comicVine.wait_for_rate_limit = self.settings.wait_and_retry_on_rate_limit + cv_md = comicVine.fetchIssueData( match['volume_id'], match['issue_number'], self.settings ) except ComicVineTalkerException: print "Network error while getting issue details. Save aborted" @@ -1601,6 +1606,7 @@ class TaggerWindow( QtGui.QMainWindow): md.issue = "1" ii.setAdditionalMetadata( md ) ii.onlyUseAdditionalMetaData = True + ii.waitAndRetryOnRateLimit = dlg.waitAndRetryOnRateLimit ii.setOutputFunction( self.autoTagLog ) ii.cover_page_index = md.getCoverPageIndexList()[0] ii.setCoverURLCallback( self.atprogdialog.setTestImage ) @@ -1682,6 +1688,7 @@ class TaggerWindow( QtGui.QMainWindow): atstartdlg = AutoTagStartWindow( self, self.settings, self.tr("You have selected {0} archive(s) to automatically identify and write {1} tags to.\n\n".format(len(ca_list), MetaDataStyle.name[style]) + "Please choose options below, and select OK to Auto-Tag.\n" )) + atstartdlg.adjustSize( ) atstartdlg.setModal( True ) if not atstartdlg.exec_(): diff --git a/comictaggerlib/ui/autotagstartwindow.ui b/comictaggerlib/ui/autotagstartwindow.ui index 2ddb8e9..78bc088 100644 --- a/comictaggerlib/ui/autotagstartwindow.ui +++ b/comictaggerlib/ui/autotagstartwindow.ui @@ -9,8 +9,8 @@ 0 0 - 607 - 319 + 497 + 378 @@ -25,183 +25,180 @@ false - + - - - + + + + 0 + 0 + + + + + + + true + + + + + + + - + 0 0 - - - - true + Specify series search string for all selected archives - - - - QLayout::SetFixedSize + + + + + 0 + 0 + - - QFormLayout::AllNonFixedFieldsGrow + + Save on low confidence match - - - - - 0 - 0 - - - - Save on low confidence match - - - - - - - - 0 - 0 - - - - Don't use publication year in indentification process - - - - - - - - 0 - 0 - - - - If no issue number, assume "1" - - - - - - - - 0 - 0 - - - - Ignore leading (sequence) numbers in filename - - - - - - - - 0 - 0 - - - - Remove archives from list after successful tagging - - - - - - - - 0 - 0 - - - - Specify series search string for all selected archives - - - - - - - - 0 - 0 - - - - - 40 - 0 - - - - - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - - 50 - 16777215 - - - - - - - - - 0 - 0 - - - - Adjust Name Length Match Tolerance: - - - - + - - - - Qt::Horizontal + + + + + 0 + 0 + - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + Ignore leading (sequence) numbers in filename + + + + + + + + 0 + 0 + + + + Remove archives from list after successful tagging + + + + + + + Wait and retry when Comic Vine rate limit is exceeded + + + + + + + + 0 + 0 + + + + Don't use publication year in indentification process + + + + + + + + 0 + 0 + + + + If no issue number, assume "1" + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Adjust Name Length Match Tolerance: + + + + + 0 + 0 + + + + + 40 + 0 + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index c8d0765..da5886f 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -6,8 +6,8 @@ 0 0 - 674 - 428 + 702 + 432 @@ -21,6 +21,12 @@ + + + 0 + 0 + + 0 @@ -336,45 +342,156 @@ Comic Vine - - - - 30 - 30 - 251 - 25 - - - - Use Series Start Date as Volume - - - - - - 30 - 50 - 341 - 25 - - - - Clear Form Before Importing Comic Vine data - - - - - - 30 - 70 - 351 - 25 - - - - Remove HTML tables from CV summary field - - + + + + + + 0 + 0 + + + + + + + + + Use Series Start Date as Volume + + + + + + + Clear Form Before Importing Comic Vine data + + + + + + + Remove HTML tables from CV summary field + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + false + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 200 + 16777215 + + + + Comic Vine API Key + + + + + + + + 0 + 0 + + + + <html><head/><body><p>A personal API key from <a href="http://www.comicvine.com/api/"><span style=" text-decoration: underline; color:#0000ff;">Comic Vine</span></a> is recommended in order to search for tag data. Login (or create a new account) there to get your key, and enter it below.</p></body></html> + + + Qt::RichText + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse + + + + + + + Tesk Key + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + diff --git a/comictaggerlib/utils.py b/comictaggerlib/utils.py index 2508f29..4d9b529 100644 --- a/comictaggerlib/utils.py +++ b/comictaggerlib/utils.py @@ -25,13 +25,14 @@ import re import platform import locale import codecs +from settings import ComicTaggerSettings class UtilsVars: already_fixed_encoding = False def get_actual_preferred_encoding(): preferred_encoding = locale.getpreferredencoding() - if getattr(sys, 'frozen', None) and platform.system() == "Darwin": + if platform.system() == "Darwin": preferred_encoding = "utf-8" return preferred_encoding @@ -629,4 +630,34 @@ if qt_available: # And vertical position the same, but with the height dimensions vpos = ( main_window_size.height() - window.height() ) / 2 # And the move call repositions the window - window.move(hpos + main_window_size.left(), vpos + main_window_size.top()) + window.move(hpos + main_window_size.left(), vpos + main_window_size.top()) + + try: + from PIL import Image + from PIL import WebPImagePlugin + import StringIO + pil_available = True + except ImportError: + pil_available = False + + def getQImageFromData(image_data): + img = QtGui.QImage() + success = img.loadFromData( image_data ) + if not success: + try: + if pil_available: + # Qt doesn't understand the format, but maybe PIL does + # so try to convert the image data to uncompressed tiff format + im = Image.open(StringIO.StringIO(image_data)) + output = StringIO.StringIO() + im.save(output, format="TIFF") + img.loadFromData( output.getvalue() ) + success = True + except Exception as e: + pass + # if still nothing, go with default image + if not success: + img.load(ComicTaggerSettings.getGraphic('nocover.png')) + return img + + diff --git a/comictaggerlib/volumeselectionwindow.py b/comictaggerlib/volumeselectionwindow.py index 4231741..c421029 100644 --- a/comictaggerlib/volumeselectionwindow.py +++ b/comictaggerlib/volumeselectionwindow.py @@ -46,15 +46,18 @@ class SearchThread( QtCore.QThread): QtCore.QThread.__init__(self) self.series_name = series_name self.refresh = refresh + self.error_code = None def run(self): - comicVine = ComicVineTalker( ) + comicVine = ComicVineTalker() try: self.cv_error = False self.cv_search_results = comicVine.searchForSeries( self.series_name, callback=self.prog_callback, refresh_cache=self.refresh ) - except ComicVineTalkerException: + except ComicVineTalkerException as e: self.cv_search_results = [] self.cv_error = True + self.error_code = e.code + finally: self.searchComplete.emit() @@ -293,7 +296,10 @@ class VolumeSelectionWindow(QtGui.QDialog): def searchComplete( self ): self.progdialog.accept() if self.search_thread.cv_error: - QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to search for series!")) + if self.search_thread.error_code == ComicVineTalkerException.RateLimit: + QtGui.QMessageBox.critical(self, self.tr("Comic Vine Error"), ComicVineTalker.getRateLimitMessage()) + else: + QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to search for series!")) return self.cv_search_results = self.search_thread.cv_search_results