Merge branch 'Renaming' into temp

This commit is contained in:
lordwelch 2019-09-11 14:43:21 -07:00
commit 37e6e81894
13 changed files with 515 additions and 309 deletions

View File

@ -24,39 +24,33 @@ from . import utils
class ComicBookInfo:
def metadataFromString(self, string):
class Default(dict):
def __missing__(self, key):
return None
cbi_container = json.loads(str(string, 'utf-8'))
metadata = GenericMetadata()
cbi = cbi_container['ComicBookInfo/1.0']
cbi = Default(cbi_container['ComicBookInfo/1.0'])
# helper func
# If item is not in CBI, return None
def xlate(cbi_entry):
if cbi_entry in cbi:
return cbi[cbi_entry]
else:
return None
metadata.series = utils.xlate(cbi['series'])
metadata.title = utils.xlate(cbi['title'])
metadata.issue = utils.xlate(cbi['issue'])
metadata.publisher = utils.xlate(cbi['publisher'])
metadata.month = utils.xlate(cbi['publicationMonth'], True)
metadata.year = utils.xlate(cbi['publicationYear'], True)
metadata.issueCount = utils.xlate(cbi['numberOfIssues'], True)
metadata.comments = utils.xlate(cbi['comments'])
metadata.genre = utils.xlate(cbi['genre'])
metadata.volume = utils.xlate(cbi['volume'], True)
metadata.volumeCount = utils.xlate(cbi['numberOfVolumes'], True)
metadata.language = utils.xlate(cbi['language'])
metadata.country = utils.xlate(cbi['country'])
metadata.criticalRating = utils.xlate(cbi['rating'])
metadata.series = xlate('series')
metadata.title = xlate('title')
metadata.issue = xlate('issue')
metadata.publisher = xlate('publisher')
metadata.month = xlate('publicationMonth')
metadata.year = xlate('publicationYear')
metadata.issueCount = xlate('numberOfIssues')
metadata.comments = xlate('comments')
metadata.credits = xlate('credits')
metadata.genre = xlate('genre')
metadata.volume = xlate('volume')
metadata.volumeCount = xlate('numberOfVolumes')
metadata.language = xlate('language')
metadata.country = xlate('country')
metadata.criticalRating = xlate('rating')
metadata.tags = xlate('tags')
metadata.credits = cbi['credits']
metadata.tags = cbi['tags']
# make sure credits and tags are at least empty lists and not None
if metadata.credits is None:
@ -103,33 +97,23 @@ class ComicBookInfo:
# helper func
def assign(cbi_entry, md_entry):
if md_entry is not None:
if md_entry is not None or isinstance(md_entry, str) and md_entry != "":
cbi[cbi_entry] = md_entry
# helper func
def toInt(s):
i = None
if type(s) in [str, str, int]:
try:
i = int(s)
except ValueError:
pass
return i
assign('series', metadata.series)
assign('title', metadata.title)
assign('issue', metadata.issue)
assign('publisher', metadata.publisher)
assign('publicationMonth', toInt(metadata.month))
assign('publicationYear', toInt(metadata.year))
assign('numberOfIssues', toInt(metadata.issueCount))
assign('comments', metadata.comments)
assign('genre', metadata.genre)
assign('volume', toInt(metadata.volume))
assign('numberOfVolumes', toInt(metadata.volumeCount))
assign('language', utils.getLanguageFromISO(metadata.language))
assign('country', metadata.country)
assign('rating', metadata.criticalRating)
assign('series', utils.xlate(metadata.series))
assign('title', utils.xlate(metadata.title))
assign('issue', utils.xlate(metadata.issue))
assign('publisher', utils.xlate(metadata.publisher))
assign('publicationMonth', utils.xlate(metadata.month, True))
assign('publicationYear', utils.xlate(metadata.year, True))
assign('numberOfIssues', utils.xlate(metadata.issueCount, True))
assign('comments', utils.xlate(metadata.comments))
assign('genre', utils.xlate(metadata.genre))
assign('volume', utils.xlate(metadata.volume, True))
assign('numberOfVolumes', utils.xlate(metadata.volumeCount, True))
assign('language', utils.xlate(utils.getLanguageFromISO(metadata.language)))
assign('country', utils.xlate(metadata.country))
assign('rating', utils.xlate(metadata.criticalRating))
assign('credits', metadata.credits)
assign('tags', metadata.tags)

