Skip to content

Commit

Permalink
Metron Service (#8)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
Buried-In-Code authored Mar 19, 2024
1 parent 54a8101 commit 0006602
Show file tree
Hide file tree
Showing 12 changed files with 632 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.2
rev: v0.3.3
hooks:
- id: ruff-format
- id: ruff
Expand Down
45 changes: 33 additions & 12 deletions perdoo/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from platform import python_version
from tempfile import TemporaryDirectory

from pydantic import ValidationError

from perdoo import ARCHIVE_EXTENSIONS, IMAGE_EXTENSIONS, __version__, setup_logging
from perdoo.archives import BaseArchive, CB7Archive, CBTArchive, CBZArchive, get_archive
from perdoo.console import CONSOLE
Expand Down Expand Up @@ -53,14 +55,33 @@ def convert_collection(path: Path, output: OutputFormat) -> None:
def read_archive(archive: BaseArchive) -> tuple[Metadata, MetronInfo, ComicInfo]:
filenames = archive.list_filenames()
metadata = None
if "Metadata.xml" in filenames:
metadata = Metadata.from_bytes(content=archive.read_file(filename="Metadata.xml"))
try:
if "/Metadata.xml" in filenames:
metadata = Metadata.from_bytes(content=archive.read_file(filename="/Metadata.xml"))
elif "Metadata.xml" in filenames:
metadata = Metadata.from_bytes(content=archive.read_file(filename="Metadata.xml"))
except ValidationError:
LOGGER.error("%s contains an invalid Metadata file", archive.path.name) # noqa: TRY400
metron_info = None
if "MetronInfo.xml" in filenames:
metron_info = MetronInfo.from_bytes(content=archive.read_file(filename="MetronInfo.xml"))
try:
if "/MetronInfo.xml" in filenames:
metron_info = MetronInfo.from_bytes(
content=archive.read_file(filename="/MetronInfo.xml")
)
elif "MetronInfo.xml" in filenames:
metron_info = MetronInfo.from_bytes(
content=archive.read_file(filename="MetronInfo.xml")
)
except ValidationError:
LOGGER.error("%s contains an invalid MetronInfo file", archive.path.name) # noqa: TRY400
comic_info = None
if "ComicInfo.xml" in filenames:
comic_info = ComicInfo.from_bytes(content=archive.read_file(filename="ComicInfo.xml"))
try:
if "/ComicInfo.xml" in filenames:
comic_info = ComicInfo.from_bytes(content=archive.read_file(filename="/ComicInfo.xml"))
elif "ComicInfo.xml" in filenames:
comic_info = ComicInfo.from_bytes(content=archive.read_file(filename="ComicInfo.xml"))
except ValidationError:
LOGGER.error("%s contains an invalid ComicInfo file", archive.path.name) # noqa: TRY400

if not metadata:
if metron_info:
Expand All @@ -80,7 +101,7 @@ def fetch_from_services(
settings: Settings, metainfo: tuple[Metadata, MetronInfo, ComicInfo]
) -> None:
marvel = None
if settings.marvel and settings.marvel.public_key and settings.marve.private_key:
if settings.marvel and settings.marvel.public_key and settings.marvel.private_key:
marvel = Marvel(settings=settings.marvel)
metron = None
if settings.metron and settings.metron.username and settings.metron.password:
Expand Down Expand Up @@ -115,7 +136,7 @@ def generate_filename(root: Path, extension: str, metadata: Metadata) -> Path:
)

