Skip to content

Commit 96efc36

Browse files
authored
Merge pull request #3475 from Spinnich/fix/ss-jeuinfos-romnom-unhashed
fix(screenscraper): utilize ss.fr jeuinfos.php endpoint for non-hashable platforms
2 parents 5cf67cd + 2b23e69 commit 96efc36

2 files changed

Lines changed: 113 additions & 9 deletions

File tree

backend/handler/metadata/ss_handler.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -699,11 +699,23 @@ async def lookup_rom(
699699
sha1_hash = first_file.sha1_hash
700700
crc_hash = first_file.crc_hash
701701
fs_size_bytes = first_file.file_size_bytes
702+
rom_name = (
703+
first_file.archive_members[0]["name"].split("/")[-1]
704+
if first_file.archive_members is not None
705+
and len(first_file.archive_members) == 1
706+
else first_file.file_name
707+
)
702708

703-
if not (md5_hash or sha1_hash or crc_hash):
709+
# Files on NON_HASHABLE_PLATFORMS (or any file when SKIP_HASH_CALCULATION
710+
# is enabled) have no hashes. jeuInfos can still identify the game from the
711+
# filename (romnom) + platform (systemeid) — a stronger matcher than the
712+
# jeuRecherche name search the get_rom fallback uses — so only bail out when
713+
# we have neither a hash nor a filename to match on.
714+
if not (md5_hash or sha1_hash or crc_hash or rom_name):
704715
log.info(
705-
"No hashes provided for ScreenScraper lookup. "
706-
"At least one of md5_hash, sha1_hash, or crc_hash is required."
716+
"No hashes or filename provided for ScreenScraper lookup. "
717+
"At least one of md5_hash, sha1_hash, crc_hash, or a filename "
718+
"is required."
707719
)
708720
return SSRom(ss_id=None), False
709721

@@ -713,12 +725,7 @@ async def lookup_rom(
713725
sha1=sha1_hash,
714726
crc=crc_hash,
715727
rom_size_bytes=fs_size_bytes,
716-
rom_name=(
717-
first_file.archive_members[0]["name"].split("/")[-1]
718-
if first_file.archive_members is not None
719-
and len(first_file.archive_members) == 1
720-
else first_file.file_name
721-
),
728+
rom_name=rom_name,
722729
rom_type=_get_rom_type(first_file),
723730
)
724731
if not res:

backend/tests/handler/metadata/test_ss_handler.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,103 @@ def _make_mock_file(self) -> MagicMock:
711711
f.file_name = "bios.bin"
712712
return f
713713

714+
def _make_unhashed_file(
715+
self, file_name: str = "Adventure Island II (USA).nes"
716+
) -> MagicMock:
717+
"""A top-level file with no hashes, as produced for NON_HASHABLE_PLATFORMS
718+
or when SKIP_HASH_CALCULATION is enabled."""
719+
f = MagicMock()
720+
f.file_size_bytes = 131072
721+
f.is_top_level = True
722+
f.file_extension = "nes"
723+
f.md5_hash = ""
724+
f.sha1_hash = ""
725+
f.crc_hash = ""
726+
f.file_name = file_name
727+
f.archive_members = None
728+
return f
729+
730+
@pytest.mark.asyncio
731+
async def test_no_hash_still_attempts_jeuinfos_by_filename(self):
732+
"""A file with no hashes must still reach jeuInfos using the filename
733+
(romnom) + platform (systemeid), instead of bailing out and degrading to
734+
the weaker jeuRecherche name search."""
735+
handler = SSHandler()
736+
mock_file = self._make_unhashed_file("Adventure Island II (USA).nes")
737+
captured = {}
738+
739+
async def capture(**kwargs):
740+
captured.update(kwargs)
741+
return None
742+
743+
with patch.object(handler.ss_service, "get_game_info", side_effect=capture):
744+
result, is_not_game = await handler.lookup_rom(
745+
MagicMock(platform_slug="nes"), 3, [mock_file]
746+
)
747+
748+
assert captured, "get_game_info should be called even without hashes"
749+
assert captured.get("rom_name") == "Adventure Island II (USA).nes"
750+
assert captured.get("system_id") == 3
751+
assert not captured.get("md5")
752+
assert not captured.get("sha1")
753+
assert not captured.get("crc")
754+
assert result["ss_id"] is None
755+
assert is_not_game is False
756+
757+
@pytest.mark.asyncio
758+
async def test_no_hash_match_builds_game(self):
759+
"""When jeuInfos matches an un-hashed file by filename, the game is built
760+
and returned (the romnom matcher bridges number-style differences such as
761+
'Adventure Island II' -> 'Adventure Island 2')."""
762+
config = _make_config(region_priority=["us"])
763+
game = {
764+
"id": "1234",
765+
"noms": [{"region": "us", "text": "Adventure Island 2"}],
766+
"medias": [],
767+
"synopsis": [],
768+
"dates": [],
769+
"genres": [],
770+
"familles": [],
771+
"modes": [],
772+
"joueurs": {},
773+
"note": {},
774+
}
775+
handler = SSHandler()
776+
mock_file = self._make_unhashed_file("Adventure Island II (USA).nes")
777+
rom = MagicMock(platform_slug="nes", platform_id=1, id=100, regions=["USA"])
778+
779+
with (
780+
patch("handler.metadata.ss_handler.cm.get_config", return_value=config),
781+
patch.object(handler.ss_service, "get_game_info", return_value=game),
782+
):
783+
result, is_not_game = await handler.lookup_rom(rom, 3, [mock_file])
784+
785+
assert result["ss_id"] == 1234
786+
assert result["name"] == "Adventure Island 2"
787+
assert is_not_game is False
788+
789+
@pytest.mark.asyncio
790+
async def test_no_hash_no_filename_skips_lookup(self):
791+
"""With neither a hash nor a filename there is nothing to match on, so the
792+
lookup is skipped without spending an API call."""
793+
handler = SSHandler()
794+
mock_file = self._make_unhashed_file(file_name="")
795+
called = False
796+
797+
async def capture(**kwargs):
798+
nonlocal called
799+
called = True
800+
return None
801+
802+
with patch.object(handler.ss_service, "get_game_info", side_effect=capture):
803+
result, is_not_game = await handler.lookup_rom(
804+
MagicMock(platform_slug="nes"), 3, [mock_file]
805+
)
806+
807+
assert called is False
808+
assert result["ss_id"] is None
809+
assert is_not_game is False
810+
714811
@pytest.mark.asyncio
715812
async def test_returns_notgame_flag_on_notgame_field(self):
716813
notgame_response = {

0 commit comments

Comments
 (0)