A slew of changes
git-svn-id: http://comictagger.googlecode.com/svn/trunk@29 6c5673fe-1810-88d6-992b-cd32ca31540c
This commit is contained in:
parent
a58442b8f6
commit
e1f3397960
@ -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' )
|
||||
|
@ -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
171
imagefetcher.py
Normal 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
|
||||
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
BIN
nocover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
37
progresswindow.py
Normal file
37
progresswindow.py
Normal 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
84
progresswindow.ui
Normal 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
106
settings.py
Normal 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
144
settingswindow.py
Normal 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
241
settingswindow.ui
Normal 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><html><head/><body><p>To perform online searches using ComicVine, you must first register and request an API key. You can start here: <a href="http://api.comicvine.com/"><span style=" text-decoration: underline; color:#0000ff;">http://api.comicvine.com/</span></a></p></body></html></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><html><head/><body><p>In order to read/write to CBR/RAR archives, you will need to have the shareware tools from <a href="www.win-rar.com/download.html"><span style=" text-decoration: underline; color:#0000ff;">WinRAR</span></a> installed. </p></body></html></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>
|
@ -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_())
|
||||
|
@ -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."))
|
||||
|
||||
|
14
todo.txt
14
todo.txt
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user