diff --git a/comicarchive.py b/comicarchive.py
index 80281a7..445b5c2 100644
--- a/comicarchive.py
+++ b/comicarchive.py
@@ -397,16 +397,11 @@ class ComicArchive:
def seemsToBeAComicArchive( self ):
- ext = os.path.splitext(self.path)[1].lower()
- if (
- ( ( ( self.isZip() ) and
- ( ext.lower() in [ '.zip', '.cbz' ] ))
- or
- (( self.isRar() ) and
- ( ext.lower() in [ '.rar', '.cbr' ] ))
- or
- ( self.isFolder() ) )
+ # Do we even care about extensions??
+ ext = os.path.splitext(self.path)[1].lower()
+ if (
+ ( self.isZip() or self.isRar() or self.isFolder() )
and
( self.getNumberOfPages() > 3)
diff --git a/comicvinecacher.py b/comicvinecacher.py
index 1284e2d..daa0401 100644
--- a/comicvinecacher.py
+++ b/comicvinecacher.py
@@ -55,7 +55,7 @@ class ComicVineCacher:
"count_of_issues INT," +
"image_url TEXT," +
"description TEXT," +
- "timestamp TEXT)"
+ "timestamp DATE DEFAULT (datetime('now','localtime')) ) "
)
cur.execute("CREATE TABLE Volumes(" +
@@ -63,7 +63,7 @@ class ComicVineCacher:
"name TEXT," +
"publisher TEXT," +
"count_of_issues INT," +
- "timestamp TEXT," +
+ "timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (id) )"
)
@@ -78,7 +78,7 @@ class ComicVineCacher:
"thumb_image_hash TEXT," +
"publish_month TEXT," +
"publish_year TEXT," +
- "timestamp TEXT," +
+ "timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (id ) )"
)
@@ -107,7 +107,9 @@ class ComicVineCacher:
else:
url = record['image']['super_url']
- cur.execute("INSERT INTO VolumeSearchCache VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ? )" ,
+ cur.execute("INSERT INTO VolumeSearchCache " +
+ "(search_term, id, name, start_year, publisher, count_of_issues, image_url, description ) " +
+ "VALUES( ?, ?, ?, ?, ?, ?, ?, ? )" ,
( search_term.lower(),
record['id'],
record['name'],
@@ -115,8 +117,7 @@ class ComicVineCacher:
pub_name,
record['count_of_issues'],
url,
- record['description'],
- timestamp )
+ record['description'])
)
def get_search_results( self, search_term ):
@@ -126,7 +127,10 @@ class ComicVineCacher:
with con:
cur = con.cursor()
- # TODO purge stale search results ( older than a day, maybe??)
+
+ # purge stale search results
+ a_day_ago = datetime.datetime.today()-datetime.timedelta(days=1)
+ cur.execute( "DELETE FROM VolumeSearchCache WHERE timestamp < ?", [ str(a_day_ago) ] )
# fetch
cur.execute("SELECT * FROM VolumeSearchCache WHERE search_term=?", [ search_term.lower() ] )
@@ -194,7 +198,13 @@ class ComicVineCacher:
with con:
cur = con.cursor()
- # TODO purge stale volume records ( older than a week, maybe??)
+ # purge stale volume info
+ a_week_ago = datetime.datetime.today()-datetime.timedelta(days=7)
+ cur.execute( "DELETE FROM Volumes WHERE timestamp < ?", [ str(a_week_ago) ] )
+
+ # purge stale issue info - probably issue data won't change much....
+ a_month_ago = datetime.datetime.today()-datetime.timedelta(days=30)
+ cur.execute( "DELETE FROM Issues WHERE timestamp < ?", [ str(a_month_ago) ] )
# fetch
cur.execute("SELECT id,name,publisher,count_of_issues FROM Volumes WHERE id = ?", [ volume_id ] )
diff --git a/filenameparser.py b/filenameparser.py
index 355ade1..3f4d261 100644
--- a/filenameparser.py
+++ b/filenameparser.py
@@ -83,12 +83,21 @@ class FileNameParser:
# use the issue number string to split the filename string
# assume first element of list is the series name, plus cruft
-
#!!! this could fail in the case of small numerics in the series name!!!
+
+ # TODO: we really should pass in the *INDEX* of the issue, that makes
+ # finding it easier
+
tmpstr = self.fixSpaces(filename)
- if issue != "":
- series = tmpstr.split(issue)[0]
+
+ #remove pound signs. this might mess up the series name if there is a# in it.
+ tmpstr = tmpstr.replace("#", " ")
+
+ if issue != "":
+ # assume that issue substr has at least on space before it
+ issue_str = " " + str(issue)
+ series = tmpstr.split(issue_str)[0]
else:
# no issue to work off of
#!!! TODO we should look for the year, and split from that
diff --git a/imagefetcher.py b/imagefetcher.py
index ca57b90..dd5ac36 100644
--- a/imagefetcher.py
+++ b/imagefetcher.py
@@ -103,7 +103,7 @@ class ImageFetcher(QObject):
# wipe any existing image cache folder too
if os.path.isdir( self.cache_folder ):
- shutil.rmtree(path)
+ shutil.rmtree( self.cache_folder )
os.makedirs( self.cache_folder )
con = lite.connect( self.db_file )
diff --git a/issueidentifier.py b/issueidentifier.py
index 6adf147..d01e90a 100644
--- a/issueidentifier.py
+++ b/issueidentifier.py
@@ -52,13 +52,16 @@ class IssueIdentifier:
self.min_score_thresh = 20
# the min distance a hamming score must be to separate itself from closest neighbor
- self.min_score_distance = 2
+ self.min_score_distance = 4
# a very strong hamming score, almost certainly the same image
self.strong_score_thresh = 8
# used to eliminate series names that are too long based on our search string
- self.length_delta_thresh = 3
+ self.length_delta_thresh = 5
+
+ # used to eliminate unlikely publishers
+ self.publisher_blacklist = [ 'panini comics', 'abril', 'scholastic book services' ]
self.additional_metadata = GenericMetadata()
self.cv_api_key = cv_api_key
@@ -75,6 +78,12 @@ class IssueIdentifier:
def setAdditionalMetadata( self, md ):
self.additional_metadata = md
+ def setNameLengthDeltaThreshold( self, delta ):
+ self.length_delta_thresh = md
+
+ def setPublisherBlackList( self, blacklist ):
+ self.publisher_blacklist = blacklist
+
def setHasherAlgorithm( self, algo ):
self.image_hasher = algo
pass
@@ -244,12 +253,31 @@ class IssueIdentifier:
series_shortlist = []
- #self.log_msg( "Removing results with too long names" )
+ #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 keys['year'].isdigit():
+ print "ATB", keys['year'] , item['start_year']
+ if int(keys['year']) < 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_shortlist.append(item)
# if we don't think it's an issue number 1, remove any series' that are one-shots
@@ -325,11 +353,15 @@ class IssueIdentifier:
match['issue_number'] = num_s
match['url_image_hash'] = url_image_hash
match['issue_title'] = issue['name']
- match['img_url'] = thumb_url
+ match['img_url'] = img_url
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']
+
self.match_list.append(match)
self.log_msg( " --> {0}".format(match['distance']), newline=False )
@@ -370,18 +402,18 @@ class IssueIdentifier:
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 archive pages now..." )
+ self.log_msg( "Comparing to some other archive pages now..." )
found = False
- for i in range(ca.getNumberOfPages()):
+ for i in range( min(5, 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)
+ self.log_msg( "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)
+ self.log_msg( "Found a good match d={0} on page {1}".format(distance, i) )
found = True
self.log_msg( ".", newline=False )
self.log_msg( "" )
diff --git a/matchselectionwindow.py b/matchselectionwindow.py
new file mode 100644
index 0000000..b271eb5
--- /dev/null
+++ b/matchselectionwindow.py
@@ -0,0 +1,118 @@
+"""
+A PyQT4 dialog to select from automated issue matches
+"""
+
+"""
+Copyright 2012 Anthony Beville
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+import sys
+import os
+from PyQt4 import QtCore, QtGui, uic
+
+from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
+
+from comicvinetalker import ComicVineTalker
+from imagefetcher import ImageFetcher
+from settings import ComicTaggerSettings
+
+class MatchSelectionWindow(QtGui.QDialog):
+
+ volume_id = 0
+
+ def __init__(self, parent, matches):
+ super(MatchSelectionWindow, self).__init__(parent)
+
+ uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'matchselectionwindow.ui' ), self)
+
+ self.matches = matches
+ self.populateTable( )
+ self.twList.resizeColumnsToContents()
+ self.twList.currentItemChanged.connect(self.currentItemChanged)
+ self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
+
+ self.current_row = 0
+ self.twList.selectRow( 0 )
+
+
+ def populateTable( self ):
+
+ while self.twList.rowCount() > 0:
+ self.twList.removeRow(0)
+
+ self.twList.setSortingEnabled(False)
+
+ row = 0
+ for match in self.matches:
+ self.twList.insertRow(row)
+
+ item_text = match['series']
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 0, item)
+
+ """
+ item_text = u"{0}".format(match['issue_number'])
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 1, item)
+ """
+ if match['publisher'] is not None:
+ item_text = u"{0}".format(match['publisher'])
+ else:
+ item_text = u"Unknown"
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 1, item)
+
+ item_text = ""
+ if match['month'] is not None:
+ item_text = u"{0}/".format(match['month'])
+ if match['year'] is not None:
+ item_text += u"{0}".format(match['year'])
+ else:
+ item_text += u"????"
+ item = QtGui.QTableWidgetItem(item_text)
+ item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
+ self.twList.setItem(row, 2, item)
+
+ row += 1
+
+
+ def cellDoubleClicked( self, r, c ):
+ self.accept()
+
+ def currentItemChanged( self, curr, prev ):
+
+ if curr is None:
+ return
+ if prev is not None and prev.row() == curr.row():
+ return
+
+ self.current_row = curr.row()
+
+ # list selection was changed, update the the issue cover
+ self.labelThumbnail.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
+
+ self.cover_fetcher = ImageFetcher( )
+ self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
+ self.cover_fetcher.fetch( self.matches[self.current_row]['img_url'] )
+
+ # called when the image is done loading
+ def coverFetchComplete( self, image_data, issue_id ):
+ img = QtGui.QImage()
+ img.loadFromData( image_data )
+ self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
+
diff --git a/matchselectionwindow.ui b/matchselectionwindow.ui
new file mode 100644
index 0000000..b08b1df
--- /dev/null
+++ b/matchselectionwindow.ui
@@ -0,0 +1,142 @@
+
+
+ dialogMatchSelect
+
+
+
+ 0
+ 0
+ 831
+ 506
+
+
+
+ Select Match
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 9
+
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ 0
+
+
+ 3
+
+
+ true
+
+
+ false
+
+
+
+ Series
+
+
+
+
+ Publisher
+
+
+
+
+ Date
+
+
+
+
+ -
+
+
+
+ 300
+ 0
+
+
+
+
+ 300
+ 450
+
+
+
+ QFrame::Panel
+
+
+ QFrame::Sunken
+
+
+
+
+
+ true
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ dialogMatchSelect
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ dialogMatchSelect
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/taggerwindow.py b/taggerwindow.py
index 02aa270..5baed15 100644
--- a/taggerwindow.py
+++ b/taggerwindow.py
@@ -252,6 +252,7 @@ class TaggerWindow( QtGui.QMainWindow):
self.statusBar()
self.updateAppTitle()
self.setAcceptDrops(True)
+ self.updateSaveMenu()
self.droppedFile=None
self.page_browser = None
@@ -292,7 +293,7 @@ class TaggerWindow( QtGui.QMainWindow):
mod_str = " [modified]"
if not self.comic_archive.isWritable():
- ro_str = " [read only ]"
+ ro_str = " [read only]"
self.setWindowTitle( self.appName + " - " + self.comic_archive.path + mod_str + ro_str)
@@ -484,12 +485,24 @@ class TaggerWindow( QtGui.QMainWindow):
self.metadataToForm()
self.clearDirtyFlag() # also updates the app title
- self.updateInfoBox()
+ self.updateInfoBox()
+ self.updateSaveMenu()
#self.updatePagesInfo()
else:
QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("That file doesn't appear to be a comic archive!"))
+ def updateSaveMenu( self ):
+
+ if ( self.comic_archive is not None and
+ self.comic_archive.isWritable() and
+ not ( self.data_style == MetaDataStyle.CBI and self.comic_archive.isRar() )
+ ):
+ self.actionWrite_Tags.setEnabled( True )
+ else:
+ self.actionWrite_Tags.setEnabled( False )
+
+
def updateInfoBox( self ):
ca = self.comic_archive
@@ -800,17 +813,23 @@ class TaggerWindow( QtGui.QMainWindow):
def useFilename( self ):
- self.metadata = self.comic_archive.metadataFromFilename( )
- self.metadataToForm()
-
+ if self.comic_archive is not None:
+ self.metadata = self.comic_archive.metadataFromFilename( )
+ self.metadataToForm()
def selectFile( self ):
dialog = QtGui.QFileDialog(self)
dialog.setFileMode(QtGui.QFileDialog.ExistingFile)
#dialog.setFileMode(QtGui.QFileDialog.Directory )
+
+ if platform.system() != "Windows" and utils.which("unrar") is None:
+ archive_filter = "Comic archive files (*.cbz *.zip)"
+ else:
+ archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar)"
+
filters = [
- "Comic archive files (*.cbz *.zip *.cbr *.rar)",
+ archive_filter,
"Any files (*)"
]
@@ -826,7 +845,7 @@ class TaggerWindow( QtGui.QMainWindow):
def autoSelectSearch(self):
if self.comic_archive is None:
- QtGui.QMessageBox.warning(self, self.tr("Automatic Search"),
+ QtGui.QMessageBox.warning(self, self.tr("Automatic Online Search"),
self.tr("You need to load a comic first!"))
return
@@ -834,20 +853,25 @@ class TaggerWindow( QtGui.QMainWindow):
def queryOnline(self, autoselect=False):
- if self.settings.cv_api_key == "":
- QtGui.QMessageBox.warning(self, self.tr("Online Search"),
- self.tr("You need an API key from ComicVine to search online. " +
- "Go to settings and enter it."))
- return
+ #if self.settings.cv_api_key == "":
+ # QtGui.QMessageBox.warning(self, self.tr("Online Search"),
+ # self.tr("You need an API key from ComicVine to search online. " +
+ # "Go to settings and enter it."))
+ # return
+
+ issue_number = str(self.leIssueNum.text()).strip()
+
+ if autoselect and issue_number == "":
+ QtGui.QMessageBox.information(self,"Automatic Online Search", "Can't auto-select without an issue number (yet!)")
+ return
if str(self.leSeries.text()).strip() != "":
series_name = str(self.leSeries.text()).strip()
else:
- QtGui.QMessageBox.information(self, self.tr("Whoops"), self.tr("Need to enter a series name to query."))
+ QtGui.QMessageBox.information(self, self.tr("Online Search"), self.tr("Need to enter a series name to search."))
return
- issue_number = str(self.leIssueNum.text()).strip()
year = str(self.lePubYear.text()).strip()
if year == "":
@@ -877,14 +901,21 @@ class TaggerWindow( QtGui.QMainWindow):
def commitMetadata(self):
if ( self.metadata is not None and self.comic_archive is not None):
- QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
- self.formToMetadata()
- self.comic_archive.writeMetadata( self.metadata, self.data_style )
- self.clearDirtyFlag()
- self.updateInfoBox()
- QtGui.QApplication.restoreOverrideCursor()
-
- QtGui.QMessageBox.information(self, self.tr("Yeah!"), self.tr("File written."))
+
+ reply = QtGui.QMessageBox.question(self,
+ self.tr("Save Tags"),
+ self.tr("Are you sure you wish to save " + MetaDataStyle.name[self.data_style] + " tags to this archive?"),
+ QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
+
+ if reply == QtGui.QMessageBox.Yes:
+ QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
+ self.formToMetadata()
+ self.comic_archive.writeMetadata( self.metadata, self.data_style )
+ self.clearDirtyFlag()
+ self.updateInfoBox()
+ QtGui.QApplication.restoreOverrideCursor()
+
+ QtGui.QMessageBox.information(self, self.tr("Yeah!"), self.tr("File written."))
else:
@@ -894,6 +925,7 @@ class TaggerWindow( QtGui.QMainWindow):
def setDataStyle(self, s):
self.data_style, b = self.cbDataStyle.itemData(s).toInt()
self.updateStyleTweaks()
+ self.updateSaveMenu()
def updateCreditColors( self ):
inactive_color = QtGui.QColor(255, 170, 150)
diff --git a/taggerwindow.ui b/taggerwindow.ui
index 6b6bc7c..002eac5 100644
--- a/taggerwindow.ui
+++ b/taggerwindow.ui
@@ -260,7 +260,7 @@
0
0
- 680
+ 685
380
@@ -1013,7 +1013,7 @@
0
0
975
- 25
+ 28