Integrated cbt/tar file support
Added logging capability Code cosmetics
This commit is contained in:
parent
15dff9ce4e
commit
3e15b950b7
@ -2,4 +2,4 @@
|
|||||||
comicapi originates [here](https://github.com/davide-romanini/comicapi), was integrated into [ComicStreamer](https://github.com/davide-romanini/ComicStreamer), was modified in [this fork](https://github.com/kounch/ComicStreamer), and has now been extracted and packaged by yours truly (Iris W).
|
comicapi originates [here](https://github.com/davide-romanini/comicapi), was integrated into [ComicStreamer](https://github.com/davide-romanini/ComicStreamer), was modified in [this fork](https://github.com/kounch/ComicStreamer), and has now been extracted and packaged by yours truly (Iris W).
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
you can use pip to install this. cbr support is off by default—you'll need to do `pip install unrar` as well as having libunrar.so available.
|
you can use pip to install this. cbr support is off by default—you'll need to do `pip install rarfile` as well as having "unrar" available.
|
||||||
|
@ -1 +1,3 @@
|
|||||||
__author__ = 'dromanin'
|
__author__ = 'dromanin'
|
||||||
|
|
||||||
|
__version__ = '2.1.1'
|
||||||
|
24
comicapi/comet.py
Executable file → Normal file
24
comicapi/comet.py
Executable file → Normal file
@ -7,7 +7,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -16,9 +16,6 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
import zipfile
|
|
||||||
from pprint import pprint
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from comicapi.genericmetadata import GenericMetadata
|
from comicapi.genericmetadata import GenericMetadata
|
||||||
import comicapi.utils
|
import comicapi.utils
|
||||||
@ -64,7 +61,7 @@ class CoMet:
|
|||||||
|
|
||||||
def convertMetadataToXML(self, filename, metadata):
|
def convertMetadataToXML(self, filename, metadata):
|
||||||
|
|
||||||
#shorthand for the metadata
|
# shorthand for the metadata
|
||||||
md = metadata
|
md = metadata
|
||||||
|
|
||||||
# build a tree structure
|
# build a tree structure
|
||||||
@ -74,7 +71,7 @@ class CoMet:
|
|||||||
root.attrib[
|
root.attrib[
|
||||||
'xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
|
'xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
|
||||||
|
|
||||||
#helper func
|
# helper func
|
||||||
def assign(comet_entry, md_entry):
|
def assign(comet_entry, md_entry):
|
||||||
if md_entry is not None:
|
if md_entry is not None:
|
||||||
ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry)
|
ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry)
|
||||||
@ -84,7 +81,7 @@ class CoMet:
|
|||||||
md.title = ""
|
md.title = ""
|
||||||
assign('title', md.title)
|
assign('title', md.title)
|
||||||
assign('series', md.series)
|
assign('series', md.series)
|
||||||
assign('issue', md.issue) #must be int??
|
assign('issue', md.issue) # must be int??
|
||||||
assign('volume', md.volume)
|
assign('volume', md.volume)
|
||||||
assign('description', md.comments)
|
assign('description', md.comments)
|
||||||
assign('publisher', md.publisher)
|
assign('publisher', md.publisher)
|
||||||
@ -116,15 +113,6 @@ class CoMet:
|
|||||||
|
|
||||||
assign('coverImage', md.coverImage)
|
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
|
# loop thru credits, and build a list for each role that CoMet supports
|
||||||
for credit in metadata.credits:
|
for credit in metadata.credits:
|
||||||
|
|
||||||
@ -169,7 +157,6 @@ class CoMet:
|
|||||||
|
|
||||||
if root.tag != 'comet':
|
if root.tag != 'comet':
|
||||||
raise KeyError("Not a comet XML!")
|
raise KeyError("Not a comet XML!")
|
||||||
#return None
|
|
||||||
|
|
||||||
metadata = GenericMetadata()
|
metadata = GenericMetadata()
|
||||||
md = metadata
|
md = metadata
|
||||||
@ -234,7 +221,7 @@ class CoMet:
|
|||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
#verify that the string actually contains CoMet data in XML format
|
# verify that the string actually contains CoMet data in XML format
|
||||||
def validateString(self, string):
|
def validateString(self, string):
|
||||||
try:
|
try:
|
||||||
tree = ET.ElementTree(ET.fromstring(string))
|
tree = ET.ElementTree(ET.fromstring(string))
|
||||||
@ -249,7 +236,6 @@ class CoMet:
|
|||||||
def writeToExternalFile(self, filename, metadata):
|
def writeToExternalFile(self, filename, metadata):
|
||||||
|
|
||||||
tree = self.convertMetadataToXML(self, metadata)
|
tree = self.convertMetadataToXML(self, metadata)
|
||||||
#ET.dump(tree)
|
|
||||||
tree.write(filename, encoding='utf-8')
|
tree.write(filename, encoding='utf-8')
|
||||||
|
|
||||||
def readFromExternalFile(self, filename):
|
def readFromExternalFile(self, filename):
|
||||||
|
380
comicapi/comicarchive.py
Executable file → Normal file
380
comicapi/comicarchive.py
Executable file → Normal file
@ -17,84 +17,36 @@ limitations under the License.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import tarfile
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import subprocess
|
import subprocess
|
||||||
import platform
|
import platform
|
||||||
import locale
|
import logging
|
||||||
import shutil
|
|
||||||
|
|
||||||
from natsort import natsorted
|
from natsort import natsorted
|
||||||
try:
|
try:
|
||||||
import rarfile
|
import rarfile
|
||||||
#from unrar import unrarlib
|
|
||||||
#import unrar.constants
|
|
||||||
#from unrar import constants
|
|
||||||
rarsupport = True
|
rarsupport = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
rarsupport = False
|
rarsupport = False
|
||||||
import ctypes
|
|
||||||
import io
|
|
||||||
|
|
||||||
|
|
||||||
'''if rarsupport:
|
|
||||||
class OpenableRarFile(rarfile.RarFile):
|
|
||||||
def open(self, member):
|
|
||||||
#print "opening %s..." % member
|
|
||||||
# based on https://github.com/matiasb/python-unrar/pull/4/files
|
|
||||||
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 b''.join(buf)'''
|
|
||||||
|
|
||||||
|
|
||||||
# if platform.system() == "Windows":
|
|
||||||
# import _subprocess
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from io import BytesIO
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
pil_available = True
|
pil_available = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pil_available = False
|
pil_available = False
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath("."))
|
logger = logging.getLogger(__name__)
|
||||||
#import UnRAR2
|
logger.setLevel(logging.INFO)
|
||||||
#from UnRAR2.rar_exceptions import *
|
|
||||||
|
sys.path.insert(0, os.path.abspath("."))
|
||||||
|
|
||||||
#from settings import ComicTaggerSettings
|
|
||||||
from comicapi.comicinfoxml import ComicInfoXml
|
from comicapi.comicinfoxml import ComicInfoXml
|
||||||
from comicapi.comicbookinfo import ComicBookInfo
|
from comicapi.comicbookinfo import ComicBookInfo
|
||||||
from comicapi.comet import CoMet
|
from comicapi.comet import CoMet
|
||||||
@ -124,7 +76,6 @@ class ZipArchiver:
|
|||||||
return self.writeZipComment(self.path, comment)
|
return self.writeZipComment(self.path, comment)
|
||||||
|
|
||||||
def readArchiveFile(self, archive_file):
|
def readArchiveFile(self, archive_file):
|
||||||
data = ""
|
|
||||||
zf = zipfile.ZipFile(self.path, 'r')
|
zf = zipfile.ZipFile(self.path, 'r')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -132,14 +83,14 @@ class ZipArchiver:
|
|||||||
except zipfile.BadZipfile as e:
|
except zipfile.BadZipfile as e:
|
||||||
errMsg = u"bad zipfile [{0}]: {1} :: {2}".format(
|
errMsg = u"bad zipfile [{0}]: {1} :: {2}".format(
|
||||||
e, self.path, archive_file)
|
e, self.path, archive_file)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
zf.close()
|
zf.close()
|
||||||
raise IOError
|
raise IOError
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
zf.close()
|
zf.close()
|
||||||
errMsg = u"bad zipfile [{0}]: {1} :: {2}".format(
|
errMsg = u"bad zipfile [{0}]: {1} :: {2}".format(
|
||||||
e, self.path, archive_file)
|
e, self.path, archive_file)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
raise IOError
|
raise IOError
|
||||||
finally:
|
finally:
|
||||||
zf.close()
|
zf.close()
|
||||||
@ -160,7 +111,7 @@ class ZipArchiver:
|
|||||||
try:
|
try:
|
||||||
self.rebuildZipFile([archive_file])
|
self.rebuildZipFile([archive_file])
|
||||||
|
|
||||||
#now just add the archive file as a new one
|
# now just add the archive file as a new one
|
||||||
zf = zipfile.ZipFile(
|
zf = zipfile.ZipFile(
|
||||||
self.path, mode='a', compression=zipfile.ZIP_DEFLATED)
|
self.path, mode='a', compression=zipfile.ZIP_DEFLATED)
|
||||||
zf.writestr(archive_file, data)
|
zf.writestr(archive_file, data)
|
||||||
@ -178,14 +129,14 @@ class ZipArchiver:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
errMsg = u"Unable to get zipfile list [{0}]: {1}".format(
|
errMsg = u"Unable to get zipfile list [{0}]: {1}".format(
|
||||||
e, self.path)
|
e, self.path)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# zip helper func
|
# zip helper func
|
||||||
def rebuildZipFile(self, exclude_list):
|
def rebuildZipFile(self, exclude_list):
|
||||||
|
|
||||||
# this recompresses the zip archive, without the files in the exclude_list
|
# this recompresses the zip archive, without the files in the exclude_list
|
||||||
#errMsg=u"Rebuilding zip {0} without {1}".format( self.path, exclude_list )
|
# errMsg=u"Rebuilding zip {0} without {1}".format( self.path, exclude_list )
|
||||||
|
|
||||||
# generate temp file
|
# generate temp file
|
||||||
tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(self.path))
|
tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(self.path))
|
||||||
@ -195,10 +146,10 @@ class ZipArchiver:
|
|||||||
zout = zipfile.ZipFile(tmp_name, 'w')
|
zout = zipfile.ZipFile(tmp_name, 'w')
|
||||||
for item in zin.infolist():
|
for item in zin.infolist():
|
||||||
buffer = zin.read(item.filename)
|
buffer = zin.read(item.filename)
|
||||||
if (item.filename not in exclude_list):
|
if item.filename not in exclude_list:
|
||||||
zout.writestr(item, buffer)
|
zout.writestr(item, buffer)
|
||||||
|
|
||||||
#preserve the old comment
|
# preserve the old comment
|
||||||
zout.comment = zin.comment
|
zout.comment = zin.comment
|
||||||
|
|
||||||
zout.close()
|
zout.close()
|
||||||
@ -218,14 +169,13 @@ class ZipArchiver:
|
|||||||
see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure
|
see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#get file size
|
|
||||||
statinfo = os.stat(filename)
|
statinfo = os.stat(filename)
|
||||||
file_length = statinfo.st_size
|
file_length = statinfo.st_size
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fo = open(filename, "r+b")
|
fo = open(filename, "r+b")
|
||||||
|
|
||||||
#the starting position, relative to EOF
|
# the starting position, relative to EOF
|
||||||
pos = -4
|
pos = -4
|
||||||
|
|
||||||
found = False
|
found = False
|
||||||
@ -238,13 +188,13 @@ class ZipArchiver:
|
|||||||
|
|
||||||
value = fo.read(4)
|
value = fo.read(4)
|
||||||
|
|
||||||
#look for the end of central directory signature
|
# look for the end of central directory signature
|
||||||
if bytearray(value) == bytearray([0x50, 0x4b, 0x05, 0x06]):
|
if bytearray(value) == bytearray([0x50, 0x4b, 0x05, 0x06]):
|
||||||
found = True
|
found = True
|
||||||
else:
|
else:
|
||||||
# not found, step back another byte
|
# not found, step back another byte
|
||||||
pos = pos - 1
|
pos = pos - 1
|
||||||
#print pos,"{1} int: {0:x}".format(bytearray(value)[0], value)
|
# print pos,"{1} int: {0:x}".format(bytearray(value)[0], value)
|
||||||
|
|
||||||
if found:
|
if found:
|
||||||
|
|
||||||
@ -282,22 +232,200 @@ class ZipArchiver:
|
|||||||
zout.writestr(fname, data)
|
zout.writestr(fname, data)
|
||||||
zout.close()
|
zout.close()
|
||||||
|
|
||||||
#preserve the old comment
|
# preserve the old comment
|
||||||
comment = otherArchive.getArchiveComment()
|
comment = otherArchive.getArchiveComment()
|
||||||
if comment is not None:
|
if comment is not None:
|
||||||
if not self.writeZipComment(self.path, comment):
|
if not self.writeZipComment(self.path, comment):
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errMsg = u"Error while copying to {0}: {1}".format(self.path, e)
|
errMsg = u"Error while copying to {0}: {1}".format(self.path, e)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------
|
class TarArchiver:
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def getArchiveComment(self):
|
||||||
|
tf = tarfile.TarFile(self.path, 'r')
|
||||||
|
comment = tf.comment
|
||||||
|
tf.close()
|
||||||
|
return comment
|
||||||
|
|
||||||
|
def setArchiveComment(self, comment):
|
||||||
|
return self.writeTarComment(self.path, comment)
|
||||||
|
|
||||||
|
def readArchiveFile(self, archive_file):
|
||||||
|
tf = tarfile.TarFile(self.path, 'r')
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = tf.extractfile(archive_file).read()
|
||||||
|
except tarfile.TarError as e:
|
||||||
|
errMsg = u"bad tarfile [{0}]: {1} :: {2}".format(
|
||||||
|
e, self.path, archive_file)
|
||||||
|
logger.info(errMsg)
|
||||||
|
tf.close()
|
||||||
|
raise IOError
|
||||||
|
except Exception as e:
|
||||||
|
tf.close()
|
||||||
|
errMsg = u"bad tarfile [{0}]: {1} :: {2}".format(
|
||||||
|
e, self.path, archive_file)
|
||||||
|
logger.info(errMsg)
|
||||||
|
raise IOError
|
||||||
|
finally:
|
||||||
|
tf.close()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def removeArchiveFile(self, archive_file):
|
||||||
|
try:
|
||||||
|
self.rebuildTarFile([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.rebuildTarFile([archive_file])
|
||||||
|
|
||||||
|
# now just add the archive file as a new one
|
||||||
|
tf = tarfile.Tarfile(
|
||||||
|
self.path, mode='a')
|
||||||
|
tf.writestr(archive_file, data)
|
||||||
|
tf.close()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getArchiveFilenameList(self):
|
||||||
|
try:
|
||||||
|
tf = tarfile.TarFile(self.path, 'r')
|
||||||
|
namelist = tf.getnames()
|
||||||
|
tf.close()
|
||||||
|
return namelist
|
||||||
|
except Exception as e:
|
||||||
|
errMsg = u"Unable to get tarfile list [{0}]: {1}".format(
|
||||||
|
e, self.path)
|
||||||
|
logger.info(errMsg)
|
||||||
|
return []
|
||||||
|
|
||||||
|
# zip helper func
|
||||||
|
def rebuildTarFile(self, exclude_list):
|
||||||
|
|
||||||
|
# this recompresses the zip archive, without the files in the exclude_list
|
||||||
|
# errMsg=u"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)
|
||||||
|
|
||||||
|
tin = tarfile.TarFile(self.path, 'r')
|
||||||
|
tout = tarfile.TarFile(tmp_name, 'w')
|
||||||
|
for item in tin.infolist():
|
||||||
|
buffer = tin.read(item.filename)
|
||||||
|
if (item.filename not in exclude_list):
|
||||||
|
tout.writestr(item, buffer)
|
||||||
|
|
||||||
|
# preserve the old comment
|
||||||
|
tout.comment = tin.comment
|
||||||
|
|
||||||
|
tout.close()
|
||||||
|
tin.close()
|
||||||
|
|
||||||
|
# replace with the new file
|
||||||
|
os.remove(self.path)
|
||||||
|
os.rename(tmp_name, self.path)
|
||||||
|
|
||||||
|
def writeTarComment(self, filename, comment):
|
||||||
|
"""
|
||||||
|
This is a custom function for writing a comment to a tar file,
|
||||||
|
since the built-in one doesn't seem to work on Windows and Mac OS/X
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 tar file!')
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def copyFromArchive(self, otherArchive):
|
||||||
|
# Replace the current zip with one copied from another archive
|
||||||
|
try:
|
||||||
|
tout = tarfile.TarFile(self.path, 'w')
|
||||||
|
for fname in otherArchive.getArchiveFilenameList():
|
||||||
|
data = otherArchive.readArchiveFile(fname)
|
||||||
|
if data is not None:
|
||||||
|
tout.writestr(fname, data)
|
||||||
|
tout.close()
|
||||||
|
|
||||||
|
# preserve the old comment
|
||||||
|
comment = otherArchive.getArchiveComment()
|
||||||
|
if comment is not None:
|
||||||
|
if not self.writeTarComment(self.path, comment):
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
errMsg = u"Error while copying to {0}: {1}".format(self.path, e)
|
||||||
|
logger.info(errMsg)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
# RAR implementation
|
# RAR implementation
|
||||||
|
|
||||||
|
|
||||||
if rarsupport:
|
if rarsupport:
|
||||||
class RarArchiver:
|
class RarArchiver:
|
||||||
|
|
||||||
@ -319,7 +447,7 @@ if rarsupport:
|
|||||||
self.startupinfo = None
|
self.startupinfo = None
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
#RarArchiver.devnull.close()
|
# RarArchiver.devnull.close()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def getArchiveComment(self):
|
def getArchiveComment(self):
|
||||||
@ -363,8 +491,6 @@ if rarsupport:
|
|||||||
|
|
||||||
# Make sure to escape brackets, since some funky stuff is going on
|
# Make sure to escape brackets, since some funky stuff is going on
|
||||||
# underneath with "fnmatch"
|
# underneath with "fnmatch"
|
||||||
#archive_file = archive_file.replace("[", '[[]')
|
|
||||||
entries = []
|
|
||||||
|
|
||||||
rarc = self.getRARObj()
|
rarc = self.getRARObj()
|
||||||
|
|
||||||
@ -376,30 +502,30 @@ if rarsupport:
|
|||||||
entries = [(rarc.getinfo(archive_file), data)]
|
entries = [(rarc.getinfo(archive_file), data)]
|
||||||
|
|
||||||
if entries[0][0].file_size != len(entries[0][1]):
|
if entries[0][0].file_size != len(entries[0][1]):
|
||||||
errMsg = u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format(
|
errMsg = u"readArchiveFile(): " \
|
||||||
entries[0][0].file_size, len(entries[0][1]), self.path,
|
u"[file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]\n".format(
|
||||||
archive_file, tries)
|
entries[0][0].file_size, len(entries[0][1]), self.path, archive_file, tries)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
errMsg = u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(
|
errMsg = u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(
|
||||||
str(e), self.path, archive_file, tries)
|
str(e), self.path, archive_file, tries)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errMsg = u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(
|
errMsg = u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(
|
||||||
str(e), self.path, archive_file, tries)
|
str(e), self.path, archive_file, tries)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
break
|
break
|
||||||
|
|
||||||
else:
|
else:
|
||||||
#Success"
|
# Success"
|
||||||
#entries is a list of of tuples: ( rarinfo, filedata)
|
# entries is a list of of tuples: ( rarinfo, filedata)
|
||||||
if tries > 1:
|
if tries > 1:
|
||||||
errMsg = u"Attempted read_files() {0} times".format(tries)
|
errMsg = u"Attempted read_files() {0} times".format(tries)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
if (len(entries) == 1):
|
if len(entries) == 1:
|
||||||
return entries[0][1]
|
return entries[0][1]
|
||||||
else:
|
else:
|
||||||
raise IOError
|
raise IOError
|
||||||
@ -463,14 +589,14 @@ if rarsupport:
|
|||||||
def getArchiveFilenameList(self):
|
def getArchiveFilenameList(self):
|
||||||
|
|
||||||
rarc = self.getRARObj()
|
rarc = self.getRARObj()
|
||||||
#namelist = [ item.filename for item in rarc.infolist() ]
|
# namelist = [ item.filename for item in rarc.infolist() ]
|
||||||
#return namelist
|
# return namelist
|
||||||
|
|
||||||
tries = 0
|
tries = 0
|
||||||
while tries < 7:
|
while tries < 7:
|
||||||
try:
|
try:
|
||||||
tries = tries + 1
|
tries = tries + 1
|
||||||
#namelist = [ item.filename for item in rarc.infolist() ]
|
# namelist = [ item.filename for item in rarc.infolist() ]
|
||||||
namelist = []
|
namelist = []
|
||||||
for item in rarc.infolist():
|
for item in rarc.infolist():
|
||||||
if item.file_size != 0:
|
if item.file_size != 0:
|
||||||
@ -479,11 +605,11 @@ if rarsupport:
|
|||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
errMsg = u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(
|
errMsg = u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(
|
||||||
str(e), self.path, tries)
|
str(e), self.path, tries)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
#Success"
|
# Success"
|
||||||
return namelist
|
return namelist
|
||||||
|
|
||||||
raise e
|
raise e
|
||||||
@ -493,24 +619,22 @@ if rarsupport:
|
|||||||
while tries < 7:
|
while tries < 7:
|
||||||
try:
|
try:
|
||||||
tries = tries + 1
|
tries = tries + 1
|
||||||
#rarc = UnRAR2.RarFile( self.path )
|
|
||||||
rarc = rarfile.RarFile(self.path)
|
rarc = rarfile.RarFile(self.path)
|
||||||
#rarc = OpenableRarFile(self.path)
|
|
||||||
|
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
errMsg = u"getRARObj(): [{0}] {1} attempt#{2}".format(
|
errMsg = u"getRARObj(): [{0}] {1} attempt#{2}".format(
|
||||||
str(e), self.path, tries)
|
str(e), self.path, tries)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
#Success"
|
# Success"
|
||||||
return rarc
|
return rarc
|
||||||
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------
|
# ------------------------------------------
|
||||||
# Folder implementation
|
# Folder implementation
|
||||||
class FolderArchiver:
|
class FolderArchiver:
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
@ -531,7 +655,7 @@ class FolderArchiver:
|
|||||||
with open(fname, 'rb') as f:
|
with open(fname, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
f.close()
|
f.close()
|
||||||
except IOError as e:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@ -573,7 +697,7 @@ class FolderArchiver:
|
|||||||
return itemlist
|
return itemlist
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------
|
# ------------------------------------------
|
||||||
# Unknown implementation
|
# Unknown implementation
|
||||||
class UnknownArchiver:
|
class UnknownArchiver:
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
@ -628,13 +752,13 @@ class PdfArchiver:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
class ComicArchive:
|
class ComicArchive:
|
||||||
|
|
||||||
logo_data = None
|
logo_data = None
|
||||||
|
|
||||||
class ArchiveType:
|
class ArchiveType:
|
||||||
Zip, Rar, Folder, Pdf, Unknown = range(5)
|
Zip, Rar, Tar, Folder, Pdf, Unknown = range(6)
|
||||||
|
|
||||||
def __init__(self, path, rar_exe_path=None, default_image_path=None):
|
def __init__(self, path, rar_exe_path=None, default_image_path=None):
|
||||||
self.path = path
|
self.path = path
|
||||||
@ -665,6 +789,10 @@ class ComicArchive:
|
|||||||
self.archive_type = self.ArchiveType.Zip
|
self.archive_type = self.ArchiveType.Zip
|
||||||
self.archiver = ZipArchiver(self.path)
|
self.archiver = ZipArchiver(self.path)
|
||||||
|
|
||||||
|
if self.tarTest():
|
||||||
|
self.archive_type = self.ArchiveType.Tar
|
||||||
|
self.archiver = TarArchiver(self.path)
|
||||||
|
|
||||||
elif self.rarTest():
|
elif self.rarTest():
|
||||||
self.archive_type = self.ArchiveType.Rar
|
self.archive_type = self.ArchiveType.Rar
|
||||||
self.archiver = RarArchiver(
|
self.archiver = RarArchiver(
|
||||||
@ -674,7 +802,6 @@ class ComicArchive:
|
|||||||
self.archiver = PdfArchiver(self.path)
|
self.archiver = PdfArchiver(self.path)
|
||||||
|
|
||||||
if ComicArchive.logo_data is None and self.default_image_path:
|
if ComicArchive.logo_data is None and self.default_image_path:
|
||||||
#fname = ComicTaggerSettings.getGraphic('nocover.png')
|
|
||||||
fname = self.default_image_path
|
fname = self.default_image_path
|
||||||
with open(fname, 'rb') as fd:
|
with open(fname, 'rb') as fd:
|
||||||
ComicArchive.logo_data = fd.read()
|
ComicArchive.logo_data = fd.read()
|
||||||
@ -702,6 +829,9 @@ class ComicArchive:
|
|||||||
def zipTest(self):
|
def zipTest(self):
|
||||||
return zipfile.is_zipfile(self.path)
|
return zipfile.is_zipfile(self.path)
|
||||||
|
|
||||||
|
def tarTest(self):
|
||||||
|
return tarfile.is_tarfile(self.path)
|
||||||
|
|
||||||
def rarTest(self):
|
def rarTest(self):
|
||||||
try:
|
try:
|
||||||
rarc = rarfile.RarFile(self.path)
|
rarc = rarfile.RarFile(self.path)
|
||||||
@ -713,6 +843,9 @@ class ComicArchive:
|
|||||||
def isZip(self):
|
def isZip(self):
|
||||||
return self.archive_type == self.ArchiveType.Zip
|
return self.archive_type == self.ArchiveType.Zip
|
||||||
|
|
||||||
|
def isTar(self):
|
||||||
|
return self.archive_type == self.ArchiveType.Tar
|
||||||
|
|
||||||
def isRar(self):
|
def isRar(self):
|
||||||
return self.archive_type == self.ArchiveType.Rar
|
return self.archive_type == self.ArchiveType.Rar
|
||||||
|
|
||||||
@ -748,11 +881,8 @@ class ComicArchive:
|
|||||||
|
|
||||||
def seemsToBeAComicArchive(self):
|
def seemsToBeAComicArchive(self):
|
||||||
|
|
||||||
# Do we even care about extensions??
|
if ((self.isZip() or self.isRar() or self.isPdf() or self.isTar()
|
||||||
ext = os.path.splitext(self.path)[1].lower()
|
)
|
||||||
|
|
||||||
if ((self.isZip() or self.isRar() or self.isPdf()
|
|
||||||
) #or self.isFolder() )
|
|
||||||
and (self.getNumberOfPages() > 0)):
|
and (self.getNumberOfPages() > 0)):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@ -811,8 +941,8 @@ class ComicArchive:
|
|||||||
try:
|
try:
|
||||||
image_data = self.archiver.readArchiveFile(filename)
|
image_data = self.archiver.readArchiveFile(filename)
|
||||||
except IOError:
|
except IOError:
|
||||||
errMsg = u"Error reading in page. Substituting logo page."
|
errMsg = u"Error reading in page. Substituting logo page."
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
image_data = ComicArchive.logo_data
|
image_data = ComicArchive.logo_data
|
||||||
|
|
||||||
return image_data
|
return image_data
|
||||||
@ -834,11 +964,11 @@ class ComicArchive:
|
|||||||
|
|
||||||
scanner_page_index = None
|
scanner_page_index = None
|
||||||
|
|
||||||
#make a guess at the scanner page
|
# make a guess at the scanner page
|
||||||
name_list = self.getPageNameList()
|
name_list = self.getPageNameList()
|
||||||
count = self.getNumberOfPages()
|
count = self.getNumberOfPages()
|
||||||
|
|
||||||
#too few pages to really know
|
# too few pages to really know
|
||||||
if count < 5:
|
if count < 5:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -870,7 +1000,7 @@ class ComicArchive:
|
|||||||
prefix = os.path.commonprefix(common_length_list)
|
prefix = os.path.commonprefix(common_length_list)
|
||||||
|
|
||||||
if mode_length <= 7 and prefix == "":
|
if mode_length <= 7 and prefix == "":
|
||||||
#probably all numbers
|
# probably all numbers
|
||||||
if len(final_name) > mode_length:
|
if len(final_name) > mode_length:
|
||||||
scanner_page_index = count - 1
|
scanner_page_index = count - 1
|
||||||
|
|
||||||
@ -890,13 +1020,13 @@ class ComicArchive:
|
|||||||
if sort_list:
|
if sort_list:
|
||||||
|
|
||||||
def keyfunc(k):
|
def keyfunc(k):
|
||||||
#hack to account for some weird scanner ID pages
|
# hack to account for some weird scanner ID pages
|
||||||
#basename=os.path.split(k)[1]
|
# basename=os.path.split(k)[1]
|
||||||
#if basename < '0':
|
# if basename < '0':
|
||||||
# k = os.path.join(os.path.split(k)[0], "z" + basename)
|
# k = os.path.join(os.path.split(k)[0], "z" + basename)
|
||||||
return k.lower()
|
return k.lower()
|
||||||
|
|
||||||
files = natsorted(files, key=keyfunc) #, signed=False)
|
files = natsorted(files, key=keyfunc) #, signed=False)
|
||||||
|
|
||||||
# make a sub-list of image files
|
# make a sub-list of image files
|
||||||
self.page_list = []
|
self.page_list = []
|
||||||
@ -927,15 +1057,15 @@ class ComicArchive:
|
|||||||
return self.cbi_md
|
return self.cbi_md
|
||||||
|
|
||||||
def readRawCBI(self):
|
def readRawCBI(self):
|
||||||
if (not self.hasCBI()):
|
if not self.hasCBI():
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
return self.archiver.getArchiveComment()
|
return self.archiver.getArchiveComment()
|
||||||
|
|
||||||
def hasCBI(self):
|
def hasCBI(self):
|
||||||
if self.has_cbi is None:
|
if self.has_cbi is None:
|
||||||
|
|
||||||
#if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
|
# if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ):
|
||||||
if not self.seemsToBeAComicArchive():
|
if not self.seemsToBeAComicArchive():
|
||||||
self.has_cbi = False
|
self.has_cbi = False
|
||||||
else:
|
else:
|
||||||
@ -975,7 +1105,7 @@ class ComicArchive:
|
|||||||
else:
|
else:
|
||||||
self.cix_md = ComicInfoXml().metadataFromString(raw_cix)
|
self.cix_md = ComicInfoXml().metadataFromString(raw_cix)
|
||||||
|
|
||||||
#validate the existing page list (make sure count is correct)
|
# validate the existing page list (make sure count is correct)
|
||||||
if len(self.cix_md.pages) != 0:
|
if len(self.cix_md.pages) != 0:
|
||||||
if len(self.cix_md.pages) != self.getNumberOfPages():
|
if len(self.cix_md.pages) != self.getNumberOfPages():
|
||||||
# pages array doesn't match the actual number of images we're seeing
|
# pages array doesn't match the actual number of images we're seeing
|
||||||
@ -1044,7 +1174,7 @@ class ComicArchive:
|
|||||||
self.comet_md = CoMet().metadataFromString(raw_comet)
|
self.comet_md = CoMet().metadataFromString(raw_comet)
|
||||||
|
|
||||||
self.comet_md.setDefaultPageList(self.getNumberOfPages())
|
self.comet_md.setDefaultPageList(self.getNumberOfPages())
|
||||||
#use the coverImage value from the comet_data to mark the cover in this struct
|
# 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
|
# walk through list of images in file, and find the matching one for md.coverImage
|
||||||
# need to remove the existing one in the default
|
# need to remove the existing one in the default
|
||||||
if self.comet_md.coverImage is not None:
|
if self.comet_md.coverImage is not None:
|
||||||
@ -1063,14 +1193,14 @@ class ComicArchive:
|
|||||||
def readRawCoMet(self):
|
def readRawCoMet(self):
|
||||||
if not self.hasCoMet():
|
if not self.hasCoMet():
|
||||||
errMsg = u"{} doesn't have CoMet data!".format(self.path)
|
errMsg = u"{} doesn't have CoMet data!".format(self.path)
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw_comet = self.archiver.readArchiveFile(self.comet_filename)
|
raw_comet = self.archiver.readArchiveFile(self.comet_filename)
|
||||||
except IOError:
|
except IOError:
|
||||||
errMsg = u"Error reading in raw CoMet!"
|
errMsg = u"Error reading in raw CoMet!"
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
raw_comet = ""
|
raw_comet = ""
|
||||||
return raw_comet
|
return raw_comet
|
||||||
|
|
||||||
@ -1114,7 +1244,7 @@ class ComicArchive:
|
|||||||
if not self.seemsToBeAComicArchive():
|
if not self.seemsToBeAComicArchive():
|
||||||
return self.has_comet
|
return self.has_comet
|
||||||
|
|
||||||
#look at all xml files in root, and search for CoMet data, get first
|
# look at all xml files in root, and search for CoMet data, get first
|
||||||
for n in self.archiver.getArchiveFilenameList():
|
for n in self.archiver.getArchiveFilenameList():
|
||||||
if (os.path.dirname(n) == ""
|
if (os.path.dirname(n) == ""
|
||||||
and os.path.splitext(n)[1].lower() == '.xml'):
|
and os.path.splitext(n)[1].lower() == '.xml'):
|
||||||
@ -1124,7 +1254,7 @@ class ComicArchive:
|
|||||||
except:
|
except:
|
||||||
data = ""
|
data = ""
|
||||||
errMsg = u"Error reading in Comet XML for validation!"
|
errMsg = u"Error reading in Comet XML for validation!"
|
||||||
sys.stderr.buffer.write(bytes(errMsg, "UTF-8"))
|
logger.info(errMsg)
|
||||||
if CoMet().validateString(data):
|
if CoMet().validateString(data):
|
||||||
# since we found it, save it!
|
# since we found it, save it!
|
||||||
self.comet_filename = n
|
self.comet_filename = n
|
||||||
@ -1182,11 +1312,3 @@ class ComicArchive:
|
|||||||
metadata.isEmpty = False
|
metadata.isEmpty = False
|
||||||
|
|
||||||
return metadata
|
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)
|
|
||||||
|
21
comicapi/comicbookinfo.py
Executable file → Normal file
21
comicapi/comicbookinfo.py
Executable file → Normal file
@ -7,7 +7,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -18,13 +18,10 @@ limitations under the License.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import zipfile
|
|
||||||
|
|
||||||
from comicapi.genericmetadata import GenericMetadata
|
from comicapi.genericmetadata import GenericMetadata
|
||||||
import comicapi.utils
|
import comicapi.utils
|
||||||
|
|
||||||
#import ctversion
|
|
||||||
|
|
||||||
|
|
||||||
class ComicBookInfo:
|
class ComicBookInfo:
|
||||||
def metadataFromString(self, string):
|
def metadataFromString(self, string):
|
||||||
@ -35,7 +32,7 @@ class ComicBookInfo:
|
|||||||
|
|
||||||
cbi = cbi_container['ComicBookInfo/1.0']
|
cbi = cbi_container['ComicBookInfo/1.0']
|
||||||
|
|
||||||
#helper func
|
# helper func
|
||||||
# If item is not in CBI, return None
|
# If item is not in CBI, return None
|
||||||
def xlate(cbi_entry):
|
def xlate(cbi_entry):
|
||||||
if cbi_entry in cbi:
|
if cbi_entry in cbi:
|
||||||
@ -66,7 +63,7 @@ class ComicBookInfo:
|
|||||||
if metadata.tags is None:
|
if metadata.tags is None:
|
||||||
metadata.tags = []
|
metadata.tags = []
|
||||||
|
|
||||||
#need to massage the language string to be ISO
|
# need to massage the language string to be ISO
|
||||||
if metadata.language is not None:
|
if metadata.language is not None:
|
||||||
# reverse look-up
|
# reverse look-up
|
||||||
pattern = metadata.language
|
pattern = metadata.language
|
||||||
@ -86,7 +83,7 @@ class ComicBookInfo:
|
|||||||
cbi_container = self.createJSONDictionary(metadata)
|
cbi_container = self.createJSONDictionary(metadata)
|
||||||
return json.dumps(cbi_container)
|
return json.dumps(cbi_container)
|
||||||
|
|
||||||
#verify that the string actually contains CBI data in JSON format
|
# verify that the string actually contains CBI data in JSON format
|
||||||
def validateString(self, string):
|
def validateString(self, string):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -94,24 +91,24 @@ class ComicBookInfo:
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return ('ComicBookInfo/1.0' in cbi_container)
|
return 'ComicBookInfo/1.0' in cbi_container
|
||||||
|
|
||||||
def createJSONDictionary(self, metadata):
|
def createJSONDictionary(self, metadata):
|
||||||
|
|
||||||
# Create the dictionary that we will convert to JSON text
|
# Create the dictionary that we will convert to JSON text
|
||||||
cbi = dict()
|
cbi = dict()
|
||||||
cbi_container = {
|
cbi_container = {
|
||||||
'appID': 'ComicTagger/' + '1.0.0', #ctversion.version,
|
'appID': 'ComicTagger/' + '1.0.0', # ctversion.version,
|
||||||
'lastModified': str(datetime.now()),
|
'lastModified': str(datetime.now()),
|
||||||
'ComicBookInfo/1.0': cbi
|
'ComicBookInfo/1.0': cbi
|
||||||
}
|
}
|
||||||
|
|
||||||
#helper func
|
# helper func
|
||||||
def assign(cbi_entry, md_entry):
|
def assign(cbi_entry, md_entry):
|
||||||
if md_entry is not None:
|
if md_entry is not None:
|
||||||
cbi[cbi_entry] = md_entry
|
cbi[cbi_entry] = md_entry
|
||||||
|
|
||||||
#helper func
|
# helper func
|
||||||
def toInt(s):
|
def toInt(s):
|
||||||
i = None
|
i = None
|
||||||
if type(s) in [str, int]:
|
if type(s) in [str, int]:
|
||||||
@ -147,4 +144,4 @@ class ComicBookInfo:
|
|||||||
|
|
||||||
f = open(filename, 'w')
|
f = open(filename, 'w')
|
||||||
f.write(json.dumps(cbi_container, indent=4))
|
f.write(json.dumps(cbi_container, indent=4))
|
||||||
f.close
|
f.close()
|
||||||
|
12
comicapi/comicinfoxml.py
Executable file → Normal file
12
comicapi/comicinfoxml.py
Executable file → Normal file
@ -7,7 +7,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -16,9 +16,6 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
import zipfile
|
|
||||||
from pprint import pprint
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from comicapi.genericmetadata import GenericMetadata
|
from comicapi.genericmetadata import GenericMetadata
|
||||||
import comicapi.utils
|
import comicapi.utils
|
||||||
@ -75,7 +72,7 @@ class ComicInfoXml:
|
|||||||
|
|
||||||
def convertMetadataToXML(self, filename, metadata):
|
def convertMetadataToXML(self, filename, metadata):
|
||||||
|
|
||||||
#shorthand for the metadata
|
# shorthand for the metadata
|
||||||
md = metadata
|
md = metadata
|
||||||
|
|
||||||
# build a tree structure
|
# build a tree structure
|
||||||
@ -83,7 +80,7 @@ class ComicInfoXml:
|
|||||||
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
|
root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
|
||||||
|
|
||||||
#helper func
|
# helper func
|
||||||
def assign(cix_entry, md_entry):
|
def assign(cix_entry, md_entry):
|
||||||
if md_entry is not None:
|
if md_entry is not None:
|
||||||
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
|
ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry)
|
||||||
@ -267,7 +264,7 @@ class ComicInfoXml:
|
|||||||
if pages_node is not None:
|
if pages_node is not None:
|
||||||
for page in pages_node:
|
for page in pages_node:
|
||||||
metadata.pages.append(page.attrib)
|
metadata.pages.append(page.attrib)
|
||||||
#print page.attrib
|
# print page.attrib
|
||||||
|
|
||||||
metadata.isEmpty = False
|
metadata.isEmpty = False
|
||||||
|
|
||||||
@ -276,7 +273,6 @@ class ComicInfoXml:
|
|||||||
def writeToExternalFile(self, filename, metadata):
|
def writeToExternalFile(self, filename, metadata):
|
||||||
|
|
||||||
tree = self.convertMetadataToXML(self, metadata)
|
tree = self.convertMetadataToXML(self, metadata)
|
||||||
#ET.dump(tree)
|
|
||||||
tree.write(filename, encoding='utf-8')
|
tree.write(filename, encoding='utf-8')
|
||||||
|
|
||||||
def readFromExternalFile(self, filename):
|
def readFromExternalFile(self, filename):
|
||||||
|
17
comicapi/filenameparser.py
Executable file → Normal file
17
comicapi/filenameparser.py
Executable file → Normal file
@ -9,7 +9,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -37,7 +37,7 @@ class FileNameParser:
|
|||||||
placeholders = ['[_]', ' +']
|
placeholders = ['[_]', ' +']
|
||||||
for ph in placeholders:
|
for ph in placeholders:
|
||||||
string = re.sub(ph, self.repl, string)
|
string = re.sub(ph, self.repl, string)
|
||||||
return string #.strip()
|
return string
|
||||||
|
|
||||||
def getIssueCount(self, filename, issue_end):
|
def getIssueCount(self, filename, issue_end):
|
||||||
|
|
||||||
@ -57,7 +57,6 @@ class FileNameParser:
|
|||||||
match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE)
|
match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE)
|
||||||
if match:
|
if match:
|
||||||
count = match.group()
|
count = match.group()
|
||||||
found = True
|
|
||||||
|
|
||||||
count = count.lstrip("0")
|
count = count.lstrip("0")
|
||||||
|
|
||||||
@ -94,8 +93,6 @@ class FileNameParser:
|
|||||||
# remove any "of NN" phrase with spaces (problem: this could break on some titles)
|
# remove any "of NN" phrase with spaces (problem: this could break on some titles)
|
||||||
filename = re.sub("of [\d]+", self.repl, filename)
|
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
|
# we should now have a cleaned up filename version with all the words in
|
||||||
# the same positions as original filename
|
# the same positions as original filename
|
||||||
|
|
||||||
@ -108,7 +105,7 @@ class FileNameParser:
|
|||||||
if len(word_list) > 1:
|
if len(word_list) > 1:
|
||||||
word_list = word_list[1:]
|
word_list = word_list[1:]
|
||||||
else:
|
else:
|
||||||
#only one word?? just bail.
|
# only one word?? just bail.
|
||||||
return issue, start, end
|
return issue, start, end
|
||||||
|
|
||||||
# Now try to search for the likely issue number word in the list
|
# Now try to search for the likely issue number word in the list
|
||||||
@ -164,7 +161,7 @@ class FileNameParser:
|
|||||||
series = tmpstr
|
series = tmpstr
|
||||||
volume = ""
|
volume = ""
|
||||||
|
|
||||||
#save the last word
|
# save the last word
|
||||||
try:
|
try:
|
||||||
last_word = series.split()[-1]
|
last_word = series.split()[-1]
|
||||||
except:
|
except:
|
||||||
@ -182,7 +179,7 @@ class FileNameParser:
|
|||||||
# if a volume wasn't found, see if the last word is a year in parentheses
|
# 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
|
# since that's a common way to designate the volume
|
||||||
if volume == "":
|
if volume == "":
|
||||||
#match either (YEAR), (YEAR-), or (YEAR-YEAR2)
|
# match either (YEAR), (YEAR-), or (YEAR-YEAR2)
|
||||||
match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word)
|
match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word)
|
||||||
if match:
|
if match:
|
||||||
volume = match.group(2)
|
volume = match.group(2)
|
||||||
@ -218,7 +215,7 @@ class FileNameParser:
|
|||||||
|
|
||||||
def getRemainder(self, filename, year, count, issue_end):
|
def getRemainder(self, filename, year, count, issue_end):
|
||||||
|
|
||||||
#make a guess at where the the non-interesting stuff begins
|
# make a guess at where the the non-interesting stuff begins
|
||||||
remainder = ""
|
remainder = ""
|
||||||
|
|
||||||
if "--" in filename:
|
if "--" in filename:
|
||||||
@ -246,7 +243,7 @@ class FileNameParser:
|
|||||||
# remove the extension
|
# remove the extension
|
||||||
filename = os.path.splitext(filename)[0]
|
filename = os.path.splitext(filename)[0]
|
||||||
|
|
||||||
#url decode, just in case
|
# url decode, just in case
|
||||||
filename = unquote(filename)
|
filename = unquote(filename)
|
||||||
|
|
||||||
# sometimes archives get messed up names from too many decodings
|
# sometimes archives get messed up names from too many decodings
|
||||||
|
20
comicapi/genericmetadata.py
Executable file → Normal file
20
comicapi/genericmetadata.py
Executable file → Normal file
@ -11,7 +11,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -38,18 +38,6 @@ class PageType:
|
|||||||
Deleted = "Deleted"
|
Deleted = "Deleted"
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
class PageInfo:
|
|
||||||
Image = 0
|
|
||||||
Type = PageType.Story
|
|
||||||
DoublePage = False
|
|
||||||
ImageSize = 0
|
|
||||||
Key = ""
|
|
||||||
ImageWidth = 0
|
|
||||||
ImageHeight = 0
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class GenericMetadata:
|
class GenericMetadata:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@ -174,8 +162,8 @@ class GenericMetadata:
|
|||||||
|
|
||||||
def overlayCredits(self, new_credits):
|
def overlayCredits(self, new_credits):
|
||||||
for c in new_credits:
|
for c in new_credits:
|
||||||
if 'primary' in c:
|
|
||||||
# if c.has_key('primary') and c['primary']:
|
# if c.has_key('primary') and c['primary']:
|
||||||
|
if 'primary' in c:
|
||||||
primary = True
|
primary = True
|
||||||
else:
|
else:
|
||||||
primary = False
|
primary = False
|
||||||
@ -295,8 +283,8 @@ class GenericMetadata:
|
|||||||
|
|
||||||
for c in self.credits:
|
for c in self.credits:
|
||||||
primary = ""
|
primary = ""
|
||||||
if 'primary' in c:
|
|
||||||
# if c.has_key('primary') and c['primary']:
|
# if c.has_key('primary') and c['primary']:
|
||||||
|
if 'primary' in c:
|
||||||
primary = " [P]"
|
primary = " [P]"
|
||||||
add_string("credit", c['role'] + ": " + c['person'] + primary)
|
add_string("credit", c['role'] + ": " + c['person'] + primary)
|
||||||
|
|
||||||
@ -306,7 +294,7 @@ class GenericMetadata:
|
|||||||
flen = max(flen, len(i[0]))
|
flen = max(flen, len(i[0]))
|
||||||
flen += 1
|
flen += 1
|
||||||
|
|
||||||
#format the data nicely
|
# format the data nicely
|
||||||
outstr = ""
|
outstr = ""
|
||||||
fmt_str = u"{0: <" + str(flen) + "} {1}\n"
|
fmt_str = u"{0: <" + str(flen) + "} {1}\n"
|
||||||
for i in vals:
|
for i in vals:
|
||||||
|
16
comicapi/issuestring.py
Executable file → Normal file
16
comicapi/issuestring.py
Executable file → Normal file
@ -17,7 +17,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -26,10 +26,6 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import comicapi.utils
|
|
||||||
import math
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class IssueString:
|
class IssueString:
|
||||||
def __init__(self, text):
|
def __init__(self, text):
|
||||||
@ -49,7 +45,7 @@ class IssueString:
|
|||||||
if len(text) == 0:
|
if len(text) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
#skip the minus sign if it's first
|
# skip the minus sign if it's first
|
||||||
if text[0] == '-':
|
if text[0] == '-':
|
||||||
start = 1
|
start = 1
|
||||||
else:
|
else:
|
||||||
@ -88,10 +84,8 @@ class IssueString:
|
|||||||
else:
|
else:
|
||||||
self.suffix = text
|
self.suffix = text
|
||||||
|
|
||||||
#print "num: {0} suf: {1}".format(self.num, self.suffix)
|
|
||||||
|
|
||||||
def asString(self, pad=0):
|
def asString(self, pad=0):
|
||||||
#return the float, left side zero-padded, with suffix attached
|
# return the float, left side zero-padded, with suffix attached
|
||||||
if self.num is None:
|
if self.num is None:
|
||||||
return self.suffix
|
return self.suffix
|
||||||
|
|
||||||
@ -119,7 +113,7 @@ class IssueString:
|
|||||||
return num_s
|
return num_s
|
||||||
|
|
||||||
def asFloat(self):
|
def asFloat(self):
|
||||||
#return the float, with no suffix
|
# return the float, with no suffix
|
||||||
if self.suffix == u"½":
|
if self.suffix == u"½":
|
||||||
if self.num is not None:
|
if self.num is not None:
|
||||||
return self.num + .5
|
return self.num + .5
|
||||||
@ -128,7 +122,7 @@ class IssueString:
|
|||||||
return self.num
|
return self.num
|
||||||
|
|
||||||
def asInt(self):
|
def asInt(self):
|
||||||
#return the int version of the float
|
# return the int version of the float
|
||||||
if self.num is None:
|
if self.num is None:
|
||||||
return None
|
return None
|
||||||
return int(self.num)
|
return int(self.num)
|
||||||
|
54
comicapi/utils.py
Executable file → Normal file
54
comicapi/utils.py
Executable file → Normal file
@ -9,7 +9,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -28,13 +28,15 @@ import codecs
|
|||||||
class UtilsVars:
|
class UtilsVars:
|
||||||
already_fixed_encoding = False
|
already_fixed_encoding = False
|
||||||
|
|
||||||
|
|
||||||
def get_actual_preferred_encoding():
|
def get_actual_preferred_encoding():
|
||||||
preferred_encoding = locale.getpreferredencoding()
|
preferred_encoding = locale.getpreferredencoding()
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
preferred_encoding = "utf-8"
|
preferred_encoding = "utf-8"
|
||||||
return preferred_encoding
|
return preferred_encoding
|
||||||
|
|
||||||
def fix_output_encoding( ):
|
|
||||||
|
def fix_output_encoding():
|
||||||
if not UtilsVars.already_fixed_encoding:
|
if not UtilsVars.already_fixed_encoding:
|
||||||
# this reads the environment and inits the right locale
|
# this reads the environment and inits the right locale
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
@ -45,37 +47,39 @@ def fix_output_encoding( ):
|
|||||||
sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr)
|
sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr)
|
||||||
UtilsVars.already_fixed_encoding = True
|
UtilsVars.already_fixed_encoding = True
|
||||||
|
|
||||||
def get_recursive_filelist( pathlist ):
|
|
||||||
|
def get_recursive_filelist(pathlist):
|
||||||
|
"""
|
||||||
|
Get a recursive list of of all files under all path items in the list
|
||||||
"""
|
"""
|
||||||
Get a recursive list of of all files under all path items in the list
|
|
||||||
"""
|
|
||||||
filename_encoding = sys.getfilesystemencoding()
|
filename_encoding = sys.getfilesystemencoding()
|
||||||
filelist = []
|
filelist = []
|
||||||
for p in pathlist:
|
for p in pathlist:
|
||||||
# if path is a folder, walk it recursivly, and all files underneath
|
# if path is a folder, walk it recursivly, and all files underneath
|
||||||
if type(p) == str:
|
if type(p) == str:
|
||||||
#make sure string is unicode
|
# make sure string is unicode
|
||||||
p = p.decode(filename_encoding) #, 'replace')
|
p = p.decode(filename_encoding)
|
||||||
elif type(p) != str:
|
elif type(p) != str:
|
||||||
#it's probably a QString
|
# it's probably a QString
|
||||||
p = str(p)
|
p = str(p)
|
||||||
|
|
||||||
if os.path.isdir( p ):
|
if os.path.isdir(p):
|
||||||
for root,dirs,files in os.walk( p ):
|
for root, dirs, files in os.walk(p):
|
||||||
for f in files:
|
for f in files:
|
||||||
if type(f) == str:
|
if type(f) == str:
|
||||||
#make sure string is unicode
|
# make sure string is unicode
|
||||||
f = f.decode(filename_encoding, 'replace')
|
f = f.decode(filename_encoding, 'replace')
|
||||||
elif type(f) != str:
|
elif type(f) != str:
|
||||||
#it's probably a QString
|
# it's probably a QString
|
||||||
f = str(f)
|
f = str(f)
|
||||||
filelist.append(os.path.join(root,f))
|
filelist.append(os.path.join(root, f))
|
||||||
else:
|
else:
|
||||||
filelist.append(p)
|
filelist.append(p)
|
||||||
|
|
||||||
return filelist
|
return filelist
|
||||||
|
|
||||||
def listToString( l ):
|
|
||||||
|
def listToString(l):
|
||||||
string = ""
|
string = ""
|
||||||
if l is not None:
|
if l is not None:
|
||||||
for item in l:
|
for item in l:
|
||||||
@ -84,17 +88,19 @@ def listToString( l ):
|
|||||||
string += item
|
string += item
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def addtopath( dirname ):
|
|
||||||
|
def addtopath(dirname):
|
||||||
if dirname is not None and dirname != "":
|
if dirname is not None and dirname != "":
|
||||||
|
|
||||||
# verify that path doesn't already contain the given dirname
|
# verify that path doesn't already contain the given dirname
|
||||||
tmpdirname = re.escape(dirname)
|
tmpdirname = re.escape(dirname)
|
||||||
pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep)
|
pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format(dir=tmpdirname, sep=os.pathsep)
|
||||||
|
|
||||||
match = re.search(pattern, os.environ['PATH'])
|
match = re.search(pattern, os.environ['PATH'])
|
||||||
if not match:
|
if not match:
|
||||||
os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH']
|
os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH']
|
||||||
|
|
||||||
|
|
||||||
# returns executable path, if it exists
|
# returns executable path, if it exists
|
||||||
def which(program):
|
def which(program):
|
||||||
|
|
||||||
@ -113,9 +119,10 @@ def which(program):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def removearticles( text ):
|
|
||||||
|
def removearticles(text):
|
||||||
text = text.lower()
|
text = text.lower()
|
||||||
articles = ['and', 'the', 'a', '&', 'issue' ]
|
articles = ['and', 'the', 'a', '&', 'issue']
|
||||||
newText = ''
|
newText = ''
|
||||||
for word in text.split(' '):
|
for word in text.split(' '):
|
||||||
if word not in articles:
|
if word not in articles:
|
||||||
@ -131,16 +138,15 @@ def removearticles( text ):
|
|||||||
# since the CV api changed, searches for series names with periods
|
# since the CV api changed, searches for series names with periods
|
||||||
# now explicity require the period to be in the search key,
|
# now explicity require the period to be in the search key,
|
||||||
# so the line below is removed (for now)
|
# so the line below is removed (for now)
|
||||||
#newText = newText.replace(".", "")
|
|
||||||
|
|
||||||
return newText
|
return newText
|
||||||
|
|
||||||
|
|
||||||
def unique_file(file_name):
|
def unique_file(file_name):
|
||||||
counter = 1
|
counter = 1
|
||||||
file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext')
|
file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext')
|
||||||
while 1:
|
while 1:
|
||||||
if not os.path.lexists( file_name):
|
if not os.path.lexists(file_name):
|
||||||
return file_name
|
return file_name
|
||||||
file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1]
|
file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1]
|
||||||
counter += 1
|
counter += 1
|
||||||
@ -573,12 +579,12 @@ countries = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def getLanguageDict():
|
def getLanguageDict():
|
||||||
return lang_dict
|
return lang_dict
|
||||||
|
|
||||||
def getLanguageFromISO( iso ):
|
|
||||||
|
def getLanguageFromISO(iso):
|
||||||
if iso == None:
|
if iso == None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return lang_dict[ iso ]
|
return lang_dict[iso]
|
||||||
|
4
setup.py
4
setup.py
@ -1,8 +1,8 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
setup(
|
setup(
|
||||||
name = 'comicapi',
|
name = 'comicapi',
|
||||||
version = '2.1',
|
version = '2.1.1',
|
||||||
description = 'Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project.',
|
description = 'Comic archive (cbr/cbz/cbt) and metadata utilities. Extracted from the comictagger project.',
|
||||||
author = 'Iris W',
|
author = 'Iris W',
|
||||||
packages = ['comicapi'],
|
packages = ['comicapi'],
|
||||||
install_requires = ['natsort>=3.5.2'],
|
install_requires = ['natsort>=3.5.2'],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user