Compare commits

...
11 Commits
Author SHA1 Message Date
hyugogirubato a005ef8127 Release v3.2.0 2024-03-17 14:49:27 +01:00
hyugogirubato 0e3e3840c7 Release v3.2.0 2024-03-17 14:48:48 +01:00
hyugogirubato 4fbb7fd73a Release v3.2.0 2024-03-17 14:47:33 +01:00
hyugogirubato 2edbd2f0c7 Merge pull request #3 from domenicoblanco/main
chore: add Translator key
2024-02-18 14:04:45 +01:00
Domenico Blanco 75ceec8a44 chore: add Translator key 2024-02-13 14:12:49 +01:00
hyugogirubato af878ca167 Release v3.1.2 2023-08-13 18:27:57 +02:00
hyugogirubato ea59025499 Release v3.1.2 2023-08-13 18:26:52 +02:00
hyugogirubato da535bdc67 Merge pull request #2 from RivMt/main
Bug: Tags does not written in ComicInfo.xml (include solution)
2023-08-13 18:21:46 +02:00
San Kang c9a4d94816 Fix: Add Tags to KEYS_STRING
Helper does not write `Tags` element in `ComicInfo.xml` because there is no `Tags` in `KEYS_STRING`
2023-08-11 08:22:34 +09:00
hyugogirubato 17ceafb384 Update test 2023-06-05 12:33:00 +02:00
hyugogirubato ac00ee20a3 Update README.md 2023-06-04 16:13:47 +02:00
27 changed files with 602 additions and 379 deletions
+176
View File
@@ -0,0 +1,176 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# 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
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__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/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python
+22 -3
View File
@@ -5,6 +5,23 @@ 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.2.0] - 2024-03-17
### Changed
- New code structure.
- Deleting temporary files usage.
- Automatic correction of variable types.
- Simplifying constants.
- Binary input support for pages.
- Project `pycbzhelper` renamed to `cbz`.
## [3.1.2] - 2023-08-13
### Fixed
- Fixed missing `Tags` key, thanks to @RivMt
## [3.1.1] - 2023-06-04
### Changed
@@ -36,6 +53,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Initial release.
[3.1.1]: https://github.com/hyugogirubato/pycbzhelper/releases/tag/v3.1.1
[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
[3.2.0]: https://github.com/hyugogirubato/cbz/releases/tag/v3.2.0
[3.1.2]: https://github.com/hyugogirubato/cbz/releases/tag/v3.1.2
[3.1.1]: https://github.com/hyugogirubato/cbz/releases/tag/v3.1.1
[3.1.0]: https://github.com/hyugogirubato/cbz/releases/tag/v3.1.0
[3.0.1]: https://github.com/hyugogirubato/cbz/releases/tag/v3.0.1
+50 -49
View File
@@ -1,73 +1,74 @@
# PyCBZHelper
# CBZ
[![License](https://img.shields.io/github/license/hyugogirubato/pycbzhelper)](https://github.com/hyugogirubato/pycbzhelper/blob/main/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/)
[![License](https://img.shields.io/github/license/hyugogirubato/cbz)](https://github.com/hyugogirubato/cbz/blob/main/LICENSE)
[![Release](https://img.shields.io/github/release-date/hyugogirubato/cbz)](https://github.com/hyugogirubato/cbz/releases)
[![Latest Version](https://img.shields.io/pypi/v/cbz)](https://pypi.org/project/cbz/)
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.
CBZ is a Python package designed to facilitate the creation, manipulation, and extraction of comic book archive files in the CBZ format. It allows users to programmatically generate CBZ files from a collection of images, add metadata to the archives, and unpack existing CBZ files. This library is ideal for comic book enthusiasts, archivists, and developers working on applications involving digital comic book distributions.
## 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.
- **Create CBZ Files**: Pack a series of images into a CBZ file with ease.
- **Extract CBZ Files**: Unpack images and metadata from existing CBZ files.
- **Manage Metadata**: Add, update, and retrieve metadata from CBZ files, including titles, authors, volume numbers, and more.
- **Support for ComicInfo.xml**: Utilizes the ComicInfo.xml standard for embedding comic book metadata within CBZ files.
- **Flexible Image Handling**: Load images from various formats and ensure they are correctly formatted and ordered within the CBZ file.
- **High-Level API**: Offers a simple, high-level API for common tasks, while also providing access to lower-level functions for advanced usage.
## Installation
You can install PyCBZHelper using pip:
To install the CBZ library, you can use pip:
````shell
pip install pycbzhelper
````
```bash
pip install cbz
```
## Usage
## Quick Start
Here's a basic example of how to use PyCBZHelper:
Here's a quick example of how to create a CBZ file from a series of images:
````python
from pycbzhelper import Helper
```python
from pathlib import Path
from cbz.page import PageInfo
from cbz.comic import ComicInfo
from cbz.constants import PageType, YesNo, Manga, AgeRating, Format
PARENT = Path(__name__).resolve().parent
if __name__ == '__main__':
# Define the path to your images
images_path = Path('path/to/your/images')
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
}
# Load images and create page objects
pages = [PageInfo.load(path) for path in images_path.iterdir()]
# Define the path to the output CBZ file
output_path = PARENT / "output.cbz"
# Create a ComicInfo object with your comic's metadata
comic = ComicInfo.from_pages(
pages=pages,
title='Your Comic Title',
series='Your Comic Series',
number='1',
language_iso='en',
format=Format.WEB_COMIC,
black_white=YesNo.NO,
manga=Manga.NO,
age_rating=AgeRating.PENDING
)
# Create an instance of the Helper class
helper = Helper(metadata)
# Pack the comic into a CBZ file
cbz_content = comic.pack()
# Create the CBZ file
helper.create_cbz(output_path)
````
# Save the CBZ file
cbz_path = Path('your_comic.cbz')
cbz_path.write_bytes(cbz_content)
```
For more information on how to use PyCBZHelper, please refer to
the [documentation](https://github.com/hyugogirubato/pycbzhelper/blob/main/docs/schema).
## Documentation
### Exceptions
For detailed documentation, including the full API reference and more examples, please refer to the [official CBZ Library documentation](https://en.wikipedia.org/wiki/Comic_book_archive).
The following exceptions can be raised by PyCBZHelper:
## License
- `InvalidKeyValue`: Raised when a key value is invalid.
- `MissingPageFile`: Raised when no page files are available.
- `InvalidFileExtension`: Raised when an invalid file extension is used.
- `FileNotFound`: Raised when the specified source file is not found.
The CBZ Library is released under the MIT License. See [LICENSE](LICENSE) for details.
### License
## Acknowledgments
This project is licensed under the [GPL v3 License](https://github.com/hyugogirubato/pycbzhelper/blob/main/LICENSE).
This library was developed with the needs of comic book fans and digital archivists in mind. We hope it helps you in managing and enjoying your comic book collections.
+5
View File
@@ -0,0 +1,5 @@
from .comic import ComicInfo
from .constants import *
from .page import PageInfo
__version__ = '3.2.0'
+110
View File
@@ -0,0 +1,110 @@
from __future__ import annotations
import io
import json
import zipfile
from typing import Union
import xmltodict
from langcodes import Language
from pathlib import Path
from cbz import utils
from cbz.constants import YesNo, Manga, AgeRating, Format
from cbz.page import PageInfo
def rating(value: int) -> int:
assert -1 <= value <= 5, 'Rating must be between 0 and 5'
return value
def language(value: str) -> str:
if value and not Language.get(value).is_valid():
raise ValueError('Invalid language')
return value
class ComicInfo:
def __init__(self, pages: [PageInfo], **kwargs):
self.__pages = pages
self.__info = {
'Title': str(kwargs.get('title', '')),
'Series': str(kwargs.get('series', '')),
'Number': str(kwargs.get('number', '')),
'Count': int(kwargs.get('count', -1)),
'Volume': int(kwargs.get('volume', -1)),
'AlternateSeries': str(kwargs.get('alternate_series', '')),
'AlternateNumber': str(kwargs.get('alternate_number', '')),
'AlternateCount': int(kwargs.get('alternate_count', -1)),
'Summary': str(kwargs.get('summary', '')),
'Notes': str(kwargs.get('notes', '')),
'Year': int(kwargs.get('year', -1)),
'Month': int(kwargs.get('month', -1)),
'Day': int(kwargs.get('day', -1)),
'Writer': str(kwargs.get('writer', '')),
'Penciller': str(kwargs.get('penciller', '')),
'Inker': str(kwargs.get('inker', '')),
'Colorist': str(kwargs.get('colorist', '')),
'Letterer': str(kwargs.get('letterer', '')),
'CoverArtist': str(kwargs.get('cover_artist', '')),
'Editor': str(kwargs.get('editor', '')),
'Translator': str(kwargs.get('translator', '')),
'Publisher': str(kwargs.get('publisher', '')),
'Imprint': str(kwargs.get('imprint', '')),
'Genre': str(kwargs.get('genre', '')),
'Tags': str(kwargs.get('tags', '')),
'Web': str(kwargs.get('web', '')),
'PageCount': len(self.__pages),
'LanguageISO': language(kwargs.get('language_iso', '')),
'Format': Format(kwargs.get('format', Format.UNKNOWN)),
'EAN': str(kwargs.get('ean', '')),
'BlackAndWhite': YesNo(kwargs.get('black_white', YesNo.UNKNOWN)),
'Manga': Manga(kwargs.get('manga', Manga.UNKNOWN)),
'Characters': str(kwargs.get('characters', '')),
'Teams': str(kwargs.get('teams', '')),
'Locations': str(kwargs.get('locations', '')),
'ScanInformation': str(kwargs.get('scan_information', '')),
'StoryArc': str(kwargs.get('story_arc', '')),
'StoryArcNumber': str(kwargs.get('story_arc_number', '')),
'SeriesGroup': str(kwargs.get('series_group', '')),
'AgeRating': AgeRating(kwargs.get('age_rating', AgeRating.UNKNOWN)),
'Pages': [{'Image': i, **page.dumps()} for i, page in enumerate(pages)],
'CommunityRating': rating(kwargs.get('community_rating', -1)),
'MainCharacterOrTeam': str(kwargs.get('main_character_or_team', '')),
'Review': str(kwargs.get('review', ''))
}
def dumps(self) -> dict:
return utils.dumps(self.__info)
def __repr__(self) -> str:
return json.dumps(self.dumps(), indent=2)
@classmethod
def from_pages(cls, pages: [PageInfo], **kwargs) -> ComicInfo:
return cls(pages, **kwargs)
@classmethod
def from_cbz(cls, path: Union[Path, str]) -> ComicInfo:
if not isinstance(path, (Path, str)):
raise ValueError(f'Expecting Path object or path string, got {path!r}')
with Path(path).open(mode='rb') as f:
return cls(cls.__unpack(f.read()))
def __unpack(self, data: bytes) -> tuple[[PageInfo], dict]:
raise NotImplemented
def pack(self) -> bytes:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_STORED) as zip_file:
zip_file.writestr(
'ComicInfo.xml',
xmltodict.unparse({'ComicInfo': self.dumps()}, pretty=True).encode('utf-8')
)
for i, page in enumerate(self.__pages):
zip_file.writestr(f'page-{i + 1:03d}{page.suffix}', page.content)
return zip_buffer.getvalue()
+100
View File
@@ -0,0 +1,100 @@
from enum import Enum
class YesNo(Enum):
UNKNOWN = 'Unknown'
NO = 'No'
YES = 'Yes'
class Manga(Enum):
UNKNOWN = 'Unknown'
NO = 'No'
YES = 'Yes'
RIGHT_LEFT = 'YesAndRightToLeft'
class PageType(Enum):
FRONT_COVER = 'FrontCover'
INNER_COVER = 'InnerCover'
ROUNDUP = 'Roundup'
STORY = 'Story'
ADVERTISEMENT = 'Advertisement'
EDITORIAL = 'Editorial'
LETTERS = 'Letters'
PREVIEW = 'Preview'
BACK_COVER = 'BackCover'
OTHER = 'Other'
DELETED = 'Deleted'
class AgeRating(Enum):
UNKNOWN = 'Unknown'
ADULTS18 = 'Adults Only 18+'
CHILDHOOD = 'Early Childhood'
EVERYONE = 'Everyone'
EVERYONE10 = 'Everyone 10+'
G = 'G'
KIDS = 'Kids to Adults'
M = 'M'
MA15 = 'MA15+'
MATURE17 = 'Mature 17+'
PG = 'PG'
R18 = 'R18+'
PENDING = 'Rating Pending'
TEEN = 'Teen'
X18 = 'X18+'
class Format(Enum):
UNKNOWN = 'Unknown'
# ONE_SHOT = '1 Shot'
# ONE_SHOT = '1/2',
# ONE_SHOT = '1-Shot'
ANNOTATION = 'Annotation'
# ANNOTATIONS = 'Annotations'
ANNUAL = 'Annual'
ANTHOLOGY = 'Anthology'
# BLACK_WHITE = 'B&W'
# BLACK_WHITE = 'B/W'
# BLACK_WHITE = 'B&&W'
BLACK_WHITE = 'Black & White'
# BOX_SET = 'Box Set'
BOX_SET = 'Box-Set'
CROSSOVER = 'Crossover'
DIRECTOR_CUT = 'Director\'s Cut'
EPILOGUE = 'Epilogue'
EVENT = 'Event'
FCBD = 'FCBD'
FLYER = 'Flyer'
# GIANT_SIZE = 'Giant'
# GIANT_SIZE = 'Giant Size'
GIANT_SIZE = 'Giant-Size'
GRAPHIC_NOVEL = 'Graphic Novel'
# HARDCOVER = 'Hardcover'
HARDCOVER = 'Hard-Cover'
# KING_SIZE = 'King'
# KING_SIZE = 'King Size'
KING_SIZE = 'King-Size'
LIMITED_SERIES = 'Limited Series'
MAGAZINE = 'Magazine'
NSFW = 'NSFW'
# ONE_SHOT = 'One Shot'
ONE_SHOT = 'One-Shot'
POINT1 = 'Point 1'
PREVIEW = 'Preview'
PROLOGUE = 'Prologue'
REFERENCE = 'Reference'
REVIEW = 'Review'
REVIEWED = 'Reviewed'
SCANLATION = 'Scanlation'
SCRIPT = 'Script'
SERIES = 'Series'
SKETCH = 'Sketch'
SPECIAL = 'Special'
# TRADE_PAPER_BACK = 'TPB'
TRADE_PAPER_BACK = 'Trade Paper Back'
# WEB_COMIC = 'WebComic'
WEB_COMIC = 'Web Comic'
# YEAR_ONE = 'Year 1'
YEAR_ONE = 'Year One'
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
import base64
import json
from io import BytesIO
from typing import Union
from pathlib import Path
from PIL import Image
from cbz import utils
from cbz.constants import PageType
class PageInfo:
def __init__(self, content: bytes, **kwargs):
image = Image.open(BytesIO(content))
self.suffix = f'.{image.format.lower()}'
self.content = content
self.__info = {
# 'Image': 0,
'Type': PageType(kwargs.get('type', PageType.STORY)),
'DoublePage': bool(kwargs.get('double', False)),
'ImageSize': len(content),
'Key': str(kwargs.get('key', '')),
'Bookmark': str(kwargs.get('bookmark', '')),
'ImageWidth': int(image.width),
'ImageHeight': int(image.height)
}
def dumps(self) -> dict:
return utils.dumps(self.__info)
def __repr__(self) -> str:
return json.dumps(self.dumps(), indent=2)
@classmethod
def loads(cls, data: Union[str, bytes], **kwargs) -> PageInfo:
if isinstance(data, str):
data = base64.b64decode(data)
if not isinstance(data, bytes):
raise ValueError(f'Expecting Bytes or Base64 input, got {data!r}')
return cls(data, **kwargs)
@classmethod
def load(cls, path: Union[Path, str], **kwargs) -> PageInfo:
if not isinstance(path, (Path, str)):
raise ValueError(f'Expecting Path object or path string, got {path!r}')
with Path(path).open(mode='rb') as f:
return cls(f.read(), **kwargs)
+5
View File
@@ -0,0 +1,5 @@
from enum import Enum
def dumps(data: dict) -> dict:
return {k: v.value if isinstance(v, Enum) else v for k, v in data.items() if v and v != -1 and v != 'Unknown'}
+50
View File
@@ -0,0 +1,50 @@
from pathlib import Path
from cbz.comic import ComicInfo
from cbz.constants import PageType, YesNo, Manga, AgeRating, Format
from cbz.page import PageInfo
PARENT = Path()
if __name__ == '__main__':
# @Info: Load comic pages
paths = list((PARENT / 'images').iterdir())
pages = [PageInfo.load(
path=path,
type=PageType.FRONT_COVER if i == 0 else PageType.BACK_COVER if i == len(paths) - 1 else PageType.STORY
) for i, path in enumerate(paths)]
# @Info: Load comic metadata
comic = ComicInfo.from_pages(
pages=pages,
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',
language_iso='fr',
format=Format.PREVIEW,
black_white=YesNo.YES,
manga=Manga.RIGHT_LEFT,
age_rating=AgeRating.EVERYONE10,
community_rating=5,
ean='9782490676569'
)
# @Info: Pack cbz
cbz_content = comic.pack()
# @Info: Write cbz
cbz_path = PARENT / 'Arrête de me chauffer, Nagatoro.cbz'
cbz_path.write_bytes(cbz_content)

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Before

Width:  |  Height:  |  Size: 619 KiB

After

Width:  |  Height:  |  Size: 619 KiB

Before

Width:  |  Height:  |  Size: 592 KiB

After

Width:  |  Height:  |  Size: 592 KiB

Before

Width:  |  Height:  |  Size: 680 KiB

After

Width:  |  Height:  |  Size: 680 KiB

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 590 KiB

Before

Width:  |  Height:  |  Size: 492 KiB

After

Width:  |  Height:  |  Size: 492 KiB

Before

Width:  |  Height:  |  Size: 479 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 549 KiB

-5
View File
@@ -1,5 +0,0 @@
from .helper import *
from .comicinfo import *
from .utils import slugify
__version__ = "3.1.1"
-37
View File
@@ -1,37 +0,0 @@
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"
]
KEYS_INT = [
"Count", "Volume", "AlternateCount", "Year", "Month", "Day", "PageCount"
]
KEYS_SPECIAL = [
"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"
]
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"
]
KEYS_PAGE_TYPE = [
"FrontCover", "InnerCover", "Roundup", "Story", "Advertisment",
"Editorial", "Letters", "Preview", "BackCover", "Other", "Deleted"
]
-18
View File
@@ -1,18 +0,0 @@
class PyCBZHelperException(Exception):
"""Exceptions used by PyCBZHelper."""
class InvalidKeyValue(PyCBZHelperException):
"""The key value is invalid."""
class MissingPageFile(PyCBZHelperException):
"""No page available."""
class InvalidFileExtension(PyCBZHelperException):
"""Invalid file extension."""
class FileNotFound(PyCBZHelperException):
"""The specified source file was not found."""
-151
View File
@@ -1,151 +0,0 @@
import shutil
import zipfile
import requests
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, FileNotFound, InvalidFileExtension
from pycbzhelper.utils import get_key_value, delete_none
TMP = Path.home().resolve() / ".cbzhelper"
class Helper:
def __init__(self, kwargs):
self._pages = []
self.comic_info = self._comic_info(kwargs)
def _comic_info(self, kwargs) -> str:
for key in KEYS_STRING:
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:
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:
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"Key must be a list: {key}")
xml_pages = []
pages = kwargs.get("Pages") or []
if len(pages) > 0:
# [{"File": "FILE_PATH", "Type": "FrontCover", "DoublePage": False, "Bookmark": "", "Key": ""}]
kwargs["PageCount"] = len(pages)
xml_pages.append(" <Pages>")
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 = [" <Page"]
page_double = get_key_value(file.get("DoublePage", False))
if page_double == "Yes":
items.append('DoublePage="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
))
xml_pages.append(" </Pages>")
xml_pages.append("</ComicInfo>")
kwargs.pop("Pages", None)
dict_data = {}
for key in KEYS_STRING + KEYS_INT + KEYS_SPECIAL:
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()
if not xml_data:
xml_data = "\n".join(['<?xml version="1.0" ?>', "<ComicInfo>", "</ComicInfo>"])
print("WARNING: No metadata to create.")
xml_data = xml_data.replace('<?xml version="1.0" ?>', '<?xml version="1.0" encoding="utf-8"?>')
xml_data = xml_data.replace("</ComicInfo>", "\n".join(xml_pages))
return xml_data.strip()
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 = []
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()
self._pages = []
shutil.rmtree(TMP)
-44
View File
@@ -1,44 +0,0 @@
from __future__ import annotations
from typing import Union
import unicodedata
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:
"""Delete None values recursively from all of the dictionaries, tuples, lists, sets"""
if isinstance(_dict, dict):
for key, value in list(_dict.items()):
if isinstance(value, (list, dict, tuple, set)):
_dict[key] = delete_none(value)
elif value is None or key is None:
del _dict[key]
if value == {}:
del _dict[key]
elif isinstance(_dict, (list, set, tuple)):
_dict = type(_dict)(delete_none(item) for item in _dict if item is not None)
return _dict
def slugify(value: Union[str, int], allow_unicode: bool = False) -> str:
"""
Taken from https://github.com/django/django/blob/master/django/utils/text.py
Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
dashes to single dashes. Remove characters that aren't alphanumerics,
underscores, or hyphens. Convert to lowercase. Also strip leading and
trailing whitespace, dashes, and underscores.
"""
value = str(value)
if allow_unicode:
value = unicodedata.normalize("NFKC", value)
else:
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
+31 -30
View File
@@ -1,41 +1,42 @@
from setuptools import setup, find_packages
setup(
name="pycbzhelper",
version="3.1.1",
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",
name='cbz',
version='3.2.0',
author='hyugogirubato',
author_email='hyugogirubato@gmail.com',
description='CBZ is a Python package designed to facilitate the creation, manipulation, and extraction of comic book archive files in the CBZ format.',
long_description=open('README.md').read(),
long_description_content_type='text/markdown',
url='https://github.com/hyugogirubato/cbz',
packages=find_packages(),
license="GPL-3.0-only",
license='GPL-3.0-only',
keywords=[
"metadata",
"manga",
"comics",
"ebooks",
"cbz"
'metadata',
'manga',
'comics',
'ebooks',
'cbz',
'webtoons'
],
classifiers=[
"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"
'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"
'requests',
'xmltodict',
'langcodes',
'Pillow'
],
python_requires=">=3.7"
python_requires='>=3.7'
)
-42
View File
@@ -1,42 +0,0 @@
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}")