Initial checking

git-svn-id: http://comictagger.googlecode.com/svn/trunk@2 6c5673fe-1810-88d6-992b-cd32ca31540c
This commit is contained in:
beville@gmail.com 2012-11-02 20:54:17 +00:00
parent 2f21c2f4d2
commit f642b46742
16 changed files with 3828 additions and 0 deletions

284
comicarchive.py Normal file
View File

@ -0,0 +1,284 @@
"""
A python class to represent a single comic, be it file or folder of images
"""
import zipfile
import os
import struct
from options import Options, MetaDataStyle
from comicinfoxml import ComicInfoXml
from comicbookinfo import ComicBookInfo
from genericmetadata import GenericMetadata
from filenameparser import FileNameParser
# This is a custom function for writing a comment to a zip file,
# since the built-in one doesn't seem to work on Windows and Mac OS/X
# Fortunately, the zip comment is at the end of the file, and it's
# easy to manipulate. See this website for more info:
# see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure
def writeZipComment( filename, comment ):
#get file size
statinfo = os.stat(filename)
file_length = statinfo.st_size
fo = open(filename, "r+b")
#the starting position, relative to EOF
pos = -4
found = False
value = bytearray()
# walk backwards to find the "End of Central Directory" record
while ( not found ) and ( -pos != file_length ):
# seek, relative to EOF
fo.seek( pos, 2)
value = fo.read( 4 )
#look for the end of central directory signature
if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]):
found = True
else:
# not found, step back another byte
pos = pos - 1
#print pos,"{1} int: {0:x}".format(bytearray(value)[0], value)
if found:
# now skip forward 20 bytes to the comment length word
pos += 20
fo.seek( pos, 2)
# Pack the length of the comment string
format = "H" # one 2-byte integer
comment_length = struct.pack(format, len(comment)) # pack integer in a binary string
# write out the length
fo.write( comment_length )
fo.seek( pos+2, 2)
# write out the comment itself
fo.write( comment )
fo.truncate()
fo.close()
else:
raise Exception('Failed to write comment to zip file!')
#------------------------------------------
class ComicArchive:
def __init__( self, path ):
self.path = path
self.ci_xml_filename = 'ComicInfo.xml'
def isZip( self ):
return zipfile.is_zipfile( self.path )
def isFolder( self ):
return False
def isNonWritableArchive( self ):
# TODO check for rar, maybe others
# also check permissions
return False
def seemsToBeAComicArchive( self ):
# TODO this will need to be fleshed out to support RAR and Folder
ext = os.path.splitext(self.path)[1].lower()
if (
( self.isZip() ) and
( ext in [ '.zip', '.cbz' ] ) and
( self.getNumberOfPages() > 3)
):
return True
else:
return False
def readMetadata( self, style ):
if style == MetaDataStyle.CIX:
return self.readCIX()
elif style == MetaDataStyle.CBI:
return self.readCBI()
else:
return GenericMetadata()
def writeMetadata( self, metadata, style ):
if style == MetaDataStyle.CIX:
self.writeCIX( metadata )
elif style == MetaDataStyle.CBI:
self.writeCBI( metadata )
def hasMetadata( self, syle ):
if style == MetaDataStyle.CIX:
return self.hasCIX()
elif style == MetaDataStyle.CBI:
return self.hasCBI()
else:
return False
def clearMetadata( self, style ):
return
def getCoverPage(self):
if self.getNumberOfPages() == 0:
return None
zf = zipfile.ZipFile (self.path, 'r')
# get the list file names in the archive, and sort
files = zf.namelist()
files.sort()
# find the first image file, assume it's the cover
for name in files:
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
break
image_data = zf.read( name )
zf.close()
return image_data
def getNumberOfPages(self):
count = 0
if self.isZip():
zf = zipfile.ZipFile (self.path, 'r')
for item in zf.infolist():
if ( item.filename[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
count += 1
zf.close()
return count
def readCBI( self ):
if ( not self.hasCBI() ):
print self.path, " isn't a zip or doesn't has CBI data!"
return GenericMetadata()
zf = zipfile.ZipFile( self.path, "r" )
cbi_string = zf.comment
zf.close()
metadata = ComicBookInfo().metadataFromString( cbi_string )
return metadata
def writeCBI( self, metadata ):
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
writeZipComment( self.path, cbi_string )
def readCIX( self ):
# !!!ATB TODO add support for folders
if (not self.isZip()) or ( not self.hasCIX()):
print self.path, " isn't a zip or doesn't has ComicInfo.xml data!"
return GenericMetadata()
zf = zipfile.ZipFile( self.path, 'r' )
cix_string = zf.read( self.ci_xml_filename )
zf.close()
metadata = ComicInfoXml().metadataFromString( cix_string )
return metadata
def writeCIX(self, metadata):
# !!!ATB TODO add support for folders
if (not self.isZip()):
print self.path, "isn't a zip archive!"
return
cix_string = ComicInfoXml().stringFromMetadata( metadata )
# check if an XML file already exists in archive
if not self.hasCIX():
#simple case: just add the new archive file
zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED )
zf.writestr( self.ci_xml_filename, cix_string )
zf.close()
else:
# If we need to replace it, well, at the moment, no other option
# but to rebuild the whole zip again.
# very sucky, but maybe another solution can be found
print "{0} already exists in {1}. Rebuilding it...".format( self.ci_xml_filename, self.path)
zin = zipfile.ZipFile (self.path, 'r')
zout = zipfile.ZipFile ('tmpnew.zip', 'w')
for item in zin.infolist():
buffer = zin.read(item.filename)
if ( item.filename != self.ci_xml_filename ):
zout.writestr(item, buffer)
# now write out the new xml file
zout.writestr( self.ci_xml_filename, cix_string )
#preserve the old comment
zout.comment = zin.comment
zout.close()
zin.close()
# replace with the new file
os.remove( self.path )
os.rename( 'tmpnew.zip', self.path )
def hasCIX(self):
has = False
zf = zipfile.ZipFile( self.path, 'r' )
if self.ci_xml_filename in zf.namelist():
has = True
zf.close()
return has
def hasCBI(self):
if (not self.isZip() ):
return False
zf = zipfile.ZipFile( self.path, 'r' )
comment = zf.comment
zf.close()
return ComicBookInfo().validateString( comment )
def metadataFromFilename( self ):
metadata = GenericMetadata()
fnp = FileNameParser()
fnp.parseFilename( self.path )
if fnp.issue != "":
metadata.issueNumber = fnp.issue
if fnp.series != "":
metadata.series = fnp.series
if fnp.volume != "":
metadata.volumeNumber = fnp.volume
if fnp.year != "":
metadata.publicationYear = fnp.year
metadata.isEmpty = False
return metadata

118
comicbookinfo.py Normal file
View File

@ -0,0 +1,118 @@
"""
A python class to encapsulate the ComicBookInfo data and file handling
"""
import json
from datetime import datetime
import zipfile
from genericmetadata import GenericMetadata
import utils
class ComicBookInfo:
def metadataFromString( self, string ):
cbi_container = json.loads( unicode(string, 'utf-8') )
metadata = GenericMetadata()
cbi = cbi_container[ 'ComicBookInfo/1.0' ]
#helper func
# If item is not in CBI, return None
def xlate( cbi_entry):
if cbi_entry in cbi:
return cbi[cbi_entry]
else:
return None
metadata.series = xlate( 'series' )
metadata.title = xlate( 'title' )
metadata.issueNumber = xlate( 'issue' )
metadata.publisher = xlate( 'publisher' )
metadata.publicationMonth = xlate( 'publicationMonth' )
metadata.publicationYear = xlate( 'publicationYear' )
metadata.issueCount = xlate( 'numberOfIssues' )
metadata.comments = xlate( 'comments' )
metadata.credits = xlate( 'credits' )
metadata.genre = xlate( 'genre' )
metadata.volumeNumber = xlate( 'volume' )
metadata.volumeCount = xlate( 'numberOfVolumes' )
metadata.language = xlate( 'language' )
metadata.country = xlate( 'country' )
metadata.criticalRating = xlate( 'rating' )
metadata.tags = xlate( 'tags' )
#need to massage the language string to be ISO
if metadata.language is not None:
# reverse look-up
pattern = metadata.language
metadata.language = None
for key in utils.getLanguageDict():
if utils.getLanguageDict()[ key ] == pattern.encode('utf-8'):
metadata.language = key
break
metadata.isEmpty = False
return metadata
def stringFromMetadata( self, metadata ):
cbi_container = self.createJSONDictionary( metadata )
return json.dumps( cbi_container )
#verify that the string actually contains CBI data in JSON format
def validateString( self, string ):
try:
cbi_container = json.loads( string )
except:
return False
return ( 'ComicBookInfo/1.0' in cbi_container )
def createJSONDictionary( self, metadata ):
# Create the dictionary that we will convert to JSON text
cbi = dict()
cbi_container = {'appID' : 'ComicTagger/0.1',
'lastModified' : str(datetime.now()),
'ComicBookInfo/1.0' : cbi }
#helper func
def assign( cbi_entry, md_entry):
if md_entry is not None:
cbi[cbi_entry] = md_entry
assign( 'series', metadata.series )
assign( 'title', metadata.title )
assign( 'issue', metadata.issueNumber )
assign( 'publisher', metadata.publisher )
assign( 'publicationMonth', metadata.publicationMonth )
assign( 'publicationYear', metadata.publicationYear )
assign( 'numberOfIssues', metadata.issueCount )
assign( 'comments', metadata.comments )
assign( 'genre', metadata.genre )
assign( 'volume', metadata.volumeNumber )
assign( 'numberOfVolumes', metadata.volumeCount )
assign( 'language', utils.getLanguageFromISO(metadata.language) )
assign( 'country', metadata.country )
assign( 'rating', metadata.criticalRating )
assign( 'credits', metadata.credits )
assign( 'tags', metadata.tags )
return cbi_container
def writeToExternalFile( self, filename, metadata ):
cbi_container = self.createJSONDictionary(metadata)
f = open(filename, 'w')
f.write(json.dumps(cbi_container, indent=4))
f.close

248
comicinfoxml.py Normal file
View File

@ -0,0 +1,248 @@
"""
A python class to encapsulate ComicRack's ComicInfo.xml data and file handling
"""
from datetime import datetime
import zipfile
from pprint import pprint
import xml.etree.ElementTree as ET
from genericmetadata import GenericMetadata
class ComicInfoXml:
def metadataFromString( self, string ):
tree = ET.ElementTree(ET.fromstring( string ))
return self.convertXMLToMetadata( tree )
def stringFromMetadata( self, metadata ):
tree = self.convertMetadataToXML( self, metadata )
return ET.tostring(tree.getroot())
def indent( self, elem, level=0 ):
# for making the XML output readable
i = "\n" + level*" "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self.indent( elem, level+1 )
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def convertMetadataToXML( self, filename, metadata ):
#shorthand for the metadata
md = metadata
# build a tree structure
root = ET.Element("ComicInfo")
#helper func
def assign( cix_entry, md_entry):
if md_entry is not None:
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
assign( 'Series', md.series )
assign( 'Number', md.issueNumber )
assign( 'Title', md.title )
assign( 'Count', md.issueCount )
assign( 'Volume', md.volumeNumber )
assign( 'AlternateSeries', md.alternateSeries )
assign( 'AlternateNumber', md.alternateNumber )
assign( 'AlternateCount', md.alternateCount )
assign( 'Summary', md.comments )
assign( 'Notes', md.notes )
assign( 'Year', md.publicationYear )
assign( 'Month', md.publicationMonth )
assign( 'Publisher', md.publisher )
assign( 'Imprint', md.imprint )
assign( 'Genre', md.genre )
assign( 'Web', md.webLink )
assign( 'PageCount', md.pageCount )
assign( 'Format', md.format )
assign( 'LanguageISO', md.language )
assign( 'Manga', md.manga )
assign( 'Characters', md.characters )
assign( 'Teams', md.teams )
assign( 'Locations', md.locations )
assign( 'ScanInformation', md.scanInfo )
assign( 'StoryArc', md.storyArc )
assign( 'SeriesGroup', md.seriesGroup )
assign( 'AgeRating', md.maturityRating )
if md.blackAndWhite is not None and md.blackAndWhite:
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
# need to specially process the credits, since they are structured differently than CIX
credit_writer = None
credit_penciller = None
credit_inker = None
credit_colorist = None
credit_letterer = None
credit_cover = None
credit_editor = None
for credit in metadata.credits:
if credit['role'].title() in set( ['Writer', 'Plotter'] ):
if credit_writer == None:
credit_writer = ET.SubElement(root, 'Writer')
credit_writer.text = ""
if len(credit_writer.text) > 0:
credit_writer.text += ", "
credit_writer.text += credit['person']
if credit['role'].title() in set( [ 'Inker', 'Artist', 'Finishes' ] ):
if credit_inker == None:
credit_inker = ET.SubElement(root, 'Inker')
credit_inker.text = ""
if len(credit_inker.text) > 0:
credit_inker.text += ", "
credit_inker.text += credit['person']
if credit['role'].title() in set( [ 'Artist', 'Penciller', 'Penciler', 'Breakdowns' ] ):
if credit_penciller == None:
credit_penciller = ET.SubElement(root, 'Penciller')
credit_penciller.text = ""
if len(credit_penciller.text) > 0:
credit_penciller.text += ", "
credit_penciller.text += credit['person']
if credit['role'].title() in set( [ 'Colorist', 'Colourist' ]):
if credit_colorist == None:
credit_colorist = ET.SubElement(root, 'Colorist')
credit_colorist.text = ""
if len(credit_colorist.text) > 0:
credit_colorist.text += ", "
credit_colorist.text += credit['person']
if credit['role'].title() == 'Letterer':
if credit_letterer == None:
credit_letterer = ET.SubElement(root, 'Letterer')
credit_letterer.text = ""
if len(credit_letterer.text) > 0:
credit_letterer.text += ", "
credit_letterer.text += credit['person']
if credit['role'].title() in set( [ 'Cover', 'Covers', 'CoverArtist', 'Cover Artist' ] ):
if credit_cover == None:
credit_cover = ET.SubElement(root, 'CoverArtist')
credit_cover.text = ""
if len(credit_cover.text) > 0:
credit_cover.text += ", "
credit_cover.text += credit['person']
if credit['role'].title() in set( [ 'Editor'] ):
if credit_editor == None:
credit_editor = ET.SubElement(root, 'Editor')
credit_editor.text = ""
if len(credit_editor.text) > 0:
credit_editor.text += ", "
credit_editor.text += credit['person']
# !!!ATB todo: loop and add the page entries under pages node
#pages = ET.SubElement(root, 'Pages')
# self pretty-print
self.indent(root)
# wrap it in an ElementTree instance, and save as XML
tree = ET.ElementTree(root)
return tree
def convertXMLToMetadata( self, tree ):
root = tree.getroot()
if root.tag != 'ComicInfo':
raise 1
return None
metadata = GenericMetadata()
md = metadata
# Helper function
def xlate( tag ):
node = root.find( tag )
if node is not None:
return node.text
else:
return None
md.series = xlate( 'Series' )
md.title = xlate( 'Title' )
md.issueNumber = xlate( 'Number' )
md.issueCount = xlate( 'Count' )
md.volumeNumber = xlate( 'Volume' )
md.alternateSeries = xlate( 'AlternateSeries' )
md.alternateNumber = xlate( 'AlternateNumber' )
md.alternateCount = xlate( 'AlternateCount' )
md.comments = xlate( 'Summary' )
md.notes = xlate( 'Notes' )
md.publicationYear = xlate( 'Year' )
md.publicationMonth = xlate( 'Month' )
md.publisher = xlate( 'Publisher' )
md.imprint = xlate( 'Imprint' )
md.genre = xlate( 'Genre' )
md.webLink = xlate( 'Web' )
md.language = xlate( 'LanguageISO' )
md.format = xlate( 'Format' )
md.manga = xlate( 'Manga' )
md.characters = xlate( 'Characters' )
md.teams = xlate( 'Teams' )
md.locations = xlate( 'Locations' )
md.pageCount = xlate( 'PageCount' )
md.scanInfo = xlate( 'ScanInformation' )
md.storyArc = xlate( 'StoryArc' )
md.seriesGroup = xlate( 'SeriesGroup' )
md.maturityRating = xlate( 'AgeRating' )
tmp = xlate( 'BlackAndWhite' )
md.blackAndWhite = False
if tmp is not None and tmp.lower() in [ "yes", "true", "1" ]:
md.blackAndWhite = True
# Now extract the credit info
for n in root:
if ( n.tag == 'Writer' or
n.tag == 'Penciller' or
n.tag == 'Inker' or
n.tag == 'Colorist' or
n.tag == 'Letterer' or
n.tag == 'Editor'
):
for name in n.text.split(','):
metadata.addCredit( name.strip(), n.tag )
if n.tag == 'CoverArtist':
for name in n.text.split(','):
metadata.addCredit( name.strip(), "Cover" )
#!!! ATB parse page data now
metadata.isEmpty = False
return metadata
def writeToExternalFile( self, filename, metadata ):
tree = self.convertMetadataToXML( self, metadata )
#ET.dump(tree)
tree.write(filename, encoding='utf-8')
def readFromExternalFile( self, filename ):
tree = ET.parse( filename )
return self.convertXMLToMetadata( tree )

186
comicvinetalker.py Normal file
View File

@ -0,0 +1,186 @@
import json
from pprint import pprint
import urllib2, urllib
import math
import re
import utils
from genericmetadata import GenericMetadata
class ComicVineTalker:
api_key = '67ac5baeba16a2f56acf7ff136a1d591324c2253'
def searchForSeries( self, series_name ):
series_name = urllib.quote_plus(str(series_name))
search_url = "http://api.comicvine.com/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + series_name + "&field_list=name,id,start_year,publisher,image,description,count_of_issues&sort=start_year"
resp = urllib2.urlopen(search_url)
content = resp.read()
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
search_results = list()
# see http://api.comicvine.com/documentation/#handling_responses
limit = cv_response['limit']
current_result_count = cv_response['number_of_page_results']
total_result_count = cv_response['number_of_total_results']
print ("Found {0} of {1} results".format( cv_response['number_of_page_results'], cv_response['number_of_total_results']))
search_results.extend( cv_response['results'])
offset = 0
# see if we need to keep asking for more pages...
while ( current_result_count < total_result_count ):
print ("getting another page of results...")
offset += limit
resp = urllib2.urlopen( search_url + "&offset="+str(offset) )
content = resp.read()
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
search_results.extend( cv_response['results'])
current_result_count += cv_response['number_of_page_results']
#for record in search_results:
# print( "{0}: {1} ({2})".format(record['id'], smart_str(record['name']) , record['start_year'] ) )
# print( "{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year'] ) )
#print "{0}: {1} ({2})".format(search_results['results'][0]['id'], smart_str(search_results['results'][0]['name']) , search_results['results'][0]['start_year'] )
return search_results
def fetchVolumeData( self, series_id ):
volume_url = "http://api.comicvine.com/volume/" + str(series_id) + "/?api_key=" + self.api_key + "&format=json"
#print "search_url = : ", volume_url
resp = urllib2.urlopen(volume_url)
content = resp.read()
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
volume_results = cv_response['results']
return volume_results
def fetchIssueData( self, series_id, issue_number ):
volume_results = self.fetchVolumeData( series_id )
found = False
for record in volume_results['issues']:
if float(record['issue_number']) == float(issue_number):
found = True
break
if (found):
issue_url = "http://api.comicvine.com/issue/" + str(record['id']) + "/?api_key=" + self.api_key + "&format=json"
resp = urllib2.urlopen(issue_url)
content = resp.read()
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
issue_results = cv_response['results']
else:
return None
# now, map the comicvine data to generic metadata
metadata = GenericMetadata()
metadata.series = issue_results['volume']['name']
# format the issue number string nicely, since it's usually something like "2.00"
num_f = float(issue_results['issue_number'])
num_s = str( int(math.floor(num_f)) )
if math.floor(num_f) != num_f:
num_s = str( num_f )
metadata.issueNumber = num_s
metadata.title = issue_results['name']
metadata.publisher = volume_results['publisher']['name']
metadata.publicationMonth = issue_results['publish_month']
metadata.publicationYear = issue_results['publish_year']
metadata.issueCount = volume_results['count_of_issues']
metadata.comments = self.cleanup_html(issue_results['description'])
metadata.notes = "Tagged with ComicTagger using info from Comic Vine:\n"
metadata.notes += issue_results['site_detail_url']
metadata.webLink = issue_results['site_detail_url']
person_credits = issue_results['person_credits']
for person in person_credits:
for role in person['roles']:
# can we determine 'primary' from CV??
role_name = role['role'].title()
metadata.addCredit( person['name'], role['role'].title(), False )
character_credits = issue_results['character_credits']
character_list = list()
for character in character_credits:
character_list.append( character['name'] )
metadata.characters = utils.listToString( character_list )
team_credits = issue_results['team_credits']
team_list = list()
for team in team_credits:
team_list.append( team['name'] )
metadata.teams = utils.listToString( team_list )
location_credits = issue_results['location_credits']
location_list = list()
for location in location_credits:
location_list.append( location['name'] )
metadata.locations = utils.listToString( location_list )
story_arc_credits = issue_results['story_arc_credits']
for arc in story_arc_credits:
metadata.storyArc = arc['name']
#just use the first one, if at all
break
return metadata
def cleanup_html( self, string):
p = re.compile(r'<[^<]*?>')
newstring = p.sub('',string)
newstring = newstring.replace('&nbsp;',' ')
newstring = newstring.replace('&amp;','&')
return newstring
def fetchIssueCoverURL( self, issue_id ):
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json&field_list=image"
resp = urllib2.urlopen(issue_url)
content = resp.read()
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
return cv_response['results']['image']['super_url']

124
filenameparser.py Normal file
View File

@ -0,0 +1,124 @@
#!/usr/bin/python
# This is horrible, and needs to be re-written, but, well, it mostly works!
import re
import os
from urllib import unquote
class FileNameParser:
def fixSpaces( self, string ):
placeholders = ['[-_]',' +']
for ph in placeholders:
string = re.sub(ph, ' ', string )
return string.strip()
# check for silly .1 or .5 style issue strings
# allow up to 5 chars total
def isPointIssue( self, word ):
ret = False
try:
float(word)
if (len(word) < 5 and not word.isdigit()):
ret = True
except ValueError:
pass
return ret
def getIssueNumber( self,filename ):
found = False
issue = ''
# guess based on position
# replace any name seperators with spaces
tmpstr = self.fixSpaces(filename)
word_list = tmpstr.split(' ')
# assume the last number in the filename that is under 4 digits is the issue number
for word in reversed(word_list):
if (
(word.isdigit() and len(word) < 4) or
(self.isPointIssue(word))
):
issue = word
found = True
#print 'Assuming issue number is ' + str(issue) + ' based on the position.'
break
if not found:
# try a regex
issnum = re.search('(?<=[_#\s-])(\d+[a-zA-Z]|\d+\.\d|\d+)', filename)
if issnum:
issue = issnum.group()
found = True
#print 'Got the issue using regex. Issue is ' + issue
return issue.strip()
def getSeriesName(self, filename, issue ):
# use the issue number string to split the filename string
# assume first element of list is the series name, plus cruft
#!!! this could fail in the case of small numerics in the series name!!!
tmpstr = self.fixSpaces(filename)
if issue != "":
series = tmpstr.split(issue)[0]
else:
# no issue to work off of
#!!! TODO we should look for the year, and split from that
# and if that doesn't exist, remove parenthetical words
series = tmpstr
volume = ""
series = series.rstrip("#")
# search for volume number
match = re.search('(?<=v)(\d+)\s*$', series)
if match:
volume = match.group()
series = series.split("v"+volume)[0]
volume = volume.lstrip("0")
return series.strip(), volume.strip()
def getYear( self,filename):
year = ""
# look for four digit number with "(" ")" or "--" around it
match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename)
if match:
year = match.group()
# remove non-numerics
year = re.sub("[^0-9]", "", year)
return year
self.issue = ""
self.series = ""
self.volume = ""
self.year = ""
def parseFilename( self, filename ):
# remove the path
filename = os.path.basename(filename)
# remove the extension
filename = os.path.splitext(filename)[0]
#url decvocde, just in case
filename = unquote(filename)
self.issue = self.getIssueNumber(filename)
self.series, self.volume = self.getSeriesName(filename, self.issue)
self.year = self.getYear(filename)
if self.issue != "":
# strip off leading zeros
self.issue = self.issue.lstrip("0")
if self.issue == "":
self.issue = "0"

