mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-26 05:37:20 -05:00
feat: add knulli, amberelec and emuelec platforms
Each OS publishes Batocera's systems dict (knulli-systems, and batocera-systems at AmberELEC's newest prerelease commit and EmuELEC v4.8), read with the shared parser. Cores come from each OS's own ES list. AmberELEC and EmuELEC declare neogeo/ and ports/ beside bios/ from /storage/roms, so their packs extract there and core_destination puts core files under bios/.
This commit is contained in:
1 parent
2fc62bd249
commit
2bdac466a9
18 files changed
+12933
-24
No files matched your search
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scraper for AmberELEC's batocera-systems.
|
||||
|
||||
Source: packages/amberelec/sources/scripts/batocera-systems
|
||||
Format: Python source, Batocera's `systems` dict, parsed by batocera_scraper
|
||||
Hash: MD5
|
||||
|
||||
AmberELEC has published no release since 20230203. Its builds come from the
|
||||
dev branch as prereleases in AmberELEC/AmberELEC-prerelease, each naming the
|
||||
commit it was built from. The scraper reads the list at the commit of the
|
||||
newest one. That list accepts every md5 the 20230203 one does for the files
|
||||
it keeps (syscard3.pce 38179df8, BS-X.bin 96cf17bf) and adds
|
||||
neogeo/aes.zip.
|
||||
|
||||
Verification (batocera-systems at that commit):
|
||||
- checkBios (lines 583-643): md5sum() of the file at /storage/roms/<file>
|
||||
against the declared md5, an empty md5 meaning presence only. For a
|
||||
zippedFile entry, checkInsideZip() (lines 530-561) matches the member name
|
||||
with casefold() and hashes it.
|
||||
- Run with no argument by EmulationStation's BIOS screen
|
||||
(AmberELEC/emulationstation e3846751, es-app/src/ApiSystem.cpp:603-613),
|
||||
which also passes `--filter <system>`. ee_check_bios
|
||||
(packages/amberelec/profile.d/99-distribution.conf:320) passes
|
||||
`--strictfilter`, which the script's argument switch (lines 665-687) does
|
||||
not handle: that call prints nothing and exits 0.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from .base_scraper import BaseScraper, BiosRequirement, github_headers
|
||||
from .batocera_scraper import (
|
||||
SYSTEM_SLUG_MAP as BATOCERA_SLUGS,
|
||||
)
|
||||
from .batocera_scraper import (
|
||||
extract_systems_dict,
|
||||
platform_systems,
|
||||
requirements_from_systems,
|
||||
)
|
||||
from .es_emulators import es_cores, xml_emulators
|
||||
|
||||
PLATFORM_NAME = "amberelec"
|
||||
|
||||
REPO = "AmberELEC/AmberELEC"
|
||||
PRERELEASE_REPO = "AmberELEC/AmberELEC-prerelease"
|
||||
_RAW = "https://raw.githubusercontent.com/AmberELEC/AmberELEC/{ref}/"
|
||||
SOURCE_TEMPLATE = _RAW + "packages/amberelec/sources/scripts/batocera-systems"
|
||||
ES_SYSTEMS_TEMPLATE = _RAW + "packages/ui/emulationstation/config/es_systems.cfg"
|
||||
|
||||
# Batocera's names, plus the keys AmberELEC writes that Batocera does not,
|
||||
# filed under the slug the other scrapers already give them.
|
||||
SYSTEM_SLUG_MAP = {
|
||||
**BATOCERA_SLUGS,
|
||||
"atari7800": "atari-7800",
|
||||
"atarilynx": "atari-lynx",
|
||||
"n64": "nintendo-64",
|
||||
"o2em": "magnavox-odyssey2",
|
||||
}
|
||||
|
||||
# runemu.py: `retroarch` and `retrorun` load the libretro core es_systems.cfg
|
||||
# names. `mame` lists MAME machines and runs them on the libretro MAME core
|
||||
# (lines 233-261 set self.core = 'mame', line 355 builds the RetroArch command).
|
||||
RUNNERS: dict[str, str | None] = {"retroarch": None, "retrorun": None, "mame": "mame"}
|
||||
# runemu.py:113, PPSSPPSDL runs /usr/bin/ppsspp.sh, installed by the
|
||||
# ppssppsa package (packages/games/emulators/ppssppsa).
|
||||
RENAMED = {"PPSSPPSDL": "ppsspp"}
|
||||
|
||||
_PRERELEASE_COMMIT = re.compile(r"AmberELEC/AmberELEC@([0-9a-f]{40})")
|
||||
|
||||
|
||||
def prerelease_commit(body: str) -> str:
|
||||
"""The commit a prerelease's notes say it was built from."""
|
||||
found = _PRERELEASE_COMMIT.search(body or "")
|
||||
if found is None:
|
||||
raise RuntimeError("the newest AmberELEC prerelease names no commit")
|
||||
return found.group(1)
|
||||
|
||||
|
||||
def fetch_newest_prerelease() -> tuple[str, str]:
|
||||
"""(tag, commit) of the newest AmberELEC prerelease."""
|
||||
url = f"https://api.github.com/repos/{PRERELEASE_REPO}/releases?per_page=1"
|
||||
try:
|
||||
req = urllib.request.Request(url, headers=github_headers())
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
releases = json.loads(resp.read())
|
||||
except (urllib.error.URLError, json.JSONDecodeError) as e:
|
||||
raise RuntimeError(f"cannot list the releases of {PRERELEASE_REPO}: {e}") from e
|
||||
if not releases or not releases[0].get("tag_name"):
|
||||
raise RuntimeError(f"{PRERELEASE_REPO} lists no release")
|
||||
return releases[0]["tag_name"], prerelease_commit(releases[0].get("body", ""))
|
||||
|
||||
|
||||
class Scraper(BaseScraper):
|
||||
"""Scraper for AmberELEC's batocera-systems."""
|
||||
|
||||
def __init__(self, url: str | None = None) -> None:
|
||||
if url:
|
||||
self.tag, self.commit = "", ""
|
||||
else:
|
||||
self.tag, self.commit = fetch_newest_prerelease()
|
||||
super().__init__(url=url or SOURCE_TEMPLATE.format(ref=self.commit))
|
||||
|
||||
def _fetch_text(self, url: str) -> str:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "retrobios-scraper/1.0"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return resp.read().decode("utf-8")
|
||||
except urllib.error.URLError as e:
|
||||
raise ConnectionError(f"Failed to fetch {url}: {e}") from e
|
||||
|
||||
def validate_format(self, raw_data: str) -> bool:
|
||||
has_dict = re.search(r"^systems\s*=\s*\{", raw_data, re.MULTILINE) is not None
|
||||
return has_dict and "def checkBios" in raw_data and '"biosFiles"' in raw_data
|
||||
|
||||
def fetch_requirements(self) -> list[BiosRequirement]:
|
||||
raw = self._fetch_raw()
|
||||
if not self.validate_format(raw):
|
||||
raise ValueError("batocera-systems format validation failed")
|
||||
# Paths are relative to /storage/roms (`prefix`, lines 667-675): the
|
||||
# list names neogeo/neogeo.zip beside bios/.
|
||||
return requirements_from_systems(extract_systems_dict(raw), SYSTEM_SLUG_MAP, strip="")
|
||||
|
||||
def generate_platform_yaml(self) -> dict:
|
||||
systems = platform_systems(
|
||||
self.fetch_requirements(), extract_systems_dict(self._fetch_raw()), SYSTEM_SLUG_MAP
|
||||
)
|
||||
cores, standalone, dual = es_cores(
|
||||
xml_emulators(self._fetch_text(ES_SYSTEMS_TEMPLATE.format(ref=self.commit))),
|
||||
RUNNERS,
|
||||
RENAMED,
|
||||
)
|
||||
return {
|
||||
"platform": "AmberELEC",
|
||||
"version": self.tag,
|
||||
"homepage": "https://amberelec.org",
|
||||
"source": self.url,
|
||||
"base_destination": "",
|
||||
# RetroArch's system_directory is ~/roms/bios
|
||||
# (packages/games/tools/retroarch/sources/retroarch.cfg:611).
|
||||
"core_destination": "bios",
|
||||
"hash_type": "md5",
|
||||
"verification_mode": "md5",
|
||||
"cores": cores,
|
||||
"standalone_cores": standalone,
|
||||
"dual_mode_cores": dual,
|
||||
"systems": systems,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
from .base_scraper import scraper_cli
|
||||
|
||||
scraper_cli(Scraper, "Scrape AmberELEC BIOS requirements")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scraper for EmuELEC's batocera-systems.
|
||||
|
||||
Source: packages/sx05re/emuelec/bin/batocera/batocera-systems
|
||||
Format: Python source, Batocera's `systems` dict, parsed by batocera_scraper
|
||||
Hash: MD5
|
||||
|
||||
`naomi` and `Dinothawr` are written twice in the dict. Python keeps the later
|
||||
value of a repeated key when the script runs, and so does the parser: naomi
|
||||
is the entry at line 461 (naomi_boot.bin and naomi.zip, line 444 naming
|
||||
naomi.zip alone), Dinothawr the one at line 776 (dinothawr.game, line 494
|
||||
naming level_3-3.tmx).
|
||||
|
||||
Verification (batocera-systems at the release tag):
|
||||
- checkBios (lines 817-833): md5sum() of the file at /storage/roms/<file>
|
||||
against the declared md5, an empty md5 meaning presence only. It never
|
||||
reads zippedFile: the FM Towns entries that name a member hash the whole
|
||||
archive against the member's md5. There is no emulator or core gate.
|
||||
- Run with no argument by EmulationStation's BIOS screen
|
||||
(EmuELEC/emuelec-emulationstation 09356d0d, es-app/src/ApiSystem.cpp:
|
||||
645-655, `--filter <system>` for one system), and with `--strictfilter
|
||||
<platform>` by ee_check_bios after a failed launch
|
||||
(packages/sx05re/emuelec/profile.d/99-emuelec.conf:261, called from
|
||||
emuelecRunEmu.sh:605).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from .base_scraper import BaseScraper, BiosRequirement, fetch_github_latest_version
|
||||
from .batocera_scraper import (
|
||||
SYSTEM_SLUG_MAP as BATOCERA_SLUGS,
|
||||
)
|
||||
from .batocera_scraper import (
|
||||
extract_systems_dict,
|
||||
platform_systems,
|
||||
requirements_from_systems,
|
||||
)
|
||||
from .es_emulators import es_cores, json_emulators
|
||||
|
||||
PLATFORM_NAME = "emuelec"
|
||||
|
||||
REPO = "EmuELEC/EmuELEC"
|
||||
_RAW = "https://raw.githubusercontent.com/EmuELEC/EmuELEC/{ref}/"
|
||||
SOURCE_TEMPLATE = _RAW + "packages/sx05re/emuelec/bin/batocera/batocera-systems"
|
||||
ES_SYSTEMS_TEMPLATE = _RAW + "packages/sx05re/emuelec-emulationstation/config/es_systems.json"
|
||||
|
||||
# Batocera's names, plus the keys EmuELEC writes that Batocera does not:
|
||||
# console keys under the slug the other scrapers give them, and each port
|
||||
# under the system of the profile of the engine its name gives
|
||||
# (devilutionx, prboom, opentyrian, cannonball, nxengine, tyrquake,
|
||||
# reminiscence, xrick, vvvvvv, eduke32, sonic2013, sonic-mania, dinothawr).
|
||||
SYSTEM_SLUG_MAP = {
|
||||
**BATOCERA_SLUGS,
|
||||
"atari7800": "atari-7800",
|
||||
"atarilynx": "atari-lynx",
|
||||
"n64": "nintendo-64",
|
||||
"o2em": "magnavox-odyssey2",
|
||||
"Diablo": "diablo",
|
||||
"Hellfire": "diablo",
|
||||
"Dinothawr": "dinothawr",
|
||||
"Quake": "quake",
|
||||
"VVVVVV": "vvvvvv",
|
||||
"DOOM1": "doom",
|
||||
"DOOM2": "doom",
|
||||
"OpenTyrian": "tyrian",
|
||||
"Cannonball": "sega-outrun",
|
||||
"CaveStory": "cave-story",
|
||||
"REminiscence": "flashback",
|
||||
"RickDangerous": "xrick",
|
||||
"eduke32": "duke3d",
|
||||
"sonic1": "sonic-1-2013",
|
||||
"sonic2": "sonic-2-2013",
|
||||
"sonicmania": "sonic-mania",
|
||||
}
|
||||
|
||||
# es_systems.json names libretro cores under `libretro`, and `retrorun`
|
||||
# loads the libretro core it lists.
|
||||
RUNNERS: dict[str, str | None] = {"libretro": None, "retrorun": None}
|
||||
# packages/sx05re/emulators/PPSSPPSDL builds PPSSPP's SDL front end.
|
||||
RENAMED = {"PPSSPPSDL": "ppsspp"}
|
||||
|
||||
|
||||
class Scraper(BaseScraper):
|
||||
"""Scraper for EmuELEC's batocera-systems."""
|
||||
|
||||
def __init__(self, url: str | None = None) -> None:
|
||||
self.tag = "" if url else fetch_github_latest_version(REPO)
|
||||
super().__init__(url=url or SOURCE_TEMPLATE.format(ref=self.tag))
|
||||
|
||||
def _fetch_text(self, url: str) -> str:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "retrobios-scraper/1.0"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return resp.read().decode("utf-8")
|
||||
except urllib.error.URLError as e:
|
||||
raise ConnectionError(f"Failed to fetch {url}: {e}") from e
|
||||
|
||||
def validate_format(self, raw_data: str) -> bool:
|
||||
has_dict = re.search(r"^systems\s*=\s*\{", raw_data, re.MULTILINE) is not None
|
||||
return has_dict and "def checkBios" in raw_data and '"biosFiles"' in raw_data
|
||||
|
||||
def fetch_requirements(self) -> list[BiosRequirement]:
|
||||
raw = self._fetch_raw()
|
||||
if not self.validate_format(raw):
|
||||
raise ValueError("batocera-systems format validation failed")
|
||||
# Paths are relative to /storage/roms (`prefix`, lines 859-865): the
|
||||
# list names neogeo/ and ports/ beside bios/.
|
||||
return requirements_from_systems(extract_systems_dict(raw), SYSTEM_SLUG_MAP, strip="")
|
||||
|
||||
def generate_platform_yaml(self) -> dict:
|
||||
systems = platform_systems(
|
||||
self.fetch_requirements(), extract_systems_dict(self._fetch_raw()), SYSTEM_SLUG_MAP
|
||||
)
|
||||
cores, standalone, dual = es_cores(
|
||||
json_emulators(self._fetch_text(ES_SYSTEMS_TEMPLATE.format(ref=self.tag))),
|
||||
RUNNERS,
|
||||
RENAMED,
|
||||
)
|
||||
return {
|
||||
"platform": "EmuELEC",
|
||||
"version": self.tag.removeprefix("v"),
|
||||
"homepage": "https://emuelec.org",
|
||||
"source": self.url,
|
||||
"base_destination": "",
|
||||
# RetroArch's system_directory is /storage/roms/bios
|
||||
# (packages/sx05re/libretro_base/retroarch/package.mk:117).
|
||||
"core_destination": "bios",
|
||||
"hash_type": "md5",
|
||||
"verification_mode": "md5",
|
||||
"cores": cores,
|
||||
"standalone_cores": standalone,
|
||||
"dual_mode_cores": dual,
|
||||
"systems": systems,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
from .base_scraper import scraper_cli
|
||||
|
||||
scraper_cli(Scraper, "Scrape EmuELEC BIOS requirements")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
"""The emulators an EmulationStation system list offers, by profile names.
|
||||
|
||||
AmberELEC writes its list as es_systems.cfg XML, EmuELEC as an es_systems.json
|
||||
it turns into that XML at build time. Both name, per system, the emulators a
|
||||
user may pick and the cores under each. Some emulators are libretro front
|
||||
ends (RetroArch, retrorun) whose cores are libretro core names. Every other
|
||||
emulator is a standalone program named by the OS (`HATARISA`, `PPSSPPSDL`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from safeparse import parse_untrusted_xml
|
||||
|
||||
|
||||
def xml_emulators(text: str) -> dict[str, set[str]]:
|
||||
"""Emulator -> cores, from an es_systems.cfg."""
|
||||
root = parse_untrusted_xml(text, "es_systems.cfg")
|
||||
pairs: dict[str, set[str]] = {}
|
||||
for emulator in root.iter("emulator"):
|
||||
name = emulator.get("name")
|
||||
if name:
|
||||
pairs.setdefault(name, set()).update(
|
||||
core.text.strip() for core in emulator.iter("core") if core.text
|
||||
)
|
||||
if not pairs:
|
||||
raise ValueError("es_systems.cfg names no emulator")
|
||||
return pairs
|
||||
|
||||
|
||||
def json_emulators(text: str) -> dict[str, set[str]]:
|
||||
"""Emulator -> cores, from an es_systems.json."""
|
||||
pairs: dict[str, set[str]] = {}
|
||||
for system in json.loads(text).get("systems", []):
|
||||
for emulator in system.get("emulators") or []:
|
||||
pairs.setdefault(emulator["name"], set()).update(
|
||||
core["name"] for core in emulator.get("cores") or []
|
||||
)
|
||||
if not pairs:
|
||||
raise ValueError("es_systems.json names no emulator")
|
||||
return pairs
|
||||
|
||||
|
||||
def standalone_names(emulator: str) -> list[str]:
|
||||
"""The names a profile may know a standalone emulator by.
|
||||
|
||||
The OS's own spelling, its lowercase, and the lowercase without the
|
||||
`sa` suffix the ELEC family appends to a standalone build of an emulator
|
||||
that also exists as a core (`HATARISA`, `scummvmsa`, `yabasanshiroSA`).
|
||||
"""
|
||||
lower = emulator.lower()
|
||||
names = [emulator, lower]
|
||||
stripped = re.sub(r"-?sa$", "", lower)
|
||||
if stripped:
|
||||
names.append(stripped)
|
||||
return list(dict.fromkeys(names))
|
||||
|
||||
|
||||
def es_cores(
|
||||
pairs: dict[str, set[str]],
|
||||
runners: dict[str, str | None],
|
||||
renamed: dict[str, str] | None = None,
|
||||
) -> tuple[list[str], list[str], list[str]]:
|
||||
"""(cores, standalone_cores, dual_mode_cores) of an ES system list.
|
||||
|
||||
`runners` maps each emulator that runs libretro cores to None when the
|
||||
cores it lists are libretro core names, or to the one core it always
|
||||
loads (AmberELEC's `mame` lists MAME machines and loads the MAME core).
|
||||
`renamed` gives the emulator a standalone name stands for when the
|
||||
suffix rule cannot (`PPSSPPSDL` is PPSSPP), each one read in the OS's
|
||||
launch code.
|
||||
"""
|
||||
renamed = renamed or {}
|
||||
libretro: set[str] = set()
|
||||
for runner, fixed in runners.items():
|
||||
if runner in pairs:
|
||||
libretro.update([fixed] if fixed else pairs[runner])
|
||||
standalone: set[str] = set()
|
||||
for emulator in pairs:
|
||||
if emulator in runners:
|
||||
continue
|
||||
standalone.update(standalone_names(emulator))
|
||||
if emulator in renamed:
|
||||
standalone.add(renamed[emulator])
|
||||
dual = {name for name in standalone if name in libretro}
|
||||
return sorted(libretro | standalone), sorted(standalone), sorted(dual)
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scraper for KNULLI's knulli-systems.
|
||||
|
||||
Source: package/system/knulli-scripts/scripts/knulli-systems
|
||||
Format: Python source, Batocera's `systems` dict, parsed by batocera_scraper
|
||||
Hash: MD5
|
||||
|
||||
`msx1` and `msx2+` are written twice in the dict. Python keeps the later
|
||||
value of a repeated key when the script runs, and so does the parser.
|
||||
|
||||
Verification (knulli-systems at the release tag):
|
||||
- checkBios (lines 867-927): md5sum() of the file at /userdata/<file>
|
||||
against the declared md5, an empty md5 meaning presence only. For a
|
||||
zippedFile entry, checkInsideZip() (lines 814-845) matches the member name
|
||||
with casefold() and hashes it. A system or file carrying `emulator` and
|
||||
`core` is checked only when es_systems.cfg offers that pair (coreExists,
|
||||
lines 847-865).
|
||||
- Run by EmulationStation's BIOS screen (knulli-cfw/batocera-emulationstation
|
||||
10834d00, es-app/src/ApiSystem.cpp:764-774) and by knulli-support:69.
|
||||
|
||||
es_systems.yml and configgen-defaults.yml have Batocera's shape, so the cores
|
||||
are read with Batocera's code. The configgen generators do not: the hatari,
|
||||
melonds and gsplus generators and libretroOptions.py are older forms than the
|
||||
ones batocera_launchers reads, so no launcher layer is written.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from .base_scraper import BaseScraper, BiosRequirement, fetch_github_latest_version
|
||||
from .batocera_scraper import (
|
||||
SYSTEM_SLUG_MAP as BATOCERA_SLUGS,
|
||||
)
|
||||
from .batocera_scraper import (
|
||||
_not_cores,
|
||||
configgen_cores,
|
||||
extract_systems_dict,
|
||||
platform_systems,
|
||||
requirements_from_systems,
|
||||
)
|
||||
|
||||
PLATFORM_NAME = "knulli"
|
||||
|
||||
REPO = "knulli-cfw/knulli-linux"
|
||||
_RAW = "https://raw.githubusercontent.com/knulli-cfw/knulli-linux/{ref}/"
|
||||
SOURCE_TEMPLATE = _RAW + "package/system/knulli-scripts/scripts/knulli-systems"
|
||||
CONFIGGEN_DEFAULTS_TEMPLATE = (
|
||||
_RAW + "package/system/knulli-configgen/configs/configgen-defaults.yml"
|
||||
)
|
||||
ES_SYSTEMS_TEMPLATE = _RAW + "package/emulationstation/knulli-es-system/es_systems.yml"
|
||||
|
||||
# Batocera's names, plus the keys KNULLI writes that Batocera does not,
|
||||
# filed under the slug the other scrapers already give them. msu-md holds the
|
||||
# Mega-CD BIOS files Batocera files under megadrive-msu.
|
||||
SYSTEM_SLUG_MAP = {
|
||||
**BATOCERA_SLUGS,
|
||||
"o2em": "magnavox-odyssey2",
|
||||
"msu-md": "sega-mega-cd",
|
||||
"xbox": "microsoft-xbox",
|
||||
}
|
||||
|
||||
|
||||
class Scraper(BaseScraper):
|
||||
"""Scraper for KNULLI's knulli-systems."""
|
||||
|
||||
def __init__(self, url: str | None = None) -> None:
|
||||
self.tag = "" if url else fetch_github_latest_version(REPO)
|
||||
super().__init__(url=url or SOURCE_TEMPLATE.format(ref=self.tag))
|
||||
|
||||
def _fetch_text(self, url: str) -> str:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "retrobios-scraper/1.0"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return resp.read().decode("utf-8")
|
||||
except urllib.error.URLError as e:
|
||||
raise ConnectionError(f"Failed to fetch {url}: {e}") from e
|
||||
|
||||
def validate_format(self, raw_data: str) -> bool:
|
||||
has_dict = re.search(r"^systems\s*=\s*\{", raw_data, re.MULTILINE) is not None
|
||||
return has_dict and "def checkBios" in raw_data and '"biosFiles"' in raw_data
|
||||
|
||||
def fetch_requirements(self) -> list[BiosRequirement]:
|
||||
raw = self._fetch_raw()
|
||||
if not self.validate_format(raw):
|
||||
raise ValueError("knulli-systems format validation failed")
|
||||
return requirements_from_systems(extract_systems_dict(raw), SYSTEM_SLUG_MAP)
|
||||
|
||||
def generate_platform_yaml(self) -> dict:
|
||||
systems = platform_systems(
|
||||
self.fetch_requirements(), extract_systems_dict(self._fetch_raw()), SYSTEM_SLUG_MAP
|
||||
)
|
||||
# es_systems.yml files machine selectors and plugins under an
|
||||
# emulator as Batocera's does: Batocera's list of them applies.
|
||||
cores, standalone, dual = configgen_cores(
|
||||
self._fetch_text(CONFIGGEN_DEFAULTS_TEMPLATE.format(ref=self.tag)),
|
||||
self._fetch_text(ES_SYSTEMS_TEMPLATE.format(ref=self.tag)),
|
||||
_not_cores("batocera"),
|
||||
)
|
||||
result = {
|
||||
"platform": "KNULLI",
|
||||
"version": self.tag,
|
||||
"homepage": "https://knulli.org",
|
||||
"source": self.url,
|
||||
"base_destination": "bios",
|
||||
"hash_type": "md5",
|
||||
"verification_mode": "md5",
|
||||
"cores": cores,
|
||||
"systems": systems,
|
||||
}
|
||||
if standalone:
|
||||
result["standalone_cores"] = standalone
|
||||
if dual:
|
||||
result["dual_mode_cores"] = dual
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
from .base_scraper import scraper_cli
|
||||
|
||||
scraper_cli(Scraper, "Scrape KNULLI BIOS requirements")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user