number_str = (
f"_#{metadata.issue.number.zfill(3 if metadata.issue.format_ == Format.COMIC else 2)}"
f"_#{metadata.issue.number.zfill(3 if metadata.issue.format == Format.COMIC else 2)}"
if metadata.issue.number
else ""
)
Expand All @@ -125,10 +146,10 @@ def generate_filename(root: Path, extension: str, metadata: Metadata) -> Path:
Format.GRAPHIC_NOVEL: "_GN",
Format.HARDCOVER: "_HC",
Format.TRADE_PAPERBACK: "_TP",
}.get(metadata.issue.format_, "")
if metadata.issue.format_ in {Format.ANNUAL, Format.DIGITAL_CHAPTER}:
}.get(metadata.issue.format, "")
if metadata.issue.format in {Format.ANNUAL, Format.DIGITAL_CHAPTER}:
issue_filename = sanitize(value=series_filename) + format_str + number_str
elif metadata.issue.format_ in {Format.GRAPHIC_NOVEL, Format.HARDCOVER, Format.TRADE_PAPERBACK}:
elif metadata.issue.format in {Format.GRAPHIC_NOVEL, Format.HARDCOVER, Format.TRADE_PAPERBACK}:
issue_filename = sanitize(value=series_filename) + number_str + format_str
else:
issue_filename = sanitize(value=series_filename) + number_str
Expand All @@ -145,7 +166,7 @@ def rename_images(folder: Path, filename: str) -> None:
image_list = list_files(folder, *IMAGE_EXTENSIONS)
pad_count = len(str(len(image_list)))
for index, img_file in enumerate(image_list):
new_filename = f"{filename}-{str(index).zfill(pad_count)}{img_file.suffix}"
new_filename = f"{filename}_{str(index).zfill(pad_count)}{img_file.suffix}"
if img_file.name != new_filename:
LOGGER.info("Renamed %s to %s", img_file.name, new_filename)
img_file.rename(folder / f"{filename}-{str(index).zfill(pad_count)}{img_file.suffix}")
Expand Down
12 changes: 6 additions & 6 deletions perdoo/models/comic_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import xmltodict
from natsort import humansorted, ns
from PIL import Image
from pydantic import Field
from pydantic import Field, HttpUrl

from perdoo.models._base import InfoModel, PascalModel

Expand Down Expand Up @@ -134,7 +134,7 @@ def __str__(self: PageType) -> str:

class Page(PascalModel):
image: int = Field(alias="@Image")
type_: PageType = Field(alias="@Type", default=PageType.STORY)
type: PageType = Field(alias="@Type", default=PageType.STORY)
double_page: bool = Field(alias="@DoublePage", default=False)
image_size: int = Field(alias="@ImageSize", default=0)
key: str | None = Field(alias="@Key", default=None)
Expand All @@ -158,7 +158,7 @@ def __hash__(self: Page) -> int:
@staticmethod
def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) -> Page:
if page:
page_type = page.type_
page_type = page.type
elif index == 0:
page_type = PageType.FRONT_COVER
elif is_final_page:
Expand All @@ -169,7 +169,7 @@ def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) ->
width, height = img.size
return Page(
image=index,
type_=page_type,
type=page_type,
double_page=width >= height,
image_size=file.stat().st_size,
image_height=height,
Expand Down Expand Up @@ -201,10 +201,10 @@ class ComicInfo(PascalModel, InfoModel):
publisher: str | None = None
imprint: str | None = None
genre: str | None = None
web: str | None = None
web: HttpUrl | None = None
page_count: int = 0
language_iso: str | None = Field(alias="LanguageISO", default=None)
format_: str | None = Field(alias="Format", default=None)
format: str | None = None
black_and_white: YesNo = YesNo.UNKNOWN
manga: Manga = Manga.UNKNOWN
characters: str | None = None
Expand Down
8 changes: 4 additions & 4 deletions perdoo/models/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class Issue(PascalModel):
characters: list[TitledResource] = Field(default_factory=list)
cover_date: date | None = None
credits: list[Credit] = Field(default_factory=list)
format_: Format = Field(alias="Format", default=Format.COMIC)
format: Format = Format.COMIC
language: str = Field(alias="@language", default="en")
locations: list[TitledResource] = Field(default_factory=list)
number: str | None = None
Expand Down Expand Up @@ -280,7 +280,7 @@ class Page(PascalModel):
height: int = Field(alias="@height")
index: int = Field(alias="@index")
size: int = Field(alias="@size")
type_: PageType = Field(alias="@type", default=PageType.STORY)
type: PageType = Field(alias="@type", default=PageType.STORY)
width: int = Field(alias="@width")

def __lt__(self: Page, other) -> int: # noqa: ANN001
Expand All @@ -299,7 +299,7 @@ def __hash__(self: Page) -> int:
@staticmethod
def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) -> Page:
if page:
page_type = page.type_
page_type = page.type
elif index == 0:
page_type = PageType.FRONT_COVER
elif is_final_page:
Expand All @@ -314,7 +314,7 @@ def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) ->
height=height,
index=index,
size=file.stat().st_size,
type_=page_type,
type=page_type,
width=width,
)

Expand Down
21 changes: 14 additions & 7 deletions perdoo/models/metron_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import xmltodict
from PIL import Image
from pydantic import Field
from pydantic import Field, HttpUrl

