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
+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