From 97e65f10cf8374d90741667b10f43d3873a9ceed Mon Sep 17 00:00:00 2001 From: lordwelch Date: Mon, 23 May 2022 22:24:13 -0700 Subject: [PATCH] argparse... --- comicapi/comicarchive.py | 1 + comicapi/utils.py | 4 +- comictaggerlib/cli.py | 115 ++--- comictaggerlib/comicvinetalker.py | 4 +- comictaggerlib/main.py | 7 +- comictaggerlib/options.py | 764 ++++++++++++++---------------- comictaggerlib/resulttypes.py | 2 +- comictaggerlib/taggerwindow.py | 2 +- 8 files changed, 413 insertions(+), 486 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 59510c8..f7d171f 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -64,6 +64,7 @@ class MetaDataStyle: CIX = 1 COMET = 2 name = ["ComicBookLover", "ComicRack", "CoMet"] + short = ["cbl", "cr", "comet"] class UnknownArchiver: diff --git a/comicapi/utils.py b/comicapi/utils.py index 01d953e..63ff7f9 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -21,7 +21,7 @@ import pathlib import re import unicodedata from collections import defaultdict -from typing import Any, List, Optional, Union +from typing import Any, Iterable, List, Optional, Union import pycountry @@ -233,7 +233,7 @@ class ImprintDict(dict): if the key does not exist the key is returned as the publisher unchanged """ - def __init__(self, publisher, mapping=(), **kwargs): + def __init__(self, publisher: str, mapping: Iterable = (), **kwargs: Any): super().__init__(mapping, **kwargs) self.publisher = publisher diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 26f394a..c086cfe 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -16,12 +16,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse import json import logging import os import sys from pprint import pprint -from typing import cast from comicapi import utils from comicapi.comicarchive import ComicArchive, MetaDataStyle @@ -30,18 +30,19 @@ from comictaggerlib.cbltransformer import CBLTransformer from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException from comictaggerlib.filerenamer import FileRenamer from comictaggerlib.issueidentifier import IssueIdentifier -from comictaggerlib.options import Options from comictaggerlib.resulttypes import IssueResult, MultipleMatch, OnlineMatchResults from comictaggerlib.settings import ComicTaggerSettings logger = logging.getLogger(__name__) -def actual_issue_data_fetch(match: IssueResult, settings: ComicTaggerSettings, opts: Options) -> GenericMetadata: +def actual_issue_data_fetch( + match: IssueResult, settings: ComicTaggerSettings, opts: argparse.Namespace +) -> GenericMetadata: # now get the particular issue data try: comic_vine = ComicVineTalker() - comic_vine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit + comic_vine.wait_for_rate_limit = opts.wait_on_cv_rate_limit cv_md = comic_vine.fetch_issue_data(match["volume_id"], match["issue_number"], settings) except ComicVineTalkerException: logger.exception("Network error while getting issue details. Save aborted") @@ -53,10 +54,10 @@ def actual_issue_data_fetch(match: IssueResult, settings: ComicTaggerSettings, o return cv_md -def actual_metadata_save(ca: ComicArchive, opts: Options, md: GenericMetadata) -> bool: +def actual_metadata_save(ca: ComicArchive, opts: argparse.Namespace, md: GenericMetadata) -> bool: if not opts.dryrun: # write out the new data - if not ca.write_metadata(md, opts.data_style if opts.data_style is not None else 0): + if not ca.write_metadata(md, opts.type if opts.type is not None else 0): logger.error("The tag save seemed to fail!") return False @@ -74,7 +75,7 @@ def actual_metadata_save(ca: ComicArchive, opts: Options, md: GenericMetadata) - def display_match_set_for_choice( - label: str, match_set: MultipleMatch, opts: Options, settings: ComicTaggerSettings + label: str, match_set: MultipleMatch, opts: argparse.Namespace, settings: ComicTaggerSettings ) -> None: print(f"{match_set.ca.path} -- {label}:") @@ -103,11 +104,9 @@ def display_match_set_for_choice( # save the data! # we know at this point, that the file is all good to go ca = match_set.ca - md = create_local_metadata( - opts, ca, ca.has_metadata(opts.data_style if opts.data_style is not None else 0), settings - ) + md = create_local_metadata(opts, ca, ca.has_metadata(opts.type if opts.type is not None else 0), settings) cv_md = actual_issue_data_fetch(match_set.matches[int(i) - 1], settings, opts) - if opts.overwrite_metadata: + if opts.overwrite: md = cv_md else: md.overlay(cv_md) @@ -118,7 +117,9 @@ def display_match_set_for_choice( actual_metadata_save(ca, opts, md) -def post_process_matches(match_results: OnlineMatchResults, opts: Options, settings: ComicTaggerSettings) -> None: +def post_process_matches( + match_results: OnlineMatchResults, opts: argparse.Namespace, settings: ComicTaggerSettings +) -> None: # now go through the match results if opts.show_save_summary: if len(match_results.good_matches) > 0: @@ -161,14 +162,14 @@ def post_process_matches(match_results: OnlineMatchResults, opts: Options, setti display_match_set_for_choice(label, match_set, opts, settings) -def cli_mode(opts: Options, settings: ComicTaggerSettings) -> None: - if len(opts.file_list) < 1: +def cli_mode(opts: argparse.Namespace, settings: ComicTaggerSettings) -> None: + if len(opts.files) < 1: logger.error("You must specify at least one filename. Use the -h option for more info") return match_results = OnlineMatchResults() - for f in opts.file_list: + for f in opts.files: process_file_cli(f, opts, settings, match_results) sys.stdout.flush() @@ -176,7 +177,7 @@ def cli_mode(opts: Options, settings: ComicTaggerSettings) -> None: def create_local_metadata( - opts: Options, ca: ComicArchive, has_desired_tags: bool, settings: ComicTaggerSettings + opts: argparse.Namespace, ca: ComicArchive, has_desired_tags: bool, settings: ComicTaggerSettings ) -> GenericMetadata: md = GenericMetadata() md.set_default_page_list(ca.get_number_of_pages()) @@ -190,17 +191,17 @@ def create_local_metadata( settings.remove_publisher, opts.split_words, ) - if opts.overwrite_metadata: + if opts.overwrite: md = f_md else: md.overlay(f_md) if has_desired_tags: - t_md = ca.read_metadata(opts.data_style if opts.data_style is not None else 0) + t_md = ca.read_metadata(opts.type if opts.type is not None else 0) md.overlay(t_md) # finally, use explicit stuff - if opts.overwrite_metadata and not opts.metadata.is_empty: + if opts.overwrite and not opts.metadata.is_empty: md = opts.metadata else: md.overlay(opts.metadata) @@ -209,9 +210,9 @@ def create_local_metadata( def process_file_cli( - filename: str, opts: Options, settings: ComicTaggerSettings, match_results: OnlineMatchResults + filename: str, opts: argparse.Namespace, settings: ComicTaggerSettings, match_results: OnlineMatchResults ) -> None: - batch_mode = len(opts.file_list) > 1 + batch_mode = len(opts.files) > 1 ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.get_graphic("nocover.png")) @@ -223,7 +224,7 @@ def process_file_cli( logger.error("Sorry, but %s is not a comic archive!", filename) return - if not ca.is_writable() and (opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file): + if not ca.is_writable() and (opts.delete or opts.copy or opts.save or opts.rename): logger.error("This archive is not writable for that tag type") return @@ -235,9 +236,9 @@ def process_file_cli( if ca.has_comet(): has[MetaDataStyle.COMET] = True - if opts.print_tags: + if opts.print: - if opts.data_style is None: + if opts.type is None: page_count = ca.get_number_of_pages() brief = "" @@ -275,7 +276,7 @@ def process_file_cli( print() - if opts.data_style is None or opts.data_style == MetaDataStyle.CIX: + if opts.type is None or opts.type == MetaDataStyle.CIX: if has[MetaDataStyle.CIX]: print("--------- ComicRack tags ---------") if opts.raw: @@ -283,7 +284,7 @@ def process_file_cli( else: print(ca.read_cix()) - if opts.data_style is None or opts.data_style == MetaDataStyle.CBI: + if opts.type is None or opts.type == MetaDataStyle.CBI: if has[MetaDataStyle.CBI]: print("------- ComicBookLover tags -------") if opts.raw: @@ -291,7 +292,7 @@ def process_file_cli( else: print(ca.read_cbi()) - if opts.data_style is None or opts.data_style == MetaDataStyle.COMET: + if opts.type is None or opts.type == MetaDataStyle.COMET: if has[MetaDataStyle.COMET]: print("----------- CoMet tags -----------") if opts.raw: @@ -299,11 +300,11 @@ def process_file_cli( else: print(ca.read_comet()) - elif opts.delete_tags: - style_name = MetaDataStyle.name[cast(int, opts.data_style)] - if has[cast(int, opts.data_style)]: + elif opts.delete: + style_name = MetaDataStyle.name[opts.type] + if has[opts.type]: if not opts.dryrun: - if not ca.remove_metadata(cast(int, opts.data_style)): + if not ca.remove_metadata(opts.type): print(f"{filename}: Tag removal seemed to fail!") else: print(f"{filename}: Removed {style_name} tags.") @@ -312,24 +313,24 @@ def process_file_cli( else: print(f"{filename}: This archive doesn't have {style_name} tags to remove.") - elif opts.copy_tags: - dst_style_name = MetaDataStyle.name[cast(int, opts.data_style)] - if opts.no_overwrite and has[cast(int, opts.data_style)]: + elif opts.copy: + dst_style_name = MetaDataStyle.name[opts.type] + if opts.no_overwrite and has[opts.type]: print(f"{filename}: Already has {dst_style_name} tags. Not overwriting.") return - if opts.copy_source == opts.data_style: + if opts.copy == opts.type: print(f"{filename}: Destination and source are same: {dst_style_name}. Nothing to do.") return - src_style_name = MetaDataStyle.name[cast(int, opts.copy_source)] - if has[cast(int, opts.copy_source)]: + src_style_name = MetaDataStyle.name[opts.copy] + if has[opts.copy]: if not opts.dryrun: - md = ca.read_metadata(cast(int, opts.copy_source)) + md = ca.read_metadata(opts.copy) - if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI: + if settings.apply_cbl_transform_on_bulk_operation and opts.type == MetaDataStyle.CBI: md = CBLTransformer(md, settings).apply() - if not ca.write_metadata(md, cast(int, opts.data_style)): + if not ca.write_metadata(md, opts.type): print(f"{filename}: Tag copy seemed to fail!") else: print(f"{filename}: Copied {src_style_name} tags to {dst_style_name}.") @@ -338,35 +339,35 @@ def process_file_cli( else: print(f"{filename}: This archive doesn't have {src_style_name} tags to copy.") - elif opts.save_tags: + elif opts.save: - if opts.no_overwrite and has[cast(int, opts.data_style)]: - print(f"{filename}: Already has {MetaDataStyle.name[cast(int, opts.data_style)]} tags. Not overwriting.") + if opts.no_overwrite and has[opts.type]: + print(f"{filename}: Already has {MetaDataStyle.name[opts.type]} tags. Not overwriting.") return if batch_mode: print(f"Processing {ca.path}...") - md = create_local_metadata(opts, ca, has[cast(int, opts.data_style)], settings) + md = create_local_metadata(opts, ca, has[opts.type], settings) if md.issue is None or md.issue == "": - if opts.assume_issue_is_one_if_not_set: + if opts.assume_issue_one: md.issue = "1" # now, search online - if opts.search_online: - if opts.issue_id is not None: + if opts.online: + if opts.id is not None: # we were given the actual ID to search with try: comic_vine = ComicVineTalker() - comic_vine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit - cv_md = comic_vine.fetch_issue_data_by_issue_id(opts.issue_id, settings) + comic_vine.wait_for_rate_limit = opts.wait_on_cv_rate_limit + cv_md = comic_vine.fetch_issue_data_by_issue_id(opts.id, settings) except ComicVineTalkerException: logger.exception("Network error while getting issue details. Save aborted") match_results.fetch_data_failures.append(str(ca.path.absolute())) return if cv_md is None: - logger.error("No match for ID %s was found.", opts.issue_id) + logger.error("No match for ID %s was found.", opts.id) match_results.no_matches.append(str(ca.path.absolute())) return @@ -387,7 +388,7 @@ def process_file_cli( # use our overlayed MD struct to search ii.set_additional_metadata(md) ii.only_use_additional_meta_data = True - ii.wait_and_retry_on_rate_limit = opts.wait_and_retry_on_rate_limit + ii.wait_and_retry_on_rate_limit = opts.wait_on_cv_rate_limit ii.set_output_function(myoutput) ii.cover_page_index = md.get_cover_page_index_list()[0] matches = ii.search() @@ -422,7 +423,7 @@ def process_file_cli( logger.error("Online search: Multiple good matches. Save aborted") match_results.multiple_matches.append(MultipleMatch(ca, matches)) return - if low_confidence and opts.abortOnLowConfidence: + if low_confidence and opts.abort_on_low_confidence: logger.error("Online search: Low confidence match. Save aborted") match_results.low_confidence_matches.append(MultipleMatch(ca, matches)) return @@ -439,7 +440,7 @@ def process_file_cli( match_results.fetch_data_failures.append(str(ca.path.absolute())) return - if opts.overwrite_metadata: + if opts.overwrite: md = cv_md else: md.overlay(cv_md) @@ -453,14 +454,14 @@ def process_file_cli( else: match_results.good_matches.append(str(ca.path.absolute())) - elif opts.rename_file: + elif opts.rename: msg_hdr = "" if batch_mode: msg_hdr = f"{ca.path}: " - if opts.data_style is not None: - use_tags = has[opts.data_style] + if opts.type is not None: + use_tags = has[opts.type] else: use_tags = False @@ -529,7 +530,7 @@ def process_file_cli( rar_file = os.path.abspath(os.path.abspath(filename)) new_file = os.path.splitext(rar_file)[0] + ".cbz" - if opts.abort_export_on_conflict and os.path.lexists(new_file): + if opts.abort_on_conflict and os.path.lexists(new_file): print(msg_hdr + f"{os.path.split(new_file)[1]} already exists in the that folder.") return diff --git a/comictaggerlib/comicvinetalker.py b/comictaggerlib/comicvinetalker.py index 56d6242..a0f30ff 100644 --- a/comictaggerlib/comicvinetalker.py +++ b/comictaggerlib/comicvinetalker.py @@ -438,9 +438,9 @@ class ComicVineTalker: # Now, map the Comic Vine data to generic metadata return self.map_cv_data_to_metadata(volume_results, issue_results, settings) - def fetch_issue_data_by_issue_id(self, issue_id: str, settings: ComicTaggerSettings) -> GenericMetadata: + def fetch_issue_data_by_issue_id(self, issue_id: int, settings: ComicTaggerSettings) -> GenericMetadata: - issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + issue_id + issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + str(issue_id) params = {"api_key": self.api_key, "format": "json"} cv_response = self.get_cv_content(issue_url, params) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index f0ba44c..3ec1ab4 100755 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -32,7 +32,7 @@ from comicapi import utils from comictaggerlib import cli from comictaggerlib.comicvinetalker import ComicVineTalker from comictaggerlib.ctversion import version -from comictaggerlib.options import Options +from comictaggerlib.options import parse_cmd_line from comictaggerlib.settings import ComicTaggerSettings logger = logging.getLogger("comictagger") @@ -111,8 +111,7 @@ def update_publishers() -> None: def ctmain() -> None: - opts = Options() - opts.parse_cmd_line_args() + opts = parse_cmd_line() SETTINGS = ComicTaggerSettings(opts.config_path) os.makedirs(ComicTaggerSettings.get_settings_folder() / "logs", exist_ok=True) @@ -138,7 +137,7 @@ def ctmain() -> None: if opts.cv_api_key != SETTINGS.cv_api_key: SETTINGS.cv_api_key = opts.cv_api_key SETTINGS.save() - if opts.only_set_key: + if opts.only_set_cv_key: print("Key set") return diff --git a/comictaggerlib/options.py b/comictaggerlib/options.py index 4e9247c..5dc13b4 100644 --- a/comictaggerlib/options.py +++ b/comictaggerlib/options.py @@ -14,12 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import getopt +import argparse import logging import os import platform import sys -from typing import Optional from comicapi import utils from comicapi.comicarchive import MetaDataStyle @@ -29,440 +28,367 @@ from comictaggerlib import ctversion logger = logging.getLogger(__name__) -class Options: - help_text = """Usage: {0} [option] ... [file [files ...]] +def define_args() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="""A utility for reading and writing metadata to comic archives. -A utility for reading and writing metadata to comic archives. + If no options are given, %(prog)s will run in windowed mode.""", + epilog="For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "--version", + action="store_true", + help="Display version.", + ) + commands = parser.add_mutually_exclusive_group() + commands.add_argument( + "-p", + "--print", + action="store_true", + help="""Print out tag info from file. Specify type\n(via -t) to get only info of that tag type.\n\n""", + ) + commands.add_argument( + "-d", + "--delete", + action="store_true", + help="Deletes the tag block of specified type (via -t).\n", + ) + commands.add_argument( + "-c", + "--copy", + type=metadata_type, + metavar="{CR,CBL,COMET}", + help="Copy the specified source tag block to\ndestination style specified via -t\n(potentially lossy operation).\n\n", + ) + commands.add_argument( + "-s", + "--save", + action="store_true", + help="Save out tags as specified type (via -t).\nMust specify also at least -o, -f, or -m.\n\n", + ) + commands.add_argument( + "-r", + "--rename", + action="store_true", + help="Rename the file based on specified tag style.", + ) + commands.add_argument( + "-e", + "--export-to-zip", + action="store_true", + help="Export RAR archive to Zip format.", + ) + commands.add_argument( + "--only-set-cv-key", + action="store_true", + help="Only set the Comic Vine API key and quit.\n\n", + ) + parser.add_argument( + "-1", + "--assume-issue-one", + action="store_true", + help="""Assume issue number is 1 if not found (relevant for -s).\n\n""", + ) + parser.add_argument( + "--abort-on-conflict", + action="store_true", + help="""Don't export to zip if intended new filename\nexists (otherwise, creates a new unique filename).\n\n""", + ) + parser.add_argument( + "-a", + "--auto-imprint", + action="store_true", + help="""Enables the auto imprint functionality.\ne.g. if the publisher is set to 'vertigo' it\nwill be updated to 'DC Comics' and the imprint\nproperty will be set to 'Vertigo'.\n\n""", + ) + parser.add_argument( + "--config", + dest="config_path", + help="""Config directory defaults to ~/.ComicTagger\non Linux/Mac and %%APPDATA%% on Windows\n""", + ) + parser.add_argument( + "--cv-api-key", + help="Use the given Comic Vine API Key (persisted in settings).", + ) + parser.add_argument( + "--delete-rar", + action="store_true", + dest="delete_after_zip_export", + help="""Delete original RAR archive after successful\nexport to Zip.""", + ) + parser.add_argument( + "-f", + "--parse-filename", + "--parsefilename", + action="store_true", + help="""Parse the filename to get some info,\nspecifically series name, issue number,\nvolume, and publication year.\n\n""", + ) + parser.add_argument( + "--id", + dest="issue_id", + type=int, + help="""Use the issue ID when searching online.\nOverrides all other metadata.\n\n""", + ) + parser.add_argument( + "-t", + "--type", + metavar="{CR,CBL,COMET}", + type=metadata_type, + help="""Specify TYPE as either CR, CBL, COMET\n(as either ComicRack, ComicBookLover,\nor CoMet style tags, respectively).\n\n""", + ) + parser.add_argument( + "-o", + "--online", + action="store_true", + help="""Search online and attempt to identify file\nusing existing metadata and images in archive.\nMay be used in conjunction with -f and -m.\n\n""", + ) + parser.add_argument( + "-m", + "--metadata", + type=parse_metadata_from_string, + help="""Explicitly define, as a list, some tags to be used. e.g.:\n"series=Plastic Man, publisher=Quality Comics"\n"series=Kickers^, Inc., issue=1, year=1986"\nName-Value pairs are comma separated. Use a\n"^" to escape an "=" or a ",", as shown in\nthe example above. Some names that can be\nused: series, issue, issue_count, year,\npublisher, title\n\n""", + ) + parser.add_argument( + "-i", + "--interactive", + action="store_true", + help="""Interactively query the user when there are\nmultiple matches for an online search.\n\n""", + ) + parser.add_argument( + "--no-overwrite", + "--nooverwrite", + action="store_true", + help="""Don't modify tag block if it already exists (relevant for -s or -c).""", + ) + parser.add_argument( + "--noabort", + dest="abort_on_low_confidence", + action="store_false", + help="""Don't abort save operation when online match\nis of low confidence.\n\n""", + ) + parser.add_argument( + "--nosummary", + dest="show_save_summary", + action="store_false", + help="Suppress the default summary after a save operation.\n\n", + ) + parser.add_argument( + "--overwrite", + action="store_true", + help="""Overwite all existing metadata.\nMay be used in conjunction with -o, -f and -m.\n\n""", + ) + parser.add_argument( + "--raw", action="store_true", help="""With -p, will print out the raw tag block(s)\nfrom the file.\n""" + ) + parser.add_argument( + "-R", + "--recursive", + action="store_true", + help="Recursively include files in sub-folders.", + ) + parser.add_argument( + "-S", + "--script", + help="""Run an "add-on" python script that uses the\nComicTagger library for custom processing.\nScript arguments can follow the script name.\n\n""", + ) + parser.add_argument( + "--split-words", + action="store_true", + help="""Splits words before parsing the filename.\ne.g. 'judgedredd' to 'judge dredd'\n\n""", + ) + parser.add_argument( + "--terse", + action="store_true", + help="Don't say much (for print mode).", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Be noisy when doing what it does.", + ) + parser.add_argument( + "-w", + "--wait-on-cv-rate-limit", + action="store_true", + help="""When encountering a Comic Vine rate limit\nerror, wait and retry query.\n\n""", + ) + parser.add_argument( + "-n", "--dryrun", action="store_true", help="Don't actually modify file (only relevant for -d, -s, or -r).\n\n" + ) + parser.add_argument( + "--darkmode", + action="store_true", + help="Windows only. Force a dark pallet", + ) + parser.add_argument( + "-g", + "--glob", + action="store_true", + help="Windows only. Enable globbing", + ) + parser.add_argument("files", nargs="*") + return parser -If no options are given, {0} will run in windowed mode. --p, --print Print out tag info from file. Specify type - (via -t) to get only info of that tag type. - --raw With -p, will print out the raw tag block(s) - from the file. --d, --delete Deletes the tag block of specified type (via - -t). --c, --copy=SOURCE Copy the specified source tag block to - destination style specified via -t - (potentially lossy operation). --s, --save Save out tags as specified type (via -t). - Must specify also at least -o, -p, or -m. - --nooverwrite Don't modify tag block if it already exists - (relevant for -s or -c). --1, --assume-issue-one Assume issue number is 1 if not found - (relevant for -s). --n, --dryrun Don't actually modify file (only relevant for - -d, -s, or -r). --t, --type=TYPE Specify TYPE as either "CR", "CBL", or - "COMET" (as either ComicRack, ComicBookLover, - or CoMet style tags, respectively). --f, --parsefilename Parse the filename to get some info, - specifically series name, issue number, - volume, and publication year. - --split-words Splits words before parsing the filename. - e.g. 'judgedredd' to 'judge dredd' --i, --interactive Interactively query the user when there are - multiple matches for an online search. - --nosummary Suppress the default summary after a save - operation. --o, --online Search online and attempt to identify file - using existing metadata and images in archive. - May be used in conjunction with -f and -m. - --overwrite Overwite any existing metadata. - May be used in conjunction with -o, -f and -m. - --id=ID Use the issue ID when searching online. - Overrides all other metadata. --m, --metadata=LIST Explicitly define, as a list, some tags to be - used. e.g.: - "series=Plastic Man, publisher=Quality Comics" - "series=Kickers^, Inc., issue=1, year=1986" - Name-Value pairs are comma separated. Use a - "^" to escape an "=" or a ",", as shown in - the example above. Some names that can be - used: series, issue, issueCount, year, - publisher, title --a, --auto-imprint Enables the auto imprint functionality. - e.g. if the publisher is set to 'vertigo' it - will be updated to 'DC Comics' and the imprint - property will be set to 'Vertigo'. --r, --rename Rename the file based on specified tag style. - --noabort Don't abort save operation when online match - is of low confidence. --e, --export-to-zip Export RAR archive to Zip format. - --delete-rar Delete original RAR archive after successful - export to Zip. - --abort-on-conflict Don't export to zip if intended new filename - exists (otherwise, creates a new unique - filename). --S, --script=FILE Run an "add-on" python script that uses the - ComicTagger library for custom processing. - Script arguments can follow the script name. --R, --recursive Recursively include files in sub-folders. - --cv-api-key=KEY Use the given Comic Vine API Key (persisted - in settings). - --only-set-cv-key Only set the Comic Vine API key and quit. --w, --wait-on-cv-rate-limit When encountering a Comic Vine rate limit - error, wait and retry query. --v, --verbose Be noisy when doing what it does. - --terse Don't say much (for print mode). - --darkmode Windows only. Force a dark pallet - --config=CONFIG_DIR Config directory defaults to ~/.ComicTagger - on Linux/Mac and %APPDATA% on Windows - --version Display version. --h, --help Display this message. +def metadata_type(typ: str) -> int: + if typ.casefold() not in MetaDataStyle.short: + choices = ", ".join(MetaDataStyle.short) + raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})") + return MetaDataStyle.short.index(typ) -For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki -""" - def __init__(self) -> None: - self.data_style: Optional[int] = None - self.no_gui = False - self.filename: Optional[str] = None - self.verbose = False - self.terse = False - self.metadata = GenericMetadata() - self.auto_imprint = False - self.print_tags = False - self.copy_tags = False - self.delete_tags = False - self.export_to_zip = False - self.abort_export_on_conflict = False - self.delete_after_zip_export = False - self.search_online = False - self.dryrun = False - self.abortOnLowConfidence = True - self.save_tags = False - self.parse_filename = False - self.show_save_summary = True - self.raw = False - self.cv_api_key: Optional[str] = None - self.only_set_key = False - self.rename_file = False - self.no_overwrite = False - self.interactive = False - self.issue_id: Optional[str] = None - self.recursive = False - self.run_script = False - self.script: Optional[str] = None - self.wait_and_retry_on_rate_limit = False - self.assume_issue_is_one_if_not_set = False - self.file_list: list[str] = [] - self.darkmode = False - self.copy_source: Optional[int] = None - self.config_path = "" - self.overwrite_metadata = False - self.split_words = False +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) + The caret is the special "escape character", since it's not common in + natural language text - def display_msg_and_quit(self, msg: Optional[str], code: int, show_help: bool = False) -> None: - appname = os.path.basename(sys.argv[0]) - if msg is not None: - print(msg) - if show_help: - print((self.help_text.format(appname))) + example = "series=Kickers^, Inc. ,issue=1, year=1986" + """ + + escaped_comma = "^," + escaped_equals = "^=" + replacement_token = "<_~_>" + + md = GenericMetadata() + + # First, replace escaped commas with with a unique token (to be changed back later) + mdstr = mdstr.replace(escaped_comma, replacement_token) + tmp_list = mdstr.split(",") + md_list = [] + for item in tmp_list: + item = item.replace(replacement_token, ",") + md_list.append(item) + + # Now build a nice dict from the list + md_dict = {} + for item in md_list: + # Make sure to fix any escaped equal signs + i = item.replace(escaped_equals, replacement_token) + key, value = i.split("=") + value = value.replace(replacement_token, "=").strip() + key = key.strip() + if key.lower() == "credit": + cred_attribs = value.split(":") + role = cred_attribs[0] + person = cred_attribs[1] if len(cred_attribs) > 1 else "" + primary = len(cred_attribs) > 2 + md.add_credit(person.strip(), role.strip(), primary) else: - print("For more help, run with '--help'") - sys.exit(code) + md_dict[key] = value - def parse_metadata_from_string(self, 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) - The caret is the special "escape character", since it's not common in - natural language text - - example = "series=Kickers^, Inc. ,issue=1, year=1986" - """ - - escaped_comma = "^," - escaped_equals = "^=" - replacement_token = "<_~_>" - - md = GenericMetadata() - - # First, replace escaped commas with with a unique token (to be changed back later) - mdstr = mdstr.replace(escaped_comma, replacement_token) - tmp_list = mdstr.split(",") - md_list = [] - for item in tmp_list: - item = item.replace(replacement_token, ",") - md_list.append(item) - - # Now build a nice dict from the list - md_dict = {} - for item in md_list: - # Make sure to fix any escaped equal signs - i = item.replace(escaped_equals, replacement_token) - key, value = i.split("=") - value = value.replace(replacement_token, "=").strip() - key = key.strip() - if key.lower() == "credit": - cred_attribs = value.split(":") - role = cred_attribs[0] - person = cred_attribs[1] if len(cred_attribs) > 1 else "" - primary = len(cred_attribs) > 2 - md.add_credit(person.strip(), role.strip(), primary) - else: - md_dict[key] = value - - # Map the dict to the metadata object - for key, value in md_dict.items(): - if not hasattr(md, key): - logger.warning("'%s' is not a valid tag name", key) - else: - md.is_empty = False - setattr(md, key, value) - return md - - def launch_script(self, scriptfile: str) -> None: - # we were given a script. special case for the args: - # 1. ignore everything before the -S, - # 2. pass all the ones that follow (including script name) to the script - script_args = [] - for idx, arg in enumerate(sys.argv): - if arg in ["-S", "--script"]: - # found script! - script_args = sys.argv[idx + 1 :] - break - sys.argv = script_args - if not os.path.exists(scriptfile): - logger.error("Can't find %s", scriptfile) + # Map the dict to the metadata object + for key, value in md_dict.items(): + if not hasattr(md, key): + raise argparse.ArgumentTypeError(f"'{key}' is not a valid tag name") else: - # I *think* this makes sense: - # assume the base name of the file is the module name - # add the folder of the given file to the python path import module - dirname = os.path.dirname(scriptfile) - module_name = os.path.splitext(os.path.basename(scriptfile))[0] - sys.path = [dirname] + sys.path - try: - script = __import__(module_name) + md.is_empty = False + setattr(md, key, value) + return md - # Determine if the entry point exists before trying to run it - if "main" in dir(script): - script.main() - else: - logger.error("Can't find entry point 'main()' in module '%s'", module_name) - except Exception: - logger.exception("Script: %s raised an unhandled exception: ", module_name) - sys.exit(0) - - def parse_cmd_line_args(self) -> None: - - if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1: - # remove the PSN (process serial number) argument from OS/X - input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a] - else: - input_args = sys.argv[1:] - - # first check if we're launching a script: - for n, _ in enumerate(input_args): - if input_args[n] in ["-S", "--script"] and n + 1 < len(input_args): - # insert a "--" which will cause getopt to ignore the remaining args - # so they will be passed to the script - input_args.insert(n + 2, "--") - break - - # parse command line options +def launch_script(scriptfile: str, args: list[str]) -> None: + # we were given a script. special case for the args: + # 1. ignore everything before the -S, + # 2. pass all the ones that follow (including script name) to the script + if not os.path.exists(scriptfile): + logger.error("Can't find %s", scriptfile) + else: + # I *think* this makes sense: + # assume the base name of the file is the module name + # add the folder of the given file to the python path import module + dirname = os.path.dirname(scriptfile) + module_name = os.path.splitext(os.path.basename(scriptfile))[0] + sys.path = [dirname] + sys.path try: - opts, args = getopt.getopt( - input_args, - "hpdt:fm:vownsrc:ieRS:1", - [ - "help", - "print", - "delete", - "type=", - "copy=", - "parsefilename", - "metadata=", - "verbose", - "online", - "dryrun", - "save", - "rename", - "raw", - "noabort", - "terse", - "nooverwrite", - "interactive", - "nosummary", - "version", - "id=", - "recursive", - "script=", - "export-to-zip", - "delete-rar", - "abort-on-conflict", - "assume-issue-one", - "cv-api-key=", - "only-set-cv-key", - "wait-on-cv-rate-limit", - "darkmode", - "config=", - "overwrite", - "split-words", - ], - ) + script = __import__(module_name) - except getopt.GetoptError as err: - self.display_msg_and_quit(str(err), 2) - - # process options - for o, a in opts: - if o in ("-h", "--help"): - self.display_msg_and_quit(None, 0, show_help=True) - if o in ("-v", "--verbose"): - self.verbose = True - if o in ("-S", "--script"): - self.run_script = True - self.script = a - if o in ("-R", "--recursive"): - self.recursive = True - if o in ("-p", "--print"): - self.print_tags = True - if o in ("-d", "--delete"): - self.delete_tags = True - if o in ("-i", "--interactive"): - self.interactive = True - if o in ("-a", "--auto-imprint"): - self.auto_imprint = True - if o in ("-c", "--copy"): - self.copy_tags = True - - if a.lower() == "cr": - self.copy_source = MetaDataStyle.CIX - elif a.lower() == "cbl": - self.copy_source = MetaDataStyle.CBI - elif a.lower() == "comet": - self.copy_source = MetaDataStyle.COMET - else: - self.display_msg_and_quit("Invalid copy tag source type", 1) - if o in ("-o", "--online"): - self.search_online = True - if o == "--overwrite": - self.overwrite_metadata = True - if o in ("-n", "--dryrun"): - self.dryrun = True - if o in ("-m", "--metadata"): - self.metadata = self.parse_metadata_from_string(a) - if o in ("-s", "--save"): - self.save_tags = True - if o in ("-r", "--rename"): - self.rename_file = True - if o in ("-e", "--export_to_zip"): - self.export_to_zip = True - if o == "--delete-rar": - self.delete_after_zip_export = True - if o == "--abort-on-conflict": - self.abort_export_on_conflict = True - if o in ("-f", "--parsefilename"): - self.parse_filename = True - if o == "--split-words": - self.split_words = True - if o in ("-w", "--wait-on-cv-rate-limit"): - self.wait_and_retry_on_rate_limit = True - if o == "--config": - self.config_path = os.path.abspath(a) - if o == "--id": - self.issue_id = a - if o == "--raw": - self.raw = True - if o == "--noabort": - self.abortOnLowConfidence = False - if o == "--terse": - self.terse = True - if o == "--nosummary": - self.show_save_summary = False - if o in ("-1", "--assume-issue-one"): - self.assume_issue_is_one_if_not_set = True - if o == "--nooverwrite": - self.no_overwrite = True - if o == "--cv-api-key": - self.cv_api_key = a - if o == "--only-set-cv-key": - self.only_set_key = True - if o == "--version": - print(f"ComicTagger {ctversion.version}: Copyright (c) 2012-2022 ComicTagger Team") - print("Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)") - sys.exit(0) - if o in ("-t", "--type"): - if a.lower() == "cr": - self.data_style = MetaDataStyle.CIX - elif a.lower() == "cbl": - self.data_style = MetaDataStyle.CBI - elif a.lower() == "comet": - self.data_style = MetaDataStyle.COMET - else: - self.display_msg_and_quit("Invalid tag type", 1) - if o == "--darkmode": - self.darkmode = True - - if any( - [ - self.print_tags, - self.delete_tags, - self.save_tags, - self.copy_tags, - self.rename_file, - self.export_to_zip, - self.only_set_key, - ] - ): - self.no_gui = True - - count = 0 - if self.run_script: - count += 1 - if self.print_tags: - count += 1 - if self.delete_tags: - count += 1 - if self.save_tags: - count += 1 - if self.copy_tags: - count += 1 - if self.rename_file: - count += 1 - if self.export_to_zip: - count += 1 - if self.only_set_key: - count += 1 - - if count > 1: - self.display_msg_and_quit( - "Must choose only one action of print, delete, save, copy, rename, export, set key, or run script", 1 - ) - - if self.script is not None: - self.launch_script(self.script) - - if len(args) > 0: - if platform.system() == "Windows": - # no globbing on windows shell, so do it for them - import glob - - self.file_list = [] - for item in args: - self.file_list.extend(glob.glob(item)) - if len(self.file_list) > 0: - self.filename = self.file_list[0] + # Determine if the entry point exists before trying to run it + if "main" in dir(script): + script.main(args) else: - self.filename = args[0] - self.file_list = args + logger.error("Can't find entry point 'main()' in module '%s'", module_name) + except Exception: + logger.exception("Script: %s raised an unhandled exception: ", module_name) - if self.only_set_key and self.cv_api_key is None: - self.display_msg_and_quit("Key not given!", 1) + sys.exit(0) - if not self.only_set_key and self.no_gui and self.filename is None: - self.display_msg_and_quit("Command requires at least one filename!", 1) - if self.delete_tags and self.data_style is None: - self.display_msg_and_quit("Please specify the type to delete with -t", 1) +def parse_cmd_line() -> argparse.Namespace: - if self.save_tags and self.data_style is None: - self.display_msg_and_quit("Please specify the type to save with -t", 1) + if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1: + # remove the PSN (process serial number) argument from OS/X + input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a] + else: + input_args = sys.argv[1:] - if self.copy_tags and self.data_style is None: - self.display_msg_and_quit("Please specify the type to copy to with -t", 1) + script_args = [] - if self.recursive: - self.file_list = utils.get_recursive_filelist(self.file_list) + # first check if we're launching a script and split off script args + for n, _ in enumerate(input_args): + if input_args[n] == "--": + break + + if input_args[n] in ["-S", "--script"] and n + 1 < len(input_args): + # insert a "--" which will cause getopt to ignore the remaining args + # so they will be passed to the script + script_args = input_args[n + 2 :] + input_args = input_args[: n + 2] + break + + parser = define_args() + opts = parser.parse_args(input_args) + + if opts.config_path: + opts.config_path = os.path.abspath(opts.config_path) + if opts.version: + 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", + ) + + opts.no_gui = any( + [ + opts.print, + opts.delete, + opts.save, + opts.copy, + opts.rename, + opts.export_to_zip, + opts.only_set_cv_key, + ] + ) + + if opts.script is not None: + launch_script(opts.script, script_args) + + if platform.system() == "Windows" and opts.glob: + # no globbing on windows shell, so do it for them + import glob + + globs = opts.files + opts.files = [] + for item in globs: + opts.files.extend(glob.glob(item)) + + if opts.only_set_cv_key and opts.cv_api_key is None: + parser.exit(message="Key not given!", status=1) + + if not opts.only_set_cv_key and opts.no_gui and not opts.files: + parser.exit(message="Command requires at least one filename!\n", status=1) + + if opts.delete and opts.type is None: + parser.exit(message="Please specify the type to delete with -t\n", status=1) + + if opts.save and opts.type is None: + parser.exit(message="Please specify the type to save with -t\n", status=1) + + if opts.copy and opts.type is None: + parser.exit(message="Please specify the type to copy to with -t\n", status=1) + + if opts.recursive: + opts.file_list = utils.get_recursive_filelist(opts.file_list) + + return opts diff --git a/comictaggerlib/resulttypes.py b/comictaggerlib/resulttypes.py index 4121666..6ea04e7 100644 --- a/comictaggerlib/resulttypes.py +++ b/comictaggerlib/resulttypes.py @@ -38,7 +38,7 @@ class OnlineMatchResults: class MultipleMatch: def __init__(self, ca: ComicArchive, match_list: List[IssueResult]) -> None: self.ca: ComicArchive = ca - self.matches = match_list + self.matches: list[IssueResult] = match_list class SelectDetails(TypedDict): diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 83d1e9f..ba78a33 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -2134,7 +2134,7 @@ Please choose options below, and select OK to Auto-Tag. self.setWindowFlags(flags) self.show() - def auto_imprint(self): + def auto_imprint(self) -> None: self.form_to_metadata() self.metadata.fix_publisher() self.metadata_to_form()