Compare commits

..

2 Commits

Author SHA1 Message Date
Timmy Welch
ed18df4678 Allow printing combined CLI tags 2025-01-05 21:03:02 -08:00
Timmy Welch
8b3cda0495 Allow recording the original hash 2025-01-05 20:40:46 -08:00
20 changed files with 323 additions and 80 deletions

View File

@ -24,6 +24,12 @@ class Archiver(Protocol):
"""
enabled: bool = True
"""
If self.path is a single file that can be hashed.
For example directories cannot be hashed.
"""
hashable: bool = True
def __init__(self) -> None:
self.path = pathlib.Path()

View File

@ -12,6 +12,8 @@ logger = logging.getLogger(__name__)
class FolderArchiver(Archiver):
"""Folder implementation"""
hashable = False
def __init__(self) -> None:
super().__init__()
self.comment_file_name = "ComicTaggerFolderComment.txt"

View File

@ -15,7 +15,9 @@
# limitations under the License.
from __future__ import annotations
import hashlib
import importlib.util
import inspect
import io
import itertools
import logging
@ -27,7 +29,7 @@ from collections.abc import Iterable
from comicapi import utils
from comicapi.archivers import Archiver, UnknownArchiver, ZipArchiver
from comicapi.genericmetadata import GenericMetadata
from comicapi.genericmetadata import GenericMetadata, Hash
from comicapi.tags import Tag
from comictaggerlib.ctversion import version
@ -124,11 +126,15 @@ class ComicArchive:
pil_available = True
def __init__(
self, path: pathlib.Path | str | Archiver, default_image_path: pathlib.Path | str | None = None
self,
path: pathlib.Path | str | Archiver,
default_image_path: pathlib.Path | str | None = None,
hash_archive: str = "",
) -> None:
self.md: dict[str, GenericMetadata] = {}
self.page_count: int | None = None
self.page_list: list[str] = []
self.hash_archive = hash_archive
self.reset_cache()
self.default_image_path = default_image_path
@ -230,7 +236,7 @@ class ComicArchive:
if not tags[tag_id].enabled:
return False
self.apply_archive_info_to_metadata(metadata, True, True)
self.apply_archive_info_to_metadata(metadata, True, True, hash_archive=self.hash_archive)
return tags[tag_id].write_tags(metadata, self.archiver)
def has_tags(self, tag_id: str) -> bool:
@ -333,11 +339,34 @@ class ComicArchive:
return self.page_count
def apply_archive_info_to_metadata(
self, md: GenericMetadata, calc_page_sizes: bool = False, detect_double_page: bool = False
self,
md: GenericMetadata,
calc_page_sizes: bool = False,
detect_double_page: bool = False,
*,
hash_archive: str = "",
) -> None:
hash_archive = hash_archive
md.page_count = self.get_number_of_pages()
md.apply_default_page_list(self.get_page_name_list())
if not calc_page_sizes or not self.seems_to_be_a_comic_archive():
if not self.seems_to_be_a_comic_archive():
return
if hash_archive in hashlib.algorithms_available and not md.original_hash:
hasher = getattr(hashlib, hash_archive, hash_archive)
try:
with self.archiver.path.open("b+r") as archive:
digest = utils.file_digest(archive, hasher)
if len(inspect.signature(digest.hexdigest).parameters) > 0:
length = digest.name.rpartition("_")[2]
if not length.isdigit():
length = "128"
md.original_hash = Hash(digest.name, digest.hexdigest(int(length) // 8)) # type: ignore[call-arg]
else:
md.original_hash = Hash(digest.name, digest.hexdigest())
except Exception:
logger.exception("Failed to calculate original hash for '%s'", self.archiver.path)
if not calc_page_sizes:
return
for p in md.pages:

View File

@ -23,6 +23,7 @@ from __future__ import annotations
import copy
import dataclasses
import hashlib
import logging
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Union, overload
@ -136,6 +137,24 @@ class MetadataOrigin(NamedTuple):
return self.name
class Hash(NamedTuple):
name: str
hash: str
def __str__(self) -> str:
return self.name + ":" + self.hash
@classmethod
def parse(cls, string: str) -> Hash:
name, _, parsed_hash = string.partition(":")
if name in hashlib.algorithms_available:
return Hash(name, parsed_hash)
return Hash("", "")
def __bool__(self) -> bool:
return all(self)
@dataclasses.dataclass
class GenericMetadata:
writer_synonyms = ("writer", "plotter", "scripter", "script")
@ -151,6 +170,7 @@ class GenericMetadata:
data_origin: MetadataOrigin | None = None
issue_id: str | None = None
series_id: str | None = None
original_hash: Hash | None = None
series: str | None = None
series_aliases: set[str] = dataclasses.field(default_factory=set)
@ -285,6 +305,9 @@ class GenericMetadata:
self.issue_id = assign(self.issue_id, new_md.issue_id)
self.series_id = assign(self.series_id, new_md.series_id)
# This should not usually be set by a talker or other online datasource
self.original_hash = assign(self.original_hash, new_md.original_hash)
self.series = assign(self.series, new_md.series)
self.series_aliases = assign_list(self.series_aliases, new_md.series_aliases)
@ -450,6 +473,7 @@ class GenericMetadata:
add_string("data_origin", self.data_origin)
add_string("series", self.series)
add_string("original_hash", self.original_hash)
add_string("series_aliases", ",".join(self.series_aliases))
add_string("issue", self.issue)
add_string("issue_count", self.issue_count)

View File

@ -1,11 +1,12 @@
from __future__ import annotations
import dataclasses
from collections import defaultdict
from collections.abc import Collection
from enum import auto
from typing import Any
from comicapi.utils import DefaultDict, StrEnum, norm_fold
from comicapi.utils import StrEnum, norm_fold
@dataclasses.dataclass
@ -54,19 +55,19 @@ def overlay(old: Any, new: Any) -> Any:
return new
attribute = DefaultDict(
attribute = defaultdict(
lambda: overlay,
{
Mode.OVERLAY: overlay,
Mode.ADD_MISSING: lambda old, new: overlay(new, old),
},
default=lambda x: overlay,
)
lists = DefaultDict(
lists = defaultdict(
lambda: overlay,
{
Mode.OVERLAY: merge_lists,
Mode.ADD_MISSING: lambda old, new: merge_lists(new, old),
},
default=lambda x: overlay,
)

View File

@ -21,7 +21,7 @@ from typing import Any
from comicapi import utils
from comicapi.archivers import Archiver
from comicapi.genericmetadata import GenericMetadata, PageMetadata
from comicapi.genericmetadata import GenericMetadata, Hash, PageMetadata
from comicapi.tags import Tag
logger = logging.getLogger(__name__)
@ -37,6 +37,7 @@ class ComicRack(Tag):
self.file = "ComicInfo.xml"
self.supported_attributes = {
"original_hash",
"series",
"issue",
"issue_count",
@ -244,7 +245,11 @@ class ComicRack(Tag):
assign("BlackAndWhite", "Yes" if md.black_and_white else None)
assign("AgeRating", md.maturity_rating)
assign("CommunityRating", md.critical_rating)
assign("ScanInformation", md.scan_info)
scan_info = md.scan_info or ""
if md.original_hash:
scan_info += f" {md.original_hash}"
assign("ScanInformation", scan_info)
assign("PageCount", md.page_count)
@ -326,7 +331,16 @@ class ComicRack(Tag):
md.manga = utils.xlate(get("Manga"))
md.maturity_rating = utils.xlate(get("AgeRating"))
md.critical_rating = utils.xlate_float(get("CommunityRating"))
md.scan_info = utils.xlate(get("ScanInformation"))
scan_info_list = (utils.xlate(get("ScanInformation")) or "").split()
for word in scan_info_list.copy():
original_hash = Hash.parse(word)
if original_hash:
md.original_hash = original_hash
scan_info_list.remove(word)
break
if scan_info_list:
md.scan_info = " ".join(scan_info_list)
md.is_empty = False
md.page_count = utils.xlate_int(get("PageCount"))

View File

@ -14,6 +14,7 @@ class Tag:
"data_origin",
"issue_id",
"series_id",
"original_hash",
"series",
"series_aliases",
"issue",

View File

@ -15,6 +15,7 @@
# limitations under the License.
from __future__ import annotations
import hashlib
import json
import logging
import os
@ -22,10 +23,11 @@ import pathlib
import platform
import sys
import unicodedata
from collections import defaultdict
from collections.abc import Iterable, Mapping
from enum import Enum, auto
from shutil import which # noqa: F401
from typing import Any, Callable, TypeVar, cast
from typing import Any, TypeVar, cast
from comicfn2dict import comicfn2dict
@ -46,6 +48,45 @@ except ImportError:
if sys.version_info < (3, 11):
def file_digest(fileobj, digest, /, *, _bufsize=2**18):
"""Hash the contents of a file-like object. Returns a digest object.
*fileobj* must be a file-like object opened for reading in binary mode.
It accepts file objects from open(), io.BytesIO(), and SocketIO objects.
The function may bypass Python's I/O and use the file descriptor *fileno*
directly.
*digest* must either be a hash algorithm name as a *str*, a hash
constructor, or a callable that returns a hash object.
"""
# On Linux we could use AF_ALG sockets and sendfile() to archive zero-copy
# hashing with hardware acceleration.
if isinstance(digest, str):
digestobj = hashlib.new(digest)
else:
digestobj = digest()
if hasattr(fileobj, "getbuffer"):
# io.BytesIO object, use zero-copy buffer
digestobj.update(fileobj.getbuffer())
return digestobj
# Only binary files implement readinto().
if not (hasattr(fileobj, "readinto") and hasattr(fileobj, "readable") and fileobj.readable()):
raise ValueError(f"'{fileobj!r}' is not a file-like object in binary reading mode.")
# binary file, socket.SocketIO object
# Note: socket I/O uses different syscalls than file I/O.
buf = bytearray(_bufsize) # Reusable buffer to reduce allocations.
view = memoryview(buf)
while True:
size = fileobj.readinto(buf)
if size == 0:
break # EOF
digestobj.update(view[:size])
return digestobj
class StrEnum(str, Enum):
"""
Enum where members are also (and must be) strings
@ -91,9 +132,10 @@ if sys.version_info < (3, 11):
return self.value
else:
from enum import StrEnum as s
from enum import StrEnum as _StrEnum
from hashlib import file_digest
class StrEnum(s):
class StrEnum(_StrEnum):
@classmethod
def _missing_(cls, value: Any) -> str | None:
if not isinstance(value, str):
@ -106,17 +148,6 @@ else:
logger = logging.getLogger(__name__)
class DefaultDict(dict):
def __init__(self, *args, default: Callable[[Any], Any] | None = None) -> None:
super().__init__(*args)
self.default = default
def __missing__(self, key: Any) -> Any:
if self.default is None:
return key
return self.default(key)
class Parser(StrEnum):
ORIGINAL = auto()
COMPLICATED = auto()
@ -370,9 +401,7 @@ def xlate_float(data: Any) -> float | None:
if isinstance(data, (int, float)):
i = data
else:
i = str(data).translate(
DefaultDict(zip((ord(c) for c in "1234567890."), "1234567890."), default=lambda x: None)
)
i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890."), "1234567890.")))
if i == "":
return None
try:
@ -505,9 +534,9 @@ def parse_version(s: str) -> tuple[int, int, int]:
return (parts[0], parts[1], parts[2])
_languages: dict[str | None, str | None] = DefaultDict(default=lambda x: None)
_languages: dict[str | None, str | None] = defaultdict(lambda: None)
_countries: dict[str | None, str | None] = DefaultDict(default=lambda x: None)
_countries: dict[str | None, str | None] = defaultdict(lambda: None)
def countries() -> dict[str | None, str | None]:
@ -529,8 +558,6 @@ def languages() -> dict[str | None, str | None]:
def get_language_from_iso(iso: str | None) -> str | None:
if not _languages:
return languages()[iso]
return _languages[iso]
@ -543,12 +570,10 @@ def get_language_iso(string: str | None) -> str | None:
lang = string.casefold()
found = None
for lng in isocodes.extendend_languages.items:
for x in ("alpha_2", "alpha_3", "bibliographic", "common_name", "name"):
if x in lng and lng[x].casefold() == lang:
found = lng
# break
if found:
break
@ -558,8 +583,6 @@ def get_language_iso(string: str | None) -> str | None:
def get_country_from_iso(iso: str | None) -> str | None:
if not _countries:
return countries()[iso]
return _countries[iso]
@ -618,3 +641,40 @@ def load_publishers() -> None:
update_publishers(json.loads((comicapi.data.data_path / "publishers.json").read_text("utf-8")))
except Exception:
logger.exception("Failed to load publishers.json; The are no publishers or imprints loaded")
__all__ = (
"load_publishers",
"file_digest",
"Parser",
"ImprintDict",
"os_sorted",
"parse_filename",
"norm_fold",
"combine_notes",
"parse_date_str",
"shorten_path",
"path_to_short_str",
"get_page_name_list",
"get_recursive_filelist",
"add_to_path",
"remove_from_path",
"xlate_int",
"xlate_float",
"xlate",
"split",
"split_urls",
"remove_articles",
"sanitize_title",
"titles_match",
"unique_file",
"parse_version",
"countries",
"languages",
"get_language_from_iso",
"get_language_iso",
"get_country_from_iso",
"get_publisher",
"update_publishers",
"load_publishers",
)

