Compare commits
2 Commits
1.3.1
...
seriesYear
Author | SHA1 | Date | |
---|---|---|---|
50c53e14da | |||
2e01a4db14 |
@ -14,7 +14,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import py7zr
|
||||
import zipfile
|
||||
import os
|
||||
import struct
|
||||
@ -26,6 +25,7 @@ import time
|
||||
import io
|
||||
|
||||
import natsort
|
||||
from PyPDF2 import PdfFileReader
|
||||
try:
|
||||
from unrar.cffi import rarfile
|
||||
except:
|
||||
@ -51,109 +51,6 @@ class MetaDataStyle:
|
||||
COMET = 2
|
||||
name = ['ComicBookLover', 'ComicRack', 'CoMet']
|
||||
|
||||
class SevenZipArchiver:
|
||||
|
||||
"""7Z implementation"""
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
# @todo: Implement Comment?
|
||||
def getArchiveComment(self):
|
||||
return ""
|
||||
|
||||
def setArchiveComment(self, comment):
|
||||
return False
|
||||
|
||||
def readArchiveFile(self, archive_file):
|
||||
data = ""
|
||||
try:
|
||||
with py7zr.SevenZipFile(self.path, 'r') as zf:
|
||||
data = zf.read(archive_file)[archive_file].read()
|
||||
except py7zr.Bad7zFile as e:
|
||||
print("bad 7zip file [{0}]: {1} :: {2}".format(e, self.path,
|
||||
archive_file), file=sys.stderr)
|
||||
raise IOError
|
||||
except Exception as e:
|
||||
print("bad 7zip file [{0}]: {1} :: {2}".format(e, self.path,
|
||||
archive_file), file=sys.stderr)
|
||||
raise IOError
|
||||
|
||||
return data
|
||||
|
||||
def removeArchiveFile(self, archive_file):
|
||||
try:
|
||||
self.rebuildSevenZipFile([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:
|
||||
files = self.getArchiveFilenameList()
|
||||
if archive_file in files:
|
||||
self.rebuildSevenZipFile([archive_file])
|
||||
|
||||
# now just add the archive file as a new one
|
||||
with py7zr.SevenZipFile(self.path, 'a') as zf:
|
||||
zf.writestr(data, archive_file)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def getArchiveFilenameList(self):
|
||||
try:
|
||||
with py7zr.SevenZipFile(self.path, 'r') as zf:
|
||||
namelist = zf.getnames()
|
||||
|
||||
return namelist
|
||||
except Exception as e:
|
||||
print("Unable to get zipfile list [{0}]: {1}".format(
|
||||
e, self.path), file=sys.stderr)
|
||||
return []
|
||||
|
||||
def rebuildSevenZipFile(self, exclude_list):
|
||||
"""Zip helper func
|
||||
|
||||
This recompresses the zip archive, without the files in the exclude_list
|
||||
"""
|
||||
tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(self.path))
|
||||
os.close(tmp_fd)
|
||||
|
||||
try:
|
||||
with py7zr.SevenZipFile(self.path, 'r') as zip:
|
||||
targets = [f for f in zip.getnames() if f not in exclude_list]
|
||||
with py7zr.SevenZipFile(self.path, 'r') as zin:
|
||||
with py7zr.SevenZipFile(tmp_name, 'w') as zout:
|
||||
for fname, bio in zin.read(targets).items():
|
||||
zout.writef(bio, fname)
|
||||
except Exception as e:
|
||||
print("Exception[{0}]: {1}".format(e, self.path))
|
||||
return []
|
||||
|
||||
# replace with the new file
|
||||
os.remove(self.path)
|
||||
os.rename(tmp_name, self.path)
|
||||
|
||||
def copyFromArchive(self, otherArchive):
|
||||
"""Replace the current zip with one copied from another archive"""
|
||||
try:
|
||||
with py7zr.SevenZipFile(self.path, 'w') as zout:
|
||||
for fname in otherArchive.getArchiveFilenameList():
|
||||
data = otherArchive.readArchiveFile(fname)
|
||||
if data is not None:
|
||||
zout.writestr(data, fname)
|
||||
except Exception as e:
|
||||
print("Error while copying to {0}: {1}".format(
|
||||
self.path, e), file=sys.stderr)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
class ZipArchiver:
|
||||
|
||||
"""ZIP implementation"""
|
||||
@ -205,9 +102,7 @@ class ZipArchiver:
|
||||
# zip archive w/o the indicated file. Very sucky, but maybe
|
||||
# another solution can be found
|
||||
try:
|
||||
files = self.getArchiveFilenameList()
|
||||
if archive_file in files:
|
||||
self.rebuildZipFile([archive_file])
|
||||
self.rebuildZipFile([archive_file])
|
||||
|
||||
# now just add the archive file as a new one
|
||||
zf = zipfile.ZipFile(
|
||||
@ -279,7 +174,7 @@ class ZipArchiver:
|
||||
|
||||
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
|
||||
@ -376,7 +271,7 @@ class RarArchiver:
|
||||
f.close()
|
||||
|
||||
working_dir = os.path.dirname(os.path.abspath(self.path))
|
||||
|
||||
|
||||
# use external program to write comment to Rar archive
|
||||
proc_args = [self.rar_exe_path,
|
||||
'c',
|
||||
@ -410,7 +305,7 @@ class RarArchiver:
|
||||
while tries < 7:
|
||||
try:
|
||||
tries = tries + 1
|
||||
data = rarc.open(archive_file).read()
|
||||
data = rarc.open(archive_file).read()
|
||||
entries = [(rarc.getinfo(archive_file), data)]
|
||||
|
||||
if entries[0][0].file_size != len(entries[0][1]):
|
||||
@ -633,10 +528,38 @@ class UnknownArchiver:
|
||||
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:
|
||||
SevenZip, Zip, Rar, Folder, Pdf, Unknown = list(range(6))
|
||||
Zip, Rar, Folder, Pdf, Unknown = list(range(5))
|
||||
|
||||
def __init__(self, path, rar_exe_path=None, default_image_path=None):
|
||||
self.path = path
|
||||
@ -664,11 +587,7 @@ class ComicArchive:
|
||||
self.archive_type = self.ArchiveType.Zip
|
||||
self.archiver = ZipArchiver(self.path)
|
||||
else:
|
||||
if self.sevenZipTest():
|
||||
self.archive_type = self.ArchiveType.SevenZip
|
||||
self.archiver = SevenZipArchiver(self.path)
|
||||
|
||||
elif self.zipTest():
|
||||
if self.zipTest():
|
||||
self.archive_type = self.ArchiveType.Zip
|
||||
self.archiver = ZipArchiver(self.path)
|
||||
|
||||
@ -677,6 +596,9 @@ class ComicArchive:
|
||||
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')
|
||||
@ -705,9 +627,6 @@ class ComicArchive:
|
||||
self.path = path
|
||||
self.archiver.path = path
|
||||
|
||||
def sevenZipTest(self):
|
||||
return py7zr.is_7zfile(self.path)
|
||||
|
||||
def zipTest(self):
|
||||
return zipfile.is_zipfile(self.path)
|
||||
|
||||
@ -717,9 +636,6 @@ class ComicArchive:
|
||||
except:
|
||||
return False
|
||||
|
||||
def isSevenZip(self):
|
||||
return self.archive_type == self.ArchiveType.SevenZip
|
||||
|
||||
def isZip(self):
|
||||
return self.archive_type == self.ArchiveType.Zip
|
||||
|
||||
@ -761,7 +677,7 @@ class ComicArchive:
|
||||
|
||||
if (
|
||||
# or self.isFolder() )
|
||||
(self.isSevenZip() or self.isZip() or self.isRar())
|
||||
(self.isZip() or self.isRar() or self.isPdf())
|
||||
and
|
||||
(self.getNumberOfPages() > 0)
|
||||
|
||||
@ -902,7 +818,7 @@ class ComicArchive:
|
||||
def keyfunc(k):
|
||||
return k.lower()
|
||||
|
||||
files = natsort.natsorted(files, alg=natsort.ns.IC | natsort.ns.I | natsort.ns.U)
|
||||
files = natsort.natsorted(files, alg=natsort.ns.IC | natsort.ns.I)
|
||||
|
||||
# make a sub-list of image files
|
||||
self.page_list = []
|
||||
@ -942,7 +858,7 @@ class ComicArchive:
|
||||
def hasCBI(self):
|
||||
if self.has_cbi is None:
|
||||
|
||||
# if ( not (self.isSevenZip() or self.isZip() or self.isRar()) or not
|
||||
# if ( not ( self.isZip() or self.isRar()) or not
|
||||
# self.seemsToBeAComicArchive() ):
|
||||
if not self.seemsToBeAComicArchive():
|
||||
self.has_cbi = False
|
||||
@ -1008,10 +924,7 @@ class ComicArchive:
|
||||
def writeCIX(self, metadata):
|
||||
if metadata is not None:
|
||||
self.applyArchiveInfoToMetadata(metadata, calc_page_sizes=True)
|
||||
rawCIX = self.readRawCIX()
|
||||
if rawCIX == "":
|
||||
rawCIX = None
|
||||
cix_string = ComicInfoXml().stringFromMetadata(metadata, xml=rawCIX)
|
||||
cix_string = ComicInfoXml().stringFromMetadata(metadata)
|
||||
write_success = self.archiver.writeArchiveFile(
|
||||
self.ci_xml_filename,
|
||||
cix_string)
|
||||
|
@ -40,6 +40,7 @@ class ComicBookInfo:
|
||||
metadata.publisher = utils.xlate(cbi['publisher'])
|
||||
metadata.month = utils.xlate(cbi['publicationMonth'], True)
|
||||
metadata.year = utils.xlate(cbi['publicationYear'], True)
|
||||
metadata.seriesYear = utils.xlate('seriesPublicationYear', True)
|
||||
metadata.issueCount = utils.xlate(cbi['numberOfIssues'], True)
|
||||
metadata.comments = utils.xlate(cbi['comments'])
|
||||
metadata.genre = utils.xlate(cbi['genre'])
|
||||
@ -106,6 +107,7 @@ class ComicBookInfo:
|
||||
assign('publisher', utils.xlate(metadata.publisher))
|
||||
assign('publicationMonth', utils.xlate(metadata.month, True))
|
||||
assign('publicationYear', utils.xlate(metadata.year, True))
|
||||
assign('seriesPublicationYear', utils.xlate(metadata.seriesYear, True))
|
||||
assign('numberOfIssues', utils.xlate(metadata.issueCount, True))
|
||||
assign('comments', utils.xlate(metadata.comments))
|
||||
assign('genre', utils.xlate(metadata.genre))
|
||||
|
@ -50,10 +50,13 @@ class ComicInfoXml:
|
||||
tree = ET.ElementTree(ET.fromstring(string))
|
||||
return self.convertXMLToMetadata(tree)
|
||||
|
||||
def stringFromMetadata(self, metadata, xml=None):
|
||||
tree = self.convertMetadataToXML(self, metadata, xml)
|
||||
tree_str = ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True).decode()
|
||||
return tree_str
|
||||
def stringFromMetadata(self, metadata):
|
||||
|
||||
header = '<?xml version="1.0"?>\n'
|
||||
|
||||
tree = self.convertMetadataToXML(self, metadata)
|
||||
tree_str = ET.tostring(tree.getroot()).decode()
|
||||
return header + tree_str
|
||||
|
||||
def indent(self, elem, level=0):
|
||||
# for making the XML output readable
|
||||
@ -71,27 +74,20 @@ class ComicInfoXml:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
def convertMetadataToXML(self, filename, metadata, xml=None):
|
||||
def convertMetadataToXML(self, filename, metadata):
|
||||
|
||||
# shorthand for the metadata
|
||||
md = metadata
|
||||
|
||||
if xml:
|
||||
root = ET.ElementTree(ET.fromstring(xml)).getroot()
|
||||
else:
|
||||
# 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"
|
||||
# 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_entry = root.find(cix_entry)
|
||||
if et_entry is not None:
|
||||
et_entry.text = "{0}".format(md_entry)
|
||||
else:
|
||||
ET.SubElement(root, cix_entry).text = "{0}".format(md_entry)
|
||||
ET.SubElement(root, cix_entry).text = "{0}".format(md_entry)
|
||||
|
||||
assign('Title', md.title)
|
||||
assign('Series', md.series)
|
||||
@ -108,6 +104,7 @@ class ComicInfoXml:
|
||||
assign('Year', md.year)
|
||||
assign('Month', md.month)
|
||||
assign('Day', md.day)
|
||||
assign('SeriesYear', md.seriesYear)
|
||||
|
||||
# need to specially process the credits, since they are structured
|
||||
# differently than CIX
|
||||
@ -145,19 +142,33 @@ class ComicInfoXml:
|
||||
credit_editor_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
# second, convert each list to string, and add to XML struct
|
||||
assign('Writer', utils.listToString(credit_writer_list))
|
||||
if len(credit_writer_list) > 0:
|
||||
node = ET.SubElement(root, 'Writer')
|
||||
node.text = utils.listToString(credit_writer_list)
|
||||
|
||||
assign('Penciller', utils.listToString(credit_penciller_list))
|
||||
if len(credit_penciller_list) > 0:
|
||||
node = ET.SubElement(root, 'Penciller')
|
||||
node.text = utils.listToString(credit_penciller_list)
|
||||
|
||||
assign('Inker', utils.listToString(credit_inker_list))
|
||||
if len(credit_inker_list) > 0:
|
||||
node = ET.SubElement(root, 'Inker')
|
||||
node.text = utils.listToString(credit_inker_list)
|
||||
|
||||
assign('Colorist', utils.listToString(credit_colorist_list))
|
||||
if len(credit_colorist_list) > 0:
|
||||
node = ET.SubElement(root, 'Colorist')
|
||||
node.text = utils.listToString(credit_colorist_list)
|
||||
|
||||
assign('Letterer', utils.listToString(credit_letterer_list))
|
||||
if len(credit_letterer_list) > 0:
|
||||
node = ET.SubElement(root, 'Letterer')
|
||||
node.text = utils.listToString(credit_letterer_list)
|
||||
|
||||
assign('CoverArtist', utils.listToString(credit_cover_list))
|
||||
if len(credit_cover_list) > 0:
|
||||
node = ET.SubElement(root, 'CoverArtist')
|
||||
node.text = utils.listToString(credit_cover_list)
|
||||
|
||||
assign('Editor', utils.listToString(credit_editor_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)
|
||||
@ -168,7 +179,7 @@ class ComicInfoXml:
|
||||
assign('Format', md.format)
|
||||
assign('AgeRating', md.maturityRating)
|
||||
if md.blackAndWhite is not None and md.blackAndWhite:
|
||||
assign('BlackAndWhite', "Yes")
|
||||
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
|
||||
assign('Manga', md.manga)
|
||||
assign('Characters', md.characters)
|
||||
assign('Teams', md.teams)
|
||||
@ -176,15 +187,11 @@ class ComicInfoXml:
|
||||
assign('ScanInformation', md.scanInfo)
|
||||
|
||||
# loop and add the page entries under pages node
|
||||
pages_node = root.find('Pages')
|
||||
if pages_node is not None:
|
||||
pages_node.clear()
|
||||
else:
|
||||
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
|
||||
for page_dict in md.pages:
|
||||
page_node = ET.SubElement(pages_node, 'Page')
|
||||
page_node.attrib = page_dict
|
||||
|
||||
# self pretty-print
|
||||
self.indent(root)
|
||||
@ -222,6 +229,7 @@ class ComicInfoXml:
|
||||
md.year = utils.xlate(get('Year'), True)
|
||||
md.month = utils.xlate(get('Month'), True)
|
||||
md.day = utils.xlate(get('Day'), True)
|
||||
md.seriesYear = utils.xlate(get('SeriesYear'), True)
|
||||
md.publisher = utils.xlate(get('Publisher'))
|
||||
md.imprint = utils.xlate(get('Imprint'))
|
||||
md.genre = utils.xlate(get('Genre'))
|
||||
@ -270,11 +278,11 @@ class ComicInfoXml:
|
||||
|
||||
return md
|
||||
|
||||
def writeToExternalFile(self, filename, metadata, xml=None):
|
||||
def writeToExternalFile(self, filename, metadata):
|
||||
|
||||
tree = self.convertMetadataToXML(self, metadata, xml)
|
||||
tree = self.convertMetadataToXML(self, metadata)
|
||||
# ET.dump(tree)
|
||||
tree.write(filename, encoding="utf-8", xml_declaration=True)
|
||||
tree.write(filename, encoding='utf-8')
|
||||
|
||||
def readFromExternalFile(self, filename):
|
||||
|
||||
|
@ -65,6 +65,7 @@ class GenericMetadata:
|
||||
self.issue = None
|
||||
self.title = None
|
||||
self.publisher = None
|
||||
self.seriesYear = None
|
||||
self.month = None
|
||||
self.year = None
|
||||
self.day = None
|
||||
@ -132,6 +133,7 @@ class GenericMetadata:
|
||||
assign("issueCount", new_md.issueCount)
|
||||
assign("title", new_md.title)
|
||||
assign("publisher", new_md.publisher)
|
||||
assign("seriesYear", new_md.seriesYear)
|
||||
assign("day", new_md.day)
|
||||
assign("month", new_md.month)
|
||||
assign("year", new_md.year)
|
||||
@ -263,6 +265,7 @@ class GenericMetadata:
|
||||
add_attr_string("issueCount")
|
||||
add_attr_string("title")
|
||||
add_attr_string("publisher")
|
||||
add_attr_string("seriesYear")
|
||||
add_attr_string("year")
|
||||
add_attr_string("month")
|
||||
add_attr_string("day")
|
||||
|
@ -38,7 +38,8 @@ class IssueString:
|
||||
if text is None:
|
||||
return
|
||||
|
||||
text = str(text)
|
||||
if isinstance(text, int):
|
||||
text = str(text)
|
||||
|
||||
if len(text) == 0:
|
||||
return
|
||||
|
@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
import localefix
|
||||
from comictaggerlib.main import ctmain
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -258,9 +258,7 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
if batch_mode:
|
||||
brief = "{0}: ".format(filename)
|
||||
|
||||
if ca.isSevenZip():
|
||||
brief += "7Z archive "
|
||||
elif ca.isZip():
|
||||
if ca.isZip():
|
||||
brief += "ZIP archive "
|
||||
elif ca.isRar():
|
||||
brief += "RAR archive "
|
||||
@ -500,9 +498,7 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
|
||||
new_ext = None # default
|
||||
if settings.rename_extension_based_on_archive:
|
||||
if ca.isSevenZip():
|
||||
new_ext = ".cb7"
|
||||
elif ca.isZip():
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
@ -511,19 +507,31 @@ def process_file_cli(filename, opts, settings, match_results):
|
||||
renamer.setTemplate(settings.rename_template)
|
||||
renamer.setIssueZeroPadding(settings.rename_issue_number_padding)
|
||||
renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup)
|
||||
renamer.move = settings.rename_move_dir
|
||||
|
||||
new_name = renamer.determineName(filename, ext=new_ext)
|
||||
|
||||
if new_name == os.path.basename(filename):
|
||||
print(msg_hdr + "Filename is already good!", file=sys.stderr)
|
||||
try:
|
||||
new_name = renamer.determineName(filename, ext=new_ext)
|
||||
except Exception as e:
|
||||
print(msg_hdr + "Invalid format string!\nYour rename template is invalid!\n\n"
|
||||
"{}\n\nPlease consult the template help in the settings "
|
||||
"and the documentation on the format at "
|
||||
"https://docs.python.org/3/library/string.html#format-string-syntax", file=sys.stderr)
|
||||
return
|
||||
|
||||
folder = os.path.dirname(os.path.abspath(filename))
|
||||
if settings.rename_move_dir and len(settings.rename_dir.strip()) > 3:
|
||||
folder = settings.rename_dir.strip()
|
||||
|
||||
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
|
||||
|
||||
if os.path.join(folder, new_name) == os.path.abspath(filename):
|
||||
print(msg_hdr + "Filename is already good!", file=sys.stderr)
|
||||
return
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
|
||||
os.rename(filename, new_abs_path)
|
||||
else:
|
||||
suffix = " (dry-run, no change)"
|
||||
|
@ -505,11 +505,12 @@ class ComicVineTalker(QObject):
|
||||
metadata.publisher = utils.xlate(volume_results['publisher']['name'])
|
||||
metadata.day, metadata.month, metadata.year = self.parseDateStr(issue_results['cover_date'])
|
||||
|
||||
#metadata.issueCount = volume_results['count_of_issues']
|
||||
metadata.seriesYear = utils.xlate(volume_results['start_year'])
|
||||
metadata.issueCount = utils.xlate(volume_results['count_of_issues'])
|
||||
metadata.comments = self.cleanup_html(
|
||||
issue_results['description'], settings.remove_html_tables)
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = volume_results['start_year']
|
||||
metadata.volume = utils.xlate(volume_results['start_year'])
|
||||
|
||||
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
|
||||
ctversion.version,
|
||||
@ -630,6 +631,7 @@ class ComicVineTalker(QObject):
|
||||
for w in col_widths:
|
||||
fmtstr += " {{:{}}}|".format(w + 1)
|
||||
width = sum(col_widths) + len(col_widths) * 2
|
||||
print("width=", width)
|
||||
table_text = ""
|
||||
counter = 0
|
||||
for row in rows:
|
||||
@ -740,15 +742,14 @@ class ComicVineTalker(QObject):
|
||||
for d in div_list:
|
||||
if 'class' in d.attrs:
|
||||
c = d['class']
|
||||
if 'imgboxart' in c and 'issue-cover' in c:
|
||||
if d.img['src'].startswith("http"):
|
||||
covers_found += 1
|
||||
if covers_found != 1:
|
||||
if ('imgboxart' in c and
|
||||
'issue-cover' in c and
|
||||
d.img['src'].startswith("http")
|
||||
):
|
||||
|
||||
covers_found += 1
|
||||
if covers_found != 1:
|
||||
alt_cover_url_list.append(d.img['src'])
|
||||
elif d.img['data-src'].startswith("http"):
|
||||
covers_found += 1
|
||||
if covers_found != 1:
|
||||
alt_cover_url_list.append(d.img['data-src'])
|
||||
|
||||
return alt_cover_url_list
|
||||
|
||||
|
@ -320,7 +320,7 @@ class CoverImageWidget(QWidget):
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.lblImage.resize(img_w, img_h)
|
||||
self.lblImage.move(int((frame_w - img_w) / 2), int((frame_h - img_h) / 2))
|
||||
self.lblImage.move((frame_w - img_w) / 2, (frame_h - img_h) / 2)
|
||||
|
||||
def showPopup(self):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
||||
|
@ -17,19 +17,96 @@
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
import sys
|
||||
import string
|
||||
|
||||
from pathvalidate import sanitize_filepath
|
||||
|
||||
from . import utils
|
||||
from .issuestring import IssueString
|
||||
|
||||
|
||||
class MetadataFormatter(string.Formatter):
|
||||
def __init__(self, smart_cleanup=False):
|
||||
super().__init__()
|
||||
self.smart_cleanup = smart_cleanup
|
||||
|
||||
def format_field(self, value, format_spec):
|
||||
if value is None or value == "":
|
||||
return ""
|
||||
return super().format_field(value, format_spec)
|
||||
|
||||
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth,
|
||||
auto_arg_index=0):
|
||||
if recursion_depth < 0:
|
||||
raise ValueError('Max string recursion exceeded')
|
||||
result = []
|
||||
lstrip = False
|
||||
for literal_text, field_name, format_spec, conversion in \
|
||||
self.parse(format_string):
|
||||
|
||||
# output the literal text
|
||||
if literal_text:
|
||||
if lstrip:
|
||||
result.append(literal_text.lstrip("-_)}]#"))
|
||||
else:
|
||||
result.append(literal_text)
|
||||
lstrip = False
|
||||
# if there's a field, output it
|
||||
if field_name is not None:
|
||||
# this is some markup, find the object and do
|
||||
# the formatting
|
||||
|
||||
# handle arg indexing when empty field_names are given.
|
||||
if field_name == '':
|
||||
if auto_arg_index is False:
|
||||
raise ValueError('cannot switch from manual field '
|
||||
'specification to automatic field '
|
||||
'numbering')
|
||||
field_name = str(auto_arg_index)
|
||||
auto_arg_index += 1
|
||||
elif field_name.isdigit():
|
||||
if auto_arg_index:
|
||||
raise ValueError('cannot switch from manual field '
|
||||
'specification to automatic field '
|
||||
'numbering')
|
||||
# disable auto arg incrementing, if it gets
|
||||
# used later on, then an exception will be raised
|
||||
auto_arg_index = False
|
||||
|
||||
# given the field_name, find the object it references
|
||||
# and the argument it came from
|
||||
obj, arg_used = self.get_field(field_name, args, kwargs)
|
||||
used_args.add(arg_used)
|
||||
|
||||
# do any conversion on the resulting object
|
||||
obj = self.convert_field(obj, conversion)
|
||||
|
||||
# expand the format spec, if needed
|
||||
format_spec, auto_arg_index = self._vformat(
|
||||
format_spec, args, kwargs,
|
||||
used_args, recursion_depth-1,
|
||||
auto_arg_index=auto_arg_index)
|
||||
|
||||
# format the object and append to the result
|
||||
fmtObj = self.format_field(obj, format_spec)
|
||||
if fmtObj == "" and len(result) > 0 and self.smart_cleanup:
|
||||
lstrip = True
|
||||
result.pop()
|
||||
result.append(fmtObj)
|
||||
|
||||
return ''.join(result), auto_arg_index
|
||||
|
||||
|
||||
class FileRenamer:
|
||||
|
||||
def __init__(self, metadata):
|
||||
self.setMetadata(metadata)
|
||||
self.setTemplate(
|
||||
"%series% v%volume% #%issue% (of %issuecount%) (%year%)")
|
||||
"{publisher}/{series}/{series} v{volume} #{issue} (of {issueCount}) ({year})")
|
||||
self.smart_cleanup = True
|
||||
self.issue_zero_padding = 3
|
||||
self.move = False
|
||||
|
||||
def setMetadata(self, metadata):
|
||||
self.metdata = metadata
|
||||
@ -43,114 +120,37 @@ class FileRenamer:
|
||||
def setTemplate(self, template):
|
||||
self.template = template
|
||||
|
||||
def replaceToken(self, text, value, token):
|
||||
# helper func
|
||||
def isToken(word):
|
||||
return (word[0] == "%" and word[-1:] == "%")
|
||||
|
||||
if value is not None:
|
||||
return text.replace(token, str(value))
|
||||
else:
|
||||
if self.smart_cleanup:
|
||||
# smart cleanup means we want to remove anything appended to token if it's empty
|
||||
# (e.g "#%issue%" or "v%volume%")
|
||||
# (TODO: This could fail if there is more than one token appended together, I guess)
|
||||
text_list = text.split()
|
||||
|
||||
# special case for issuecount, remove preceding non-token word,
|
||||
# as in "...(of %issuecount%)..."
|
||||
if token == '%issuecount%':
|
||||
for idx, word in enumerate(text_list):
|
||||
if token in word and not isToken(text_list[idx - 1]):
|
||||
text_list[idx - 1] = ""
|
||||
|
||||
text_list = [x for x in text_list if token not in x]
|
||||
return " ".join(text_list)
|
||||
else:
|
||||
return text.replace(token, "")
|
||||
|
||||
def determineName(self, filename, ext=None):
|
||||
|
||||
class Default(dict):
|
||||
def __missing__(self, key):
|
||||
return "{" + key + "}"
|
||||
md = self.metdata
|
||||
new_name = self.template
|
||||
preferred_encoding = utils.get_actual_preferred_encoding()
|
||||
|
||||
# print(u"{0}".format(md))
|
||||
|
||||
new_name = self.replaceToken(new_name, md.series, '%series%')
|
||||
new_name = self.replaceToken(new_name, md.volume, '%volume%')
|
||||
# padding for issue
|
||||
md.issue = IssueString(md.issue).asString(pad=self.issue_zero_padding)
|
||||
|
||||
if md.issue is not None:
|
||||
issue_str = "{0}".format(
|
||||
IssueString(md.issue).asString(pad=self.issue_zero_padding))
|
||||
else:
|
||||
issue_str = None
|
||||
new_name = self.replaceToken(new_name, issue_str, '%issue%')
|
||||
template = self.template
|
||||
|
||||
new_name = self.replaceToken(new_name, md.issueCount, '%issuecount%')
|
||||
new_name = self.replaceToken(new_name, md.year, '%year%')
|
||||
new_name = self.replaceToken(new_name, md.publisher, '%publisher%')
|
||||
new_name = self.replaceToken(new_name, md.title, '%title%')
|
||||
new_name = self.replaceToken(new_name, md.month, '%month%')
|
||||
month_name = None
|
||||
if md.month is not None:
|
||||
if (isinstance(md.month, str) and md.month.isdigit()) or isinstance(
|
||||
md.month, int):
|
||||
if int(md.month) in range(1, 13):
|
||||
dt = datetime.datetime(1970, int(md.month), 1, 0, 0)
|
||||
#month_name = dt.strftime("%B".encode(preferred_encoding)).decode(preferred_encoding)
|
||||
month_name = dt.strftime("%B")
|
||||
new_name = self.replaceToken(new_name, month_name, '%month_name%')
|
||||
pathComponents = template.split(os.sep)
|
||||
new_name = ""
|
||||
|
||||
new_name = self.replaceToken(new_name, md.genre, '%genre%')
|
||||
new_name = self.replaceToken(new_name, md.language, '%language_code%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.criticalRating, '%criticalrating%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateSeries, '%alternateseries%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateNumber, '%alternatenumber%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.alternateCount, '%alternatecount%')
|
||||
new_name = self.replaceToken(new_name, md.imprint, '%imprint%')
|
||||
new_name = self.replaceToken(new_name, md.format, '%format%')
|
||||
new_name = self.replaceToken(
|
||||
new_name, md.maturityRating, '%maturityrating%')
|
||||
new_name = self.replaceToken(new_name, md.storyArc, '%storyarc%')
|
||||
new_name = self.replaceToken(new_name, md.seriesGroup, '%seriesgroup%')
|
||||
new_name = self.replaceToken(new_name, md.scanInfo, '%scaninfo%')
|
||||
fmt = MetadataFormatter(self.smart_cleanup)
|
||||
for Component in pathComponents:
|
||||
new_name = os.path.join(new_name, fmt.vformat(Component, args=[], kwargs=Default(vars(md))).replace("/", "-"))
|
||||
|
||||
if self.smart_cleanup:
|
||||
|
||||
# remove empty braces,brackets, parentheses
|
||||
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name)
|
||||
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name)
|
||||
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name)
|
||||
|
||||
# remove duplicate spaces
|
||||
new_name = " ".join(new_name.split())
|
||||
|
||||
# remove remove duplicate -, _,
|
||||
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name)
|
||||
new_name = re.sub("(\s--)+", " --", new_name)
|
||||
new_name = re.sub("(\s-)+", " -", new_name)
|
||||
|
||||
# remove dash or double dash at end of line
|
||||
new_name = re.sub("[-]{1,2}\s*$", "", new_name)
|
||||
|
||||
# remove duplicate spaces (again!)
|
||||
new_name = " ".join(new_name.split())
|
||||
|
||||
if ext is None:
|
||||
if ext is None or ext == "":
|
||||
ext = os.path.splitext(filename)[1]
|
||||
|
||||
new_name += ext
|
||||
|
||||
# some tweaks to keep various filesystems happy
|
||||
new_name = new_name.replace("/", "-")
|
||||
new_name = new_name.replace(" :", " -")
|
||||
new_name = new_name.replace(": ", " - ")
|
||||
new_name = new_name.replace(":", "-")
|
||||
new_name = new_name.replace("?", "")
|
||||
|
||||
return new_name
|
||||
# remove padding
|
||||
md.issue = IssueString(md.issue).asString()
|
||||
if self.move:
|
||||
return sanitize_filepath(new_name.strip())
|
||||
else:
|
||||
return os.path.basename(sanitize_filepath(new_name.strip()))
|
||||
|
@ -197,7 +197,7 @@ class FileSelectionList(QWidget):
|
||||
centerWindowOnParent(progdialog)
|
||||
#QCoreApplication.processEvents()
|
||||
#progdialog.show()
|
||||
|
||||
|
||||
QCoreApplication.processEvents()
|
||||
firstAdded = None
|
||||
self.twList.setSortingEnabled(False)
|
||||
@ -212,7 +212,7 @@ class FileSelectionList(QWidget):
|
||||
row = self.addPathItem(f)
|
||||
if firstAdded is None and row is not None:
|
||||
firstAdded = row
|
||||
|
||||
|
||||
progdialog.hide()
|
||||
QCoreApplication.processEvents()
|
||||
|
||||
@ -335,9 +335,7 @@ class FileSelectionList(QWidget):
|
||||
filename_item.setText(item_text)
|
||||
filename_item.setData(Qt.ToolTipRole, item_text)
|
||||
|
||||
if fi.ca.isSevenZip():
|
||||
item_text = "7Z"
|
||||
elif fi.ca.isZip():
|
||||
if fi.ca.isZip():
|
||||
item_text = "ZIP"
|
||||
elif fi.ca.isRar():
|
||||
item_text = "RAR"
|
||||
|
@ -20,6 +20,7 @@ from functools import reduce
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
@ -87,7 +87,7 @@ class ImagePopup(QtWidgets.QDialog):
|
||||
img_w = display_pixmap.width()
|
||||
img_h = display_pixmap.height()
|
||||
self.lblImage.resize(img_w, img_h)
|
||||
self.lblImage.move(int((win_w - img_w) / 2), int((win_h - img_h) / 2))
|
||||
self.lblImage.move((win_w - img_w) / 2, (win_h - img_h) / 2)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.close()
|
||||
|
@ -19,6 +19,7 @@ import io
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
@ -75,8 +76,8 @@ class IssueIdentifier:
|
||||
self.length_delta_thresh = settings.id_length_delta_thresh
|
||||
|
||||
# used to eliminate unlikely publishers
|
||||
self.publisher_filter = [
|
||||
s.strip().lower() for s in settings.id_publisher_filter.split(',')]
|
||||
self.publisher_blacklist = [
|
||||
s.strip().lower() for s in settings.id_publisher_blacklist.split(',')]
|
||||
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function = IssueIdentifier.defaultWriteOutput
|
||||
@ -99,8 +100,8 @@ class IssueIdentifier:
|
||||
def setNameLengthDeltaThreshold(self, delta):
|
||||
self.length_delta_thresh = delta
|
||||
|
||||
def setPublisherFilter(self, filter):
|
||||
self.publisher_filter = filter
|
||||
def setPublisherBlackList(self, blacklist):
|
||||
self.publisher_blacklist = blacklist
|
||||
|
||||
def setHasherAlgorithm(self, algo):
|
||||
self.image_hasher = algo
|
||||
@ -230,9 +231,9 @@ class IssueIdentifier:
|
||||
sys.stdout.flush()
|
||||
|
||||
def log_msg(self, msg, newline=True):
|
||||
if newline:
|
||||
msg += "\n"
|
||||
self.output_function(msg)
|
||||
if newline:
|
||||
self.output_function("\n")
|
||||
|
||||
def getIssueCoverMatchScore(
|
||||
self,
|
||||
@ -393,7 +394,7 @@ class IssueIdentifier:
|
||||
if keys['month'] is not None:
|
||||
self.log_msg("\tMonth: " + str(keys['month']))
|
||||
|
||||
#self.log_msg("Publisher Filter: " + str(self.publisher_filter))
|
||||
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
|
||||
comicVine = ComicVineTalker()
|
||||
comicVine.wait_for_rate_limit = self.waitAndRetryOnRateLimit
|
||||
|
||||
@ -441,11 +442,11 @@ class IssueIdentifier:
|
||||
len(shortened_key) + self.length_delta_thresh):
|
||||
length_approved = True
|
||||
|
||||
# remove any series from publishers on the filter
|
||||
# remove any series from publishers on the blacklist
|
||||
if item['publisher'] is not None:
|
||||
publisher = item['publisher']['name']
|
||||
if publisher is not None and publisher.lower(
|
||||
) in self.publisher_filter:
|
||||
) in self.publisher_blacklist:
|
||||
publisher_approved = False
|
||||
|
||||
if length_approved and publisher_approved and date_approved:
|
||||
|
@ -15,7 +15,6 @@
|
||||
# limitations under the License.
|
||||
|
||||
#import os
|
||||
from operator import itemgetter, attrgetter
|
||||
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
@ -127,75 +126,25 @@ class PageListEditor(QWidget):
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
|
||||
def getNewIndexes(self, movement):
|
||||
selection = self.listWidget.selectionModel().selectedRows()
|
||||
selection.sort(reverse=movement>0)
|
||||
current = 0
|
||||
newindexes = []
|
||||
oldindexes = []
|
||||
for x in selection:
|
||||
current = x.row()
|
||||
oldindexes.append(current)
|
||||
if current + movement >= 0 and current + movement <= self.listWidget.count()-1:
|
||||
if len(newindexes) < 1 or current + movement != newindexes[-1]:
|
||||
current += movement
|
||||
else:
|
||||
prev = current
|
||||
newindexes.append(current)
|
||||
oldindexes.sort()
|
||||
newindexes.sort()
|
||||
return list(zip(newindexes, oldindexes))
|
||||
|
||||
def SetSelection(self, indexes):
|
||||
selectionRanges = []
|
||||
first = 0
|
||||
for i, selection in enumerate(indexes):
|
||||
if i == 0:
|
||||
first = selection[0]
|
||||
continue
|
||||
|
||||
if selection != indexes[i-1][0]+1:
|
||||
selectionRanges.append((first,indexes[i-1][0]))
|
||||
first = selection[0]
|
||||
|
||||
selectionRanges.append((first, indexes[-1][0]))
|
||||
selection = QItemSelection()
|
||||
for x in selectionRanges:
|
||||
selection.merge(QItemSelection(self.listWidget.model().index(x[0], 0), self.listWidget.model().index(x[1], 0)), QItemSelectionModel.Select)
|
||||
|
||||
self.listWidget.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect)
|
||||
return selectionRanges
|
||||
|
||||
|
||||
def moveCurrentUp(self):
|
||||
row = self.listWidget.currentRow()
|
||||
selection = self.getNewIndexes(-1)
|
||||
for sel in selection:
|
||||
item = self.listWidget.takeItem(sel[1])
|
||||
self.listWidget.insertItem(sel[0], item)
|
||||
|
||||
if row > 0:
|
||||
item = self.listWidget.takeItem(row)
|
||||
self.listWidget.insertItem(row - 1, item)
|
||||
self.listWidget.setCurrentRow(row - 1)
|
||||
self.SetSelection(selection)
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def moveCurrentDown(self):
|
||||
row = self.listWidget.currentRow()
|
||||
selection = self.getNewIndexes(1)
|
||||
selection.sort(reverse=True)
|
||||
for sel in selection:
|
||||
item = self.listWidget.takeItem(sel[1])
|
||||
self.listWidget.insertItem(sel[0], item)
|
||||
|
||||
if row < self.listWidget.count() - 1:
|
||||
item = self.listWidget.takeItem(row)
|
||||
self.listWidget.insertItem(row + 1, item)
|
||||
self.listWidget.setCurrentRow(row + 1)
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.SetSelection(selection)
|
||||
self.modified.emit()
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
def itemMoveEvent(self, s):
|
||||
# print "move event: ", s, self.listWidget.currentRow()
|
||||
|
@ -67,9 +67,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
|
||||
new_ext = None # default
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
if ca.isSevenZip():
|
||||
new_ext = ".cb7"
|
||||
elif ca.isZip():
|
||||
if ca.isZip():
|
||||
new_ext = ".cbz"
|
||||
elif ca.isRar():
|
||||
new_ext = ".cbr"
|
||||
@ -78,7 +76,20 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
if md.isEmpty:
|
||||
md = ca.metadataFromFilename(self.settings.parse_scan_info)
|
||||
self.renamer.setMetadata(md)
|
||||
new_name = self.renamer.determineName(ca.path, ext=new_ext)
|
||||
self.renamer.move = self.settings.rename_move_dir
|
||||
|
||||
try:
|
||||
new_name = self.renamer.determineName(ca.path, ext=new_ext)
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.critical(self, 'Invalid format string!',
|
||||
'Your rename template is invalid!'
|
||||
'<br/><br/>{}<br/><br/>'
|
||||
'Please consult the template help in the '
|
||||
'settings and the documentation on the format at '
|
||||
'<a href=\'https://docs.python.org/3/library/string.html#format-string-syntax\'>'
|
||||
'https://docs.python.org/3/library/string.html#format-string-syntax</a>'.format(e))
|
||||
return
|
||||
|
||||
|
||||
row = self.twList.rowCount()
|
||||
self.twList.insertRow(row)
|
||||
@ -151,17 +162,20 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
centerWindowOnParent(progdialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if item['new_name'] == os.path.basename(item['archive'].path):
|
||||
folder = os.path.dirname(os.path.abspath(item['archive'].path))
|
||||
if self.settings.rename_move_dir and len(self.settings.rename_dir.strip()) > 3:
|
||||
folder = self.settings.rename_dir.strip()
|
||||
|
||||
new_abs_path = utils.unique_file(os.path.join(folder, item['new_name']))
|
||||
|
||||
if os.path.join(folder, item['new_name']) == item['archive'].path:
|
||||
print(item['new_name'], "Filename is already good!")
|
||||
continue
|
||||
|
||||
if not item['archive'].isWritable(check_rar_status=False):
|
||||
continue
|
||||
|
||||
folder = os.path.dirname(os.path.abspath(item['archive'].path))
|
||||
new_abs_path = utils.unique_file(
|
||||
os.path.join(folder, item['new_name']))
|
||||
|
||||
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
|
||||
os.rename(item['archive'].path, new_abs_path)
|
||||
|
||||
item['archive'].rename(new_abs_path)
|
||||
|
@ -78,8 +78,8 @@ class ComicTaggerSettings:
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_filter = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
|
||||
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
|
||||
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
@ -95,10 +95,6 @@ class ComicTaggerSettings:
|
||||
self.remove_html_tables = False
|
||||
self.cv_api_key = ""
|
||||
|
||||
self.sort_series_by_year = True
|
||||
self.exact_series_matches_first = True
|
||||
self.always_use_publisher_filter = False
|
||||
|
||||
# CBL Tranform settings
|
||||
|
||||
self.assume_lone_credit_is_primary = False
|
||||
@ -112,10 +108,12 @@ class ComicTaggerSettings:
|
||||
self.apply_cbl_transform_on_bulk_operation = False
|
||||
|
||||
# Rename settings
|
||||
self.rename_template = "%series% #%issue% (%year%)"
|
||||
self.rename_template = "{publisher}/{series}/{series} #{issue} - {title} ({year})"
|
||||
self.rename_issue_number_padding = 3
|
||||
self.rename_use_smart_string_cleanup = True
|
||||
self.rename_extension_based_on_archive = True
|
||||
self.rename_dir = ""
|
||||
self.rename_move_dir = False
|
||||
|
||||
# Auto-tag stickies
|
||||
self.save_on_low_confidence = False
|
||||
@ -226,9 +224,9 @@ class ComicTaggerSettings:
|
||||
if self.config.has_option('identifier', 'id_length_delta_thresh'):
|
||||
self.id_length_delta_thresh = self.config.getint(
|
||||
'identifier', 'id_length_delta_thresh')
|
||||
if self.config.has_option('identifier', 'id_publisher_filter'):
|
||||
self.id_publisher_filter = self.config.get(
|
||||
'identifier', 'id_publisher_filter')
|
||||
if self.config.has_option('identifier', 'id_publisher_blacklist'):
|
||||
self.id_publisher_blacklist = self.config.get(
|
||||
'identifier', 'id_publisher_blacklist')
|
||||
|
||||
if self.config.has_option('filenameparser', 'parse_scan_info'):
|
||||
self.parse_scan_info = self.config.getboolean(
|
||||
@ -258,17 +256,6 @@ class ComicTaggerSettings:
|
||||
if self.config.has_option('comicvine', 'remove_html_tables'):
|
||||
self.remove_html_tables = self.config.getboolean(
|
||||
'comicvine', 'remove_html_tables')
|
||||
|
||||
if self.config.has_option('comicvine', 'sort_series_by_year'):
|
||||
self.sort_series_by_year = self.config.getboolean(
|
||||
'comicvine', 'sort_series_by_year')
|
||||
if self.config.has_option('comicvine', 'exact_series_matches_first'):
|
||||
self.exact_series_matches_first = self.config.getboolean(
|
||||
'comicvine', 'exact_series_matches_first')
|
||||
if self.config.has_option('comicvine', 'always_use_publisher_filter'):
|
||||
self.always_use_publisher_filter = self.config.getboolean(
|
||||
'comicvine', 'always_use_publisher_filter')
|
||||
|
||||
if self.config.has_option('comicvine', 'cv_api_key'):
|
||||
self.cv_api_key = self.config.get('comicvine', 'cv_api_key')
|
||||
|
||||
@ -316,6 +303,10 @@ class ComicTaggerSettings:
|
||||
'rename', 'rename_extension_based_on_archive'):
|
||||
self.rename_extension_based_on_archive = self.config.getboolean(
|
||||
'rename', 'rename_extension_based_on_archive')
|
||||
if self.config.has_option('rename', 'rename_dir'):
|
||||
self.rename_dir = self.config.get('rename', 'rename_dir')
|
||||
if self.config.has_option('rename', 'rename_move_dir'):
|
||||
self.rename_move_dir = self.config.getboolean('rename', 'rename_move_dir')
|
||||
|
||||
if self.config.has_option('autotag', 'save_on_low_confidence'):
|
||||
self.save_on_low_confidence = self.config.getboolean(
|
||||
@ -390,8 +381,8 @@ class ComicTaggerSettings:
|
||||
self.id_length_delta_thresh)
|
||||
self.config.set(
|
||||
'identifier',
|
||||
'id_publisher_filter',
|
||||
self.id_publisher_filter)
|
||||
'id_publisher_blacklist',
|
||||
self.id_publisher_blacklist)
|
||||
|
||||
if not self.config.has_section('dialogflags'):
|
||||
self.config.add_section('dialogflags')
|
||||
@ -423,14 +414,6 @@ class ComicTaggerSettings:
|
||||
self.clear_form_before_populating_from_cv)
|
||||
self.config.set(
|
||||
'comicvine', 'remove_html_tables', self.remove_html_tables)
|
||||
|
||||
self.config.set(
|
||||
'comicvine', 'sort_series_by_year', self.sort_series_by_year)
|
||||
self.config.set(
|
||||
'comicvine', 'exact_series_matches_first', self.exact_series_matches_first)
|
||||
self.config.set(
|
||||
'comicvine', 'always_use_publisher_filter', self.always_use_publisher_filter)
|
||||
|
||||
self.config.set('comicvine', 'cv_api_key', self.cv_api_key)
|
||||
|
||||
if not self.config.has_section('cbl_transform'):
|
||||
@ -485,6 +468,8 @@ class ComicTaggerSettings:
|
||||
self.rename_use_smart_string_cleanup)
|
||||
self.config.set('rename', 'rename_extension_based_on_archive',
|
||||
self.rename_extension_based_on_archive)
|
||||
self.config.set('rename', 'rename_dir', self.rename_dir)
|
||||
self.config.set('rename', 'rename_move_dir', self.rename_move_dir)
|
||||
|
||||
if not self.config.has_section('autotag'):
|
||||
self.config.add_section('autotag')
|
||||
|
@ -24,6 +24,8 @@ from .settings import ComicTaggerSettings
|
||||
from .comicvinecacher import ComicVineCacher
|
||||
from .comicvinetalker import ComicVineTalker
|
||||
from .imagefetcher import ImageFetcher
|
||||
from .filerenamer import FileRenamer
|
||||
from .genericmetadata import GenericMetadata
|
||||
from . import utils
|
||||
|
||||
|
||||
@ -94,12 +96,12 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
pblTip = (
|
||||
"""<html>
|
||||
The <b>Publisher Filter</b> is for eliminating automatic matches to certain publishers
|
||||
The <b>Publisher Blacklist</b> is for eliminating automatic matches to certain publishers
|
||||
that you know are incorrect. Useful for avoiding international re-prints with same
|
||||
covers or series names. Enter publisher names separated by commas.
|
||||
</html>"""
|
||||
)
|
||||
self.tePublisherFilter.setToolTip(pblTip)
|
||||
self.tePublisherBlacklist.setToolTip(pblTip)
|
||||
|
||||
validator = QtGui.QIntValidator(1, 4, self)
|
||||
self.leIssueNumPadding.setValidator(validator)
|
||||
@ -113,6 +115,66 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.btnClearCache.clicked.connect(self.clearCache)
|
||||
self.btnResetSettings.clicked.connect(self.resetSettings)
|
||||
self.btnTestKey.clicked.connect(self.testAPIKey)
|
||||
self.btnTemplateHelp.clicked.connect(self.showTemplateHelp)
|
||||
|
||||
def configRenamer(self):
|
||||
md = GenericMetadata()
|
||||
md.isEmpty = False
|
||||
md.tagOrigin = "testing"
|
||||
|
||||
md.series = "series name"
|
||||
md.issue = "1"
|
||||
md.title = "issue title"
|
||||
md.publisher = "publisher"
|
||||
md.year = 1998
|
||||
md.month = 4
|
||||
md.day = 4
|
||||
md.issueCount = 1
|
||||
md.volume = 256
|
||||
md.genre = "test"
|
||||
md.language = "en" # 2 letter iso code
|
||||
md.comments = "This is definitly a comic." # use same way as Summary in CIX
|
||||
|
||||
md.volumeCount = 4096
|
||||
md.criticalRating = "Worst Comic Ever"
|
||||
md.country = "US"
|
||||
|
||||
md.alternateSeries = "None"
|
||||
md.alternateNumber = 4.4
|
||||
md.alternateCount = 4444
|
||||
md.imprint = 'imprint'
|
||||
md.notes = "This doesn't actually exist"
|
||||
md.webLink = "https://example.com/series name/1"
|
||||
md.format = "Box Set"
|
||||
md.manga = "Yes"
|
||||
md.blackAndWhite = False
|
||||
md.pageCount = 4
|
||||
md.maturityRating = "Everyone"
|
||||
|
||||
md.storyArc = "story"
|
||||
md.seriesGroup = "seriesGroup"
|
||||
md.scanInfo = "(lordwelch)"
|
||||
|
||||
md.characters = "character 1, character 2"
|
||||
md.teams = "None"
|
||||
md.locations = "Earth, 444 B.C."
|
||||
|
||||
md.credits = [dict({'role': 'Everything', 'person': 'author', 'primary': True})]
|
||||
md.tags = ["testing", "not real"]
|
||||
md.pages = [dict({'Image': '0', 'Type': 'Front Cover'}), dict({'Image': '1', 'Type': 'Story'})]
|
||||
|
||||
# Some CoMet-only items
|
||||
md.price = 0.00
|
||||
md.isVersionOf = "SERIES #1"
|
||||
md.rights = "None"
|
||||
md.identifier = "LW4444-Comic"
|
||||
md.lastMark = "0"
|
||||
md.coverImage = "https://example.com/series name/1/cover"
|
||||
|
||||
self.renamer = FileRenamer(md)
|
||||
self.renamer.setTemplate(str(self.leRenameTemplate.text()))
|
||||
self.renamer.setIssueZeroPadding(self.settings.rename_issue_number_padding)
|
||||
self.renamer.setSmartCleanup(self.settings.rename_use_smart_string_cleanup)
|
||||
|
||||
def settingsToForm(self):
|
||||
|
||||
@ -120,8 +182,8 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.leRarExePath.setText(self.settings.rar_exe_path)
|
||||
self.leNameLengthDeltaThresh.setText(
|
||||
str(self.settings.id_length_delta_thresh))
|
||||
self.tePublisherFilter.setPlainText(
|
||||
self.settings.id_publisher_filter)
|
||||
self.tePublisherBlacklist.setPlainText(
|
||||
self.settings.id_publisher_blacklist)
|
||||
|
||||
if self.settings.check_for_new_version:
|
||||
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
|
||||
@ -135,14 +197,6 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.remove_html_tables:
|
||||
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.always_use_publisher_filter:
|
||||
self.cbxUseFilter.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.sort_series_by_year:
|
||||
self.cbxSortByYear.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.exact_series_matches_first:
|
||||
self.cbxExactMatches.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
self.leKey.setText(str(self.settings.cv_api_key))
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
@ -173,9 +227,27 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.cbxSmartCleanup.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
self.cbxChangeExtension.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.rename_move_dir:
|
||||
self.cbxMoveFiles.setCheckState(QtCore.Qt.Checked)
|
||||
self.leDirectory.setText(self.settings.rename_dir)
|
||||
|
||||
def accept(self):
|
||||
|
||||
self.configRenamer()
|
||||
|
||||
|
||||
try:
|
||||
new_name = self.renamer.determineName('test.cbz')
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.critical(self, 'Invalid format string!',
|
||||
'Your rename template is invalid!'
|
||||
'<br/><br/>{}<br/><br/>'
|
||||
'Please consult the template help in the '
|
||||
'settings and the documentation on the format at '
|
||||
'<a href=\'https://docs.python.org/3/library/string.html#format-string-syntax\'>'
|
||||
'https://docs.python.org/3/library/string.html#format-string-syntax</a>'.format(e))
|
||||
return
|
||||
|
||||
# Copy values from form to settings and save
|
||||
self.settings.rar_exe_path = str(self.leRarExePath.text())
|
||||
|
||||
@ -193,19 +265,14 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
self.settings.id_length_delta_thresh = int(
|
||||
self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_filter = str(
|
||||
self.tePublisherFilter.toPlainText())
|
||||
self.settings.id_publisher_blacklist = str(
|
||||
self.tePublisherBlacklist.toPlainText())
|
||||
|
||||
self.settings.parse_scan_info = self.cbxParseScanInfo.isChecked()
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
|
||||
|
||||
self.settings.always_use_publisher_filter = self.cbxUseFilter.isChecked()
|
||||
self.settings.sort_series_by_year = self.cbxSortByYear.isChecked()
|
||||
self.settings.exact_series_matches_first = self.cbxExactMatches.isChecked()
|
||||
|
||||
self.settings.cv_api_key = str(self.leKey.text())
|
||||
ComicVineTalker.api_key = self.settings.cv_api_key.strip()
|
||||
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
@ -223,6 +290,8 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.leIssueNumPadding.text())
|
||||
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
|
||||
self.settings.rename_dir = self.leDirectory.text()
|
||||
|
||||
self.settings.save()
|
||||
QtWidgets.QDialog.accept(self)
|
||||
@ -281,3 +350,17 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
def showRenameTab(self):
|
||||
self.tabWidget.setCurrentIndex(5)
|
||||
|
||||
def showTemplateHelp(self):
|
||||
TemplateHelpWin = TemplateHelpWindow(self)
|
||||
TemplateHelpWin.setModal(False)
|
||||
TemplateHelpWin.show()
|
||||
|
||||
class TemplateHelpWindow(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
super(TemplateHelpWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('TemplateHelp.ui'), self)
|
||||
|
||||
|
||||
|
@ -87,7 +87,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def __init__(self, file_list, settings, parent=None, opts=None):
|
||||
super(TaggerWindow, self).__init__(parent)
|
||||
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.getUIFile('taggerwindow.ui'), self)
|
||||
self.settings = settings
|
||||
|
||||
@ -187,6 +187,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
validator = QtGui.QIntValidator(1900, 2099, self)
|
||||
self.lePubYear.setValidator(validator)
|
||||
|
||||
self.leSeriesPubYear.setValidator(validator)
|
||||
|
||||
validator = QtGui.QIntValidator(1, 12, self)
|
||||
self.lePubMonth.setValidator(validator)
|
||||
|
||||
@ -294,7 +296,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.setWindowIcon(
|
||||
QtGui.QIcon(ComicTaggerSettings.getGraphic('app.png')))
|
||||
|
||||
|
||||
if self.comic_archive is None:
|
||||
self.setWindowTitle(self.appName)
|
||||
else:
|
||||
@ -584,7 +586,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
event.accept()
|
||||
|
||||
def getUrlFromLocalFileID(self, localFileID):
|
||||
return localFileID.toLocalFile()
|
||||
return localFileID.toLocalFile()
|
||||
|
||||
def dropEvent(self, event):
|
||||
# if self.dirtyFlagVerification("Open Archive",
|
||||
@ -675,9 +677,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.lblFilename.setText(filename)
|
||||
|
||||
if ca.isSevenZip():
|
||||
self.lblArchiveType.setText("7Z archive")
|
||||
elif ca.isZip():
|
||||
if ca.isZip():
|
||||
self.lblArchiveType.setText("ZIP archive")
|
||||
elif ca.isRar():
|
||||
self.lblArchiveType.setText("RAR archive")
|
||||
@ -782,6 +782,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
assignText(self.lePubMonth, md.month)
|
||||
assignText(self.lePubYear, md.year)
|
||||
assignText(self.lePubDay, md.day)
|
||||
assignText(self.leSeriesPubYear, md.seriesYear)
|
||||
assignText(self.leGenre, md.genre)
|
||||
assignText(self.leImprint, md.imprint)
|
||||
assignText(self.teComments, md.comments)
|
||||
@ -907,6 +908,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
md.month = utils.xlate(self.lePubMonth.text(), True)
|
||||
md.year = utils.xlate(self.lePubYear.text(), True)
|
||||
md.day = utils.xlate(self.lePubDay.text(), True)
|
||||
md.seriesYear = utils.xlate(self.leSeriesPubYear.text(), "int")
|
||||
md.criticalRating = utils.xlate(self.leCriticalRating.text(), True)
|
||||
md.alternateCount = utils.xlate(self.leAltIssueCount.text(), True)
|
||||
|
||||
@ -960,6 +962,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
row += 1
|
||||
|
||||
md.pages = self.pageListEditor.getPageList()
|
||||
|
||||
self.metadata = md
|
||||
|
||||
def useFilename(self):
|
||||
@ -987,7 +990,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
dialog.setDirectory(self.settings.last_opened_folder)
|
||||
|
||||
if not folder_mode:
|
||||
archive_filter = "Comic archive files (*.cb7 *.7z *.cbz *.zip *.cbr *.rar)"
|
||||
archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar)"
|
||||
filters = [
|
||||
archive_filter,
|
||||
"Any files (*)"
|
||||
@ -1166,11 +1169,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def updateCreditColors(self):
|
||||
#!!!ATB qt5 porting TODO
|
||||
#return
|
||||
#return
|
||||
inactive_color = QtGui.QColor(255, 170, 150)
|
||||
active_palette = self.leSeries.palette()
|
||||
active_color = active_palette.color(QtGui.QPalette.Base)
|
||||
|
||||
|
||||
inactive_brush = QtGui.QBrush(inactive_color)
|
||||
active_brush = QtGui.QBrush(active_color)
|
||||
|
||||
@ -1387,8 +1390,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
else:
|
||||
screen = QtWidgets.QDesktopWidget().screenGeometry()
|
||||
size = self.frameGeometry()
|
||||
self.move(int((screen.width() - size.width()) / 2),
|
||||
int((screen.height() - size.height()) / 2))
|
||||
self.move((screen.width() - size.width()) / 2,
|
||||
(screen.height() - size.height()) / 2)
|
||||
|
||||
def adjustLoadStyleCombo(self):
|
||||
# select the current style
|
||||
@ -1424,7 +1427,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
# Add the entries to the language combobox
|
||||
self.cbLanguage.addItem("", "")
|
||||
lang_dict = utils.getLanguageDict()
|
||||
for key in sorted(lang_dict, key=lang_dict.get):
|
||||
for key in sorted(lang_dict, key=functools.cmp_to_key(locale.strcoll)):
|
||||
self.cbLanguage.addItem(lang_dict[key], key)
|
||||
|
||||
# Add the entries to the manga combobox
|
||||
@ -1723,7 +1726,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def autoTagLog(self, text):
|
||||
IssueIdentifier.defaultWriteOutput(text)
|
||||
if self.atprogdialog is not None:
|
||||
self.atprogdialog.textEdit.append(text.rstrip())
|
||||
self.atprogdialog.textEdit.insertPlainText(text)
|
||||
self.atprogdialog.textEdit.ensureCursorVisible()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -2066,13 +2069,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.comic_archive = None
|
||||
self.clearForm()
|
||||
|
||||
if not os.path.exists(comic_archive.path):
|
||||
self.fileSelectionList.dirty_flag = False
|
||||
self.fileSelectionList.removeArchiveList([comic_archive])
|
||||
QtCore.QTimer.singleShot(1, self.fileSelectionList.revertSelection)
|
||||
return
|
||||
|
||||
self.settings.last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
|
||||
self.settings.last_opened_folder = os.path.abspath(
|
||||
os.path.split(comic_archive.path)[0])
|
||||
self.comic_archive = comic_archive
|
||||
self.metadata = self.comic_archive.readMetadata(self.load_data_style)
|
||||
if self.metadata is None:
|
||||
|
107
comictaggerlib/ui/TemplateHelp.ui
Normal file
107
comictaggerlib/ui/TemplateHelp.ui
Normal file
@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>702</width>
|
||||
<height>452</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Template Help</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="textBrowser">
|
||||
<property name="html">
|
||||
<string><html>
|
||||
<head/>
|
||||
<body>
|
||||
<h1 style="text-align: center">Template help</h1>
|
||||
<p>The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the
|
||||
|
||||
<a href="https://docs.python.org/3/library/string.html#format-string-syntax">Python 3 documentation</a></p>
|
||||
<pre>Accepts the following variables:
|
||||
{isEmpty}		(boolean)
|
||||
{tagOrigin}		(string)
|
||||
{series}		(string)
|
||||
{issue}		(string)
|
||||
{title}		(string)
|
||||
{publisher}		(string)
|
||||
{month}		(integer)
|
||||
{year}		(integer)
|
||||
{day}		(integer)
|
||||
{issueCount}	(integer)
|
||||
{volume}		(integer)
|
||||
{genre}		(string)
|
||||
{language}		(string)
|
||||
{comments}		(string)
|
||||
{volumeCount}	(integer)
|
||||
{criticalRating}	(string)
|
||||
{country}		(string)
|
||||
{alternateSeries}	(string)
|
||||
{alternateNumber}	(string)
|
||||
{alternateCount}	(integer)
|
||||
{imprint}		(string)
|
||||
{notes}		(string)
|
||||
{webLink}		(string)
|
||||
{format}		(string)
|
||||
{manga}		(string)
|
||||
{blackAndWhite}	(boolean)
|
||||
{pageCount}		(integer)
|
||||
{maturityRating}	(string)
|
||||
{storyArc}		(string)
|
||||
{seriesGroup}	(string)
|
||||
{scanInfo}		(string)
|
||||
{characters}	(string)
|
||||
{teams}		(string)
|
||||
{locations}		(string)
|
||||
{credits}		(list of dict({'role': 'str', 'person': 'str', 'primary': boolean}))
|
||||
{tags}		(list of str)
|
||||
{pages}		(list of dict({'Image': 'str(int)', 'Type': 'str'}))
|
||||
|
||||
CoMet-only items:
|
||||
{price}		(float)
|
||||
{isVersionOf}	(string)
|
||||
{rights}		(string)
|
||||
{identifier}	(string)
|
||||
{lastMark}	(string)
|
||||
{coverImage}	(string)
|
||||
|
||||
Examples:
|
||||
|
||||
{series} {issue} ({year})
|
||||
Spider-Geddon 1 (2018)
|
||||
|
||||
{series} #{issue} - {title}
|
||||
Spider-Geddon #1 - New Players; Check In
|
||||
|
||||
</pre>
|
||||
</body>
|
||||
</html></string>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -37,9 +37,6 @@
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -51,9 +51,9 @@ if qt_available:
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calculated as screen width - window width
|
||||
# /2
|
||||
hpos = int((main_window_size.width() - window.width()) / 2)
|
||||
hpos = (main_window_size.width() - window.width()) / 2
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = int((main_window_size.height() - window.height()) / 2)
|
||||
vpos = (main_window_size.height() - window.height()) / 2
|
||||
# And the move call repositions the window
|
||||
window.move(
|
||||
hpos +
|
||||
@ -63,6 +63,7 @@ if qt_available:
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
import io
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>702</width>
|
||||
<height>478</height>
|
||||
<height>432</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -28,7 +28,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
@ -133,7 +133,7 @@
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Searching</string>
|
||||
<string>Identifier</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
@ -187,15 +187,15 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Publisher Filter:</string>
|
||||
<string>Publisher Blacklist:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPlainTextEdit" name="tePublisherFilter">
|
||||
<item row="1" column="1">
|
||||
<widget class="QPlainTextEdit" name="tePublisherBlacklist">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@ -204,23 +204,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="cbxUseFilter">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Applies the <span style=" font-weight:600;">Publisher Filter</span> on all searches.<br/>The search window has a dynamic toggle to show the unfiltered results.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Always use Publisher Filter on "manual" searches:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@ -280,33 +263,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_4">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxSortByYear">
|
||||
<property name="text">
|
||||
<string>Initally sort Series search results by Starting Year instead of No. Issues</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxExactMatches">
|
||||
<property name="text">
|
||||
<string>Initally show Series Name exact matches first</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@ -547,7 +503,7 @@
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<widget class="QLabel" name="lblTemplate">
|
||||
<property name="text">
|
||||
<string>Template:</string>
|
||||
</property>
|
||||
@ -556,31 +512,80 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="leRenameTemplate">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%month%<br/>%month_name%<br/>%publisher%<br/>%title%<br/>
|
||||
%genre%<br/>
|
||||
%language_code%<br/>
|
||||
%criticalrating%<br/>
|
||||
%alternateseries%<br/>
|
||||
%alternatenumber%<br/>
|
||||
%alternatecount%<br/>
|
||||
%imprint%<br/>
|
||||
%format%<br/>
|
||||
%maturityrating%<br/>
|
||||
%storyarc%<br/>
|
||||
%seriesgroup%<br/>
|
||||
%scaninfo%
|
||||
</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
<string><pre>The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
|
||||
Accepts the following variables:
|
||||
{isEmpty} (boolean)
|
||||
{tagOrigin} (string)
|
||||
{series} (string)
|
||||
{issue} (string)
|
||||
{title} (string)
|
||||
{publisher} (string)
|
||||
{month} (integer)
|
||||
{year} (integer)
|
||||
{day} (integer)
|
||||
{issueCount} (integer)
|
||||
{volume} (integer)
|
||||
{genre} (string)
|
||||
{language} (string)
|
||||
{comments} (string)
|
||||
{volumeCount} (integer)
|
||||
{criticalRating} (string)
|
||||
{country} (string)
|
||||
{alternateSeries} (string)
|
||||
{alternateNumber} (string)
|
||||
{alternateCount} (integer)
|
||||
{imprint} (string)
|
||||
{notes} (string)
|
||||
{webLink} (string)
|
||||
{format} (string)
|
||||
{manga} (string)
|
||||
{blackAndWhite} (boolean)
|
||||
{pageCount} (integer)
|
||||
{maturityRating} (string)
|
||||
{storyArc} (string)
|
||||
{seriesGroup} (string)
|
||||
{scanInfo} (string)
|
||||
{characters} (string)
|
||||
{teams} (string)
|
||||
{locations} (string)
|
||||
{credits} (list of dict({&apos;role&apos;: &apos;str&apos;, &apos;person&apos;: &apos;str&apos;, &apos;primary&apos;: boolean}))
|
||||
{tags} (list of str)
|
||||
{pages} (list of dict({&apos;Image&apos;: &apos;str(int)&apos;, &apos;Type&apos;: &apos;str&apos;}))
|
||||
|
||||
CoMet-only items:
|
||||
{price} (float)
|
||||
{isVersionOf} (string)
|
||||
{rights} (string)
|
||||
{identifier} (string)
|
||||
{lastMark} (string)
|
||||
{coverImage} (string)
|
||||
|
||||
Examples:
|
||||
|
||||
{series} {issue} ({year})
|
||||
Spider-Geddon 1 (2018)
|
||||
|
||||
{series} #{issue} - {title}
|
||||
Spider-Geddon #1 - New Players; Check In
|
||||
</pre></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<widget class="QPushButton" name="btnTemplateHelp">
|
||||
<property name="text">
|
||||
<string>Template Help</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="lblPadding">
|
||||
<property name="text">
|
||||
<string>Issue # Zero Padding</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="leIssueNumPadding">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
@ -599,7 +604,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxSmartCleanup">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600;">&quot;Smart Text Cleanup&quot; </span>will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.</p></body></html></string>
|
||||
@ -609,13 +614,33 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="cbxChangeExtension">
|
||||
<property name="text">
|
||||
<string>Change Extension Based On Archive Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="cbxMoveFiles">
|
||||
<property name="toolTip">
|
||||
<string>If checked moves files to specified folder</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Move files when renaming</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="lblDirectory">
|
||||
<property name="text">
|
||||
<string>Destination Directory:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QLineEdit" name="leDirectory"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -512,6 +512,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QLineEdit" name="leSeriesPubYear"/>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Series Year</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -148,16 +148,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxFilter">
|
||||
<property name="text">
|
||||
<string>Filter Publishers</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Filter the publishers based on the publisher filter.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
|
@ -32,11 +32,9 @@ from .settings import ComicTaggerSettings
|
||||
from .matchselectionwindow import MatchSelectionWindow
|
||||
from .coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
|
||||
|
||||
from comictaggerlib import settings
|
||||
#from imagefetcher import ImageFetcher
|
||||
#import utils
|
||||
|
||||
from . import utils
|
||||
|
||||
class SearchThread(QtCore.QThread):
|
||||
|
||||
@ -126,8 +124,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.cover_index_list = cover_index_list
|
||||
self.cv_search_results = None
|
||||
|
||||
self.use_filter = self.settings.always_use_publisher_filter
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
@ -135,9 +131,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.btnIssues.clicked.connect(self.showIssues)
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
self.cbxFilter.setChecked(self.use_filter)
|
||||
self.cbxFilter.toggled.connect(self.filterToggled)
|
||||
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
@ -158,10 +151,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.performQuery(refresh=True)
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def filterToggled(self):
|
||||
self.use_filter = not self.use_filter
|
||||
self.performQuery(refresh=False)
|
||||
|
||||
def autoSelect(self):
|
||||
|
||||
if self.comic_archive is None:
|
||||
@ -304,6 +293,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.progdialog.canceled.connect(self.searchCanceled)
|
||||
self.progdialog.setModal(True)
|
||||
self.progdialog.setMinimumDuration(300)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
self.search_thread = SearchThread(self.series_name, refresh)
|
||||
self.search_thread.searchComplete.connect(self.searchComplete)
|
||||
self.search_thread.progressUpdate.connect(self.searchProgressUpdate)
|
||||
@ -344,40 +334,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
return
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
# filter the publishers if enabled set
|
||||
if self.use_filter:
|
||||
try:
|
||||
publisher_filter = {s.strip().lower() for s in self.settings.id_publisher_filter.split(',')}
|
||||
# use '' as publisher name if None
|
||||
self.cv_search_results = list(filter(lambda d: ('' if d['publisher'] is None else str(d['publisher']['name']).lower()) not in publisher_filter, self.cv_search_results))
|
||||
except:
|
||||
print('bad data error filtering filter publishers')
|
||||
|
||||
# pre sort the data - so that we can put exact matches first afterwards
|
||||
# compare as str incase extra chars ie. '1976?'
|
||||
# - missing (none) values being converted to 'None' - consistant with prior behaviour in v1.2.3
|
||||
# sort by start_year if set
|
||||
if self.settings.sort_series_by_year:
|
||||
try:
|
||||
self.cv_search_results = sorted(self.cv_search_results, key = lambda i: (str(i['start_year']), str(i['count_of_issues'])), reverse=True)
|
||||
except:
|
||||
print('bad data error sorting results by start_year,count_of_issues')
|
||||
else:
|
||||
try:
|
||||
self.cv_search_results = sorted(self.cv_search_results, key = lambda i: str(i['count_of_issues']), reverse=True)
|
||||
except:
|
||||
print('bad data error sorting results by count_of_issues')
|
||||
|
||||
# move sanitized matches to the front
|
||||
if self.settings.exact_series_matches_first:
|
||||
try:
|
||||
sanitized = utils.sanitize_title(self.series_name)
|
||||
exactMatches = list(filter(lambda d: utils.sanitize_title(str(d['name'])) in sanitized, self.cv_search_results))
|
||||
nonMatches = list(filter(lambda d: utils.sanitize_title(str(d['name'])) not in sanitized, self.cv_search_results))
|
||||
self.cv_search_results = exactMatches + nonMatches
|
||||
except:
|
||||
print('bad data error filtering exact/near matches')
|
||||
|
||||
self.updateButtons()
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
@ -419,7 +375,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.DescendingOrder)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
@ -448,7 +406,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
return
|
||||
|
||||
self.volume_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
|
||||
|
||||
|
||||
# list selection was changed, update the info on the volume
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
|
44
localefix.py
44
localefix.py
@ -1,44 +0,0 @@
|
||||
import locale
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def _lang_code_mac():
|
||||
"""
|
||||
stolen from https://github.com/mu-editor/mu
|
||||
Returns the user's language preference as defined in the Language & Region
|
||||
preference pane in macOS's System Preferences.
|
||||
"""
|
||||
|
||||
# Uses the shell command `defaults read -g AppleLocale` that prints out a
|
||||
# language code to standard output. Assumptions about the command:
|
||||
# - It exists and is in the shell's PATH.
|
||||
# - It accepts those arguments.
|
||||
# - It returns a usable language code.
|
||||
#
|
||||
# Reference documentation:
|
||||
# - The man page for the `defaults` command on macOS.
|
||||
# - The macOS underlying API:
|
||||
# https://developer.apple.com/documentation/foundation/nsuserdefaults.
|
||||
|
||||
LANG_DETECT_COMMAND = 'defaults read -g AppleLocale'
|
||||
|
||||
status, output = subprocess.getstatusoutput(LANG_DETECT_COMMAND)
|
||||
if status == 0:
|
||||
# Command was successful.
|
||||
lang_code = output
|
||||
else:
|
||||
_logger.warning('Language detection command failed: %r', output)
|
||||
lang_code = ''
|
||||
|
||||
return lang_code
|
||||
|
||||
if sys.platform == "darwin" and "LANG" not in os.environ:
|
||||
code = _lang_code_mac()
|
||||
if code != "":
|
||||
os.environ["LANG"] = f"{code}.utf-8"
|
||||
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
sys.stdout.reconfigure(encoding=sys.getdefaultencoding())
|
||||
sys.stderr.reconfigure(encoding=sys.getdefaultencoding())
|
||||
sys.stdin.reconfigure(encoding=sys.getdefaultencoding())
|
@ -1,6 +1,7 @@
|
||||
beautifulsoup4 >= 4.1
|
||||
PyPDF2==1.24
|
||||
configparser
|
||||
natsort >=8.1.0
|
||||
natsort
|
||||
pillow>=4.3.0
|
||||
requests
|
||||
py7zr
|
||||
pathvalidate
|
||||
|
@ -1,4 +1,4 @@
|
||||
pyinstaller>=4.10
|
||||
pyinstaller==4.3
|
||||
setuptools>=42
|
||||
setuptools_scm[toml]>=3.4
|
||||
wheel
|
||||
|
Reference in New Issue
Block a user