From 87cd106b28bb2e91c0d87af48a03b5c364f2206a Mon Sep 17 00:00:00 2001 From: Mizaki Date: Wed, 4 Jan 2023 23:51:39 +0000 Subject: [PATCH 01/18] Add source logo and URL to series window --- comictaggerlib/seriesselectionwindow.py | 11 +++ comictaggerlib/ui/seriesselectionwindow.ui | 95 ++++++++++++++++----- comictalker/talkers/comicvine.py | 4 +- comictalker/talkers/logos/comicvine.png | Bin 0 -> 17414 bytes 4 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 comictalker/talkers/logos/comicvine.png diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 7c1c479..7ab4fda 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -161,6 +161,17 @@ class SeriesSelectionWindow(QtWidgets.QDialog): # Load to retrieve settings self.talker_api = talker_api + # Display talker logo and set url + self.lblSourceName.setText( + f'

Data Source: {talker_api.static_options.website}

' + ) + source_label_logo = QtGui.QPixmap(talker_api.source_details.logo) + if source_label_logo.height() > 100 or source_label_logo.width() > 300: + source_label_logo = source_label_logo.scaled( + 300, 100, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation + ) + self.lblSourceLogo.setPixmap(source_label_logo) + # Set the minimum row height to the default. # this way rows will be more consistent when resizeRowsToContents is called self.twList.verticalHeader().setMinimumSectionSize(self.twList.verticalHeader().defaultSectionSize()) diff --git a/comictaggerlib/ui/seriesselectionwindow.ui b/comictaggerlib/ui/seriesselectionwindow.ui index 4812423..967768f 100644 --- a/comictaggerlib/ui/seriesselectionwindow.ui +++ b/comictaggerlib/ui/seriesselectionwindow.ui @@ -7,7 +7,7 @@ 0 0 950 - 480 + 600 @@ -17,23 +17,84 @@ false - + - - - - 300 - 450 - + + + 0 - - - 300 - 450 - + + QLayout::SetMaximumSize - + + 0 + + + + + + 0 + 0 + + + + + 300 + 450 + + + + + 300 + 450 + + + + + + + + 2 + + + 1 + + + Qt::Horizontal + + + + + + + Data Source: + + + + + + + + 300 + 100 + + + + + 300 + 100 + + + + 0 + + + + + + + @@ -85,17 +146,11 @@ Year - - AlignCenter - Issues - - AlignCenter - diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 3b264f3..0bc7223 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -167,7 +167,9 @@ class ComicVineTalker(ComicTalker): wait_on_ratelimit: bool = False, ): super().__init__(version, cache_folder, api_url, api_key) - self.source_details = SourceDetails(name="Comic Vine", ident="comicvine") + self.source_details = SourceDetails( + name="Comic Vine", ident="comicvine", logo="comictalker/talkers/logos/comicvine.png" + ) self.static_options = SourceStaticOptions( website="https://comicvine.gamespot.com/", has_issues=True, diff --git a/comictalker/talkers/logos/comicvine.png b/comictalker/talkers/logos/comicvine.png new file mode 100644 index 0000000000000000000000000000000000000000..9e3bb89ad0b447a2d6091bfe80cff89d7e39fb17 GIT binary patch literal 17414 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 JPDHLkV1hfU6hr_3 literal 0 HcmV?d00001 From 725b2c66d3017bd023aa62efa557e8edb6b52513 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Thu, 12 Jan 2023 16:58:50 +0000 Subject: [PATCH 02/18] Use imageWidget for source logo and URL. --- comictaggerlib/seriesselectionwindow.py | 20 +++++++++++++------- comictalker/talkers/comicvine.py | 4 +++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 7ab4fda..215c037 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -163,14 +163,20 @@ class SeriesSelectionWindow(QtWidgets.QDialog): # Display talker logo and set url self.lblSourceName.setText( - f'

Data Source: {talker_api.static_options.website}

