diff --git a/comicapi/tags/comet.py b/comicapi/tags/comet.py deleted file mode 100644 index c69d7ec..0000000 --- a/comicapi/tags/comet.py +++ /dev/null @@ -1,323 +0,0 @@ -"""A class to encapsulate CoMet data""" - -# -# Copyright 2012-2014 ComicTagger Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -import logging -import os -import xml.etree.ElementTree as ET -from typing import Any - -from comicapi import utils -from comicapi.archivers import Archiver -from comicapi.comicarchive import ComicArchive -from comicapi.genericmetadata import GenericMetadata, PageMetadata, PageType -from comicapi.tags import Tag - -logger = logging.getLogger(__name__) - - -class CoMet(Tag): - enabled = True - - id = "comet" - - def __init__(self, version: str) -> None: - super().__init__(version) - - self.comet_filename = "CoMet.xml" - self.file = "CoMet.xml" - self.supported_attributes = { - "series", - "issue", - "title", - "volume", - "genres", - "description", - "publisher", - "language", - "format", - "maturity_rating", - "month", - "year", - "page_count", - "characters", - "credits", - "credits.person", - "credits.primary", - "credits.role", - "price", - "is_version_of", - "rights", - "identifier", - "last_mark", - "pages.type", # This is required for setting the cover image none of the other types will be saved - "pages", - } - - def supports_credit_role(self, role: str) -> bool: - return role.casefold() in self._get_parseable_credits() - - def supports_tags(self, archive: Archiver) -> bool: - return archive.supports_files() - - def has_tags(self, archive: Archiver) -> bool: - if not self.supports_tags(archive): - return False - has_tags = False - # look at all xml files in root, and search for CoMet data, get first - for n in archive.get_filename_list(): - if os.path.dirname(n) == "" and os.path.splitext(n)[1].casefold() == ".xml": - # read in XML file, and validate it - data = b"" - try: - data = archive.read_file(n) - except Exception as e: - logger.warning("Error reading in Comet XML for validation! from %s: %s", archive.path, e) - if self._validate_bytes(data): - # since we found it, save it! - self.file = n - has_tags = True - break - return has_tags - - def remove_tags(self, archive: Archiver) -> bool: - return self.has_tags(archive) and archive.remove_file(self.file) - - def read_tags(self, archive: Archiver) -> GenericMetadata: - if self.has_tags(archive): - metadata = archive.read_file(self.file) or b"" - if self._validate_bytes(metadata): - return self._metadata_from_bytes(metadata, archive) - return GenericMetadata() - - def read_raw_tags(self, archive: Archiver) -> str: - if self.has_tags(archive): - return ET.tostring(ET.fromstring(archive.read_file(self.file)), encoding="unicode", xml_declaration=True) - return "" - - def write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool: - if self.supports_tags(archive): - success = True - xml = b"" - if self.has_tags(archive): - xml = archive.read_file(self.file) - if self.file != self.comet_filename: - success = self.remove_tags(archive) - - return success and archive.write_file(self.comet_filename, self._bytes_from_metadata(metadata, xml)) - else: - logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") - return False - - def name(self) -> str: - return "Comic Metadata (CoMet)" - - @classmethod - def _get_parseable_credits(cls) -> list[str]: - parsable_credits: list[str] = [] - parsable_credits.extend(GenericMetadata.writer_synonyms) - parsable_credits.extend(GenericMetadata.penciller_synonyms) - parsable_credits.extend(GenericMetadata.inker_synonyms) - parsable_credits.extend(GenericMetadata.colorist_synonyms) - parsable_credits.extend(GenericMetadata.letterer_synonyms) - parsable_credits.extend(GenericMetadata.cover_synonyms) - parsable_credits.extend(GenericMetadata.editor_synonyms) - return parsable_credits - - def _metadata_from_bytes(self, string: bytes, archive: Archiver) -> GenericMetadata: - tree = ET.ElementTree(ET.fromstring(string)) - return self._convert_xml_to_metadata(tree, archive) - - def _bytes_from_metadata(self, metadata: GenericMetadata, xml: bytes = b"") -> bytes: - tree = self._convert_metadata_to_xml(metadata, xml) - return ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True) - - def _convert_metadata_to_xml(self, metadata: GenericMetadata, xml: bytes = b"") -> ET.ElementTree: - # shorthand for the metadata - md = metadata - - if xml: - root = ET.fromstring(xml) - else: - # build a tree structure - root = ET.Element("comet") - root.attrib["xmlns:comet"] = "http://www.denvog.com/comet/" - root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance" - root.attrib["xsi:schemaLocation"] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd" - - # helper func - def assign(comet_entry: str, md_entry: Any) -> None: - if md_entry is not None: - ET.SubElement(root, comet_entry).text = str(md_entry) - - # title is manditory - assign("title", md.title or "") - assign("series", md.series) - assign("issue", md.issue) # must be int?? - assign("volume", md.volume) - assign("description", md.description) - assign("publisher", md.publisher) - assign("pages", md.page_count) - assign("format", md.format) - assign("language", md.language) - assign("rating", md.maturity_rating) - assign("price", md.price) - assign("isVersionOf", md.is_version_of) - assign("rights", md.rights) - assign("identifier", md.identifier) - assign("lastMark", md.last_mark) - assign("genre", ",".join(md.genres)) # TODO repeatable - - for c in md.characters: - assign("character", c.strip()) - - if md.manga is not None and md.manga == "YesAndRightToLeft": - assign("readingDirection", "rtl") - - if md.year is not None: - date_str = f"{md.year:04}" - if md.month is not None: - date_str += f"-{md.month:02}" - assign("date", date_str) - - cover_index = md.get_cover_page_index_list()[0] - assign("coverImage", md.pages[cover_index].filename) - - # loop thru credits, and build a list for each role that CoMet supports - for credit in metadata.credits: - if credit.role.casefold() in set(GenericMetadata.writer_synonyms): - ET.SubElement(root, "writer").text = str(credit.person) - - if credit.role.casefold() in set(GenericMetadata.penciller_synonyms): - ET.SubElement(root, "penciller").text = str(credit.person) - - if credit.role.casefold() in set(GenericMetadata.inker_synonyms): - ET.SubElement(root, "inker").text = str(credit.person) - - if credit.role.casefold() in set(GenericMetadata.colorist_synonyms): - ET.SubElement(root, "colorist").text = str(credit.person) - - if credit.role.casefold() in set(GenericMetadata.letterer_synonyms): - ET.SubElement(root, "letterer").text = str(credit.person) - - if credit.role.casefold() in set(GenericMetadata.cover_synonyms): - ET.SubElement(root, "coverDesigner").text = str(credit.person) - - if credit.role.casefold() in set(GenericMetadata.editor_synonyms): - ET.SubElement(root, "editor").text = str(credit.person) - - ET.indent(root) - - # wrap it in an ElementTree instance, and save as XML - tree = ET.ElementTree(root) - return tree - - def _convert_xml_to_metadata(self, tree: ET.ElementTree, archive: Archiver) -> GenericMetadata: - root = tree.getroot() - - if root.tag != "comet": - raise Exception("Not a CoMet file") - - metadata = GenericMetadata() - md = metadata - - # Helper function - def get(tag: str) -> Any: - node = root.find(tag) - if node is not None: - return node.text - return None - - md.series = utils.xlate(get("series")) - md.title = utils.xlate(get("title")) - md.issue = utils.xlate(get("issue")) - md.volume = utils.xlate_int(get("volume")) - md.description = utils.xlate(get("description")) - md.publisher = utils.xlate(get("publisher")) - md.language = utils.xlate(get("language")) - md.format = utils.xlate(get("format")) - md.page_count = utils.xlate_int(get("pages")) - md.maturity_rating = utils.xlate(get("rating")) - md.price = utils.xlate_float(get("price")) - md.is_version_of = utils.xlate(get("isVersionOf")) - md.rights = utils.xlate(get("rights")) - md.identifier = utils.xlate(get("identifier")) - md.last_mark = utils.xlate(get("lastMark")) - - _, md.month, md.year = utils.parse_date_str(utils.xlate(get("date"))) - - ca = ComicArchive(archive) - cover_filename = utils.xlate(get("coverImage")) - page_list = ca.get_page_name_list() - if cover_filename in page_list: - cover_index = page_list.index(cover_filename) - md.pages = [ - PageMetadata( - archive_index=cover_index, - display_index=0, - filename=cover_filename, - type=PageType.FrontCover, - bookmark="", - ) - ] - - reading_direction = utils.xlate(get("readingDirection")) - if reading_direction is not None and reading_direction == "rtl": - md.manga = "YesAndRightToLeft" - - # loop for genre tags - for n in root: - if n.tag == "genre": - md.genres.add((n.text or "").strip()) - - # loop for character tags - for n in root: - if n.tag == "character": - md.characters.add((n.text or "").strip()) - - # Now extract the credit info - for n in root: - if any( - [ - n.tag == "writer", - n.tag == "penciller", - n.tag == "inker", - n.tag == "colorist", - n.tag == "letterer", - n.tag == "editor", - ] - ): - metadata.add_credit((n.text or "").strip(), n.tag.title()) - - if n.tag == "coverDesigner": - metadata.add_credit((n.text or "").strip(), "Cover") - - metadata.is_empty = False - - return metadata - - # verify that the string actually contains CoMet data in XML format - def _validate_bytes(self, string: bytes) -> bool: - try: - tree = ET.ElementTree(ET.fromstring(string)) - root = tree.getroot() - if root.tag != "comet": - return False - except ET.ParseError: - return False - - return True diff --git a/comicapi/tags/comicbookinfo.py b/comicapi/tags/comicbookinfo.py deleted file mode 100644 index 5af869d..0000000 --- a/comicapi/tags/comicbookinfo.py +++ /dev/null @@ -1,229 +0,0 @@ -"""A class to encapsulate the ComicBookInfo data""" - -# Copyright 2012-2014 ComicTagger Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -import json -import logging -from datetime import datetime -from typing import Any, Literal, TypedDict - -from comicapi import utils -from comicapi.archivers import Archiver -from comicapi.genericmetadata import Credit, GenericMetadata -from comicapi.tags import Tag - -logger = logging.getLogger(__name__) - -_CBILiteralType = Literal[ - "series", - "title", - "issue", - "publisher", - "publicationMonth", - "publicationYear", - "numberOfIssues", - "comments", - "genre", - "volume", - "numberOfVolumes", - "language", - "country", - "rating", - "credits", - "tags", -] - - -class credit(TypedDict): - person: str - role: str - primary: bool - - -class _ComicBookInfoJson(TypedDict, total=False): - series: str - title: str - publisher: str - publicationMonth: int - publicationYear: int - issue: int - numberOfIssues: int - volume: int - numberOfVolumes: int - rating: int - genre: str - language: str - country: str - credits: list[credit] - tags: list[str] - comments: str - - -_CBIContainer = TypedDict("_CBIContainer", {"appID": str, "lastModified": str, "ComicBookInfo/1.0": _ComicBookInfoJson}) - - -class ComicBookInfo(Tag): - enabled = True - - id = "cbi" - - def __init__(self, version: str) -> None: - super().__init__(version) - - self.supported_attributes = { - "series", - "issue", - "issue_count", - "title", - "volume", - "volume_count", - "genres", - "description", - "publisher", - "month", - "year", - "language", - "country", - "critical_rating", - "tags", - "credits", - "credits.person", - "credits.primary", - "credits.role", - } - - def supports_credit_role(self, role: str) -> bool: - return True - - def supports_tags(self, archive: Archiver) -> bool: - return archive.supports_comment() - - def has_tags(self, archive: Archiver) -> bool: - return self.supports_tags(archive) and self._validate_string(archive.get_comment()) - - def remove_tags(self, archive: Archiver) -> bool: - return archive.set_comment("") - - def read_tags(self, archive: Archiver) -> GenericMetadata: - if self.has_tags(archive): - comment = archive.get_comment() - if self._validate_string(comment): - return self._metadata_from_string(comment) - return GenericMetadata() - - def read_raw_tags(self, archive: Archiver) -> str: - if self.has_tags(archive): - return json.dumps(json.loads(archive.get_comment()), indent=2) - return "" - - def write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool: - if self.supports_tags(archive): - return archive.set_comment(self._string_from_metadata(metadata)) - else: - logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") - return False - - def name(self) -> str: - return "ComicBookInfo" - - def _metadata_from_string(self, string: str) -> GenericMetadata: - cbi_container: _CBIContainer = json.loads(string) - - metadata = GenericMetadata() - - cbi = cbi_container["ComicBookInfo/1.0"] - - metadata.series = utils.xlate(cbi.get("series")) - metadata.title = utils.xlate(cbi.get("title")) - metadata.issue = utils.xlate(cbi.get("issue")) - metadata.publisher = utils.xlate(cbi.get("publisher")) - metadata.month = utils.xlate_int(cbi.get("publicationMonth")) - metadata.year = utils.xlate_int(cbi.get("publicationYear")) - metadata.issue_count = utils.xlate_int(cbi.get("numberOfIssues")) - metadata.description = utils.xlate(cbi.get("comments")) - metadata.genres = set(utils.split(cbi.get("genre"), ",")) - metadata.volume = utils.xlate_int(cbi.get("volume")) - metadata.volume_count = utils.xlate_int(cbi.get("numberOfVolumes")) - metadata.language = utils.xlate(cbi.get("language")) - metadata.country = utils.xlate(cbi.get("country")) - metadata.critical_rating = utils.xlate_int(cbi.get("rating")) - - metadata.credits = [ - Credit( - person=x["person"] if "person" in x else "", - role=x["role"] if "role" in x else "", - primary=x["primary"] if "primary" in x else False, - ) - for x in cbi.get("credits", []) - ] - metadata.tags.update(cbi.get("tags", set())) - - # need the language string to be ISO - if metadata.language: - metadata.language = utils.get_language_iso(metadata.language) - - metadata.is_empty = False - - return metadata - - def _string_from_metadata(self, metadata: GenericMetadata) -> str: - cbi_container = self._create_json_dictionary(metadata) - return json.dumps(cbi_container) - - def _validate_string(self, string: bytes | str) -> bool: - """Verify that the string actually contains CBI data in JSON format""" - - try: - cbi_container = json.loads(string) - except json.JSONDecodeError: - return False - - return "ComicBookInfo/1.0" in cbi_container - - def _create_json_dictionary(self, metadata: GenericMetadata) -> _CBIContainer: - """Create the dictionary that we will convert to JSON text""" - - cbi_container = _CBIContainer( - { - "appID": "ComicTagger/1.0.0", - "lastModified": str(datetime.now()), - "ComicBookInfo/1.0": {}, - } - ) # TODO: ctversion.version, - - # helper func - def assign(cbi_entry: _CBILiteralType, md_entry: Any) -> None: - if md_entry is not None or isinstance(md_entry, str) and md_entry != "": - cbi_container["ComicBookInfo/1.0"][cbi_entry] = md_entry - - assign("series", utils.xlate(metadata.series)) - assign("title", utils.xlate(metadata.title)) - assign("issue", utils.xlate(metadata.issue)) - assign("publisher", utils.xlate(metadata.publisher)) - assign("publicationMonth", utils.xlate_int(metadata.month)) - assign("publicationYear", utils.xlate_int(metadata.year)) - assign("numberOfIssues", utils.xlate_int(metadata.issue_count)) - assign("comments", utils.xlate(metadata.description)) - assign("genre", utils.xlate(",".join(metadata.genres))) - assign("volume", utils.xlate_int(metadata.volume)) - assign("numberOfVolumes", utils.xlate_int(metadata.volume_count)) - assign("language", utils.xlate(utils.get_language_from_iso(metadata.language))) - assign("country", utils.xlate(metadata.country)) - assign("rating", utils.xlate_int(metadata.critical_rating)) - assign("credits", [credit(person=c.person, role=c.role, primary=c.primary) for c in metadata.credits]) - assign("tags", list(metadata.tags)) - - return cbi_container diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index 1a9b1be..41273ce 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -27,8 +27,8 @@ def general(parser: settngs.Manager) -> None: def internal(parser: settngs.Manager) -> None: # automatic settings parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False) - parser.add_setting("write_tags", default=["cbi"], cmdline=False) - parser.add_setting("read_tags", default=["cbi"], cmdline=False) + parser.add_setting("write_tags", default=["cr"], cmdline=False) + parser.add_setting("read_tags", default=["cr"], cmdline=False) parser.add_setting("last_opened_folder", default="", cmdline=False) parser.add_setting("window_width", default=0, cmdline=False) parser.add_setting("window_height", default=0, cmdline=False) @@ -356,7 +356,7 @@ def migrate_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]: elif isinstance(write_Tags, str): config[0].internal__write_tags = [write_Tags] else: - config[0].internal__write_tags = ["cbi"] + config[0].internal__write_tags = ["cr"] read_tags = config[0].internal__read_tags if not isinstance(read_tags, list): @@ -365,7 +365,7 @@ def migrate_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]: elif isinstance(read_tags, str): config[0].internal__read_tags = [read_tags] else: - config[0].internal__read_tags = ["cbi"] + config[0].internal__read_tags = ["cr"] return config diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 6205cce..d109715 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -228,8 +228,8 @@ class TaggerWindow(QtWidgets.QMainWindow): if tag_id not in tags: config[0].internal__read_tags.remove(tag_id) - self.selected_write_tags: list[str] = config[0].internal__write_tags - self.selected_read_tags: list[str] = config[0].internal__read_tags + self.selected_write_tags: list[str] = config[0].internal__write_tags or ["cr"] + self.selected_read_tags: list[str] = config[0].internal__read_tags or ["cr"] self.setAcceptDrops(True) self.view_tag_actions, self.remove_tag_actions = self.tag_actions() diff --git a/setup.cfg b/setup.cfg index eb48d5a..761ef77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,8 +66,6 @@ comicapi.archiver = folder = comicapi.archivers.folder:FolderArchiver comicapi.tags = cr = comicapi.tags.comicrack:ComicRack - cbi = comicapi.tags.comicbookinfo:ComicBookInfo - comet = comicapi.tags.comet:CoMet comictagger.talker = comicvine = comictalker.talkers.comicvine:ComicVineTalker pyinstaller40 = @@ -96,6 +94,8 @@ all = py7zr rarfile>=4.0 pyicu;sys_platform == 'linux' or sys_platform == 'darwin' +archived_tags = + ct-archived-tags avif = pillow-avif-plugin>=1.4.1 cix =