diff --git a/cbz/comic.py b/cbz/comic.py index 7e395f4..6fa830c 100644 --- a/cbz/comic.py +++ b/cbz/comic.py @@ -11,6 +11,7 @@ from pathlib import Path from cbz import utils from cbz.constants import YesNo, Manga, AgeRating, Format, xml_name +from cbz.models import ComicModel from cbz.page import PageInfo @@ -25,59 +26,101 @@ def language(value: str) -> str: return value -class ComicInfo: - +class ComicInfo(ComicModel): def __init__(self, pages: [PageInfo], **kwargs): self.__pages = pages - self.__info = { - 'Title': str(kwargs.get('title', kwargs.get('Title', ''))), - 'Series': str(kwargs.get('series', kwargs.get('Series', ''))), - 'Number': str(kwargs.get('number', kwargs.get('Number', ''))), - 'Count': int(kwargs.get('count', kwargs.get('Count', -1))), - 'Volume': int(kwargs.get('volume', kwargs.get('Volume', -1))), - 'AlternateSeries': str(kwargs.get('alternate_series', kwargs.get('AlternateSeries', ''))), - 'AlternateNumber': str(kwargs.get('alternate_number', kwargs.get('AlternateNumber', ''))), - 'AlternateCount': int(kwargs.get('alternate_count', kwargs.get('AlternateCount', -1))), - 'Summary': str(kwargs.get('summary', kwargs.get('Summary', ''))), - 'Notes': str(kwargs.get('notes', kwargs.get('Notes', ''))), - 'Year': int(kwargs.get('year', kwargs.get('Year', -1))), - 'Month': int(kwargs.get('month', kwargs.get('Month', -1))), - 'Day': int(kwargs.get('day', kwargs.get('Day', -1))), - 'Writer': str(kwargs.get('writer', kwargs.get('Writer', ''))), - 'Penciller': str(kwargs.get('penciller', kwargs.get('Penciller', ''))), - 'Inker': str(kwargs.get('inker', kwargs.get('Inker', ''))), - 'Colorist': str(kwargs.get('colorist', kwargs.get('Colorist', ''))), - 'Letterer': str(kwargs.get('letterer', kwargs.get('Letterer', ''))), - 'CoverArtist': str(kwargs.get('cover_artist', kwargs.get('CoverArtist', ''))), - 'Editor': str(kwargs.get('editor', kwargs.get('Editor', ''))), - 'Translator': str(kwargs.get('translator', kwargs.get('Translator', ''))), - 'Publisher': str(kwargs.get('publisher', kwargs.get('Publisher', ''))), - 'Imprint': str(kwargs.get('imprint', kwargs.get('imprint', 'Imprint'))), - 'Genre': str(kwargs.get('genre', kwargs.get('Genre', ''))), - 'Tags': str(kwargs.get('tags', kwargs.get('Tags', ''))), - 'Web': str(kwargs.get('web', kwargs.get('Web', ''))), - 'PageCount': len(self.__pages), - 'LanguageISO': language(kwargs.get('language_iso', kwargs.get('LanguageISO', ''))), - 'Format': Format(kwargs.get('format', kwargs.get('Format', Format.UNKNOWN))), - 'EAN': str(kwargs.get('ean', kwargs.get('EAN', ''))), - 'BlackAndWhite': YesNo(kwargs.get('black_white', kwargs.get('BlackAndWhite', YesNo.UNKNOWN))), - 'Manga': Manga(kwargs.get('manga', kwargs.get('Manga', Manga.UNKNOWN))), - 'Characters': str(kwargs.get('characters', kwargs.get('Characters', ''))), - 'Teams': str(kwargs.get('teams', kwargs.get('Teams', ''))), - 'Locations': str(kwargs.get('locations', kwargs.get('Locations', ''))), - 'ScanInformation': str(kwargs.get('scan_information', kwargs.get('ScanInformation', ''))), - 'StoryArc': str(kwargs.get('story_arc', kwargs.get('StoryArc', ''))), - 'StoryArcNumber': str(kwargs.get('story_arc_number', kwargs.get('StoryArcNumber', ''))), - 'SeriesGroup': str(kwargs.get('series_group', kwargs.get('series_group', ''))), - 'AgeRating': AgeRating(kwargs.get('age_rating', kwargs.get('AgeRating', AgeRating.UNKNOWN))), - 'Pages': [{'Image': i, **page.dumps()} for i, page in enumerate(pages)], - 'CommunityRating': rating(kwargs.get('community_rating', kwargs.get('CommunityRating', -1))), - 'MainCharacterOrTeam': str(kwargs.get('main_character_or_team', kwargs.get('MainCharacterOrTeam', ''))), - 'Review': str(kwargs.get('review', kwargs.get('Review', ''))) - } + # TODO: need change to loop with setattr + self.title = str(kwargs.get('title', kwargs.get('Title', ''))) + self.series = str(kwargs.get('series', kwargs.get('Series', ''))) + self.number = str(kwargs.get('number', kwargs.get('Number', ''))) + self.count = int(kwargs.get('count', kwargs.get('Count', -1))) + self.volume = int(kwargs.get('volume', kwargs.get('Volume', -1))) + self.alternate_series = str(kwargs.get('alternate_series', kwargs.get('AlternateSeries', ''))) + self.alternate_number = str(kwargs.get('alternate_number', kwargs.get('AlternateNumber', ''))) + self.alternate_count = int(kwargs.get('alternate_count', kwargs.get('AlternateCount', -1))) + self.summary = str(kwargs.get('summary', kwargs.get('Summary', ''))) + self.notes = str(kwargs.get('notes', kwargs.get('Notes', ''))) + self.year = int(kwargs.get('year', kwargs.get('Year', -1))) + self.month = int(kwargs.get('month', kwargs.get('Month', -1))) + self.day = int(kwargs.get('day', kwargs.get('Day', -1))) + self.writer = str(kwargs.get('writer', kwargs.get('Writer', ''))) + self.penciller = str(kwargs.get('penciller', kwargs.get('Penciller', ''))) + self.inker = str(kwargs.get('inker', kwargs.get('Inker', ''))) + self.colorist = str(kwargs.get('colorist', kwargs.get('Colorist', ''))) + self.letterer = str(kwargs.get('letterer', kwargs.get('Letterer', ''))) + self.cover_artist = str(kwargs.get('cover_artist', kwargs.get('CoverArtist', ''))) + self.editor = str(kwargs.get('editor', kwargs.get('Editor', ''))) + self.translator = str(kwargs.get('translator', kwargs.get('Translator', ''))) + self.publisher = str(kwargs.get('publisher', kwargs.get('Publisher', ''))) + self.imprint = str(kwargs.get('imprint', kwargs.get('imprint', 'Imprint'))) + self.genre = str(kwargs.get('genre', kwargs.get('Genre', ''))) + self.tags = str(kwargs.get('tags', kwargs.get('Tags', ''))) + self.web = str(kwargs.get('web', kwargs.get('Web', ''))) + self.language_iso = language(kwargs.get('language_iso', kwargs.get('LanguageISO', ''))) + self.format = Format(kwargs.get('format', kwargs.get('Format', Format.UNKNOWN))) + self.ean = str(kwargs.get('ean', kwargs.get('EAN', ''))) + self.black_white = YesNo(kwargs.get('black_white', kwargs.get('BlackAndWhite', YesNo.UNKNOWN))) + self.manga = Manga(kwargs.get('manga', kwargs.get('Manga', Manga.UNKNOWN))) + self.characters = str(kwargs.get('characters', kwargs.get('Characters', ''))) + self.teams = str(kwargs.get('teams', kwargs.get('Teams', ''))) + self.locations = str(kwargs.get('locations', kwargs.get('Locations', ''))) + self.scan_information = str(kwargs.get('scan_information', kwargs.get('ScanInformation', ''))) + self.story_arc = str(kwargs.get('story_arc', kwargs.get('StoryArc', ''))) + self.story_arc_number = str(kwargs.get('story_arc_number', kwargs.get('StoryArcNumber', ''))) + self.series_group = str(kwargs.get('series_group', kwargs.get('series_group', ''))) + self.age_rating = AgeRating(kwargs.get('age_rating', kwargs.get('AgeRating', AgeRating.UNKNOWN))) + self.community_rating = rating(kwargs.get('community_rating', kwargs.get('CommunityRating', -1))) + self.main_character_or_team = str(kwargs.get('main_character_or_team', kwargs.get('MainCharacterOrTeam', ''))) + self.review = str(kwargs.get('review', kwargs.get('Review', ''))) def dumps(self) -> dict: - return utils.dumps(self.__info) + # TODO: need change to loop with getattr + return utils.dumps({ + 'Title': str(self.title), + 'Series': str(self.series), + 'Number': str(self.number), + 'Count': int(self.count), + 'Volume': int(self.volume), + 'AlternateSeries': str(self.alternate_series), + 'AlternateNumber': str(self.alternate_number), + 'AlternateCount': int(self.alternate_count), + 'Summary': str(self.summary), + 'Notes': str(self.notes), + 'Year': int(self.year), + 'Month': int(self.month), + 'Day': int(self.day), + 'Writer': str(self.writer), + 'Penciller': str(self.penciller), + 'Inker': str(self.inker), + 'Colorist': str(self.colorist), + 'Letterer': str(self.letterer), + 'CoverArtist': str(self.cover_artist), + 'Editor': str(self.editor), + 'Translator': str(self.translator), + 'Publisher': str(self.publisher), + 'Imprint': str(self.imprint), + 'Genre': str(self.genre), + 'Tags': str(self.tags), + 'Web': str(self.web), + 'PageCount': len(self.__pages), + 'LanguageISO': language(self.language_iso), + 'Format': self.format, + 'EAN': str(self.ean), + 'BlackAndWhite': self.black_white, + 'Manga': self.manga, + 'Characters': str(self.characters), + 'Teams': str(self.teams), + 'Locations': str(self.locations), + 'ScanInformation': str(self.scan_information), + 'StoryArc': str(self.story_arc), + 'StoryArcNumber': str(self.story_arc_number), + 'SeriesGroup': str(self.series_group), + 'AgeRating': self.age_rating, + 'Pages': [{'Image': i, **page.dumps()} for i, page in enumerate(self.__pages)], + 'CommunityRating': rating(self.community_rating), + 'MainCharacterOrTeam': str(self.main_character_or_team), + 'Review': str(self.review) + }) def __repr__(self) -> str: return json.dumps(self.dumps(), indent=2) @@ -88,9 +131,51 @@ class ComicInfo: @classmethod def from_cbz(cls, path: Union[Path, str]) -> ComicInfo: + """ + read comic from cbz/cbx file + :param path: path to the file + :return: Exemplar of ComicInfo class + """ if not isinstance(path, (Path, str)): raise ValueError(f'Expecting Path object or path string, got {path!r}') - return cls(**cls.__unpack(Path(path))) + exemplar = cls(**cls.__unpack(Path(path))) + exemplar._filepath = path + return exemplar + + @staticmethod + def get_info(path: Union[Path, str]) -> dict: + """ + Get from file only info, without images loading + :return: dictionary with comic info + """ + 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: + with zipfile.ZipFile(f, 'r', zipfile.ZIP_STORED) as zip_file: + xml_file = zip_file.open(xml_name) + info = xmltodict.parse(xml_file.read()).get("ComicInfo", {}) + xml_file.close() + return info + + def to_cbz(self, path: Union[Path, str]) -> None: + """ + Save comic to file + :param path: path for a save (will be overwritten if already exists) + :return: + """ + if not isinstance(path, (Path, str)): + raise ValueError(f'Expecting Path object or path string, got {path!r}') + self._filepath = path + with Path(path).open(mode='wb') as f: + f.write(self.pack()) + + def save(self) -> None: + """ + Save changes to opened from file or already saved to file comic + :return: + """ + assert self._filepath, "Use to_cbz or from_cbz before save changes" + self.to_cbz(self._filepath) @staticmethod def __unpack(file_path: Path) -> dict: @@ -98,7 +183,7 @@ class ComicInfo: _files = zip_file.namelist() assert xml_name in _files, f"{xml_name} not found in: {zip_file.filename}" xml_file = zip_file.open(xml_name) - info = xmltodict.parse(xml_file.read()).get("ComicInfo") + info = xmltodict.parse(xml_file.read()).get("ComicInfo", {}) xml_file.close() _files.remove(xml_name) info["pages"] = list() @@ -118,5 +203,6 @@ class ComicInfo: for i, page in enumerate(self.__pages): zip_file.writestr(f'page-{i + 1:03d}{page.suffix}', page.content) - - return zip_buffer.getvalue() + result = zip_buffer.getvalue() + zip_buffer.close() + return result diff --git a/cbz/models.py b/cbz/models.py new file mode 100644 index 0000000..d1b67d0 --- /dev/null +++ b/cbz/models.py @@ -0,0 +1,47 @@ +from cbz.constants import YesNo, Manga, AgeRating, Format + + +class ComicModel: + title: str = "" + series: str = "" + number: str = "" + count: int = -1 + volume: int = -1 + alternate_series: str = "" + alternate_number: str = "" + alternate_count: int = -1 + summary: str = "" + notes: str = "" + year: int = -1 + month: int = -1 + day: int = -1 + writer: str = "" + penciller: str = "" + inker: str = "" + colorist: str = "" + letterer: str = "" + cover_artist: str = "" + editor: str = "" + translator: str = "" + publisher: str = "" + imprint: str = "" + genre: str = "" + tags: str = "" + web: str = "" + language_iso: str = "" + format: Format = Format.UNKNOWN + ean: str = "" + black_white: YesNo = YesNo.UNKNOWN + manga: Manga = Manga.UNKNOWN + characters: str = "" + teams: str = "" + locations: str = "" + scan_information: str = "" + story_arc: str = "" + story_arc_number: str = "" + series_group: str = "" + age_rating: AgeRating = AgeRating.UNKNOWN + community_rating: int = -1 + main_character_or_team: str = "" + review: str = "" + _filepath: str = "" diff --git a/cbz/page.py b/cbz/page.py index 28bc870..320d687 100644 --- a/cbz/page.py +++ b/cbz/page.py @@ -16,20 +16,19 @@ 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) - } + with Image.open(BytesIO(content)) as image: + 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)