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({&apos;role&apos;: &apos;str&apos;, &apos;person&apos;: &apos;str&apos;, &apos;primary&apos;: boolean})) +{tags} (list of str) +{pages} (list of dict({&apos;Image&apos;: &apos;str(int)&apos;, &apos;Type&apos;: &apos;str&apos;})) + +CoMet-only items: +{price} (float) +{isVersionOf} (string) +{rights} (string) +{identifier} (string) +{lastMark} (string) +{coverImage} (string) + +Examples: + +{series} {issue} ({year}) +Spider-Geddon 1 (2018) + +{series} #{issue} - {title} +Spider-Geddon #1 - New Players; Check In +</pre> - + + + Template Help + + + + + Issue # Zero Padding - + @@ -599,7 +648,7 @@ - + <html><head/><body><p><span style=" font-weight:600;">&quot;Smart Text Cleanup&quot; </span>will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.</p></body></html> @@ -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 """ +old_template_tooltip = """ +

The template for the new filename. Accepts the following variables:

%series%
%issue%
%volume%
%issuecount%
%year%
%month%
%month_name%
%publisher%
%title%
+%genre%
+%language_code%
+%criticalrating%
+%alternateseries%
+%alternatenumber%
+%alternatecount%
+%imprint%
+%format%
+%maturityrating%
+%storyarc%
+%seriesgroup%
+%scaninfo% +

Examples:

%series% %issue% (%year%)
%series% #%issue% - %title%