View File

@ -20,6 +20,7 @@ import xml.etree.ElementTree as ET
#import zipfile
from .genericmetadata import GenericMetadata
from .issuestring import IssueString
from . import utils
@ -206,48 +207,44 @@ class ComicInfoXml:
raise 1
return None
metadata = GenericMetadata()
md = metadata
# Helper function
def xlate(tag):
node = root.find(tag)
if node is not None:
return node.text
else:
def get(name):
tag = root.find(name)
if tag is None:
return None
return tag.text
md.series = xlate('Series')
md.title = xlate('Title')
md.issue = xlate('Number')
md.issueCount = xlate('Count')
md.volume = xlate('Volume')
md.alternateSeries = xlate('AlternateSeries')
md.alternateNumber = xlate('AlternateNumber')
md.alternateCount = xlate('AlternateCount')
md.comments = xlate('Summary')
md.notes = xlate('Notes')
md.year = xlate('Year')
md.month = xlate('Month')
md.day = xlate('Day')
md.publisher = xlate('Publisher')
md.imprint = xlate('Imprint')
md.genre = xlate('Genre')
md.webLink = xlate('Web')
md.language = xlate('LanguageISO')
md.format = xlate('Format')
md.manga = xlate('Manga')
md.characters = xlate('Characters')
md.teams = xlate('Teams')
md.locations = xlate('Locations')
md.pageCount = xlate('PageCount')
md.scanInfo = xlate('ScanInformation')
md.storyArc = xlate('StoryArc')
md.seriesGroup = xlate('SeriesGroup')
md.maturityRating = xlate('AgeRating')
md = GenericMetadata()
tmp = xlate('BlackAndWhite')
md.blackAndWhite = False
md.series = utils.xlate(get('Series'))
md.title = utils.xlate(get('Title'))
md.issue = IssueString(utils.xlate(get('Number'))).asString()
md.issueCount = utils.xlate(get('Count'), True)
md.volume = utils.xlate(get('Volume'), True)
md.alternateSeries = utils.xlate(get('AlternateSeries'))
md.alternateNumber = IssueString(utils.xlate(get('AlternateNumber'))).asString()
md.alternateCount = utils.xlate(get('AlternateCount'), True)
md.comments = utils.xlate(get('Summary'))
md.notes = utils.xlate(get('Notes'))
md.year = utils.xlate(get('Year'), True)
md.month = utils.xlate(get('Month'), True)
md.day = utils.xlate(get('Day'), True)
md.publisher = utils.xlate(get('Publisher'))
md.imprint = utils.xlate(get('Imprint'))
md.genre = utils.xlate(get('Genre'))
md.webLink = utils.xlate(get('Web'))
md.language = utils.xlate(get('LanguageISO'))
md.format = utils.xlate(get('Format'))
md.manga = utils.xlate(get('Manga'))
md.characters = utils.xlate(get('Characters'))
md.teams = utils.xlate(get('Teams'))
md.locations = utils.xlate(get('Locations'))
md.pageCount = utils.xlate(get('PageCount'), True)
md.scanInfo = utils.xlate(get('ScanInformation'))
md.storyArc = utils.xlate(get('StoryArc'))
md.seriesGroup = utils.xlate(get('SeriesGroup'))
md.maturityRating = utils.xlate(get('AgeRating'))
tmp = utils.xlate(get('BlackAndWhite'))
if tmp is not None and tmp.lower() in ["yes", "true", "1"]:
md.blackAndWhite = True
# Now extract the credit info
@ -261,23 +258,23 @@ class ComicInfoXml:
):
if n.text is not None:
for name in n.text.split(','):
metadata.addCredit(name.strip(), n.tag)
md.addCredit(name.strip(), n.tag)
if n.tag == 'CoverArtist':
if n.text is not None:
for name in n.text.split(','):
metadata.addCredit(name.strip(), "Cover")
md.addCredit(name.strip(), "Cover")
# parse page data now
pages_node = root.find("Pages")
if pages_node is not None:
for page in pages_node:
metadata.pages.append(page.attrib)
md.pages.append(page.attrib)
# print page.attrib
metadata.isEmpty = False
md.isEmpty = False
return metadata
return md
def writeToExternalFile(self, filename, metadata):

