From 8b3cda049575ca906e472ee313a87c525cb72f57 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 5 Jan 2025 20:32:49 -0800 Subject: [PATCH] Allow recording the original hash --- comicapi/archivers/archiver.py | 6 ++ comicapi/archivers/folder.py | 2 + comicapi/comicarchive.py | 39 +++++++-- comicapi/genericmetadata.py | 24 ++++++ comicapi/tags/comicrack.py | 20 ++++- comicapi/tags/tag.py | 1 + comicapi/utils.py | 82 ++++++++++++++++++- comictaggerlib/autotagmatchwindow.py | 8 +- comictaggerlib/cli.py | 6 +- comictaggerlib/ctsettings/commandline.py | 18 ++++ comictaggerlib/ctsettings/file.py | 1 + .../ctsettings/settngs_namespace.py | 6 ++ comictaggerlib/fileselectionlist.py | 4 +- comictaggerlib/pagelisteditor.py | 2 +- comictaggerlib/taggerwindow.py | 52 ++++++++++-- comictaggerlib/ui/qtutils.py | 4 +- comictaggerlib/ui/taggerwindow.ui | 44 +++++++++- 17 files changed, 291 insertions(+), 28 deletions(-) diff --git a/comicapi/archivers/archiver.py b/comicapi/archivers/archiver.py index 46b9fe2..7d9ad96 100644 --- a/comicapi/archivers/archiver.py +++ b/comicapi/archivers/archiver.py @@ -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() diff --git a/comicapi/archivers/folder.py b/comicapi/archivers/folder.py index b5f4079..e4502d9 100644 --- a/comicapi/archivers/folder.py +++ b/comicapi/archivers/folder.py @@ -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" diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 8348a65..25ca78a 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -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: diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 6713e8e..585a74d 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -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) diff --git a/comicapi/tags/comicrack.py b/comicapi/tags/comicrack.py index d0e89fb..9cfb4d0 100644 --- a/comicapi/tags/comicrack.py +++ b/comicapi/tags/comicrack.py @@ -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")) diff --git a/comicapi/tags/tag.py b/comicapi/tags/tag.py index b303d68..d69875c 100644 --- a/comicapi/tags/tag.py +++ b/comicapi/tags/tag.py @@ -14,6 +14,7 @@ class Tag: "data_origin", "issue_id", "series_id", + "original_hash", "series", "series_aliases", "issue", diff --git a/comicapi/utils.py b/comicapi/utils.py index 977cb15..8955ff5 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -15,6 +15,7 @@ # limitations under the License. from __future__ import annotations +import hashlib import json import logging import os @@ -47,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 @@ -92,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): @@ -600,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", +) diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index f49c2fa..4b531e3 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -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) diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 7344415..5a42859 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -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) @@ -790,7 +790,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) diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index da8910b..f821774 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -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": diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index f26591d..02cae5b 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -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) diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py index a39f0d6..4e44609 100644 --- a/comictaggerlib/ctsettings/settngs_namespace.py +++ b/comictaggerlib/ctsettings/settngs_namespace.py @@ -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 diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index 9244ace..a6b771a 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -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() diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index 25194d5..1b9b2b5 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -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() diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index fefcf61..13bf3fd 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -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) @@ -921,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()) @@ -1233,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() @@ -1403,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) @@ -2102,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() diff --git a/comictaggerlib/ui/qtutils.py b/comictaggerlib/ui/qtutils.py index c0c4208..0389678 100644 --- a/comictaggerlib/ui/qtutils.py +++ b/comictaggerlib/ui/qtutils.py @@ -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) diff --git a/comictaggerlib/ui/taggerwindow.ui b/comictaggerlib/ui/taggerwindow.ui index 4a8c5d3..7d6f88f 100644 --- a/comictaggerlib/ui/taggerwindow.ui +++ b/comictaggerlib/ui/taggerwindow.ui @@ -955,7 +955,7 @@ Source - QFormLayout::ExpandingFieldsGrow + QFormLayout::AllNonFixedFieldsGrow @@ -970,6 +970,30 @@ Source + + + + + + + Original Hash + + + leOriginalHash + + + + + + + + + + + Hash Type + + + @@ -1428,7 +1452,8 @@ Source - + + @@ -1717,9 +1742,12 @@ Source Ctrl+Shift+F - + - Re-Calculate Page Dimensions + Re-Calculate Archive Information + + + Only includes page size info and original hash, original hash wil be overwritten Ctrl+Shift+R @@ -1814,6 +1842,14 @@ Source Ctrl+S + + + true + + + Enable Embedding Hashes + +