Skip to content

Commit 9d2ec08

Browse files
Refactor Flow (#11)
Refactored workflow so existing Info files are discarded, after parsing. Update MetronInfo to match schema updates
1 parent 0006602 commit 9d2ec08

15 files changed

Lines changed: 737 additions & 1280 deletions

.github/dependabot.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ updates:
44
directory: /
55
schedule:
66
interval: daily
7+
groups:
8+
github_actions:
9+
patterns:
10+
- "*"
11+
712
- package-ecosystem: pip
813
directory: /
914
schedule:
1015
interval: daily
16+
groups:
17+
python:
18+
patterns:
19+
- "*"

.pre-commit-config.yaml

Lines changed: 2 additions & 5 deletions
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.3
3+
rev: v0.3.5
44
hooks:
55
- id: ruff-format
66
- id: ruff
@@ -15,7 +15,7 @@ repos:
1515
- --number
1616
- --wrap=keep
1717
- repo: https://github.com/pre-commit/pre-commit-hooks
18-
rev: v4.5.0
18+
rev: v4.6.0
1919
hooks:
2020
- id: check-ast
2121
- id: check-builtin-literals
@@ -30,9 +30,6 @@ repos:
3030
exclude_types:
3131
- json
3232
- xml
33-
- id: fix-encoding-pragma
34-
args:
35-
- --remove
3633
- id: mixed-line-ending
3734
args:
3835
- --fix=auto

perdoo/__main__.py

Lines changed: 118 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,18 @@
99
from tempfile import TemporaryDirectory
1010

1111
from pydantic import ValidationError
12+
from rich.prompt import Prompt
1213

1314
from perdoo import ARCHIVE_EXTENSIONS, IMAGE_EXTENSIONS, __version__, setup_logging
1415
from perdoo.archives import BaseArchive, CB7Archive, CBTArchive, CBZArchive, get_archive
1516
from perdoo.console import CONSOLE
1617
from perdoo.models import ComicInfo, Metadata, MetronInfo
17-
from perdoo.models.metadata import Format, Meta, Tool
18+
from perdoo.models._base import InfoModel
19+
from perdoo.models.metadata import Format, Meta, Source, Tool
20+
from perdoo.models.metron_info import InformationSource
1821
from perdoo.services import Comicvine, League, Marvel, Metron
1922
from perdoo.settings import OutputFormat, Settings
20-
from perdoo.utils import (
21-
comic_to_metadata,
22-
create_metadata,
23-
list_files,
24-
metadata_to_comic,
25-
metadata_to_metron,
26-
metron_to_metadata,
27-
sanitize,
28-
)
23+
from perdoo.utils import Details, Identifications, get_metadata_id, list_files, sanitize
2924

3025
LOGGER = logging.getLogger("perdoo")
3126

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

5449

55-
def read_archive(archive: BaseArchive) -> tuple[Metadata, MetronInfo, ComicInfo]:
50+
def read_meta(archive: BaseArchive) -> tuple[Meta, Details]:
5651
filenames = archive.list_filenames()
57-
metadata = None
52+
53+
def read_meta_file(cls: type[InfoModel], filename: str) -> InfoModel | None:
54+
if filename in filenames:
55+
return cls.from_bytes(content=archive.read_file(filename=filename))
56+
return None
57+
5858
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"))
59+
metadata = read_meta_file(cls=Metadata, filename="/Metadata.xml") or read_meta_file(
60+
cls=Metadata, filename="Metadata.xml"
61+
)
62+
if metadata:
63+
meta = metadata.meta
64+
details = Details(
65+
series=Identifications(
66+
search=metadata.issue.series.title,
67+
comicvine=get_metadata_id(
68+
resources=metadata.issue.series.resources, source=Source.COMICVINE
69+
),
70+
league=get_metadata_id(
71+
resources=metadata.issue.series.resources,
72+
source=Source.LEAGUE_OF_COMIC_GEEKS,
73+
),
74+
marvel=get_metadata_id(
75+
resources=metadata.issue.series.resources, source=Source.MARVEL
76+
),
77+
metron=get_metadata_id(
78+
resources=metadata.issue.series.resources, source=Source.METRON
79+
),
80+
),
81+
issue=Identifications(
82+
search=metadata.issue.number,
83+
comicvine=get_metadata_id(
84+
resources=metadata.issue.resources, source=Source.COMICVINE
85+
),
86+
league=get_metadata_id(
87+
resources=metadata.issue.resources, source=Source.LEAGUE_OF_COMIC_GEEKS
88+
),
89+
marvel=get_metadata_id(
90+
resources=metadata.issue.resources, source=Source.MARVEL
91+
),
92+
metron=get_metadata_id(
93+
resources=metadata.issue.resources, source=Source.METRON
94+
),
95+
),
96+
)
97+
return meta, details
6398
except ValidationError:
6499
LOGGER.error("%s contains an invalid Metadata file", archive.path.name) # noqa: TRY400
65-
metron_info = None
66100
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")
101+
metron_info = read_meta_file(cls=MetronInfo, filename="/MetronInfo.xml") or read_meta_file(
102+
cls=MetronInfo, filename="MetronInfo.xml"
103+
)
104+
if metron_info:
105+
details = Details(
106+
series=Identifications(
107+
search=metron_info.series.name,
108+
comicvine=metron_info.series.id
109+
if metron_info.id and metron_info.id.source == InformationSource.COMIC_VINE
110+
else None,
111+
league=metron_info.series.id
112+
if metron_info.id
113+
and metron_info.id.source == InformationSource.LEAGUE_OF_COMIC_GEEKS
114+
else None,
115+
marvel=metron_info.series.id
116+
if metron_info.id and metron_info.id.source == InformationSource.MARVEL
117+
else None,
118+
metron=metron_info.series.id
119+
if metron_info.id and metron_info.id.source == InformationSource.METRON
120+
else None,
121+
),
122+
issue=Identifications(
123+
search=metron_info.number,
124+
comicvine=metron_info.id.value
125+
if metron_info.id and metron_info.id.source == InformationSource.COMIC_VINE
126+
else None,
127+
league=metron_info.id.value
128+
if metron_info.id
129+
and metron_info.id.source == InformationSource.LEAGUE_OF_COMIC_GEEKS
130+
else None,
131+
marvel=metron_info.id.value
132+
if metron_info.id and metron_info.id.source == InformationSource.MARVEL
133+
else None,
134+
metron=metron_info.id.value
135+
if metron_info.id and metron_info.id.source == InformationSource.METRON
136+
else None,
137+
),
74138
)
139+
return Meta(date_=date.today(), tool=Tool(value="MetronInfo")), details
75140
except ValidationError:
76141
LOGGER.error("%s contains an invalid MetronInfo file", archive.path.name) # noqa: TRY400
77-
comic_info = None
78142
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"))
143+
comic_info = read_meta_file(cls=ComicInfo, filename="/ComicInfo.xml") or read_meta_file(
144+
cls=ComicInfo, filename="ComicInfo.xml"
145+
)
146+
if comic_info:
147+
details = Details(
148+
series=Identifications(search=comic_info.series),
149+
issue=Identifications(search=comic_info.number),
150+
)
151+
return Meta(date_=date.today(), tool=Tool(value="ComicInfo")), details
83152
except ValidationError:
84153
LOGGER.error("%s contains an invalid ComicInfo file", archive.path.name) # noqa: TRY400
85154

86-
if not metadata:
87-
if metron_info:
88-
metadata = metron_to_metadata(metron_info=metron_info)
89-
elif comic_info:
90-
metadata = comic_to_metadata(comic_info=comic_info)
91-
else:
92-
metadata = create_metadata(archive=archive)
93-
if not metron_info:
94-
metron_info = metadata_to_metron(metadata=metadata)
95-
if not comic_info:
96-
comic_info = metadata_to_comic(metadata=metadata)
97-
return metadata, metron_info, comic_info
155+
return Meta(date_=date.today(), tool=Tool(value="Manual")), Details(
156+
series=Identifications(search=Prompt.ask("Series title", console=CONSOLE)),
157+
issue=Identifications(),
158+
)
98159

99160

100161
def fetch_from_services(
101-
settings: Settings, metainfo: tuple[Metadata, MetronInfo, ComicInfo]
102-
) -> None:
162+
settings: Settings, details: Details
163+
) -> tuple[Metadata | None, MetronInfo | None, ComicInfo | None]:
103164
marvel = None
104165
if settings.marvel and settings.marvel.public_key and settings.marvel.private_key:
105166
marvel = Marvel(settings=settings.marvel)
@@ -118,13 +179,13 @@ def fetch_from_services(
118179
league = League(settings.league_of_comic_geeks)
119180
if not marvel and not metron and not comicvine and not league:
120181
LOGGER.warning("No external services configured")
121-
return
182+
return None, None, None
122183

123-
success = any(
124-
service and service.fetch(*metainfo) for service in (marvel, metron, comicvine, league)
125-
)
126-
if not success:
127-
LOGGER.warning("Unable to fetch information fron any service")
184+
for service in (marvel, metron, comicvine, league):
185+
metadata, metron_info, comic_info = service.fetch(details=details)
186+
if metadata and metron_info and comic_info:
187+
return metadata, metron_info, comic_info
188+
return None, None, None
128189

129190

130191
def generate_filename(root: Path, extension: str, metadata: Metadata) -> Path:
@@ -169,7 +230,7 @@ def rename_images(folder: Path, filename: str) -> None:
169230
new_filename = f"{filename}_{str(index).zfill(pad_count)}{img_file.suffix}"
170231
if img_file.name != new_filename:
171232
LOGGER.info("Renamed %s to %s", img_file.name, new_filename)
172-
img_file.rename(folder / f"{filename}-{str(index).zfill(pad_count)}{img_file.suffix}")
233+
shutil.move(img_file, folder / new_filename)
173234

174235

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

215278
if not force:
216-
difference = abs(date.today() - metadata.meta.date_)
217-
if metadata.meta.tool == Tool() and difference.days < 28:
279+
difference = abs(date.today() - meta.date_)
280+
if meta.tool == Tool() and difference.days < 28:
218281
continue
219282

220-
CONSOLE.rule(file.stem)
221-
LOGGER.info("Processing %s", file.name)
222-
fetch_from_services(settings=settings, metainfo=(metadata, metron_info, comic_info))
283+
metadata, metron_info, comic_info = fetch_from_services(settings=settings, details=details)
223284
new_file = generate_filename(
224285
root=settings.collection_folder,
225286
extension=settings.output.format.value,
@@ -229,6 +290,7 @@ def start(settings: Settings, force: bool = False) -> None:
229290
temp_folder = Path(temp_str)
230291
if not archive.extract_files(destination=temp_folder):
231292
return
293+
LOGGER.info("Processing %s pages", file.stem)
232294
process_pages(
233295
folder=temp_folder,
234296
metadata=metadata,

perdoo/console.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from __future__ import annotations
22

3-
__all__ = ["CONSOLE", "create_menu", "DatePrompt"]
3+
__all__ = ["CONSOLE", "create_menu"]
44

55
import logging
6-
from datetime import date, datetime
76

87
from rich import box
98
from rich.console import Console
109
from rich.panel import Panel
11-
from rich.prompt import IntPrompt, InvalidResponse, PromptBase
10+
from rich.prompt import IntPrompt
1211
from rich.theme import Theme
1312

1413
LOGGER = logging.getLogger(__name__)
@@ -70,17 +69,3 @@ def create_menu(
7069
options=options, title=title, subtitle=subtitle, prompt=prompt, default=default
7170
)
7271
return selected
73-
74-
75-
class DatePrompt(PromptBase[date]):
76-
response_type = date
77-
validate_error_message = (
78-
"[prompt.invalid]Please enter a valid date in the format of 'yyyy-mm-dd'"
79-
)
80-
prompt_suffix = "[yyyy-mm-dd]: "
81-
82-
def process_response(self: DatePrompt, value: str) -> date:
83-
try:
84-
return datetime.strptime(value.strip(), "%Y-%m-%d").date()
85-
except ValueError as err:
86-
raise InvalidResponse(self.validate_error_message) from err

perdoo/models/comic_info.py

Lines changed: 7 additions & 7 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, HttpUrl
13+
from pydantic import Field, HttpUrl, NonNegativeFloat
1414

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

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

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

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

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

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

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

224224
def __init__(self: ComicInfo, **data: Any):
225-
self.unwrap_list(mappings=ComicInfo.list_fields, content=data)
225+
self.unwrap_list(mappings=self.list_fields, content=data)
226226
super().__init__(**data)
227227

228228
@property

0 commit comments

Comments
 (0)