Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a005ef8127 | ||
|
|
0e3e3840c7 | ||
|
|
4fbb7fd73a | ||
|
|
2edbd2f0c7 | ||
|
|
75ceec8a44 | ||
|
|
af878ca167 | ||
|
|
ea59025499 | ||
|
|
da535bdc67 | ||
|
|
c9a4d94816 | ||
|
|
17ceafb384 | ||
|
|
ac00ee20a3 |
@@ -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
|
||||||
@@ -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
|
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).
|
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
|
## [3.1.1] - 2023-06-04
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -36,6 +53,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
- Initial release.
|
- Initial release.
|
||||||
|
|
||||||
[3.1.1]: https://github.com/hyugogirubato/pycbzhelper/releases/tag/v3.1.1
|
[3.2.0]: https://github.com/hyugogirubato/cbz/releases/tag/v3.2.0
|
||||||
[3.1.0]: https://github.com/hyugogirubato/pycbzhelper/releases/tag/v3.1.0
|
[3.1.2]: https://github.com/hyugogirubato/cbz/releases/tag/v3.1.2
|
||||||
[3.0.1]: https://github.com/hyugogirubato/pycbzhelper/releases/tag/v3.0.1
|
[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
|
||||||
|
|||||||
@@ -1,73 +1,74 @@
|
|||||||
# PyCBZHelper
|
# CBZ
|
||||||
|
|
||||||
[](https://github.com/hyugogirubato/pycbzhelper/blob/main/LICENSE)
|
[](https://github.com/hyugogirubato/cbz/blob/main/LICENSE)
|
||||||
[](https://github.com/hyugogirubato/pycbzhelper/releases)
|
[](https://github.com/hyugogirubato/cbz/releases)
|
||||||
[](https://pypi.org/project/pycbzhelper/)
|
[](https://pypi.org/project/cbz/)
|
||||||
|
|
||||||
PyCBZHelper is a Python library for creating CBZ (Comic Book Zip) files with metadata. It provides functionality to
|
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.
|
||||||
generate a CBZ file from a list of image pages and associated comic book metadata.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Create CBZ files from images.
|
- **Create CBZ Files**: Pack a series of images into a CBZ file with ease.
|
||||||
- Generate ComicInfo.xml metadata.
|
- **Extract CBZ Files**: Unpack images and metadata from existing CBZ files.
|
||||||
- Support for various metadata fields.
|
- **Manage Metadata**: Add, update, and retrieve metadata from CBZ files, including titles, authors, volume numbers, and more.
|
||||||
- Handle page files from local disk or web URLs.
|
- **Support for ComicInfo.xml**: Utilizes the ComicInfo.xml standard for embedding comic book metadata within CBZ files.
|
||||||
- Automatic cleanup of temporary 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
|
## Installation
|
||||||
|
|
||||||
You can install PyCBZHelper using pip:
|
To install the CBZ library, you can use pip:
|
||||||
|
|
||||||
````shell
|
```bash
|
||||||
pip install pycbzhelper
|
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
|
```python
|
||||||
from pycbzhelper import Helper
|
|
||||||
from pathlib import Path
|
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__":
|
# Load images and create page objects
|
||||||
# Define metadata for the comic
|
pages = [PageInfo.load(path) for path in images_path.iterdir()]
|
||||||
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
|
# Create a ComicInfo object with your comic's metadata
|
||||||
output_path = PARENT / "output.cbz"
|
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
|
# Pack the comic into a CBZ file
|
||||||
helper = Helper(metadata)
|
cbz_content = comic.pack()
|
||||||
|
|
||||||
# Create the CBZ file
|
# Save the CBZ file
|
||||||
helper.create_cbz(output_path)
|
cbz_path = Path('your_comic.cbz')
|
||||||
````
|
cbz_path.write_bytes(cbz_content)
|
||||||
|
```
|
||||||
|
|
||||||
For more information on how to use PyCBZHelper, please refer to
|
## Documentation
|
||||||
the [documentation](https://github.com/hyugogirubato/pycbzhelper/blob/main/docs/schema).
|
|
||||||
|
|
||||||
### 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.
|
The CBZ Library is released under the MIT License. See [LICENSE](LICENSE) for details.
|
||||||
- `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.
|
|
||||||
|
|
||||||
### 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.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .comic import ComicInfo
|
||||||
|
from .constants import *
|
||||||
|
from .page import PageInfo
|
||||||
|
|
||||||
|
__version__ = '3.2.0'
|
||||||
@@ -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()
|
||||||
@@ -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'
|
||||||
@@ -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)
|
||||||
@@ -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'}
|
||||||
@@ -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 |
@@ -1,5 +0,0 @@
|
|||||||
from .helper import *
|
|
||||||
from .comicinfo import *
|
|
||||||
from .utils import slugify
|
|
||||||
|
|
||||||
__version__ = "3.1.1"
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
@@ -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."""
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -1,41 +1,42 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="pycbzhelper",
|
name='cbz',
|
||||||
version="3.1.1",
|
version='3.2.0',
|
||||||
author="hyugogirubato",
|
author='hyugogirubato',
|
||||||
author_email="hyugogirubato@gmail.com",
|
author_email='hyugogirubato@gmail.com',
|
||||||
description="Python library for creating CBZ files with metadata.",
|
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=open('README.md').read(),
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type='text/markdown',
|
||||||
url="https://github.com/hyugogirubato/pycbzhelper",
|
url='https://github.com/hyugogirubato/cbz',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
license="GPL-3.0-only",
|
license='GPL-3.0-only',
|
||||||
keywords=[
|
keywords=[
|
||||||
"metadata",
|
'metadata',
|
||||||
"manga",
|
'manga',
|
||||||
"comics",
|
'comics',
|
||||||
"ebooks",
|
'ebooks',
|
||||||
"cbz"
|
'cbz',
|
||||||
|
'webtoons'
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
'Development Status :: 5 - Production/Stable',
|
||||||
"Intended Audience :: Developers",
|
'Intended Audience :: Developers',
|
||||||
"Intended Audience :: End Users/Desktop",
|
'Intended Audience :: End Users/Desktop',
|
||||||
"Natural Language :: English",
|
'Natural Language :: English',
|
||||||
"Operating System :: OS Independent",
|
'Operating System :: OS Independent',
|
||||||
"Programming Language :: Python :: 3",
|
'Programming Language :: Python :: 3',
|
||||||
"Programming Language :: Python :: 3.7",
|
'Programming Language :: Python :: 3.7',
|
||||||
"Programming Language :: Python :: 3.8",
|
'Programming Language :: Python :: 3.8',
|
||||||
"Programming Language :: Python :: 3.9",
|
'Programming Language :: Python :: 3.9',
|
||||||
"Programming Language :: Python :: 3.10",
|
'Programming Language :: Python :: 3.10',
|
||||||
"Topic :: Utilities"
|
'Topic :: Utilities'
|
||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"requests",
|
'requests',
|
||||||
"json2xml",
|
'xmltodict',
|
||||||
"langcodes",
|
'langcodes',
|
||||||
"Pillow"
|
'Pillow'
|
||||||
],
|
],
|
||||||
python_requires=">=3.7"
|
python_requires='>=3.7'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}")
|
|
||||||