Basculer le menu
Changer de menu des préférences
Basculer le menu personnel
Non connecté(e)
Votre adresse IP sera visible au public si vous faites des modifications.

ROMM

De Le Wiki de Lug

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

      - SCAN_TIMEOUT=86400 # timeout de 24h
      - SCAN_WORKERS=4 # limite IGBD

    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 (déconseillé)

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("/mnt/Media/Media/Emulation/roms")
if len(sys.argv) > 1:
    ROOT = Path(sys.argv[1])

ADD_LAUNCH = True
RETROARCH = "retroarch --fullscreen"
CORE_DIR = "/home/retro/.config/retroarch/cores"


def clean(text):
    if text is None:
        return ""
    return text.replace("\r", " ").replace("\n", " ").strip()


def collection_info_from_dir(dirname: str):
    slug = dirname.strip().lower()

    mapping = {
        "arcade": ("Arcade", "arcade"),
        "neogeo": ("Neo Geo", "neogeo"),
        "neogeoaes": ("Neo Geo AES", "neogeo"),
        "neogeomvs": ("Neo Geo MVS", "neogeo"),
        "neo-geo-cd": ("Neo Geo CD", "neo-geo-cd"),
        "neo-geo-pocket": ("Neo Geo Pocket", "ngp"),
        "neo-geo-pocket-color": ("Neo Geo Pocket Color", "ngpc"),
        "genesis": ("Sega Mega Drive/Genesis", "megadrive"),
        "megadrive": ("Sega Mega Drive/Genesis", "megadrive"),
        "segacd": ("Sega CD", "segacd"),
        "segacd32": ("Sega CD 32X", "segacd32"),
        "sega32": ("Sega 32X", "sega32x"),
        "sms": ("Sega Master System/Mark III", "mastersystem"),
        "mastersystem": ("Sega Master System/Mark III", "sms"),
        "gamegear": ("Sega Game Gear", "gamegear"),
        "sg1000": ("SG-1000", "sg1000"),
        "tg16": ("TurboGrafx-16/PC Engine", "tg16"),
        "pcengine": ("TurboGrafx-16/PC Engine", "tg16"),
        "pcenginecd": ("Turbografx-16/PC Engine CD", "tg-cd"),
        "pcecd": ("Turbografx-16/PC Engine CD", "tg-cd"),
        "turbografx-cd": ("Turbografx-16/PC Engine CD", "tg-cd"),
        "supergrafx": ("PC Engine SuperGrafx", "supergrafx"),
        "nes": ("Nintendo Entertainment System", "nes"),
        "snes": ("Super Nintendo Entertainment System", "snes"),
        "sfam": ("Super Famicom", "sfam"),
        "gb": ("Game Boy", "gb"),
        "gbc": ("Game Boy Color", "gbc"),
        "gba": ("Game Boy Advance", "gba"),
        "psx": ("PlayStation", "psx"),
        "ps1": ("PlayStation", "psx"),
        "saturn": ("Sega Saturn", "saturn"),
        "dc": ("Dreamcast", "dc"),
        "n64": ("Nintendo 64", "n64"),
        "nds": ("Nintendo DS", "nds"),
        "ngc": ("Nintendo GameCube", "ngc"),
        "wii": ("Wii", "wii"),
        "lynx": ("Atari Lynx", "atarilynx"),
        "jaguar": ("Atari Jaguar", "atarijaguar"),
        "wonderswan": ("WonderSwan", "wonderswan"),
        "wonderswan-color": ("WonderSwan Color", "wonderswancolor"),
        "msx": ("MSX", "msx"),
        "msx2": ("MSX2", "msx2"),
        "amiga": ("Amiga", "amiga"),
        "amiga-cd32": ("Amiga CD32", "amiga-cd32"),
        "scummvm": ("ScummVM", "scummvm"),
        "3do": ("3DO Interactive Multiplayer", "3do"),
        "psp": ("PlayStation Portable", "psp"),
        "psvita": ("PlayStation Vita", "psvita"),
        "vectrex": ("Vectrex", "vectrex"),
        "virtualboy": ("Virtual Boy", "virtualboy"),
        "zxs": ("ZX Spectrum", "zxs"),
        "c64": ("Commodore C64/128/MAX", "c64"),
        "c128": ("Commodore 128", "c128"),
        "vic-20": ("Commodore VIC-20", "vic-20"),
        "colecovision": ("ColecoVision", "colecovision"),
        "atari2600": ("Atari 2600", "atari2600"),
        "atari5200": ("Atari 5200", "atari5200"),
        "atari7800": ("Atari 7800", "atari7800"),
    }

    if slug in mapping:
        return mapping[slug]

    return slug.replace("-", " ").replace("_", " ").title(), slug


