« ROMM » : différence entre les versions
Aller à la navigation
Aller à la recherche
| Ligne 595 : | Ligne 595 : | ||
# chmod +x /mnt/Media/Media/Emulation/gamelist_to_pegasus.py | # chmod +x /mnt/Media/Media/Emulation/gamelist_to_pegasus.py | ||
Pour l'exécuter : | Pour l'exécuter : | ||
# python3 /mnt/Media/Media/Emulation/gamelist_to_pegasus.py | # python3 /mnt/Media/Media/Emulation/gamelist_to_pegasus.py | ||
Version du 8 mars 2026 à 17:16
Prérequis
- Alpine LXC avec Nesting et keyctl activé (Docker).
Ressource confortable :
- 2 CPU
- 2Gb de RAM
- 16GB de Disque
Il est recommandé d'organiser les fichiers de la façon suivante :
/roms/{platform}/
/bios/{platform}/
Il est également nécessaire d'utiliser des noms de dossier reconnu par ROMM, exemple :
roms/ nes snes n64 gb gbc gba genesis segacd sega32 neogeomvs neo-geo-pocket tg16 pcenginecd wonderswan wonderswan-color
Metadata
Afin d'obtenir les jaquettes etc il est recommandé de :
* créer un compte chez screenscraper * créer une clef API chez steamgriddb * créer une application "twitch developers" (nécessite un mobile) pour certaines focntions, suivre ce tutoriel.
Installation
# apk update && apk upgrade # apk add docker docker-cli-compose # rc-update add docker default # rc-service docker start # mkdir -p /opt/romm /opt/romm/config /opt/romm/data /opt/romm/assets # cd /opt/romm
Fichier de configuration du conteneur :
# vi docker-compose.yml
version: "3.8"
volumes:
mysql_data:
romm_resources:
romm_redis_data:
services:
romm:
image: ghcr.io/rommapp/romm:latest
container_name: romm
restart: unless-stopped
environment:
- TZ=Europe/Madrid
# Database (MariaDB)
- DB_HOST=romm-db
- DB_NAME=romm
- DB_USER=romm-user
- DB_PASSWD=CHANGE_MOI_ROMM_MARIADB_PASSWORD # mot de passe de "romm-user", identique plus bas!!
# Required auth secret (IMPORTANT)
- ROMM_AUTH_SECRET_KEY=CHANGE_MOI_AUTH_SECRET_KEY
# Metadata providers (optionnel mais recommandé)
- SCREENSCRAPER_USER=ton_user
- SCREENSCRAPER_PASSWORD=ton_mdp
# - RETROACHIEVEMENTS_API_KEY=ta_cle
- STEAMGRIDDB_API_KEY=ta_cle
- HASHEOUS_API_ENABLED=true
- IGDB_CLIENT_ID=idapplitwitch
- IGDB_CLIENT_SECRET=motdepassapplitwitch
volumes:
# Cache / ressources / jobs
#- romm_resources:/romm/resources
- /mnt/Emulation/resources:/romm/resources:rw # stockage images, manuels etc..
- romm_redis_data:/redis-data
# Ta bibliothèque ROM (lecture seule)
- /mnt/Emulation/roms:/romm/library/roms:rw # ro pour read-only
- /mnt/Emulation/bios:/romm/library/bios:rw
# Assets RomM (écriture)
- /opt/romm/assets:/romm/assets
# Config (config.yml)
- /opt/romm/config:/romm/config:rw
ports:
- "8080:8080"
depends_on:
romm-db:
condition: service_healthy
romm-db:
image: mariadb:11
container_name: romm-db
restart: unless-stopped
environment:
- MARIADB_ROOT_PASSWORD=CHANGE_MOI_ROOT_PASSWORD
- MARIADB_DATABASE=romm
- MARIADB_USER=romm-user
- MARIADB_PASSWORD=CHANGE_MOI_ROMM_MARIADB_PASSWORD # mot de passe de "romm-user", identique plus haut!!
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 30s
interval: 10s
timeout: 5s
retries: 10
Fichier de configuration de ROMM :
# vi /opt/romm/config/config.yml
filesystem:
roms: /romm/library/roms
bios: /romm/library/bios
scan:
export_gamelist: true # optionnel
priority:
metadata:
- "ss"
- "hasheous"
artwork:
- "ss"
- "steamgriddb"
region:
- "eu"
- "us"
- "jp"
language:
- "fr"
- "en"
- "es"
media:
- box2d
- screenshot
- marquee
- fanart
- manual
- title_screen
- bezel
emulatorjs:
debug: false
cache_limit: null
disable_batch_bootup: false
disable_auto_unload: false
netplay:
enabled: true
ice_servers:
- urls: "stun:stun.l.google.com:19302"
- urls: "stun:stun1.l.google.com:19302"
- urls: "stun:stun2.l.google.com:19302"
- urls: "turn:openrelay.metered.ca:80"
username: "openrelayproject"
credential: "openrelayproject"
- urls: "turn:openrelay.metered.ca:443"
username: "openrelayproject"
credential: "openrelayproject"
# docker compose up -d
vhost nginx
server {
listen 80;
listen [::]:80;
server_name site.example.net;
# Force HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name site.example.net;
# Uploads (RomM peut être "gros")
client_max_body_size 0;
# TLS (Certbot)
ssl_certificate /etc/letsencrypt/live/site.example.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site.example.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# HSTS (optionnel)
add_header Strict-Transport-Security "max-age=31536000" always;
# Sécurité / info
server_tokens off;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
# WebSocket + keep-alive
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Headers reverse-proxy
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Timeouts (scans/transferts longs)
proxy_read_timeout 3600;
proxy_send_timeout 3600;
# Backend RomM
proxy_pass http://IP_SERVEUR_WEB:8080;
# Optionnel: si tu as des soucis de buffering
# proxy_buffering off;
}
}
convertir les gamelist.xml en metadata.txt
Pour passer de ROMM à Pegasus : lien
Mais le convertisseur officiel ne convertie pas les assets et il faut rajouter à la main les collections etc, on peut utiliser un script local plus optimisé :
# vi /mnt/Media/Media/Emulation/gamelist_to_pegasus.py
#!/usr/bin/env python3
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/mnt/Media/Media/Emulation/roms")
# Mets True si tu veux ajouter automatiquement une commande de lancement par système
ADD_LAUNCH = True
RETROARCH = "retroarch --fullscreen"
CORE_DIR = "/home/retro/.config/retroarch/cores"
def clean(text: str | None) -> str:
if text is None:
return ""
return text.replace("\r", " ").replace("\n", " ").strip()
def escape_metadata_value(text: str) -> str:
# Pegasus n'aime pas les retours ligne bruts dans les champs
return clean(text)
def collection_info_from_dir(dirname: str) -> tuple[str, str]:
slug = dirname.strip().lower()
mapping = {
"3do": ("3DO Interactive Multiplayer", "3do"),
"acpc": ("Amstrad CPC", "acpc"),
"amiga": ("Amiga", "amiga"),
"amiga-cd": ("Amiga CD", "amiga-cd"),
"amiga-cd32": ("Amiga CD32", "amiga-cd32"),
"apple": ("Apple I", "apple"),
"appleii": ("Apple II", "appleii"),
"apple-iigs": ("Apple IIGS", "apple-iigs"),
"appleiii": ("Apple III", "appleiii"),
"apple-lisa": ("Apple Lisa", "apple-lisa"),
"apple-pippin": ("Apple Pippin", "apple-pippin"),
"arcade": ("Arcade", "arcade"),
"astrocade": ("Bally Astrocade", "astrocade"),
"atari2600": ("Atari 2600", "atari2600"),
"atari5200": ("Atari 5200", "atari5200"),
"atari7800": ("Atari 7800", "atari7800"),
"atari8bit": ("Atari 8-bit", "atari8bit"),
"atari-st": ("Atari ST/STE", "atari-st"),
"bbcmicro": ("BBC Microcomputer System", "bbcmicro"),
"c128": ("Commodore 128", "c128"),
"c16": ("Commodore 16", "c16"),
"c64": ("Commodore C64/128/MAX", "c64"),
"c-plus-4": ("Commodore Plus/4", "c-plus-4"),
"colecoadam": ("Coleco Adam", "colecoadam"),
"colecovision": ("ColecoVision", "colecovision"),
"dc": ("Dreamcast", "dc"),
"dos": ("DOS", "dos"),
"dragon-32-slash-64": ("Dragon 32/64", "dragon-32-slash-64"),
"fds": ("Family Computer Disk System", "fds"),
"famicom": ("Family Computer", "famicom"),
"fairchild-channel-f": ("Fairchild Channel F", "fairchild-channel-f"),
"fm-towns": ("FM Towns", "fm-towns"),
"fm-7": ("FM-7", "fm-7"),
"gb": ("Game Boy", "gb"),
"gba": ("Game Boy Advance", "gba"),
"gbc": ("Game Boy Color", "gbc"),
"game-dot-com": ("Game.com", "game-dot-com"),
"gamegear": ("Sega Game Gear", "gamegear"),
"genesis": ("Sega Mega Drive/Genesis", "genesis"),
"jaguar": ("Atari Jaguar", "jaguar"),
"laseractive": ("LaserActive", "laseractive"),
"linux": ("Linux", "linux"),
"lynx": ("Atari Lynx", "lynx"),
"mac": ("Mac", "mac"),
"mastersystem": ("Sega Master System/Mark III", "sms"),
"megacd": ("Sega CD", "segacd"),
"megadrive": ("Sega Mega Drive/Genesis", "genesis"),
"microvision": ("Microvision", "microvision"),
"msx": ("MSX", "msx"),
"msx2": ("MSX2", "msx2"),
"msx2plus": ("Microsoft MSX2+", "msx2plus"),
"msx-turbo": ("MSX Turbo R", "msx-turbo"),
"n64": ("Nintendo 64", "n64"),
"64dd": ("Nintendo 64DD", "64dd"),
"nds": ("Nintendo DS", "nds"),
"neogeoaes": ("Neo Geo AES", "neogeoaes"),
"neogeomvs": ("Neo Geo MVS", "neogeomvs"),
"neogeo": ("Neo Geo AES", "neogeoaes"),
"neo-geo-cd": ("Neo Geo CD", "neo-geo-cd"),
"neo-geo-pocket": ("Neo Geo Pocket", "neo-geo-pocket"),
"neo-geo-pocket-color": ("Neo Geo Pocket Color", "neo-geo-pocket-color"),
"nes": ("Nintendo Entertainment System", "nes"),
"new-nintendo-3ds": ("New Nintendo 3DS", "new-nintendo-3ds"),
"ngage": ("N-Gage", "ngage"),
"ngc": ("Nintendo GameCube", "ngc"),
"odyssey": ("Magnavox Odyssey", "odyssey"),
"odyssey-2": ("Odyssey 2", "odyssey-2"),
"openbor": ("OpenBOR", "openbor"),
"oric": ("Oric", "oric"),
"palm-os": ("Palm OS", "palm-os"),
"pc-fx": ("PC-FX", "pc-fx"),
"pc-jr": ("IBM PCjr", "pc-jr"),
"pc-booter": ("PC Booter", "pc-booter"),
"pcengine": ("TurboGrafx-16/PC Engine", "tg16"),
"pcenginecd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
"pcecd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
"philips-cd-i": ("Philips CD-i", "philips-cd-i"),
"pico": ("PICO", "pico"),
"pokemon-mini": ("Pokémon mini", "pokemon-mini"),
"psx": ("PlayStation", "psx"),
"ps1": ("PlayStation", "psx"),
"ps2": ("PlayStation 2", "ps2"),
"ps3": ("PlayStation 3", "ps3"),
"ps4": ("PlayStation 4", "ps4"),
"ps5": ("PlayStation 5", "ps5"),
"psp": ("PlayStation Portable", "psp"),
"psvita": ("PlayStation Vita", "psvita"),
"sam-coupe": ("SAM Coupé", "sam-coupe"),
"satellaview": ("Satellaview", "satellaview"),
"saturn": ("Sega Saturn", "saturn"),
"sc3000": ("Sega SC-3000", "sc3000"),
"scummvm": ("ScummVM", "scummvm"),
"sega-pico": ("Sega Pico", "sega-pico"),
"sega32": ("Sega 32X", "sega32"),
"segacd": ("Sega CD", "segacd"),
"segacd32": ("Sega CD 32X", "segacd32"),
"sg1000": ("SG-1000", "sg1000"),
"sharp-x68000": ("Sharp X68000", "sharp-x68000"),
"sms": ("Sega Master System/Mark III", "sms"),
"snes": ("Super Nintendo Entertainment System", "snes"),
"sfam": ("Super Famicom", "sfam"),
"super-acan": ("Super A'Can", "super-acan"),
"supergrafx": ("PC Engine SuperGrafx", "supergrafx"),
"supervision": ("Watara/QuickShot Supervision", "supervision"),
"switch": ("Nintendo Switch", "switch"),
"switch-2": ("Nintendo Switch 2", "switch-2"),
"tg16": ("TurboGrafx-16/PC Engine", "tg16"),
"turbografx-cd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
"ti-99": ("Texas Instruments TI-99", "ti-99"),
"ti-994a": ("TI-99/4A", "ti-994a"),
"trs-80": ("TRS-80", "trs-80"),
"trs-80-color-computer": ("TRS-80 Color Computer", "trs-80-color-computer"),
"vb": ("Virtual Boy", "virtualboy"),
"vectrex": ("Vectrex", "vectrex"),
"vic-20": ("Commodore VIC-20", "vic-20"),
"virtualboy": ("Virtual Boy", "virtualboy"),
"vmu": ("Sega Dreamcast VMU", "vmu"),
"wii": ("Wii", "wii"),
"wiiu": ("Wii U", "wiiu"),
"win": ("Windows", "win"),
"win3x": ("Windows 3.x", "win3x"),
"windows-apps": ("Windows Apps", "windows-apps"),
"wonderswan": ("WonderSwan", "wonderswan"),
"wonderswan-color": ("WonderSwan Color", "wonderswan-color"),
"x1": ("Sharp X1", "x1"),
"xavixport": ("XaviXPORT", "xavixport"),
"xbox": ("Xbox", "xbox"),
"xbox360": ("Xbox 360", "xbox360"),
"xboxone": ("Xbox One", "xboxone"),
"series-x-s": ("Xbox Series X/S", "series-x-s"),
"z-machine": ("Z-machine", "z-machine"),
"z88": ("Cambridge Computer Z88", "z88"),
"zinc": ("ZiNc", "zinc"),
"zx80": ("ZX80", "zx80"),
"zx81": ("Sinclair ZX81", "zx81"),
"zxs": ("ZX Spectrum", "zxs"),
"zx-spectrum-next": ("ZX Spectrum Next", "zx-spectrum-next"),
}
if slug in mapping:
return mapping[slug]
pretty = slug.replace("-", " ").replace("_", " ").title()
return pretty, slug
def launch_for_shortname(shortname: str) -> str | None:
launches = {
"arcade": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "Modèle:File.path"',
"neogeoaes": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "Modèle:File.path"',
"neogeomvs": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "Modèle:File.path"',
"neo-geo-cd": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "Modèle:File.path"',
"genesis": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
"segacd": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
"segacd32": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "Modèle:File.path"',
"sega32": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "Modèle:File.path"',
"sms": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
"sg1000": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
"gamegear": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "Modèle:File.path"',
"tg16": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "Modèle:File.path"',
"turbografx-cd": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "Modèle:File.path"',
"supergrafx": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "Modèle:File.path"',
"nes": f'{RETROARCH} -L {CORE_DIR}/nestopia_libretro.so "Modèle:File.path"',
"snes": f'{RETROARCH} -L {CORE_DIR}/snes9x_libretro.so "Modèle:File.path"',
"sfam": f'{RETROARCH} -L {CORE_DIR}/snes9x_libretro.so "Modèle:File.path"',
"gb": f'{RETROARCH} -L {CORE_DIR}/gambatte_libretro.so "Modèle:File.path"',
"gbc": f'{RETROARCH} -L {CORE_DIR}/gambatte_libretro.so "Modèle:File.path"',
"gba": f'{RETROARCH} -L {CORE_DIR}/mgba_libretro.so "Modèle:File.path"',
"psx": f'{RETROARCH} -L {CORE_DIR}/pcsx_rearmed_libretro.so "Modèle:File.path"',
"saturn": f'{RETROARCH} -L {CORE_DIR}/beetle_saturn_libretro.so "Modèle:File.path"',
"dc": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "Modèle:File.path"',
"n64": f'{RETROARCH} -L {CORE_DIR}/mupen64plus_next_libretro.so "Modèle:File.path"',
"nds": f'{RETROARCH} -L {CORE_DIR}/melonds_libretro.so "Modèle:File.path"',
"ngc": f'{RETROARCH} -L {CORE_DIR}/dolphin_libretro.so "Modèle:File.path"',
"wii": f'{RETROARCH} -L {CORE_DIR}/dolphin_libretro.so "Modèle:File.path"',
"virtualboy": f'{RETROARCH} -L {CORE_DIR}/mednafen_vb_libretro.so "Modèle:File.path"',
"lynx": f'{RETROARCH} -L {CORE_DIR}/handy_libretro.so "Modèle:File.path"',
"jaguar": f'{RETROARCH} -L {CORE_DIR}/virtualjaguar_libretro.so "Modèle:File.path"',
"wonderswan": f'{RETROARCH} -L {CORE_DIR}/mednafen_wswan_libretro.so "Modèle:File.path"',
"wonderswan-color": f'{RETROARCH} -L {CORE_DIR}/mednafen_wswan_libretro.so "Modèle:File.path"',
"msx": f'{RETROARCH} -L {CORE_DIR}/fmsx_libretro.so "Modèle:File.path"',
"msx2": f'{RETROARCH} -L {CORE_DIR}/fmsx_libretro.so "Modèle:File.path"',
"amiga": f'{RETROARCH} -L {CORE_DIR}/puae_libretro.so "Modèle:File.path"',
"amiga-cd32": f'{RETROARCH} -L {CORE_DIR}/puae_libretro.so "Modèle:File.path"',
"scummvm": f'{RETROARCH} -L {CORE_DIR}/scummvm_libretro.so "Modèle:File.path"',
}
return launches.get(shortname)
def detect_extensions(games: list[ET.Element]) -> list[str]:
exts = set()
for game in games:
path = clean(game.findtext("path"))
if not path:
continue
suffix = Path(path).suffix.lower().lstrip(".")
if suffix:
exts.add(suffix)
return sorted(exts)
def write_line(out, key: str, value: str | None):
value = escape_metadata_value(value or "")
if value:
out.write(f"{key}: {value}\n")
def convert_gamelist(gamelist_path: Path):
romdir = gamelist_path.parent
dirname = romdir.name
out_path = romdir / "metadata.pegasus.txt"
try:
tree = ET.parse(gamelist_path)
root = tree.getroot()
except Exception as e:
print(f"[ERR] {gamelist_path}: {e}")
return
games = root.findall("game")
if not games:
print(f"[WARN] {gamelist_path}: no <game> entries")
return
collection, shortname = collection_info_from_dir(dirname)
exts = detect_extensions(games)
with out_path.open("w", encoding="utf-8") as out:
out.write(f"collection: {collection}\n")
out.write(f"shortname: {shortname}\n")
if exts:
out.write(f"extensions: {' '.join(exts)}\n")
if ADD_LAUNCH:
launch = launch_for_shortname(shortname)
if launch:
out.write(f"launch: {launch}\n")
out.write("\n")
for game in games:
name = clean(game.findtext("name"))
path = clean(game.findtext("path"))
if not path:
continue
if path.startswith("./"):
path = path[2:]
out.write(f"game: {name or Path(path).stem}\n")
out.write(f"file: {path}\n")
write_line(out, "description", game.findtext("desc"))
write_line(out, "developer", game.findtext("developer"))
write_line(out, "publisher", game.findtext("publisher"))
write_line(out, "genre", game.findtext("genre"))
releasedate = clean(game.findtext("releasedate"))
if releasedate:
if len(releasedate) >= 8 and releasedate[:8].isdigit():
rd = releasedate[:8]
out.write(f"release: {rd[:4]}-{rd[4:6]}-{rd[6:8]}\n")
else:
out.write(f"release: {releasedate}\n")
rating = clean(game.findtext("rating"))
if rating:
out.write(f"rating: {rating}\n")
thumbnail = clean(game.findtext("thumbnail"))
image = clean(game.findtext("image"))
screenshot = clean(game.findtext("screenshot"))
fanart = clean(game.findtext("fanart"))
video = clean(game.findtext("video"))
# jaquette
if thumbnail:
out.write(f"assets.box_front: {thumbnail}\n")
elif image:
out.write(f"assets.box_front: {image}\n")
if screenshot:
out.write(f"assets.screenshot: {screenshot}\n")
if fanart:
out.write(f"assets.background: {fanart}\n")
if video:
out.write(f"assets.video: {video}\n")
out.write("\n")
print(f"[OK] {gamelist_path} -> {out_path} ({len(games)} games)")
def main():
gamelists = list(ROOT.rglob("gamelist.xml"))
if not gamelists:
print(f"[WARN] No gamelist.xml found under {ROOT}")
return
for gamelist in sorted(gamelists):
convert_gamelist(gamelist)
if __name__ == "__main__":
main()
# chmod +x /mnt/Media/Media/Emulation/gamelist_to_pegasus.py
Pour l'exécuter :
# python3 /mnt/Media/Media/Emulation/gamelist_to_pegasus.py