From 2fbcd5e8bd1258162d73c40c799eb75ae55984fe Mon Sep 17 00:00:00 2001 From: ZTF-Zeiram Date: Sat, 6 Dec 2025 03:36:01 +0000 Subject: [PATCH] Upload files to "/" --- .gitignore | 140 +++++++++++++++++++ __init__.py | 79 +++++++++++ about.txt | 8 ++ comicbookinfo.py | 138 +++++++++++++++++++ comicinfoxml.py | 346 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 711 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 about.txt create mode 100644 comicbookinfo.py create mode 100644 comicinfoxml.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c904e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,140 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +EmbedComicMetadata.zip diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..80a3ca7 --- /dev/null +++ b/__init__.py @@ -0,0 +1,79 @@ +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2015, dloraine' +__docformat__ = 'restructuredtext en' + +# The class that all Interface Action plugin wrappers must inherit from +from calibre.customize import InterfaceActionBase + + +class EmbedComicMetadataBase(InterfaceActionBase): + ''' + This class is a simple wrapper that provides information about the actual + plugin class. The actual interface plugin class is called InterfacePlugin + and is defined in the ui.py file, as specified in the actual_plugin field + below. + + The reason for having two classes is that it allows the command line + calibre utilities to run without needing to load the GUI libraries. + ''' + name = 'Embed Comic Metadata' + description = 'Embeds calibre metadata into comic archives and imports \ + metadata from comic archives into calibre.' + supported_platforms = ['windows', 'osx', 'linux'] + author = 'dloraine' + version = (1, 6, 7) + minimum_calibre_version = (3, 1, 1) + + #: This field defines the GUI plugin class that contains all the code + #: that actually does something. Its format is module_path:class_name + #: The specified class must be defined in the specified module. + actual_plugin = 'calibre_plugins.EmbedComicMetadata.ui:EmbedComicMetadata' + + def is_customizable(self): + ''' + This method must return True to enable customization via + Preferences->Plugins + ''' + return True + + def config_widget(self): + ''' + Implement this method and :meth:`save_settings` in your plugin to + use a custom configuration dialog. + + This method, if implemented, must return a QWidget. The widget can have + an optional method validate() that takes no arguments and is called + immediately after the user clicks OK. Changes are applied if and only + if the method returns True. + + If for some reason you cannot perform the configuration at this time, + return a tuple of two strings (message, details), these will be + displayed as a warning dialog to the user and the process will be + aborted. + + The base class implementation of this method raises NotImplementedError + so by default no user configuration is possible. + ''' + # It is important to put this import statement here rather than at the + # top of the module as importing the config class will also cause the + # GUI libraries to be loaded, which we do not want when using calibre + # from the command line + if self.actual_plugin_: + from calibre_plugins.EmbedComicMetadata.config import ConfigWidget + return ConfigWidget(self.actual_plugin_) + + def save_settings(self, config_widget): + ''' + Save the settings specified by the user with config_widget. + + :param config_widget: The widget returned by :meth:`config_widget`. + ''' + config_widget.save_settings() + + # Apply the changes + ac = self.actual_plugin_ + if ac is not None: + ac.apply_settings() diff --git a/about.txt b/about.txt new file mode 100644 index 0000000..c845b5f --- /dev/null +++ b/about.txt @@ -0,0 +1,8 @@ +Embed Comic Metadata +=========================== + +This plugin embeds the calibre metadata into cbz comic files. +It supports writing to the zip-comment (Comicbook Lover style metadata) +and writing a ComicInfo.xml file into the cbz (ComicRack style) + +It also can import the metadata from cbz and cbr into calibre. diff --git a/comicbookinfo.py b/comicbookinfo.py new file mode 100644 index 0000000..f9e2b0c --- /dev/null +++ b/comicbookinfo.py @@ -0,0 +1,138 @@ +""" +A python class to encapsulate the ComicBookInfo data +""" + +""" +Copyright 2012-2014 Anthony Beville + +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. +""" +import json +from datetime import datetime +from calibre.utils.localization import calibre_langcode_to_name, canonicalize_lang, lang_as_iso639_1 +from calibre_plugins.EmbedComicMetadata.genericmetadata import GenericMetadata + +import sys + +if sys.version_info[0] > 2: + unicode = str + + +class ComicBookInfo: + + def metadataFromString(self, string): + + cbi_container = json.loads(unicode(string, 'utf-8')) + + metadata = GenericMetadata() + + cbi = cbi_container['ComicBookInfo/1.0'] + + # helper func + # If item is not in CBI, return None + def xlate(cbi_entry): + if cbi_entry in cbi: + return cbi[cbi_entry] + else: + return None + + metadata.series = xlate('series') + metadata.title = xlate('title') + metadata.issue = xlate('issue') + metadata.publisher = xlate('publisher') + metadata.month = xlate('publicationMonth') + metadata.year = xlate('publicationYear') + metadata.issueCount = xlate('numberOfIssues') + metadata.comments = xlate('comments') + metadata.credits = xlate('credits') + metadata.genre = xlate('genre') + metadata.volume = xlate('volume') + metadata.volumeCount = xlate('numberOfVolumes') + metadata.language = xlate('language') + metadata.country = xlate('country') + metadata.criticalRating = xlate('rating') + metadata.tags = xlate('tags') + metadata.gtin = xlate('GTIN') + + # make sure credits and tags are at least empty lists and not None + if metadata.credits is None: + metadata.credits = [] + if metadata.tags is None: + metadata.tags = [] + + # need to massage the language string to be ISO + # modified to use a calibre function + if metadata.language is not None: + metadata.language = lang_as_iso639_1(metadata.language) + + metadata.isEmpty = False + + return metadata + + def stringFromMetadata(self, metadata): + + cbi_container = self.createJSONDictionary(metadata) + return json.dumps(cbi_container) + + # verify that the string actually contains CBI data in JSON format + def validateString(self, string): + + try: + cbi_container = json.loads(string) + except: + return False + + return ('ComicBookInfo/1.0' in cbi_container) + + def createJSONDictionary(self, metadata): + + # Create the dictionary that we will convert to JSON text + cbi = dict() + cbi_container = {'appID': 'ComicTagger/', + 'lastModified': str(datetime.now()), + 'ComicBookInfo/1.0': cbi} + + # helper func + def assign(cbi_entry, md_entry): + if md_entry is not None: + cbi[cbi_entry] = md_entry + + # helper func + def toInt(s): + i = None + if type(s) in [str, unicode, int]: + try: + i = int(s) + except ValueError: + pass + return i + + assign('series', metadata.series) + assign('title', metadata.title) + assign('issue', metadata.issue) + assign('publisher', metadata.publisher) + assign('publicationMonth', toInt(metadata.month)) + assign('publicationYear', toInt(metadata.year)) + assign('numberOfIssues', toInt(metadata.issueCount)) + assign('comments', metadata.comments) + assign('genre', metadata.genre) + assign('volume', toInt(metadata.volume)) + assign('numberOfVolumes', toInt(metadata.volumeCount)) + assign('language', calibre_langcode_to_name(canonicalize_lang(metadata.language))) + assign('country', metadata.country) + assign('rating', metadata.criticalRating) + assign('credits', metadata.credits) + assign('tags', metadata.tags) + assign('GTIN', metadata.gtin) + + return cbi_container diff --git a/comicinfoxml.py b/comicinfoxml.py new file mode 100644 index 0000000..ab3dda2 --- /dev/null +++ b/comicinfoxml.py @@ -0,0 +1,346 @@ +""" +A python class to encapsulate ComicRack's ComicInfo.xml data +""" + +""" +Copyright 2012-2014 Anthony Beville + +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 pprint import pprint +import xml.etree.ElementTree as ET +from calibre_plugins.EmbedComicMetadata.genericmetadata import GenericMetadata + +import sys + +if sys.version_info[0] > 2: + python3 = True + unicode = str +else: + python3 = False + + +class ComicInfoXml: + + writer_synonyms = ['writer', 'plotter', 'scripter'] + penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns'] + inker_synonyms = ['inker', 'artist', 'finishes'] + colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer'] + letterer_synonyms = ['letterer'] + cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist'] + editor_synonyms = ['editor'] + translator_synonyms = ['translator'] + + def getParseableCredits(self): + parsable_credits = [] + parsable_credits.extend(self.writer_synonyms) + parsable_credits.extend(self.penciller_synonyms) + parsable_credits.extend(self.inker_synonyms) + parsable_credits.extend(self.colorist_synonyms) + parsable_credits.extend(self.letterer_synonyms) + parsable_credits.extend(self.cover_synonyms) + parsable_credits.extend(self.editor_synonyms) + parsable_credits.extend(self.translator_synonyms) + return parsable_credits + + def metadataFromString(self, string): + + tree = ET.ElementTree(ET.fromstring(string)) + return self.convertXMLToMetadata(tree) + + def stringFromMetadata(self, metadata): + + header = '\n' + + tree = self.convertMetadataToXML(self, metadata) + if python3: + return header + ET.tostring(tree.getroot(), "unicode") + return header + ET.tostring(tree.getroot()) + + def indent(self, elem, level=0): + # for making the XML output readable + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + def convertMetadataToXML(self, filename, metadata): + + # shorthand for the metadata + md = metadata + + # build a tree structure + root = ET.Element("ComicInfo") + root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" + root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema" + # helper func + + def assign(cix_entry, md_entry): + if md_entry is not None: + ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry) + + assign('Title', md.title) + assign('Series', md.series) + assign('Number', md.issue) + assign('Count', md.issueCount) + assign('Volume', md.volume) + assign('AlternateSeries', md.alternateSeries) + assign('AlternateNumber', md.alternateNumber) + assign('StoryArc', md.storyArc) + assign('SeriesGroup', md.seriesGroup) + assign('AlternateCount', md.alternateCount) + assign('Summary', md.comments) + assign('Notes', md.notes) + assign('Year', md.year) + assign('Month', md.month) + assign('Day', md.day) + + # need to specially process the credits, since they are structured differently than CIX + credit_writer_list = list() + credit_penciller_list = list() + credit_inker_list = list() + credit_colorist_list = list() + credit_letterer_list = list() + credit_cover_list = list() + credit_editor_list = list() + credit_translator_list = list() + + # first, loop thru credits, and build a list for each role that CIX supports + for credit in metadata.credits: + + if credit['role'].lower() in set(self.writer_synonyms): + credit_writer_list.append(credit['person'].replace(",", "")) + + if credit['role'].lower() in set(self.penciller_synonyms): + credit_penciller_list.append(credit['person'].replace(",", "")) + + if credit['role'].lower() in set(self.inker_synonyms): + credit_inker_list.append(credit['person'].replace(",", "")) + + if credit['role'].lower() in set(self.colorist_synonyms): + credit_colorist_list.append(credit['person'].replace(",", "")) + + if credit['role'].lower() in set(self.letterer_synonyms): + credit_letterer_list.append(credit['person'].replace(",", "")) + + if credit['role'].lower() in set(self.cover_synonyms): + credit_cover_list.append(credit['person'].replace(",", "")) + + if credit['role'].lower() in set(self.editor_synonyms): + credit_editor_list.append(credit['person'].replace(",", "")) + + if credit['role'].lower() in set(self.translator_synonyms): + credit_translator_list.append(credit['person'].replace(",", "")) + + # second, convert each list to string, and add to XML struct + if len(credit_writer_list) > 0: + node = ET.SubElement(root, 'Writer') + node.text = listToString(credit_writer_list) + + if len(credit_penciller_list) > 0: + node = ET.SubElement(root, 'Penciller') + node.text = listToString(credit_penciller_list) + + if len(credit_inker_list) > 0: + node = ET.SubElement(root, 'Inker') + node.text = listToString(credit_inker_list) + + if len(credit_colorist_list) > 0: + node = ET.SubElement(root, 'Colorist') + node.text = listToString(credit_colorist_list) + + if len(credit_letterer_list) > 0: + node = ET.SubElement(root, 'Letterer') + node.text = listToString(credit_letterer_list) + + if len(credit_cover_list) > 0: + node = ET.SubElement(root, 'CoverArtist') + node.text = listToString(credit_cover_list) + + if len(credit_editor_list) > 0: + node = ET.SubElement(root, 'Editor') + node.text = listToString(credit_editor_list) + + if len(credit_translator_list) > 0: + node = ET.SubElement(root, 'Translator') + node.text = listToString(credit_translator_list) + + # calibre custom columns like tags return tuples, so we need to handle + # these specially + md.characters = tuple_to_string(md.characters) + md.teams = tuple_to_string(md.teams) + md.locations = tuple_to_string(md.locations) + md.genre = tuple_to_string(md.genre) + md.tags = tuple_to_string(md.tags) + + assign('Publisher', md.publisher) + assign('Imprint', md.imprint) + assign('Genre', md.genre) + if md.tags: + assign('Tags', md.tags) + assign('Web', md.webLink) + assign('PageCount', md.pageCount) + assign('LanguageISO', md.language) + assign('Format', md.format) + assign('AgeRating', md.maturityRating) + if md.blackAndWhite is not None and md.blackAndWhite: + ET.SubElement(root, 'BlackAndWhite').text = "Yes" + assign('Manga', md.manga) + assign('Characters', md.characters) + assign('Teams', md.teams) + assign('Locations', md.locations) + assign('ScanInformation', md.scanInfo) + assign('GTIN', md.gtin) + + # loop and add the page entries under pages node + if len(md.pages) > 0: + pages_node = ET.SubElement(root, 'Pages') + for page_dict in md.pages: + page_node = ET.SubElement(pages_node, 'Page') + page_node.attrib = page_dict + + # self pretty-print + self.indent(root) + + # wrap it in an ElementTree instance, and save as XML + tree = ET.ElementTree(root) + return tree + + def convertXMLToMetadata(self, tree): + + root = tree.getroot() + + if root.tag != 'ComicInfo': + raise 1 + return None + + metadata = GenericMetadata() + md = metadata + + # Helper function + def xlate(tag): + node = root.find(tag) + if node is not None: + return node.text + else: + return None + + md.series = xlate('Series') + md.title = xlate('Title') + md.issue = xlate('Number') + md.issueCount = xlate('Count') + md.volume = xlate('Volume') + md.alternateSeries = xlate('AlternateSeries') + md.alternateNumber = xlate('AlternateNumber') + md.alternateCount = xlate('AlternateCount') + md.comments = xlate('Summary') + md.notes = xlate('Notes') + md.year = xlate('Year') + md.month = xlate('Month') + md.day = xlate('Day') + md.publisher = xlate('Publisher') + md.imprint = xlate('Imprint') + md.genre = xlate('Genre') + md.webLink = xlate('Web') + md.language = xlate('LanguageISO') + md.format = xlate('Format') + md.manga = xlate('Manga') + md.characters = xlate('Characters') + md.teams = xlate('Teams') + md.locations = xlate('Locations') + md.pageCount = xlate('PageCount') + md.scanInfo = xlate('ScanInformation') + md.storyArc = xlate('StoryArc') + md.seriesGroup = xlate('SeriesGroup') + md.maturityRating = xlate('AgeRating') + md.gtin = xlate('GTIN') + + tmp = xlate('BlackAndWhite') + md.blackAndWhite = False + if tmp is not None and tmp.lower() in ["yes", "true", "1"]: + md.blackAndWhite = True + # Now extract the credit info + for n in root: + if (n.tag == 'Writer' or + n.tag == 'Penciller' or + n.tag == 'Inker' or + n.tag == 'Colorist' or + n.tag == 'Letterer' or + n.tag == 'Editor' or + n.tag == 'Translator'): + if n.text is not None: + for name in n.text.split(','): + metadata.addCredit(name.strip(), n.tag) + + if n.tag == 'CoverArtist': + if n.text is not None: + for name in n.text.split(','): + metadata.addCredit(name.strip(), "Cover") + + # Tags + tags = xlate('Tags') + if tags is not None: + md.tags = [t for t in tags.split(", ")] + + # parse page data now + pages_node = root.find("Pages") + if pages_node is not None: + for page in pages_node: + metadata.pages.append(page.attrib) + # print page.attrib + + metadata.isEmpty = False + + return metadata + + def writeToExternalFile(self, filename, metadata): + + tree = self.convertMetadataToXML(self, metadata) + # ET.dump(tree) + tree.write(filename, encoding='utf-8') + + def readFromExternalFile(self, filename): + + tree = ET.parse(filename) + return self.convertXMLToMetadata(tree) + + +def listToString(l): + string = "" + if l is not None: + for item in l: + if len(string) > 0: + string += ", " + string += item + return string + + +def tuple_to_string(metadata): + if metadata and not (isinstance(metadata, str) or isinstance(metadata, unicode)): + string = "" + for item in metadata: + if len(string) > 0: + string += ", " + string += item + return string + return metadata