' + f'Data Source: {talker_api.static_options.website}' ) - source_label_logo = QtGui.QPixmap(talker_api.source_details.logo) - if source_label_logo.height() > 100 or source_label_logo.width() > 300: - source_label_logo = source_label_logo.scaled( - 300, 100, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation - ) - self.lblSourceLogo.setPixmap(source_label_logo) + + self.imageSourceWidget = CoverImageWidget( + self.lblSourceLogo, + CoverImageWidget.URLMode, + options.runtime_config.user_cache_dir, + talker_api, + False, + ) + gridlayoutSourceLogo = QtWidgets.QGridLayout(self.lblSourceLogo) + gridlayoutSourceLogo.addWidget(self.imageSourceWidget) + gridlayoutSourceLogo.setContentsMargins(0, 0, 0, 0) + self.imageSourceWidget.set_url(talker_api.source_details.logo) # Set the minimum row height to the default. # this way rows will be more consistent when resizeRowsToContents is called diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 0bc7223..46be864 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -168,7 +168,9 @@ class ComicVineTalker(ComicTalker): ): super().__init__(version, cache_folder, api_url, api_key) self.source_details = SourceDetails( - name="Comic Vine", ident="comicvine", logo="comictalker/talkers/logos/comicvine.png" + name="Comic Vine", + ident="comicvine", + logo="https://comicvine.gamespot.com/a/bundles/comicvinesite/images/logo.png", ) self.static_options = SourceStaticOptions( website="https://comicvine.gamespot.com/", From 80f42fdc3fe51900edd52adfc4732978326a7378 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 12 Jan 2023 14:43:12 -0800 Subject: [PATCH 03/18] Move log header to execute immediately after the log is configured --- comictaggerlib/log.py | 11 +++++++++++ comictaggerlib/main.py | 14 +++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/comictaggerlib/log.py b/comictaggerlib/log.py index efdf98b..1bb9f53 100644 --- a/comictaggerlib/log.py +++ b/comictaggerlib/log.py @@ -2,6 +2,10 @@ from __future__ import annotations import logging.handlers import pathlib +import platform +import sys + +from comictaggerlib.ctversion import version logger = logging.getLogger("comictagger") @@ -44,3 +48,10 @@ def setup_logging(verbose: int, log_dir: pathlib.Path) -> None: format="%(asctime)s | %(name)s | %(levelname)s | %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) + + logger.info( + "ComicTagger Version: %s running on: %s PyInstaller: %s", + version, + platform.system(), + "Yes" if getattr(sys, "frozen", None) else "No", + ) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 639ebb3..7ed6030 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -18,14 +18,13 @@ from __future__ import annotations import argparse import json import logging.handlers -import platform import signal import sys import settngs +import comicapi import comictalker.comictalkerapi as ct_api -from comicapi import utils from comictaggerlib import cli, ctoptions from comictaggerlib.ctversion import version from comictaggerlib.log import setup_logging @@ -53,7 +52,7 @@ def update_publishers(options: settngs.Namespace) -> None: json_file = options.runtime_config.user_config_dir / "publishers.json" if json_file.exists(): try: - utils.update_publishers(json.loads(json_file.read_text("utf-8"))) + comicapi.utils.update_publishers(json.loads(json_file.read_text("utf-8"))) except Exception: logger.exception("Failed to load publishers from %s", json_file) # show_exception_box(str(e)) @@ -118,18 +117,11 @@ class App: signal.signal(signal.SIGINT, signal.SIG_DFL) - logger.info( - "ComicTagger Version: %s running on: %s PyInstaller: %s", - version, - platform.system(), - "Yes" if getattr(sys, "frozen", None) else "No", - ) - logger.debug("Installed Packages") for pkg in sorted(importlib_metadata.distributions(), key=lambda x: x.name): logger.debug("%s\t%s", pkg.metadata["Name"], pkg.metadata["Version"]) - utils.load_publishers() + comicapi.utils.load_publishers() update_publishers(self.options[0]) if not qt_available and not self.options[0].runtime_no_gui: From 2f7e3921eff3201fd857ebce1a56f21f059324f4 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Wed, 11 Jan 2023 16:19:44 -0800 Subject: [PATCH 04/18] Separate archivers into their own packages --- comicapi/archivers/__init__.py | 19 ++ comicapi/archivers/folder.py | 90 +++++ comicapi/archivers/rar.py | 246 +++++++++++++ comicapi/archivers/sevenzip.py | 117 +++++++ comicapi/archivers/unknown.py | 35 ++ comicapi/archivers/zip.py | 179 ++++++++++ comicapi/comicarchive.py | 607 +-------------------------------- 7 files changed, 688 insertions(+), 605 deletions(-) create mode 100644 comicapi/archivers/__init__.py create mode 100644 comicapi/archivers/folder.py create mode 100644 comicapi/archivers/rar.py create mode 100644 comicapi/archivers/sevenzip.py create mode 100644 comicapi/archivers/unknown.py create mode 100644 comicapi/archivers/zip.py diff --git a/comicapi/archivers/__init__.py b/comicapi/archivers/__init__.py new file mode 100644 index 0000000..8758c55 --- /dev/null +++ b/comicapi/archivers/__init__.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from comicapi.archivers.unknown import UnknownArchiver + +__all__ = ["UnknownArchiver"] +from comicapi.archivers.folder import FolderArchiver +from comicapi.archivers.rar import RarArchiver, rar_support +from comicapi.archivers.sevenzip import SevenZipArchiver, z7_support +from comicapi.archivers.zip import ZipArchiver + +__all__ = [ + "UnknownArchiver", + "FolderArchiver", + "RarArchiver", + "rar_support", + "ZipArchiver", + "SevenZipArchiver", + "z7_support", +] diff --git a/comicapi/archivers/folder.py b/comicapi/archivers/folder.py new file mode 100644 index 0000000..4b0186f --- /dev/null +++ b/comicapi/archivers/folder.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import logging +import os +import pathlib + +from comicapi.archivers import UnknownArchiver + +logger = logging.getLogger(__name__) + + +class FolderArchiver(UnknownArchiver): + + """Folder implementation""" + + def __init__(self, path: pathlib.Path | str) -> None: + super().__init__(path) + self.comment_file_name = "ComicTaggerFolderComment.txt" + + def get_comment(self) -> str: + try: + return self.read_file(self.comment_file_name).decode("utf-8") + except OSError: + return "" + + def set_comment(self, comment: str) -> bool: + if (self.path / self.comment_file_name).exists() or comment: + return self.write_file(self.comment_file_name, comment.encode("utf-8")) + return True + + def read_file(self, archive_file: str) -> bytes: + try: + with open(self.path / archive_file, mode="rb") as f: + data = f.read() + except OSError as e: + logger.error("Error reading folder archive [%s]: %s :: %s", e, self.path, archive_file) + raise + + return data + + def remove_file(self, archive_file: str) -> bool: + try: + (self.path / archive_file).unlink(missing_ok=True) + except OSError as e: + logger.error("Error removing file for folder archive [%s]: %s :: %s", e, self.path, archive_file) + return False + else: + return True + + def write_file(self, archive_file: str, data: bytes) -> bool: + try: + file_path = self.path / archive_file + file_path.parent.mkdir(exist_ok=True, parents=True) + with open(self.path / archive_file, mode="wb") as f: + f.write(data) + except OSError as e: + logger.error("Error writing folder archive [%s]: %s :: %s", e, self.path, archive_file) + return False + else: + return True + + def get_filename_list(self) -> list[str]: + filenames = [] + try: + for root, _dirs, files in os.walk(self.path): + for f in files: + filenames.append(os.path.relpath(os.path.join(root, f), self.path).replace(os.path.sep, "/")) + return filenames + except OSError as e: + logger.error("Error listing files in folder archive [%s]: %s", e, self.path) + return [] + + def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + """Replace the current zip with one copied from another archive""" + try: + for filename in other_archive.get_filename_list(): + data = other_archive.read_file(filename) + if data is not None: + self.write_file(filename, data) + + # preserve the old comment + comment = other_archive.get_comment() + if comment is not None: + if not self.set_comment(comment): + return False + except Exception: + logger.exception("Error while copying archive from %s to %s", other_archive.path, self.path) + return False + else: + return True diff --git a/comicapi/archivers/rar.py b/comicapi/archivers/rar.py new file mode 100644 index 0000000..db04b8b --- /dev/null +++ b/comicapi/archivers/rar.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +import logging +import os +import pathlib +import platform +import shutil +import subprocess +import tempfile +import time + +from comicapi.archivers import UnknownArchiver + +try: + from unrar.cffi import rarfile + + rar_support = True +except ImportError: + rar_support = False + + +logger = logging.getLogger(__name__) + +if not rar_support: + logger.error("unrar-cffi unavailable") + + +class RarArchiver(UnknownArchiver): + """RAR implementation""" + + def __init__(self, path: pathlib.Path | str, rar_exe_path: str = "rar") -> None: + super().__init__(path) + self.rar_exe_path = shutil.which(rar_exe_path) or "" + + # windows only, keeps the cmd.exe from popping up + if platform.system() == "Windows": + self.startupinfo = subprocess.STARTUPINFO() # type: ignore + self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore + else: + self.startupinfo = None + + def get_comment(self) -> str: + rarc = self.get_rar_obj() + return rarc.comment.decode("utf-8") if rarc else "" + + def set_comment(self, comment: str) -> bool: + if rar_support and self.rar_exe_path: + try: + # write comment to temp file + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_file = pathlib.Path(tmp_dir) / "rar_comment.txt" + tmp_file.write_text(comment, encoding="utf-8") + + working_dir = os.path.dirname(os.path.abspath(self.path)) + + # use external program to write comment to Rar archive + proc_args = [ + self.rar_exe_path, + "c", + f"-w{working_dir}", + "-c-", + f"-z{tmp_file}", + str(self.path), + ] + subprocess.run( + proc_args, + startupinfo=self.startupinfo, + stdout=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + + if platform.system() == "Darwin": + time.sleep(1) + except (subprocess.CalledProcessError, OSError) as e: + logger.exception("Error writing comment to rar archive [%s]: %s", e, self.path) + return False + else: + return True + else: + return False + + def read_file(self, archive_file: str) -> bytes: + + rarc = self.get_rar_obj() + if rarc is None: + return b"" + + tries = 0 + while tries < 7: + try: + tries = tries + 1 + data: bytes = rarc.open(archive_file).read() + entries = [(rarc.getinfo(archive_file), data)] + + if entries[0][0].file_size != len(entries[0][1]): + logger.info( + "Error reading rar archive [file is not expected size: %d vs %d] %s :: %s :: tries #%d", + entries[0][0].file_size, + len(entries[0][1]), + self.path, + archive_file, + tries, + ) + continue + + except OSError as e: + logger.error("Error reading rar archive [%s]: %s :: %s :: tries #%d", e, self.path, archive_file, tries) + time.sleep(1) + except Exception as e: + logger.error( + "Unexpected exception reading rar archive [%s]: %s :: %s :: tries #%d", + e, + self.path, + archive_file, + tries, + ) + break + + else: + # Success. Entries is a list of of tuples: ( rarinfo, filedata) + if len(entries) == 1: + return entries[0][1] + + raise OSError + + raise OSError + + def remove_file(self, archive_file: str) -> bool: + if self.rar_exe_path: + # use external program to remove file from Rar archive + result = subprocess.run( + [self.rar_exe_path, "d", "-c-", self.path, archive_file], + startupinfo=self.startupinfo, + stdout=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + if platform.system() == "Darwin": + time.sleep(1) + if result.returncode != 0: + logger.error( + "Error removing file from rar archive [exitcode: %d]: %s :: %s", + result.returncode, + self.path, + archive_file, + ) + return False + return True + else: + return False + + def write_file(self, archive_file: str, data: bytes) -> bool: + if self.rar_exe_path: + archive_path = pathlib.PurePosixPath(archive_file) + archive_name = archive_path.name + archive_parent = str(archive_path.parent).lstrip("./") + + # use external program to write file to Rar archive + result = subprocess.run( + [self.rar_exe_path, "a", f"-si{archive_name}", f"-ap{archive_parent}", "-c-", "-ep", self.path], + input=data, + startupinfo=self.startupinfo, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + if platform.system() == "Darwin": + time.sleep(1) + if result.returncode != 0: + logger.error( + "Error writing rar archive [exitcode: %d]: %s :: %s", result.returncode, self.path, archive_file + ) + return False + else: + return True + else: + return False + + def get_filename_list(self) -> list[str]: + rarc = self.get_rar_obj() + tries = 0 + if rar_support and rarc: + while tries < 7: + try: + tries = tries + 1 + namelist = [] + for item in rarc.infolist(): + if item.file_size != 0: + namelist.append(item.filename) + + except OSError as e: + logger.error("Error listing files in rar archive [%s]: %s :: attempt #%d", e, self.path, tries) + time.sleep(1) + + else: + return namelist + return [] + + def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + """Replace the current archive with one copied from another archive""" + try: + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = pathlib.Path(tmp_dir) + rar_cwd = tmp_path / "rar" + rar_cwd.mkdir(exist_ok=True) + rar_path = (tmp_path / self.path.name).with_suffix(".rar") + + for filename in other_archive.get_filename_list(): + (rar_cwd / filename).parent.mkdir(exist_ok=True, parents=True) + data = other_archive.read_file(filename) + if data is not None: + with open(rar_cwd / filename, mode="w+b") as tmp_file: + tmp_file.write(data) + result = subprocess.run( + [self.rar_exe_path, "a", "-r", "-c-", str(rar_path.absolute()), "."], + cwd=rar_cwd.absolute(), + startupinfo=self.startupinfo, + stdout=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if result.returncode != 0: + logger.error("Error while copying to rar archive [exitcode: %d]: %s", result.returncode, self.path) + return False + + self.path.unlink(missing_ok=True) + shutil.move(rar_path, self.path) + except Exception as e: + logger.exception("Error while copying to rar archive [%s]: from %s to %s", e, other_archive.path, self.path) + return False + else: + return True + + def get_rar_obj(self) -> rarfile.RarFile | None: + if rar_support: + try: + rarc = rarfile.RarFile(str(self.path)) + except (OSError, rarfile.RarFileError) as e: + logger.error("Unable to get rar object [%s]: %s", e, self.path) + else: + return rarc + + return None diff --git a/comicapi/archivers/sevenzip.py b/comicapi/archivers/sevenzip.py new file mode 100644 index 0000000..9e0f3f0 --- /dev/null +++ b/comicapi/archivers/sevenzip.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import logging +import os +import pathlib +import shutil +import tempfile + +from comicapi.archivers import UnknownArchiver + +try: + import py7zr + + z7_support = True +except ImportError: + z7_support = False + +logger = logging.getLogger(__name__) + + +class SevenZipArchiver(UnknownArchiver): + + """7Z implementation""" + + def __init__(self, path: pathlib.Path | str) -> None: + super().__init__(path) + + # @todo: Implement Comment? + def get_comment(self) -> str: + return "" + + def set_comment(self, comment: str) -> bool: + return False + + def read_file(self, archive_file: str) -> bytes: + data = b"" + try: + with py7zr.SevenZipFile(self.path, "r") as zf: + data = zf.read(archive_file)[archive_file].read() + except (py7zr.Bad7zFile, OSError) as e: + logger.error("Error reading 7zip archive [%s]: %s :: %s", e, self.path, archive_file) + raise + + return data + + def remove_file(self, archive_file: str) -> bool: + return self.rebuild([archive_file]) + + def write_file(self, archive_file: str, data: bytes) -> bool: + # At the moment, no other option but to rebuild the whole + # archive w/o the indicated file. Very sucky, but maybe + # another solution can be found + files = self.get_filename_list() + if archive_file in files: + if not self.rebuild([archive_file]): + return False + + try: + # now just add the archive file as a new one + with py7zr.SevenZipFile(self.path, "a") as zf: + zf.writestr(data, archive_file) + return True + except (py7zr.Bad7zFile, OSError) as e: + logger.error("Error writing 7zip archive [%s]: %s :: %s", e, self.path, archive_file) + return False + + def get_filename_list(self) -> list[str]: + try: + with py7zr.SevenZipFile(self.path, "r") as zf: + namelist: list[str] = [file.filename for file in zf.list() if not file.is_directory] + + return namelist + except (py7zr.Bad7zFile, OSError) as e: + logger.error("Error listing files in 7zip archive [%s]: %s", e, self.path) + return [] + + def rebuild(self, exclude_list: list[str]) -> bool: + """Zip helper func + + This recompresses the zip archive, without the files in the exclude_list + """ + + try: + # py7zr treats all archives as if they used solid compression + # so we need to get the filename list first to read all the files at once + with py7zr.SevenZipFile(self.path, mode="r") as zin: + targets = [f for f in zin.getnames() if f not in exclude_list] + with tempfile.NamedTemporaryFile(dir=os.path.dirname(self.path), delete=False) as tmp_file: + with py7zr.SevenZipFile(tmp_file.file, mode="w") as zout: + with py7zr.SevenZipFile(self.path, mode="r") as zin: + for filename, buffer in zin.read(targets).items(): + zout.writef(buffer, filename) + + self.path.unlink(missing_ok=True) + tmp_file.close() # Required on windows + + shutil.move(tmp_file.name, self.path) + except (py7zr.Bad7zFile, OSError) as e: + logger.error("Error rebuilding 7zip file [%s]: %s", e, self.path) + return False + return True + + def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + """Replace the current zip with one copied from another archive""" + try: + with py7zr.SevenZipFile(self.path, "w") as zout: + for filename in other_archive.get_filename_list(): + data = other_archive.read_file( + filename + ) # This will be very inefficient if other_archive is a 7z file + if data is not None: + zout.writestr(data, filename) + except Exception as e: + logger.error("Error while copying to 7zip archive [%s]: from %s to %s", e, other_archive.path, self.path) + return False + else: + return True diff --git a/comicapi/archivers/unknown.py b/comicapi/archivers/unknown.py new file mode 100644 index 0000000..5f79720 --- /dev/null +++ b/comicapi/archivers/unknown.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import pathlib + + +class UnknownArchiver: + + """Unknown implementation""" + + def __init__(self, path: pathlib.Path | str) -> None: + self.path = pathlib.Path(path) + + def get_comment(self) -> str: + return "" + + def set_comment(self, comment: str) -> bool: + return False + + def read_file(self, archive_file: str) -> bytes: + raise NotImplementedError + + def remove_file(self, archive_file: str) -> bool: + return False + + def write_file(self, archive_file: str, data: bytes) -> bool: + return False + + def get_filename_list(self) -> list[str]: + return [] + + def rebuild(self, exclude_list: list[str]) -> bool: + return False + + def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + return False diff --git a/comicapi/archivers/zip.py b/comicapi/archivers/zip.py new file mode 100644 index 0000000..8d3d976 --- /dev/null +++ b/comicapi/archivers/zip.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import logging +import os +import pathlib +import shutil +import struct +import tempfile +import zipfile +from typing import cast + +from comicapi.archivers import UnknownArchiver + +logger = logging.getLogger(__name__) + + +class ZipArchiver(UnknownArchiver): + + """ZIP implementation""" + + def __init__(self, path: pathlib.Path | str) -> None: + super().__init__(path) + + def get_comment(self) -> str: + with zipfile.ZipFile(self.path, "r") as zf: + comment = zf.comment.decode("utf-8") + return comment + + def set_comment(self, comment: str) -> bool: + with zipfile.ZipFile(self.path, mode="a") as zf: + zf.comment = bytes(comment, "utf-8") + return True + + def read_file(self, archive_file: str) -> bytes: + with zipfile.ZipFile(self.path, mode="r") as zf: + try: + data = zf.read(archive_file) + except (zipfile.BadZipfile, OSError) as e: + logger.error("Error reading zip archive [%s]: %s :: %s", e, self.path, archive_file) + raise + return data + + def remove_file(self, archive_file: str) -> bool: + return self.rebuild([archive_file]) + + def write_file(self, archive_file: str, data: bytes) -> bool: + # At the moment, no other option but to rebuild the whole + # zip archive w/o the indicated file. Very sucky, but maybe + # another solution can be found + files = self.get_filename_list() + if archive_file in files: + if not self.rebuild([archive_file]): + return False + + try: + # now just add the archive file as a new one + with zipfile.ZipFile(self.path, mode="a", allowZip64=True, compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr(archive_file, data) + return True + except (zipfile.BadZipfile, OSError) as e: + logger.error("Error writing zip archive [%s]: %s :: %s", e, self.path, archive_file) + return False + + def get_filename_list(self) -> list[str]: + try: + with zipfile.ZipFile(self.path, mode="r") as zf: + namelist = [file.filename for file in zf.infolist() if not file.is_dir()] + return namelist + except (zipfile.BadZipfile, OSError) as e: + logger.error("Error listing files in zip archive [%s]: %s", e, self.path) + return [] + + def rebuild(self, exclude_list: list[str]) -> bool: + """Zip helper func + + This recompresses the zip archive, without the files in the exclude_list + """ + try: + with zipfile.ZipFile( + tempfile.NamedTemporaryFile(dir=os.path.dirname(self.path), delete=False), "w", allowZip64=True + ) as zout: + with zipfile.ZipFile(self.path, mode="r") as zin: + for item in zin.infolist(): + buffer = zin.read(item.filename) + if item.filename not in exclude_list: + zout.writestr(item, buffer) + + # preserve the old comment + zout.comment = zin.comment + + # replace with the new file + self.path.unlink(missing_ok=True) + zout.close() # Required on windows + + shutil.move(cast(str, zout.filename), self.path) + + except (zipfile.BadZipfile, OSError) as e: + logger.error("Error rebuilding zip file [%s]: %s", e, self.path) + return False + return True + + def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + """Replace the current zip with one copied from another archive""" + try: + with zipfile.ZipFile(self.path, mode="w", allowZip64=True) as zout: + for filename in other_archive.get_filename_list(): + data = other_archive.read_file(filename) + if data is not None: + zout.writestr(filename, data) + + # preserve the old comment + comment = other_archive.get_comment() + if comment is not None: + if not self.write_zip_comment(self.path, comment): + return False + except Exception as e: + logger.error("Error while copying to zip archive [%s]: from %s to %s", e, other_archive.path, self.path) + return False + else: + return True + + def write_zip_comment(self, filename: pathlib.Path | str, comment: str) -> bool: + """ + This is a custom function for writing a comment to a zip file, + since the built-in one doesn't seem to work on Windows and Mac OS/X + + Fortunately, the zip comment is at the end of the file, and it's + easy to manipulate. See this website for more info: + see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure + """ + + # get file size + statinfo = os.stat(filename) + file_length = statinfo.st_size + + try: + with open(filename, mode="r+b") as file: + + # the starting position, relative to EOF + pos = -4 + found = False + + # walk backwards to find the "End of Central Directory" record + while (not found) and (-pos != file_length): + # seek, relative to EOF + file.seek(pos, 2) + value = file.read(4) + + # look for the end of central directory signature + if bytearray(value) == bytearray([0x50, 0x4B, 0x05, 0x06]): + found = True + else: + # not found, step back another byte + pos = pos - 1 + + if found: + + # now skip forward 20 bytes to the comment length word + pos += 20 + file.seek(pos, 2) + + # Pack the length of the comment string + fmt = "H" # one 2-byte integer + comment_length = struct.pack(fmt, len(comment)) # pack integer in a binary string + + # write out the length + file.write(comment_length) + file.seek(pos + 2, 2) + + # write out the comment itself + file.write(comment.encode("utf-8")) + file.truncate() + else: + raise Exception("Could not find the End of Central Directory record!") + except Exception as e: + logger.error("Error writing comment to zip archive [%s]: %s", e, self.path) + return False + else: + return True diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index da4fd83..1009b5d 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -18,12 +18,7 @@ import io import logging import os import pathlib -import platform import shutil -import struct -import subprocess -import tempfile -import time import zipfile from typing import cast @@ -31,6 +26,7 @@ import natsort import wordninja from comicapi import filenamelexer, filenameparser, utils +from comicapi.archivers import FolderArchiver, RarArchiver, SevenZipArchiver, UnknownArchiver, ZipArchiver from comicapi.comet import CoMet from comicapi.comicbookinfo import ComicBookInfo from comicapi.comicinfoxml import ComicInfoXml @@ -58,10 +54,9 @@ except ImportError: logger = logging.getLogger(__name__) + if not pil_available: logger.error("PIL unavalable") -if not rar_support: - logger.error("unrar-cffi unavailable") class MetaDataStyle: @@ -72,604 +67,6 @@ class MetaDataStyle: short_name = ["cbl", "cr", "comet"] -class UnknownArchiver: - - """Unknown implementation""" - - def __init__(self, path: pathlib.Path | str) -> None: - self.path = pathlib.Path(path) - - def get_comment(self) -> str: - return "" - - def set_comment(self, comment: str) -> bool: - return False - - def read_file(self, archive_file: str) -> bytes: - raise NotImplementedError - - def remove_file(self, archive_file: str) -> bool: - return False - - def write_file(self, archive_file: str, data: bytes) -> bool: - return False - - def get_filename_list(self) -> list[str]: - return [] - - def rebuild(self, exclude_list: list[str]) -> bool: - return False - - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: - return False - - -class SevenZipArchiver(UnknownArchiver): - - """7Z implementation""" - - def __init__(self, path: pathlib.Path | str) -> None: - super().__init__(path) - - # @todo: Implement Comment? - def get_comment(self) -> str: - return "" - - def set_comment(self, comment: str) -> bool: - return False - - def read_file(self, archive_file: str) -> bytes: - data = b"" - try: - with py7zr.SevenZipFile(self.path, "r") as zf: - data = zf.read(archive_file)[archive_file].read() - except (py7zr.Bad7zFile, OSError) as e: - logger.error("Error reading 7zip archive [%s]: %s :: %s", e, self.path, archive_file) - raise - - return data - - def remove_file(self, archive_file: str) -> bool: - return self.rebuild([archive_file]) - - def write_file(self, archive_file: str, data: bytes) -> bool: - # At the moment, no other option but to rebuild the whole - # archive w/o the indicated file. Very sucky, but maybe - # another solution can be found - files = self.get_filename_list() - if archive_file in files: - if not self.rebuild([archive_file]): - return False - - try: - # now just add the archive file as a new one - with py7zr.SevenZipFile(self.path, "a") as zf: - zf.writestr(data, archive_file) - return True - except (py7zr.Bad7zFile, OSError) as e: - logger.error("Error writing 7zip archive [%s]: %s :: %s", e, self.path, archive_file) - return False - - def get_filename_list(self) -> list[str]: - try: - with py7zr.SevenZipFile(self.path, "r") as zf: - namelist: list[str] = [file.filename for file in zf.list() if not file.is_directory] - - return namelist - except (py7zr.Bad7zFile, OSError) as e: - logger.error("Error listing files in 7zip archive [%s]: %s", e, self.path) - return [] - - def rebuild(self, exclude_list: list[str]) -> bool: - """Zip helper func - - This recompresses the zip archive, without the files in the exclude_list - """ - - try: - # py7zr treats all archives as if they used solid compression - # so we need to get the filename list first to read all the files at once - with py7zr.SevenZipFile(self.path, mode="r") as zin: - targets = [f for f in zin.getnames() if f not in exclude_list] - with tempfile.NamedTemporaryFile(dir=os.path.dirname(self.path), delete=False) as tmp_file: - with py7zr.SevenZipFile(tmp_file.file, mode="w") as zout: - with py7zr.SevenZipFile(self.path, mode="r") as zin: - for filename, buffer in zin.read(targets).items(): - zout.writef(buffer, filename) - - self.path.unlink(missing_ok=True) - tmp_file.close() # Required on windows - - shutil.move(tmp_file.name, self.path) - except (py7zr.Bad7zFile, OSError) as e: - logger.error("Error rebuilding 7zip file [%s]: %s", e, self.path) - return False - return True - - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: - """Replace the current zip with one copied from another archive""" - try: - with py7zr.SevenZipFile(self.path, "w") as zout: - for filename in other_archive.get_filename_list(): - data = other_archive.read_file( - filename - ) # This will be very inefficient if other_archive is a 7z file - if data is not None: - zout.writestr(data, filename) - except Exception as e: - logger.error("Error while copying to 7zip archive [%s]: from %s to %s", e, other_archive.path, self.path) - return False - else: - return True - - -class ZipArchiver(UnknownArchiver): - - """ZIP implementation""" - - def __init__(self, path: pathlib.Path | str) -> None: - super().__init__(path) - - def get_comment(self) -> str: - with zipfile.ZipFile(self.path, "r") as zf: - comment = zf.comment.decode("utf-8") - return comment - - def set_comment(self, comment: str) -> bool: - with zipfile.ZipFile(self.path, mode="a") as zf: - zf.comment = bytes(comment, "utf-8") - return True - - def read_file(self, archive_file: str) -> bytes: - with zipfile.ZipFile(self.path, mode="r") as zf: - try: - data = zf.read(archive_file) - except (zipfile.BadZipfile, OSError) as e: - logger.error("Error reading zip archive [%s]: %s :: %s", e, self.path, archive_file) - raise - return data - - def remove_file(self, archive_file: str) -> bool: - return self.rebuild([archive_file]) - - def write_file(self, archive_file: str, data: bytes) -> bool: - # At the moment, no other option but to rebuild the whole - # zip archive w/o the indicated file. Very sucky, but maybe - # another solution can be found - files = self.get_filename_list() - if archive_file in files: - if not self.rebuild([archive_file]): - return False - - try: - # now just add the archive file as a new one - with zipfile.ZipFile(self.path, mode="a", allowZip64=True, compression=zipfile.ZIP_DEFLATED) as zf: - zf.writestr(archive_file, data) - return True - except (zipfile.BadZipfile, OSError) as e: - logger.error("Error writing zip archive [%s]: %s :: %s", e, self.path, archive_file) - return False - - def get_filename_list(self) -> list[str]: - try: - with zipfile.ZipFile(self.path, mode="r") as zf: - namelist = [file.filename for file in zf.infolist() if not file.is_dir()] - return namelist - except (zipfile.BadZipfile, OSError) as e: - logger.error("Error listing files in zip archive [%s]: %s", e, self.path) - return [] - - def rebuild(self, exclude_list: list[str]) -> bool: - """Zip helper func - - This recompresses the zip archive, without the files in the exclude_list - """ - try: - with zipfile.ZipFile( - tempfile.NamedTemporaryFile(dir=os.path.dirname(self.path), delete=False), "w", allowZip64=True - ) as zout: - with zipfile.ZipFile(self.path, mode="r") as zin: - for item in zin.infolist(): - buffer = zin.read(item.filename) - if item.filename not in exclude_list: - zout.writestr(item, buffer) - - # preserve the old comment - zout.comment = zin.comment - - # replace with the new file - self.path.unlink(missing_ok=True) - zout.close() # Required on windows - - shutil.move(cast(str, zout.filename), self.path) - - except (zipfile.BadZipfile, OSError) as e: - logger.error("Error rebuilding zip file [%s]: %s", e, self.path) - return False - return True - - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: - """Replace the current zip with one copied from another archive""" - try: - with zipfile.ZipFile(self.path, mode="w", allowZip64=True) as zout: - for filename in other_archive.get_filename_list(): - data = other_archive.read_file(filename) - if data is not None: - zout.writestr(filename, data) - - # preserve the old comment - comment = other_archive.get_comment() - if comment is not None: - if not self.write_zip_comment(self.path, comment): - return False - except Exception as e: - logger.error("Error while copying to zip archive [%s]: from %s to %s", e, other_archive.path, self.path) - return False - else: - return True - - def write_zip_comment(self, filename: pathlib.Path | str, comment: str) -> bool: - """ - This is a custom function for writing a comment to a zip file, - since the built-in one doesn't seem to work on Windows and Mac OS/X - - Fortunately, the zip comment is at the end of the file, and it's - easy to manipulate. See this website for more info: - see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure - """ - - # get file size - statinfo = os.stat(filename) - file_length = statinfo.st_size - - try: - with open(filename, mode="r+b") as file: - - # the starting position, relative to EOF - pos = -4 - found = False - - # walk backwards to find the "End of Central Directory" record - while (not found) and (-pos != file_length): - # seek, relative to EOF - file.seek(pos, 2) - value = file.read(4) - - # look for the end of central directory signature - if bytearray(value) == bytearray([0x50, 0x4B, 0x05, 0x06]): - found = True - else: - # not found, step back another byte - pos = pos - 1 - - if found: - - # now skip forward 20 bytes to the comment length word - pos += 20 - file.seek(pos, 2) - - # Pack the length of the comment string - fmt = "H" # one 2-byte integer - comment_length = struct.pack(fmt, len(comment)) # pack integer in a binary string - - # write out the length - file.write(comment_length) - file.seek(pos + 2, 2) - - # write out the comment itself - file.write(comment.encode("utf-8")) - file.truncate() - else: - raise Exception("Could not find the End of Central Directory record!") - except Exception as e: - logger.error("Error writing comment to zip archive [%s]: %s", e, self.path) - return False - else: - return True - - -class RarArchiver(UnknownArchiver): - """RAR implementation""" - - def __init__(self, path: pathlib.Path | str, rar_exe_path: str = "rar") -> None: - super().__init__(path) - self.rar_exe_path = shutil.which(rar_exe_path) or "" - - # windows only, keeps the cmd.exe from popping up - if platform.system() == "Windows": - self.startupinfo = subprocess.STARTUPINFO() # type: ignore - self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore - else: - self.startupinfo = None - - def get_comment(self) -> str: - rarc = self.get_rar_obj() - return rarc.comment.decode("utf-8") if rarc else "" - - def set_comment(self, comment: str) -> bool: - if rar_support and self.rar_exe_path: - try: - # write comment to temp file - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_file = pathlib.Path(tmp_dir) / "rar_comment.txt" - tmp_file.write_text(comment, encoding="utf-8") - - working_dir = os.path.dirname(os.path.abspath(self.path)) - - # use external program to write comment to Rar archive - proc_args = [ - self.rar_exe_path, - "c", - f"-w{working_dir}", - "-c-", - f"-z{tmp_file}", - str(self.path), - ] - subprocess.run( - proc_args, - startupinfo=self.startupinfo, - stdout=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=True, - ) - - if platform.system() == "Darwin": - time.sleep(1) - except (subprocess.CalledProcessError, OSError) as e: - logger.exception("Error writing comment to rar archive [%s]: %s", e, self.path) - return False - else: - return True - else: - return False - - def read_file(self, archive_file: str) -> bytes: - - rarc = self.get_rar_obj() - if rarc is None: - return b"" - - tries = 0 - while tries < 7: - try: - tries = tries + 1 - data: bytes = rarc.open(archive_file).read() - entries = [(rarc.getinfo(archive_file), data)] - - if entries[0][0].file_size != len(entries[0][1]): - logger.info( - "Error reading rar archive [file is not expected size: %d vs %d] %s :: %s :: tries #%d", - entries[0][0].file_size, - len(entries[0][1]), - self.path, - archive_file, - tries, - ) - continue - - except OSError as e: - logger.error("Error reading rar archive [%s]: %s :: %s :: tries #%d", e, self.path, archive_file, tries) - time.sleep(1) - except Exception as e: - logger.error( - "Unexpected exception reading rar archive [%s]: %s :: %s :: tries #%d", - e, - self.path, - archive_file, - tries, - ) - break - - else: - # Success. Entries is a list of of tuples: ( rarinfo, filedata) - if len(entries) == 1: - return entries[0][1] - - raise OSError - - raise OSError - - def remove_file(self, archive_file: str) -> bool: - if self.rar_exe_path: - # use external program to remove file from Rar archive - result = subprocess.run( - [self.rar_exe_path, "d", "-c-", self.path, archive_file], - startupinfo=self.startupinfo, - stdout=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - if platform.system() == "Darwin": - time.sleep(1) - if result.returncode != 0: - logger.error( - "Error removing file from rar archive [exitcode: %d]: %s :: %s", - result.returncode, - self.path, - archive_file, - ) - return False - return True - else: - return False - - def write_file(self, archive_file: str, data: bytes) -> bool: - if self.rar_exe_path: - archive_path = pathlib.PurePosixPath(archive_file) - archive_name = archive_path.name - archive_parent = str(archive_path.parent).lstrip("./") - - # use external program to write file to Rar archive - result = subprocess.run( - [self.rar_exe_path, "a", f"-si{archive_name}", f"-ap{archive_parent}", "-c-", "-ep", self.path], - input=data, - startupinfo=self.startupinfo, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - if platform.system() == "Darwin": - time.sleep(1) - if result.returncode != 0: - logger.error( - "Error writing rar archive [exitcode: %d]: %s :: %s", result.returncode, self.path, archive_file - ) - return False - else: - return True - else: - return False - - def get_filename_list(self) -> list[str]: - rarc = self.get_rar_obj() - tries = 0 - if rar_support and rarc: - while tries < 7: - try: - tries = tries + 1 - namelist = [] - for item in rarc.infolist(): - if item.file_size != 0: - namelist.append(item.filename) - - except OSError as e: - logger.error("Error listing files in rar archive [%s]: %s :: attempt #%d", e, self.path, tries) - time.sleep(1) - - else: - return namelist - return [] - - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: - """Replace the current archive with one copied from another archive""" - try: - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_path = pathlib.Path(tmp_dir) - rar_cwd = tmp_path / "rar" - rar_cwd.mkdir(exist_ok=True) - rar_path = (tmp_path / self.path.name).with_suffix(".rar") - - for filename in other_archive.get_filename_list(): - (rar_cwd / filename).parent.mkdir(exist_ok=True, parents=True) - data = other_archive.read_file(filename) - if data is not None: - with open(rar_cwd / filename, mode="w+b") as tmp_file: - tmp_file.write(data) - result = subprocess.run( - [self.rar_exe_path, "a", "-r", "-c-", str(rar_path.absolute()), "."], - cwd=rar_cwd.absolute(), - startupinfo=self.startupinfo, - stdout=subprocess.DEVNULL, - stdin=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - if result.returncode != 0: - logger.error("Error while copying to rar archive [exitcode: %d]: %s", result.returncode, self.path) - return False - - self.path.unlink(missing_ok=True) - shutil.move(rar_path, self.path) - except Exception as e: - logger.exception("Error while copying to rar archive [%s]: from %s to %s", e, other_archive.path, self.path) - return False - else: - return True - - def get_rar_obj(self) -> rarfile.RarFile | None: - if rar_support: - try: - rarc = rarfile.RarFile(str(self.path)) - except (OSError, rarfile.RarFileError) as e: - logger.error("Unable to get rar object [%s]: %s", e, self.path) - else: - return rarc - - return None - - -class FolderArchiver(UnknownArchiver): - - """Folder implementation""" - - def __init__(self, path: pathlib.Path | str) -> None: - super().__init__(path) - self.comment_file_name = "ComicTaggerFolderComment.txt" - - def get_comment(self) -> str: - try: - return self.read_file(self.comment_file_name).decode("utf-8") - except OSError: - return "" - - def set_comment(self, comment: str) -> bool: - if (self.path / self.comment_file_name).exists() or comment: - return self.write_file(self.comment_file_name, comment.encode("utf-8")) - return True - - def read_file(self, archive_file: str) -> bytes: - try: - with open(self.path / archive_file, mode="rb") as f: - data = f.read() - except OSError as e: - logger.error("Error reading folder archive [%s]: %s :: %s", e, self.path, archive_file) - raise - - return data - - def remove_file(self, archive_file: str) -> bool: - try: - (self.path / archive_file).unlink(missing_ok=True) - except OSError as e: - logger.error("Error removing file for folder archive [%s]: %s :: %s", e, self.path, archive_file) - return False - else: - return True - - def write_file(self, archive_file: str, data: bytes) -> bool: - try: - file_path = self.path / archive_file - file_path.parent.mkdir(exist_ok=True, parents=True) - with open(self.path / archive_file, mode="wb") as f: - f.write(data) - except OSError as e: - logger.error("Error writing folder archive [%s]: %s :: %s", e, self.path, archive_file) - return False - else: - return True - - def get_filename_list(self) -> list[str]: - filenames = [] - try: - for root, _dirs, files in os.walk(self.path): - for f in files: - filenames.append(os.path.relpath(os.path.join(root, f), self.path).replace(os.path.sep, "/")) - return filenames - except OSError as e: - logger.error("Error listing files in folder archive [%s]: %s", e, self.path) - return [] - - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: - """Replace the current zip with one copied from another archive""" - try: - for filename in other_archive.get_filename_list(): - data = other_archive.read_file(filename) - if data is not None: - self.write_file(filename, data) - - # preserve the old comment - comment = other_archive.get_comment() - if comment is not None: - if not self.set_comment(comment): - return False - except Exception: - logger.exception("Error while copying archive from %s to %s", other_archive.path, self.path) - return False - else: - return True - - class ComicArchive: logo_data = b"" From 712986ee69f3ea41e3dfc5dc9c06329daff84517 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Wed, 17 Aug 2022 15:53:19 -0700 Subject: [PATCH 05/18] Turn comicapi.archivers.* into plugins --- comicapi/archivers/__init__.py | 24 +++-- comicapi/archivers/archiver.py | 62 +++++++++++++ comicapi/archivers/folder.py | 17 ++-- comicapi/archivers/rar.py | 27 ++++-- comicapi/archivers/sevenzip.py | 26 ++++-- comicapi/archivers/unknown.py | 35 -------- comicapi/archivers/zip.py | 23 +++-- comicapi/comicarchive.py | 132 +++++++++------------------- comictaggerlib/cli.py | 16 +--- comictaggerlib/ctoptions/cmdline.py | 20 +++-- comictaggerlib/fileselectionlist.py | 16 +--- comictaggerlib/main.py | 3 + comictaggerlib/renamewindow.py | 11 +-- comictaggerlib/taggerwindow.py | 11 +-- setup.py | 14 ++- tests/comicarchive_test.py | 26 +++--- tests/conftest.py | 9 +- 17 files changed, 237 insertions(+), 235 deletions(-) create mode 100644 comicapi/archivers/archiver.py delete mode 100644 comicapi/archivers/unknown.py diff --git a/comicapi/archivers/__init__.py b/comicapi/archivers/__init__.py index 8758c55..c445eb5 100644 --- a/comicapi/archivers/__init__.py +++ b/comicapi/archivers/__init__.py @@ -1,19 +1,15 @@ from __future__ import annotations -from comicapi.archivers.unknown import UnknownArchiver - -__all__ = ["UnknownArchiver"] +from comicapi.archivers.archiver import Archiver from comicapi.archivers.folder import FolderArchiver -from comicapi.archivers.rar import RarArchiver, rar_support -from comicapi.archivers.sevenzip import SevenZipArchiver, z7_support +from comicapi.archivers.rar import RarArchiver +from comicapi.archivers.sevenzip import SevenZipArchiver from comicapi.archivers.zip import ZipArchiver -__all__ = [ - "UnknownArchiver", - "FolderArchiver", - "RarArchiver", - "rar_support", - "ZipArchiver", - "SevenZipArchiver", - "z7_support", -] + +class UnknownArchiver(Archiver): + def name(self) -> str: + return "Unknown" + + +__all__ = ["Archiver", "UnknownArchiver", "FolderArchiver", "RarArchiver", "ZipArchiver", "SevenZipArchiver"] diff --git a/comicapi/archivers/archiver.py b/comicapi/archivers/archiver.py new file mode 100644 index 0000000..dd11c1b --- /dev/null +++ b/comicapi/archivers/archiver.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import pathlib +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class Archiver(Protocol): + + """Archiver Protocol""" + + path: pathlib.Path + enabled: bool = True + + def __init__(self): + self.path = pathlib.Path() + + def get_comment(self) -> str: + return "" + + def set_comment(self, comment: str) -> bool: + return False + + def supports_comment(self) -> bool: + return False + + def read_file(self, archive_file: str) -> bytes: + raise NotImplementedError + + def remove_file(self, archive_file: str) -> bool: + return False + + def write_file(self, archive_file: str, data: bytes) -> bool: + return False + + def get_filename_list(self) -> list[str]: + return [] + + def rebuild(self, exclude_list: list[str]) -> bool: + return False + + def copy_from_archive(self, other_archive: Archiver) -> bool: + return False + + def is_writable(self) -> bool: + return False + + def extension(self) -> str: + return "" + + def name(self) -> str: + return "" + + @classmethod + def is_valid(cls, path: pathlib.Path) -> bool: + return False + + @classmethod + def open(cls, path: pathlib.Path) -> Archiver: + archiver = cls() + archiver.path = path + return archiver diff --git a/comicapi/archivers/folder.py b/comicapi/archivers/folder.py index 4b0186f..c0292dc 100644 --- a/comicapi/archivers/folder.py +++ b/comicapi/archivers/folder.py @@ -4,17 +4,17 @@ import logging import os import pathlib -from comicapi.archivers import UnknownArchiver +from comicapi.archivers import Archiver logger = logging.getLogger(__name__) -class FolderArchiver(UnknownArchiver): +class FolderArchiver(Archiver): """Folder implementation""" - def __init__(self, path: pathlib.Path | str) -> None: - super().__init__(path) + def __init__(self) -> None: + super().__init__() self.comment_file_name = "ComicTaggerFolderComment.txt" def get_comment(self) -> str: @@ -70,7 +70,7 @@ class FolderArchiver(UnknownArchiver): logger.error("Error listing files in folder archive [%s]: %s", e, self.path) return [] - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + def copy_from_archive(self, other_archive: Archiver) -> bool: """Replace the current zip with one copied from another archive""" try: for filename in other_archive.get_filename_list(): @@ -88,3 +88,10 @@ class FolderArchiver(UnknownArchiver): return False else: return True + + def name(self) -> str: + return "Folder" + + @classmethod + def is_valid(cls, path: pathlib.Path | str) -> bool: + return os.path.isdir(path) diff --git a/comicapi/archivers/rar.py b/comicapi/archivers/rar.py index db04b8b..84537ae 100644 --- a/comicapi/archivers/rar.py +++ b/comicapi/archivers/rar.py @@ -9,7 +9,7 @@ import subprocess import tempfile import time -from comicapi.archivers import UnknownArchiver +from comicapi.archivers import Archiver try: from unrar.cffi import rarfile @@ -25,11 +25,13 @@ if not rar_support: logger.error("unrar-cffi unavailable") -class RarArchiver(UnknownArchiver): +class RarArchiver(Archiver): """RAR implementation""" - def __init__(self, path: pathlib.Path | str, rar_exe_path: str = "rar") -> None: - super().__init__(path) + enabled = rar_support + + def __init__(self, rar_exe_path: str = "rar") -> None: + super().__init__() self.rar_exe_path = shutil.which(rar_exe_path) or "" # windows only, keeps the cmd.exe from popping up @@ -199,7 +201,7 @@ class RarArchiver(UnknownArchiver): return namelist return [] - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + def copy_from_archive(self, other_archive: Archiver) -> bool: """Replace the current archive with one copied from another archive""" try: with tempfile.TemporaryDirectory() as tmp_dir: @@ -234,6 +236,21 @@ class RarArchiver(UnknownArchiver): else: return True + def is_writable(self) -> bool: + return bool(self.rar_exe_path and os.path.exists(self.rar_exe_path)) + + def extension(self) -> str: + return ".cbr" + + def name(self) -> str: + return "RAR" + + @classmethod + def is_valid(cls, path: pathlib.Path | str) -> bool: + if rar_support: + return rarfile.is_rarfile(str(path)) + return False + def get_rar_obj(self) -> rarfile.RarFile | None: if rar_support: try: diff --git a/comicapi/archivers/sevenzip.py b/comicapi/archivers/sevenzip.py index 9e0f3f0..a8b574c 100644 --- a/comicapi/archivers/sevenzip.py +++ b/comicapi/archivers/sevenzip.py @@ -6,7 +6,7 @@ import pathlib import shutil import tempfile -from comicapi.archivers import UnknownArchiver +from comicapi.archivers import Archiver try: import py7zr @@ -18,12 +18,13 @@ except ImportError: logger = logging.getLogger(__name__) -class SevenZipArchiver(UnknownArchiver): - +class SevenZipArchiver(Archiver): """7Z implementation""" - def __init__(self, path: pathlib.Path | str) -> None: - super().__init__(path) + enabled = z7_support + + def __init__(self) -> None: + super().__init__() # @todo: Implement Comment? def get_comment(self) -> str: @@ -100,7 +101,7 @@ class SevenZipArchiver(UnknownArchiver): return False return True - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + def copy_from_archive(self, other_archive: Archiver) -> bool: """Replace the current zip with one copied from another archive""" try: with py7zr.SevenZipFile(self.path, "w") as zout: @@ -115,3 +116,16 @@ class SevenZipArchiver(UnknownArchiver): return False else: return True + + def is_writable(self) -> bool: + return True + + def extension(self) -> str: + return ".cb7" + + def name(self) -> str: + return "Seven Zip" + + @classmethod + def is_valid(cls, path: pathlib.Path | str) -> bool: + return py7zr.is_7zfile(path) diff --git a/comicapi/archivers/unknown.py b/comicapi/archivers/unknown.py deleted file mode 100644 index 5f79720..0000000 --- a/comicapi/archivers/unknown.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -import pathlib - - -class UnknownArchiver: - - """Unknown implementation""" - - def __init__(self, path: pathlib.Path | str) -> None: - self.path = pathlib.Path(path) - - def get_comment(self) -> str: - return "" - - def set_comment(self, comment: str) -> bool: - return False - - def read_file(self, archive_file: str) -> bytes: - raise NotImplementedError - - def remove_file(self, archive_file: str) -> bool: - return False - - def write_file(self, archive_file: str, data: bytes) -> bool: - return False - - def get_filename_list(self) -> list[str]: - return [] - - def rebuild(self, exclude_list: list[str]) -> bool: - return False - - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: - return False diff --git a/comicapi/archivers/zip.py b/comicapi/archivers/zip.py index 8d3d976..0ab8941 100644 --- a/comicapi/archivers/zip.py +++ b/comicapi/archivers/zip.py @@ -9,17 +9,17 @@ import tempfile import zipfile from typing import cast -from comicapi.archivers import UnknownArchiver +from comicapi.archivers import Archiver logger = logging.getLogger(__name__) -class ZipArchiver(UnknownArchiver): +class ZipArchiver(Archiver): """ZIP implementation""" - def __init__(self, path: pathlib.Path | str) -> None: - super().__init__(path) + def __init__(self) -> None: + super().__init__() def get_comment(self) -> str: with zipfile.ZipFile(self.path, "r") as zf: @@ -99,7 +99,7 @@ class ZipArchiver(UnknownArchiver): return False return True - def copy_from_archive(self, other_archive: UnknownArchiver) -> bool: + def copy_from_archive(self, other_archive: Archiver) -> bool: """Replace the current zip with one copied from another archive""" try: with zipfile.ZipFile(self.path, mode="w", allowZip64=True) as zout: @@ -119,6 +119,19 @@ class ZipArchiver(UnknownArchiver): else: return True + def is_writable(self) -> bool: + return True + + def extension(self) -> str: + return ".cbz" + + def name(self) -> str: + return "ZIP" + + @classmethod + def is_valid(cls, path: pathlib.Path | str) -> bool: + return zipfile.is_zipfile(path) + def write_zip_comment(self, filename: pathlib.Path | str, comment: str) -> bool: """ This is a custom function for writing a comment to a zip file, diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 1009b5d..05da647 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -19,31 +19,23 @@ import logging import os import pathlib import shutil -import zipfile +import sys from typing import cast import natsort import wordninja from comicapi import filenamelexer, filenameparser, utils -from comicapi.archivers import FolderArchiver, RarArchiver, SevenZipArchiver, UnknownArchiver, ZipArchiver +from comicapi.archivers import Archiver, UnknownArchiver, ZipArchiver from comicapi.comet import CoMet from comicapi.comicbookinfo import ComicBookInfo from comicapi.comicinfoxml import ComicInfoXml from comicapi.genericmetadata import GenericMetadata, PageType -try: - import py7zr - - z7_support = True -except ImportError: - z7_support = False -try: - from unrar.cffi import rarfile - - rar_support = True -except ImportError: - rar_support = False +if sys.version_info < (3, 10): + from importlib_metadata import entry_points +else: + from importlib.metadata import entry_points try: from PIL import Image @@ -52,12 +44,26 @@ try: except ImportError: pil_available = False - logger = logging.getLogger(__name__) if not pil_available: logger.error("PIL unavalable") +archivers: list[type[Archiver]] = [] + + +def load_archive_plugins() -> None: + for arch in entry_points(group="comicapi_archivers"): + try: + archiver: type[Archiver] = arch.load() + if archiver.enabled: + if not arch.module.startswith("comicapi"): + archivers.insert(0, archiver) + else: + archivers.append(archiver) + except Exception: + logger.warning("Failed to load talker: %s", arch.name) + class MetaDataStyle: CBI = 0 @@ -70,9 +76,6 @@ class MetaDataStyle: class ComicArchive: logo_data = b"" - class ArchiveType: - SevenZip, Zip, Rar, Folder, Pdf, Unknown = list(range(6)) - def __init__( self, path: pathlib.Path | str, @@ -96,36 +99,12 @@ class ComicArchive: self.reset_cache() self.default_image_path = default_image_path - # Use file extension to decide which archive test we do first - ext = self.path.suffix + self.archiver: Archiver = UnknownArchiver.open(self.path) - self.archive_type = self.ArchiveType.Unknown - self.archiver = UnknownArchiver(self.path) - - if ext in [".cbr", ".rar"]: - if self.rar_test(): - self.archive_type = self.ArchiveType.Rar - self.archiver = RarArchiver(self.path, rar_exe_path=self.rar_exe_path) - - elif self.zip_test(): - self.archive_type = self.ArchiveType.Zip - self.archiver = ZipArchiver(self.path) - else: - if self.sevenzip_test(): - self.archive_type = self.ArchiveType.SevenZip - self.archiver = SevenZipArchiver(self.path) - - elif self.zip_test(): - self.archive_type = self.ArchiveType.Zip - self.archiver = ZipArchiver(self.path) - - elif self.rar_test(): - self.archive_type = self.ArchiveType.Rar - self.archiver = RarArchiver(self.path, rar_exe_path=self.rar_exe_path) - - elif self.folder_test(): - self.archive_type = self.ArchiveType.Folder - self.archiver = FolderArchiver(self.path) + for archiver in archivers: + if archiver.is_valid(self.path): + self.archiver = archiver.open(self.path) + break if not ComicArchive.logo_data and self.default_image_path: with open(self.default_image_path, mode="rb") as fd: @@ -157,63 +136,33 @@ class ComicArchive: self.path = new_path self.archiver.path = pathlib.Path(path) - def sevenzip_test(self) -> bool: - return z7_support and py7zr.is_7zfile(self.path) - - def zip_test(self) -> bool: - return zipfile.is_zipfile(self.path) - - def rar_test(self) -> bool: - return rar_support and rarfile.is_rarfile(str(self.path)) - - def folder_test(self) -> bool: - return self.path.is_dir() - - def is_sevenzip(self) -> bool: - return self.archive_type == self.ArchiveType.SevenZip - - def is_zip(self) -> bool: - return self.archive_type == self.ArchiveType.Zip - - def is_rar(self) -> bool: - return self.archive_type == self.ArchiveType.Rar - - def is_pdf(self) -> bool: - return self.archive_type == self.ArchiveType.Pdf - - def is_folder(self) -> bool: - return self.archive_type == self.ArchiveType.Folder - - def is_writable(self, check_rar_status: bool = True) -> bool: - if self.archive_type == self.ArchiveType.Unknown: + def is_writable(self, check_archive_status: bool = True) -> bool: + if isinstance(self.archiver, UnknownArchiver): return False - if check_rar_status and self.is_rar() and not self.rar_exe_path: + if check_archive_status and not self.archiver.is_writable(): return False - if not os.access(self.path, os.W_OK): - return False - - if (self.archive_type != self.ArchiveType.Folder) and (not os.access(self.path.parent, os.W_OK)): + if not (os.access(self.path, os.W_OK) or os.access(self.path.parent, os.W_OK)): return False return True def is_writable_for_style(self, data_style: int) -> bool: + return not (data_style == MetaDataStyle.CBI and not self.archiver.supports_comment) - if (self.is_rar() or self.is_sevenzip()) and data_style == MetaDataStyle.CBI: - return False - - return self.is_writable() + def is_zip(self) -> bool: + return self.archiver.name() == "ZIP" def seems_to_be_a_comic_archive(self) -> bool: - if (self.is_zip() or self.is_rar() or self.is_sevenzip() or self.is_folder()) and ( - self.get_number_of_pages() > 0 - ): + if not (isinstance(self.archiver, UnknownArchiver)) and self.get_number_of_pages() > 0: return True return False + def extension(self) -> str: + return self.archiver.extension() + def read_metadata(self, style: int) -> GenericMetadata: if style == MetaDataStyle.CIX: @@ -334,7 +283,6 @@ class ComicArchive: # seems like some archive creators are on Windows, and don't know about case-sensitivity! if sort_list: - files = cast(list[str], natsort.os_sorted(files)) # make a sub-list of image files @@ -653,10 +601,10 @@ class ComicArchive: return metadata - def export_as_zip(self, zip_filename: pathlib.Path | str) -> bool: - if self.archive_type == self.ArchiveType.Zip: + def export_as_zip(self, zip_filename: pathlib.Path) -> bool: + if self.archiver.name() == "ZIP": # nothing to do, we're already a zip return True - zip_archiver = ZipArchiver(zip_filename) + zip_archiver = ZipArchiver.open(zip_filename) return zip_archiver.copy_from_archive(self.archiver) diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 9da4228..d5c9a13 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -217,14 +217,7 @@ class CLI: if self.batch_mode: brief = f"{ca.path}: " - if ca.is_sevenzip(): - brief += "7Z archive " - elif ca.is_zip(): - brief += "ZIP archive " - elif ca.is_rar(): - brief += "RAR archive " - elif ca.is_folder(): - brief += "Folder archive " + brief += ca.archiver.name() + " archive " brief += f"({page_count: >3} pages)" brief += " tags:[ " @@ -460,12 +453,7 @@ class CLI: new_ext = "" # default if self.options.filename_rename_set_extension_based_on_archive: - if ca.is_sevenzip(): - new_ext = ".cb7" - elif ca.is_zip(): - new_ext = ".cbz" - elif ca.is_rar(): - new_ext = ".cbr" + new_ext = ca.extension() renamer = FileRenamer( md, diff --git a/comictaggerlib/ctoptions/cmdline.py b/comictaggerlib/ctoptions/cmdline.py index f0c1c36..464e668 100644 --- a/comictaggerlib/ctoptions/cmdline.py +++ b/comictaggerlib/ctoptions/cmdline.py @@ -18,6 +18,7 @@ from __future__ import annotations import argparse import logging import os +import pathlib import platform import settngs @@ -325,20 +326,23 @@ def validate_commandline_options(options: settngs.Config[settngs.Values], parser else: options[0].runtime_file_list = options[0].runtime_files - # take a crack at finding rar exe, if not set already - if options[0].general_rar_exe_path.strip() in ("", "rar"): + rar_path = pathlib.Path(options[0].general_rar_exe_path) + if rar_path.is_absolute() and rar_path.exists(): + if rar_path.is_dir(): + utils.add_to_path(str(rar_path)) + else: + utils.add_to_path(str(rar_path.parent)) + + # take a crack at finding rar exe if it's not in the path + if not utils.which("rar"): if platform.system() == "Windows": # look in some likely places for Windows machines if os.path.exists(r"C:\Program Files\WinRAR\Rar.exe"): - options[0].general_rar_exe_path = r"C:\Program Files\WinRAR\Rar.exe" + utils.add_to_path(r"C:\Program Files\WinRAR") elif os.path.exists(r"C:\Program Files (x86)\WinRAR\Rar.exe"): - options[0].general_rar_exe_path = r"C:\Program Files (x86)\WinRAR\Rar.exe" + utils.add_to_path(r"C:\Program Files (x86)\WinRAR") else: if os.path.exists("/opt/homebrew/bin"): utils.add_to_path("/opt/homebrew/bin") - # see if it's in the path of unix user - rarpath = utils.which("rar") - if rarpath is not None: - options[0].general_rar_exe_path = "rar" return options diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index e5066a5..8e88f6f 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -193,7 +193,7 @@ class FileSelectionList(QtWidgets.QWidget): QtCore.QCoreApplication.processEvents() first_added = None - rar_added = False + rar_added_ro = False self.twList.setSortingEnabled(False) for idx, f in enumerate(filelist): QtCore.QCoreApplication.processEvents() @@ -206,8 +206,7 @@ class FileSelectionList(QtWidgets.QWidget): row = self.add_path_item(f) if row is not None: ca = self.get_archive_by_row(row) - if ca and ca.is_rar(): - rar_added = True + rar_added_ro = bool(ca and ca.archiver.name() == "RAR" and not ca.archiver.is_writable()) if first_added is None: first_added = row @@ -224,7 +223,7 @@ class FileSelectionList(QtWidgets.QWidget): else: QtWidgets.QMessageBox.information(self, "File/Folder Open", "No readable comic archives were found.") - if rar_added and not utils.which(self.options.general_rar_exe_path or "rar"): + if rar_added_ro: self.rar_ro_message() self.twList.setSortingEnabled(True) @@ -339,14 +338,7 @@ class FileSelectionList(QtWidgets.QWidget): filename_item.setText(item_text) filename_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text) - if fi.ca.is_sevenzip(): - item_text = "7Z" - elif fi.ca.is_zip(): - item_text = "ZIP" - elif fi.ca.is_rar(): - item_text = "RAR" - else: - item_text = "" + item_text = fi.ca.archiver.name() type_item.setText(item_text) type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 7ed6030..7299914 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -160,6 +160,9 @@ class App: f"Failed to load settings, check the log located in '{self.options[0].runtime_config.user_log_dir}' for more details", True, ) + + comicapi.comicarchive.load_archive_plugins() + if self.options[0].runtime_no_gui: if error and error[1]: print(f"A fatal error occurred please check the log for more information: {error[0]}") # noqa: T201 diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index c896d70..a856ef5 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -73,13 +73,8 @@ class RenameWindow(QtWidgets.QDialog): self.renamer.replacements = self.options[0].rename_replacements new_ext = ca.path.suffix # default - if self.options[0].rename_set_extension_based_on_archive: - if ca.is_sevenzip(): - new_ext = ".cb7" - elif ca.is_zip(): - new_ext = ".cbz" - elif ca.is_rar(): - new_ext = ".cbr" + if self.options[0].filename_rename_set_extension_based_on_archive: + new_ext = ca.extension() if md is None: md = ca.read_metadata(self.data_style) @@ -206,7 +201,7 @@ class RenameWindow(QtWidgets.QDialog): logger.info("%s: Filename is already good!", comic[1]) continue - if not comic[0].is_writable(check_rar_status=False): + if not comic[0].is_writable(check_archive_status=False): continue comic[0].rename(utils.unique_file(full_path)) diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index cd46aac..2f56890 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -696,16 +696,7 @@ Have fun! self.lblFilename.setText(filename) - if ca.is_sevenzip(): - self.lblArchiveType.setText("7Z archive") - elif ca.is_zip(): - self.lblArchiveType.setText("ZIP archive") - elif ca.is_rar(): - self.lblArchiveType.setText("RAR archive") - elif ca.is_folder(): - self.lblArchiveType.setText("Folder archive") - else: - self.lblArchiveType.setText("") + self.lblArchiveType.setText(ca.archiver.name() + " archive") page_count = f" ({ca.get_number_of_pages()} pages)" self.lblPageCount.setText(page_count) diff --git a/setup.py b/setup.py index e8d1c3d..014b996 100644 --- a/setup.py +++ b/setup.py @@ -59,12 +59,18 @@ setup( exclude=["tests", "testing"], ), package_data={"comictaggerlib": ["ui/*", "graphics/*"], "comicapi": ["data/*"]}, - entry_points=dict( - console_scripts=["comictagger=comictaggerlib.main:main"], - pyinstaller40=[ + entry_points={ + "console_scripts": ["comictagger=comictaggerlib.main:main"], + "pyinstaller40": [ "hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs", ], - ), + "comicapi.archivers": [ + "zip = comicapi.archivers.zip:ZipArchiver", + "sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver", + "rar = comicapi.archivers.rar:RarArchiver", + "folder = comicapi.archivers.folder:FolderArchiver", + ], + }, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index b675431..2c939fe 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -4,15 +4,17 @@ import platform import shutil import pytest +from importlib_metadata import entry_points import comicapi.comicarchive import comicapi.genericmetadata from testing.filenames import datadir -@pytest.mark.xfail(not comicapi.comicarchive.rar_support, reason="rar support") -def test_getPageNameList(): +@pytest.mark.xfail(not comicapi.archivers.rar.rar_support, reason="rar support") +def test_getPageNameList(load_archive_plugins): c = comicapi.comicarchive.ComicArchive(datadir / "fake_cbr.cbr") + assert c.seems_to_be_a_comic_archive() pageNameList = c.get_page_name_list() assert pageNameList == [ @@ -56,24 +58,26 @@ def test_save_cbi(tmp_comic): md = tmp_comic.read_cbi() -@pytest.mark.xfail(not (comicapi.comicarchive.rar_support and shutil.which("rar")), reason="rar support") +@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support") def test_save_cix_rar(tmp_path): cbr_path = datadir / "fake_cbr.cbr" shutil.copy(cbr_path, tmp_path) tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name) + assert tmp_comic.seems_to_be_a_comic_archive() assert tmp_comic.write_cix(comicapi.genericmetadata.md_test) md = tmp_comic.read_cix() assert md.replace(pages=[]) == comicapi.genericmetadata.md_test.replace(pages=[]) -@pytest.mark.xfail(not (comicapi.comicarchive.rar_support and shutil.which("rar")), reason="rar support") +@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support") def test_save_cbi_rar(tmp_path): cbr_path = datadir / "fake_cbr.cbr" shutil.copy(cbr_path, tmp_path) tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name) + assert tmp_comic.seems_to_be_a_comic_archive() assert tmp_comic.write_cbi(comicapi.genericmetadata.md_test) md = tmp_comic.read_cbi() @@ -118,16 +122,8 @@ def test_invalid_zip(tmp_comic): archivers = [ - comicapi.comicarchive.ZipArchiver, - comicapi.comicarchive.FolderArchiver, - pytest.param( - comicapi.comicarchive.SevenZipArchiver, - marks=pytest.mark.xfail(not (comicapi.comicarchive.z7_support), reason="7z support"), - ), - pytest.param( - comicapi.comicarchive.RarArchiver, - marks=pytest.mark.xfail(not (comicapi.comicarchive.rar_support and shutil.which("rar")), reason="rar support"), - ), + pytest.param(x.load(), marks=pytest.mark.xfail(not (x.load().enabled), reason="archiver not enabled")) + for x in entry_points(group="comicapi_archivers") ] @@ -135,7 +131,7 @@ archivers = [ def test_copy_from_archive(archiver, tmp_path, cbz): comic_path = tmp_path / cbz.path.with_suffix("").name - archive = archiver(comic_path) + archive = archiver.open(comic_path) assert archive.copy_from_archive(cbz.archiver) diff --git a/tests/conftest.py b/tests/conftest.py index 7b20081..89fd3d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,12 +23,17 @@ from testing.comicdata import all_seed_imprints, seed_imprints @pytest.fixture -def cbz(): +def cbz(load_archive_plugins): yield comicapi.comicarchive.ComicArchive(filenames.cbz_path) @pytest.fixture -def tmp_comic(tmp_path): +def load_archive_plugins(): + comicapi.comicarchive.load_archive_plugins() + + +@pytest.fixture +def tmp_comic(tmp_path, load_archive_plugins): shutil.copy(filenames.cbz_path, tmp_path) yield comicapi.comicarchive.ComicArchive(tmp_path / filenames.cbz_path.name) From 50614d52fcb1ee72f286236794662d5788ca460a Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 12 Jan 2023 15:37:27 -0800 Subject: [PATCH 06/18] Update PyInstaller hook --- comicapi/__pyinstaller/hook-comicapi.py | 4 ++-- comicapi/comicarchive.py | 21 +++++++++++---------- setup.py | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/comicapi/__pyinstaller/hook-comicapi.py b/comicapi/__pyinstaller/hook-comicapi.py index 0be9c56..6101d69 100644 --- a/comicapi/__pyinstaller/hook-comicapi.py +++ b/comicapi/__pyinstaller/hook-comicapi.py @@ -1,6 +1,6 @@ from __future__ import annotations -from PyInstaller.utils.hooks import collect_data_files +from PyInstaller.utils.hooks import collect_data_files, collect_entry_point -datas = [] +datas, hiddenimports = collect_entry_point("comicapi.archiver") datas += collect_data_files("comicapi.data") diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 05da647..18e0eb4 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -53,16 +53,17 @@ archivers: list[type[Archiver]] = [] def load_archive_plugins() -> None: - for arch in entry_points(group="comicapi_archivers"): - try: - archiver: type[Archiver] = arch.load() - if archiver.enabled: - if not arch.module.startswith("comicapi"): - archivers.insert(0, archiver) - else: - archivers.append(archiver) - except Exception: - logger.warning("Failed to load talker: %s", arch.name) + if not archivers: + for arch in entry_points(group="comicapi.archiver"): + try: + archiver: type[Archiver] = arch.load() + if archiver.enabled: + if not arch.module.startswith("comicapi"): + archivers.insert(0, archiver) + else: + archivers.append(archiver) + except Exception: + logger.warning("Failed to load talker: %s", arch.name) class MetaDataStyle: diff --git a/setup.py b/setup.py index 014b996..a6f8c0f 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ setup( "pyinstaller40": [ "hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs", ], - "comicapi.archivers": [ + "comicapi.archiver": [ "zip = comicapi.archivers.zip:ZipArchiver", "sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver", "rar = comicapi.archivers.rar:RarArchiver", From f6698f7f0a8f83c2692905128c837e427856b3f6 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 12 Jan 2023 17:00:11 -0800 Subject: [PATCH 07/18] Call load_archive_plugins in ComicArchive __init__ --- comicapi/comicarchive.py | 1 + comictaggerlib/main.py | 2 -- tests/comicarchive_test.py | 2 +- tests/conftest.py | 9 ++------- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 18e0eb4..b0c2be2 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -102,6 +102,7 @@ class ComicArchive: self.archiver: Archiver = UnknownArchiver.open(self.path) + load_archive_plugins() for archiver in archivers: if archiver.is_valid(self.path): self.archiver = archiver.open(self.path) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 7299914..e0dfb14 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -161,8 +161,6 @@ class App: True, ) - comicapi.comicarchive.load_archive_plugins() - if self.options[0].runtime_no_gui: if error and error[1]: print(f"A fatal error occurred please check the log for more information: {error[0]}") # noqa: T201 diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index 2c939fe..e0949aa 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -12,7 +12,7 @@ from testing.filenames import datadir @pytest.mark.xfail(not comicapi.archivers.rar.rar_support, reason="rar support") -def test_getPageNameList(load_archive_plugins): +def test_getPageNameList(): c = comicapi.comicarchive.ComicArchive(datadir / "fake_cbr.cbr") assert c.seems_to_be_a_comic_archive() pageNameList = c.get_page_name_list() diff --git a/tests/conftest.py b/tests/conftest.py index 89fd3d7..7b20081 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,17 +23,12 @@ from testing.comicdata import all_seed_imprints, seed_imprints @pytest.fixture -def cbz(load_archive_plugins): +def cbz(): yield comicapi.comicarchive.ComicArchive(filenames.cbz_path) @pytest.fixture -def load_archive_plugins(): - comicapi.comicarchive.load_archive_plugins() - - -@pytest.fixture -def tmp_comic(tmp_path, load_archive_plugins): +def tmp_comic(tmp_path): shutil.copy(filenames.cbz_path, tmp_path) yield comicapi.comicarchive.ComicArchive(tmp_path / filenames.cbz_path.name) From 55e3b7c7e0f5f9fb1213bb5ac5039de5eca799d1 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Fri, 13 Jan 2023 21:27:40 +0000 Subject: [PATCH 08/18] Use name for URL display. Window sizes. --- comictaggerlib/seriesselectionwindow.py | 8 +++---- comictaggerlib/ui/seriesselectionwindow.ui | 25 ++++++++++++++------- comictalker/talkers/logos/comicvine.png | Bin 17414 -> 0 bytes 3 files changed, 21 insertions(+), 12 deletions(-) delete mode 100644 comictalker/talkers/logos/comicvine.png diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 215c037..7fc512d 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -163,19 +163,19 @@ class SeriesSelectionWindow(QtWidgets.QDialog): # Display talker logo and set url self.lblSourceName.setText( - f'Data Source: {talker_api.static_options.website}' + f'Data Source: {talker_api.source_details.name}' ) self.imageSourceWidget = CoverImageWidget( - self.lblSourceLogo, + self.imageSourceLogo, CoverImageWidget.URLMode, options.runtime_config.user_cache_dir, talker_api, False, ) - gridlayoutSourceLogo = QtWidgets.QGridLayout(self.lblSourceLogo) + gridlayoutSourceLogo = QtWidgets.QGridLayout(self.imageSourceLogo) gridlayoutSourceLogo.addWidget(self.imageSourceWidget) - gridlayoutSourceLogo.setContentsMargins(0, 0, 0, 0) + gridlayoutSourceLogo.setContentsMargins(0, 2, 0, 0) self.imageSourceWidget.set_url(talker_api.source_details.logo) # Set the minimum row height to the default. diff --git a/comictaggerlib/ui/seriesselectionwindow.ui b/comictaggerlib/ui/seriesselectionwindow.ui index 967768f..2cb570e 100644 --- a/comictaggerlib/ui/seriesselectionwindow.ui +++ b/comictaggerlib/ui/seriesselectionwindow.ui @@ -67,13 +67,28 @@
+ + + 0 + 0 + + + + + 300 + 16777215 + + Data Source: + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + - + 300 @@ -83,15 +98,9 @@ 300 - 100 + 16777215 - - 0 - - - -
diff --git a/comictalker/talkers/logos/comicvine.png b/comictalker/talkers/logos/comicvine.png deleted file mode 100644 index 9e3bb89ad0b447a2d6091bfe80cff89d7e39fb17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17414 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 JPDHLkV1hfU6hr_3 From 5d66815765ca5496590beb723040aca63bf2fb85 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Fri, 20 Jan 2023 00:29:02 +0000 Subject: [PATCH 09/18] Add attrib string for source. Add logo and URL to issues window. --- comictaggerlib/issueselectionwindow.py | 17 +++++ comictaggerlib/seriesselectionwindow.py | 2 +- comictaggerlib/ui/issueselectionwindow.ui | 88 ++++++++++++++++++----- comictalker/talkerbase.py | 2 + comictalker/talkers/comicvine.py | 1 + 5 files changed, 91 insertions(+), 19 deletions(-) diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index fc48fc2..0f04800 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -76,6 +76,23 @@ class IssueSelectionWindow(QtWidgets.QDialog): self.url_fetch_thread = None self.issue_list: list[ComicIssue] = [] + # Display talker logo and set url + self.lblIssuesSourceName.setText( + f'{talker_api.static_options.attribution_string} {talker_api.source_details.name}' + ) + + self.imageIssuesSourceWidget = CoverImageWidget( + self.imageIssuesSourceLogo, + CoverImageWidget.URLMode, + options.runtime_config.user_cache_dir, + talker_api, + False, + ) + gridlayoutIssuesSourceLogo = QtWidgets.QGridLayout(self.imageIssuesSourceLogo) + gridlayoutIssuesSourceLogo.addWidget(self.imageIssuesSourceWidget) + gridlayoutIssuesSourceLogo.setContentsMargins(0, 2, 0, 0) + self.imageIssuesSourceWidget.set_url(talker_api.source_details.logo) + if issue_number is None or issue_number == "": self.issue_number = "1" else: diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 7fc512d..05bcc84 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -163,7 +163,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): # Display talker logo and set url self.lblSourceName.setText( - f'Data Source: {talker_api.source_details.name}' + f'{talker_api.static_options.attribution_string} {talker_api.source_details.name}' ) self.imageSourceWidget = CoverImageWidget( diff --git a/comictaggerlib/ui/issueselectionwindow.ui b/comictaggerlib/ui/issueselectionwindow.ui index 8e3845c..2362f9c 100644 --- a/comictaggerlib/ui/issueselectionwindow.ui +++ b/comictaggerlib/ui/issueselectionwindow.ui @@ -7,7 +7,7 @@ 0 0 872 - 550 + 670 @@ -74,9 +74,6 @@ Title - - AlignCenter - @@ -93,20 +90,75 @@ - - - - 300 - 450 - - - - - 300 - 450 - - - + + + + + + 300 + 450 + + + + + 300 + 450 + + + + + + + + + 0 + 0 + + + + 2 + + + 1 + + + Qt::Horizontal + + + + + + + + 300 + 16777215 + + + + Data Source: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + 300 + 100 + + + + + 300 + 16777215 + + + + + diff --git a/comictalker/talkerbase.py b/comictalker/talkerbase.py index 7e87304..fcd7be6 100644 --- a/comictalker/talkerbase.py +++ b/comictalker/talkerbase.py @@ -40,6 +40,7 @@ class SourceStaticOptions: def __init__( self, website: str = "", + attribution_string: str = "", # Website link will be added after this text using source_details.name has_issues: bool = False, has_alt_covers: bool = False, requires_apikey: bool = False, @@ -47,6 +48,7 @@ class SourceStaticOptions: has_censored_covers: bool = False, ) -> None: self.website = website + self.attribution_string = attribution_string self.has_issues = has_issues self.has_alt_covers = has_alt_covers self.requires_apikey = requires_apikey diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 46be864..f10e7c9 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -174,6 +174,7 @@ class ComicVineTalker(ComicTalker): ) self.static_options = SourceStaticOptions( website="https://comicvine.gamespot.com/", + attribution_string="Data Source:", has_issues=True, has_alt_covers=True, requires_apikey=True, From 2de241cdd5653633897548bc5a0a3e50f3d05ce5 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Fri, 20 Jan 2023 19:32:06 -0800 Subject: [PATCH 10/18] Fix typing --- comicapi/archivers/folder.py | 4 ++-- comicapi/archivers/rar.py | 2 +- comicapi/archivers/sevenzip.py | 2 +- comicapi/archivers/zip.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/comicapi/archivers/folder.py b/comicapi/archivers/folder.py index c0292dc..ec94a3a 100644 --- a/comicapi/archivers/folder.py +++ b/comicapi/archivers/folder.py @@ -93,5 +93,5 @@ class FolderArchiver(Archiver): return "Folder" @classmethod - def is_valid(cls, path: pathlib.Path | str) -> bool: - return os.path.isdir(path) + def is_valid(cls, path: pathlib.Path) -> bool: + return path.is_dir() diff --git a/comicapi/archivers/rar.py b/comicapi/archivers/rar.py index 84537ae..96cbff9 100644 --- a/comicapi/archivers/rar.py +++ b/comicapi/archivers/rar.py @@ -246,7 +246,7 @@ class RarArchiver(Archiver): return "RAR" @classmethod - def is_valid(cls, path: pathlib.Path | str) -> bool: + def is_valid(cls, path: pathlib.Path) -> bool: if rar_support: return rarfile.is_rarfile(str(path)) return False diff --git a/comicapi/archivers/sevenzip.py b/comicapi/archivers/sevenzip.py index a8b574c..be6e059 100644 --- a/comicapi/archivers/sevenzip.py +++ b/comicapi/archivers/sevenzip.py @@ -127,5 +127,5 @@ class SevenZipArchiver(Archiver): return "Seven Zip" @classmethod - def is_valid(cls, path: pathlib.Path | str) -> bool: + def is_valid(cls, path: pathlib.Path) -> bool: return py7zr.is_7zfile(path) diff --git a/comicapi/archivers/zip.py b/comicapi/archivers/zip.py index 0ab8941..afc7ffa 100644 --- a/comicapi/archivers/zip.py +++ b/comicapi/archivers/zip.py @@ -129,7 +129,7 @@ class ZipArchiver(Archiver): return "ZIP" @classmethod - def is_valid(cls, path: pathlib.Path | str) -> bool: + def is_valid(cls, path: pathlib.Path) -> bool: return zipfile.is_zipfile(path) def write_zip_comment(self, filename: pathlib.Path | str, comment: str) -> bool: From ad48ad757c95733467eaf7906b63cdb619c87938 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Fri, 20 Jan 2023 19:32:32 -0800 Subject: [PATCH 11/18] Fix plugin order --- comicapi/comicarchive.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index b0c2be2..6e88d3f 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -54,16 +54,18 @@ archivers: list[type[Archiver]] = [] def load_archive_plugins() -> None: if not archivers: + builtin: list[type[Archiver]] = [] for arch in entry_points(group="comicapi.archiver"): try: archiver: type[Archiver] = arch.load() if archiver.enabled: - if not arch.module.startswith("comicapi"): - archivers.insert(0, archiver) + if arch.module.startswith("comicapi"): + builtin.append(archiver) else: archivers.append(archiver) except Exception: logger.warning("Failed to load talker: %s", arch.name) + archivers.extend(builtin) class MetaDataStyle: From 92eb79df71e21862e3e977d2bf74e6ebc32b2e00 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sat, 21 Jan 2023 00:27:39 -0800 Subject: [PATCH 12/18] Fix console_scripts entry point --- comictaggerlib/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 7ed6030..0809d32 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -170,3 +170,7 @@ class App: logger.exception("CLI mode failed") else: gui.open_tagger_window(talker_api, self.options, error) + + +def main(): + App().run() From c80627575a57bbdffa0202cb6db7a732bd0f38d5 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sat, 21 Jan 2023 15:24:27 -0800 Subject: [PATCH 13/18] Add docstrings to Archiver --- comicapi/archivers/archiver.py | 65 ++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/comicapi/archivers/archiver.py b/comicapi/archivers/archiver.py index dd11c1b..6ed8d25 100644 --- a/comicapi/archivers/archiver.py +++ b/comicapi/archivers/archiver.py @@ -9,54 +9,113 @@ class Archiver(Protocol): """Archiver Protocol""" + """The path to the archive""" path: pathlib.Path + + """ + Whether or not this archiver is enabled. + If external imports are required and are not available this should be false. See rar.py and sevenzip.py. + """ enabled: bool = True def __init__(self): self.path = pathlib.Path() def get_comment(self) -> str: + """ + Returns the comment from the archive as a string. + Should always return a string. If comments are not supported in the archive the empty string should be returned. + """ return "" def set_comment(self, comment: str) -> bool: + """ + Returns True if the comment was successfully set on the archive. + Should always return a boolean. If comments are not supported in the archive False should be returned. + """ return False def supports_comment(self) -> bool: + """ + Returns True if the archive supports comments. + Should always return a boolean. If comments are not supported in the archive False should be returned. + """ return False def read_file(self, archive_file: str) -> bytes: + """ + Reads the named file from the archive. + archive_file should always come from the output of get_filename_list. + Should always return a bytes object. Exceptions should be of the type OSError. + """ raise NotImplementedError def remove_file(self, archive_file: str) -> bool: + """ + Removes the named file from the archive. + archive_file should always come from the output of get_filename_list. + Should always return a boolean. Failures should return False. + + Rebuilding the archive without the named file is a standard way to remove a file. + """ return False def write_file(self, archive_file: str, data: bytes) -> bool: + """ + Writes the named file to the archive. + Should always return a boolean. Failures should return False. + """ return False def get_filename_list(self) -> list[str]: + """ + Returns a list of filenames in the archive. + Should always return a list of string. Failures should return an empty list. + """ return [] - def rebuild(self, exclude_list: list[str]) -> bool: - return False - def copy_from_archive(self, other_archive: Archiver) -> bool: + """ + Copies the contents of another achive to this archive. + Should always return a boolean. Failures should return False. + """ return False def is_writable(self) -> bool: + """ + Retuns True if the archive is writeable + Should always return a boolean. Failures should return False. + """ return False def extension(self) -> str: + """ + Returns the extension that this archive should use eg ".cbz". + Should always return a string. Failures should return the empty string. + """ return "" def name(self) -> str: + """ + Returns the name of this archive for display purposes eg "CBZ". + Should always return a string. Failures should return the empty string. + """ return "" @classmethod def is_valid(cls, path: pathlib.Path) -> bool: + """ + Returns True if the given path can be opened by this archive. + Should always return a boolean. Failures should return False. + """ return False @classmethod def open(cls, path: pathlib.Path) -> Archiver: + """ + Opens the given archive. + Should always return a an Archver. Should never cause an exception, is_valid will always be called before open. + """ archiver = cls() archiver.path = path return archiver From d0e3b487ebd42fd16f81427b5605d7389591c0e8 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Sun, 22 Jan 2023 17:16:33 +0000 Subject: [PATCH 14/18] Mark label for external links. attrib str to be complete. --- comictaggerlib/issueselectionwindow.py | 4 +--- comictaggerlib/seriesselectionwindow.py | 4 +--- comictaggerlib/ui/issueselectionwindow.ui | 3 +++ comictaggerlib/ui/seriesselectionwindow.ui | 3 +++ comictalker/talkerbase.py | 2 +- comictalker/talkers/comicvine.py | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index 0f04800..12cc245 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -77,9 +77,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): self.issue_list: list[ComicIssue] = [] # Display talker logo and set url - self.lblIssuesSourceName.setText( - f'{talker_api.static_options.attribution_string} {talker_api.source_details.name}' - ) + self.lblIssuesSourceName.setText(talker_api.static_options.attribution_string) self.imageIssuesSourceWidget = CoverImageWidget( self.imageIssuesSourceLogo, diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 05bcc84..0401930 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -162,9 +162,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.talker_api = talker_api # Display talker logo and set url - self.lblSourceName.setText( - f'{talker_api.static_options.attribution_string} {talker_api.source_details.name}' - ) + self.lblSourceName.setText(talker_api.static_options.attribution_string) self.imageSourceWidget = CoverImageWidget( self.imageSourceLogo, diff --git a/comictaggerlib/ui/issueselectionwindow.ui b/comictaggerlib/ui/issueselectionwindow.ui index 2362f9c..4d23de9 100644 --- a/comictaggerlib/ui/issueselectionwindow.ui +++ b/comictaggerlib/ui/issueselectionwindow.ui @@ -140,6 +140,9 @@ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + true + diff --git a/comictaggerlib/ui/seriesselectionwindow.ui b/comictaggerlib/ui/seriesselectionwindow.ui index 2cb570e..ceafc65 100644 --- a/comictaggerlib/ui/seriesselectionwindow.ui +++ b/comictaggerlib/ui/seriesselectionwindow.ui @@ -85,6 +85,9 @@ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + true + diff --git a/comictalker/talkerbase.py b/comictalker/talkerbase.py index fcd7be6..9c70c7c 100644 --- a/comictalker/talkerbase.py +++ b/comictalker/talkerbase.py @@ -40,7 +40,7 @@ class SourceStaticOptions: def __init__( self, website: str = "", - attribution_string: str = "", # Website link will be added after this text using source_details.name + attribution_string: str = "", # Full string including web link: Source: Example has_issues: bool = False, has_alt_covers: bool = False, requires_apikey: bool = False, diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index f10e7c9..54aa372 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -174,7 +174,7 @@ class ComicVineTalker(ComicTalker): ) self.static_options = SourceStaticOptions( website="https://comicvine.gamespot.com/", - attribution_string="Data Source:", + attribution_string="Data Source: Comic Vine", has_issues=True, has_alt_covers=True, requires_apikey=True, From 46899255c89cfa7b16903850a461547a77f05ead Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 30 Jan 2023 21:36:47 -0800 Subject: [PATCH 15/18] Generate settings for an archivers executable --- comicapi/archivers/archiver.py | 11 ++++++- comicapi/archivers/rar.py | 20 ++++++------- comictaggerlib/ctoptions/__init__.py | 3 ++ comictaggerlib/ctoptions/file.py | 2 +- comictaggerlib/ctoptions/plugin.py | 43 ++++++++++++++++++++++++++++ comictaggerlib/main.py | 8 +++++- 6 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 comictaggerlib/ctoptions/plugin.py diff --git a/comicapi/archivers/archiver.py b/comicapi/archivers/archiver.py index 6ed8d25..4d7efb7 100644 --- a/comicapi/archivers/archiver.py +++ b/comicapi/archivers/archiver.py @@ -12,6 +12,13 @@ class Archiver(Protocol): """The path to the archive""" path: pathlib.Path + """ + The name of the executable used for this archiver. This should be the base name of the executable. + For example if 'rar.exe' is needed this should be "rar". + If an executable is not used this should be the empty string. + """ + exe: str = "" + """ Whether or not this archiver is enabled. If external imports are required and are not available this should be false. See rar.py and sevenzip.py. @@ -114,7 +121,9 @@ class Archiver(Protocol): def open(cls, path: pathlib.Path) -> Archiver: """ Opens the given archive. - Should always return a an Archver. Should never cause an exception, is_valid will always be called before open. + Should always return a an Archver. + Should never cause an exception no file operations should take place in this method, + is_valid will always be called before open. """ archiver = cls() archiver.path = path diff --git a/comicapi/archivers/rar.py b/comicapi/archivers/rar.py index 96cbff9..fd3a61f 100644 --- a/comicapi/archivers/rar.py +++ b/comicapi/archivers/rar.py @@ -29,10 +29,10 @@ class RarArchiver(Archiver): """RAR implementation""" enabled = rar_support + exe = "rar" - def __init__(self, rar_exe_path: str = "rar") -> None: + def __init__(self) -> None: super().__init__() - self.rar_exe_path = shutil.which(rar_exe_path) or "" # windows only, keeps the cmd.exe from popping up if platform.system() == "Windows": @@ -46,7 +46,7 @@ class RarArchiver(Archiver): return rarc.comment.decode("utf-8") if rarc else "" def set_comment(self, comment: str) -> bool: - if rar_support and self.rar_exe_path: + if rar_support and self.exe: try: # write comment to temp file with tempfile.TemporaryDirectory() as tmp_dir: @@ -57,7 +57,7 @@ class RarArchiver(Archiver): # use external program to write comment to Rar archive proc_args = [ - self.rar_exe_path, + self.exe, "c", f"-w{working_dir}", "-c-", @@ -130,10 +130,10 @@ class RarArchiver(Archiver): raise OSError def remove_file(self, archive_file: str) -> bool: - if self.rar_exe_path: + if self.exe: # use external program to remove file from Rar archive result = subprocess.run( - [self.rar_exe_path, "d", "-c-", self.path, archive_file], + [self.exe, "d", "-c-", self.path, archive_file], startupinfo=self.startupinfo, stdout=subprocess.DEVNULL, stdin=subprocess.DEVNULL, @@ -155,14 +155,14 @@ class RarArchiver(Archiver): return False def write_file(self, archive_file: str, data: bytes) -> bool: - if self.rar_exe_path: + if self.exe: archive_path = pathlib.PurePosixPath(archive_file) archive_name = archive_path.name archive_parent = str(archive_path.parent).lstrip("./") # use external program to write file to Rar archive result = subprocess.run( - [self.rar_exe_path, "a", f"-si{archive_name}", f"-ap{archive_parent}", "-c-", "-ep", self.path], + [self.exe, "a", f"-si{archive_name}", f"-ap{archive_parent}", "-c-", "-ep", self.path], input=data, startupinfo=self.startupinfo, stdout=subprocess.DEVNULL, @@ -217,7 +217,7 @@ class RarArchiver(Archiver): with open(rar_cwd / filename, mode="w+b") as tmp_file: tmp_file.write(data) result = subprocess.run( - [self.rar_exe_path, "a", "-r", "-c-", str(rar_path.absolute()), "."], + [self.exe, "a", "-r", "-c-", str(rar_path.absolute()), "."], cwd=rar_cwd.absolute(), startupinfo=self.startupinfo, stdout=subprocess.DEVNULL, @@ -237,7 +237,7 @@ class RarArchiver(Archiver): return True def is_writable(self) -> bool: - return bool(self.rar_exe_path and os.path.exists(self.rar_exe_path)) + return bool(self.exe and (os.path.exists(self.exe) or shutil.which(self.exe))) def extension(self) -> str: return ".cbr" diff --git a/comictaggerlib/ctoptions/__init__.py b/comictaggerlib/ctoptions/__init__.py index fd1f386..2099a78 100644 --- a/comictaggerlib/ctoptions/__init__.py +++ b/comictaggerlib/ctoptions/__init__.py @@ -2,13 +2,16 @@ from __future__ import annotations from comictaggerlib.ctoptions.cmdline import initial_cmd_line_parser, register_commandline, validate_commandline_options from comictaggerlib.ctoptions.file import register_settings, validate_settings +from comictaggerlib.ctoptions.plugin import register_plugin_settings, validate_plugin_settings from comictaggerlib.ctoptions.types import ComicTaggerPaths __all__ = [ "initial_cmd_line_parser", "register_commandline", "register_settings", + "register_plugin_settings", "validate_commandline_options", "validate_settings", + "validate_plugin_settings", "ComicTaggerPaths", ] diff --git a/comictaggerlib/ctoptions/file.py b/comictaggerlib/ctoptions/file.py index 0d6e4f7..3c6285a 100644 --- a/comictaggerlib/ctoptions/file.py +++ b/comictaggerlib/ctoptions/file.py @@ -248,7 +248,7 @@ def autotag(parser: settngs.Manager) -> None: ) -def validate_settings(options: settngs.Config[settngs.Values], parser: settngs.Manager) -> dict[str, dict[str, Any]]: +def validate_settings(options: settngs.Config[settngs.Values]) -> dict[str, dict[str, Any]]: options[0].identifier_publisher_filter = [x.strip() for x in options[0].identifier_publisher_filter if x.strip()] options[0].rename_replacements = Replacements( [Replacement(x[0], x[1], x[2]) for x in options[0].rename_replacements[0]], diff --git a/comictaggerlib/ctoptions/plugin.py b/comictaggerlib/ctoptions/plugin.py new file mode 100644 index 0000000..818a849 --- /dev/null +++ b/comictaggerlib/ctoptions/plugin.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import logging +import os + +import settngs + +import comicapi.comicarchive + +logger = logging.getLogger("comictagger") + + +def archiver(manager: settngs.Manager) -> None: + exe_registered: set[str] = set() + for archiver in comicapi.comicarchive.archivers: + if archiver.exe and archiver.exe not in exe_registered: + manager.add_setting( + f"--{archiver.exe.replace(' ', '-').replace('_', '-').strip().strip('-')}", + default=archiver.exe, + help="Path to the %(default)s executable\n\n", + ) + exe_registered.add(archiver.exe) + + +def validate_plugin_settings(options: settngs.Config) -> settngs.Config: + cfg = settngs.normalize_config(options, file=True, cmdline=True, defaults=False) + for archiver in comicapi.comicarchive.archivers: + exe_name = archiver.exe.replace(" ", "-").replace("_", "-").strip().strip("-").replace("-", "_") + if ( + exe_name in cfg[0]["archiver"] + and cfg[0]["archiver"][exe_name] + and cfg[0]["archiver"][exe_name] != archiver.exe + ): + if os.path.basename(cfg[0]["archiver"][exe_name]) == archiver.exe: + comicapi.utils.add_to_path(os.path.dirname(cfg[0]["archiver"][exe_name])) + else: + archiver.exe = cfg[0]["archiver"][exe_name] + + return options + + +def register_plugin_settings(manager: settngs.Manager): + manager.add_group("archiver", archiver, False) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index e0dfb14..df1a3e4 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -68,12 +68,16 @@ class App: def run(self) -> None: opts = self.initialize() + self.load_plugins() self.register_options() self.parse_options(opts.config) self.initialize_dirs() self.main() + def load_plugins(self) -> None: + comicapi.comicarchive.load_archive_plugins() + def initialize(self) -> argparse.Namespace: opts, _ = self.initial_arg_parser.parse_known_args() assert opts is not None @@ -87,6 +91,7 @@ class App: ) ctoptions.register_commandline(self.manager) ctoptions.register_settings(self.manager) + ctoptions.register_plugin_settings(self.manager) def parse_options(self, config_paths: ctoptions.ComicTaggerPaths) -> None: self.options, self.config_load_success = self.manager.parse_config( @@ -95,7 +100,8 @@ class App: self.options = self.manager.get_namespace(self.options) self.options = ctoptions.validate_commandline_options(self.options, self.manager) - self.options = ctoptions.validate_settings(self.options, self.manager) + self.options = ctoptions.validate_settings(self.options) + self.options = ctoptions.validate_plugin_settings(self.options) self.options = self.options def initialize_dirs(self) -> None: From 2c5d419ee9534212bbbe17e98e6e92d859ebff55 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 31 Jan 2023 00:01:50 -0800 Subject: [PATCH 16/18] Remove legacy rar settings --- comicapi/comicarchive.py | 2 -- comictaggerlib/cli.py | 2 +- comictaggerlib/ctoptions/cmdline.py | 8 -------- comictaggerlib/ctoptions/file.py | 8 -------- comictaggerlib/ctoptions/plugin.py | 2 ++ comictaggerlib/fileselectionlist.py | 2 +- comictaggerlib/settingswindow.py | 14 +++++++++----- 7 files changed, 13 insertions(+), 25 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 6e88d3f..b10eaf9 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -82,7 +82,6 @@ class ComicArchive: def __init__( self, path: pathlib.Path | str, - rar_exe_path: str = "rar", default_image_path: pathlib.Path | str | None = None, ) -> None: self.cbi_md: GenericMetadata | None = None @@ -96,7 +95,6 @@ class ComicArchive: self.page_count: int | None = None self.page_list: list[str] = [] - self.rar_exe_path = shutil.which(rar_exe_path or "rar") or "" self.ci_xml_filename = "ComicInfo.xml" self.comet_default_filename = "CoMet.xml" self.reset_cache() diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index d5c9a13..0edf278 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -560,7 +560,7 @@ class CLI: logger.error("Cannot find %s", filename) return - ca = ComicArchive(filename, self.options.general_rar_exe_path, str(graphics_path / "nocover.png")) + ca = ComicArchive(filename, str(graphics_path / "nocover.png")) if not ca.seems_to_be_a_comic_archive(): logger.error("Sorry, but %s is not a comic archive!", filename) diff --git a/comictaggerlib/ctoptions/cmdline.py b/comictaggerlib/ctoptions/cmdline.py index 464e668..4d9d413 100644 --- a/comictaggerlib/ctoptions/cmdline.py +++ b/comictaggerlib/ctoptions/cmdline.py @@ -18,7 +18,6 @@ from __future__ import annotations import argparse import logging import os -import pathlib import platform import settngs @@ -326,13 +325,6 @@ def validate_commandline_options(options: settngs.Config[settngs.Values], parser else: options[0].runtime_file_list = options[0].runtime_files - rar_path = pathlib.Path(options[0].general_rar_exe_path) - if rar_path.is_absolute() and rar_path.exists(): - if rar_path.is_dir(): - utils.add_to_path(str(rar_path)) - else: - utils.add_to_path(str(rar_path.parent)) - # 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/ctoptions/file.py b/comictaggerlib/ctoptions/file.py index 3c6285a..8ec54ca 100644 --- a/comictaggerlib/ctoptions/file.py +++ b/comictaggerlib/ctoptions/file.py @@ -12,13 +12,6 @@ from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replaceme def general(parser: settngs.Manager) -> None: # General Settings - parser.add_setting("--rar-exe-path", default="rar", help="The path to the rar program") - parser.add_setting( - "--allow-cbi-in-rar", - default=True, - action=argparse.BooleanOptionalAction, - help="Allows ComicBookLover tags in RAR/CBR files", - ) parser.add_setting("check_for_new_version", default=False, cmdline=False) parser.add_setting("send_usage_stats", default=False, cmdline=False) @@ -59,7 +52,6 @@ def identifier(parser: settngs.Manager) -> None: def dialog(parser: settngs.Manager) -> None: # Show/ask dialog flags - parser.add_setting("ask_about_cbi_in_rar", default=True, cmdline=False) parser.add_setting("show_disclaimer", default=True, cmdline=False) parser.add_setting("dont_notify_about_this_version", default="", cmdline=False) parser.add_setting("ask_about_usage_stats", default=True, cmdline=False) diff --git a/comictaggerlib/ctoptions/plugin.py b/comictaggerlib/ctoptions/plugin.py index 818a849..cc48b64 100644 --- a/comictaggerlib/ctoptions/plugin.py +++ b/comictaggerlib/ctoptions/plugin.py @@ -23,6 +23,8 @@ def archiver(manager: settngs.Manager) -> None: def validate_plugin_settings(options: settngs.Config) -> settngs.Config: + if "archiver" not in options[1]: + return options cfg = settngs.normalize_config(options, file=True, cmdline=True, defaults=False) for archiver in comicapi.comicarchive.archivers: exe_name = archiver.exe.replace(" ", "-").replace("_", "-").strip().strip("-").replace("-", "_") diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index 8e88f6f..db2abab 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -277,7 +277,7 @@ class FileSelectionList(QtWidgets.QWidget): if self.is_list_dupe(path): return self.get_current_list_row(path) - ca = ComicArchive(path, self.options.general_rar_exe_path, str(graphics_path / "nocover.png")) + ca = ComicArchive(path, str(graphics_path / "nocover.png")) if ca.seems_to_be_a_comic_archive(): row: int = self.twList.rowCount() diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 74c9043..bf0c9a0 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -269,7 +269,10 @@ class SettingsWindow(QtWidgets.QDialog): def settings_to_form(self) -> None: # Copy values from settings to form - self.leRarExePath.setText(self.options[0].general_rar_exe_path) + if "archiver" in self.options[1] and "rar" in self.options[1]["archiver"]: + self.leRarExePath.setText(getattr(self.options[0], self.options[1]["archiver"]["rar"].internal_name)) + else: + self.leRarExePath.setEnabled(False) self.sbNameMatchIdentifyThresh.setValue(self.options[0].identifier_series_match_identify_thresh) self.sbNameMatchSearchThresh.setValue(self.options[0].comicvine_series_match_search_thresh) self.tePublisherFilter.setPlainText("\n".join(self.options[0].identifier_publisher_filter)) @@ -374,11 +377,12 @@ class SettingsWindow(QtWidgets.QDialog): ) # Copy values from form to settings and save - self.options[0].general_rar_exe_path = str(self.leRarExePath.text()) + if "archiver" in self.options[1] and "rar" in self.options[1]["archiver"]: + setattr(self.options[0], self.options[1]["archiver"]["rar"].internal_name, str(self.leRarExePath.text())) - # make sure rar program is now in the path for the rar class - if self.options[0].general_rar_exe_path: - utils.add_to_path(os.path.dirname(self.options[0].general_rar_exe_path)) + # make sure rar program is now in the path for the rar class + if self.options[0].archivers_rar: + utils.add_to_path(os.path.dirname(str(self.leRarExePath.text()))) if not str(self.leIssueNumPadding.text()).isdigit(): self.leIssueNumPadding.setText("0") From bc02a9a2a2e3e01915b3cfa6bef6cbdf4b6f6032 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 31 Jan 2023 19:41:19 -0800 Subject: [PATCH 17/18] Use a persistent setting group for archiver settings --- comictaggerlib/ctoptions/plugin.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comictaggerlib/ctoptions/plugin.py b/comictaggerlib/ctoptions/plugin.py index cc48b64..5276907 100644 --- a/comictaggerlib/ctoptions/plugin.py +++ b/comictaggerlib/ctoptions/plugin.py @@ -42,4 +42,4 @@ def validate_plugin_settings(options: settngs.Config) -> settngs.Config: def register_plugin_settings(manager: settngs.Manager): - manager.add_group("archiver", archiver, False) + manager.add_persistent_group("archiver", archiver, False) diff --git a/requirements.txt b/requirements.txt index 3e4ea89..2d29204 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ pycountry pyicu; sys_platform == 'linux' or sys_platform == 'darwin' rapidfuzz>=2.12.0 requests==2.* -settngs==0.3.0 +settngs==0.5.0 text2digits typing_extensions wordninja From c6e3266f605fefdd3ca0950ddccc32793890fc1b Mon Sep 17 00:00:00 2001 From: Mizaki Date: Wed, 1 Feb 2023 15:39:24 +0000 Subject: [PATCH 18/18] More verbose attrib string --- comictalker/talkerbase.py | 2 +- comictalker/talkers/comicvine.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comictalker/talkerbase.py b/comictalker/talkerbase.py index 9c70c7c..6ef88aa 100644 --- a/comictalker/talkerbase.py +++ b/comictalker/talkerbase.py @@ -40,7 +40,7 @@ class SourceStaticOptions: def __init__( self, website: str = "", - attribution_string: str = "", # Full string including web link: Source: Example + attribution_string: str = "", # Full string including web link, example: Metadata provided by Example has_issues: bool = False, has_alt_covers: bool = False, requires_apikey: bool = False, diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 54aa372..6826edb 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -174,7 +174,7 @@ class ComicVineTalker(ComicTalker): ) self.static_options = SourceStaticOptions( website="https://comicvine.gamespot.com/", - attribution_string="Data Source: Comic Vine", + attribution_string="Metadata provided by Comic Vine", has_issues=True, has_alt_covers=True, requires_apikey=True,