96
genericmetadata.py Normal file
View File

@ -0,0 +1,96 @@
"""
A python class for internal metadata storage
The goal of this class is to handle ALL the data that might come from various
tagging schemes and databases, such as ComicVine or GCD. This makes conversion
possible, however lossy it might be
"""
from sets import Set
# These page info classes are exactly the same as the CIX scheme, since it's unique
class PageType:
FrontCover = "FrontCover"
InnerCover = "InnerCover"
Roundup = "Roundup"
Story = "Story"
Advertisment = "Advertisment"
Story = "Story"
Editorial = "Editorial"
Letters = "Letters"
Preview = "Preview"
BackCover = "BackCover"
Other = "Other"
Deleted = "Deleted"
class PageInfo:
SeqNum = 0
Type = PageType.FrontCover
DoublePage = False
ImageSize = 0
Key = ""
ImageWidth = 0
ImageHeight = 0
class GenericMetadata:
def __init__(self):
self.isEmpty = True
self.tagOrigin = None
self.series = None
self.issueNumber = None
self.title = None
self.publisher = None
self.publicationMonth = None
self.publicationYear = None
self.issueCount = None
self.volumeNumber = None
self.genre = None
self.language = None # 2 letter iso code
self.comments = None # use same way as Summary in CIX
self.volumeCount = None
self.criticalRating = None
self.country = None
self.alternateSeries = None
self.alternateNumber = None
self.alternateCount = None
self.imprint = None
self.notes = None
self.webLink = None
self.format = None
self.manga = None
self.blackAndWhite = None
self.pageCount = None
self.maturityRating = None
self.storyArc = None
self.seriesGroup = None
self.scanInfo = None
self.characters = None
self.teams = None
self.locations = None
self.credits = list()
self.tags = list()
self.pages = list()
def addCredit( self, person, role, primary = False ):
credit = dict()
credit['person'] = person
credit['role'] = role
if primary:
credit['primary'] = primary
self.credits.append(credit)