View File

@ -182,7 +182,9 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
self.teDescription.setText(match.description)
def set_cover_image(self) -> None:
ca = ComicArchive(self.current_match_set.original_path)
ca = ComicArchive(
self.current_match_set.original_path, hash_archive=self.config.Runtime_Options__preferred_hash
)
self.archiveCoverWidget.set_archive(ca)
def current_match(self) -> IssueResult:
@ -225,7 +227,9 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
def save_match(self) -> None:
match = self.current_match()
ca = ComicArchive(self.current_match_set.original_path)
ca = ComicArchive(
self.current_match_set.original_path, hash_archive=self.config.Runtime_Options__preferred_hash
)
md, error = self.parent().read_selected_tags(self._tags, ca)
if error is not None:
logger.error("Failed to load tags for %s: %s", ca.path, error)

View File

@ -184,7 +184,7 @@ class CLI:
if i != "s":
# save the data!
# we know at this point, that the file is all good to go
ca = ComicArchive(match_set.original_path)
ca = ComicArchive(match_set.original_path, hash_archive=self.config.Runtime_Options__preferred_hash)
md, match_set.tags_read = self.create_local_metadata(ca, self.config.Runtime_Options__tags_read)
ct_md = self.fetch_metadata(match_set.online_results[int(i) - 1].issue_id)
@ -330,6 +330,17 @@ class CLI:
self.output(md)
except Exception as e:
logger.error("Failed to read tags from %s: %s", ca.path, e)
if not self.config.Auto_Tag__metadata.is_empty and not self.config.Runtime_Options__raw:
try:
md, tags_read = self.create_local_metadata(
ca, self.config.Runtime_Options__tags_read or list(tags.keys())
)
tags_read_names = ", ".join(["CLI"] + [tags[t].name() for t in tags_read])
self.output(f"--------- Combined {tags_read_names} tags ---------")
self.output(md)
except Exception as e:
logger.error("Failed to read tags from %s: %s", ca.path, e)
return Result(Action.print, Status.success, ca.path, md=md)
def delete_tags(self, ca: ComicArchive, tag_id: str) -> Status:
@ -790,7 +801,9 @@ class CLI:
logger.error("Cannot find %s", filename)
return Result(command, Status.read_failure, pathlib.Path(filename)), match_results
ca = ComicArchive(filename, str(graphics_path / "nocover.png"))
ca = ComicArchive(
filename, str(graphics_path / "nocover.png"), hash_archive=self.config.Runtime_Options__preferred_hash
)
if not ca.seems_to_be_a_comic_archive():
logger.error("Sorry, but %s is not a comic archive!", filename)

