A slew of changes

git-svn-id: http://comictagger.googlecode.com/svn/trunk@29 6c5673fe-1810-88d6-992b-cd32ca31540c
This commit is contained in:
beville@gmail.com 2012-11-13 00:12:43 +00:00
parent a58442b8f6
commit e1f3397960
18 changed files with 1379 additions and 397 deletions

View File

@ -50,7 +50,7 @@ class ZipArchiver:
return comment
def setArchiveComment( self, comment ):
writeZipComment( self.path, comment )
self.writeZipComment( self.path, comment )
def readArchiveFile( self, archive_file ):
zf = zipfile.ZipFile( self.path, 'r' )

View File

@ -25,14 +25,20 @@ import urllib2, urllib
import math
import re
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
import utils
from settings import ComicTaggerSettings
from comicvinecacher import ComicVineCacher
from genericmetadata import GenericMetadata
class ComicVineTalker:
class ComicVineTalker(QObject):
def __init__(self, api_key):
QObject.__init__(self)
self.api_key = api_key
@ -48,15 +54,16 @@ class ComicVineTalker:
return cv_response[ 'status_code' ] != 100
def searchForSeries( self, series_name ):
def searchForSeries( self, series_name , callback=None, refresh_cache=False ):
# before we search online, look in our cache, since we might have
# done this same search recently
cvc = ComicVineCacher( ComicTaggerSettings.getSettingsFolder() )
cached_search_results = cvc.get_search_results( series_name )
if len (cached_search_results) > 0:
return cached_search_results
if not refresh_cache:
cached_search_results = cvc.get_search_results( series_name )
if len (cached_search_results) > 0:
return cached_search_results
original_series_name = series_name
@ -84,9 +91,12 @@ class ComicVineTalker:
search_results.extend( cv_response['results'])
offset = 0
if callback is not None:
callback( current_result_count, total_result_count )
# see if we need to keep asking for more pages...
while ( current_result_count < total_result_count ):
print ("getting another page of results...")
print ("getting another page of results {0} of {1}...".format( current_result_count, total_result_count))
offset += limit
resp = urllib2.urlopen( search_url + "&offset="+str(offset) )
content = resp.read()
@ -99,6 +109,9 @@ class ComicVineTalker:
search_results.extend( cv_response['results'])
current_result_count += cv_response['number_of_page_results']
if callback is not None:
callback( current_result_count, total_result_count )
#for record in search_results:
# print( "{0}: {1} ({2})".format(record['id'], smart_str(record['name']) , record['start_year'] ) )
@ -231,13 +244,10 @@ class ComicVineTalker:
return newstring
def fetchIssueCoverURLs( self, issue_id ):
# before we search online, look in our cache, since we might already
# have this info
cvc = ComicVineCacher( ComicTaggerSettings.getSettingsFolder() )
cached_image_url,cached_thumb_url = cvc.get_issue_image_url( issue_id )
cached_image_url,cached_thumb_url = self.fetchCachedIssueCoverURLs( issue_id )
if cached_image_url is not None:
return cached_image_url,cached_thumb_url
@ -247,9 +257,57 @@ class ComicVineTalker:
cv_response = json.loads(content)
if cv_response[ 'status_code' ] != 1:
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
return None
cvc.add_issue_image_url( issue_id, cv_response['results']['image']['super_url'], cv_response['results']['image']['thumb_url'] )
return cv_response['results']['image']['super_url'], cv_response['results']['image']['thumb_url']
return None, None
image_url = cv_response['results']['image']['super_url']
thumb_url = cv_response['results']['image']['thumb_url']
if image_url is not None:
self.cacheIssueCoverURLs( issue_id, image_url,thumb_url )
return image_url,thumb_url
def fetchCachedIssueCoverURLs( self, issue_id ):
# before we search online, look in our cache, since we might already
# have this info
cvc = ComicVineCacher( ComicTaggerSettings.getSettingsFolder() )
return cvc.get_issue_image_url( issue_id )
def cacheIssueCoverURLs( self, issue_id, image_url,thumb_url ):
cvc = ComicVineCacher( ComicTaggerSettings.getSettingsFolder() )
cvc.add_issue_image_url( issue_id, image_url, thumb_url )
#---------------------------------------------------------------------------
urlFetchComplete = pyqtSignal( str , str, int)
def asyncFetchIssueCoverURLs( self, issue_id ):
self.issue_id = issue_id
cached_image_url,cached_thumb_url = self.fetchCachedIssueCoverURLs( issue_id )
if cached_image_url is not None:
self.urlFetchComplete.emit( cached_image_url,cached_thumb_url, self.issue_id )
return
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image"
self.nam = QNetworkAccessManager()
self.nam.finished.connect( self.asyncFetchIssueCoverURLComplete )
self.nam.get(QNetworkRequest(QUrl(issue_url)))
def asyncFetchIssueCoverURLComplete( self, reply ):
# read in the response
data = reply.readAll()
cv_response = json.loads(str(data))
if cv_response[ 'status_code' ] != 1:
print ( "Comic Vine query failed with error: [{0}]. ".format( cv_response[ 'error' ] ))
return
image_url = cv_response['results']['image']['super_url']
thumb_url = cv_response['results']['image']['thumb_url']
self.cacheIssueCoverURLs( self.issue_id, image_url, thumb_url )
self.urlFetchComplete.emit( image_url, thumb_url, self.issue_id )