118
issueselectionwindow.py Normal file
View File

@ -0,0 +1,118 @@
import sys
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import QUrl
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comicvinetalker import *
class IssueSelectionWindow(QtGui.QDialog):
volume_id = 0
def __init__(self, parent, series_id, issue_number):
super(IssueSelectionWindow, self).__init__(parent)
uic.loadUi('issueselectionwindow.ui', self)
self.series_id = series_id
if issue_number is None or issue_number == "":
self.issue_number = 1
else:
self.issue_number = issue_number
self.initial_id = None
self.performQuery()
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
#now that the list has been sorted, find the initial record, and select it
if self.initial_id is None:
self.twList.selectRow( 0 )
else:
for r in range(0, self.twList.rowCount()):
issue_id, b = self.twList.item( r, 0 ).data( QtCore.Qt.UserRole ).toInt()
if (issue_id == self.initial_id):
self.twList.selectRow( r )
break
def performQuery( self ):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
comicVine = ComicVineTalker()
volume_data = comicVine.fetchVolumeData( self.series_id )
self.issue_list = volume_data['issues']
self.twList.setSortingEnabled(False)
row = 0
for record in self.issue_list:
self.twList.insertRow(row)
item_text = record['issue_number']
item = QtGui.QTableWidgetItem(item_text)
item.setData( QtCore.Qt.UserRole ,record['id'])
item.setData(QtCore.Qt.DisplayRole, float(item_text))
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = u"{0}".format(record['name'])
item = QtGui.QTableWidgetItem(item_text)
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']
row += 1
#TODO look for given issue in list, and select that one
self.twList.setSortingEnabled(True)
self.twList.sortItems( 0 , QtCore.Qt.AscendingOrder )
def cellDoubleClicked( self, r, c ):
self.accept()
def currentItemChanged( self, curr, prev ):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.issue_id, b = self.twList.item( curr.row(), 0 ).data( QtCore.Qt.UserRole ).toInt()
# list selection was changed, update the the issue cover
for record in self.issue_list:
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'] = ComicVineTalker().fetchIssueCoverURL( self.issue_id )
self.labelThumbnail.setText("loading...")
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.finishedImageRequest)
self.nam.get(QNetworkRequest(QUrl(record['url'])))
break
# called when the image is done loading
def finishedImageRequest(self, reply):
img = QtGui.QImage()
img.loadFromData(reply.readAll())
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))

