Improve file renaming

Moves to Python format strings for renaming, handles directory
structures, moving of files to a destination directory, sanitizes
file paths with pathvalidate and takes a different approach to
smart filename cleanup using the Python string.Formatter class

Moving to Python format strings means we can point to python
documentation for syntax and all we have to do is document the
properties and types that are attached to the GenericMetadata class.

Switching to pathvalidate allows comictagger to more simply handle both
directories and symbols in filenames.

The only changes to the string.Formatter class is:
1. format_field returns
an empty string if the value is none or an empty string regardless of
the format specifier.
2. _vformat drops the previous literal text if the field value
is an empty string and lstrips the following literal text of closing
special characters.
This commit is contained in:
Timmy Welch 2022-04-18 18:44:20 -07:00
parent 23f323f52d
commit 028b728d82
9 changed files with 484 additions and 116 deletions

View File

@ -332,3 +332,93 @@ class GenericMetadata:
outstr += fmt_str.format(i[0] + ":", i[1])
return outstr
md_test = GenericMetadata()
md_test.is_empty = False
md_test.tag_origin = None
md_test.series = "Cory Doctorow's Futuristic Tales of the Here and Now"
md_test.issue = "1"
md_test.title = "Anda's Game"
md_test.publisher = "IDW Publishing"
md_test.month = 10
md_test.year = 2007
md_test.day = 1
md_test.issue_count = 6
md_test.volume = 1
md_test.genre = "Sci-Fi"
md_test.language = "en"
md_test.comments = (
"For 12-year-old Anda, getting paid real money to kill the characters of players who were cheating in her favorite online "
"computer game was a win-win situation. Until she found out who was paying her, and what those characters meant to the "
"livelihood of children around the world."
)
md_test.volume_count = None
md_test.critical_rating = None
md_test.country = None
md_test.alternate_series = "Tales"
md_test.alternate_number = "2"
md_test.alternate_count = 7
md_test.imprint = "craphound.com"
md_test.notes = "Tagged with ComicTagger 1.3.2a5 using info from Comic Vine on 2022-04-16 15:52:26. [Issue ID 140529]"
md_test.web_link = "https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4000-140529/"
md_test.format = "Series"
md_test.manga = "No"
md_test.black_and_white = None
md_test.page_count = 24
md_test.maturity_rating = "Everyone 10+"
md_test.story_arc = "Here and Now"
md_test.series_group = "Futuristic Tales"
md_test.scan_info = "(CC BY-NC-SA 3.0)"
md_test.characters = "Anda"
md_test.teams = "Fahrenheit"
md_test.locations = "lonely cottage"
md_test.credits = [
{"person": "Dara Naraghi", "role": "Writer"},
{"person": "Esteve Polls", "role": "Penciller"},
{"person": "Esteve Polls", "role": "Inker"},
{"person": "Neil Uyetake", "role": "Letterer"},
{"person": "Sam Kieth", "role": "Cover"},
{"person": "Ted Adams", "role": "Editor"},
]
md_test.tags = []
md_test.pages = [
{"Image": "0", "ImageHeight": "1280", "ImageSize": "195977", "ImageWidth": "800", "Type": "FrontCover"},
{"Image": "1", "ImageHeight": "2039", "ImageSize": "611993", "ImageWidth": "1327"},
{"Image": "2", "ImageHeight": "2039", "ImageSize": "783726", "ImageWidth": "1327"},
{"Image": "3", "ImageHeight": "2039", "ImageSize": "679584", "ImageWidth": "1327"},
{"Image": "4", "ImageHeight": "2039", "ImageSize": "788179", "ImageWidth": "1327"},
{"Image": "5", "ImageHeight": "2039", "ImageSize": "864433", "ImageWidth": "1327"},
{"Image": "6", "ImageHeight": "2039", "ImageSize": "765606", "ImageWidth": "1327"},
{"Image": "7", "ImageHeight": "2039", "ImageSize": "876427", "ImageWidth": "1327"},
{"Image": "8", "ImageHeight": "2039", "ImageSize": "852622", "ImageWidth": "1327"},
{"Image": "9", "ImageHeight": "2039", "ImageSize": "800205", "ImageWidth": "1327"},
{"Image": "10", "ImageHeight": "2039", "ImageSize": "746243", "ImageWidth": "1326"},
{"Image": "11", "ImageHeight": "2039", "ImageSize": "718062", "ImageWidth": "1327"},
{"Image": "12", "ImageHeight": "2039", "ImageSize": "532179", "ImageWidth": "1326"},
{"Image": "13", "ImageHeight": "2039", "ImageSize": "686708", "ImageWidth": "1327"},
{"Image": "14", "ImageHeight": "2039", "ImageSize": "641907", "ImageWidth": "1327"},
{"Image": "15", "ImageHeight": "2039", "ImageSize": "805388", "ImageWidth": "1327"},
{"Image": "16", "ImageHeight": "2039", "ImageSize": "668927", "ImageWidth": "1326"},
{"Image": "17", "ImageHeight": "2039", "ImageSize": "710605", "ImageWidth": "1327"},
{"Image": "18", "ImageHeight": "2039", "ImageSize": "761398", "ImageWidth": "1326"},
{"Image": "19", "ImageHeight": "2039", "ImageSize": "743807", "ImageWidth": "1327"},
{"Image": "20", "ImageHeight": "2039", "ImageSize": "552911", "ImageWidth": "1326"},
{"Image": "21", "ImageHeight": "2039", "ImageSize": "556827", "ImageWidth": "1327"},
{"Image": "22", "ImageHeight": "2039", "ImageSize": "675078", "ImageWidth": "1326"},
{
"Bookmark": "Interview",
"Image": "23",
"ImageHeight": "2032",
"ImageSize": "800965",
"ImageWidth": "1338",
"Type": "Letters",
},
]
md_test.price = None
md_test.is_version_of = None
md_test.rights = None
md_test.identifier = None
md_test.last_mark = None
md_test.cover_image = None

