1111from perdoo import __version__ , get_cache_root , setup_logging
1212from perdoo .cli import archive_app , settings_app
1313from perdoo .comic import Comic
14+ from perdoo .comic .archive import ArchiveSession
1415from perdoo .comic .errors import ComicArchiveError , ComicMetadataError
1516from perdoo .comic .metadata import ComicInfo , MetronInfo
1617from perdoo .comic .metadata .metron_info import Id , InformationSource
1718from perdoo .console import CONSOLE
18- from perdoo .processing import ProcessingPlan
1919from perdoo .services import BaseService , Comicvine , Metron
20- from perdoo .settings import Service , Services , Settings
20+ from perdoo .settings import Naming , Output , Service , Services , Settings
2121from perdoo .utils import (
2222 IssueSearch ,
2323 Search ,
3333LOGGER = 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 )
5043def 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
170162def 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
181173def 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
0 commit comments