Skip to content

Commit

Permalink
Refactor Flow (#11)
Browse files Browse the repository at this point in the history
Refactored workflow so existing Info files are discarded, after parsing.
Update MetronInfo to match schema updates
  • Loading branch information
Buried-In-Code authored Apr 11, 2024
1 parent 0006602 commit 9d2ec08
Show file tree
Hide file tree
Showing 15 changed files with 737 additions and 1,280 deletions.
9 changes: 9 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ updates:
directory: /
schedule:
interval: daily
groups:
github_actions:
patterns:
- "*"

- package-ecosystem: pip
directory: /
schedule:
interval: daily
groups:
python:
patterns:
- "*"
7 changes: 2 additions & 5 deletions .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.3
rev: v0.3.5
hooks:
- id: ruff-format
- id: ruff
Expand All @@ -15,7 +15,7 @@ repos:
- --number
- --wrap=keep
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: check-ast
- id: check-builtin-literals
Expand All @@ -30,9 +30,6 @@ repos:
exclude_types:
- json
- xml
- id: fix-encoding-pragma
args:
- --remove
- id: mixed-line-ending
args:
- --fix=auto
Expand Down
174 changes: 118 additions & 56 deletions perdoo/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,18 @@
from tempfile import TemporaryDirectory

from pydantic import ValidationError
from rich.prompt import Prompt

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
from perdoo.models import ComicInfo, Metadata, MetronInfo
from perdoo.models.metadata import Format, Meta, Tool
from perdoo.models._base import InfoModel
from perdoo.models.metadata import Format, Meta, Source, Tool
from perdoo.models.metron_info import InformationSource
from perdoo.services import Comicvine, League, Marvel, Metron
from perdoo.settings import OutputFormat, Settings
from perdoo.utils import (
comic_to_metadata,
create_metadata,
list_files,
metadata_to_comic,
metadata_to_metron,
metron_to_metadata,
sanitize,
)
from perdoo.utils import Details, Identifications, get_metadata_id, list_files, sanitize

LOGGER = logging.getLogger("perdoo")

Expand All @@ -52,54 +47,120 @@ def convert_collection(path: Path, output: OutputFormat) -> None:
archive_type.convert(old_archive=archive)


def read_archive(archive: BaseArchive) -> tuple[Metadata, MetronInfo, ComicInfo]:
def read_meta(archive: BaseArchive) -> tuple[Meta, Details]:
filenames = archive.list_filenames()
metadata = None

def read_meta_file(cls: type[InfoModel], filename: str) -> InfoModel | None:
if filename in filenames:
return cls.from_bytes(content=archive.read_file(filename=filename))
return None

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"))
metadata = read_meta_file(cls=Metadata, filename="/Metadata.xml") or read_meta_file(
cls=Metadata, filename="Metadata.xml"
)
if metadata:
meta = metadata.meta
details = Details(
series=Identifications(
search=metadata.issue.series.title,
comicvine=get_metadata_id(
resources=metadata.issue.series.resources, source=Source.COMICVINE
),
league=get_metadata_id(
resources=metadata.issue.series.resources,
source=Source.LEAGUE_OF_COMIC_GEEKS,
),
marvel=get_metadata_id(
resources=metadata.issue.series.resources, source=Source.MARVEL
),
metron=get_metadata_id(
resources=metadata.issue.series.resources, source=Source.METRON
),
),
issue=Identifications(
search=metadata.issue.number,
comicvine=get_metadata_id(
resources=metadata.issue.resources, source=Source.COMICVINE
),
league=get_metadata_id(
resources=metadata.issue.resources, source=Source.LEAGUE_OF_COMIC_GEEKS
),
marvel=get_metadata_id(
resources=metadata.issue.resources, source=Source.MARVEL
),
metron=get_metadata_id(
resources=metadata.issue.resources, source=Source.METRON
),
),
)
return meta, details
except ValidationError:
LOGGER.error("%s contains an invalid Metadata file", archive.path.name) # noqa: TRY400
metron_info = None
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")
metron_info = read_meta_file(cls=MetronInfo, filename="/MetronInfo.xml") or read_meta_file(
cls=MetronInfo, filename="MetronInfo.xml"
)
if metron_info:
details = Details(
series=Identifications(
search=metron_info.series.name,
comicvine=metron_info.series.id
if metron_info.id and metron_info.id.source == InformationSource.COMIC_VINE
else None,
league=metron_info.series.id
if metron_info.id
and metron_info.id.source == InformationSource.LEAGUE_OF_COMIC_GEEKS
else None,
marvel=metron_info.series.id
if metron_info.id and metron_info.id.source == InformationSource.MARVEL
else None,
metron=metron_info.series.id
if metron_info.id and metron_info.id.source == InformationSource.METRON
else None,
),
issue=Identifications(
search=metron_info.number,
comicvine=metron_info.id.value
if metron_info.id and metron_info.id.source == InformationSource.COMIC_VINE
else None,
league=metron_info.id.value
if metron_info.id
and metron_info.id.source == InformationSource.LEAGUE_OF_COMIC_GEEKS
else None,
marvel=metron_info.id.value
if metron_info.id and metron_info.id.source == InformationSource.MARVEL
else None,
metron=metron_info.id.value
if metron_info.id and metron_info.id.source == InformationSource.METRON
else None,
),
)
return Meta(date_=date.today(), tool=Tool(value="MetronInfo")), details
except ValidationError:
LOGGER.error("%s contains an invalid MetronInfo file", archive.path.name) # noqa: TRY400
comic_info = None
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"))
comic_info = read_meta_file(cls=ComicInfo, filename="/ComicInfo.xml") or read_meta_file(
cls=ComicInfo, filename="ComicInfo.xml"
)
if comic_info:
details = Details(
series=Identifications(search=comic_info.series),
issue=Identifications(search=comic_info.number),
)
return Meta(date_=date.today(), tool=Tool(value="ComicInfo")), details
except ValidationError:
LOGGER.error("%s contains an invalid ComicInfo file", archive.path.name) # noqa: TRY400

if not metadata:
if metron_info:
metadata = metron_to_metadata(metron_info=metron_info)
elif comic_info:
metadata = comic_to_metadata(comic_info=comic_info)
else:
metadata = create_metadata(archive=archive)
if not metron_info:
metron_info = metadata_to_metron(metadata=metadata)
if not comic_info:
comic_info = metadata_to_comic(metadata=metadata)
return metadata, metron_info, comic_info
return Meta(date_=date.today(), tool=Tool(value="Manual")), Details(
series=Identifications(search=Prompt.ask("Series title", console=CONSOLE)),
issue=Identifications(),
)


def fetch_from_services(
settings: Settings, metainfo: tuple[Metadata, MetronInfo, ComicInfo]
) -> None:
settings: Settings, details: Details
) -> tuple[Metadata | None, MetronInfo | None, ComicInfo | None]:
marvel = None
if settings.marvel and settings.marvel.public_key and settings.marvel.private_key:
marvel = Marvel(settings=settings.marvel)
Expand All @@ -118,13 +179,13 @@ def fetch_from_services(
league = League(settings.league_of_comic_geeks)
if not marvel and not metron and not comicvine and not league:
LOGGER.warning("No external services configured")
return
return None, None, None

success = any(
service and service.fetch(*metainfo) for service in (marvel, metron, comicvine, league)
)
if not success:
LOGGER.warning("Unable to fetch information fron any service")
for service in (marvel, metron, comicvine, league):
metadata, metron_info, comic_info = service.fetch(details=details)
if metadata and metron_info and comic_info:
return metadata, metron_info, comic_info
return None, None, None


def generate_filename(root: Path, extension: str, metadata: Metadata) -> Path:
Expand Down Expand Up @@ -169,7 +230,7 @@ def rename_images(folder: Path, filename: str) -> None:
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}")
shutil.move(img_file, folder / new_filename)


