diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7bbc71c
--- /dev/null
+++ b/.gitignore
@@ -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/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..238144d
--- /dev/null
+++ b/.travis.yml
@@ -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
diff --git a/README.md b/README.md
index 690415a..ccc40bc 100644
--- a/README.md
+++ b/README.md
@@ -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
+[](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)
\ No newline at end of file
diff --git a/cbzhelper.py b/cbzhelper.py
deleted file mode 100644
index dfdf6a6..0000000
--- a/cbzhelper.py
+++ /dev/null
@@ -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("")
- comic_info.append('')
-
- 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(' ')
- for item in metadata[key]:
- item_keys =item.keys()
- line = []
-
- line.append(' ')
- comic_info.append(' '.join(line))
-
- comic_info.append(' ')
- comic_info.append('')
-
- 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)
diff --git a/images/page-000.jpg b/images/page-000.jpg
new file mode 100644
index 0000000..19b675b
Binary files /dev/null and b/images/page-000.jpg differ
diff --git a/images/page-001.jpg b/images/page-001.jpg
new file mode 100644
index 0000000..c40b1d3
Binary files /dev/null and b/images/page-001.jpg differ
diff --git a/images/page-002.jpg b/images/page-002.jpg
new file mode 100644
index 0000000..6a85c4f
Binary files /dev/null and b/images/page-002.jpg differ
diff --git a/images/page-003.jpg b/images/page-003.jpg
new file mode 100644
index 0000000..23e6547
Binary files /dev/null and b/images/page-003.jpg differ
diff --git a/images/page-004.jpg b/images/page-004.jpg
new file mode 100644
index 0000000..2e1c2d1
Binary files /dev/null and b/images/page-004.jpg differ
diff --git a/images/page-005.jpg b/images/page-005.jpg
new file mode 100644
index 0000000..bbeb8bc
Binary files /dev/null and b/images/page-005.jpg differ
diff --git a/images/page-006.jpg b/images/page-006.jpg
new file mode 100644
index 0000000..1d002db
Binary files /dev/null and b/images/page-006.jpg differ
diff --git a/images/page-007.jpg b/images/page-007.jpg
new file mode 100644
index 0000000..9f2e2c5
Binary files /dev/null and b/images/page-007.jpg differ
diff --git a/images/page-008.jpg b/images/page-008.jpg
new file mode 100644
index 0000000..a7bd0e3
Binary files /dev/null and b/images/page-008.jpg differ
diff --git a/images/page-009.jpg b/images/page-009.jpg
new file mode 100644
index 0000000..a835f60
Binary files /dev/null and b/images/page-009.jpg differ
diff --git a/images/page-010.jpg b/images/page-010.jpg
new file mode 100644
index 0000000..b93badd
Binary files /dev/null and b/images/page-010.jpg differ
diff --git a/pycbzhelper/__init__.py b/pycbzhelper/__init__.py
new file mode 100644
index 0000000..06de162
--- /dev/null
+++ b/pycbzhelper/__init__.py
@@ -0,0 +1,4 @@
+from .helper import *
+from .comicinfo import *
+
+__version__ = "3.0.1"
diff --git a/pycbzhelper/comicinfo.py b/pycbzhelper/comicinfo.py
new file mode 100644
index 0000000..ac72aa8
--- /dev/null
+++ b/pycbzhelper/comicinfo.py
@@ -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'
+]
diff --git a/pycbzhelper/exceptions.py b/pycbzhelper/exceptions.py
new file mode 100644
index 0000000..8e47152
--- /dev/null
+++ b/pycbzhelper/exceptions.py
@@ -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."""
diff --git a/pycbzhelper/helper.py b/pycbzhelper/helper.py
new file mode 100644
index 0000000..1bbe03f
--- /dev/null
+++ b/pycbzhelper/helper.py
@@ -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(" ")
+ 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 = ' '.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(" ")
+ del kwargs['Pages']
+ pages.append("")
+
+ 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(['', '', ''])
+ print('WARNING: No metadata to create.')
+ xml_data = xml_data.replace('', '')
+ xml_data = xml_data.replace('', '\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}")
diff --git a/pycbzhelper/utils.py b/pycbzhelper/utils.py
new file mode 100644
index 0000000..80acd45
--- /dev/null
+++ b/pycbzhelper/utils.py
@@ -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
diff --git a/schema/v1.0/ComicInfo.xsd b/schema/v1.0/ComicInfo.xsd
new file mode 100644
index 0000000..556c3a0
--- /dev/null
+++ b/schema/v1.0/ComicInfo.xsd
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/schema/v2.0/ComicInfo.xsd b/schema/v2.0/ComicInfo.xsd
new file mode 100644
index 0000000..6732fe8
--- /dev/null
+++ b/schema/v2.0/ComicInfo.xsd
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/schema/v2.1/ComicInfo.xsd b/schema/v2.1/ComicInfo.xsd
new file mode 100644
index 0000000..02ea57c
--- /dev/null
+++ b/schema/v2.1/ComicInfo.xsd
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..3c76193
--- /dev/null
+++ b/setup.py
@@ -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'
+ ]
+)
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000..6e1393a
--- /dev/null
+++ b/tests.py
@@ -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
+ )