Skip to content

Commit 0006602

Browse files
Metron Service (#8)
- Add Metron Implementation - Change urls to Pydantic's HttpUrl - Check for Info file that may have a leading "/" - Add more Manual matches for MetronInfo.Format
1 parent 54a8101 commit 0006602

12 files changed

Lines changed: 632 additions & 76 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
3-
rev: v0.3.2
3+
rev: v0.3.3
44
hooks:
55
- id: ruff-format
66
- id: ruff

perdoo/__main__.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from platform import python_version
99
from tempfile import TemporaryDirectory
1010

11+
from pydantic import ValidationError
12+
1113
from perdoo import ARCHIVE_EXTENSIONS, IMAGE_EXTENSIONS, __version__, setup_logging
1214
from perdoo.archives import BaseArchive, CB7Archive, CBTArchive, CBZArchive, get_archive
1315
from perdoo.console import CONSOLE
@@ -53,14 +55,33 @@ def convert_collection(path: Path, output: OutputFormat) -> None:
5355
def read_archive(archive: BaseArchive) -> tuple[Metadata, MetronInfo, ComicInfo]:
5456
filenames = archive.list_filenames()
5557
metadata = None
56-
if "Metadata.xml" in filenames:
57-
metadata = Metadata.from_bytes(content=archive.read_file(filename="Metadata.xml"))
58+
try:
59+
if "/Metadata.xml" in filenames:
60+
metadata = Metadata.from_bytes(content=archive.read_file(filename="/Metadata.xml"))
61+
elif "Metadata.xml" in filenames:
62+
metadata = Metadata.from_bytes(content=archive.read_file(filename="Metadata.xml"))
63+
except ValidationError:
64+
LOGGER.error("%s contains an invalid Metadata file", archive.path.name) # noqa: TRY400
5865
metron_info = None
59-
if "MetronInfo.xml" in filenames:
60-
metron_info = MetronInfo.from_bytes(content=archive.read_file(filename="MetronInfo.xml"))
66+
try:
67+
if "/MetronInfo.xml" in filenames:
68+
metron_info = MetronInfo.from_bytes(
69+
content=archive.read_file(filename="/MetronInfo.xml")
70+
)
71+
elif "MetronInfo.xml" in filenames:
72+
metron_info = MetronInfo.from_bytes(
73+
content=archive.read_file(filename="MetronInfo.xml")
74+
)
75+
except ValidationError:
76+
LOGGER.error("%s contains an invalid MetronInfo file", archive.path.name) # noqa: TRY400
6177
comic_info = None
62-
if "ComicInfo.xml" in filenames:
63-
comic_info = ComicInfo.from_bytes(content=archive.read_file(filename="ComicInfo.xml"))
78+
try:
79+
if "/ComicInfo.xml" in filenames:
80+
comic_info = ComicInfo.from_bytes(content=archive.read_file(filename="/ComicInfo.xml"))
81+
elif "ComicInfo.xml" in filenames:
82+
comic_info = ComicInfo.from_bytes(content=archive.read_file(filename="ComicInfo.xml"))
83+
except ValidationError:
84+
LOGGER.error("%s contains an invalid ComicInfo file", archive.path.name) # noqa: TRY400
6485

6586
if not metadata:
6687
if metron_info:
@@ -80,7 +101,7 @@ def fetch_from_services(
80101
settings: Settings, metainfo: tuple[Metadata, MetronInfo, ComicInfo]
81102
) -> None:
82103
marvel = None
83-
if settings.marvel and settings.marvel.public_key and settings.marve.private_key:
104+
if settings.marvel and settings.marvel.public_key and settings.marvel.private_key:
84105
marvel = Marvel(settings=settings.marvel)
85106
metron = None
86107
if settings.metron and settings.metron.username and settings.metron.password:
@@ -115,7 +136,7 @@ def generate_filename(root: Path, extension: str, metadata: Metadata) -> Path:
115136
)
116137

117138
number_str = (
118-
f"_#{metadata.issue.number.zfill(3 if metadata.issue.format_ == Format.COMIC else 2)}"
139+
f"_#{metadata.issue.number.zfill(3 if metadata.issue.format == Format.COMIC else 2)}"
119140
if metadata.issue.number
120141
else ""
121142
)
@@ -125,10 +146,10 @@ def generate_filename(root: Path, extension: str, metadata: Metadata) -> Path:
125146
Format.GRAPHIC_NOVEL: "_GN",
126147
Format.HARDCOVER: "_HC",
127148
Format.TRADE_PAPERBACK: "_TP",
128-
}.get(metadata.issue.format_, "")
129-
if metadata.issue.format_ in {Format.ANNUAL, Format.DIGITAL_CHAPTER}:
149+
}.get(metadata.issue.format, "")
150+
if metadata.issue.format in {Format.ANNUAL, Format.DIGITAL_CHAPTER}:
130151
issue_filename = sanitize(value=series_filename) + format_str + number_str
131-
elif metadata.issue.format_ in {Format.GRAPHIC_NOVEL, Format.HARDCOVER, Format.TRADE_PAPERBACK}:
152+
elif metadata.issue.format in {Format.GRAPHIC_NOVEL, Format.HARDCOVER, Format.TRADE_PAPERBACK}:
132153
issue_filename = sanitize(value=series_filename) + number_str + format_str
133154
else:
134155
issue_filename = sanitize(value=series_filename) + number_str
@@ -145,7 +166,7 @@ def rename_images(folder: Path, filename: str) -> None:
145166
image_list = list_files(folder, *IMAGE_EXTENSIONS)
146167
pad_count = len(str(len(image_list)))
147168
for index, img_file in enumerate(image_list):
148-
new_filename = f"{filename}-{str(index).zfill(pad_count)}{img_file.suffix}"
169+
new_filename = f"{filename}_{str(index).zfill(pad_count)}{img_file.suffix}"
149170
if img_file.name != new_filename:
150171
LOGGER.info("Renamed %s to %s", img_file.name, new_filename)
151172
img_file.rename(folder / f"{filename}-{str(index).zfill(pad_count)}{img_file.suffix}")