171
imagefetcher.py Normal file
View File

@ -0,0 +1,171 @@
"""
A python class to manage fetching and caching of images by URL
"""
"""
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 sqlite3 as lite
import os
import datetime
import shutil
import tempfile
import urllib2, urllib
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt4.QtCore import QUrl, pyqtSignal, QObject, QByteArray
from PyQt4 import QtGui
from settings import ComicTaggerSettings
class ImageFetcher(QObject):
fetchComplete = pyqtSignal( QByteArray , int)
def __init__(self ):
QObject.__init__(self)
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
self.db_file = os.path.join( self.settings_folder, "image_url_cache.db" )
self.cache_folder = os.path.join( self.settings_folder, "image_cache" )
if not os.path.exists( self.db_file ):
self.create_image_db()
def fetch( self, url, user_data=None, blocking=False ):
"""
If called with blocking=True, this will block until the image is
fetched.
If called with blocking=False, this will run the fetch in the
background, and emit a signal when done
"""
self.user_data = user_data
self.fetched_url = url
# first look in the DB
image_data = self.get_image_from_cache( url )
if blocking:
if image_data is None:
image_data = urllib.urlopen(url).read()
# save the image to the cache
self.add_image_to_cache( self.fetched_url, image_data )
return image_data
else:
# if we found it, just emit the signal asap
if image_data is not None:
self.fetchComplete.emit( QByteArray(image_data), self.user_data )
return
# didn't find it. look online
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.finishRequest)
self.nam.get(QNetworkRequest(QUrl(url)))
#we'll get called back when done...
def finishRequest(self, reply):
# read in the image data
image_data = reply.readAll()
# save the image to the cache
self.add_image_to_cache( self.fetched_url, image_data )
self.fetchComplete.emit( QByteArray(image_data), self.user_data )
def create_image_db( self ):
# this will wipe out any existing version
open( self.db_file, 'w').close()
# wipe any existing image cache folder too
if os.path.isdir( self.cache_folder ):
shutil.rmtree(path)
os.makedirs( self.cache_folder )
con = lite.connect( self.db_file )
# create tables
with con:
cur = con.cursor()
cur.execute("CREATE TABLE Images(" +
"url TEXT," +
"filename TEXT," +
"timestamp TEXT," +
"PRIMARY KEY (url) )"
)
def add_image_to_cache( self, url, image_data ):
con = lite.connect( self.db_file )
with con:
cur = con.cursor()
timestamp = datetime.datetime.now()
tmp_fd, filename = tempfile.mkstemp(dir=self.cache_folder, prefix="img")
f = os.fdopen(tmp_fd, 'w+b')
f.write( image_data )
f.close()
cur.execute("INSERT or REPLACE INTO Images VALUES( ?, ?, ? )" ,
(url,
filename,
timestamp )
)
def get_image_from_cache( self, url ):
con = lite.connect( self.db_file )
with con:
cur = con.cursor()
cur.execute("SELECT filename FROM Images WHERE url=?", [ url ])
row = cur.fetchone()
if row is None :
return None
else:
filename = row[0]
image_data = None
try:
with open( filename, 'rb' ) as f:
image_data = f.read()
f.close()
except IOError as e:
pass
return image_data

View File

@ -140,8 +140,7 @@ class ImageHasher(object):
@staticmethod
def hamming_distance(h1, h2):
if type(h1) == long:
if type(h1) == long or type(h1) == int:
n1 = h1
n2 = h2
else:

View File

@ -27,6 +27,8 @@ from comicvinecacher import ComicVineCacher
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker
from imagehasher import ImageHasher
from imagefetcher import ImageFetcher
import utils
class IssueIdentifier:
@ -40,6 +42,7 @@ class IssueIdentifier:
self.additional_metadata = GenericMetadata()
self.cv_api_key = cv_api_key
self.output_function = IssueIdentifier.defaultWriteOutput
self.callback = None
def setScoreMinThreshold( self, thresh ):
self.min_score_thresh = thresh
@ -66,6 +69,9 @@ class IssueIdentifier:
else:
return ImageHasher( data=image_data ).average_hash()
def setProgressCallback( self, cb_func ):
self.callback = cb_func
def getSearchKeys( self ):
ca = self.comic_archive
@ -137,6 +143,9 @@ class IssueIdentifier:
def search( self ):
ca = self.comic_archive
self.match_list = []
self.cancel = False
if not ca.seemsToBeAComicArchive():
self.log_msg( "Sorry, but "+ opts.filename + " is not a comic archive!")
return []
@ -173,6 +182,8 @@ class IssueIdentifier:
cv_search_results = comicVine.searchForSeries( keys['series'] )
#self.log_msg( "Found " + str(len(cv_search_results)) + " initial results" )
if self.cancel == True:
return []
series_shortlist = []
@ -189,22 +200,26 @@ class IssueIdentifier:
self.log_msg( "Searching in " + str(len(series_shortlist)) +" series" )
if self.callback is not None:
self.callback( 0, len(series_shortlist))
# now sort the list by name length
series_shortlist.sort(key=lambda x: len(x['name']), reverse=False)
# Now we've got a list of series that we can dig into,
# and look for matching issue number, date, and cover image
match_list = []
self.log_msg( "Fetching issue data", newline=False)
counter = 0
for series in series_shortlist:
#self.log_msg( "Fetching info for ID: {0} {1} ({2}) ...".format(
# series['id'],
# series['name'],
# series['start_year']) )
self.log_msg( ".", newline=False)
if self.callback is not None:
counter += 1
self.callback( counter, len(series_shortlist))
self.log_msg( "Fetching info for ID: {0} {1} ({2}) ...".format(
series['id'],
series['name'],
series['start_year']), newline=False )
cv_series_results = comicVine.fetchVolumeData( series['id'] )
issue_list = cv_series_results['issues']
@ -220,8 +235,11 @@ class IssueIdentifier:
if num_s == keys['issue_number']:
# found a matching issue number! now get the issue data
img_url, thumb_url = comicVine.fetchIssueCoverURLs( issue['id'] )
#TODO get the image from URL, and calc hash!!
url_image_data = urllib.urlopen(thumb_url).read()
url_image_data = ImageFetcher().fetch(thumb_url, blocking=True)
if self.cancel == True:
self.match_list = []
return self.match_list
url_image_hash = self.calculateHash( url_image_data )
@ -234,60 +252,62 @@ class IssueIdentifier:
match['img_url'] = thumb_url
match['issue_id'] = issue['id']
match['volume_id'] = series['id']
match_list.append(match)
self.match_list.append(match)
self.log_msg( " --> {0}".format(match['distance']), newline=False )
break
self.log_msg( "done!" )
self.log_msg( "" )
if len(match_list) == 0:
if len(self.match_list) == 0:
self.log_msg( ":-( no matches!" )
return match_list
return self.match_list
# sort list by image match scores
match_list.sort(key=lambda k: k['distance'])
self.match_list.sort(key=lambda k: k['distance'])
l = []
for i in match_list:
for i in self.match_list:
l.append( i['distance'] )
self.log_msg( "Compared {0} covers".format(len(match_list)), newline=False)
self.log_msg( "Compared {0} covers".format(len(self.match_list)), newline=False)
self.log_msg( str(l))
def print_match(item):
self.log_msg( u"-----> {0} #{1} {2} -- score: {3}\n-------> url:{4}".format(
self.log_msg( u"-----> {0} #{1} {2} -- score: {3}".format(
item['series'],
item['issue_number'],
item['issue_title'],
item['distance'],
item['img_url']) )
item['distance']) )
best_score = match_list[0]['distance']
best_score = self.match_list[0]['distance']
if len(match_list) == 1:
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?" )
print_match(match_list[0])
return match_list
print_match(self.match_list[0])
return self.match_list
elif best_score > self.min_score_thresh and len(match_list) > 1:
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 match_list
return self.match_list
#now pare down list, remove any item more than specified distant from the top scores
for item in reversed(match_list):
for item in reversed(self.match_list):
if item['distance'] > best_score + self.min_score_distance:
match_list.remove(item)
self.match_list.remove(item)
if len(match_list) == 1:
print_match(match_list[0])
elif len(match_list) == 0:
if len(self.match_list) == 1:
print_match(self.match_list[0])
elif len(self.match_list) == 0:
self.log_msg( "No matches found :(" )
else:
print
self.log_msg( "More than one likley candiate. Maybe a lexical comparison??" )
for item in match_list:
for item in self.match_list:
print_match(item)
return match_list
return self.match_list

View File

@ -19,25 +19,28 @@ limitations under the License.
"""
import sys
import os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import QUrl
from PyQt4.QtCore import QUrl, pyqtSignal, QByteArray
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comicvinetalker import ComicVineTalker
from imagefetcher import ImageFetcher
class IssueSelectionWindow(QtGui.QDialog):
volume_id = 0
def __init__(self, parent, cv_api_key, series_id, issue_number):
def __init__(self, parent, settings, series_id, issue_number):
super(IssueSelectionWindow, self).__init__(parent)
uic.loadUi('issueselectionwindow.ui', self)
self.series_id = series_id
self.cv_api_key = cv_api_key
self.settings = settings
self.url_fetch_thread = None
if issue_number is None or issue_number == "":
self.issue_number = 1
else:
@ -59,14 +62,17 @@ class IssueSelectionWindow(QtGui.QDialog):
if (issue_id == self.initial_id):
self.twList.selectRow( r )
break
def performQuery( self ):
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
comicVine = ComicVineTalker( self.cv_api_key )
comicVine = ComicVineTalker( self.settings.cv_api_key )
volume_data = comicVine.fetchVolumeData( self.series_id )
self.issue_list = volume_data['issues']
@ -88,8 +94,6 @@ class IssueSelectionWindow(QtGui.QDialog):
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
record['url'] = None
if float(record['issue_number']) == float(self.issue_number):
self.initial_id = record['id']
@ -100,6 +104,8 @@ class IssueSelectionWindow(QtGui.QDialog):
self.twList.setSortingEnabled(True)
self.twList.sortItems( 0 , QtCore.Qt.AscendingOrder )
QtGui.QApplication.restoreOverrideCursor()
def cellDoubleClicked( self, r, c ):
self.accept()
@ -118,22 +124,26 @@ class IssueSelectionWindow(QtGui.QDialog):
if record['id'] == self.issue_id:
self.issue_number = record['issue_number']
# We don't yet have an image URL for this issue. Make a request for URL, and hold onto it
# TODO: this should be reworked... too much UI latency, maybe chain the NAMs??
if record['url'] == None:
record['url'], dummy = ComicVineTalker( self.cv_api_key ).fetchIssueCoverURLs( self.issue_id )
self.labelThumbnail.setText("loading...")
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.finishedImageRequest)
self.nam.get(QNetworkRequest(QUrl(record['url'])))
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.getcwd() + "/nocover.png"))
self.cv = ComicVineTalker( self.settings.cv_api_key )
self.cv.urlFetchComplete.connect( self.urlFetchComplete )
self.cv.asyncFetchIssueCoverURLs( int(self.issue_id) )
break
# called when the cover URL has been fetched
def urlFetchComplete( self, image_url, thumb_url, issue_id ):
self.cover_fetcher = ImageFetcher( )
self.cover_fetcher.fetchComplete.connect(self.coverFetchComplete)
self.cover_fetcher.fetch( str(image_url), user_data=issue_id )
# called when the image is done loading
def finishedImageRequest(self, reply):
img = QtGui.QImage()
img.loadFromData(reply.readAll())
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))
def coverFetchComplete( self, image_data, issue_id ):
if self.issue_id == issue_id:
img = QtGui.QImage()
img.loadFromData( image_data )
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))