def process_pages(
Expand Down Expand Up @@ -210,16 +271,16 @@ def start(settings: Settings, force: bool = False) -> None:
convert_collection(path=settings.collection_folder, output=settings.output.format)
for file in list_files(settings.collection_folder, f".{settings.output.format}"):
archive = get_archive(path=file)
metadata, metron_info, comic_info = read_archive(archive=archive)
CONSOLE.rule(file.stem)
LOGGER.info("Processing %s", file.stem)
meta, details = read_meta(archive=archive)

if not force:
difference = abs(date.today() - metadata.meta.date_)
if metadata.meta.tool == Tool() and difference.days < 28:
difference = abs(date.today() - meta.date_)
if meta.tool == Tool() and difference.days < 28:
continue

CONSOLE.rule(file.stem)
LOGGER.info("Processing %s", file.name)
fetch_from_services(settings=settings, metainfo=(metadata, metron_info, comic_info))
metadata, metron_info, comic_info = fetch_from_services(settings=settings, details=details)
new_file = generate_filename(
root=settings.collection_folder,
extension=settings.output.format.value,
Expand All @@ -229,6 +290,7 @@ def start(settings: Settings, force: bool = False) -> None:
temp_folder = Path(temp_str)
if not archive.extract_files(destination=temp_folder):
return
LOGGER.info("Processing %s pages", file.stem)
process_pages(
folder=temp_folder,
metadata=metadata,
Expand Down
19 changes: 2 additions & 17 deletions perdoo/console.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from __future__ import annotations

__all__ = ["CONSOLE", "create_menu", "DatePrompt"]
__all__ = ["CONSOLE", "create_menu"]

import logging
from datetime import date, datetime

from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.prompt import IntPrompt, InvalidResponse, PromptBase
from rich.prompt import IntPrompt
from rich.theme import Theme

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -70,17 +69,3 @@ def create_menu(
options=options, title=title, subtitle=subtitle, prompt=prompt, default=default
)
return selected


class DatePrompt(PromptBase[date]):
response_type = date
validate_error_message = (
"[prompt.invalid]Please enter a valid date in the format of 'yyyy-mm-dd'"
)
prompt_suffix = "[yyyy-mm-dd]: "

def process_response(self: DatePrompt, value: str) -> date:
try:
return datetime.strptime(value.strip(), "%Y-%m-%d").date()
except ValueError as err:
raise InvalidResponse(self.validate_error_message) from err
14 changes: 7 additions & 7 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, HttpUrl
from pydantic import Field, HttpUrl, NonNegativeFloat

from perdoo.models._base import InfoModel, PascalModel

Expand All @@ -37,7 +37,7 @@ def load(value: str) -> YesNo:
for entry in YesNo:
if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold():
return entry
raise ValueError(f"'{value}' isnt a valid comic_info.YesNo")
raise ValueError(f"`{value}` isn't a valid comic_info.YesNo")

def __lt__(self: YesNo, other) -> int: # noqa: ANN001
if not isinstance(other, type(self)):
Expand All @@ -59,7 +59,7 @@ def load(value: str) -> Manga:
for entry in Manga:
if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold():
return entry
raise ValueError(f"'{value}' isnt a valid comic_info.Manga")
raise ValueError(f"`{value}` isn't a valid comic_info.Manga")

def __lt__(self: Manga, other) -> int: # noqa: ANN001
if not isinstance(other, type(self)):
Expand Down Expand Up @@ -92,7 +92,7 @@ def load(value: str) -> AgeRating:
for entry in AgeRating:
if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold():
return entry
raise ValueError(f"'{value}' isnt a valid comic_info.AgeRating")
raise ValueError(f"`{value}` isn't a valid comic_info.AgeRating")

def __lt__(self: AgeRating, other) -> int: # noqa: ANN001
if not isinstance(other, type(self)):
Expand Down Expand Up @@ -121,7 +121,7 @@ def load(value: str) -> PageType:
for entry in PageType:
if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold():
return entry
raise ValueError(f"'{value}' isnt a valid comic_info.PageType")
raise ValueError(f"`{value}` isn't a valid comic_info.PageType")

def __lt__(self: PageType, other) -> int: # noqa: ANN001
if not isinstance(other, type(self)):
Expand Down Expand Up @@ -215,14 +215,14 @@ class ComicInfo(PascalModel, InfoModel):
series_group: str | None = None
age_rating: AgeRating = AgeRating.UNKNOWN
pages: list[Page] = Field(default_factory=list)
community_rating: float | None = Field(default=None, ge=0, le=5)
community_rating: NonNegativeFloat | None = Field(default=None, le=5)
main_character_or_team: str | None = None
review: str | None = None

list_fields: ClassVar[dict[str, str]] = {"Pages": "Page"}

def __init__(self: ComicInfo, **data: Any):
self.unwrap_list(mappings=ComicInfo.list_fields, content=data)
self.unwrap_list(mappings=self.list_fields, content=data)
super().__init__(**data)

@property
Expand Down
Loading

0 comments on commit 9d2ec08

Please sign in to comment.