forked from mirrors/cbz
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45da4c11f4 | ||
|
|
1e23fad6b5 | ||
|
|
4ebee2a6cf | ||
|
|
ef4eff4868 | ||
|
|
d37be87c2f | ||
|
|
0b011a7efc | ||
|
|
62b8cf5739 | ||
|
|
871716971a | ||
|
|
597cb41b00 | ||
|
|
a489ae17fb | ||
|
|
abc69d4673 | ||
|
|
6a1111a6bb | ||
|
|
220f19fe46 | ||
|
|
bbab9ba3b3 | ||
|
|
5824a8428a | ||
|
|
23f7e37561 | ||
|
|
5cc4732238 | ||
|
|
9d5d539311 | ||
|
|
f8274ae26b |
@@ -10,10 +10,10 @@ jobs:
|
|||||||
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.4.5] - 2025-10-19
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- More flexible suffix checking to support complex archive paths.
|
||||||
|
- Improved validation in `PageInfo.load()` to handle empty or invalid data correctly.
|
||||||
|
|
||||||
|
## [3.4.4] - 2025-09-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `[pillow]` extension to support `.jxl` and `.avif` images.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Files in the input archives are now sorted alphabetically.
|
||||||
|
|
||||||
## [3.4.3] - 2025-08-31
|
## [3.4.3] - 2025-08-31
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -215,6 +232,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
- Initial release.
|
- Initial release.
|
||||||
|
|
||||||
|
[3.4.5]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.5
|
||||||
|
[3.4.4]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.4
|
||||||
[3.4.3]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.3
|
[3.4.3]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.3
|
||||||
[3.4.2]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.2
|
[3.4.2]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.2
|
||||||
[3.4.1]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.1
|
[3.4.1]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.1
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ This project uses the following third-party libraries:
|
|||||||
|
|
||||||
- **[langcodes](https://pypi.org/project/langcodes/)** - MIT License
|
- **[langcodes](https://pypi.org/project/langcodes/)** - MIT License
|
||||||
- **[Pillow](https://pypi.org/project/Pillow/)** - HPND License
|
- **[Pillow](https://pypi.org/project/Pillow/)** - HPND License
|
||||||
|
- **[pillow-avif-plugin](https://pypi.org/project/pillow-avif-plugin/)** - MIT License
|
||||||
|
- **[pillow-jxl-plugin](https://pypi.org/project/pillow-jxl-plugin/)** - MIT License
|
||||||
- **[pypdf](https://pypi.org/project/pypdf/)** - BSD License
|
- **[pypdf](https://pypi.org/project/pypdf/)** - BSD License
|
||||||
- **[rarfile](https://pypi.org/project/rarfile/)** - ISC License
|
- **[rarfile](https://pypi.org/project/rarfile/)** - ISC License
|
||||||
- **[xmltodict](https://pypi.org/project/xmltodict/)** - MIT License
|
- **[xmltodict](https://pypi.org/project/xmltodict/)** - MIT License
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
from .comic import ComicInfo
|
from .comic import ComicInfo
|
||||||
from .page import PageInfo
|
from .page import PageInfo
|
||||||
|
|
||||||
|
# Register optional Pillow plugins
|
||||||
|
for _plugin in ('pillow_avif', 'pillow_jxl'):
|
||||||
|
try:
|
||||||
|
__import__(_plugin)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|||||||
+9
-3
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -156,7 +157,7 @@ class ComicInfo(ComicModel):
|
|||||||
ComicInfo: An instance of ComicInfo.
|
ComicInfo: An instance of ComicInfo.
|
||||||
"""
|
"""
|
||||||
pages = []
|
pages = []
|
||||||
names = archive_file.namelist()
|
names = sorted(archive_file.namelist())
|
||||||
comic_info = {}
|
comic_info = {}
|
||||||
|
|
||||||
if XML_NAME in names:
|
if XML_NAME in names:
|
||||||
@@ -172,8 +173,13 @@ class ComicInfo(ComicModel):
|
|||||||
pages_info = comic_info.get('Pages', {}).get('Page', [])
|
pages_info = comic_info.get('Pages', {}).get('Page', [])
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
suffix = Path(name).suffix
|
suffix = Path(name).suffix
|
||||||
if suffix:
|
|
||||||
assert suffix in IMAGE_FORMAT, f'Unsupported image format: {suffix}'
|
# Some archives may contain folders or hidden files (e.g., ".extra/" or "__MACOSX/.DS_Store")
|
||||||
|
# that could corrupt the suffix detection and cause invalid image handling.
|
||||||
|
if suffix not in IMAGE_FORMAT:
|
||||||
|
logging.warning(f'Skipping unsupported or invalid file: {name!r} (suffix={suffix!r})')
|
||||||
|
continue
|
||||||
|
|
||||||
with archive_file.open(name, 'r') as f:
|
with archive_file.open(name, 'r') as f:
|
||||||
page_info = {}
|
page_info = {}
|
||||||
if i < len(pages_info):
|
if i < len(pages_info):
|
||||||
|
|||||||
+140
-134
@@ -3,189 +3,195 @@ from typing import Union
|
|||||||
|
|
||||||
from langcodes import Language
|
from langcodes import Language
|
||||||
|
|
||||||
XML_NAME = 'ComicInfo.xml'
|
XML_NAME = "ComicInfo.xml"
|
||||||
IMAGE_FORMAT = {
|
IMAGE_FORMAT = {
|
||||||
'.jpeg', '.jpg', # Joint Photographic Experts Group
|
".jpeg",
|
||||||
'.png', # Portable Network Graphics
|
".jpg", # JPEG (Joint Photographic Experts Group)
|
||||||
'.gif', # Graphics Interchange Format
|
".png", # PNG (Portable Network Graphics)
|
||||||
'.bmp', # Bitmap Image File
|
".gif", # GIF (Graphics Interchange Format)
|
||||||
'.tiff', '.tif', # Tagged Image File Format
|
".bmp", # BMP (Bitmap Image File)
|
||||||
'.webp' # Web Picture Format
|
".tiff",
|
||||||
|
".tif", # TIFF (Tagged Image File Format)
|
||||||
|
".webp", # WebP (Web Picture Format, by Google)
|
||||||
|
".jxl", # JPEG XL (Next-generation JPEG format)
|
||||||
|
".avif", # AVIF (AV1 Image File Format, based on AV1 codec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class YesNo(Enum):
|
class YesNo(Enum):
|
||||||
UNKNOWN = 'Unknown'
|
UNKNOWN = "Unknown"
|
||||||
NO = 'No'
|
NO = "No"
|
||||||
YES = 'Yes'
|
YES = "Yes"
|
||||||
|
|
||||||
|
|
||||||
class Manga(Enum):
|
class Manga(Enum):
|
||||||
UNKNOWN = 'Unknown'
|
UNKNOWN = "Unknown"
|
||||||
NO = 'No'
|
NO = "No"
|
||||||
YES = 'Yes'
|
YES = "Yes"
|
||||||
RIGHT_LEFT = 'YesAndRightToLeft'
|
RIGHT_LEFT = "YesAndRightToLeft"
|
||||||
|
|
||||||
|
|
||||||
class PageType(Enum):
|
class PageType(Enum):
|
||||||
FRONT_COVER = 'FrontCover'
|
FRONT_COVER = "FrontCover"
|
||||||
INNER_COVER = 'InnerCover'
|
INNER_COVER = "InnerCover"
|
||||||
ROUNDUP = 'Roundup'
|
ROUNDUP = "Roundup"
|
||||||
STORY = 'Story'
|
STORY = "Story"
|
||||||
ADVERTISEMENT = 'Advertisement'
|
ADVERTISEMENT = "Advertisement"
|
||||||
EDITORIAL = 'Editorial'
|
EDITORIAL = "Editorial"
|
||||||
LETTERS = 'Letters'
|
LETTERS = "Letters"
|
||||||
PREVIEW = 'Preview'
|
PREVIEW = "Preview"
|
||||||
BACK_COVER = 'BackCover'
|
BACK_COVER = "BackCover"
|
||||||
OTHER = 'Other'
|
OTHER = "Other"
|
||||||
DELETED = 'Deleted'
|
DELETED = "Deleted"
|
||||||
|
|
||||||
|
|
||||||
class AgeRating(Enum):
|
class AgeRating(Enum):
|
||||||
UNKNOWN = 'Unknown'
|
UNKNOWN = "Unknown"
|
||||||
ADULTS18 = 'Adults Only 18+'
|
ADULTS18 = "Adults Only 18+"
|
||||||
CHILDHOOD = 'Early Childhood'
|
CHILDHOOD = "Early Childhood"
|
||||||
EVERYONE = 'Everyone'
|
EVERYONE = "Everyone"
|
||||||
EVERYONE10 = 'Everyone 10+'
|
EVERYONE10 = "Everyone 10+"
|
||||||
G = 'G'
|
G = "G"
|
||||||
KIDS = 'Kids to Adults'
|
KIDS = "Kids to Adults"
|
||||||
M = 'M'
|
M = "M"
|
||||||
MA15 = 'MA15+'
|
MA15 = "MA15+"
|
||||||
MATURE17 = 'Mature 17+'
|
MATURE17 = "Mature 17+"
|
||||||
PG = 'PG'
|
PG = "PG"
|
||||||
R18 = 'R18+'
|
R18 = "R18+"
|
||||||
PENDING = 'Rating Pending'
|
PENDING = "Rating Pending"
|
||||||
TEEN = 'Teen'
|
TEEN = "Teen"
|
||||||
X18 = 'X18+'
|
X18 = "X18+"
|
||||||
|
|
||||||
|
|
||||||
class Format(Enum):
|
class Format(Enum):
|
||||||
UNKNOWN = 'Unknown'
|
UNKNOWN = "Unknown"
|
||||||
# ONE_SHOT = '1 Shot'
|
# ONE_SHOT = '1 Shot'
|
||||||
# ONE_SHOT = '1/2',
|
# ONE_SHOT = '1/2',
|
||||||
# ONE_SHOT = '1-Shot'
|
# ONE_SHOT = '1-Shot'
|
||||||
ANNOTATION = 'Annotation'
|
ANNOTATION = "Annotation"
|
||||||
# ANNOTATIONS = 'Annotations'
|
# ANNOTATIONS = 'Annotations'
|
||||||
ANNUAL = 'Annual'
|
ANNUAL = "Annual"
|
||||||
ANTHOLOGY = 'Anthology'
|
ANTHOLOGY = "Anthology"
|
||||||
# BLACK_WHITE = 'B&W'
|
# BLACK_WHITE = 'B&W'
|
||||||
# BLACK_WHITE = 'B/W'
|
# BLACK_WHITE = 'B/W'
|
||||||
# BLACK_WHITE = 'B&&W'
|
# BLACK_WHITE = 'B&&W'
|
||||||
BLACK_WHITE = 'Black & White'
|
BLACK_WHITE = "Black & White"
|
||||||
# BOX_SET = 'Box Set'
|
# BOX_SET = 'Box Set'
|
||||||
BOX_SET = 'Box-Set'
|
BOX_SET = "Box-Set"
|
||||||
CROSSOVER = 'Crossover'
|
CROSSOVER = "Crossover"
|
||||||
DIRECTOR_CUT = 'Director\'s Cut'
|
DIRECTOR_CUT = "Director's Cut"
|
||||||
EPILOGUE = 'Epilogue'
|
EPILOGUE = "Epilogue"
|
||||||
EVENT = 'Event'
|
EVENT = "Event"
|
||||||
FCBD = 'FCBD'
|
FCBD = "FCBD"
|
||||||
FLYER = 'Flyer'
|
FLYER = "Flyer"
|
||||||
# GIANT_SIZE = 'Giant'
|
# GIANT_SIZE = 'Giant'
|
||||||
# GIANT_SIZE = 'Giant Size'
|
# GIANT_SIZE = 'Giant Size'
|
||||||
GIANT_SIZE = 'Giant-Size'
|
GIANT_SIZE = "Giant-Size"
|
||||||
GRAPHIC_NOVEL = 'Graphic Novel'
|
GRAPHIC_NOVEL = "Graphic Novel"
|
||||||
# HARDCOVER = 'Hardcover'
|
# HARDCOVER = 'Hardcover'
|
||||||
HARDCOVER = 'Hard-Cover'
|
HARDCOVER = "Hard-Cover"
|
||||||
# KING_SIZE = 'King'
|
# KING_SIZE = 'King'
|
||||||
# KING_SIZE = 'King Size'
|
# KING_SIZE = 'King Size'
|
||||||
KING_SIZE = 'King-Size'
|
KING_SIZE = "King-Size"
|
||||||
LIMITED_SERIES = 'Limited Series'
|
LIMITED_SERIES = "Limited Series"
|
||||||
MAGAZINE = 'Magazine'
|
MAGAZINE = "Magazine"
|
||||||
NSFW = 'NSFW'
|
NSFW = "NSFW"
|
||||||
# ONE_SHOT = 'One Shot'
|
# ONE_SHOT = 'One Shot'
|
||||||
ONE_SHOT = 'One-Shot'
|
ONE_SHOT = "One-Shot"
|
||||||
POINT1 = 'Point 1'
|
POINT1 = "Point 1"
|
||||||
PREVIEW = 'Preview'
|
PREVIEW = "Preview"
|
||||||
PROLOGUE = 'Prologue'
|
PROLOGUE = "Prologue"
|
||||||
REFERENCE = 'Reference'
|
REFERENCE = "Reference"
|
||||||
REVIEW = 'Review'
|
REVIEW = "Review"
|
||||||
REVIEWED = 'Reviewed'
|
REVIEWED = "Reviewed"
|
||||||
SCANLATION = 'Scanlation'
|
SCANLATION = "Scanlation"
|
||||||
SCRIPT = 'Script'
|
SCRIPT = "Script"
|
||||||
SERIES = 'Series'
|
SERIES = "Series"
|
||||||
SKETCH = 'Sketch'
|
SKETCH = "Sketch"
|
||||||
SPECIAL = 'Special'
|
SPECIAL = "Special"
|
||||||
# TRADE_PAPER_BACK = 'TPB'
|
# TRADE_PAPER_BACK = 'TPB'
|
||||||
TRADE_PAPER_BACK = 'Trade Paper Back'
|
TRADE_PAPER_BACK = "Trade Paper Back"
|
||||||
# WEB_COMIC = 'WebComic'
|
# WEB_COMIC = 'WebComic'
|
||||||
WEB_COMIC = 'Web Comic'
|
WEB_COMIC = "Web Comic"
|
||||||
# YEAR_ONE = 'Year 1'
|
# YEAR_ONE = 'Year 1'
|
||||||
YEAR_ONE = 'Year One'
|
YEAR_ONE = "Year One"
|
||||||
|
|
||||||
|
|
||||||
class Rating(float):
|
class Rating(float):
|
||||||
|
|
||||||
def __new__(cls, value: Union[int, float] = -1) -> float:
|
def __new__(cls, value: Union[int, float] = -1) -> float:
|
||||||
assert -1 <= float(value) <= 5, f'Rating must be between 0 and 5, input {value}'
|
assert -1 <= float(value) <= 5, f"Rating must be between 0 and 5, input {value}"
|
||||||
return super().__new__(cls, value)
|
return super().__new__(cls, value)
|
||||||
|
|
||||||
|
|
||||||
class LanguageISO(str):
|
class LanguageISO(str):
|
||||||
|
|
||||||
def __new__(cls, value: str = '') -> str:
|
def __new__(cls, value: str = "") -> str:
|
||||||
assert not value or Language.get(str(value)).is_valid(), f'Invalid {value} language'
|
assert (
|
||||||
|
not value or Language.get(str(value)).is_valid()
|
||||||
|
), f"Invalid {value} language"
|
||||||
return super().__new__(cls, value)
|
return super().__new__(cls, value)
|
||||||
|
|
||||||
|
|
||||||
COMIC_FIELDS = {
|
COMIC_FIELDS = {
|
||||||
'title': ('Title', str),
|
"title": ("Title", str),
|
||||||
'series': ('Series', str),
|
"series": ("Series", str),
|
||||||
'number': ('Number', int),
|
"number": ("Number", float),
|
||||||
'count': ('Count', int),
|
"count": ("Count", int),
|
||||||
'volume': ('Volume', int),
|
"volume": ("Volume", int),
|
||||||
'alternate_series': ('AlternateSeries', str),
|
"alternate_series": ("AlternateSeries", str),
|
||||||
'alternate_number': ('AlternateNumber', int),
|
"alternate_number": ("AlternateNumber", int),
|
||||||
'alternate_count': ('AlternateCount', int),
|
"alternate_count": ("AlternateCount", int),
|
||||||
'summary': ('Summary', str),
|
"summary": ("Summary", str),
|
||||||
'notes': ('Notes', str),
|
"notes": ("Notes", str),
|
||||||
'year': ('Year', int),
|
"year": ("Year", int),
|
||||||
'month': ('Month', int),
|
"month": ("Month", int),
|
||||||
'day': ('Day', int),
|
"day": ("Day", int),
|
||||||
'writer': ('Writer', str),
|
"writer": ("Writer", str),
|
||||||
'penciller': ('Penciller', str),
|
"penciller": ("Penciller", str),
|
||||||
'inker': ('Inker', str),
|
"inker": ("Inker", str),
|
||||||
'colorist': ('Colorist', str),
|
"colorist": ("Colorist", str),
|
||||||
'letterer': ('Letterer', str),
|
"letterer": ("Letterer", str),
|
||||||
'cover_artist': ('CoverArtist', str),
|
"cover_artist": ("CoverArtist", str),
|
||||||
'editor': ('Editor', str),
|
"editor": ("Editor", str),
|
||||||
'translator': ('Translator', str),
|
"translator": ("Translator", str),
|
||||||
'publisher': ('Publisher', str),
|
"publisher": ("Publisher", str),
|
||||||
'imprint': ('Imprint', str),
|
"imprint": ("Imprint", str),
|
||||||
'genre': ('Genre', str),
|
"genre": ("Genre", str),
|
||||||
'tags': ('Tags', str),
|
"tags": ("Tags", str),
|
||||||
'web': ('Web', str),
|
"web": ("Web", str),
|
||||||
'format': ('Format', Format),
|
"format": ("Format", Format),
|
||||||
'ean': ('EAN', str),
|
"ean": ("EAN", str),
|
||||||
'black_white': ('BlackAndWhite', YesNo),
|
"black_white": ("BlackAndWhite", YesNo),
|
||||||
'manga': ('Manga', Manga),
|
"manga": ("Manga", Manga),
|
||||||
'characters': ('Characters', str),
|
"characters": ("Characters", str),
|
||||||
'teams': ('Teams', str),
|
"teams": ("Teams", str),
|
||||||
'locations': ('Locations', str),
|
"locations": ("Locations", str),
|
||||||
'scan_information': ('ScanInformation', str),
|
"scan_information": ("ScanInformation", str),
|
||||||
'story_arc': ('StoryArc', str),
|
"story_arc": ("StoryArc", str),
|
||||||
'story_arc_number': ('StoryArcNumber', int),
|
"story_arc_number": ("StoryArcNumber", int),
|
||||||
'series_group': ('SeriesGroup', str),
|
"series_group": ("SeriesGroup", str),
|
||||||
'age_rating': ('AgeRating', AgeRating),
|
"age_rating": ("AgeRating", AgeRating),
|
||||||
'main_character_or_team': ('MainCharacterOrTeam', str),
|
"main_character_or_team": ("MainCharacterOrTeam", str),
|
||||||
'review': ('Review', str),
|
"review": ("Review", str),
|
||||||
'language_iso': ('LanguageISO', LanguageISO),
|
"language_iso": ("LanguageISO", LanguageISO),
|
||||||
'community_rating': ('CommunityRating', Rating),
|
"community_rating": ("CommunityRating", Rating),
|
||||||
'added': ('Added', str),
|
"added": ("Added", str),
|
||||||
'released': ('Released', str),
|
"released": ("Released", str),
|
||||||
'file_size': ('FileSize', int),
|
"file_size": ("FileSize", int),
|
||||||
'file_modified_time': ('FileModifiedTime', str),
|
"file_modified_time": ("FileModifiedTime", str),
|
||||||
'file_creation_time': ('FileCreationTime', str),
|
"file_creation_time": ("FileCreationTime", str),
|
||||||
'book_price': ('BookPrice', str),
|
"book_price": ("BookPrice", str),
|
||||||
'custom_values_store': ('CustomValuesStore', str)
|
"custom_values_store": ("CustomValuesStore", str),
|
||||||
}
|
}
|
||||||
|
|
||||||
PAGE_FIELDS = {
|
PAGE_FIELDS = {
|
||||||
'image': ('@Image', int),
|
"image": ("@Image", int),
|
||||||
'type': ('@Type', PageType),
|
"type": ("@Type", PageType),
|
||||||
'double': ('@DoublePage', bool),
|
"double": ("@DoublePage", bool),
|
||||||
'key': ('@Key', str),
|
"key": ("@Key", str),
|
||||||
'bookmark': ('@Bookmark', str),
|
"bookmark": ("@Bookmark", str),
|
||||||
'image_size': ('@ImageSize', int),
|
"image_size": ("@ImageSize", int),
|
||||||
'image_width': ('@ImageWidth', int),
|
"image_width": ("@ImageWidth", int),
|
||||||
'image_height': ('@ImageHeight', int)
|
"image_height": ("@ImageHeight", int),
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-5
@@ -1,5 +1,19 @@
|
|||||||
from cbz.constants import COMIC_FIELDS, PAGE_FIELDS, Format, YesNo, Manga, AgeRating, LanguageISO, Rating, PageType
|
from cbz.constants import (
|
||||||
|
COMIC_FIELDS,
|
||||||
|
PAGE_FIELDS,
|
||||||
|
Format,
|
||||||
|
YesNo,
|
||||||
|
Manga,
|
||||||
|
AgeRating,
|
||||||
|
LanguageISO,
|
||||||
|
Rating,
|
||||||
|
PageType,
|
||||||
|
)
|
||||||
from cbz.utils import verify_attr, default_attr
|
from cbz.utils import verify_attr, default_attr
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class BaseModel:
|
class BaseModel:
|
||||||
@@ -21,7 +35,7 @@ class BaseModel:
|
|||||||
# Set default values for each attribute based on its type
|
# Set default values for each attribute based on its type
|
||||||
setattr(self, key, kwargs.get(key, default_attr(field_type)))
|
setattr(self, key, kwargs.get(key, default_attr(field_type)))
|
||||||
|
|
||||||
def __setattr__(self, key: str, value: any) -> None:
|
def __setattr__(self, key: str, value: "Any") -> None:
|
||||||
"""
|
"""
|
||||||
Sets the value of an attribute and verifies its type.
|
Sets the value of an attribute and verifies its type.
|
||||||
|
|
||||||
@@ -39,6 +53,9 @@ class BaseModel:
|
|||||||
value = field_type(value)
|
value = field_type(value)
|
||||||
# Verify that the assigned value matches the expected type
|
# Verify that the assigned value matches the expected type
|
||||||
verify_attr(field_type, key, value)
|
verify_attr(field_type, key, value)
|
||||||
|
if isinstance(value, float):
|
||||||
|
if value.is_integer():
|
||||||
|
value = int(value)
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
pass
|
pass
|
||||||
super().__setattr__(key, value)
|
super().__setattr__(key, value)
|
||||||
@@ -50,9 +67,15 @@ class BaseModel:
|
|||||||
Returns:
|
Returns:
|
||||||
str: A string representation of the object, displaying its class name and attribute key-value pairs.
|
str: A string representation of the object, displaying its class name and attribute key-value pairs.
|
||||||
"""
|
"""
|
||||||
return '{name}({items})'.format(
|
return "{name}({items})".format(
|
||||||
name=self.__class__.__name__,
|
name=self.__class__.__name__,
|
||||||
items=', '.join([f'{k}={repr(v)}' for k, v in self.__dict__.items() if not k.startswith('_')])
|
items=", ".join(
|
||||||
|
[
|
||||||
|
f"{k}={repr(v)}"
|
||||||
|
for k, v in self.__dict__.items()
|
||||||
|
if not k.startswith("_")
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,9 +83,10 @@ class ComicModel(BaseModel):
|
|||||||
"""
|
"""
|
||||||
Model for representing comic book metadata.
|
Model for representing comic book metadata.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title: str
|
title: str
|
||||||
series: str
|
series: str
|
||||||
number: int
|
number: float
|
||||||
count: int
|
count: int
|
||||||
volume: int
|
volume: int
|
||||||
alternate_series: str
|
alternate_series: str
|
||||||
@@ -124,6 +148,7 @@ class PageModel(BaseModel):
|
|||||||
"""
|
"""
|
||||||
Model for representing comic book pages.
|
Model for representing comic book pages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type: PageType
|
type: PageType
|
||||||
double: bool
|
double: bool
|
||||||
image_size: int
|
image_size: int
|
||||||
|
|||||||
+3
-1
@@ -75,6 +75,8 @@ class PageInfo(PageModel):
|
|||||||
data = base64.b64decode(data)
|
data = base64.b64decode(data)
|
||||||
if not isinstance(data, bytes):
|
if not isinstance(data, bytes):
|
||||||
raise ValueError(f'Expecting Bytes or Base64 input, got {data!r}')
|
raise ValueError(f'Expecting Bytes or Base64 input, got {data!r}')
|
||||||
|
if not data.strip():
|
||||||
|
raise ValueError(f'Empty or null data provided (length={len(data)})')
|
||||||
return cls(data, **kwargs)
|
return cls(data, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -98,7 +100,7 @@ class PageInfo(PageModel):
|
|||||||
path = Path(path)
|
path = Path(path)
|
||||||
with path.open(mode='rb') as f:
|
with path.open(mode='rb') as f:
|
||||||
kwargs.setdefault('name', path.name)
|
kwargs.setdefault('name', path.name)
|
||||||
return cls(f.read(), **kwargs)
|
return cls.loads(f.read(), **kwargs)
|
||||||
|
|
||||||
def show(self) -> None:
|
def show(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+17
-28
@@ -1,15 +1,10 @@
|
|||||||
[build-system]
|
[project]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|
||||||
[tool.poetry]
|
|
||||||
name = "cbz"
|
name = "cbz"
|
||||||
version = "3.4.3"
|
version = "3.4.5"
|
||||||
description = "CBZ simplifies creating, managing, and viewing comic book files in CBZ, CBR, and PDF formats, offering seamless packaging, metadata handling and built-in viewing capabilities."
|
description = "CBZ simplifies creating, managing, and viewing comic book files in CBZ, CBR, and PDF formats, offering seamless packaging, metadata handling and built-in viewing capabilities."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["hyugogirubato <65763543+hyugogirubato@users.noreply.github.com>"]
|
authors = [{name="hyugogirubato", email="65763543+hyugogirubato@users.noreply.github.com"}]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/hyugogirubato/cbz"
|
|
||||||
keywords = [
|
keywords = [
|
||||||
"python",
|
"python",
|
||||||
"cbz",
|
"cbz",
|
||||||
@@ -29,35 +24,29 @@ classifiers = [
|
|||||||
"Topic :: Utilities",
|
"Topic :: Utilities",
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
]
|
]
|
||||||
include = [
|
requires-python = ">=3.8,<4.0"
|
||||||
{ path = "CHANGELOG.md", format = "sdist" },
|
|
||||||
{ path = "README.md", format = "sdist" },
|
dependencies= [
|
||||||
{ path = "LICENSE", format = "sdist" },
|
"langcodes>=3.4.0",
|
||||||
|
"Pillow>=10.4.0",
|
||||||
|
"pypdf >=5.7.0",
|
||||||
|
"rarfile>=4.2",
|
||||||
|
"xmltodict>=0.14.2"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.urls]
|
[tool.poetry.urls]
|
||||||
"Issues" = "https://github.com/hyugogirubato/cbz/issues"
|
"Issues" = "https://github.com/hyugogirubato/cbz/issues"
|
||||||
"Changelog" = "https://github.com/hyugogirubato/cbz/blob/main/CHANGELOG.md"
|
"Changelog" = "https://github.com/hyugogirubato/cbz/blob/main/CHANGELOG.md"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = ">=3.8,<4.0"
|
|
||||||
langcodes = ">=3.4.0"
|
|
||||||
Pillow = ">=10.4.0"
|
|
||||||
pypdf = ">=5.7.0"
|
|
||||||
rarfile = ">=4.2"
|
|
||||||
xmltodict = ">=0.14.2"
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[project.optional-dependencies]
|
||||||
cbzplayer = "cbz.__main__:main"
|
pillow = ["pillow-avif-plugin>=1.5.2", "pillow-jxl-plugin>=1.3.4"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["pytest>=7.4.0,<9.0.0", "pytest-cov>=5.0.0"]
|
||||||
|
|
||||||
[[tool.poetry.source]]
|
|
||||||
name = "localpypi"
|
|
||||||
url = "https://pypi.org/simple/"
|
|
||||||
priority = "primary"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
pytest = ">=7.4.0,<9.0.0"
|
|
||||||
pytest-cov = ">=5.0.0"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|||||||
Reference in New Issue
Block a user