Skip to content

Commit fd732f4

Browse files
Remove ProcessingPlan
1 parent 7d01b6e commit fd732f4

File tree

7 files changed

+132
-178
lines changed

7 files changed

+132
-178
lines changed

perdoo/__main__.py

Lines changed: 115 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
from perdoo import __version__, get_cache_root, setup_logging
1212
from perdoo.cli import archive_app, settings_app
1313
from perdoo.comic import Comic
14+
from perdoo.comic.archive import ArchiveSession
1415
from perdoo.comic.errors import ComicArchiveError, ComicMetadataError
1516
from perdoo.comic.metadata import ComicInfo, MetronInfo
1617
from perdoo.comic.metadata.metron_info import Id, InformationSource
1718
from perdoo.console import CONSOLE
18-
from perdoo.processing import ProcessingPlan
1919
from perdoo.services import BaseService, Comicvine, Metron
20-
from perdoo.settings import Service, Services, Settings
20+
from perdoo.settings import Naming, Output, Service, Services, Settings
2121
from perdoo.utils import (
2222
IssueSearch,
2323
Search,
@@ -33,18 +33,11 @@
3333
LOGGER = logging.getLogger("perdoo")
3434

3535

36-
class SyncOption(Enum):
36+
class SyncOption(str, Enum):
3737
FORCE = "Force"
3838
OUTDATED = "Outdated"
3939
SKIP = "Skip"
4040

41-
@staticmethod
42-
def load(value: str) -> "SyncOption":
43-
for entry in SyncOption:
44-
if entry.value.casefold() == value.casefold():
45-
return entry
46-
raise ValueError(f"'{value}' isn't a valid SyncOption")
47-
4841

4942
@app.callback(invoke_without_command=True)
5043
def common(
@@ -81,7 +74,7 @@ def setup_environment(
8174
recursive_delete(path=get_cache_root())
8275

8376
services = get_services(settings=settings.services)
84-
if not services and sync != SyncOption.SKIP:
77+
if not services and sync is not SyncOption.SKIP:
8578
LOGGER.warning("No external services configured")
8679
sync = SyncOption.SKIP
8780
return services, sync
@@ -107,22 +100,22 @@ def prepare_comic(entry: Comic, settings: Settings, skip_convert: bool) -> bool:
107100
return True
108101

109102

110-
def should_sync_metadata(sync: SyncOption, metroninfo: MetronInfo | None) -> bool:
111-
if sync == SyncOption.SKIP:
103+
def should_sync_metadata(sync: SyncOption, metron_info: MetronInfo | None) -> bool:
104+
if sync is SyncOption.SKIP:
112105
return False
113-
if sync == SyncOption.FORCE:
106+
if sync is SyncOption.FORCE:
114107
return True
115-
if metroninfo and metroninfo.last_modified:
116-
age = (date.today() - metroninfo.last_modified.date()).days
108+
if metron_info and metron_info.last_modified:
109+
age = (date.today() - metron_info.last_modified.date()).days
117110
return age >= 28
118111
return True
119112

120113

121-
def _get_id_value(ids: list[Id], source: InformationSource) -> str | None:
122-
return next((x.value for x in ids if x.source == source), None)
114+
def get_id(ids: list[Id], source: InformationSource) -> str | None:
115+
return next((x.value for x in ids if x.source is source), None)
123116

124117

125-
def _create_search_from_metron_info(metron_info: MetronInfo) -> Search:
118+
def search_from_metron_info(metron_info: MetronInfo) -> Search:
126119
series_id = metron_info.series.id
127120
source = next((x.source for x in metron_info.ids if x.primary), None)
128121
return Search(
@@ -135,14 +128,14 @@ def _create_search_from_metron_info(metron_info: MetronInfo) -> Search:
135128
),
136129
issue=IssueSearch(
137130
number=metron_info.number,
138-
comicvine=_get_id_value(metron_info.ids, InformationSource.COMIC_VINE),
139-
metron=_get_id_value(metron_info.ids, InformationSource.METRON),
131+
comicvine=get_id(metron_info.ids, InformationSource.COMIC_VINE),
132+
metron=get_id(metron_info.ids, InformationSource.METRON),
140133
),
141134
)
142135

143136

144-
def _create_search_from_comic_info(comic_info: ComicInfo, filename: str) -> Search:
145-
volume = comic_info.volume if comic_info.volume else None
137+
def search_from_comic_info(comic_info: ComicInfo, filename: str) -> Search:
138+
volume = comic_info.volume
146139
year = volume if volume and volume > 1900 else None
147140
volume = volume if volume and volume < 1900 else None
148141
return Search(
@@ -151,26 +144,25 @@ def _create_search_from_comic_info(comic_info: ComicInfo, filename: str) -> Sear
151144
)
152145

153146

154-
def _create_search_from_filename(filename: str) -> Search:
147+
def search_from_filename(filename: str) -> Search:
155148
series_name = comicfn2dict(filename).get("series", filename).replace("-", " ")
156149
return Search(series=SeriesSearch(name=series_name), issue=IssueSearch())
157150

158151

159-
def get_search_details(
160-
metadata: tuple[MetronInfo | None, ComicInfo | None], filename: str
152+
def build_search(
153+
metron_info: MetronInfo | None, comic_info: ComicInfo | None, filename: str
161154
) -> Search:
162-
metron_info, comic_info = metadata
163155
if metron_info and metron_info.series and metron_info.series.name:
164-
return _create_search_from_metron_info(metron_info=metron_info)
156+
return search_from_metron_info(metron_info=metron_info)
165157
if comic_info and comic_info.series:
166-
return _create_search_from_comic_info(comic_info=comic_info, filename=filename)
167-
return _create_search_from_filename(filename=filename)
158+
return search_from_comic_info(comic_info=comic_info, filename=filename)
159+
return search_from_filename(filename=filename)
168160

169161

170162
def sync_metadata(
171-
search: Search, services: dict[Service, BaseService | None], settings: Settings
163+
search: Search, services: dict[Service, BaseService], service_order: tuple[Service, ...]
172164
) -> tuple[MetronInfo | None, ComicInfo | None]:
173-
for service_name in settings.services.order:
165+
for service_name in service_order:
174166
if service := services.get(service_name):
175167
metron_info, comic_info = service.fetch(search=search)
176168
if metron_info or comic_info:
@@ -179,14 +171,77 @@ def sync_metadata(
179171

180172

181173
def resolve_metadata(
182-
entry: Comic, services: dict[Service, BaseService], settings: Settings, sync: SyncOption
174+
entry: Comic,
175+
session: ArchiveSession,
176+
services: dict[Service, BaseService],
177+
settings: Services,
178+
sync: SyncOption,
183179
) -> tuple[MetronInfo | None, ComicInfo | None]:
184-
metroninfo, comicinfo = entry.read_metadata()
185-
if not should_sync_metadata(sync=sync, metroninfo=metroninfo):
186-
return metroninfo, comicinfo
187-
search = get_search_details(metadata=(metroninfo, comicinfo), filename=entry.filepath.stem)
180+
metron_info, comic_info = entry.read_metadata(session=session)
181+
if not should_sync_metadata(sync=sync, metron_info=metron_info):
182+
return metron_info, comic_info
183+
search = build_search(
184+
metron_info=metron_info, comic_info=comic_info, filename=entry.filepath.stem
185+
)
188186
search.filename = entry.filepath.stem
189-
return sync_metadata(search=search, services=services, settings=settings)
187+
return sync_metadata(search=search, services=services, service_order=settings.order)
188+
189+
190+
def generate_naming(
191+
settings: Naming, metron_info: MetronInfo | None, comic_info: ComicInfo | None
192+
) -> str | None:
193+
filepath = None
194+
if metron_info:
195+
filepath = metron_info.get_filename(settings=settings)
196+
if not filepath and comic_info:
197+
filepath = comic_info.get_filename(settings=settings)
198+
return filepath.lstrip("/") if filepath else None
199+
200+
201+
def apply_changes(
202+
entry: Comic,
203+
session: ArchiveSession,
204+
metron_info: MetronInfo | None,
205+
comic_info: ComicInfo | None,
206+
skip_clean: bool,
207+
skip_rename: bool,
208+
settings: Output,
209+
) -> str | None:
210+
local_metron_info, local_comic_info = entry.read_metadata(session=session)
211+
if local_metron_info != metron_info:
212+
if metron_info:
213+
session.write(filename=MetronInfo.FILENAME, data=metron_info.to_bytes())
214+
else:
215+
session.remove(filename=MetronInfo.FILENAME)
216+
session.updated = True
217+
218+
if local_comic_info != comic_info:
219+
if comic_info:
220+
session.write(filename=ComicInfo.FILENAME, data=comic_info.to_bytes())
221+
else:
222+
session.remove(filename=ComicInfo.FILENAME)
223+
session.updated = True
224+
225+
if not skip_clean:
226+
for extra in entry.list_extras():
227+
session.remove(filename=extra.name)
228+
session.updated = True
229+
230+
naming = None
231+
if not skip_rename and (
232+
naming := generate_naming(
233+
settings=settings.naming, metron_info=metron_info, comic_info=comic_info
234+
)
235+
):
236+
images = entry.list_images()
237+
stem = Path(naming).stem
238+
pad = len(str(len(images)))
239+
for idx, img in enumerate(images):
240+
new_name = f"{stem}_{str(idx).zfill(pad)}{img.suffix}"
241+
if img.name != new_name:
242+
session.rename(old_name=img.name, new_name=new_name)
243+
session.updated = True
244+
return naming
190245

191246

192247
@app.command(name="import", help="Import comics into your collection using Perdoo.")
@@ -244,29 +299,33 @@ def run(
244299
)
245300

246301
comics = load_comics(target=target)
247-
for index, entry in enumerate(comics):
302+
total = len(comics)
303+
for index, entry in enumerate(comics, start=1):
248304
CONSOLE.rule(
249-
f"[{index + 1}/{len(comics)}] Importing {entry.filepath.name}",
250-
align="left",
251-
style="subtitle",
305+
f"[{index}/{total}] Importing {entry.filepath.name}", align="left", style="subtitle"
252306
)
253307

254308
if not prepare_comic(entry=entry, settings=settings, skip_convert=skip_convert):
255309
continue
256-
metroninfo, comicinfo = resolve_metadata(
257-
entry=entry, services=services, settings=settings, sync=sync
258-
)
259-
plan = ProcessingPlan.build(
260-
entry=entry,
261-
metroninfo=metroninfo,
262-
comicinfo=comicinfo,
263-
settings=settings.output,
264-
skip_clean=skip_clean,
265-
skip_rename=skip_rename,
266-
)
267-
plan.apply()
268-
if plan.naming:
269-
entry.move_to(naming=plan.naming, output_folder=settings.output.folder)
310+
with entry.open_session() as session:
311+
metron_info, comic_info = resolve_metadata(
312+
entry=entry,
313+
session=session,
314+
services=services,
315+
settings=settings.services,
316+
sync=sync,
317+
)
318+
naming = apply_changes(
319+
entry=entry,
320+
session=session,
321+
metron_info=metron_info,
322+
comic_info=comic_info,
323+
skip_clean=skip_clean,
324+
skip_rename=skip_rename,
325+
settings=settings.output,
326+
)
327+
if naming:
328+
entry.move_to(naming=naming, output_folder=settings.output.folder)
270329
with CONSOLE.status("Cleaning up empty folders"):
271330
delete_empty_folders(folder=target)
272331

perdoo/comic/archive/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
__all__ = ["Archive", "CB7Archive", "CBRArchive", "CBTArchive", "CBZArchive"]
1+
__all__ = ["Archive", "ArchiveSession", "CB7Archive", "CBRArchive", "CBTArchive", "CBZArchive"]
22

33
from perdoo.comic.archive._base import Archive
44
from perdoo.comic.archive.rar import CBRArchive
5+
from perdoo.comic.archive.session import ArchiveSession
56
from perdoo.comic.archive.sevenzip import CB7Archive
67
from perdoo.comic.archive.tar import CBTArchive
78
from perdoo.comic.archive.zip import CBZArchive

perdoo/comic/archive/_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ def is_archive(cls, path: Path) -> bool: ...
4444
@abstractmethod
4545
def list_filenames(self) -> list[str]: ...
4646

47-
@abstractmethod
48-
def read_file(self, filename: str) -> bytes: ...
47+
def read_file(self, filename: str) -> bytes:
48+
raise ComicArchiveError(f"Unable to read {filename} from {self.filepath.name}.")
4949

5050
def write_file(self, filename: str, data: str | bytes) -> None: # noqa: ARG002
5151
raise ComicArchiveError(f"Unable to write {filename} to {self.filepath.name}.")

perdoo/comic/archive/session.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def __init__(self, archive: Archive) -> None:
2525
self._temp_dir: TemporaryDirectory | None = None
2626
self._folder: Path | None = None
2727
self._extracted = False
28+
self.updated = False
2829

2930
def __enter__(self) -> Self:
3031
if self._archive.IS_EDITABLE:
@@ -37,6 +38,7 @@ def __enter__(self) -> Self:
3738
):
3839
self._archive.extract_files(destination=self._folder)
3940
self._extracted = True
41+
self.updated = False
4042
return self
4143

4244
def __exit__(
@@ -46,7 +48,7 @@ def __exit__(
4648
tb: TracebackType | None,
4749
) -> None:
4850
try:
49-
if exc_type is None and self._extracted:
51+
if exc_type is None and self._extracted and self.updated:
5052
with CONSOLE.status(
5153
f"Archiving {self._folder} to {self._archive.filepath}",
5254
spinner="simpleDotsScrolling",
@@ -69,8 +71,11 @@ def list(self) -> list[str]:
6971
return self._archive.list_filenames()
7072
return [p.name for p in self._folder.iterdir()]
7173

74+
def contains(self, filename: str) -> bool:
75+
return filename in self.list()
76+
7277
def read(self, filename: str) -> bytes:
73-
if self._archive.IS_EDITABLE:
78+
if self._archive.IS_READABLE:
7479
return self._archive.read_file(filename)
7580
return (self._folder / filename).read_bytes()
7681

perdoo/comic/archive/tar.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
class CBTArchive(Archive):
2323
EXTENSION: ClassVar[str] = ".cbt"
24-
IS_READABLE: ClassVar[bool] = True
24+
IS_READABLE: ClassVar[bool] = False
2525
IS_WRITEABLE: ClassVar[bool] = True
2626
IS_EDITABLE: ClassVar[bool] = False
2727

@@ -38,13 +38,6 @@ def list_filenames(self) -> list[str]:
3838
except Exception as err:
3939
raise ComicArchiveError(f"Unable to list files in {self.filepath.name}") from err
4040

41-
def read_file(self, filename: str) -> bytes:
42-
try:
43-
with tarfile.open(name=self.filepath, mode="r") as archive:
44-
return archive.extractfile(filename).read()
45-
except Exception as err:
46-
raise ComicArchiveError(f"Unable to read {filename} in {self.filepath.name}") from err
47-
4841
def extract_files(self, destination: Path) -> None:
4942
try:
5043
with tarfile.open(name=self.filepath, mode="r") as archive:

perdoo/comic/comic.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,14 @@ def convert_to(self, extension: Literal["cbz", "cbt", "cb7"]) -> None:
3535
if not isinstance(self.archive, cls):
3636
self._archive = cls.convert_from(old_archive=self.archive)
3737

38-
def contains(self, filename: str) -> bool:
39-
return filename in self.archive.list_filenames()
40-
41-
def read_metadata(self) -> tuple[MetronInfo | None, ComicInfo | None]:
38+
def read_metadata(self, session: ArchiveSession) -> tuple[MetronInfo | None, ComicInfo | None]:
4239
metroninfo = None
43-
if self.contains(filename=MetronInfo.FILENAME):
44-
metroninfo = MetronInfo.from_bytes(
45-
content=self.archive.read_file(filename=MetronInfo.FILENAME)
46-
)
40+
if session.contains(filename=MetronInfo.FILENAME):
41+
metroninfo = MetronInfo.from_bytes(content=session.read(filename=MetronInfo.FILENAME))
4742

4843
comicinfo = None
49-
if self.contains(filename=ComicInfo.FILENAME):
50-
comicinfo = ComicInfo.from_bytes(
51-
content=self.archive.read_file(filename=ComicInfo.FILENAME)
52-
)
44+
if session.contains(filename=ComicInfo.FILENAME):
45+
comicinfo = ComicInfo.from_bytes(content=session.read(filename=ComicInfo.FILENAME))
5346

5447
return metroninfo, comicinfo
5548

0 commit comments

Comments
 (0)