@@ -272,43 +272,97 @@ async def _identify_rom(
272272 parsed_tags = fs_rom_handler .parse_tags (fs_rom ["fs_name" ])
273273 roms_path = fs_rom_handler .get_roms_fs_structure (platform .fs_slug )
274274
275- # Create the entry early so we have the ID
275+ rom_attrs = {
276+ "fs_name" : fs_rom ["fs_name" ],
277+ "fs_path" : roms_path ,
278+ "regions" : parsed_tags .regions ,
279+ "revision" : parsed_tags .revision ,
280+ "version" : parsed_tags .version ,
281+ "languages" : parsed_tags .languages ,
282+ "tags" : parsed_tags .other_tags ,
283+ "platform_id" : platform .id ,
284+ "name" : fs_rom_handler .get_file_name_with_no_tags (fs_rom ["fs_name" ]),
285+ "url_cover" : "" ,
286+ "url_manual" : "" ,
287+ "url_screenshots" : [],
288+ }
289+
290+ calculate_hashes = not cm .get_config ().SKIP_HASH_CALCULATION
291+
276292 newly_added : bool = rom is None
277- if not rom :
278- try :
279- rom = db_rom_handler .add_rom (
280- Rom (
281- fs_name = fs_rom ["fs_name" ],
282- fs_path = roms_path ,
283- regions = parsed_tags .regions ,
284- revision = parsed_tags .revision ,
285- version = parsed_tags .version ,
286- languages = parsed_tags .languages ,
287- tags = parsed_tags .other_tags ,
288- platform_id = platform .id ,
289- name = fs_rom_handler .get_file_name_with_no_tags (fs_rom ["fs_name" ]),
290- url_cover = "" ,
291- url_manual = "" ,
292- url_screenshots = [],
293- )
293+ reassociated : bool = False
294+ files_built : bool = False
295+
296+ if rom is None :
297+ # No entry matches this filename. Before treating it as new, hash
298+ # the files and check whether they belong to an existing entry that went
299+ # missing (a renamed or moved ROM), so its collections, notes, and
300+ # uploaded assets carry over instead of being orphaned on a duplicate.
301+ parsed_rom_files = await fs_rom_handler .get_rom_files (
302+ Rom (
303+ ** rom_attrs ,
304+ platform = platform ,
305+ ),
306+ calculate_hashes = calculate_hashes ,
307+ )
308+ fs_rom .update (
309+ {
310+ "files" : parsed_rom_files .rom_files ,
311+ "crc_hash" : parsed_rom_files .crc_hash ,
312+ "md5_hash" : parsed_rom_files .md5_hash ,
313+ "sha1_hash" : parsed_rom_files .sha1_hash ,
314+ "ra_hash" : parsed_rom_files .ra_hash ,
315+ }
316+ )
317+ files_built = True
318+
319+ missing_match = db_rom_handler .get_matching_missing_rom (
320+ platform_id = platform .id ,
321+ crc_hash = parsed_rom_files .crc_hash ,
322+ md5_hash = parsed_rom_files .md5_hash ,
323+ sha1_hash = parsed_rom_files .sha1_hash ,
324+ )
325+ if missing_match is not None :
326+ # Move the existing entry onto the new file, clearing its missing state.
327+ rom = db_rom_handler .update_rom (
328+ missing_match .id ,
329+ {
330+ "fs_name" : fs_rom ["fs_name" ],
331+ "fs_path" : roms_path ,
332+ "regions" : parsed_tags .regions ,
333+ "revision" : parsed_tags .revision ,
334+ "version" : parsed_tags .version ,
335+ "languages" : parsed_tags .languages ,
336+ "tags" : parsed_tags .other_tags ,
337+ "missing_from_fs" : False ,
338+ },
294339 )
295- except IntegrityError :
296- # A concurrent scan already created this ROM, so skip it here.
297- log .debug (
298- f"Skipping { hl (fs_rom ['fs_name' ])} : already created by a concurrent scan"
340+ reassociated = True
341+ newly_added = False
342+ log .info (
343+ f"Reassociated { hl (fs_rom ['fs_name' ])} with existing entry "
344+ f"{ hl (rom .name or rom .fs_name , color = BLUE )} by file hash"
299345 )
300- return
346+ else :
347+ try :
348+ rom = db_rom_handler .add_rom (Rom (** rom_attrs ))
349+ except IntegrityError :
350+ # A concurrent scan already created this ROM, so skip it here.
351+ log .debug (
352+ f"Skipping { hl (fs_rom ['fs_name' ])} : already created by a concurrent scan"
353+ )
354+ return
301355
302- # Build rom files object before scanning
303- should_update_files = _should_get_rom_files (
356+ # Build rom files object before scanning. A reassociated ROM always rebuilds
357+ # its files so the stale paths from the old filename are replaced.
358+ should_update_files = reassociated or _should_get_rom_files (
304359 scan_type = scan_type ,
305360 rom = rom ,
306361 newly_added = newly_added ,
307362 roms_ids = roms_ids ,
308363 )
309- if should_update_files :
364+ if should_update_files and not files_built :
310365 # Get hash calculation setting from config
311- calculate_hashes = not cm .get_config ().SKIP_HASH_CALCULATION
312366 if calculate_hashes :
313367 log .debug (f"Calculating file hashes for { rom .fs_name } ..." )
314368
@@ -612,6 +666,12 @@ async def _identify_platform(
612666 else :
613667 log .info (f"{ hl (str (len (fs_roms )))} roms found in the file system" )
614668
669+ # Flag entries whose file is gone before identifying files, so a renamed or
670+ # moved ROM (a new file with no fs_name match) can be reassociated by hash
671+ # with its now-missing entry instead of spawning a duplicate. The end-of-scan
672+ # call below re-syncs and logs, unmarking any entry that got reassociated.
673+ db_rom_handler .mark_missing_roms (platform .id , [rom ["fs_name" ] for rom in fs_roms ])
674+
615675 # Create semaphore to limit concurrent ROM scanning
616676 scan_semaphore = asyncio .Semaphore (SCAN_WORKERS )
617677
0 commit comments