mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-27 22:27:21 -05:00
fix: link file cards to the entry that reads them
A directory entry was joined to the collection by its own name, so the 11,976 files under alephone's Plugins/ and the cdogs, thextech and easyrpg trees named no emulator on their card. FileConsumers now expands the directory and records which entry reads each file. The emulator links on a file card land on that entry, and a directory card names its copies and the folder that holds them.
This commit is contained in:
1 parent
90e54dcb43
commit
0145e6bfc8
3 files changed
+158
-5
No files matched your search
@@ -27,6 +27,7 @@ from common import (
|
||||
ProfileSelectionError,
|
||||
_norm_system_id,
|
||||
directory_members,
|
||||
expand_directory_entries,
|
||||
get_mame_clone_map,
|
||||
list_emulator_profiles,
|
||||
list_registered_platforms,
|
||||
@@ -341,6 +342,10 @@ class FileConsumers:
|
||||
# Where each platform puts the file in its BIOS directory.
|
||||
self.platform_destinations: dict[str, dict[str, set[str]]] = {}
|
||||
self.emulators: dict[str, set[str]] = {}
|
||||
# The entry that reads each file, by its position in the profile's
|
||||
# files: a link lands on that entry's card, not the page top.
|
||||
self.emulator_entries: dict[str, dict[str, int]] = {}
|
||||
self.emulator_files: dict[str, list] = {}
|
||||
# Where each platform's pack installs the file, its cores' files
|
||||
# included: the platform's own list names a fraction of them.
|
||||
self.installed: dict[str, dict[str, set[str]]] = {}
|
||||
@@ -401,14 +406,49 @@ class FileConsumers:
|
||||
"""The collection path the database records for a file."""
|
||||
return str(self._db.get("files", {}).get(sha1, {}).get("path", ""))
|
||||
|
||||
def directory_members(self, entry: dict) -> dict[str, str]:
|
||||
"""{destination: sha1} of the files a ``type: directory`` entry reads."""
|
||||
if entry.get("type") != "directory":
|
||||
return {}
|
||||
return {
|
||||
member["path"]: member["sha1"]
|
||||
for member in expand_directory_entries([entry], self._db)
|
||||
if member is not entry
|
||||
}
|
||||
|
||||
def add_emulators(self, profiles: dict[str, dict]) -> None:
|
||||
"""Each file a profile entry resolves to, a directory's members included.
|
||||
|
||||
A directory entry was resolved by its own name and found nothing:
|
||||
11,976 files under alephone's Plugins/, cdogs' and thextech's trees
|
||||
named no emulator on their card.
|
||||
"""
|
||||
for name, profile in profiles.items():
|
||||
if profile.get("type") == "alias":
|
||||
continue
|
||||
for entry in profile.get("files", []):
|
||||
sha1 = self.sha1_of(entry, entry.get("path") or "", owner=name)
|
||||
if sha1:
|
||||
self.emulators.setdefault(sha1, set()).add(name)
|
||||
files = profile.get("files") or []
|
||||
self.emulator_files[name] = files
|
||||
# A file a profile names keeps its own entry over the directory
|
||||
# that also holds it.
|
||||
order = sorted(
|
||||
(i for i, entry in enumerate(files) if isinstance(entry, dict)),
|
||||
key=lambda i: files[i].get("type") == "directory",
|
||||
)
|
||||
for index in order:
|
||||
entry = files[index]
|
||||
members = self.directory_members(entry)
|
||||
if members:
|
||||
found = [
|
||||
self.sha1_of({**entry, "name": dest, "path": dest, "sha1": sha1},
|
||||
dest, owner=name)
|
||||
for dest, sha1 in members.items()
|
||||
]
|
||||
else:
|
||||
found = [self.sha1_of(entry, entry.get("path") or "", owner=name)]
|
||||
for sha1 in found:
|
||||
if sha1:
|
||||
self.emulators.setdefault(sha1, set()).add(name)
|
||||
self.emulator_entries.setdefault(sha1, {}).setdefault(name, index)
|
||||
|
||||
|
||||
def entry_source(f: dict, index: dict) -> str | None:
|
||||
|
||||
@@ -2415,6 +2415,36 @@ def _platform_links(sha1: str, consumers: FileConsumers) -> list[str]:
|
||||
return links
|
||||
|
||||
|
||||
_card_anchors: dict[str, tuple[list, list[str]]] = {}
|
||||
|
||||
|
||||
def _emulator_card_anchor(emulator: str, files: list, index: int) -> str:
|
||||
"""The id of a profile entry's card on its emulator page."""
|
||||
cached = _card_anchors.get(emulator)
|
||||
if cached is None or cached[0] is not files:
|
||||
cached = _card_anchors[emulator] = (files, emulator_file_anchors(files))
|
||||
return cached[1][index] if 0 <= index < len(cached[1]) else ""
|
||||
|
||||
|
||||
def _emulator_links(sha1: str, consumers: FileConsumers) -> list[str]:
|
||||
"""Each emulator reading the file, linked to the entry that reads it.
|
||||
|
||||
The platform links landed on their system section, and the emulator
|
||||
links on the page top: duckstation's scph5501.bin entry, with the line
|
||||
that accepts it, sat 260 lines down a page of 105 entries.
|
||||
"""
|
||||
entries = getattr(consumers, "emulator_entries", {}).get(sha1, {})
|
||||
files_of = getattr(consumers, "emulator_files", {})
|
||||
links = []
|
||||
for emulator in sorted(consumers.emulators.get(sha1, ())):
|
||||
index = entries.get(emulator)
|
||||
anchor = (_emulator_card_anchor(emulator, files_of[emulator], index)
|
||||
if index is not None and emulator in files_of else "")
|
||||
fragment = f"#{anchor}" if anchor else ""
|
||||
links.append(f"[{emulator}](../emulators/{emulator}.md{fragment})")
|
||||
return links
|
||||
|
||||
|
||||
def _installed_links(sha1: str, consumers: FileConsumers, declared: set[str]) -> list[str]:
|
||||
"""Each pack installing the file that its platform's list does not name,
|
||||
with the place the pack gives it."""
|
||||
@@ -2482,7 +2512,7 @@ def _system_file_card(
|
||||
# pack installs it for six cores.
|
||||
lines.append(f"- Also installed by: {', '.join(installed)}")
|
||||
if emus:
|
||||
lines.append(f"- Emulators: {', '.join(_emulator_link(e, '../') for e in emus)}")
|
||||
lines.append(f"- Emulators: {', '.join(_emulator_links(sha1_full, consumers))}")
|
||||
provenance = f.get("provenance", {})
|
||||
if provenance:
|
||||
lines.extend(_provenance_lines(provenance, "../"))
|
||||
@@ -2876,6 +2906,8 @@ def _render_emulator_file(
|
||||
details.append(
|
||||
f"Source: {_source_ref_markdown(profile, source_ref)}"
|
||||
)
|
||||
if consumers and f.get("type") == "directory":
|
||||
details.extend(_directory_copies(f, consumers))
|
||||
if consumers:
|
||||
sha1 = consumers.sha1_of(f, f.get("path") or "", owner=owner)
|
||||
if sha1 and in_repo:
|
||||
@@ -2945,6 +2977,27 @@ def _render_emulator_file(
|
||||
return lines
|
||||
|
||||
|
||||
def _directory_copies(entry: dict, consumers: FileConsumers) -> list[str]:
|
||||
"""What the collection holds under a directory entry, and where.
|
||||
|
||||
The card said "in repo" and linked none of the files: alephone's
|
||||
Plugins/ holds 393 of them, each with its own card on a system page.
|
||||
"""
|
||||
members = consumers.directory_members(entry)
|
||||
held = sorted({consumers.path_of(sha1) for sha1 in members.values()} - {""})
|
||||
if not held:
|
||||
return []
|
||||
folder = os.path.commonpath(held) if len(held) > 1 else os.path.dirname(held[0])
|
||||
parts = folder.split("/")
|
||||
where = _code(folder + "/")
|
||||
if len(parts) >= 3 and parts[0] == "bios":
|
||||
page = urllib.parse.quote(parts[1].lower().replace(" ", "-"))
|
||||
where = f"[{where}](../systems/{page}.md#{_slugify_anchor(parts[2])})"
|
||||
tree = f"{REPO_URL}/tree/main/{urllib.parse.quote(folder)}"
|
||||
return [f"Collection copies: {_count(len(members), 'file')} under {where} "
|
||||
f"([browse]({tree}))"]
|
||||
|
||||
|
||||
def _render_metadata_rows(profile: dict) -> list[str]:
|
||||
"""Scalar profile fields, one table row each.
|
||||
|
||||
|
||||
@@ -181,5 +181,65 @@ class StructuredBlocks(unittest.TestCase):
|
||||
self.assertIn("end.", page)
|
||||
|
||||
|
||||
class FileCardsLinkTheReadingEntry(unittest.TestCase):
|
||||
def test_an_emulator_link_lands_on_its_entry(self):
|
||||
"""The platform links landed on their section, the emulator links on
|
||||
the top of a page of a hundred entries."""
|
||||
import generate_site
|
||||
from cross_reference import FileConsumers
|
||||
|
||||
consumers = FileConsumers({"files": {}})
|
||||
files = [{"name": "scph1001.bin"}, {"name": "scph5501.bin"},
|
||||
{"name": "tmss.md", "sha1": "d0fd0c16" + "0" * 32},
|
||||
{"name": "tmss.md", "sha1": "aa" * 20}]
|
||||
consumers.emulators = {"s": {"duckstation", "other"}, "t": {"blastem"}}
|
||||
consumers.emulator_entries = {"s": {"duckstation": 1}, "t": {"blastem": 2}}
|
||||
consumers.emulator_files = {"duckstation": files, "blastem": files}
|
||||
self.assertEqual(generate_site._emulator_links("s", consumers), [
|
||||
"[duckstation](../emulators/duckstation.md#file-scph5501-bin)",
|
||||
"[other](../emulators/other.md)",
|
||||
])
|
||||
self.assertEqual(generate_site._emulator_links("t", consumers),
|
||||
["[blastem](../emulators/blastem.md#file-tmss-md-d0fd0c16)"])
|
||||
|
||||
|
||||
class DirectoryMembersNameTheirEmulator(unittest.TestCase):
|
||||
"""The site resolved a ``type: directory`` entry by its own name: 11,976
|
||||
files under alephone's Plugins/, cdogs' and thextech's trees had no
|
||||
emulator on their card, and the directory card linked none of them."""
|
||||
|
||||
SHA1 = "c63078a6c9ccaca7339a726a5d39410983933070"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
from common import build_zip_contents_index, load_database, load_emulator_profiles
|
||||
from cross_reference import FileConsumers
|
||||
|
||||
cls.db = load_database(str(REPO_ROOT / "database.json"))
|
||||
if cls.SHA1 not in cls.db.get("files", {}):
|
||||
raise unittest.SkipTest("the collection does not hold the alephone plugin")
|
||||
cls.profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["alephone"]
|
||||
cls.consumers = FileConsumers(cls.db, build_zip_contents_index(cls.db))
|
||||
cls.consumers.add_emulators({"alephone": cls.profile})
|
||||
|
||||
def test_a_member_names_the_emulator_and_its_directory_entry(self):
|
||||
import generate_site
|
||||
|
||||
files = self.profile["files"]
|
||||
index = self.consumers.emulator_entries[self.SHA1]["alephone"]
|
||||
self.assertEqual(files[index].get("type"), "directory")
|
||||
self.assertEqual(generate_site._emulator_links(self.SHA1, self.consumers),
|
||||
["[alephone](../emulators/alephone.md#file-marathon-plugins)"])
|
||||
|
||||
def test_the_directory_card_points_at_its_copies(self):
|
||||
import generate_site
|
||||
|
||||
entry = next(f for f in self.profile["files"]
|
||||
if f.get("type") == "directory" and f.get("path") == "Marathon/Plugins/")
|
||||
line = generate_site._directory_copies(entry, self.consumers)[0]
|
||||
self.assertRegex(line, r"^Collection copies: \d+ files under "
|
||||
r"\[`bios/Bungie/Marathon/Plugins/`\]\(\.\./systems/bungie\.md#")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user