11import logging
22from datetime import date
33from enum import Enum
4- from io import BytesIO
54from pathlib import Path
65from platform import python_version
76from typing import Annotated
1413from perdoo .comic import Comic
1514from perdoo .comic .errors import ComicArchiveError , ComicMetadataError
1615from perdoo .comic .metadata import ComicInfo , MetronInfo
17- from perdoo .comic .metadata .comic_info import Page
1816from perdoo .comic .metadata .metron_info import Id , InformationSource
1917from perdoo .console import CONSOLE
18+ from perdoo .processing import ProcessingPlan
2019from perdoo .services import BaseService , Comicvine , Metron
2120from perdoo .settings import Service , Services , Settings
2221from perdoo .utils import (
@@ -70,7 +69,25 @@ def get_services(settings: Services) -> dict[Service, BaseService]:
7069 return output
7170
7271
73- def _load_comics (target : Path ) -> list [Comic ]:
72+ def setup_environment (
73+ clean_cache : bool , sync : SyncOption , settings : Settings , debug : bool = False
74+ ) -> tuple [dict [Service , BaseService ], SyncOption ]:
75+ setup_logging (debug = debug )
76+ LOGGER .info ("Python v%s" , python_version ())
77+ LOGGER .info ("Perdoo v%s" , __version__ )
78+
79+ if clean_cache :
80+ LOGGER .info ("Cleaning Cache" )
81+ recursive_delete (path = get_cache_root ())
82+
83+ services = get_services (settings = settings .services )
84+ if not services and sync != SyncOption .SKIP :
85+ LOGGER .warning ("No external services configured" )
86+ sync = SyncOption .SKIP
87+ return services , sync
88+
89+
90+ def load_comics (target : Path ) -> list [Comic ]:
7491 comics = []
7592 files = list_files (target ) if target .is_dir () else [target ]
7693 for file in files :
@@ -81,11 +98,31 @@ def _load_comics(target: Path) -> list[Comic]:
8198 return comics
8299
83100
101+ def prepare_comic (entry : Comic , settings : Settings , skip_convert : bool ) -> bool :
102+ if not skip_convert :
103+ entry .convert_to (settings .output .format )
104+ if not entry .archive .IS_WRITEABLE :
105+ LOGGER .warning ("Archive format %s is not writeable" , entry .archive .EXTENSION )
106+ return False
107+ return True
108+
109+
110+ def should_sync_metadata (sync : SyncOption , metroninfo : MetronInfo | None ) -> bool :
111+ if sync == SyncOption .SKIP :
112+ return False
113+ if sync == SyncOption .FORCE :
114+ return True
115+ if metroninfo and metroninfo .last_modified :
116+ age = (date .today () - metroninfo .last_modified .date ()).days
117+ return age >= 28
118+ return True
119+
120+
84121def _get_id_value (ids : list [Id ], source : InformationSource ) -> str | None :
85122 return next ((x .value for x in ids if x .source == source ), None )
86123
87124
88- def _create_search_from_metron (metron_info : MetronInfo ) -> Search :
125+ def _create_search_from_metron_info (metron_info : MetronInfo ) -> Search :
89126 series_id = metron_info .series .id
90127 source = next ((x .source for x in metron_info .ids if x .primary ), None )
91128 return Search (
@@ -124,46 +161,12 @@ def get_search_details(
124161) -> Search :
125162 metron_info , comic_info = metadata
126163 if metron_info and metron_info .series and metron_info .series .name :
127- return _create_search_from_metron (metron_info = metron_info )
164+ return _create_search_from_metron_info (metron_info = metron_info )
128165 if comic_info and comic_info .series :
129166 return _create_search_from_comic_info (comic_info = comic_info , filename = filename )
130167 return _create_search_from_filename (filename = filename )
131168
132169
133- def load_page_info (entry : Comic , comic_info : ComicInfo ) -> list [Page ]:
134- from PIL import Image # noqa: PLC0415
135-
136- from perdoo .comic import IMAGE_EXTENSIONS # noqa: PLC0415
137- from perdoo .comic .metadata .comic_info import PageType # noqa: PLC0415
138-
139- pages = set ()
140- image_files = [
141- x for x in entry .archive .list_filenames () if Path (x ).suffix .lower () in IMAGE_EXTENSIONS
142- ]
143- for idx , file in enumerate (image_files ):
144- page = next ((x for x in comic_info .pages if x .image == idx ), None )
145- if page :
146- page_type = page .type
147- elif idx == 0 :
148- page_type = PageType .FRONT_COVER
149- elif idx == len (image_files ) - 1 :
150- page_type = PageType .BACK_COVER
151- else :
152- page_type = PageType .STORY
153- if not page :
154- page = Page (image = idx )
155- page .type = page_type
156- page_bytes = entry .archive .read_file (file )
157- page .image_size = len (page_bytes )
158- with Image .open (BytesIO (page_bytes )) as page_data :
159- width , height = page_data .size
160- page .double_page = width >= height
161- page .image_height = height
162- page .image_width = width
163- pages .add (page )
164- return sorted (pages )
165-
166-
167170def sync_metadata (
168171 search : Search , services : dict [Service , BaseService | None ], settings : Settings
169172) -> tuple [MetronInfo | None , ComicInfo | None ]:
@@ -175,6 +178,17 @@ def sync_metadata(
175178 return None , None
176179
177180
181+ def resolve_metadata (
182+ entry : Comic , services : dict [Service , BaseService ], settings : Settings , sync : SyncOption
183+ ) -> 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 )
188+ search .filename = entry .filepath .stem
189+ return sync_metadata (search = search , services = services , settings = settings )
190+
191+
178192@app .command (name = "import" , help = "Import comics into your collection using Perdoo." )
179193def run (
180194 target : Annotated [
@@ -223,74 +237,36 @@ def run(
223237 bool , Option ("--debug" , help = "Enable debug mode to show extra information." )
224238 ] = False ,
225239) -> None :
226- setup_logging (debug = debug )
227- LOGGER .info ("Python v%s" , python_version ())
228- LOGGER .info ("Perdoo v%s" , __version__ )
229-
230240 settings = Settings .load ()
231241 settings .save ()
232- if debug :
233- CONSOLE .print (
234- {
235- "target" : target ,
236- "flags.skip-convert" : skip_convert ,
237- "flags.sync" : sync ,
238- "flags.skip-clean" : skip_clean ,
239- "flags.skip-rename" : skip_rename ,
240- "flags.clean-cache" : clean_cache ,
241- }
242- )
243- if clean_cache :
244- LOGGER .info ("Cleaning Cache" )
245- recursive_delete (path = get_cache_root ())
246- services = get_services (settings = settings .services )
247- if not services and sync != SyncOption .SKIP :
248- LOGGER .warning ("No external services configured" )
249- sync = SyncOption .SKIP
242+ services , sync = setup_environment (
243+ clean_cache = clean_cache , sync = sync , settings = settings , debug = debug
244+ )
250245
251- comics = _load_comics (target = target )
246+ comics = load_comics (target = target )
252247 for index , entry in enumerate (comics ):
253248 CONSOLE .rule (
254249 f"[{ index + 1 } /{ len (comics )} ] Importing { entry .filepath .name } " ,
255250 align = "left" ,
256251 style = "subtitle" ,
257252 )
258- if not skip_convert :
259- with CONSOLE .status ("Converting to '.cbz'" , spinner = "simpleDotsScrolling" ):
260- entry .convert_to (extension = "cbz" )
261-
262- metadata : tuple [MetronInfo | None , ComicInfo | None ] = (entry .metron_info , entry .comic_info )
263-
264- if sync != SyncOption .SKIP :
265- search = get_search_details (metadata = metadata , filename = entry .filepath .stem )
266- search .filename = entry .filepath .stem
267- last_modified = date (1900 , 1 , 1 )
268- if sync == SyncOption .OUTDATED :
269- metron_info , _ = metadata
270- if metron_info and metron_info .last_modified :
271- last_modified = metron_info .last_modified .date ()
272- if (date .today () - last_modified ).days >= 28 :
273- metadata = sync_metadata (search = search , services = services , settings = settings )
274- else :
275- LOGGER .info ("Metadata up-to-date" )
276-
277- if not skip_clean :
278- with CONSOLE .status ("Cleaning Archive" , spinner = "simpleDotsScrolling" ):
279- entry .clean_archive ()
280- if settings .output .metron_info .create and metadata [0 ]:
281- entry .write_metadata (metadata = metadata [0 ])
282- if settings .output .comic_info .create and metadata [1 ]:
283- metadata [1 ].pages = (
284- load_page_info (entry = entry , comic_info = metadata [1 ])
285- if settings .output .comic_info .handle_pages
286- else []
287- )
288- entry .write_metadata (metadata = metadata [1 ])
289-
290- if not skip_rename :
291- with CONSOLE .status ("Renaming based on metadata" , spinner = "simpleDotsScrolling" ):
292- entry .rename (naming = settings .output .naming , output_folder = settings .output .folder )
293253
254+ if not prepare_comic (entry = entry , settings = settings , skip_convert = skip_convert ):
255+ 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 )
294270 with CONSOLE .status ("Cleaning up empty folders" ):
295271 delete_empty_folders (folder = target )
296272
0 commit comments