Merge branch 'master' into feat/patcher.js

This commit is contained in:
zurdi
2025-12-23 17:05:31 +00:00
15 changed files with 516 additions and 95 deletions

View File

@@ -0,0 +1,363 @@
"""empty message
Revision ID: 0058_roms_metadata_launchbox
Revises: 0057_multi_notes
Create Date: 2025-12-22 00:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
from utils.database import is_postgresql
# revision identifiers, used by Alembic.
revision = "0058_roms_metadata_launchbox"
down_revision = "0057_multi_notes"
branch_labels = None
depends_on = None
def upgrade():
connection = op.get_bind()
if is_postgresql(connection):
connection.execute(
sa.text(
"""
CREATE OR REPLACE VIEW roms_metadata AS
SELECT
r.id AS rom_id,
NOW() AS created_at,
NOW() AS updated_at,
COALESCE(
(r.igdb_metadata -> 'genres'),
(r.moby_metadata -> 'genres'),
(r.ss_metadata -> 'genres'),
(r.launchbox_metadata -> 'genres'),
(r.ra_metadata -> 'genres'),
(r.flashpoint_metadata -> 'genres'),
(r.gamelist_metadata -> 'genres'),
'[]'::jsonb
) AS genres,
COALESCE(
(r.igdb_metadata -> 'franchises'),
(r.ss_metadata -> 'franchises'),
(r.flashpoint_metadata -> 'franchises'),
(r.gamelist_metadata -> 'franchises'),
'[]'::jsonb
) AS franchises,
COALESCE(
(r.igdb_metadata -> 'collections'),
'[]'::jsonb
) AS collections,
COALESCE(
(r.igdb_metadata -> 'companies'),
(r.ss_metadata -> 'companies'),
(r.ra_metadata -> 'companies'),
(r.launchbox_metadata -> 'companies'),
(r.flashpoint_metadata -> 'companies'),
(r.gamelist_metadata -> 'companies'),
'[]'::jsonb
) AS companies,
COALESCE(
(r.igdb_metadata -> 'game_modes'),
(r.ss_metadata -> 'game_modes'),
(r.flashpoint_metadata -> 'game_modes'),
'[]'::jsonb
) AS game_modes,
COALESCE(
CASE
WHEN r.igdb_metadata IS NOT NULL
AND r.igdb_metadata ? 'age_ratings'
AND jsonb_array_length(r.igdb_metadata -> 'age_ratings') > 0
THEN
jsonb_path_query_array(r.igdb_metadata, '$.age_ratings[*].rating')
ELSE
'[]'::jsonb
END,
CASE
WHEN r.launchbox_metadata IS NOT NULL
AND r.launchbox_metadata ? 'esrb'
AND r.launchbox_metadata ->> 'esrb' IS NOT NULL
AND r.launchbox_metadata ->> 'esrb' != ''
THEN
jsonb_build_array(r.launchbox_metadata ->> 'esrb')
ELSE
'[]'::jsonb
END,
'[]'::jsonb
) AS age_ratings,
CASE
WHEN r.igdb_metadata IS NOT NULL AND r.igdb_metadata ? 'first_release_date' AND
r.igdb_metadata ->> 'first_release_date' NOT IN ('null', 'None', '0', '0.0') AND
r.igdb_metadata ->> 'first_release_date' ~ '^[0-9]+$'
THEN (r.igdb_metadata ->> 'first_release_date')::bigint * 1000
WHEN r.ss_metadata IS NOT NULL AND r.ss_metadata ? 'first_release_date' AND
r.ss_metadata ->> 'first_release_date' NOT IN ('null', 'None', '0', '0.0') AND
r.ss_metadata ->> 'first_release_date' ~ '^[0-9]+$'
THEN (r.ss_metadata ->> 'first_release_date')::bigint * 1000
WHEN r.ra_metadata IS NOT NULL AND r.ra_metadata ? 'first_release_date' AND
r.ra_metadata ->> 'first_release_date' NOT IN ('null', 'None', '0', '0.0') AND
r.ra_metadata ->> 'first_release_date' ~ '^[0-9]+$'
THEN (r.ra_metadata ->> 'first_release_date')::bigint * 1000
WHEN r.launchbox_metadata IS NOT NULL AND r.launchbox_metadata ? 'first_release_date' AND
r.launchbox_metadata ->> 'first_release_date' NOT IN ('null', 'None', '0', '0.0') AND
r.launchbox_metadata ->> 'first_release_date' ~ '^[0-9]+$'
THEN (r.launchbox_metadata ->> 'first_release_date')::bigint * 1000
WHEN r.flashpoint_metadata IS NOT NULL AND r.flashpoint_metadata ? 'first_release_date' AND
r.flashpoint_metadata ->> 'first_release_date' NOT IN ('null', 'None', '0', '0.0') AND
r.flashpoint_metadata ->> 'first_release_date' ~ '^[0-9]+$'
THEN (r.flashpoint_metadata ->> 'first_release_date')::bigint * 1000
WHEN r.gamelist_metadata IS NOT NULL
AND r.gamelist_metadata ? 'first_release_date'
AND r.gamelist_metadata ->> 'first_release_date' NOT IN ('null', 'None', '0', '0.0')
AND r.gamelist_metadata ->> 'first_release_date' ~ '^[0-9]{8}T[0-9]{6}$'
THEN (extract(epoch FROM to_timestamp(r.gamelist_metadata ->> 'first_release_date', 'YYYYMMDD"T"HH24MISS')) * 1000)::bigint
ELSE NULL
END AS first_release_date,
CASE
WHEN (igdb_rating IS NOT NULL OR moby_rating IS NOT NULL OR ss_rating IS NOT NULL OR launchbox_rating IS NOT NULL OR gamelist_rating IS NOT NULL) THEN
(COALESCE(igdb_rating, 0) + COALESCE(moby_rating, 0) + COALESCE(ss_rating, 0) + COALESCE(launchbox_rating, 0) + COALESCE(gamelist_rating, 0)) /
(CASE WHEN igdb_rating IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN moby_rating IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN ss_rating IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN launchbox_rating IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN gamelist_rating IS NOT NULL THEN 1 ELSE 0 END)
ELSE NULL
END AS average_rating
FROM (
SELECT
r.id,
r.igdb_metadata,
r.moby_metadata,
r.ss_metadata,
r.ra_metadata,
r.launchbox_metadata,
r.flashpoint_metadata,
r.gamelist_metadata,
CASE
WHEN r.igdb_metadata IS NOT NULL AND r.igdb_metadata ? 'total_rating' AND
r.igdb_metadata ->> 'total_rating' NOT IN ('null', 'None', '0', '0.0') AND
r.igdb_metadata ->> 'total_rating' ~ '^[0-9]+(\\.[0-9]+)?$'
THEN (r.igdb_metadata ->> 'total_rating')::float
ELSE NULL
END AS igdb_rating,
CASE
WHEN r.moby_metadata IS NOT NULL AND r.moby_metadata ? 'moby_score' AND
r.moby_metadata ->> 'moby_score' NOT IN ('null', 'None', '0', '0.0') AND
r.moby_metadata ->> 'moby_score' ~ '^[0-9]+(\\.[0-9]+)?$'
THEN (r.moby_metadata ->> 'moby_score')::float * 10
ELSE NULL
END AS moby_rating,
CASE
WHEN r.ss_metadata IS NOT NULL AND r.ss_metadata ? 'ss_score' AND
r.ss_metadata ->> 'ss_score' NOT IN ('null', 'None', '0', '0.0') AND
r.ss_metadata ->> 'ss_score' ~ '^[0-9]+(\\.[0-9]+)?$'
THEN (r.ss_metadata ->> 'ss_score')::float * 10
ELSE NULL
END AS ss_rating,
CASE
WHEN r.launchbox_metadata IS NOT NULL AND r.launchbox_metadata ? 'community_rating' AND
r.launchbox_metadata ->> 'community_rating' NOT IN ('null', 'None', '0', '0.0') AND
r.launchbox_metadata ->> 'community_rating' ~ '^[0-9]+(\\.[0-9]+)?$'
THEN (r.launchbox_metadata ->> 'community_rating')::float * 20
ELSE NULL
END AS launchbox_rating,
CASE
WHEN r.gamelist_metadata IS NOT NULL AND r.gamelist_metadata ? 'rating' AND
r.gamelist_metadata ->> 'rating' NOT IN ('null', 'None', '0', '0.0') AND
r.gamelist_metadata ->> 'rating' ~ '^[0-9]+(\\.[0-9]+)?$'
THEN (r.gamelist_metadata ->> 'rating')::float * 100
ELSE NULL
END AS gamelist_rating
FROM roms r
) AS r;
"""
)
)
else:
connection.execute(
sa.text(
"""CREATE OR REPLACE VIEW roms_metadata AS
SELECT
r.id as rom_id,
NOW() AS created_at,
NOW() AS updated_at,
COALESCE(
JSON_EXTRACT(r.igdb_metadata, '$.genres'),
JSON_EXTRACT(r.moby_metadata, '$.genres'),
JSON_EXTRACT(r.ss_metadata, '$.genres'),
JSON_EXTRACT(r.launchbox_metadata, '$.genres'),
JSON_EXTRACT(r.ra_metadata, '$.genres'),
JSON_EXTRACT(r.flashpoint_metadata, '$.genres'),
JSON_EXTRACT(r.gamelist_metadata, '$.genres'),
JSON_ARRAY()
) AS genres,
COALESCE(
JSON_EXTRACT(r.igdb_metadata, '$.franchises'),
JSON_EXTRACT(r.ss_metadata, '$.franchises'),
JSON_EXTRACT(r.flashpoint_metadata, '$.franchises'),
JSON_EXTRACT(r.gamelist_metadata, '$.franchises'),
JSON_ARRAY()
) AS franchises,
COALESCE(
JSON_EXTRACT(r.igdb_metadata, '$.collections'),
JSON_ARRAY()
) AS collections,
COALESCE(
JSON_EXTRACT(r.igdb_metadata, '$.companies'),
JSON_EXTRACT(r.ss_metadata, '$.companies'),
JSON_EXTRACT(r.ra_metadata, '$.companies'),
JSON_EXTRACT(r.launchbox_metadata, '$.companies'),
JSON_EXTRACT(r.flashpoint_metadata, '$.companies'),
JSON_EXTRACT(r.gamelist_metadata, '$.companies'),
JSON_ARRAY()
) AS companies,
COALESCE(
JSON_EXTRACT(r.igdb_metadata, '$.game_modes'),
JSON_EXTRACT(r.ss_metadata, '$.game_modes'),
JSON_EXTRACT(r.flashpoint_metadata, '$.game_modes'),
JSON_ARRAY()
) AS game_modes,
COALESCE(
CASE
WHEN JSON_CONTAINS_PATH(r.igdb_metadata, 'one', '$.age_ratings')
AND JSON_LENGTH(JSON_EXTRACT(r.igdb_metadata, '$.age_ratings')) > 0
THEN
JSON_EXTRACT(r.igdb_metadata, '$.age_ratings[*].rating')
ELSE
JSON_ARRAY()
END,
CASE
WHEN JSON_CONTAINS_PATH(r.launchbox_metadata, 'one', '$.esrb')
AND JSON_EXTRACT(r.launchbox_metadata, '$.esrb') IS NOT NULL
AND JSON_EXTRACT(r.launchbox_metadata, '$.esrb') != ''
THEN
JSON_ARRAY(JSON_EXTRACT(r.launchbox_metadata, '$.esrb'))
ELSE
JSON_ARRAY()
END,
JSON_ARRAY()
) AS age_ratings,
CASE
WHEN JSON_CONTAINS_PATH(r.igdb_metadata, 'one', '$.first_release_date') AND
JSON_UNQUOTE(JSON_EXTRACT(r.igdb_metadata, '$.first_release_date')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(r.igdb_metadata, '$.first_release_date')) REGEXP '^[0-9]+$'
THEN CAST(JSON_EXTRACT(r.igdb_metadata, '$.first_release_date') AS SIGNED) * 1000
WHEN JSON_CONTAINS_PATH(r.ss_metadata, 'one', '$.first_release_date') AND
JSON_UNQUOTE(JSON_EXTRACT(r.ss_metadata, '$.first_release_date')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(r.ss_metadata, '$.first_release_date')) REGEXP '^[0-9]+$'
THEN CAST(JSON_EXTRACT(r.ss_metadata, '$.first_release_date') AS SIGNED) * 1000
WHEN JSON_CONTAINS_PATH(r.ra_metadata, 'one', '$.first_release_date') AND
JSON_UNQUOTE(JSON_EXTRACT(r.ra_metadata, '$.first_release_date')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(r.ra_metadata, '$.first_release_date')) REGEXP '^[0-9]+$'
THEN CAST(JSON_EXTRACT(r.ra_metadata, '$.first_release_date') AS SIGNED) * 1000
WHEN JSON_CONTAINS_PATH(r.launchbox_metadata, 'one', '$.first_release_date') AND
JSON_UNQUOTE(JSON_EXTRACT(r.launchbox_metadata, '$.first_release_date')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(r.launchbox_metadata, '$.first_release_date')) REGEXP '^[0-9]+$'
THEN CAST(JSON_EXTRACT(r.launchbox_metadata, '$.first_release_date') AS SIGNED) * 1000
WHEN JSON_CONTAINS_PATH(r.flashpoint_metadata, 'one', '$.first_release_date') AND
JSON_UNQUOTE(JSON_EXTRACT(r.flashpoint_metadata, '$.first_release_date')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(r.flashpoint_metadata, '$.first_release_date')) REGEXP '^[0-9]+$'
THEN CAST(JSON_EXTRACT(r.flashpoint_metadata, '$.first_release_date') AS SIGNED) * 1000
WHEN JSON_CONTAINS_PATH(r.gamelist_metadata, 'one', '$.first_release_date')
AND JSON_UNQUOTE(JSON_EXTRACT(r.gamelist_metadata, '$.first_release_date')) NOT IN ('null', 'None', '0', '0.0')
AND JSON_UNQUOTE(JSON_EXTRACT(r.gamelist_metadata, '$.first_release_date')) REGEXP '^[0-9]{8}T[0-9]{6}$'
THEN UNIX_TIMESTAMP(
STR_TO_DATE(
JSON_UNQUOTE(JSON_EXTRACT(r.gamelist_metadata, '$.first_release_date')),
'%Y%m%dT%H%i%S'
)
) * 1000
ELSE NULL
END AS first_release_date,
CASE
WHEN (igdb_rating IS NOT NULL OR moby_rating IS NOT NULL OR ss_rating IS NOT NULL OR launchbox_rating IS NOT NULL OR gamelist_rating IS NOT NULL) THEN
(COALESCE(igdb_rating, 0) + COALESCE(moby_rating, 0) + COALESCE(ss_rating, 0) + COALESCE(launchbox_rating, 0) + COALESCE(gamelist_rating, 0)) /
(CASE WHEN igdb_rating IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN moby_rating IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN ss_rating IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN launchbox_rating IS NOT NULL THEN 1 ELSE 0 END +
CASE WHEN gamelist_rating IS NOT NULL THEN 1 ELSE 0 END)
ELSE NULL
END AS average_rating
FROM (
SELECT
id,
igdb_metadata,
moby_metadata,
ss_metadata,
ra_metadata,
launchbox_metadata,
flashpoint_metadata,
gamelist_metadata,
CASE
WHEN JSON_CONTAINS_PATH(igdb_metadata, 'one', '$.total_rating') AND
JSON_UNQUOTE(JSON_EXTRACT(igdb_metadata, '$.total_rating')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(igdb_metadata, '$.total_rating')) REGEXP '^[0-9]+(\\.[0-9]+)?$'
THEN CAST(JSON_EXTRACT(igdb_metadata, '$.total_rating') AS DECIMAL(10,2))
ELSE NULL
END AS igdb_rating,
CASE
WHEN JSON_CONTAINS_PATH(moby_metadata, 'one', '$.moby_score') AND
JSON_UNQUOTE(JSON_EXTRACT(moby_metadata, '$.moby_score')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(moby_metadata, '$.moby_score')) REGEXP '^[0-9]+(\\.[0-9]+)?$'
THEN CAST(JSON_EXTRACT(moby_metadata, '$.moby_score') AS DECIMAL(10,2)) * 10
ELSE NULL
END AS moby_rating,
CASE
WHEN JSON_CONTAINS_PATH(ss_metadata, 'one', '$.ss_score') AND
JSON_UNQUOTE(JSON_EXTRACT(ss_metadata, '$.ss_score')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(ss_metadata, '$.ss_score')) REGEXP '^[0-9]+(\\.[0-9]+)?$'
THEN CAST(JSON_EXTRACT(ss_metadata, '$.ss_score') AS DECIMAL(10,2)) * 10
ELSE NULL
END AS ss_rating,
CASE
WHEN JSON_CONTAINS_PATH(launchbox_metadata, 'one', '$.community_rating') AND
JSON_UNQUOTE(JSON_EXTRACT(launchbox_metadata, '$.community_rating')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(launchbox_metadata, '$.community_rating')) REGEXP '^[0-9]+(\\.[0-9]+)?$'
THEN CAST(JSON_EXTRACT(launchbox_metadata, '$.community_rating') AS DECIMAL(10,2)) * 20
ELSE NULL
END AS launchbox_rating,
CASE
WHEN JSON_CONTAINS_PATH(gamelist_metadata, 'one', '$.rating') AND
JSON_UNQUOTE(JSON_EXTRACT(gamelist_metadata, '$.rating')) NOT IN ('null', 'None', '0', '0.0') AND
JSON_UNQUOTE(JSON_EXTRACT(gamelist_metadata, '$.rating')) REGEXP '^[0-9]+(\\.[0-9]+)?$'
THEN CAST(JSON_EXTRACT(gamelist_metadata, '$.rating') AS DECIMAL(10,2)) * 100
ELSE NULL
END AS gamelist_rating
FROM roms
) AS r;
"""
)
)
def downgrade():
op.execute("DROP VIEW IF EXISTS roms_metadata")

View File

@@ -84,6 +84,8 @@ class Config:
HIGH_PRIO_STRUCTURE_PATH: str
EJS_DEBUG: bool
EJS_CACHE_LIMIT: int | None
EJS_DISABLE_AUTO_UNLOAD: bool
EJS_DISABLE_BATCH_BOOTUP: bool
EJS_NETPLAY_ENABLED: bool
EJS_NETPLAY_ICE_SERVERS: list[NetplayICEServer]
EJS_SETTINGS: dict[str, EjsOption] # core_name -> EjsOption
@@ -228,6 +230,12 @@ class ConfigManager:
EJS_CACHE_LIMIT=pydash.get(
self._raw_config, "emulatorjs.cache_limit", None
),
EJS_DISABLE_AUTO_UNLOAD=pydash.get(
self._raw_config, "emulatorjs.disable_auto_unload", False
),
EJS_DISABLE_BATCH_BOOTUP=pydash.get(
self._raw_config, "emulatorjs.disable_batch_bootup", False
),
EJS_NETPLAY_ENABLED=pydash.get(
self._raw_config, "emulatorjs.netplay.enabled", False
),
@@ -419,6 +427,18 @@ class ConfigManager:
)
sys.exit(3)
if not isinstance(self.config.EJS_DISABLE_AUTO_UNLOAD, bool):
log.critical(
"Invalid config.yml: emulatorjs.disable_auto_unload must be a boolean"
)
sys.exit(3)
if not isinstance(self.config.EJS_DISABLE_BATCH_BOOTUP, bool):
log.critical(
"Invalid config.yml: emulatorjs.disable_batch_bootup must be a boolean"
)
sys.exit(3)
if not isinstance(self.config.EJS_NETPLAY_ICE_SERVERS, list):
log.critical(
"Invalid config.yml: emulatorjs.netplay.ice_servers must be a list"
@@ -533,6 +553,8 @@ class ConfigManager:
"emulatorjs": {
"debug": self.config.EJS_DEBUG,
"cache_limit": self.config.EJS_CACHE_LIMIT,
"disable_auto_unload": self.config.EJS_DISABLE_AUTO_UNLOAD,
"disable_batch_bootup": self.config.EJS_DISABLE_BATCH_BOOTUP,
"netplay": {
"enabled": self.config.EJS_NETPLAY_ENABLED,
"ice_servers": self.config.EJS_NETPLAY_ICE_SERVERS,

View File

@@ -37,6 +37,8 @@ def get_config() -> ConfigResponse:
SKIP_HASH_CALCULATION=cfg.SKIP_HASH_CALCULATION,
EJS_DEBUG=cfg.EJS_DEBUG,
EJS_CACHE_LIMIT=cfg.EJS_CACHE_LIMIT,
EJS_DISABLE_AUTO_UNLOAD=cfg.EJS_DISABLE_AUTO_UNLOAD,
EJS_DISABLE_BATCH_BOOTUP=cfg.EJS_DISABLE_BATCH_BOOTUP,
EJS_NETPLAY_ENABLED=cfg.EJS_NETPLAY_ENABLED,
EJS_NETPLAY_ICE_SERVERS=cfg.EJS_NETPLAY_ICE_SERVERS,
EJS_CONTROLS=cfg.EJS_CONTROLS,

View File

@@ -17,6 +17,8 @@ class ConfigResponse(TypedDict):
SKIP_HASH_CALCULATION: bool
EJS_DEBUG: bool
EJS_CACHE_LIMIT: int | None
EJS_DISABLE_AUTO_UNLOAD: bool
EJS_DISABLE_BATCH_BOOTUP: bool
EJS_NETPLAY_ENABLED: bool
EJS_NETPLAY_ICE_SERVERS: list[NetplayICEServer]
EJS_SETTINGS: dict[str, dict[str, str]]

View File

@@ -21,7 +21,11 @@ from handler.metadata.launchbox_handler import LaunchboxRom
from handler.metadata.moby_handler import MobyGamesRom
from handler.metadata.sgdb_handler import SGDBRom
from handler.metadata.ss_handler import SSRom
from handler.scan_handler import get_main_platform_igdb_id
from handler.scan_handler import (
MetadataSource,
get_main_platform_igdb_id,
get_priority_ordered_metadata_sources,
)
from logger.formatter import BLUE, CYAN
from logger.formatter import highlight as hl
from logger.logger import log
@@ -79,7 +83,6 @@ async def search_rom(
log.info(
f"{emoji.EMOJI_MAGNIFYING_GLASS_TILTED_RIGHT} Searching metadata providers..."
)
matched_roms: list = []
log.info(f"Searching by {hl(search_by.lower(), color=CYAN)}:")
log.info(
@@ -94,10 +97,11 @@ async def search_rom(
if search_by.lower() == "id":
try:
igdb_rom, moby_rom, ss_rom = await asyncio.gather(
igdb_rom, moby_rom, ss_rom, lb_rom = await asyncio.gather(
meta_igdb_handler.get_matched_rom_by_id(int(search_term)),
meta_moby_handler.get_matched_rom_by_id(int(search_term)),
meta_ss_handler.get_matched_rom_by_id(rom, int(search_term)),
meta_launchbox_handler.get_matched_rom_by_id(int(search_term)),
)
except ValueError as exc:
log.error(f"Search error: invalid ID '{search_term}'")
@@ -109,6 +113,7 @@ async def search_rom(
igdb_matched_roms = [igdb_rom] if igdb_rom else []
moby_matched_roms = [moby_rom] if moby_rom else []
ss_matched_roms = [ss_rom] if ss_rom else []
launchbox_matched_roms = [lb_rom] if lb_rom else []
elif search_by.lower() == "name":
(
igdb_matched_roms,
@@ -136,80 +141,56 @@ async def search_rom(
merged_dict: dict[str, dict] = {}
for igdb_rom in igdb_matched_roms:
if igdb_rom["igdb_id"]:
igdb_name = meta_igdb_handler.normalize_search_term(
igdb_rom.get("name", ""),
remove_articles=False,
)
merged_dict[igdb_name] = {
**igdb_rom,
"is_identified": True,
"is_unidentified": False,
"platform_id": rom.platform_id,
"igdb_url_cover": igdb_rom.pop("url_cover", ""),
**merged_dict.get(igdb_name, {}),
}
source_configs = {
MetadataSource.IGDB: (
igdb_matched_roms,
meta_igdb_handler,
"igdb_id",
"igdb_url_cover",
),
MetadataSource.MOBY: (
moby_matched_roms,
meta_moby_handler,
"moby_id",
"moby_url_cover",
),
MetadataSource.FLASHPOINT: (
flashpoint_matched_roms,
meta_flashpoint_handler,
"flashpoint_id",
"flashpoint_url_cover",
),
MetadataSource.LAUNCHBOX: (
launchbox_matched_roms,
meta_launchbox_handler,
"launchbox_id",
"launchbox_url_cover",
),
MetadataSource.SS: (ss_matched_roms, meta_ss_handler, "ss_id", "ss_url_cover"),
}
for moby_rom in moby_matched_roms:
if moby_rom["moby_id"]:
moby_name = meta_moby_handler.normalize_search_term(
moby_rom.get("name", ""),
remove_articles=False,
)
merged_dict[moby_name] = {
**moby_rom,
"is_identified": True,
"is_unidentified": False,
"platform_id": rom.platform_id,
"moby_url_cover": moby_rom.pop("url_cover", ""),
**merged_dict.get(moby_name, {}),
}
ordered_sources = get_priority_ordered_metadata_sources(
metadata_sources=list(source_configs.keys()), priority_type="metadata"
)
for ss_rom in ss_matched_roms:
if ss_rom["ss_id"]:
ss_name = meta_ss_handler.normalize_search_term(
ss_rom.get("name", ""),
remove_articles=False,
)
merged_dict[ss_name] = {
**ss_rom,
"is_identified": True,
"is_unidentified": False,
"platform_id": rom.platform_id,
"ss_url_cover": ss_rom.pop("url_cover", ""),
**merged_dict.get(ss_name, {}),
}
for flashpoint_rom in flashpoint_matched_roms:
if flashpoint_rom["flashpoint_id"]:
flashpoint_name = meta_flashpoint_handler.normalize_search_term(
flashpoint_rom.get("name", ""),
remove_articles=False,
)
merged_dict[flashpoint_name] = {
**flashpoint_rom,
"is_identified": True,
"is_unidentified": False,
"platform_id": rom.platform_id,
"flashpoint_url_cover": flashpoint_rom.pop("url_cover", ""),
**merged_dict.get(flashpoint_name, {}),
}
for launchbox_rom in launchbox_matched_roms:
if launchbox_rom["launchbox_id"]:
launchbox_name = meta_launchbox_handler.normalize_search_term(
launchbox_rom.get("name", ""),
remove_articles=False,
)
merged_dict[launchbox_name] = {
**launchbox_rom,
"is_identified": True,
"is_unidentified": False,
"platform_id": rom.platform_id,
"launchbox_url_cover": launchbox_rom.pop("url_cover", ""),
**merged_dict.get(launchbox_name, {}),
}
for meta_source in ordered_sources:
source_matched_roms, meta_handler, id_key, cover_key = source_configs[
meta_source
]
for source_rom in source_matched_roms: # trunk-ignore(mypy/attr-defined)
if source_rom[id_key]:
normalized_name = meta_handler.normalize_search_term(
source_rom.get("name", ""),
remove_articles=False,
)
merged_dict[normalized_name] = {
**source_rom,
"is_identified": True,
"is_unidentified": False,
"platform_id": rom.platform_id,
cover_key: source_rom.get("url_cover", ""),
**merged_dict.get(normalized_name, {}),
}
async def get_sgdb_rom(name: str) -> tuple[str, SGDBRom]:
return name, await meta_sgdb_handler.get_details_by_names([name])
@@ -226,7 +207,7 @@ async def search_rom(
"sgdb_url_cover": sgdb_rom.get("url_cover", ""),
}
matched_roms = list(merged_dict.values())
matched_roms: list = list(merged_dict.values())
log.info("Results:")
for m_rom in matched_roms:

View File

@@ -230,7 +230,9 @@ class LaunchboxHandler(MetadataHandler):
name=platform["name"],
)
async def get_rom(self, fs_name: str, platform_slug: str) -> LaunchboxRom:
async def get_rom(
self, fs_name: str, platform_slug: str, keep_tags: bool = False
) -> LaunchboxRom:
from handler.filesystem import fs_rom_handler
fallback_rom = LaunchboxRom(launchbox_id=None)
@@ -253,10 +255,19 @@ class LaunchboxHandler(MetadataHandler):
f"LaunchBox ID {launchbox_id_from_tag} from filename tag not found in LaunchBox"
)
# We replace " - " with ": " to match Launchbox's naming convention
search_term = fs_rom_handler.get_file_name_with_no_tags(fs_name).replace(
" - ", ": "
)
# `keep_tags` prevents stripping content that is considered a tag, e.g., anything between `()` or `[]`.
# By default, tags are still stripped to keep scan behavior consistent with previous versions.
# If `keep_tags` is True, the full `fs_name` is used for searching.
if not keep_tags:
# We replace " - " with ": " to match Launchbox's naming convention
search_term = fs_rom_handler.get_file_name_with_no_tags(fs_name).replace(
" - ", ": "
)
else:
search_term = fs_name
search_term = search_term.lower()
index_entry = await self._get_rom_from_metadata(search_term, platform_slug)
if not index_entry:
@@ -329,7 +340,7 @@ class LaunchboxHandler(MetadataHandler):
if not self.is_enabled():
return []
rom = await self.get_rom(search_term, platform_slug)
rom = await self.get_rom(search_term, platform_slug, True)
return [rom] if rom else []

View File

@@ -126,7 +126,7 @@ class UpdateLaunchboxMetadataTask(RemoteFilePullTask):
await pipe.hset(
LAUNCHBOX_METADATA_NAME_KEY,
mapping={
f"{name_elem.text}:{platform_elem.text}": json.dumps(
f"{name_elem.text.lower()}:{platform_elem.text}": json.dumps(
{
child.tag: child.text
for child in elem
@@ -145,7 +145,7 @@ class UpdateLaunchboxMetadataTask(RemoteFilePullTask):
await pipe.hset(
LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY,
mapping={
alternate_name_elem.text: json.dumps(
alternate_name_elem.text.lower(): json.dumps(
{
child.tag: child.text
for child in elem

View File

@@ -55,6 +55,8 @@ scan:
emulatorjs:
debug: true
cache_limit: 1000
disable_auto_unload: true
disable_batch_bootup: true
netplay:
enabled: true
ice_servers:

View File

@@ -21,6 +21,8 @@ def test_config_loader():
assert loader.config.FIRMWARE_FOLDER_NAME == "BIOS"
assert loader.config.SKIP_HASH_CALCULATION
assert loader.config.EJS_DEBUG
assert loader.config.EJS_DISABLE_AUTO_UNLOAD
assert loader.config.EJS_DISABLE_BATCH_BOOTUP
assert loader.config.EJS_CACHE_LIMIT == 1000
assert loader.config.EJS_NETPLAY_ENABLED
assert loader.config.EJS_NETPLAY_ICE_SERVERS == [
@@ -69,6 +71,8 @@ def test_empty_config_loader():
assert not loader.config.SKIP_HASH_CALCULATION
assert not loader.config.EJS_DEBUG
assert loader.config.EJS_CACHE_LIMIT is None
assert not loader.config.EJS_DISABLE_AUTO_UNLOAD
assert not loader.config.EJS_DISABLE_BATCH_BOOTUP
assert not loader.config.EJS_NETPLAY_ENABLED
assert loader.config.EJS_NETPLAY_ICE_SERVERS == []
assert loader.config.EJS_SETTINGS == {}

View File

@@ -137,6 +137,8 @@
# emulatorjs:
# debug: true # Available options will be logged to the browser console
# cache_limit: null # Cache limit per ROM (in bytes)
# disable_batch_bootup: false
# disable_auto_unload: false
# settings:
# parallel_n64: # Use the exact core name
# vsync: disabled

View File

@@ -18,6 +18,8 @@ export type ConfigResponse = {
SKIP_HASH_CALCULATION: boolean;
EJS_DEBUG: boolean;
EJS_CACHE_LIMIT: (number | null);
EJS_DISABLE_AUTO_UNLOAD: boolean;
EJS_DISABLE_BATCH_BOOTUP: boolean;
EJS_NETPLAY_ENABLED: boolean;
EJS_NETPLAY_ICE_SERVERS: Array<NetplayICEServer>;
EJS_SETTINGS: Record<string, Record<string, string>>;

View File

@@ -40,7 +40,7 @@ import type { SimpleRom } from "@/stores/roms";
const { t } = useI18n();
const router = useRouter();
const platformsStore = storePlatforms();
const { allPlatforms, fetchingPlatforms } = storeToRefs(platformsStore);
const { filledPlatforms, fetchingPlatforms } = storeToRefs(platformsStore);
const collectionsStore = storeCollections();
const { allCollections, smartCollections, virtualCollections } =
storeToRefs(collectionsStore);
@@ -105,8 +105,8 @@ const virtualCollectionElementAt = (i: number) =>
// Spatial navigation
const { moveLeft: moveSystemLeft, moveRight: moveSystemRight } = useSpatialNav(
platformIndex,
() => allPlatforms.value.length || 1,
() => allPlatforms.value.length,
() => filledPlatforms.value.length || 1,
() => filledPlatforms.value.length,
);
const {
moveLeft: moveContinuePlayingLeft,
@@ -223,7 +223,7 @@ const navigationFunctions = {
const before = platformIndex.value;
moveSystemLeft();
if (platformIndex.value === before) {
platformIndex.value = Math.max(0, allPlatforms.value.length - 1);
platformIndex.value = Math.max(0, filledPlatforms.value.length - 1);
}
},
next: () => {
@@ -234,10 +234,10 @@ const navigationFunctions = {
}
},
confirm: () => {
if (!allPlatforms.value[platformIndex.value]) return false;
if (!filledPlatforms.value[platformIndex.value]) return false;
router.push({
name: ROUTES.CONSOLE_PLATFORM,
params: { id: allPlatforms.value[platformIndex.value].id },
params: { id: filledPlatforms.value[platformIndex.value].id },
});
return true;
},
@@ -649,7 +649,8 @@ onBeforeMount(async () => {
onMounted(async () => {
// Restore indices within bounds
if (platformIndex.value >= allPlatforms.value.length) platformIndex.value = 0;
if (platformIndex.value >= filledPlatforms.value.length)
platformIndex.value = 0;
if (continuePlayingIndex.value >= continuePlayingRoms.value.length)
continuePlayingIndex.value = 0;
if (collectionsIndex.value >= allCollections.value.length)
@@ -770,7 +771,7 @@ onUnmounted(() => {
>
<div class="flex items-center gap-6 h-full px-12 min-w-max">
<SystemCard
v-for="(p, i) in allPlatforms"
v-for="(p, i) in filledPlatforms"
:key="p.id"
:platform="p"
:index="i"

View File

@@ -435,9 +435,16 @@ async function boot() {
window.EJS_language = selectedLanguage.value.value.replace("_", "-");
window.EJS_disableAutoLang = true;
const { EJS_DEBUG, EJS_CACHE_LIMIT } = configStore.config;
if (EJS_CACHE_LIMIT !== null) window.EJS_CacheLimit = EJS_CACHE_LIMIT;
const {
EJS_DEBUG,
EJS_CACHE_LIMIT,
EJS_DISABLE_AUTO_UNLOAD,
EJS_DISABLE_BATCH_BOOTUP,
} = configStore.config;
window.EJS_DEBUG_XX = EJS_DEBUG;
window.EJS_disableAutoUnload = EJS_DISABLE_AUTO_UNLOAD;
window.EJS_disableBatchBootup = EJS_DISABLE_BATCH_BOOTUP;
if (EJS_CACHE_LIMIT !== null) window.EJS_CacheLimit = EJS_CACHE_LIMIT;
// Set a valid game name (affects per-game settings keys)
window.EJS_gameName = rom.fs_name_no_tags

View File

@@ -26,6 +26,8 @@ const defaultConfig = {
EJS_DEBUG: false,
EJS_NETPLAY_ENABLED: false,
EJS_CACHE_LIMIT: null,
EJS_DISABLE_AUTO_UNLOAD: false,
EJS_DISABLE_BATCH_BOOTUP: false,
EJS_NETPLAY_ICE_SERVERS: [],
EJS_SETTINGS: {},
EJS_CONTROLS: {},

View File

@@ -68,6 +68,7 @@ declare global {
EJS_gameName: string;
EJS_backgroundImage: string;
EJS_backgroundColor: string;
EJS_backgroundBlur: boolean;
EJS_gameUrl: string;
EJS_loadStateURL: string | null;
EJS_cheats: string;
@@ -89,6 +90,21 @@ declare global {
EJS_CacheLimit: number;
EJS_Buttons: Record<string, boolean>;
EJS_VirtualGamepadSettings: Record<string, unknown>;
EJS_volume: number;
EJS_paths: Record<string, string>;
EJS_startButtonName: string;
EJS_softLoad: boolean;
EJS_screenCapture: object;
EJS_externalFiles: Record<string, string>;
EJS_videoRotation: number;
EJS_fixedSaveInterval: number;
EJS_disableCue: boolean;
EJS_dontExtractRom: boolean;
EJS_dontExtractBIOS: boolean;
EJS_disableDatabases: boolean;
EJS_disableLocalStorage: boolean;
EJS_disableAutoUnload: boolean;
EJS_disableBatchBootup: boolean;
EJS_onGameStart: () => void;
EJS_onSaveState: (args: {
screenshot: ArrayBuffer;
@@ -147,6 +163,8 @@ window.EJS_disableAutoLang = true;
const {
EJS_DEBUG,
EJS_CACHE_LIMIT,
EJS_DISABLE_AUTO_UNLOAD,
EJS_DISABLE_BATCH_BOOTUP,
EJS_NETPLAY_ICE_SERVERS,
EJS_NETPLAY_ENABLED,
} = configStore.config;
@@ -154,8 +172,10 @@ window.EJS_netplayServer = EJS_NETPLAY_ENABLED ? window.location.host : "";
window.EJS_netplayICEServers = EJS_NETPLAY_ENABLED
? EJS_NETPLAY_ICE_SERVERS
: [];
if (EJS_CACHE_LIMIT !== null) window.EJS_CacheLimit = EJS_CACHE_LIMIT;
window.EJS_DEBUG_XX = EJS_DEBUG;
window.EJS_disableAutoUnload = EJS_DISABLE_AUTO_UNLOAD;
window.EJS_disableBatchBootup = EJS_DISABLE_BATCH_BOOTUP;
if (EJS_CACHE_LIMIT !== null) window.EJS_CacheLimit = EJS_CACHE_LIMIT;
onMounted(() => {
window.scrollTo(0, 0);