View File

@ -6,108 +6,101 @@
<rect>
<x>0</x>
<y>0</y>
<width>843</width>
<height>454</height>
<width>657</width>
<height>400</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Issue</string>
</property>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>571</width>
<height>392</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
<widget class="QLabel" name="labelThumbnail">
<property name="geometry">
<rect>
<x>589</x>
<y>9</y>
<width>241</width>
<height>391</height>
</rect>
</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>
<widget class="QTableWidget" name="twList">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>569</width>
<height>390</height>
</rect>
</property>
<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>2</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Issue</string>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
</widget>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>260</x>
<y>420</y>
<width>569</width>
<height>27</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<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>2</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Issue</string>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</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>

BIN
nocover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

37
progresswindow.py Normal file
View File

@ -0,0 +1,37 @@
"""
A PyQT4 dialog to show ID log and progress
"""
"""
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
from PyQt4 import QtCore, QtGui, uic
class IDProgressWindow(QtGui.QDialog):
def __init__(self, parent):
super(IDProgressWindow, self).__init__(parent)
uic.loadUi('progresswindow.ui', self)

84
progresswindow.ui Normal file
View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogIssueSelect</class>
<widget class="QDialog" name="dialogIssueSelect">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>556</width>
<height>287</height>
</rect>
</property>
<property name="windowTitle">
<string>Issue Identification Progress</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="textEdit"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogIssueSelect</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>dialogIssueSelect</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>

106
settings.py Normal file
View File

@ -0,0 +1,106 @@
"""
Settings class for comictagger app
"""
"""
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
import ConfigParser
import platform
import utils
class ComicTaggerSettings:
settings_file = ""
folder = ""
rar_exe_path = ""
unrar_exe_path = ""
cv_api_key = ""
@staticmethod
def getSettingsFolder():
if platform.system() == "Windows":
return os.path.join( os.environ['APPDATA'], 'ComicTagger' )
else:
return os.path.join( os.path.expanduser('~') , '.ComicTagger')
def __init__(self):
self.config = ConfigParser.RawConfigParser()
self.folder = ComicTaggerSettings.getSettingsFolder()
if not os.path.exists( self.folder ):
os.makedirs( self.folder )
self.settings_file = os.path.join( self.folder, "settings")
# if config file doesn't exist, write one out
if not os.path.exists( self.settings_file ):
self.save()
else:
self.load()
# take a crack at finding rar exes, if not set already
if self.rar_exe_path == "":
if platform.system() == "Windows":
# look in some likely places for windows machine
if os.path.exists( "C:\Program Files\WinRAR\Rar.exe" ):
self.rar_exe_path = "C:\Program Files\WinRAR\Rar.exe"
elif os.path.exists( "C:\Program Files (x86)\WinRAR\Rar.exe" ):
self.rar_exe_path = "C:\Program Files (x86)\WinRAR\Rar.exe"
else:
# see if it's in the path of unix user
if utils.which("rar") is not None:
self.rar_exe_path = utils.which("rar")
if self.rar_exe_path != "":
self.save()
if self.unrar_exe_path == "":
if platform.system() != "Windows":
# see if it's in the path of unix user
if utils.which("unrar") is not None:
self.unrar_exe_path = utils.which("unrar")
if self.unrar_exe_path != "":
self.save()
def load(self):
#print "reading", self.path
self.config.read( self.settings_file )
self.rar_exe_path = self.config.get( 'settings', 'rar_exe_path' )
self.unrar_exe_path = self.config.get( 'settings', 'unrar_exe_path' )
self.cv_api_key = self.config.get( 'settings', 'cv_api_key' )
def save( self ):
if not self.config.has_section( 'settings' ):
self.config.add_section( 'settings' )
self.config.set( 'settings', 'cv_api_key', self.cv_api_key )
self.config.set( 'settings', 'rar_exe_path', self.rar_exe_path )
self.config.set( 'settings', 'unrar_exe_path', self.unrar_exe_path )
with open( self.settings_file, 'wb') as configfile:
self.config.write(configfile)

