A slew of enhancements
git-svn-id: http://comictagger.googlecode.com/svn/trunk@56 6c5673fe-1810-88d6-992b-cd32ca31540c
This commit is contained in:
parent
47a61885d9
commit
263598ef4a
@ -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)
|
||||
|
||||
|
@ -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 ] )
|
||||
|
@ -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
|
||||
|
@ -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 )
|
||||
|
@ -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( "" )
|
||||
|
118
matchselectionwindow.py
Normal file
118
matchselectionwindow.py
Normal file
@ -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))
|
||||
|
142
matchselectionwindow.ui
Normal file
142
matchselectionwindow.ui
Normal file
@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogMatchSelect</class>
|
||||
<widget class="QDialog" name="dialogMatchSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>831</width>
|
||||
<height>506</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Select Match</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rowCount">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Series</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Publisher</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelThumbnail">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>450</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Panel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogMatchSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -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)
|
||||
|
@ -260,7 +260,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>680</width>
|
||||
<width>685</width>
|
||||
<height>380</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -1013,7 +1013,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>975</width>
|
||||
<height>25</height>
|
||||
<height>28</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuComicTagger">
|
||||
@ -1080,7 +1080,7 @@
|
||||
<set>Qt::NoToolBarArea</set>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonFollowStyle</enum>
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
<property name="floatable">
|
||||
<bool>false</bool>
|
||||
|
90
todo.txt
90
todo.txt
@ -1,53 +1,52 @@
|
||||
|
||||
-----------------
|
||||
Features
|
||||
----------------
|
||||
|
||||
Auto-select:
|
||||
Multi-match dialog
|
||||
Check aspect ratio, and maybe break cover into two parts for hashing?
|
||||
|
||||
Stand-alone CLI
|
||||
Info dump
|
||||
optionless args
|
||||
remove tags
|
||||
copy tags
|
||||
|
||||
Settings/Preferences Dialog
|
||||
Remove API Key
|
||||
Add clear cache
|
||||
Add reset settings
|
||||
Tab w/Identifier Settings
|
||||
Add publisher blacklist
|
||||
|
||||
Add class for warning/info messages with "Don't show again" checkbox.
|
||||
Add list of these flags to settings
|
||||
|
||||
TaggerWindow entry fields
|
||||
Special tabbed Dialog needed for:
|
||||
Pages Info - maybe a custom painted widget
|
||||
At minimum, preserve the page data
|
||||
|
||||
|
||||
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
|
||||
-----------
|
||||
|
||||
Style sheets for windows/mac/linux
|
||||
|
||||
-----------------
|
||||
Bugs
|
||||
----------------
|
||||
|
||||
Disable CBL for RAR
|
||||
SQLite chokes on "Batman\ Li'l Gotham 001.cbr" name -- Doesn't like single quote '
|
||||
|
||||
SERIOUS BUG: rebuilding zips!
|
||||
http://stackoverflow.com/questions/11578443/trigger-io-errno-18-cross-device-link
|
||||
http://stackoverflow.com/questions/11578443/trigger-io-errno-18-cross-device-link
|
||||
|
||||
OSX:
|
||||
toolbar
|
||||
weird unrar complaints
|
||||
Page browser sizing
|
||||
Override curson is not beachball
|
||||
|
||||
Disable save when read-only
|
||||
|
||||
Be more tolerant of mis-labled extensions i.e. cbr when it's a cbz
|
||||
Other settings possibilities:
|
||||
Last tag style
|
||||
Last "Open" folder (include dragged)
|
||||
Clear caches
|
||||
|
||||
Filename parsing:
|
||||
Rework how series name is separated from issue
|
||||
|
||||
Form type validation Ints vs strings for month, year. etc
|
||||
|
||||
@ -55,45 +54,26 @@ Check all HTTP responses for errors
|
||||
|
||||
Lots of error checking
|
||||
|
||||
Other settings possibilities:
|
||||
Last tag style
|
||||
Last "Open" folder (include dragged)
|
||||
Clear caches
|
||||
|
||||
|
||||
Image Hashes:
|
||||
Failures of image hash:
|
||||
Thor 600 Wrap-around w/ different aspect ratio
|
||||
Bone 3 - Variant Cover,
|
||||
Avengers #1, #13, #81
|
||||
|
||||
|
||||
Filename parsing:
|
||||
Concatenation of Name and Issue??
|
||||
"1602"
|
||||
|
||||
|
||||
App option to covert RAR to ZIP
|
||||
-------------
|
||||
Future
|
||||
------------
|
||||
Add warning message to allow writing CBI to RAR, and ask them to contact CBL ! :-)
|
||||
|
||||
If no unrar in path, then filter out CBR/RAR from open dialog
|
||||
Scrape alternate Covers from ComicVine issue pages
|
||||
|
||||
GCD scraper or DB reader
|
||||
|
||||
GUI to handle mutliple files or folders
|
||||
|
||||
Auto search:
|
||||
Searching w/o issue #?
|
||||
|
||||
Content Hashes!!
|
||||
|
||||
Wizard for converting between tag styles
|
||||
|
||||
Remove stale data from cache DB
|
||||
|
||||
SQLite chokes on "Batman\ Li'l Gotham 001.cbr" name
|
||||
|
||||
Auto search:
|
||||
Choosing with pub year
|
||||
Lexical analysis
|
||||
Searching w/o issue #?
|
||||
|
||||
Determine alternate covers from CV somehow
|
||||
|
||||
-------------
|
||||
Other
|
||||
------------
|
||||
Content Hashes!!
|
||||
App option to covert RAR to ZIP
|
||||
|
||||
|
||||
Archive function to detect tag blocks out of sync
|
||||
|
20
utils.py
20
utils.py
@ -53,9 +53,27 @@ def addtopath( dir ):
|
||||
if dir is not None and dir != "":
|
||||
os.environ['PATH'] = dir + os.pathsep + os.environ['PATH']
|
||||
|
||||
# returns executable path, if it exists
|
||||
def which(program):
|
||||
import os
|
||||
def is_exe(fpath):
|
||||
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
||||
|
||||
fpath, fname = os.path.split(program)
|
||||
if fpath:
|
||||
if is_exe(program):
|
||||
return program
|
||||
else:
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
exe_file = os.path.join(path, program)
|
||||
if is_exe(exe_file):
|
||||
return exe_file
|
||||
|
||||
return None
|
||||
|
||||
def removearticles( text ):
|
||||
text = text.lower()
|
||||
articles = ['and', 'the', 'a', '&' ]
|
||||
articles = ['and', 'the', 'a', '&', 'issue' ]
|
||||
newText = ''
|
||||
for word in text.split(' '):
|
||||
if word not in articles:
|
||||
|
@ -33,7 +33,7 @@ from genericmetadata import GenericMetadata
|
||||
from imagefetcher import ImageFetcher
|
||||
from progresswindow import IDProgressWindow
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
from matchselectionwindow import MatchSelectionWindow
|
||||
|
||||
class SearchThread( QtCore.QThread):
|
||||
|
||||
@ -111,6 +111,14 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
def autoSelect( self ):
|
||||
|
||||
if self.comic_archive is None:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select", "You need to load a comic first!")
|
||||
return
|
||||
|
||||
if self.issue_number is None or self.issue_number == "":
|
||||
QtGui.QMessageBox.information(self,"Auto-Select", "Can't auto-select without an issue number (yet!)")
|
||||
return
|
||||
|
||||
self.iddialog = IDProgressWindow( self)
|
||||
self.iddialog.setModal(True)
|
||||
self.iddialog.rejected.connect( self.identifyCancel )
|
||||
@ -118,6 +126,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
self.ii = IssueIdentifier( self.comic_archive, self.cv_api_key )
|
||||
|
||||
|
||||
md = GenericMetadata()
|
||||
md.series = self.series_name
|
||||
md.issueNumber = self.issue_number
|
||||
@ -151,29 +160,49 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
matches = self.ii.match_list
|
||||
result = self.ii.search_result
|
||||
match_index = 0
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
if result == self.ii.ResultNoMatches:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " No matches found :-(")
|
||||
elif result == self.ii.ResultFoundMatchButBadCoverScore:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but cover doesn't seem to match. Verify before commiting!")
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but cover doesn't seem the same. Verify before commiting!")
|
||||
found_match = True
|
||||
elif result == self.ii.ResultFoundMatchButNotFirstPage :
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found a match, but not with the first page of the archive.")
|
||||
found_match = True
|
||||
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually.")
|
||||
choices = True
|
||||
elif result == self.ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == self.ii.ResultMultipleGoodMatches:
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found multiple likely matches! Selection DIALOG TBD.")
|
||||
QtGui.QMessageBox.information(self,"Auto-Select Result", " Found multiple likely matches. Please select.")
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
selector = MatchSelectionWindow( self, matches )
|
||||
selector.setModal(True)
|
||||
|
||||
title = self.series_name
|
||||
title += " #" + self.issue_number
|
||||
if self.year is not None:
|
||||
title += " (" + str(self.year) + ")"
|
||||
title += " - "
|
||||
|
||||
selector.setWindowTitle( title + "Select Match")
|
||||
selector.exec_()
|
||||
if selector.result():
|
||||
#we should now have a list index
|
||||
found_match = True
|
||||
match_index = selector.current_row
|
||||
|
||||
if found_match:
|
||||
self.iddialog.accept()
|
||||
|
||||
print "VolumeSelectionWindow found a match!!", matches[0]['volume_id'], matches[0]['issue_number']
|
||||
self.volume_id = matches[0]['volume_id']
|
||||
self.issue_number = matches[0]['issue_number']
|
||||
self.volume_id = matches[match_index]['volume_id']
|
||||
self.issue_number = matches[match_index]['issue_number']
|
||||
self.selectByID()
|
||||
self.showIssues()
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>796</width>
|
||||
<height>454</height>
|
||||
<width>801</width>
|
||||
<height>470</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -156,7 +156,7 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnRequery">
|
||||
<property name="text">
|
||||
<string>Re-Query</string>
|
||||
<string>Re-Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
Loading…
Reference in New Issue
Block a user