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