perdoo/models/comic_info.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import xmltodict
1111
from natsort import humansorted, ns
1212
from PIL import Image
13-
from pydantic import Field
13+
from pydantic import Field, HttpUrl
1414

1515
from perdoo.models._base import InfoModel, PascalModel
1616

@@ -134,7 +134,7 @@ def __str__(self: PageType) -> str:
134134

135135
class Page(PascalModel):
136136
image: int = Field(alias="@Image")
137-
type_: PageType = Field(alias="@Type", default=PageType.STORY)
137+
type: PageType = Field(alias="@Type", default=PageType.STORY)
138138
double_page: bool = Field(alias="@DoublePage", default=False)
139139
image_size: int = Field(alias="@ImageSize", default=0)
140140
key: str | None = Field(alias="@Key", default=None)
@@ -158,7 +158,7 @@ def __hash__(self: Page) -> int:
158158
@staticmethod
159159
def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) -> Page:
160160
if page:
161-
page_type = page.type_
161+
page_type = page.type
162162
elif index == 0:
163163
page_type = PageType.FRONT_COVER
164164
elif is_final_page:
@@ -169,7 +169,7 @@ def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) ->
169169
width, height = img.size
170170
return Page(
171171
image=index,
172-
type_=page_type,
172+
type=page_type,
173173
double_page=width >= height,
174174
image_size=file.stat().st_size,
175175
image_height=height,
@@ -201,10 +201,10 @@ class ComicInfo(PascalModel, InfoModel):
201201
publisher: str | None = None
202202
imprint: str | None = None
203203
genre: str | None = None
204-
web: str | None = None
204+
web: HttpUrl | None = None
205205
page_count: int = 0
206206
language_iso: str | None = Field(alias="LanguageISO", default=None)
207-
format_: str | None = Field(alias="Format", default=None)
207+
format: str | None = None
208208
black_and_white: YesNo = YesNo.UNKNOWN
209209
manga: Manga = Manga.UNKNOWN
210210
characters: str | None = None

perdoo/models/metadata.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ class Issue(PascalModel):
184184
characters: list[TitledResource] = Field(default_factory=list)
185185
cover_date: date | None = None
186186
credits: list[Credit] = Field(default_factory=list)
187-
format_: Format = Field(alias="Format", default=Format.COMIC)
187+
format: Format = Format.COMIC
188188
language: str = Field(alias="@language", default="en")
189189
locations: list[TitledResource] = Field(default_factory=list)
190190
number: str | None = None
@@ -280,7 +280,7 @@ class Page(PascalModel):
280280
height: int = Field(alias="@height")
281281
index: int = Field(alias="@index")
282282
size: int = Field(alias="@size")
283-
type_: PageType = Field(alias="@type", default=PageType.STORY)
283+
type: PageType = Field(alias="@type", default=PageType.STORY)
284284
width: int = Field(alias="@width")
285285

286286
def __lt__(self: Page, other) -> int: # noqa: ANN001
@@ -299,7 +299,7 @@ def __hash__(self: Page) -> int:
299299
@staticmethod
300300
def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) -> Page:
301301
if page:
302-
page_type = page.type_
302+
page_type = page.type
303303
elif index == 0:
304304
page_type = PageType.FRONT_COVER
305305
elif is_final_page:
@@ -314,7 +314,7 @@ def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) ->
314314
height=height,
315315
index=index,
316316
size=file.stat().st_size,
317-
type_=page_type,
317+
type=page_type,
318318
width=width,
319319
)
320320

perdoo/models/metron_info.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import xmltodict
2929
from PIL import Image
30-
from pydantic import Field
30+
from pydantic import Field, HttpUrl
3131

3232
from perdoo.models._base import InfoModel, PascalModel
3333