144
settingswindow.py Normal file
View File

@ -0,0 +1,144 @@
"""
A PyQT4 dialog to enter app settings
"""
"""
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 platform
import os
from PyQt4 import QtCore, QtGui, uic
from settings import ComicTaggerSettings
from comicvinetalker import *
windowsRarHelp = """
<html><head/><body><p>In order to write to CBR/RAR archives,
you will need to have the tools from
<a href="http://www.win-rar.com/download.html">
<span style=" text-decoration: underline; color:#0000ff;">WinRAR</span>
</a> installed. </p></body></html>
"""
linuxRarHelp = """
<html><head/><body><p>In order to read/write to CBR/RAR archives, you will
need to have the shareware tools from WinRar installed. Your package manager
should have unrar, and probably rar. If not, download them <a href="http://www.win-rar.com/download.html">
<span style=" text-decoration: underline; color:#0000ff;">here</span></a>, and install in your path.</p>
</body></html>
"""
macRarHelp = """
<html><head/><body><p>In order to read/write to CBR/RAR archives,
you will need the shareware tools from <a href="http://www.win-rar.com/download.html">
<span style=" text-decoration: underline; color:#0000ff;">WinRAR</span></a>.
</p></body></html>
"""
class SettingsWindow(QtGui.QDialog):
def __init__(self, parent, settings ):
super(SettingsWindow, self).__init__(parent)
uic.loadUi('settingswindow.ui', self)
self.settings = settings
if platform.system() == "Windows":
self.lblUnrar.hide()
self.leUnrarExePath.hide()
self.btnBrowseUnrar.hide()
self.lblRarHelp.setText( windowsRarHelp )
elif platform.system() == "Linux":
self.lblRarHelp.setText( linuxRarHelp )
elif platform.system() == "Darwin":
self.lblRarHelp.setText( macRarHelp )
# Copy values from settings to form
self.leCVAPIKey.setText( self.settings.cv_api_key )
self.leRarExePath.setText( self.settings.rar_exe_path )
self.leUnrarExePath.setText( self.settings.unrar_exe_path )
self.btnTestKey.clicked.connect(self.testAPIKey)
self.btnBrowseRar.clicked.connect(self.selectRar)
self.btnBrowseUnrar.clicked.connect(self.selectUnrar)
def accept( self ):
# Copy values from form to settings and save
self.settings.cv_api_key = str(self.leCVAPIKey.text())
self.settings.rar_exe_path = str(self.leRarExePath.text())
self.settings.unrar_exe_path = str(self.leUnrarExePath.text())
# make sure unrar program is now in the path for the UnRAR class
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
self.settings.save()
QtGui.QDialog.accept(self)
def testAPIKey( self ):
# TODO hourglass
palette = self.lblResult.palette()
bad_color = QtGui.QColor(255, 0, 0)
good_color = QtGui.QColor(0, 255, 0)
comicVine = ComicVineTalker( str(self.leCVAPIKey.text()) )
if comicVine.testKey( ):
palette.setColor(self.lblResult.foregroundRole(), good_color)
self.lblResult.setText("Good Key!")
else:
palette.setColor(self.lblResult.foregroundRole(), bad_color)
self.lblResult.setText("Bad Key :(")
self.lblResult.setPalette(palette)
def selectRar( self ):
self.selectFile( self.leRarExePath, "RAR" )
def selectUnrar( self ):
self.selectFile( self.leUnrarExePath, "UnRAR" )
def selectFile( self, control, name ):
dialog = QtGui.QFileDialog(self)
dialog.setFileMode(QtGui.QFileDialog.ExistingFile)
if platform.system() == "Windows":
if name == "RAR":
filter = self.tr("Rar Program (Rar.exe)")
else:
filter = self.tr("Programs (*.exe)")
dialog.setNameFilter(filter)
else:
dialog.setFilter(QtCore.QDir.Files) #QtCore.QDir.Executable | QtCore.QDir.Files)
pass
dialog.setDirectory(os.path.dirname(str(control.text())))
dialog.setWindowTitle("Find " + name + " program")
if (dialog.exec_()):
fileList = dialog.selectedFiles()
control.setText( str(fileList[0]) )

241
settingswindow.ui Normal file
View File

@ -0,0 +1,241 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dialogCreditEditor</class>
<widget class="QDialog" name="dialogCreditEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>750</width>
<height>403</height>
</rect>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>530</x>
<y>340</y>
<width>191</width>
<height>30</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>581</width>
<height>61</height>
</rect>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;To perform online searches using ComicVine, you must first register and request an API key. You can start here: &lt;a href=&quot;http://api.comicvine.com/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;http://api.comicvine.com/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
<widget class="QWidget" name="formLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>581</width>
<height>41</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Comic Vine API Key</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leCVAPIKey"/>
</item>
</layout>
</widget>
<widget class="QLabel" name="lblRarHelp">
<property name="geometry">
<rect>
<x>20</x>
<y>150</y>
<width>611</width>
<height>61</height>
</rect>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In order to read/write to CBR/RAR archives, you will need to have the shareware tools from &lt;a href=&quot;www.win-rar.com/download.html&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;WinRAR&lt;/span&gt;&lt;/a&gt; installed. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
<widget class="QWidget" name="formLayoutWidget_3">
<property name="geometry">
<rect>
<x>20</x>
<y>220</y>
<width>611</width>
<height>71</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>RAR program</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leRarExePath">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblUnrar">
<property name="text">
<string>UnRAR program</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leUnrarExePath">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QPushButton" name="btnBrowseRar">
<property name="geometry">
<rect>
<x>640</x>
<y>220</y>
<width>41</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
<widget class="QPushButton" name="btnBrowseUnrar">
<property name="geometry">
<rect>
<x>640</x>
<y>250</y>
<width>41</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
<widget class="QPushButton" name="btnTestKey">
<property name="geometry">
<rect>
<x>610</x>
<y>90</y>
<width>94</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>Test Key</string>
</property>
</widget>
<widget class="QLabel" name="lblResult">
<property name="geometry">
<rect>
<x>620</x>
<y>120</y>
<width>71</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dialogCreditEditor</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>dialogCreditEditor</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>

View File

@ -34,6 +34,7 @@ from issueidentifier import IssueIdentifier
import utils
#-----------------------------
def cli_mode( opts, settings ):
@ -80,6 +81,7 @@ def main():
else:
app = QtGui.QApplication(sys.argv)
tagger_window = TaggerWindow( opts, settings )
tagger_window.show()
sys.exit(app.exec_())

View File

@ -32,10 +32,10 @@ from settingswindow import SettingsWindow
from settings import ComicTaggerSettings
import utils
# this reads the environment and inits the right locale
locale.setlocale(locale.LC_ALL, "")
import os
class TaggerWindow( QtGui.QMainWindow):
@ -555,12 +555,13 @@ class TaggerWindow( QtGui.QMainWindow):
issue_number = str(self.leIssueNum.text()).strip()
selector = VolumeSelectionWindow( self, self.settings.cv_api_key, series_name, issue_number, self.comic_archive )
selector = VolumeSelectionWindow( self, self.settings.cv_api_key, series_name, issue_number, self.comic_archive, self.settings )
selector.setModal(True)
selector.exec_()
if selector.result():
#we should now have a volume ID
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
comicVine = ComicVineTalker( self.settings.cv_api_key )
self.metadata = comicVine.fetchIssueData( selector.volume_id, selector.issue_number )
@ -568,14 +569,17 @@ class TaggerWindow( QtGui.QMainWindow):
# Now push the right data into the edit controls
self.metadataToForm()
#!!!ATB should I clear the form???
QtGui.QApplication.restoreOverrideCursor()
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."))

View File

@ -1,8 +1,16 @@
Windows Packaging:
Makefile or script to
CX freeze
other files
NSIS
Toolbar icons
Page Browser
Multi-match dialog
Stand-alone CLI
TaggerWindow entry fields
@ -27,15 +35,13 @@ Lots of error checking
Archive function to detect tag blocks out of sync
Hourglass popup, or whatever, for when busy
Idea: Support only CBI or CIX for any given file, and not both
If user selects different one, warn about potential loss/re-arranging of data
Longer term:
Think about mass tagging and (semi) automatic volume selection
Maybe: keep a history of tagged volumes IDs from CV, and present those first
Other settings possibilities:
Last tag style
@ -60,8 +66,6 @@ App option to covert RAR to ZIP
If no unrar in path, then filter out CBR/RAR from open dialog
"Select Issues" dialog request cover URLs in background
"Select Issues" dialog cache cover images
Wizard for converting between tag styles

View File

@ -19,32 +19,80 @@ limitations under the License.
"""
import sys
import time
import os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import QUrl
from PyQt4.QtCore import QObject
from PyQt4.QtCore import QUrl,pyqtSignal
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comicvinetalker import ComicVineTalker
from issueselectionwindow import IssueSelectionWindow
from issueidentifier import IssueIdentifier
from genericmetadata import GenericMetadata
from imagefetcher import ImageFetcher
from progresswindow import IDProgressWindow
class SearchThread( QtCore.QThread):
searchComplete = pyqtSignal()
progressUpdate = pyqtSignal(int, int)
def __init__(self, series_name, cv_api_key, refresh):
QtCore.QThread.__init__(self)
self.series_name = series_name
self.cv_api_key = cv_api_key
self.refresh = refresh
def run(self):
comicVine = ComicVineTalker( self.cv_api_key )
matches = self.cv_search_results = comicVine.searchForSeries( self.series_name, callback=self.prog_callback, refresh_cache=self.refresh )
self.searchComplete.emit()
def prog_callback(self, current, total):
self.progressUpdate.emit(current, total)
class IdentifyThread( QtCore.QThread):
identifyComplete = pyqtSignal( )
identifyLogMsg = pyqtSignal( str )
identifyProgress = pyqtSignal( int, int )
def __init__(self, identifier):
QtCore.QThread.__init__(self)
self.identifier = identifier
self.identifier.setOutputFunction( self.logOutput )
self.identifier.setProgressCallback( self.progressCallback )
def logOutput(self, text):
self.identifyLogMsg.emit( text )
def progressCallback(self, cur, total):
self.identifyProgress.emit( cur, total )
def run(self):
matches =self.identifier.search()
self.identifyComplete.emit( )
class VolumeSelectionWindow(QtGui.QDialog):
volume_id = 0
def __init__(self, parent, cv_api_key, series_name, issue_number, comic_archive):
def __init__(self, parent, cv_api_key, series_name, issue_number, comic_archive, settings):
super(VolumeSelectionWindow, self).__init__(parent)
uic.loadUi('volumeselectionwindow.ui', self)
self.settings = settings
self.series_name = series_name
self.issue_number = issue_number
self.cv_api_key = cv_api_key
self.volume_id = 0
self.comic_archive = comic_archive
self.performQuery()
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
@ -52,22 +100,58 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.btnIssues.clicked.connect(self.showIssues)
self.btnAutoSelect.clicked.connect(self.autoSelect)
self.show()
QtCore.QCoreApplication.processEvents()
self.performQuery()
self.twList.selectRow(0)
def requery( self ):
self.performQuery()
def requery( self, ):
self.performQuery( refresh=True )
self.twList.selectRow(0)
def autoSelect( self ):
ii = IssueIdentifier( self.comic_archive, self.cv_api_key )
self.iddialog = IDProgressWindow( self)
self.iddialog.setModal(True)
self.iddialog.rejected.connect( self.identifyCancel )
self.iddialog.show()
self.ii = IssueIdentifier( self.comic_archive, self.cv_api_key )
md = GenericMetadata()
md.series = self.series_name
md.issue_number = self.issue_number
ii.setAdditionalMetadata( md )
md.issueNumber = self.issue_number
self.ii.setAdditionalMetadata( md )
matches = ii.search()
self.id_thread = IdentifyThread( self.ii )
self.id_thread.identifyComplete.connect( self.identifyComplete )
self.id_thread.identifyLogMsg.connect( self.logIDOutput )
self.id_thread.identifyProgress.connect( self.identifyProgress )
self.id_thread.start()
self.iddialog.exec_()
def logIDOutput( self, text ):
print text,
self.iddialog.textEdit.ensureCursorVisible()
self.iddialog.textEdit.insertPlainText(text)
def identifyProgress( self, cur, total ):
self.iddialog.progressBar.setMaximum( total )
self.iddialog.progressBar.setValue( cur )
def identifyCancel( self ):
self.ii.cancel = True
def identifyComplete( self ):
matches = self.ii.match_list
if len(matches) == 1:
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']
@ -75,7 +159,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.showIssues()
def showIssues( self ):
selector = IssueSelectionWindow( self, self.cv_api_key, self.volume_id, self.issue_number )
selector = IssueSelectionWindow( self, self.settings, self.volume_id, self.issue_number )
selector.setModal(True)
selector.exec_()
if selector.result():
@ -91,20 +175,52 @@ class VolumeSelectionWindow(QtGui.QDialog):
self.twList.selectRow( r )
break
def performQuery( self ):
def performQuery( self, refresh=False ):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
comicVine = ComicVineTalker( self.cv_api_key )
self.cv_search_results = comicVine.searchForSeries( self.series_name )
self.progdialog = QtGui.QProgressDialog("Searching Online", "Cancel", 0, 100, self)
self.progdialog.setWindowTitle( "Online Search" )
self.progdialog.canceled.connect( self.searchCanceled )
self.progdialog.setModal(True)
self.search_thread = SearchThread( self.series_name, self.cv_api_key, refresh )
self.search_thread.searchComplete.connect( self.searchComplete )
self.search_thread.progressUpdate.connect( self.searchProgressUpdate )
self.search_thread.start()
QtCore.QCoreApplication.processEvents()
self.progdialog.exec_()
def searchCanceled( self ):
print "query cancelled"
self.search_thread.searchComplete.disconnect( self.searchComplete )
self.search_thread.progressUpdate.disconnect( self.searchProgressUpdate )
self.progdialog.canceled.disconnect( self.searchCanceled )
self.progdialog.reject()
QtCore.QTimer.singleShot(200, self.closeMe)
def closeMe( self ):
print "closeme"
self.reject()
def searchProgressUpdate( self , current, total ):
self.progdialog.setMaximum(total)
self.progdialog.setValue(current)
def searchComplete( self ):
self.progdialog.accept()
self.cv_search_results = self.search_thread.cv_search_results
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.UserRole ,record['id'])
@ -128,12 +244,13 @@ class VolumeSelectionWindow(QtGui.QDialog):
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
record['cover_image'] = None
row += 1
self.twList.resizeColumnsToContents()
self.twList.setSortingEnabled(True)
self.twList.sortItems( 2 , QtCore.Qt.DescendingOrder )
self.twList.selectRow(0)
self.twList.resizeColumnsToContents()
def cellDoubleClicked( self, r, c ):
@ -146,7 +263,6 @@ class VolumeSelectionWindow(QtGui.QDialog):
if prev is not None and prev.row() == curr.row():
return
self.volume_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
# list selection was changed, update the info on the volume
@ -154,30 +270,21 @@ class VolumeSelectionWindow(QtGui.QDialog):
if record['id'] == self.volume_id:
self.teDetails.setText ( record['description'] )
if record['cover_image'] == None:
url = record['image']['super_url']
self.labelThumbnail.setText("loading...")
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.finishRequest)
self.nam.get(QNetworkRequest(QUrl(url)))
self.pending_cover_record = record
else:
self.setCover(record['cover_image'])
# called when the image is done loading
def finishRequest(self, reply):
self.labelThumbnail.setPixmap(QtGui.QPixmap(os.getcwd() + "/nocover.png"))
url = record['image']['super_url']
self.fetcher = ImageFetcher( )
self.fetcher.fetchComplete.connect(self.finishRequest)
self.fetcher.fetch( url, user_data=record['id'] )
def finishRequest(self, image_data, user_data):
# called when the image is done loading
img = QtGui.QImage()
img.loadFromData(reply.readAll())
self.pending_cover_record['cover_image'] = img
self.pending_cover_record = None
self.setCover( img )
img.loadFromData( image_data )
self.setCover( img )
def setCover( self, img ):
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>480</height>
<width>796</width>
<height>454</height>
</rect>
</property>
<property name="windowTitle">
@ -16,172 +16,174 @@
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<widget class="QLabel" name="labelThumbnail">
<property name="geometry">
<rect>
<x>20</x>
<y>30</y>
<width>241</width>
<height>391</height>
</rect>
</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>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>272</x>
<y>430</y>
<width>611</width>
<height>32</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="btnAutoSelect">
<property name="text">
<string>Auto-Select</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnRequery">
<property name="text">
<string>Re-Query</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnIssues">
<property name="text">
<string>Show Issues</string>
</property>
</widget>
</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>
</widget>
<widget class="QSplitter" name="splitter">
<property name="geometry">
<rect>
<x>270</x>
<y>33</y>
<width>611</width>
<height>391</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>250</height>
</size>
</property>
<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>4</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>Year</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Issues</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Publisher</string>
</property>
</column>
</widget>
<widget class="QTextEdit" name="teDetails">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>200</height>
</size>
</property>
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</widget>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="labelThumbnail">
<property name="minimumSize">
<size>
<width>300</width>
<height>450</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>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QTableWidget" name="twList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>250</height>
</size>
</property>
<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>4</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>Year</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Issues</string>
</property>
<property name="textAlignment">
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Publisher</string>
</property>
</column>
</widget>
<widget class="QTextEdit" name="teDetails">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>200</height>
</size>
</property>
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="btnAutoSelect">
<property name="text">
<string>Auto-Select</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnRequery">
<property name="text">
<string>Re-Query</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnIssues">
<property name="text">
<string>Show Issues</string>
</property>
</widget>
</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>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>