Skip to content

Commit 434d886

Browse files
Start services refactor
1 parent 1698212 commit 434d886

8 files changed

Lines changed: 205 additions & 171 deletions

File tree

‎perdoo/cli/rename.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def rename_comic(comic: Comic, new_filename: str, image_exts: Sequence[str]) ->
157157
idx += 1
158158
if filename != new_name:
159159
CONSOLE.print(f"Renaming {filename!r} to {new_name!r}")
160-
comic.rename_file(filename=filename, new_name=new_filename, override=True)
160+
comic.rename_file(filename=filename, new_name=new_name, override=True)
161161
except ArchiveCapabilityError as err:
162162
CONSOLE.print(
163163
f"{err}: Consider converting to another format first", style="logging.level.warning"

‎perdoo/services/__init__.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
__all__ = ["BaseService", "Comicvine", "Metron"]
1+
__all__ = ["Comicvine", "MetadataResult", "Metron", "Search", "Service"]
22

3-
from perdoo.services._base import BaseService
3+
from perdoo.services._base import Service
4+
from perdoo.services._models import MetadataResult, Search
45
from perdoo.services.comicvine import Comicvine
56
from perdoo.services.metron import Metron

‎perdoo/services/_base.py‎

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,28 @@
1-
__all__ = ["BaseService"]
1+
__all__ = ["Service", "prompt_select"]
22

3-
from abc import ABC, abstractmethod
4-
from typing import Any, Generic, TypeVar
3+
from typing import Any, Protocol
54

6-
from comic_archive.metadata import ComicInfo, MetronInfo
75
from prompt_toolkit.styles import Style
86
from questionary import Choice, select
97

10-
from perdoo.utils import IssueSearch, Search, SeriesSearch
8+
from perdoo.services._models import MetadataResult, Search
119

12-
S = TypeVar("S")
13-
C = TypeVar("C")
1410
DEFAULT_CHOICE = Choice(title="None of the Above", value=None)
1511

1612

17-
class BaseService(ABC, Generic[S, C]):
18-
@staticmethod
19-
def _prompt_select(message: str, choices: list[Choice]) -> Any: # noqa: ANN401
20-
if not choices:
21-
return None
22-
selected = select(
23-
message,
24-
default=DEFAULT_CHOICE,
25-
choices=[*choices, DEFAULT_CHOICE],
26-
style=Style([("dim", "dim")]),
27-
).ask()
28-
if select and selected != DEFAULT_CHOICE.title:
29-
return selected
13+
def prompt_select(message: str, choices: list[Choice]) -> Any: # noqa: ANN401
14+
if not choices:
3015
return None
31-
32-
@abstractmethod
33-
def _search_series(
34-
self, name: str | None, volume: int | None, year: int | None, filename: str
35-
) -> int | None: ...
36-
37-
@abstractmethod
38-
def fetch_series(self, search: SeriesSearch, filename: str) -> S | None: ...
39-
40-
@abstractmethod
41-
def _search_issue(self, series_id: int, number: str | None, filename: str) -> int | None: ...
42-
43-
@abstractmethod
44-
def fetch_issue(self, series_id: int, search: IssueSearch, filename: str) -> C | None: ...
45-
46-
@abstractmethod
47-
def _process_metron_info(self, series: S, issue: C) -> MetronInfo | None: ...
48-
49-
@abstractmethod
50-
def _process_comic_info(self, series: S, issue: C) -> ComicInfo | None: ...
51-
52-
@abstractmethod
53-
def fetch(self, search: Search) -> tuple[MetronInfo | None, ComicInfo | None]: ...
16+
selected = select(
17+
message,
18+
default=DEFAULT_CHOICE,
19+
choices=[*choices, DEFAULT_CHOICE],
20+
style=Style([("dim", "dim")]),
21+
).ask()
22+
if select and selected != DEFAULT_CHOICE.title:
23+
return selected
24+
return None
25+
26+
27+
class Service(Protocol):
28+
def fetch(self, search: Search) -> MetadataResult: ...

‎perdoo/services/_models.py‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
__all__ = ["MetadataResult", "Search"]
2+
3+
from dataclasses import dataclass
4+
5+
from comic_archive import Comic
6+
from comic_archive.metadata import ComicInfo, MetronInfo
7+
from comic_archive.metadata.metron_info import Id, InformationSource
8+
from comicfn2dict import comicfn2dict
9+
10+
11+
@dataclass(frozen=True)
12+
class MetadataResult:
13+
comic_info: ComicInfo | None = None
14+
metron_info: MetronInfo | None = None
15+
16+
17+
@dataclass
18+
class SeriesSearch:
19+
name: str
20+
volume: int | None = None
21+
year: int | None = None
22+
comicvine: int | None = None
23+
metron: int | None = None
24+
25+
26+
@dataclass
27+
class IssueSearch:
28+
number: str | None = None
29+
comicvine: int | None = None
30+
metron: int | None = None
31+
32+
33+
@dataclass
34+
class Search:
35+
series: SeriesSearch
36+
issue: IssueSearch
37+
filename: str
38+
39+
@staticmethod
40+
def build(comic: Comic) -> "Search":
41+
if (
42+
(metron_info := comic.get_metadata(MetronInfo))
43+
and metron_info.series
44+
and metron_info.series.name
45+
):
46+
return search_from_metron_info(metadata=metron_info, filename=comic.file.stem)
47+
if (comic_info := comic.get_metadata(ComicInfo)) and comic_info.series:
48+
return search_from_comic_info(metadata=comic_info, filename=comic.file.stem)
49+
return search_from_filename(filename=comic.file.stem)
50+
51+
52+
def get_id(ids: list[Id], source: InformationSource) -> str | None:
53+
return next((x.value for x in ids if x.source is source), None)
54+
55+
56+
def search_from_metron_info(metadata: MetronInfo, filename: str) -> Search:
57+
series_id = metadata.series.id
58+
comicvine_id = get_id(metadata.ids, InformationSource.COMIC_VINE)
59+
metron_id = get_id(metadata.ids, InformationSource.METRON)
60+
source = next((x.source for x in metadata.ids if x.primary), None)
61+
return Search(
62+
series=SeriesSearch(
63+
name=metadata.series.name,
64+
volume=metadata.series.volume,
65+
year=metadata.series.start_year,
66+
comicvine=int(series_id)
67+
if series_id and source == InformationSource.COMIC_VINE
68+
else None,
69+
metron=int(series_id) if series_id and source == InformationSource.METRON else None,
70+
),
71+
issue=IssueSearch(
72+
number=metadata.number,
73+
comicvine=int(comicvine_id) if comicvine_id else None,
74+
metron=int(metron_id) if metron_id else None,
75+
),
76+
filename=filename,
77+
)
78+
79+
80+
def search_from_comic_info(metadata: ComicInfo, filename: str) -> Search:
81+
volume = metadata.volume
82+
year = volume if volume and volume > 1900 else None
83+
volume = volume if volume and volume < 1900 else None
84+
return Search(
85+
series=SeriesSearch(name=metadata.series or filename, volume=volume, year=year),
86+
issue=IssueSearch(number=metadata.number),
87+
filename=filename,
88+
)
89+
90+
91+
def search_from_filename(filename: str) -> Search:
92+
series_name = comicfn2dict(filename).get("series", filename)
93+
series_name = str(series_name).replace("-", " ")
94+
return Search(series=SeriesSearch(name=series_name), issue=IssueSearch(), filename=filename)

‎perdoo/services/comicvine.py‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
from simyan.schemas.volume import Volume
1515

1616
from perdoo import get_cache_home
17-
from perdoo.services._base import BaseService
18-
from perdoo.utils import IssueSearch, Search, SeriesSearch
17+
from perdoo.services._base import prompt_select
18+
from perdoo.services._models import IssueSearch, MetadataResult, Search, SeriesSearch
1919

2020
LOGGER = logging.getLogger(__name__)
2121

2222

23-
class Comicvine(BaseService[Volume, Issue]):
23+
class Comicvine:
2424
def __init__(self, api_key: str):
2525
self.session = Simyan(
2626
api_key=api_key,
@@ -63,7 +63,7 @@ def _search_series(
6363
)
6464
for x in options
6565
]
66-
selected = self._prompt_select(
66+
selected = prompt_select(
6767
message=f"Searching Comicvine for Volumes matching '{filename}'"
6868
if not year
6969
else f"Searching Comicvine for Volume '{search}'",
@@ -124,7 +124,7 @@ def _search_issue(self, series_id: int, number: str | None, filename: str) -> in
124124
)
125125
for x in options
126126
]
127-
selected = self._prompt_select(
127+
selected = prompt_select(
128128
message=f"Searching Comicvine for Issues matching '{filename}'"
129129
if not number
130130
else f"Searching Comicvine for Issues with number '{number}'",
@@ -160,7 +160,7 @@ def fetch_issue(self, series_id: int, search: IssueSearch, filename: str) -> Iss
160160
return self.fetch_issue(series_id=series_id, search=search, filename=filename)
161161
return None
162162

163-
def _process_metron_info(self, series: Volume, issue: Issue) -> MetronInfo | None:
163+
def _build_metron_info(self, series: Volume, issue: Issue) -> MetronInfo | None:
164164
from comic_archive.metadata.metron_info import ( # noqa: PLC0415
165165
Arc,
166166
Credit,
@@ -224,7 +224,7 @@ def load_role(value: str) -> Role:
224224
universes=[],
225225
)
226226

227-
def _process_comic_info(self, series: Volume, issue: Issue) -> ComicInfo | None:
227+
def _build_comic_info(self, series: Volume, issue: Issue) -> ComicInfo | None:
228228
comic_info = ComicInfo(
229229
title=issue.name,
230230
series=series.name,
@@ -248,7 +248,7 @@ def _process_comic_info(self, series: Volume, issue: Issue) -> ComicInfo | None:
248248

249249
return comic_info
250250

251-
def fetch(self, search: Search) -> tuple[MetronInfo | None, ComicInfo | None]:
251+
def fetch(self, search: Search) -> MetadataResult:
252252
if not search.series.comicvine and search.issue.comicvine:
253253
try:
254254
temp = self.session.get_issue(issue_id=search.issue.comicvine)
@@ -258,13 +258,13 @@ def fetch(self, search: Search) -> tuple[MetronInfo | None, ComicInfo | None]:
258258

259259
series = self.fetch_series(search=search.series, filename=search.filename)
260260
if not series:
261-
return None, None
261+
return MetadataResult()
262262

263263
issue = self.fetch_issue(series_id=series.id, search=search.issue, filename=search.filename)
264264
if not issue:
265-
return None, None
265+
return MetadataResult()
266266

267-
metron_info = self._process_metron_info(series=series, issue=issue)
268-
comic_info = self._process_comic_info(series=series, issue=issue)
269-
270-
return metron_info, comic_info
267+
return MetadataResult(
268+
comic_info=self._build_comic_info(series=series, issue=issue),
269+
metron_info=self._build_metron_info(series=series, issue=issue),
270+
)

‎perdoo/utils.py‎

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,23 @@
11
__all__ = [
2-
"IssueSearch",
3-
"Search",
4-
"SeriesSearch",
52
"delete_empty_folders",
63
"display",
74
"flatten_dict",
8-
"get_id",
95
"list_files",
106
"recursive_delete",
117
"sanitize",
128
]
139

14-
import logging
1510
import re
16-
from dataclasses import dataclass
1711
from pathlib import Path
1812
from typing import Any, Literal
1913

2014
from comic_archive.metadata import ComicInfo, MetronInfo
21-
from comic_archive.metadata.metron_info import Id, InformationSource
2215
from msgspec import to_builtins
2316
from natsort import humansorted, ns
2417
from rich.panel import Panel
2518

2619
from perdoo.console import CONSOLE
2720

28-
LOGGER = logging.getLogger(__name__)
29-
30-
31-
@dataclass
32-
class SeriesSearch:
33-
name: str
34-
volume: int | None = None
35-
year: int | None = None
36-
comicvine: int | None = None
37-
metron: int | None = None
38-
39-
40-
@dataclass
41-
class IssueSearch:
42-
number: str | None = None
43-
comicvine: int | None = None
44-
metron: int | None = None
45-
46-
47-
@dataclass
48-
class Search:
49-
series: SeriesSearch
50-
issue: IssueSearch
51-
filename: str
52-
5321

5422
def list_files(path: Path, *extensions: str) -> list[Path]:
5523
files = []
@@ -94,7 +62,7 @@ def delete_empty_folders(folder: Path) -> None:
9462
delete_empty_folders(subfolder)
9563
if not any(folder.iterdir()):
9664
folder.rmdir()
97-
LOGGER.info("Deleted empty folder: %s", folder)
65+
CONSOLE.print(f"Deleted empty folder: {folder.name!r}")
9866

9967

10068
def display(data: ComicInfo | MetronInfo, title: str | None = None) -> None:
@@ -110,10 +78,6 @@ def encoder(obj: object) -> object:
11078
CONSOLE.print(Panel.fit("\n".join(data_vals), title=title))
11179

11280

113-
def get_id(ids: list[Id], source: InformationSource) -> str | None:
114-
return next((x.value for x in ids if x.source is source), None)
115-
116-
11781
def sanitize(value: str | int | None, seperator: Literal["-", "_", ".", " "]) -> str | None:
11882
if value is None:
11983
return value

0 commit comments

Comments
 (0)