View File

@ -20,7 +20,7 @@ import logging
import operator
import natsort
from PyQt5 import QtCore, QtWidgets, uic
from PyQt5 import QtWidgets, uic
from comicapi import utils
from comicapi.genericmetadata import Credit
@ -75,11 +75,7 @@ class CreditEditorWindow(QtWidgets.QDialog):
self.cbRole.setCurrentIndex(i)
if credit.language != "":
i = (
self.cbLanguage.findData(credit.language, QtCore.Qt.ItemDataRole.UserRole)
if self.cbLanguage.findData(credit.language, QtCore.Qt.ItemDataRole.UserRole) > -1
else self.cbLanguage.findText(credit.language)
)
i = self.cbLanguage.findText(credit.language)
if i == -1:
self.cbLanguage.setEditText(credit.language)
else:
@ -88,8 +84,9 @@ class CreditEditorWindow(QtWidgets.QDialog):
self.cbPrimary.setChecked(credit.primary)
def get_credit(self) -> Credit:
lang = self.cbLanguage.currentData() or self.cbLanguage.currentText()
return Credit(self.leName.text(), self.cbRole.currentText(), self.cbPrimary.isChecked(), lang)
return Credit(
self.leName.text(), self.cbRole.currentText(), self.cbPrimary.isChecked(), self.cbLanguage.currentText()
)
def accept(self) -> None:
if self.leName.text() == "":