from perdoo.models._base import InfoModel, PascalModel

Expand Down Expand Up @@ -91,6 +91,14 @@ def load(value: str) -> Format:
for entry in Format:
if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold():
return entry
# region Manual matches
if value.casefold() == "Limited Series".casefold():
return Format.LIMITED
if value.casefold() == "Cancelled Series".casefold():
return Format.SERIES
if value.casefold() == "Hard Cover".casefold():
return Format.SERIES
# endregion
raise ValueError(f"'{value}' isnt a valid metron_info.Format")

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


class Price(PascalModel):
Expand Down Expand Up @@ -362,7 +370,7 @@ def __str__(self: PageType) -> str:

class Page(PascalModel):
image: int = Field(alias="@Image")
type_: PageType = Field(alias="@Type", default=PageType.STORY)
type: PageType = Field(alias="@Type", default=PageType.STORY)
double_page: bool = Field(alias="@DoublePage", default=False)
image_size: int = Field(alias="@ImageSize", default=0)
key: str | None = Field(alias="@Key", default=None)
Expand All @@ -386,7 +394,7 @@ def __hash__(self: Page) -> int:
@staticmethod
def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) -> Page:
if page:
page_type = page.type_
page_type = page.type
elif index == 0:
page_type = PageType.FRONT_COVER
elif is_final_page:
Expand All @@ -397,7 +405,7 @@ def from_path(file: Path, index: int, is_final_page: bool, page: Page | None) ->
width, height = img.size
return Page(
image=index,
type_=page_type,
type=page_type,
double_page=width >= height,
image_size=file.stat().st_size,
image_height=height,
Expand Down Expand Up @@ -428,7 +436,7 @@ class MetronInfo(PascalModel, InfoModel):
gtin: GTIN | None = Field(alias="GTIN", default=None)
age_rating: AgeRating = Field(default=AgeRating.UNKNOWN)
reprints: list[Resource] = Field(default_factory=list)
url: str | None = Field(alias="URL", default=None)
url: HttpUrl | None = Field(alias="URL", default=None)
credits: list[Credit] = Field(default_factory=list)
pages: list[Page] = Field(default_factory=list)

Expand Down Expand Up @@ -481,7 +489,6 @@ def to_file(self: MetronInfo, file: Path) -> None:
with file.open("wb") as stream:
xmltodict.unparse(
{"MetronInfo": {k: content[k] for k in sorted(content)}},
# {"MetronInfo": content},
output=stream,
short_empty_elements=True,
pretty=True,
Expand Down
54 changes: 53 additions & 1 deletion perdoo/services/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,63 @@
__all__ = ["BaseService"]

from abc import abstractmethod
from typing import Generic, TypeVar

from perdoo.models import ComicInfo, Metadata, MetronInfo

P = TypeVar("P")
S = TypeVar("S")
C = TypeVar("C")


class BaseService(Generic[P, S, C]):
@abstractmethod
def _search_publishers(self: BaseService, title: str | None) -> int | None: ...

@abstractmethod
def _get_publisher_id(
self: BaseService, metadata: Metadata, metron_info: MetronInfo
) -> int | None: ...

@abstractmethod
def fetch_publisher(
self: BaseService, metadata: Metadata, metron_info: MetronInfo, comic_info: ComicInfo
) -> P | None: ...

@abstractmethod
def _search_series(self: BaseService, publisher_id: int, title: str | None) -> int | None: ...

@abstractmethod
def _get_series_id(
self: BaseService, publisher_id: int, metadata: Metadata, metron_info: MetronInfo
) -> int | None: ...

@abstractmethod
def fetch_series(
self: BaseService,
metadata: Metadata,
metron_info: MetronInfo,
comic_info: ComicInfo,
publisher_id: int,
) -> S | None: ...

@abstractmethod
def _search_issues(self: BaseService, series_id: int, number: str | None) -> int | None: ...

@abstractmethod
def _get_issue_id(
self: BaseService, series_id: int, metadata: Metadata, metron_info: MetronInfo
) -> int | None: ...

@abstractmethod
def fetch_issue(
self: BaseService,
metadata: Metadata,
metron_info: MetronInfo,
comic_info: ComicInfo,
series_id: int,
) -> C | None: ...

class BaseService:
@abstractmethod
def fetch(
self: BaseService,
Expand Down
Loading

0 comments on commit 0006602

Please sign in to comment.