Add replacement settings

This commit is contained in:
Timmy Welch 2022-11-23 22:16:46 -08:00
parent 82d737407f
commit ed1df400d8
No known key found for this signature in database
8 changed files with 340 additions and 115 deletions

View File

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

View File

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

View File

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

View File

@ -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"]:

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>702</width>
<height>488</height>
<height>513</height>
</rect>
</property>
<property name="windowTitle">
@ -616,7 +616,21 @@
<string>Rename</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<item row="3" column="2">
<widget class="QPushButton" name="btnAddValueReplacement">
<property name="text">
<string>Add Replacement</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="btnRemoveLiteralReplacement">
<property name="text">
<string>Remove Replacement</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
@ -717,8 +731,7 @@
<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>
<string>If checked will ensure reserved characters and filenames are removed for all Operating Systems.&lt;br/&gt;By default only removes restricted characters and filenames for the current Operating System.</string>
</property>
<property name="text">
<string>Strict renaming</string>
@ -730,6 +743,118 @@ By default only removes restricted characters and filenames for the current Oper
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="btnAddLiteralReplacement">
<property name="text">
<string>Add Replacement</string>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QPushButton" name="btnRemoveValueReplacement">
<property name="text">
<string>Remove Replacement</string>
</property>
</widget>
</item>
<item row="2" column="2" colspan="2">
<widget class="QTableWidget" name="twValueReplacements">
<property name="font">
<font>
<family>Monaco</family>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Find</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Replacement</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Strict Only</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QTableWidget" name="twLiteralReplacements">
<property name="font">
<font>
<family>Monaco</family>
</font>
</property>
<property name="contextMenuPolicy">
<enum>Qt::ActionsContextMenu</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="columnCount">
<number>3</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Find</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Replacement</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Strict Only</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
</widget>
</item>
<item row="1" column="2" colspan="2">
<widget class="QLabel" name="lblValueReplacements">
<property name="text">
<string>Value Text Replacements</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="lblLiteralReplacements">
<property name="text">
<string>Literal Text Replacements</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tRARTools">