argparse...

This commit is contained in:
lordwelch 2022-05-23 22:24:13 -07:00
parent 36adf91744
commit 97e65f10cf
8 changed files with 413 additions and 486 deletions

View File

@ -64,6 +64,7 @@ class MetaDataStyle:
CIX = 1
COMET = 2
name = ["ComicBookLover", "ComicRack", "CoMet"]
short = ["cbl", "cr", "comet"]
class UnknownArchiver:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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