Compare commits
81 Commits
0.9.0-beta
...
0.9.5-beta
Author | SHA1 | Date | |
---|---|---|---|
41f730a558 | |||
550b84361c | |||
fb4248fda2 | |||
9626c3fd77 | |||
3f305c6788 | |||
9e68516dac | |||
8f45994b9a | |||
4ea56c0bd0 | |||
5445417404 | |||
db6423aea9 | |||
aa62a3e8ff | |||
cd1733a975 | |||
c81319402d | |||
8a8e53d9c9 | |||
7614e95084 | |||
bd9f314496 | |||
bebd09d3f6 | |||
8a5430c83e | |||
93be1b42f4 | |||
01be389fad | |||
ca9aaf9279 | |||
ee9175087e | |||
94c5882175 | |||
ff74b3e5bc | |||
0017903a4f | |||
3d98118fa9 | |||
faf0b5d437 | |||
e14c9dfe19 | |||
4343f3f08d | |||
4a94bf4d6f | |||
a602c42f0e | |||
1efdc0e623 | |||
152040964e | |||
584f78bc3c | |||
3f1868222d | |||
45b94ce1fd | |||
7289f6915a | |||
a5d39a88c8 | |||
2acf2f60f3 | |||
f6ff6c3b73 | |||
6b88fb7e58 | |||
3364e437c6 | |||
1e5f40121c | |||
2a347522e4 | |||
7f1ce793e3 | |||
f7cb6e9d2b | |||
487c8a5bf4 | |||
5b8f73528b | |||
8af7651a50 | |||
1e3d8ccad3 | |||
c367b8806b | |||
d3ea8d1b2c | |||
c5f1542874 | |||
ab5d8599ac | |||
a2d0068522 | |||
c6c5728cb3 | |||
e6f63beee2 | |||
72af8f8564 | |||
5390a92b98 | |||
c814436899 | |||
dbec1999dc | |||
a970ed0e36 | |||
6d8d90d5b7 | |||
117d8d8998 | |||
3689317518 | |||
c845c786e4 | |||
9ccdc60c19 | |||
aec0477170 | |||
134dcbaba3 | |||
f040f8dc74 | |||
948acf9b23 | |||
3c2f4fa662 | |||
f99d466bae | |||
a773ab6539 | |||
ff2fca44f4 | |||
97fe437bb4 | |||
32aabb100b | |||
b385be4338 | |||
deeeef90a6 | |||
121889ed1b | |||
d300f51c7f |
6
Makefile
6
Makefile
@ -16,5 +16,9 @@ zip:
|
||||
rm -rf comictagger-src-$(VERSION_STR)
|
||||
|
||||
@echo When satisfied with release, do this:
|
||||
@echo svn fpoooo $(VERSION_STR)
|
||||
@echo make svn_tag
|
||||
|
||||
svn_tag:
|
||||
svn copy https://comictagger.googlecode.com/svn/trunk \
|
||||
https://comictagger.googlecode.com/svn/tags/$(VERSION_STR) -m "Release $(VERSION_STR)"
|
||||
|
89
cbltransformer.py
Normal file
89
cbltransformer.py
Normal file
@ -0,0 +1,89 @@
|
||||
"""
|
||||
Class to manage modifying metadata specifically for CBL/CBI
|
||||
"""
|
||||
|
||||
"""
|
||||
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 os
|
||||
import utils
|
||||
|
||||
|
||||
class CBLTransformer:
|
||||
def __init__( self, metadata, settings ):
|
||||
self.metadata = metadata
|
||||
self.settings = settings
|
||||
|
||||
|
||||
def apply( self ):
|
||||
# helper funcs
|
||||
def append_to_tags_if_unique( item ):
|
||||
if item.lower() not in (tag.lower() for tag in self.metadata.tags):
|
||||
self.metadata.tags.append( item )
|
||||
|
||||
def add_string_list_to_tags( str_list ):
|
||||
if str_list is not None and str_list != "":
|
||||
items = [ s.strip() for s in str_list.split(',') ]
|
||||
for item in items:
|
||||
append_to_tags_if_unique( item )
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
|
||||
# helper
|
||||
def setLonePrimary( role_list ):
|
||||
lone_credit = None
|
||||
count = 0
|
||||
for c in self.metadata.credits:
|
||||
if c['role'].lower() in role_list:
|
||||
count += 1
|
||||
lone_credit = c
|
||||
if count > 1:
|
||||
lone_credit = None
|
||||
break
|
||||
if lone_credit is not None:
|
||||
lone_credit['primary'] = True
|
||||
return lone_credit, count
|
||||
|
||||
#need to loop three times, once for 'writer', 'artist', and then 'penciler' if no artist
|
||||
setLonePrimary( ['writer'] )
|
||||
c, count = setLonePrimary( ['artist'] )
|
||||
if c is None and count == 0:
|
||||
c, count = setLonePrimary( ['penciler', 'penciller'] )
|
||||
if c is not None:
|
||||
c['primary'] = False
|
||||
self.metadata.addCredit( c['person'], 'Artist', True )
|
||||
|
||||
if self.settings.copy_characters_to_tags:
|
||||
add_string_list_to_tags( self.metadata.characters )
|
||||
|
||||
if self.settings.copy_teams_to_tags:
|
||||
add_string_list_to_tags( self.metadata.teams )
|
||||
|
||||
if self.settings.copy_locations_to_tags:
|
||||
add_string_list_to_tags( self.metadata.locations )
|
||||
|
||||
if self.settings.copy_notes_to_comments:
|
||||
if self.metadata.notes is not None and self.metadata.notes not in self.metadata.comments:
|
||||
self.metadata.comments += "\n\n" + self.metadata.notes
|
||||
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
if self.metadata.webLink is not None and self.metadata.webLink not in self.metadata.comments:
|
||||
self.metadata.comments += "\n\n" + self.metadata.webLink
|
||||
|
||||
return self.metadata
|
||||
|
||||
|
||||
|
260
comet.py
Normal file
260
comet.py
Normal file
@ -0,0 +1,260 @@
|
||||
"""
|
||||
A python class to encapsulate CoMet data
|
||||
"""
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import zipfile
|
||||
from pprint import pprint
|
||||
import xml.etree.ElementTree as ET
|
||||
from genericmetadata import GenericMetadata
|
||||
import utils
|
||||
|
||||
class CoMet:
|
||||
|
||||
writer_synonyms = ['writer', 'plotter', 'scripter']
|
||||
penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ]
|
||||
inker_synonyms = [ 'inker', 'artist', 'finishes' ]
|
||||
colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ]
|
||||
letterer_synonyms = [ 'letterer']
|
||||
cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ]
|
||||
editor_synonyms = [ 'editor']
|
||||
|
||||
def metadataFromString( self, string ):
|
||||
|
||||
tree = ET.ElementTree(ET.fromstring( string ))
|
||||
return self.convertXMLToMetadata( tree )
|
||||
|
||||
def stringFromMetadata( self, metadata ):
|
||||
|
||||
header = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
|
||||
tree = self.convertMetadataToXML( self, metadata )
|
||||
return header + 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("comet")
|
||||
root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/"
|
||||
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
root.attrib['xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
|
||||
|
||||
#helper func
|
||||
def assign( comet_entry, md_entry):
|
||||
if md_entry is not None:
|
||||
ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry)
|
||||
|
||||
# title is manditory
|
||||
if md.title is None:
|
||||
md.title = ""
|
||||
assign( 'title', md.title )
|
||||
assign( 'series', md.series )
|
||||
assign( 'issue', md.issue ) #must be int??
|
||||
assign( 'volume', md.volume )
|
||||
assign( 'description', md.comments )
|
||||
assign( 'publisher', md.publisher )
|
||||
assign( 'pages', md.pageCount )
|
||||
assign( 'format', md.format )
|
||||
assign( 'language', md.language )
|
||||
assign( 'rating', md.maturityRating )
|
||||
assign( 'price', md.price )
|
||||
assign( 'isVersionOf', md.isVersionOf )
|
||||
assign( 'rights', md.rights )
|
||||
assign( 'identifier', md.identifier )
|
||||
assign( 'lastMark', md.lastMark )
|
||||
assign( 'genre', md.genre ) # TODO repeatable
|
||||
|
||||
if md.characters is not None:
|
||||
char_list = [ c.strip() for c in md.characters.split(',') ]
|
||||
for c in char_list:
|
||||
assign( 'character', c )
|
||||
|
||||
if md.manga is not None and md.manga == "YesAndRightToLeft":
|
||||
assign( 'readingDirection', "rtl")
|
||||
|
||||
date_str = ""
|
||||
if md.year is not None:
|
||||
date_str = str(md.year).zfill(4)
|
||||
if md.month is not None:
|
||||
date_str += "-" + str(md.month).zfill(2)
|
||||
assign( 'date', date_str )
|
||||
|
||||
assign( 'coverImage', md.coverImage )
|
||||
|
||||
# need to specially process the credits, since they are structured differently than CIX
|
||||
credit_writer_list = list()
|
||||
credit_penciller_list = list()
|
||||
credit_inker_list = list()
|
||||
credit_colorist_list = list()
|
||||
credit_letterer_list = list()
|
||||
credit_cover_list = list()
|
||||
credit_editor_list = list()
|
||||
|
||||
# loop thru credits, and build a list for each role that CoMet supports
|
||||
for credit in metadata.credits:
|
||||
|
||||
if credit['role'].lower() in set( self.writer_synonyms ):
|
||||
ET.SubElement(root, 'writer').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.penciller_synonyms ):
|
||||
ET.SubElement(root, 'penciller').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.inker_synonyms ):
|
||||
ET.SubElement(root, 'inker').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.colorist_synonyms ):
|
||||
ET.SubElement(root, 'colorist').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.letterer_synonyms ):
|
||||
ET.SubElement(root, 'letterer').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.cover_synonyms ):
|
||||
ET.SubElement(root, 'coverDesigner').text = u"{0}".format(credit['person'])
|
||||
|
||||
if credit['role'].lower() in set( self.editor_synonyms ):
|
||||
ET.SubElement(root, 'editor').text = u"{0}".format(credit['person'])
|
||||
|
||||
|
||||
# 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 != 'comet':
|
||||
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.issue = xlate( 'issue' )
|
||||
md.volume = xlate( 'volume' )
|
||||
md.comments = xlate( 'description' )
|
||||
md.publisher = xlate( 'publisher' )
|
||||
md.language = xlate( 'language' )
|
||||
md.format = xlate( 'format' )
|
||||
md.pageCount = xlate( 'pages' )
|
||||
md.maturityRating = xlate( 'rating' )
|
||||
md.price = xlate( 'price' )
|
||||
md.isVersionOf = xlate( 'isVersionOf' )
|
||||
md.rights = xlate( 'rights' )
|
||||
md.identifier = xlate( 'identifier' )
|
||||
md.lastMark = xlate( 'lastMark' )
|
||||
md.genre = xlate( 'genre' ) # TODO - repeatable field
|
||||
|
||||
date = xlate( 'date' )
|
||||
if date is not None:
|
||||
parts = date.split('-')
|
||||
if len( parts) > 0:
|
||||
md.year = parts[0]
|
||||
if len( parts) > 1:
|
||||
md.month = parts[1]
|
||||
|
||||
md.coverImage = xlate( 'coverImage' )
|
||||
|
||||
readingDirection = xlate( 'readingDirection' )
|
||||
if readingDirection is not None and readingDirection == "rtl":
|
||||
md.manga = "YesAndRightToLeft"
|
||||
|
||||
# loop for character tags
|
||||
char_list = []
|
||||
for n in root:
|
||||
if n.tag == 'character':
|
||||
char_list.append(n.text.strip())
|
||||
md.characters = utils.listToString( char_list )
|
||||
|
||||
# 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'
|
||||
):
|
||||
metadata.addCredit( n.text.strip(), n.tag.title() )
|
||||
|
||||
if n.tag == 'coverDesigner':
|
||||
metadata.addCredit( n.text.strip(), "Cover" )
|
||||
|
||||
|
||||
metadata.isEmpty = False
|
||||
|
||||
return metadata
|
||||
|
||||
#verify that the string actually contains CoMet data in XML format
|
||||
def validateString( self, string ):
|
||||
try:
|
||||
tree = ET.ElementTree(ET.fromstring( string ))
|
||||
root = tree.getroot()
|
||||
if root.tag != 'comet':
|
||||
raise Exception
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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 )
|
||||
|
277
comicarchive.py
277
comicarchive.py
@ -29,6 +29,13 @@ if platform.system() == "Windows":
|
||||
import _subprocess
|
||||
import time
|
||||
|
||||
import StringIO
|
||||
try:
|
||||
import Image
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".") )
|
||||
import UnRAR2
|
||||
from UnRAR2.rar_exceptions import *
|
||||
@ -36,7 +43,8 @@ from UnRAR2.rar_exceptions import *
|
||||
from options import Options, MetaDataStyle
|
||||
from comicinfoxml import ComicInfoXml
|
||||
from comicbookinfo import ComicBookInfo
|
||||
from genericmetadata import GenericMetadata
|
||||
from comet import CoMet
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from filenameparser import FileNameParser
|
||||
|
||||
|
||||
@ -92,7 +100,6 @@ class ZipArchiver:
|
||||
# zip helper func
|
||||
def rebuildZipFile( self, exclude_list ):
|
||||
|
||||
# TODO: use tempfile.mkstemp
|
||||
# this recompresses the zip archive, without the files in the exclude_list
|
||||
#print "Rebuilding zip {0} without {1}".format( self.path, exclude_list )
|
||||
|
||||
@ -180,6 +187,26 @@ class ZipArchiver:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def copyFromArchive( self, otherArchive ):
|
||||
# Replace the current zip with one copied from another archive
|
||||
try:
|
||||
zout = zipfile.ZipFile (self.path, 'w')
|
||||
for fname in otherArchive.getArchiveFilenameList():
|
||||
data = otherArchive.readArchiveFile( fname )
|
||||
zout.writestr( fname, data )
|
||||
zout.close()
|
||||
|
||||
#preserve the old comment
|
||||
comment = otherArchive.getArchiveComment()
|
||||
if comment is not None:
|
||||
if not self.writeZipComment( self.path, comment ):
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
#------------------------------------------
|
||||
# RAR implementation
|
||||
@ -216,8 +243,10 @@ class RarArchiver:
|
||||
f.write( comment )
|
||||
f.close()
|
||||
|
||||
working_dir = os.path.dirname( os.path.abspath( self.path ) )
|
||||
|
||||
# use external program to write comment to Rar archive
|
||||
subprocess.call([self.rar_exe_path, 'c', '-c-', '-z' + tmp_name, self.path],
|
||||
subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path],
|
||||
startupinfo=self.startupinfo,
|
||||
stdout=self.devnull)
|
||||
|
||||
@ -249,13 +278,17 @@ class RarArchiver:
|
||||
tmp_folder = tempfile.mkdtemp()
|
||||
|
||||
tmp_file = os.path.join( tmp_folder, archive_file )
|
||||
|
||||
working_dir = os.path.dirname( os.path.abspath( self.path ) )
|
||||
|
||||
# TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt"
|
||||
# will need to create the subfolder above, I guess...
|
||||
f = open(tmp_file, 'w')
|
||||
f.write( data )
|
||||
f.close()
|
||||
|
||||
|
||||
# use external program to write file to Rar archive
|
||||
subprocess.call([self.rar_exe_path, 'a', '-c-', '-ep', self.path, tmp_file],
|
||||
subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file],
|
||||
startupinfo=self.startupinfo,
|
||||
stdout=self.devnull)
|
||||
|
||||
@ -385,7 +418,9 @@ class ComicArchive:
|
||||
def __init__( self, path ):
|
||||
self.path = path
|
||||
self.ci_xml_filename = 'ComicInfo.xml'
|
||||
|
||||
self.comet_default_filename = 'CoMet.xml'
|
||||
self.resetCache()
|
||||
|
||||
if self.zipTest():
|
||||
self.archive_type = self.ArchiveType.Zip
|
||||
self.archiver = ZipArchiver( self.path )
|
||||
@ -401,6 +436,14 @@ class ComicArchive:
|
||||
self.archive_type = self.ArchiveType.Unknown
|
||||
self.archiver = UnknownArchiver( self.path )
|
||||
|
||||
# Clears the cached data
|
||||
def resetCache( self ):
|
||||
self.has_cix = None
|
||||
self.has_cbi = None
|
||||
self.comet_filename = None
|
||||
self.page_count = None
|
||||
self.page_list = None
|
||||
|
||||
def setExternalRarProgram( self, rar_exe_path ):
|
||||
if self.isRar():
|
||||
self.archiver.rar_exe_path = rar_exe_path
|
||||
@ -470,15 +513,23 @@ class ComicArchive:
|
||||
return self.readCIX()
|
||||
elif style == MetaDataStyle.CBI:
|
||||
return self.readCBI()
|
||||
elif style == MetaDataStyle.COMET:
|
||||
return self.readCoMet()
|
||||
else:
|
||||
return GenericMetadata()
|
||||
|
||||
def writeMetadata( self, metadata, style ):
|
||||
|
||||
retcode = None
|
||||
if style == MetaDataStyle.CIX:
|
||||
return self.writeCIX( metadata )
|
||||
retcode = self.writeCIX( metadata )
|
||||
elif style == MetaDataStyle.CBI:
|
||||
return self.writeCBI( metadata )
|
||||
retcode = self.writeCBI( metadata )
|
||||
elif style == MetaDataStyle.COMET:
|
||||
retcode = self.writeCoMet( metadata )
|
||||
self.resetCache()
|
||||
return retcode
|
||||
|
||||
|
||||
def hasMetadata( self, style ):
|
||||
|
||||
@ -486,20 +537,22 @@ class ComicArchive:
|
||||
return self.hasCIX()
|
||||
elif style == MetaDataStyle.CBI:
|
||||
return self.hasCBI()
|
||||
elif style == MetaDataStyle.COMET:
|
||||
return self.hasCoMet()
|
||||
else:
|
||||
return False
|
||||
|
||||
def removeMetadata( self, style ):
|
||||
retcode = True
|
||||
if style == MetaDataStyle.CIX:
|
||||
return self.removeCIX()
|
||||
retcode = self.removeCIX()
|
||||
elif style == MetaDataStyle.CBI:
|
||||
return self.removeCBI()
|
||||
retcode = self.removeCBI()
|
||||
elif style == MetaDataStyle.COMET:
|
||||
retcode = self.removeCoMet()
|
||||
self.resetCache()
|
||||
return retcode
|
||||
|
||||
def getCoverPage(self):
|
||||
|
||||
# assume first page is the cover (for now)
|
||||
return self.getPage( 0 )
|
||||
|
||||
def getPage( self, index ):
|
||||
|
||||
image_data = None
|
||||
@ -523,31 +576,38 @@ class ComicArchive:
|
||||
|
||||
def getPageNameList( self , sort_list=True):
|
||||
|
||||
# get the list file names in the archive, and sort
|
||||
files = self.archiver.getArchiveFilenameList()
|
||||
|
||||
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
|
||||
if sort_list:
|
||||
files.sort(key=lambda x: x.lower())
|
||||
|
||||
# make a sub-list of image files
|
||||
page_list = []
|
||||
for name in files:
|
||||
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] ):
|
||||
page_list.append(name)
|
||||
if self.page_list is None:
|
||||
# get the list file names in the archive, and sort
|
||||
files = self.archiver.getArchiveFilenameList()
|
||||
|
||||
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
|
||||
if sort_list:
|
||||
files.sort(key=lambda x: x.lower())
|
||||
|
||||
# make a sub-list of image files
|
||||
self.page_list = []
|
||||
for name in files:
|
||||
if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png" ] and os.path.basename(name)[0] != "." ):
|
||||
self.page_list.append(name)
|
||||
|
||||
return page_list
|
||||
return self.page_list
|
||||
|
||||
def getNumberOfPages( self ):
|
||||
|
||||
return len( self.getPageNameList( sort_list=False ) )
|
||||
if self.page_count is None:
|
||||
self.page_count = len( self.getPageNameList( ) )
|
||||
return self.page_count
|
||||
|
||||
def readCBI( self ):
|
||||
raw_cbi = self.readRawCBI()
|
||||
if raw_cbi is None:
|
||||
return GenericMetadata()
|
||||
md = GenericMetadata()
|
||||
else:
|
||||
md = ComicBookInfo().metadataFromString( raw_cbi )
|
||||
|
||||
return ComicBookInfo().metadataFromString( raw_cbi )
|
||||
md.setDefaultPageList( self.getNumberOfPages() )
|
||||
|
||||
return md
|
||||
|
||||
def readRawCBI( self ):
|
||||
if ( not self.hasCBI() ):
|
||||
@ -555,8 +615,20 @@ class ComicArchive:
|
||||
|
||||
return self.archiver.getArchiveComment()
|
||||
|
||||
def hasCBI(self):
|
||||
if self.has_cbi is None:
|
||||
|
||||
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
|
||||
if not self.seemsToBeAComicArchive():
|
||||
self.has_cbi = False
|
||||
else:
|
||||
comment = self.archiver.getArchiveComment()
|
||||
self.has_cbi = ComicBookInfo().validateString( comment )
|
||||
|
||||
return self.has_cbi
|
||||
|
||||
def writeCBI( self, metadata ):
|
||||
self.applyArchiveInfoToMetadata( metadata )
|
||||
cbi_string = ComicBookInfo().stringFromMetadata( metadata )
|
||||
return self.archiver.setArchiveComment( cbi_string )
|
||||
|
||||
@ -566,13 +638,23 @@ class ComicArchive:
|
||||
def readCIX( self ):
|
||||
raw_cix = self.readRawCIX()
|
||||
if raw_cix is None:
|
||||
return GenericMetadata()
|
||||
md = GenericMetadata()
|
||||
else:
|
||||
md = ComicInfoXml().metadataFromString( raw_cix )
|
||||
|
||||
#validate the existing page list (make sure count is correct)
|
||||
if len ( md.pages ) != 0 :
|
||||
if len ( md.pages ) != self.getNumberOfPages():
|
||||
# pages array doesn't match the actual number of images we're seeing
|
||||
# in the archive, so discard the data
|
||||
md.pages = []
|
||||
|
||||
return ComicInfoXml().metadataFromString( raw_cix )
|
||||
if len( md.pages ) == 0:
|
||||
md.setDefaultPageList( self.getNumberOfPages() )
|
||||
return md
|
||||
|
||||
def readRawCIX( self ):
|
||||
if not self.hasCIX():
|
||||
print self.path, "doesn't has ComicInfo.xml data!"
|
||||
return None
|
||||
|
||||
return self.archiver.readArchiveFile( self.ci_xml_filename )
|
||||
@ -580,6 +662,7 @@ class ComicArchive:
|
||||
def writeCIX(self, metadata):
|
||||
|
||||
if metadata is not None:
|
||||
self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True )
|
||||
cix_string = ComicInfoXml().stringFromMetadata( metadata )
|
||||
return self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string )
|
||||
else:
|
||||
@ -590,22 +673,117 @@ class ComicArchive:
|
||||
return self.archiver.removeArchiveFile( self.ci_xml_filename )
|
||||
|
||||
def hasCIX(self):
|
||||
if not self.seemsToBeAComicArchive():
|
||||
return False
|
||||
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
|
||||
return True
|
||||
if self.has_cix is None:
|
||||
|
||||
if not self.seemsToBeAComicArchive():
|
||||
self.has_cix = False
|
||||
elif self.ci_xml_filename in self.archiver.getArchiveFilenameList():
|
||||
self.has_cix = True
|
||||
else:
|
||||
self.has_cix = False
|
||||
return self.has_cix
|
||||
|
||||
|
||||
def readCoMet( self ):
|
||||
raw_comet = self.readRawCoMet()
|
||||
if raw_comet is None:
|
||||
md = GenericMetadata()
|
||||
else:
|
||||
md = CoMet().metadataFromString( raw_comet )
|
||||
|
||||
md.setDefaultPageList( self.getNumberOfPages() )
|
||||
#use the coverImage value from the comet_data to mark the cover in this struct
|
||||
# walk through list of images in file, and find the matching one for md.coverImage
|
||||
# need to remove the existing one in the default
|
||||
if md.coverImage is not None:
|
||||
cover_idx = 0
|
||||
for idx,f in enumerate(self.getPageNameList()):
|
||||
if md.coverImage == f:
|
||||
cover_idx = idx
|
||||
break
|
||||
if cover_idx != 0:
|
||||
del (md.pages[0]['Type'] )
|
||||
md.pages[ cover_idx ]['Type'] = PageType.FrontCover
|
||||
|
||||
|
||||
return md
|
||||
|
||||
def readRawCoMet( self ):
|
||||
if not self.hasCoMet():
|
||||
print self.path, "doesn't have CoMet data!"
|
||||
return None
|
||||
|
||||
return self.archiver.readArchiveFile( self.comet_filename )
|
||||
|
||||
def writeCoMet(self, metadata):
|
||||
|
||||
if metadata is not None:
|
||||
if not self.hasCoMet():
|
||||
self.comet_filename = self.comet_default_filename
|
||||
|
||||
self.applyArchiveInfoToMetadata( metadata )
|
||||
# Set the coverImage value, if it's not the first page
|
||||
cover_idx = int(metadata.getCoverPageIndexList()[0])
|
||||
if cover_idx != 0:
|
||||
metadata.coverImage = self.getPageName( cover_idx )
|
||||
|
||||
comet_string = CoMet().stringFromMetadata( metadata )
|
||||
return self.archiver.writeArchiveFile( self.comet_filename, comet_string )
|
||||
else:
|
||||
return False
|
||||
|
||||
def hasCBI(self):
|
||||
|
||||
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
|
||||
if not self.seemsToBeAComicArchive():
|
||||
|
||||
def removeCoMet( self ):
|
||||
if self.hasCoMet():
|
||||
retcode = self.archiver.removeArchiveFile( self.comet_filename )
|
||||
self.comet_filename = None
|
||||
return retcode
|
||||
return True
|
||||
|
||||
def hasCoMet(self):
|
||||
if not self.seemsToBeAComicArchive():
|
||||
return False
|
||||
|
||||
#Use the existence of self.comet_filename as a cue that the tag block exists
|
||||
if self.comet_filename is None:
|
||||
#TODO look at all xml files in root, and search for CoMet data, get first
|
||||
for n in self.archiver.getArchiveFilenameList():
|
||||
if ( os.path.dirname(n) == "" and
|
||||
os.path.splitext(n)[1].lower() == '.xml'):
|
||||
# read in XML file, and validate it
|
||||
data = self.archiver.readArchiveFile( n )
|
||||
if CoMet().validateString( data ):
|
||||
# since we found it, save it!
|
||||
self.comet_filename = n
|
||||
return True
|
||||
# if we made it through the loop, no CoMet here...
|
||||
return False
|
||||
|
||||
else:
|
||||
return True
|
||||
|
||||
comment = self.archiver.getArchiveComment()
|
||||
return ComicBookInfo().validateString( comment )
|
||||
def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False):
|
||||
md.pageCount = self.getNumberOfPages()
|
||||
|
||||
if calc_page_sizes:
|
||||
for p in md.pages:
|
||||
idx = int( p['Image'] )
|
||||
if pil_available:
|
||||
if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p:
|
||||
data = self.getPage( idx )
|
||||
|
||||
im = Image.open(StringIO.StringIO(data))
|
||||
w,h = im.size
|
||||
|
||||
p['ImageSize'] = str(len(data))
|
||||
p['ImageHeight'] = str(h)
|
||||
p['ImageWidth'] = str(w)
|
||||
else:
|
||||
if 'ImageSize' not in p:
|
||||
data = self.getPage( idx )
|
||||
p['ImageSize'] = str(len(data))
|
||||
|
||||
|
||||
|
||||
def metadataFromFilename( self ):
|
||||
|
||||
metadata = GenericMetadata()
|
||||
@ -627,3 +805,12 @@ class ComicArchive:
|
||||
metadata.isEmpty = False
|
||||
|
||||
return metadata
|
||||
|
||||
def exportAsZip( self, zipfilename ):
|
||||
if self.archive_type == self.ArchiveType.Zip:
|
||||
# nothing to do, we're already a zip
|
||||
return True
|
||||
|
||||
zip_archiver = ZipArchiver( zipfilename )
|
||||
return zip_archiver.copyFromArchive( self.archiver )
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
A python class to encapsulate the ComicBookInfo data and file handling
|
||||
A python class to encapsulate the ComicBookInfo data
|
||||
"""
|
||||
|
||||
"""
|
||||
@ -25,6 +25,7 @@ import zipfile
|
||||
|
||||
from genericmetadata import GenericMetadata
|
||||
import utils
|
||||
import ctversion
|
||||
|
||||
class ComicBookInfo:
|
||||
|
||||
@ -62,6 +63,12 @@ class ComicBookInfo:
|
||||
metadata.criticalRating = xlate( 'rating' )
|
||||
metadata.tags = xlate( 'tags' )
|
||||
|
||||
# make sure credits and tags are at least empty lists and not None
|
||||
if metadata.credits is None:
|
||||
metadata.credits = []
|
||||
if metadata.tags is None:
|
||||
metadata.tags = []
|
||||
|
||||
#need to massage the language string to be ISO
|
||||
if metadata.language is not None:
|
||||
# reverse look-up
|
||||
@ -96,7 +103,7 @@ class ComicBookInfo:
|
||||
|
||||
# Create the dictionary that we will convert to JSON text
|
||||
cbi = dict()
|
||||
cbi_container = {'appID' : 'ComicTagger/0.1',
|
||||
cbi_container = {'appID' : 'ComicTagger/' + ctversion.version,
|
||||
'lastModified' : str(datetime.now()),
|
||||
'ComicBookInfo/1.0' : cbi }
|
||||
|
||||
@ -104,18 +111,28 @@ class ComicBookInfo:
|
||||
def assign( cbi_entry, md_entry):
|
||||
if md_entry is not None:
|
||||
cbi[cbi_entry] = md_entry
|
||||
|
||||
#helper func
|
||||
def toInt(s):
|
||||
i = None
|
||||
if type(s) == str or type(s) == int:
|
||||
try:
|
||||
i = int(s)
|
||||
except ValueError:
|
||||
pass
|
||||
return i
|
||||
|
||||
assign( 'series', metadata.series )
|
||||
assign( 'title', metadata.title )
|
||||
assign( 'issue', metadata.issue )
|
||||
assign( 'publisher', metadata.publisher )
|
||||
assign( 'publicationMonth', metadata.month )
|
||||
assign( 'publicationYear', metadata.year )
|
||||
assign( 'numberOfIssues', metadata.issueCount )
|
||||
assign( 'publicationMonth', toInt(metadata.month) )
|
||||
assign( 'publicationYear', toInt(metadata.year) )
|
||||
assign( 'numberOfIssues', toInt(metadata.issueCount) )
|
||||
assign( 'comments', metadata.comments )
|
||||
assign( 'genre', metadata.genre )
|
||||
assign( 'volume', metadata.volume )
|
||||
assign( 'numberOfVolumes', metadata.volumeCount )
|
||||
assign( 'volume', toInt(metadata.volume) )
|
||||
assign( 'numberOfVolumes', toInt(metadata.volumeCount) )
|
||||
assign( 'language', utils.getLanguageFromISO(metadata.language) )
|
||||
assign( 'country', metadata.country )
|
||||
assign( 'rating', metadata.criticalRating )
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
A python class to encapsulate ComicRack's ComicInfo.xml data and file handling
|
||||
A python class to encapsulate ComicRack's ComicInfo.xml data
|
||||
"""
|
||||
|
||||
"""
|
||||
@ -270,6 +270,7 @@ class ComicInfoXml:
|
||||
if pages_node is not None:
|
||||
for page in pages_node:
|
||||
metadata.pages.append( page.attrib )
|
||||
#print page.attrib
|
||||
|
||||
metadata.isEmpty = False
|
||||
|
||||
|
512
comictagger.py
512
comictagger.py
@ -28,7 +28,10 @@ import time
|
||||
from pprint import pprint
|
||||
import json
|
||||
import platform
|
||||
import locale
|
||||
|
||||
filename_encoding = sys.getfilesystemencoding()
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt4 import QtCore, QtGui
|
||||
@ -43,24 +46,151 @@ from comicarchive import ComicArchive
|
||||
from issueidentifier import IssueIdentifier
|
||||
from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from filerenamer import FileRenamer
|
||||
from cbltransformer import CBLTransformer
|
||||
|
||||
import utils
|
||||
import codecs
|
||||
|
||||
class MultipleMatch():
|
||||
def __init__( self, filename, match_list):
|
||||
self.filename = filename
|
||||
self.matches = match_list
|
||||
|
||||
class OnlineMatchResults():
|
||||
def __init__(self):
|
||||
self.goodMatches = []
|
||||
self.noMatches = []
|
||||
self.multipleMatches = []
|
||||
self.writeFailures = []
|
||||
|
||||
#-----------------------------
|
||||
|
||||
def actual_issue_data_fetch( match, settings ):
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( match['volume_id'], match['issue_number'], settings )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
|
||||
return cv_md
|
||||
|
||||
def actual_metadata_save( ca, opts, md ):
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print "The tag save seemed to fail!"
|
||||
return False
|
||||
else:
|
||||
print "Save complete."
|
||||
else:
|
||||
if opts.terse:
|
||||
print "dry-run option was set, so nothing was written"
|
||||
else:
|
||||
print "dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print u"{0}".format(md)
|
||||
return True
|
||||
|
||||
|
||||
def post_process_matches( match_results, opts, settings ):
|
||||
# now go through the match results
|
||||
if opts.show_save_summary:
|
||||
if len( match_results.goodMatches ) > 0:
|
||||
print "\nSuccessful matches:"
|
||||
print "------------------"
|
||||
for f in match_results.goodMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.noMatches ) > 0:
|
||||
print "\nNo matches:"
|
||||
print "------------------"
|
||||
for f in match_results.noMatches:
|
||||
print f
|
||||
|
||||
if len( match_results.writeFailures ) > 0:
|
||||
print "\nFile Write Failures:"
|
||||
print "------------------"
|
||||
for f in match_results.writeFailures:
|
||||
print f
|
||||
|
||||
if not opts.show_save_summary and not opts.interactive:
|
||||
#jusr quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len( match_results.multipleMatches ) > 0:
|
||||
print "\nMultiple matches:"
|
||||
print "------------------"
|
||||
for mm in match_results.multipleMatches:
|
||||
print mm.filename
|
||||
for (counter,m) in enumerate(mm.matches):
|
||||
print u" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(counter,
|
||||
m['series'],
|
||||
m['issue_number'],
|
||||
m['publisher'],
|
||||
m['month'],
|
||||
m['year'],
|
||||
m['issue_title'])
|
||||
if opts.interactive:
|
||||
while True:
|
||||
i = raw_input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(len(mm.matches))) or i == 's':
|
||||
break
|
||||
if i != 's':
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = ComicArchive( mm.filename )
|
||||
md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style) )
|
||||
cv_md = actual_issue_data_fetch(mm.matches[int(i)], settings)
|
||||
md.overlay( cv_md )
|
||||
actual_metadata_save( ca, opts, md )
|
||||
|
||||
|
||||
print
|
||||
|
||||
|
||||
def cli_mode( opts, settings ):
|
||||
if len( opts.file_list ) < 1:
|
||||
print "You must specify at least one filename. Use the -h option for more info"
|
||||
return
|
||||
|
||||
for f in opts.file_list:
|
||||
if len( opts.file_list ) > 1:
|
||||
print "Processing: ", f
|
||||
process_file_cli( f, opts, settings )
|
||||
|
||||
def process_file_cli( filename, opts, settings ):
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
f = f.decode(filename_encoding, 'replace')
|
||||
process_file_cli( f, opts, settings, match_results )
|
||||
sys.stdout.flush()
|
||||
|
||||
post_process_matches( match_results, opts, settings )
|
||||
|
||||
|
||||
def create_local_metadata( opts, ca, has_desired_tags ):
|
||||
|
||||
md = GenericMetadata()
|
||||
md.setDefaultPageList( ca.getNumberOfPages() )
|
||||
|
||||
if has_desired_tags:
|
||||
md = ca.readMetadata( opts.data_style )
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if opts.parse_filename:
|
||||
md.overlay( ca.metadataFromFilename() )
|
||||
|
||||
# finally, use explicit stuff
|
||||
if opts.metadata is not None:
|
||||
md.overlay( opts.metadata )
|
||||
|
||||
return md
|
||||
|
||||
def process_file_cli( filename, opts, settings, match_results ):
|
||||
|
||||
batch_mode = len( opts.file_list ) > 1
|
||||
|
||||
ca = ComicArchive(filename)
|
||||
if settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( settings.rar_exe_path )
|
||||
@ -70,246 +200,254 @@ def process_file_cli( filename, opts, settings ):
|
||||
return
|
||||
|
||||
#if not ca.isWritableForStyle( opts.data_style ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ):
|
||||
if not ca.isWritable( ) and ( opts.delete_tags or opts.save_tags or opts.rename_file ):
|
||||
if not ca.isWritable( ) and ( opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file ):
|
||||
print "This archive is not writable for that tag type"
|
||||
return
|
||||
|
||||
|
||||
cix = False
|
||||
cbi = False
|
||||
if ca.hasCIX(): cix = True
|
||||
if ca.hasCBI(): cbi = True
|
||||
has = [ False, False, False ]
|
||||
if ca.hasCIX(): has[ MetaDataStyle.CIX ] = True
|
||||
if ca.hasCBI(): has[ MetaDataStyle.CBI ] = True
|
||||
if ca.hasCoMet(): has[ MetaDataStyle.COMET ] = True
|
||||
|
||||
if opts.print_tags:
|
||||
|
||||
|
||||
if opts.data_style is None:
|
||||
page_count = ca.getNumberOfPages()
|
||||
|
||||
brief = ""
|
||||
if ca.isZip(): brief = "ZIP archive "
|
||||
elif ca.isRar(): brief = "RAR archive "
|
||||
elif ca.isFolder(): brief = "Folder archive "
|
||||
|
||||
if batch_mode:
|
||||
brief = "{0}: ".format(filename)
|
||||
|
||||
if ca.isZip(): brief += "ZIP archive "
|
||||
elif ca.isRar(): brief += "RAR archive "
|
||||
elif ca.isFolder(): brief += "Folder archive "
|
||||
|
||||
brief += "({0: >3} pages)".format(page_count)
|
||||
brief += " tags:[ "
|
||||
|
||||
if not (cbi or cix):
|
||||
if not ( has[ MetaDataStyle.CBI ] or has[ MetaDataStyle.CIX ] or has[ MetaDataStyle.COMET ] ):
|
||||
brief += "none "
|
||||
else:
|
||||
if cbi: brief += "CBL "
|
||||
if cix: brief += "CR "
|
||||
if has[ MetaDataStyle.CBI ]: brief += "CBL "
|
||||
if has[ MetaDataStyle.CIX ]: brief += "CR "
|
||||
if has[ MetaDataStyle.COMET ]: brief += "CoMet "
|
||||
brief += "]"
|
||||
|
||||
print brief
|
||||
print
|
||||
|
||||
|
||||
if opts.terse:
|
||||
return
|
||||
|
||||
print
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CIX:
|
||||
if cix:
|
||||
if has[ MetaDataStyle.CIX ]:
|
||||
print "------ComicRack tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(ca.readRawCIX())
|
||||
print u"{0}".format(unicode(ca.readRawCIX(), errors='ignore'))
|
||||
else:
|
||||
print u"{0}".format(ca.readCIX())
|
||||
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
|
||||
if cbi:
|
||||
if has[ MetaDataStyle.CBI ]:
|
||||
print "------ComicBookLover tags--------"
|
||||
if opts.raw:
|
||||
pprint(json.loads(ca.readRawCBI()))
|
||||
else:
|
||||
print u"{0}".format(ca.readCBI())
|
||||
|
||||
|
||||
elif opts.delete_tags:
|
||||
if opts.data_style == MetaDataStyle.CIX:
|
||||
if cix:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeCIX():
|
||||
print "Tag removal seemed to fail!"
|
||||
else:
|
||||
print "Removed ComicRack tags."
|
||||
else:
|
||||
print "dry-run. ComicRack tags not removed"
|
||||
else:
|
||||
print "This archive doesn't have ComicRack tags."
|
||||
|
||||
if opts.data_style == MetaDataStyle.CBI:
|
||||
if cbi:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeCBI():
|
||||
print "Tag removal seemed to fail!"
|
||||
else:
|
||||
print "Removed ComicBookLover tags."
|
||||
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
|
||||
if has[ MetaDataStyle.COMET ]:
|
||||
print "------CoMet tags--------"
|
||||
if opts.raw:
|
||||
print u"{0}".format(ca.readRawCoMet())
|
||||
else:
|
||||
print "dry-run. ComicBookLover tags not removed"
|
||||
print u"{0}".format(ca.readCoMet())
|
||||
|
||||
|
||||
elif opts.delete_tags:
|
||||
style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if has[ opts.data_style ]:
|
||||
if not opts.dryrun:
|
||||
if not ca.removeMetadata( opts.data_style ):
|
||||
print "{0}: Tag removal seemed to fail!".format( filename )
|
||||
else:
|
||||
print "{0}: Removed {1} tags.".format( filename, style_name )
|
||||
else:
|
||||
print "This archive doesn't have ComicBookLover tags."
|
||||
print "{0}: dry-run. {1} tags not removed".format( filename, style_name )
|
||||
else:
|
||||
print "{0}: This archive doesn't have {1} tags to remove.".format( filename, style_name )
|
||||
|
||||
elif opts.copy_tags:
|
||||
dst_style_name = MetaDataStyle.name[ opts.data_style ]
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print "{0}: Already has {1} tags. Not overwriting.".format(filename, dst_style_name)
|
||||
return
|
||||
if opts.copy_source == opts.data_style:
|
||||
print "{0}: Destination and source are same: {1}. Nothing to do.".format(filename, dst_style_name)
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[ opts.copy_source ]
|
||||
if has[ opts.copy_source ]:
|
||||
if not opts.dryrun:
|
||||
md = ca.readMetadata( opts.copy_source )
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation:
|
||||
md = CBLTransformer( md, settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print u"{0}: Tag copy seemed to fail!".format( filename )
|
||||
else:
|
||||
print u"{0}: Copied {1} tags to {2} .".format( filename, src_style_name, dst_style_name )
|
||||
else:
|
||||
print u"{0}: dry-run. {1} tags not copied".format( filename, src_style_name )
|
||||
else:
|
||||
print u"{0}: This archive doesn't have {1} tags to copy.".format( filename, src_style_name )
|
||||
|
||||
|
||||
elif opts.save_tags:
|
||||
|
||||
# OK we're gonna do a save of some new data
|
||||
md = GenericMetadata()
|
||||
|
||||
# First read in existing data, if it's there
|
||||
if opts.data_style == MetaDataStyle.CIX and cix:
|
||||
md = ca.readCIX()
|
||||
elif opts.data_style == MetaDataStyle.CBI and cbi:
|
||||
md = ca.readCBI()
|
||||
|
||||
# now, overlay the new data onto the old, in order
|
||||
if opts.no_overwrite and has[ opts.data_style ]:
|
||||
print u"{0}: Already has {1} tags. Not overwriting.".format(filename, MetaDataStyle.name[ opts.data_style ])
|
||||
return
|
||||
|
||||
if opts.parse_filename:
|
||||
md.overlay( ca.metadataFromFilename() )
|
||||
|
||||
if opts.metadata is not None:
|
||||
md.overlay( opts.metadata )
|
||||
if batch_mode:
|
||||
print u"Processing {0}: ".format(filename)
|
||||
|
||||
md = create_local_metadata( opts, ca, has[ opts.data_style ] )
|
||||
|
||||
# finally, search online
|
||||
# now, search online
|
||||
if opts.search_online:
|
||||
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print "No metadata given to search online with!"
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
if opts.issue_id is not None:
|
||||
# we were given the actual ID to search with
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueDataByIssueID( opts.issue_id, settings )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return None
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage :
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
print "Online search: Multiple matches. Save aborted"
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
print "Online search: Low confidence match. Save aborted"
|
||||
return
|
||||
if not found_match:
|
||||
print "Online search: No match found. Save aborted"
|
||||
return
|
||||
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
try:
|
||||
cv_md = ComicVineTalker().fetchIssueData( matches[0]['volume_id'], matches[0]['issue_number'] )
|
||||
except ComicVineTalkerException:
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
return
|
||||
if cv_md is None:
|
||||
print "No match for ID {0} was found.".format(opts.issue_id)
|
||||
return None
|
||||
|
||||
md.overlay( cv_md )
|
||||
# ok, done building our metadata. time to save
|
||||
|
||||
#HACK
|
||||
#opts.dryrun = True
|
||||
#HACK
|
||||
|
||||
if not opts.dryrun:
|
||||
# write out the new data
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
print "The tag save seemed to fail!"
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, settings ).apply()
|
||||
else:
|
||||
print "Save complete."
|
||||
ii = IssueIdentifier( ca, settings )
|
||||
|
||||
if md is None or md.isEmpty:
|
||||
print "No metadata given to search online with!"
|
||||
return
|
||||
|
||||
def myoutput( text ):
|
||||
if opts.verbose:
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
|
||||
# use our overlayed MD struct to search
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
found_match = False
|
||||
choices = False
|
||||
low_confidence = False
|
||||
|
||||
if result == ii.ResultNoMatches:
|
||||
pass
|
||||
elif result == ii.ResultFoundMatchButBadCoverScore:
|
||||
low_confidence = True
|
||||
found_match = True
|
||||
elif result == ii.ResultFoundMatchButNotFirstPage :
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleMatchesWithBadImageScores:
|
||||
low_confidence = True
|
||||
choices = True
|
||||
elif result == ii.ResultOneGoodMatch:
|
||||
found_match = True
|
||||
elif result == ii.ResultMultipleGoodMatches:
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
print "Online search: Multiple matches. Save aborted"
|
||||
match_results.multipleMatches.append(MultipleMatch(filename,matches))
|
||||
return
|
||||
if low_confidence and opts.abortOnLowConfidence:
|
||||
print "Online search: Low confidence match. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
if not found_match:
|
||||
print "Online search: No match found. Save aborted"
|
||||
match_results.noMatches.append(filename)
|
||||
return
|
||||
|
||||
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
cv_md = actual_issue_data_fetch(matches[0], settings)
|
||||
if cv_md is None:
|
||||
return
|
||||
|
||||
md.overlay( cv_md )
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
if not actual_metadata_save( ca, opts, md ):
|
||||
match_results.writeFailures.append(filename)
|
||||
else:
|
||||
print "dry-run option was set, so nothing was written, but here is the final set of tags:"
|
||||
print u"{0}".format(md)
|
||||
match_results.goodMatches.append(filename)
|
||||
|
||||
elif opts.rename_file:
|
||||
|
||||
md = GenericMetadata()
|
||||
# First read in existing data, if it's there
|
||||
if opts.data_style == MetaDataStyle.CIX and cix:
|
||||
md = ca.readCIX()
|
||||
elif opts.data_style == MetaDataStyle.CBI and cbi:
|
||||
md = ca.readCBI()
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = u"{0}: ".format(filename)
|
||||
|
||||
if md.isEmpty:
|
||||
print "Comic archive contains no tags!"
|
||||
|
||||
if opts.data_style == MetaDataStyle.CIX:
|
||||
if cix:
|
||||
md = ca.readCIX()
|
||||
else:
|
||||
print "Comic archive contains no ComicRack tags!"
|
||||
|
||||
if opts.data_style == MetaDataStyle.CBI:
|
||||
if cbi:
|
||||
md = ca.readCBI()
|
||||
else:
|
||||
print "Comic archive contains no ComicBookLover tags!"
|
||||
|
||||
# TODO move this to ComicArchive, or maybe another class???
|
||||
new_name = ""
|
||||
if md.series is not None:
|
||||
new_name += "{0}".format( md.series )
|
||||
if opts.data_style is not None:
|
||||
use_tags = has[ opts.data_style ]
|
||||
else:
|
||||
print "Can't rename without series name"
|
||||
return
|
||||
use_tags = False
|
||||
|
||||
if md.volume is not None:
|
||||
new_name += " v{0}".format( md.volume )
|
||||
|
||||
if md.issue is not None:
|
||||
new_name += " #{:03d}".format( int(md.issue) )
|
||||
else:
|
||||
print "Can't rename without issue number"
|
||||
md = create_local_metadata( opts, ca, use_tags )
|
||||
|
||||
if md.series is None:
|
||||
print msg_hdr + "Can't rename without series name"
|
||||
return
|
||||
|
||||
if md.issueCount is not None:
|
||||
new_name += " (of {0})".format( md.issueCount )
|
||||
|
||||
if md.year is not None:
|
||||
new_name += " ({0})".format( md.year )
|
||||
|
||||
|
||||
if ca.isZip():
|
||||
new_name += ".cbz"
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_name += ".cbr"
|
||||
new_ext = ".cbr"
|
||||
else:
|
||||
new_ext = None # default
|
||||
|
||||
renamer = FileRenamer( md )
|
||||
renamer.setTemplate( settings.rename_template )
|
||||
renamer.setIssueZeroPadding( settings.rename_issue_number_padding )
|
||||
renamer.setSmartCleanup( settings.rename_use_smart_string_cleanup )
|
||||
|
||||
new_name = renamer.determineName( filename, ext=new_ext )
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print "Filename is already good!"
|
||||
print msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( filename ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, new_name ) )
|
||||
|
||||
#HACK
|
||||
#opts.dryrun = True
|
||||
#HACK
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.rename( filename, new_abs_path )
|
||||
else:
|
||||
print "dry-run option was set, so nothing was changed, but here is the proposed filename:"
|
||||
print "'{0}'".format(new_abs_path)
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print u"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)
|
||||
|
||||
|
||||
|
||||
@ -319,7 +457,7 @@ def process_file_cli( filename, opts, settings ):
|
||||
def main():
|
||||
|
||||
# try to make stdout encodings happy for unicode
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
|
||||
|
||||
opts = Options()
|
||||
opts.parseCmdLineArgs()
|
||||
@ -349,7 +487,10 @@ def main():
|
||||
app.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow( opts.filename, settings )
|
||||
fname = None
|
||||
if opts.filename is not None:
|
||||
fname = opts.filename.decode(filename_encoding, 'replace')
|
||||
tagger_window = TaggerWindow( fname, settings )
|
||||
tagger_window.show()
|
||||
|
||||
if platform.system() != "Linux":
|
||||
@ -361,8 +502,7 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
@ -25,6 +25,7 @@ import sys
|
||||
import os
|
||||
import datetime
|
||||
|
||||
import ctversion
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
class ComicVineCacher:
|
||||
@ -32,15 +33,38 @@ class ComicVineCacher:
|
||||
def __init__(self ):
|
||||
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
|
||||
self.db_file = os.path.join( self.settings_folder, "cv_cache.db")
|
||||
self.version_file = os.path.join( self.settings_folder, "cache_version.txt")
|
||||
|
||||
#verify that cache is from same version as this one
|
||||
data = ""
|
||||
try:
|
||||
with open( self.version_file, 'rb' ) as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
except:
|
||||
pass
|
||||
if data != ctversion.version:
|
||||
self.clearCache()
|
||||
|
||||
if not os.path.exists( self.db_file ):
|
||||
self.create_cache_db()
|
||||
|
||||
def clearCache( self ):
|
||||
os.unlink( self.db_file )
|
||||
try:
|
||||
os.unlink( self.db_file )
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
os.unlink( self.version_file )
|
||||
except:
|
||||
pass
|
||||
|
||||
def create_cache_db( self ):
|
||||
|
||||
#create the version file
|
||||
with open( self.version_file, 'w' ) as f:
|
||||
f.write( ctversion.version )
|
||||
|
||||
# this will wipe out any existing version
|
||||
open( self.db_file, 'w').close()
|
||||
|
||||
@ -68,6 +92,7 @@ class ComicVineCacher:
|
||||
"name TEXT," +
|
||||
"publisher TEXT," +
|
||||
"count_of_issues INT," +
|
||||
"start_year INT," +
|
||||
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
|
||||
"PRIMARY KEY (id) )"
|
||||
)
|
||||
@ -92,7 +117,7 @@ class ComicVineCacher:
|
||||
con = lite.connect( self.db_file )
|
||||
|
||||
with con:
|
||||
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
@ -130,6 +155,7 @@ class ComicVineCacher:
|
||||
results = list()
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
con.text_factory = unicode
|
||||
cur = con.cursor()
|
||||
|
||||
|
||||
@ -178,6 +204,7 @@ class ComicVineCacher:
|
||||
"name": cv_volume_record['name'],
|
||||
"publisher": pub_name,
|
||||
"count_of_issues": cv_volume_record['count_of_issues'],
|
||||
"start_year": cv_volume_record['start_year'],
|
||||
"timestamp": timestamp
|
||||
}
|
||||
self.upsert( cur, "volumes", "id", cv_volume_record['id'], data)
|
||||
@ -202,6 +229,7 @@ class ComicVineCacher:
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
# purge stale volume info
|
||||
a_week_ago = datetime.datetime.today()-datetime.timedelta(days=7)
|
||||
@ -212,7 +240,7 @@ class ComicVineCacher:
|
||||
cur.execute( "DELETE FROM Issues WHERE timestamp < ?", [ str(a_month_ago) ] )
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues FROM Volumes WHERE id = ?", [ volume_id ] )
|
||||
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [ volume_id ] )
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
@ -227,6 +255,7 @@ class ComicVineCacher:
|
||||
result['publisher'] = dict()
|
||||
result['publisher']['name'] = row[2]
|
||||
result['count_of_issues'] = row[3]
|
||||
result['start_year'] = row[4]
|
||||
result['issues'] = list()
|
||||
|
||||
cur.execute("SELECT id,name,issue_number,image_url,image_hash FROM Issues WHERE volume_id = ?", [ volume_id ] )
|
||||
@ -252,6 +281,7 @@ class ComicVineCacher:
|
||||
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
@ -270,6 +300,7 @@ class ComicVineCacher:
|
||||
con = lite.connect( self.db_file )
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
con.text_factory = unicode
|
||||
|
||||
cur.execute("SELECT image_url,thumb_image_url,publish_month,publish_year FROM Issues WHERE id=?", [ issue_id ])
|
||||
row = cur.fetchone()
|
||||
|
@ -43,6 +43,8 @@ import utils
|
||||
from settings import ComicTaggerSettings
|
||||
from comicvinecacher import ComicVineCacher
|
||||
from genericmetadata import GenericMetadata
|
||||
from issuestring import IssueString
|
||||
|
||||
|
||||
class ComicVineTalkerException(Exception):
|
||||
pass
|
||||
@ -91,7 +93,8 @@ class ComicVineTalker(QObject):
|
||||
|
||||
original_series_name = series_name
|
||||
|
||||
series_name = urllib.quote_plus(str(series_name))
|
||||
series_name = urllib.quote_plus(series_name.encode("utf-8"))
|
||||
#series_name = urllib.quote_plus(unicode(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"
|
||||
|
||||
content = self.getUrlContent(search_url)
|
||||
@ -175,7 +178,7 @@ class ComicVineTalker(QObject):
|
||||
return volume_results
|
||||
|
||||
|
||||
def fetchIssueData( self, series_id, issue_number ):
|
||||
def fetchIssueData( self, series_id, issue_number, settings ):
|
||||
|
||||
volume_results = self.fetchVolumeData( series_id )
|
||||
|
||||
@ -198,16 +201,35 @@ class ComicVineTalker(QObject):
|
||||
else:
|
||||
return None
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
return self.mapCVDataToMetadata( volume_results, issue_results, settings )
|
||||
|
||||
def fetchIssueDataByIssueID( self, issue_id, settings ):
|
||||
|
||||
issue_url = "http://api.comicvine.com/issue/" + str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
|
||||
content = self.getUrlContent(issue_url)
|
||||
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']
|
||||
|
||||
volume_results = self.fetchVolumeData( issue_results['volume']['id'] )
|
||||
|
||||
# now, map the comicvine data to generic metadata
|
||||
md = self.mapCVDataToMetadata( volume_results, issue_results, settings )
|
||||
md.isEmpty = False
|
||||
return md
|
||||
|
||||
def mapCVDataToMetadata(self, volume_results, issue_results, settings ):
|
||||
|
||||
# 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 )
|
||||
num_s = IssueString(issue_results['issue_number']).asString()
|
||||
|
||||
metadata.issue = num_s
|
||||
metadata.title = issue_results['name']
|
||||
@ -216,7 +238,9 @@ class ComicVineTalker(QObject):
|
||||
metadata.year = issue_results['publish_year']
|
||||
#metadata.issueCount = volume_results['count_of_issues']
|
||||
metadata.comments = self.cleanup_html(issue_results['description'])
|
||||
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = volume_results['start_year']
|
||||
|
||||
metadata.notes = "Tagged with ComicTagger app using info from Comic Vine."
|
||||
#metadata.notes += issue_results['site_detail_url']
|
||||
|
||||
@ -227,7 +251,7 @@ class ComicVineTalker(QObject):
|
||||
for role in person['roles']:
|
||||
# can we determine 'primary' from CV??
|
||||
role_name = role['role'].title()
|
||||
metadata.addCredit( person['name'], role['role'].title(), False )
|
||||
metadata.addCredit( person['name'], role['role'].title(), False )
|
||||
|
||||
character_credits = issue_results['character_credits']
|
||||
character_list = list()
|
||||
@ -248,11 +272,12 @@ class ComicVineTalker(QObject):
|
||||
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
|
||||
|
||||
arc_list = []
|
||||
for arc in story_arc_credits:
|
||||
arc_list.append(arc['name'])
|
||||
if len(arc_list) > 0:
|
||||
metadata.storyArc = utils.listToString(arc_list)
|
||||
|
||||
return metadata
|
||||
|
||||
def cleanup_html( self, string):
|
||||
|
@ -30,7 +30,7 @@ class CreditEditorWindow(QtGui.QDialog):
|
||||
ModeNew = 1
|
||||
|
||||
|
||||
def __init__(self, parent, mode, role, name ):
|
||||
def __init__(self, parent, mode, role, name, primary ):
|
||||
super(CreditEditorWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'crediteditorwindow.ui' ), self)
|
||||
@ -64,10 +64,33 @@ class CreditEditorWindow(QtGui.QDialog):
|
||||
self.cbRole.setEditText( role )
|
||||
else:
|
||||
self.cbRole.setCurrentIndex( i )
|
||||
|
||||
if primary:
|
||||
self.cbPrimary.setCheckState( QtCore.Qt.Checked )
|
||||
|
||||
self.cbRole.currentIndexChanged.connect(self.roleChanged)
|
||||
self.cbRole.editTextChanged.connect(self.roleChanged)
|
||||
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def updatePrimaryButton( self ):
|
||||
enabled =self.currentRoleCanBePrimary()
|
||||
self.cbPrimary.setEnabled( enabled )
|
||||
|
||||
def currentRoleCanBePrimary( self ):
|
||||
role = self.cbRole.currentText()
|
||||
if str(role).lower() == "writer" or str(role).lower() == "artist":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def roleChanged( self, s ):
|
||||
self.updatePrimaryButton()
|
||||
|
||||
def getCredits( self ):
|
||||
return self.cbRole.currentText(), self.leName.text()
|
||||
|
||||
primary = self.currentRoleCanBePrimary() and self.cbPrimary.isChecked()
|
||||
return self.cbRole.currentText(), self.leName.text(), primary
|
||||
|
||||
|
||||
def accept( self ):
|
||||
if self.cbRole.currentText() == "" or self.leName.text() == "":
|
||||
|
@ -66,6 +66,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QCheckBox" name="cbPrimary">
|
||||
<property name="text">
|
||||
<string>Primary</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file should contan only these comments, and the line below.
|
||||
# Used by packaging makefiles and app
|
||||
version="0.9.0-beta"
|
||||
version="0.9.5-beta"
|
@ -80,8 +80,11 @@ class FileNameParser:
|
||||
|
||||
# first, look for multiple "--", this mean's it's formatted differently from most:
|
||||
if "--" in filename:
|
||||
# the pattern seems to be that anything to left of the first "--" is the series name follow
|
||||
# the pattern seems to be that anything to left of the first "--" is the series name followed by issue
|
||||
filename = filename.split("--")[0]
|
||||
elif "___" in filename:
|
||||
# the pattern seems to be that anything to left of the first "__" is the series name followed by issue
|
||||
filename = filename.split("__")[0]
|
||||
|
||||
# guess based on position
|
||||
|
||||
@ -89,8 +92,19 @@ class FileNameParser:
|
||||
tmpstr = self.fixSpaces(filename)
|
||||
word_list = tmpstr.split(' ')
|
||||
|
||||
#before we search, remove any kind of likely "of X" phrase
|
||||
for i in range(0, len(word_list)-2):
|
||||
if ( word_list[i].isdigit() and
|
||||
word_list[i+1] == "of" and
|
||||
word_list[i+2].isdigit() ):
|
||||
word_list[i+1] ="XXX"
|
||||
word_list[i+2] ="XXX"
|
||||
|
||||
|
||||
# assume the last number in the filename that is under 4 digits is the issue number
|
||||
for word in reversed(word_list):
|
||||
for word in reversed(word_list):
|
||||
if word[0] == "#":
|
||||
word = word[1:]
|
||||
if (
|
||||
(word.isdigit() and len(word) < 4) or
|
||||
(self.isPointIssue(word))
|
||||
@ -126,14 +140,15 @@ class FileNameParser:
|
||||
tmpstr = tmpstr.replace("#", " ")
|
||||
|
||||
if issue != "":
|
||||
# assume that issue substr has at least on space before it
|
||||
# assume that issue substr has at least one space before it
|
||||
issue_str = " " + str(issue)
|
||||
series = tmpstr.split(issue_str)[0]
|
||||
else:
|
||||
# no issue to work off of
|
||||
#!!! TODO we should look for the year, and split from that
|
||||
# and if that doesn't exist, remove parenthetical words
|
||||
# and if that doesn't exist, remove parenthetical phrases
|
||||
series = tmpstr
|
||||
series = re.sub( "\(.*\)", "", tmpstr)
|
||||
|
||||
volume = ""
|
||||
|
||||
@ -161,7 +176,7 @@ class FileNameParser:
|
||||
return year
|
||||
|
||||
def parseFilename( self, filename ):
|
||||
|
||||
|
||||
# remove the path
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
@ -171,6 +186,13 @@ class FileNameParser:
|
||||
#url decode, just in case
|
||||
filename = unquote(filename)
|
||||
|
||||
# sometimes archives get messed up names from too many decodings
|
||||
# often url encodings will break and leave "_28" and "_29" in place
|
||||
# of "(" and ")" see if there are a number of these, and replace them
|
||||
if filename.count("_28") > 1 and filename.count("_29") > 1:
|
||||
filename = filename.replace("_28", "(")
|
||||
filename = filename.replace("_29", ")")
|
||||
|
||||
# ----HACK
|
||||
# remove the first word that word is a 3 digit number.
|
||||
# some story arcs collection packs do this, but it's ugly
|
||||
|
111
filerenamer.py
Normal file
111
filerenamer.py
Normal file
@ -0,0 +1,111 @@
|
||||
"""
|
||||
Functions for renaming files based on metadata
|
||||
"""
|
||||
|
||||
"""
|
||||
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 os
|
||||
import re
|
||||
from issuestring import IssueString
|
||||
|
||||
class FileRenamer:
|
||||
def __init__( self, metadata ):
|
||||
self.setMetadata( metadata )
|
||||
self.setTemplate( "%series% v%volume% #%issue% (of %issuecount%) (%year%)" )
|
||||
self.smart_cleanup = True
|
||||
self.issue_zero_padding = 3
|
||||
|
||||
def setMetadata( self, metadata ):
|
||||
self.metdata = metadata
|
||||
|
||||
def setIssueZeroPadding( self, count ):
|
||||
self.issue_zero_padding = count
|
||||
|
||||
def setSmartCleanup( self, on ):
|
||||
self.smart_cleanup = on
|
||||
|
||||
def setTemplate( self, template ):
|
||||
self.template = template
|
||||
|
||||
def replaceToken( self, text, value, token ):
|
||||
#helper func
|
||||
def isToken( word ):
|
||||
return (word[0] == "%" and word[-1:] == "%")
|
||||
|
||||
if value is not None:
|
||||
return text.replace( token, unicode(value) )
|
||||
else:
|
||||
if self.smart_cleanup:
|
||||
# smart cleanup means we want to remove anything appended to token if it's empty
|
||||
# (e.g "#%issue%" or "v%volume%" )
|
||||
# (TODO: This could fail if there is more than one token appended together, I guess)
|
||||
text_list = text.split()
|
||||
|
||||
#special case for issuecount, remove preceding non-token word, as in "...(of %issuecount%)..."
|
||||
if token == '%issuecount%':
|
||||
for idx,word in enumerate( text_list ):
|
||||
if token in word and not isToken(text_list[idx -1]) :
|
||||
text_list[idx -1] = ""
|
||||
|
||||
text_list = [ x for x in text_list if token not in x ]
|
||||
return " ".join( text_list )
|
||||
else:
|
||||
return text.replace( token, "" )
|
||||
|
||||
def determineName( self, filename, ext=None ):
|
||||
|
||||
md = self.metdata
|
||||
new_name = self.template
|
||||
|
||||
#print u"{0}".format(md)
|
||||
|
||||
new_name = self.replaceToken( new_name, md.series, '%series%')
|
||||
new_name = self.replaceToken( new_name, md.volume, '%volume%')
|
||||
|
||||
if md.issue is not None:
|
||||
issue_str = u"{0}".format( IssueString(md.issue).asString(pad=self.issue_zero_padding) )
|
||||
else:
|
||||
issue_str = None
|
||||
new_name = self.replaceToken( new_name, issue_str, '%issue%')
|
||||
|
||||
new_name = self.replaceToken( new_name, md.issueCount, '%issuecount%')
|
||||
new_name = self.replaceToken( new_name, md.year, '%year%')
|
||||
new_name = self.replaceToken( new_name, md.publisher, '%publisher%')
|
||||
new_name = self.replaceToken( new_name, md.title, '%title%')
|
||||
|
||||
if self.smart_cleanup:
|
||||
|
||||
# remove empty braces,brackets, parentheses
|
||||
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name )
|
||||
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name )
|
||||
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name )
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]+\s+", "- ", new_name )
|
||||
new_name = re.sub("(\s-)+", " -", new_name )
|
||||
|
||||
# remove duplicate spaces
|
||||
new_name = u" ".join(new_name.split())
|
||||
|
||||
if ext is None:
|
||||
ext = os.path.splitext( filename )[1]
|
||||
|
||||
new_name += ext
|
||||
|
||||
return new_name
|
||||
|
||||
|
217
fileselectionlist.py
Normal file
217
fileselectionlist.py
Normal file
@ -0,0 +1,217 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
A PyQt4 widget for managing list of files
|
||||
"""
|
||||
|
||||
"""
|
||||
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 os
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from comicarchive import ComicArchive
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
|
||||
class FileTableWidget( QTableWidget ):
|
||||
|
||||
def __init__(self, parent ):
|
||||
super(FileTableWidget, self).__init__(parent)
|
||||
|
||||
|
||||
self.setColumnCount(5)
|
||||
self.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
|
||||
self.horizontalHeader().setStretchLastSection( True )
|
||||
|
||||
|
||||
class FileTableWidgetItem(QTableWidgetItem):
|
||||
def __lt__(self, other):
|
||||
return (self.data(Qt.UserRole).toBool() <
|
||||
other.data(Qt.UserRole).toBool())
|
||||
|
||||
|
||||
class FileInfo( ):
|
||||
def __init__(self, path, ca, cix_md, cbi_md ):
|
||||
self.path = path
|
||||
self.cix_md = cix_md
|
||||
self.cbi_md = cbi_md
|
||||
self.ca = ca
|
||||
|
||||
class FileSelectionList(QWidget):
|
||||
|
||||
selectionChanged = pyqtSignal(QVariant)
|
||||
|
||||
def __init__(self, parent , settings ):
|
||||
super(FileSelectionList, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'fileselectionlist.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
#self.twList = FileTableWidget( self )
|
||||
#gridlayout = QGridLayout( self )
|
||||
#gridlayout.addWidget( self.twList )
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
self.twList.itemSelectionChanged.connect( self.itemSelectionChangedCB )
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
self.droppedFiles = None
|
||||
if event.mimeData().hasUrls():
|
||||
|
||||
# walk through the URL list and build a file list
|
||||
for url in event.mimeData().urls():
|
||||
if url.isValid() and url.scheme() == "file":
|
||||
if self.droppedFiles is None:
|
||||
self.droppedFiles = []
|
||||
self.droppedFiles.append(url.toLocalFile())
|
||||
|
||||
if self.droppedFiles is not None:
|
||||
event.accept()
|
||||
|
||||
def dropEvent(self, event):
|
||||
self.addPathList( self.droppedFiles)
|
||||
event.accept()
|
||||
|
||||
def addPathList( self, pathlist ):
|
||||
filelist = []
|
||||
for p in pathlist:
|
||||
# if path is a folder, walk it recursivly, and all files underneath
|
||||
if os.path.isdir( unicode(p)):
|
||||
for root,dirs,files in os.walk( unicode(p) ):
|
||||
for f in files:
|
||||
filelist.append(os.path.join(root,unicode(f)))
|
||||
else:
|
||||
filelist.append(unicode(p))
|
||||
|
||||
# we now have a list of files to add
|
||||
|
||||
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), self)
|
||||
progdialog.setWindowTitle( "Adding Files" )
|
||||
progdialog.setWindowModality(Qt.WindowModal)
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
for idx,f in enumerate(filelist):
|
||||
QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(idx)
|
||||
self.addPathItem( f )
|
||||
|
||||
progdialog.close()
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
#Maybe set a max size??
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
|
||||
def isListDupe( self, path ):
|
||||
r = 0
|
||||
while r < self.twList.rowCount():
|
||||
fi = self.twList.item(r, 0).data( Qt.UserRole ).toPyObject()
|
||||
if fi.path == path:
|
||||
return True
|
||||
r = r + 1
|
||||
|
||||
return False
|
||||
|
||||
def addPathItem( self, path):
|
||||
path = unicode( path )
|
||||
#print "processing", path
|
||||
|
||||
if self.isListDupe(path):
|
||||
return
|
||||
|
||||
ca = ComicArchive( path )
|
||||
if self.settings.rar_exe_path != "":
|
||||
ca.setExternalRarProgram( self.settings.rar_exe_path )
|
||||
|
||||
if ca.seemsToBeAComicArchive() :
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow( row )
|
||||
|
||||
cix_md = None
|
||||
cbi_md = None
|
||||
|
||||
has_cix = ca.hasCIX()
|
||||
if has_cix:
|
||||
cix_md = ca.readCIX()
|
||||
|
||||
has_cbi = ca.hasCBI()
|
||||
if has_cbi:
|
||||
cbi_md = ca.readCBI()
|
||||
|
||||
fi = FileInfo( path, ca, cix_md, cbi_md )
|
||||
|
||||
item_text = os.path.split(path)[1]
|
||||
item = QTableWidgetItem(item_text)
|
||||
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
item.setData( Qt.UserRole , fi )
|
||||
item.setData( Qt.ToolTipRole ,item_text)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = os.path.split(path)[0]
|
||||
item = QTableWidgetItem(item_text)
|
||||
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
item.setData( Qt.ToolTipRole ,item_text)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
# Attempt to use a special checkbox widget in the cell.
|
||||
# Couldn't figure out how to disable it with "enabled" colors
|
||||
#w = QWidget()
|
||||
#cb = QCheckBox(w)
|
||||
#cb.setCheckState(Qt.Checked)
|
||||
#layout = QHBoxLayout()
|
||||
#layout.addWidget( cb )
|
||||
#layout.setAlignment(Qt.AlignHCenter)
|
||||
#layout.setMargin(2)
|
||||
#w.setLayout(layout)
|
||||
#self.twList.setCellWidget( row, 2, w )
|
||||
|
||||
item = FileTableWidgetItem()
|
||||
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
item.setTextAlignment(Qt.AlignHCenter)
|
||||
if has_cix:
|
||||
item.setCheckState(Qt.Checked)
|
||||
item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
item.setData(Qt.UserRole, False)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
item = FileTableWidgetItem()
|
||||
item.setFlags(Qt.ItemIsSelectable| Qt.ItemIsEnabled)
|
||||
item.setTextAlignment(Qt.AlignHCenter)
|
||||
if has_cbi:
|
||||
item.setCheckState(Qt.Checked)
|
||||
item.setData(Qt.UserRole, True)
|
||||
else:
|
||||
item.setData(Qt.UserRole, False)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
def itemSelectionChangedCB( self ):
|
||||
idx = self.twList.currentRow()
|
||||
|
||||
fi = self.twList.item(idx, 0).data( Qt.UserRole ).toPyObject()
|
||||
|
||||
#if fi.cix_md is not None:
|
||||
# print u"{0}".format(fi.cix_md)
|
||||
|
||||
self.selectionChanged.emit( QVariant(fi))
|
69
fileselectionlist.ui
Normal file
69
fileselectionlist.ui
Normal file
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>pageListEditor</class>
|
||||
<widget class="QWidget" name="pageListEditor">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>527</width>
|
||||
<height>323</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="twList">
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||
<number>61</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||
<number>36</number>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>File</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Path</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CR</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CBL</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -32,7 +32,6 @@ class PageType:
|
||||
Roundup = "Roundup"
|
||||
Story = "Story"
|
||||
Advertisment = "Advertisment"
|
||||
Story = "Story"
|
||||
Editorial = "Editorial"
|
||||
Letters = "Letters"
|
||||
Preview = "Preview"
|
||||
@ -93,11 +92,19 @@ class GenericMetadata:
|
||||
self.characters = None
|
||||
self.teams = None
|
||||
self.locations = None
|
||||
|
||||
|
||||
self.credits = list()
|
||||
self.tags = list()
|
||||
self.pages = list()
|
||||
|
||||
# Some CoMet-only items
|
||||
self.price = None
|
||||
self.isVersionOf = None
|
||||
self.rights = None
|
||||
self.identifier = None
|
||||
self.lastMark = None
|
||||
self.coverImage = None
|
||||
|
||||
def overlay( self, new_md ):
|
||||
# Overlay a metadata object on this one
|
||||
# that is, when the new object has non-None
|
||||
@ -125,24 +132,30 @@ class GenericMetadata:
|
||||
assign( "genre", new_md.genre )
|
||||
assign( "language", new_md.language )
|
||||
assign( "country", new_md.country )
|
||||
assign( "alternateSeries", new_md.criticalRating )
|
||||
assign( "criticalRating", new_md.criticalRating )
|
||||
assign( "alternateSeries", new_md.alternateSeries )
|
||||
assign( "alternateNumber", new_md.alternateNumber )
|
||||
assign( "alternateCount", new_md.alternateCount )
|
||||
assign( "imprint", new_md.imprint )
|
||||
assign( "webLink", new_md.webLink )
|
||||
assign( "webLink", new_md.webLink )
|
||||
assign( "format", new_md.format )
|
||||
assign( "manga", new_md.manga )
|
||||
assign( "blackAndWhite", new_md.blackAndWhite )
|
||||
assign( "maturityRating", new_md.maturityRating )
|
||||
assign( "scanInfo", new_md.scanInfo )
|
||||
assign( "scanInfo", new_md.scanInfo )
|
||||
assign( "storyArc", new_md.storyArc )
|
||||
assign( "seriesGroup", new_md.seriesGroup )
|
||||
assign( "scanInfo", new_md.scanInfo )
|
||||
assign( "characters", new_md.characters )
|
||||
assign( "teams", new_md.teams )
|
||||
assign( "locations", new_md.locations )
|
||||
assign( "comments", new_md.comments )
|
||||
assign( "notes", new_md.notes )
|
||||
|
||||
assign( "price", new_md.price )
|
||||
assign( "isVersionOf", new_md.isVersionOf )
|
||||
assign( "rights", new_md.rights )
|
||||
assign( "identifier", new_md.identifier )
|
||||
assign( "lastMark", new_md.lastMark )
|
||||
|
||||
self.overlayCredits( new_md.credits )
|
||||
# TODO
|
||||
@ -153,7 +166,8 @@ class GenericMetadata:
|
||||
# For now, go the easy route, where any overlay
|
||||
# value wipes out the whole list
|
||||
if len(new_md.tags) > 0:
|
||||
assign( "tags", new_md.tags )
|
||||
assign( "tags", new_md.tags )
|
||||
|
||||
if len(new_md.pages) > 0:
|
||||
assign( "pages", new_md.pages )
|
||||
|
||||
@ -173,7 +187,35 @@ class GenericMetadata:
|
||||
# otherwise, add it!
|
||||
else:
|
||||
self.addCredit( c['person'], c['role'], primary )
|
||||
|
||||
|
||||
def setDefaultPageList( self, count ):
|
||||
# generate a default page list, with the first page marked as the cover
|
||||
for i in range(count):
|
||||
page_dict = dict()
|
||||
page_dict['Image'] = str(i)
|
||||
if i == 0:
|
||||
page_dict['Type'] = PageType.FrontCover
|
||||
self.pages.append( page_dict )
|
||||
|
||||
def getArchivePageIndex( self, pagenum ):
|
||||
# convert the displayed page number to the page index of the file in the archive
|
||||
if pagenum < len( self.pages ):
|
||||
return int( self.pages[pagenum]['Image'] )
|
||||
else:
|
||||
return 0
|
||||
|
||||
def getCoverPageIndexList( self ):
|
||||
# return a list of archive page indices of cover pages
|
||||
coverlist = []
|
||||
for p in self.pages:
|
||||
if 'Type' in p and p['Type'] == PageType.FrontCover:
|
||||
coverlist.append( int(p['Image']))
|
||||
|
||||
if len(coverlist) == 0:
|
||||
coverlist.append( 0 )
|
||||
|
||||
return coverlist
|
||||
|
||||
def addCredit( self, person, role, primary = False ):
|
||||
|
||||
credit = dict()
|
||||
@ -229,6 +271,13 @@ class GenericMetadata:
|
||||
add_attr_string( "webLink" )
|
||||
add_attr_string( "format" )
|
||||
add_attr_string( "manga" )
|
||||
|
||||
add_attr_string( "price" )
|
||||
add_attr_string( "isVersionOf" )
|
||||
add_attr_string( "rights" )
|
||||
add_attr_string( "identifier" )
|
||||
add_attr_string( "lastMark" )
|
||||
|
||||
if self.blackAndWhite:
|
||||
add_attr_string( "blackAndWhite" )
|
||||
add_attr_string( "maturityRating" )
|
||||
@ -246,7 +295,7 @@ class GenericMetadata:
|
||||
for c in self.credits:
|
||||
primary = ""
|
||||
if c.has_key('primary') and c['primary']:
|
||||
primary == " [P]"
|
||||
primary = " [P]"
|
||||
add_string( "credit", c['role']+": "+c['person'] + primary)
|
||||
|
||||
# find the longest field name
|
||||
|
@ -17,11 +17,16 @@ class ImageHasher(object):
|
||||
|
||||
if path is None and data is None:
|
||||
raise IOError
|
||||
elif path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(StringIO.StringIO(data))
|
||||
|
||||
try:
|
||||
if path is not None:
|
||||
self.image = Image.open(path)
|
||||
else:
|
||||
self.image = Image.open(StringIO.StringIO(data))
|
||||
except:
|
||||
print "Image data seems corrupted!"
|
||||
# just generate a bogus image
|
||||
self.image = Image.new( "L", (1,1))
|
||||
|
||||
def average_hash(self):
|
||||
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
|
||||
|
@ -34,6 +34,7 @@ from genericmetadata import GenericMetadata
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagehasher import ImageHasher
|
||||
from imagefetcher import ImageFetcher, ImageFetcherException
|
||||
from issuestring import IssueString
|
||||
|
||||
import utils
|
||||
|
||||
@ -72,7 +73,8 @@ class IssueIdentifier:
|
||||
self.output_function = IssueIdentifier.defaultWriteOutput
|
||||
self.callback = None
|
||||
self.search_result = self.ResultNoMatches
|
||||
|
||||
self.cover_page_index = 0
|
||||
|
||||
def setScoreMinThreshold( self, thresh ):
|
||||
self.min_score_thresh = thresh
|
||||
|
||||
@ -105,11 +107,13 @@ class IssueIdentifier:
|
||||
return ImageHasher( data=image_data ).average_hash()
|
||||
|
||||
def getAspectRatio( self, image_data ):
|
||||
try:
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w,h = im.size
|
||||
return float(h)/float(w)
|
||||
except:
|
||||
return 1.5
|
||||
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
w,h = im.size
|
||||
return float(h)/float(w)
|
||||
|
||||
def cropCover( self, image_data ):
|
||||
|
||||
im = Image.open(StringIO.StringIO(image_data))
|
||||
@ -217,7 +221,7 @@ class IssueIdentifier:
|
||||
self.log_msg( "Sorry, but "+ opts.filename + " is not a comic archive!")
|
||||
return self.match_list
|
||||
|
||||
cover_image_data = ca.getCoverPage()
|
||||
cover_image_data = ca.getPage( self.cover_page_index )
|
||||
cover_hash = self.calculateHash( cover_image_data )
|
||||
|
||||
#check the apect ratio
|
||||
@ -253,7 +257,7 @@ class IssueIdentifier:
|
||||
comicVine = ComicVineTalker( )
|
||||
|
||||
#self.log_msg( ( "Searching for " + keys['series'] + "...")
|
||||
self.log_msg( "Searching for {0} #{1} ...".format( keys['series'], keys['issue_number']) )
|
||||
self.log_msg( u"Searching for {0} #{1} ...".format( keys['series'], keys['issue_number']) )
|
||||
try:
|
||||
cv_search_results = comicVine.searchForSeries( keys['series'] )
|
||||
except ComicVineTalkerException:
|
||||
@ -273,7 +277,7 @@ class IssueIdentifier:
|
||||
date_approved = True
|
||||
|
||||
# remove any series that starts after the issue year
|
||||
if keys['year'] is not None and keys['year'].isdigit():
|
||||
if keys['year'] is not None and str(keys['year']).isdigit():
|
||||
if int(keys['year']) < item['start_year']:
|
||||
date_approved = False
|
||||
|
||||
@ -315,7 +319,7 @@ class IssueIdentifier:
|
||||
counter += 1
|
||||
self.callback( counter, len(series_shortlist))
|
||||
|
||||
self.log_msg( "Fetching info for ID: {0} {1} ({2}) ...".format(
|
||||
self.log_msg( u"Fetching info for ID: {0} {1} ({2}) ...".format(
|
||||
series['id'],
|
||||
series['name'],
|
||||
series['start_year']), newline=False )
|
||||
@ -328,13 +332,8 @@ class IssueIdentifier:
|
||||
|
||||
issue_list = cv_series_results['issues']
|
||||
for issue in issue_list:
|
||||
num_s = IssueString(issue['issue_number']).asString()
|
||||
|
||||
# format the issue number string nicely, since it's usually something like "2.00"
|
||||
num_f = float(issue['issue_number'])
|
||||
num_s = str( int(math.floor(num_f)) )
|
||||
if math.floor(num_f) != num_f:
|
||||
num_s = str( num_f )
|
||||
|
||||
# look for a matching issue number
|
||||
if num_s == keys['issue_number']:
|
||||
# found a matching issue number! now get the issue data
|
||||
@ -368,7 +367,7 @@ class IssueIdentifier:
|
||||
score = min( score, score2 )
|
||||
|
||||
match = dict()
|
||||
match['series'] = "{0} ({1})".format(series['name'], series['start_year'])
|
||||
match['series'] = u"{0} ({1})".format(series['name'], series['start_year'])
|
||||
match['distance'] = score
|
||||
match['issue_number'] = num_s
|
||||
match['url_image_hash'] = url_image_hash
|
||||
@ -424,7 +423,7 @@ class IssueIdentifier:
|
||||
|
||||
self.log_msg( "Comparing to some other archive pages now..." )
|
||||
found = False
|
||||
for i in range( min(5, ca.getNumberOfPages())):
|
||||
for i in range( min(3, ca.getNumberOfPages())):
|
||||
image_data = ca.getPage(i)
|
||||
page_hash = self.calculateHash( image_data )
|
||||
distance = ImageHasher.hamming_distance(page_hash, self.match_list[0]['url_image_hash'])
|
||||
|
@ -28,6 +28,7 @@ from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
||||
from comicvinetalker import ComicVineTalker, ComicVineTalkerException
|
||||
from imagefetcher import ImageFetcher
|
||||
from settings import ComicTaggerSettings
|
||||
from issuestring import IssueString
|
||||
|
||||
class IssueSelectionWindow(QtGui.QDialog):
|
||||
|
||||
@ -94,12 +95,12 @@ class IssueSelectionWindow(QtGui.QDialog):
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = u"{0}".format(record['name'])
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
if float(record['issue_number']) == float(self.issue_number):
|
||||
if IssueString(record['issue_number']).asString() == IssueString(self.issue_number).asString():
|
||||
self.initial_id = record['id']
|
||||
|
||||
row += 1
|
||||
|
95
issuestring.py
Normal file
95
issuestring.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""
|
||||
Class for handling the odd permutations of an 'issue number' that the comics industry throws at us
|
||||
|
||||
e.g.:
|
||||
|
||||
"12"
|
||||
"12.1"
|
||||
"0"
|
||||
"-1"
|
||||
"5AU"
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
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 utils
|
||||
import math
|
||||
import re
|
||||
|
||||
class IssueString:
|
||||
def __init__(self, text):
|
||||
self.text = str(text)
|
||||
#strip out non float-y stuff
|
||||
tmp_num_str = re.sub('[^0-9.-]',"", self.text )
|
||||
|
||||
if tmp_num_str == "":
|
||||
self.num = None
|
||||
self.suffix = self.text
|
||||
|
||||
else:
|
||||
if tmp_num_str.count(".") > 1:
|
||||
#make sure it's a valid float or int.
|
||||
parts = tmp_num_str.split('.')
|
||||
self.num = float( parts[0] + '.' + parts[1] )
|
||||
else:
|
||||
self.num = float( tmp_num_str )
|
||||
|
||||
self.suffix = ""
|
||||
parts = self.text.split(tmp_num_str)
|
||||
if len( parts ) > 1 :
|
||||
self.suffix = parts[1]
|
||||
|
||||
def asString( self, pad = 0 ):
|
||||
#return the float, left side zero-padded, with suffix attached
|
||||
if self.num is None:
|
||||
return self.suffix
|
||||
|
||||
negative = self.num < 0
|
||||
|
||||
num_f = abs(self.num)
|
||||
|
||||
num_int = int( num_f )
|
||||
num_s = str( num_int )
|
||||
if float( num_int ) != num_f:
|
||||
num_s = str( num_f )
|
||||
|
||||
num_s += self.suffix
|
||||
|
||||
# create padding
|
||||
padding = ""
|
||||
l = len( str(num_int))
|
||||
if l < pad :
|
||||
padding = "0" * (pad - l)
|
||||
|
||||
num_s = padding + num_s
|
||||
if negative:
|
||||
num_s = "-" + num_s
|
||||
|
||||
return num_s
|
||||
|
||||
def asFloat( self ):
|
||||
#return the float, with no suffix
|
||||
return self.num
|
||||
|
||||
def asInt( self ):
|
||||
#return the int version of the float
|
||||
if self.num is None:
|
||||
return None
|
||||
return int( self.num )
|
||||
|
||||
|
@ -33,6 +33,7 @@ diskimage:
|
||||
rm -rf $(STAGING)
|
||||
mkdir $(STAGING)
|
||||
cp $(TAGGER_BASE)/release_notes.txt $(STAGING)
|
||||
ln -s /Applications $(STAGING)/Applications
|
||||
cp -a $(APP_BUNDLE) $(STAGING)
|
||||
cp $(MAC_BASE)/volume.icns $(STAGING)/.VolumeIcon.icns
|
||||
SetFile -c icnC $(STAGING)/.VolumeIcon.icns
|
||||
|
102
options.py
102
options.py
@ -23,6 +23,7 @@ import getopt
|
||||
import platform
|
||||
import os
|
||||
|
||||
import ctversion
|
||||
from genericmetadata import GenericMetadata
|
||||
|
||||
class Enum(set):
|
||||
@ -34,7 +35,8 @@ class Enum(set):
|
||||
class MetaDataStyle:
|
||||
CBI = 0
|
||||
CIX = 1
|
||||
name = [ 'ComicBookLover', 'ComicRack' ]
|
||||
COMET = 2
|
||||
name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ]
|
||||
|
||||
|
||||
class Options:
|
||||
@ -50,17 +52,24 @@ If no options are given, {0} will run in windowed mode
|
||||
--raw With -p, will print out the raw tag block(s)
|
||||
from the file
|
||||
-d, --delete Deletes the tag block of specified type (via -t)
|
||||
-c, --copy=SOURCE Copy the specified source tag block to destination style
|
||||
specified via via -t (potentially lossy operation)
|
||||
-s, --save Save out tags as specified type (via -t)
|
||||
Must specify also at least -o, -p, or -m
|
||||
--nooverwrite Don't modify tag block if it already exists ( relevent for -s or -c )
|
||||
-n, --dryrun Don't actually modify file (only relevent for -d, -s, or -r)
|
||||
-t, --type=TYPE Specify TYPE as either "CR" or "CBL", (as either
|
||||
ComicRack or ComicBookLover style tags, respectivly)
|
||||
-t, --type=TYPE Specify TYPE as either "CR", "CBL", or "COMET" (as either
|
||||
ComicRack, ComicBookLover, or CoMet style tags, respectivly)
|
||||
-f, --parsefilename Parse the filename to get some info, specifically
|
||||
series name, issue number, volume, and publication
|
||||
year
|
||||
-i, --interactive Interactively query the user when there are multiple matches for
|
||||
an online search
|
||||
--nosummary Suppress the default summary after a save operation
|
||||
-o, --online Search online and attempt to identify file using
|
||||
existing metadata and images in archive. May be used
|
||||
in conjuntion with -f and -m
|
||||
--id=ID Use the issue ID when searching online. Overrides all other metadata
|
||||
-m, --metadata=LIST Explicity define, as a list, some tags to be used
|
||||
e.g. "series=Plastic Man , publisher=Quality Comics"
|
||||
"series=Kickers^, Inc., issue=1, year=1986"
|
||||
@ -71,6 +80,8 @@ If no options are given, {0} will run in windowed mode
|
||||
-r, --rename Rename the file based on specified tag style.
|
||||
--noabort Don't abort save operation when online match is of low confidence
|
||||
-v, --verbose Be noisy when doing what it does
|
||||
--terse Don't say much (for print mode)
|
||||
--version Display version
|
||||
-h, --help Display this message
|
||||
"""
|
||||
|
||||
@ -80,23 +91,32 @@ If no options are given, {0} will run in windowed mode
|
||||
self.no_gui = False
|
||||
self.filename = None
|
||||
self.verbose = False
|
||||
self.terse = False
|
||||
self.metadata = None
|
||||
self.print_tags = False
|
||||
self.copy_tags = False
|
||||
self.delete_tags = False
|
||||
self.search_online = False
|
||||
self.dryrun = False
|
||||
self.abortOnLowConfidence = True
|
||||
self.save_tags = False
|
||||
self.parse_filename = False
|
||||
self.show_save_summary = True
|
||||
self.raw = False
|
||||
self.rename_file = False
|
||||
self.no_overwrite = False
|
||||
self.interactive = False
|
||||
self.issue_id = None
|
||||
self.file_list = []
|
||||
|
||||
def display_help_and_quit( self, msg, code ):
|
||||
def display_msg_and_quit( self, msg, code, show_help=False ):
|
||||
appname = os.path.basename(sys.argv[0])
|
||||
if msg is not None:
|
||||
print( msg )
|
||||
print self.help_text.format(appname)
|
||||
if show_help:
|
||||
print self.help_text.format(appname)
|
||||
else:
|
||||
print "For more help, run with '--help'"
|
||||
sys.exit(code)
|
||||
|
||||
def parseMetadataFromString( self, mdstr ):
|
||||
@ -159,23 +179,36 @@ If no options are given, {0} will run in windowed mode
|
||||
# parse command line options
|
||||
try:
|
||||
opts, args = getopt.getopt( input_args,
|
||||
"hpdt:fm:vonsr",
|
||||
[ "help", "print", "delete", "type=", "parsefilename", "metadata=", "verbose",
|
||||
"online", "dryrun", "save", "rename" , "raw", "noabort" ])
|
||||
"hpdt:fm:vonsrc:i",
|
||||
[ "help", "print", "delete", "type=", "copy=", "parsefilename", "metadata=", "verbose",
|
||||
"online", "dryrun", "save", "rename" , "raw", "noabort", "terse", "nooverwrite",
|
||||
"interactive", "nosummary", "version", "id=" ])
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
self.display_help_and_quit( str(err), 2 )
|
||||
self.display_msg_and_quit( str(err), 2 )
|
||||
|
||||
# process options
|
||||
for o, a in opts:
|
||||
if o in ("-h", "--help"):
|
||||
self.display_help_and_quit( None, 0 )
|
||||
self.display_msg_and_quit( None, 0, show_help=True )
|
||||
if o in ("-v", "--verbose"):
|
||||
self.verbose = True
|
||||
if o in ("-p", "--print"):
|
||||
self.print_tags = True
|
||||
if o in ("-d", "--delete"):
|
||||
self.delete_tags = True
|
||||
if o in ("-i", "--interactive"):
|
||||
self.interactive = True
|
||||
if o in ("-c", "--copy"):
|
||||
self.copy_tags = True
|
||||
if a.lower() == "cr":
|
||||
self.copy_source = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
self.copy_source = MetaDataStyle.CBI
|
||||
elif a.lower() == "comet":
|
||||
self.copy_source = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_msg_and_quit( "Invalid copy tag source type", 1 )
|
||||
if o in ("-o", "--online"):
|
||||
self.search_online = True
|
||||
if o in ("-n", "--dryrun"):
|
||||
@ -188,43 +221,68 @@ If no options are given, {0} will run in windowed mode
|
||||
self.rename_file = True
|
||||
if o in ("-f", "--parsefilename"):
|
||||
self.parse_filename = True
|
||||
if o in ("--raw"):
|
||||
if o == "--id":
|
||||
self.issue_id = a
|
||||
if o == "--raw":
|
||||
self.raw = True
|
||||
if o in ("--noabort"):
|
||||
if o == "--noabort":
|
||||
self.abortOnLowConfidence = False
|
||||
if o == "--terse":
|
||||
self.terse = True
|
||||
if o == "--nosummary":
|
||||
self.show_save_summary = False
|
||||
if o == "--nooverwrite":
|
||||
self.no_overwrite = True
|
||||
if o == "--version":
|
||||
print "ComicTagger version: ", ctversion.version
|
||||
sys.exit(0)
|
||||
if o in ("-t", "--type"):
|
||||
if a.lower() == "cr":
|
||||
self.data_style = MetaDataStyle.CIX
|
||||
elif a.lower() == "cbl":
|
||||
self.data_style = MetaDataStyle.CBI
|
||||
elif a.lower() == "comet":
|
||||
self.data_style = MetaDataStyle.COMET
|
||||
else:
|
||||
self.display_help_and_quit( "Invalid tag type", 1 )
|
||||
self.display_msg_and_quit( "Invalid tag type", 1 )
|
||||
|
||||
if self.print_tags or self.delete_tags or self.save_tags or self.rename_file:
|
||||
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file:
|
||||
self.no_gui = True
|
||||
|
||||
count = 0
|
||||
if self.print_tags: count += 1
|
||||
if self.delete_tags: count += 1
|
||||
if self.save_tags: count += 1
|
||||
if self.copy_tags: count += 1
|
||||
if self.rename_file: count += 1
|
||||
|
||||
if count > 1:
|
||||
self.display_help_and_quit( "Must choose only one action of print, delete, save, or rename", 1 )
|
||||
self.display_msg_and_quit( "Must choose only one action of print, delete, save, copy, or rename", 1 )
|
||||
|
||||
if len(args) > 0:
|
||||
self.filename = args[0]
|
||||
self.file_list = args
|
||||
if platform.system() == "Windows":
|
||||
# no globbing on windows shell, so do it for them
|
||||
import glob
|
||||
self.file_list = []
|
||||
for item in args:
|
||||
self.file_list.extend(glob.glob(item))
|
||||
self.filename = self.file_list[0]
|
||||
else:
|
||||
self.filename = args[0]
|
||||
self.file_list = args
|
||||
|
||||
if self.no_gui and self.filename is None:
|
||||
self.display_help_and_quit( "Command requires a filename!", 1 )
|
||||
self.display_msg_and_quit( "Command requires a filename!", 1 )
|
||||
|
||||
if self.delete_tags and self.data_style is None:
|
||||
self.display_help_and_quit( "Please specify the type to delete with -t", 1 )
|
||||
self.display_msg_and_quit( "Please specify the type to delete with -t", 1 )
|
||||
|
||||
if self.save_tags and self.data_style is None:
|
||||
self.display_help_and_quit( "Please specify the type to save with -t", 1 )
|
||||
self.display_msg_and_quit( "Please specify the type to save with -t", 1 )
|
||||
|
||||
if self.copy_tags and self.data_style is None:
|
||||
self.display_msg_and_quit( "Please specify the type to copy to with -t", 1 )
|
||||
|
||||
if self.rename_file and self.data_style is None:
|
||||
self.display_help_and_quit( "Please specify the type to use for renaming with -t", 1 )
|
||||
#if self.rename_file and self.data_style is None:
|
||||
# self.display_msg_and_quit( "Please specify the type to use for renaming with -t", 1 )
|
||||
|
||||
|
@ -26,7 +26,7 @@ from settings import ComicTaggerSettings
|
||||
|
||||
class PageBrowserWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent, metadata):
|
||||
super(PageBrowserWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagebrowser.ui' ), self)
|
||||
@ -37,6 +37,7 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
self.current_pixmap = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = metadata
|
||||
|
||||
self.btnNext.clicked.connect( self.nextPage )
|
||||
self.btnPrev.clicked.connect( self.prevPage )
|
||||
@ -66,7 +67,8 @@ class PageBrowserWindow(QtGui.QDialog):
|
||||
self.setPage()
|
||||
|
||||
def setPage( self ):
|
||||
image_data = self.comic_archive.getPage( self.current_page_num )
|
||||
archive_page_index = self.metadata.getArchivePageIndex( self.current_page_num )
|
||||
image_data = self.comic_archive.getPage( archive_page_index )
|
||||
|
||||
if image_data is not None:
|
||||
self.setCurrentPixmap( image_data )
|
||||
|
303
pagelisteditor.py
Normal file
303
pagelisteditor.py
Normal file
@ -0,0 +1,303 @@
|
||||
"""
|
||||
A PyQt4 widget for editing the page list info
|
||||
"""
|
||||
|
||||
"""
|
||||
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 os
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4 import uic
|
||||
|
||||
from settings import ComicTaggerSettings
|
||||
from genericmetadata import GenericMetadata, PageType
|
||||
from options import MetaDataStyle
|
||||
from pageloader import PageLoader
|
||||
|
||||
def itemMoveEvents( widget ):
|
||||
|
||||
class Filter(QObject):
|
||||
|
||||
mysignal = pyqtSignal( str )
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
|
||||
if obj == widget:
|
||||
#print event.type()
|
||||
if event.type() == QEvent.ChildRemoved:
|
||||
#print "ChildRemoved"
|
||||
self.mysignal.emit("finish")
|
||||
if event.type() == QEvent.ChildAdded:
|
||||
#print "ChildAdded"
|
||||
self.mysignal.emit("start")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
filter = Filter( widget )
|
||||
widget.installEventFilter( filter )
|
||||
return filter.mysignal
|
||||
|
||||
class PageListEditor(QWidget):
|
||||
|
||||
firstFrontCoverChanged = pyqtSignal( int )
|
||||
listOrderChanged = pyqtSignal( )
|
||||
modified = pyqtSignal( )
|
||||
|
||||
pageTypeNames = {
|
||||
PageType.FrontCover: "Front Cover",
|
||||
PageType.InnerCover: "Inner Cover",
|
||||
PageType.Advertisment: "Advertisment",
|
||||
PageType.Roundup: "Roundup",
|
||||
PageType.Story: "Story",
|
||||
PageType.Editorial: "Editorial",
|
||||
PageType.Letters: "Letters",
|
||||
PageType.Preview: "Preview",
|
||||
PageType.BackCover: "Back Cover",
|
||||
PageType.Other: "Other",
|
||||
PageType.Deleted: "Deleted",
|
||||
}
|
||||
|
||||
def __init__(self, parent ):
|
||||
super(PageListEditor, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'pagelisteditor.ui' ), self )
|
||||
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
self.page_loader = None
|
||||
|
||||
self.current_pixmap = QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' ))
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
|
||||
# Add the entries to the manga combobox
|
||||
self.comboBox.addItem( "", "" )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.FrontCover], PageType.FrontCover )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.InnerCover], PageType.InnerCover )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Advertisment], PageType.Advertisment )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Roundup], PageType.Roundup )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Story], PageType.Story )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Editorial], PageType.Editorial )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Letters], PageType.Letters )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Preview], PageType.Preview )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.BackCover], PageType.BackCover )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Other], PageType.Other )
|
||||
self.comboBox.addItem( self.pageTypeNames[ PageType.Deleted], PageType.Deleted )
|
||||
|
||||
self.listWidget.itemSelectionChanged.connect( self.changePage )
|
||||
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
|
||||
self.comboBox.activated.connect( self.changePageType )
|
||||
self.btnUp.clicked.connect( self.moveCurrentUp )
|
||||
self.btnDown.clicked.connect( self.moveCurrentDown )
|
||||
self.pre_move_row = -1
|
||||
self.first_front_page = None
|
||||
|
||||
def moveCurrentUp( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
if row > 0:
|
||||
item = self.listWidget.takeItem ( row )
|
||||
self.listWidget.insertItem( row-1, item )
|
||||
self.listWidget.setCurrentRow( row-1 )
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def moveCurrentDown( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
if row < self.listWidget.count()-1:
|
||||
item = self.listWidget.takeItem ( row )
|
||||
self.listWidget.insertItem( row+1, item )
|
||||
self.listWidget.setCurrentRow( row+1 )
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def itemMoveEvent(self, s):
|
||||
#print "move event: ", s, self.listWidget.currentRow()
|
||||
if s == "start":
|
||||
self.pre_move_row = self.listWidget.currentRow()
|
||||
if s == "finish":
|
||||
if self.pre_move_row != self.listWidget.currentRow():
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def changePageType( self , i):
|
||||
new_type = self.comboBox.itemData(i).toString()
|
||||
if self.getCurrentPageType() != new_type:
|
||||
self.setCurrentPageType( new_type )
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def changePage( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
pagetype = self.getCurrentPageType()
|
||||
|
||||
i = self.comboBox.findData( pagetype )
|
||||
self.comboBox.setCurrentIndex( i )
|
||||
|
||||
#idx = int(str (self.listWidget.item( row ).text()))
|
||||
idx = int(self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]['Image'])
|
||||
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
|
||||
if self.comic_archive is not None:
|
||||
self.page_loader = PageLoader( self.comic_archive, idx )
|
||||
self.page_loader.loadComplete.connect( self.actualChangePageImage )
|
||||
self.page_loader.start()
|
||||
|
||||
def actualChangePageImage( self, img ):
|
||||
self.page_loader = None
|
||||
self.current_pixmap = QPixmap(img)
|
||||
self.setDisplayPixmap( 0, 0)
|
||||
|
||||
def getFirstFrontCover( self ):
|
||||
frontCover = 0
|
||||
for i in range( self.listWidget.count() ):
|
||||
item = self.listWidget.item( i )
|
||||
page_dict = item.data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict and page_dict['Type'] == PageType.FrontCover:
|
||||
frontCover = int(page_dict['Image'])
|
||||
break
|
||||
return frontCover
|
||||
|
||||
|
||||
def getCurrentPageType( self ):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]
|
||||
if 'Type' in page_dict:
|
||||
return page_dict['Type']
|
||||
else:
|
||||
return ""
|
||||
|
||||
def setCurrentPageType( self, t ):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item( row ).data(Qt.UserRole).toPyObject()[0]
|
||||
|
||||
if t == "":
|
||||
if 'Type' in page_dict:
|
||||
del(page_dict['Type'])
|
||||
else:
|
||||
page_dict['Type'] = str(t)
|
||||
|
||||
item = self.listWidget.item( row )
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (page_dict,) )
|
||||
item.setText( self.listEntryText( page_dict ) )
|
||||
|
||||
|
||||
def resizeEvent( self, resize_event ):
|
||||
if self.current_pixmap is not None:
|
||||
delta_w = resize_event.size().width() - resize_event.oldSize().width()
|
||||
delta_h = resize_event.size().height() - resize_event.oldSize().height()
|
||||
self.setDisplayPixmap( delta_w , delta_h )
|
||||
|
||||
def setDisplayPixmap( self, delta_w , delta_h ):
|
||||
# the deltas let us know what the new width and height of the label will be
|
||||
new_h = self.frame.height() + delta_h
|
||||
new_w = self.frame.width() + delta_w
|
||||
|
||||
frame_w = new_w
|
||||
frame_h = new_h
|
||||
|
||||
new_h -= 4
|
||||
new_w -= 4
|
||||
|
||||
if new_h < 0:
|
||||
new_h = 0;
|
||||
if new_w < 0:
|
||||
new_w = 0;
|
||||
|
||||
# scale the pixmap to fit in the frame
|
||||
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, Qt.KeepAspectRatio)
|
||||
self.label.setPixmap( scaled_pixmap )
|
||||
|
||||
# ,pve and resize the label to be centered in the fame
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.label.resize( img_w, img_h )
|
||||
self.label.move( (frame_w - img_w)/2, (frame_h - img_h)/2 )
|
||||
|
||||
|
||||
def setData( self, comic_archive, pages_list ):
|
||||
self.comic_archive = comic_archive
|
||||
self.pages_list = pages_list
|
||||
|
||||
self.listWidget.clear()
|
||||
for p in pages_list:
|
||||
item = QListWidgetItem( self.listEntryText( p ) )
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(Qt.UserRole, (p, ))
|
||||
|
||||
self.listWidget.addItem( item )
|
||||
self.listWidget.setCurrentRow ( 0 )
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
|
||||
def listEntryText(self, page_dict):
|
||||
text = page_dict['Image']
|
||||
if 'Type' in page_dict:
|
||||
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
|
||||
return text
|
||||
|
||||
def getPageList( self ):
|
||||
page_list = []
|
||||
for i in range( self.listWidget.count() ):
|
||||
item = self.listWidget.item( i )
|
||||
page_list.append( item.data(Qt.UserRole).toPyObject()[0] )
|
||||
return page_list
|
||||
|
||||
def emitFrontCoverChange( self ):
|
||||
if self.first_front_page != self.getFirstFrontCover():
|
||||
self.first_front_page = self.getFirstFrontCover()
|
||||
self.firstFrontCoverChanged.emit( self.first_front_page )
|
||||
|
||||
def setMetadataStyle( self, data_style ):
|
||||
|
||||
# depending on the current data style, certain fields are disabled
|
||||
|
||||
inactive_color = QColor(255, 170, 150)
|
||||
active_palette = self.label.palette()
|
||||
|
||||
inactive_palette3 = self.label.palette()
|
||||
inactive_palette3.setColor(QPalette.Base, inactive_color)
|
||||
|
||||
|
||||
if data_style == MetaDataStyle.CIX:
|
||||
self.btnUp.setEnabled( True )
|
||||
self.btnDown.setEnabled( True )
|
||||
self.comboBox.setEnabled( True )
|
||||
self.listWidget.setEnabled( True )
|
||||
|
||||
self.listWidget.setPalette(active_palette)
|
||||
|
||||
elif data_style == MetaDataStyle.CBI:
|
||||
self.btnUp.setEnabled( False )
|
||||
self.btnDown.setEnabled( False )
|
||||
self.comboBox.setEnabled( False )
|
||||
self.listWidget.setEnabled( False )
|
||||
|
||||
self.listWidget.setPalette(inactive_palette3)
|
||||
|
||||
elif data_style == MetaDataStyle.CoMet:
|
||||
pass
|
||||
|
||||
def showEvent( self, event ):
|
||||
# make sure to adjust the size and pos of the pixmap based on frame size
|
||||
self.setDisplayPixmap( 0,0 )
|
||||
|
135
pagelisteditor.ui
Normal file
135
pagelisteditor.ui
Normal file
@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>pageListEditor</class>
|
||||
<widget class="QWidget" name="pageListEditor">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>527</width>
|
||||
<height>323</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="listWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>150</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::InternalMove</enum>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnUp">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>^</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btnDown">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>v</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Page Type:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="comboBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>90</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Sunken</enum>
|
||||
</property>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>151</width>
|
||||
<height>141</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
77
pageloader.py
Normal file
77
pageloader.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""
|
||||
A PyQT4 class to load a page image from a ComicArchive in a background thread
|
||||
"""
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
|
||||
from comicarchive import ComicArchive
|
||||
|
||||
"""
|
||||
This class holds onto a reference of each instance in a list
|
||||
since problems occur if the ref count goes to zero and the GC
|
||||
tries to reap the object while the thread is going.
|
||||
|
||||
If the client class wants to stop the thread, they should mark
|
||||
it as "abandoned", and no signals will be issued
|
||||
"""
|
||||
|
||||
class PageLoader( QtCore.QThread ):
|
||||
|
||||
loadComplete = pyqtSignal( QtGui.QImage )
|
||||
|
||||
instanceList = []
|
||||
mutex = QtCore.QMutex()
|
||||
|
||||
"""
|
||||
Remove all finished threads from the list
|
||||
"""
|
||||
@staticmethod
|
||||
def reapInstances():
|
||||
for obj in reversed(PageLoader.instanceList ):
|
||||
if obj.isFinished():
|
||||
PageLoader.instanceList.remove(obj)
|
||||
|
||||
def __init__(self, ca, page_num ):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.ca = ca
|
||||
self.page_num = page_num
|
||||
self.abandoned = False
|
||||
|
||||
# remove any old instances, and then add ourself
|
||||
PageLoader.mutex.lock()
|
||||
PageLoader.reapInstances()
|
||||
PageLoader.instanceList.append( self )
|
||||
PageLoader.mutex.unlock()
|
||||
|
||||
def run(self):
|
||||
image_data = self.ca.getPage( self.page_num )
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
if image_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( image_data )
|
||||
|
||||
if self.abandoned:
|
||||
return
|
||||
|
||||
self.loadComplete.emit( img )
|
||||
|
||||
|
@ -1,4 +1,57 @@
|
||||
|
||||
30-Nov-2012
|
||||
0.9.0-beta
|
||||
---------------------------------
|
||||
0.9.5-beta - 16-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Added CLI option to search by comicvine issue ID
|
||||
Some image loading optimizations
|
||||
Bug Fix: Some CBL fields that should have been ints were written as strings
|
||||
|
||||
---------------------------------
|
||||
0.9.4-beta - 7-Jan-2013
|
||||
---------------------------------
|
||||
Changes:
|
||||
Better handling of non-ascii characters in filenames and data
|
||||
Add CBL Transform to copy Web Link and Notes to comments
|
||||
Minor bug fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.3-beta - 19-Dec-2012
|
||||
---------------------------------
|
||||
Changes:
|
||||
File rename in GUI
|
||||
Setting for file rename
|
||||
Option to use series start year as volume
|
||||
Added "CBL Transform" to handle primary credits copying data into the generic tags field
|
||||
Bug Fix: unicode characters in credits caused crash
|
||||
Bug Fix: bad or non-image data in file caused crash
|
||||
|
||||
Note:
|
||||
The user should clear the cache and delete the existing settings when first running this version.
|
||||
|
||||
---------------------------------
|
||||
0.9.2-beta - 13-Dec-2012
|
||||
---------------------------------
|
||||
Page List/Type editing in GUI
|
||||
File globbing for windows CLI (i.e. use of wildcards like '*.cbz')
|
||||
Fixed RAR writing bug on windows
|
||||
Minor bug and crash fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.1-beta - 07-Dec-2012
|
||||
---------------------------------
|
||||
Export as ZIP Archive
|
||||
Added help menu option for websites
|
||||
Added Primary Credit Flag editing
|
||||
Menu enhancements
|
||||
CLI Enhancements:
|
||||
Interactive selection of matches
|
||||
Tag copy
|
||||
Better output
|
||||
CoMet support
|
||||
Minor bug and crash fixes
|
||||
|
||||
---------------------------------
|
||||
0.9.0-beta - 30-Nov-2012
|
||||
---------------------------------
|
||||
Initial beta release
|
79
renamewindow.py
Normal file
79
renamewindow.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""
|
||||
A PyQT4 dialog to confirm rename
|
||||
"""
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
import os
|
||||
import utils
|
||||
|
||||
class RenameWindow(QtGui.QDialog):
|
||||
|
||||
def __init__( self, parent, comic_archive, metadata, settings ):
|
||||
super(RenameWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
|
||||
|
||||
self.settings = settings
|
||||
self.metadata = metadata
|
||||
self.comic_archive = comic_archive
|
||||
self.new_name = None
|
||||
|
||||
self.btnSettings.clicked.connect( self.modifySettings )
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def configRenamer( self ):
|
||||
self.renamer = FileRenamer( self.metadata )
|
||||
self.renamer.setTemplate( self.settings.rename_template )
|
||||
self.renamer.setIssueZeroPadding( self.settings.rename_issue_number_padding )
|
||||
self.renamer.setSmartCleanup( self.settings.rename_use_smart_string_cleanup )
|
||||
|
||||
def doPreview( self ):
|
||||
self.new_name = self.renamer.determineName( self.comic_archive.path )
|
||||
preview = u"\"{0}\" ==> \"{1}\"".format( self.comic_archive.path, self.new_name )
|
||||
self.textEdit.setPlainText( preview )
|
||||
|
||||
def modifySettings( self ):
|
||||
settingswin = SettingsWindow( self, self.settings )
|
||||
settingswin.setModal(True)
|
||||
settingswin.showRenameTab()
|
||||
settingswin.exec_()
|
||||
if settingswin.result():
|
||||
self.configRenamer()
|
||||
self.doPreview()
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
if self.new_name == os.path.basename( self.comic_archive.path ):
|
||||
#print msg_hdr + "Filename is already good!"
|
||||
return
|
||||
|
||||
folder = os.path.dirname( os.path.abspath( self.comic_archive.path ) )
|
||||
new_abs_path = utils.unique_file( os.path.join( folder, self.new_name ) )
|
||||
|
||||
os.rename( self.comic_archive.path, new_abs_path )
|
||||
|
||||
self.new_name = new_abs_path
|
||||
|
||||
|
106
renamewindow.ui
Normal file
106
renamewindow.ui
Normal file
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogRename</class>
|
||||
<widget class="QDialog" name="dialogRename">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>556</width>
|
||||
<height>210</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Archive Rename</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string> Preview:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="btnSettings">
|
||||
<property name="text">
|
||||
<string>Rename Settings</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<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 class="QWidget" name="layoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>140</x>
|
||||
<y>10</y>
|
||||
<width>133</width>
|
||||
<height>29</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout"/>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogRename</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogRename</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>346</x>
|
||||
<y>187</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>277</x>
|
||||
<y>104</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
82
settings.py
82
settings.py
@ -65,7 +65,27 @@ class ComicTaggerSettings:
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
|
||||
|
||||
# Comic Vine settings
|
||||
self.use_series_start_as_volume = False
|
||||
|
||||
# CBL Tranform settings
|
||||
|
||||
self.assume_lone_credit_is_primary = False
|
||||
self.copy_characters_to_tags = False
|
||||
self.copy_teams_to_tags = False
|
||||
self.copy_locations_to_tags = False
|
||||
self.copy_notes_to_comments = False
|
||||
self.copy_weblink_to_comments = False
|
||||
self.apply_cbl_transform_on_cv_import = False
|
||||
self.apply_cbl_transform_on_bulk_operation = False
|
||||
|
||||
# Rename settings
|
||||
self.rename_template = "%series% #%issue% (%year%)"
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.settings_file = ""
|
||||
@ -136,13 +156,41 @@ class ComicTaggerSettings:
|
||||
if self.config.has_option('identifier', 'id_length_delta_thresh'):
|
||||
self.id_length_delta_thresh = self.config.getint( 'identifier', 'id_length_delta_thresh' )
|
||||
if self.config.has_option('identifier', 'id_publisher_blacklist'):
|
||||
self.id_publisher_blacklist = self.config.get( 'identifier', 'id_publisher_blacklist' )
|
||||
self.id_publisher_blacklist = self.config.get( 'identifier', 'id_publisher_blacklist' )
|
||||
|
||||
if self.config.has_option('dialogflags', 'ask_about_cbi_in_rar'):
|
||||
self.ask_about_cbi_in_rar = self.config.getboolean( 'dialogflags', 'ask_about_cbi_in_rar' )
|
||||
if self.config.has_option('dialogflags', 'show_disclaimer'):
|
||||
self.show_disclaimer = self.config.getboolean( 'dialogflags', 'show_disclaimer' )
|
||||
|
||||
|
||||
if self.config.has_option('comicvine', 'use_series_start_as_volume'):
|
||||
self.use_series_start_as_volume = self.config.getboolean( 'comicvine', 'use_series_start_as_volume' )
|
||||
|
||||
if self.config.has_option('cbl_transform', 'assume_lone_credit_is_primary'):
|
||||
self.assume_lone_credit_is_primary = self.config.getboolean( 'cbl_transform', 'assume_lone_credit_is_primary' )
|
||||
if self.config.has_option('cbl_transform', 'copy_characters_to_tags'):
|
||||
self.copy_characters_to_tags = self.config.getboolean( 'cbl_transform', 'copy_characters_to_tags' )
|
||||
if self.config.has_option('cbl_transform', 'copy_teams_to_tags'):
|
||||
self.copy_teams_to_tags = self.config.getboolean( 'cbl_transform', 'copy_teams_to_tags' )
|
||||
if self.config.has_option('cbl_transform', 'copy_locations_to_tags'):
|
||||
self.copy_locations_to_tags = self.config.getboolean( 'cbl_transform', 'copy_locations_to_tags' )
|
||||
if self.config.has_option('cbl_transform', 'copy_notes_to_comments'):
|
||||
self.copy_notes_to_comments = self.config.getboolean( 'cbl_transform', 'copy_notes_to_comments' )
|
||||
if self.config.has_option('cbl_transform', 'copy_weblink_to_comments'):
|
||||
self.copy_weblink_to_comments = self.config.getboolean( 'cbl_transform', 'copy_weblink_to_comments' )
|
||||
if self.config.has_option('cbl_transform', 'apply_cbl_transform_on_cv_import'):
|
||||
self.apply_cbl_transform_on_cv_import = self.config.getboolean( 'cbl_transform', 'apply_cbl_transform_on_cv_import' )
|
||||
if self.config.has_option('cbl_transform', 'apply_cbl_transform_on_bulk_operation'):
|
||||
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean( 'cbl_transform', 'apply_cbl_transform_on_bulk_operation' )
|
||||
|
||||
if self.config.has_option('rename', 'rename_template'):
|
||||
self.rename_template = self.config.get( 'rename', 'rename_template' )
|
||||
if self.config.has_option('rename', 'rename_issue_number_padding'):
|
||||
self.rename_issue_number_padding = self.config.getint( 'rename', 'rename_issue_number_padding' )
|
||||
if self.config.has_option('rename', 'rename_use_smart_string_cleanup'):
|
||||
self.rename_use_smart_string_cleanup = self.config.getboolean( 'rename', 'rename_use_smart_string_cleanup' )
|
||||
|
||||
|
||||
def save( self ):
|
||||
|
||||
if not self.config.has_section( 'settings' ):
|
||||
@ -173,8 +221,30 @@ class ComicTaggerSettings:
|
||||
self.config.set( 'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar )
|
||||
self.config.set( 'dialogflags', 'show_disclaimer', self.show_disclaimer )
|
||||
|
||||
if not self.config.has_section( 'comicvine' ):
|
||||
self.config.add_section( 'comicvine' )
|
||||
|
||||
self.config.set( 'comicvine', 'use_series_start_as_volume', self.use_series_start_as_volume )
|
||||
|
||||
if not self.config.has_section( 'cbl_transform' ):
|
||||
self.config.add_section( 'cbl_transform' )
|
||||
|
||||
self.config.set( 'cbl_transform', 'assume_lone_credit_is_primary', self.assume_lone_credit_is_primary )
|
||||
self.config.set( 'cbl_transform', 'copy_characters_to_tags', self.copy_characters_to_tags )
|
||||
self.config.set( 'cbl_transform', 'copy_teams_to_tags', self.copy_teams_to_tags )
|
||||
self.config.set( 'cbl_transform', 'copy_locations_to_tags', self.copy_locations_to_tags )
|
||||
self.config.set( 'cbl_transform', 'copy_notes_to_comments', self.copy_notes_to_comments )
|
||||
self.config.set( 'cbl_transform', 'copy_weblink_to_comments', self.copy_weblink_to_comments )
|
||||
self.config.set( 'cbl_transform', 'apply_cbl_transform_on_cv_import', self.apply_cbl_transform_on_cv_import )
|
||||
self.config.set( 'cbl_transform', 'apply_cbl_transform_on_bulk_operation', self.apply_cbl_transform_on_bulk_operation )
|
||||
|
||||
if not self.config.has_section( 'rename' ):
|
||||
self.config.add_section( 'rename' )
|
||||
|
||||
self.config.set( 'rename', 'rename_template', self.rename_template )
|
||||
self.config.set( 'rename', 'rename_issue_number_padding', self.rename_issue_number_padding )
|
||||
self.config.set( 'rename', 'rename_use_smart_string_cleanup', self.rename_use_smart_string_cleanup )
|
||||
|
||||
with open( self.settings_file, 'wb') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -96,6 +96,12 @@ class SettingsWindow(QtGui.QDialog):
|
||||
)
|
||||
self.tePublisherBlacklist.setToolTip(pblTip)
|
||||
|
||||
validator = QtGui.QIntValidator(1, 4, self)
|
||||
self.leIssueNumPadding.setValidator(validator)
|
||||
|
||||
validator = QtGui.QIntValidator(0, 99, self)
|
||||
self.leNameLengthDeltaThresh.setValidator(validator)
|
||||
|
||||
self.settingsToForm()
|
||||
|
||||
self.btnBrowseRar.clicked.connect(self.selectRar)
|
||||
@ -110,7 +116,32 @@ class SettingsWindow(QtGui.QDialog):
|
||||
self.leUnrarExePath.setText( self.settings.unrar_exe_path )
|
||||
self.leNameLengthDeltaThresh.setText( str(self.settings.id_length_delta_thresh) )
|
||||
self.tePublisherBlacklist.setPlainText( self.settings.id_publisher_blacklist )
|
||||
|
||||
if self.settings.use_series_start_as_volume:
|
||||
self.cbxUseSeriesStartAsVolume.setCheckState( QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
self.cbxAssumeLoneCreditIsPrimary.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_characters_to_tags:
|
||||
self.cbxCopyCharactersToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_teams_to_tags:
|
||||
self.cbxCopyTeamsToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_locations_to_tags:
|
||||
self.cbxCopyLocationsToTags.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_notes_to_comments:
|
||||
self.cbxCopyNotesToComments.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
self.cbxCopyWebLinkToComments.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.apply_cbl_transform_on_cv_import:
|
||||
self.cbxApplyCBLTransformOnCVIMport.setCheckState( QtCore.Qt.Checked)
|
||||
if self.settings.apply_cbl_transform_on_bulk_operation:
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setCheckState( QtCore.Qt.Checked)
|
||||
|
||||
self.leRenameTemplate.setText( self.settings.rename_template )
|
||||
self.leIssueNumPadding.setText( str(self.settings.rename_issue_number_padding) )
|
||||
if self.settings.rename_use_smart_string_cleanup:
|
||||
self.cbxSmartCleanup.setCheckState( QtCore.Qt.Checked )
|
||||
|
||||
def accept( self ):
|
||||
|
||||
# Copy values from form to settings and save
|
||||
@ -121,12 +152,29 @@ class SettingsWindow(QtGui.QDialog):
|
||||
utils.addtopath(os.path.dirname(self.settings.unrar_exe_path))
|
||||
|
||||
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
|
||||
QtGui.QMessageBox.information(self,"Settings", "The Name Length Delta Threshold must be a number!")
|
||||
return
|
||||
self.leNameLengthDeltaThresh.setText("0")
|
||||
|
||||
if not str(self.leIssueNumPadding.text()).isdigit():
|
||||
self.leIssueNumPadding.setText("0")
|
||||
|
||||
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_blacklist = str(self.tePublisherBlacklist.toPlainText())
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
|
||||
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
|
||||
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
|
||||
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
|
||||
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
|
||||
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
|
||||
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
|
||||
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
|
||||
|
||||
self.settings.rename_template = str(self.leRenameTemplate.text())
|
||||
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
|
||||
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
|
||||
self.settings.save()
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
@ -170,5 +218,6 @@ class SettingsWindow(QtGui.QDialog):
|
||||
fileList = dialog.selectedFiles()
|
||||
control.setText( str(fileList[0]) )
|
||||
|
||||
|
||||
def showRenameTab( self ):
|
||||
self.tabWidget.setCurrentIndex(4)
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>640</width>
|
||||
<height>416</height>
|
||||
<width>674</width>
|
||||
<height>428</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -301,6 +301,192 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<attribute name="title">
|
||||
<string>Comic Vine</string>
|
||||
</attribute>
|
||||
<widget class="QCheckBox" name="cbxUseSeriesStartAsVolume">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>30</y>
|
||||
<width>240</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use Series Start Date as Volume</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_4">
|
||||
<attribute name="title">
|
||||
<string>CBL</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_6">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="cbxApplyCBLTransformOnCVIMport">
|
||||
<property name="text">
|
||||
<string>Apply CBL Transforms on ComicVine Import</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="cbxApplyCBLTransformOnBatchOperation">
|
||||
<property name="text">
|
||||
<string>Apply CBL Transforms on Batch/CLI Operations</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>CBL Transforms</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>11</x>
|
||||
<y>21</y>
|
||||
<width>242</width>
|
||||
<height>182</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="cbxAssumeLoneCreditIsPrimary">
|
||||
<property name="text">
|
||||
<string>Assume Lone Credit Is Primary</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyCharactersToTags">
|
||||
<property name="text">
|
||||
<string>Copy Characters to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyTeamsToTags">
|
||||
<property name="text">
|
||||
<string>Copy Teams to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyLocationsToTags">
|
||||
<property name="text">
|
||||
<string>Copy Locations to Generic Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyNotesToComments">
|
||||
<property name="text">
|
||||
<string>Copy Notes to Comments</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="cbxCopyWebLinkToComments">
|
||||
<property name="text">
|
||||
<string>Copy Web Link to Comments</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_5">
|
||||
<attribute name="title">
|
||||
<string>Rename</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="0">
|
||||
<layout class="QFormLayout" name="formLayout_3">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Template:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRenameTemplate">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%publisher%<br/>%title%</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Issue # Zero Padding</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="leIssueNumPadding">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>50</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600;">Issue # Zero Padding</span> dictates if the issue number should be padded on left with zeros. A value of 2, for example, means that the number will always be at least two digits.</p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSmartCleanup">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600;">&quot;Smart Text Cleanup&quot; </span>will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use Smart Text Cleanup (Experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
430
taggerwindow.py
430
taggerwindow.py
@ -28,6 +28,7 @@ import platform
|
||||
import os
|
||||
import pprint
|
||||
import json
|
||||
import webbrowser
|
||||
|
||||
from volumeselectionwindow import VolumeSelectionWindow
|
||||
from options import MetaDataStyle
|
||||
@ -42,6 +43,11 @@ from pagebrowser import PageBrowserWindow
|
||||
from filenameparser import FileNameParser
|
||||
from logwindow import LogWindow
|
||||
from optionalmsgdialog import OptionalMessageDialog
|
||||
from pagelisteditor import PageListEditor
|
||||
from fileselectionlist import FileSelectionList
|
||||
from cbltransformer import CBLTransformer
|
||||
from renamewindow import RenameWindow
|
||||
from pageloader import PageLoader
|
||||
import utils
|
||||
import ctversion
|
||||
|
||||
@ -87,13 +93,32 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
#signal.signal(signal.SIGINT, self.sigint_handler)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'taggerwindow.ui' ), self)
|
||||
self.settings = settings
|
||||
|
||||
self.pageListEditor = PageListEditor( self.tabPages )
|
||||
gridlayout = QtGui.QGridLayout( self.tabPages )
|
||||
gridlayout.addWidget( self.pageListEditor )
|
||||
|
||||
#---------------------------
|
||||
self.fileSelectionList = FileSelectionList( self.widgetListHolder, self.settings )
|
||||
gridlayout = QtGui.QGridLayout( self.widgetListHolder )
|
||||
gridlayout.addWidget( self.fileSelectionList )
|
||||
|
||||
self.fileSelectionList.selectionChanged.connect( self.fileListSelectionChanged )
|
||||
# ATB: Disable the list for now...
|
||||
self.splitter.setSizes([100,0])
|
||||
self.splitter.setHandleWidth(0)
|
||||
self.splitter.handle(1).setDisabled(True)
|
||||
|
||||
#---------------------------
|
||||
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/app.png' )))
|
||||
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
|
||||
#print platform.system(), platform.release()
|
||||
self.dirtyFlag = False
|
||||
self.settings = settings
|
||||
self.data_style = settings.last_selected_data_style
|
||||
|
||||
#set up a default metadata object
|
||||
@ -104,10 +129,11 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.statusBar()
|
||||
self.updateAppTitle()
|
||||
self.setAcceptDrops(True)
|
||||
self.updateSaveMenu()
|
||||
self.updateMenus()
|
||||
self.droppedFile = None
|
||||
|
||||
self.page_browser = None
|
||||
self.page_loader = None
|
||||
|
||||
self.populateComboBoxes()
|
||||
|
||||
@ -135,7 +161,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.btnRemoveCredit.clicked.connect(self.removeCredit)
|
||||
self.twCredits.cellDoubleClicked.connect(self.editCredit)
|
||||
clickable(self.lblCover).connect(self.showPageBrowser)
|
||||
|
||||
self.connectDirtyFlagSignals()
|
||||
self.pageListEditor.modified.connect(self.setDirtyFlag)
|
||||
|
||||
self.pageListEditor.firstFrontCoverChanged.connect( self.frontCoverChanged )
|
||||
self.pageListEditor.listOrderChanged.connect( self.pageListOrderChanged )
|
||||
|
||||
self.updateStyleTweaks()
|
||||
|
||||
@ -197,6 +228,10 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionWrite_Tags.setStatusTip( 'Save tags to comic archive' )
|
||||
self.actionWrite_Tags.triggered.connect( self.commitMetadata )
|
||||
|
||||
self.actionRemoveAuto.setShortcut( 'Ctrl+D' )
|
||||
self.actionRemoveAuto.setStatusTip( 'Remove selected style tags from archive' )
|
||||
self.actionRemoveAuto.triggered.connect( self.removeAuto )
|
||||
|
||||
self.actionRemoveCBLTags.setStatusTip( 'Remove ComicBookLover tags from comic archive' )
|
||||
self.actionRemoveCBLTags.triggered.connect( self.removeCBLTags )
|
||||
|
||||
@ -219,11 +254,14 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionViewRawCBLTags.setStatusTip( 'View raw ComicBookLover tag block from file' )
|
||||
self.actionViewRawCBLTags.triggered.connect( self.viewRawCBLTags )
|
||||
|
||||
#self.actionRepackage.setShortcut( )
|
||||
self.actionRepackage.setShortcut( 'Ctrl+E' )
|
||||
self.actionRepackage.setStatusTip( 'Re-create archive as CBZ' )
|
||||
self.actionRepackage.triggered.connect( self.repackageArchive )
|
||||
|
||||
self.actionRename.setShortcut( 'Ctrl+N' )
|
||||
self.actionRename.setStatusTip( 'Rename archive based on tags' )
|
||||
self.actionRename.triggered.connect( self.renameArchive )
|
||||
|
||||
#self.actionRepackage.setShortcut( )
|
||||
self.actionSettings.setStatusTip( 'Configure ComicTagger' )
|
||||
self.actionSettings.triggered.connect( self.showSettings )
|
||||
|
||||
@ -236,8 +274,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionSearchOnline.setStatusTip( 'Search online for tags' )
|
||||
self.actionSearchOnline.triggered.connect( self.queryOnline )
|
||||
|
||||
self.actionAutoSearch.setShortcut( 'Ctrl+A' )
|
||||
self.actionAutoSearch.triggered.connect( self.autoSelectSearch )
|
||||
|
||||
self.actionApplyCBLTransform.setShortcut( 'Ctrl+L' )
|
||||
self.actionApplyCBLTransform.setStatusTip( 'Modify tags specifically for CBL format' )
|
||||
self.actionApplyCBLTransform.triggered.connect( self.applyCBLTransform )
|
||||
|
||||
#self.actionClearEntryForm.setShortcut( 'Ctrl+C' )
|
||||
self.actionClearEntryForm.setStatusTip( 'Clear all the data on the screen' )
|
||||
self.actionClearEntryForm.triggered.connect( self.clearForm )
|
||||
@ -248,9 +291,11 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.actionPageBrowser.triggered.connect( self.showPageBrowser )
|
||||
|
||||
# Help Menu
|
||||
self.actionAbout.setShortcut( 'Ctrl+A' )
|
||||
self.actionAbout.setStatusTip( 'Show the ' + self.appName + ' info' )
|
||||
self.actionAbout.triggered.connect( self.aboutApp )
|
||||
self.actionWiki.triggered.connect( self.showWiki )
|
||||
self.actionReportBug.triggered.connect( self.reportBug )
|
||||
self.actionComicTaggerForum.triggered.connect( self.showForum )
|
||||
|
||||
# ToolBar
|
||||
|
||||
@ -271,12 +316,54 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.toolBar.addAction( self.actionPageBrowser )
|
||||
|
||||
def repackageArchive( self ):
|
||||
QtGui.QMessageBox.information(self, self.tr("Repackage Comic Archive"), self.tr("TBD"))
|
||||
if self.comic_archive is not None:
|
||||
if self.comic_archive.isZip():
|
||||
QtGui.QMessageBox.information(self, self.tr("Export as Zip Archive"), self.tr("It's already a Zip Archive!"))
|
||||
return
|
||||
|
||||
if not self.dirtyFlagVerification( "Export as Zip Archive",
|
||||
"If you export the archive to Zip format, the data in the form won't be in the new " +
|
||||
"archive unless you save it first. Do you want to continue with the export?"):
|
||||
return
|
||||
|
||||
export_name = os.path.splitext(self.comic_archive.path)[0] + ".cbz"
|
||||
#export_name = utils.unique_file( export_name )
|
||||
|
||||
dialog = QtGui.QFileDialog(self)
|
||||
dialog.setFileMode(QtGui.QFileDialog.AnyFile)
|
||||
if self.settings.last_opened_folder is not None:
|
||||
dialog.setDirectory( self.settings.last_opened_folder )
|
||||
filters = [
|
||||
"Comic archive files (*.cbz *.zip)",
|
||||
"Any files (*)"
|
||||
]
|
||||
dialog.setNameFilters(filters)
|
||||
dialog.selectFile( export_name )
|
||||
|
||||
if (dialog.exec_()):
|
||||
fileList = dialog.selectedFiles()
|
||||
if os.path.lexists( fileList[0] ):
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Export as Zip Archive"),
|
||||
self.tr(fileList[0] + " already exisits. Are you sure you want to overwrite it?"),
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
|
||||
|
||||
if reply == QtGui.QMessageBox.No:
|
||||
return
|
||||
|
||||
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
|
||||
retcode = self.comic_archive.exportAsZip( str(fileList[0]) )
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not retcode:
|
||||
QtGui.QMessageBox.information(self, self.tr("Export as Zip Archive"), self.tr("An error occure while exporting."))
|
||||
|
||||
def aboutApp( self ):
|
||||
|
||||
website = "http://code.google.com/p/comictagger"
|
||||
email = "comictagger@gmail.com"
|
||||
license_link = "http://www.apache.org/licenses/LICENSE-2.0"
|
||||
license_name = "Apache License 2.0"
|
||||
|
||||
msgBox = QtGui.QMessageBox()
|
||||
msgBox.setWindowTitle( self.tr("About " + self.appName ) )
|
||||
@ -286,14 +373,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
+ self.appName + " v" + self.version + "<br>"
|
||||
+ "(c)2012 Anthony Beville<br><br>"
|
||||
+ "<a href='{0}'>{0}</a><br><br>".format(website)
|
||||
+ "<a href='mailto:{0}'>{0}</a>".format(email) )
|
||||
+ "<a href='mailto:{0}'>{0}</a><br><br>".format(email)
|
||||
+ "License: <a href='{0}'>{1}</a>".format(license_link, license_name) )
|
||||
|
||||
msgBox.setStandardButtons( QtGui.QMessageBox.Ok )
|
||||
msgBox.exec_()
|
||||
|
||||
|
||||
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
self.droppedFile=None
|
||||
if event.mimeData().hasUrls():
|
||||
@ -302,12 +387,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if url.scheme()=="file":
|
||||
self.droppedFile=url.toLocalFile()
|
||||
event.accept()
|
||||
|
||||
|
||||
def dropEvent(self, event):
|
||||
if self.dirtyFlagVerification( "Open Archive",
|
||||
"If you open a new archive now, data in the form will be lost. Are you sure?"):
|
||||
self.openArchive( str(self.droppedFile) )
|
||||
|
||||
self.openArchive( unicode(self.droppedFile))
|
||||
|
||||
def openArchive( self, path, explicit_style=None, clear_form=True ):
|
||||
|
||||
if path is None or path == "":
|
||||
@ -335,6 +420,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
# no style indicated, so try to choose
|
||||
if hasNeither:
|
||||
self.metadata = self.comic_archive.metadataFromFilename( )
|
||||
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
|
||||
else:
|
||||
if hasCBI and not hasCIX:
|
||||
self.data_style = MetaDataStyle.CBI
|
||||
@ -361,42 +447,93 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
else:
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
if self.page_browser is not None:
|
||||
self.page_browser.setComicArchive( self.comic_archive )
|
||||
|
||||
self.metadataToForm()
|
||||
self.clearDirtyFlag() # also updates the app title
|
||||
self.updateInfoBox()
|
||||
self.updateSaveMenu()
|
||||
#self.updatePagesInfo()
|
||||
self.loadCurrentArchive()
|
||||
|
||||
else:
|
||||
QtGui.QMessageBox.information(self, self.tr("Whoops!"), self.tr("That file doesn't appear to be a comic archive!"))
|
||||
|
||||
def updateSaveMenu( self ):
|
||||
def loadCurrentArchive( self ):
|
||||
if self.metadata.isEmpty:
|
||||
self.metadata = self.comic_archive.metadataFromFilename( )
|
||||
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
|
||||
|
||||
if ( self.comic_archive is not None and
|
||||
self.comic_archive.isWritable( )
|
||||
):
|
||||
self.actionRemoveCRTags.setEnabled( True )
|
||||
self.actionRemoveCBLTags.setEnabled( True )
|
||||
self.actionWrite_Tags.setEnabled( True )
|
||||
else:
|
||||
self.actionRemoveCRTags.setEnabled( False )
|
||||
self.actionRemoveCBLTags.setEnabled( False )
|
||||
self.actionWrite_Tags.setEnabled( False )
|
||||
self.updateCoverImage()
|
||||
|
||||
if self.page_browser is not None:
|
||||
self.page_browser.setComicArchive( self.comic_archive )
|
||||
self.page_browser.metadata = self.metadata
|
||||
|
||||
self.metadataToForm()
|
||||
self.pageListEditor.setData( self.comic_archive, self.metadata.pages )
|
||||
self.clearDirtyFlag() # also updates the app title
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
|
||||
def updateCoverImage( self ):
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
|
||||
cover_idx = self.metadata.getCoverPageIndexList()[0]
|
||||
|
||||
self.page_loader = PageLoader( self.comic_archive, cover_idx )
|
||||
self.page_loader.loadComplete.connect( self.actualUpdateCoverImage )
|
||||
self.page_loader.start()
|
||||
|
||||
def actualUpdateCoverImage( self, img ):
|
||||
self.page_loader = None
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(img))
|
||||
self.lblCover.setScaledContents(True)
|
||||
|
||||
|
||||
def updateMenus( self ):
|
||||
|
||||
# First just disable all the questionable items
|
||||
self.actionRemoveAuto.setEnabled( False )
|
||||
self.actionRemoveCRTags.setEnabled( False )
|
||||
self.actionRemoveCBLTags.setEnabled( False )
|
||||
self.actionWrite_Tags.setEnabled( False )
|
||||
self.actionRepackage.setEnabled(False)
|
||||
self.actionViewRawCBLTags.setEnabled( False )
|
||||
self.actionViewRawCRTags.setEnabled( False )
|
||||
self.actionReloadCRTags.setEnabled( False )
|
||||
self.actionReloadCBLTags.setEnabled( False )
|
||||
self.actionReloadAuto.setEnabled( False )
|
||||
self.actionParse_Filename.setEnabled( False )
|
||||
self.actionAutoSearch.setEnabled( False )
|
||||
self.actionRename.setEnabled( False )
|
||||
self.actionApplyCBLTransform.setEnabled( False )
|
||||
|
||||
# now, selectively re-enable
|
||||
if self.comic_archive is not None :
|
||||
has_cix = self.comic_archive.hasCIX()
|
||||
has_cbi = self.comic_archive.hasCBI()
|
||||
|
||||
self.actionParse_Filename.setEnabled( True )
|
||||
self.actionAutoSearch.setEnabled( True )
|
||||
self.actionRename.setEnabled( True )
|
||||
self.actionApplyCBLTransform.setEnabled( True )
|
||||
|
||||
if not self.comic_archive.isZip():
|
||||
self.actionRepackage.setEnabled(True)
|
||||
|
||||
if has_cix or has_cbi:
|
||||
self.actionReloadAuto.setEnabled( True )
|
||||
if has_cix:
|
||||
self.actionReloadCRTags.setEnabled( True )
|
||||
self.actionViewRawCRTags.setEnabled( True )
|
||||
if has_cbi:
|
||||
self.actionReloadCBLTags.setEnabled( True )
|
||||
self.actionViewRawCBLTags.setEnabled( True )
|
||||
|
||||
if self.comic_archive.isWritable():
|
||||
self.actionWrite_Tags.setEnabled( True )
|
||||
if has_cix or has_cbi:
|
||||
self.actionRemoveAuto.setEnabled( True )
|
||||
if has_cix:
|
||||
self.actionRemoveCRTags.setEnabled( True )
|
||||
if has_cbi:
|
||||
self.actionRemoveCBLTags.setEnabled( True )
|
||||
|
||||
def updateInfoBox( self ):
|
||||
|
||||
ca = self.comic_archive
|
||||
@ -428,39 +565,6 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
tag_info += u"• ComicBookLover tags"
|
||||
|
||||
self.lblTagList.setText( tag_info )
|
||||
|
||||
def updatePagesInfo( self ):
|
||||
|
||||
#tablemodel = PageTableModel( self.comic_archive, self )
|
||||
#self.tableView.setModel(tablemodel)
|
||||
|
||||
listmodel = PageListModel( self.comic_archive, self )
|
||||
self.listView.setModel(listmodel)
|
||||
|
||||
#self.listView.setDragDropMode(self.InternalMove)
|
||||
#listmodel.installEventFilter(self)
|
||||
|
||||
self.listView.setDragEnabled(True)
|
||||
self.listView.setAcceptDrops(True)
|
||||
self.listView.setDropIndicatorShown(True)
|
||||
|
||||
#listmodel.rowsMoved.connect( self.rowsMoved )
|
||||
#listmodel.rowsRemoved.connect( self.rowsRemoved )
|
||||
#listmodel.beginMoveRows.connect( self.beginMoveRows )
|
||||
|
||||
#def rowsMoved( self, b, c, d):
|
||||
# print "rowsMoved"
|
||||
#def rowsRemoved( self,b, c, d):
|
||||
# print "rowsRemoved"
|
||||
|
||||
|
||||
"""
|
||||
def eventFilter(self, sender, event):
|
||||
if (event.type() == QtCore.QEvent.ChildRemoved):
|
||||
print "QEvent::ChildRemoved"
|
||||
return False # don't actually interrupt anything
|
||||
"""
|
||||
|
||||
|
||||
def setDirtyFlag( self, param1=None, param2=None, param3=None ):
|
||||
if not self.dirtyFlag:
|
||||
@ -489,20 +593,24 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
# recursive call on chillun
|
||||
for child in widget.children():
|
||||
self.connectChildDirtyFlagSignals( child )
|
||||
if child != self.pageListEditor:
|
||||
self.connectChildDirtyFlagSignals( child )
|
||||
|
||||
|
||||
def clearForm( self ):
|
||||
|
||||
# get a minty fresh metadata object
|
||||
self.metadata = GenericMetadata()
|
||||
if self.comic_archive is not None:
|
||||
self.metadata.setDefaultPageList( self.comic_archive.getNumberOfPages() )
|
||||
|
||||
# recursivly clear the tab form
|
||||
self.clearChildren( self.tabWidget )
|
||||
|
||||
# clear the dirty flag, since there is nothing in there now to lose
|
||||
self.clearDirtyFlag()
|
||||
|
||||
|
||||
self.pageListEditor.setData( self.comic_archive, self.metadata.pages )
|
||||
|
||||
def clearChildren (self, widget ):
|
||||
|
||||
@ -528,7 +636,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
#helper func
|
||||
def assignText( field, value):
|
||||
if value is not None:
|
||||
field.setText( u"{0}".format(value) )
|
||||
field.setText( unicode(value) )
|
||||
|
||||
md = self.metadata
|
||||
|
||||
@ -615,20 +723,23 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
item_text = role
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twCredits.setItem(row, 0, item)
|
||||
self.twCredits.setItem(row, 1, item)
|
||||
|
||||
item_text = name
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twCredits.setItem(row, 1, item)
|
||||
# for now, jusr preserve the primary flag
|
||||
item.setData( QtCore.Qt.UserRole, primary_flag)
|
||||
self.twCredits.setItem(row, 2, item)
|
||||
|
||||
item = QtGui.QTableWidgetItem("")
|
||||
item.setFlags(QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsEnabled)
|
||||
self.twCredits.setItem(row, 0, item)
|
||||
self.updateCreditPrimaryFlag( row, primary_flag )
|
||||
|
||||
def isDupeCredit( self, role, name ):
|
||||
r = 0
|
||||
while r < self.twCredits.rowCount():
|
||||
if ( self.twCredits.item(r, 0).text() == role and
|
||||
self.twCredits.item(r, 1).text() == name ):
|
||||
if ( self.twCredits.item(r, 1).text() == role and
|
||||
self.twCredits.item(r, 2).text() == name ):
|
||||
return True
|
||||
r = r + 1
|
||||
|
||||
@ -701,18 +812,23 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
md.credits = list()
|
||||
row = 0
|
||||
while row < self.twCredits.rowCount():
|
||||
role = str(self.twCredits.item(row, 0).text())
|
||||
name = str(self.twCredits.item(row, 1).text())
|
||||
primary_flag = self.twCredits.item( row, 1 ).data( QtCore.Qt.UserRole ).toBool()
|
||||
role = u"{0}".format(self.twCredits.item(row, 1).text())
|
||||
name = u"{0}".format(self.twCredits.item(row, 2).text())
|
||||
primary_flag = self.twCredits.item( row, 0 ).text() != ""
|
||||
|
||||
md.addCredit( name, role, bool(primary_flag) )
|
||||
row += 1
|
||||
|
||||
md.pages = self.pageListEditor.getPageList()
|
||||
|
||||
def useFilename( self ):
|
||||
if self.comic_archive is not None:
|
||||
self.metadata = self.comic_archive.metadataFromFilename( )
|
||||
self.metadataToForm()
|
||||
#copy the form onto metadata object
|
||||
self.formToMetadata()
|
||||
new_metadata = self.comic_archive.metadataFromFilename( )
|
||||
if new_metadata is not None:
|
||||
self.metadata.overlay( new_metadata )
|
||||
self.metadataToForm()
|
||||
|
||||
def selectFile( self ):
|
||||
|
||||
@ -739,7 +855,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
fileList = dialog.selectedFiles()
|
||||
if self.dirtyFlagVerification( "Open Archive",
|
||||
"If you open a new archive now, data in the form will be lost. Are you sure?"):
|
||||
self.openArchive( str(fileList[0]) )
|
||||
self.openArchive( unicode(fileList[0]) )
|
||||
|
||||
|
||||
def autoSelectSearch(self):
|
||||
@ -758,8 +874,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
QtGui.QMessageBox.information(self,"Automatic Online Search", "Can't auto-select without an issue number (yet!)")
|
||||
return
|
||||
|
||||
if str(self.leSeries.text()).strip() != "":
|
||||
series_name = str(self.leSeries.text()).strip()
|
||||
if unicode(self.leSeries.text()).strip() != "":
|
||||
series_name = unicode(self.leSeries.text()).strip()
|
||||
else:
|
||||
QtGui.QMessageBox.information(self, self.tr("Online Search"), self.tr("Need to enter a series name to search."))
|
||||
return
|
||||
@ -769,7 +885,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if year == "":
|
||||
year = None
|
||||
|
||||
selector = VolumeSelectionWindow( self, series_name, issue_number, year, self.comic_archive, self.settings, autoselect )
|
||||
cover_index_list = self.metadata.getCoverPageIndexList()
|
||||
selector = VolumeSelectionWindow( self, series_name, issue_number, year, cover_index_list, self.comic_archive, self.settings, autoselect )
|
||||
|
||||
title = "Search: '" + series_name + "' - "
|
||||
selector.setWindowTitle( title + "Select Series")
|
||||
@ -786,16 +903,23 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
try:
|
||||
comicVine = ComicVineTalker( )
|
||||
new_metadata = comicVine.fetchIssueData( selector.volume_id, selector.issue_number )
|
||||
new_metadata = comicVine.fetchIssueData( selector.volume_id, selector.issue_number, self.settings )
|
||||
except ComicVineTalkerException:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to get issue details!"))
|
||||
else:
|
||||
self.metadata.overlay( new_metadata )
|
||||
# Now push the new combined data into the edit controls
|
||||
self.metadataToForm()
|
||||
finally:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
if new_metadata is not None:
|
||||
|
||||
if self.settings.apply_cbl_transform_on_cv_import:
|
||||
new_metadata = CBLTransformer( new_metadata, self.settings ).apply()
|
||||
|
||||
self.metadata.overlay( new_metadata )
|
||||
# Now push the new combined data into the edit controls
|
||||
self.metadataToForm()
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, self.tr("Search"), self.tr("Could not find an issue {0} for that series".format(selector.issue_number)))
|
||||
|
||||
|
||||
def commitMetadata(self):
|
||||
|
||||
@ -836,6 +960,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
else:
|
||||
self.clearDirtyFlag()
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
#QtGui.QMessageBox.information(self, self.tr("Yeah!"), self.tr("File written."))
|
||||
|
||||
else:
|
||||
@ -847,7 +972,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
self.settings.last_selected_data_style = self.data_style
|
||||
self.updateStyleTweaks()
|
||||
self.updateSaveMenu()
|
||||
self.updateMenus()
|
||||
|
||||
def updateCreditColors( self ):
|
||||
inactive_color = QtGui.QColor(255, 170, 150)
|
||||
@ -860,10 +985,12 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
#loop over credit table, mark selected rows
|
||||
r = 0
|
||||
while r < self.twCredits.rowCount():
|
||||
if str(self.twCredits.item(r, 0).text()).lower() not in cix_credits:
|
||||
self.twCredits.item(r, 0).setBackgroundColor( inactive_color )
|
||||
if str(self.twCredits.item(r, 1).text()).lower() not in cix_credits:
|
||||
self.twCredits.item(r, 1).setBackgroundColor( inactive_color )
|
||||
else:
|
||||
self.twCredits.item(r, 0).setBackgroundColor( active_color )
|
||||
self.twCredits.item(r, 1).setBackgroundColor( active_color )
|
||||
# turn off entire primary column
|
||||
self.twCredits.item(r, 0).setBackgroundColor( inactive_color )
|
||||
r = r + 1
|
||||
|
||||
if self.data_style == MetaDataStyle.CBI:
|
||||
@ -871,6 +998,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
r = 0
|
||||
while r < self.twCredits.rowCount():
|
||||
self.twCredits.item(r, 0).setBackgroundColor( active_color )
|
||||
self.twCredits.item(r, 1).setBackgroundColor( active_color )
|
||||
r = r + 1
|
||||
|
||||
|
||||
@ -941,6 +1069,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
enableWidget(item, False )
|
||||
|
||||
self.updateCreditColors()
|
||||
self.pageListEditor.setMetadataStyle( self.data_style )
|
||||
|
||||
def cellDoubleClicked( self, r, c ):
|
||||
self.editCredit()
|
||||
@ -951,27 +1080,53 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
def editCredit( self ):
|
||||
if ( self.twCredits.currentRow() > -1 ):
|
||||
self.modifyCredits( "edit" )
|
||||
|
||||
def updateCreditPrimaryFlag( self, row, primary ):
|
||||
|
||||
# if we're clearing a flagm do it and quit
|
||||
if not primary:
|
||||
self.twCredits.item(row, 0).setText( "" )
|
||||
return
|
||||
|
||||
# otherwise, we need to check for, and clear, other primaries with same role
|
||||
role = str(self.twCredits.item(row, 1).text())
|
||||
r = 0
|
||||
while r < self.twCredits.rowCount():
|
||||
if ( self.twCredits.item(r, 0).text() != "" and
|
||||
str(self.twCredits.item(r, 1).text()).lower() == role.lower() ):
|
||||
self.twCredits.item(r, 0).setText( "" )
|
||||
r = r + 1
|
||||
|
||||
# Now set our new primary
|
||||
self.twCredits.item(row, 0).setText( "Yes" )
|
||||
|
||||
def modifyCredits( self , action ):
|
||||
|
||||
if action == "edit":
|
||||
row = self.twCredits.currentRow()
|
||||
role = self.twCredits.item( row, 0 ).text()
|
||||
name = self.twCredits.item( row, 1 ).text()
|
||||
role = self.twCredits.item( row, 1 ).text()
|
||||
name = self.twCredits.item( row, 2 ).text()
|
||||
primary = self.twCredits.item( row, 0 ).text() != ""
|
||||
else:
|
||||
role = ""
|
||||
name = ""
|
||||
primary = False
|
||||
|
||||
editor = CreditEditorWindow( self, CreditEditorWindow.ModeEdit, role, name )
|
||||
editor = CreditEditorWindow( self, CreditEditorWindow.ModeEdit, role, name, primary )
|
||||
editor.setModal(True)
|
||||
editor.exec_()
|
||||
if editor.result():
|
||||
new_role, new_name = editor.getCredits()
|
||||
new_role, new_name, new_primary = editor.getCredits()
|
||||
|
||||
if new_name == name and new_role == role:
|
||||
if new_name == name and new_role == role and new_primary == primary:
|
||||
#nothing has changed, just quit
|
||||
return
|
||||
|
||||
# name and role is the same, but primary flag changed
|
||||
if new_name == name and new_role == role:
|
||||
self.updateCreditPrimaryFlag( row, new_primary )
|
||||
return
|
||||
|
||||
# check for dupes
|
||||
ok_to_mod = True
|
||||
if self.isDupeCredit( new_role, new_name):
|
||||
@ -986,6 +1141,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if action == "edit":
|
||||
# just remove the row that would be same
|
||||
self.twCredits.removeRow( row )
|
||||
# TODO -- need to find the row of the dupe, and possible change the primary flag
|
||||
|
||||
ok_to_mod = False
|
||||
|
||||
@ -993,12 +1149,13 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if ok_to_mod:
|
||||
#modify it
|
||||
if action == "edit":
|
||||
self.twCredits.item(row, 0).setText( new_role )
|
||||
self.twCredits.item(row, 1).setText( new_name )
|
||||
self.twCredits.item(row, 1).setText( new_role )
|
||||
self.twCredits.item(row, 2).setText( new_name )
|
||||
self.updateCreditPrimaryFlag( row, new_primary )
|
||||
else:
|
||||
# add new entry
|
||||
row = self.twCredits.rowCount()
|
||||
self.addNewCreditEntry( row, new_role, new_name)
|
||||
self.addNewCreditEntry( row, new_role, new_name, new_primary)
|
||||
|
||||
self.updateCreditColors()
|
||||
self.setDirtyFlag()
|
||||
@ -1132,6 +1289,8 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.cbFormat.addItem("Year 1")
|
||||
self.cbFormat.addItem("Year One")
|
||||
|
||||
def removeAuto( self ):
|
||||
self.removeTags( self.data_style )
|
||||
|
||||
def removeCBLTags( self ):
|
||||
self.removeTags( MetaDataStyle.CBI )
|
||||
@ -1154,6 +1313,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
QtGui.QMessageBox.warning(self, self.tr("Remove failed"), self.tr("The tag removal operation seemed to fail!"))
|
||||
else:
|
||||
self.updateInfoBox()
|
||||
self.updateMenus()
|
||||
|
||||
|
||||
def reloadAuto( self ):
|
||||
@ -1199,7 +1359,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
def showPageBrowser( self ):
|
||||
if self.page_browser is None:
|
||||
self.page_browser = PageBrowserWindow( self )
|
||||
self.page_browser = PageBrowserWindow( self, self.metadata )
|
||||
if self.comic_archive is not None:
|
||||
self.page_browser.setComicArchive( self.comic_archive )
|
||||
self.page_browser.finished.connect(self.pageBrowserClosed)
|
||||
@ -1218,9 +1378,55 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
if self.comic_archive is not None and self.comic_archive.hasCBI():
|
||||
dlg = LogWindow( self )
|
||||
text = pprint.pformat( json.loads(self.comic_archive.readRawCBI()), indent=4 )
|
||||
print text
|
||||
dlg.setText(text )
|
||||
dlg.setWindowTitle( "Raw ComicBookLover Tag View" )
|
||||
dlg.exec_()
|
||||
|
||||
def showWiki( self ):
|
||||
webbrowser.open("http://code.google.com/p/comictagger/wiki/Home?tm=6")
|
||||
|
||||
def reportBug( self ):
|
||||
webbrowser.open("http://code.google.com/p/comictagger/issues/list")
|
||||
|
||||
def showForum( self ):
|
||||
webbrowser.open("http://comictagger.forumotion.com/")
|
||||
|
||||
def frontCoverChanged( self, int ):
|
||||
self.metadata.pages = self.pageListEditor.getPageList()
|
||||
self.updateCoverImage()
|
||||
|
||||
def pageListOrderChanged( self ):
|
||||
self.metadata.pages = self.pageListEditor.getPageList()
|
||||
|
||||
def applyCBLTransform(self):
|
||||
self.formToMetadata()
|
||||
self.metadata = CBLTransformer( self.metadata, self.settings ).apply()
|
||||
self.metadataToForm()
|
||||
|
||||
def renameArchive(self):
|
||||
if self.comic_archive is not None:
|
||||
self.formToMetadata()
|
||||
dlg = RenameWindow( self, self.comic_archive, self.metadata, self.settings )
|
||||
dlg.setModal( True )
|
||||
if dlg.exec_():
|
||||
#reopen the archive, since the filename changed
|
||||
print dlg.new_name
|
||||
self.comic_archive = None
|
||||
self.openArchive( dlg.new_name )
|
||||
|
||||
def fileListSelectionChanged( self, qvarFI ):
|
||||
fi = qvarFI.toPyObject()
|
||||
#if fi.cix_md is not None:
|
||||
# print u"{0}".format(fi.cix_md)
|
||||
self.comic_archive = None
|
||||
self.clearForm()
|
||||
|
||||
self.comic_archive = fi.ca
|
||||
if self.data_style == MetaDataStyle.CIX:
|
||||
self.metadata = fi.cix_md
|
||||
else:
|
||||
self.metadata = fi.cbi_md
|
||||
if self.metadata is None:
|
||||
self.metadata = GenericMetadata()
|
||||
|
||||
self.loadCurrentArchive()
|
||||
|
1835
taggerwindow.ui
1835
taggerwindow.ui
File diff suppressed because it is too large
Load Diff
147
todo.txt
147
todo.txt
@ -1,5 +1,81 @@
|
||||
-----------------------------------------------------
|
||||
Config Mgmt
|
||||
Features
|
||||
-----------------------------------------------------
|
||||
|
||||
Multi-file:
|
||||
Does the main UI need to have "View/Read Tag Style" and "Write Tag style" concept?
|
||||
|
||||
Edit functions on list: select, select all, delete,
|
||||
|
||||
Batch Functions:
|
||||
Auto-Select
|
||||
Start/Options Dialog
|
||||
Progress Dialog - maybe reuse
|
||||
Interactive dialog at end
|
||||
Rename
|
||||
Start dialog with preview
|
||||
maybe table with checkboxes?
|
||||
|
||||
Copy Block
|
||||
Verify overwrites
|
||||
|
||||
Turn off drop accept for edit lines/boxes
|
||||
Drop on app goes to list and selects it
|
||||
Accept multiple files on file open dialog
|
||||
Warn on moving selection list away from modified form
|
||||
|
||||
|
||||
ComicArchive: cache each metadata block? Need to make sure cache is cleared on file modify
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
-----------------------------------------------------
|
||||
|
||||
-----------------------------------------------------
|
||||
Big Future Features
|
||||
-----------------------------------------------------
|
||||
GUI to handle mutliple files or folders
|
||||
|
||||
Scrape alternate Covers from ComicVine issue pages
|
||||
|
||||
GCD scraper or DB reader
|
||||
|
||||
pyComicMetaThis CBI features
|
||||
|
||||
Auto search:
|
||||
Searching w/o issue #
|
||||
-----------------------------------------------------
|
||||
Small(er) Future Feature
|
||||
-----------------------------------------------------
|
||||
Parse out the rest of the scan info from filename
|
||||
|
||||
Style sheets for windows/mac/linux
|
||||
|
||||
CLI
|
||||
explicit metadata settings option format
|
||||
-- figure out how to add CBI "tags"
|
||||
-- delete CBI "tags"
|
||||
-- set primary credit flags
|
||||
-- set frontcover and others?
|
||||
|
||||
Archive function to detect tag blocks out of sync
|
||||
|
||||
Settings
|
||||
Add setting to dis-allow writing CBI to RAR
|
||||
Overwrite or overlay
|
||||
|
||||
Google App engine to store hashes
|
||||
Content Hashes, Image hashes, who knows?
|
||||
|
||||
Filename parsing:
|
||||
Rework how series name is separated from issue
|
||||
|
||||
Support marvel's "AU" issues...
|
||||
Mostly done, gotta wait and see what CV does
|
||||
|
||||
Internal GenericMetadata - Make Characters, Genre into lists?
|
||||
|
||||
-----------------------------------------------------
|
||||
Config Mgmt check list
|
||||
-----------------------------------------------------
|
||||
|
||||
Release Process
|
||||
@ -12,75 +88,8 @@ Release Process
|
||||
Make zip on Mac or Linux
|
||||
Tag the repository
|
||||
Upload packages
|
||||
Announce on Forum and Main Page
|
||||
|
||||
-----------------------------------------------------
|
||||
Features
|
||||
-----------------------------------------------------
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
-----------------------------------------------------
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Future
|
||||
-----------------------------------------------------
|
||||
Add license info to About Dialog
|
||||
|
||||
Add CoMet Support
|
||||
|
||||
Support marvel's "AU" issues...
|
||||
|
||||
Style sheets for windows/mac/linux
|
||||
|
||||
File rename
|
||||
-Dialog??
|
||||
formatting with missing pieces.
|
||||
|
||||
TaggerWindow entry fields
|
||||
Special tabbed Dialog needed for:
|
||||
Pages Info - maybe a custom painted widget
|
||||
|
||||
CLI
|
||||
explicit metadata settings option format
|
||||
-- figure out how to add tags
|
||||
-- delete tags
|
||||
write a log for multiple file processing
|
||||
override abort on low confidence flag
|
||||
interactive for choices option?
|
||||
--- or defer choices to end, by keeping special log of match option for each file
|
||||
|
||||
Scrape alternate Covers from ComicVine issue pages
|
||||
|
||||
GCD scraper or DB reader
|
||||
|
||||
Overlay tags, maybe more, on online search
|
||||
|
||||
GUI to handle mutliple files or folders
|
||||
|
||||
Auto search:
|
||||
Searching w/o issue #?
|
||||
|
||||
Wizard for converting between tag styles
|
||||
|
||||
App option to covert RAR to ZIP
|
||||
|
||||
Archive function to detect tag blocks out of sync
|
||||
|
||||
app tour?
|
||||
|
||||
Settings
|
||||
Add setting to dis-allow writing CBI to RAR
|
||||
Overwrite or overlay
|
||||
|
||||
Google App engine to store hashes
|
||||
Content Hashes, Image hashes, who knows?
|
||||
|
||||
Support primary credit flag editing
|
||||
|
||||
Filename parsing:
|
||||
Rework how series name is separated from issue
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
|
@ -85,7 +85,7 @@ class IdentifyThread( QtCore.QThread):
|
||||
|
||||
class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
def __init__(self, parent, series_name, issue_number, year, comic_archive, settings, autoselect=False):
|
||||
def __init__(self, parent, series_name, issue_number, year, cover_index_list, comic_archive, settings, autoselect=False):
|
||||
super(VolumeSelectionWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'volumeselectionwindow.ui' ), self)
|
||||
@ -97,7 +97,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.volume_id = 0
|
||||
self.comic_archive = comic_archive
|
||||
self.immediate_autoselect = autoselect
|
||||
|
||||
self.cover_index_list = cover_index_list
|
||||
self.cv_search_results = None
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
@ -105,9 +107,21 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.btnIssues.clicked.connect(self.showIssues)
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def updateButtons( self ):
|
||||
if self.cv_search_results is not None and len(self.cv_search_results) > 0:
|
||||
enabled = True
|
||||
else:
|
||||
enabled = False
|
||||
|
||||
self.btnRequery.setEnabled( enabled )
|
||||
self.btnIssues.setEnabled( enabled )
|
||||
self.btnAutoSelect.setEnabled( enabled )
|
||||
self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled( enabled )
|
||||
|
||||
def requery( self, ):
|
||||
self.performQuery( refresh=True )
|
||||
self.twList.selectRow(0)
|
||||
@ -136,6 +150,8 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
|
||||
self.ii.setAdditionalMetadata( md )
|
||||
self.ii.onlyUseAdditionalMetaData = True
|
||||
print self.cover_index_list
|
||||
self.ii.cover_page_index = int(self.cover_index_list[0])
|
||||
|
||||
self.id_thread = IdentifyThread( self.ii )
|
||||
self.id_thread.identifyComplete.connect( self.identifyComplete )
|
||||
@ -147,7 +163,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.iddialog.exec_()
|
||||
|
||||
def logIDOutput( self, text ):
|
||||
print text,
|
||||
print unicode(text),
|
||||
self.iddialog.textEdit.ensureCursorVisible()
|
||||
self.iddialog.textEdit.insertPlainText(text)
|
||||
|
||||
@ -216,7 +232,7 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
title = record['name']
|
||||
title += " (" + str(record['start_year']) + ")"
|
||||
title += " (" + unicode(record['start_year']) + ")"
|
||||
title += " - "
|
||||
break
|
||||
|
||||
@ -274,8 +290,9 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
QtGui.QMessageBox.critical(self, self.tr("Network Issue"), self.tr("Could not connect to ComicVine to search for series!"))
|
||||
return
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
self.updateButtons()
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
while self.twList.rowCount() > 0:
|
||||
@ -285,8 +302,8 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
for record in self.cv_search_results:
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record['name']
|
||||
item = QtGui.QTableWidgetItem(item_text)
|
||||
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)
|
||||
@ -316,7 +333,11 @@ class VolumeSelectionWindow(QtGui.QDialog):
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
if self.immediate_autoselect:
|
||||
if len( self.cv_search_results ) == 0:
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtGui.QMessageBox.information(self,"Search Result", "No matches found!")
|
||||
|
||||
if self.immediate_autoselect and len( self.cv_search_results ) > 0:
|
||||
# defer the immediate autoselect so this dialog has time to pop up
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)
|
||||
|
Reference in New Issue
Block a user