11import logging
22from datetime import date
33from enum import Enum
4+ from io import BytesIO
45from pathlib import Path
56from platform import python_version
67from typing import Annotated
1415from perdoo .comic .archives import ArchiveSession
1516from perdoo .comic .errors import ComicArchiveError , ComicMetadataError
1617from perdoo .comic .metadata import ComicInfo , MetronInfo
18+ from perdoo .comic .metadata .comic_info import Page , PageType
1719from perdoo .comic .metadata .metron_info import Id , InformationSource
1820from perdoo .console import CONSOLE
1921from perdoo .services import BaseService , Comicvine , Metron
20- from perdoo .settings import Naming , Output , Service , Services , Settings
22+ from perdoo .settings import SETTINGS , Service
2123from perdoo .utils import (
2224 IssueSearch ,
2325 Search ,
@@ -36,19 +38,19 @@ class SyncOption(str, Enum):
3638 SKIP = "Skip"
3739
3840
39- def get_services (settings : Services ) -> dict [Service , BaseService ]:
41+ def get_services () -> dict [Service , BaseService ]:
4042 output = {}
41- if settings .comicvine .api_key :
42- output [Service .COMICVINE ] = Comicvine (api_key = settings .comicvine .api_key )
43- if settings . metron .username and settings .metron .password :
43+ if SETTINGS . services .comicvine .api_key :
44+ output [Service .COMICVINE ] = Comicvine (api_key = SETTINGS . services .comicvine .api_key )
45+ if SETTINGS . services . metron .username and SETTINGS . services .metron .password :
4446 output [Service .METRON ] = Metron (
45- username = settings . metron .username , password = settings .metron .password
47+ username = SETTINGS . services . metron .username , password = SETTINGS . services .metron .password
4648 )
4749 return output
4850
4951
5052def setup_environment (
51- clean_cache : bool , sync : SyncOption , settings : Settings , debug : bool = False
53+ clean_cache : bool , sync : SyncOption , debug : bool = False
5254) -> tuple [dict [Service , BaseService ], SyncOption ]:
5355 setup_logging (debug = debug )
5456 LOGGER .info ("Python v%s" , python_version ())
@@ -58,7 +60,7 @@ def setup_environment(
5860 LOGGER .info ("Cleaning Cache" )
5961 recursive_delete (path = get_cache_root ())
6062
61- services = get_services (settings = settings . services )
63+ services = get_services ()
6264 if not services and sync is not SyncOption .SKIP :
6365 LOGGER .warning ("No external services configured" )
6466 sync = SyncOption .SKIP
@@ -76,9 +78,9 @@ def load_comics(target: Path) -> list[Comic]:
7678 return comics
7779
7880
79- def prepare_comic (entry : Comic , settings : Settings , skip_convert : bool ) -> bool :
81+ def prepare_comic (entry : Comic , skip_convert : bool ) -> bool :
8082 if not skip_convert :
81- entry .convert_to (settings .output .format )
83+ entry .convert_to (SETTINGS .output .format )
8284 if not entry .archive .IS_WRITEABLE :
8385 LOGGER .warning ("Archive format %s is not writeable" , entry .archive .EXTENSION )
8486 return False
@@ -163,40 +165,64 @@ def sync_metadata(
163165
164166
165167def resolve_metadata (
166- entry : Comic ,
167- session : ArchiveSession ,
168- services : dict [Service , BaseService ],
169- settings : Services ,
170- sync : SyncOption ,
168+ entry : Comic , session : ArchiveSession , services : dict [Service , BaseService ], sync : SyncOption
171169) -> tuple [MetronInfo | None , ComicInfo | None ]:
172170 metron_info , comic_info = entry .read_metadata (session = session )
173171 if not should_sync_metadata (sync = sync , metron_info = metron_info ):
174172 return metron_info , comic_info
175173 search = build_search (
176174 metron_info = metron_info , comic_info = comic_info , filename = entry .filepath .stem
177175 )
178- return sync_metadata (search = search , services = services , service_order = settings .order )
176+ return sync_metadata (search = search , services = services , service_order = SETTINGS . services .order )
179177
180178
181- def generate_naming (
182- settings : Naming , metron_info : MetronInfo | None , comic_info : ComicInfo | None
183- ) -> str | None :
179+ def generate_naming (metron_info : MetronInfo | None , comic_info : ComicInfo | None ) -> str | None :
184180 filepath = None
185181 if metron_info :
186- filepath = metron_info .get_filename (settings = settings )
182+ filepath = metron_info .get_filename ()
187183 if not filepath and comic_info :
188- filepath = comic_info .get_filename (settings = settings )
184+ filepath = comic_info .get_filename ()
189185 return filepath .lstrip ("/" ) if filepath else None
190186
191187
188+ def load_page_info (entry : Comic , session : ArchiveSession , comic_info : ComicInfo ) -> None :
189+ from PIL import Image # noqa: PLC0415
190+
191+ pages = set ()
192+ image_files = entry .list_images (image_extensions = SETTINGS .output .image_extensions )
193+ for idx , file in enumerate (image_files ):
194+ page = next ((x for x in comic_info .pages if x .image == idx ), None )
195+ if page :
196+ page_type = page .type
197+ elif idx == 0 :
198+ page_type = PageType .FRONT_COVER
199+ elif idx == len (image_files ) - 1 :
200+ page_type = PageType .BACK_COVER
201+ else :
202+ page_type = PageType .STORY
203+ if not page :
204+ page = Page (image = idx )
205+ page .type = page_type
206+ page_bytes = entry .read_file (session = session , filename = file .name )
207+ if not page_bytes :
208+ continue
209+ page .image_size = len (page_bytes )
210+ with Image .open (BytesIO (page_bytes )) as page_data :
211+ width , height = page_data .size
212+ page .double_page = width >= height
213+ page .image_height = height
214+ page .image_width = width
215+ pages .add (page )
216+ comic_info .pages = sorted (pages )
217+
218+
192219def apply_changes (
193220 entry : Comic ,
194221 session : ArchiveSession ,
195222 metron_info : MetronInfo | None ,
196223 comic_info : ComicInfo | None ,
197224 skip_clean : bool ,
198225 skip_rename : bool ,
199- settings : Output ,
200226) -> str | None :
201227 local_metron_info , local_comic_info = entry .read_metadata (session = session )
202228 if local_metron_info != metron_info :
@@ -205,23 +231,23 @@ def apply_changes(
205231 else :
206232 session .delete (filename = MetronInfo .FILENAME )
207233
234+ if comic_info and SETTINGS .output .comic_info .handle_pages :
235+ load_page_info (entry = entry , session = session , comic_info = comic_info )
208236 if local_comic_info != comic_info :
209237 if comic_info :
210238 session .write (filename = ComicInfo .FILENAME , data = comic_info .to_bytes ())
211239 else :
212240 session .delete (filename = ComicInfo .FILENAME )
213241
214242 if not skip_clean :
215- for extra in entry .list_extras ():
243+ for extra in entry .list_extras (image_extensions = SETTINGS . output . image_extensions ):
216244 session .delete (filename = extra .name )
217245
218246 naming = None
219247 if not skip_rename and (
220- naming := generate_naming (
221- settings = settings .naming , metron_info = metron_info , comic_info = comic_info
222- )
248+ naming := generate_naming (metron_info = metron_info , comic_info = comic_info )
223249 ):
224- images = entry .list_images ()
250+ images = entry .list_images (image_extensions = SETTINGS . output . image_extensions )
225251 stem = Path (naming ).stem
226252 pad = len (str (len (images )))
227253 for idx , img in enumerate (images ):
@@ -268,11 +294,7 @@ def process(
268294 bool , Option ("--debug" , help = "Enable debug mode to show extra information." )
269295 ] = False ,
270296) -> None :
271- settings = Settings .load ()
272- settings .save ()
273- services , sync = setup_environment (
274- clean_cache = clean_cache , sync = sync , settings = settings , debug = debug
275- )
297+ services , sync = setup_environment (clean_cache = clean_cache , sync = sync , debug = debug )
276298
277299 comics = load_comics (target = target )
278300 total = len (comics )
@@ -281,15 +303,11 @@ def process(
281303 f"[{ index } /{ total } ] Importing { entry .filepath .name } " , align = "left" , style = "subtitle"
282304 )
283305
284- if not prepare_comic (entry = entry , settings = settings , skip_convert = skip_convert ):
306+ if not prepare_comic (entry = entry , skip_convert = skip_convert ):
285307 continue
286308 with entry .open_session () as session :
287309 metron_info , comic_info = resolve_metadata (
288- entry = entry ,
289- session = session ,
290- services = services ,
291- settings = settings .services ,
292- sync = sync ,
310+ entry = entry , session = session , services = services , sync = sync
293311 )
294312 naming = apply_changes (
295313 entry = entry ,
@@ -298,10 +316,9 @@ def process(
298316 comic_info = comic_info ,
299317 skip_clean = skip_clean ,
300318 skip_rename = skip_rename ,
301- settings = settings .output ,
302319 )
303320 if naming :
304- entry .move_to (naming = naming , output_folder = settings .output .folder )
321+ entry .move_to (naming = naming , output_folder = SETTINGS .output .folder )
305322 with CONSOLE .status ("Cleaning up empty folders" ):
306323 delete_empty_folders (folder = target )
307324
0 commit comments