From 79a21ba1f1e9a70ea47467f225b8e638272b0a7c Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 09:28:03 +0100 Subject: [PATCH] feat: name the files in emulator search snippets A search for gba_bios.bin or scph5500.bin landed on a page described as "It reads 4 files, none of them required". The snippet now names the console and the files, required first, in the longest form that fits. --- scripts/generate_site.py | 64 ++++++++++++++++++++++++++++++++++++++- tests/test_site_search.py | 28 +++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 32f926da0..ab1cb8dce 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2591,8 +2591,10 @@ def _emulator_description(markdown: str, key: str, title: str, intro: str) -> st intro = f"{title}: {intro[:1].lower()}{intro[1:]}" if key in NOTHING_TO_FETCH: return _shorten(f"{intro} Every file it reads comes with the app.", 155) + if PAGE_DATA.get(key, {}).get("hasPart"): + return _shorten(_files_snippet(markdown, key, title, intro), 155) # An alias page sends the reader to the core whose files it shares. - if PAGE_DATA.get(key, {}).get("hasPart") or "uses the same firmware as" in intro: + if "uses the same firmware as" in intro: return _shorten(intro, 155) needs = "It needs no BIOS or firmware file." if "\n## Data directories" in markdown: @@ -2600,6 +2602,44 @@ def _emulator_description(markdown: str, key: str, title: str, intro: str) -> st return _shorten(f"{intro} {needs}", 155) +def _files_snippet(markdown: str, key: str, title: str, intro: str) -> str: + """The snippet of an emulator page with files to get. + + A search for "gba_bios.bin" or "scph5500.bin" landed on a page described + as "It reads 4 files, none of them required": the names the reader typed + were not in the result, and fewer than 1 in 70 clicked. The longest + form that fits is kept: a snippet cut by "..." reads as unfinished. + """ + if " any region." in intro: + step = ("Download one from this page and put it in the folder it shows." + if re.search(r"^1\. Download ", _lead(markdown), re.M) + else "Where to get one and the folder it goes in.") + head = f"{intro.split(' any region.')[0]} any region." + forms = [f"{head} {step} Any file name works.", f"{head} {step}"] + return next((form for form in forms if len(form) <= 155), intro) + snippet = EMULATOR_SNIPPETS.get(key) + if not snippet or not snippet[1] or "It reads " not in intro: + return intro + system, names, required, noun = snippet + if required == 0: + need = "none required" + elif required == len(names): + need = "all required" if len(names) > 1 else "required" + else: + need = f"{required:,} required" + subject = f"{system} {noun} for {title}" if system else f"{title} {noun}" + tails = ["Where each goes, its hashes and one command that installs them." + if "Install this emulator's files" in markdown else "", + "Where each goes and its hashes.", "Where each goes."] + forms = [] + for count in range(min(len(names), 4), 0, -1): + shown = (names if count == len(names) + else [*names[:count - 1], f"{len(names) - count + 1:,} more"]) + head = f"{subject}: {_said_as_list(shown)}, {need}." + forms += [f"{head} {tail}" for tail in tails if tail] + [head] + return next((form for form in forms if len(form) <= 155), intro) + + # Lines a page opens with that say where it sits, not what it holds. _PLACE_LINES = ("Part of ", "This part holds ", "On a card, ") @@ -2664,6 +2704,9 @@ PAGE_DATA: dict[str, dict] = {} # Emulator pages whose every file comes with the app: no "BIOS files" in # their title, and their description says so (DaedalusX64, ZEsarUX). NOTHING_TO_FETCH: set[str] = set() +# The first system, the file names, the count of required names and the +# word for them ("BIOS" or "files") of each emulator page, for its snippet. +EMULATOR_SNIPPETS: dict[str, tuple[str, list[str], int, str]] = {} # Pages that hold part of a page too long for one (a console of a split # manufacturer, a run of an emulator's cards), by path relative to docs/: # their H1 already says what they are. @@ -2996,6 +3039,25 @@ def record_emulator_page(name: str, profile: dict) -> None: key = f"emulators/{name}.md" PAGE_DATA[key] = emulator_structured_data(profile) listed = [f for f in profile.get("files") or [] if isinstance(f, dict)] + fetched = [f for f in listed if f.get("name") and not comes_with_app(f)] + systems = profile.get("systems") or [] + first = systems[0] if systems else "" + # The first system's files first: "dreamcast bios" found Flycast's + # page, whose required NAOMI archives came before dc_boot.bin. + names: dict[str, str] = {} + for f in sorted(fetched, key=lambda f: (f.get("system", first) != first, + f.get("required") is not True)): + base = Path(str(f["name"])).name + names.setdefault(base.casefold(), base) + firmware = [f for f in fetched if f.get("category", "bios") in ("bios", "bios_zip")] + EMULATOR_SNIPPETS[key] = ( + # One console names an emulator of five systems, not BizHawk of 40. + system_name(first) if 0 < len(systems) <= 5 else "", + list(names.values()), + len({Path(str(f["name"])).name.casefold() for f in fetched if f.get("required") is True}), + # Ship of Harkinian reads game ROMs: they are no BIOS. + "BIOS" if len(firmware) * 2 >= len(fetched) else "files", + ) if listed and all(comes_with_app(f) for f in listed): NOTHING_TO_FETCH.add(key) else: diff --git a/tests/test_site_search.py b/tests/test_site_search.py index 051765332..f12b381e0 100644 --- a/tests/test_site_search.py +++ b/tests/test_site_search.py @@ -245,6 +245,34 @@ class Descriptions(unittest.TestCase): self.assertEqual(gs._page_description(body, key, "DaedalusX64"), "DaedalusX64 emulates Nintendo 64. Every file it reads comes with the app.") + def test_an_emulator_snippet_names_the_files_searched_for(self) -> None: + """"gba_bios.bin" and "dreamcast bios" landed on pages described as + "It reads 4 files, none of them required": fewer than 1 in 70 clicked.""" + saved, snippets = dict(gs.PAGE_DATA), dict(gs.EMULATOR_SNIPPETS) + self.addCleanup(lambda: (gs.PAGE_DATA.clear(), gs.PAGE_DATA.update(saved), + gs.EMULATOR_SNIPPETS.clear(), + gs.EMULATOR_SNIPPETS.update(snippets))) + with open(REPO_ROOT / "emulators" / "flycast.yml", encoding="utf-8") as handle: + gs.record_emulator_page("flycast", yaml.safe_load(handle)) + key = Path("emulators/flycast.md") + body = ("# Flycast\n\nFlycast emulates Sega Dreamcast. It reads 10 files, 4 of them " + "required: without a required file it does not start games.\n") + text = gs._page_description(body, key, "Flycast") + self.assertTrue(text.startswith("Sega Dreamcast BIOS for Flycast: dc_boot.bin, "), text) + self.assertLessEqual(len(text), 155) + self.assertFalse(text.endswith("..."), text) + + def test_an_any_name_snippet_says_what_to_do(self) -> None: + saved = dict(gs.PAGE_DATA) + self.addCleanup(lambda: (gs.PAGE_DATA.clear(), gs.PAGE_DATA.update(saved))) + gs.PAGE_DATA["emulators/aethersx2.md"] = {"hasPart": [{"name": "ps2.bin"}]} + body = ("# AetherSX2\n\nAetherSX2 needs one Sony PlayStation 2 BIOS file, any region." + "\n\n1. Download [`ps2.bin`](../x/).\n2. Put it in this folder:\n") + self.assertEqual( + gs._page_description(body, Path("emulators/aethersx2.md"), "AetherSX2"), + "AetherSX2 needs one Sony PlayStation 2 BIOS file, any region. Download one " + "from this page and put it in the folder it shows. Any file name works.") + def test_an_initial_does_not_end_the_sentence(self) -> None: """hcl's description was "Hydra Castle Labyrinth: open-source port of E.".""" body = ("# Hydra Castle Labyrinth\n\n
\nTechnical notes\n\n"