From ab6b97006305e4e138fbb649c6d1e955710f9e06 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 17 Dec 2023 16:16:21 -0800 Subject: [PATCH] Create an Action tuple for determining the current command --- comictaggerlib/cli.py | 142 +++++++++--------- comictaggerlib/ctsettings/commandline.py | 50 +++--- .../ctsettings/settngs_namespace.py | 9 +- comictaggerlib/main.py | 5 +- comictaggerlib/resulttypes.py | 27 +++- comictaggerlib/taggerwindow.py | 72 ++++++++- setup.cfg | 2 +- 7 files changed, 192 insertions(+), 115 deletions(-) diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 4ca8639..e20784c 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -35,7 +35,7 @@ from comictaggerlib.ctsettings import ct_ns from comictaggerlib.filerenamer import FileRenamer, get_rename_dir from comictaggerlib.graphics import graphics_path from comictaggerlib.issueidentifier import IssueIdentifier -from comictaggerlib.resulttypes import IssueResult, OnlineMatchResults, Result, Status +from comictaggerlib.resulttypes import Action, IssueResult, MatchStatus, OnlineMatchResults, Result, Status from comictalker.comictalker import ComicTalker, TalkerError from comictalker.talker_utils import cleanup_html @@ -94,7 +94,7 @@ class CLI: self.batch_mode = len(self.config.Runtime_Options__files) > 1 for f in self.config.Runtime_Options__files: - results.append(self.process_file_cli(f, match_results)) + results.append(self.process_file_cli(self.config.Commands__command, f, match_results)) if self.config.Runtime_Options__json: print(dataclasses.asdict(results[-1])) print(json.dumps(dataclasses.asdict(results[-1]), cls=OutputEncoder, indent=2)) @@ -292,7 +292,7 @@ class CLI: self.output(brief) if self.config.Runtime_Options__quiet: - return Result(ca.path, None, [], Status.success) + return Result(Action.print, Status.success, ca.path) self.output() @@ -343,7 +343,7 @@ class CLI: except Exception as e: logger.error("Failed to load metadata for %s: %s", ca.path, e) - return Result(ca.path, None, [], Status.success, md=md) + return Result(Action.print, Status.success, ca.path, md=md) def delete_style(self, ca: ComicArchive, style: int) -> Status: style_name = MetaDataStyle.name[style] @@ -363,11 +363,11 @@ class CLI: return Status.success def delete(self, ca: ComicArchive) -> Result: - res = Result(ca.path, None, status=Status.success) + res = Result(Action.delete, Status.success, ca.path) for metadata_style in self.config.Runtime_Options__type: status = self.delete_style(ca, metadata_style) if status == Status.success: - res.tags_removed.append(metadata_style) + res.tags_deleted.append(metadata_style) else: res.status = status return res @@ -400,7 +400,7 @@ class CLI: return Status.read_failure def copy(self, ca: ComicArchive) -> Result: - res = Result(ca.path, None) + res = Result(Action.copy, Status.success, ca.path) try: res.md = ca.read_metadata(self.config.Commands__copy) except Exception as e: @@ -409,7 +409,7 @@ class CLI: for metadata_style in self.config.Runtime_Options__type: status = self.copy_style(ca, res.md, metadata_style) if status == Status.success: - res.tags_saved.append(metadata_style) + res.tags_written.append(metadata_style) else: res.status = status return res @@ -420,11 +420,10 @@ class CLI: if ca.has_metadata(metadata_style): self.output(f"{ca.path}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.") return Result( + Action.save, original_path=ca.path, - renamed_path=None, - online_results=[], status=Status.existing_tags, - tags_saved=self.config.Runtime_Options__type, + tags_written=self.config.Runtime_Options__type, ) if self.batch_mode: @@ -445,11 +444,10 @@ class CLI: except TalkerError as e: logger.exception(f"Error retrieving issue details. Save aborted.\n{e}") res = Result( + Action.save, original_path=ca.path, - renamed_path=None, - online_results=[], status=Status.fetch_data_failure, - tags_saved=self.config.Runtime_Options__type, + tags_written=self.config.Runtime_Options__type, ) match_results.fetch_data_failures.append(res) return res @@ -457,11 +455,11 @@ class CLI: if ct_md is None: logger.error("No match for ID %s was found.", self.config.Runtime_Options__issue_id) res = Result( + Action.save, + status=Status.match_failure, original_path=ca.path, - renamed_path=None, - online_results=[], - status=Status.no_match, - tags_saved=self.config.Runtime_Options__type, + match_status=MatchStatus.no_match, + tags_written=self.config.Runtime_Options__type, ) match_results.no_matches.append(res) return res @@ -472,11 +470,11 @@ class CLI: if md is None or md.is_empty: logger.error("No metadata given to search online with!") res = Result( + Action.save, + status=Status.match_failure, original_path=ca.path, - renamed_path=None, - online_results=[], - status=Status.no_match, - tags_saved=self.config.Runtime_Options__type, + match_status=MatchStatus.no_match, + tags_written=self.config.Runtime_Options__type, ) match_results.no_matches.append(res) return res @@ -519,44 +517,48 @@ class CLI: if low_confidence: logger.error("Online search: Multiple low confidence matches. Save aborted") res = Result( + Action.save, + status=Status.match_failure, original_path=ca.path, - renamed_path=None, online_results=matches, - status=Status.low_confidence_match, - tags_saved=self.config.Runtime_Options__type, + match_status=MatchStatus.low_confidence_match, + tags_written=self.config.Runtime_Options__type, ) match_results.low_confidence_matches.append(res) return res logger.error("Online search: Multiple good matches. Save aborted") res = Result( + Action.save, + status=Status.match_failure, original_path=ca.path, - renamed_path=None, online_results=matches, - status=Status.multiple_match, - tags_saved=self.config.Runtime_Options__type, + match_status=MatchStatus.multiple_match, + tags_written=self.config.Runtime_Options__type, ) match_results.multiple_matches.append(res) return res if low_confidence and self.config.Runtime_Options__abort_on_low_confidence: logger.error("Online search: Low confidence match. Save aborted") res = Result( + Action.save, + status=Status.match_failure, original_path=ca.path, - renamed_path=None, online_results=matches, - status=Status.low_confidence_match, - tags_saved=self.config.Runtime_Options__type, + match_status=MatchStatus.low_confidence_match, + tags_written=self.config.Runtime_Options__type, ) match_results.low_confidence_matches.append(res) return res if not found_match: logger.error("Online search: No match found. Save aborted") res = Result( + Action.save, + status=Status.match_failure, original_path=ca.path, - renamed_path=None, online_results=matches, - status=Status.no_match, - tags_saved=self.config.Runtime_Options__type, + match_status=MatchStatus.no_match, + tags_written=self.config.Runtime_Options__type, ) match_results.no_matches.append(res) return res @@ -567,11 +569,12 @@ class CLI: ct_md = self.actual_issue_data_fetch(matches[0].issue_id) if ct_md.is_empty: res = Result( - original_path=ca.path, - renamed_path=None, - online_results=matches, + Action.save, status=Status.fetch_data_failure, - tags_saved=self.config.Runtime_Options__type, + original_path=ca.path, + online_results=matches, + match_status=MatchStatus.good_match, + tags_written=self.config.Runtime_Options__type, ) match_results.fetch_data_failures.append(res) return res @@ -594,12 +597,13 @@ class CLI: md.fix_publisher() res = Result( - original_path=ca.path, - renamed_path=None, - online_results=matches, + Action.save, status=Status.success, + original_path=ca.path, + online_results=matches, + match_status=MatchStatus.good_match, md=md, - tags_saved=self.config.Runtime_Options__type, + tags_written=self.config.Runtime_Options__type, ) # ok, done building our metadata. time to save if self.actual_metadata_save(ca, md): @@ -619,7 +623,7 @@ class CLI: if md.series is None: logger.error(msg_hdr + "Can't rename without series name") - return Result(original_path, None, [], Status.no_match) + return Result(Action.rename, Status.read_failure, original_path) new_ext = "" # default if self.config.File_Rename__set_extension_based_on_archive: @@ -648,10 +652,10 @@ class CLI: + "https://docs.python.org/3/library/string.html#format-string-syntax", self.config.File_Rename__template, ) - return Result(original_path, None, [], Status.rename_failure, md=md) + return Result(Action.rename, Status.rename_failure, original_path, md=md) except Exception: logger.exception("Formatter failure: %s metadata: %s", self.config.File_Rename__template, renamer.metadata) - return Result(original_path, None, [], Status.rename_failure, md=md) + return Result(Action.rename, Status.rename_failure, original_path, md=md) folder = get_rename_dir(ca, self.config.File_Rename__dir if self.config.File_Rename__move_to_dir else None) @@ -659,7 +663,7 @@ class CLI: if full_path == ca.path: self.output(msg_hdr + "Filename is already good!") - return Result(original_path, full_path, [], Status.existing_tags, md=md) + return Result(Action.rename, Status.success, original_path, full_path, md=md) suffix = "" if not self.config.Runtime_Options__dryrun: @@ -668,43 +672,41 @@ class CLI: ca.rename(utils.unique_file(full_path)) except OSError: logger.exception("Failed to rename comic archive: %s", ca.path) - return Result(original_path, full_path, [], Status.write_failure, md=md) + return Result(Action.rename, Status.write_failure, original_path, full_path, md=md) else: suffix = " (dry-run, no change)" self.output(f"renamed '{original_path.name}' -> '{new_name}' {suffix}") - return Result(original_path, None, [], Status.success, md=md) + return Result(Action.rename, Status.success, original_path, md=md) - def export(self, ca: ComicArchive) -> None: + def export(self, ca: ComicArchive) -> Result: msg_hdr = "" if self.batch_mode: msg_hdr = f"{ca.path}: " if ca.is_zip(): logger.error(msg_hdr + "Archive is already a zip file.") - return + return Result(Action.export, Status.success, ca.path) filename_path = ca.path new_file = filename_path.with_suffix(".cbz") if self.config.Runtime_Options__abort_on_conflict and new_file.exists(): self.output(msg_hdr + f"{new_file.name} already exists in the that folder.") - return + return Result(Action.export, Status.write_failure, ca.path) new_file = utils.unique_file(new_file) delete_success = False export_success = False if not self.config.Runtime_Options__dryrun: - if ca.export_as_zip(new_file): - export_success = True + if export_success := ca.export_as_zip(new_file): if self.config.Runtime_Options__delete_after_zip_export: try: filename_path.unlink(missing_ok=True) delete_success = True except OSError: logger.exception(msg_hdr + "Error deleting original archive after export") - delete_success = False else: # last export failed, so remove the zip, if it exists new_file.unlink(missing_ok=True) @@ -713,7 +715,7 @@ class CLI: if self.config.Runtime_Options__delete_after_zip_export: msg += " and delete original." self.output(msg) - return + return Result(Action.export, Status.success, ca.path, new_file) msg = msg_hdr if export_success: @@ -725,40 +727,38 @@ class CLI: self.output(msg) - def process_file_cli(self, filename: str, match_results: OnlineMatchResults) -> Result: + return Result(Action.export, Status.success, ca.path, new_file) + + def process_file_cli(self, command: Action, filename: str, match_results: OnlineMatchResults) -> Result: if not os.path.lexists(filename): logger.error("Cannot find %s", filename) - return Result(pathlib.Path(filename), None, [], Status.read_failure) + return Result(command, Status.read_failure, pathlib.Path(filename)) ca = ComicArchive(filename, str(graphics_path / "nocover.png")) if not ca.seems_to_be_a_comic_archive(): logger.error("Sorry, but %s is not a comic archive!", filename) - return Result(ca.path, None, [], Status.read_failure) + return Result(Action.rename, Status.read_failure, ca.path) - if not ca.is_writable() and ( - self.config.Commands__delete - or self.config.Commands__copy - or self.config.Commands__save - or self.config.Commands__rename - ): + if not ca.is_writable() and (command in (Action.delete, Action.copy, Action.save, Action.rename)): logger.error("This archive is not writable") - return Result(ca.path, None, [], Status.write_permission_failure) + return Result(command, Status.write_permission_failure, ca.path) - if self.config.Commands__print: + if command == Action.print: return self.print(ca) - elif self.config.Commands__delete: + elif command == Action.delete: return self.delete(ca) - elif self.config.Commands__copy is not None: + elif command == Action.copy is not None: return self.copy(ca) - elif self.config.Commands__save: + elif command == Action.save: return self.save(ca, match_results) - elif self.config.Commands__rename: + elif command == Action.rename: return self.rename(ca) - elif self.config.Commands__export_to_zip: + elif command == Action.export: return self.export(ca) + return Result(None, Status.read_failure, ca.path) # type: ignore[arg-type] diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index 68478b8..06fbc10 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -32,6 +32,7 @@ from comictaggerlib.ctsettings.types import ( metadata_type_single, parse_metadata_from_string, ) +from comictaggerlib.resulttypes import Action logger = logging.getLogger(__name__) @@ -188,14 +189,18 @@ def register_commands(parser: settngs.Manager) -> None: parser.add_setting( "-p", "--print", - action="store_true", + dest="command", + action="store_const", + const=Action.print, help="""Print out tag info from file. Specify type\n(via -t) to get only info of that tag type.\n\n""", file=False, ) parser.add_setting( "-d", "--delete", - action="store_true", + dest="command", + action="store_const", + const=Action.delete, help="Deletes the tag block of specified type (via -t).\n", file=False, ) @@ -210,33 +215,43 @@ def register_commands(parser: settngs.Manager) -> None: parser.add_setting( "-s", "--save", - action="store_true", + dest="command", + action="store_const", + const=Action.save, help="Save out tags as specified type (via -t).\nMust specify also at least -o, -f, or -m.\n\n", file=False, ) parser.add_setting( "-r", "--rename", - action="store_true", + dest="command", + action="store_const", + const=Action.print, help="Rename the file based on specified tag style.", file=False, ) parser.add_setting( "-e", "--export-to-zip", - action="store_true", + dest="command", + action="store_const", + const=Action.export, help="Export RAR archive to Zip format.", file=False, ) parser.add_setting( "--only-save-config", - action="store_true", + dest="command", + action="store_const", + const=Action.save_config, help="Only save the configuration (eg, Comic Vine API key) and quit.", file=False, ) parser.add_setting( "--list-plugins", - action="store_true", + dest="command", + action="store_const", + const=Action.list_plugins, help="List the available plugins.\n\n", file=False, ) @@ -252,21 +267,11 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs parser.exit( status=1, message=f"ComicTagger {ctversion.version}: Copyright (c) 2012-2022 ComicTagger Team\n" - "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n", + + "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n", ) config[0].Runtime_Options__no_gui = any( - [ - config[0].Commands__print, - config[0].Commands__delete, - config[0].Commands__save, - config[0].Commands__copy, - config[0].Commands__rename, - config[0].Commands__export_to_zip, - config[0].Commands__only_save_config, - config[0].Commands__list_plugins, - config[0].Runtime_Options__no_gui, - ] + (config[0].Commands__command, config[0].Runtime_Options__no_gui, config[0].Commands__copy) ) if platform.system() == "Windows" and config[0].Runtime_Options__glob: @@ -279,19 +284,20 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs config[0].Runtime_Options__files.extend(glob.glob(item)) if ( - not config[0].Commands__only_save_config + config[0].Commands__command != Action.save_config and config[0].Runtime_Options__no_gui and not config[0].Runtime_Options__files ): parser.exit(message="Command requires at least one filename!\n", status=1) - if config[0].Commands__delete and not config[0].Runtime_Options__type: + if config[0].Commands__command == Action.delete and not config[0].Runtime_Options__type: parser.exit(message="Please specify the type to delete with -t\n", status=1) - if config[0].Commands__save and not config[0].Runtime_Options__type: + if config[0].Commands__command == Action.save and not config[0].Runtime_Options__type: parser.exit(message="Please specify the type to save with -t\n", status=1) if config[0].Commands__copy: + config[0].Commands__command = Action.copy if not config[0].Runtime_Options__type: parser.exit(message="Please specify the type to copy to with -t\n", status=1) diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py index ecb5521..66d03f3 100644 --- a/comictaggerlib/ctsettings/settngs_namespace.py +++ b/comictaggerlib/ctsettings/settngs_namespace.py @@ -5,18 +5,13 @@ import settngs import comicapi.genericmetadata import comictaggerlib.ctsettings.types import comictaggerlib.defaults +import comictaggerlib.resulttypes class settngs_namespace(settngs.TypedNS): Commands__version: bool - Commands__print: bool - Commands__delete: bool + Commands__command: comictaggerlib.resulttypes.Action Commands__copy: int - Commands__save: bool - Commands__rename: bool - Commands__export_to_zip: bool - Commands__only_save_config: bool - Commands__list_plugins: bool Runtime_Options__config: comictaggerlib.ctsettings.types.ComicTaggerPaths Runtime_Options__verbose: int diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index d5855a4..72754fd 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -35,6 +35,7 @@ from comictaggerlib import cli, ctsettings from comictaggerlib.ctsettings import ct_ns from comictaggerlib.ctversion import version from comictaggerlib.log import setup_logging +from comictaggerlib.resulttypes import Action if sys.version_info < (3, 10): import importlib_metadata @@ -239,11 +240,11 @@ class App: comicapi.utils.load_publishers() update_publishers(self.config) - if self.config[0].Commands__list_plugins: + if self.config[0].Commands__command == Action.list_plugins: self.list_plugins(list(talkers.values()), comicapi.comicarchive.archivers) return - if self.config[0].Commands__only_save_config: + if self.config[0].Commands__command == Action.save_config: if self.config_load_success: settings_path = self.config[0].Runtime_Options__config.user_config_dir / "settings.json" if self.config_load_success: diff --git a/comictaggerlib/resulttypes.py b/comictaggerlib/resulttypes.py index 7470f6f..e0196ba 100644 --- a/comictaggerlib/resulttypes.py +++ b/comictaggerlib/resulttypes.py @@ -69,6 +69,17 @@ class IssueResult: return f"series: {self.series}; series id: {self.series_id}; issue number: {self.issue_number}; issue id: {self.issue_id}; published: {self.month} {self.year}" +class Action(StrEnum): + print = auto() + delete = auto() + copy = auto() + save = auto() + rename = auto() + export = auto() + save_config = auto() + list_plugins = auto() + + class MatchStatus(StrEnum): good_match = auto() no_match = auto() @@ -77,11 +88,14 @@ class MatchStatus(StrEnum): class Status(StrEnum): + success = auto() + match_failure = auto() write_failure = auto() fetch_data_failure = auto() existing_tags = auto() read_failure = auto() write_permission_failure = auto() + rename_failure = auto() @dataclasses.dataclass @@ -96,14 +110,19 @@ class OnlineMatchResults: @dataclasses.dataclass class Result: + action: Action + status: Status | None + original_path: pathlib.Path - renamed_path: pathlib.Path | None + renamed_path: pathlib.Path | None = None + online_results: list[IssueResult] = dataclasses.field(default_factory=list) match_status: MatchStatus | None = None - status: Status | None = None + md: GenericMetadata | None = None - tags_removed: list[int] = dataclasses.field(default_factory=list) - tags_saved: list[int] = dataclasses.field(default_factory=list) + + tags_deleted: list[int] = dataclasses.field(default_factory=list) + tags_written: list[int] = dataclasses.field(default_factory=list) def __str__(self) -> str: if len(self.online_results) == 0: diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 9dc0a04..cbd2df9 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -57,7 +57,7 @@ from comictaggerlib.optionalmsgdialog import OptionalMessageDialog from comictaggerlib.pagebrowser import PageBrowserWindow from comictaggerlib.pagelisteditor import PageListEditor from comictaggerlib.renamewindow import RenameWindow -from comictaggerlib.resulttypes import IssueResult, OnlineMatchResults, Result +from comictaggerlib.resulttypes import Action, IssueResult, MatchStatus, OnlineMatchResults, Result, Status from comictaggerlib.seriesselectionwindow import SeriesSelectionWindow from comictaggerlib.settingswindow import SettingsWindow from comictaggerlib.ui import ui_path @@ -1779,16 +1779,48 @@ class TaggerWindow(QtWidgets.QMainWindow): if choices: if low_confidence: self.auto_tag_log("Online search: Multiple low-confidence matches. Save aborted\n") - match_results.low_confidence_matches.append(Result(ca.path, None, matches)) + match_results.low_confidence_matches.append( + Result( + Action.save, + Status.match_failure, + ca.path, + online_results=matches, + match_status=MatchStatus.low_confidence_match, + ) + ) else: self.auto_tag_log("Online search: Multiple matches. Save aborted\n") - match_results.multiple_matches.append(Result(ca.path, None, matches)) + match_results.multiple_matches.append( + Result( + Action.save, + Status.match_failure, + ca.path, + online_results=matches, + match_status=MatchStatus.multiple_match, + ) + ) elif low_confidence and not dlg.auto_save_on_low: self.auto_tag_log("Online search: Low confidence match. Save aborted\n") - match_results.low_confidence_matches.append(Result(ca.path, None, matches)) + match_results.low_confidence_matches.append( + Result( + Action.save, + Status.match_failure, + ca.path, + online_results=matches, + match_status=MatchStatus.low_confidence_match, + ) + ) elif not found_match: self.auto_tag_log("Online search: No match found. Save aborted\n") - match_results.no_matches.append(Result(ca.path, None, matches)) + match_results.no_matches.append( + Result( + Action.save, + Status.match_failure, + ca.path, + online_results=matches, + match_status=MatchStatus.no_match, + ) + ) else: # a single match! if low_confidence: @@ -1797,7 +1829,15 @@ class TaggerWindow(QtWidgets.QMainWindow): # now get the particular issue data ct_md = self.actual_issue_data_fetch(matches[0]) if ct_md is None: - match_results.fetch_data_failures.append(Result(ca.path, None, matches)) + match_results.fetch_data_failures.append( + Result( + Action.save, + Status.fetch_data_failure, + ca.path, + online_results=matches, + match_status=MatchStatus.good_match, + ) + ) if ct_md is not None: if dlg.cbxRemoveMetadata.isChecked(): @@ -1813,10 +1853,26 @@ class TaggerWindow(QtWidgets.QMainWindow): md.fix_publisher() if not ca.write_metadata(md, self.save_data_style): - match_results.write_failures.append(Result(ca.path, None, matches)) + match_results.write_failures.append( + Result( + Action.save, + Status.write_failure, + ca.path, + online_results=matches, + match_status=MatchStatus.good_match, + ) + ) self.auto_tag_log("Save failed ;-(\n") else: - match_results.good_matches.append(Result(ca.path, None, matches)) + match_results.good_matches.append( + Result( + Action.save, + Status.success, + ca.path, + online_results=matches, + match_status=MatchStatus.good_match, + ) + ) success = True self.auto_tag_log("Save complete!\n") ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX]) diff --git a/setup.cfg b/setup.cfg index 304037f..9a0f235 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ install_requires = pyrate-limiter>=2.6,<3 rapidfuzz>=2.12.0 requests==2.* - settngs==0.8.0 + settngs==0.9.1 text2digits typing-extensions>=4.3.0 wordninja