View File

@ -121,6 +121,23 @@ def which(program):
return None
def xlate(data, isInt=False):
class Default(dict):
def __missing__(self, key):
return None
if data is None or data == "":
return None
if isInt:
i = str(data).translate(Default(zip((ord(c) for c in "1234567890"),"1234567890")))
if i == "0":
return "0"
if i is "":
return None
return int(i)
else:
return str(data)
def removearticles(text):
text = text.lower()
articles = ['and', 'a', '&', 'issue', 'the']

View File

@ -516,19 +516,24 @@ 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):
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
folder = os.path.dirname(os.path.abspath(filename))
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
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)"

View File

@ -124,11 +124,11 @@ class ComicVineTalker(QObject):
year = None
if date_str is not None:
parts = date_str.split('-')
year = parts[0]
year = utils.xlate(parts[0], True)
if len(parts) > 1:
month = parts[1]
month = utils.xlate(parts[1], True)
if len(parts) > 2:
day = parts[2]
day = utils.xlate(parts[2], True)
return day, month, year
def testKey(self, key):
@ -497,15 +497,13 @@ class ComicVineTalker(QObject):
# Now, map the Comic Vine data to generic metadata
metadata = GenericMetadata()
metadata.series = issue_results['volume']['name']
metadata.series = utils.xlate(issue_results['volume']['name'])
metadata.issue = IssueString(issue_results['issue_number']).asString()
metadata.title = utils.xlate(issue_results['name'])
num_s = IssueString(issue_results['issue_number']).asString()
metadata.issue = num_s
metadata.title = issue_results['name']
metadata.publisher = volume_results['publisher']['name']
metadata.day, metadata.month, metadata.year = self.parseDateStr(
issue_results['cover_date'])
if volume_results['publisher'] is not None:
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.comments = self.cleanup_html(

View File

@ -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()))

View File

@ -76,6 +76,7 @@ class RenameWindow(QtWidgets.QDialog):
if md.isEmpty:
md = ca.metadataFromFilename(self.settings.parse_scan_info)
self.renamer.setMetadata(md)
self.renamer.move = self.settings.rename_move_dir
new_name = self.renamer.determineName(ca.path, ext=new_ext)
row = self.twList.rowCount()
@ -149,17 +150,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)

View File

