Allow recording the original hash
This commit is contained in:
parent
e8e21eb1b6
commit
8b3cda0495
@ -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()
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"))
|
||||
|
||||
|
@ -14,6 +14,7 @@ class Tag:
|
||||
"data_origin",
|
||||
"issue_id",
|
||||
"series_id",
|
||||
"original_hash",
|
||||
"series",
|
||||
"series_aliases",
|
||||
"issue",
|
||||
|
@ -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",
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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":
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user