diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index 097147f40..30002411a 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -791,22 +791,83 @@ async def fetch_hasheous_rom(hasheous_rom: HasheousRom) -> HasheousRom: fetch_libretro_rom(), ) - metadata_handlers = { - MetadataSource.IGDB: igdb_handler_rom, - MetadataSource.MOBY: moby_handler_rom, - MetadataSource.SS: ss_handler_rom, - MetadataSource.RA: ra_handler_rom, - MetadataSource.LAUNCHBOX: launchbox_handler_rom, - MetadataSource.HASHEOUS: hasheous_handler_rom, - MetadataSource.FLASHPOINT: flashpoint_handler_rom, - MetadataSource.HLTB: hltb_handler_rom, - MetadataSource.GAMELIST: gamelist_handler_rom, - MetadataSource.LIBRETRO: libretro_handler_rom, + metadata_handlers: dict[MetadataSource, dict] = { + MetadataSource.IGDB: { + "handler": igdb_handler_rom, + "id_field": "igdb_id", + "metadata_field": "igdb_metadata", + }, + MetadataSource.MOBY: { + "handler": moby_handler_rom, + "id_field": "moby_id", + "metadata_field": "moby_metadata", + }, + MetadataSource.SS: { + "handler": ss_handler_rom, + "id_field": "ss_id", + "metadata_field": "ss_metadata", + }, + MetadataSource.RA: { + "handler": ra_handler_rom, + "id_field": "ra_id", + "metadata_field": "ra_metadata", + }, + MetadataSource.LAUNCHBOX: { + "handler": launchbox_handler_rom, + "id_field": "launchbox_id", + "metadata_field": "launchbox_metadata", + }, + MetadataSource.HASHEOUS: { + "handler": hasheous_handler_rom, + "id_field": "hasheous_id", + "metadata_field": "hasheous_metadata", + }, + MetadataSource.FLASHPOINT: { + "handler": flashpoint_handler_rom, + "id_field": "flashpoint_id", + "metadata_field": "flashpoint_metadata", + }, + MetadataSource.HLTB: { + "handler": hltb_handler_rom, + "id_field": "hltb_id", + "metadata_field": "hltb_metadata", + }, + MetadataSource.GAMELIST: { + "handler": gamelist_handler_rom, + "id_field": "gamelist_id", + "metadata_field": "gamelist_metadata", + }, + MetadataSource.LIBRETRO: { + "handler": libretro_handler_rom, + "id_field": "libretro_id", + "metadata_field": None, + }, + MetadataSource.SGDB: { + "handler": {}, + "id_field": "sgdb_id", + "metadata_field": None, + }, + MetadataSource.TGDB: { + "handler": {}, + "id_field": "tgdb_id", + "metadata_field": None, + }, } + # For COMPLETE rescans, explicitly clear metadata IDs and metadata for unselected sources + # This ensures that when a source is no longer selected, its data is removed from the ROM + if not newly_added and scan_type == ScanType.COMPLETE: + for source, fields in metadata_handlers.items(): + if source not in metadata_sources: + rom_attrs[fields["id_field"]] = None + if fields["metadata_field"]: + rom_attrs[fields["metadata_field"]] = {} + # Determine which metadata sources are available available_sources = [ - name for name, handler in metadata_handlers.items() if handler.get(f"{name}_id") + name + for name, fields in metadata_handlers.items() + if fields["handler"].get(fields["id_field"]) ] # Apply metadata priority order @@ -815,7 +876,7 @@ async def fetch_hasheous_rom(hasheous_rom: HasheousRom) -> HasheousRom: ) # Reverse priority order to apply highest priority last for source_name in reversed(priority_ordered): - handler_data = metadata_handlers[source_name] + handler_data = metadata_handlers[source_name]["handler"] # Only update fields that have valid values for key, field_value in handler_data.items(): if field_value: @@ -827,7 +888,7 @@ async def fetch_hasheous_rom(hasheous_rom: HasheousRom) -> HasheousRom: ) # Reverse priority order to apply highest priority last for source_name in reversed(priority_ordered_artwork): - handler_data = metadata_handlers[source_name] + handler_data = metadata_handlers[source_name]["handler"] for field in ["url_cover", "url_screenshots", "url_manual"]: # Only update fields that have valid values field_value = handler_data.get(field) @@ -931,7 +992,9 @@ async def fetch_sgdb_details(playmatch_rom: PlaymatchRomMatch) -> SGDBRom: ) if sgdb_cover and not manual_cover_preserved: cover_sources = [ - name for name, h in metadata_handlers.items() if h.get("url_cover") + name + for name, fields in metadata_handlers.items() + if fields["handler"].get("url_cover") ] ranked = get_priority_ordered_metadata_sources( cover_sources + [MetadataSource.SGDB], "artwork" diff --git a/backend/tests/handler/test_fastapi.py b/backend/tests/handler/test_fastapi.py index 62b2218f7..d333e38fb 100644 --- a/backend/tests/handler/test_fastapi.py +++ b/backend/tests/handler/test_fastapi.py @@ -1,6 +1,10 @@ +from unittest.mock import AsyncMock, patch + import pytest -from handler.database import db_platform_handler +from handler.database import db_platform_handler, db_rom_handler +from handler.metadata import meta_hasheous_handler, meta_playmatch_handler +from handler.metadata.hasheous_handler import HasheousRom from handler.scan_handler import MetadataSource, ScanType, scan_platform, scan_rom from models.platform import Platform from models.rom import Rom, RomFile @@ -93,3 +97,80 @@ async def test_scan_rom(): # assert rom.hasheous_id == 4872 # assert rom.fs_size_bytes == 23175094 # assert rom.tags == [] + + +@patch.object(meta_playmatch_handler, "is_enabled", return_value=False) +@patch.object(meta_hasheous_handler, "get_ra_game", new_callable=AsyncMock) +@patch.object(meta_hasheous_handler, "get_igdb_game", new_callable=AsyncMock) +@patch.object(meta_hasheous_handler, "lookup_rom", new_callable=AsyncMock) +async def test_scan_rom_complete_clears_unselected_metadata( + mock_lookup, mock_get_igdb, mock_get_ra, mock_playmatch_enabled +): + """COMPLETE rescan with newly_added=False must clear id and *_metadata + fields for sources that are no longer in metadata_sources.""" + hasheous_result = HasheousRom( + hasheous_id=999, + igdb_id=None, + tgdb_id=None, + ra_id=None, + name="Mock Hasheous Game", + ) + mock_lookup.return_value = hasheous_result + mock_get_igdb.return_value = hasheous_result + mock_get_ra.return_value = hasheous_result + + platform = Platform( + id=1, + slug="n64", + fs_slug="n64", + name="Nintendo 64", + igdb_id=4, + ra_id=2, + hasheous_id=64, + ) + platform = db_platform_handler.add_platform(platform) + + rom = Rom( + platform_id=platform.id, + fs_name="Paper Mario (USA).z64", + fs_name_no_tags="Paper Mario", + fs_name_no_ext="Paper Mario", + fs_extension="z64", + fs_path="n64/Paper Mario (USA)", + name="Paper Mario", + igdb_id=3340, + igdb_metadata={"summary": "stale IGDB metadata"}, + ra_id=1234, + ra_metadata={"name": "stale RA metadata"}, + hasheous_id=4872, + fs_size_bytes=1024, + tags=[], + ) + rom = db_rom_handler.add_rom(rom) + + async with initialize_context(): + result = await scan_rom( + platform=platform, + scan_type=ScanType.COMPLETE, + rom=rom, + fs_rom={ + "fs_name": "Paper Mario (USA).z64", + "flat": True, + "nested": False, + "files": [], + "crc_hash": "", + "md5_hash": "", + "sha1_hash": "", + "ra_hash": "", + }, + metadata_sources=[MetadataSource.HASHEOUS], + newly_added=False, + ) + + # IGDB and RA were unselected — their id and metadata must be cleared. + assert result.igdb_id is None + assert result.igdb_metadata == {} + assert result.ra_id is None + assert result.ra_metadata == {} + # Hasheous is still selected and should remain populated. + assert result.hasheous_id == 999