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
This commit is contained in:
Timmy Welch 2022-04-18 22:46:46 -07:00
parent 6cccf22d54
commit 7037877a77
9 changed files with 149 additions and 61 deletions

View File

@ -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"},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -580,6 +580,19 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="lblRenameTest">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblPadding">
<property name="text">
@ -623,6 +636,13 @@
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxNewRenamer">
<property name="text">
<string>Enable the new renamer</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="cbxMoveFiles">
<property name="toolTip">
@ -633,33 +653,24 @@
</property>
</widget>
</item>
<item row="8" column="0">
<item row="9" column="0">
<widget class="QLabel" name="lblDirectory">
<property name="text">
<string>Destination Directory:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="9" column="1">
<widget class="QLineEdit" name="leDirectory"/>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxNewRenamer">
<item row="8" column="0">
<widget class="QCheckBox" name="cbxRenameStrict">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Enable the new renamer</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="lblRenameTest">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
<string>Strict renaming</string>
</property>
</widget>
</item>

View File

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

View File

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