From 028b728d82257a48a4a3c5767c01fca22bc933c5 Mon Sep 17 00:00:00 2001
From: Timmy Welch
Date: Mon, 18 Apr 2022 18:44:20 -0700
Subject: [PATCH 1/3] 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.
---
comicapi/genericmetadata.py | 90 ++++++++++++++
comictaggerlib/cli.py | 27 ++++-
comictaggerlib/filerenamer.py | 178 +++++++++++++++-------------
comictaggerlib/renamewindow.py | 29 ++++-
comictaggerlib/settings.py | 14 ++-
comictaggerlib/settingswindow.py | 44 +++++++
comictaggerlib/ui/TemplateHelp.ui | 110 +++++++++++++++++
comictaggerlib/ui/settingswindow.ui | 107 ++++++++++++++---
requirements.txt | 1 +
9 files changed, 484 insertions(+), 116 deletions(-)
create mode 100644 comictaggerlib/ui/TemplateHelp.ui
diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py
index 5045ba0..ed8baf0 100644
--- a/comicapi/genericmetadata.py
+++ b/comicapi/genericmetadata.py
@@ -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
diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py
index a386095..47d1ecb 100644
--- a/comictaggerlib/cli.py
+++ b/comictaggerlib/cli.py
@@ -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)"
diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py
index e35aa63..3a79e44 100644
--- a/comictaggerlib/filerenamer.py
+++ b/comictaggerlib/filerenamer.py
@@ -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()))
diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py
index bb02a84..d452d92 100644
--- a/comictaggerlib/renamewindow.py
+++ b/comictaggerlib/renamewindow.py
@@ -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!"
+ "
{}
"
+ "Please 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".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)
diff --git a/comictaggerlib/settings.py b/comictaggerlib/settings.py
index 0af8402..307d439 100644
--- a/comictaggerlib/settings.py
+++ b/comictaggerlib/settings.py
@@ -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")
diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py
index 904ee36..cba663b 100644
--- a/comictaggerlib/settingswindow.py
+++ b/comictaggerlib/settingswindow.py
@@ -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!"
+ "
{}
"
+ "Please 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".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)
diff --git a/comictaggerlib/ui/TemplateHelp.ui b/comictaggerlib/ui/TemplateHelp.ui
new file mode 100644
index 0000000..0a692f1
--- /dev/null
+++ b/comictaggerlib/ui/TemplateHelp.ui
@@ -0,0 +1,110 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 702
+ 452
+
+
+
+ Template Help
+
+
+ true
+
+
+
+ 0
+
+
+ 2
+
+
+ 2
+
+ -
+
+
+ true
+
+
+ <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>
+
+
+ true
+
+
+
+
+
+
+
+
diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui
index c64e7ce..7ca3dbf 100644
--- a/comictaggerlib/ui/settingswindow.ui
+++ b/comictaggerlib/ui/settingswindow.ui
@@ -547,7 +547,7 @@
QFormLayout::AllNonFixedFieldsGrow
-
-
+
Template:
@@ -556,31 +556,80 @@
-
- <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>
+ <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({'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>
-
-
+
+
+ Template Help
+
+
+
+ -
+
Issue # Zero Padding
- -
+
-
@@ -599,7 +648,7 @@
- -
+
-
<html><head/><body><p><span style=" font-weight:600;">"Smart Text Cleanup" </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>
@@ -609,13 +658,33 @@
- -
+
-
Change Extension Based On Archive Type
+ -
+
+
+ If checked moves files to specified folder
+
+
+ Move files when renaming
+
+
+
+ -
+
+
+ Destination Directory:
+
+
+
+ -
+
+
diff --git a/requirements.txt b/requirements.txt
index e929a15..c6e833d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,5 +2,6 @@ beautifulsoup4 >= 4.1
natsort>=8.1.0
pillow>=4.3.0
requests==2.*
+pathvalidate
pycountry
py7zr
From 6cccf22d54e71dfc7bf208eb16228597eb20c5ee Mon Sep 17 00:00:00 2001
From: Timmy Welch
Date: Mon, 18 Apr 2022 18:59:17 -0700
Subject: [PATCH 2/3] Allow switching between old and new rename templates
Show a message dialog explaining that there is a new template format
Add a dynamic label to show the effect of a rename
Add tests for FileRenamer
Remove the filename parameter from the determine_name function
---
comicapi/genericmetadata.py | 9 ++
comictaggerlib/cli.py | 18 +++-
comictaggerlib/filerenamer.py | 148 ++++++++++++++++++++++++++--
comictaggerlib/renamewindow.py | 10 +-
comictaggerlib/settings.py | 14 ++-
comictaggerlib/settingswindow.py | 139 +++++++++++++++++++++++---
comictaggerlib/taggerwindow.py | 9 ++
comictaggerlib/ui/settingswindow.ui | 98 +++++++-----------
tests/filenames.py | 12 +++
tests/test_rename.py | 21 ++++
10 files changed, 384 insertions(+), 94 deletions(-)
create mode 100644 tests/test_rename.py
diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py
index ed8baf0..280dc84 100644
--- a/comicapi/genericmetadata.py
+++ b/comicapi/genericmetadata.py
@@ -259,6 +259,15 @@ class GenericMetadata:
if not found:
self.credits.append(credit)
+ def get_primary_credit(self, role):
+ primary = ""
+ for credit in self.credits:
+ if (primary == "" and credit["role"].lower() == role.lower()) or (
+ credit["role"].lower() == role.lower() and credit["primary"]
+ ):
+ primary = credit["person"]
+ return primary
+
def __str__(self):
vals = []
if self.is_empty:
diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py
index 47d1ecb..25f90f8 100644
--- a/comictaggerlib/cli.py
+++ b/comictaggerlib/cli.py
@@ -27,7 +27,7 @@ from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.cbltransformer import CBLTransformer
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
-from comictaggerlib.filerenamer import FileRenamer
+from comictaggerlib.filerenamer import FileRenamer, FileRenamer2
from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.resulttypes import MultipleMatch, OnlineMatchResults
from comictaggerlib.settings import ComicTaggerSettings
@@ -155,6 +155,13 @@ def cli_mode(opts, settings):
logger.error("You must specify at least one filename. Use the -h option for more info")
return
+ if not settings.hide_rename_message:
+ print(
+ "There is a new rename template format available. "
+ "Please use the settings window to enable and test if you use this feature.\n\n"
+ "The old rename template format will be removed in the next release, "
+ "please reference the template help button in the settings or https://github.com/comictagger/comictagger/wiki/UserGuide#rename",
+ )
match_results = OnlineMatchResults()
for f in opts.file_list:
@@ -445,20 +452,23 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
elif ca.is_rar():
new_ext = ".cbr"
- renamer = FileRenamer(md)
+ if settings.rename_new_renamer:
+ renamer = FileRenamer2(md)
+ else:
+ renamer = FileRenamer(md)
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
try:
- new_name = renamer.determine_name(filename, ext=new_ext)
+ new_name = renamer.determine_name(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",
+ "https://docs.python.org/3/library/string.html#format-string-syntax".format(e),
file=sys.stderr,
)
return
diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py
index 3a79e44..742a321 100644
--- a/comictaggerlib/filerenamer.py
+++ b/comictaggerlib/filerenamer.py
@@ -14,9 +14,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import calendar
import datetime
import logging
import os
+import re
import string
from pathvalidate import sanitize_filepath
@@ -27,6 +29,122 @@ from comicapi.issuestring import IssueString
logger = logging.getLogger(__name__)
+class FileRenamer:
+ def __init__(self, metadata):
+ self.template = "%series% v%volume% #%issue% (of %issuecount%) (%year%)"
+ self.smart_cleanup = True
+ self.issue_zero_padding = 3
+ self.metadata = metadata
+
+ def set_metadata(self, metadata: GenericMetadata):
+ self.metadata = metadata
+
+ def set_issue_zero_padding(self, count):
+ self.issue_zero_padding = count
+
+ def set_smart_cleanup(self, on):
+ self.smart_cleanup = on
+
+ 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, ext):
+
+ 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%")
+
+ 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%")
+
+ 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%")
+
+ 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%")
+
+ 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())
+
+ 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
+
+
class MetadataFormatter(string.Formatter):
def __init__(self, smart_cleanup=False):
super().__init__()
@@ -53,6 +171,7 @@ class MetadataFormatter(string.Formatter):
lstrip = False
# if there's a field, output it
if field_name is not None:
+ field_name = field_name.lower()
# this is some markup, find the object and do
# the formatting
@@ -96,9 +215,9 @@ class MetadataFormatter(string.Formatter):
return "".join(result), auto_arg_index
-class FileRenamer:
+class FileRenamer2:
def __init__(self, metadata):
- self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issueCount}) ({year})"
+ self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})"
self.smart_cleanup = True
self.issue_zero_padding = 3
self.metadata = metadata
@@ -116,7 +235,7 @@ class FileRenamer:
def set_template(self, template: str):
self.template = template
- def determine_name(self, filename, ext=None):
+ def determine_name(self, ext):
class Default(dict):
def __missing__(self, key):
return "{" + key + "}"
@@ -132,19 +251,28 @@ class FileRenamer:
new_name = ""
fmt = MetadataFormatter(self.smart_cleanup)
+ md_dict = vars(md)
+ for role in ["writer", "penciller", "inker", "colorist", "letterer", "cover artist", "editor"]:
+ md_dict[role] = md.get_primary_credit(role)
+
+ if (isinstance(md.month, int) or isinstance(md.month, str) and md.month.isdigit()) and 0 < int(md.month) < 13:
+ md_dict["month_name"] = calendar.month_name[int(md.month)]
+ md_dict["month_abbr"] = calendar.month_abbr[int(md.month)]
+ else:
+ print(md.month)
+ md_dict["month_name"] = ""
+ md_dict["month_abbr"] = ""
+
for Component in path_components:
new_name = os.path.join(
- new_name, fmt.vformat(Component, args=[], kwargs=Default(vars(md))).replace("/", "-")
+ new_name, fmt.vformat(Component, args=[], kwargs=Default(md_dict)).replace("/", "-")
)
- 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(":", "-")
+ # # some tweaks to keep various filesystems happy
+ # new_name = new_name.replace(": ", " - ")
+ # new_name = new_name.replace(":", "-")
# remove padding
md.issue = IssueString(md.issue).as_string()
diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py
index d452d92..a7102f5 100644
--- a/comictaggerlib/renamewindow.py
+++ b/comictaggerlib/renamewindow.py
@@ -23,7 +23,7 @@ from PyQt5 import QtCore, QtWidgets, uic
import comicapi.comicarchive
from comicapi import utils
from comicapi.comicarchive import MetaDataStyle
-from comictaggerlib.filerenamer import FileRenamer
+from comictaggerlib.filerenamer import FileRenamer, FileRenamer2
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.settingswindow import SettingsWindow
from comictaggerlib.ui.qtutils import center_window_on_parent
@@ -52,7 +52,11 @@ class RenameWindow(QtWidgets.QDialog):
self.rename_list = []
self.btnSettings.clicked.connect(self.modify_settings)
- self.renamer = FileRenamer(None)
+ if self.settings.rename_new_renamer:
+ self.renamer = FileRenamer2(None)
+ else:
+ self.renamer = FileRenamer(None)
+
self.config_renamer()
self.do_preview()
@@ -85,7 +89,7 @@ class RenameWindow(QtWidgets.QDialog):
self.renamer.move = self.settings.rename_move_dir
try:
- new_name = self.renamer.determine_name(ca.path, ext=new_ext)
+ new_name = self.renamer.determine_name(new_ext)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
diff --git a/comictaggerlib/settings.py b/comictaggerlib/settings.py
index 307d439..12f159e 100644
--- a/comictaggerlib/settings.py
+++ b/comictaggerlib/settings.py
@@ -83,6 +83,7 @@ class ComicTaggerSettings:
self.show_disclaimer = True
self.dont_notify_about_this_version = ""
self.ask_about_usage_stats = True
+ self.hide_rename_message = False
# filename parsing settings
self.parse_scan_info = True
@@ -110,12 +111,13 @@ class ComicTaggerSettings:
self.apply_cbl_transform_on_bulk_operation = False
# Rename settings
- self.rename_template = "{publisher}/{series}/{series} #{issue} - {title} ({year})"
+ self.rename_template = "%series% #%issue% (%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
+ self.rename_new_renamer = False
# Auto-tag stickies
self.save_on_low_confidence = False
@@ -158,6 +160,7 @@ class ComicTaggerSettings:
self.show_disclaimer = True
self.dont_notify_about_this_version = ""
self.ask_about_usage_stats = True
+ self.hide_rename_message = False
# filename parsing settings
self.parse_scan_info = True
@@ -185,12 +188,13 @@ class ComicTaggerSettings:
self.apply_cbl_transform_on_bulk_operation = False
# Rename settings
- self.rename_template = "{publisher}/{series}/{series} #{issue} - {title} ({year})"
+ self.rename_template = "%series% #%issue% (%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
+ self.rename_new_renamer = False
# Auto-tag stickies
self.save_on_low_confidence = False
@@ -293,6 +297,8 @@ class ComicTaggerSettings:
self.dont_notify_about_this_version = self.config.get("dialogflags", "dont_notify_about_this_version")
if self.config.has_option("dialogflags", "ask_about_usage_stats"):
self.ask_about_usage_stats = self.config.getboolean("dialogflags", "ask_about_usage_stats")
+ if self.config.has_option("dialogflags", "hide_rename_message"):
+ self.hide_rename_message = self.config.getboolean("dialogflags", "hide_rename_message")
if self.config.has_option("comicvine", "use_series_start_as_volume"):
self.use_series_start_as_volume = self.config.getboolean("comicvine", "use_series_start_as_volume")
@@ -352,6 +358,8 @@ class ComicTaggerSettings:
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("rename", "rename_new_renamer"):
+ self.rename_new_renamer = self.config.getboolean("rename", "rename_new_renamer")
if self.config.has_option("autotag", "save_on_low_confidence"):
self.save_on_low_confidence = self.config.getboolean("autotag", "save_on_low_confidence")
@@ -408,6 +416,7 @@ class ComicTaggerSettings:
self.config.set("dialogflags", "show_disclaimer", self.show_disclaimer)
self.config.set("dialogflags", "dont_notify_about_this_version", self.dont_notify_about_this_version)
self.config.set("dialogflags", "ask_about_usage_stats", self.ask_about_usage_stats)
+ self.config.set("dialogflags", "hide_rename_message", self.hide_rename_message)
if not self.config.has_section("filenameparser"):
self.config.add_section("filenameparser")
@@ -451,6 +460,7 @@ class ComicTaggerSettings:
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)
+ self.config.set("rename", "rename_new_renamer", self.rename_new_renamer)
if not self.config.has_section("autotag"):
self.config.add_section("autotag")
diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py
index cba663b..9115f12 100644
--- a/comictaggerlib/settingswindow.py
+++ b/comictaggerlib/settingswindow.py
@@ -17,6 +17,7 @@
import logging
import os
import platform
+import re
from PyQt5 import QtCore, QtGui, QtWidgets, uic
@@ -24,7 +25,7 @@ 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.filerenamer import FileRenamer, FileRenamer2
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.settings import ComicTaggerSettings
@@ -56,6 +57,82 @@ macRarHelp = """
Once homebrew is installed, run: brew install caskroom/cask/rar