66from typing import Annotated
77
88from comicfn2dict import comicfn2dict
9+ from darkseid .comic import SUPPORTED_IMAGE_EXTENSIONS
910from typer import Argument , Context , Exit , Option , Typer
1011
1112from perdoo import __version__ , get_cache_root , setup_logging
12- from perdoo .archives import CBRArchive , get_archive
1313from perdoo .cli import archive_app , settings_app
14+ from perdoo .comic import Comic , ComicArchiveError , ComicMetadataError
1415from perdoo .console import CONSOLE
15- from perdoo .main import clean_archive , convert_file , rename_file , save_metadata , sync_metadata
16- from perdoo .metadata import ComicInfo , MetronInfo , get_metadata
17- from perdoo .metadata .metron_info import InformationSource
16+ from perdoo .metadata import ComicInfo , MetronInfo
17+ from perdoo .metadata . comic_info import Page
18+ from perdoo .metadata .metron_info import Id , InformationSource
1819from perdoo .services import BaseService , Comicvine , Marvel , Metron
1920from perdoo .settings import Service , Services , Settings
2021from perdoo .utils import (
@@ -40,7 +41,7 @@ class SyncOption(Enum):
4041 @staticmethod
4142 def load (value : str ) -> "SyncOption" :
4243 for entry in SyncOption :
43- if entry .value .replace ( " " , "" ). casefold () == value . replace ( " " , "" ) .casefold ():
44+ if entry .value .casefold () == value .casefold ():
4445 return entry
4546 raise ValueError (f"'{ value } ' isn't a valid SyncOption" )
4647
@@ -73,54 +74,93 @@ def get_services(settings: Services) -> dict[Service, BaseService]:
7374 return output
7475
7576
77+ def _load_comics (target : Path ) -> list [Comic ]:
78+ comics = []
79+ files = list_files (target ) if target .is_dir () else [target ]
80+ for file in files :
81+ try :
82+ comics .append (Comic (file = file ))
83+ except (ComicArchiveError , ComicMetadataError ) as err : # noqa: PERF203
84+ LOGGER .error ("Failed to load '%s' as a Comic: %s" , file , err )
85+ return comics
86+
87+
88+ def _get_id_value (ids : list [Id ], source : InformationSource ) -> str | None :
89+ return next ((x .value for x in ids if x .source == source ), None )
90+
91+
92+ def _create_search_from_metron (metron_info : MetronInfo ) -> Search :
93+ series_id = metron_info .series .id
94+ source = next ((x .source for x in metron_info .ids if x .primary ), None )
95+ return Search (
96+ series = SeriesSearch (
97+ name = metron_info .series .name ,
98+ volume = metron_info .series .volume ,
99+ year = metron_info .series .start_year ,
100+ comicvine = series_id if source == InformationSource .COMIC_VINE else None ,
101+ marvel = series_id if source == InformationSource .MARVEL else None ,
102+ metron = series_id if source == InformationSource .METRON else None ,
103+ ),
104+ issue = IssueSearch (
105+ number = metron_info .number ,
106+ comicvine = _get_id_value (metron_info .ids , InformationSource .COMIC_VINE ),
107+ marvel = _get_id_value (metron_info .ids , InformationSource .MARVEL ),
108+ metron = _get_id_value (metron_info .ids , InformationSource .METRON ),
109+ ),
110+ )
111+
112+
113+ def _create_search_from_comic_info (comic_info : ComicInfo ) -> Search :
114+ volume = comic_info .volume if comic_info .volume else None
115+ year = volume if volume and volume > 1900 else None
116+ volume = volume if volume and volume < 1900 else None
117+ return Search (
118+ series = SeriesSearch (name = comic_info .series , volume = volume , year = year ),
119+ issue = IssueSearch (number = comic_info .number ),
120+ )
121+
122+
123+ def _create_search_from_filename (fallback_title : str ) -> Search :
124+ series_name = comicfn2dict (fallback_title ).get ("series" , fallback_title ).replace ("-" , " " )
125+ return Search (series = SeriesSearch (name = series_name ), issue = IssueSearch ())
126+
127+
76128def get_search_details (
77129 metadata : tuple [MetronInfo | None , ComicInfo | None ], fallback_title : str
78130) -> Search :
79131 metron_info , comic_info = metadata
80-
81132 if metron_info and metron_info .series and metron_info .series .name :
82- series_id = metron_info .series .id
83- source = next (iter (x .source for x in metron_info .ids if x .primary ), None )
84- return Search (
85- series = SeriesSearch (
86- name = metron_info .series .name ,
87- volume = metron_info .series .volume ,
88- year = metron_info .series .start_year ,
89- comicvine = series_id if source == InformationSource .COMIC_VINE else None ,
90- marvel = series_id if source == InformationSource .MARVEL else None ,
91- metron = series_id if source == InformationSource .METRON else None ,
92- ),
93- issue = IssueSearch (
94- number = metron_info .number ,
95- comicvine = next (
96- iter (
97- x .value for x in metron_info .ids if x .source == InformationSource .COMIC_VINE
98- ),
99- None ,
100- ),
101- marvel = next (
102- iter (x .value for x in metron_info .ids if x .source == InformationSource .MARVEL ),
103- None ,
104- ),
105- metron = next (
106- iter (x .value for x in metron_info .ids if x .source == InformationSource .METRON ),
107- None ,
108- ),
109- ),
110- )
133+ return _create_search_from_metron (metron_info )
111134 if comic_info and comic_info .series :
112- return Search (
113- series = SeriesSearch (
114- name = comic_info .series ,
115- volume = comic_info .volume
116- if comic_info .volume and comic_info .volume < 1900
117- else None ,
118- year = comic_info .volume if comic_info .volume and comic_info .volume > 1900 else None ,
119- ),
120- issue = IssueSearch (number = comic_info .number ),
121- )
122- series_name = comicfn2dict (fallback_title ).get ("series" , fallback_title ).replace ("-" , " " )
123- return Search (series = SeriesSearch (name = series_name ), issue = IssueSearch ())
135+ return _create_search_from_comic_info (comic_info )
136+ return _create_search_from_filename (fallback_title )
137+
138+
139+ def load_page_info (entry : Comic , comic_info : ComicInfo ) -> list [Page ]:
140+ pages = set ()
141+ image_files = [
142+ x
143+ for x in entry .archive .get_filename_list ()
144+ if Path (x ).suffix .lower () in SUPPORTED_IMAGE_EXTENSIONS
145+ ]
146+ for idx , file in enumerate (image_files ):
147+ img_file = Path (file )
148+ is_final_page = idx == len (image_files ) - 1
149+ page = next ((x for x in comic_info .pages if x .image == idx ), None )
150+ pages .add (Page .from_path (file = img_file , index = idx , is_final_page = is_final_page , page = page ))
151+ return sorted (pages )
152+
153+
154+ def sync_metadata (
155+ search : Search , services : dict [Service , BaseService | None ], settings : Settings
156+ ) -> tuple [MetronInfo | None , ComicInfo | None ]:
157+ for service_name in settings .services .order :
158+ if service := services .get (service_name ):
159+ LOGGER .info ("Searching %s for matching issue" , type (service ).__name__ )
160+ metron_info , comic_info = service .fetch (search = search )
161+ if metron_info or comic_info :
162+ return metron_info , comic_info
163+ return None , None
124164
125165
126166@app .command (name = "import" , help = "Import comics into your collection using Perdoo." )
@@ -178,8 +218,8 @@ def run(
178218 settings = Settings .load ()
179219 settings .save ()
180220 if debug :
181- Settings . display (
182- extras = {
221+ CONSOLE . print (
222+ {
183223 "target" : target ,
184224 "flags.skip-convert" : skip_convert ,
185225 "flags.sync" : sync ,
@@ -196,28 +236,18 @@ def run(
196236 LOGGER .warning ("No external services configured" )
197237 sync = SyncOption .SKIP
198238
199- entries = []
200- for file in list_files (target ) if target .is_dir () else [target ]:
201- try :
202- entries .append (get_archive (file ))
203- except NotImplementedError as nie : # noqa: PERF203
204- LOGGER .error ("%s, Skipping" , nie )
205-
206- for index , entry in enumerate (entries ):
239+ comics = _load_comics (target = target )
240+ for index , entry in enumerate (comics ):
207241 CONSOLE .rule (
208- f"[{ index + 1 } /{ len (entries )} ] Importing { entry .path .name } " ,
242+ f"[{ index + 1 } /{ len (comics )} ] Importing { entry .path .name } " ,
209243 align = "left" ,
210244 style = "subtitle" ,
211245 )
212246 if not skip_convert :
213- with CONSOLE .status (
214- f"Converting to '{ settings .output .format } '" , spinner = "simpleDotsScrolling"
215- ):
216- entry = convert_file (entry , output_format = settings .output .format )
217- if entry is None or isinstance (entry , CBRArchive ):
218- continue
247+ with CONSOLE .status ("Converting to 'CBZ'" , spinner = "simpleDotsScrolling" ):
248+ entry .convert_to_cbz ()
219249
220- metadata = get_metadata ( archive = entry , debug = debug )
250+ metadata : tuple [ MetronInfo | None , ComicInfo | None ] = ( entry . metron_info , entry . comic_info )
221251
222252 if sync != SyncOption .SKIP :
223253 search = get_search_details (metadata = metadata , fallback_title = entry .path .stem )
@@ -233,12 +263,20 @@ def run(
233263
234264 if not skip_clean :
235265 with CONSOLE .status ("Cleaning Archive" , spinner = "simpleDotsScrolling" ):
236- clean_archive (entry = entry , settings = settings )
237- save_metadata (entry = entry , metadata = metadata , settings = settings )
266+ entry .clean_archive ()
267+ if settings .output .metron_info .create :
268+ entry .write_metadata (metadata = metadata [0 ])
269+ if settings .output .comic_info .create :
270+ metadata [1 ].pages = (
271+ load_page_info (entry = entry , comic_info = metadata [1 ])
272+ if settings .output .comic_info .handle_pages
273+ else []
274+ )
275+ entry .write_metadata (metadata = metadata [1 ])
238276
239277 if not skip_rename :
240278 with CONSOLE .status ("Renaming based on metadata" , spinner = "simpleDotsScrolling" ):
241- rename_file ( entry = entry , metadata = metadata , settings = settings , target = target . parent )
279+ entry . rename ( naming = settings . output . naming , output_folder = settings . output . folder )
242280
243281 with CONSOLE .status ("Cleaning up empty folders" ):
244282 delete_empty_folders (folder = target )
0 commit comments