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", "sg-1000"),
        "sc3000": ("SC-3000", "sc-3000"),
        "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", "dreamcast"),
        "n64": ("Nintendo 64", "n64"),
        "nds": ("Nintendo DS", "nds"),
        "ngc": ("Nintendo GameCube", "gamecube"),
        "wii": ("Wii", "wii"),
        "lynx": ("Atari Lynx", "atarilynx"),
        "jaguar": ("Atari Jaguar", "atarijaguar"),
        "atari-jaguar-cd": ("Atari Jaguar CD", "atarijaguarcd"),
        "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"),
        "g-and-w": ("Game & Watch", "gameandwatch"),
    }

    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
         "megadrive": 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}}"',
         "segacd32x": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "{{file.path}}"',
         "sega32x": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "{{file.path}}"',
         "mastersystem": 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}}"',
         "sg-1000": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"',
         "sc-3000": 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}}"',
         "tg-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}/melondsds_libretro.so "{{file.path}}"',
         "gamecube": f'/Applications/dolphin-emu.AppImage --appimage-extract-and-run -b -e "{{file.path}}"',
         "wii": f'/Applications/dolphin-emu.AppImage --appimage-extract-and-run -b -e "{{file.path}}"',
         "virtualboy": f'{RETROARCH} -L {CORE_DIR}/mednafen_vb_libretro.so "{{file.path}}"',
         "gameandwatch": f'{RETROARCH} -L {CORE_DIR}/gw_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}/mednafen_saturn_libretro.so "{{file.path}}"',
         "dreamcast": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
 
         # Atari
         "atarilynx": f'{RETROARCH} -L {CORE_DIR}/mednafen_lynx_libretro.so "{{file.path}}"',
         #"atarijaguar": f'{RETROARCH} -L {CORE_DIR}/virtualjaguar_libretro.so "{{file.path}}"',
         #"atarijaguarcd": f'{RETROARCH} -L {CORE_DIR}/virtualjaguar_libretro.so "{{file.path}}"',
         "atarijaguar": f'/opt/emulator/bigpemu/bigpemu "{{file.path}}"',
         "atarijaguarcd": f'/opt/emulator/bigpemu/bigpemu "{{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}}"',
         "wonderswancolor": 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)

Fichiers .m3u

Afin de gérer les jeu utilisant plusieurs CD/DVD il est nécessaire de creer un fichier liste .m3u :

Pour les fichiers .CHD ou .RVZ :

# cd /mes/roms/plateforme/

Coller la commande suivante :

#!/usr/bin/env bash

unset groups counts seen_files
declare -A groups
declare -A counts
declare -A seen_files

for file in *.chd *.rvz; do
    [ -e "$file" ] || continue

    name="${file%.*}"

    # Enlever uniquement le suffixe disque en fin de nom
    base="$(printf '%s\n' "$name" | sed -E '
           s/[[:space:]]*-[[:space:]]*(disc|disk|cd)[[:space:]]*[0-9]+$//I
           s/[[:space:]]*\((disc|disk|cd)[[:space:]]*[0-9]+\)$//I
           s/[[:space:]]+(disc|disk|cd)[[:space:]]*[0-9]+$//I
           s/(disc|disk|cd)[[:space:]]*[0-9]+$//I
    ')"

    [ "$base" != "$name" ] || continue

    group="$base"

    key="$group"$'\t'"$file"
    [ -n "${seen_files["$key"]}" ] && continue
    seen_files["$key"]=1

    groups["$group"]+="$file"$'\n'
    ((counts["$group"]++))
done

for group in "${!groups[@]}"; do
    [ "${counts["$group"]}" -ge 2 ] || continue
    printf '%s' "${groups["$group"]}" | sed '/^$/d' | sort -V > "$group.m3u"
done

Convertir bin et cue dans une archive 7z/zip en fichier CHD

Si nécessaire il faut installer les outils de MAME pour avoir chdman :

# apt install mame-tools

Si nécessaire installer les outils pour gérer les archives 7z :

# apt install p7zip-full

Puis on créé le script (à placer dans le dossier contenant les archives .7z/zip :

# cd /mon/dossier/
# vi convertchd.sh
#!/usr/bin/env bash

shopt -s nullglob

for f in *.7z *.zip; do
    [ -e "$f" ] || continue

    name="${f%.*}"
    tmpdir="$(mktemp -d)"

    echo "==> Extraction de : $f"

    case "${f##*.}" in
        7z)
            if ! 7z x -y "$f" -o"$tmpdir" >/dev/null; then
                echo "Erreur extraction : $f"
                rm -rf "$tmpdir"
                continue
            fi
            ;;
        zip)
            if ! unzip -q "$f" -d "$tmpdir"; then
                echo "Erreur extraction : $f"
                rm -rf "$tmpdir"
                continue
            fi
            ;;
        *)
            echo "Extension non supportée : $f"
            rm -rf "$tmpdir"
            continue
            ;;
    esac

    cue="$(find "$tmpdir" -type f -iname "*.cue" | head -n 1)"

    if [ -z "$cue" ]; then
        echo "Aucun .cue trouvé dans : $f"
        rm -rf "$tmpdir"
        continue
    fi

    echo "==> Conversion en CHD : $name.chd"
    if chdman createcd -i "$cue" -o "${name}.chd"; then
        echo "OK : ${name}.chd"
        rm -f -- "$f"
    else
        echo "Erreur conversion : $f"
    fi

    rm -rf "$tmpdir"
done
# chmod +x convertchd.sh

Puis il suffit de lancer le script :

# ./convertchd.sh

trier les roms par langues, régions.. (à revérifier/tester)

# apt update && apt upgrade
# apt install git python3 pip python3.13-venv python3-pip
# mkdir /opt/triederoms
# cd /opt/triederoms
# python3 -m venv retool-env
# source retool-env/bin/activate
# python -m pip install --upgrade pip
# pip install alive-progress darkdetect lxml psutil pyside6 strictyaml validators
# git clone https://github.com/unexpectedpanda/retool.git
# cd retool
# source ../retool-env/bin/activate

On organise le trie avec pour exemple :

# nano config/user-config.yaml
---
config version: 2.4.0

# If the -l option is used, only include titles with the following languages.
language order:
- French
- English

region order:
- France
- Europe
- World
- USA
- Japan

video order:
- PAL
- PAL 60Hz
- NTSC
- MPAL
- SECAM

list prefix:

list suffix:

exclude:

include:

filters:

On récupère le ficher .dat correspondant à la plateforme à trier à cette adresse puis on lance le trie :

# python retool.py maplateforme.dat --exclude a B d D m M o P r u v --output .