From 94d18bd7e5f585dd1a354272c8e3d76c05a7f01e Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Fri, 23 Oct 2026 10:54:34 +0200
Subject: [PATCH] feat: name the system in each lookup record
---
schemas/site-api-envelope.schema.json | 4 +++
scripts/generate_site.py | 42 +++++++++++++++++++++++++--
tests/test_site_r3_reference.py | 27 +++++++++++++++++
3 files changed, 70 insertions(+), 3 deletions(-)
diff --git a/schemas/site-api-envelope.schema.json b/schemas/site-api-envelope.schema.json
index da48f568..5372333e 100644
--- a/schemas/site-api-envelope.schema.json
+++ b/schemas/site-api-envelope.schema.json
@@ -407,6 +407,10 @@
},
"owner": {
"type": "string"
+ },
+ "system_name": {
+ "type": "string",
+ "description": "The name the site gives the system the record belongs to (Sony PlayStation): the console a collection file is filed under, the system a platform lists it in, the system of an emulator's entry. Absent when none is known."
}
}
}
diff --git a/scripts/generate_site.py b/scripts/generate_site.py
index 4319a7f8..079387a7 100644
--- a/scripts/generate_site.py
+++ b/scripts/generate_site.py
@@ -1792,7 +1792,8 @@ def generate_data_page(exports: list[dict]) -> str:
"is the first two characters of the hash in lowercase, holds every SHA1, "
"MD5, CRC32 and SHA256 starting with them. Each record names its `layer` "
"(the collection, a data directory, a platform's list or an emulator "
- "profile), the file `name` and the `url` of the page that shows it. "
+ "profile), the file `name`, the `url` of the page that shows it and, "
+ "when one is known, the `system_name` the site gives its system. "
"[Find a file](files/index.md) reads it.",
"",
"Two kinds of `url`:",
@@ -3651,6 +3652,30 @@ def file_location_items(
LOOKUP_PREFIX = 2 # hex characters naming a lookup shard: 256 files
+def _lookup_system_name(sys_id: str) -> str | None:
+ """The system's name as every page prints it, None for an id the site
+ cannot name. The Find a file page reads it, and never builds one."""
+ try:
+ return system_name(sys_id)
+ except KeyError:
+ return None
+
+
+def _console_system_names(profiles: dict, coverages: dict) -> dict[tuple[str, str], str]:
+ """(manufacturer slug, console folder) -> the name of the system whose
+ own section that folder is: a collection file is named after the console
+ it is filed under. A folder no id owns, or that two names own, is left out.
+ """
+ from siterender import _own_section, system_ids
+
+ found: dict[tuple[str, str], set[str]] = {}
+ for sys_id in system_ids(profiles, [c["config"] for c in coverages.values()]):
+ own = _own_section(sys_id)
+ if own is not None:
+ found.setdefault((_slug(own[0]), own[1]), set()).add(system_name(sys_id))
+ return {key: next(iter(names)) for key, names in found.items() if len(names) == 1}
+
+
def file_lookup_records(
db: dict,
coverages: dict,
@@ -3666,6 +3691,7 @@ def file_lookup_records(
it made every visitor download: the lookup reads one shard instead.
"""
files = db.get("files", {})
+ named = _console_system_names(profiles, coverages)
member_names = {
str(info.get("sha1", "")): member.rsplit("/", 1)[-1]
for tree in (members or {}).values() for member, info in sorted(tree.items())
@@ -3680,28 +3706,37 @@ def file_lookup_records(
"name": member_names.get(sha1, ""), "sha1": sha1,
"url": item["url"]})
continue
+ parts = str(entry.get("path", "")).split("/")
+ held_by = named.get((_slug(parts[1]), parts[2])) if len(parts) > 3 else None
for algorithm in HASH_ALGORITHMS:
for value in _hash_values(entry.get(algorithm)):
records.append({
"value": value, "algorithm": algorithm, "layer": "collection",
"name": str(entry.get("name", "")), "sha1": sha1, "url": item["url"],
+ **({"system_name": held_by} if held_by else {}),
})
for key, coverage in sorted(coverages.items()):
for system_id, system in sorted((coverage["config"].get("systems") or {}).items()):
url = _site_path(f"platforms/{key}.md#{system_id.replace(' ', '-')}")
+ system_label = _lookup_system_name(system_id)
for entry in system.get("files") or []:
for algorithm in HASH_ALGORITHMS:
for value in _hash_values(entry.get(algorithm)):
records.append({
"value": value, "algorithm": algorithm, "layer": "platform",
"owner": key, "name": str(entry.get("name", "")), "url": url,
+ **({"system_name": system_label} if system_label else {}),
})
for key, profile in sorted(profiles.items()):
if profile.get("type") in ("alias", "test"):
continue
entries = profile.get("files") or []
+ own_systems = [str(s) for s in profile.get("systems") or []]
for entry, anchor in zip(entries, emulator_file_anchors(entries)):
url = _site_path(f"emulators/{emulator_card_page(key, anchor)}.md#{anchor}")
+ sys_id = str(entry.get("system") or "") or (
+ own_systems[0] if len(own_systems) == 1 else "")
+ entry_label = _lookup_system_name(sys_id) if sys_id else None
members = [(str(entry.get("name", "")), entry)] + [
(str(member["name"]), member) for member in entry.get("contents") or []
if isinstance(member, dict) and member.get("name")
@@ -3712,6 +3747,7 @@ def file_lookup_records(
records.append({
"value": value, "algorithm": algorithm, "layer": "emulator",
"owner": key, "name": name, "url": url,
+ **({"system_name": entry_label} if entry_label else {}),
})
unique = {json.dumps(r, sort_keys=True): r for r in records}
return sorted(unique.values(), key=lambda r: (
@@ -7109,7 +7145,7 @@ def generate_gap_analysis(
)
req = "yes" if m["required"] else "no"
algorithm, _, value = m["identity"].partition(":")
- expected = f"{algorithm} {_code(value)}" if value else "-"
+ expected = f"{algorithm} {short_hash(value)}" if value else "-"
destination = _code(m["path"]) if m["path"] != m["name"] else "-"
emulators = ", ".join(
f"[{_escape_tags(display)}](emulators/{key}.md)"
@@ -7167,7 +7203,7 @@ def generate_gap_analysis(
])
for u in sorted(all_unsourceable, key=lambda x: x["name"]):
lines.append(
- f"| `{u['name']}` | {u['emulator']} | {u['reason']} "
+ f"| `{u['name']}` | {u['emulator']} | {_reason_cell(u['reason'])} "
f"| {_source_ref_markdown(profiles[u['emu_key']], u['source_ref'])} |"
)
lines.append("")
diff --git a/tests/test_site_r3_reference.py b/tests/test_site_r3_reference.py
index fad13ffe..d631c7ba 100644
--- a/tests/test_site_r3_reference.py
+++ b/tests/test_site_r3_reference.py
@@ -336,6 +336,30 @@ class LookupUrlsResolve(unittest.TestCase):
shard = SITE + "api/v1/lookup/ab.json"
self.assertTrue(urllib.parse.urljoin(shard, item["url"]).startswith(SITE + "systems/"))
+ def test_each_record_names_its_system_as_the_pages_do(self):
+ """Find a file opens on "a BIOS file for Sony PlayStation": the name
+ comes from system_name, never built in the browser."""
+ from siterender import system_name # noqa: PLC0415
+
+ db = {"files": {"ab" * 20: {"path": "bios/Sony/PlayStation/scph5501.bin",
+ "name": "scph5501.bin", "md5": "490f666e1afb15b7362b406ed1cea246"}}}
+ coverages = {"batocera": {"platform": "Batocera", "config": {"systems": {
+ "sony-playstation": {"files": [{"name": "scph5501.bin", "md5": "490f666e1afb15b7362b406ed1cea246"}]}}}}}
+ profiles = {"duckstation": {"systems": ["sony-playstation"], "files": [
+ {"name": "scph5501.bin", "md5": "490f666e1afb15b7362b406ed1cea246"}]}}
+ records = generate_site.file_lookup_records(
+ db, coverages, profiles, generate_site.file_location_items(db))
+ expected = system_name("sony-playstation")
+ by_layer = {r["layer"]: r for r in records if r["algorithm"] == "md5"}
+ for layer in ("collection", "platform", "emulator"):
+ with self.subTest(layer=layer):
+ self.assertEqual(by_layer[layer]["system_name"], expected)
+
+ def test_built_shards_carry_the_name(self):
+ shard = json.loads(_built(API / "lookup" / "05.json").read_text())
+ record = next(r for r in shard["items"] if r["value"].startswith("0555c6fae890"))
+ self.assertEqual(record["system_name"], "Sony PlayStation")
+
def test_built_records_land_on_built_pages(self):
shard = json.loads(_built(API / "lookup" / "05.json").read_text())
dist = REPO_ROOT / "website" / "dist"
@@ -401,6 +425,9 @@ class TablesFitTheirColumn(unittest.TestCase):
row = next(line for line in table.splitlines() if "x.bin" in line)
self.assertIn(f'{"b" * 12}', row)
self.assertIn('expected md5 differs', row)
+ source = Path(generate_site.__file__).read_text(encoding="utf-8")
+ self.assertIn('expected = f"{algorithm} {short_hash(value)}"', source)
+ self.assertIn("{_reason_cell(u['reason'])}", source)
class SourceLinksGoWhereTheFileIs(unittest.TestCase):