147
issueselectionwindow.ui Normal file
View File

@ -0,0 +1,147 @@
<?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>843</width>
<height>454</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>
</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>

65
options.py Normal file
View File

@ -0,0 +1,65 @@
import sys
import getopt
class Enum(set):
def __getattr__(self, name):
if name in self:
return name
raise AttributeError
class MetaDataStyle:
CBI = 0
CIX = 1
class Options:
def __init__(self):
self.data_style = MetaDataStyle.CBI
self.no_gui = False
# Some defaults for testing
self.series_name = '' #'Watchmen'
self.issue_number = '' #'1'
self.filename = '' # "Watchmen #01.cbz"
def parseCmdLineArgs(self):
# parse command line options
try:
opts, args = getopt.getopt(sys.argv[1:], "cht:s:i:vf:", ["cli", "help", "type=", "series=", "issue=", "verbose", "file" ])
except (getopt.error, msg):
print( msg )
print( "for help use --help" )
sys.exit(2)
# process options
for o, a in opts:
if o in ("-h", "--help"):
print( __doc__ )
sys.exit(0)
if o in ("-v", "--verbose"):
print( "Verbose output!" )
if o in ("-c", "--cli"):
self.no_gui = True
if o in ("-s", "--series"):
self.series_name = a
if o in ("-i", "--issue"):
self.issue_number = a
if o in ("-f", "--file"):
self.filename = a
if o in ("-t", "--type"):
if a == "cr":
self.data_style = MetaDataStyle.CIX
elif a == "cbl":
self.data_style = MetaDataStyle.CBI
else:
print( __doc__ )
sys.exit(0)
# process arguments
for arg in args:
process(arg) # process() is defined elsewhere
return opts

88
tagger.py Executable file
View File

@ -0,0 +1,88 @@
#!/usr/bin/python
"""
A python script to tag CBZ files
"""
import sys
import getopt
import json
import xml
from pprint import pprint
from PyQt4 import QtCore, QtGui
import signal
from taggerwindow import TaggerWindow
from options import Options, MetaDataStyle
from comicarchive import ComicArchive
from comicvinetalker import ComicVineTalker
from comicinfoxml import ComicInfoXml
from comicbookinfo import ComicBookInfo
#-----------------------------
def cliProcedure( opts ):
comicVine = ComicVineTalker()
cv_search_results = comicVine.searchForSeries( opts.series_name )
#error checking here: did we get any results?
# we will eventualy want user interaction to choose the appropriate result, but for now, assume the first one
series_id = cv_search_results[0]['id']
print( "-->Auto-selecting volume ID:", cv_search_results[0]['id'] )
print(" ")
# now get the particular issue data
metadata = comicVine.fetchIssueData( series_id, opts.issue_number )
#pprint( cv_volume_data, indent=4 )
ca = ComicArchive(opts.filename)
ca.writeMetadata( metadata, opts.data_style )
#debugging
ComicBookInfo().writeToExternalFile( "test.json" )
ComicBookInfo().writeToExternalFile( "test.xml" )
#-----------------------------
def main():
opts = Options()
opts.parseCmdLineArgs()
signal.signal(signal.SIGINT, signal.SIG_DFL)
#ca = ComicArchive( opts.filename )
#metadata = ca.readMetadata( MetaDataStyle.CBI )
#ca.writeMetadata( metadata, MetaDataStyle.CIX )
#ComicInfoXml().writeToExternalFile( "test.xml", metadata )
#ComicBookInfo().writeToExternalFile("test.json", metadata)
#quit()
if opts.no_gui:
cliProcedure( opts )
else:
app = QtGui.QApplication(sys.argv)
tagger_window = TaggerWindow( opts )
tagger_window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

480
taggerwindow.py Normal file
View File