@@ -91,6 +91,14 @@ def load(value: str) -> Format:
9191
for entry in Format:
9292
if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold():
9393
return entry
94+
# region Manual matches
95+
if value.casefold() == "Limited Series".casefold():
96+
return Format.LIMITED
97+
if value.casefold() == "Cancelled Series".casefold():
98+
return Format.SERIES
99+
if value.casefold() == "Hard Cover".casefold():
100+
return Format.SERIES
101+
# endregion
94102
raise ValueError(f"'{value}' isnt a valid metron_info.Format")
95103

96104
def __lt__(self: Format, other) -> int: # noqa: ANN001
@@ -108,7 +116,7 @@ class Series(PascalModel):
108116
name: str
109117
sort_name: str | None = None
110118
volume: int | None = None
111-
format_: Format | None = Field(alias="Format", default=None)
119+
format: Format | None = None
112120

113121

114122
class Price(PascalModel):
@@ -362,7 +370,7 @@ def __str__(self: PageType) -> str:
362370

363371
class Page(PascalModel):
364372
image: int = Field(alias="@Image")
365-
type_: PageType = Field(alias="@Type", default=PageType.STORY)
373+
type: PageType = Field(alias="@Type", default=PageType.STORY)
366374
double_page: bool = Field(alias="@DoublePage", default=False)
367375
image_size: int = Field(alias="@ImageSize", default=0)
368376
key: str | None = Field(alias="@Key", default=None)
@@ -386,7 +394,7 @@ def __hash__(self: Page) -> int:
386394
@staticmethod
387395
def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) -> Page:
388396
if page:
389-
page_type = page.type_
397+
page_type = page.type
390398
elif index == 0:
391399
page_type = PageType.FRONT_COVER
392400
elif is_final_page:
@@ -397,7 +405,7 @@ def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) ->
397405
width, height = img.size
398406
return Page(
399407
image=index,
400-
type_=page_type,
408+
type=page_type,
401409
double_page=width >= height,
402410
image_size=file.stat().st_size,
403411
image_height=height,
@@ -428,7 +436,7 @@ class MetronInfo(PascalModel, InfoModel):
428436
gtin: GTIN | None = Field(alias="GTIN", default=None)
429437
age_rating: AgeRating = Field(default=AgeRating.UNKNOWN)
430438
reprints: list[Resource] = Field(default_factory=list)
431-
url: str | None = Field(alias="URL", default=None)
439+
url: HttpUrl | None = Field(alias="URL", default=None)
432440
credits: list[Credit] = Field(default_factory=list)
433441
pages: list[Page] = Field(default_factory=list)
434442

@@ -481,7 +489,6 @@ def to_file(self: MetronInfo, file: Path) -> None:
481489
with file.open("wb") as stream:
482490
xmltodict.unparse(
483491
{"MetronInfo": {k: content[k] for k in sorted(content)}},
484-
# {"MetronInfo": content},
485492
output=stream,
486493
short_empty_elements=True,
487494
pretty=True,

perdoo/services/_base.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,63 @@
33
__all__ = ["BaseService"]
44

55
from abc import abstractmethod
6+
from typing import Generic, TypeVar
67

78
from perdoo.models import ComicInfo, Metadata, MetronInfo
89

10+
P = TypeVar("P")
11+
S = TypeVar("S")
12+
C = TypeVar("C")
13+
14+
15+
class BaseService(Generic[P, S, C]):
16+
@abstractmethod
17+
def _search_publishers(self: BaseService, title: str | None) -> int | None: ...
18+
19+
@abstractmethod
20+
def _get_publisher_id(
21+
self: BaseService, metadata: Metadata, metron_info: MetronInfo
22+
) -> int | None: ...
23+
24+
@abstractmethod
25+
def fetch_publisher(
26+
self: BaseService, metadata: Metadata, metron_info: MetronInfo, comic_info: ComicInfo
27+
) -> P | None: ...
28+
29+
@abstractmethod
30+
def _search_series(self: BaseService, publisher_id: int, title: str | None) -> int | None: ...
31+
32+
@abstractmethod
33+
def _get_series_id(
34+
self: BaseService, publisher_id: int, metadata: Metadata, metron_info: MetronInfo
35+
) -> int | None: ...
36+
37+
@abstractmethod
38+
def fetch_series(
39+
self: BaseService,
40+
metadata: Metadata,
41+
metron_info: MetronInfo,
42+
comic_info: ComicInfo,
43+
publisher_id: int,
44+
) -> S | None: ...
45+
46+
@abstractmethod
47+
def _search_issues(self: BaseService, series_id: int, number: str | None) -> int | None: ...
48+
49+
@abstractmethod
50+
def _get_issue_id(
51+
self: BaseService, series_id: int, metadata: Metadata, metron_info: MetronInfo
52+
) -> int | None: ...
53+
54+
@abstractmethod
55+
def fetch_issue(
56+
self: BaseService,
57+
metadata: Metadata,
58+
metron_info: MetronInfo,
59+
comic_info: ComicInfo,
60+
series_id: int,
61+
) -> C | None: ...
962

10-
class BaseService:
1163
@abstractmethod
1264
def fetch(
1365
self: BaseService,

0 commit comments

Comments
 (0)