View File

@ -449,20 +449,35 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
renamer.set_template(settings.rename_template)
renamer.set_issue_zero_padding(settings.rename_issue_number_padding)
renamer.set_smart_cleanup(settings.rename_use_smart_string_cleanup)
renamer.move = settings.rename_move_dir
new_name = renamer.determine_name(ca.path, ext=new_ext)
if new_name == os.path.basename(ca.path):
logger.error(msg_hdr + "Filename is already good!")
try:
new_name = renamer.determine_name(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(ca.path))
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.rename(ca.path, new_abs_path)
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(filename, new_abs_path)
else:
suffix = " (dry-run, no change)"

View File

@ -17,7 +17,9 @@
import datetime
import logging
import os
import re
import string
from pathvalidate import sanitize_filepath
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
@ -25,12 +27,82 @@ from comicapi.issuestring import IssueString
logger = logging.getLogger(__name__)
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
fmt_obj = self.format_field(obj, format_spec)
if fmt_obj == "" and len(result) > 0 and self.smart_cleanup:
lstrip = True
result.pop()
result.append(fmt_obj)
return "".join(result), auto_arg_index
class FileRenamer:
def __init__(self, metadata):
self.template = "%series% v%volume% #%issue% (of %issuecount%) (%year%)"
self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issueCount}) ({year})"
self.smart_cleanup = True
self.issue_zero_padding = 3
self.metadata = metadata
self.move = False
def set_metadata(self, metadata: GenericMetadata):
self.metadata = metadata
@ -44,101 +116,39 @@ class FileRenamer:
def set_template(self, template: str):
self.template = template
def replace_token(self, text, value, token):
# helper func
def is_token(word):
return word[0] == "%" and word[-1:] == "%"
if value is not None:
return text.replace(token, str(value))
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 is_token(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)
return text.replace(token, "")
def determine_name(self, filename, ext=None):
class Default(dict):
def __missing__(self, key):
return "{" + key + "}"
md = self.metadata
new_name = self.template
new_name = self.replace_token(new_name, md.series, "%series%")
new_name = self.replace_token(new_name, md.volume, "%volume%")
# padding for issue
md.issue = IssueString(md.issue).as_string(pad=self.issue_zero_padding)
if md.issue is not None:
issue_str = IssueString(md.issue).as_string(pad=self.issue_zero_padding)
else:
issue_str = None
new_name = self.replace_token(new_name, issue_str, "%issue%")
template = self.template
new_name = self.replace_token(new_name, md.issue_count, "%issuecount%")
new_name = self.replace_token(new_name, md.year, "%year%")
new_name = self.replace_token(new_name, md.publisher, "%publisher%")
new_name = self.replace_token(new_name, md.title, "%title%")
new_name = self.replace_token(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")
new_name = self.replace_token(new_name, month_name, "%month_name%")
path_components = template.split(os.sep)
new_name = ""
new_name = self.replace_token(new_name, md.genre, "%genre%")
new_name = self.replace_token(new_name, md.language, "%language_code%")
new_name = self.replace_token(new_name, md.critical_rating, "%criticalrating%")
new_name = self.replace_token(new_name, md.alternate_series, "%alternateseries%")
new_name = self.replace_token(new_name, md.alternate_number, "%alternatenumber%")
new_name = self.replace_token(new_name, md.alternate_count, "%alternatecount%")
new_name = self.replace_token(new_name, md.imprint, "%imprint%")
new_name = self.replace_token(new_name, md.format, "%format%")
new_name = self.replace_token(new_name, md.maturity_rating, "%maturityrating%")
new_name = self.replace_token(new_name, md.story_arc, "%storyarc%")
new_name = self.replace_token(new_name, md.series_group, "%seriesgroup%")
new_name = self.replace_token(new_name, md.scan_info, "%scaninfo%")
fmt = MetadataFormatter(self.smart_cleanup)
for Component in path_components:
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(r"\(\s*[-:]*\s*\)", "", new_name)
new_name = re.sub(r"\[\s*[-:]*\s*]", "", new_name)
new_name = re.sub(r"{\s*[-:]*\s*}", "", new_name)
# remove duplicate spaces
new_name = " ".join(new_name.split())
# remove remove duplicate -, _,
new_name = re.sub(r"[-_]{2,}\s+", "-- ", new_name)
new_name = re.sub(r"(\s--)+", " --", new_name)
new_name = re.sub(r"(\s-)+", " -", new_name)
# remove dash or double dash at end of line
new_name = re.sub(r"[-]{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).as_string()
if self.move:
return sanitize_filepath(new_name.strip())
else:
return os.path.basename(sanitize_filepath(new_name.strip()))

View File

@ -82,7 +82,22 @@ class RenameWindow(QtWidgets.QDialog):
if md.is_empty:
md = ca.metadata_from_filename(self.settings.parse_scan_info)
self.renamer.set_metadata(md)
new_name = self.renamer.determine_name(ca.path, ext=new_ext)
self.renamer.move = self.settings.rename_move_dir
try:
new_name = self.renamer.determine_name(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)
@ -150,7 +165,13 @@ class RenameWindow(QtWidgets.QDialog):
center_window_on_parent(prog_dialog)
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!")
logger.info(item["new_name"], "Filename is already good!")
continue
@ -158,9 +179,7 @@ class RenameWindow(QtWidgets.QDialog):
if not item["archive"].is_writable(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

@ -110,10 +110,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
@ -183,10 +185,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
@ -344,6 +348,10 @@ class ComicTaggerSettings:
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("autotag", "save_on_low_confidence")
@ -441,6 +449,8 @@ class ComicTaggerSettings:
self.config.set("rename", "rename_issue_number_padding", self.rename_issue_number_padding)
self.config.set("rename", "rename_use_smart_string_cleanup", self.rename_use_smart_string_cleanup)
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

@ -21,8 +21,10 @@ import platform
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi import utils
from comicapi.genericmetadata import md_test
from comictaggerlib.comicvinecacher import ComicVineCacher
from comictaggerlib.comicvinetalker import ComicVineTalker
from comictaggerlib.filerenamer import FileRenamer
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.settings import ComicTaggerSettings
@ -111,6 +113,14 @@ class SettingsWindow(QtWidgets.QDialog):
self.btnClearCache.clicked.connect(self.clear_cache)
self.btnResetSettings.clicked.connect(self.reset_settings)
self.btnTestKey.clicked.connect(self.test_api_key)
self.btnTemplateHelp.clicked.connect(self.show_template_help)
def config_renamer(self):
self.renamer = FileRenamer(md_test)
self.renamer.set_template(str(self.leRenameTemplate.text()))
self.renamer.set_issue_zero_padding(self.settings.rename_issue_number_padding)
self.renamer.set_smart_cleanup(self.settings.rename_use_smart_string_cleanup)
def settings_to_form(self):
@ -166,9 +176,29 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxSmartCleanup.setCheckState(QtCore.Qt.CheckState.Checked)
if self.settings.rename_extension_based_on_archive:
self.cbxChangeExtension.setCheckState(QtCore.Qt.CheckState.Checked)
if self.settings.rename_move_dir:
self.cbxMoveFiles.setCheckState(QtCore.Qt.CheckState.Checked)
self.leDirectory.setText(self.settings.rename_dir)
def accept(self):
self.config_renamer()
try:
new_name = self.renamer.determine_name("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())
@ -213,6 +243,8 @@ class SettingsWindow(QtWidgets.QDialog):
self.settings.rename_issue_number_padding = int(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)
@ -262,3 +294,15 @@ class SettingsWindow(QtWidgets.QDialog):
def show_rename_tab(self):
self.tabWidget.setCurrentIndex(5)
def show_template_help(self):
template_help_win = TemplateHelpWindow(self)
template_help_win.setModal(False)
template_help_win.show()
class TemplateHelpWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(TemplateHelpWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("TemplateHelp.ui"), self)

View File

@ -0,0 +1,110 @@
<?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="QTextEdit" name="textEdit">
<property name="readOnly">
<bool>true</bool>
</property>
<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;&#009;(boolean)
{tagOrigin}&#009;&#009;(string)
{series}&#009;&#009;(string)
{issue}&#009;&#009;(string)
{title}&#009;&#009;(string)
{publisher}&#009;&#009;(string)
{month}&#009;&#009;(integer)
{year}&#009;&#009;(integer)
{day}&#009;&#009;(integer)
{issueCount}&#009;(integer)
{volume}&#009;&#009;(integer)
{genre}&#009;&#009;(string)
{language}&#009;&#009;(string)
{comments}&#009;&#009;(string)
{volumeCount}&#009;(integer)
{criticalRating}&#009;(string)
{country}&#009;&#009;(string)
{alternateSeries}&#009;(string)
{alternateNumber}&#009;(string)
{alternateCount}&#009;(integer)
{imprint}&#009;&#009;(string)
{notes}&#009;&#009;(string)
{webLink}&#009;&#009;(string)
{format}&#009;&#009;(string)
{manga}&#009;&#009;(string)
{blackAndWhite}&#009;(boolean)
{pageCount}&#009;&#009;(integer)
{maturityRating}&#009;(string)
{storyArc}&#009;&#009;(string)
{seriesGroup}&#009;(string)
{scanInfo}&#009;&#009;(string)
{characters}&#009;(string)
{teams}&#009;&#009;(string)
{locations}&#009;&#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;(float)
{isVersionOf}&#009;(string)
{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" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -547,7 +547,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 +556,80 @@
<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} (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({&amp;apos;role&amp;apos;: &amp;apos;str&amp;apos;, &amp;apos;person&amp;apos;: &amp;apos;str&amp;apos;, &amp;apos;primary&amp;apos;: boolean}))
{tags} (list of str)
{pages} (list of dict({&amp;apos;Image&amp;apos;: &amp;apos;str(int)&amp;apos;, &amp;apos;Type&amp;apos;: &amp;apos;str&amp;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
&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">
@ -599,7 +648,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>
@ -609,13 +658,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

@ -2,5 +2,6 @@ beautifulsoup4 >= 4.1
natsort>=8.1.0
pillow>=4.3.0
requests==2.*
pathvalidate
pycountry
py7zr