diff --git a/comicarchive.py b/comicarchive.py index 9752b5e..80281a7 100644 --- a/comicarchive.py +++ b/comicarchive.py @@ -453,36 +453,45 @@ class ComicArchive: def getPage( self, index ): - num_pages = self.getNumberOfPages() + image_data = None + + filename = self.getPageName( index ) + + if filename is not None: + image_data = self.archiver.readArchiveFile( filename ) + + return image_data + + def getPageName( self, index ): + + page_list = self.getPageNameList() + + num_pages = len( page_list ) if num_pages == 0 or index >= num_pages: return None - + + return page_list[index] + + def getPageNameList( self , sort_list=True): + # get the list file names in the archive, and sort files = self.archiver.getArchiveFilenameList() # seems like some archive creators are on Windows, and don't know about case-sensitivity! - files.sort(key=lambda x: x.lower()) + if sort_list: + files.sort(key=lambda x: x.lower()) # make a sub-list of image files page_list = [] for name in files: if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ): page_list.append(name) + + return page_list - image_data = self.archiver.readArchiveFile( page_list[index] ) - - return image_data - - - def getNumberOfPages(self): - - count = 0 + def getNumberOfPages( self ): - for item in self.archiver.getArchiveFilenameList(): - if ( item[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ): - count += 1 - - return count + return len( self.getPageNameList( sort_list=False ) ) def readCBI( self ): diff --git a/comicinfoxml.py b/comicinfoxml.py index efa56b9..e421c72 100644 --- a/comicinfoxml.py +++ b/comicinfoxml.py @@ -26,7 +26,27 @@ from genericmetadata import GenericMetadata import utils class ComicInfoXml: + + writer_synonyms = ['writer', 'plotter', 'scripter'] + penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ] + inker_synonyms = [ 'inker', 'artist', 'finishes' ] + colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ] + letterer_synonyms = [ 'letterer'] + cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ] + editor_synonyms = [ 'editor'] + + def getParseableCredits( self ): + parsable_credits = [] + parsable_credits.extend( self.writer_synonyms ) + parsable_credits.extend( self.penciller_synonyms ) + parsable_credits.extend( self.inker_synonyms ) + parsable_credits.extend( self.colorist_synonyms ) + parsable_credits.extend( self.letterer_synonyms ) + parsable_credits.extend( self.cover_synonyms ) + parsable_credits.extend( self.editor_synonyms ) + return parsable_credits + def metadataFromString( self, string ): tree = ET.ElementTree(ET.fromstring( string )) @@ -110,25 +130,25 @@ class ComicInfoXml: # first, loop thru credits, and build a list for each role that CIX supports for credit in metadata.credits: - if credit['role'].lower() in set( ['writer', 'plotter', 'scripter'] ): + if credit['role'].lower() in set( self.writer_synonyms ): credit_writer_list.append(credit['person']) - if credit['role'].lower() in set( [ 'artist', 'penciller', 'penciler', 'breakdowns' ] ): + if credit['role'].lower() in set( self.penciller_synonyms ): credit_penciller_list.append(credit['person']) - if credit['role'].lower() in set( [ 'inker', 'artist', 'finishes' ] ): + if credit['role'].lower() in set( self.inker_synonyms ): credit_inker_list.append(credit['person']) - if credit['role'].lower() in set( [ 'colorist', 'colourist', 'colorer', 'colourer' ]): + if credit['role'].lower() in set( self.colorist_synonyms ): credit_colorist_list.append(credit['person']) - if credit['role'].lower() in set( [ 'letterer'] ): + if credit['role'].lower() in set( self.letterer_synonyms ): credit_letterer_list.append(credit['person']) - if credit['role'].lower() in set( [ 'cover', 'covers', 'coverartist', 'cover artist' ] ): + if credit['role'].lower() in set( self.cover_synonyms ): credit_cover_list.append(credit['person']) - if credit['role'].lower() in set( [ 'editor'] ): + if credit['role'].lower() in set( self.editor_synonyms ): credit_editor_list.append(credit['person']) # second, convert each list to string, and add to XML struct diff --git a/comicvinetalker.py b/comicvinetalker.py index 527107a..1a790bb 100644 --- a/comicvinetalker.py +++ b/comicvinetalker.py @@ -39,7 +39,8 @@ class ComicVineTalker(QObject): def __init__(self, api_key): QObject.__init__(self) - self.api_key = api_key + # key that is registered to comictagger + self.api_key = '27431e6787042105bd3e47e169a624521f89f3a4' def testKey( self ): diff --git a/issueidentifier.py b/issueidentifier.py index bdf71dd..eb73582 100644 --- a/issueidentifier.py +++ b/issueidentifier.py @@ -32,6 +32,13 @@ from imagefetcher import ImageFetcher import utils class IssueIdentifier: + + ResultNoMatches = 0 + ResultFoundMatchButBadCoverScore = 1 + ResultFoundMatchButNotFirstPage = 2 + ResultMultipleMatchesWithBadImageScores = 3 + ResultOneGoodMatch = 4 + ResultMultipleGoodMatches = 5 def __init__(self, comic_archive, cv_api_key ): self.comic_archive = comic_archive @@ -39,6 +46,7 @@ class IssueIdentifier: self.additional_metadata = None self.min_score_thresh = 22 self.min_score_distance = 2 + self.strong_score_thresh = 8 self.additional_metadata = GenericMetadata() self.cv_api_key = cv_api_key self.output_function = IssueIdentifier.defaultWriteOutput @@ -263,7 +271,7 @@ class IssueIdentifier: if len(self.match_list) == 0: self.log_msg( ":-( no matches!" ) return self.match_list - + # sort list by image match scores self.match_list.sort(key=lambda k: k['distance']) @@ -286,13 +294,33 @@ class IssueIdentifier: if len(self.match_list) == 1: if best_score > self.min_score_thresh: self.log_msg( "!!!! Very weak score for the cover. Maybe it's not the cover?" ) + + + self.log_msg( "Comparing other pages now..." ) + found = False + for i in range(ca.getNumberOfPages()): + image_data = ca.getPage(i) + page_hash = self.calculateHash( image_data ) + distance = ImageHasher.hamming_distance(page_hash, self.match_list[0]['url_image_hash']) + if distance <= self.strong_score_thresh: + print "Found a great match d={0} on page {1}!".format(distance, i+1) + found = True + break + elif distance < self.min_score_thresh: + print "Found a good match d={0} on page {1}".format(distance, i) + found = True + self.log_msg( ".", newline=False ) + self.log_msg( "" ) + if not found: + self.log_msg( "No matching pages in the issue. Bummer" ) + print_match(self.match_list[0]) return self.match_list elif best_score > self.min_score_thresh and len(self.match_list) > 1: self.log_msg( "No good image matches! Need to use other info..." ) return self.match_list - + #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: @@ -307,7 +335,7 @@ class IssueIdentifier: self.log_msg( "More than one likley candiate. Maybe a lexical comparison??" ) for item in self.match_list: print_match(item) - + return self.match_list \ No newline at end of file diff --git a/pagebrowser.py b/pagebrowser.py index c7f53c3..67b8411 100644 --- a/pagebrowser.py +++ b/pagebrowser.py @@ -109,6 +109,6 @@ class PageBrowserWindow(QtGui.QDialog): new_w = 0; scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, QtCore.Qt.KeepAspectRatio) self.lblPage.setPixmap( scaled_pixmap ) - - + + \ No newline at end of file diff --git a/tagger.py b/tagger.py index 41d6e3d..d116bfc 100755 --- a/tagger.py +++ b/tagger.py @@ -89,7 +89,20 @@ def main(): splash.show() splash.raise_() app.processEvents() - app.processEvents() + + """ + lw = QtGui.QListWidget() + icon = QtGui.QIcon('app.png') + for i in range(10): + lw.addItem( QtGui.QListWidgetItem( icon, "Item {0}".format(i) ) ) + + lw.setDragDropMode(QtGui.QAbstractItemView.InternalMove) + #lw.setViewMode(QtGui.QListView.IconMode) + lw.setMovement(QtGui.QListView.Snap) + lw.setGridSize(QtCore.QSize(100,100)) + lw.show() + sys.exit(app.exec_()) + """ try: tagger_window = TaggerWindow( opts, settings ) diff --git a/taggerwindow.py b/taggerwindow.py index 5efed9c..ea11404 100644 --- a/taggerwindow.py +++ b/taggerwindow.py @@ -28,6 +28,7 @@ import os from volumeselectionwindow import VolumeSelectionWindow from options import Options, MetaDataStyle +from comicinfoxml import ComicInfoXml from genericmetadata import GenericMetadata from comicvinetalker import ComicVineTalker from comicarchive import ComicArchive @@ -63,6 +64,163 @@ def clickable(widget): widget.installEventFilter(filter) return filter.dblclicked +""" +class PageTableModel(QtCore.QAbstractTableModel): + + def __init__(self, comic_archive, parent=None, *args): + QtCore.QAbstractTableModel.__init__(self, parent, *args) + + self.comic_archive = comic_archive + page_list = comic_archive.getPageNameList() + + self.page_model = [] + i = 0 + for page in page_list: + item = dict() + item['number'] = i + item['filename'] = page + item['thumb'] = None + + self.page_model.append( item ) + i +=1 + + + def rowCount(self, parent): + return len(self.page_model) + + def columnCount(self, parent): + return 3 + + def data(self, index, role): + + if not index.isValid(): + return QtCore.QVariant() + + elif role == QtCore.Qt.DisplayRole: + # page num + if index.column() == 0: + return QtCore.QVariant(self.page_model[index.row()]['number']) + + # page filename + if index.column() == 1: + return QtCore.QVariant(self.page_model[index.row()]['filename']) + + elif role == QtCore.Qt.DecorationRole: + + if index.column() == 2: + if self.page_model[index.row()]['thumb'] is None: + + image_data = self.comic_archive.getPage( self.page_model[index.row()]['number'] ) + img = QtGui.QImage() + img.loadFromData( image_data ) + pixmap = QtGui.QPixmap(QtGui.QPixmap(img)) + #scaled_pixmap = pixmap.scaled(100, 150, QtCore.Qt.KeepAspectRatio) + + self.page_model[index.row()]['thumb'] = pixmap #scaled_pixmap + + return QtCore.QVariant(self.page_model[index.row()]['thumb']) + + else: + return QtCore.QVariant() +""" +class PageListModel(QtCore.QAbstractListModel): + + def __init__(self, comic_archive, parent=None, *args): + QtCore.QAbstractTableModel.__init__(self, parent, *args) + + self.comic_archive = comic_archive + page_list = comic_archive.getPageNameList() + + self.page_model = [] + i = 0 + for page in page_list: + item = dict() + item['number'] = i + item['filename'] = page + item['thumb'] = None + + self.page_model.append( item ) + i +=1 + + def rowCount(self, parent): + return len(self.page_model) + + def data(self, index, role): + + if not index.isValid(): + return QtCore.QVariant() + + elif role == QtCore.Qt.DisplayRole: + # page num + return QtCore.QVariant(self.page_model[index.row()]['number']) + + elif role == QtCore.Qt.DecorationRole: + + if self.page_model[index.row()]['thumb'] is None: + + #timestamp = datetime.datetime.now() + + image_data = self.comic_archive.getPage( self.page_model[index.row()]['number'] ) + img = QtGui.QImage() + img.loadFromData( image_data ) + pixmap = QtGui.QPixmap(QtGui.QPixmap(img)) + scaled_pixmap = pixmap.scaled(100, 150, QtCore.Qt.KeepAspectRatio) + + self.page_model[index.row()]['thumb'] = scaled_pixmap + + return QtCore.QVariant(self.page_model[index.row()]['thumb']) + + else: + return QtCore.QVariant() + + def flags( self, index): + defaultFlags = QtCore.QAbstractTableModel.flags(self, index) + if index.isValid(): + return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | defaultFlags + else: + return QtCore.Qt.ItemIsDropEnabled | defaultFlags + + def removeRows(self, row, count, parent=QtCore.QModelIndex()): + + print "removeRows", row, count + return True + + def insertRows(self, row, count, parent=QtCore.QModelIndex()): + + print "insertRows", row, count + return False + + def beginRemoveRows(self, sourceParent, start, end, destinationParent, dest): + print "beginRemoveRows" + + def dropMimeData(self,data, action, row, col, parent): + print "dropMimeData", action, row, col + + + if (row != -1): + beginRow = row + + elif (parent.isValid()): + beginRow = parent.row() + + print beginRow + + return True + if (action == QtCore.Qt.IgnoreAction): + return True + + #if ( not data.hasFormat("application/vnd.text.list")) + # return False + + if (column > 0): + return False + #def beginMoveRows(self, sourceParent, start, end, destinationParent, dest): + # print "rowsMoved" + + def supportedDropActions(self): + #print "supportedDropActions" + return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction + class TaggerWindow( QtGui.QMainWindow): @@ -312,7 +470,8 @@ class TaggerWindow( QtGui.QMainWindow): self.metadataToForm() self.clearDirtyFlag() # also updates the app title - self.updateInfoBox() + self.updateInfoBox() + #self.updatePagesInfo() else: QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("That file doesn't appear to be a comic archive!")) @@ -349,7 +508,39 @@ class TaggerWindow( QtGui.QMainWindow): self.lblTagList.setText( tag_info ) + def updatePagesInfo( self ): + + #tablemodel = PageTableModel( self.comic_archive, self ) + #self.tableView.setModel(tablemodel) + listmodel = PageListModel( self.comic_archive, self ) + self.listView.setModel(listmodel) + + #self.listView.setDragDropMode(self.InternalMove) + #listmodel.installEventFilter(self) + + self.listView.setDragEnabled(True) + self.listView.setAcceptDrops(True) + self.listView.setDropIndicatorShown(True) + + #listmodel.rowsMoved.connect( self.rowsMoved ) + #listmodel.rowsRemoved.connect( self.rowsRemoved ) + #listmodel.beginMoveRows.connect( self.beginMoveRows ) + + #def rowsMoved( self, b, c, d): + # print "rowsMoved" + #def rowsRemoved( self,b, c, d): + # print "rowsRemoved" + + + """ + def eventFilter(self, sender, event): + if (event.type() == QtCore.QEvent.ChildRemoved): + print "QEvent::ChildRemoved" + return False # don't actually interrupt anything + """ + + def setDirtyFlag( self, param1=None, param2=None, param3=None ): if not self.dirtyFlag: self.dirtyFlag = True @@ -495,6 +686,7 @@ class TaggerWindow( QtGui.QMainWindow): row += 1 self.twCredits.setSortingEnabled( True ) + self.updateCreditColors() def addNewCreditEntry( self, row, role, name ): self.twCredits.insertRow(row) @@ -520,7 +712,6 @@ class TaggerWindow( QtGui.QMainWindow): return False - def formToMetadata( self ): #helper func @@ -681,6 +872,32 @@ class TaggerWindow( QtGui.QMainWindow): def setDataStyle(self, s): self.data_style, b = self.cbDataStyle.itemData(s).toInt() self.updateStyleTweaks() + + def updateCreditColors( self ): + inactive_color = QtGui.QColor(255, 170, 150) + active_palette = self.leSeries.palette() + active_color = active_palette.color( QtGui.QPalette.Base ) + + cix_credits = ComicInfoXml().getParseableCredits() + + if self.data_style == MetaDataStyle.CIX: + #loop over credit table, mark selected rows + r = 0 + while r < self.twCredits.rowCount(): + if str(self.twCredits.item(r, 0).text()).lower() not in cix_credits: + print "Bad credit for CIX:", self.twCredits.item(r, 0).text() + self.twCredits.item(r, 0).setBackgroundColor( inactive_color ) + else: + self.twCredits.item(r, 0).setBackgroundColor( active_color ) + r = r + 1 + + if self.data_style == MetaDataStyle.CBI: + #loop over credit table, make all active color + r = 0 + while r < self.twCredits.rowCount(): + self.twCredits.item(r, 0).setBackgroundColor( active_color ) + r = r + 1 + def updateStyleTweaks( self ): @@ -707,6 +924,7 @@ class TaggerWindow( QtGui.QMainWindow): if enable: item.setPalette(active_palette) + item.setAutoFillBackground( False ) if type(item) == QtGui.QCheckBox: item.setEnabled( True ) elif type(item) == QtGui.QComboBox: @@ -714,6 +932,7 @@ class TaggerWindow( QtGui.QMainWindow): else: item.setReadOnly( False ) else: + item.setAutoFillBackground( True ) if type(item) == QtGui.QCheckBox: item.setPalette(inactive_palette2) item.setEnabled( False ) @@ -733,7 +952,7 @@ class TaggerWindow( QtGui.QMainWindow): self.leWebLink, self.teCharacters, self.teTeams, self.teLocations, self.cbMaturityRating, self.cbFormat ] - + if self.data_style == MetaDataStyle.CIX: for item in cix_only: enableWidget( item, True ) @@ -746,6 +965,8 @@ class TaggerWindow( QtGui.QMainWindow): for item in cix_only: enableWidget(item, False ) + self.updateCreditColors() + def cellDoubleClicked( self, r, c ): self.editCredit() @@ -804,7 +1025,8 @@ class TaggerWindow( QtGui.QMainWindow): # add new entry row = self.twCredits.rowCount() self.addNewCreditEntry( row, new_role, new_name) - + + self.updateCreditColors() self.setDirtyFlag() def removeCredit( self ): diff --git a/taggerwindow.ui b/taggerwindow.ui index 738e31e..6b6bc7c 100644 --- a/taggerwindow.ui +++ b/taggerwindow.ui @@ -96,7 +96,7 @@ - filename + true @@ -131,7 +131,7 @@ QFrame::Sunken - archive type + false @@ -152,7 +152,7 @@ - page count + @@ -167,7 +167,7 @@ - tag list + @@ -386,12 +386,12 @@ - # Issues + Volume - + @@ -403,12 +403,12 @@ - Volume + # Issues - + @@ -458,6 +458,9 @@ + + QFormLayout::AllNonFixedFieldsGrow + @@ -497,7 +500,7 @@ - true + false true @@ -557,7 +560,7 @@ - true + false Black && White @@ -574,7 +577,7 @@ - true + false @@ -962,6 +965,39 @@ Pages + + + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + QListView::Snap + + + QListView::Adjust + + + + 100 + 170 + + + + QListView::IconMode + + + true + + + + @@ -977,7 +1013,7 @@ 0 0 975 - 28 + 25 diff --git a/todo.txt b/todo.txt index df019fc..441ca69 100644 --- a/todo.txt +++ b/todo.txt @@ -3,17 +3,12 @@ Features ---------------- -Toolbar icons - -Infobox needs fixing up - -Page Browser, mode-less dialog Auto-select: msgbox on autoselect failure, or warning Multi-match dialog More auto-select logic using metadata - Page search on only match, but bad image match, to find cover?? + Maybe, if only one match, but bad score, compare each page in the archive to online cover Check aspect ratio, and maybe break cover into two parts for hashing? Stand-alone CLI @@ -24,16 +19,16 @@ Stand-alone CLI TaggerWindow entry fields - General layout Special tabbed Dialogs needed for: - Pages Info - Color changing stuff need more work - - Indicate credits for CR style - - CR has editable dropdowns/comboboxes for Format, Publisher, Imprint + Pages Info - maybe a custom painted widget + + +Verify/warn on save (maybe just on over-write?) Style sheets for windows/mac/linux +Add class for warning/info messages with "Don't show again" checkbox. + Add list of these flag to settings Version 2 - GUI to handle mutliple files or folders ----------- @@ -42,9 +37,19 @@ Version 2 - GUI to handle mutliple files or folders ----------------- Bugs ---------------- + +Disable CBL for RAR + SERIOUS BUG: rebuilding zips! http://stackoverflow.com/questions/11578443/trigger-io-errno-18-cross-device-link +MAC: + toolbar + weird unrar complaints + Page browser sizing + +Disable save when read-only + Be more tolerant of mis-labled extensions i.e. cbr when it's a cbz Form type validation Ints vs strings for month, year. etc @@ -59,10 +64,8 @@ Other settings possibilities: Clear caches -Content Hashes!! - Image Hashes: - Failures of average hash: + Failures of image hash: Thor 600 Wrap-around w/ different aspect ratio Bone 3 - Variant Cover, Avengers #1, #13, #81 @@ -90,9 +93,13 @@ Auto search: Lexical analysis Searching w/o issue #? +Determine alternate covers from CV somehow + ------------- Other ------------ +Content Hashes!! + Archive function to detect tag blocks out of sync @@ -104,7 +111,6 @@ Longer term: Think about mass tagging and (semi) automatic volume selection - ---------------------------------------------- COMIC RACK Questions @@ -119,9 +125,3 @@ Some that seem library only: Proposed Values Community Rating -Mac Notes: - -python ~/pyinstaller-2.0/pyinstaller.py tagger.py -w -i windows/nsis/app.ico - - -