@ -0,0 +1,480 @@
from PyQt4 import QtCore, QtGui, uic
from volumeselectionwindow import VolumeSelectionWindow
from options import Options, MetaDataStyle
from genericmetadata import GenericMetadata
from comicvinetalker import ComicVineTalker
from comicarchive import ComicArchive
import utils
import locale
# this reads the environment and inits the right locale
locale.setlocale(locale.LC_ALL, "")
import os
class TaggerWindow( QtGui.QMainWindow):
appName = "ComicTagger"
def __init__(self, opts , parent = None):
super(TaggerWindow, self).__init__(parent)
uic.loadUi('taggerwindow.ui', self)
self.setWindowIcon(QtGui.QIcon('app.png'))
self.center()
self.raise_()
self.opts = opts
self.data_style = opts.data_style
#set up a default metadata object
self.metadata = GenericMetadata()
self.comic_archive = None
self.configMenus()
self.statusBar()
self.updateAppTitle()
self.setAcceptDrops(True)
self.droppedFile=None
self.populateComboBoxes()
# hook up the callbacks
self.cbDataStyle.currentIndexChanged.connect(self.setDataStyle)
self.updateStyleTweaks()
self.openArchive( opts.filename )
# fill in some explicit metadata stuff from our options
# this overrides what we just read in
if self.metadata.series is None:
self.metadata.series = opts.series_name
if self.metadata.issueNumber is None:
self.metadata.issueNumber = opts.issue_number
def updateAppTitle( self ):
if self.comic_archive is None:
self.setWindowTitle( self.appName )
else:
self.setWindowTitle( self.appName + " - " + self.comic_archive.path)
def configMenus( self):
# File Menu
self.actionExit.setShortcut( 'Ctrl+Q' )
self.actionExit.setStatusTip( 'Exit application' )
self.actionExit.triggered.connect( QtGui.qApp.quit )
self.actionLoad.setShortcut( 'Ctrl+O' )
self.actionLoad.setStatusTip( 'Load comic archive' )
self.actionLoad.triggered.connect( self.selectFile )
self.actionWrite_Tags.setShortcut( 'Ctrl+S' )
self.actionWrite_Tags.setStatusTip( 'Save tags to comic archive' )
self.actionWrite_Tags.triggered.connect( self.commitMetadata )
#self.actionRepackage.setShortcut( )
self.actionRepackage.setStatusTip( 'Re-create archive as CBZ' )
self.actionRepackage.triggered.connect( self.repackageArchive )
# Tag Menu
self.actionParse_Filename.setShortcut( 'Ctrl+F' )
self.actionParse_Filename.setStatusTip( 'Try to extract tags from filename' )
self.actionParse_Filename.triggered.connect( self.useFilename )
self.actionQuery_Online.setShortcut( 'Ctrl+W' )
self.actionQuery_Online.setStatusTip( 'Search online for tags' )
self.actionQuery_Online.triggered.connect( self.queryOnline )
# Help Menu
self.actionAbout.setShortcut( 'Ctrl+A' )
self.actionAbout.setStatusTip( 'Show the ' + self.appName + ' info' )
self.actionAbout.triggered.connect( self.aboutApp )
# ToolBar
self.toolBar.addAction( self.actionLoad )
self.toolBar.addAction( self.actionWrite_Tags )
self.toolBar.addAction( self.actionParse_Filename )
self.toolBar.addAction( self.actionQuery_Online )
def repackageArchive( self ):
QtGui.QMessageBox.information(self, self.tr("Repackage Comic Archive"), self.tr("TBD"))
def aboutApp( self ):
QtGui.QMessageBox.information(self, self.tr("About " + self.appName ), self.tr(self.appName + " 0.1"))
def dragEnterEvent(self, event):
self.droppedFile=None
if event.mimeData().hasUrls():
url=event.mimeData().urls()[0]
if url.isValid():
if url.scheme()=="file":
self.droppedFile=url.toLocalFile()
event.accept()
def dropEvent(self, event):
#print self.droppedFile # displays the file name
self.openArchive( str(self.droppedFile) )
def openArchive( self, path ):
if path is None or path == "":
return
ca = ComicArchive( path )
if ca is not None and ca.seemsToBeAComicArchive():
self.comic_archive = ca
self.metadata = self.comic_archive.readMetadata( self.data_style )
if self.metadata.isEmpty:
self.metadata = self.comic_archive.metadataFromFilename( )
image_data = self.comic_archive.getCoverPage()
if not image_data is None:
img = QtGui.QImage()
img.loadFromData( image_data )
self.lblCover.setPixmap(QtGui.QPixmap(img))
self.lblCover.setScaledContents(True)
#!!!ATB should I clear the form???
self.metadataToForm()
self.updateAppTitle()
self.updateInfoBox()
else:
QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("That file doesn't appear to be a comic archive!"))
def updateInfoBox( self ):
ca = self.comic_archive
info_text = os.path.basename( ca.path ) + "\n"
info_text += str(ca.getNumberOfPages()) + " pages \n"
if ca.hasCIX():
info_text += "* ComicRack tags\n"
if ca.hasCBI():
info_text += "* ComicBookLover tags\n"
self.lblArchiveInfo.setText( info_text )
def metadataToForm( self ):
# copy the the metadata object into to the form
#helper func
def assignText( field, value):
if value is not None:
field.setText( u"{0}".format(value) )
md = self.metadata
assignText( self.leSeries, md.series )
assignText( self.leIssueNum, md.issueNumber )
assignText( self.leIssueCount, md.issueCount )
assignText( self.leVolumeNum, md.volumeNumber )
assignText( self.leVolumeCount, md.volumeCount )
assignText( self.leTitle, md.title )
assignText( self.lePublisher, md.publisher )
assignText( self.lePubMonth, md.publicationMonth )
assignText( self.lePubYear, md.publicationYear )
assignText( self.leGenre, md.genre )
assignText( self.leImprint, md.imprint )
assignText( self.teComments, md.comments )
assignText( self.teNotes, md.notes )
assignText( self.leCriticalRating, md.criticalRating )
assignText( self.leMaturityRating, md.maturityRating )
assignText( self.leStoryArc, md.storyArc )
assignText( self.leScanInfo, md.scanInfo )
assignText( self.leSeriesGroup, md.seriesGroup )
assignText( self.leAltSeries, md.alternateSeries )
assignText( self.leAltIssueNum, md.alternateNumber )
assignText( self.leAltIssueCount, md.alternateCount )
assignText( self.leWebLink, md.webLink )
assignText( self.teCharacters, md.characters )
assignText( self.teTeams, md.teams )
assignText( self.teLocations, md.locations )
assignText( self.leFormat, md.format )
if md.language is not None:
i = self.cbLanguage.findData( md.language )
self.cbLanguage.setCurrentIndex( i )
if md.country is not None:
i = self.cbCountry.findText( md.country )
self.cbCountry.setCurrentIndex( i )
if md.manga is not None:
i = self.cbManga.findData( md.manga )
self.cbManga.setCurrentIndex( i )
if md.blackAndWhite is not None and md.blackAndWhite:
self.cbBW.setChecked( True )
assignText( self.teTags, utils.listToString( md.tags ) )
# !!! Should we clear the credits table or just avoid duplicates?
while self.twCredits.rowCount() > 0:
self.twCredits.removeRow(0)
if md.credits is not None and len(md.credits) != 0:
self.twCredits.setSortingEnabled( False )
row = 0
for credit in md.credits:
# before we add the credit, see if the role-person pair already exists:
r = 0
while r < self.twCredits.rowCount():
if ( self.twCredits.item(r, 0).text() == credit['role'].title() and
self.twCredits.item(r, 1).text() == credit['person'] ):
break
r = r + 1
# if we didn't make it through the table, it's there alread, so continue without adding
if ( r != self.twCredits.rowCount() ):
continue
self.twCredits.insertRow(row)
item_text = credit['role'].title()
item = QtGui.QTableWidgetItem(item_text)
#item.setData( QtCore.Qt.UserRole ,record['id'])
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twCredits.setItem(row, 0, item)
item_text = credit['person']
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twCredits.setItem(row, 1, item)
row += 1
self.twCredits.setSortingEnabled( True )
def formToMetadata( self ):
#helper func
def xlate( data, type_str):
s = u"{0}".format(data).strip()
if s == "":
return None
elif type_str == "str":
return s
else:
return int(s)
# copy the data from the form into the metadata
md = self.metadata
md.series = xlate( self.leSeries.text(), "str" )
md.issueNumber = xlate( self.leIssueNum.text(), "str" )
md.issueCount = xlate( self.leIssueCount.text(), "int" )
md.volumeNumber = xlate( self.leVolumeNum.text(), "int" )
md.volumeCount = xlate( self.leVolumeCount.text(), "int" )
md.title = xlate( self.leTitle.text(), "str" )
md.publisher = xlate( self.lePublisher.text(), "str" )
md.publicationMonth = xlate( self.lePubMonth.text(), "int" )
md.publicationYear = xlate( self.lePubYear.text(), "int" )
md.genre = xlate( self.leGenre.text(), "str" )
md.imprint = xlate( self.leImprint.text(), "str" )
md.comments = xlate( self.teComments.toPlainText(), "str" )
md.notes = xlate( self.teNotes.toPlainText(), "str" )
md.criticalRating = xlate( self.leCriticalRating.text(), "int" )
md.maturityRating = xlate( self.leMaturityRating.text(), "str" )
md.storyArc = xlate( self.leStoryArc.text(), "str" )
md.scanInfo = xlate( self.leScanInfo.text(), "str" )
md.seriesGroup = xlate( self.leSeriesGroup.text(), "str" )
md.alternateSeries = xlate( self.leAltSeries.text(), "str" )
md.alternateNumber = xlate( self.leAltIssueNum.text(), "int" )
md.alternateCount = xlate( self.leAltIssueCount.text(), "int" )
md.webLink = xlate( self.leWebLink.text(), "str" )
md.characters = xlate( self.teCharacters.toPlainText(), "str" )
md.teams = xlate( self.teTeams.toPlainText(), "str" )
md.locations = xlate( self.teLocations.toPlainText(), "str" )
md.format = xlate( self.leFormat.text(), "str" )
md.country = xlate( self.cbCountry.currentText(), "str" )
langiso = self.cbLanguage.itemData(self.cbLanguage.currentIndex()).toString()
md.language = xlate( langiso, "str" )
manga_code = self.cbManga.itemData(self.cbManga.currentIndex()).toString()
md.manga = xlate( manga_code, "str" )
# Make a list from the coma delimited tags string
tmp = xlate( self.teTags.toPlainText(), "str" )
if tmp != None:
def striplist(l):
return([x.strip() for x in l])
md.tags = striplist(tmp.split( "," ))
if ( self.cbBW.isChecked() ):
md.blackAndWhite = True
else:
md.blackAndWhite = False
def useFilename( self ):
self.metadata = self.comic_archive.metadataFromFilename( )
self.metadataToForm()
def selectFile( self ):
path = str(QtGui.QFileDialog.getOpenFileName())
self.openArchive( path )
def queryOnline(self):
if str(self.leSeries.text()).strip() != "":
series_name = str(self.leSeries.text()).strip()
else:
QtGui.QMessageBox.information(self, self.tr("Whoops"), self.tr("Need to enter a series name to query."))
return
issue_number = str(self.leIssueNum.text()).strip()
selector = VolumeSelectionWindow( self, series_name, issue_number )
selector.setModal(True)
selector.exec_()
if selector.result():
#we should now have a volume ID
comicVine = ComicVineTalker()
self.metadata = comicVine.fetchIssueData( selector.volume_id, selector.issue_number )
# Now push the right data into the edit controls
self.metadataToForm()
#!!!ATB should I clear the form???
def commitMetadata(self):
if (not self.metadata is None and not self.comic_archive is None):
self.formToMetadata()
self.comic_archive.writeMetadata( self.metadata, self.data_style )
self.updateInfoBox()
QtGui.QMessageBox.information(self, self.tr("Yeah!"), self.tr("File written."))
else:
QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("No data to commit!"))
def setDataStyle(self, s):
self.data_style, b = self.cbDataStyle.itemData(s).toInt()
self.updateStyleTweaks()
def updateStyleTweaks( self ):
# depending on the current data style, certain fields are disabled
inactive_color = QtGui.QColor(255, 170, 150)
active_palette = self.leSeries.palette()
inactive_palette1 = self.leSeries.palette()
inactive_palette1.setColor(QtGui.QPalette.Base, inactive_color)
inactive_palette2 = self.leSeries.palette()
inactive_palette3 = self.leSeries.palette()
inactive_palette3.setColor(QtGui.QPalette.Base, inactive_color)
inactive_palette3.setColor(QtGui.QPalette.Base, inactive_color)
#helper func
def enableWidget( item, enable ):
inactive_palette3.setColor(item.backgroundRole(), inactive_color)
inactive_palette2.setColor(item.backgroundRole(), inactive_color)
inactive_palette3.setColor(item.foregroundRole(), inactive_color)
if enable:
item.setPalette(active_palette)
if type(item) == QtGui.QCheckBox:
item.setEnabled( True )
elif type(item) == QtGui.QComboBox:
item.setEnabled( True )
else:
item.setReadOnly( False )
else:
if type(item) == QtGui.QCheckBox:
item.setPalette(inactive_palette2)
item.setEnabled( False )
elif type(item) == QtGui.QComboBox:
item.setPalette(inactive_palette3)
item.setEnabled( False )
else:
item.setReadOnly( True )
item.setPalette(inactive_palette1)
cbi_only = [ self.leVolumeCount, self.cbCountry, self.leCriticalRating, self.teTags ]
cix_only = [
self.leImprint, self.teNotes, self.cbBW, self.cbManga,
self.leStoryArc, self.leScanInfo, self.leSeriesGroup,
self.leAltSeries, self.leAltIssueNum, self.leAltIssueCount,
self.leWebLink, self.teCharacters, self.teTeams,
self.teLocations, self.leMaturityRating, self.leFormat
]
if self.data_style == MetaDataStyle.CIX:
for item in cix_only:
enableWidget( item, True )
for item in cbi_only:
enableWidget(item, False )
if self.data_style == MetaDataStyle.CBI:
for item in cbi_only:
enableWidget( item, True )
for item in cix_only:
enableWidget(item, False )
def center(self):
screen = QtGui.QDesktopWidget().screenGeometry()
size = self.geometry()
self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)
def populateComboBoxes( self ):
# Add the entries to the tag style combobox
self.cbDataStyle.addItem( "ComicBookLover", MetaDataStyle.CBI )
self.cbDataStyle.addItem( "ComicRack", MetaDataStyle.CIX )
# select the current style
if ( self.data_style == MetaDataStyle.CBI ):
self.cbDataStyle.setCurrentIndex ( 0 )
elif ( self.data_style == MetaDataStyle.CIX ):
self.cbDataStyle.setCurrentIndex ( 1 )
# Add the entries to the country combobox
self.cbCountry.addItem( "", "" )
for c in utils.countries:
self.cbCountry.addItem( c[1], c[0] )
# Add the entries to the language combobox
self.cbLanguage.addItem( "", "" )
lang_dict = utils.getLanguageDict()
for key in sorted(lang_dict, cmp=locale.strcoll, key=lang_dict.get):
self.cbLanguage.addItem( lang_dict[key], key )
# Add the entries to the manga combobox
self.cbManga.addItem( "", "" )
self.cbManga.addItem( "Yes", "Yes" )
self.cbManga.addItem( "Yes (Right to Left)", "YesAndRightToLeft" )
self.cbManga.addItem( "No", "No" )

