This commit is contained in:
hyugogirubato
2023-02-06 10:42:08 +01:00
parent 1629a79694
commit b0a8cde0f0
25 changed files with 786 additions and 186 deletions
+101
View File
@@ -0,0 +1,101 @@
# 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/
+23
View File
@@ -0,0 +1,23 @@
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
+34 -21
View File
@@ -1,25 +1,38 @@
# CBZ-Helper
In order to be able to save digital books from images while saving metadata, this library will allow you to do it simply in python without having to go through a graphical client or external commands.
# pydvdfab
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
Python library to create a cbz file with metadata.
# Usage
### Basic Usage
## Use
```python
import cbzhelper
if __name__ == '__main__':
pages = [{'page_number': 0, 'content': b'', 'double_page': False}]
helper = cbzhelper.Helper('eBook', 'Volume 1', replace=True)
for page in pages:
helper.addPage(page['page_number'], page['content'], double_page=page['double_page'])
helper.addMetadata({
'Title': 'Volume 1',
'Series': 'Exemples',
'Number': '1',
'Volume': 1
})
helper.saveCBZ()
>>> 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
... )
```
---
*This scripts are created by __hyugogirubato__.
Find us on [discord](https://discord.com/invite/g6JzYbh) for more information on projects in development.*
# 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)
-165
View File
@@ -1,165 +0,0 @@
"""
Project: cbzhelper
File: cbzhelper.py
Author: hyuogirubato
Date: 2022.08.30
"""
import os.path
import re
import shutil
import zipfile
from PIL import Image
def clear(value):
return re.sub('[^\w\-_\.\(\)\[\] ]', '_', value)
class Helper:
def __init__(self, path, file, replace=True):
self.path = path
self.file = f"{clear(file)}.cbz"
self.output = os.path.join(self.path, self.file)
self.tmp = os.path.join(self.path, 'tmp')
self.pages = []
if not os.path.exists(path):
os.makedirs(path)
if os.path.exists(self.output) and replace:
os.remove(self.output)
# https://github.com/Kussie/ComicInfoStandard/blob/main/ComicInfo.xsd
def addMetadata(self, metadata):
metadata['Pages'] = self.pages
comic_config = {
'str': ['Title', 'Series', 'Number', 'AlternateSeries', 'AlternateNumber',
'Summary', 'Notes', 'Writer', 'Penciller', 'Inker', 'Colorist',
'Letterer', 'CoverArtist', 'Editor', 'Publisher', 'Imprint',
'Genre', 'Web', 'LanguageISO', 'Characters', 'Teams',
'Locations', 'ScanInformation', 'StoryArc', 'SeriesGroup'],
'int': ['Count', 'Volume', 'AlternateCount', 'Year', 'Month', 'PageCount'],
'float': ['CommunityRating', 'BookPrice'],
'YesNo': ['BlackAndWhite'],
'Manga': ['Manga'],
'AgeRating': ['AgeRating'],
'ArrayOfComicPageInfo': ['Pages'],
'Format': ['Format']
}
page_config = {
'int': ['Image', 'ImageWidth', 'ImageHeight'],
'bool': ['DoublePage'],
'long': ['ImageSize'],
'str': ['Key'],
'ComicPageType': ['Type']
}
comic_info = []
comic_info.append("<?xml version='1.0' encoding='utf-8'?>")
comic_info.append('<ComicInfo>')
metadata_keys = metadata.keys()
# default
if not 'BlackAndWhite' in metadata_keys:
metadata['BlackAndWhite'] = 'Unknown'
if not 'Manga' in metadata_keys:
metadata['Manga'] = 'Unknown'
if not 'AgeRating' in metadata_keys:
metadata['AgeRating'] = 'Unknown'
for key in metadata_keys:
if key in comic_config['YesNo'] and metadata[key] in ['No', 'Yes']:
comic_info.append(f' <{key}>{metadata[key]}</{key}>')
elif key in comic_config['Manga'] and metadata[key] in ['No', 'Yes', 'YesAndRightToLeft']:
comic_info.append(f' <{key}>{metadata[key]}</{key}>')
elif key in comic_config['AgeRating'] and metadata[key] in ['Everyone', 'G', 'Early Childhood', 'Everyone 10+', 'PG',
'Kids to Adults', 'Teen', 'M', 'MA15+', 'Mature 17+',
'R18+', 'X18+', 'Adults Only 18+', 'Rating Pending']:
comic_info.append(f' <{key}>{metadata[key]}</{key}>')
elif key in comic_config['str']:
comic_info.append(f' <{key}>{metadata[key]}</{key}>')
elif key in comic_config['int']:
comic_info.append(f' <{key}>{metadata[key]}</{key}>')
elif key in comic_config['float']:
comic_info.append(f' <{key}>{metadata[key]}</{key}>')
elif key in comic_config['Format'] and metadata[key] in ['.1', '-1', '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', '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']:
comic_info.append(f' <{key}>{metadata[key]}</{key}>')
elif key in comic_config['ArrayOfComicPageInfo']:
comic_info.append(' <Pages>')
for item in metadata[key]:
item_keys =item.keys()
line = []
line.append(' <Page')
# default
if not 'Type' in item_keys:
metadata[key]['Type'] = 'Story'
if not 'DoublePage' in item_keys:
metadata[key]['DoublePage'] = False
for item_key in item_keys:
if item_key in page_config['int']:
line.append(f'{item_key}="{metadata[key][item_key]}"')
elif item_key in page_config['bool']:
value = 'true' if metadata[key][item_key] else 'false'
line.append(f'{item_key}="{value}"')
elif item_key in page_config['long']:
line.append(f'{item_key}="{metadata[key][item_key]}"')
elif item_key in page_config['str']:
line.append(f'{item_key}="{metadata[key][item_key]}"')
elif item_key in page_config['ComicPageType'] and metadata[key][item_key] in ['FrontCover', 'InnerCover', 'Roundup', 'Story',
'Advertisment', 'Editorial', 'Letters', 'Preview',
'BackCover', 'Other', 'Deleted']:
line.append(f'{item_key}="{metadata[key][item_key]}"')
line.append('/>')
comic_info.append(' '.join(line))
comic_info.append(' </Pages>')
comic_info.append('</ComicInfo>')
with open(os.path.join(self.tmp, 'ComicInfo.xml'), mode='w', encoding='utf-8') as f:
f.write('\n'.join(comic_info))
def addPage(self, image, content, double_page=False, page_type='DEFAULT', extension='jpg'):
page = os.path.join(self.tmp, f"page-{image:03d}.{extension}")
if not os.path.exists(page):
with open(page, mode='wb') as f:
f.write(content)
properties = Image.open(page)
if page_type == 'DEFAULT' or not page_type in ['FrontCover', 'InnerCover', 'Roundup', 'Story',
'Advertisment', 'Editorial', 'Letters', 'Preview',
'BackCover', 'Other', 'Deleted']:
page_type = 'FrontCover' if image == 1 else 'Story'
self.pages.append({
'Image': image,
'Type': page_type,
'DoublePage': double_page,
'ImageSize': properties.size,
'ImageWidth': properties.width,
'ImageHeight': properties.height
})
def saveCBZ(self):
if os.path.exists(self.tmp):
if not os.path.exists(self.output):
cbz = zipfile.ZipFile(f"{self.output}.cbz", 'w', compression=zipfile.ZIP_STORED)
for file in sorted(os.listdir(self.tmp), key=len):
with open(os.path.join(self.tmp, file), mode='rb') as f:
cbz.writestr(file, data=f.read())
cbz.close()
shutil.rmtree(self.tmp)
Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

+4
View File
@@ -0,0 +1,4 @@
from .helper import *
from .comicinfo import *
__version__ = "3.0.1"
+44
View File
@@ -0,0 +1,44 @@
"""
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'
]
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'
]
+14
View File
@@ -0,0 +1,14 @@
class PyCBZHelperException(Exception):
"""Exceptions used by pycbzhelper."""
class InvalidKeyValue(PyCBZHelperException):
"""The key value is invalid."""
class MissingPageFile(PyCBZHelperException):
"""No page available."""
class InvalidFilePermission(PyCBZHelperException):
"""Unable to delete existing file."""
+120
View File
@@ -0,0 +1,120 @@
import os
import shutil
import zipfile
from json2xml import json2xml
from langcodes import Language
from PIL import Image
from pycbzhelper.comicinfo import KEYS_STRING, KEYS_INT, KEYS_SPECIAL, KEYS_AGE, KEYS_FORMAT, KEYS_PAGE_TYPE
from pycbzhelper.exceptions import InvalidKeyValue, MissingPageFile, InvalidFilePermission
from pycbzhelper.utils import get_key_value, delete_none, slugify
class Helper:
def __init__(self, kwargs):
self._files = []
self.metadata = self._get_metadata(kwargs)
def _get_metadata(self, kwargs) -> str:
# NOTE: Check metadata
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}")
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}")
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")
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}]
pages.append(" <Pages>")
kwargs['PageCount'] = len(kwargs.get('Pages'))
for i in range(kwargs.get('PageCount')):
page = kwargs.get('Pages')[i]
properties = Image.open(page['File'])
self._files.append(page['File'])
if not page.get('Type'):
page['Type'] = 'FrontCover' if i == 0 else 'Story'
item = ' <Page DoublePage="True"' if get_key_value(
page.get('DoublePage', False)) == 'Yes' else ' <Page'
pages.append(
item + ' Image="{image}" ImageHeight="{height}" ImageSize="{size}" ImageWidth="{width}" Type="{type}" />'.format(
double="False" if get_key_value(page.get('DoublePage', False)) == 'No' else "True",
image=i,
height=properties.height,
size=len(properties.fp.read()),
width=properties.width,
type=page['Type']
)
)
pages.append(" </Pages>")
del kwargs['Pages']
pages.append("</ComicInfo>")
json_data = {}
for key in KEYS_STRING + KEYS_INT + KEYS_SPECIAL:
json_data[key] = get_key_value(kwargs.get(key))
xml_data = json2xml.Json2xml(delete_none(json_data), wrapper="ComicInfo", pretty=True, attr_type=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(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)
clear_path = []
cbz = zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_STORED)
for file in self._files:
if os.path.dirname(file) not in clear_path:
clear_path.append(os.path.dirname(file))
with open(file, mode='rb') as f:
cbz.writestr(os.path.basename(file), data=f.read())
cbz.writestr('ComicInfo.xml', data=self.metadata.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}")
+47
View File
@@ -0,0 +1,47 @@
from __future__ import annotations
from typing import Union
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 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
+77
View File
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="ComicInfo" nillable="true" type="ComicInfo" />
<xs:complexType name="ComicInfo">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="1" default="" name="Title" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Series" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Number" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Count" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Volume" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateSeries" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateNumber" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="AlternateCount" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Summary" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Notes" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Year" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Month" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Writer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Penciller" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Inker" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Colorist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Letterer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="CoverArtist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Editor" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Publisher" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Imprint" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Genre" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Web" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="0" name="PageCount" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="LanguageISO" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Format" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="BlackAndWhite" type="YesNo" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="Manga" type="YesNo" />
<xs:element minOccurs="0" maxOccurs="1" name="Pages" type="ArrayOfComicPageInfo" />
</xs:sequence>
</xs:complexType>
<xs:simpleType name="YesNo">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="No" />
<xs:enumeration value="Yes" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="ArrayOfComicPageInfo">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Page" nillable="true" type="ComicPageInfo" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="ComicPageInfo">
<xs:attribute name="Image" type="xs:int" use="required" />
<xs:attribute default="Story" name="Type" type="ComicPageType" />
<xs:attribute default="false" name="DoublePage" type="xs:boolean" />
<xs:attribute default="0" name="ImageSize" type="xs:long" />
<xs:attribute default="" name="Key" type="xs:string" />
<xs:attribute default="-1" name="ImageWidth" type="xs:int" />
<xs:attribute default="-1" name="ImageHeight" type="xs:int" />
</xs:complexType>
<xs:simpleType name="ComicPageType">
<xs:list>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="FrontCover" />
<xs:enumeration value="InnerCover" />
<xs:enumeration value="Roundup" />
<xs:enumeration value="Story" />
<xs:enumeration value="Advertisement" />
<xs:enumeration value="Editorial" />
<xs:enumeration value="Letters" />
<xs:enumeration value="Preview" />
<xs:enumeration value="BackCover" />
<xs:enumeration value="Other" />
<xs:enumeration value="Deleted" />
</xs:restriction>
</xs:simpleType>
</xs:list>
</xs:simpleType>
</xs:schema>
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="ComicInfo" nillable="true" type="ComicInfo" />
<xs:complexType name="ComicInfo">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="1" default="" name="Title" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Series" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Number" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Count" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Volume" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateSeries" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateNumber" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="AlternateCount" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Summary" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Notes" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Year" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Month" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Day" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Writer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Penciller" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Inker" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Colorist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Letterer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="CoverArtist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Editor" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Publisher" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Imprint" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Genre" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Web" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="0" name="PageCount" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="LanguageISO" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Format" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="BlackAndWhite" type="YesNo" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="Manga" type="Manga" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Characters" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Teams" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Locations" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="ScanInformation" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="StoryArc" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="SeriesGroup" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="AgeRating" type="AgeRating" />
<xs:element minOccurs="0" maxOccurs="1" name="Pages" type="ArrayOfComicPageInfo" />
<xs:element minOccurs="0" maxOccurs="1" name="CommunityRating" type="Rating" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="MainCharacterOrTeam" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Review" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:simpleType name="YesNo">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="No" />
<xs:enumeration value="Yes" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Manga">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="No" />
<xs:enumeration value="Yes" />
<xs:enumeration value="YesAndRightToLeft" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Rating">
<xs:restriction base="xs:decimal">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="5"/>
<xs:fractionDigits value="2"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="AgeRating">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="Adults Only 18+" />
<xs:enumeration value="Early Childhood" />
<xs:enumeration value="Everyone" />
<xs:enumeration value="Everyone 10+" />
<xs:enumeration value="G" />
<xs:enumeration value="Kids to Adults" />
<xs:enumeration value="M" />
<xs:enumeration value="MA15+" />
<xs:enumeration value="Mature 17+" />
<xs:enumeration value="PG" />
<xs:enumeration value="R18+" />
<xs:enumeration value="Rating Pending" />
<xs:enumeration value="Teen" />
<xs:enumeration value="X18+" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="ArrayOfComicPageInfo">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Page" nillable="true" type="ComicPageInfo" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="ComicPageInfo">
<xs:attribute name="Image" type="xs:int" use="required" />
<xs:attribute default="Story" name="Type" type="ComicPageType" />
<xs:attribute default="false" name="DoublePage" type="xs:boolean" />
<xs:attribute default="0" name="ImageSize" type="xs:long" />
<xs:attribute default="" name="Key" type="xs:string" />
<xs:attribute default="" name="Bookmark" type="xs:string" />
<xs:attribute default="-1" name="ImageWidth" type="xs:int" />
<xs:attribute default="-1" name="ImageHeight" type="xs:int" />
</xs:complexType>
<xs:simpleType name="ComicPageType">
<xs:list>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="FrontCover" />
<xs:enumeration value="InnerCover" />
<xs:enumeration value="Roundup" />
<xs:enumeration value="Story" />
<xs:enumeration value="Advertisement" />
<xs:enumeration value="Editorial" />
<xs:enumeration value="Letters" />
<xs:enumeration value="Preview" />
<xs:enumeration value="BackCover" />
<xs:enumeration value="Other" />
<xs:enumeration value="Deleted" />
</xs:restriction>
</xs:simpleType>
</xs:list>
</xs:simpleType>
</xs:schema>
+126
View File
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="ComicInfo" nillable="true" type="ComicInfo" />
<xs:complexType name="ComicInfo">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="1" default="" name="Title" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Series" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Number" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Count" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Volume" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateSeries" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="AlternateNumber" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="AlternateCount" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Summary" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Notes" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Year" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Month" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="-1" name="Day" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Writer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Penciller" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Inker" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Colorist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Letterer" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="CoverArtist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Editor" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Translator" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Publisher" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Imprint" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Genre" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Tags" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Web" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="0" name="PageCount" type="xs:int" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="LanguageISO" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Format" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="BlackAndWhite" type="YesNo" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="Manga" type="Manga" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Characters" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Teams" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Locations" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="ScanInformation" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="StoryArc" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="StoryArcNumber" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="SeriesGroup" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="Unknown" name="AgeRating" type="AgeRating" />
<xs:element minOccurs="0" maxOccurs="1" name="Pages" type="ArrayOfComicPageInfo" />
<xs:element minOccurs="0" maxOccurs="1" name="CommunityRating" type="Rating" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="MainCharacterOrTeam" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" default="" name="Review" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:simpleType name="YesNo">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="No" />
<xs:enumeration value="Yes" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Manga">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="No" />
<xs:enumeration value="Yes" />
<xs:enumeration value="YesAndRightToLeft" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Rating">
<xs:restriction base="xs:decimal">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="5"/>
<xs:fractionDigits value="1"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="AgeRating">
<xs:restriction base="xs:string">
<xs:enumeration value="Unknown" />
<xs:enumeration value="Adults Only 18+" />
<xs:enumeration value="Early Childhood" />
<xs:enumeration value="Everyone" />
<xs:enumeration value="Everyone 10+" />
<xs:enumeration value="G" />
<xs:enumeration value="Kids to Adults" />
<xs:enumeration value="M" />
<xs:enumeration value="MA15+" />
<xs:enumeration value="Mature 17+" />
<xs:enumeration value="PG" />
<xs:enumeration value="R18+" />
<xs:enumeration value="Rating Pending" />
<xs:enumeration value="Teen" />
<xs:enumeration value="X18+" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="ArrayOfComicPageInfo">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Page" nillable="true" type="ComicPageInfo" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="ComicPageInfo">
<xs:attribute name="Image" type="xs:int" use="required" />
<xs:attribute default="Story" name="Type" type="ComicPageType" />
<xs:attribute default="false" name="DoublePage" type="xs:boolean" />
<xs:attribute default="0" name="ImageSize" type="xs:long" />
<xs:attribute default="" name="Key" type="xs:string" />
<xs:attribute default="" name="Bookmark" type="xs:string" />
<xs:attribute default="-1" name="ImageWidth" type="xs:int" />
<xs:attribute default="-1" name="ImageHeight" type="xs:int" />
</xs:complexType>
<xs:simpleType name="ComicPageType">
<xs:list>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="FrontCover" />
<xs:enumeration value="InnerCover" />
<xs:enumeration value="Roundup" />
<xs:enumeration value="Story" />
<xs:enumeration value="Advertisement" />
<xs:enumeration value="Editorial" />
<xs:enumeration value="Letters" />
<xs:enumeration value="Preview" />
<xs:enumeration value="BackCover" />
<xs:enumeration value="Other" />
<xs:enumeration value="Deleted" />
</xs:restriction>
</xs:simpleType>
</xs:list>
</xs:simpleType>
</xs:schema>
+30
View File
@@ -0,0 +1,30 @@
"""Setup module"""
from setuptools import setup
with open('README.md', 'r') as fh:
LONG_DESCRIPTION = fh.read()
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'],
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'
]
)
+43
View File
@@ -0,0 +1,43 @@
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
)