View File

@ -17,6 +17,7 @@
from __future__ import annotations
import argparse
import hashlib
import logging
import os
import platform
@ -83,6 +84,20 @@ def register_runtime(parser: settngs.Manager) -> None:
help='Enable the expiremental "quick tagger"',
file=False,
)
parser.add_setting(
"--enable-embedding-hashes",
action=argparse.BooleanOptionalAction,
default=False,
help="Enable embedding hashes in metadata (currently only CR/CIX has support)",
file=False,
)
parser.add_setting(
"--preferred-hash",
default="shake_256",
choices=hashlib.algorithms_available,
help="The type of embedded hash to save when --enable-embedding-hashes is set\n\n",
file=False,
)
parser.add_setting("-q", "--quiet", action="store_true", help="Don't say much (for print mode).", file=False)
parser.add_setting(
"-j",
@ -307,6 +322,9 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs
if config[0].Runtime_Options__recursive:
config[0].Runtime_Options__files = utils.get_recursive_filelist(config[0].Runtime_Options__files)
if not config[0].Runtime_Options__enable_embedding_hashes:
config[0].Runtime_Options__preferred_hash = ""
# take a crack at finding rar exe if it's not in the path
if not utils.which("rar"):
if platform.system() == "Windows":

View File

@ -27,6 +27,7 @@ def general(parser: settngs.Manager) -> None:
def internal(parser: settngs.Manager) -> None:
# automatic settings
parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False)
parser.add_setting("embedded_hash_type", default="shake_256", cmdline=False)
parser.add_setting("write_tags", default=["cr"], cmdline=False)
parser.add_setting("read_tags", default=["cr"], cmdline=False)
parser.add_setting("last_opened_folder", default="", cmdline=False)