764
taggerwindow.ui Normal file
View File

@ -0,0 +1,764 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>943</width>
<height>507</height>
</rect>
</property>
<property name="windowTitle">
<string>ComicTagger</string>
</property>
<widget class="QWidget" name="centralWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<widget class="QLabel" name="lblCover">
<property name="geometry">
<rect>
<x>20</x>
<y>150</y>
<width>211</width>
<height>291</height>
</rect>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QWidget" name="formLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>20</y>
<width>211</width>
<height>32</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Tag Style:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cbDataStyle"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>250</x>
<y>10</y>
<width>671</width>
<height>431</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Details</string>
</attribute>
<widget class="QWidget" name="formLayoutWidget_2">
<property name="geometry">
<rect>
<x>10</x>
<y>0</y>
<width>371</width>
<height>284</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Series</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leSeries"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leTitle"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Publisher</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="lePublisher"/>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="leImprint"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Series Group</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="leSeriesGroup"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Imprint</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="leStoryArc"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_25">
<property name="text">
<string>Story Arc</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="leGenre"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>Genre</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
<string>Format</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="leFormat"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="formLayoutWidget_3">
<property name="geometry">
<rect>
<x>390</x>
<y>0</y>
<width>151</width>
<height>140</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_4">
<property name="text">
<string>Issue</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leIssueNum"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string># Issues</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leIssueCount"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Volume</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="leVolumeNum"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string># Volumes</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="leVolumeCount"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="formLayoutWidget_4">
<property name="geometry">
<rect>
<x>390</x>
<y>140</y>
<width>112</width>
<height>71</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_4">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="1">
<widget class="QLineEdit" name="lePubMonth"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Month</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lePubYear"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Year</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="formLayoutWidget_5">
<property name="geometry">
<rect>
<x>390</x>
<y>210</y>
<width>251</width>
<height>81</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_5">
<item row="0" column="1">
<widget class="QComboBox" name="cbLanguage"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cbCountry"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Country</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="formLayoutWidget_7">
<property name="geometry">
<rect>
<x>550</x>
<y>0</y>
<width>103</width>
<height>120</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_7">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<property name="rowWrapPolicy">
<enum>QFormLayout::WrapAllRows</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label_18">
<property name="text">
<string>Maturity Rating</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leMaturityRating"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Critical Rating</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leCriticalRating"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="formLayoutWidget_9">
<property name="geometry">
<rect>
<x>10</x>
<y>290</y>
<width>371</width>
<height>111</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_9">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Alt. Series</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leAltSeries"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Alt. Issue</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="leAltIssueNum"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Alt. # Issues</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="leAltIssueCount"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="formLayoutWidget_10">
<property name="geometry">
<rect>
<x>390</x>
<y>300</y>
<width>211</width>
<height>71</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_10">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="2" column="1">
<widget class="QComboBox" name="cbManga">
<property name="autoFillBackground">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_31">
<property name="text">
<string>Manga</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="cbBW">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="text">
<string>Black &amp;&amp; White</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Credits</string>
</attribute>
<widget class="QWidget" name="formLayoutWidget_8">
<property name="geometry">
<rect>
<x>30</x>
<y>350</y>
<width>481</width>
<height>32</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_8">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Scan Info</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leScanInfo"/>
</item>
</layout>
</widget>
<widget class="QTableWidget" name="twCredits">
<property name="geometry">
<rect>
<x>30</x>
<y>10</y>
<width>481</width>
<height>321</height>
</rect>
</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>Credit</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
</widget>
<widget class="QPushButton" name="btnAddCredit">
<property name="geometry">
<rect>
<x>520</x>
<y>20</y>
<width>121</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Add Credit</string>
</property>
</widget>
<widget class="QPushButton" name="btnRemoveCredit">
<property name="geometry">
<rect>
<x>520</x>
<y>60</y>
<width>121</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Remove Credit</string>
</property>
</widget>
<widget class="QPushButton" name="btnEditCredit">
<property name="geometry">
<rect>
<x>520</x>
<y>100</y>
<width>121</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Edit Credit</string>
</property>
</widget>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Notes</string>
</attribute>
<widget class="QWidget" name="formLayoutWidget_6">
<property name="geometry">
<rect>
<x>30</x>
<y>10</y>
<width>591</width>
<height>371</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_6">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Comments</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Notes</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QTextEdit" name="teComments"/>
</item>
<item row="1" column="1">
<widget class="QTextEdit" name="teNotes"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="leWebLink"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
<string>Web</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="tab_4">
<attribute name="title">
<string>Other</string>
</attribute>
<widget class="QWidget" name="formLayoutWidget_11">
<property name="geometry">
<rect>
<x>20</x>
<y>17</y>
<width>581</width>
<height>371</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_11">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="2" column="0">
<widget class="QLabel" name="label_30">
<property name="text">
<string>Teams</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_29">
<property name="text">
<string>Characters</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_27">
<property name="text">
<string>Locations</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_28">
<property name="text">
<string>Other Tags</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QTextEdit" name="teCharacters"/>
</item>
<item row="2" column="1">
<widget class="QTextEdit" name="teTeams"/>
</item>
<item row="3" column="1">
<widget class="QTextEdit" name="teLocations"/>
</item>
<item row="4" column="1">
<widget class="QTextEdit" name="teTags"/>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="tab_5">
<attribute name="title">
<string>Pages</string>
</attribute>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QLabel" name="lblArchiveInfo">
<property name="geometry">
<rect>
<x>20</x>
<y>60</y>
<width>211</width>
<height>81</height>
</rect>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>943</width>
<height>28</height>
</rect>
</property>
<widget class="QMenu" name="menuComicTagger">
<property name="title">
<string>File</string>
</property>
<addaction name="actionLoad"/>
<addaction name="actionWrite_Tags"/>
<addaction name="actionRepackage"/>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAbout"/>
</widget>
<widget class="QMenu" name="menuTags">
<property name="title">
<string>Tags</string>
</property>
<addaction name="actionParse_Filename"/>
<addaction name="actionQuery_Online"/>
</widget>
<addaction name="menuComicTagger"/>
<addaction name="menuTags"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<property name="movable">
<bool>false</bool>
</property>
<property name="floatable">
<bool>false</bool>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="separator"/>
</widget>
<action name="actionLoad">
<property name="text">
<string>Open</string>
</property>
</action>
<action name="actionWrite_Tags">
<property name="text">
<string>Save Tags</string>
</property>
</action>
<action name="actionRepackage">
<property name="text">
<string>Repackage</string>
</property>
</action>
<action name="actionExit">
<property name="text">
<string>Exit</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>About ComicTagger</string>
</property>
</action>
<action name="actionParse_Filename">
<property name="text">
<string>Parse Filename</string>
</property>
</action>
<action name="actionQuery_Online">
<property name="text">
<string>Query Online</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>

311
test.ui Normal file
View File

