From 6cccf22d54e71dfc7bf208eb16228597eb20c5ee Mon Sep 17 00:00:00 2001
From: Timmy Welch
Date: Mon, 18 Apr 2022 18:59:17 -0700
Subject: [PATCH] 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