def launch_for_shortname(shortname: str):
    launches = {
        # arcade / neo geo
         "arcade": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "{{file.path}}"',
         "neogeo": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "{{file.path}}"',
         "neogeoaes": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "{{file.path}}"',
         "neogeomvs": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "{{file.path}}"',
         "ngp": f'{RETROARCH} -L {CORE_DIR}/mednafen_ngp_libretro.so "{{file.path}}"',
         "ngpc": f'{RETROARCH} -L {CORE_DIR}/mednafen_ngp_libretro.so "{{file.path}}"',
 
         # sega
         "genesis": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"',
         "segacd": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"',
         "segacd32": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "{{file.path}}"',
         "sega32": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "{{file.path}}"',
         "sms": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"',
         "gamegear": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"',
         "sg1000": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"',
 
         # NEC
         "tg16": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "{{file.path}}"',
         "turbografx-cd": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "{{file.path}}"',
         "supergrafx": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "{{file.path}}"',
 
         # Nintendo
         "nes": f'{RETROARCH} -L {CORE_DIR}/nestopia_libretro.so "{{file.path}}"',
         "snes": f'{RETROARCH} -L {CORE_DIR}/snes9x_libretro.so "{{file.path}}"',
         "sfam": f'{RETROARCH} -L {CORE_DIR}/snes9x_libretro.so "{{file.path}}"',
         "gb": f'{RETROARCH} -L {CORE_DIR}/gambatte_libretro.so "{{file.path}}"',
         "gbc": f'{RETROARCH} -L {CORE_DIR}/gambatte_libretro.so "{{file.path}}"',
         "gba": f'{RETROARCH} -L {CORE_DIR}/mgba_libretro.so "{{file.path}}"',
         "n64": f'{RETROARCH} -L {CORE_DIR}/mupen64plus_next_libretro.so "{{file.path}}"',
         "nds": f'{RETROARCH} -L {CORE_DIR}/melonds_libretro.so "{{file.path}}"',
         "ngc": f'{RETROARCH} -L {CORE_DIR}/dolphin_libretro.so "{{file.path}}"',
         "wii": f'{RETROARCH} -L {CORE_DIR}/dolphin_libretro.so "{{file.path}}"',
         "virtualboy": f'{RETROARCH} -L {CORE_DIR}/mednafen_vb_libretro.so "{{file.path}}"',
 
         # Sony
         "psx": f'{RETROARCH} -L {CORE_DIR}/pcsx_rearmed_libretro.so "{{file.path}}"',
         "psp": f'{RETROARCH} -L {CORE_DIR}/ppsspp_libretro.so "{{file.path}}"',
 
         # Sega Saturn / Dreamcast
         "saturn": f'{RETROARCH} -L {CORE_DIR}/beetle_saturn_libretro.so "{{file.path}}"',
         "dc": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
 
         # Atari
         "lynx": f'{RETROARCH} -L {CORE_DIR}/mednafen_lynx_libretro.so "{{file.path}}"',
         "jaguar": f'{RETROARCH} -L {CORE_DIR}/virtualjaguar_libretro.so "{{file.path}}"',
         "atari2600": f'{RETROARCH} -L {CORE_DIR}/stella_libretro.so "{{file.path}}"',
         "atari5200": f'{RETROARCH} -L {CORE_DIR}/a5200_libretro.so "{{file.path}}"',
         "atari7800": f'{RETROARCH} -L {CORE_DIR}/prosystem_libretro.so "{{file.path}}"',
 
         # others
         "wonderswan": f'{RETROARCH} -L {CORE_DIR}/mednafen_wswan_libretro.so "{{file.path}}"',
         "wonderswan-color": f'{RETROARCH} -L {CORE_DIR}/mednafen_wswan_libretro.so "{{file.path}}"',
         "msx": f'{RETROARCH} -L {CORE_DIR}/fmsx_libretro.so "{{file.path}}"',
         "msx2": f'{RETROARCH} -L {CORE_DIR}/fmsx_libretro.so "{{file.path}}"',
         "amiga": f'{RETROARCH} -L {CORE_DIR}/puae_libretro.so "{{file.path}}"',
         "amiga-cd32": f'{RETROARCH} -L {CORE_DIR}/puae_libretro.so "{{file.path}}"',
         "scummvm": f'{RETROARCH} -L {CORE_DIR}/scummvm_libretro.so "{{file.path}}"',
         "vectrex": f'{RETROARCH} -L {CORE_DIR}/vecx_libretro.so "{{file.path}}"',
         "colecovision": f'{RETROARCH} -L {CORE_DIR}/gearcoleco_libretro.so "{{file.path}}"',
         "c64": f'{RETROARCH} -L {CORE_DIR}/vice_x64_libretro.so "{{file.path}}"',
         "c128": f'{RETROARCH} -L {CORE_DIR}/vice_x128_libretro.so "{{file.path}}"',
         "vic-20": f'{RETROARCH} -L {CORE_DIR}/vice_xvic_libretro.so "{{file.path}}"',
    }
    return launches.get(shortname)


def detect_extensions(games):
    exts = set()
    for game in games:
        path = clean(game.findtext("path"))
        if path:
            suffix = Path(path).suffix.lower().lstrip(".")
            if suffix:
                exts.add(suffix)
    return sorted(exts)


def write_line(out, key, value):
    value = clean(value)
    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

            # IMPORTANT: on garde le chemin relatif, comme quand ça marchait
            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"))

            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")

            # On garde la vidéo, mais si GameOS spam avec YouTube, commente ces 2 lignes
            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

Il faudra penser à télécharger le launcher associé si nécessaire dans le Retroarch de Pegasus (Main Menu -> Online Updater -> -> Update Core Info Files -> Core Downloader)