View File

@ -21,6 +21,8 @@ class SettngsNS(settngs.TypedNS):
Runtime_Options__config: comictaggerlib.ctsettings.types.ComicTaggerPaths
Runtime_Options__verbose: int
Runtime_Options__enable_quick_tag: bool
Runtime_Options__enable_embedding_hashes: bool
Runtime_Options__preferred_hash: str
Runtime_Options__quiet: bool
Runtime_Options__json: bool
Runtime_Options__raw: bool
@ -47,6 +49,7 @@ class SettngsNS(settngs.TypedNS):
Quick_Tag__exact_only: bool
internal__install_id: str
internal__embedded_hash_type: str
internal__write_tags: list[str]
internal__read_tags: list[str]
internal__last_opened_folder: str
@ -143,6 +146,8 @@ class Runtime_Options(typing.TypedDict):
config: comictaggerlib.ctsettings.types.ComicTaggerPaths
verbose: int
enable_quick_tag: bool
enable_embedding_hashes: bool
preferred_hash: str
quiet: bool
json: bool
raw: bool
@ -173,6 +178,7 @@ class Quick_Tag(typing.TypedDict):
class internal(typing.TypedDict):
install_id: str
embedded_hash_type: str
write_tags: list[str]
read_tags: list[str]
last_opened_folder: str

View File

@ -274,7 +274,9 @@ class FileSelectionList(QtWidgets.QWidget):
if self.is_list_dupe(path):
return self.get_current_list_row(path)
ca = ComicArchive(path, str(graphics_path / "nocover.png"))
ca = ComicArchive(
path, str(graphics_path / "nocover.png"), hash_archive=self.config.Runtime_Options__preferred_hash
)
if ca.seems_to_be_a_comic_archive():
row: int = self.twList.rowCount()

View File