+""" + +new_template_tooltip = """ +
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
+
+""" + class SettingsWindow(QtWidgets.QDialog): def __init__(self, parent, settings): @@ -107,23 +184,55 @@ class SettingsWindow(QtWidgets.QDialog): validator = QtGui.QIntValidator(0, 99, self) self.leNameLengthDeltaThresh.setValidator(validator) - self.settings_to_form() + new_rename = self.settings.rename_new_renamer + self.cbxNewRenamer.setChecked(new_rename) + self.cbxMoveFiles.setEnabled(new_rename) + self.leDirectory.setEnabled(new_rename) + self.lblDirectory.setEnabled(new_rename) + if self.settings.rename_new_renamer: + self.leRenameTemplate.setToolTip(new_template_tooltip) + self.settings_to_form() + self.rename_error = None + self.rename_test(self.leRenameTemplate.text()) + + self.cbxNewRenamer.clicked.connect(self.new_rename_toggle) self.btnBrowseRar.clicked.connect(self.select_rar) 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) + self.leRenameTemplate.textEdited.connect(self.rename_test) - def config_renamer(self): + def new_rename_toggle(self): + new_rename = self.cbxNewRenamer.isChecked() + if new_rename: + self.leRenameTemplate.setText(re.sub(r"%(\w+)%", r"{\1}", self.leRenameTemplate.text())) + self.leRenameTemplate.setToolTip(new_template_tooltip) + else: + self.leRenameTemplate.setText(re.sub(r"{(\w+)}", r"%\1%", self.leRenameTemplate.text())) + self.leRenameTemplate.setToolTip(old_template_tooltip) + self.cbxMoveFiles.setEnabled(new_rename) + self.leDirectory.setEnabled(new_rename) + self.lblDirectory.setEnabled(new_rename) + self.rename_test(self.leRenameTemplate.text()) - 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 rename_test(self, template): + fr = FileRenamer(md_test) + if self.cbxNewRenamer.isChecked(): + fr = FileRenamer2(md_test) + fr.set_template(template) + fr.set_issue_zero_padding(int(self.leIssueNumPadding.text())) + fr.set_smart_cleanup(self.cbxSmartCleanup.isChecked()) + fr.move = self.cbxMoveFiles.isChecked() + try: + self.lblRenameTest.setText(fr.determine_name(".cbz")) + self.rename_error = None + except Exception as e: + self.rename_error = e + self.lblRenameTest.setText(str(e)) def settings_to_form(self): - # Copy values from settings to form self.leRarExePath.setText(self.settings.rar_exe_path) self.leNameLengthDeltaThresh.setText(str(self.settings.id_length_delta_thresh)) @@ -181,12 +290,8 @@ class SettingsWindow(QtWidgets.QDialog): 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: + self.rename_test(self.leRenameTemplate.text()) + if self.rename_error is not None: QtWidgets.QMessageBox.critical( self, "Invalid format string!", @@ -195,7 +300,7 @@ class SettingsWindow(QtWidgets.QDialog): "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), + "https://docs.python.org/3/library/string.html#format-string-syntax".format(self.rename_error), ) return @@ -246,6 +351,8 @@ class SettingsWindow(QtWidgets.QDialog): self.settings.rename_move_dir = self.cbxMoveFiles.isChecked() self.settings.rename_dir = self.leDirectory.text() + self.settings.rename_new_renamer = self.cbxNewRenamer.isChecked() + self.settings.save() QtWidgets.QDialog.accept(self) @@ -298,6 +405,8 @@ class SettingsWindow(QtWidgets.QDialog): def show_template_help(self): template_help_win = TemplateHelpWindow(self) template_help_win.setModal(False) + if not self.cbxNewRenamer.isChecked(): + template_help_win.textEdit.setHtml(old_template_tooltip) template_help_win.show() diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 323e6f1..6cffdea 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -246,6 +246,15 @@ Have fun! """, ) self.settings.show_disclaimer = not checked + if not self.settings.hide_rename_message: + self.settings.hide_rename_message = OptionalMessageDialog.msg( + self, + "New rename template!", + "There is a new rename template format available. " + "Please use the settings window to enable and test if you use this feature.

" + "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", + ) if self.settings.check_for_new_version: # self.checkLatestVersionOnline() diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index 7ca3dbf..d0a8fc7 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -28,7 +28,7 @@ - 1 + 0 @@ -556,62 +556,20 @@ - <pre>The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax -Accepts the following variables: -{isEmpty} (boolean) -{tagOrigin} (string) -{series} (string) -{issue} (string) -{title} (string) -{publisher} (string) -{month} (integer) -{year} (integer) -{day} (integer) -{issueCount} (integer) -{volume} (integer) -{genre} (string) -{language} (string) -{comments} (string) -{volumeCount} (integer) -{criticalRating} (string) -{country} (string) -{alternateSeries} (string) -{alternateNumber} (string) -{alternateCount} (integer) -{imprint} (string) -{notes} (string) -{webLink} (string) -{format} (string) -{manga} (string) -{blackAndWhite} (boolean) -{pageCount} (integer) -{maturityRating} (string) -{storyArc} (string) -{seriesGroup} (string) -{scanInfo} (string) -{characters} (string) -{teams} (string) -{locations} (string) -{credits} (list of dict({&apos;role&apos;: &apos;str&apos;, &apos;person&apos;: &apos;str&apos;, &apos;primary&apos;: boolean})) -{tags} (list of str) -{pages} (list of dict({&apos;Image&apos;: &apos;str(int)&apos;, &apos;Type&apos;: &apos;str&apos;})) - -CoMet-only items: -{price} (float) -{isVersionOf} (string) -{rights} (string) -{identifier} (string) -{lastMark} (string) -{coverImage} (string) - -Examples: - -{series} {issue} ({year}) -Spider-Geddon 1 (2018) - -{series} #{issue} - {title} -Spider-Geddon #1 - New Players; Check In -</pre> + <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> @@ -665,7 +623,7 @@ Spider-Geddon #1 - New Players; Check In
- + If checked moves files to specified folder @@ -675,16 +633,36 @@ Spider-Geddon #1 - New Players; Check In - + Destination Directory: - + + + + + Enable the new renamer + + + + + + + + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + diff --git a/tests/filenames.py b/tests/filenames.py index 40ed63b..46c0b25 100644 --- a/tests/filenames.py +++ b/tests/filenames.py @@ -419,3 +419,15 @@ fnames = [ marks=pytest.mark.xfail, ), ] + +rnames = [ + ( + "{series} #{issue} - {title} ({year})", + "Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz", + ), + pytest.param( + "{series} #{issue} - {title} - {WriteR}, {EDITOR} ({year})", + "Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game - Dara Naraghi, Ted Adams (2007).cbz", + marks=pytest.mark.xfail, + ), +] diff --git a/tests/test_rename.py b/tests/test_rename.py new file mode 100644 index 0000000..bcd57db --- /dev/null +++ b/tests/test_rename.py @@ -0,0 +1,21 @@ +import re + +import pytest +from filenames import rnames + +from comicapi.genericmetadata import md_test +from comictaggerlib.filerenamer import FileRenamer, FileRenamer2 + + +@pytest.mark.parametrize("template,result", rnames) +def test_rename_old(template, result): + fr = FileRenamer(md_test) + fr.set_template(re.sub(r"{(\w+)}", r"%\1%", template)) + assert fr.determine_name(".cbz") == result + + +@pytest.mark.parametrize("template,result", rnames) +def test_rename_new(template, result): + fr = FileRenamer2(md_test) + fr.set_template(template) + assert fr.determine_name(".cbz") == result From 7037877a77b2cc3f7f07699be4e1f580b2372406 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 18 Apr 2022 22:46:46 -0700 Subject: [PATCH 3/3] Add a strict mode to file renaming Strict renaming removes all reserved names and characters regardless of operating system, with out strict mode only for the current Operating System Add more edge cases to smart cleanup Add more tests for file renaming --- comicapi/genericmetadata.py | 2 +- comictaggerlib/cli.py | 2 +- comictaggerlib/filerenamer.py | 68 ++++++++++++++++++----------- comictaggerlib/renamewindow.py | 2 +- comictaggerlib/settings.py | 5 +++ comictaggerlib/settingswindow.py | 23 +++++++--- comictaggerlib/ui/settingswindow.ui | 47 ++++++++++++-------- tests/filenames.py | 44 +++++++++++++++++++ tests/test_rename.py | 17 +++++--- 9 files changed, 149 insertions(+), 61 deletions(-) diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 280dc84..a8d03b3 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -382,7 +382,7 @@ 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.locations = "lonely cottage " md_test.credits = [ {"person": "Dara Naraghi", "role": "Writer"}, {"person": "Esteve Polls", "role": "Penciller"}, diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 25f90f8..c9a29b8 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -453,7 +453,7 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults new_ext = ".cbr" if settings.rename_new_renamer: - renamer = FileRenamer2(md) + renamer = FileRenamer2(md, platform="universal" if settings.rename_strict else "auto") else: renamer = FileRenamer(md) renamer.set_template(settings.rename_template) diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py index 742a321..6570f64 100644 --- a/comictaggerlib/filerenamer.py +++ b/comictaggerlib/filerenamer.py @@ -20,8 +20,9 @@ import logging import os import re import string +import sys -from pathvalidate import sanitize_filepath +from pathvalidate import sanitize_filename from comicapi.genericmetadata import GenericMetadata from comicapi.issuestring import IssueString @@ -146,9 +147,10 @@ class FileRenamer: class MetadataFormatter(string.Formatter): - def __init__(self, smart_cleanup=False): + def __init__(self, smart_cleanup=False, platform="auto"): super().__init__() self.smart_cleanup = smart_cleanup + self.platform = platform def format_field(self, value, format_spec): if value is None or value == "": @@ -165,31 +167,36 @@ class MetadataFormatter(string.Formatter): # output the literal text if literal_text: if lstrip: - result.append(literal_text.lstrip("-_)}]#")) - else: - result.append(literal_text) + literal_text = literal_text.lstrip("-_)}]#") + if self.smart_cleanup: + lspace = literal_text[0].isspace() + rspace = literal_text[-1].isspace() + literal_text = " ".join(literal_text.split()) + if literal_text == "": + literal_text = " " + else: + if lspace: + literal_text = " " + literal_text + if rspace: + literal_text += " " + result.append(literal_text) + 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 + # 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" - ) + 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 + 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 @@ -210,18 +217,22 @@ class MetadataFormatter(string.Formatter): if fmt_obj == "" and len(result) > 0 and self.smart_cleanup: lstrip = True result.pop() + if self.smart_cleanup: + fmt_obj = " ".join(fmt_obj.split()) + fmt_obj = sanitize_filename(fmt_obj, platform=self.platform) result.append(fmt_obj) return "".join(result), auto_arg_index class FileRenamer2: - def __init__(self, metadata): + def __init__(self, metadata, platform="auto"): self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})" self.smart_cleanup = True self.issue_zero_padding = 3 self.metadata = metadata self.move = False + self.platform = platform def set_metadata(self, metadata: GenericMetadata): self.metadata = metadata @@ -250,7 +261,7 @@ class FileRenamer2: path_components = template.split(os.sep) new_name = "" - fmt = MetadataFormatter(self.smart_cleanup) + fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform) md_dict = vars(md) for role in ["writer", "penciller", "inker", "colorist", "letterer", "cover artist", "editor"]: md_dict[role] = md.get_primary_credit(role) @@ -264,19 +275,24 @@ class FileRenamer2: md_dict["month_abbr"] = "" for Component in path_components: - new_name = os.path.join( - new_name, fmt.vformat(Component, args=[], kwargs=Default(md_dict)).replace("/", "-") - ) + if ( + self.platform.lower() in ["universal", "windows"] or sys.platform.lower() in ["windows"] + ) and self.smart_cleanup: + # colons get special treatment + Component = Component.replace(": ", " - ") + Component = Component.replace(":", "-") + + new_basename = sanitize_filename( + fmt.vformat(Component, args=[], kwargs=Default(md_dict)), platform=self.platform + ).strip() + new_name = os.path.join(new_name, new_basename) new_name += ext - - # # some tweaks to keep various filesystems happy - # new_name = new_name.replace(": ", " - ") - # new_name = new_name.replace(":", "-") + new_basename += ext # remove padding md.issue = IssueString(md.issue).as_string() if self.move: - return sanitize_filepath(new_name.strip()) + return new_name.strip() else: - return os.path.basename(sanitize_filepath(new_name.strip())) + return new_basename.strip() diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index a7102f5..cd4e9e9 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -53,7 +53,7 @@ class RenameWindow(QtWidgets.QDialog): self.btnSettings.clicked.connect(self.modify_settings) if self.settings.rename_new_renamer: - self.renamer = FileRenamer2(None) + self.renamer = FileRenamer2(None, platform="universal" if self.settings.rename_strict else "auto") else: self.renamer = FileRenamer(None) diff --git a/comictaggerlib/settings.py b/comictaggerlib/settings.py index 12f159e..c3aac19 100644 --- a/comictaggerlib/settings.py +++ b/comictaggerlib/settings.py @@ -118,6 +118,7 @@ class ComicTaggerSettings: self.rename_dir = "" self.rename_move_dir = False self.rename_new_renamer = False + self.rename_strict = False # Auto-tag stickies self.save_on_low_confidence = False @@ -195,6 +196,7 @@ class ComicTaggerSettings: self.rename_dir = "" self.rename_move_dir = False self.rename_new_renamer = False + self.rename_strict = False # Auto-tag stickies self.save_on_low_confidence = False @@ -360,6 +362,8 @@ class ComicTaggerSettings: 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("rename", "rename_strict"): + self.rename_strict = self.config.getboolean("rename", "rename_strict") if self.config.has_option("autotag", "save_on_low_confidence"): self.save_on_low_confidence = self.config.getboolean("autotag", "save_on_low_confidence") @@ -461,6 +465,7 @@ class ComicTaggerSettings: 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) + self.config.set("rename", "rename_strict", self.rename_strict) if not self.config.has_section("autotag"): self.config.add_section("autotag") diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 9115f12..1e35ca6 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -194,7 +194,7 @@ class SettingsWindow(QtWidgets.QDialog): self.settings_to_form() self.rename_error = None - self.rename_test(self.leRenameTemplate.text()) + self.rename_test() self.cbxNewRenamer.clicked.connect(self.new_rename_toggle) self.btnBrowseRar.clicked.connect(self.select_rar) @@ -202,7 +202,10 @@ class SettingsWindow(QtWidgets.QDialog): self.btnResetSettings.clicked.connect(self.reset_settings) self.btnTestKey.clicked.connect(self.test_api_key) self.btnTemplateHelp.clicked.connect(self.show_template_help) - self.leRenameTemplate.textEdited.connect(self.rename_test) + self.leRenameTemplate.textEdited.connect(self.rename__test) + self.cbxMoveFiles.clicked.connect(self.rename_test) + self.cbxRenameStrict.clicked.connect(self.rename_test) + self.leDirectory.textEdited.connect(self.rename_test) def new_rename_toggle(self): new_rename = self.cbxNewRenamer.isChecked() @@ -215,16 +218,19 @@ class SettingsWindow(QtWidgets.QDialog): self.cbxMoveFiles.setEnabled(new_rename) self.leDirectory.setEnabled(new_rename) self.lblDirectory.setEnabled(new_rename) - self.rename_test(self.leRenameTemplate.text()) + self.rename_test() - def rename_test(self, template): + def rename_test(self): + self.rename__test(self.leRenameTemplate.text()) + + def rename__test(self, template): fr = FileRenamer(md_test) if self.cbxNewRenamer.isChecked(): - fr = FileRenamer2(md_test) + fr = FileRenamer2(md_test, platform="universal" if self.cbxRenameStrict.isChecked() else "auto") + fr.move = self.cbxMoveFiles.isChecked() fr.set_template(template) fr.set_issue_zero_padding(int(self.leIssueNumPadding.text())) fr.set_smart_cleanup(self.cbxSmartCleanup.isChecked()) - fr.move = self.cbxMoveFiles.isChecked() try: self.lblRenameTest.setText(fr.determine_name(".cbz")) self.rename_error = None @@ -288,9 +294,11 @@ class SettingsWindow(QtWidgets.QDialog): if self.settings.rename_move_dir: self.cbxMoveFiles.setCheckState(QtCore.Qt.CheckState.Checked) self.leDirectory.setText(self.settings.rename_dir) + if self.settings.rename_strict: + self.cbxRenameStrict.setCheckState(QtCore.Qt.CheckState.Checked) def accept(self): - self.rename_test(self.leRenameTemplate.text()) + self.rename_test() if self.rename_error is not None: QtWidgets.QMessageBox.critical( self, @@ -352,6 +360,7 @@ class SettingsWindow(QtWidgets.QDialog): self.settings.rename_dir = self.leDirectory.text() self.settings.rename_new_renamer = self.cbxNewRenamer.isChecked() + self.settings.rename_strict = self.cbxRenameStrict.isChecked() self.settings.save() QtWidgets.QDialog.accept(self) diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index d0a8fc7..bfc112d 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -580,6 +580,19 @@
+ + + + + + + Qt::PlainText + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + @@ -623,6 +636,13 @@ + + + + Enable the new renamer + + + @@ -633,33 +653,24 @@ - + Destination Directory: - + - - + + + + If checked will ensure reserved characters and filenames are removed for all Operating Systems. +By default only removes restricted characters and filenames for the current Operating System. + - Enable the new renamer - - - - - - - - - - Qt::PlainText - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Strict renaming diff --git a/tests/filenames.py b/tests/filenames.py index 46c0b25..0595d9a 100644 --- a/tests/filenames.py +++ b/tests/filenames.py @@ -423,10 +423,54 @@ fnames = [ rnames = [ ( "{series} #{issue} - {title} ({year})", + False, + "universal", "Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz", ), + ( + "{series}: {title} #{issue} ({year})", + False, + "universal", + "Cory Doctorow's Futuristic Tales of the Here and Now - Anda's Game #001 (2007).cbz", + ), + pytest.param( + "{series}: {title} #{issue} ({year})", + False, + "Linux", + "Cory Doctorow's Futuristic Tales of the Here and Now: Anda's Game #001 (2007).cbz", + marks=pytest.mark.xfail, + ), + pytest.param( + "{publisher}/ {series} #{issue} - {title} ({year})", + True, + "universal", + "IDW Publishing/Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz", + marks=pytest.mark.xfail, + ), + pytest.param( + "{publisher}/ {series} #{issue} - {title} ({year})", + False, + "universal", + "Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz", + marks=pytest.mark.xfail, + ), + ( + "{series} # {issue} - {title} ({year})", + False, + "universal", + "Cory Doctorow's Futuristic Tales of the Here and Now # 001 - Anda's Game (2007).cbz", + ), + pytest.param( + "{series} # {issue} - {locations} ({year})", + False, + "universal", + "Cory Doctorow's Futuristic Tales of the Here and Now # 001 - lonely cottage (2007).cbz", + marks=pytest.mark.xfail, + ), pytest.param( "{series} #{issue} - {title} - {WriteR}, {EDITOR} ({year})", + False, + "universal", "Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game - Dara Naraghi, Ted Adams (2007).cbz", marks=pytest.mark.xfail, ), diff --git a/tests/test_rename.py b/tests/test_rename.py index bcd57db..9f01aef 100644 --- a/tests/test_rename.py +++ b/tests/test_rename.py @@ -7,15 +7,18 @@ from comicapi.genericmetadata import md_test from comictaggerlib.filerenamer import FileRenamer, FileRenamer2 -@pytest.mark.parametrize("template,result", rnames) -def test_rename_old(template, result): +@pytest.mark.parametrize("template, move, platform, expected", rnames) +def test_rename_old(template, platform, move, expected): + _ = platform + _ = move fr = FileRenamer(md_test) fr.set_template(re.sub(r"{(\w+)}", r"%\1%", template)) - assert fr.determine_name(".cbz") == result + assert fr.determine_name(".cbz") == expected -@pytest.mark.parametrize("template,result", rnames) -def test_rename_new(template, result): - fr = FileRenamer2(md_test) +@pytest.mark.parametrize("template, move, platform, expected", rnames) +def test_rename_new(template, platform, move, expected): + fr = FileRenamer2(md_test, platform=platform) + fr.move = move fr.set_template(template) - assert fr.determine_name(".cbz") == result + assert fr.determine_name(".cbz") == expected