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:
parent
6cccf22d54
commit
7037877a77
@ -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"},
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user