@ -44,7 +44,7 @@ class ComicTaggerSettings:
@staticmethod
def haveOwnUnrarLib():
return os.path.exists(ComicTaggerSettings.defaultLibunrarPath())
@staticmethod
def baseDir():
if getattr(sys, 'frozen', None):
@ -119,10 +119,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
@ -167,15 +169,15 @@ class ComicTaggerSettings:
if self.rar_exe_path != "":
self.save()
if self.rar_exe_path != "":
# make sure rar program is now in the path for the rar class
# make sure rar program is now in the path for the rar class
utils.addtopath(os.path.dirname(self.rar_exe_path))
if self.haveOwnUnrarLib():
# We have a 'personal' copy of the unrar lib in the basedir, so
# don't search and change the setting
# NOTE: a manual edit of the settings file overrides this below
os.environ["UNRAR_LIB_PATH"] = self.defaultLibunrarPath()
elif self.unrar_lib_path == "":
# Priority is for unrar lib search is:
# 1. explicit setting in settings file
@ -187,11 +189,11 @@ class ComicTaggerSettings:
# look in some platform specific places:
if platform.system() == "Windows":
# Default location for the RARLab DLL installer
if (platform.architecture()[0] == '64bit' and
if (platform.architecture()[0] == '64bit' and
os.path.exists("C:\\Program Files (x86)\\UnrarDLL\\x64\\UnRAR64.dll")
):
self.unrar_lib_path = "C:\\Program Files (x86)\\UnrarDLL\\x64\\UnRAR64.dll"
elif (platform.architecture()[0] == '32bit' and
elif (platform.architecture()[0] == '32bit' and
os.path.exists("C:\\Program Files\\UnrarDLL\\UnRAR.dll")
):
self.unrar_lib_path = "C:\\Program Files\\UnrarDLL\\UnRAR.dll"
@ -203,14 +205,14 @@ class ComicTaggerSettings:
if os.path.exists("/usr/local/lib/libunrar.so"):
self.unrar_lib_path = "/usr/local/lib/libunrar.so"
elif os.path.exists("/usr/lib/libunrar.so"):
self.unrar_lib_path = "/usr/lib/libunrar.so"
self.unrar_lib_path = "/usr/lib/libunrar.so"
if self.unrar_lib_path != "":
self.save()
if self.unrar_lib_path != "":
# This needs to occur before the unrar module is loaded for the first time
os.environ["UNRAR_LIB_PATH"] = self.unrar_lib_path
os.environ["UNRAR_LIB_PATH"] = self.unrar_lib_path
def reset(self):
os.unlink(self.settings_file)
@ -359,6 +361,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(
@ -525,6 +531,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')

View File

@ -44,13 +44,13 @@ linuxRarHelp = """
<a href="https://www.rarlab.com/download.htm">here</a></span>,
and install in your path. </p></body></html>
"""
macRarHelp = """
<html><head/><body><p>To write to CBR/RAR archives,
you will need the rar tool. The easiest way to get this is
to install <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://brew.sh/">homebrew</a></span>.
</p>Once homebrew is installed, run: <b>brew install caskroom/cask/rar</b></body></html>
</p>Once homebrew is installed, run: <b>brew install caskroom/cask/rar</b></body></html>
"""
windowsUnrarHelp = """
@ -70,7 +70,7 @@ linuxUnrarHelp = """
<a href="https://www.rarlab.com/rar_add.htm">here</a></span>
for the UnRAR source (which is easy to compile on Linux). </p></body></html>
"""
macUnrarHelp = """
<html><head/><body><p>To read CBR/RAR archives,
you will need the unrar library. The easiest way to get this is
@ -92,13 +92,13 @@ class SettingsWindow(QtWidgets.QDialog):
self.settings = settings
self.name = "Settings"
self.priorUnrarLibPath = self.settings.unrar_lib_path
if self.settings.haveOwnUnrarLib():
# We have our own unrarlib, so no need for this GUI
self.grpBoxUnrar.hide()
if platform.system() == "Windows":
self.lblRarHelp.setText(windowsRarHelp)
self.lblUnrarHelp.setText(windowsUnrarHelp)
@ -111,7 +111,7 @@ class SettingsWindow(QtWidgets.QDialog):
# Mac file dialog hides "/usr" and others, so allow user to type
self.leUnrarLibPath.setReadOnly(False)
self.leRarExePath.setReadOnly(False)
self.lblRarHelp.setText(macRarHelp)
self.lblUnrarHelp.setText(macUnrarHelp)
self.name = "Preferences"
@ -152,6 +152,7 @@ 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 settingsToForm(self):
@ -205,12 +206,15 @@ 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):
# Copy values from form to settings and save
self.settings.rar_exe_path = str(self.leRarExePath.text())
# Don't accept the form info if we have our own unrar lib
if not self.settings.haveOwnUnrarLib():
self.settings.unrar_lib_path = str(self.leUnrarLibPath.text())
@ -222,7 +226,7 @@ class SettingsWindow(QtWidgets.QDialog):
if self.settings.unrar_lib_path:
os.environ["UNRAR_LIB_PATH"] = self.settings.unrar_lib_path
# This doesn't do anything... we need to restart!
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
self.leNameLengthDeltaThresh.setText("0")
@ -258,6 +262,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)
@ -266,7 +272,7 @@ class SettingsWindow(QtWidgets.QDialog):
QtWidgets.QMessageBox.information(
self, "UnRar Library Change",
"ComicTagger will need to be restarted for changes to take effect.")
def selectRar(self):
self.selectFile(self.leRarExePath, "RAR")
@ -317,10 +323,23 @@ class SettingsWindow(QtWidgets.QDialog):
dialog.setWindowTitle("Find " + name + " program")
else:
dialog.setWindowTitle("Find " + name + " library")
if (dialog.exec_()):
fileList = dialog.selectedFiles()
control.setText(str(fileList[0]))
def showRenameTab(self):
self.tabWidget.setCurrentIndex(5)
def showTemplateHelp(self):
TemplateHelpWin = TemplateHelpWindow(self)
TemplateHelpWin.exec_()
class TemplateHelpWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(TemplateHelpWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('TemplateHelp.ui'), self)

View File

@ -25,6 +25,7 @@ import json
import webbrowser
import re
import pickle
import datetime
#import signal
from PyQt5 import QtCore, QtGui, QtWidgets, uic
@ -51,6 +52,7 @@ from .cbltransformer import CBLTransformer
from .renamewindow import RenameWindow
from .exportwindow import ExportWindow, ExportConflictOpts
from .issueidentifier import IssueIdentifier
from .issuestring import IssueString
from .autotagstartwindow import AutoTagStartWindow
from .autotagprogresswindow import AutoTagProgressWindow
from .autotagmatchwindow import AutoTagMatchWindow
@ -86,7 +88,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
@ -183,7 +185,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.resetApp()
# set up some basic field validators
validator = QtGui.QIntValidator(1900, 2099, self)
validator = QtGui.QIntValidator(1900, datetime.date.today().year + 15, self)
self.lePubYear.setValidator(validator)
validator = QtGui.QIntValidator(1, 12, self)
@ -295,7 +297,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.setWindowIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('app.png')))
if self.comic_archive is None:
self.setWindowTitle(self.appName)
else:
@ -611,7 +613,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
)
local = QUrl(str(absCFURL[0])).toLocalFile()
return local
def dropEvent(self, event):
@ -790,14 +792,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
for child in widget.children():
self.clearChildren(child)
# Copy all of the metadata object into to the form.
# Merging of metadata should be done via the overlay function
def metadataToForm(self):
# copy the the metadata object into to the form
# helper func
def assignText(field, value):
if value is not None:
field.setText(str(value))
md = self.metadata
assignText(self.leSeries, md.series)
@ -839,23 +839,33 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.cbMaturityRating.setEditText(md.maturityRating)
else:
self.cbMaturityRating.setCurrentIndex(i)
else:
self.cbMaturityRating.setCurrentIndex(0)
if md.language is not None:
i = self.cbLanguage.findData(md.language)
self.cbLanguage.setCurrentIndex(i)
else:
self.cbLanguage.setCurrentIndex(0)
if md.country is not None:
i = self.cbCountry.findText(md.country)
self.cbCountry.setCurrentIndex(i)
else:
self.cbCountry.setCurrentIndex(0)
if md.manga is not None:
i = self.cbManga.findData(md.manga)
self.cbManga.setCurrentIndex(i)
else:
self.cbManga.setCurrentIndex(0)
if md.blackAndWhite is not None and md.blackAndWhite:
if md.blackAndWhite != None and md.blackAndWhite:
self.cbBW.setChecked(True)
else:
self.cbBW.setChecked(False)
assignText(self.teTags, utils.listToString(md.tags))
self.teTags.setText(utils.listToString(md.tags))
# !!! Should we clear the credits table or just avoid duplicates?
while self.twCredits.rowCount() > 0:
@ -914,58 +924,47 @@ class TaggerWindow(QtWidgets.QMainWindow):
return False
def formToMetadata(self):
# helper func
def xlate(data, type_str):
s = "{0}".format(data).strip()
if s == "":
return None
elif type_str == "str":
return s
else:
return int(s)
# copy the data from the form into the metadata
md = self.metadata
md.series = xlate(self.leSeries.text(), "str")
md.issue = xlate(self.leIssueNum.text(), "str")
md.issueCount = xlate(self.leIssueCount.text(), "int")
md.volume = xlate(self.leVolumeNum.text(), "int")
md.volumeCount = xlate(self.leVolumeCount.text(), "int")
md.title = xlate(self.leTitle.text(), "str")
md.publisher = xlate(self.lePublisher.text(), "str")
md.month = xlate(self.lePubMonth.text(), "int")
md.year = xlate(self.lePubYear.text(), "int")
md.day = xlate(self.lePubDay.text(), "int")
md.genre = xlate(self.leGenre.text(), "str")
md.imprint = xlate(self.leImprint.text(), "str")
md.comments = xlate(self.teComments.toPlainText(), "str")
md.notes = xlate(self.teNotes.toPlainText(), "str")
md.criticalRating = xlate(self.leCriticalRating.text(), "int")
md.maturityRating = xlate(self.cbMaturityRating.currentText(), "str")
md = GenericMetadata()
md.isEmpty = False
md.alternateNumber = IssueString(self.leAltIssueNum.text()).asString()
md.issue = IssueString(self.leIssueNum.text()).asString()
md.issueCount = utils.xlate(self.leIssueCount.text(), True)
md.volume = utils.xlate(self.leVolumeNum.text(), True)
md.volumeCount = utils.xlate(self.leVolumeCount.text(), True)
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.criticalRating = utils.xlate(self.leCriticalRating.text(), True)
md.alternateCount = utils.xlate(self.leAltIssueCount.text(), True)
md.storyArc = xlate(self.leStoryArc.text(), "str")
md.scanInfo = xlate(self.leScanInfo.text(), "str")
md.seriesGroup = xlate(self.leSeriesGroup.text(), "str")
md.alternateSeries = xlate(self.leAltSeries.text(), "str")
md.alternateNumber = xlate(self.leAltIssueNum.text(), "int")
md.alternateCount = xlate(self.leAltIssueCount.text(), "int")
md.webLink = xlate(self.leWebLink.text(), "str")
md.characters = xlate(self.teCharacters.toPlainText(), "str")
md.teams = xlate(self.teTeams.toPlainText(), "str")
md.locations = xlate(self.teLocations.toPlainText(), "str")
md.series = self.leSeries.text()
md.title = self.leTitle.text()
md.publisher = self.lePublisher.text()
md.genre = self.leGenre.text()
md.imprint = self.leImprint.text()
md.comments = self.teComments.toPlainText()
md.notes = self.teNotes.toPlainText()
md.maturityRating = self.cbMaturityRating.currentText()
md.format = xlate(self.cbFormat.currentText(), "str")
md.country = xlate(self.cbCountry.currentText(), "str")
md.storyArc = self.leStoryArc.text()
md.scanInfo = self.leScanInfo.text()
md.seriesGroup = self.leSeriesGroup.text()
md.alternateSeries = self.leAltSeries.text()
md.webLink = self.leWebLink.text()
md.characters = self.teCharacters.toPlainText()
md.teams = self.teTeams.toPlainText()
md.locations = self.teLocations.toPlainText()
langiso = self.cbLanguage.itemData(self.cbLanguage.currentIndex())
md.language = xlate(langiso, "str")
md.format = self.cbFormat.currentText()
md.country = self.cbCountry.currentText()
manga_code = self.cbManga.itemData(self.cbManga.currentIndex())
md.manga = xlate(manga_code, "str")
md.language = utils.xlate(self.cbLanguage.itemData(self.cbLanguage.currentIndex()))
md.manga = utils.xlate(self.cbManga.itemData(self.cbManga.currentIndex()))
# Make a list from the coma delimited tags string
tmp = xlate(self.teTags.toPlainText(), "str")
tmp = self.teTags.toPlainText()
if tmp is not None:
def striplist(l):
return([x.strip() for x in l])
@ -989,6 +988,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
row += 1
md.pages = self.pageListEditor.getPageList()
self.metadata = md
def useFilename(self):
if self.comic_archive is not None:
@ -1198,11 +1198,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)

