def __init__(self, parent, mode, expand_on_click=True): super(CoverImageWidget, self).__init__(parent) uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui'), self) reduceWidgetFontSize(self.label) self.mode = mode self.comicVine = ComicVineTalker() self.page_loader = None self.showControls = True self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png'))) self.btnRight.setIcon( QIcon(ComicTaggerSettings.getGraphic('right.png'))) self.btnLeft.clicked.connect(self.decrementImage) self.btnRight.clicked.connect(self.incrementImage) self.resetWidget() if expand_on_click: clickable(self.lblImage).connect(self.showPopup) else: self.lblImage.setToolTip("") self.updateContent()
def performQuery( self ): QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) try: 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!")) return while self.twList.rowCount() > 0: self.twList.removeRow(0) self.twList.setSortingEnabled(False) row = 0 for record in self.issue_list: self.twList.insertRow(row) item_text = record['issue_number'] item = IssueNumberTableWidgetItem(item_text) item.setData( QtCore.Qt.ToolTipRole, item_text ) item.setData( QtCore.Qt.UserRole ,record['id']) item.setData(QtCore.Qt.DisplayRole, item_text) item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled) self.twList.setItem(row, 0, item) item_text = record['cover_date'] if item_text is None: item_text = "" #remove the day of "YYYY-MM-DD" parts = item_text.split("-") if len(parts) > 1: item_text = parts[0] + "-" + parts[1] item = QtGui.QTableWidgetItem(item_text) item.setData( QtCore.Qt.ToolTipRole, item_text ) item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled) self.twList.setItem(row, 1, item) item_text = record['name'] if item_text is None: item_text = "" item = QtGui.QTableWidgetItem(item_text) item.setData( QtCore.Qt.ToolTipRole, item_text ) item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled) self.twList.setItem(row, 2, item) if IssueString(record['issue_number']).asString().lower() == IssueString(self.issue_number).asString().lower(): self.initial_id = record['id'] row += 1 self.twList.setSortingEnabled(True) self.twList.sortItems( 0 , QtCore.Qt.AscendingOrder ) QtGui.QApplication.restoreOverrideCursor()
def run(self): 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: self.cv_search_results = [] self.cv_error = True finally: self.searchComplete.emit()
def setIssueID(self, issue_id): if self.mode == CoverImageWidget.AltCoverMode: self.resetWidget() self.updateContent() self.issue_id = issue_id self.comicVine = ComicVineTalker() self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete) self.comicVine.asyncFetchIssueCoverURLs(int(self.issue_id))
def startAltCoverSearch(self): # now we need to get the list of alt cover URLs self.label.setText("Searching for alt. covers...") # page URL should already be cached, so no need to defer self.comicVine = ComicVineTalker() issue_page_url = self.comicVine.fetchIssuePageURL(self.issue_id) self.comicVine.altUrlListFetchComplete.connect( self.altCoverUrlListFetchComplete) self.comicVine.asyncFetchAlternateCoverURLs(int(self.issue_id), issue_page_url)
def run(self): 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 as e: self.cv_search_results = [] self.cv_error = True self.error_code = e.code finally: self.searchComplete.emit()
def actual_issue_data_fetch( match, settings, opts ): # now get the particular issue data try: 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 "Network error while getting issue details. Save aborted" return None if settings.apply_cbl_transform_on_cv_import: cv_md = CBLTransformer( cv_md, settings ).apply() return cv_md
def actual_issue_data_fetch( match, settings, opts ): # now get the particular issue data try: 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 if settings.apply_cbl_transform_on_cv_import: cv_md = CBLTransformer( cv_md, settings ).apply() return cv_md
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 startAltCoverSearch( self ): # now we need to get the list of alt cover URLs self.label.setText("Searching for alt. covers...") # page URL should already be cached, so no need to defer self.comicVine = ComicVineTalker() issue_page_url = self.comicVine.fetchIssuePageURL( self.issue_id ) self.comicVine.altUrlListFetchComplete.connect( self.altCoverUrlListFetchComplete ) self.comicVine.asyncFetchAlternateCoverURLs( int(self.issue_id), issue_page_url)
def setIssueID( self, issue_id ): if self.mode == CoverImageWidget.AltCoverMode: self.resetWidget() self.updateContent() self.issue_id = issue_id self.comicVine = ComicVineTalker() self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete ) self.comicVine.asyncFetchIssueCoverURLs( int(self.issue_id) )
class CoverImageWidget(QWidget): ArchiveMode = 0 AltCoverMode = 1 URLMode = 1 DataMode = 3 def __init__(self, parent, mode, expand_on_click=True): super(CoverImageWidget, self).__init__(parent) uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui'), self) reduceWidgetFontSize(self.label) self.mode = mode self.comicVine = ComicVineTalker() self.page_loader = None self.showControls = True self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png'))) self.btnRight.setIcon( QIcon(ComicTaggerSettings.getGraphic('right.png'))) self.btnLeft.clicked.connect(self.decrementImage) self.btnRight.clicked.connect(self.incrementImage) self.resetWidget() if expand_on_click: clickable(self.lblImage).connect(self.showPopup) else: self.lblImage.setToolTip("") self.updateContent() def resetWidget(self): self.comic_archive = None self.issue_id = None self.comicVine = None self.cover_fetcher = None self.url_list = [] if self.page_loader is not None: self.page_loader.abandoned = True self.page_loader = None self.imageIndex = -1 self.imageCount = 1 self.imageData = None def clear(self): self.resetWidget() self.updateContent() def incrementImage(self): self.imageIndex += 1 if self.imageIndex == self.imageCount: self.imageIndex = 0 self.updateContent() def decrementImage(self): self.imageIndex -= 1 if self.imageIndex == -1: self.imageIndex = self.imageCount - 1 self.updateContent() def setArchive(self, ca, page=0): if self.mode == CoverImageWidget.ArchiveMode: self.resetWidget() self.comic_archive = ca self.imageIndex = page self.imageCount = ca.getNumberOfPages() self.updateContent() def setURL(self, url): if self.mode == CoverImageWidget.URLMode: self.resetWidget() self.updateContent() self.url_list = [url] self.imageIndex = 0 self.imageCount = 1 self.updateContent() def setIssueID(self, issue_id): if self.mode == CoverImageWidget.AltCoverMode: self.resetWidget() self.updateContent() self.issue_id = issue_id self.comicVine = ComicVineTalker() self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete) self.comicVine.asyncFetchIssueCoverURLs(int(self.issue_id)) def setImageData(self, image_data): if self.mode == CoverImageWidget.DataMode: self.resetWidget() if image_data is None: self.imageIndex = -1 else: self.imageIndex = 0 self.imageData = image_data self.updateContent() def primaryUrlFetchComplete(self, primary_url, thumb_url, issue_id): self.url_list.append(str(primary_url)) self.imageIndex = 0 self.imageCount = len(self.url_list) self.updateContent() # defer the alt cover search QTimer.singleShot(1, self.startAltCoverSearch) def startAltCoverSearch(self): # now we need to get the list of alt cover URLs self.label.setText("Searching for alt. covers...") # page URL should already be cached, so no need to defer self.comicVine = ComicVineTalker() issue_page_url = self.comicVine.fetchIssuePageURL(self.issue_id) self.comicVine.altUrlListFetchComplete.connect( self.altCoverUrlListFetchComplete) self.comicVine.asyncFetchAlternateCoverURLs(int(self.issue_id), issue_page_url) def altCoverUrlListFetchComplete(self, url_list, issue_id): if len(url_list) > 0: self.url_list.extend(url_list) self.imageCount = len(self.url_list) self.updateControls() def setPage(self, pagenum): if self.mode == CoverImageWidget.ArchiveMode: self.imageIndex = pagenum self.updateContent() def updateContent(self): self.updateImage() self.updateControls() def updateImage(self): if self.imageIndex == -1: self.loadDefault() elif self.mode in [ CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode ]: self.loadURL() elif self.mode == CoverImageWidget.DataMode: self.coverRemoteFetchComplete(self.imageData, 0) else: self.loadPage() def updateControls(self): if not self.showControls or self.mode == CoverImageWidget.DataMode: self.btnLeft.hide() self.btnRight.hide() self.label.hide() return if self.imageIndex == -1 or self.imageCount == 1: self.btnLeft.setEnabled(False) self.btnRight.setEnabled(False) self.btnLeft.hide() self.btnRight.hide() else: self.btnLeft.setEnabled(True) self.btnRight.setEnabled(True) self.btnLeft.show() self.btnRight.show() if self.imageIndex == -1 or self.imageCount == 1: self.label.setText("") elif self.mode == CoverImageWidget.AltCoverMode: self.label.setText("Cover {0} (of {1})".format( self.imageIndex + 1, self.imageCount)) else: self.label.setText("Page {0} (of {1})".format( self.imageIndex + 1, self.imageCount)) def loadURL(self): self.loadDefault() self.cover_fetcher = ImageFetcher() self.cover_fetcher.fetchComplete.connect(self.coverRemoteFetchComplete) self.cover_fetcher.fetch(self.url_list[self.imageIndex]) #print("ATB cover fetch started...") # called when the image is done loading from internet def coverRemoteFetchComplete(self, image_data, issue_id): img = getQImageFromData(image_data) self.current_pixmap = QPixmap(img) self.setDisplayPixmap(0, 0) #print("ATB cover fetch complete!") def loadPage(self): if self.comic_archive is not None: if self.page_loader is not None: self.page_loader.abandoned = True self.page_loader = PageLoader(self.comic_archive, self.imageIndex) self.page_loader.loadComplete.connect(self.pageLoadComplete) self.page_loader.start() def pageLoadComplete(self, img): self.current_pixmap = QPixmap(img) self.setDisplayPixmap(0, 0) self.page_loader = None def loadDefault(self): self.current_pixmap = QPixmap( ComicTaggerSettings.getGraphic('nocover.png')) #print("loadDefault called") self.setDisplayPixmap(0, 0) def resizeEvent(self, resize_event): if self.current_pixmap is not None: delta_w = resize_event.size().width() - \ resize_event.oldSize().width() delta_h = resize_event.size().height() - \ resize_event.oldSize().height() # print "ATB resizeEvent deltas", resize_event.size().width(), # resize_event.size().height() self.setDisplayPixmap(delta_w, delta_h) def setDisplayPixmap(self, delta_w, delta_h): """The deltas let us know what the new width and height of the label will be""" #new_h = self.frame.height() + delta_h #new_w = self.frame.width() + delta_w # print "ATB setDisplayPixmap deltas", delta_w , delta_h # print "ATB self.frame", self.frame.width(), self.frame.height() # print "ATB self.", self.width(), self.height() #frame_w = new_w #frame_h = new_h new_h = self.frame.height() new_w = self.frame.width() frame_w = self.frame.width() frame_h = self.frame.height() new_h -= 4 new_w -= 4 if new_h < 0: new_h = 0 if new_w < 0: new_w = 0 # print "ATB setDisplayPixmap deltas", delta_w , delta_h # print "ATB self.frame", frame_w, frame_h # print "ATB new size", new_w, new_h # scale the pixmap to fit in the frame scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio) self.lblImage.setPixmap(scaled_pixmap) # move and resize the label to be centered in the fame img_w = scaled_pixmap.width() img_h = scaled_pixmap.height() self.lblImage.resize(img_w, img_h) self.lblImage.move((frame_w - img_w) / 2, (frame_h - img_h) / 2) def showPopup(self): self.popup = ImagePopup(self, self.current_pixmap)
def process_file_cli( filename, opts, settings, match_results ): batch_mode = len( opts.file_list ) > 1 ca = ComicArchive(filename, settings.rar_exe_path) if not os.path.lexists( filename ): print "Cannot find "+ filename return if not ca.seemsToBeAComicArchive(): print "Sorry, but "+ filename + " is not a comic archive!" return #if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ): if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ): print "This archive is not writable for that tag type" return has = [ False, False, False ] if ca.hasCIX(): has[ MetaDataStyle.CIX ] = True if ca.hasCBI(): has[ MetaDataStyle.CBI ] = True if ca.hasCoMet(): has[ MetaDataStyle.COMET ] = True if opts.print_tags: if opts.data_style is None: page_count = ca.getNumberOfPages() brief = "" if batch_mode: brief = u"{0}: ".format(filename) if ca.isZip(): brief += "ZIP archive " elif ca.isRar(): brief += "RAR archive " elif ca.isFolder(): brief += "Folder archive " brief += "({0: >3} pages)".format(page_count) brief += " tags:[ " if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ): brief += "none " else: if has[ MetaDataStyle.CBI ]: brief += "CBL " if has[ MetaDataStyle.CIX ]: brief += "CR " if has[ MetaDataStyle.COMET ]: brief += "CoMet " brief += "]" print brief if opts.terse: return print if opts.data_style is None or opts.data_style == MetaDataStyle.CIX: if has[ MetaDataStyle.CIX ]: print "------ComicRack tags--------" if opts.raw: print u"{0}".format(unicode(ca.readRawCIX(), errors='ignore')) else: print u"{0}".format(ca.readCIX()) if opts.data_style is None or opts.data_style == MetaDataStyle.CBI: if has[ MetaDataStyle.CBI ]: print "------ComicBookLover tags--------" if opts.raw: pprint(json.loads(ca.readRawCBI())) else: print u"{0}".format(ca.readCBI()) if opts.data_style is None or opts.data_style == MetaDataStyle.COMET: if has[ MetaDataStyle.COMET ]: print "------CoMet tags--------" if opts.raw: print u"{0}".format(ca.readRawCoMet()) else: print u"{0}".format(ca.readCoMet()) elif opts.delete_tags: style_name = MetaDataStyle.name[ opts.data_style ] if has[ opts.data_style ]: if not opts.dryrun: if not ca.removeMetadata( opts.data_style ): print u"{0}: Tag removal seemed to fail!".format( filename ) else: print u"{0}: Removed {1} tags.".format( filename, style_name ) else: print u"{0}: dry-run. {1} tags not removed".format( filename, style_name ) else: print u"{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name ) elif opts.copy_tags: dst_style_name = MetaDataStyle.name[ opts.data_style ] if opts.no_overwrite and has[ opts.data_style ]: print u"{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name) return if opts.copy_source == opts.data_style: print u"{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name) return src_style_name = MetaDataStyle.name[ opts.copy_source ] if has[ opts.copy_source ]: if not opts.dryrun: md = ca.readMetadata( opts.copy_source ) if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI: md = CBLTransformer( md, settings ).apply() if not ca.writeMetadata( md, opts.data_style ): print u"{0}: Tag copy seemed to fail!".format( filename ) else: print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name ) else: print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name ) else: print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name ) elif opts.save_tags: if opts.no_overwrite and has[ opts.data_style ]: print u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ]) return if batch_mode: print u"Processing {0}...".format(filename) md = create_local_metadata( opts, ca, has[ opts.data_style ] ) if md.issue is None or md.issue == "": if opts.assume_issue_is_one_if_not_set: md.issue = "1" # now, search online if opts.search_online: if opts.issue_id is not None: # we were given the actual ID to search with try: 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 "Network error while getting issue details. Save aborted" match_results.fetchDataFailures.append(filename) return if cv_md is None: print "No match for ID {0} was found.".format(opts.issue_id) match_results.noMatches.append(filename) return if settings.apply_cbl_transform_on_cv_import: cv_md = CBLTransformer( cv_md, settings ).apply() else: ii = IssueIdentifier( ca, settings ) if md is None or md.isEmpty: print "No metadata given to search online with!" match_results.noMatches.append(filename) return def myoutput( text ): if opts.verbose: IssueIdentifier.defaultWriteOutput( text ) # 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() result = ii.search_result found_match = False choices = False low_confidence = False if result == ii.ResultNoMatches: pass elif result == ii.ResultFoundMatchButBadCoverScore: low_confidence = True found_match = True elif result == ii.ResultFoundMatchButNotFirstPage : found_match = True elif result == ii.ResultMultipleMatchesWithBadImageScores: low_confidence = True choices = True elif result == ii.ResultOneGoodMatch: found_match = True elif result == ii.ResultMultipleGoodMatches: choices = True if choices: if low_confidence: print "Online search: Multiple low confidence matches. Save aborted" match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches)) return else: print "Online search: Multiple good matches. Save aborted" match_results.multipleMatches.append(MultipleMatch(filename,matches)) return if low_confidence and opts.abortOnLowConfidence: print "Online search: Low confidence match. Save aborted" match_results.lowConfidenceMatches.append(MultipleMatch(filename,matches)) return if not found_match: print "Online search: No match found. Save aborted" match_results.noMatches.append(filename) return # we got here, so we have a single match # now get the particular issue data cv_md = actual_issue_data_fetch(matches[0], settings, opts) if cv_md is None: match_results.fetchDataFailures.append(filename) return md.overlay( cv_md ) # ok, done building our metadata. time to save if not actual_metadata_save( ca, opts, md ): match_results.writeFailures.append(filename) else: match_results.goodMatches.append(filename) elif opts.rename_file: msg_hdr = "" if batch_mode: msg_hdr = u"{0}: ".format(filename) if opts.data_style is not None: use_tags = has[ opts.data_style ] else: use_tags = False md = create_local_metadata( opts, ca, use_tags ) if md.series is None: print msg_hdr + "Can't rename without series name" return new_ext = None # default if settings.rename_extension_based_on_archive: if ca.isZip(): new_ext = ".cbz" elif ca.isRar(): new_ext = ".cbr" renamer = FileRenamer( md ) renamer.setTemplate( settings.rename_template ) renamer.setIssueZeroPadding( settings.rename_issue_number_padding ) renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup ) new_name = renamer.determineName( filename, ext=new_ext ) if new_name == os.path.basename(filename): print msg_hdr + "Filename is already good!" return folder = os.path.dirname( os.path.abspath( filename ) ) new_abs_path = utils.unique_file( os.path.join( folder, new_name ) ) suffix = "" if not opts.dryrun: # rename the file os.rename( filename, new_abs_path ) else: suffix = " (dry-run, no change)" print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix) elif opts.export_to_zip: msg_hdr = "" if batch_mode: msg_hdr = u"{0}: ".format(filename) if not ca.isRar(): print msg_hdr + "Archive is not a RAR." return rar_file = os.path.abspath( os.path.abspath( filename ) ) new_file = os.path.splitext(rar_file)[0] + ".cbz" if opts.abort_export_on_conflict and os.path.lexists( new_file ): print msg_hdr + "{0} already exists in the that folder.".format(os.path.split(new_file)[1]) return new_file = utils.unique_file( os.path.join( new_file ) ) delete_success = False export_success = False if not opts.dryrun: if ca.exportAsZip( new_file ): export_success = True if opts.delete_rar_after_export: try: os.unlink( rar_file ) except: print msg_hdr + "Error deleting original RAR after export" delete_success = False else: delete_success = True else: # last export failed, so remove the zip, if it exists if os.path.lexists( new_file ): os.remove( new_file ) else: msg = msg_hdr + u"Dry-run: Would try to create {0}".format(os.path.split(new_file)[1]) if opts.delete_rar_after_export: msg += u" and delete orginal." print msg return msg = msg_hdr if export_success: msg += u"Archive exported successfully to: {0}".format( os.path.split(new_file)[1] ) if opts.delete_rar_after_export and delete_success: msg += u" (Original deleted) " else: msg += u"Archive failed to export!" print msg
def search( self ): ca = self.comic_archive self.match_list = [] self.cancel = False self.search_result = self.ResultNoMatches if not pil_available: self.log_msg( "Python Imaging Library (PIL) is not available and is needed for issue identification." ) return self.match_list if not ca.seemsToBeAComicArchive(): self.log_msg( "Sorry, but "+ opts.filename + " is not a comic archive!") return self.match_list cover_image_data = ca.getPage( self.cover_page_index ) cover_hash = self.calculateHash( cover_image_data ) #check the apect ratio # if it's wider than it is high, it's probably a two page spread # if so, crop it and calculate a second hash narrow_cover_hash = None aspect_ratio = self.getAspectRatio( cover_image_data ) if aspect_ratio < 1.0: right_side_image_data = self.cropCover( cover_image_data ) if right_side_image_data is not None: narrow_cover_hash = self.calculateHash( right_side_image_data ) #self.log_msg( "Cover hash = {0:016x}".format(cover_hash) ) keys = self.getSearchKeys() #normalize the issue number keys['issue_number'] = IssueString(keys['issue_number']).asString() # we need, at minimum, a series and issue number if keys['series'] is None or keys['issue_number'] is None: self.log_msg("Not enough info for a search!") return [] self.log_msg( "Going to search for:" ) self.log_msg( "\tSeries: " + keys['series'] ) self.log_msg( "\tIssue : " + keys['issue_number'] ) if keys['issue_count'] is not None: self.log_msg( "\tCount : " + str(keys['issue_count']) ) if keys['year'] is not None: self.log_msg( "\tYear : " + str(keys['year']) ) if keys['month'] is not None: self.log_msg( "\tMonth : " + str(keys['month']) ) #self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist)) comicVine = ComicVineTalker( ) comicVine.setLogFunc( self.output_function ) #self.log_msg( ( "Searching for " + keys['series'] + "...") self.log_msg( u"Searching for {0} #{1} ...".format( keys['series'], keys['issue_number']) ) try: cv_search_results = comicVine.searchForSeries( keys['series'] ) except ComicVineTalkerException: self.log_msg( "Network issue while searching for series. Aborting...") return [] #self.log_msg( "Found " + str(len(cv_search_results)) + " initial results" ) if self.cancel == True: return [] series_second_round_list = [] #self.log_msg( "Removing results with too long names, banned publishers, or future start dates" ) for item in cv_search_results: length_approved = False publisher_approved = True date_approved = True # remove any series that starts after the issue year if keys['year'] is not None and str(keys['year']).isdigit() and item['start_year'] is not None and str(item['start_year']).isdigit(): if int(keys['year']) < int(item['start_year']): date_approved = False #assume that our search name is close to the actual name, say within ,e.g. 5 chars shortened_key = utils.removearticles(keys['series']) shortened_item_name = utils.removearticles(item['name']) if len( shortened_item_name ) < ( len( shortened_key ) + self.length_delta_thresh) : length_approved = True # remove any series from publishers on the blacklist if item['publisher'] is not None: publisher = item['publisher']['name'] if publisher is not None and publisher.lower() in self.publisher_blacklist: publisher_approved = False if length_approved and publisher_approved and date_approved: series_second_round_list.append(item) self.log_msg( "Searching in " + str(len(series_second_round_list)) +" series" ) if self.callback is not None: self.callback( 0, len(series_second_round_list)) # now sort the list by name length series_second_round_list.sort(key=lambda x: len(x['name']), reverse=False) #build a list of volume IDs volume_id_list = list() for series in series_second_round_list: volume_id_list.append( series['id']) try: issue_list = comicVine.fetchIssuesByVolumeIssueNumAndYear( volume_id_list, keys['issue_number'], keys['year']) except ComicVineTalkerException: self.log_msg( "Network issue while searching for series details. Aborting...") return [] shortlist = list() #now re-associate the issues and volumes for issue in issue_list: for series in series_second_round_list: if series['id'] == issue['volume']['id']: shortlist.append( (series, issue) ) break if keys['year'] is None: self.log_msg( u"Found {0} series that have an issue #{1}".format(len(shortlist), keys['issue_number']) ) else: self.log_msg( u"Found {0} series that have an issue #{1} from {2}".format(len(shortlist), keys['issue_number'], keys['year'] )) # now we have a shortlist of volumes with the desired issue number # Do first round of cover matching counter = len(shortlist) for series, issue in shortlist: if self.callback is not None: self.callback( counter, len(shortlist)*3) counter += 1 self.log_msg( u"Examining covers for ID: {0} {1} ({2}) ...".format( series['id'], series['name'], series['start_year']), newline=False ) # parse out the cover date day, month, year = comicVine.parseDateStr( issue['cover_date'] ) # Now check the cover match against the primary image hash_list = [ cover_hash ] if narrow_cover_hash is not None: hash_list.append(narrow_cover_hash) try: image_url = issue['image']['super_url'] thumb_url = issue['image']['thumb_url'] page_url = issue['site_detail_url'] score_item = self.getIssueCoverMatchScore( comicVine, issue['id'], image_url, thumb_url, page_url, hash_list, useRemoteAlternates = False ) except: self.match_list = [] return self.match_list match = dict() match['series'] = u"{0} ({1})".format(series['name'], series['start_year']) match['distance'] = score_item['score'] match['issue_number'] = keys['issue_number'] match['cv_issue_count'] = series['count_of_issues'] match['url_image_hash'] = score_item['hash'] match['issue_title'] = issue['name'] match['issue_id'] = issue['id'] match['volume_id'] = series['id'] match['month'] = month match['year'] = year match['publisher'] = None if series['publisher'] is not None: match['publisher'] = series['publisher']['name'] match['image_url'] = image_url match['thumb_url'] = thumb_url match['page_url'] = page_url match['description'] = issue['description'] self.match_list.append(match) self.log_msg( " --> {0}".format(match['distance']), newline=False ) self.log_msg( "" ) if len(self.match_list) == 0: self.log_msg( ":-( no matches!" ) self.search_result = self.ResultNoMatches return self.match_list # sort list by image match scores self.match_list.sort(key=lambda k: k['distance']) l = [] for i in self.match_list: l.append( i['distance'] ) self.log_msg( "Compared to covers in {0} issue(s):".format(len(self.match_list)), newline=False) self.log_msg( str(l)) def print_match(item): self.log_msg( u"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format( item['series'], item['issue_number'], item['issue_title'], item['month'], item['year'], item['distance']) ) best_score = self.match_list[0]['distance'] if best_score >= self.min_score_thresh: # we have 1 or more low-confidence matches (all bad cover scores) # look at a few more pages in the archive, and also alternate covers online self.log_msg( "Very weak scores for the cover. Analyzing alternate pages and covers..." ) hash_list = [ cover_hash ] if narrow_cover_hash is not None: hash_list.append(narrow_cover_hash) for i in range( 1, min(3, ca.getNumberOfPages())): image_data = ca.getPage(i) page_hash = self.calculateHash( image_data ) hash_list.append( page_hash ) second_match_list = [] counter = 2*len(self.match_list) for m in self.match_list: if self.callback is not None: self.callback( counter, len(self.match_list)*3) counter += 1 self.log_msg( u"Examining alternate covers for ID: {0} {1} ...".format( m['volume_id'], m['series']), newline=False ) try: score_item = self.getIssueCoverMatchScore( comicVine, m['issue_id'], m['image_url'], m['thumb_url'], m['page_url'], hash_list, useRemoteAlternates = True ) except: self.match_list = [] return self.match_list self.log_msg("--->{0}".format(score_item['score'])) self.log_msg( "" ) if score_item['score'] < self.min_alternate_score_thresh: second_match_list.append(m) m['distance'] = score_item['score'] if len( second_match_list ) == 0: if len( self.match_list) == 1: self.log_msg( "No matching pages in the issue." ) self.log_msg( u"--------------------------------------------------") print_match(self.match_list[0]) self.log_msg( u"--------------------------------------------------") self.search_result = self.ResultFoundMatchButBadCoverScore else: self.log_msg( u"--------------------------------------------------") self.log_msg( u"Multiple bad cover matches! Need to use other info..." ) self.log_msg( u"--------------------------------------------------") self.search_result = self.ResultMultipleMatchesWithBadImageScores return self.match_list else: # We did good, found something! self.log_msg( "Success in secondary/alternate cover matching!" ) self.match_list = second_match_list # sort new list by image match scores self.match_list.sort(key=lambda k: k['distance']) best_score = self.match_list[0]['distance'] self.log_msg("[Second round cover matching: best score = {0}]".format(best_score)) # now drop down into the rest of the processing if self.callback is not None: self.callback( 99, 100) #now pare down list, remove any item more than specified distant from the top scores for item in reversed(self.match_list): if item['distance'] > best_score + self.min_score_distance: self.match_list.remove(item) # One more test for the case choosing limited series first issue vs a trade with the same cover: # if we have a given issue count > 1 and the volume from CV has count==1, remove it from match list if len(self.match_list) >= 2 and keys['issue_count'] is not None and keys['issue_count'] != 1: new_list = list() for match in self.match_list: if match['cv_issue_count'] != 1: new_list.append(match) else: self.log_msg("Removing volume {0} [{1}] from consideration (only 1 issue)".format(match['series'], match['volume_id'])) if len(new_list) > 0: self.match_list = new_list if len(self.match_list) == 1: self.log_msg( u"--------------------------------------------------") print_match(self.match_list[0]) self.log_msg( u"--------------------------------------------------") self.search_result = self.ResultOneGoodMatch elif len(self.match_list) == 0: self.log_msg( u"--------------------------------------------------") self.log_msg( "No matches found :(" ) self.log_msg( u"--------------------------------------------------") self.search_result = self.ResultNoMatches else: # we've got multiple good matches: self.log_msg( "More than one likley candiate." ) self.search_result = self.ResultMultipleGoodMatches self.log_msg( u"--------------------------------------------------") for item in self.match_list: print_match(item) self.log_msg( u"--------------------------------------------------") return self.match_list
def search(self): ca = self.comic_archive self.match_list = [] self.cancel = False self.search_result = self.ResultNoMatches if not pil_available: self.log_msg( "Python Imaging Library (PIL) is not available and is needed for issue identification.") return self.match_list if not ca.seemsToBeAComicArchive(): self.log_msg( "Sorry, but " + opts.filename + " is not a comic archive!") return self.match_list cover_image_data = ca.getPage(self.cover_page_index) cover_hash = self.calculateHash(cover_image_data) # check the aspect ratio # if it's wider than it is high, it's probably a two page spread # if so, crop it and calculate a second hash narrow_cover_hash = None aspect_ratio = self.getAspectRatio(cover_image_data) if aspect_ratio < 1.0: right_side_image_data = self.cropCover(cover_image_data) if right_side_image_data is not None: narrow_cover_hash = self.calculateHash(right_side_image_data) #self.log_msg("Cover hash = {0:016x}".format(cover_hash)) keys = self.getSearchKeys() # normalize the issue number keys['issue_number'] = IssueString(keys['issue_number']).asString() # we need, at minimum, a series and issue number if keys['series'] is None or keys['issue_number'] is None: self.log_msg("Not enough info for a search!") return [] self.log_msg("Going to search for:") self.log_msg("\tSeries: " + keys['series']) self.log_msg("\tIssue: " + keys['issue_number']) if keys['issue_count'] is not None: self.log_msg("\tCount: " + str(keys['issue_count'])) if keys['year'] is not None: self.log_msg("\tYear: " + str(keys['year'])) if keys['month'] is not None: 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'] + "...") self.log_msg(u"Searching for {0} #{1} ...".format( keys['series'], keys['issue_number'])) try: cv_search_results = comicVine.searchForSeries(keys['series']) except ComicVineTalkerException: self.log_msg( "Network issue while searching for series. Aborting...") return [] #self.log_msg("Found " + str(len(cv_search_results)) + " initial results") if self.cancel: return [] if cv_search_results is None: return [] series_second_round_list = [] #self.log_msg("Removing results with too long names, banned publishers, or future start dates") for item in cv_search_results: length_approved = False publisher_approved = True date_approved = True # remove any series that starts after the issue year if keys['year'] is not None and str( keys['year']).isdigit() and item['start_year'] is not None and str( item['start_year']).isdigit(): if int(keys['year']) < int(item['start_year']): date_approved = False # assume that our search name is close to the actual name, say # within ,e.g. 5 chars shortened_key = utils.removearticles(keys['series']) shortened_item_name = utils.removearticles(item['name']) if len(shortened_item_name) < ( len(shortened_key) + self.length_delta_thresh): length_approved = True # remove any series from publishers on the blacklist if item['publisher'] is not None: publisher = item['publisher']['name'] if publisher is not None and publisher.lower( ) in self.publisher_blacklist: publisher_approved = False if length_approved and publisher_approved and date_approved: series_second_round_list.append(item) self.log_msg( "Searching in " + str(len(series_second_round_list)) + " series") if self.callback is not None: self.callback(0, len(series_second_round_list)) # now sort the list by name length series_second_round_list.sort( key=lambda x: len(x['name']), reverse=False) # build a list of volume IDs volume_id_list = list() for series in series_second_round_list: volume_id_list.append(series['id']) try: issue_list = comicVine.fetchIssuesByVolumeIssueNumAndYear( volume_id_list, keys['issue_number'], keys['year']) except ComicVineTalkerException: 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: for series in series_second_round_list: if series['id'] == issue['volume']['id']: shortlist.append((series, issue)) break if keys['year'] is None: self.log_msg(u"Found {0} series that have an issue #{1}".format( len(shortlist), keys['issue_number'])) else: self.log_msg( u"Found {0} series that have an issue #{1} from {2}".format( len(shortlist), keys['issue_number'], keys['year'])) # now we have a shortlist of volumes with the desired issue number # Do first round of cover matching counter = len(shortlist) for series, issue in shortlist: if self.callback is not None: self.callback(counter, len(shortlist) * 3) counter += 1 self.log_msg(u"Examining covers for ID: {0} {1} ({2}) ...".format( series['id'], series['name'], series['start_year']), newline=False) # parse out the cover date day, month, year = comicVine.parseDateStr(issue['cover_date']) # Now check the cover match against the primary image hash_list = [cover_hash] if narrow_cover_hash is not None: hash_list.append(narrow_cover_hash) try: image_url = issue['image']['super_url'] thumb_url = issue['image']['thumb_url'] page_url = issue['site_detail_url'] score_item = self.getIssueCoverMatchScore( comicVine, issue['id'], image_url, thumb_url, page_url, hash_list, useRemoteAlternates=False) except: self.match_list = [] return self.match_list match = dict() match['series'] = u"{0} ({1})".format( series['name'], series['start_year']) match['distance'] = score_item['score'] match['issue_number'] = keys['issue_number'] match['cv_issue_count'] = series['count_of_issues'] match['url_image_hash'] = score_item['hash'] match['issue_title'] = issue['name'] match['issue_id'] = issue['id'] match['volume_id'] = series['id'] match['month'] = month match['year'] = year match['publisher'] = None if series['publisher'] is not None: match['publisher'] = series['publisher']['name'] match['image_url'] = image_url match['thumb_url'] = thumb_url match['page_url'] = page_url match['description'] = issue['description'] self.match_list.append(match) self.log_msg(" --> {0}".format(match['distance']), newline=False) self.log_msg("") if len(self.match_list) == 0: self.log_msg(":-(no matches!") self.search_result = self.ResultNoMatches return self.match_list # sort list by image match scores self.match_list.sort(key=lambda k: k['distance']) l = [] for i in self.match_list: l.append(i['distance']) self.log_msg("Compared to covers in {0} issue(s):".format( len(self.match_list)), newline=False) self.log_msg(str(l)) def print_match(item): self.log_msg(u"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format( item['series'], item['issue_number'], item['issue_title'], item['month'], item['year'], item['distance'])) best_score = self.match_list[0]['distance'] if best_score >= self.min_score_thresh: # we have 1 or more low-confidence matches (all bad cover scores) # look at a few more pages in the archive, and also alternate # covers online self.log_msg( "Very weak scores for the cover. Analyzing alternate pages and covers...") hash_list = [cover_hash] if narrow_cover_hash is not None: hash_list.append(narrow_cover_hash) for i in range(1, min(3, ca.getNumberOfPages())): image_data = ca.getPage(i) page_hash = self.calculateHash(image_data) hash_list.append(page_hash) second_match_list = [] counter = 2 * len(self.match_list) for m in self.match_list: if self.callback is not None: self.callback(counter, len(self.match_list) * 3) counter += 1 self.log_msg( u"Examining alternate covers for ID: {0} {1} ...".format( m['volume_id'], m['series']), newline=False) try: score_item = self.getIssueCoverMatchScore( comicVine, m['issue_id'], m['image_url'], m['thumb_url'], m['page_url'], hash_list, useRemoteAlternates=True) except: self.match_list = [] return self.match_list self.log_msg("--->{0}".format(score_item['score'])) self.log_msg("") if score_item['score'] < self.min_alternate_score_thresh: second_match_list.append(m) m['distance'] = score_item['score'] if len(second_match_list) == 0: if len(self.match_list) == 1: self.log_msg("No matching pages in the issue.") self.log_msg( u"--------------------------------------------------------------------------") print_match(self.match_list[0]) self.log_msg( u"--------------------------------------------------------------------------") self.search_result = self.ResultFoundMatchButBadCoverScore else: self.log_msg( u"--------------------------------------------------------------------------") self.log_msg( u"Multiple bad cover matches! Need to use other info...") self.log_msg( u"--------------------------------------------------------------------------") self.search_result = self.ResultMultipleMatchesWithBadImageScores return self.match_list else: # We did good, found something! self.log_msg("Success in secondary/alternate cover matching!") self.match_list = second_match_list # sort new list by image match scores self.match_list.sort(key=lambda k: k['distance']) best_score = self.match_list[0]['distance'] self.log_msg( "[Second round cover matching: best score = {0}]".format(best_score)) # now drop down into the rest of the processing if self.callback is not None: self.callback(99, 100) # now pare down list, remove any item more than specified distant from # the top scores for item in reversed(self.match_list): if item['distance'] > best_score + self.min_score_distance: self.match_list.remove(item) # One more test for the case choosing limited series first issue vs a trade with the same cover: # if we have a given issue count > 1 and the volume from CV has # count==1, remove it from match list if len(self.match_list) >= 2 and keys[ 'issue_count'] is not None and keys['issue_count'] != 1: new_list = list() for match in self.match_list: if match['cv_issue_count'] != 1: new_list.append(match) else: self.log_msg( "Removing volume {0} [{1}] from consideration (only 1 issue)".format( match['series'], match['volume_id'])) if len(new_list) > 0: self.match_list = new_list if len(self.match_list) == 1: self.log_msg( u"--------------------------------------------------------------------------") print_match(self.match_list[0]) self.log_msg( u"--------------------------------------------------------------------------") self.search_result = self.ResultOneGoodMatch elif len(self.match_list) == 0: self.log_msg( u"--------------------------------------------------------------------------") self.log_msg("No matches found :(") self.log_msg( u"--------------------------------------------------------------------------") self.search_result = self.ResultNoMatches else: # we've got multiple good matches: self.log_msg("More than one likely candidate.") self.search_result = self.ResultMultipleGoodMatches self.log_msg( u"--------------------------------------------------------------------------") for item in self.match_list: print_match(item) self.log_msg( u"--------------------------------------------------------------------------") return self.match_list
def process_file_cli(filename, opts, settings, match_results): batch_mode = len(opts.file_list) > 1 ca = ComicArchive(filename, settings.rar_exe_path) if not os.path.lexists(filename): print "Cannot find " + filename return if not ca.seemsToBeAComicArchive(): print "Sorry, but " + filename + " is not a comic archive!" return #if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ): if not ca.isWritable() and (opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file): print "This archive is not writable for that tag type" return has = [False, False, False] if ca.hasCIX(): has[MetaDataStyle.CIX] = True if ca.hasCBI(): has[MetaDataStyle.CBI] = True if ca.hasCoMet(): has[MetaDataStyle.COMET] = True if opts.print_tags: if opts.data_style is None: page_count = ca.getNumberOfPages() brief = "" if batch_mode: brief = u"{0}: ".format(filename) if ca.isZip(): brief += "ZIP archive " elif ca.isRar(): brief += "RAR archive " elif ca.isFolder(): brief += "Folder archive " brief += "({0: >3} pages)".format(page_count) brief += " tags:[ " if not (has[MetaDataStyle.CBI] or has[MetaDataStyle.CIX] or has[MetaDataStyle.COMET]): brief += "none " else: if has[MetaDataStyle.CBI]: brief += "CBL " if has[MetaDataStyle.CIX]: brief += "CR " if has[MetaDataStyle.COMET]: brief += "CoMet " brief += "]" print brief if opts.terse: return print if opts.data_style is None or opts.data_style == MetaDataStyle.CIX: if has[MetaDataStyle.CIX]: print "------ComicRack tags--------" if opts.raw: print u"{0}".format( unicode(ca.readRawCIX(), errors='ignore')) else: print u"{0}".format(ca.readCIX()) if opts.data_style is None or opts.data_style == MetaDataStyle.CBI: if has[MetaDataStyle.CBI]: print "------ComicBookLover tags--------" if opts.raw: pprint(json.loads(ca.readRawCBI())) else: print u"{0}".format(ca.readCBI()) if opts.data_style is None or opts.data_style == MetaDataStyle.COMET: if has[MetaDataStyle.COMET]: print "------CoMet tags--------" if opts.raw: print u"{0}".format(ca.readRawCoMet()) else: print u"{0}".format(ca.readCoMet()) elif opts.delete_tags: style_name = MetaDataStyle.name[opts.data_style] if has[opts.data_style]: if not opts.dryrun: if not ca.removeMetadata(opts.data_style): print u"{0}: Tag removal seemed to fail!".format(filename) else: print u"{0}: Removed {1} tags.".format( filename, style_name) else: print u"{0}: dry-run. {1} tags not removed".format( filename, style_name) else: print u"{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name) elif opts.copy_tags: dst_style_name = MetaDataStyle.name[opts.data_style] if opts.no_overwrite and has[opts.data_style]: print u"{0}: Already has {1} tags. Not overwriting.".format( filename, dst_style_name) return if opts.copy_source == opts.data_style: print u"{0}: Destination and source are same: {1}. Nothing to do.".format( filename, dst_style_name) return src_style_name = MetaDataStyle.name[opts.copy_source] if has[opts.copy_source]: if not opts.dryrun: md = ca.readMetadata(opts.copy_source) if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI: md = CBLTransformer(md, settings).apply() if not ca.writeMetadata(md, opts.data_style): print u"{0}: Tag copy seemed to fail!".format(filename) else: print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name) else: print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name) else: print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name) elif opts.save_tags: if opts.no_overwrite and has[opts.data_style]: print u"{0}: Already has {1} tags. Not overwriting.".format( filename, MetaDataStyle.name[opts.data_style]) return if batch_mode: print u"Processing {0}...".format(filename) md = create_local_metadata(opts, ca, has[opts.data_style]) if md.issue is None or md.issue == "": if opts.assume_issue_is_one_if_not_set: md.issue = "1" # now, search online if opts.search_online: if opts.issue_id is not None: # we were given the actual ID to search with try: 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 "Network error while getting issue details. Save aborted" match_results.fetchDataFailures.append(filename) return if cv_md is None: print "No match for ID {0} was found.".format( opts.issue_id) match_results.noMatches.append(filename) return if settings.apply_cbl_transform_on_cv_import: cv_md = CBLTransformer(cv_md, settings).apply() else: ii = IssueIdentifier(ca, settings) if md is None or md.isEmpty: print "No metadata given to search online with!" match_results.noMatches.append(filename) return def myoutput(text): if opts.verbose: IssueIdentifier.defaultWriteOutput(text) # 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() result = ii.search_result found_match = False choices = False low_confidence = False if result == ii.ResultNoMatches: pass elif result == ii.ResultFoundMatchButBadCoverScore: low_confidence = True found_match = True elif result == ii.ResultFoundMatchButNotFirstPage: found_match = True elif result == ii.ResultMultipleMatchesWithBadImageScores: low_confidence = True choices = True elif result == ii.ResultOneGoodMatch: found_match = True elif result == ii.ResultMultipleGoodMatches: choices = True if choices: if low_confidence: print "Online search: Multiple low confidence matches. Save aborted" match_results.lowConfidenceMatches.append( MultipleMatch(filename, matches)) return else: print "Online search: Multiple good matches. Save aborted" match_results.multipleMatches.append( MultipleMatch(filename, matches)) return if low_confidence and opts.abortOnLowConfidence: print "Online search: Low confidence match. Save aborted" match_results.lowConfidenceMatches.append( MultipleMatch(filename, matches)) return if not found_match: print "Online search: No match found. Save aborted" match_results.noMatches.append(filename) return # we got here, so we have a single match # now get the particular issue data cv_md = actual_issue_data_fetch(matches[0], settings, opts) if cv_md is None: match_results.fetchDataFailures.append(filename) return md.overlay(cv_md) # ok, done building our metadata. time to save if not actual_metadata_save(ca, opts, md): match_results.writeFailures.append(filename) else: match_results.goodMatches.append(filename) elif opts.rename_file: msg_hdr = "" if batch_mode: msg_hdr = u"{0}: ".format(filename) if opts.data_style is not None: use_tags = has[opts.data_style] else: use_tags = False md = create_local_metadata(opts, ca, use_tags) if md.series is None: print msg_hdr + "Can't rename without series name" return new_ext = None # default if settings.rename_extension_based_on_archive: if ca.isZip(): new_ext = ".cbz" elif ca.isRar(): new_ext = ".cbr" renamer = FileRenamer(md) renamer.setTemplate(settings.rename_template) renamer.setIssueZeroPadding(settings.rename_issue_number_padding) renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup) new_name = renamer.determineName(filename, ext=new_ext) if new_name == os.path.basename(filename): print msg_hdr + "Filename is already good!" return folder = os.path.dirname(os.path.abspath(filename)) new_abs_path = utils.unique_file(os.path.join(folder, new_name)) suffix = "" if not opts.dryrun: # rename the file os.rename(filename, new_abs_path) else: suffix = " (dry-run, no change)" print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix) elif opts.export_to_zip: msg_hdr = "" if batch_mode: msg_hdr = u"{0}: ".format(filename) if not ca.isRar(): print msg_hdr + "Archive is not a RAR." return rar_file = os.path.abspath(os.path.abspath(filename)) new_file = os.path.splitext(rar_file)[0] + ".cbz" if opts.abort_export_on_conflict and os.path.lexists(new_file): print msg_hdr + "{0} already exists in the that folder.".format( os.path.split(new_file)[1]) return new_file = utils.unique_file(os.path.join(new_file)) delete_success = False export_success = False if not opts.dryrun: if ca.exportAsZip(new_file): export_success = True if opts.delete_rar_after_export: try: os.unlink(rar_file) except: print msg_hdr + "Error deleting original RAR after export" delete_success = False else: delete_success = True else: # last export failed, so remove the zip, if it exists if os.path.lexists(new_file): os.remove(new_file) else: msg = msg_hdr + u"Dry-run: Would try to create {0}".format( os.path.split(new_file)[1]) if opts.delete_rar_after_export: msg += u" and delete orginal." print msg return msg = msg_hdr if export_success: msg += u"Archive exported successfully to: {0}".format( os.path.split(new_file)[1]) if opts.delete_rar_after_export and delete_success: msg += u" (Original deleted) " else: msg += u"Archive failed to export!" print msg
def searchComplete(self): self.progdialog.accept() if self.search_thread.cv_error: 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 Comic Vine to search for series!")) return self.cv_search_results = self.search_thread.cv_search_results self.updateButtons() self.twList.setSortingEnabled(False) while self.twList.rowCount() > 0: self.twList.removeRow(0) 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.ToolTipRole, 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.setData(QtCore.Qt.ToolTipRole, 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.ToolTipRole, 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.setData(QtCore.Qt.ToolTipRole, item_text) item = QtGui.QTableWidgetItem(item_text) item.setFlags( QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.twList.setItem(row, 3, item) row += 1 self.twList.resizeColumnsToContents() self.twList.setSortingEnabled(True) self.twList.sortItems(2, QtCore.Qt.DescendingOrder) self.twList.selectRow(0) self.twList.resizeColumnsToContents() if len(self.cv_search_results) == 0: QtCore.QCoreApplication.processEvents() QtGui.QMessageBox.information( self, "Search Result", "No matches found!") if self.immediate_autoselect and len(self.cv_search_results) > 0: # defer the immediate autoselect so this dialog has time to pop up QtCore.QCoreApplication.processEvents() QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)
class CoverImageWidget(QWidget): ArchiveMode = 0 AltCoverMode = 1 URLMode = 1 DataMode = 3 def __init__(self, parent, mode, expand_on_click = True ): super(CoverImageWidget, self).__init__(parent) uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui' ), self) utils.reduceWidgetFontSize( self.label ) self.mode = mode self.comicVine = ComicVineTalker() self.page_loader = None self.showControls = True self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png'))) self.btnRight.setIcon(QIcon(ComicTaggerSettings.getGraphic('right.png'))) self.btnLeft.clicked.connect( self.decrementImage ) self.btnRight.clicked.connect( self.incrementImage ) self.resetWidget() if expand_on_click: clickable(self.lblImage).connect(self.showPopup) else: self.lblImage.setToolTip( "" ) self.updateContent() def resetWidget(self): self.comic_archive = None self.issue_id = None self.comicVine = None self.cover_fetcher = None self.url_list = [] if self.page_loader is not None: self.page_loader.abandoned = True self.page_loader = None self.imageIndex = -1 self.imageCount = 1 self.imageData = None def clear( self ): self.resetWidget() self.updateContent() def incrementImage( self ): self.imageIndex += 1 if self.imageIndex == self.imageCount: self.imageIndex = 0 self.updateContent() def decrementImage( self ): self.imageIndex -= 1 if self.imageIndex == -1: self.imageIndex = self.imageCount -1 self.updateContent() def setArchive( self, ca, page=0 ): if self.mode == CoverImageWidget.ArchiveMode: self.resetWidget() self.comic_archive = ca self.imageIndex = page self.imageCount = ca.getNumberOfPages() self.updateContent() def setURL( self, url ): if self.mode == CoverImageWidget.URLMode: self.resetWidget() self.updateContent() self.url_list = [ url ] self.imageIndex = 0 self.imageCount = 1 self.updateContent() def setIssueID( self, issue_id ): if self.mode == CoverImageWidget.AltCoverMode: self.resetWidget() self.updateContent() self.issue_id = issue_id self.comicVine = ComicVineTalker() self.comicVine.urlFetchComplete.connect( self.primaryUrlFetchComplete ) self.comicVine.asyncFetchIssueCoverURLs( int(self.issue_id) ) def setImageData( self, image_data ): if self.mode == CoverImageWidget.DataMode: self.resetWidget() if image_data is None: self.imageIndex = -1 else: self.imageIndex = 0 self.imageData = image_data self.updateContent() def primaryUrlFetchComplete( self, primary_url, thumb_url, issue_id ): self.url_list.append(str(primary_url)) self.imageIndex = 0 self.imageCount = len(self.url_list) self.updateContent() #defer the alt cover search QTimer.singleShot(1, self.startAltCoverSearch) def startAltCoverSearch( self ): # now we need to get the list of alt cover URLs self.label.setText("Searching for alt. covers...") # page URL should already be cached, so no need to defer self.comicVine = ComicVineTalker() issue_page_url = self.comicVine.fetchIssuePageURL( self.issue_id ) self.comicVine.altUrlListFetchComplete.connect( self.altCoverUrlListFetchComplete ) self.comicVine.asyncFetchAlternateCoverURLs( int(self.issue_id), issue_page_url) def altCoverUrlListFetchComplete( self, url_list, issue_id ): if len(url_list) > 0: self.url_list.extend(url_list) self.imageCount = len(self.url_list) self.updateControls() def setPage( self, pagenum ): if self.mode == CoverImageWidget.ArchiveMode: self.imageIndex = pagenum self.updateContent() def updateContent( self ): self.updateImage() self.updateControls() def updateImage( self ): if self.imageIndex == -1: self.loadDefault() elif self.mode in [ CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode ]: self.loadURL() elif self.mode == CoverImageWidget.DataMode: self.coverRemoteFetchComplete( self.imageData, 0 ) else: self.loadPage() def updateControls( self ): if not self.showControls or self.mode == CoverImageWidget.DataMode: self.btnLeft.hide() self.btnRight.hide() self.label.hide() return if self.imageIndex == -1 or self.imageCount == 1: self.btnLeft.setEnabled(False) self.btnRight.setEnabled(False) self.btnLeft.hide() self.btnRight.hide() else: self.btnLeft.setEnabled(True) self.btnRight.setEnabled(True) self.btnLeft.show() self.btnRight.show() if self.imageIndex == -1 or self.imageCount == 1: self.label.setText("") elif self.mode == CoverImageWidget.AltCoverMode: self.label.setText("Cover {0} ( of {1} )".format(self.imageIndex+1, self.imageCount)) else: self.label.setText("Page {0} ( of {1} )".format(self.imageIndex+1, self.imageCount)) def loadURL( self ): self.loadDefault() self.cover_fetcher = ImageFetcher( ) self.cover_fetcher.fetchComplete.connect(self.coverRemoteFetchComplete) self.cover_fetcher.fetch( self.url_list[self.imageIndex] ) #print "ATB cover fetch started...." # called when the image is done loading from internet def coverRemoteFetchComplete( self, image_data, issue_id ): img = QImage() img.loadFromData( image_data ) self.current_pixmap = QPixmap(img) self.setDisplayPixmap( 0, 0) #print "ATB cover fetch complete!" def loadPage( self ): if self.comic_archive is not None: if self.page_loader is not None: self.page_loader.abandoned = True self.page_loader = PageLoader( self.comic_archive, self.imageIndex ) self.page_loader.loadComplete.connect( self.pageLoadComplete ) self.page_loader.start() def pageLoadComplete( self, img ): self.current_pixmap = QPixmap(img) self.setDisplayPixmap( 0, 0) self.page_loader = None def loadDefault( self ): self.current_pixmap = QPixmap(ComicTaggerSettings.getGraphic('nocover.png')) #print "loadDefault called" self.setDisplayPixmap( 0, 0) def resizeEvent( self, resize_event ): if self.current_pixmap is not None: delta_w = resize_event.size().width() - resize_event.oldSize().width() delta_h = resize_event.size().height() - resize_event.oldSize().height() #print "ATB resizeEvent deltas", resize_event.size().width(), resize_event.size().height() self.setDisplayPixmap( delta_w , delta_h ) def setDisplayPixmap( self, delta_w , delta_h ): # the deltas let us know what the new width and height of the label will be """ new_h = self.frame.height() + delta_h new_w = self.frame.width() + delta_w print "ATB setDisplayPixmap deltas", delta_w , delta_h print "ATB self.frame", self.frame.width(), self.frame.height() print "ATB self.", self.width(), self.height() frame_w = new_w frame_h = new_h """ new_h = self.frame.height() new_w = self.frame.width() frame_w = self.frame.width() frame_h = self.frame.height() new_h -= 4 new_w -= 4 if new_h < 0: new_h = 0; if new_w < 0: new_w = 0; #print "ATB setDisplayPixmap deltas", delta_w , delta_h #print "ATB self.frame", frame_w, frame_h #print "ATB new size", new_w, new_h # scale the pixmap to fit in the frame scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio) self.lblImage.setPixmap( scaled_pixmap ) # move and resize the label to be centered in the fame img_w = scaled_pixmap.width() img_h = scaled_pixmap.height() self.lblImage.resize( img_w, img_h ) self.lblImage.move( (frame_w - img_w)/2, (frame_h - img_h)/2 ) def showPopup( self ): self.popup = ImagePopup(self, self.current_pixmap)
def performQuery(self): QtGui.QApplication.setOverrideCursor( QtGui.QCursor(QtCore.Qt.WaitCursor)) try: comicVine = ComicVineTalker() volume_data = comicVine.fetchVolumeData(self.series_id) self.issue_list = comicVine.fetchIssuesByVolume(self.series_id) 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: self.twList.removeRow(0) self.twList.setSortingEnabled(False) row = 0 for record in self.issue_list: self.twList.insertRow(row) item_text = record['issue_number'] item = IssueNumberTableWidgetItem(item_text) item.setData(QtCore.Qt.ToolTipRole, item_text) item.setData(QtCore.Qt.UserRole, record['id']) item.setData(QtCore.Qt.DisplayRole, item_text) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.twList.setItem(row, 0, item) item_text = record['cover_date'] if item_text is None: item_text = "" #remove the day of "YYYY-MM-DD" parts = item_text.split("-") if len(parts) > 1: item_text = parts[0] + "-" + parts[1] item = QtGui.QTableWidgetItem(item_text) item.setData(QtCore.Qt.ToolTipRole, item_text) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.twList.setItem(row, 1, item) item_text = record['name'] if item_text is None: item_text = "" item = QtGui.QTableWidgetItem(item_text) item.setData(QtCore.Qt.ToolTipRole, item_text) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.twList.setItem(row, 2, item) if IssueString( record['issue_number']).asString().lower() == IssueString( self.issue_number).asString().lower(): self.initial_id = record['id'] row += 1 self.twList.setSortingEnabled(True) self.twList.sortItems(0, QtCore.Qt.AscendingOrder) QtGui.QApplication.restoreOverrideCursor()
def searchComplete(self): self.progdialog.accept() if self.search_thread.cv_error: 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 Comic Vine to search for series!" )) return self.cv_search_results = self.search_thread.cv_search_results self.updateButtons() self.twList.setSortingEnabled(False) while self.twList.rowCount() > 0: self.twList.removeRow(0) 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.ToolTipRole, 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.setData(QtCore.Qt.ToolTipRole, 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.ToolTipRole, 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.setData(QtCore.Qt.ToolTipRole, item_text) item = QtGui.QTableWidgetItem(item_text) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.twList.setItem(row, 3, item) row += 1 self.twList.resizeColumnsToContents() self.twList.setSortingEnabled(True) self.twList.sortItems(2, QtCore.Qt.DescendingOrder) self.twList.selectRow(0) self.twList.resizeColumnsToContents() if len(self.cv_search_results) == 0: QtCore.QCoreApplication.processEvents() QtGui.QMessageBox.information(self, "Search Result", "No matches found!") if self.immediate_autoselect and len(self.cv_search_results) > 0: # defer the immediate autoselect so this dialog has time to pop up QtCore.QCoreApplication.processEvents() QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)