diff --git a/README.md b/README.md index 9d2e278..a039326 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,18 @@ This is a fork derived from google code: https://code.google.com/p/comictagger/ -I've cloned the full subversion repository (with git-svn). -Eventually I'll develop some experimental idea for comic tagging... + +Changes in this fork: + - using different unrar library https://pypi.python.org/pypi/unrar/. The previous one used unrar.dll on windows and hackish wrapping of + unrar command on linux, while this new one should use unrarlib on both platforms. From my tests + it is more stable and faster. *Requires unrarlib availability, check unrar module documentation for more information*. + - extracted core libraries in its own package comicapi, shared in a new repository using git subtree for better alignment with comicstreamer + - support for *day of month* field in the GUI + - merge of changes from fcanc fork + +Todo: + - more tests in non-linux platforms + - repackage for simple user installation Follows original readme: diff --git a/comictaggerlib/UnRAR2/UnRARDLL/license.txt b/comicapi/UnRAR2/UnRARDLL/license.txt similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/license.txt rename to comicapi/UnRAR2/UnRARDLL/license.txt diff --git a/comictaggerlib/UnRAR2/UnRARDLL/unrar.dll b/comicapi/UnRAR2/UnRARDLL/unrar.dll similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/unrar.dll rename to comicapi/UnRAR2/UnRARDLL/unrar.dll diff --git a/comictaggerlib/UnRAR2/UnRARDLL/unrar.h b/comicapi/UnRAR2/UnRARDLL/unrar.h similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/unrar.h rename to comicapi/UnRAR2/UnRARDLL/unrar.h diff --git a/comictaggerlib/UnRAR2/UnRARDLL/unrar.lib b/comicapi/UnRAR2/UnRARDLL/unrar.lib similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/unrar.lib rename to comicapi/UnRAR2/UnRARDLL/unrar.lib diff --git a/comictaggerlib/UnRAR2/UnRARDLL/unrardll.txt b/comicapi/UnRAR2/UnRARDLL/unrardll.txt similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/unrardll.txt rename to comicapi/UnRAR2/UnRARDLL/unrardll.txt diff --git a/comictaggerlib/UnRAR2/UnRARDLL/whatsnew.txt b/comicapi/UnRAR2/UnRARDLL/whatsnew.txt similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/whatsnew.txt rename to comicapi/UnRAR2/UnRARDLL/whatsnew.txt diff --git a/comictaggerlib/UnRAR2/UnRARDLL/x64/readme.txt b/comicapi/UnRAR2/UnRARDLL/x64/readme.txt similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/x64/readme.txt rename to comicapi/UnRAR2/UnRARDLL/x64/readme.txt diff --git a/comictaggerlib/UnRAR2/UnRARDLL/x64/unrar64.dll b/comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/x64/unrar64.dll rename to comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll diff --git a/comictaggerlib/UnRAR2/UnRARDLL/x64/unrar64.lib b/comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib similarity index 100% rename from comictaggerlib/UnRAR2/UnRARDLL/x64/unrar64.lib rename to comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib diff --git a/comictaggerlib/UnRAR2/__init__.py b/comicapi/UnRAR2/__init__.py similarity index 100% rename from comictaggerlib/UnRAR2/__init__.py rename to comicapi/UnRAR2/__init__.py diff --git a/comictaggerlib/UnRAR2/rar_exceptions.py b/comicapi/UnRAR2/rar_exceptions.py similarity index 100% rename from comictaggerlib/UnRAR2/rar_exceptions.py rename to comicapi/UnRAR2/rar_exceptions.py diff --git a/comictaggerlib/UnRAR2/test_UnRAR2.py b/comicapi/UnRAR2/test_UnRAR2.py similarity index 100% rename from comictaggerlib/UnRAR2/test_UnRAR2.py rename to comicapi/UnRAR2/test_UnRAR2.py diff --git a/comictaggerlib/UnRAR2/unix.py b/comicapi/UnRAR2/unix.py similarity index 100% rename from comictaggerlib/UnRAR2/unix.py rename to comicapi/UnRAR2/unix.py diff --git a/comictaggerlib/UnRAR2/windows.py b/comicapi/UnRAR2/windows.py similarity index 100% rename from comictaggerlib/UnRAR2/windows.py rename to comicapi/UnRAR2/windows.py diff --git a/comicapi/__init__.py b/comicapi/__init__.py new file mode 100644 index 0000000..0d9bd7c --- /dev/null +++ b/comicapi/__init__.py @@ -0,0 +1 @@ +__author__ = 'dromanin' diff --git a/comicapi/comet.py b/comicapi/comet.py new file mode 100644 index 0000000..1a06977 --- /dev/null +++ b/comicapi/comet.py @@ -0,0 +1,260 @@ +""" +A python class to encapsulate CoMet data +""" + +""" +Copyright 2012-2014 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 = '\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 ) + diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py new file mode 100644 index 0000000..a2ef85c --- /dev/null +++ b/comicapi/comicarchive.py @@ -0,0 +1,1143 @@ +""" +A python class to represent a single comic, be it file or folder of images +""" + +""" +Copyright 2012-2014 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 zipfile +import os +import struct +import sys +import tempfile +import subprocess +import platform +import locale +import shutil + +from natsort import natsorted +from unrar import rarfile +from unrar import unrarlib +import unrar.constants +import ctypes +import io +from unrar import constants + +class OpenableRarFile(rarfile.RarFile): + def open(self, member): + #print "opening %s..." % member + # based on https://github.com/matiasb/python-unrar/pull/4/files + res = [] + if isinstance(member, rarfile.RarInfo): + member = member.filename + archive = unrarlib.RAROpenArchiveDataEx(self.filename, mode=constants.RAR_OM_EXTRACT) + handle = self._open(archive) + found, buf = False, [] + def _callback(msg, UserData, P1, P2): + if msg == constants.UCM_PROCESSDATA: + data = (ctypes.c_char*P2).from_address(P1).raw + buf.append(data) + return 1 + c_callback = unrarlib.UNRARCALLBACK(_callback) + unrarlib.RARSetCallback(handle, c_callback, 1) + try: + rarinfo = self._read_header(handle) + while rarinfo is not None: + #print "checking rar archive %s against %s" % (rarinfo.filename, member) + if rarinfo.filename == member: + self._process_current(handle, constants.RAR_TEST) + found = True + else: + self._process_current(handle, constants.RAR_SKIP) + rarinfo = self._read_header(handle) + except unrarlib.UnrarException: + raise rarfile.BadRarFile("Bad RAR archive data.") + finally: + self._close(handle) + if not found: + raise KeyError('There is no item named %r in the archive' % member) + return ''.join(buf) + + +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 * + +#from settings import ComicTaggerSettings +from comicinfoxml import ComicInfoXml +from comicbookinfo import ComicBookInfo +from comet import CoMet +from genericmetadata import GenericMetadata, PageType +from filenameparser import FileNameParser +from PyPDF2 import PdfFileReader + +class MetaDataStyle: + CBI = 0 + CIX = 1 + COMET = 2 + name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ] + +class ZipArchiver: + + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + zf = zipfile.ZipFile( self.path, 'r' ) + comment = zf.comment + zf.close() + return comment + + def setArchiveComment( self, comment ): + return self.writeZipComment( self.path, comment ) + + def readArchiveFile( self, archive_file ): + data = "" + zf = zipfile.ZipFile( self.path, 'r' ) + + try: + data = zf.read( archive_file ) + except zipfile.BadZipfile as e: + print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + zf.close() + raise IOError + except Exception as e: + zf.close() + print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + raise IOError + finally: + zf.close() + return data + + def removeArchiveFile( self, archive_file ): + try: + self.rebuildZipFile( [ archive_file ] ) + except: + return False + else: + return True + + def writeArchiveFile( self, archive_file, data ): + # At the moment, no other option but to rebuild the whole + # zip archive w/o the indicated file. Very sucky, but maybe + # another solution can be found + try: + self.rebuildZipFile( [ archive_file ] ) + + #now just add the archive file as a new one + zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED ) + zf.writestr( archive_file, data ) + zf.close() + return True + except: + return False + + def getArchiveFilenameList( self ): + try: + zf = zipfile.ZipFile( self.path, 'r' ) + namelist = zf.namelist() + zf.close() + return namelist + except Exception as e: + print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(e, self.path) + return [] + + # zip helper func + def rebuildZipFile( self, exclude_list ): + + # this recompresses the zip archive, without the files in the exclude_list + #print ">> sys.stderr, Rebuilding zip {0} without {1}".format( self.path, exclude_list ) + + # generate temp file + tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) ) + os.close( tmp_fd ) + + zin = zipfile.ZipFile (self.path, 'r') + zout = zipfile.ZipFile (tmp_name, 'w') + for item in zin.infolist(): + buffer = zin.read(item.filename) + if ( item.filename not in exclude_list ): + zout.writestr(item, buffer) + + #preserve the old comment + zout.comment = zin.comment + + zout.close() + zin.close() + + # replace with the new file + os.remove( self.path ) + os.rename( tmp_name, self.path ) + + + def writeZipComment( self, filename, comment ): + """ + This is a custom function for writing a comment to a zip file, + since the built-in one doesn't seem to work on Windows and Mac OS/X + + Fortunately, the zip comment is at the end of the file, and it's + easy to manipulate. See this website for more info: + see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure + """ + + #get file size + statinfo = os.stat(filename) + file_length = statinfo.st_size + + try: + fo = open(filename, "r+b") + + #the starting position, relative to EOF + pos = -4 + + found = False + value = bytearray() + + # walk backwards to find the "End of Central Directory" record + while ( not found ) and ( -pos != file_length ): + # seek, relative to EOF + fo.seek( pos, 2) + + value = fo.read( 4 ) + + #look for the end of central directory signature + if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]): + found = True + else: + # not found, step back another byte + pos = pos - 1 + #print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) + + if found: + + # now skip forward 20 bytes to the comment length word + pos += 20 + fo.seek( pos, 2) + + # Pack the length of the comment string + format = "H" # one 2-byte integer + comment_length = struct.pack(format, len(comment)) # pack integer in a binary string + + # write out the length + fo.write( comment_length ) + fo.seek( pos+2, 2) + + # write out the comment itself + fo.write( comment ) + fo.truncate() + fo.close() + else: + raise Exception('Failed to write comment to zip file!') + except: + 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 ) + if data is not None: + 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 Exception as e: + print >> sys.stderr, u"Error while copying to {0}: {1}".format(self.path, e) + return False + else: + return True + + +#------------------------------------------ +# RAR implementation + +class RarArchiver: + + devnull = None + def __init__( self, path, rar_exe_path ): + self.path = path + self.rar_exe_path = rar_exe_path + + if RarArchiver.devnull is None: + RarArchiver.devnull = open(os.devnull, "w") + + # windows only, keeps the cmd.exe from popping up + if platform.system() == "Windows": + self.startupinfo = subprocess.STARTUPINFO() + self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + else: + self.startupinfo = None + + def __del__(self): + #RarArchiver.devnull.close() + pass + + def getArchiveComment( self ): + + rarc = self.getRARObj() + return rarc.comment + + def setArchiveComment( self, comment ): + + if self.rar_exe_path is not None: + try: + # write comment to temp file + tmp_fd, tmp_name = tempfile.mkstemp() + f = os.fdopen(tmp_fd, 'w+b') + 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', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + + os.remove( tmp_name) + except: + return False + else: + return True + else: + return False + + def readArchiveFile( self, archive_file ): + + # Make sure to escape brackets, since some funky stuff is going on + # underneath with "fnmatch" + #archive_file = archive_file.replace("[", '[[]') + entries = [] + + rarc = self.getRARObj() + + tries = 0 + while tries < 7: + try: + tries = tries+1 + #tmp_folder = tempfile.mkdtemp() + #tmp_file = os.path.join(tmp_folder, archive_file) + #rarc.extract(archive_file, tmp_folder) + data = rarc.open(archive_file) + #data = open(tmp_file).read() + entries = [(rarc.getinfo(archive_file), data)] + + + #shutil.rmtree(tmp_folder, ignore_errors=True) + + #entries = rarc.read_files( archive_file ) + + if entries[0][0].file_size != len(entries[0][1]): + print >> sys.stderr, u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( + entries[0][0].file_size,len(entries[0][1]), self.path, archive_file, tries) + continue + + except (OSError, IOError) as e: + print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + time.sleep(1) + except Exception as e: + print >> sys.stderr, u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + break + + else: + #Success" + #entries is a list of of tuples: ( rarinfo, filedata) + if tries > 1: + print >> sys.stderr, u"Attempted read_files() {0} times".format(tries) + if (len(entries) == 1): + return entries[0][1] + else: + raise IOError + + raise IOError + + + + def writeArchiveFile( self, archive_file, data ): + + if self.rar_exe_path is not None: + try: + 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', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + os.remove( tmp_file) + os.rmdir( tmp_folder) + except: + return False + else: + return True + else: + return False + + def removeArchiveFile( self, archive_file ): + if self.rar_exe_path is not None: + try: + # use external program to remove file from Rar archive + subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + except: + return False + else: + return True + else: + return False + + def getArchiveFilenameList( self ): + + rarc = self.getRARObj() + #namelist = [ item.filename for item in rarc.infolist() ] + #return namelist + + tries = 0 + while tries < 7: + try: + tries = tries+1 + #namelist = [ item.filename for item in rarc.infolist() ] + namelist = [] + for item in rarc.infolist(): + if item.file_size != 0: + namelist.append( item.filename ) + + except (OSError, IOError) as e: + print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + time.sleep(1) + + else: + #Success" + return namelist + + raise e + + + def getRARObj( self ): + tries = 0 + while tries < 7: + try: + tries = tries+1 + #rarc = UnRAR2.RarFile( self.path ) + rarc = OpenableRarFile(self.path) + + except (OSError, IOError) as e: + print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + time.sleep(1) + + else: + #Success" + return rarc + + raise e + +#------------------------------------------ +# Folder implementation +class FolderArchiver: + + def __init__( self, path ): + self.path = path + self.comment_file_name = "ComicTaggerFolderComment.txt" + + def getArchiveComment( self ): + return self.readArchiveFile( self.comment_file_name ) + + def setArchiveComment( self, comment ): + return self.writeArchiveFile( self.comment_file_name, comment ) + + def readArchiveFile( self, archive_file ): + + data = "" + fname = os.path.join( self.path, archive_file ) + try: + with open( fname, 'rb' ) as f: + data = f.read() + f.close() + except IOError as e: + pass + + return data + + def writeArchiveFile( self, archive_file, data ): + + fname = os.path.join( self.path, archive_file ) + try: + with open(fname, 'w+') as f: + f.write( data ) + f.close() + except: + return False + else: + return True + + def removeArchiveFile( self, archive_file ): + + fname = os.path.join( self.path, archive_file ) + try: + os.remove( fname ) + except: + return False + else: + return True + + def getArchiveFilenameList( self ): + return self.listFiles( self.path ) + + def listFiles( self, folder ): + + itemlist = list() + + for item in os.listdir( folder ): + itemlist.append( item ) + if os.path.isdir( item ): + itemlist.extend( self.listFiles( os.path.join( folder, item ) )) + + return itemlist + +#------------------------------------------ +# Unknown implementation +class UnknownArchiver: + + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + return "" + def setArchiveComment( self, comment ): + return False + def readArchiveFile( self ): + return "" + def writeArchiveFile( self, archive_file, data ): + return False + def removeArchiveFile( self, archive_file ): + return False + def getArchiveFilenameList( self ): + return [] + +class PdfArchiver: + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + return "" + def setArchiveComment( self, comment ): + return False + def readArchiveFile( self, page_num ): + return subprocess.check_output(['mudraw', '-o','-', self.path, str(int(os.path.basename(page_num)[:-4]))]) + def writeArchiveFile( self, archive_file, data ): + return False + def removeArchiveFile( self, archive_file ): + return False + def getArchiveFilenameList( self ): + out = [] + pdf = PdfFileReader(open(self.path, 'rb')) + for page in range(1, pdf.getNumPages() + 1): + out.append("/%04d.jpg" % (page)) + return out + +#------------------------------------------------------------------ +class ComicArchive: + + logo_data = None + + class ArchiveType: + Zip, Rar, Folder, Pdf, Unknown = range(5) + + def __init__( self, path, rar_exe_path=None, default_image_path=None ): + self.path = path + + self.rar_exe_path = rar_exe_path + self.ci_xml_filename = 'ComicInfo.xml' + self.comet_default_filename = 'CoMet.xml' + self.resetCache() + self.default_image_path = default_image_path + + # Use file extension to decide which archive test we do first + ext = os.path.splitext(path)[1].lower() + + self.archive_type = self.ArchiveType.Unknown + self.archiver = UnknownArchiver( self.path ) + + if ext == ".cbr" or ext == ".rar": + if self.rarTest(): + self.archive_type = self.ArchiveType.Rar + self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) + + elif self.zipTest(): + self.archive_type = self.ArchiveType.Zip + self.archiver = ZipArchiver( self.path ) + else: + if self.zipTest(): + self.archive_type = self.ArchiveType.Zip + self.archiver = ZipArchiver( self.path ) + + elif self.rarTest(): + self.archive_type = self.ArchiveType.Rar + self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) + elif os.path.basename(self.path)[-3:] == 'pdf': + self.archive_type = self.ArchiveType.Pdf + self.archiver = PdfArchiver(self.path) + + if ComicArchive.logo_data is None: + #fname = ComicTaggerSettings.getGraphic('nocover.png') + fname = self.default_image_path + with open(fname, 'rb') as fd: + ComicArchive.logo_data = fd.read() + + # Clears the cached data + def resetCache( self ): + self.has_cix = None + self.has_cbi = None + self.has_comet = None + self.comet_filename = None + self.page_count = None + self.page_list = None + self.cix_md = None + self.cbi_md = None + self.comet_md = None + + def loadCache( self, style_list ): + for style in style_list: + self.readMetadata(style) + + def rename( self, path ): + self.path = path + self.archiver.path = path + + def zipTest( self ): + return zipfile.is_zipfile( self.path ) + + def rarTest( self ): + try: + rarc = rarfile.RarFile( self.path ) + except: # InvalidRARArchive: + return False + else: + return True + + + def isZip( self ): + return self.archive_type == self.ArchiveType.Zip + + def isRar( self ): + return self.archive_type == self.ArchiveType.Rar + def isPdf(self): + return self.archive_type == self.ArchiveType.Pdf + def isFolder( self ): + return self.archive_type == self.ArchiveType.Folder + + def isWritable( self, check_rar_status=True ): + if self.archive_type == self.ArchiveType.Unknown : + return False + + elif check_rar_status and self.isRar() and self.rar_exe_path is None: + return False + + elif not os.access(self.path, os.W_OK): + return False + + elif ((self.archive_type != self.ArchiveType.Folder) and + (not os.access( os.path.dirname( os.path.abspath(self.path)), os.W_OK ))): + return False + + return True + + def isWritableForStyle( self, data_style ): + + if self.isRar() and data_style == MetaDataStyle.CBI: + return False + + return self.isWritable() + + def seemsToBeAComicArchive( self ): + + # Do we even care about extensions?? + ext = os.path.splitext(self.path)[1].lower() + + if ( + ( self.isZip() or self.isRar() or self.isPdf()) #or self.isFolder() ) + and + ( self.getNumberOfPages() > 0) + + ): + return True + else: + return False + + def readMetadata( self, style ): + + if style == MetaDataStyle.CIX: + 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: + retcode = self.writeCIX( metadata ) + elif style == MetaDataStyle.CBI: + retcode = self.writeCBI( metadata ) + elif style == MetaDataStyle.COMET: + retcode = self.writeCoMet( metadata ) + return retcode + + + def hasMetadata( self, style ): + + if style == MetaDataStyle.CIX: + 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: + retcode = self.removeCIX() + elif style == MetaDataStyle.CBI: + retcode = self.removeCBI() + elif style == MetaDataStyle.COMET: + retcode = self.removeCoMet() + return retcode + + def getPage( self, index ): + + image_data = None + + filename = self.getPageName( index ) + + if filename is not None: + try: + image_data = self.archiver.readArchiveFile( filename ) + except IOError: + print >> sys.stderr, u"Error reading in page. Substituting logo page." + image_data = ComicArchive.logo_data + + return image_data + + def getPageName( self, index ): + + if index is None: + return None + + page_list = self.getPageNameList() + + num_pages = len( page_list ) + if num_pages == 0 or index >= num_pages: + return None + + return page_list[index] + + def getScannerPageIndex( self ): + + scanner_page_index = None + + #make a guess at the scanner page + name_list = self.getPageNameList() + count = self.getNumberOfPages() + + #too few pages to really know + if count < 5: + return None + + # count the length of every filename, and count occurences + length_buckets = dict() + for name in name_list: + fname = os.path.split(name)[1] + length = len(fname) + if length_buckets.has_key( length ): + length_buckets[ length ] += 1 + else: + length_buckets[ length ] = 1 + + # sort by most common + sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True) + + # statistical mode occurence is first + mode_length = sorted_buckets[0][0] + + # we are only going to consider the final image file: + final_name = os.path.split(name_list[count-1])[1] + + common_length_list = list() + for name in name_list: + if len(os.path.split(name)[1]) == mode_length: + common_length_list.append( os.path.split(name)[1] ) + + prefix = os.path.commonprefix(common_length_list) + + if mode_length <= 7 and prefix == "": + #probably all numbers + if len(final_name) > mode_length: + scanner_page_index = count-1 + + # see if the last page doesn't start with the same prefix as most others + elif not final_name.startswith(prefix): + scanner_page_index = count-1 + + return scanner_page_index + + + def getPageNameList( self , sort_list=True): + + 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: + def keyfunc(k): + #hack to account for some weird scanner ID pages + #basename=os.path.split(k)[1] + #if basename < '0': + # k = os.path.join(os.path.split(k)[0], "z" + basename) + return k.lower() + + files = natsorted(files, key=keyfunc,signed=False) + + # make a sub-list of image files + self.page_list = [] + for name in files: + if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif", "webp" ] and os.path.basename(name)[0] != "." ): + self.page_list.append(name) + + return self.page_list + + def getNumberOfPages( self ): + + if self.page_count is None: + self.page_count = len( self.getPageNameList( ) ) + return self.page_count + + def readCBI( self ): + if self.cbi_md is None: + raw_cbi = self.readRawCBI() + if raw_cbi is None: + self.cbi_md = GenericMetadata() + else: + self.cbi_md = ComicBookInfo().metadataFromString( raw_cbi ) + + self.cbi_md.setDefaultPageList( self.getNumberOfPages() ) + + return self.cbi_md + + def readRawCBI( self ): + if ( not self.hasCBI() ): + return None + + 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 ): + if metadata is not None: + self.applyArchiveInfoToMetadata( metadata ) + cbi_string = ComicBookInfo().stringFromMetadata( metadata ) + write_success = self.archiver.setArchiveComment( cbi_string ) + if write_success: + self.has_cbi = True + self.cbi_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCBI( self ): + if self.hasCBI(): + write_success = self.archiver.setArchiveComment( "" ) + if write_success: + self.has_cbi = False + self.cbi_md = None + self.resetCache() + return write_success + return True + + def readCIX( self ): + if self.cix_md is None: + raw_cix = self.readRawCIX() + if raw_cix is None or raw_cix == "": + self.cix_md = GenericMetadata() + else: + self.cix_md = ComicInfoXml().metadataFromString( raw_cix ) + + #validate the existing page list (make sure count is correct) + if len ( self.cix_md.pages ) != 0 : + if len ( self.cix_md.pages ) != self.getNumberOfPages(): + # pages array doesn't match the actual number of images we're seeing + # in the archive, so discard the data + self.cix_md.pages = [] + + if len( self.cix_md.pages ) == 0: + self.cix_md.setDefaultPageList( self.getNumberOfPages() ) + + return self.cix_md + + def readRawCIX( self ): + if not self.hasCIX(): + return None + try: + raw_cix = self.archiver.readArchiveFile( self.ci_xml_filename ) + except IOError: + print "Error reading in raw CIX!" + raw_cix = "" + return raw_cix + + def writeCIX(self, metadata): + + if metadata is not None: + self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True ) + cix_string = ComicInfoXml().stringFromMetadata( metadata ) + write_success = self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string ) + if write_success: + self.has_cix = True + self.cix_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCIX( self ): + if self.hasCIX(): + write_success = self.archiver.removeArchiveFile( self.ci_xml_filename ) + if write_success: + self.has_cix = False + self.cix_md = None + self.resetCache() + return write_success + return True + + + def hasCIX(self): + 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 ): + if self.comet_md is None: + raw_comet = self.readRawCoMet() + if raw_comet is None or raw_comet == "": + self.comet_md = GenericMetadata() + else: + self.comet_md = CoMet().metadataFromString( raw_comet ) + + self.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 self.comet_md.coverImage is not None: + cover_idx = 0 + for idx,f in enumerate(self.getPageNameList()): + if self.comet_md.coverImage == f: + cover_idx = idx + break + if cover_idx != 0: + del (self.comet_md.pages[0]['Type'] ) + self.comet_md.pages[ cover_idx ]['Type'] = PageType.FrontCover + + return self.comet_md + + def readRawCoMet( self ): + if not self.hasCoMet(): + print >> sys.stderr, self.path, "doesn't have CoMet data!" + return None + + try: + raw_comet = self.archiver.readArchiveFile( self.comet_filename ) + except IOError: + print >> sys.stderr, u"Error reading in raw CoMet!" + raw_comet = "" + return raw_comet + + 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 ) + write_success = self.archiver.writeArchiveFile( self.comet_filename, comet_string ) + if write_success: + self.has_comet = True + self.comet_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCoMet( self ): + if self.hasCoMet(): + write_success = self.archiver.removeArchiveFile( self.comet_filename ) + if write_success: + self.has_comet = False + self.comet_md = None + self.resetCache() + return write_success + return True + + def hasCoMet(self): + if self.has_comet is None: + self.has_comet = False + if not self.seemsToBeAComicArchive(): + return self.has_comet + + #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 + try: + data = self.archiver.readArchiveFile( n ) + except: + data = "" + print >> sys.stderr, u"Error reading in Comet XML for validation!" + if CoMet().validateString( data ): + # since we found it, save it! + self.comet_filename = n + self.has_comet = True + break + + return self.has_comet + + + + 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 ) + if data is not None: + try: + im = Image.open(StringIO.StringIO(data)) + w,h = im.size + + p['ImageSize'] = str(len(data)) + p['ImageHeight'] = str(h) + p['ImageWidth'] = str(w) + except IOError: + p['ImageSize'] = str(len(data)) + + else: + if 'ImageSize' not in p: + data = self.getPage( idx ) + p['ImageSize'] = str(len(data)) + + + + def metadataFromFilename( self , parse_scan_info=True): + + metadata = GenericMetadata() + + fnp = FileNameParser() + fnp.parseFilename( self.path ) + + if fnp.issue != "": + metadata.issue = fnp.issue + if fnp.series != "": + metadata.series = fnp.series + if fnp.volume != "": + metadata.volume = fnp.volume + if fnp.year != "": + metadata.year = fnp.year + if fnp.issue_count != "": + metadata.issueCount = fnp.issue_count + if parse_scan_info: + if fnp.remainder != "": + metadata.scanInfo = fnp.remainder + + 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 ) + diff --git a/comicapi/comicbookinfo.py b/comicapi/comicbookinfo.py new file mode 100644 index 0000000..a0bbaf0 --- /dev/null +++ b/comicapi/comicbookinfo.py @@ -0,0 +1,152 @@ +""" +A python class to encapsulate the ComicBookInfo data +""" + +""" +Copyright 2012-2014 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 json +from datetime import datetime +import zipfile + +from genericmetadata import GenericMetadata +import utils +#import ctversion + +class ComicBookInfo: + + + def metadataFromString( self, string ): + + cbi_container = json.loads( unicode(string, 'utf-8') ) + + metadata = GenericMetadata() + + cbi = cbi_container[ 'ComicBookInfo/1.0' ] + + #helper func + # If item is not in CBI, return None + def xlate( cbi_entry): + if cbi_entry in cbi: + return cbi[cbi_entry] + else: + return None + + metadata.series = xlate( 'series' ) + metadata.title = xlate( 'title' ) + metadata.issue = xlate( 'issue' ) + metadata.publisher = xlate( 'publisher' ) + metadata.month = xlate( 'publicationMonth' ) + metadata.year = xlate( 'publicationYear' ) + metadata.issueCount = xlate( 'numberOfIssues' ) + metadata.comments = xlate( 'comments' ) + metadata.credits = xlate( 'credits' ) + metadata.genre = xlate( 'genre' ) + metadata.volume = xlate( 'volume' ) + metadata.volumeCount = xlate( 'numberOfVolumes' ) + metadata.language = xlate( 'language' ) + metadata.country = xlate( 'country' ) + 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 + pattern = metadata.language + metadata.language = None + for key in utils.getLanguageDict(): + if utils.getLanguageDict()[ key ] == pattern.encode('utf-8'): + metadata.language = key + break + + metadata.isEmpty = False + + return metadata + + def stringFromMetadata( self, metadata ): + + cbi_container = self.createJSONDictionary( metadata ) + return json.dumps( cbi_container ) + + #verify that the string actually contains CBI data in JSON format + def validateString( self, string ): + + try: + cbi_container = json.loads( string ) + except: + return False + + return ( 'ComicBookInfo/1.0' in cbi_container ) + + + def createJSONDictionary( self, metadata ): + + # Create the dictionary that we will convert to JSON text + cbi = dict() + cbi_container = {'appID' : 'ComicTagger/' + '1.0.0', #ctversion.version, + 'lastModified' : str(datetime.now()), + 'ComicBookInfo/1.0' : cbi } + + #helper func + def assign( cbi_entry, md_entry): + if md_entry is not None: + cbi[cbi_entry] = md_entry + + #helper func + def toInt(s): + i = None + if type(s) in [ str, unicode, 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', toInt(metadata.month) ) + assign( 'publicationYear', toInt(metadata.year) ) + assign( 'numberOfIssues', toInt(metadata.issueCount) ) + assign( 'comments', metadata.comments ) + assign( 'genre', metadata.genre ) + assign( 'volume', toInt(metadata.volume) ) + assign( 'numberOfVolumes', toInt(metadata.volumeCount) ) + assign( 'language', utils.getLanguageFromISO(metadata.language) ) + assign( 'country', metadata.country ) + assign( 'rating', metadata.criticalRating ) + assign( 'credits', metadata.credits ) + assign( 'tags', metadata.tags ) + + return cbi_container + + + def writeToExternalFile( self, filename, metadata ): + + cbi_container = self.createJSONDictionary(metadata) + + f = open(filename, 'w') + f.write(json.dumps(cbi_container, indent=4)) + f.close + diff --git a/comicapi/comicinfoxml.py b/comicapi/comicinfoxml.py new file mode 100644 index 0000000..9e9df07 --- /dev/null +++ b/comicapi/comicinfoxml.py @@ -0,0 +1,293 @@ +""" +A python class to encapsulate ComicRack's ComicInfo.xml data +""" + +""" +Copyright 2012-2014 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 ComicInfoXml: + + 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 getParseableCredits( self ): + parsable_credits = [] + parsable_credits.extend( self.writer_synonyms ) + parsable_credits.extend( self.penciller_synonyms ) + parsable_credits.extend( self.inker_synonyms ) + parsable_credits.extend( self.colorist_synonyms ) + parsable_credits.extend( self.letterer_synonyms ) + parsable_credits.extend( self.cover_synonyms ) + parsable_credits.extend( self.editor_synonyms ) + return parsable_credits + + def metadataFromString( self, string ): + + tree = ET.ElementTree(ET.fromstring( string )) + return self.convertXMLToMetadata( tree ) + + def stringFromMetadata( self, metadata ): + + header = '\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("ComicInfo") + root.attrib['xmlns:xsi']="http://www.w3.org/2001/XMLSchema-instance" + root.attrib['xmlns:xsd']="http://www.w3.org/2001/XMLSchema" + #helper func + def assign( cix_entry, md_entry): + if md_entry is not None: + ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry) + + assign( 'Title', md.title ) + assign( 'Series', md.series ) + assign( 'Number', md.issue ) + assign( 'Count', md.issueCount ) + assign( 'Volume', md.volume ) + assign( 'AlternateSeries', md.alternateSeries ) + assign( 'AlternateNumber', md.alternateNumber ) + assign( 'StoryArc', md.storyArc ) + assign( 'SeriesGroup', md.seriesGroup ) + assign( 'AlternateCount', md.alternateCount ) + assign( 'Summary', md.comments ) + assign( 'Notes', md.notes ) + assign( 'Year', md.year ) + assign( 'Month', md.month ) + assign( 'Day', md.day ) + + # 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() + + # first, loop thru credits, and build a list for each role that CIX supports + for credit in metadata.credits: + + if credit['role'].lower() in set( self.writer_synonyms ): + credit_writer_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.penciller_synonyms ): + credit_penciller_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.inker_synonyms ): + credit_inker_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.colorist_synonyms ): + credit_colorist_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.letterer_synonyms ): + credit_letterer_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.cover_synonyms ): + credit_cover_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.editor_synonyms ): + credit_editor_list.append(credit['person'].replace(",","")) + + # second, convert each list to string, and add to XML struct + if len( credit_writer_list ) > 0: + node = ET.SubElement(root, 'Writer') + node.text = utils.listToString( credit_writer_list ) + + if len( credit_penciller_list ) > 0: + node = ET.SubElement(root, 'Penciller') + node.text = utils.listToString( credit_penciller_list ) + + if len( credit_inker_list ) > 0: + node = ET.SubElement(root, 'Inker') + node.text = utils.listToString( credit_inker_list ) + + if len( credit_colorist_list ) > 0: + node = ET.SubElement(root, 'Colorist') + node.text = utils.listToString( credit_colorist_list ) + + if len( credit_letterer_list ) > 0: + node = ET.SubElement(root, 'Letterer') + node.text = utils.listToString( credit_letterer_list ) + + if len( credit_cover_list ) > 0: + node = ET.SubElement(root, 'CoverArtist') + node.text = utils.listToString( credit_cover_list ) + + if len( credit_editor_list ) > 0: + node = ET.SubElement(root, 'Editor') + node.text = utils.listToString( credit_editor_list ) + + assign( 'Publisher', md.publisher ) + assign( 'Imprint', md.imprint ) + assign( 'Genre', md.genre ) + assign( 'Web', md.webLink ) + assign( 'PageCount', md.pageCount ) + assign( 'LanguageISO', md.language ) + assign( 'Format', md.format ) + assign( 'AgeRating', md.maturityRating ) + if md.blackAndWhite is not None and md.blackAndWhite: + ET.SubElement(root, 'BlackAndWhite').text = "Yes" + assign( 'Manga', md.manga ) + assign( 'Characters', md.characters ) + assign( 'Teams', md.teams ) + assign( 'Locations', md.locations ) + assign( 'ScanInformation', md.scanInfo ) + + # loop and add the page entries under pages node + if len( md.pages ) > 0: + pages_node = ET.SubElement(root, 'Pages') + for page_dict in md.pages: + page_node = ET.SubElement(pages_node, 'Page') + page_node.attrib = page_dict + + # self pretty-print + self.indent(root) + + # wrap it in an ElementTree instance, and save as XML + tree = ET.ElementTree(root) + return tree + + + def convertXMLToMetadata( self, tree ): + + root = tree.getroot() + + if root.tag != 'ComicInfo': + raise 1 + return None + + metadata = GenericMetadata() + md = metadata + + + # Helper function + def xlate( tag ): + node = root.find( tag ) + if node is not None: + return node.text + else: + return None + + md.series = xlate( 'Series' ) + md.title = xlate( 'Title' ) + md.issue = xlate( 'Number' ) + md.issueCount = xlate( 'Count' ) + md.volume = xlate( 'Volume' ) + md.alternateSeries = xlate( 'AlternateSeries' ) + md.alternateNumber = xlate( 'AlternateNumber' ) + md.alternateCount = xlate( 'AlternateCount' ) + md.comments = xlate( 'Summary' ) + md.notes = xlate( 'Notes' ) + md.year = xlate( 'Year' ) + md.month = xlate( 'Month' ) + md.day = xlate( 'Day' ) + md.publisher = xlate( 'Publisher' ) + md.imprint = xlate( 'Imprint' ) + md.genre = xlate( 'Genre' ) + md.webLink = xlate( 'Web' ) + md.language = xlate( 'LanguageISO' ) + md.format = xlate( 'Format' ) + md.manga = xlate( 'Manga' ) + md.characters = xlate( 'Characters' ) + md.teams = xlate( 'Teams' ) + md.locations = xlate( 'Locations' ) + md.pageCount = xlate( 'PageCount' ) + md.scanInfo = xlate( 'ScanInformation' ) + md.storyArc = xlate( 'StoryArc' ) + md.seriesGroup = xlate( 'SeriesGroup' ) + md.maturityRating = xlate( 'AgeRating' ) + + tmp = xlate( 'BlackAndWhite' ) + md.blackAndWhite = False + if tmp is not None and tmp.lower() in [ "yes", "true", "1" ]: + md.blackAndWhite = True + # Now extract the credit info + for n in root: + if ( n.tag == 'Writer' or + n.tag == 'Penciller' or + n.tag == 'Inker' or + n.tag == 'Colorist' or + n.tag == 'Letterer' or + n.tag == 'Editor' + ): + if n.text is not None: + for name in n.text.split(','): + metadata.addCredit( name.strip(), n.tag ) + + if n.tag == 'CoverArtist': + if n.text is not None: + for name in n.text.split(','): + metadata.addCredit( name.strip(), "Cover" ) + + # parse page data now + pages_node = root.find( "Pages" ) + if pages_node is not None: + for page in pages_node: + metadata.pages.append( page.attrib ) + #print page.attrib + + metadata.isEmpty = False + + return metadata + + def writeToExternalFile( self, filename, metadata ): + + tree = self.convertMetadataToXML( self, metadata ) + #ET.dump(tree) + tree.write(filename, encoding='utf-8') + + def readFromExternalFile( self, filename ): + + tree = ET.parse( filename ) + return self.convertXMLToMetadata( tree ) + diff --git a/comicapi/filenameparser.py b/comicapi/filenameparser.py new file mode 100644 index 0000000..db1790d --- /dev/null +++ b/comicapi/filenameparser.py @@ -0,0 +1,286 @@ +""" +Functions for parsing comic info from filename + +This should probably be re-written, but, well, it mostly works! + +""" + +""" +Copyright 2012-2014 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. +""" + + +# Some portions of this code were modified from pyComicMetaThis project +# http://code.google.com/p/pycomicmetathis/ + +import re +import os +from urllib import unquote + +class FileNameParser: + + def repl(self, m): + return ' ' * len(m.group()) + + def fixSpaces( self, string, remove_dashes=True ): + if remove_dashes: + placeholders = ['[-_]',' +'] + else: + placeholders = ['[_]',' +'] + for ph in placeholders: + string = re.sub(ph, self.repl, string ) + return string #.strip() + + + def getIssueCount( self,filename, issue_end ): + + count = "" + filename = filename[issue_end:] + + # replace any name seperators with spaces + tmpstr = self.fixSpaces(filename) + found = False + + match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE) + if match: + count = match.group() + found = True + + if not found: + match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE) + if match: + count = match.group() + found = True + + + count = count.lstrip("0") + + return count + + def getIssueNumber( self, filename ): + + # Returns a tuple of issue number string, and start and end indexs in the filename + # (The indexes will be used to split the string up for further parsing) + + found = False + issue = '' + start = 0 + end = 0 + + # first, look for multiple "--", this means 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 followed by issue + filename = re.sub("--.*", self.repl, filename) + + elif "__" in filename: + # the pattern seems to be that anything to left of the first "__" is the series name followed by issue + filename = re.sub("__.*", self.repl, filename) + + filename = filename.replace("+", " ") + + # replace parenthetical phrases with spaces + filename = re.sub( "\(.*?\)", self.repl, filename) + filename = re.sub( "\[.*?\]", self.repl, filename) + + # replace any name seperators with spaces + filename = self.fixSpaces(filename) + + # remove any "of NN" phrase with spaces (problem: this could break on some titles) + filename = re.sub( "of [\d]+", self.repl, filename) + + #print u"[{0}]".format(filename) + + # we should now have a cleaned up filename version with all the words in + # the same positions as original filename + + # make a list of each word and its position + word_list = list() + for m in re.finditer("\S+", filename): + word_list.append( (m.group(0), m.start(), m.end()) ) + + # remove the first word, since it can't be the issue number + if len(word_list) > 1: + word_list = word_list[1:] + else: + #only one word?? just bail. + return issue, start, end + + # Now try to search for the likely issue number word in the list + + # first look for a word with "#" followed by digits with optional sufix + # this is almost certainly the issue number + for w in reversed(word_list): + if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): + found = True + break + + # same as above but w/o a '#', and only look at the last word in the list + if not found: + w = word_list[-1] + if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): + found = True + + # now try to look for a # followed by any characters + if not found: + for w in reversed(word_list): + if re.match("#\S+", w[0]): + found = True + break + + if found: + issue = w[0] + start = w[1] + end = w[2] + if issue[0] == '#': + issue = issue[1:] + + return issue, start, end + + def getSeriesName(self, filename, issue_start ): + + # use the issue number string index to split the filename string + + if issue_start != 0: + filename = filename[:issue_start] + + # in case there is no issue number, remove some obvious stuff + if "--" in filename: + # the pattern seems to be that anything to left of the first "--" is the series name followed by issue + filename = re.sub("--.*", self.repl, filename) + + elif "__" in filename: + # the pattern seems to be that anything to left of the first "__" is the series name followed by issue + filename = re.sub("__.*", self.repl, filename) + + filename = filename.replace("+", " ") + tmpstr = self.fixSpaces(filename, remove_dashes=False) + + series = tmpstr + volume = "" + + #save the last word + try: + last_word = series.split()[-1] + except: + last_word = "" + + # remove any parenthetical phrases + series = re.sub( "\(.*?\)", "", series) + + # search for volume number + match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series) + if match: + series = match.group(1) + volume = match.group(3) + + # if a volume wasn't found, see if the last word is a year in parentheses + # since that's a common way to designate the volume + if volume == "": + #match either (YEAR), (YEAR-), or (YEAR-YEAR2) + match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word) + if match: + volume = match.group(2) + + series = series.strip() + + # if we don't have an issue number (issue_start==0), look + # for hints i.e. "TPB", "one-shot", "OS", "OGN", etc that might + # be removed to help search online + if issue_start == 0: + one_shot_words = [ "tpb", "os", "one-shot", "ogn", "gn" ] + try: + last_word = series.split()[-1] + if last_word.lower() in one_shot_words: + series = series.rsplit(' ', 1)[0] + except: + pass + + return series, volume.strip() + + def getYear( self,filename, issue_end): + + filename = filename[issue_end:] + + year = "" + # look for four digit number with "(" ")" or "--" around it + match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename) + if match: + year = match.group() + # remove non-numerics + year = re.sub("[^0-9]", "", year) + return year + + def getRemainder( self, filename, year, count, volume, issue_end ): + + #make a guess at where the the non-interesting stuff begins + remainder = "" + + if "--" in filename: + remainder = filename.split("--",1)[1] + elif "__" in filename: + remainder = filename.split("__",1)[1] + elif issue_end != 0: + remainder = filename[issue_end:] + + remainder = self.fixSpaces(remainder, remove_dashes=False) + if volume != "": + remainder = remainder.replace("Vol."+volume,"",1) + if year != "": + remainder = remainder.replace(year,"",1) + if count != "": + remainder = remainder.replace("of "+count,"",1) + + remainder = remainder.replace("()","") + remainder = remainder.replace(" "," ") # cleans some whitespace mess + + return remainder.strip() + + def parseFilename( self, filename ): + + # remove the path + filename = os.path.basename(filename) + + # remove the extension + filename = os.path.splitext(filename)[0] + + #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", ")") + + self.issue, issue_start, issue_end = self.getIssueNumber(filename) + self.series, self.volume = self.getSeriesName(filename, issue_start) + + + # provides proper value when the filename doesn't have a issue number + if issue_end == 0: + issue_end=len(self.series) + + self.year = self.getYear(filename, issue_end) + self.issue_count = self.getIssueCount(filename, issue_end) + self.remainder = self.getRemainder( filename, self.year, self.issue_count, self.volume, issue_end ) + + if self.issue != "": + # strip off leading zeros + self.issue = self.issue.lstrip("0") + if self.issue == "": + self.issue = "0" + if self.issue[0] == ".": + self.issue = "0" + self.issue diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py new file mode 100644 index 0000000..8e7aeaf --- /dev/null +++ b/comicapi/genericmetadata.py @@ -0,0 +1,316 @@ +""" + A python class for internal metadata storage + + The goal of this class is to handle ALL the data that might come from various + tagging schemes and databases, such as ComicVine or GCD. This makes conversion + possible, however lossy it might be + +""" + +""" +Copyright 2012-2014 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 + +# These page info classes are exactly the same as the CIX scheme, since it's unique +class PageType: + FrontCover = "FrontCover" + InnerCover = "InnerCover" + Roundup = "Roundup" + Story = "Story" + Advertisement = "Advertisement" + Editorial = "Editorial" + Letters = "Letters" + Preview = "Preview" + BackCover = "BackCover" + Other = "Other" + Deleted = "Deleted" + +""" +class PageInfo: + Image = 0 + Type = PageType.Story + DoublePage = False + ImageSize = 0 + Key = "" + ImageWidth = 0 + ImageHeight = 0 +""" + +class GenericMetadata: + + def __init__(self): + + self.isEmpty = True + self.tagOrigin = None + + self.series = None + self.issue = None + self.title = None + self.publisher = None + self.month = None + self.year = None + self.day = None + self.issueCount = None + self.volume = None + self.genre = None + self.language = None # 2 letter iso code + self.comments = None # use same way as Summary in CIX + + self.volumeCount = None + self.criticalRating = None + self.country = None + + self.alternateSeries = None + self.alternateNumber = None + self.alternateCount = None + self.imprint = None + self.notes = None + self.webLink = None + self.format = None + self.manga = None + self.blackAndWhite = None + self.pageCount = None + self.maturityRating = None + + self.storyArc = None + self.seriesGroup = None + self.scanInfo = None + + self.characters = None + self.teams = None + self.locations = None + + self.credits = list() + self.tags = list() + self.pages = list() + + # 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 + # values, over-write them to this one + + def assign( cur, new ): + if new is not None: + if type(new) == str and len(new) == 0: + setattr(self, cur, None) + else: + setattr(self, cur, new) + + if not new_md.isEmpty: + self.isEmpty = False + + assign( 'series', new_md.series ) + assign( "issue", new_md.issue ) + assign( "issueCount", new_md.issueCount ) + assign( "title", new_md.title ) + assign( "publisher", new_md.publisher ) + assign( "day", new_md.day ) + assign( "month", new_md.month ) + assign( "year", new_md.year ) + assign( "volume", new_md.volume ) + assign( "volumeCount", new_md.volumeCount ) + assign( "genre", new_md.genre ) + assign( "language", new_md.language ) + assign( "country", new_md.country ) + 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( "format", new_md.format ) + assign( "manga", new_md.manga ) + assign( "blackAndWhite", new_md.blackAndWhite ) + assign( "maturityRating", new_md.maturityRating ) + 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 + + # not sure if the tags and pages should broken down, or treated + # as whole lists.... + + # 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 ) + + if len(new_md.pages) > 0: + assign( "pages", new_md.pages ) + + + def overlayCredits( self, new_credits ): + for c in new_credits: + if c.has_key('primary') and c['primary']: + primary = True + else: + primary = False + + # Remove credit role if person is blank + if c['person'] == "": + for r in reversed(self.credits): + if r['role'].lower() == c['role'].lower(): + self.credits.remove(r) + # 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() + credit['person'] = person + credit['role'] = role + if primary: + credit['primary'] = primary + + # look to see if it's not already there... + found = False + for c in self.credits: + if ( c['person'].lower() == person.lower() and + c['role'].lower() == role.lower() ): + # no need to add it. just adjust the "primary" flag as needed + c['primary'] = primary + found = True + break + + if not found: + self.credits.append(credit) + + + def __str__( self ): + vals = [] + if self.isEmpty: + return "No metadata" + + def add_string( tag, val ): + if val is not None and u"{0}".format(val) != "": + vals.append( (tag, val) ) + + def add_attr_string( tag ): + val = getattr(self,tag) + add_string( tag, getattr(self,tag) ) + + add_attr_string( "series" ) + add_attr_string( "issue" ) + add_attr_string( "issueCount" ) + add_attr_string( "title" ) + add_attr_string( "publisher" ) + add_attr_string( "year" ) + add_attr_string( "month" ) + add_attr_string( "day" ) + add_attr_string( "volume" ) + add_attr_string( "volumeCount" ) + add_attr_string( "genre" ) + add_attr_string( "language" ) + add_attr_string( "country" ) + add_attr_string( "criticalRating" ) + add_attr_string( "alternateSeries" ) + add_attr_string( "alternateNumber" ) + add_attr_string( "alternateCount" ) + add_attr_string( "imprint" ) + 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" ) + add_attr_string( "storyArc" ) + add_attr_string( "seriesGroup" ) + add_attr_string( "scanInfo" ) + add_attr_string( "characters" ) + add_attr_string( "teams" ) + add_attr_string( "locations" ) + add_attr_string( "comments" ) + add_attr_string( "notes" ) + + add_string( "tags", utils.listToString( self.tags ) ) + + for c in self.credits: + primary = "" + if c.has_key('primary') and c['primary']: + primary = " [P]" + add_string( "credit", c['role']+": "+c['person'] + primary) + + # find the longest field name + flen = 0 + for i in vals: + flen = max( flen, len(i[0]) ) + flen += 1 + + #format the data nicely + outstr = "" + fmt_str = u"{0: <" + str(flen) + "} {1}\n" + for i in vals: + outstr += fmt_str.format( i[0]+":", i[1] ) + + return outstr diff --git a/comicapi/issuestring.py b/comicapi/issuestring.py new file mode 100644 index 0000000..751aa8c --- /dev/null +++ b/comicapi/issuestring.py @@ -0,0 +1,140 @@ +# coding=utf-8 +""" +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" +"100-2" + +""" + +""" +Copyright 2012-2014 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): + + # break up the issue number string into 2 parts: the numeric and suffix string. + # ( assumes that the numeric portion is always first ) + + self.num = None + self.suffix = "" + + if text is None: + return + + if type(text) == int: + text = str(text) + + if len(text) == 0: + return + + text = unicode(text) + + #skip the minus sign if it's first + if text[0] == '-': + start = 1 + else: + start = 0 + + # if it's still not numeric at start skip it + if text[start].isdigit() or text[start] == ".": + # walk through the string, look for split point (the first non-numeric) + decimal_count = 0 + for idx in range( start, len(text) ): + if text[idx] not in "0123456789.": + break + # special case: also split on second "." + if text[idx] == ".": + decimal_count += 1 + if decimal_count > 1: + break + else: + idx = len(text) + + # move trailing numeric decimal to suffix + # (only if there is other junk after ) + if text[idx-1] == "." and len(text) != idx: + idx = idx -1 + + # if there is no numeric after the minus, make the minus part of the suffix + if idx == 1 and start == 1: + idx = 0 + + part1 = text[0:idx] + part2 = text[idx:len(text)] + + if part1 != "": + self.num = float( part1 ) + self.suffix = part2 + else: + self.suffix = text + + #print "num: {0} suf: {1}".format(self.num, self.suffix) + + 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 + if self.suffix == u"½": + if self.num is not None: + return self.num + .5 + else: + return .5 + return self.num + + def asInt( self ): + #return the int version of the float + if self.num is None: + return None + return int( self.num ) + + diff --git a/comicapi/utils.py b/comicapi/utils.py new file mode 100644 index 0000000..e315cd7 --- /dev/null +++ b/comicapi/utils.py @@ -0,0 +1,597 @@ +# coding=utf-8 + +""" +Some generic utilities +""" + + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +import sys +import os +import re +import platform +import locale +import codecs + + +class UtilsVars: + already_fixed_encoding = False + +def get_actual_preferred_encoding(): + preferred_encoding = locale.getpreferredencoding() + if platform.system() == "Darwin": + preferred_encoding = "utf-8" + return preferred_encoding + +def fix_output_encoding( ): + if not UtilsVars.already_fixed_encoding: + # this reads the environment and inits the right locale + locale.setlocale(locale.LC_ALL, "") + + # try to make stdout/stderr encodings happy for unicode printing + preferred_encoding = get_actual_preferred_encoding() + sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout) + sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr) + UtilsVars.already_fixed_encoding = True + +def get_recursive_filelist( pathlist ): + """ + Get a recursive list of of all files under all path items in the list + """ + filename_encoding = sys.getfilesystemencoding() + filelist = [] + for p in pathlist: + # if path is a folder, walk it recursivly, and all files underneath + if type(p) == str: + #make sure string is unicode + p = p.decode(filename_encoding) #, 'replace') + elif type(p) != unicode: + #it's probably a QString + p = unicode(p) + + if os.path.isdir( p ): + for root,dirs,files in os.walk( p ): + for f in files: + if type(f) == str: + #make sure string is unicode + f = f.decode(filename_encoding, 'replace') + elif type(f) != unicode: + #it's probably a QString + f = unicode(f) + filelist.append(os.path.join(root,f)) + else: + filelist.append(p) + + return filelist + +def listToString( l ): + string = "" + if l is not None: + for item in l: + if len(string) > 0: + string += ", " + string += item + return string + +def addtopath( dirname ): + if dirname is not None and dirname != "": + + # verify that path doesn't already contain the given dirname + tmpdirname = re.escape(dirname) + pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep) + + match = re.search(pattern, os.environ['PATH']) + if not match: + os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH'] + +# returns executable path, if it exists +def which(program): + + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + + return None + +def removearticles( text ): + text = text.lower() + articles = ['and', 'the', 'a', '&', 'issue' ] + newText = '' + for word in text.split(' '): + if word not in articles: + newText += word+' ' + + newText = newText[:-1] + + # now get rid of some other junk + newText = newText.replace(":", "") + newText = newText.replace(",", "") + newText = newText.replace("-", " ") + + # since the CV api changed, searches for series names with periods + # now explicity require the period to be in the search key, + # so the line below is removed (for now) + #newText = newText.replace(".", "") + + return newText + + +def unique_file(file_name): + counter = 1 + file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext') + while 1: + if not os.path.lexists( file_name): + return file_name + file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1] + counter += 1 + + +# -o- coding: utf-8 -o- +# ISO639 python dict +# oficial list in http://www.loc.gov/standards/iso639-2/php/code_list.php + +lang_dict = { + 'ab': 'Abkhaz', + 'aa': 'Afar', + 'af': 'Afrikaans', + 'ak': 'Akan', + 'sq': 'Albanian', + 'am': 'Amharic', + 'ar': 'Arabic', + 'an': 'Aragonese', + 'hy': 'Armenian', + 'as': 'Assamese', + 'av': 'Avaric', + 'ae': 'Avestan', + 'ay': 'Aymara', + 'az': 'Azerbaijani', + 'bm': 'Bambara', + 'ba': 'Bashkir', + 'eu': 'Basque', + 'be': 'Belarusian', + 'bn': 'Bengali', + 'bh': 'Bihari', + 'bi': 'Bislama', + 'bs': 'Bosnian', + 'br': 'Breton', + 'bg': 'Bulgarian', + 'my': 'Burmese', + 'ca': 'Catalan; Valencian', + 'ch': 'Chamorro', + 'ce': 'Chechen', + 'ny': 'Chichewa; Chewa; Nyanja', + 'zh': 'Chinese', + 'cv': 'Chuvash', + 'kw': 'Cornish', + 'co': 'Corsican', + 'cr': 'Cree', + 'hr': 'Croatian', + 'cs': 'Czech', + 'da': 'Danish', + 'dv': 'Divehi; Maldivian;', + 'nl': 'Dutch', + 'dz': 'Dzongkha', + 'en': 'English', + 'eo': 'Esperanto', + 'et': 'Estonian', + 'ee': 'Ewe', + 'fo': 'Faroese', + 'fj': 'Fijian', + 'fi': 'Finnish', + 'fr': 'French', + 'ff': 'Fula', + 'gl': 'Galician', + 'ka': 'Georgian', + 'de': 'German', + 'el': 'Greek, Modern', + 'gn': 'Guaraní', + 'gu': 'Gujarati', + 'ht': 'Haitian', + 'ha': 'Hausa', + 'he': 'Hebrew (modern)', + 'hz': 'Herero', + 'hi': 'Hindi', + 'ho': 'Hiri Motu', + 'hu': 'Hungarian', + 'ia': 'Interlingua', + 'id': 'Indonesian', + 'ie': 'Interlingue', + 'ga': 'Irish', + 'ig': 'Igbo', + 'ik': 'Inupiaq', + 'io': 'Ido', + 'is': 'Icelandic', + 'it': 'Italian', + 'iu': 'Inuktitut', + 'ja': 'Japanese', + 'jv': 'Javanese', + 'kl': 'Kalaallisut', + 'kn': 'Kannada', + 'kr': 'Kanuri', + 'ks': 'Kashmiri', + 'kk': 'Kazakh', + 'km': 'Khmer', + 'ki': 'Kikuyu, Gikuyu', + 'rw': 'Kinyarwanda', + 'ky': 'Kirghiz, Kyrgyz', + 'kv': 'Komi', + 'kg': 'Kongo', + 'ko': 'Korean', + 'ku': 'Kurdish', + 'kj': 'Kwanyama, Kuanyama', + 'la': 'Latin', + 'lb': 'Luxembourgish', + 'lg': 'Luganda', + 'li': 'Limburgish', + 'ln': 'Lingala', + 'lo': 'Lao', + 'lt': 'Lithuanian', + 'lu': 'Luba-Katanga', + 'lv': 'Latvian', + 'gv': 'Manx', + 'mk': 'Macedonian', + 'mg': 'Malagasy', + 'ms': 'Malay', + 'ml': 'Malayalam', + 'mt': 'Maltese', + 'mi': 'Māori', + 'mr': 'Marathi (Marāṭhī)', + 'mh': 'Marshallese', + 'mn': 'Mongolian', + 'na': 'Nauru', + 'nv': 'Navajo, Navaho', + 'nb': 'Norwegian Bokmål', + 'nd': 'North Ndebele', + 'ne': 'Nepali', + 'ng': 'Ndonga', + 'nn': 'Norwegian Nynorsk', + 'no': 'Norwegian', + 'ii': 'Nuosu', + 'nr': 'South Ndebele', + 'oc': 'Occitan', + 'oj': 'Ojibwe, Ojibwa', + 'cu': 'Old Church Slavonic', + 'om': 'Oromo', + 'or': 'Oriya', + 'os': 'Ossetian, Ossetic', + 'pa': 'Panjabi, Punjabi', + 'pi': 'Pāli', + 'fa': 'Persian', + 'pl': 'Polish', + 'ps': 'Pashto, Pushto', + 'pt': 'Portuguese', + 'qu': 'Quechua', + 'rm': 'Romansh', + 'rn': 'Kirundi', + 'ro': 'Romanian, Moldavan', + 'ru': 'Russian', + 'sa': 'Sanskrit (Saṁskṛta)', + 'sc': 'Sardinian', + 'sd': 'Sindhi', + 'se': 'Northern Sami', + 'sm': 'Samoan', + 'sg': 'Sango', + 'sr': 'Serbian', + 'gd': 'Scottish Gaelic', + 'sn': 'Shona', + 'si': 'Sinhala, Sinhalese', + 'sk': 'Slovak', + 'sl': 'Slovene', + 'so': 'Somali', + 'st': 'Southern Sotho', + 'es': 'Spanish; Castilian', + 'su': 'Sundanese', + 'sw': 'Swahili', + 'ss': 'Swati', + 'sv': 'Swedish', + 'ta': 'Tamil', + 'te': 'Telugu', + 'tg': 'Tajik', + 'th': 'Thai', + 'ti': 'Tigrinya', + 'bo': 'Tibetan', + 'tk': 'Turkmen', + 'tl': 'Tagalog', + 'tn': 'Tswana', + 'to': 'Tonga', + 'tr': 'Turkish', + 'ts': 'Tsonga', + 'tt': 'Tatar', + 'tw': 'Twi', + 'ty': 'Tahitian', + 'ug': 'Uighur, Uyghur', + 'uk': 'Ukrainian', + 'ur': 'Urdu', + 'uz': 'Uzbek', + 've': 'Venda', + 'vi': 'Vietnamese', + 'vo': 'Volapük', + 'wa': 'Walloon', + 'cy': 'Welsh', + 'wo': 'Wolof', + 'fy': 'Western Frisian', + 'xh': 'Xhosa', + 'yi': 'Yiddish', + 'yo': 'Yoruba', + 'za': 'Zhuang, Chuang', + 'zu': 'Zulu', +} + + +countries = [ + ('AF', 'Afghanistan'), + ('AL', 'Albania'), + ('DZ', 'Algeria'), + ('AS', 'American Samoa'), + ('AD', 'Andorra'), + ('AO', 'Angola'), + ('AI', 'Anguilla'), + ('AQ', 'Antarctica'), + ('AG', 'Antigua And Barbuda'), + ('AR', 'Argentina'), + ('AM', 'Armenia'), + ('AW', 'Aruba'), + ('AU', 'Australia'), + ('AT', 'Austria'), + ('AZ', 'Azerbaijan'), + ('BS', 'Bahamas'), + ('BH', 'Bahrain'), + ('BD', 'Bangladesh'), + ('BB', 'Barbados'), + ('BY', 'Belarus'), + ('BE', 'Belgium'), + ('BZ', 'Belize'), + ('BJ', 'Benin'), + ('BM', 'Bermuda'), + ('BT', 'Bhutan'), + ('BO', 'Bolivia'), + ('BA', 'Bosnia And Herzegowina'), + ('BW', 'Botswana'), + ('BV', 'Bouvet Island'), + ('BR', 'Brazil'), + ('BN', 'Brunei Darussalam'), + ('BG', 'Bulgaria'), + ('BF', 'Burkina Faso'), + ('BI', 'Burundi'), + ('KH', 'Cambodia'), + ('CM', 'Cameroon'), + ('CA', 'Canada'), + ('CV', 'Cape Verde'), + ('KY', 'Cayman Islands'), + ('CF', 'Central African Rep'), + ('TD', 'Chad'), + ('CL', 'Chile'), + ('CN', 'China'), + ('CX', 'Christmas Island'), + ('CC', 'Cocos Islands'), + ('CO', 'Colombia'), + ('KM', 'Comoros'), + ('CG', 'Congo'), + ('CK', 'Cook Islands'), + ('CR', 'Costa Rica'), + ('CI', 'Cote D`ivoire'), + ('HR', 'Croatia'), + ('CU', 'Cuba'), + ('CY', 'Cyprus'), + ('CZ', 'Czech Republic'), + ('DK', 'Denmark'), + ('DJ', 'Djibouti'), + ('DM', 'Dominica'), + ('DO', 'Dominican Republic'), + ('TP', 'East Timor'), + ('EC', 'Ecuador'), + ('EG', 'Egypt'), + ('SV', 'El Salvador'), + ('GQ', 'Equatorial Guinea'), + ('ER', 'Eritrea'), + ('EE', 'Estonia'), + ('ET', 'Ethiopia'), + ('FK', 'Falkland Islands (Malvinas)'), + ('FO', 'Faroe Islands'), + ('FJ', 'Fiji'), + ('FI', 'Finland'), + ('FR', 'France'), + ('GF', 'French Guiana'), + ('PF', 'French Polynesia'), + ('TF', 'French S. Territories'), + ('GA', 'Gabon'), + ('GM', 'Gambia'), + ('GE', 'Georgia'), + ('DE', 'Germany'), + ('GH', 'Ghana'), + ('GI', 'Gibraltar'), + ('GR', 'Greece'), + ('GL', 'Greenland'), + ('GD', 'Grenada'), + ('GP', 'Guadeloupe'), + ('GU', 'Guam'), + ('GT', 'Guatemala'), + ('GN', 'Guinea'), + ('GW', 'Guinea-bissau'), + ('GY', 'Guyana'), + ('HT', 'Haiti'), + ('HN', 'Honduras'), + ('HK', 'Hong Kong'), + ('HU', 'Hungary'), + ('IS', 'Iceland'), + ('IN', 'India'), + ('ID', 'Indonesia'), + ('IR', 'Iran'), + ('IQ', 'Iraq'), + ('IE', 'Ireland'), + ('IL', 'Israel'), + ('IT', 'Italy'), + ('JM', 'Jamaica'), + ('JP', 'Japan'), + ('JO', 'Jordan'), + ('KZ', 'Kazakhstan'), + ('KE', 'Kenya'), + ('KI', 'Kiribati'), + ('KP', 'Korea (North)'), + ('KR', 'Korea (South)'), + ('KW', 'Kuwait'), + ('KG', 'Kyrgyzstan'), + ('LA', 'Laos'), + ('LV', 'Latvia'), + ('LB', 'Lebanon'), + ('LS', 'Lesotho'), + ('LR', 'Liberia'), + ('LY', 'Libya'), + ('LI', 'Liechtenstein'), + ('LT', 'Lithuania'), + ('LU', 'Luxembourg'), + ('MO', 'Macau'), + ('MK', 'Macedonia'), + ('MG', 'Madagascar'), + ('MW', 'Malawi'), + ('MY', 'Malaysia'), + ('MV', 'Maldives'), + ('ML', 'Mali'), + ('MT', 'Malta'), + ('MH', 'Marshall Islands'), + ('MQ', 'Martinique'), + ('MR', 'Mauritania'), + ('MU', 'Mauritius'), + ('YT', 'Mayotte'), + ('MX', 'Mexico'), + ('FM', 'Micronesia'), + ('MD', 'Moldova'), + ('MC', 'Monaco'), + ('MN', 'Mongolia'), + ('MS', 'Montserrat'), + ('MA', 'Morocco'), + ('MZ', 'Mozambique'), + ('MM', 'Myanmar'), + ('NA', 'Namibia'), + ('NR', 'Nauru'), + ('NP', 'Nepal'), + ('NL', 'Netherlands'), + ('AN', 'Netherlands Antilles'), + ('NC', 'New Caledonia'), + ('NZ', 'New Zealand'), + ('NI', 'Nicaragua'), + ('NE', 'Niger'), + ('NG', 'Nigeria'), + ('NU', 'Niue'), + ('NF', 'Norfolk Island'), + ('MP', 'Northern Mariana Islands'), + ('NO', 'Norway'), + ('OM', 'Oman'), + ('PK', 'Pakistan'), + ('PW', 'Palau'), + ('PA', 'Panama'), + ('PG', 'Papua New Guinea'), + ('PY', 'Paraguay'), + ('PE', 'Peru'), + ('PH', 'Philippines'), + ('PN', 'Pitcairn'), + ('PL', 'Poland'), + ('PT', 'Portugal'), + ('PR', 'Puerto Rico'), + ('QA', 'Qatar'), + ('RE', 'Reunion'), + ('RO', 'Romania'), + ('RU', 'Russian Federation'), + ('RW', 'Rwanda'), + ('KN', 'Saint Kitts And Nevis'), + ('LC', 'Saint Lucia'), + ('VC', 'St Vincent/Grenadines'), + ('WS', 'Samoa'), + ('SM', 'San Marino'), + ('ST', 'Sao Tome'), + ('SA', 'Saudi Arabia'), + ('SN', 'Senegal'), + ('SC', 'Seychelles'), + ('SL', 'Sierra Leone'), + ('SG', 'Singapore'), + ('SK', 'Slovakia'), + ('SI', 'Slovenia'), + ('SB', 'Solomon Islands'), + ('SO', 'Somalia'), + ('ZA', 'South Africa'), + ('ES', 'Spain'), + ('LK', 'Sri Lanka'), + ('SH', 'St. Helena'), + ('PM', 'St.Pierre'), + ('SD', 'Sudan'), + ('SR', 'Suriname'), + ('SZ', 'Swaziland'), + ('SE', 'Sweden'), + ('CH', 'Switzerland'), + ('SY', 'Syrian Arab Republic'), + ('TW', 'Taiwan'), + ('TJ', 'Tajikistan'), + ('TZ', 'Tanzania'), + ('TH', 'Thailand'), + ('TG', 'Togo'), + ('TK', 'Tokelau'), + ('TO', 'Tonga'), + ('TT', 'Trinidad And Tobago'), + ('TN', 'Tunisia'), + ('TR', 'Turkey'), + ('TM', 'Turkmenistan'), + ('TV', 'Tuvalu'), + ('UG', 'Uganda'), + ('UA', 'Ukraine'), + ('AE', 'United Arab Emirates'), + ('UK', 'United Kingdom'), + ('US', 'United States'), + ('UY', 'Uruguay'), + ('UZ', 'Uzbekistan'), + ('VU', 'Vanuatu'), + ('VA', 'Vatican City State'), + ('VE', 'Venezuela'), + ('VN', 'Viet Nam'), + ('VG', 'Virgin Islands (British)'), + ('VI', 'Virgin Islands (U.S.)'), + ('EH', 'Western Sahara'), + ('YE', 'Yemen'), + ('YU', 'Yugoslavia'), + ('ZR', 'Zaire'), + ('ZM', 'Zambia'), + ('ZW', 'Zimbabwe') +] + + + +def getLanguageDict(): + return lang_dict + +def getLanguageFromISO( iso ): + if iso == None: + return None + else: + return lang_dict[ iso ] + + + + + + + + + + diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index a3db85a..791e7e5 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -1,6 +1,7 @@ """ A PyQT4 dialog to select from automated issue matches """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize """ Copyright 2012-2014 Anthony Beville @@ -54,8 +55,8 @@ class AutoTagMatchWindow(QtGui.QDialog): gridlayout.addWidget(self.archiveCoverWidget) gridlayout.setContentsMargins(0, 0, 0, 0) - utils.reduceWidgetFontSize(self.twList) - utils.reduceWidgetFontSize(self.teDescription, 1) + reduceWidgetFontSize(self.twList) + reduceWidgetFontSize(self.teDescription, 1) self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | diff --git a/comictaggerlib/autotagprogresswindow.py b/comictaggerlib/autotagprogresswindow.py index 6be3f3c..081e92d 100644 --- a/comictaggerlib/autotagprogresswindow.py +++ b/comictaggerlib/autotagprogresswindow.py @@ -1,6 +1,7 @@ """ A PyQT4 dialog to show ID log and progress """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize """ Copyright 2012-2014 Anthony Beville @@ -54,7 +55,7 @@ class AutoTagProgressWindow(QtGui.QDialog): QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint) - utils.reduceWidgetFontSize(self.textEdit) + reduceWidgetFontSize(self.textEdit) def setArchiveImage(self, img_data): self.setCoverImage(img_data, self.archiveCoverWidget) diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 112a14b..045682b 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -127,7 +127,7 @@ def display_match_set_for_choice(label, match_set, opts, settings): i = int(i) - 1 # save the data! # we know at this point, that the file is all good to go - ca = ComicArchive(match_set.filename, settings.rar_exe_path) + ca = ComicArchive(match_set.filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic('nocover.png')) md = create_local_metadata( opts, ca, ca.hasMetadata(opts.data_style)) cv_md = actual_issue_data_fetch( @@ -220,7 +220,7 @@ def process_file_cli(filename, opts, settings, match_results): batch_mode = len(opts.file_list) > 1 - ca = ComicArchive(filename, settings.rar_exe_path) + ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic('nocover.png')) if not os.path.lexists(filename): print >> sys.stderr, "Cannot find " + filename diff --git a/comictaggerlib/comet.py b/comictaggerlib/comet.py index 0bac1fc..b8ffe83 100644 --- a/comictaggerlib/comet.py +++ b/comictaggerlib/comet.py @@ -1,266 +1 @@ -""" -A python class to encapsulate CoMet data -""" - -""" -Copyright 2012-2014 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 = '\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) +from comicapi.comet import * diff --git a/comictaggerlib/comicarchive.py b/comictaggerlib/comicarchive.py index b98f0b6..28b69e7 100644 --- a/comictaggerlib/comicarchive.py +++ b/comictaggerlib/comicarchive.py @@ -1,1100 +1 @@ -""" -A python class to represent a single comic, be it file or folder of images -""" - -""" -Copyright 2012-2014 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 zipfile -import os -import struct -import sys -import tempfile -import subprocess -import platform -import locale -import time -import StringIO - -if platform.system() == "Windows": - import _subprocess - -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 * - -from settings import ComicTaggerSettings -from comicinfoxml import ComicInfoXml -from comicbookinfo import ComicBookInfo -from comet import CoMet -from genericmetadata import GenericMetadata, PageType -from filenameparser import FileNameParser - - -class MetaDataStyle: - - CBI = 0 - CIX = 1 - COMET = 2 - name = ['ComicBookLover', 'ComicRack', 'CoMet'] - - -class ZipArchiver: - - def __init__(self, path): - self.path = path - - def getArchiveComment(self): - zf = zipfile.ZipFile(self.path, 'r') - comment = zf.comment - zf.close() - return comment - - def setArchiveComment(self, comment): - return self.writeZipComment(self.path, comment) - - def readArchiveFile(self, archive_file): - data = "" - zf = zipfile.ZipFile(self.path, 'r') - - try: - data = zf.read(archive_file) - except zipfile.BadZipfile as e: - print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format( - e, self.path, archive_file) - zf.close() - raise IOError - except Exception as e: - zf.close() - print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format( - e, self.path, archive_file) - raise IOError - finally: - zf.close() - return data - - def removeArchiveFile(self, archive_file): - try: - self.rebuildZipFile([archive_file]) - except: - return False - else: - return True - - def writeArchiveFile(self, archive_file, data): - # At the moment, no other option but to rebuild the whole - # zip archive w/o the indicated file. Very sucky, but maybe - # another solution can be found - try: - self.rebuildZipFile([archive_file]) - - # now just add the archive file as a new one - zf = zipfile.ZipFile( - self.path, mode='a', compression=zipfile.ZIP_DEFLATED) - zf.writestr(archive_file, data) - zf.close() - return True - except: - return False - - def getArchiveFilenameList(self): - try: - zf = zipfile.ZipFile(self.path, 'r') - namelist = zf.namelist() - zf.close() - return namelist - except Exception as e: - print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format( - e, self.path) - return [] - - # zip helper func - def rebuildZipFile(self, exclude_list): - - # this recompresses the zip archive, - # without the files in the exclude_list - #print(">> sys.stderr, Rebuilding zip {0} without {1}".format(self.path, exclude_list)) - - # generate temp file - tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(self.path)) - os.close(tmp_fd) - - zin = zipfile.ZipFile(self.path, 'r') - zout = zipfile.ZipFile(tmp_name, 'w') - for item in zin.infolist(): - buffer = zin.read(item.filename) - if (item.filename not in exclude_list): - zout.writestr(item, buffer) - - # preserve the old comment - zout.comment = zin.comment - - zout.close() - zin.close() - - # replace with the new file - os.remove(self.path) - os.rename(tmp_name, self.path) - - def writeZipComment(self, filename, comment): - """ - This is a custom function for writing a comment to a zip file, - since the built-in one doesn't seem to work on Windows and Mac OS/X - - Fortunately, the zip comment is at the end of the file, and it's - easy to manipulate. See this website for more info: - see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure - """ - - # get file size - statinfo = os.stat(filename) - file_length = statinfo.st_size - - try: - fo = open(filename, "r+b") - - # the starting position, relative to EOF - pos = -4 - - found = False - value = bytearray() - - # walk backwards to find the "End of Central Directory" record - while (not found) and (-pos != file_length): - # seek, relative to EOF - fo.seek(pos, 2) - - value = fo.read(4) - - # look for the end of central directory signature - if bytearray(value) == bytearray([0x50, 0x4b, 0x05, 0x06]): - found = True - else: - # not found, step back another byte - pos = pos - 1 - # print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) - - if found: - - # now skip forward 20 bytes to the comment length word - pos += 20 - fo.seek(pos, 2) - - # Pack the length of the comment string - format = "H" # one 2-byte integer - # pack integer in a binary string - comment_length = struct.pack(format, len(comment)) - - # write out the length - fo.write(comment_length) - fo.seek(pos + 2, 2) - - # write out the comment itself - fo.write(comment) - fo.truncate() - fo.close() - else: - raise Exception('Failed to write comment to zip file!') - except: - 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) - if data is not None: - 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 Exception as e: - print >> sys.stderr, u"Error while copying to {0}: {1}".format( - self.path, e) - return False - else: - return True - - -#------------------------------------------ -# RAR implementation - -class RarArchiver: - - devnull = None - - def __init__(self, path, rar_exe_path): - self.path = path - self.rar_exe_path = rar_exe_path - - if RarArchiver.devnull is None: - RarArchiver.devnull = open(os.devnull, "w") - - # windows only, keeps the cmd.exe from popping up - if platform.system() == "Windows": - self.startupinfo = subprocess.STARTUPINFO() - self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW - else: - self.startupinfo = None - - def __del__(self): - # RarArchiver.devnull.close() - pass - - def getArchiveComment(self): - - rarc = self.getRARObj() - return rarc.comment - - def setArchiveComment(self, comment): - - if self.rar_exe_path is not None: - try: - # write comment to temp file - tmp_fd, tmp_name = tempfile.mkstemp() - f = os.fdopen(tmp_fd, 'w+b') - 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', - '-w' + working_dir, - '-c-', - '-z' + tmp_name, - self.path], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) - - if platform.system() == "Darwin": - time.sleep(1) - - os.remove(tmp_name) - except: - return False - else: - return True - else: - return False - - def readArchiveFile(self, archive_file): - - # Make sure to escape brackets, since some funky stuff is going on - # underneath with "fnmatch" - archive_file = archive_file.replace("[", '[[]') - entries = [] - - rarc = self.getRARObj() - - tries = 0 - while tries < 7: - try: - tries = tries + 1 - entries = rarc.read_files(archive_file) - - if entries[0][0].size != len(entries[0][1]): - print >> sys.stderr, u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( - entries[0][0].size, len( - entries[0][1]), self.path, archive_file, tries) - continue - - except (OSError, IOError) as e: - print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format( - str(e), self.path, archive_file, tries) - time.sleep(1) - except Exception as e: - print >> sys.stderr, u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format( - str(e), self.path, archive_file, tries) - break - - else: - # Success" - # entries is a list of of tuples: (rarinfo, filedata) - if tries > 1: - print >> sys.stderr, u"Attempted read_files() {0} times".format( - tries) - if (len(entries) == 1): - return entries[0][1] - else: - raise IOError - - raise IOError - - def writeArchiveFile(self, archive_file, data): - - if self.rar_exe_path is not None: - try: - 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', - '-w' + working_dir, - '-c-', - '-ep', - self.path, - tmp_file], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) - - if platform.system() == "Darwin": - time.sleep(1) - os.remove(tmp_file) - os.rmdir(tmp_folder) - except: - return False - else: - return True - else: - return False - - def removeArchiveFile(self, archive_file): - if self.rar_exe_path is not None: - try: - # use external program to remove file from Rar archive - subprocess.call([self.rar_exe_path, - 'd', - '-c-', - self.path, - archive_file], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) - - if platform.system() == "Darwin": - time.sleep(1) - except: - return False - else: - return True - else: - return False - - def getArchiveFilenameList(self): - - rarc = self.getRARObj() - #namelist = [ item.filename for item in rarc.infolist() ] - # return namelist - - tries = 0 - while tries < 7: - try: - tries = tries + 1 - #namelist = [ item.filename for item in rarc.infolist() ] - namelist = [] - for item in rarc.infolist(): - if item.size != 0: - namelist.append(item.filename) - - except (OSError, IOError) as e: - print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format( - str(e), self.path, tries) - time.sleep(1) - - else: - # Success" - return namelist - - raise e - - def getRARObj(self): - tries = 0 - while tries < 7: - try: - tries = tries + 1 - rarc = UnRAR2.RarFile(self.path) - - except (OSError, IOError) as e: - print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format( - str(e), self.path, tries) - time.sleep(1) - - else: - # Success" - return rarc - - raise e - -#------------------------------------------ -# Folder implementation - - -class FolderArchiver: - - def __init__(self, path): - self.path = path - self.comment_file_name = "ComicTaggerFolderComment.txt" - - def getArchiveComment(self): - return self.readArchiveFile(self.comment_file_name) - - def setArchiveComment(self, comment): - return self.writeArchiveFile(self.comment_file_name, comment) - - def readArchiveFile(self, archive_file): - - data = "" - fname = os.path.join(self.path, archive_file) - try: - with open(fname, 'rb') as f: - data = f.read() - f.close() - except IOError as e: - pass - - return data - - def writeArchiveFile(self, archive_file, data): - - fname = os.path.join(self.path, archive_file) - try: - with open(fname, 'w+') as f: - f.write(data) - f.close() - except: - return False - else: - return True - - def removeArchiveFile(self, archive_file): - - fname = os.path.join(self.path, archive_file) - try: - os.remove(fname) - except: - return False - else: - return True - - def getArchiveFilenameList(self): - return self.listFiles(self.path) - - def listFiles(self, folder): - - itemlist = list() - - for item in os.listdir(folder): - itemlist.append(item) - if os.path.isdir(item): - itemlist.extend(self.listFiles(os.path.join(folder, item))) - - return itemlist - -#------------------------------------------ -# Unknown implementation - - -class UnknownArchiver: - - def __init__(self, path): - self.path = path - - def getArchiveComment(self): - return "" - - def setArchiveComment(self, comment): - return False - - def readArchiveFile(self): - return "" - - def writeArchiveFile(self, archive_file, data): - return False - - def removeArchiveFile(self, archive_file): - return False - - def getArchiveFilenameList(self): - return [] - -#------------------------------------------------------------------ - - -class ComicArchive: - - logo_data = None - - class ArchiveType: - Zip, Rar, Folder, Unknown = range(4) - - def __init__(self, path, rar_exe_path=None): - self.path = path - - self.rar_exe_path = rar_exe_path - self.ci_xml_filename = 'ComicInfo.xml' - self.comet_default_filename = 'CoMet.xml' - self.resetCache() - - if self.rarTest(): - self.archive_type = self.ArchiveType.Rar - self.archiver = RarArchiver( - self.path, rar_exe_path=self.rar_exe_path) - - elif self.zipTest(): - self.archive_type = self.ArchiveType.Zip - self.archiver = ZipArchiver(self.path) - - elif os.path.isdir(self.path): - self.archive_type = self.ArchiveType.Folder - self.archiver = FolderArchiver(self.path) - else: - self.archive_type = self.ArchiveType.Unknown - self.archiver = UnknownArchiver(self.path) - - if ComicArchive.logo_data is None: - fname = ComicTaggerSettings.getGraphic('nocover.png') - with open(fname, 'rb') as fd: - ComicArchive.logo_data = fd.read() - - # Clears the cached data - def resetCache(self): - self.has_cix = None - self.has_cbi = None - self.has_comet = None - self.comet_filename = None - self.page_count = None - self.page_list = None - self.cix_md = None - self.cbi_md = None - self.comet_md = None - - def loadCache(self, style_list): - for style in style_list: - self.readMetadata(style) - - def rename(self, path): - self.path = path - self.archiver.path = path - - def zipTest(self): - return zipfile.is_zipfile(self.path) - - def rarTest(self): - try: - rarc = UnRAR2.RarFile(self.path) - except: # InvalidRARArchive: - return False - else: - return True - - def isZip(self): - return self.archive_type == self.ArchiveType.Zip - - def isRar(self): - return self.archive_type == self.ArchiveType.Rar - - def isFolder(self): - return self.archive_type == self.ArchiveType.Folder - - def isWritable(self, check_rar_status=True): - if self.archive_type == self.ArchiveType.Unknown: - return False - - elif check_rar_status and self.isRar() and self.rar_exe_path is None: - return False - - elif not os.access(self.path, os.W_OK): - return False - - elif ((self.archive_type != self.ArchiveType.Folder) and - (not os.access(os.path.dirname(os.path.abspath(self.path)), os.W_OK))): - return False - - return True - - def isWritableForStyle(self, data_style): - - if self.isRar() and data_style == MetaDataStyle.CBI: - return False - - return self.isWritable() - - def seemsToBeAComicArchive(self): - - # Do we even care about extensions?? - ext = os.path.splitext(self.path)[1].lower() - - if ( - (self.isZip() or self.isRar()) # or self.isFolder()) - and - (self.getNumberOfPages() > 0) - - ): - return True - else: - return False - - def readMetadata(self, style): - - if style == MetaDataStyle.CIX: - 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: - retcode = self.writeCIX(metadata) - elif style == MetaDataStyle.CBI: - retcode = self.writeCBI(metadata) - elif style == MetaDataStyle.COMET: - retcode = self.writeCoMet(metadata) - return retcode - - def hasMetadata(self, style): - - if style == MetaDataStyle.CIX: - 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: - retcode = self.removeCIX() - elif style == MetaDataStyle.CBI: - retcode = self.removeCBI() - elif style == MetaDataStyle.COMET: - retcode = self.removeCoMet() - return retcode - - def getPage(self, index): - - image_data = None - - filename = self.getPageName(index) - - if filename is not None: - try: - image_data = self.archiver.readArchiveFile(filename) - except IOError: - print >> sys.stderr, u"Error reading in page. Substituting logo page." - image_data = ComicArchive.logo_data - - return image_data - - def getPageName(self, index): - - if index is None: - return None - - page_list = self.getPageNameList() - - num_pages = len(page_list) - if num_pages == 0 or index >= num_pages: - return None - - return page_list[index] - - def getScannerPageIndex(self): - - scanner_page_index = None - - # make a guess at the scanner page - name_list = self.getPageNameList() - count = self.getNumberOfPages() - - # too few pages to really know - if count < 5: - return None - - # count the length of every filename, and count occurences - length_buckets = dict() - for name in name_list: - fname = os.path.split(name)[1] - length = len(fname) - if length in length_buckets: - length_buckets[length] += 1 - else: - length_buckets[length] = 1 - - # sort by most common - sorted_buckets = sorted( - length_buckets.iteritems(), - key=lambda k_v: ( - k_v[1], - k_v[0]), - reverse=True) - - # statistical mode occurence is first - mode_length = sorted_buckets[0][0] - - # we are only going to consider the final image file: - final_name = os.path.split(name_list[count - 1])[1] - - common_length_list = list() - for name in name_list: - if len(os.path.split(name)[1]) == mode_length: - common_length_list.append(os.path.split(name)[1]) - - prefix = os.path.commonprefix(common_length_list) - - if mode_length <= 7 and prefix == "": - # probably all numbers - if len(final_name) > mode_length: - scanner_page_index = count - 1 - - # see if the last page doesn't start with the same prefix as most - # others - elif not final_name.startswith(prefix): - scanner_page_index = count - 1 - - return scanner_page_index - - def getPageNameList(self, sort_list=True): - - 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: - def keyfunc(k): - # hack to account for some weird scanner ID pages - basename = os.path.split(k)[1] - if basename < '0': - k = os.path.join(os.path.split(k)[0], "z" + basename) - return k.lower() - - files.sort(key=keyfunc) - - # make a sub-list of image files - self.page_list = [] - for name in files: - if (name[-4:].lower() in [".jpg", - "jpeg", - ".png", - ".gif", - "webp"] and os.path.basename(name)[0] != "."): - self.page_list.append(name) - - return self.page_list - - def getNumberOfPages(self): - - if self.page_count is None: - self.page_count = len(self.getPageNameList()) - return self.page_count - - def readCBI(self): - if self.cbi_md is None: - raw_cbi = self.readRawCBI() - if raw_cbi is None: - self.cbi_md = GenericMetadata() - else: - self.cbi_md = ComicBookInfo().metadataFromString(raw_cbi) - - self.cbi_md.setDefaultPageList(self.getNumberOfPages()) - - return self.cbi_md - - def readRawCBI(self): - if (not self.hasCBI()): - return None - - 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): - if metadata is not None: - self.applyArchiveInfoToMetadata(metadata) - cbi_string = ComicBookInfo().stringFromMetadata(metadata) - write_success = self.archiver.setArchiveComment(cbi_string) - if write_success: - self.has_cbi = True - self.cbi_md = metadata - self.resetCache() - return write_success - else: - return False - - def removeCBI(self): - if self.hasCBI(): - write_success = self.archiver.setArchiveComment("") - if write_success: - self.has_cbi = False - self.cbi_md = None - self.resetCache() - return write_success - return True - - def readCIX(self): - if self.cix_md is None: - raw_cix = self.readRawCIX() - if raw_cix is None or raw_cix == "": - self.cix_md = GenericMetadata() - else: - self.cix_md = ComicInfoXml().metadataFromString(raw_cix) - - # validate the existing page list (make sure count is correct) - if len(self.cix_md.pages) != 0: - if len(self.cix_md.pages) != self.getNumberOfPages(): - # pages array doesn't match the actual number of images we're seeing - # in the archive, so discard the data - self.cix_md.pages = [] - - if len(self.cix_md.pages) == 0: - self.cix_md.setDefaultPageList(self.getNumberOfPages()) - - return self.cix_md - - def readRawCIX(self): - if not self.hasCIX(): - return None - try: - raw_cix = self.archiver.readArchiveFile(self.ci_xml_filename) - except IOError: - print("Error reading in raw CIX!") - raw_cix = "" - return raw_cix - - def writeCIX(self, metadata): - - if metadata is not None: - self.applyArchiveInfoToMetadata(metadata, calc_page_sizes=True) - cix_string = ComicInfoXml().stringFromMetadata(metadata) - write_success = self.archiver.writeArchiveFile( - self.ci_xml_filename, cix_string) - if write_success: - self.has_cix = True - self.cix_md = metadata - self.resetCache() - return write_success - else: - return False - - def removeCIX(self): - if self.hasCIX(): - write_success = self.archiver.removeArchiveFile( - self.ci_xml_filename) - if write_success: - self.has_cix = False - self.cix_md = None - self.resetCache() - return write_success - return True - - def hasCIX(self): - 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): - if self.comet_md is None: - raw_comet = self.readRawCoMet() - if raw_comet is None or raw_comet == "": - self.comet_md = GenericMetadata() - else: - self.comet_md = CoMet().metadataFromString(raw_comet) - - self.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 self.comet_md.coverImage is not None: - cover_idx = 0 - for idx, f in enumerate(self.getPageNameList()): - if self.comet_md.coverImage == f: - cover_idx = idx - break - if cover_idx != 0: - del (self.comet_md.pages[0]['Type']) - self.comet_md.pages[cover_idx][ - 'Type'] = PageType.FrontCover - - return self.comet_md - - def readRawCoMet(self): - if not self.hasCoMet(): - print >> sys.stderr, self.path, "doesn't have CoMet data!" - return None - - try: - raw_comet = self.archiver.readArchiveFile(self.comet_filename) - except IOError: - print >> sys.stderr, u"Error reading in raw CoMet!" - raw_comet = "" - return raw_comet - - 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) - write_success = self.archiver.writeArchiveFile( - self.comet_filename, comet_string) - if write_success: - self.has_comet = True - self.comet_md = metadata - self.resetCache() - return write_success - else: - return False - - def removeCoMet(self): - if self.hasCoMet(): - write_success = self.archiver.removeArchiveFile( - self.comet_filename) - if write_success: - self.has_comet = False - self.comet_md = None - self.resetCache() - return write_success - return True - - def hasCoMet(self): - if self.has_comet is None: - self.has_comet = False - if not self.seemsToBeAComicArchive(): - return self.has_comet - - # 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 - try: - data = self.archiver.readArchiveFile(n) - except: - data = "" - print >> sys.stderr, u"Error reading in Comet XML for validation!" - if CoMet().validateString(data): - # since we found it, save it! - self.comet_filename = n - self.has_comet = True - break - - return self.has_comet - - 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) - if data is not None: - try: - im = Image.open(StringIO.StringIO(data)) - w, h = im.size - - p['ImageSize'] = str(len(data)) - p['ImageHeight'] = str(h) - p['ImageWidth'] = str(w) - except IOError: - p['ImageSize'] = str(len(data)) - - else: - if 'ImageSize' not in p: - data = self.getPage(idx) - p['ImageSize'] = str(len(data)) - - def metadataFromFilename(self, parse_scan_info=True): - - metadata = GenericMetadata() - - fnp = FileNameParser() - fnp.parseFilename(self.path) - - if fnp.issue != "": - metadata.issue = fnp.issue - if fnp.series != "": - metadata.series = fnp.series - if fnp.volume != "": - metadata.volume = fnp.volume - if fnp.year != "": - metadata.year = fnp.year - if fnp.issue_count != "": - metadata.issueCount = fnp.issue_count - if parse_scan_info: - if fnp.remainder != "": - metadata.scanInfo = fnp.remainder - - 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) +from comicapi.comicarchive import * diff --git a/comictaggerlib/comicbookinfo.py b/comictaggerlib/comicbookinfo.py index 977695d..6b839fa 100644 --- a/comictaggerlib/comicbookinfo.py +++ b/comictaggerlib/comicbookinfo.py @@ -1,149 +1 @@ -""" -A python class to encapsulate the ComicBookInfo data -""" - -""" -Copyright 2012-2014 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 json -from datetime import datetime -import zipfile - -from genericmetadata import GenericMetadata -import utils -import ctversion - - -class ComicBookInfo: - - def metadataFromString(self, string): - - cbi_container = json.loads(unicode(string, 'utf-8')) - - metadata = GenericMetadata() - - cbi = cbi_container['ComicBookInfo/1.0'] - - # helper func - # If item is not in CBI, return None - def xlate(cbi_entry): - if cbi_entry in cbi: - return cbi[cbi_entry] - else: - return None - - metadata.series = xlate('series') - metadata.title = xlate('title') - metadata.issue = xlate('issue') - metadata.publisher = xlate('publisher') - metadata.month = xlate('publicationMonth') - metadata.year = xlate('publicationYear') - metadata.issueCount = xlate('numberOfIssues') - metadata.comments = xlate('comments') - metadata.credits = xlate('credits') - metadata.genre = xlate('genre') - metadata.volume = xlate('volume') - metadata.volumeCount = xlate('numberOfVolumes') - metadata.language = xlate('language') - metadata.country = xlate('country') - 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 - pattern = metadata.language - metadata.language = None - for key in utils.getLanguageDict(): - if utils.getLanguageDict()[key] == pattern.encode('utf-8'): - metadata.language = key - break - - metadata.isEmpty = False - - return metadata - - def stringFromMetadata(self, metadata): - - cbi_container = self.createJSONDictionary(metadata) - return json.dumps(cbi_container) - - # verify that the string actually contains CBI data in JSON format - def validateString(self, string): - - try: - cbi_container = json.loads(string) - except: - return False - - return ('ComicBookInfo/1.0' in cbi_container) - - def createJSONDictionary(self, metadata): - - # Create the dictionary that we will convert to JSON text - cbi = dict() - cbi_container = {'appID': 'ComicTagger/' + ctversion.version, - 'lastModified': str(datetime.now()), - 'ComicBookInfo/1.0': cbi} - - # helper func - def assign(cbi_entry, md_entry): - if md_entry is not None: - cbi[cbi_entry] = md_entry - - # helper func - def toInt(s): - i = None - if type(s) in [str, unicode, 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', toInt(metadata.month)) - assign('publicationYear', toInt(metadata.year)) - assign('numberOfIssues', toInt(metadata.issueCount)) - assign('comments', metadata.comments) - assign('genre', metadata.genre) - assign('volume', toInt(metadata.volume)) - assign('numberOfVolumes', toInt(metadata.volumeCount)) - assign('language', utils.getLanguageFromISO(metadata.language)) - assign('country', metadata.country) - assign('rating', metadata.criticalRating) - assign('credits', metadata.credits) - assign('tags', metadata.tags) - - return cbi_container - - def writeToExternalFile(self, filename, metadata): - - cbi_container = self.createJSONDictionary(metadata) - - f = open(filename, 'w') - f.write(json.dumps(cbi_container, indent=4)) - f.close +from comicapi.comicbookinfo import * \ No newline at end of file diff --git a/comictaggerlib/comicinfoxml.py b/comictaggerlib/comicinfoxml.py index e429ce3..6943c9c 100644 --- a/comictaggerlib/comicinfoxml.py +++ b/comictaggerlib/comicinfoxml.py @@ -1,294 +1 @@ -""" -A python class to encapsulate ComicRack's ComicInfo.xml data -""" - -""" -Copyright 2012-2014 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 ComicInfoXml: - - 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 getParseableCredits(self): - parsable_credits = [] - parsable_credits.extend(self.writer_synonyms) - parsable_credits.extend(self.penciller_synonyms) - parsable_credits.extend(self.inker_synonyms) - parsable_credits.extend(self.colorist_synonyms) - parsable_credits.extend(self.letterer_synonyms) - parsable_credits.extend(self.cover_synonyms) - parsable_credits.extend(self.editor_synonyms) - return parsable_credits - - def metadataFromString(self, string): - - tree = ET.ElementTree(ET.fromstring(string)) - return self.convertXMLToMetadata(tree) - - def stringFromMetadata(self, metadata): - - header = '\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("ComicInfo") - root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" - root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema" - # helper func - - def assign(cix_entry, md_entry): - if md_entry is not None: - ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry) - - assign('Title', md.title) - assign('Series', md.series) - assign('Number', md.issue) - assign('Count', md.issueCount) - assign('Volume', md.volume) - assign('AlternateSeries', md.alternateSeries) - assign('AlternateNumber', md.alternateNumber) - assign('StoryArc', md.storyArc) - assign('SeriesGroup', md.seriesGroup) - assign('AlternateCount', md.alternateCount) - assign('Summary', md.comments) - assign('Notes', md.notes) - assign('Year', md.year) - assign('Month', md.month) - assign('Day', md.day) - - # 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() - - # first, loop thru credits, and build a list for each role that CIX - # supports - for credit in metadata.credits: - - if credit['role'].lower() in set(self.writer_synonyms): - credit_writer_list.append(credit['person'].replace(",", "")) - - if credit['role'].lower() in set(self.penciller_synonyms): - credit_penciller_list.append(credit['person'].replace(",", "")) - - if credit['role'].lower() in set(self.inker_synonyms): - credit_inker_list.append(credit['person'].replace(",", "")) - - if credit['role'].lower() in set(self.colorist_synonyms): - credit_colorist_list.append(credit['person'].replace(",", "")) - - if credit['role'].lower() in set(self.letterer_synonyms): - credit_letterer_list.append(credit['person'].replace(",", "")) - - if credit['role'].lower() in set(self.cover_synonyms): - credit_cover_list.append(credit['person'].replace(",", "")) - - if credit['role'].lower() in set(self.editor_synonyms): - credit_editor_list.append(credit['person'].replace(",", "")) - - # second, convert each list to string, and add to XML struct - if len(credit_writer_list) > 0: - node = ET.SubElement(root, 'Writer') - node.text = utils.listToString(credit_writer_list) - - if len(credit_penciller_list) > 0: - node = ET.SubElement(root, 'Penciller') - node.text = utils.listToString(credit_penciller_list) - - if len(credit_inker_list) > 0: - node = ET.SubElement(root, 'Inker') - node.text = utils.listToString(credit_inker_list) - - if len(credit_colorist_list) > 0: - node = ET.SubElement(root, 'Colorist') - node.text = utils.listToString(credit_colorist_list) - - if len(credit_letterer_list) > 0: - node = ET.SubElement(root, 'Letterer') - node.text = utils.listToString(credit_letterer_list) - - if len(credit_cover_list) > 0: - node = ET.SubElement(root, 'CoverArtist') - node.text = utils.listToString(credit_cover_list) - - if len(credit_editor_list) > 0: - node = ET.SubElement(root, 'Editor') - node.text = utils.listToString(credit_editor_list) - - assign('Publisher', md.publisher) - assign('Imprint', md.imprint) - assign('Genre', md.genre) - assign('Web', md.webLink) - assign('PageCount', md.pageCount) - assign('LanguageISO', md.language) - assign('Format', md.format) - assign('AgeRating', md.maturityRating) - if md.blackAndWhite is not None and md.blackAndWhite: - ET.SubElement(root, 'BlackAndWhite').text = "Yes" - assign('Manga', md.manga) - assign('Characters', md.characters) - assign('Teams', md.teams) - assign('Locations', md.locations) - assign('ScanInformation', md.scanInfo) - - # loop and add the page entries under pages node - if len(md.pages) > 0: - pages_node = ET.SubElement(root, 'Pages') - for page_dict in md.pages: - page_node = ET.SubElement(pages_node, 'Page') - page_node.attrib = page_dict - - # self pretty-print - self.indent(root) - - # wrap it in an ElementTree instance, and save as XML - tree = ET.ElementTree(root) - return tree - - def convertXMLToMetadata(self, tree): - - root = tree.getroot() - - if root.tag != 'ComicInfo': - raise 1 - return None - - metadata = GenericMetadata() - md = metadata - - # Helper function - def xlate(tag): - node = root.find(tag) - if node is not None: - return node.text - else: - return None - - md.series = xlate('Series') - md.title = xlate('Title') - md.issue = xlate('Number') - md.issueCount = xlate('Count') - md.volume = xlate('Volume') - md.alternateSeries = xlate('AlternateSeries') - md.alternateNumber = xlate('AlternateNumber') - md.alternateCount = xlate('AlternateCount') - md.comments = xlate('Summary') - md.notes = xlate('Notes') - md.year = xlate('Year') - md.month = xlate('Month') - md.day = xlate('Day') - md.publisher = xlate('Publisher') - md.imprint = xlate('Imprint') - md.genre = xlate('Genre') - md.webLink = xlate('Web') - md.language = xlate('LanguageISO') - md.format = xlate('Format') - md.manga = xlate('Manga') - md.characters = xlate('Characters') - md.teams = xlate('Teams') - md.locations = xlate('Locations') - md.pageCount = xlate('PageCount') - md.scanInfo = xlate('ScanInformation') - md.storyArc = xlate('StoryArc') - md.seriesGroup = xlate('SeriesGroup') - md.maturityRating = xlate('AgeRating') - - tmp = xlate('BlackAndWhite') - md.blackAndWhite = False - if tmp is not None and tmp.lower() in ["yes", "true", "1"]: - md.blackAndWhite = True - # Now extract the credit info - for n in root: - if (n.tag == 'Writer' or - n.tag == 'Penciller' or - n.tag == 'Inker' or - n.tag == 'Colorist' or - n.tag == 'Letterer' or - n.tag == 'Editor' - ): - if n.text is not None: - for name in n.text.split(','): - metadata.addCredit(name.strip(), n.tag) - - if n.tag == 'CoverArtist': - if n.text is not None: - for name in n.text.split(','): - metadata.addCredit(name.strip(), "Cover") - - # parse page data now - pages_node = root.find("Pages") - if pages_node is not None: - for page in pages_node: - metadata.pages.append(page.attrib) - # print(page.attrib) - - metadata.isEmpty = False - - return metadata - - def writeToExternalFile(self, filename, metadata): - - tree = self.convertMetadataToXML(self, metadata) - # ET.dump(tree) - tree.write(filename, encoding='utf-8') - - def readFromExternalFile(self, filename): - - tree = ET.parse(filename) - return self.convertXMLToMetadata(tree) +from comicapi.comicinfoxml import * \ No newline at end of file diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py index 5285086..1d8abbf 100644 --- a/comictaggerlib/coverimagewidget.py +++ b/comictaggerlib/coverimagewidget.py @@ -3,6 +3,7 @@ A PyQt4 widget display cover images from either local archive, or from ComicVine (TODO: This should be re-factored using subclasses!) """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize, getQImageFromData """ Copyright 2012-2014 Anthony Beville @@ -69,7 +70,7 @@ class CoverImageWidget(QWidget): uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui'), self) - utils.reduceWidgetFontSize(self.label) + reduceWidgetFontSize(self.label) self.mode = mode self.comicVine = ComicVineTalker() @@ -248,7 +249,7 @@ class CoverImageWidget(QWidget): # called when the image is done loading from internet def coverRemoteFetchComplete(self, image_data, issue_id): - img = utils.getQImageFromData(image_data) + img = getQImageFromData(image_data) self.current_pixmap = QPixmap(img) self.setDisplayPixmap(0, 0) #print("ATB cover fetch complete!") diff --git a/comictaggerlib/filenameparser.py b/comictaggerlib/filenameparser.py index 3c04aa4..77a1c5b 100644 --- a/comictaggerlib/filenameparser.py +++ b/comictaggerlib/filenameparser.py @@ -1,292 +1 @@ -""" -Functions for parsing comic info from filename - -This should probably be re-written, but, well, it mostly works! - -""" - -""" -Copyright 2012-2014 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. -""" - -# Some portions of this code were modified from pyComicMetaThis project -# http://code.google.com/p/pycomicmetathis/ - -import re -import os -from urllib import unquote - - -class FileNameParser: - - def repl(self, m): - return ' ' * len(m.group()) - - def fixSpaces(self, string, remove_dashes=True): - if remove_dashes: - placeholders = ['[-_]', ' +'] - else: - placeholders = ['[_]', ' +'] - for ph in placeholders: - string = re.sub(ph, self.repl, string) - return string # .strip() - - def getIssueCount(self, filename, issue_end): - - count = "" - filename = filename[issue_end:] - - # replace any name seperators with spaces - tmpstr = self.fixSpaces(filename) - found = False - - match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE) - if match: - count = match.group() - found = True - - if not found: - match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE) - if match: - count = match.group() - found = True - - count = count.lstrip("0") - - return count - - def getIssueNumber(self, filename): - - # Returns a tuple of issue number string, and start and end indexs in the filename - # (The indexes will be used to split the string up for further parsing) - - found = False - issue = '' - start = 0 - end = 0 - - # first, look for multiple "--", this means 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 followed by issue - filename = re.sub("--.*", self.repl, filename) - - elif "__" in filename: - # the pattern seems to be that anything to left of the first "__" - # is the series name followed by issue - filename = re.sub("__.*", self.repl, filename) - - filename = filename.replace("+", " ") - - # replace parenthetical phrases with spaces - filename = re.sub("\(.*?\)", self.repl, filename) - filename = re.sub("\[.*?\]", self.repl, filename) - - # replace any name seperators with spaces - filename = self.fixSpaces(filename) - - # remove any "of NN" phrase with spaces (problem: this could break on - # some titles) - filename = re.sub("of [\d]+", self.repl, filename) - - # print(u"[{0}]".format(filename)) - - # we should now have a cleaned up filename version with all the words in - # the same positions as original filename - - # make a list of each word and its position - word_list = list() - for m in re.finditer("\S+", filename): - word_list.append((m.group(0), m.start(), m.end())) - - # remove the first word, since it can't be the issue number - if len(word_list) > 1: - word_list = word_list[1:] - else: - # only one word?? just bail. - return issue, start, end - - # Now try to search for the likely issue number word in the list - - # first look for a word with "#" followed by digits with optional sufix - # this is almost certainly the issue number - for w in reversed(word_list): - if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): - found = True - break - - # same as above but w/o a '#', and only look at the last word in the - # list - if not found: - w = word_list[-1] - if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): - found = True - - # now try to look for a # followed by any characters - if not found: - for w in reversed(word_list): - if re.match("#\S+", w[0]): - found = True - break - - if found: - issue = w[0] - start = w[1] - end = w[2] - if issue[0] == '#': - issue = issue[1:] - - return issue, start, end - - def getSeriesName(self, filename, issue_start): - - # use the issue number string index to split the filename string - - if issue_start != 0: - filename = filename[:issue_start] - - # in case there is no issue number, remove some obvious stuff - if "--" in filename: - # the pattern seems to be that anything to left of the first "--" - # is the series name followed by issue - filename = re.sub("--.*", self.repl, filename) - - elif "__" in filename: - # the pattern seems to be that anything to left of the first "__" - # is the series name followed by issue - filename = re.sub("__.*", self.repl, filename) - - filename = filename.replace("+", " ") - tmpstr = self.fixSpaces(filename, remove_dashes=False) - - series = tmpstr - volume = "" - - # save the last word - try: - last_word = series.split()[-1] - except: - last_word = "" - - # remove any parenthetical phrases - series = re.sub("\(.*?\)", "", series) - - # search for volume number - match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series) - if match: - series = match.group(1) - volume = match.group(3) - - # if a volume wasn't found, see if the last word is a year in parentheses - # since that's a common way to designate the volume - if volume == "": - # match either (YEAR), (YEAR-), or (YEAR-YEAR2) - match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word) - if match: - volume = match.group(2) - - series = series.strip() - - # if we don't have an issue number (issue_start==0), look - # for hints i.e. "TPB", "one-shot", "OS", "OGN", etc that might - # be removed to help search online - if issue_start == 0: - one_shot_words = ["tpb", "os", "one-shot", "ogn", "gn"] - try: - last_word = series.split()[-1] - if last_word.lower() in one_shot_words: - series = series.rsplit(' ', 1)[0] - except: - pass - - return series, volume.strip() - - def getYear(self, filename, issue_end): - - filename = filename[issue_end:] - - year = "" - # look for four digit number with "(" ")" or "--" around it - match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename) - if match: - year = match.group() - # remove non-numerics - year = re.sub("[^0-9]", "", year) - return year - - def getRemainder(self, filename, year, count, volume, issue_end): - - # make a guess at where the the non-interesting stuff begins - remainder = "" - - if "--" in filename: - remainder = filename.split("--", 1)[1] - elif "__" in filename: - remainder = filename.split("__", 1)[1] - elif issue_end != 0: - remainder = filename[issue_end:] - - remainder = self.fixSpaces(remainder, remove_dashes=False) - if volume != "": - remainder = remainder.replace("Vol." + volume, "", 1) - if year != "": - remainder = remainder.replace(year, "", 1) - if count != "": - remainder = remainder.replace("of " + count, "", 1) - - remainder = remainder.replace("()", "") - # cleans some whitespace mess - remainder = remainder.replace(" ", " ") - - return remainder.strip() - - def parseFilename(self, filename): - - # remove the path - filename = os.path.basename(filename) - - # remove the extension - filename = os.path.splitext(filename)[0] - - # 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", ")") - - self.issue, issue_start, issue_end = self.getIssueNumber(filename) - self.series, self.volume = self.getSeriesName(filename, issue_start) - - # provides proper value when the filename doesn't have a issue number - if issue_end == 0: - issue_end = len(self.series) - - self.year = self.getYear(filename, issue_end) - self.issue_count = self.getIssueCount(filename, issue_end) - self.remainder = self.getRemainder( - filename, self.year, self.issue_count, self.volume, issue_end) - - if self.issue != "": - # strip off leading zeros - self.issue = self.issue.lstrip("0") - if self.issue == "": - self.issue = "0" - if self.issue[0] == ".": - self.issue = "0" + self.issue +from comicapi.filenameparser import * \ No newline at end of file diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index 573d67f..60986f1 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -2,6 +2,7 @@ """ A PyQt4 widget for managing list of comic archive files """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent """ Copyright 2012-2014 Anthony Beville @@ -70,7 +71,7 @@ class FileSelectionList(QWidget): self.settings = settings - utils.reduceWidgetFontSize(self.twList) + reduceWidgetFontSize(self.twList) self.twList.setColumnCount(6) #self.twlist.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""]) @@ -203,7 +204,7 @@ class FileSelectionList(QWidget): break progdialog.setValue(idx) progdialog.setLabelText(f) - utils.centerWindowOnParent(progdialog) + centerWindowOnParent(progdialog) QCoreApplication.processEvents() row = self.addPathItem(f) if firstAdded is None and row is not None: @@ -281,7 +282,7 @@ class FileSelectionList(QWidget): if self.isListDupe(path): return self.getCurrentListRow(path) - ca = ComicArchive(path, self.settings.rar_exe_path) + ca = ComicArchive(path, self.settings.rar_exe_path, ComicTaggerSettings.getGraphic('nocover.png')) if ca.seemsToBeAComicArchive(): row = self.twList.rowCount() diff --git a/comictaggerlib/genericmetadata.py b/comictaggerlib/genericmetadata.py index 81b03ba..8bcf694 100644 --- a/comictaggerlib/genericmetadata.py +++ b/comictaggerlib/genericmetadata.py @@ -1,318 +1 @@ -""" - A python class for internal metadata storage - - The goal of this class is to handle ALL the data that might come from various - tagging schemes and databases, such as ComicVine or GCD. This makes conversion - possible, however lossy it might be - -""" - -""" -Copyright 2012-2014 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 - - -# These page info classes are exactly the same as the CIX scheme, since -# it's unique -class PageType: - FrontCover = "FrontCover" - InnerCover = "InnerCover" - Roundup = "Roundup" - Story = "Story" - Advertisement = "Advertisement" - Editorial = "Editorial" - Letters = "Letters" - Preview = "Preview" - BackCover = "BackCover" - Other = "Other" - Deleted = "Deleted" - -""" -class PageInfo: - Image = 0 - Type = PageType.Story - DoublePage = False - ImageSize = 0 - Key = "" - ImageWidth = 0 - ImageHeight = 0 -""" - - -class GenericMetadata: - - def __init__(self): - - self.isEmpty = True - self.tagOrigin = None - - self.series = None - self.issue = None - self.title = None - self.publisher = None - self.month = None - self.year = None - self.day = None - self.issueCount = None - self.volume = None - self.genre = None - self.language = None # 2 letter iso code - self.comments = None # Use same way as Summary in CIX - - self.volumeCount = None - self.criticalRating = None - self.country = None - - self.alternateSeries = None - self.alternateNumber = None - self.alternateCount = None - self.imprint = None - self.notes = None - self.webLink = None - self.format = None - self.manga = None - self.blackAndWhite = None - self.pageCount = None - self.maturityRating = None - - self.storyArc = None - self.seriesGroup = None - self.scanInfo = None - - self.characters = None - self.teams = None - self.locations = None - - self.credits = list() - self.tags = list() - self.pages = list() - - # 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 - # values, over-write them to this one - - def assign(cur, new): - if new is not None: - if isinstance(new, str) and len(new) == 0: - setattr(self, cur, None) - else: - setattr(self, cur, new) - - if not new_md.isEmpty: - self.isEmpty = False - - assign('series', new_md.series) - assign("issue", new_md.issue) - assign("issueCount", new_md.issueCount) - assign("title", new_md.title) - assign("publisher", new_md.publisher) - assign("day", new_md.day) - assign("month", new_md.month) - assign("year", new_md.year) - assign("volume", new_md.volume) - assign("volumeCount", new_md.volumeCount) - assign("genre", new_md.genre) - assign("language", new_md.language) - assign("country", new_md.country) - 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("format", new_md.format) - assign("manga", new_md.manga) - assign("blackAndWhite", new_md.blackAndWhite) - assign("maturityRating", new_md.maturityRating) - 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 - # not sure if the tags and pages should broken down, or treated - # as whole lists.... - - # 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) - - if len(new_md.pages) > 0: - assign("pages", new_md.pages) - - def overlayCredits(self, new_credits): - for c in new_credits: - if 'primary' in c and c['primary']: - primary = True - else: - primary = False - - # Remove credit role if person is blank - if c['person'] == "": - for r in reversed(self.credits): - if r['role'].lower() == c['role'].lower(): - self.credits.remove(r) - # 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() - credit['person'] = person - credit['role'] = role - if primary: - credit['primary'] = primary - - # look to see if it's not already there... - found = False - for c in self.credits: - if (c['person'].lower() == person.lower() and - c['role'].lower() == role.lower()): - # no need to add it. just adjust the "primary" flag as needed - c['primary'] = primary - found = True - break - - if not found: - self.credits.append(credit) - - def __str__(self): - vals = [] - if self.isEmpty: - return "No metadata" - - def add_string(tag, val): - if val is not None and u"{0}".format(val) != "": - vals.append((tag, val)) - - def add_attr_string(tag): - val = getattr(self, tag) - add_string(tag, getattr(self, tag)) - - add_attr_string("series") - add_attr_string("issue") - add_attr_string("issueCount") - add_attr_string("title") - add_attr_string("publisher") - add_attr_string("year") - add_attr_string("month") - add_attr_string("day") - add_attr_string("volume") - add_attr_string("volumeCount") - add_attr_string("genre") - add_attr_string("language") - add_attr_string("country") - add_attr_string("criticalRating") - add_attr_string("alternateSeries") - add_attr_string("alternateNumber") - add_attr_string("alternateCount") - add_attr_string("imprint") - 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") - add_attr_string("storyArc") - add_attr_string("seriesGroup") - add_attr_string("scanInfo") - add_attr_string("characters") - add_attr_string("teams") - add_attr_string("locations") - add_attr_string("comments") - add_attr_string("notes") - - add_string("tags", utils.listToString(self.tags)) - - for c in self.credits: - primary = "" - if 'primary' in c and c['primary']: - primary = " [P]" - add_string("credit", c['role'] + ": " + c['person'] + primary) - - # find the longest field name - flen = 0 - for i in vals: - flen = max(flen, len(i[0])) - flen += 1 - - # format the data nicely - outstr = "" - fmt_str = u"{0: <" + str(flen) + "} {1}\n" - for i in vals: - outstr += fmt_str.format(i[0] + ":", i[1]) - - return outstr +from comicapi.genericmetadata import * \ No newline at end of file diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index 5f3daee..9cd3cb7 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -1,6 +1,7 @@ """ A PyQT4 dialog to select specific issue from list """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize """ Copyright 2012-2014 Anthony Beville @@ -59,8 +60,8 @@ class IssueSelectionWindow(QtGui.QDialog): gridlayout.addWidget(self.coverWidget) gridlayout.setContentsMargins(0, 0, 0, 0) - utils.reduceWidgetFontSize(self.twList) - utils.reduceWidgetFontSize(self.teDescription, 1) + reduceWidgetFontSize(self.twList) + reduceWidgetFontSize(self.teDescription, 1) self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | diff --git a/comictaggerlib/issuestring.py b/comictaggerlib/issuestring.py index 2d13290..d9872b2 100644 --- a/comictaggerlib/issuestring.py +++ b/comictaggerlib/issuestring.py @@ -1,136 +1 @@ -""" -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" -"100-2" - -""" - -""" -Copyright 2012-2014 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): - - # break up the issue number string into 2 parts: the numeric and suffix string. - # (assumes that the numeric portion is always first) - - self.num = None - self.suffix = "" - - if text is None: - return - - if isinstance(text, int): - text = str(text) - - if len(text) == 0: - return - - text = unicode(text) - - # skip the minus sign if it's first - if text[0] == '-': - start = 1 - else: - start = 0 - - # if it's still not numeric at start skip it - if text[start].isdigit() or text[start] == ".": - # walk through the string, look for split point (the first - # non-numeric) - decimal_count = 0 - for idx in range(start, len(text)): - if text[idx] not in "0123456789.": - break - # special case: also split on second "." - if text[idx] == ".": - decimal_count += 1 - if decimal_count > 1: - break - else: - idx = len(text) - - # move trailing numeric decimal to suffix - # (only if there is other junk after) - if text[idx - 1] == "." and len(text) != idx: - idx = idx - 1 - - # if there is no numeric after the minus, make the minus part of - # the suffix - if idx == 1 and start == 1: - idx = 0 - - part1 = text[0:idx] - part2 = text[idx:len(text)] - - if part1 != "": - self.num = float(part1) - self.suffix = part2 - else: - self.suffix = text - - #print("num: {0} suf: {1}".format(self.num, self.suffix)) - - 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) +from comicapi.issuestring import * diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py index d8751e0..95c7c9c 100644 --- a/comictaggerlib/matchselectionwindow.py +++ b/comictaggerlib/matchselectionwindow.py @@ -1,6 +1,7 @@ """ A PyQT4 dialog to select from automated issue matches """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize """ Copyright 2012-2014 Anthony Beville @@ -54,8 +55,8 @@ class MatchSelectionWindow(QtGui.QDialog): gridlayout.addWidget(self.archiveCoverWidget) gridlayout.setContentsMargins(0, 0, 0, 0) - utils.reduceWidgetFontSize(self.twList) - utils.reduceWidgetFontSize(self.teDescription, 1) + reduceWidgetFontSize(self.twList) + reduceWidgetFontSize(self.teDescription, 1) self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | diff --git a/comictaggerlib/pageloader.py b/comictaggerlib/pageloader.py index 4cc1dff..184713f 100644 --- a/comictaggerlib/pageloader.py +++ b/comictaggerlib/pageloader.py @@ -1,6 +1,7 @@ """ A PyQT4 class to load a page image from a ComicArchive in a background thread """ +from comictaggerlib.ui.qtutils import getQImageFromData """ Copyright 2012-2014 Anthony Beville @@ -67,7 +68,7 @@ class PageLoader(QtCore.QThread): return if image_data is not None: - img = utils.getQImageFromData(image_data) + img = getQImageFromData(image_data) if self.abandoned: return diff --git a/comictaggerlib/progresswindow.py b/comictaggerlib/progresswindow.py index fd8009e..0759ff8 100644 --- a/comictaggerlib/progresswindow.py +++ b/comictaggerlib/progresswindow.py @@ -1,6 +1,7 @@ """ A PyQT4 dialog to show ID log and progress """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize """ Copyright 2012-2014 Anthony Beville @@ -38,4 +39,4 @@ class IDProgressWindow(QtGui.QDialog): QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMaximizeButtonHint) - utils.reduceWidgetFontSize(self.textEdit) + reduceWidgetFontSize(self.textEdit) diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 1dd556e..6efa174 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -2,6 +2,7 @@ """ The main window of the ComicTagger app """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent """ Copyright 2012-2014 Anthony Beville @@ -191,6 +192,10 @@ class TaggerWindow(QtGui.QMainWindow): validator = QtGui.QIntValidator(1, 12, self) self.lePubMonth.setValidator(validator) + # TODO: for now keep it simple, ideally we should check the full date + validator = QtGui.QIntValidator(1, 31, self) + self.lePubDay.setValidator(validator) + validator = QtGui.QIntValidator(1, 99999, self) self.leIssueCount.setValidator(validator) self.leVolumeNum.setValidator(validator) @@ -202,10 +207,10 @@ class TaggerWindow(QtGui.QMainWindow): # for all sorts of wacky things # tweak some control fonts - utils.reduceWidgetFontSize(self.lblFilename, 1) - utils.reduceWidgetFontSize(self.lblArchiveType) - utils.reduceWidgetFontSize(self.lblTagList) - utils.reduceWidgetFontSize(self.lblPageCount) + reduceWidgetFontSize(self.lblFilename, 1) + reduceWidgetFontSize(self.lblArchiveType) + reduceWidgetFontSize(self.lblTagList) + reduceWidgetFontSize(self.lblPageCount) # make sure some editable comboboxes don't take drop actions self.cbFormat.lineEdit().setAcceptDrops(False) @@ -783,6 +788,7 @@ class TaggerWindow(QtGui.QMainWindow): assignText(self.lePublisher, md.publisher) assignText(self.lePubMonth, md.month) assignText(self.lePubYear, md.year) + assignText(self.lePubDay, md.day) assignText(self.leGenre, md.genre) assignText(self.leImprint, md.imprint) assignText(self.teComments, md.comments) @@ -909,6 +915,7 @@ class TaggerWindow(QtGui.QMainWindow): md.publisher = xlate(self.lePublisher.text(), "str") md.month = xlate(self.lePubMonth.text(), "int") md.year = xlate(self.lePubYear.text(), "int") + md.day = xlate(self.lePubDay.text(), "int") md.genre = xlate(self.leGenre.text(), "str") md.imprint = xlate(self.leImprint.text(), "str") md.comments = xlate(self.teComments.toPlainText(), "str") diff --git a/comictaggerlib/ui/__init__.py b/comictaggerlib/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/comictaggerlib/ui/qtutils.py b/comictaggerlib/ui/qtutils.py new file mode 100644 index 0000000..9ab05ea --- /dev/null +++ b/comictaggerlib/ui/qtutils.py @@ -0,0 +1,82 @@ +import StringIO +from PIL import Image +from comictaggerlib.settings import ComicTaggerSettings + + +try: + from PyQt4 import QtGui + qt_available = True +except ImportError: + qt_available = False + +if qt_available: + + def reduceWidgetFontSize( widget , delta = 2): + f = widget.font() + if f.pointSize() > 10: + f.setPointSize( f.pointSize() - delta ) + widget.setFont( f ) + + + def centerWindowOnScreen( window ): + """ + Center the window on screen. This implemention will handle the window + being resized or the screen resolution changing. + """ + # Get the current screens' dimensions... + screen = QtGui.QDesktopWidget().screenGeometry() + # ... and get this windows' dimensions + mysize = window.geometry() + # The horizontal position is calulated as screenwidth - windowwidth /2 + hpos = ( screen.width() - window.width() ) / 2 + # And vertical position the same, but with the height dimensions + vpos = ( screen.height() - window.height() ) / 2 + # And the move call repositions the window + window.move(hpos, vpos) + + + def centerWindowOnParent( window ): + + top_level = window + while top_level.parent() is not None: + top_level = top_level.parent() + + # Get the current screens' dimensions... + main_window_size = top_level.geometry() + # ... and get this windows' dimensions + mysize = window.geometry() + # The horizontal position is calulated as screenwidth - windowwidth /2 + hpos = ( main_window_size.width() - window.width() ) / 2 + # And vertical position the same, but with the height dimensions + vpos = ( main_window_size.height() - window.height() ) / 2 + # And the move call repositions the window + window.move(hpos + main_window_size.left(), vpos + main_window_size.top()) + + try: + from PIL import Image + from PIL import WebPImagePlugin + import StringIO + pil_available = True + except ImportError: + pil_available = False + + def getQImageFromData(image_data): + img = QtGui.QImage() + success = img.loadFromData( image_data ) + if not success: + try: + if pil_available: + # Qt doesn't understand the format, but maybe PIL does + # so try to convert the image data to uncompressed tiff format + im = Image.open(StringIO.StringIO(image_data)) + output = StringIO.StringIO() + im.save(output, format="TIFF") + img.loadFromData( output.getvalue() ) + success = True + except Exception as e: + pass + # if still nothing, go with default image + if not success: + img.load(ComicTaggerSettings.getGraphic('nocover.png')) + return img + diff --git a/comictaggerlib/ui/taggerwindow.ui b/comictaggerlib/ui/taggerwindow.ui index 0ff3f56..d5fcc3b 100644 --- a/comictaggerlib/ui/taggerwindow.ui +++ b/comictaggerlib/ui/taggerwindow.ui @@ -402,14 +402,26 @@ - + - # Issues + Day - + + + + 0 + 0 + + + + + 16777215 + 16777215 + + false @@ -419,27 +431,44 @@ + + + # Issues + + + + + + + false + + + Qt::ImhDigitsOnly + + + + Volume - + false - + # Volumes - + false @@ -449,14 +478,14 @@ - + Alt.Issue - + @@ -469,14 +498,14 @@ - + Alt. # Issues - + false diff --git a/comictaggerlib/utils.py b/comictaggerlib/utils.py index b9c5d7d..3cf0b80 100644 --- a/comictaggerlib/utils.py +++ b/comictaggerlib/utils.py @@ -1,674 +1 @@ -# coding=utf-8 -""" -Some generic utilities -""" - -""" -Copyright 2012-2014 Anthony Beville - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import sys -import os -import re -import platform -import locale -import codecs - -from settings import ComicTaggerSettings - - -class UtilsVars: - already_fixed_encoding = False - - -def get_actual_preferred_encoding(): - preferred_encoding = locale.getpreferredencoding() - if platform.system() == "Darwin": - preferred_encoding = "utf-8" - return preferred_encoding - - -def fix_output_encoding(): - if not UtilsVars.already_fixed_encoding: - # this reads the environment and inits the right locale - locale.setlocale(locale.LC_ALL, "") - - # try to make stdout/stderr encodings happy for unicode printing - preferred_encoding = get_actual_preferred_encoding() - sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout) - sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr) - UtilsVars.already_fixed_encoding = True - - -def get_recursive_filelist(pathlist): - """Get a recursive list of of all files under all path items in the list""" - filename_encoding = sys.getfilesystemencoding() - filelist = [] - for p in pathlist: - # if path is a folder, walk it recursivly, and all files underneath - if isinstance(p, str): - # make sure string is unicode - p = p.decode(filename_encoding) # , 'replace') - elif not isinstance(p, unicode): - # it's probably a QString - p = unicode(p) - - if os.path.isdir(p): - for root, dirs, files in os.walk(p): - for f in files: - if isinstance(f, str): - # make sure string is unicode - f = f.decode(filename_encoding, 'replace') - elif not isinstance(f, unicode): - # it's probably a QString - f = unicode(f) - filelist.append(os.path.join(root, f)) - else: - filelist.append(p) - - return filelist - - -def listToString(l): - string = "" - if l is not None: - for item in l: - if len(string) > 0: - string += ", " - string += item - return string - - -def addtopath(dirname): - if dirname is not None and dirname != "": - - # verify that path doesn't already contain the given dirname - tmpdirname = re.escape(dirname) - pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( - dir=tmpdirname, sep=os.pathsep) - - match = re.search(pattern, os.environ['PATH']) - if not match: - os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH'] - -# returns executable path, if it exists - - -def which(program): - - def is_exe(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - for path in os.environ["PATH"].split(os.pathsep): - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return exe_file - - return None - - -def removearticles(text): - text = text.lower() - articles = ['and', 'the', 'a', '&', 'issue'] - newText = '' - for word in text.split(' '): - if word not in articles: - newText += word + ' ' - - newText = newText[:-1] - - # now get rid of some other junk - newText = newText.replace(":", "") - newText = newText.replace(",", "") - newText = newText.replace("-", " ") - - # since the CV api changed, searches for series names with periods - # now explicity require the period to be in the search key, - # so the line below is removed (for now) - #newText = newText.replace(".", "") - - return newText - - -def unique_file(file_name): - counter = 1 - # returns ('/path/file', '.ext') - file_name_parts = os.path.splitext(file_name) - while True: - if not os.path.lexists(file_name): - return file_name - file_name = file_name_parts[ - 0] + ' (' + str(counter) + ')' + file_name_parts[1] - counter += 1 - - -# -o- coding: utf-8 -o- -# ISO639 python dict -# oficial list in http://www.loc.gov/standards/iso639-2/php/code_list.php - -lang_dict = { - 'ab': 'Abkhaz', - 'aa': 'Afar', - 'af': 'Afrikaans', - 'ak': 'Akan', - 'sq': 'Albanian', - 'am': 'Amharic', - 'ar': 'Arabic', - 'an': 'Aragonese', - 'hy': 'Armenian', - 'as': 'Assamese', - 'av': 'Avaric', - 'ae': 'Avestan', - 'ay': 'Aymara', - 'az': 'Azerbaijani', - 'bm': 'Bambara', - 'ba': 'Bashkir', - 'eu': 'Basque', - 'be': 'Belarusian', - 'bn': 'Bengali', - 'bh': 'Bihari', - 'bi': 'Bislama', - 'bs': 'Bosnian', - 'br': 'Breton', - 'bg': 'Bulgarian', - 'my': 'Burmese', - 'ca': 'Catalan; Valencian', - 'ch': 'Chamorro', - 'ce': 'Chechen', - 'ny': 'Chichewa; Chewa; Nyanja', - 'zh': 'Chinese', - 'cv': 'Chuvash', - 'kw': 'Cornish', - 'co': 'Corsican', - 'cr': 'Cree', - 'hr': 'Croatian', - 'cs': 'Czech', - 'da': 'Danish', - 'dv': 'Divehi; Maldivian;', - 'nl': 'Dutch', - 'dz': 'Dzongkha', - 'en': 'English', - 'eo': 'Esperanto', - 'et': 'Estonian', - 'ee': 'Ewe', - 'fo': 'Faroese', - 'fj': 'Fijian', - 'fi': 'Finnish', - 'fr': 'French', - 'ff': 'Fula', - 'gl': 'Galician', - 'ka': 'Georgian', - 'de': 'German', - 'el': 'Greek, Modern', - 'gn': 'Guaraní', - 'gu': 'Gujarati', - 'ht': 'Haitian', - 'ha': 'Hausa', - 'he': 'Hebrew (modern)', - 'hz': 'Herero', - 'hi': 'Hindi', - 'ho': 'Hiri Motu', - 'hu': 'Hungarian', - 'ia': 'Interlingua', - 'id': 'Indonesian', - 'ie': 'Interlingue', - 'ga': 'Irish', - 'ig': 'Igbo', - 'ik': 'Inupiaq', - 'io': 'Ido', - 'is': 'Icelandic', - 'it': 'Italian', - 'iu': 'Inuktitut', - 'ja': 'Japanese', - 'jv': 'Javanese', - 'kl': 'Kalaallisut', - 'kn': 'Kannada', - 'kr': 'Kanuri', - 'ks': 'Kashmiri', - 'kk': 'Kazakh', - 'km': 'Khmer', - 'ki': 'Kikuyu, Gikuyu', - 'rw': 'Kinyarwanda', - 'ky': 'Kirghiz, Kyrgyz', - 'kv': 'Komi', - 'kg': 'Kongo', - 'ko': 'Korean', - 'ku': 'Kurdish', - 'kj': 'Kwanyama, Kuanyama', - 'la': 'Latin', - 'lb': 'Luxembourgish', - 'lg': 'Luganda', - 'li': 'Limburgish', - 'ln': 'Lingala', - 'lo': 'Lao', - 'lt': 'Lithuanian', - 'lu': 'Luba-Katanga', - 'lv': 'Latvian', - 'gv': 'Manx', - 'mk': 'Macedonian', - 'mg': 'Malagasy', - 'ms': 'Malay', - 'ml': 'Malayalam', - 'mt': 'Maltese', - 'mi': 'Māori', - 'mr': 'Marathi (Marāṭhī)', - 'mh': 'Marshallese', - 'mn': 'Mongolian', - 'na': 'Nauru', - 'nv': 'Navajo, Navaho', - 'nb': 'Norwegian Bokmål', - 'nd': 'North Ndebele', - 'ne': 'Nepali', - 'ng': 'Ndonga', - 'nn': 'Norwegian Nynorsk', - 'no': 'Norwegian', - 'ii': 'Nuosu', - 'nr': 'South Ndebele', - 'oc': 'Occitan', - 'oj': 'Ojibwe, Ojibwa', - 'cu': 'Old Church Slavonic', - 'om': 'Oromo', - 'or': 'Oriya', - 'os': 'Ossetian, Ossetic', - 'pa': 'Panjabi, Punjabi', - 'pi': 'Pāli', - 'fa': 'Persian', - 'pl': 'Polish', - 'ps': 'Pashto, Pushto', - 'pt': 'Portuguese', - 'qu': 'Quechua', - 'rm': 'Romansh', - 'rn': 'Kirundi', - 'ro': 'Romanian, Moldavan', - 'ru': 'Russian', - 'sa': 'Sanskrit (Saṁskṛta)', - 'sc': 'Sardinian', - 'sd': 'Sindhi', - 'se': 'Northern Sami', - 'sm': 'Samoan', - 'sg': 'Sango', - 'sr': 'Serbian', - 'gd': 'Scottish Gaelic', - 'sn': 'Shona', - 'si': 'Sinhala, Sinhalese', - 'sk': 'Slovak', - 'sl': 'Slovene', - 'so': 'Somali', - 'st': 'Southern Sotho', - 'es': 'Spanish; Castilian', - 'su': 'Sundanese', - 'sw': 'Swahili', - 'ss': 'Swati', - 'sv': 'Swedish', - 'ta': 'Tamil', - 'te': 'Telugu', - 'tg': 'Tajik', - 'th': 'Thai', - 'ti': 'Tigrinya', - 'bo': 'Tibetan', - 'tk': 'Turkmen', - 'tl': 'Tagalog', - 'tn': 'Tswana', - 'to': 'Tonga', - 'tr': 'Turkish', - 'ts': 'Tsonga', - 'tt': 'Tatar', - 'tw': 'Twi', - 'ty': 'Tahitian', - 'ug': 'Uighur, Uyghur', - 'uk': 'Ukrainian', - 'ur': 'Urdu', - 'uz': 'Uzbek', - 've': 'Venda', - 'vi': 'Vietnamese', - 'vo': 'Volapük', - 'wa': 'Walloon', - 'cy': 'Welsh', - 'wo': 'Wolof', - 'fy': 'Western Frisian', - 'xh': 'Xhosa', - 'yi': 'Yiddish', - 'yo': 'Yoruba', - 'za': 'Zhuang, Chuang', - 'zu': 'Zulu', -} - - -countries = [ - ('AF', 'Afghanistan'), - ('AL', 'Albania'), - ('DZ', 'Algeria'), - ('AS', 'American Samoa'), - ('AD', 'Andorra'), - ('AO', 'Angola'), - ('AI', 'Anguilla'), - ('AQ', 'Antarctica'), - ('AG', 'Antigua And Barbuda'), - ('AR', 'Argentina'), - ('AM', 'Armenia'), - ('AW', 'Aruba'), - ('AU', 'Australia'), - ('AT', 'Austria'), - ('AZ', 'Azerbaijan'), - ('BS', 'Bahamas'), - ('BH', 'Bahrain'), - ('BD', 'Bangladesh'), - ('BB', 'Barbados'), - ('BY', 'Belarus'), - ('BE', 'Belgium'), - ('BZ', 'Belize'), - ('BJ', 'Benin'), - ('BM', 'Bermuda'), - ('BT', 'Bhutan'), - ('BO', 'Bolivia'), - ('BA', 'Bosnia And Herzegowina'), - ('BW', 'Botswana'), - ('BV', 'Bouvet Island'), - ('BR', 'Brazil'), - ('BN', 'Brunei Darussalam'), - ('BG', 'Bulgaria'), - ('BF', 'Burkina Faso'), - ('BI', 'Burundi'), - ('KH', 'Cambodia'), - ('CM', 'Cameroon'), - ('CA', 'Canada'), - ('CV', 'Cape Verde'), - ('KY', 'Cayman Islands'), - ('CF', 'Central African Rep'), - ('TD', 'Chad'), - ('CL', 'Chile'), - ('CN', 'China'), - ('CX', 'Christmas Island'), - ('CC', 'Cocos Islands'), - ('CO', 'Colombia'), - ('KM', 'Comoros'), - ('CG', 'Congo'), - ('CK', 'Cook Islands'), - ('CR', 'Costa Rica'), - ('CI', 'Cote D`ivoire'), - ('HR', 'Croatia'), - ('CU', 'Cuba'), - ('CY', 'Cyprus'), - ('CZ', 'Czech Republic'), - ('DK', 'Denmark'), - ('DJ', 'Djibouti'), - ('DM', 'Dominica'), - ('DO', 'Dominican Republic'), - ('TP', 'East Timor'), - ('EC', 'Ecuador'), - ('EG', 'Egypt'), - ('SV', 'El Salvador'), - ('GQ', 'Equatorial Guinea'), - ('ER', 'Eritrea'), - ('EE', 'Estonia'), - ('ET', 'Ethiopia'), - ('FK', 'Falkland Islands (Malvinas)'), - ('FO', 'Faroe Islands'), - ('FJ', 'Fiji'), - ('FI', 'Finland'), - ('FR', 'France'), - ('GF', 'French Guiana'), - ('PF', 'French Polynesia'), - ('TF', 'French S. Territories'), - ('GA', 'Gabon'), - ('GM', 'Gambia'), - ('GE', 'Georgia'), - ('DE', 'Germany'), - ('GH', 'Ghana'), - ('GI', 'Gibraltar'), - ('GR', 'Greece'), - ('GL', 'Greenland'), - ('GD', 'Grenada'), - ('GP', 'Guadeloupe'), - ('GU', 'Guam'), - ('GT', 'Guatemala'), - ('GN', 'Guinea'), - ('GW', 'Guinea-bissau'), - ('GY', 'Guyana'), - ('HT', 'Haiti'), - ('HN', 'Honduras'), - ('HK', 'Hong Kong'), - ('HU', 'Hungary'), - ('IS', 'Iceland'), - ('IN', 'India'), - ('ID', 'Indonesia'), - ('IR', 'Iran'), - ('IQ', 'Iraq'), - ('IE', 'Ireland'), - ('IL', 'Israel'), - ('IT', 'Italy'), - ('JM', 'Jamaica'), - ('JP', 'Japan'), - ('JO', 'Jordan'), - ('KZ', 'Kazakhstan'), - ('KE', 'Kenya'), - ('KI', 'Kiribati'), - ('KP', 'Korea (North)'), - ('KR', 'Korea (South)'), - ('KW', 'Kuwait'), - ('KG', 'Kyrgyzstan'), - ('LA', 'Laos'), - ('LV', 'Latvia'), - ('LB', 'Lebanon'), - ('LS', 'Lesotho'), - ('LR', 'Liberia'), - ('LY', 'Libya'), - ('LI', 'Liechtenstein'), - ('LT', 'Lithuania'), - ('LU', 'Luxembourg'), - ('MO', 'Macau'), - ('MK', 'Macedonia'), - ('MG', 'Madagascar'), - ('MW', 'Malawi'), - ('MY', 'Malaysia'), - ('MV', 'Maldives'), - ('ML', 'Mali'), - ('MT', 'Malta'), - ('MH', 'Marshall Islands'), - ('MQ', 'Martinique'), - ('MR', 'Mauritania'), - ('MU', 'Mauritius'), - ('YT', 'Mayotte'), - ('MX', 'Mexico'), - ('FM', 'Micronesia'), - ('MD', 'Moldova'), - ('MC', 'Monaco'), - ('MN', 'Mongolia'), - ('MS', 'Montserrat'), - ('MA', 'Morocco'), - ('MZ', 'Mozambique'), - ('MM', 'Myanmar'), - ('NA', 'Namibia'), - ('NR', 'Nauru'), - ('NP', 'Nepal'), - ('NL', 'Netherlands'), - ('AN', 'Netherlands Antilles'), - ('NC', 'New Caledonia'), - ('NZ', 'New Zealand'), - ('NI', 'Nicaragua'), - ('NE', 'Niger'), - ('NG', 'Nigeria'), - ('NU', 'Niue'), - ('NF', 'Norfolk Island'), - ('MP', 'Northern Mariana Islands'), - ('NO', 'Norway'), - ('OM', 'Oman'), - ('PK', 'Pakistan'), - ('PW', 'Palau'), - ('PA', 'Panama'), - ('PG', 'Papua New Guinea'), - ('PY', 'Paraguay'), - ('PE', 'Peru'), - ('PH', 'Philippines'), - ('PN', 'Pitcairn'), - ('PL', 'Poland'), - ('PT', 'Portugal'), - ('PR', 'Puerto Rico'), - ('QA', 'Qatar'), - ('RE', 'Reunion'), - ('RO', 'Romania'), - ('RU', 'Russian Federation'), - ('RW', 'Rwanda'), - ('KN', 'Saint Kitts And Nevis'), - ('LC', 'Saint Lucia'), - ('VC', 'St Vincent/Grenadines'), - ('WS', 'Samoa'), - ('SM', 'San Marino'), - ('ST', 'Sao Tome'), - ('SA', 'Saudi Arabia'), - ('SN', 'Senegal'), - ('SC', 'Seychelles'), - ('SL', 'Sierra Leone'), - ('SG', 'Singapore'), - ('SK', 'Slovakia'), - ('SI', 'Slovenia'), - ('SB', 'Solomon Islands'), - ('SO', 'Somalia'), - ('ZA', 'South Africa'), - ('ES', 'Spain'), - ('LK', 'Sri Lanka'), - ('SH', 'St. Helena'), - ('PM', 'St.Pierre'), - ('SD', 'Sudan'), - ('SR', 'Suriname'), - ('SZ', 'Swaziland'), - ('SE', 'Sweden'), - ('CH', 'Switzerland'), - ('SY', 'Syrian Arab Republic'), - ('TW', 'Taiwan'), - ('TJ', 'Tajikistan'), - ('TZ', 'Tanzania'), - ('TH', 'Thailand'), - ('TG', 'Togo'), - ('TK', 'Tokelau'), - ('TO', 'Tonga'), - ('TT', 'Trinidad And Tobago'), - ('TN', 'Tunisia'), - ('TR', 'Turkey'), - ('TM', 'Turkmenistan'), - ('TV', 'Tuvalu'), - ('UG', 'Uganda'), - ('UA', 'Ukraine'), - ('AE', 'United Arab Emirates'), - ('UK', 'United Kingdom'), - ('US', 'United States'), - ('UY', 'Uruguay'), - ('UZ', 'Uzbekistan'), - ('VU', 'Vanuatu'), - ('VA', 'Vatican City State'), - ('VE', 'Venezuela'), - ('VN', 'Viet Nam'), - ('VG', 'Virgin Islands (British)'), - ('VI', 'Virgin Islands (U.S.)'), - ('EH', 'Western Sahara'), - ('YE', 'Yemen'), - ('YU', 'Yugoslavia'), - ('ZR', 'Zaire'), - ('ZM', 'Zambia'), - ('ZW', 'Zimbabwe') -] - - -def getLanguageDict(): - return lang_dict - - -def getLanguageFromISO(iso): - if iso is None: - return None - else: - return lang_dict[iso] - - -try: - from PyQt4 import QtGui - qt_available = True -except ImportError: - qt_available = False - -if qt_available: - def reduceWidgetFontSize(widget, delta=2): - f = widget.font() - if f.pointSize() > 10: - f.setPointSize(f.pointSize() - delta) - widget.setFont(f) - - def centerWindowOnScreen(window): - """ - Center the window on screen. This implemention will handle the window - being resized or the screen resolution changing. - """ - # Get the current screens' dimensions... - screen = QtGui.QDesktopWidget().screenGeometry() - # ... and get this windows' dimensions - mysize = window.geometry() - # The horizontal position is calulated as screenwidth - windowwidth /2 - hpos = (screen.width() - window.width()) / 2 - # And vertical position the same, but with the height dimensions - vpos = (screen.height() - window.height()) / 2 - # And the move call repositions the window - window.move(hpos, vpos) - - def centerWindowOnParent(window): - - top_level = window - while top_level.parent() is not None: - top_level = top_level.parent() - - # Get the current screens' dimensions... - main_window_size = top_level.geometry() - # ... and get this windows' dimensions - mysize = window.geometry() - # The horizontal position is calulated as screenwidth - windowwidth /2 - hpos = (main_window_size.width() - window.width()) / 2 - # And vertical position the same, but with the height dimensions - vpos = (main_window_size.height() - window.height()) / 2 - # And the move call repositions the window - window.move( - hpos + main_window_size.left(), vpos + main_window_size.top()) - - try: - from PIL import Image - from PIL import WebPImagePlugin - import StringIO - pil_available = True - except ImportError: - pil_available = False - - def getQImageFromData(image_data): - img = QtGui.QImage() - success = img.loadFromData(image_data) - if not success: - try: - if pil_available: - # Qt doesn't understand the format, but maybe PIL does - # so try to convert the image data to uncompressed tiff - # format - im = Image.open(StringIO.StringIO(image_data)) - output = StringIO.StringIO() - im.save(output, format="TIFF") - img.loadFromData(output.getvalue()) - success = True - except Exception as e: - pass - # if still nothing, go with default image - if not success: - img.load(ComicTaggerSettings.getGraphic('nocover.png')) - return img +from comicapi.utils import * diff --git a/comictaggerlib/volumeselectionwindow.py b/comictaggerlib/volumeselectionwindow.py index 68607e4..58d57c5 100644 --- a/comictaggerlib/volumeselectionwindow.py +++ b/comictaggerlib/volumeselectionwindow.py @@ -1,6 +1,7 @@ """ A PyQT4 dialog to select specific series/volume from list """ +from comictaggerlib.ui.qtutils import reduceWidgetFontSize """ Copyright 2012-2014 Anthony Beville @@ -108,8 +109,8 @@ class VolumeSelectionWindow(QtGui.QDialog): gridlayout.addWidget(self.imageWidget) gridlayout.setContentsMargins(0, 0, 0, 0) - utils.reduceWidgetFontSize(self.teDetails, 1) - utils.reduceWidgetFontSize(self.twList) + reduceWidgetFontSize(self.teDetails, 1) + reduceWidgetFontSize(self.twList) self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowSystemMenuHint | diff --git a/requirements.txt b/requirements.txt index 2d95f59..fc0dc23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ configparser beautifulsoup4 >= 4.1 +-e git+https://github.com/matiasb/python-unrar.git@1010cf5f6fcf09a9b1fe7c958faf0167fafaa7d9#egg=unrar-master +natsort==3.5.2 +PyPDF2==1.24 \ No newline at end of file