diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py
index f2424fe..c9bbce9 100644
--- a/comictaggerlib/cli.py
+++ b/comictaggerlib/cli.py
@@ -502,7 +502,11 @@ def process_file_cli(
elif ca.is_rar():
new_ext = ".cbr"
- renamer = FileRenamer(md, platform="universal" if options[filename]["rename_strict"] else "auto")
+ renamer = FileRenamer(
+ md,
+ platform="universal" if options[filename]["rename_strict"] else "auto",
+ replacements=options["rename"]["replacements"],
+ )
renamer.set_template(options[filename]["rename_template"])
renamer.set_issue_zero_padding(options[filename]["rename_issue_number_padding"])
renamer.set_smart_cleanup(options[filename]["rename_use_smart_string_cleanup"])
diff --git a/comictaggerlib/defaults.py b/comictaggerlib/defaults.py
new file mode 100644
index 0000000..8717d12
--- /dev/null
+++ b/comictaggerlib/defaults.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+from typing import NamedTuple
+
+
+class Replacement(NamedTuple):
+ find: str
+ replce: str
+ strict_only: bool
+
+
+class Replacements(NamedTuple):
+ literal_text: list[Replacement]
+ format_value: list[Replacement]
+
+
+DEFAULT_REPLACEMENTS = Replacements(
+ literal_text=[
+ Replacement(": ", " - ", True),
+ Replacement(":", "-", True),
+ ],
+ format_value=[
+ Replacement(": ", " - ", True),
+ Replacement(":", "-", True),
+ Replacement("/", "-", False),
+ Replacement("//", "--", False),
+ Replacement("\\", "-", True),
+ ],
+)
diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py
index d52214f..828f8f2 100644
--- a/comictaggerlib/filerenamer.py
+++ b/comictaggerlib/filerenamer.py
@@ -20,42 +20,18 @@ import logging
import os
import pathlib
import string
-from typing import Any, NamedTuple, cast
+from typing import Any, cast
from pathvalidate import Platform, normalize_platform, sanitize_filename
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
+from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements
logger = logging.getLogger(__name__)
-class Replacement(NamedTuple):
- find: str
- replce: str
- strict_only: bool
-
-
-class Replacements(NamedTuple):
- literal_text: list[Replacement]
- format_value: list[Replacement]
-
-
-REPLACEMENTS = Replacements(
- literal_text=[
- Replacement(": ", " - ", True),
- Replacement(":", "-", True),
- ],
- format_value=[
- Replacement(": ", " - ", True),
- Replacement(":", "-", True),
- Replacement("/", "-", False),
- Replacement("\\", "-", True),
- ],
-)
-
-
def get_rename_dir(ca: ComicArchive, rename_dir: str | pathlib.Path | None) -> pathlib.Path:
folder = ca.path.parent.absolute()
if rename_dir is not None:
@@ -67,7 +43,7 @@ def get_rename_dir(ca: ComicArchive, rename_dir: str | pathlib.Path | None) -> p
class MetadataFormatter(string.Formatter):
def __init__(
- self, smart_cleanup: bool = False, platform: str = "auto", replacements: Replacements = REPLACEMENTS
+ self, smart_cleanup: bool = False, platform: str = "auto", replacements: Replacements = DEFAULT_REPLACEMENTS
) -> None:
super().__init__()
self.smart_cleanup = smart_cleanup
@@ -200,13 +176,19 @@ class MetadataFormatter(string.Formatter):
class FileRenamer:
- def __init__(self, metadata: GenericMetadata | None, platform: str = "auto") -> None:
+ def __init__(
+ self,
+ metadata: GenericMetadata | None,
+ platform: str = "auto",
+ replacements: Replacements = DEFAULT_REPLACEMENTS,
+ ) -> None:
self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})"
self.smart_cleanup = True
self.issue_zero_padding = 3
self.metadata = metadata or GenericMetadata()
self.move = False
self.platform = platform
+ self.replacements = replacements
def set_metadata(self, metadata: GenericMetadata) -> None:
self.metadata = metadata
@@ -234,7 +216,7 @@ class FileRenamer:
new_name = ""
- fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform)
+ fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform, replacements=self.replacements)
md_dict = vars(md)
for role in ["writer", "penciller", "inker", "colorist", "letterer", "cover artist", "editor"]:
md_dict[role] = md.get_primary_credit(role)
diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py
index f231618..a3f3d78 100644
--- a/comictaggerlib/renamewindow.py
+++ b/comictaggerlib/renamewindow.py
@@ -62,7 +62,7 @@ class RenameWindow(QtWidgets.QDialog):
self.btnSettings.clicked.connect(self.modify_settings)
platform = "universal" if self.options["filename"]["rename_strict"] else "auto"
- self.renamer = FileRenamer(None, platform=platform)
+ self.renamer = FileRenamer(None, platform=platform, replacements=self.options["rename"]["replacements"])
self.do_preview()
@@ -70,6 +70,7 @@ class RenameWindow(QtWidgets.QDialog):
self.renamer.set_template(self.options["filename"]["rename_template"])
self.renamer.set_issue_zero_padding(self.options["filename"]["rename_issue_number_padding"])
self.renamer.set_smart_cleanup(self.options["filename"]["rename_use_smart_string_cleanup"])
+ self.renamer.replacements = self.options["rename"]["replacements"]
new_ext = ca.path.suffix # default
if self.options["filename"]["rename_set_extension_based_on_archive"]:
diff --git a/comictaggerlib/settings/file.py b/comictaggerlib/settings/file.py
index 36931f5..bafa90c 100644
--- a/comictaggerlib/settings/file.py
+++ b/comictaggerlib/settings/file.py
@@ -2,10 +2,11 @@ from __future__ import annotations
import argparse
import uuid
-from collections.abc import Sequence
-from typing import Any, Callable
+from typing import Any
+from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements
from comictaggerlib.settings.manager import Manager
+from comictaggerlib.settings.types import AppendAction
def general(parser: Manager) -> None:
@@ -38,75 +39,10 @@ def identifier(parser: Manager) -> None:
parser.add_setting(
"--publisher-filter",
default=["Panini Comics", "Abril", "Planeta DeAgostini", "Editorial Televisa", "Dino Comics"],
- action=_AppendAction,
+ action=AppendAction,
)
-def _copy_items(items: Sequence[Any] | None) -> Sequence[Any]:
- if items is None:
- return []
- # The copy module is used only in the 'append' and 'append_const'
- # actions, and it is needed only when the default value isn't a list.
- # Delay its import for speeding up the common case.
- if type(items) is list:
- return items[:]
- import copy
-
- return copy.copy(items)
-
-
-class _AppendAction(argparse.Action):
- def __init__(
- self,
- option_strings: list[str],
- dest: str,
- nargs: str | None = None,
- const: Any = None,
- default: Any = None,
- type: Callable[[str], Any] | None = None, # noqa: A002
- choices: list[Any] | None = None,
- required: bool = False,
- help: str | None = None, # noqa: A002
- metavar: str | None = None,
- ):
- self.called = False
- if nargs == 0:
- raise ValueError(
- "nargs for append actions must be != 0; if arg "
- "strings are not supplying the value to append, "
- "the append const action may be more appropriate"
- )
- if const is not None and nargs != argparse.OPTIONAL:
- raise ValueError("nargs must be %r to supply const" % argparse.OPTIONAL)
- super().__init__(
- option_strings=option_strings,
- dest=dest,
- nargs=nargs,
- const=const,
- default=default,
- type=type,
- choices=choices,
- required=required,
- help=help,
- metavar=metavar,
- )
-
- def __call__(
- self,
- parser: argparse.ArgumentParser,
- namespace: argparse.Namespace,
- values: str | Sequence[Any] | None,
- option_string: str | None = None,
- ) -> None:
- if values:
- if not self.called:
- setattr(namespace, self.dest, [])
- items = getattr(namespace, self.dest, None)
- items = _copy_items(items)
- items.append(values) # type: ignore
- setattr(namespace, self.dest, items)
-
-
def dialog(parser: Manager) -> None:
# Show/ask dialog flags
parser.add_setting("ask_about_cbi_in_rar", default=True, cmdline=False)
@@ -140,12 +76,10 @@ def comicvine(parser: Manager) -> None:
parser.add_setting("--remove-html-tables", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting(
"--cv-api-key",
- default="",
help="Use the given Comic Vine API Key (persisted in settings).",
)
parser.add_setting(
"--cv-url",
- default="",
help="Use the given Comic Vine URL (persisted in settings).",
)
parser.add_setting(
@@ -184,6 +118,11 @@ def rename(parser: Manager) -> None:
parser.add_setting("--dir", default="")
parser.add_setting("--move-to-dir", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--strict", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting(
+ "replacements",
+ default=DEFAULT_REPLACEMENTS,
+ cmdline=False,
+ )
def autotag(parser: Manager) -> None:
@@ -214,6 +153,10 @@ def validate_settings(options: dict[str, dict[str, Any]], parser: Manager) -> di
options["identifier"]["publisher_filter"] = [
x.strip() for x in options["identifier"]["publisher_filter"] if x.strip()
]
+ options["rename"]["replacements"] = Replacements(
+ [Replacement(x[0], x[1], x[2]) for x in options["rename"]["replacements"][0]],
+ [Replacement(x[0], x[1], x[2]) for x in options["rename"]["replacements"][1]],
+ )
return options
diff --git a/comictaggerlib/settings/types.py b/comictaggerlib/settings/types.py
index 774fbdb..7028e89 100644
--- a/comictaggerlib/settings/types.py
+++ b/comictaggerlib/settings/types.py
@@ -2,7 +2,8 @@ from __future__ import annotations
import argparse
import pathlib
-from typing import Any
+from collections.abc import Sequence
+from typing import Any, Callable
from appdirs import AppDirs
@@ -72,6 +73,71 @@ def metadata_type(types: str) -> list[int]:
return result
+def _copy_items(items: Sequence[Any] | None) -> Sequence[Any]:
+ if items is None:
+ return []
+ # The copy module is used only in the 'append' and 'append_const'
+ # actions, and it is needed only when the default value isn't a list.
+ # Delay its import for speeding up the common case.
+ if type(items) is list:
+ return items[:]
+ import copy
+
+ return copy.copy(items)
+
+
+class AppendAction(argparse.Action):
+ def __init__(
+ self,
+ option_strings: list[str],
+ dest: str,
+ nargs: str | None = None,
+ const: Any = None,
+ default: Any = None,
+ type: Callable[[str], Any] | None = None, # noqa: A002
+ choices: list[Any] | None = None,
+ required: bool = False,
+ help: str | None = None, # noqa: A002
+ metavar: str | None = None,
+ ):
+ self.called = False
+ if nargs == 0:
+ raise ValueError(
+ "nargs for append actions must be != 0; if arg "
+ "strings are not supplying the value to append, "
+ "the append const action may be more appropriate"
+ )
+ if const is not None and nargs != argparse.OPTIONAL:
+ raise ValueError("nargs must be %r to supply const" % argparse.OPTIONAL)
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=nargs,
+ const=const,
+ default=default,
+ type=type,
+ choices=choices,
+ required=required,
+ help=help,
+ metavar=metavar,
+ )
+
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: str | Sequence[Any] | None,
+ option_string: str | None = None,
+ ) -> None:
+ if values:
+ if not self.called:
+ setattr(namespace, self.dest, [])
+ items = getattr(namespace, self.dest, None)
+ items = _copy_items(items)
+ items.append(values) # type: ignore
+ setattr(namespace, self.dest, items)
+
+
def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
"""The metadata string is a comma separated list of name-value pairs
The names match the attributes of the internal metadata struct (for now)
diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py
index b786869..daf585e 100644
--- a/comictaggerlib/settingswindow.py
+++ b/comictaggerlib/settingswindow.py
@@ -20,6 +20,7 @@ import logging
import os
import pathlib
import platform
+from typing import Any
from PyQt5 import QtCore, QtGui, QtWidgets, uic
@@ -27,7 +28,7 @@ from comicapi import utils
from comicapi.genericmetadata import md_test
from comictaggerlib import settings
from comictaggerlib.ctversion import version
-from comictaggerlib.filerenamer import FileRenamer
+from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.ui import ui_path
from comictalker.comiccacher import ComicCacher
@@ -190,23 +191,64 @@ 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.cbxMoveFiles.clicked.connect(self.rename_test)
self.cbxMoveFiles.clicked.connect(self.dir_test)
- self.cbxRenameStrict.clicked.connect(self.rename_test)
self.leDirectory.textEdited.connect(self.dir_test)
self.cbxComplicatedParser.clicked.connect(self.switch_parser)
- def rename_test(self) -> None:
+ self.btnAddLiteralReplacement.clicked.connect(self.addLiteralReplacement)
+ self.btnAddValueReplacement.clicked.connect(self.addValueReplacement)
+ self.btnRemoveLiteralReplacement.clicked.connect(self.removeLiteralReplacement)
+ self.btnRemoveValueReplacement.clicked.connect(self.removeValueReplacement)
+
+ self.leRenameTemplate.textEdited.connect(self.rename_test)
+ self.cbxMoveFiles.clicked.connect(self.rename_test)
+ self.cbxRenameStrict.clicked.connect(self.rename_test)
+ self.cbxSmartCleanup.clicked.connect(self.rename_test)
+ self.cbxChangeExtension.clicked.connect(self.rename_test)
+ self.leIssueNumPadding.textEdited.connect(self.rename_test)
+ self.twLiteralReplacements.cellChanged.connect(self.rename_test)
+ self.twValueReplacements.cellChanged.connect(self.rename_test)
+
+ def addLiteralReplacement(self) -> None:
+ self.insertRow(self.twLiteralReplacements, self.twLiteralReplacements.rowCount(), Replacement("", "", False))
+
+ def addValueReplacement(self) -> None:
+ self.insertRow(self.twValueReplacements, self.twValueReplacements.rowCount(), Replacement("", "", False))
+
+ def removeLiteralReplacement(self) -> None:
+ if self.twLiteralReplacements.currentRow() >= 0:
+ self.twLiteralReplacements.removeRow(self.twLiteralReplacements.currentRow())
+
+ def removeValueReplacement(self) -> None:
+ if self.twValueReplacements.currentRow() >= 0:
+ self.twValueReplacements.removeRow(self.twValueReplacements.currentRow())
+
+ def insertRow(self, table: QtWidgets.QTableWidget, row: int, replacement: Replacement) -> None:
+ find, replace, strict_only = replacement
+ table.insertRow(row)
+ table.setItem(row, 0, QtWidgets.QTableWidgetItem(find))
+ table.setItem(row, 1, QtWidgets.QTableWidgetItem(replace))
+ tmp = QtWidgets.QTableWidgetItem()
+ if strict_only:
+ tmp.setCheckState(QtCore.Qt.Checked)
+ else:
+ tmp.setCheckState(QtCore.Qt.Unchecked)
+ table.setItem(row, 2, tmp)
+
+ def rename_test(self, *args: Any, **kwargs: Any) -> None:
self._rename_test(self.leRenameTemplate.text())
def dir_test(self) -> None:
self.lblDir.setText(
- str(pathlib.Path(self.leDirectory.text().strip()).absolute()) if self.cbxMoveFiles.isChecked() else ""
+ str(pathlib.Path(self.leDirectory.text().strip()).resolve()) if self.cbxMoveFiles.isChecked() else ""
)
def _rename_test(self, template: str) -> None:
- fr = FileRenamer(md_test, platform="universal" if self.cbxRenameStrict.isChecked() else "auto")
+ fr = FileRenamer(
+ md_test,
+ platform="universal" if self.cbxRenameStrict.isChecked() else "auto",
+ replacements=self.get_replacemnts(),
+ )
fr.move = self.cbxMoveFiles.isChecked()
fr.set_template(template)
fr.set_issue_zero_padding(int(self.leIssueNumPadding.text()))
@@ -271,6 +313,38 @@ class SettingsWindow(QtWidgets.QDialog):
self.leDirectory.setText(self.options["rename"]["dir"])
self.cbxRenameStrict.setChecked(self.options["rename"]["strict"])
+ for table, replacments in zip(
+ (self.twLiteralReplacements, self.twValueReplacements), self.options["rename"]["replacements"]
+ ):
+ table.clearContents()
+ for i in reversed(range(table.rowCount())):
+ table.removeRow(i)
+ for row, replacement in enumerate(replacments):
+ self.insertRow(table, row, replacement)
+
+ def get_replacemnts(self) -> Replacements:
+ literal_replacements = []
+ value_replacements = []
+ for row in range(self.twLiteralReplacements.rowCount()):
+ if self.twLiteralReplacements.item(row, 0).text():
+ literal_replacements.append(
+ Replacement(
+ self.twLiteralReplacements.item(row, 0).text(),
+ self.twLiteralReplacements.item(row, 1).text(),
+ self.twLiteralReplacements.item(row, 2).checkState() == QtCore.Qt.Checked,
+ )
+ )
+ for row in range(self.twValueReplacements.rowCount()):
+ if self.twValueReplacements.item(row, 0).text():
+ value_replacements.append(
+ Replacement(
+ self.twValueReplacements.item(row, 0).text(),
+ self.twValueReplacements.item(row, 1).text(),
+ self.twValueReplacements.item(row, 2).checkState() == QtCore.Qt.Checked,
+ )
+ )
+ return Replacements(literal_replacements, value_replacements)
+
def accept(self) -> None:
self.rename_test()
if self.rename_error is not None:
@@ -362,6 +436,7 @@ class SettingsWindow(QtWidgets.QDialog):
self.options["rename"]["dir"] = self.leDirectory.text()
self.options["rename"]["strict"] = self.cbxRenameStrict.isChecked()
+ self.options["rename"]["replacements"] = self.get_replacemnts()
settings.Manager().save_file(self.options, self.options["runtime"]["config"].user_config_dir / "settings.json")
self.parent().options = self.options
@@ -402,9 +477,9 @@ class SettingsWindow(QtWidgets.QDialog):
dialog.setDirectory(os.path.dirname(str(control.text())))
if name == "RAR":
- dialog.setWindowTitle("Find " + name + " program")
+ dialog.setWindowTitle(f"Find {name} program")
else:
- dialog.setWindowTitle("Find " + name + " library")
+ dialog.setWindowTitle(f"Find {name} library")
if dialog.exec():
file_list = dialog.selectedFiles()
diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui
index 418bf4c..813c7a8 100644
--- a/comictaggerlib/ui/settingswindow.ui
+++ b/comictaggerlib/ui/settingswindow.ui
@@ -7,7 +7,7 @@
0
0
702
- 488
+ 513
@@ -616,7 +616,21 @@
Rename
- -
+
-
+
+
+ Add Replacement
+
+
+
+ -
+
+
+ Remove Replacement
+
+
+
+ -
QFormLayout::AllNonFixedFieldsGrow
@@ -717,8 +731,7 @@
-
- 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.
+ If checked will ensure reserved characters and filenames are removed for all Operating Systems.<br/>By default only removes restricted characters and filenames for the current Operating System.
Strict renaming
@@ -730,6 +743,118 @@ By default only removes restricted characters and filenames for the current Oper
+ -
+
+
+ Add Replacement
+
+
+
+ -
+
+
+ Remove Replacement
+
+
+
+ -
+
+
+
+ Monaco
+
+
+
+ QAbstractItemView::SingleSelection
+
+
+ true
+
+
+
+ Find
+
+
+ AlignCenter
+
+
+
+
+ Replacement
+
+
+ AlignCenter
+
+
+
+
+ Strict Only
+
+
+ AlignCenter
+
+
+
+
+ -
+
+
+
+ Monaco
+
+
+
+ Qt::ActionsContextMenu
+
+
+ QAbstractItemView::SingleSelection
+
+
+ 3
+
+
+ true
+
+
+
+ Find
+
+
+ AlignCenter
+
+
+
+
+ Replacement
+
+
+ AlignCenter
+
+
+
+
+ Strict Only
+
+
+ AlignCenter
+
+
+
+
+ -
+
+
+ Value Text Replacements
+
+
+
+ -
+
+
+ Literal Text Replacements
+
+
+