@ -0,0 +1,311 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>943</width>
<height>565</height>
</rect>
</property>
<property name="windowTitle">
<string>ComicTagger</string>
</property>
<widget class="QWidget" name="centralWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<widget class="QWidget" name="formLayoutWidget">
<property name="geometry">
<rect>
<x>50</x>
<y>20</y>
<width>251</width>
<height>60</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>Tag Style:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cbDataStyle"/>
</item>
</layout>
</widget>
<widget class="QPushButton" name="btnQuery">
<property name="geometry">
<rect>
<x>590</x>
<y>170</y>
<width>181</width>
<height>27</height>
</rect>
</property>
<property name="text">
<string>Query Online</string>
</property>
</widget>
<widget class="QPushButton" name="btnCommit">
<property name="geometry">
<rect>
<x>570</x>
<y>20</y>
<width>111</width>
<height>27</height>
</rect>
</property>
<property name="text">
<string>Save Tags</string>
</property>
</widget>
<widget class="QLabel" name="lblCover">
<property name="geometry">
<rect>
<x>590</x>
<y>260</y>
<width>181</width>
<height>51</height>
</rect>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
<widget class="QStackedWidget" name="stackedWidget">
<property name="geometry">
<rect>
<x>40</x>
<y>80</y>
<width>461</width>
<height>411</height>
</rect>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<widget class="QLabel" name="label_7">
<property name="geometry">
<rect>
<x>130</x>
<y>380</y>
<width>66</width>
<height>17</height>
</rect>
</property>
<property name="text">
<string>CR Entry</string>
</property>
</widget>
</widget>
<widget class="QWidget" name="page_2">
<widget class="QTabWidget" name="tabWidget">
<property name="geometry">
<rect>
<x>30</x>
<y>10</y>
<width>361</width>
<height>351</height>
</rect>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Tab 1</string>
</attribute>
<widget class="QWidget" name="formLayoutWidget_2">
<property name="geometry">
<rect>
<x>50</x>
<y>20</y>
<width>261</width>
<height>291</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="3" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Issue</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_2"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Series</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Publication Year</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_3"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Publication Month</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_4"/>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Tab 2</string>
</attribute>
<widget class="QWidget" name="formLayoutWidget_3">
<property name="geometry">
<rect>
<x>40</x>
<y>10</y>
<width>261</width>
<height>291</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="3" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Notes</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="lineEdit_5"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_6"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Comments</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Tags</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_7"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Credits</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_8"/>
</item>
</layout>
</widget>
</widget>
</widget>
<widget class="QLabel" name="label_6">
<property name="geometry">
<rect>
<x>120</x>
<y>370</y>
<width>66</width>
<height>17</height>
</rect>
</property>
<property name="text">
<string>CBL Entry</string>
</property>
</widget>
</widget>
</widget>
<widget class="QLineEdit" name="leFileSelection">
<property name="geometry">
<rect>
<x>620</x>
<y>90</y>
<width>113</width>
<height>27</height>
</rect>
</property>
</widget>
<widget class="QTableWidget" name="tableWidget">
<property name="geometry">
<rect>
<x>585</x>
<y>371</y>
<width>221</width>
<height>91</height>
</rect>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>943</width>
<height>25</height>
</rect>
</property>
<widget class="QMenu" name="menuComicTagger">
<property name="title">
<string>File</string>
</property>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
</widget>
<addaction name="menuComicTagger"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusBar"/>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>

450
utils.py Normal file
View File

@ -0,0 +1,450 @@
# coding=utf-8
def listToString( l ):
string = ""
if l is not None:
for item in l:
if len(string) > 0:
string += ", "
string += item
return string
# -o- coding: utf-8 -o-
# ISO639 python dict
# oficial list in http://www.loc.gov/standards/iso639-2/php/code_list.php
lang_dict = {
'ab': 'Abkhaz',
'aa': 'Afar',
'af': 'Afrikaans',
'ak': 'Akan',
'sq': 'Albanian',
'am': 'Amharic',
'ar': 'Arabic',
'an': 'Aragonese',
'hy': 'Armenian',
'as': 'Assamese',
'av': 'Avaric',
'ae': 'Avestan',
'ay': 'Aymara',
'az': 'Azerbaijani',
'bm': 'Bambara',
'ba': 'Bashkir',
'eu': 'Basque',
'be': 'Belarusian',
'bn': 'Bengali',
'bh': 'Bihari',
'bi': 'Bislama',
'bs': 'Bosnian',
'br': 'Breton',
'bg': 'Bulgarian',
'my': 'Burmese',
'ca': 'Catalan; Valencian',
'ch': 'Chamorro',
'ce': 'Chechen',
'ny': 'Chichewa; Chewa; Nyanja',
'zh': 'Chinese',
'cv': 'Chuvash',
'kw': 'Cornish',
'co': 'Corsican',
'cr': 'Cree',
'hr': 'Croatian',
'cs': 'Czech',
'da': 'Danish',
'dv': 'Divehi; Maldivian;',
'nl': 'Dutch',
'dz': 'Dzongkha',
'en': 'English',
'eo': 'Esperanto',
'et': 'Estonian',
'ee': 'Ewe',
'fo': 'Faroese',
'fj': 'Fijian',
'fi': 'Finnish',
'fr': 'French',
'ff': 'Fula',
'gl': 'Galician',
'ka': 'Georgian',
'de': 'German',
'el': 'Greek, Modern',
'gn': 'Guaraní',
'gu': 'Gujarati',
'ht': 'Haitian',
'ha': 'Hausa',
'he': 'Hebrew (modern)',
'hz': 'Herero',
'hi': 'Hindi',
'ho': 'Hiri Motu',
'hu': 'Hungarian',
'ia': 'Interlingua',
'id': 'Indonesian',
'ie': 'Interlingue',
'ga': 'Irish',
'ig': 'Igbo',
'ik': 'Inupiaq',
'io': 'Ido',
'is': 'Icelandic',
'it': 'Italian',
'iu': 'Inuktitut',
'ja': 'Japanese',
'jv': 'Javanese',
'kl': 'Kalaallisut',
'kn': 'Kannada',
'kr': 'Kanuri',
'ks': 'Kashmiri',
'kk': 'Kazakh',
'km': 'Khmer',
'ki': 'Kikuyu, Gikuyu',
'rw': 'Kinyarwanda',
'ky': 'Kirghiz, Kyrgyz',
'kv': 'Komi',
'kg': 'Kongo',
'ko': 'Korean',
'ku': 'Kurdish',
'kj': 'Kwanyama, Kuanyama',
'la': 'Latin',
'lb': 'Luxembourgish',
'lg': 'Luganda',
'li': 'Limburgish',
'ln': 'Lingala',
'lo': 'Lao',
'lt': 'Lithuanian',
'lu': 'Luba-Katanga',
'lv': 'Latvian',
'gv': 'Manx',
'mk': 'Macedonian',
'mg': 'Malagasy',
'ms': 'Malay',
'ml': 'Malayalam',
'mt': 'Maltese',
'mi': 'Māori',
'mr': 'Marathi (Marāṭhī)',
'mh': 'Marshallese',
'mn': 'Mongolian',
'na': 'Nauru',
'nv': 'Navajo, Navaho',
'nb': 'Norwegian Bokmål',
'nd': 'North Ndebele',
'ne': 'Nepali',
'ng': 'Ndonga',
'nn': 'Norwegian Nynorsk',
'no': 'Norwegian',
'ii': 'Nuosu',
'nr': 'South Ndebele',
'oc': 'Occitan',
'oj': 'Ojibwe, Ojibwa',
'cu': 'Old Church Slavonic',
'om': 'Oromo',
'or': 'Oriya',
'os': 'Ossetian, Ossetic',
'pa': 'Panjabi, Punjabi',
'pi': 'Pāli',
'fa': 'Persian',
'pl': 'Polish',
'ps': 'Pashto, Pushto',
'pt': 'Portuguese',
'qu': 'Quechua',
'rm': 'Romansh',
'rn': 'Kirundi',
'ro': 'Romanian, Moldavan',
'ru': 'Russian',
'sa': 'Sanskrit (Saṁskṛta)',
'sc': 'Sardinian',
'sd': 'Sindhi',
'se': 'Northern Sami',
'sm': 'Samoan',
'sg': 'Sango',
'sr': 'Serbian',
'gd': 'Scottish Gaelic',
'sn': 'Shona',
'si': 'Sinhala, Sinhalese',
'sk': 'Slovak',
'sl': 'Slovene',
'so': 'Somali',
'st': 'Southern Sotho',
'es': 'Spanish; Castilian',
'su': 'Sundanese',
'sw': 'Swahili',
'ss': 'Swati',
'sv': 'Swedish',
'ta': 'Tamil',
'te': 'Telugu',
'tg': 'Tajik',
'th': 'Thai',
'ti': 'Tigrinya',
'bo': 'Tibetan',
'tk': 'Turkmen',
'tl': 'Tagalog',
'tn': 'Tswana',
'to': 'Tonga',
'tr': 'Turkish',
'ts': 'Tsonga',
'tt': 'Tatar',
'tw': 'Twi',
'ty': 'Tahitian',
'ug': 'Uighur, Uyghur',
'uk': 'Ukrainian',
'ur': 'Urdu',
'uz': 'Uzbek',
've': 'Venda',
'vi': 'Vietnamese',
'vo': 'Volapük',
'wa': 'Walloon',
'cy': 'Welsh',
'wo': 'Wolof',
'fy': 'Western Frisian',
'xh': 'Xhosa',
'yi': 'Yiddish',
'yo': 'Yoruba',
'za': 'Zhuang, Chuang',
'zu': 'Zulu',
}
countries = [
('AF', 'Afghanistan'),
('AL', 'Albania'),
('DZ', 'Algeria'),
('AS', 'American Samoa'),
('AD', 'Andorra'),
('AO', 'Angola'),
('AI', 'Anguilla'),
('AQ', 'Antarctica'),
('AG', 'Antigua And Barbuda'),
('AR', 'Argentina'),
('AM', 'Armenia'),
('AW', 'Aruba'),
('AU', 'Australia'),
('AT', 'Austria'),
('AZ', 'Azerbaijan'),
('BS', 'Bahamas'),
('BH', 'Bahrain'),
('BD', 'Bangladesh'),
('BB', 'Barbados'),
('BY', 'Belarus'),
('BE', 'Belgium'),
('BZ', 'Belize'),
('BJ', 'Benin'),
('BM', 'Bermuda'),
('BT', 'Bhutan'),
('BO', 'Bolivia'),
('BA', 'Bosnia And Herzegowina'),
('BW', 'Botswana'),
('BV', 'Bouvet Island'),
('BR', 'Brazil'),
('BN', 'Brunei Darussalam'),
('BG', 'Bulgaria'),
('BF', 'Burkina Faso'),
('BI', 'Burundi'),
('KH', 'Cambodia'),
('CM', 'Cameroon'),
('CA', 'Canada'),
('CV', 'Cape Verde'),
('KY', 'Cayman Islands'),
('CF', 'Central African Rep'),
('TD', 'Chad'),
('CL', 'Chile'),
('CN', 'China'),
('CX', 'Christmas Island'),
('CC', 'Cocos Islands'),
('CO', 'Colombia'),
('KM', 'Comoros'),
('CG', 'Congo'),
('CK', 'Cook Islands'),
('CR', 'Costa Rica'),
('CI', 'Cote D`ivoire'),
('HR', 'Croatia'),
('CU', 'Cuba'),
('CY', 'Cyprus'),
('CZ', 'Czech Republic'),
('DK', 'Denmark'),
('DJ', 'Djibouti'),
('DM', 'Dominica'),
('DO', 'Dominican Republic'),
('TP', 'East Timor'),
('EC', 'Ecuador'),
('EG', 'Egypt'),
('SV', 'El Salvador'),
('GQ', 'Equatorial Guinea'),
('ER', 'Eritrea'),
('EE', 'Estonia'),
('ET', 'Ethiopia'),
('FK', 'Falkland Islands (Malvinas)'),
('FO', 'Faroe Islands'),
('FJ', 'Fiji'),
('FI', 'Finland'),
('FR', 'France'),
('GF', 'French Guiana'),
('PF', 'French Polynesia'),
('TF', 'French S. Territories'),
('GA', 'Gabon'),
('GM', 'Gambia'),
('GE', 'Georgia'),
('DE', 'Germany'),
('GH', 'Ghana'),
('GI', 'Gibraltar'),
('GR', 'Greece'),
('GL', 'Greenland'),
('GD', 'Grenada'),
('GP', 'Guadeloupe'),
('GU', 'Guam'),
('GT', 'Guatemala'),
('GN', 'Guinea'),
('GW', 'Guinea-bissau'),
('GY', 'Guyana'),
('HT', 'Haiti'),
('HN', 'Honduras'),
('HK', 'Hong Kong'),
('HU', 'Hungary'),
('IS', 'Iceland'),
('IN', 'India'),
('ID', 'Indonesia'),
('IR', 'Iran'),
('IQ', 'Iraq'),
('IE', 'Ireland'),
('IL', 'Israel'),
('IT', 'Italy'),
('JM', 'Jamaica'),
('JP', 'Japan'),
('JO', 'Jordan'),
('KZ', 'Kazakhstan'),
('KE', 'Kenya'),
('KI', 'Kiribati'),
('KP', 'Korea (North)'),
('KR', 'Korea (South)'),
('KW', 'Kuwait'),
('KG', 'Kyrgyzstan'),
('LA', 'Laos'),
('LV', 'Latvia'),
('LB', 'Lebanon'),
('LS', 'Lesotho'),
('LR', 'Liberia'),
('LY', 'Libya'),
('LI', 'Liechtenstein'),
('LT', 'Lithuania'),
('LU', 'Luxembourg'),
('MO', 'Macau'),
('MK', 'Macedonia'),
('MG', 'Madagascar'),
('MW', 'Malawi'),
('MY', 'Malaysia'),
('MV', 'Maldives'),
('ML', 'Mali'),
('MT', 'Malta'),
('MH', 'Marshall Islands'),
('MQ', 'Martinique'),
('MR', 'Mauritania'),
('MU', 'Mauritius'),
('YT', 'Mayotte'),
('MX', 'Mexico'),
('FM', 'Micronesia'),
('MD', 'Moldova'),
('MC', 'Monaco'),
('MN', 'Mongolia'),
('MS', 'Montserrat'),
('MA', 'Morocco'),
('MZ', 'Mozambique'),
('MM', 'Myanmar'),
('NA', 'Namibia'),
('NR', 'Nauru'),
('NP', 'Nepal'),
('NL', 'Netherlands'),
('AN', 'Netherlands Antilles'),
('NC', 'New Caledonia'),
('NZ', 'New Zealand'),
('NI', 'Nicaragua'),
('NE', 'Niger'),
('NG', 'Nigeria'),
('NU', 'Niue'),
('NF', 'Norfolk Island'),
('MP', 'Northern Mariana Islands'),
('NO', 'Norway'),
('OM', 'Oman'),
('PK', 'Pakistan'),
('PW', 'Palau'),
('PA', 'Panama'),
('PG', 'Papua New Guinea'),
('PY', 'Paraguay'),
('PE', 'Peru'),
('PH', 'Philippines'),
('PN', 'Pitcairn'),
('PL', 'Poland'),
('PT', 'Portugal'),
('PR', 'Puerto Rico'),
('QA', 'Qatar'),
('RE', 'Reunion'),
('RO', 'Romania'),
('RU', 'Russian Federation'),
('RW', 'Rwanda'),
('KN', 'Saint Kitts And Nevis'),
('LC', 'Saint Lucia'),
('VC', 'St Vincent/Grenadines'),
('WS', 'Samoa'),
('SM', 'San Marino'),
('ST', 'Sao Tome'),
('SA', 'Saudi Arabia'),
('SN', 'Senegal'),
('SC', 'Seychelles'),
('SL', 'Sierra Leone'),
('SG', 'Singapore'),
('SK', 'Slovakia'),
('SI', 'Slovenia'),
('SB', 'Solomon Islands'),
('SO', 'Somalia'),
('ZA', 'South Africa'),
('ES', 'Spain'),
('LK', 'Sri Lanka'),
('SH', 'St. Helena'),
('PM', 'St.Pierre'),
('SD', 'Sudan'),
('SR', 'Suriname'),
('SZ', 'Swaziland'),
('SE', 'Sweden'),
('CH', 'Switzerland'),
('SY', 'Syrian Arab Republic'),
('TW', 'Taiwan'),
('TJ', 'Tajikistan'),
('TZ', 'Tanzania'),
('TH', 'Thailand'),
('TG', 'Togo'),
('TK', 'Tokelau'),
('TO', 'Tonga'),
('TT', 'Trinidad And Tobago'),
('TN', 'Tunisia'),
('TR', 'Turkey'),
('TM', 'Turkmenistan'),
('TV', 'Tuvalu'),
('UG', 'Uganda'),
('UA', 'Ukraine'),
('AE', 'United Arab Emirates'),
('UK', 'United Kingdom'),
('US', 'United States'),
('UY', 'Uruguay'),
('UZ', 'Uzbekistan'),
('VU', 'Vanuatu'),
('VA', 'Vatican City State'),
('VE', 'Venezuela'),
('VN', 'Viet Nam'),
('VG', 'Virgin Islands (British)'),
('VI', 'Virgin Islands (U.S.)'),
('EH', 'Western Sahara'),
('YE', 'Yemen'),
('YU', 'Yugoslavia'),
('ZR', 'Zaire'),
('ZM', 'Zambia'),
('ZW', 'Zimbabwe')
]
def getLanguageDict():
return lang_dict
def getLanguageFromISO( iso ):
if iso == None:
return None
else:
return lang_dict[ iso ]

