mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 14:56:01 +00:00
Wire up LaunchBox MAME arcade lookups
The scheduled task populated LAUNCHBOX_MAME_KEY from Mame.xml but no handler code ever read it, so arcade ROMs like pacman.zip never matched against Metadata.xml (which keys on full titles like "Pac-Man"). Add RemoteSource.get_mame_entry() and an ARCADE branch in get_rom() that resolves the MAME filename to its Description before name lookup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,16 @@ class LaunchboxHandler(MetadataHandler):
|
||||
else:
|
||||
search_term = fs_name
|
||||
|
||||
# Resolve MAME arcade filename (e.g. pacman.zip) to its full title
|
||||
# via LaunchBox's Mame.xml before name-based lookup.
|
||||
if platform_slug == UPS.ARCADE:
|
||||
mame_entry = await self._remote.get_mame_entry(fs_name)
|
||||
if mame_entry:
|
||||
description = (mame_entry.get("Description") or "").strip()
|
||||
if description:
|
||||
search_term = description
|
||||
fallback_rom = LaunchboxRom(launchbox_id=None, name=description)
|
||||
|
||||
# We replace " - "/"- " with ": " to match Launchbox's naming convention
|
||||
search_term = re.sub(DASH_COLON_REGEX, ": ", search_term).lower()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from logger.logger import log
|
||||
|
||||
from .platforms import get_platform
|
||||
from .types import (
|
||||
LAUNCHBOX_MAME_KEY,
|
||||
LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY,
|
||||
LAUNCHBOX_METADATA_DATABASE_ID_KEY,
|
||||
LAUNCHBOX_METADATA_IMAGE_KEY,
|
||||
@@ -74,6 +75,21 @@ class RemoteSource:
|
||||
|
||||
return None
|
||||
|
||||
async def get_mame_entry(self, file_name: str) -> dict | None:
|
||||
"""Resolve a MAME arcade filename to its LaunchBox MAME entry.
|
||||
|
||||
LaunchBox's Mame.xml indexes `<MameFile>` records by `<FileName>` (e.g.
|
||||
`pacman.zip`). The entry carries `Description` — the full title to
|
||||
search for in Metadata.xml.
|
||||
"""
|
||||
file_name_clean = (file_name or "").strip()
|
||||
if not file_name_clean:
|
||||
return None
|
||||
entry = await async_cache.hget(LAUNCHBOX_MAME_KEY, file_name_clean)
|
||||
if not entry:
|
||||
return None
|
||||
return json.loads(entry)
|
||||
|
||||
async def fetch_images(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -23,6 +23,7 @@ from handler.metadata.launchbox_handler.media import build_launchbox_metadata, b
|
||||
from handler.metadata.launchbox_handler.platforms import get_platform
|
||||
from handler.metadata.launchbox_handler.remote_source import RemoteSource
|
||||
from handler.metadata.launchbox_handler.types import (
|
||||
LAUNCHBOX_MAME_KEY,
|
||||
LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY,
|
||||
LAUNCHBOX_METADATA_DATABASE_ID_KEY,
|
||||
LAUNCHBOX_METADATA_IMAGE_KEY,
|
||||
@@ -520,6 +521,46 @@ class TestRemoteSourceGetRom:
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestRemoteSourceGetMameEntry:
|
||||
@pytest.fixture
|
||||
def source(self) -> RemoteSource:
|
||||
return RemoteSource()
|
||||
|
||||
async def test_cache_miss_returns_none(self, source: RemoteSource):
|
||||
with patch.object(
|
||||
async_cache, "hget", new_callable=AsyncMock, return_value=None
|
||||
):
|
||||
result = await source.get_mame_entry("pacman.zip")
|
||||
assert result is None
|
||||
|
||||
async def test_cache_hit_returns_dict(self, source: RemoteSource):
|
||||
mame_entry = {
|
||||
"FileName": "pacman.zip",
|
||||
"GameName": "pacman",
|
||||
"Description": "Pac-Man",
|
||||
}
|
||||
with patch.object(
|
||||
async_cache,
|
||||
"hget",
|
||||
new_callable=AsyncMock,
|
||||
return_value=json.dumps(mame_entry),
|
||||
) as mock_hget:
|
||||
result = await source.get_mame_entry("pacman.zip")
|
||||
mock_hget.assert_called_once_with(LAUNCHBOX_MAME_KEY, "pacman.zip")
|
||||
assert result == mame_entry
|
||||
|
||||
async def test_empty_input_returns_none(self, source: RemoteSource):
|
||||
result = await source.get_mame_entry("")
|
||||
assert result is None
|
||||
|
||||
async def test_whitespace_stripped(self, source: RemoteSource):
|
||||
with patch.object(
|
||||
async_cache, "hget", new_callable=AsyncMock, return_value=None
|
||||
) as mock_hget:
|
||||
await source.get_mame_entry(" pacman.zip ")
|
||||
mock_hget.assert_called_once_with(LAUNCHBOX_MAME_KEY, "pacman.zip")
|
||||
|
||||
|
||||
class TestRemoteSourceFetchImages:
|
||||
@pytest.fixture
|
||||
def source(self) -> RemoteSource:
|
||||
@@ -739,6 +780,7 @@ class TestLaunchboxHandlerGetRom:
|
||||
h._local.get_rom = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
h._remote.get_rom = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
h._remote.get_by_id = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
h._remote.get_mame_entry = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
h._remote.fetch_images = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
monkeypatch.setattr(LaunchboxHandler, "is_enabled", lambda *_: True)
|
||||
monkeypatch.setattr(async_cache, "exists", AsyncMock(return_value=True))
|
||||
@@ -868,6 +910,100 @@ class TestLaunchboxHandlerGetRom:
|
||||
# fs_rom_handler.get_file_name_with_no_tags should NOT be called
|
||||
mock_fs.get_file_name_with_no_tags.assert_not_called()
|
||||
|
||||
async def test_arcade_mame_resolves_shortname_to_full_title(
|
||||
self, handler: LaunchboxHandler
|
||||
):
|
||||
mame_entry = {
|
||||
"FileName": "pacman.zip",
|
||||
"GameName": "pacman",
|
||||
"Description": "Pac-Man",
|
||||
}
|
||||
remote_entry = {"DatabaseID": "999", "Name": "Pac-Man"}
|
||||
with (
|
||||
patch.object(
|
||||
handler._remote,
|
||||
"get_mame_entry",
|
||||
new=AsyncMock(return_value=mame_entry),
|
||||
) as mock_mame,
|
||||
patch.object(
|
||||
handler._remote,
|
||||
"get_rom",
|
||||
new=AsyncMock(return_value=remote_entry),
|
||||
) as mock_get_rom,
|
||||
patch(
|
||||
"handler.metadata.launchbox_handler.handler.fs_rom_handler"
|
||||
) as mock_fs,
|
||||
):
|
||||
mock_fs.get_file_name_with_no_tags.return_value = "pacman"
|
||||
result = await handler.get_rom("pacman.zip", "arcade")
|
||||
|
||||
mock_mame.assert_called_once_with("pacman.zip")
|
||||
# search term should be the MAME Description, lowercased
|
||||
assert mock_get_rom.call_args.args[0] == "pac-man"
|
||||
assert result.get("name", None) == "Pac-Man"
|
||||
assert result.get("launchbox_id", None) == 999
|
||||
|
||||
async def test_arcade_mame_miss_falls_back_to_filename_search(
|
||||
self, handler: LaunchboxHandler
|
||||
):
|
||||
with (
|
||||
patch.object(
|
||||
handler._remote,
|
||||
"get_mame_entry",
|
||||
new=AsyncMock(return_value=None),
|
||||
) as mock_mame,
|
||||
patch(
|
||||
"handler.metadata.launchbox_handler.handler.fs_rom_handler"
|
||||
) as mock_fs,
|
||||
):
|
||||
mock_fs.get_file_name_with_no_tags.return_value = "pacman"
|
||||
result = await handler.get_rom("pacman.zip", "arcade")
|
||||
|
||||
mock_mame.assert_called_once_with("pacman.zip")
|
||||
assert result["launchbox_id"] is None
|
||||
|
||||
async def test_arcade_mame_only_match_sets_fallback_name(
|
||||
self, handler: LaunchboxHandler
|
||||
):
|
||||
# MAME entry exists but Metadata.xml has no matching game: still surface
|
||||
# the MAME description as the rom name.
|
||||
mame_entry = {
|
||||
"FileName": "pacman.zip",
|
||||
"GameName": "pacman",
|
||||
"Description": "Pac-Man",
|
||||
}
|
||||
with (
|
||||
patch.object(
|
||||
handler._remote,
|
||||
"get_mame_entry",
|
||||
new=AsyncMock(return_value=mame_entry),
|
||||
),
|
||||
patch(
|
||||
"handler.metadata.launchbox_handler.handler.fs_rom_handler"
|
||||
) as mock_fs,
|
||||
):
|
||||
mock_fs.get_file_name_with_no_tags.return_value = "pacman"
|
||||
result = await handler.get_rom("pacman.zip", "arcade")
|
||||
|
||||
assert result["launchbox_id"] is None
|
||||
assert result.get("name", None) == "Pac-Man"
|
||||
|
||||
async def test_non_arcade_platform_skips_mame_lookup(
|
||||
self, handler: LaunchboxHandler
|
||||
):
|
||||
with (
|
||||
patch.object(
|
||||
handler._remote, "get_mame_entry", new=AsyncMock()
|
||||
) as mock_mame,
|
||||
patch(
|
||||
"handler.metadata.launchbox_handler.handler.fs_rom_handler"
|
||||
) as mock_fs,
|
||||
):
|
||||
mock_fs.get_file_name_with_no_tags.return_value = "pacman"
|
||||
await handler.get_rom("pacman.zip", "nes")
|
||||
|
||||
mock_mame.assert_not_called()
|
||||
|
||||
|
||||
class TestLaunchboxHandlerGetRomById:
|
||||
@pytest.fixture
|
||||
@@ -923,6 +1059,7 @@ class TestLaunchboxHandlerSearch:
|
||||
h._local.get_rom = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
h._remote.get_rom = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
h._remote.get_by_id = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
h._remote.get_mame_entry = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
h._remote.fetch_images = AsyncMock(return_value=None) # type: ignore[method-assign]
|
||||
monkeypatch.setattr(LaunchboxHandler, "is_enabled", lambda *_: True)
|
||||
monkeypatch.setattr(async_cache, "exists", AsyncMock(return_value=True))
|
||||
|
||||
Reference in New Issue
Block a user