View File

@ -0,0 +1,105 @@
<?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>&lt;html&gt;
&lt;head/&gt;
&lt;body&gt;
&lt;h1 style=&quot;text-align: center&quot;&gt;Template help&lt;/h1&gt;
&lt;p&gt;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
&lt;a href=&quot;https://docs.python.org/3/library/string.html#format-string-syntax&quot;&gt;Python 3 documentation&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;Accepts the following variables:
{isEmpty}&#009;(boolean)
{tagOrigin}&#009;(string)
{series}&#009;&#009;(string)
{issue}&#009;&#009;(string)
{title}&#009;&#009;(string)
{publisher}&#009;(string)
{month}&#009;&#009;(integer)
{year}&#009;&#009;(integer)
{day}&#009;&#009;(integer)
{issueCount}&#009;(integer)
{volume}&#009;(integer)
{genre}&#009;&#009;(string)
{language}&#009;(string)
{comments}&#009;(string)
{volumeCount}&#009;(integer)
{criticalRating}&#009;(string)
{country}&#009;(string)
{alternateSeries}&#009;(string)
{alternateinteger}&#009;(string)
{alternateCount}&#009;(integer)
{imprint}&#009;&#009;(string)
{notes}&#009;&#009;(string)
{webLink}&#009;(string)
{format}&#009;&#009;(string)
{manga}&#009;&#009;(string)
{blackAndWhite}&#009;(boolean)
{pageCount}&#009;(integer)
{maturityRating}&#009;(string)
{storyArc}&#009;(string)
{seriesGroup}&#009;(string)
{scanInfo}&#009;(string)
{characters}&#009;(string)
{teams}&#009;&#009;(string)
{locations}&#009;(string)
{credits}&#009;&#009;(list of dict({&apos;role&apos;: &apos;str&apos;, &apos;person&apos;: &apos;str&apos;, &apos;primary&apos;: boolean}))
{tags}&#009;&#009;(list of str)
{pages}&#009;&#009;(list of dict({&apos;Image&apos;: &apos;str(int)&apos;, &apos;Type&apos;: &apos;str&apos;}))
CoMet-only items:
{price}&#009;&#009;(integer)
{isVersionOf}&#009;(boolean)
{rights}&#009;&#009;(string)
{identifier}&#009;(string)
{lastMark}&#009;(string)
{coverImage}&#009;(string)
Examples:
{series} {issue} ({year})
Spider-Geddon 1 (2018)
{series} #{issue} - {title}
Spider-Geddon #1 - New Players; Check In
&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -503,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>
@ -512,31 +512,79 @@
<item row="1" column="1">
<widget class="QLineEdit" name="leRenameTemplate">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The template for the new filename. Accepts the following variables:&lt;/p&gt;&lt;p&gt;%series%&lt;br/&gt;%issue%&lt;br/&gt;%volume%&lt;br/&gt;%issuecount%&lt;br/&gt;%year%&lt;br/&gt;%month%&lt;br/&gt;%month_name%&lt;br/&gt;%publisher%&lt;br/&gt;%title%&lt;br/&gt;
%genre%&lt;br/&gt;
%language_code%&lt;br/&gt;
%criticalrating%&lt;br/&gt;
%alternateseries%&lt;br/&gt;
%alternatenumber%&lt;br/&gt;
%alternatecount%&lt;br/&gt;
%imprint%&lt;br/&gt;
%format%&lt;br/&gt;
%maturityrating%&lt;br/&gt;
%storyarc%&lt;br/&gt;
%seriesgroup%&lt;br/&gt;
%scaninfo%
&lt;/p&gt;&lt;p&gt;Examples:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% %issue% (%year%)&lt;/span&gt;&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% #%issue% - %title%&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;pre&gt;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} (number)
{year} (number)
{day} (number)
{date} (Date)
{issueCount} (number)
{volume} (number)
{genre} (string)
{language} (string)
{comments} (string)
{volumeCount} (number)
{criticalRating} (string)
{country} (string)
{alternateSeries} (string)
{alternateNumber} (string)
{alternateCount} (number)
{imprint} (string)
{notes} (string)
{webLink} (string)
{format} (string)
{manga} (string)
{blackAndWhite} (boolean)
{pageCount} (number)
{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} (number)
{isVersionOf} (boolean)
{rights} (string)
{identifier} (string)
{lastMark} (string)
{coverImage} (string)
Examples:
{series} {issue} ({year})
{series} #{issue} - {title}
&lt;/pre&gt;</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">
@ -555,7 +603,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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&amp;quot;Smart Text Cleanup&amp;quot; &lt;/span&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -565,13 +613,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>

View File

@ -5,4 +5,5 @@ natsort==3.5.2
PyPDF2==1.24
pillow>=4.3.0
PyQt5>=5.10.1
git+https://github.com/pyinstaller/pyinstaller@develop
git+https://github.com/pyinstaller/pyinstaller@develop
pathvalidate