mirror of
https://github.com/hyugogirubato/cbz.git
synced 2026-09-26 21:41:03 +00:00
Init
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .helper import *
|
||||
from .comicinfo import *
|
||||
|
||||
__version__ = "3.0.1"
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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."""
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user