@ -170,7 +170,7 @@ class PageListEditor(QtWidgets.QWidget):
return
md = GenericMetadata(pages=self.get_page_list())
double_pages = [bool(x.double_page) for x in md.pages]
self.comic_archive.apply_archive_info_to_metadata(md, True, True)
self.comic_archive.apply_archive_info_to_metadata(md, True, True, hash_archive="")
self.set_data(self.comic_archive, pages_list=md.pages)
if double_pages != [bool(x.double_page) for x in md.pages]:
self.modified.emit()

View File

@ -17,6 +17,7 @@
from __future__ import annotations
import functools
import hashlib
import logging
import operator
import os
@ -38,7 +39,7 @@ import comictaggerlib.ui
from comicapi import utils
from comicapi.comicarchive import ComicArchive, tags
from comicapi.filenameparser import FileNameParser
from comicapi.genericmetadata import Credit, GenericMetadata
from comicapi.genericmetadata import Credit, GenericMetadata, Hash
from comicapi.issuestring import IssueString
from comictaggerlib import ctsettings, ctversion
from comictaggerlib.applicationlogwindow import ApplicationLogWindow, QTextEditLogger
@ -93,6 +94,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.md_attributes = {
"data_origin": None,
"issue_id": None,
"original_hash": (self.cbHashName, self.leOriginalHash),
"series": self.leSeries,
"issue": self.leIssueNum,
"title": self.leTitle,
@ -229,6 +231,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
for tag_id in config[0].internal__read_tags.copy():
if tag_id not in self.enabled_tags():
config[0].internal__read_tags.remove(tag_id)
if self.config[0].Runtime_Options__preferred_hash:
self.config[0].internal__embedded_hash_type = self.config[0].Runtime_Options__preferred_hash
self.selected_write_tags: list[str] = config[0].internal__write_tags or [self.enabled_tags()[0]]
self.selected_read_tags: list[str] = config[0].internal__read_tags or [self.enabled_tags()[0]]
@ -432,6 +436,16 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.setWindowTitle(f"{self.appName} - {self.comic_archive.path}{mod_str}{ro_str}")
def toggle_enable_embedding_hashes(self) -> None:
self.config[0].Runtime_Options__enable_embedding_hashes = self.actionEnableEmbeddingHashes.isChecked()
enable_widget(self.md_attributes["original_hash"], self.config[0].Runtime_Options__enable_embedding_hashes)
if not self.leOriginalHash.text().strip():
self.cbHashName.setCurrentText(self.config[0].internal__embedded_hash_type)
if self.config[0].Runtime_Options__enable_embedding_hashes:
self.config[0].Runtime_Options__preferred_hash = self.config[0].internal__embedded_hash_type
else:
self.config[0].Runtime_Options__preferred_hash = ""
def config_menus(self) -> None:
# File Menu
self.actionAutoTag.triggered.connect(self.auto_tag)
@ -453,8 +467,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.actionLiteralSearch.triggered.connect(self.literal_search)
self.actionParse_Filename.triggered.connect(self.use_filename)
self.actionParse_Filename_split_words.triggered.connect(self.use_filename_split)
self.actionReCalcPageDims.triggered.connect(self.recalc_page_dimensions)
self.actionReCalcArchiveInfo.triggered.connect(self.recalc_archive_info)
self.actionSearchOnline.triggered.connect(self.query_online)
self.actionEnableEmbeddingHashes: QtWidgets.QAction
self.actionEnableEmbeddingHashes.triggered.connect(self.toggle_enable_embedding_hashes)
self.actionEnableEmbeddingHashes.setChecked(self.config[0].Runtime_Options__enable_embedding_hashes)
# Window Menu
self.actionLogWindow.triggered.connect(self.log_window.show)
self.actionPageBrowser.triggered.connect(self.show_page_browser)
@ -658,8 +675,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.page_browser.set_comic_archive(self.comic_archive)
self.page_browser.metadata = self.metadata
if self.comic_archive is not None:
self.page_list_editor.set_data(self.comic_archive, self.metadata.pages)
self.metadata_to_form()
self.clear_dirty_flag() # also updates the app title
self.update_info_box()
@ -680,7 +695,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.actionCopyTags.setEnabled(enabled)
self.actionParse_Filename.setEnabled(enabled)
self.actionParse_Filename_split_words.setEnabled(enabled)
self.actionReCalcPageDims.setEnabled(enabled)
self.actionReCalcArchiveInfo.setEnabled(enabled)
self.actionRemoveAuto.setEnabled(enabled)
self.actionRename.setEnabled(enabled)
self.actionRepackage.setEnabled(enabled)
@ -795,6 +810,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
md = self.metadata
original_hash = md.original_hash or Hash("", "")
self.cbHashName.setCurrentText(original_hash.name or self.config[0].internal__embedded_hash_type)
assign_text(self.leOriginalHash, original_hash.hash)
assign_text(self.leSeries, md.series)
assign_text(self.leIssueNum, md.issue)
assign_text(self.leIssueCount, md.issue_count)
@ -876,9 +894,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
continue
self.add_new_credit_entry(row, credit)
if self.comic_archive:
self.page_list_editor.set_data(self.comic_archive, self.metadata.pages)
self.twCredits.setSortingEnabled(True)
self.update_credit_colors()
self.toggle_enable_embedding_hashes()
def add_new_credit_entry(self, row: int, credit: Credit) -> None:
self.twCredits.insertRow(row)
@ -891,7 +912,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
language = utils.get_language_from_iso(credit.language) or credit.language
item = QtWidgets.QTableWidgetItem(language)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, credit.language)
item.setData(QtCore.Qt.ItemDataRole.UserRole, credit.language)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twCredits.setItem(row, self.md_attributes["credits.language"], item)
@ -922,6 +942,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
# copy the data from the form into the metadata
md = GenericMetadata()
md.is_empty = False
if utils.xlate(self.cbHashName.currentText()) and utils.xlate(self.leOriginalHash.text()):
md.original_hash = Hash(
utils.xlate(self.cbHashName.currentText()) or "", utils.xlate(self.leOriginalHash.text()) or ""
)
md.alternate_number = utils.xlate(IssueString(self.leAltIssueNum.text()).as_string())
md.issue = utils.xlate(IssueString(self.leIssueNum.text()).as_string())
md.issue_count = utils.xlate_int(self.leIssueCount.text())
@ -971,10 +996,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
for row in range(self.twCredits.rowCount()):
role = self.twCredits.item(row, self.md_attributes["credits.role"]).text()
lang = (
self.twCredits.item(row, self.md_attributes["credits.language"]).data(QtCore.Qt.ItemDataRole.UserRole)
or self.twCredits.item(row, self.md_attributes["credits.language"]).text()
)
lang = self.twCredits.item(row, self.md_attributes["credits.language"]).text()
name = self.twCredits.item(row, self.md_attributes["credits.person"]).text()
primary_flag = self.twCredits.item(row, self.md_attributes["credits.primary"]).text() != ""
@ -1237,6 +1259,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.update_credit_colors()
self.page_list_editor.select_read_tags(self.selected_write_tags)
self.toggle_enable_embedding_hashes()
def cell_double_clicked(self, r: int, c: int) -> None:
self.edit_credit()
@ -1272,15 +1295,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
old = Credit()
if edit:
row = self.twCredits.currentRow()
lang = str(
self.twCredits.item(row, self.md_attributes["credits.language"]).data(QtCore.Qt.ItemDataRole.UserRole)
or utils.get_language_iso(self.twCredits.item(row, self.md_attributes["credits.language"]).text())
)
old = Credit(
self.twCredits.item(row, self.md_attributes["credits.person"]).text(),
self.twCredits.item(row, self.md_attributes["credits.role"]).text(),
self.twCredits.item(row, self.md_attributes["credits.primary"]).text() != "",
lang,
self.twCredits.item(row, self.md_attributes["credits.language"]).text(),
)
editor = CreditEditorWindow(self, CreditEditorWindow.ModeEdit, old)
@ -1317,13 +1336,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
if ok_to_mod:
# modify it
if edit:
lang = utils.get_language_from_iso(new.language) or new.language
self.twCredits.item(row, self.md_attributes["credits.role"]).setText(new.role)
self.twCredits.item(row, self.md_attributes["credits.person"]).setText(new.person)
self.twCredits.item(row, self.md_attributes["credits.language"]).setText(lang)
self.twCredits.item(row, self.md_attributes["credits.language"]).setData(
QtCore.Qt.ItemDataRole.UserRole, new.language
)
self.twCredits.item(row, self.md_attributes["credits.language"]).setText(new.language)
self.update_credit_primary_flag(row, new.primary)
else:
# add new entry
@ -1415,6 +1430,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.adjust_tags_combo()
self.cbHashName: QtWidgets.QComboBox
self.cbHashName.addItems(sorted(hashlib.algorithms_available))
# Add talker entries
for t_id, talker in self.talkers.items():
self.cbx_sources.addItem(talker.name, t_id)
@ -2114,12 +2132,22 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.metadata = CBLTransformer(self.metadata, self.config[0]).apply()
self.metadata_to_form()
def recalc_page_dimensions(self) -> None:
def recalc_archive_info(self) -> None:
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
for p in self.metadata.pages:
p.byte_size = None
p.height = None
p.width = None
if self.comic_archive and self.config[0].Runtime_Options__preferred_hash:
self.metadata.original_hash = None
self.comic_archive.apply_archive_info_to_metadata(
self.metadata, True, hash_archive=self.cbHashName.currentText()
)
original_hash = self.metadata.original_hash or Hash("", "")
self.leOriginalHash.setText(original_hash.hash)
self.cbHashName.setCurrentText(original_hash.name or self.config[0].internal__embedded_hash_type)
self.page_list_editor.set_data(self.comic_archive, self.metadata.pages)
self.set_dirty_flag()
QtWidgets.QApplication.restoreOverrideCursor()

