diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7bbc71c..0000000 --- a/.gitignore +++ /dev/null @@ -1,101 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# 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/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 238144d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -matrix: - include: - - python: 3.4 - dist: trusty - sudo: false - - python: 3.5 - dist: trusty - sudo: false - - python: 3.5-dev - dist: trusty - sudo: false - - python: 3.6 - dist: trusty - sudo: false - - python: 3.6-dev - dist: trusty - sudo: false - - python: 3.7 - dist: xenial - sudo: true -install: - - python setup.py -q install diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9f36742 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres +to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.1.0] - 2023-06-04 + +### Added + +- Installation with pip. +- Support for non-existent location. +- Support for web loading errors of pages. + +### Fixed + +- Fixed page support. + +### Changed + +- New name of some variables. +- Automatic cleanup of temporary files. +- New location for temporary files. +- File path now uses `pathlib`. + +## [3.0.1] - 2023-02-06 + +### Added + +- Initial release. + +[3.1.0]: https://github.com/hyugogirubato/pycbzhelper/releases/tag/v3.1.0 +[3.0.1]: https://github.com/hyugogirubato/pycbzhelper/releases/tag/v3.0.1 diff --git a/README.md b/README.md index 369b8bd..5c5a1b6 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,64 @@ -# pycbzhelper -[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -[![Release](https://img.shields.io/github/release-date/hyugogirubato/pycbzhelper?style=plastic)](https://github.com/hyugogirubato/pycbzhelper/releases) -![Total Downloads](https://img.shields.io/github/downloads/hyugogirubato/pycbzhelper/total.svg?style=plastic) - -Python library to create a cbz file with metadata. - -# Usage - -### Basic Usage - -```python ->>> import os ->>> from pycbzhelper import Helper ->>> metadata = { ->>> "Title": "T1 - Arrête de me chauffer, Nagatoro", ->>> "Series": "Arrête de me chauffer, Nagatoro", ->>> "Number": "1", ->>> "Count": 8, ->>> "Volume": 1 ->>> ... ->>> } ->>> ... ->>> helper = Helper(metadata) ->>> helper.save_cbz( -... path=os.path.join("eBooks", "Arrête de me chauffer, Nagatoro"), -... file="T1 - Arrête de me chauffer, Nagatoro", -... clear=False, -... replace=True -... ) -``` - -# Installation - -To install, you can either clone the repository and run `python setup.py install` - -## About -- Graphical metadata editing software [here](https://github.com/comictagger/comictagger) -- Standard ComicInfo file structure information [here](https://github.com/Kussie/ComicInfoStandard) -- New version of ComicInfo file structure [here](https://github.com/anansi-project/comicinfo) \ No newline at end of file +# PyCBZHelper + +[![License](https://img.shields.io/github/license/hyugogirubato/pycbzhelper)](https://github.com/hyugogirubato/pycbzhelper/blob/master/LICENSE) +[![Release](https://img.shields.io/github/release-date/hyugogirubato/pycbzhelper)](https://github.com/hyugogirubato/pycbzhelper/releases) +[![Latest Version](https://img.shields.io/pypi/v/pycbzhelper)](https://pypi.org/project/pycbzhelper/) + +PyCBZHelper is a Python library for creating CBZ (Comic Book Zip) files with metadata. It provides functionality to +generate a CBZ file from a list of image pages and associated comic book metadata. + +## Features + +- Create CBZ files from images. +- Generate ComicInfo.xml metadata. +- Support for various metadata fields. +- Handle page files from local disk or web URLs. +- Automatic cleanup of temporary files. + +## Installation + +You can install PyCBZHelper using pip: + +````shell +pip install pycbzhelper +```` + +## Usage + +Here's a basic example of how to use PyCBZHelper: + +````python +from pycbzhelper import Helper +from pathlib import Path + +PARENT = Path(__name__).resolve().parent + +if __name__ == "__main__": + # Define metadata for the comic + metadata = { + "Title": "My Comic", + "Series": "Comic Series", + "Number": "1", + "Pages": [ + {"File": PARENT / "image1.jpg"}, + {"File": PARENT / "image2.jpg"}, + ] + # Add more metadata fields here + } + + # Define the path to the output CBZ file + output_path = PARENT / "output.cbz" + + # Create an instance of the Helper class + helper = Helper(metadata) + + # Create the CBZ file + helper.create_cbz(output_path) +```` + +For more information on how to use PyCBZHelper, please refer to +the [documentation](https://github.com/hyugogirubato/pycbzhelper/blob/master/docs/schema). + +### License + +This project is licensed under the [GPL v3 License](https://github.com/hyugogirubato/pycbzhelper/blob/master/LICENSE). \ No newline at end of file diff --git a/images/page-000.jpg b/docs/images/page-000.jpg similarity index 100% rename from images/page-000.jpg rename to docs/images/page-000.jpg diff --git a/images/page-001.jpg b/docs/images/page-001.jpg similarity index 100% rename from images/page-001.jpg rename to docs/images/page-001.jpg diff --git a/images/page-002.jpg b/docs/images/page-002.jpg similarity index 100% rename from images/page-002.jpg rename to docs/images/page-002.jpg diff --git a/images/page-003.jpg b/docs/images/page-003.jpg similarity index 100% rename from images/page-003.jpg rename to docs/images/page-003.jpg diff --git a/images/page-004.jpg b/docs/images/page-004.jpg similarity index 100% rename from images/page-004.jpg rename to docs/images/page-004.jpg diff --git a/images/page-005.jpg b/docs/images/page-005.jpg similarity index 100% rename from images/page-005.jpg rename to docs/images/page-005.jpg diff --git a/images/page-006.jpg b/docs/images/page-006.jpg similarity index 100% rename from images/page-006.jpg rename to docs/images/page-006.jpg diff --git a/images/page-007.jpg b/docs/images/page-007.jpg similarity index 100% rename from images/page-007.jpg rename to docs/images/page-007.jpg diff --git a/images/page-008.jpg b/docs/images/page-008.jpg similarity index 100% rename from images/page-008.jpg rename to docs/images/page-008.jpg diff --git a/images/page-009.jpg b/docs/images/page-009.jpg similarity index 100% rename from images/page-009.jpg rename to docs/images/page-009.jpg diff --git a/images/page-010.jpg b/docs/images/page-010.jpg similarity index 100% rename from images/page-010.jpg rename to docs/images/page-010.jpg diff --git a/schema/v1.0/ComicInfo.xsd b/docs/schema/v1.0/ComicInfo.xsd similarity index 61% rename from schema/v1.0/ComicInfo.xsd rename to docs/schema/v1.0/ComicInfo.xsd index 556c3a0..8e8c23c 100644 --- a/schema/v1.0/ComicInfo.xsd +++ b/docs/schema/v1.0/ComicInfo.xsd @@ -1,75 +1,75 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - + - - - - - - - + + + + + + + - - - - - - - - - - - + + + + + + + + + + + diff --git a/schema/v2.0/ComicInfo.xsd b/docs/schema/v2.0/ComicInfo.xsd similarity index 57% rename from schema/v2.0/ComicInfo.xsd rename to docs/schema/v2.0/ComicInfo.xsd index 6732fe8..ff31035 100644 --- a/schema/v2.0/ComicInfo.xsd +++ b/docs/schema/v2.0/ComicInfo.xsd @@ -1,63 +1,63 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - + + + + @@ -69,53 +69,53 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - - - - - - - - + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + diff --git a/schema/v2.1/ComicInfo.xsd b/docs/schema/v2.1/ComicInfo.xsd similarity index 56% rename from schema/v2.1/ComicInfo.xsd rename to docs/schema/v2.1/ComicInfo.xsd index 02ea57c..745de8b 100644 --- a/schema/v2.1/ComicInfo.xsd +++ b/docs/schema/v2.1/ComicInfo.xsd @@ -1,66 +1,66 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - + + + + @@ -72,53 +72,53 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - - - - - - - - + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + diff --git a/pycbzhelper/__init__.py b/pycbzhelper/__init__.py index 06de162..e662ba7 100644 --- a/pycbzhelper/__init__.py +++ b/pycbzhelper/__init__.py @@ -1,4 +1,5 @@ from .helper import * from .comicinfo import * +from .utils import slugify -__version__ = "3.0.1" +__version__ = "3.1.0" diff --git a/pycbzhelper/comicinfo.py b/pycbzhelper/comicinfo.py index ac72aa8..ff923f7 100644 --- a/pycbzhelper/comicinfo.py +++ b/pycbzhelper/comicinfo.py @@ -1,44 +1,37 @@ -""" -Refer: -- https://github.com/comictagger/comictagger -- https://github.com/Kussie/ComicInfoStandard -- https://github.com/anansi-project/comicinfo -""" - KEYS_STRING = [ - 'Title', 'Series', 'Number', 'AlternateSeries', 'AlternateNumber', - 'Summary', 'Notes', 'Writer', 'Penciller', 'Inker', 'Colorist', - 'Letterer', 'CoverArtist', 'Editor', 'Publisher', 'Imprint', 'Genre', - 'Web', 'Characters', 'Teams', 'Locations', 'ScanInformation', - 'StoryArc', 'SeriesGroup', 'CommunityRating' + "Title", "Series", "Number", "AlternateSeries", "AlternateNumber", + "Summary", "Notes", "Writer", "Penciller", "Inker", "Colorist", + "Letterer", "CoverArtist", "Editor", "Publisher", "Imprint", "Genre", + "Web", "Characters", "Teams", "Locations", "ScanInformation", + "StoryArc", "SeriesGroup", "CommunityRating" ] KEYS_INT = [ - 'Count', 'Volume', 'AlternateCount', 'Year', 'Month', 'Day', 'PageCount' + "Count", "Volume", "AlternateCount", "Year", "Month", "Day", "PageCount" ] KEYS_SPECIAL = [ - 'BlackAndWhite', 'Manga', 'AgeRating', 'Pages', 'LanguageISO', 'Format', 'ESN' + "BlackAndWhite", "Manga", "AgeRating", "Pages", "LanguageISO", "Format", "ESN" ] KEYS_AGE = [ - 'Adults Only 18+', 'Early Childhood', 'Everyone', 'Everyone 10+', - 'G', 'Kids to Adults', 'M', 'MA 15+', 'Mature 17+', 'PG', 'R18+', - 'Rating Pending', 'Teen', 'X18+', 'Rating Pending' + "Adults Only 18+", "Early Childhood", "Everyone", "Everyone 10+", + "G", "Kids to Adults", "M", "MA 15+", "Mature 17+", "PG", "R18+", + "Rating Pending", "Teen", "X18+", "Rating Pending" ] KEYS_FORMAT = [ - '1 Shot', '1/2', '1-Shot', 'Annotation', 'Annotations', - 'Annual', 'Anthology', 'B&W', 'B/W', 'B&&W', 'Black & White', 'Box Set', - 'Box-Set', 'Crossover', "Director's Cut", 'Epilogue', 'Event', 'FCBD', - 'Flyer', 'Giant', 'Giant Size', 'Giant-Size', 'Graphic Novel', 'Hardcover', - 'Hard-Cover', 'King', 'King Size', 'King-Size', 'Limited Series', 'Magazine', - 'NSFW', 'One Shot', 'One-Shot', 'Point 1', 'Preview', 'Prologue', 'Reference', - 'Review', 'Reviewed', 'Scanlation', 'Script', 'Series', 'Sketch', 'Special', - 'TPB', 'Trade Paper Back', 'WebComic', 'Web Comic', 'Year 1', 'Year One' + "1 Shot", "1/2", "1-Shot", "Annotation", "Annotations", + "Annual", "Anthology", "B&W", "B/W", "B&&W", "Black & White", "Box Set", + "Box-Set", "Crossover", "Director's Cut", "Epilogue", "Event", "FCBD", + "Flyer", "Giant", "Giant Size", "Giant-Size", "Graphic Novel", "Hardcover", + "Hard-Cover", "King", "King Size", "King-Size", "Limited Series", "Magazine", + "NSFW", "One Shot", "One-Shot", "Point 1", "Preview", "Prologue", "Reference", + "Review", "Reviewed", "Scanlation", "Script", "Series", "Sketch", "Special", + "TPB", "Trade Paper Back", "WebComic", "Web Comic", "Year 1", "Year One" ] KEYS_PAGE_TYPE = [ - 'FrontCover', 'InnerCover', 'Roundup', 'Story', 'Advertisment', - 'Editorial', 'Letters', 'Preview', 'BackCover', 'Other', 'Deleted' + "FrontCover", "InnerCover", "Roundup", "Story", "Advertisment", + "Editorial", "Letters", "Preview", "BackCover", "Other", "Deleted" ] diff --git a/pycbzhelper/exceptions.py b/pycbzhelper/exceptions.py index 86db50e..9b4d434 100644 --- a/pycbzhelper/exceptions.py +++ b/pycbzhelper/exceptions.py @@ -1,5 +1,5 @@ class PyCBZHelperException(Exception): - """Exceptions used by pycbzhelper.""" + """Exceptions used by PyCBZHelper.""" class InvalidKeyValue(PyCBZHelperException): @@ -10,8 +10,8 @@ class MissingPageFile(PyCBZHelperException): """No page available.""" -class InvalidFilePermission(PyCBZHelperException): - """Unable to delete existing file.""" +class InvalidFileExtension(PyCBZHelperException): + """Invalid file extension.""" class FileNotFound(PyCBZHelperException): diff --git a/pycbzhelper/helper.py b/pycbzhelper/helper.py index 2836834..47ab787 100644 --- a/pycbzhelper/helper.py +++ b/pycbzhelper/helper.py @@ -1,5 +1,3 @@ -import os -import pathlib import shutil import zipfile @@ -7,133 +5,147 @@ from json2xml import json2xml from langcodes import Language from PIL import Image +from pathlib import Path from pycbzhelper.comicinfo import KEYS_STRING, KEYS_INT, KEYS_SPECIAL, KEYS_AGE, KEYS_FORMAT, KEYS_PAGE_TYPE -from pycbzhelper.exceptions import InvalidKeyValue, MissingPageFile, InvalidFilePermission, FileNotFound -from pycbzhelper.utils import get_key_value, delete_none, slugify +from pycbzhelper.exceptions import InvalidKeyValue, MissingPageFile, FileNotFound, InvalidFileExtension +from pycbzhelper.utils import get_key_value, delete_none + +TMP = Path.home().resolve() / ".cbzhelper" class Helper: def __init__(self, kwargs): - self._files = [] - self.metadata = self._get_metadata(kwargs) + self._pages = [] + self.comic_info = self._comic_info(kwargs) - def _get_metadata(self, kwargs) -> str: - # NOTE: Check metadata + def _comic_info(self, kwargs) -> str: for key in KEYS_STRING: - if kwargs.get(key) and not isinstance(kwargs.get(key), str): - raise InvalidKeyValue(f"ERROR: Key must be string: {key}") + value = kwargs.get(key) + if value is not None and not isinstance(value, str): + raise InvalidKeyValue(f"Key must be string: {key}") for key in KEYS_INT: - if kwargs.get(key) and not isinstance(kwargs.get(key), int): - raise InvalidKeyValue(f"ERROR: Key must be integer: {key}") + value = kwargs.get(key) + if value is not None and not isinstance(value, int): + raise InvalidKeyValue(f"Key must be integer: {key}") for key in KEYS_SPECIAL: - if kwargs.get(key): - if key == 'BlackAndWhite' and get_key_value(kwargs.get(key)) not in ['Yes', 'No']: - raise InvalidKeyValue(f"ERROR: Key must be boolean: {key}") - elif key == 'Manga' and get_key_value(kwargs.get(key)) not in ['YesAndRightToLeft', 'Yes', 'No']: - raise InvalidKeyValue(f"ERROR: Key must be boolean or special boolean: {key}") - elif key == 'AgeRating' and kwargs.get(key) not in KEYS_AGE: - raise InvalidKeyValue(f"ERROR: Key must be special age: {key}") - elif key == 'LanguageISO' and not Language.get(kwargs.get(key)).is_valid(): - raise InvalidKeyValue(f"ERROR: Key must be ISO language: {key}") - elif key == 'Format' and kwargs.get(key) not in KEYS_FORMAT: - raise InvalidKeyValue(f"ERROR: Key must be special format: {key}") - elif key == 'Pages': - if isinstance(kwargs.get(key), list): - for page in kwargs.get(key): - if not page.get('File') or not isinstance(page.get('File'), str) or not os.path.exists(page.get('File')): - raise InvalidKeyValue(f"ERROR: Key must be existing string path: {key}") - if page.get('Type') and not page.get('Type') in KEYS_PAGE_TYPE: - raise InvalidKeyValue("ERROR: Key must be special type: Type") - if page.get('DoublePage') and get_key_value(page.get('DoublePage')) not in ['Yes', 'No']: - raise InvalidKeyValue("ERROR: Key must be boolean: DoublePage") + value = kwargs.get(key) + if value is not None: + if key == "BlackAndWhite" and get_key_value(value) not in ["Yes", "No"]: + raise InvalidKeyValue(f"Key must be boolean: {key}") + elif key == "Manga" and get_key_value(value) not in ["YesAndRightToLeft", "Yes", "No"]: + raise InvalidKeyValue(f"Key must be boolean or special boolean: {key}") + elif key == "AgeRating" and value not in KEYS_AGE: + raise InvalidKeyValue(f"Key must be special age: {key}") + elif key == "LanguageISO" and not Language.get(value).is_valid(): + raise InvalidKeyValue(f"Key must be ISO language: {key}") + elif key == "Format" and value not in KEYS_FORMAT: + raise InvalidKeyValue(f"Key must be special format: {key}") + elif key == "Pages": + if isinstance(value, list): + for file in value: + page_file = file.get("File") + if isinstance(page_file, Path): + if not page_file.is_file(): + raise FileNotFound("File does not exist.") + elif isinstance(page_file, str): + if not page_file.startswith("http"): + raise InvalidKeyValue(f"Key must be an existing file path: {key}") + else: + raise InvalidKeyValue(f"Key must be an existing file path: {key}") + + page_type = file.get("Type") + if page_type and page_type not in KEYS_PAGE_TYPE: + raise InvalidKeyValue("Key must be special type: Type") + page_double = file.get("DoublePage") + if page_double and get_key_value(page_double) not in ["Yes", "No"]: + raise InvalidKeyValue("Key must be boolean: DoublePage") else: - raise InvalidKeyValue(f"ERROR: Key must be a list: {key}") - - # NOTE: Set metadata - pages = [] - if kwargs.get('Pages'): - # [{'File': 'FILE_PATH', 'Type': 'FrontCover', 'DoublePage': False, 'Bookmark': '', 'Key': ''}] - pages.append(" ") - kwargs['PageCount'] = len(kwargs.get('Pages')) - for i in range(kwargs.get('PageCount')): - page = kwargs.get('Pages')[i] - if isinstance(page['File'], bytes) or (isinstance(page['File'], str) and page['File'].startswith('https://')): - file = os.path.join("tmp", f"page-{i:03d}.jpg") - with open(file, mode="wb") as f: - f.write(page['File'] if isinstance(page['File'], bytes) else requests.get(url=page['File']).content) - f.close() - page['File'] = file - - if not os.path.exists(page['File']): - raise FileNotFound(f"ERROR: Source file is invalid: {page['File']}") - - properties = Image.open(page['File']) - self._files.append(page['File']) - if not page.get('Type'): - page['Type'] = 'FrontCover' if i == 0 else 'Story' - - items = [' 0: + # [{"File": "FILE_PATH", "Type": "FrontCover", "DoublePage": False, "Bookmark": "", "Key": ""}] + kwargs["PageCount"] = len(pages) + xml_pages.append(" ") + + for i, file in enumerate(pages): + page_file = file["File"] + if isinstance(page_file, str): + r = requests.get(page_file) + r.raise_for_status() + content = r.content + else: + content = page_file.read_bytes() + + page_file = TMP / f"page-{i:03d}.jpg" + page_file.parent.mkdir(parents=True, exist_ok=True) + page_file.write_bytes(content) + self._pages.append(page_file) + + properties = Image.open(page_file) + page_type = file.get("Type") or "FrontCover" if i == 0 else "Story" + + items = [" '.format( - double="False" if get_key_value(page.get('DoublePage', False)) == 'No' else "True", + + page_bookmark = file.get("Bookmark") + if page_bookmark: + items.append(f'Bookmark="{page_bookmark}"') + + page_key = file.get("Key") + if page_key: + items.append(f'Key="{page_key}"') + + xml_pages.append( + " ".join( + items) + ' Image="{image}" ImageHeight="{height}" ImageSize="{size}" ImageWidth="{width}" Type="{type}"/>'.format( + double=str(page_double == "Yes"), image=i, height=properties.height, size=len(properties.fp.read()), width=properties.width, - type=page['Type'] - ) - ) - pages.append(" ") - del kwargs['Pages'] - pages.append("") - - json_data = {} + type=page_type + )) + xml_pages.append(" ") + xml_pages.append("") + kwargs.pop("Pages", None) + + dict_data = {} for key in KEYS_STRING + KEYS_INT + KEYS_SPECIAL: - json_data[key] = get_key_value(kwargs.get(key)) + dict_data[key] = get_key_value(kwargs.get(key)) + + xml_data = json2xml.Json2xml(delete_none(dict_data), wrapper="ComicInfo", pretty=True, attr_type=False, item_wrap=False).to_xml() - xml_data = json2xml.Json2xml(delete_none(json_data), wrapper="ComicInfo", pretty=True, attr_type=False, item_wrap=False).to_xml() if not xml_data: - xml_data = '\n'.join(['', '', '']) - print('WARNING: No metadata to create.') + xml_data = "\n".join(['', "", ""]) + print("WARNING: No metadata to create.") xml_data = xml_data.replace('', '') - xml_data = xml_data.replace('', '\n'.join(pages)) + xml_data = xml_data.replace("", "\n".join(xml_pages)) return xml_data.strip() - def save_cbz(self, path: str, file: str, clear: bool = False, replace: bool = True) -> None: - if len(self._files) == 0: - raise MissingPageFile('ERROR: No pages available.') - - output = os.path.join(path, f"{slugify(file, allow_unicode=False)}.cbz") - if not os.path.exists(path): - os.makedirs(path) - if os.path.exists(output) and not replace: - raise InvalidFilePermission('ERROR: File already exists. The replace option is not enabled.') - elif os.path.exists(output): - os.remove(output) + def create_cbz(self, path: Path) -> None: + if not len(self._pages) > 0: + raise MissingPageFile("No pages available.") + if path.suffix != ".cbz": + raise InvalidFileExtension("Invalid file extension.") + path.parent.mkdir(parents=True, exist_ok=True) + cbz = zipfile.ZipFile(path, "w", compression=zipfile.ZIP_STORED) clear_path = [] - cbz = zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_STORED) - for i in range(len(self._files)): - if os.path.dirname(self._files[i]) not in clear_path: - clear_path.append(os.path.dirname(self._files[i])) - with open(self._files[i], mode='rb') as f: - extension = pathlib.Path(self._files[i]).suffix - cbz.writestr(f"page-{i + 1:03d}.{'.jpg' if extension == '' else extension}", data=f.read()) - f.close() - cbz.writestr('ComicInfo.xml', data=self.metadata.encode('utf-8')) + + for i, page in enumerate(self._pages): + clear_path.append(page) + cbz.writestr(f"page-{i + 1:03d}{page.suffix}", data=page.read_bytes()) + + cbz.writestr("ComicInfo.xml", data=self.comic_info.encode("utf-8")) cbz.close() - print(f"INFO: File create: {output}") - if clear: - for path in clear_path: - shutil.rmtree(path) - print(f"INFO: Folder deleted: {path}") + + self._pages = [] + shutil.rmtree(TMP) diff --git a/pycbzhelper/utils.py b/pycbzhelper/utils.py index 80acd45..e592587 100644 --- a/pycbzhelper/utils.py +++ b/pycbzhelper/utils.py @@ -4,13 +4,10 @@ import unicodedata -def get_key_value(_value) -> str: - if isinstance(_value, str): - if _value in ['YesAndRightToLeft', 'Yes', 'No']: - return _value - elif isinstance(_value, bool): - return 'Yes' if _value else 'No' - return _value +def get_key_value(value: Union[str, bool]) -> str: + if isinstance(value, bool): + return "Yes" if value else "No" + return value def delete_none(_dict: dict) -> dict: @@ -39,9 +36,9 @@ def slugify(value: Union[str, int], allow_unicode: bool = False) -> str: """ value = str(value) if allow_unicode: - value = unicodedata.normalize('NFKC', value) + value = unicodedata.normalize("NFKC", value) else: - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii') + value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") # value = re.sub(r'[^\w\s-]', '', value.lower()) # return re.sub(r'[-\s]+', '-', value).strip('-_') return value diff --git a/setup.py b/setup.py index 3c76193..8539fbc 100644 --- a/setup.py +++ b/setup.py @@ -1,30 +1,41 @@ -"""Setup module""" - -from setuptools import setup - -with open('README.md', 'r') as fh: - LONG_DESCRIPTION = fh.read() +from setuptools import setup, find_packages setup( - name='pycbzhelper', - version='3.0.1', - description='Python library to create a cbz file with metadata.', - long_description=LONG_DESCRIPTION, - long_description_content_type='text/markdown', - url='https://github.com/hyugogirubato/pycbzhelper', - author='hyugogirubato', - author_email='hyugogirubato@gmail.com', - license='GNU GPLv3', - packages=['pycbzhelper'], - install_requires=['json2xml', 'langcodes', 'pillow'], + name="pycbzhelper", + version="3.1.0", + author="hyugogirubato", + author_email="hyugogirubato@gmail.com", + description="Python library for creating CBZ files with metadata.", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + url="https://github.com/hyugogirubato/pycbzhelper", + packages=find_packages(), + license="GPL-3.0-only", + keywords=[ + "metadata", + "manga", + "comics", + "ebooks", + "cbz" + ], classifiers=[ - 'Environment :: Console', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Utilities' - ] + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Utilities" + ], + install_requires=[ + "requests", + "json2xml", + "langcodes", + "Pillow" + ], + python_requires=">=3.7" ) diff --git a/test.py b/test.py new file mode 100644 index 0000000..fa23831 --- /dev/null +++ b/test.py @@ -0,0 +1,42 @@ +from pathlib import Path + +import pycbzhelper + +PARENT = Path(__file__).resolve().parent + +if __name__ == "__main__": + pages = [] + for i in range(11): + pages.append({"File": (PARENT / "docs" / "images" / f"page-{i:03d}.jpg")}) + + metadata = { + "Title": "T1 - Arrête de me chauffer, Nagatoro", + "Series": "Arrête de me chauffer, Nagatoro", + "Number": "1", + "Count": 8, + "Volume": 1, + "Summary": "Nagatoro est en seconde. Pleine d\u2019assurance, joueuse, moqueuse, elle se d\u00e9couvre un jour un passe-temps favori : martyriser son \u201cSenpai\u201d, lyc\u00e9en de premi\u00e8re timide et mal dans sa peau. Nagatoro taquine, agace, aguiche, va parfois trop loin... mais qu\u2019a-t-elle vraiment derri\u00e8re la t\u00eate ? Et si derri\u00e8re ses moqueries elle cachait une v\u00e9ritable affection ? Et si finalement, ses farces permettaient \u00e0 Senpai de s\u2019affirmer ?", + "Year": 2021, + "Month": 3, + "Day": 12, + "Writer": "Nanashi", + "Inker": "Nanashi", + "Editor": "Noeve Grafx", + "Publisher": "Noeve Grafx", + "Imprint": "Noeve Grafx", + "Genre": "Shonen", + "Web": "http://www.izneo.com/en/manga/shonen/arrete-de-me-chauffer-nagatoro-37560/arrete-de-me-chauffer-nagatoro-86232", + "LanguageISO": "fr", + "Format": "Preview", + "BlackAndWhite": True, + "Manga": "YesAndRightToLeft", + "AgeRating": "Everyone 10+", + "CommunityRating": "5.0", + "ean": "9782490676569", + "Pages": pages + } + + path = PARENT / "eBooks" / "Arrête de me chauffer, Nagatoro.cbz" + helper = pycbzhelper.Helper(metadata) + helper.create_cbz(path=path) + print(f"I: File created: {path}") diff --git a/tests.py b/tests.py deleted file mode 100644 index 6e1393a..0000000 --- a/tests.py +++ /dev/null @@ -1,43 +0,0 @@ -import os - -import pycbzhelper - -if __name__ == '__main__': - pages = [] - for i in range(11): - pages.append({'File': os.path.join('images', f"page-{i:03d}.jpg")}) - - metadata = { - 'Title': 'T1 - Arrête de me chauffer, Nagatoro', - 'Series': 'Arrête de me chauffer, Nagatoro', - 'Number': '1', - 'Count': 8, - 'Volume': 1, - 'Summary': 'Nagatoro est en seconde. Pleine d\u2019assurance, joueuse, moqueuse, elle se d\u00e9couvre un jour un passe-temps favori : martyriser son \u201cSenpai\u201d, lyc\u00e9en de premi\u00e8re timide et mal dans sa peau. Nagatoro taquine, agace, aguiche, va parfois trop loin... mais qu\u2019a-t-elle vraiment derri\u00e8re la t\u00eate ? Et si derri\u00e8re ses moqueries elle cachait une v\u00e9ritable affection ? Et si finalement, ses farces permettaient \u00e0 Senpai de s\u2019affirmer ?', - 'Year': 2021, - 'Month': 3, - 'Day': 12, - 'Writer': 'Nanashi', - 'Inker': 'Nanashi', - 'Editor': 'Noeve Grafx', - 'Publisher': 'Noeve Grafx', - 'Imprint': 'Noeve Grafx', - 'Genre': 'Shonen', - 'Web': 'http://www.izneo.com/en/manga/shonen/arrete-de-me-chauffer-nagatoro-37560/arrete-de-me-chauffer-nagatoro-86232', - 'LanguageISO': 'fr', - 'Format': 'Preview', - 'BlackAndWhite': True, - 'Manga': 'YesAndRightToLeft', - 'AgeRating': 'Everyone 10+', - 'CommunityRating': '5.0', - 'ean': '9782490676569', - 'Pages': pages - } - - helper = pycbzhelper.Helper(metadata) - helper.save_cbz( - path=os.path.join('eBooks', 'Arrête de me chauffer, Nagatoro'), - file='T1 - Arrête de me chauffer, Nagatoro', - clear=False, - replace=True - )