aadeb07c49
Refactor add_to_path with tests Fix type hints for titles_match Use casefold in get_language Fix using the recursive flag in cli mode Add http status code to ComicVine exceptions Fix parenthesis getting removed when renaming Add more tests
413 lines
14 KiB
Python
413 lines
14 KiB
Python
"""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.",
|
|
)
|
|
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(
|
|
"--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.",
|
|
)
|
|
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,
|
|
]
|
|
)
|
|
|
|
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 and opts.cv_url is None:
|
|
parser.exit(message="Key not given!\n", 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 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
|