135
volumeselectionwindow.py Normal file
View File

@ -0,0 +1,135 @@
import sys
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import QUrl
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comicvinetalker import ComicVineTalker
from issueselectionwindow import IssueSelectionWindow
class VolumeSelectionWindow(QtGui.QDialog):
volume_id = 0
def __init__(self, parent, series_name, issue_number):
super(VolumeSelectionWindow, self).__init__(parent)
uic.loadUi('volumeselectionwindow.ui', self)
self.series_name = series_name
self.issue_number = issue_number
self.performQuery()
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
self.btnRequery.clicked.connect(self.requery)
self.btnIssues.clicked.connect(self.showIssues)
self.twList.selectRow(0)
def requery( self ):
self.performQuery()
self.twList.selectRow(0)
def showIssues( self ):
selector = IssueSelectionWindow( self, self.volume_id, self.issue_number )
selector.setModal(True)
selector.exec_()
if selector.result():
#we should now have a volume ID
self.issue_number = selector.issue_number
self.accept()
return
def performQuery( self ):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
comicVine = ComicVineTalker()
self.cv_search_results = comicVine.searchForSeries( self.series_name )
self.twList.setSortingEnabled(False)
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'])
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = str(record['start_year'])
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
item_text = record['count_of_issues']
item = QtGui.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
if record['publisher'] is not None:
item_text = record['publisher']['name']
item = QtGui.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
record['cover_image'] = None
row += 1
self.twList.setSortingEnabled(True)
self.twList.sortItems( 2 , QtCore.Qt.DescendingOrder )
def cellDoubleClicked( self, r, c ):
self.showIssues()
def currentItemChanged( self, curr, prev ):
if curr is None:
return
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
for record in self.cv_search_results:
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):
img = QtGui.QImage()
img.loadFromData(reply.readAll())
self.pending_cover_record['cover_image'] = img
self.pending_cover_record = None
self.setCover( img )
def setCover( self, img ):
self.labelThumbnail.setPixmap(QtGui.QPixmap(img))

214
volumeselectionwindow.ui Normal file
View File

@ -0,0 +1,214 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SelectSeries</class>
<widget class="QDialog" name="SelectSeries">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>480</height>
</rect>
</property>
<property name="windowTitle">
<string>Select Series</string>
</property>
<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>29</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="btnIssues">
<property name="text">
<string>Show Issues</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnRequery">
<property name="text">
<string>Re-Query</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>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SelectSeries</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>SelectSeries</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>