View File

@ -6,7 +6,7 @@ import io
import logging
import traceback
import webbrowser
from collections.abc import Sequence
from collections.abc import Collection, Sequence
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QWidget
@ -147,7 +147,7 @@ if qt_available:
active_palette = None
def enable_widget(widget: QtWidgets.QWidget | list[QtWidgets.QWidget], enable: bool) -> None:
def enable_widget(widget: QtWidgets.QWidget | Collection[QtWidgets.QWidget], enable: bool) -> None:
if isinstance(widget, Sequence):
for w in widget:
_enable_widget(w, enable)

View File

@ -955,7 +955,7 @@ Source</string>
<item>
<layout class="QFormLayout" name="formLayout_8">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_20">
@ -970,6 +970,30 @@ Source</string>
<item row="0" column="1">
<widget class="QLineEdit" name="leScanInfo"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="leOriginalHash"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblOriginalHash">
<property name="text">
<string>Original Hash</string>
</property>
<property name="buddy">
<cstring>leOriginalHash</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cbHashName">
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblHashName">
<property name="text">
<string>Hash Type</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
@ -1428,7 +1452,8 @@ Source</string>
<addaction name="actionLiteralSearch"/>
<addaction name="separator"/>
<addaction name="actionApplyCBLTransform"/>
<addaction name="actionReCalcPageDims"/>
<addaction name="actionReCalcArchiveInfo"/>
<addaction name="actionEnableEmbeddingHashes"/>
</widget>
<widget class="QMenu" name="menuWindow">
<property name="title">
@ -1717,9 +1742,12 @@ Source</string>
<string>Ctrl+Shift+F</string>
</property>
</action>
<action name="actionReCalcPageDims">
<action name="actionReCalcArchiveInfo">
<property name="text">
<string>Re-Calculate Page Dimensions</string>
<string>Re-Calculate Archive Information</string>
</property>
<property name="toolTip">
<string>Only includes page size info and original hash, original hash wil be overwritten</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+R</string>
@ -1814,6 +1842,14 @@ Source</string>
<string>Ctrl+S</string>
</property>
</action>
<action name="actionEnableEmbeddingHashes">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Enable Embedding Hashes</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@ -22,6 +22,7 @@ import json
import logging
import pathlib
import time
from collections import defaultdict
from typing import Any, Callable, Generic, TypeVar, cast
from urllib.parse import parse_qsl, urljoin
@ -185,7 +186,7 @@ class ComicVineTalker(ComicTalker):
self.default_api_url = self.api_url = f"{self.website}/api/"
self.default_api_key = self.api_key = "27431e6787042105bd3e47e169a624521f89f3a4"
self.use_series_start_as_volume: bool = False
self.total_requests_made: dict[str, int] = utils.DefaultDict(default=lambda x: 0)
self.total_requests_made: dict[str, int] = defaultdict(int)
self.custom_url_parameters: dict[str, str] = {}
def _log_total_requests(self) -> None: