"""CLI options class for ComicTagger app""" # # Copyright 2012-2014 Anthony Beville # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import argparse import logging import os import platform import sys from comicapi import utils from comicapi.comicarchive import MetaDataStyle from comicapi.genericmetadata import GenericMetadata from comictaggerlib import ctversion logger = logging.getLogger(__name__) def define_args() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="""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.", ) # TODO: update for new api 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""", ) # TODO: update for new api parser.add_argument( "--cv-api-key", help="Use the given Comic Vine API Key (persisted in settings).", ) # TODO: update for new api parser.add_argument( "--cv-url", help="Use the given Comic Vine URL (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}", default=[], type=metadata_type, help="""Specify TYPE as either CR, CBL or COMET\n(as either ComicRack, ComicBookLover,\nor CoMet style tags, respectively).\nUse commas for multiple types.\nFor searching the metadata will use the first listed:\neg '-t cbl,cr' with no CBL tags, CR will be used if they exist\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", default=GenericMetadata(), 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="""Overwrite 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.", ) # TODO: update for new api 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 def metadata_type(types: str) -> list[int]: result = [] types = types.casefold() for typ in types.split(","): typ = typ.strip() if typ not in MetaDataStyle.short_name: choices = ", ".join(MetaDataStyle.short_name) raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})") result.append(MetaDataStyle.short_name.index(typ)) return result 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 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.casefold() == "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): raise argparse.ArgumentTypeError(f"'{key}' is not a valid tag name") else: md.is_empty = False setattr(md, key, value) return md 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: script = __import__(module_name) # Determine if the entry point exists before trying to run it if "main" in dir(script): script.main(args) 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() -> argparse.Namespace: if platform.system() == "Darwin" and getattr(sys, "frozen", False): # 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:] script_args = [] # 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, # TODO: update for new api ] ) 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)) # TODO: update for new api if opts.only_set_cv_key and opts.cv_api_key is None and opts.cv_url is None: parser.exit(message="Key not given!\n", status=1) # TODO: update for new api 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 not opts.type: parser.exit(message="Please specify the type to delete with -t\n", status=1) if opts.save and not opts.type: parser.exit(message="Please specify the type to save with -t\n", status=1) if opts.copy: if not opts.type: parser.exit(message="Please specify the type to copy to with -t\n", status=1) if len(opts.copy) > 1: parser.exit(message="Please specify only one type to copy to with -c\n", status=1) opts.copy = opts.copy[0] if opts.recursive: opts.file_list = utils.get_recursive_filelist(opts.files) else: opts.file_list = opts.files return opts