« ROMM » : différence entre les versions

De Le Wiki de Lug
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
Balise : Révocation manuelle
 
(52 versions intermédiaires par le même utilisateur non affichées)
Ligne 14 : Ligne 14 :
* 2 CPU
* 2 CPU
* 2Gb de RAM
* 2Gb de RAM
* 16GB de Disque
* 16GB de Disque (Dépend du nombres de roms, peut monter très vite..)
Il est recommandé d'organiser les fichiers de la façon suivante :
Il est recommandé d'organiser les fichiers de la façon suivante :
  /roms/{platform}/
  /roms/{platform}/
Ligne 61 : Ligne 61 :
  services:
  services:
   romm:
   romm:
     image: ghcr.io/rommapp/romm:latest
     image: rommapp/romm:latest
     container_name: romm
     container_name: romm
     restart: unless-stopped
     restart: unless-stopped
Ligne 85 : Ligne 85 :
       - IGDB_CLIENT_SECRET=<font color = blue>motdepassapplitwitch</font>
       - IGDB_CLIENT_SECRET=<font color = blue>motdepassapplitwitch</font>
   
   
      - SCAN_TIMEOUT=86400 # timeout de 24h
      - SCAN_WORKERS=4 # limite IGBD
      - TASK_TIMEOUT=3600 # plus gros timeout
      - WEB_SERVER_TIMEOUT=3600 # plus gros timeout
     volumes:
     volumes:
       # Cache / ressources / jobs
       # Cache / ressources / jobs
Ligne 247 : Ligne 251 :
     }
     }
  }
  }
= Augmenter le timeout du Nginx du conteneur pour grosse BDD de roms =
{{Méta bandeau
  | niveau = information
  | icône = loupe
  | texte  = Le changement ne sera pas persistant, permet de faire un nettoyage important lors de manipulation de grosse base de donénes de romes..
}}
On rentre dans le conteneur :
# docker exec -it romm sh
(Optionnel) On sauvegarde la configuration d'origine :
# cp /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak
On édite la configuration :
# vi /etc/nginx/conf.d/default.conf
Et on ajoute (en bleu) :
<font color = grey>...
    # OpenAPI for swagger and redoc
    location /openapi.json {
        proxy_pass <nowiki>http://</nowiki>wsgi_server;
        <font color = blue>proxy_connect_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_read_timeout 3600s;</font>
    }
    # Backend api calls
    location /api {
        proxy_pass <nowiki>http://</nowiki>wsgi_server;
        proxy_request_buffering off;
        proxy_buffering off;
        <font color = blue>proxy_connect_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_read_timeout 3600s;</font>
    }
    location ~ ^/(ws|netplay) {
        proxy_pass <nowiki>http://</nowiki>wsgi_server;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        <font color = blue>proxy_connect_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_read_timeout 3600s;</font>
    }
...</font>
Puis on redémarre le service :
# nginx -s reload
# exit
= convertir les gamelist.xml en metadata.txt =
= convertir les gamelist.xml en metadata.txt =
Pour passer de ROMM à Pegasus : [https://pegasus-frontend.org/tools/convert/ lien]
{{Méta bandeau
  | niveau = information
  | icône = loupe
  | texte  = Penser à exclure "'''metadata.pegasus.txt'''" des fichiers à scanner dans ROMM.}}
Pour passer de ROMM à Pegasus : [https://pegasus-frontend.org/tools/convert/ 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é :
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é :
Ligne 260 : Ligne 314 :
  from pathlib import Path
  from pathlib import Path
   
   
  ROOT = Path("<font color = blue>/mnt/Media/Media/Emulation/roms</font>")
  ROOT = Path("/mnt/Media/Media/Emulation/roms")
  if len(sys.argv) > 1:
  if len(sys.argv) > 1:
     ROOT = Path(sys.argv[1])
     ROOT = Path(sys.argv[1])
   
   
  ADD_LAUNCH = <font color = red>True</font>
  ADD_LAUNCH = True
  RETROARCH = "retroarch --fullscreen"
  RETROARCH = "retroarch --fullscreen"
  CORE_DIR = "/home/retro/.config/retroarch/cores"
  CORE_DIR = "/home/retro/.config/retroarch/cores"
Ligne 281 : Ligne 335 :
         "arcade": ("Arcade", "arcade"),
         "arcade": ("Arcade", "arcade"),
         "neogeo": ("Neo Geo", "neogeo"),
         "neogeo": ("Neo Geo", "neogeo"),
         "neogeoaes": ("Neo Geo AES", "neogeoaes"),
         "neogeoaes": ("Neo Geo AES", "neogeo"),
         "neogeomvs": ("Neo Geo MVS", "neogeomvs"),
         "neogeomvs": ("Neo Geo MVS", "neogeo"),
         "neo-geo-cd": ("Neo Geo CD", "neo-geo-cd"),
         "neo-geo-cd": ("Neo Geo CD", "neo-geo-cd"),
         "genesis": ("Sega Mega Drive/Genesis", "genesis"),
        "neo-geo-pocket": ("Neo Geo Pocket", "ngp"),
         "megadrive": ("Sega Mega Drive/Genesis", "genesis"),
        "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"),
         "segacd": ("Sega CD", "segacd"),
         "segacd32": ("Sega CD 32X", "segacd32"),
         "segacd32": ("Sega CD 32X", "segacd32"),
         "sega32": ("Sega 32X", "sega32"),
         "sega32": ("Sega 32X", "sega32x"),
         "sms": ("Sega Master System/Mark III", "sms"),
         "sms": ("Sega Master System/Mark III", "mastersystem"),
         "mastersystem": ("Sega Master System/Mark III", "sms"),
         "mastersystem": ("Sega Master System/Mark III", "sms"),
         "gamegear": ("Sega Game Gear", "gamegear"),
         "gamegear": ("Sega Game Gear", "gamegear"),
         "sg1000": ("SG-1000", "sg1000"),
         "sg1000": ("SG-1000", "sg-1000"),
        "sc3000": ("SC-3000", "sc-3000"),
         "tg16": ("TurboGrafx-16/PC Engine", "tg16"),
         "tg16": ("TurboGrafx-16/PC Engine", "tg16"),
         "pcengine": ("TurboGrafx-16/PC Engine", "tg16"),
         "pcengine": ("TurboGrafx-16/PC Engine", "tg16"),
         "pcenginecd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
         "pcenginecd": ("Turbografx-16/PC Engine CD", "tg-cd"),
         "pcecd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
         "pcecd": ("Turbografx-16/PC Engine CD", "tg-cd"),
         "turbografx-cd": ("Turbografx-16/PC Engine CD", "turbografx-cd"),
         "turbografx-cd": ("Turbografx-16/PC Engine CD", "tg-cd"),
         "supergrafx": ("PC Engine SuperGrafx", "supergrafx"),
         "supergrafx": ("PC Engine SuperGrafx", "supergrafx"),
         "nes": ("Nintendo Entertainment System", "nes"),
         "nes": ("Nintendo Entertainment System", "nes"),
Ligne 308 : Ligne 365 :
         "ps1": ("PlayStation", "psx"),
         "ps1": ("PlayStation", "psx"),
         "saturn": ("Sega Saturn", "saturn"),
         "saturn": ("Sega Saturn", "saturn"),
         "dc": ("Dreamcast", "dc"),
         "dc": ("Dreamcast", "dreamcast"),
         "n64": ("Nintendo 64", "n64"),
         "n64": ("Nintendo 64", "n64"),
         "nds": ("Nintendo DS", "nds"),
         "nds": ("Nintendo DS", "nds"),
         "ngc": ("Nintendo GameCube", "ngc"),
         "ngc": ("Nintendo GameCube", "gc"),
         "wii": ("Wii", "wii"),
         "wii": ("Wii", "wii"),
         "lynx": ("Atari Lynx", "lynx"),
         "lynx": ("Atari Lynx", "atarilynx"),
         "jaguar": ("Atari Jaguar", "jaguar"),
         "jaguar": ("Atari Jaguar", "atarijaguar"),
        "atari-jaguar-cd": ("Atari Jaguar CD", "atarijaguarcd"),
         "wonderswan": ("WonderSwan", "wonderswan"),
         "wonderswan": ("WonderSwan", "wonderswan"),
         "wonderswan-color": ("WonderSwan Color", "wonderswan-color"),
         "wonderswan-color": ("WonderSwan Color", "wonderswancolor"),
         "msx": ("MSX", "msx"),
         "msx": ("MSX", "msx"),
         "msx2": ("MSX2", "msx2"),
         "msx2": ("MSX2", "msx2"),
Ligne 335 : Ligne 393 :
         "atari5200": ("Atari 5200", "atari5200"),
         "atari5200": ("Atari 5200", "atari5200"),
         "atari7800": ("Atari 7800", "atari7800"),
         "atari7800": ("Atari 7800", "atari7800"),
        "naomi": ("Sega Naomi", "naomi"),
        "naomi2": ("Sega Naomi 2", "naomi2"),
        "triforce": ("Sega Triforce", "triforce"),
        "3do": ("3DO Interactive Multiplayer", "3do"),
        "g-and-w": ("Game & Watch", "gameandwatch"),
     }
     }
   
   
Ligne 345 : Ligne 408 :
  def launch_for_shortname(shortname: str):
  def launch_for_shortname(shortname: str):
     launches = {
     launches = {
        # arcade / neo geo
          <nowiki># arcade / neo geo
         "arcade": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "{{file.path}}"',
         "arcade": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "{{file.path}}"',
         "neogeo": 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}}"',
         "neogeoaes": f'{RETROARCH} -L {CORE_DIR}/fbneo_libretro.so "{{file.path}}"',
         "neogeomvs": 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
         # sega
         "genesis": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"',
         "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}}"',
         "segacd": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"',
         "segacd32": f'{RETROARCH} -L {CORE_DIR}/picodrive_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}}"',
         "sega32x": f'{RETROARCH} -L {CORE_DIR}/picodrive_libretro.so "{{file.path}}"',
         "sms": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_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}}"',
         "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}}"',
         "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
         # NEC
         "tg16": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_fast_libretro.so "{{file.path}}"',
         "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}}"',
         "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}}"',
         "supergrafx": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_libretro.so "{{file.path}}"',
   
   
         # Nintendo
         # Nintendo
Ligne 373 : Ligne 439 :
         "gba": f'{RETROARCH} -L {CORE_DIR}/mgba_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}}"',
         "n64": f'{RETROARCH} -L {CORE_DIR}/mupen64plus_next_libretro.so "{{file.path}}"',
         "nds": f'{RETROARCH} -L {CORE_DIR}/melonds_libretro.so "{{file.path}}"',
         "nds": f'{RETROARCH} -L {CORE_DIR}/melondsds_libretro.so "{{file.path}}"',
         "ngc": f'{RETROARCH} -L {CORE_DIR}/dolphin_libretro.so "{{file.path}}"',
         "gc": '/Applications/dolphin-emu.AppImage --appimage-extract-and-run -b -e "{file.path}"',
         "wii": f'{RETROARCH} -L {CORE_DIR}/dolphin_libretro.so "{{file.path}}"',
         "wii": '/Applications/dolphin-emu.AppImage --appimage-extract-and-run -b -e "{file.path}"',
         "virtualboy": f'{RETROARCH} -L {CORE_DIR}/mednafen_vb_libretro.so "{{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}}"',
        "gameandwatch": f'{RETROARCH} -L {CORE_DIR}/mame_libretro.so "{{file.path}}"',
   
   
         # Sony
         # Sony
         "psx": f'{RETROARCH} -L {CORE_DIR}/pcsx_rearmed_libretro.so "{{file.path}}"',
         "psx": f'{RETROARCH} -L {CORE_DIR}/pcsx_rearmed_libretro.so "{{file.path}}"',
         "psp": f'{RETROARCH} -L {CORE_DIR}/ppsspp_libretro.so "{{file.path}}"',
         "psp": '/opt/emulator/ppsspp/PPSSPP-v1.20.3-anylinux-x86_64.AppImage "{file.path}"',
   
   
         # Sega Saturn / Dreamcast
         # Sega Saturn / Dreamcast
         "saturn": f'{RETROARCH} -L {CORE_DIR}/beetle_saturn_libretro.so "{{file.path}}"',
         "saturn": f'{RETROARCH} -L {CORE_DIR}/mednafen_saturn_libretro.so "{{file.path}}"',
         "dc": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
         "dreamcast": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
   
   
         # Atari
         # Atari
         "lynx": f'{RETROARCH} -L {CORE_DIR}/handy_libretro.so "{{file.path}}"',
         "atarilynx": f'{RETROARCH} -L {CORE_DIR}/mednafen_lynx_libretro.so "{{file.path}}"',
         "jaguar": f'{RETROARCH} -L {CORE_DIR}/virtualjaguar_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": '/opt/emulator/bigpemu/bigpemu "{file.path}"',
        "atarijaguarcd": '/opt/emulator/bigpemu/bigpemu "{file.path}"',
         "atari2600": f'{RETROARCH} -L {CORE_DIR}/stella_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}}"',
         "atari5200": f'{RETROARCH} -L {CORE_DIR}/a5200_libretro.so "{{file.path}}"',
Ligne 395 : Ligne 466 :
         # others
         # others
         "wonderswan": f'{RETROARCH} -L {CORE_DIR}/mednafen_wswan_libretro.so "{{file.path}}"',
         "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}}"',
         "wonderswancolor": f'{RETROARCH} -L {CORE_DIR}/mednafen_wswan_libretro.so "{{file.path}}"',
         "msx": f'{RETROARCH} -L {CORE_DIR}/fmsx_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}}"',
         "msx2": f'{RETROARCH} -L {CORE_DIR}/fmsx_libretro.so "{{file.path}}"',
Ligne 405 : Ligne 476 :
         "c64": f'{RETROARCH} -L {CORE_DIR}/vice_x64_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}}"',
         "c128": f'{RETROARCH} -L {CORE_DIR}/vice_x128_libretro.so "{{file.path}}"',
        "naomi": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
        "naomi2": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
        "triforce": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
         "vic-20": f'{RETROARCH} -L {CORE_DIR}/vice_xvic_libretro.so "{{file.path}}"',
         "vic-20": f'{RETROARCH} -L {CORE_DIR}/vice_xvic_libretro.so "{{file.path}}"',
        "3do": f'{RETROARCH} -L {CORE_DIR}/opera_libretro.so "{{file.path}}"',</nowiki>
     }
     }
     return launches.get(shortname)
     return launches.get(shortname)
Ligne 425 : Ligne 500 :
     if value:
     if value:
         out.write(f"{key}: {value}\n")
         out.write(f"{key}: {value}\n")
def normalize_relpath(path_str: str) -> str:
    path_str = clean(path_str)
    if path_str.startswith("./"):
        path_str = path_str[2:]
    return path_str
def read_m3u_entries(m3u_path: Path):
    """
    Lit un .m3u et retourne un set de chemins relatifs normalisés,
    tels qu'ils apparaissent dans le dossier de la rom.
    """
    entries = set()
    try:
        with m3u_path.open("r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                line = line.strip()
                  if not line or line.startswith("#"):
                    continue
                rel = normalize_relpath(line)
                entries.add(rel)
    except Exception as e:
        print(f"[WARN] Failed to read m3u {m3u_path}: {e}")
    return entries
def build_m3u_referenced_files(games, romdir: Path):
    """
    Retourne l'ensemble des fichiers référencés par les .m3u présents
    dans ce gamelist.xml.
    """
    referenced = set()
    for game in games:
        path = clean(game.findtext("path"))
        if not path:
            continue
        rel = normalize_relpath(path)
        p = romdir / rel
        if p.suffix.lower() == ".m3u" and p.is_file():
            referenced.update(read_m3u_entries(p))
    return referenced
   
   
   
   
Ligne 446 : Ligne 571 :
     collection, shortname = collection_info_from_dir(dirname)
     collection, shortname = collection_info_from_dir(dirname)
     exts = detect_extensions(games)
     exts = detect_extensions(games)
    # Tous les fichiers référencés par des .m3u présents dans ce dossier
    m3u_referenced_files = build_m3u_referenced_files(games, romdir)
    written_count = 0
    skipped_count = 0
   
   
     with out_path.open("w", encoding="utf-8") as out:
     with out_path.open("w", encoding="utf-8") as out:
Ligne 466 : Ligne 597 :
                 continue
                 continue
   
   
             # IMPORTANT: on garde le chemin relatif, comme quand ça marchait
             rel_path = normalize_relpath(path)
             if path.startswith("./"):
             rel_path_posix = Path(rel_path).as_posix()
                path = path[2:]
   
   
             out.write(f"game: {name or Path(path).stem}\n")
            # On skip si ce fichier est référencé par un .m3u
             out.write(f"file: {path}\n")
            # MAIS on ne skip pas le .m3u lui-même
            if rel_path_posix in m3u_referenced_files and Path(rel_path_posix).suffix.lower() != ".m3u":
                skipped_count += 1
                continue
             out.write(f"game: {name or Path(rel_path_posix).stem}\n")
             out.write(f"file: {rel_path_posix}\n")
   
   
             write_line(out, "description", game.findtext("desc"))
             write_line(out, "description", game.findtext("desc"))
Ligne 505 : Ligne 641 :
             if fanart:
             if fanart:
                 out.write(f"assets.background: {fanart}\n")
                 out.write(f"assets.background: {fanart}\n")
            # On garde la vidéo, mais si GameOS spam avec YouTube, commente ces 2 lignes
             if video:
             if video:
                 out.write(f"assets.video: {video}\n")
                 out.write(f"assets.video: {video}\n")
   
   
             out.write("\n")
             out.write("\n")
            written_count += 1
   
   
     print(f"[OK] {gamelist_path} -> {out_path} ({len(games)} games)")
     print(f"[OK] {gamelist_path} -> {out_path} ({written_count} games, {skipped_count} skipped via m3u)")
   
   
   
   
Ligne 532 : Ligne 667 :
  # python3 /mnt/Media/Media/Emulation/gamelist_to_pegasus.py
  # python3 /mnt/Media/Media/Emulation/gamelist_to_pegasus.py


Il faudra penser à télécharger le launcher associé si nécessaire dans Retroarch (Main Menu -> Online Updater -> Core Downloader)
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 <font color = blue>/mes/roms/plateforme/</font>
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 '
            <nowiki>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</nowiki>
    ')"
    [ "$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
 
Variante pour ignorer parenthèses après "(Disc 1)" etc :
#!/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%.*}"
    base="$(printf '%s\n' "$name" | sed -E '
          <nowiki>s/[[:space:]]*-[[:space:]]*(disc|disk|cd)[[:space:]]*[0-9]+([[:space:]]*\([^)]*\))*$//I
        s/[[:space:]]*\((disc|disk|cd)[[:space:]]*[0-9]+\)([[:space:]]*\([^)]*\))*$//I
        s/[[:space:]]+(disc|disk|cd)[[:space:]]*[0-9]+([[:space:]]*\([^)]*\))*$//I
        s/(disc|disk|cd)[[:space:]]*[0-9]+([[:space:]]*\([^)]*\))*$//I</nowiki>
    ')"
    [ "$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
== Spécial Saturn ==
# vi make_m3u.py
 
#!/usr/bin/env python3
from __future__ import annotations
import re
from pathlib import Path
from collections import defaultdict
DELETE_OLD_M3U = True
EXTENSIONS = {".chd", ".rvz"}
# Si un fichier contient un de ces termes, on l'ignore complètement
EXCLUDE_FILE_TERMS = [
    "omake",
    "portrait",
    "interview",
    "making",
    "calendar",
    "yobikake-kun",
    "demo",
    "trial",
    "kawaraban",
    "digital edition",
    "special cd",
    "premium cd",
    "opening disc",
    "extra disc",
    "addition",
    "append",
    "bonus disc",
    "bonus",
    "premium disc",
]
# Groupes qu'on ne veut jamais transformer en .m3u
EXCLUDE_GROUP_TERMS = [
    "private idol",
    "thunder storm & road blaster",
    "time gal & ninja hayate",
    "dungeons & dragons collection",
    "street fighter collection",
    "wangan dead heat + real arrange",
    "virtuacall s",
    "twinkle star sprites",
    "senkutsu katsuryu taisen - chaos seed",
    "pia carrot e youkoso!! 2",
    "mahjong-kyou jidai cebu island",
    "mahjong doukyuusei special",
    "mahjong gakuensai",
    "falcom classics",
    "friends - seishun no kagayaki",
    "last bronx",
    "voice fantasia s - ushinawareta voice power",
    "idol janshi suchie-pai ii",
    "jantei battle cos-player",
    "elf o karu monotachi",
    "elf o karu monotachi ii",
    "daisuki",
    "sengoku blade - sengoku ace episode ii",
    "nanatsu kaze no shima monogatari",
]
# Détection stricte de Disc/CD/Disk + numéro/lettre
DISC_RE = re.compile(
    r'(?i)(?:^|[\s(.\-])(?:disc|disk|cd)\s*([0-9]+|[A-Za-z])(?:[^\w]|$)'
)
# Pour extraire la base commune avant le premier marqueur disque
BASE_PATTERNS = [
    re.compile(r'(?i)^(.*?)[ \t]*-[ \t]*(?:disc|disk|cd)\s*([0-9]+|[A-Za-z])(?:[^\w].*|$)'),
    re.compile(r'(?i)^(.*?)\s*\((?:disc|disk|cd)\s*([0-9]+|[A-Za-z])\)(?:.*|$)'),
    re.compile(r'(?i)^(.*?)\s+(?:disc|disk|cd)\s*([0-9]+|[A-Za-z])(?:[^\w].*|$)'),
]
def norm_spaces(s: str) -> str:
    s = re.sub(r"\s+", " ", s).strip()
    s = re.sub(r"\s+\)", ")", s)
    s = re.sub(r"\(\s+", "(", s)
    return s.strip()
def should_exclude_file(stem: str) -> bool:
    s = stem.lower()
    return any(term in s for term in EXCLUDE_FILE_TERMS)
def should_exclude_group(group: str) -> bool:
    s = group.lower()
      return any(term in s for term in EXCLUDE_GROUP_TERMS)
def has_real_disc_marker(stem: str) -> bool:
    return DISC_RE.search(stem) is not None
def extract_base(stem: str) -> str | None:
    for pat in BASE_PATTERNS:
        m = pat.match(stem)
        if m:
            return norm_spaces(m.group(1))
    return None
def disc_sort_key(stem: str) -> tuple[int, str]:
    m = DISC_RE.search(stem)
    if not m:
        return (9999, stem.lower())
    token = m.group(1)
    if token.isdigit():
        return (int(token), stem.lower())
    return (ord(token.lower()) - 96, stem.lower())
def main() -> None:
    cwd = Path(".")
    if DELETE_OLD_M3U:
        for m3u in cwd.glob("*.m3u"):
            m3u.unlink()
    groups: dict[str, list[Path]] = defaultdict(list)
    seen = set()
    for path in sorted(cwd.iterdir(), key=lambda p: p.name.lower()):
        if not path.is_file():
            continue
        if path.suffix.lower() not in EXTENSIONS:
            continue
        stem = path.stem
        if should_exclude_file(stem):
            continue
        if not has_real_disc_marker(stem):
            continue
        base = extract_base(stem)
        if not base or base == stem:
            continue
        if should_exclude_group(base):
            continue
        key = (base, path.name)
        if key in seen:
            continue
        seen.add(key)
        groups[base].append(path)
    generated = 0
    for base in sorted(groups, key=str.lower):
        files = sorted(groups[base], key=lambda p: disc_sort_key(p.stem))
        if len(files) < 2:
            continue
        outfile = cwd / f"{base}.m3u"
        outfile.write_text(
            "\n".join(p.name for p in files) + "\n",
            encoding="utf-8"
        )
        print(f"Créé : {outfile.name} ({len(files)} disques)")
        generated += 1
    print(f"\nTerminé. {generated} fichier(s) .m3u généré(s).")
if __name__ == "__main__":
    main()
 
# chmod +x make_m3u.py
# ./make_m3u.py
# rm make_m3u.py
 
= 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 <font color = blue>/mon/dossier/</font>
# 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 [http://redump.org/downloads/ à cette adresse] puis on lance le trie :
# python retool.py <font color = blue>maplateforme</font>.dat --exclude a B d D m M o P r u v --output .

Dernière version du 4 avril 2026 à 10:40

Prérequis

  • Alpine LXC avec Nesting et keyctl activé (Docker).

Ressource confortable :

  • 2 CPU
  • 2Gb de RAM
  • 16GB de Disque (Dépend du nombres de roms, peut monter très vite..)

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: 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
      - TASK_TIMEOUT=3600 # plus gros timeout
      - WEB_SERVER_TIMEOUT=3600 # plus gros timeout
    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;
    }
}

Augmenter le timeout du Nginx du conteneur pour grosse BDD de roms

On rentre dans le conteneur :

# docker exec -it romm sh

(Optionnel) On sauvegarde la configuration d'origine :

# cp /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak

On édite la configuration :

# vi /etc/nginx/conf.d/default.conf

Et on ajoute (en bleu) :

...
    # OpenAPI for swagger and redoc
    location /openapi.json {
        proxy_pass http://wsgi_server;
        proxy_connect_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_read_timeout 3600s;
    }

    # Backend api calls
    location /api {
        proxy_pass http://wsgi_server;
        proxy_request_buffering off;
        proxy_buffering off;
        proxy_connect_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_read_timeout 3600s;
    }

    location ~ ^/(ws|netplay) {
        proxy_pass http://wsgi_server;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_connect_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_read_timeout 3600s;
    }
...

Puis on redémarre le service :

# nginx -s reload
# exit

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", "gc"),
        "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"),
        "naomi": ("Sega Naomi", "naomi"),
        "naomi2": ("Sega Naomi 2", "naomi2"),
        "triforce": ("Sega Triforce", "triforce"),
        "3do": ("3DO Interactive Multiplayer", "3do"),
        "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}}"',
         "segacd32": 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_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}}"',
         "gc": '/Applications/dolphin-emu.AppImage --appimage-extract-and-run -b -e "{file.path}"',
         "wii": '/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}}"',
         "gameandwatch": f'{RETROARCH} -L {CORE_DIR}/mame_libretro.so "{{file.path}}"',
 
         # Sony
         "psx": f'{RETROARCH} -L {CORE_DIR}/pcsx_rearmed_libretro.so "{{file.path}}"',
         "psp": '/opt/emulator/ppsspp/PPSSPP-v1.20.3-anylinux-x86_64.AppImage "{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": '/opt/emulator/bigpemu/bigpemu "{file.path}"',
         "atarijaguarcd": '/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}}"',
         "naomi": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
         "naomi2": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
         "triforce": f'{RETROARCH} -L {CORE_DIR}/flycast_libretro.so "{{file.path}}"',
         "vic-20": f'{RETROARCH} -L {CORE_DIR}/vice_xvic_libretro.so "{{file.path}}"',
         "3do": f'{RETROARCH} -L {CORE_DIR}/opera_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 normalize_relpath(path_str: str) -> str:
    path_str = clean(path_str)
    if path_str.startswith("./"):
        path_str = path_str[2:]
    return path_str


def read_m3u_entries(m3u_path: Path):
    """
    Lit un .m3u et retourne un set de chemins relatifs normalisés,
    tels qu'ils apparaissent dans le dossier de la rom.
    """
    entries = set()

    try:
        with m3u_path.open("r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                line = line.strip()
                 if not line or line.startswith("#"):
                    continue

                rel = normalize_relpath(line)
                entries.add(rel)
    except Exception as e:
        print(f"[WARN] Failed to read m3u {m3u_path}: {e}")

    return entries


def build_m3u_referenced_files(games, romdir: Path):
    """
    Retourne l'ensemble des fichiers référencés par les .m3u présents
    dans ce gamelist.xml.
    """
    referenced = set()

    for game in games:
        path = clean(game.findtext("path"))
        if not path:
            continue

        rel = normalize_relpath(path)
        p = romdir / rel

        if p.suffix.lower() == ".m3u" and p.is_file():
            referenced.update(read_m3u_entries(p))

    return referenced


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)

    # Tous les fichiers référencés par des .m3u présents dans ce dossier
    m3u_referenced_files = build_m3u_referenced_files(games, romdir)

    written_count = 0
    skipped_count = 0

    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

            rel_path = normalize_relpath(path)
            rel_path_posix = Path(rel_path).as_posix()

            # On skip si ce fichier est référencé par un .m3u
            # MAIS on ne skip pas le .m3u lui-même
            if rel_path_posix in m3u_referenced_files and Path(rel_path_posix).suffix.lower() != ".m3u":
                skipped_count += 1
                continue

            out.write(f"game: {name or Path(rel_path_posix).stem}\n")
            out.write(f"file: {rel_path_posix}\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")
            if video:
                out.write(f"assets.video: {video}\n")

            out.write("\n")
            written_count += 1

    print(f"[OK] {gamelist_path} -> {out_path} ({written_count} games, {skipped_count} skipped via m3u)")


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

Variante pour ignorer parenthèses après "(Disc 1)" etc :

#!/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%.*}"

    base="$(printf '%s\n' "$name" | sed -E '
         s/[[:space:]]*-[[:space:]]*(disc|disk|cd)[[:space:]]*[0-9]+([[:space:]]*\([^)]*\))*$//I
         s/[[:space:]]*\((disc|disk|cd)[[:space:]]*[0-9]+\)([[:space:]]*\([^)]*\))*$//I
         s/[[:space:]]+(disc|disk|cd)[[:space:]]*[0-9]+([[:space:]]*\([^)]*\))*$//I
         s/(disc|disk|cd)[[:space:]]*[0-9]+([[:space:]]*\([^)]*\))*$//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

Spécial Saturn

# vi make_m3u.py
#!/usr/bin/env python3
from __future__ import annotations

import re
from pathlib import Path
from collections import defaultdict

DELETE_OLD_M3U = True
EXTENSIONS = {".chd", ".rvz"}

# Si un fichier contient un de ces termes, on l'ignore complètement
EXCLUDE_FILE_TERMS = [
    "omake",
    "portrait",
    "interview",
    "making",
    "calendar",
    "yobikake-kun",
    "demo",
    "trial",
    "kawaraban",
    "digital edition",
    "special cd",
    "premium cd",
    "opening disc",
    "extra disc",
    "addition",
    "append",
    "bonus disc",
    "bonus",
    "premium disc",
]

# Groupes qu'on ne veut jamais transformer en .m3u
EXCLUDE_GROUP_TERMS = [
    "private idol",
    "thunder storm & road blaster",
    "time gal & ninja hayate",
    "dungeons & dragons collection",
    "street fighter collection",
    "wangan dead heat + real arrange",
    "virtuacall s",
    "twinkle star sprites",
    "senkutsu katsuryu taisen - chaos seed",
    "pia carrot e youkoso!! 2",
    "mahjong-kyou jidai cebu island",
    "mahjong doukyuusei special",
    "mahjong gakuensai",
    "falcom classics",
    "friends - seishun no kagayaki",
    "last bronx",
    "voice fantasia s - ushinawareta voice power",
    "idol janshi suchie-pai ii",
    "jantei battle cos-player",
    "elf o karu monotachi",
    "elf o karu monotachi ii",
    "daisuki",
    "sengoku blade - sengoku ace episode ii",
    "nanatsu kaze no shima monogatari",
]

# Détection stricte de Disc/CD/Disk + numéro/lettre
DISC_RE = re.compile(
    r'(?i)(?:^|[\s(.\-])(?:disc|disk|cd)\s*([0-9]+|[A-Za-z])(?:[^\w]|$)'
)

# Pour extraire la base commune avant le premier marqueur disque
BASE_PATTERNS = [
    re.compile(r'(?i)^(.*?)[ \t]*-[ \t]*(?:disc|disk|cd)\s*([0-9]+|[A-Za-z])(?:[^\w].*|$)'),
    re.compile(r'(?i)^(.*?)\s*\((?:disc|disk|cd)\s*([0-9]+|[A-Za-z])\)(?:.*|$)'),
    re.compile(r'(?i)^(.*?)\s+(?:disc|disk|cd)\s*([0-9]+|[A-Za-z])(?:[^\w].*|$)'),
]

def norm_spaces(s: str) -> str:
    s = re.sub(r"\s+", " ", s).strip()
    s = re.sub(r"\s+\)", ")", s)
    s = re.sub(r"\(\s+", "(", s)
    return s.strip()

def should_exclude_file(stem: str) -> bool:
    s = stem.lower()
    return any(term in s for term in EXCLUDE_FILE_TERMS)

def should_exclude_group(group: str) -> bool:
   s = group.lower()
     return any(term in s for term in EXCLUDE_GROUP_TERMS)

def has_real_disc_marker(stem: str) -> bool:
    return DISC_RE.search(stem) is not None

def extract_base(stem: str) -> str | None:
    for pat in BASE_PATTERNS:
        m = pat.match(stem)
        if m:
            return norm_spaces(m.group(1))
    return None

def disc_sort_key(stem: str) -> tuple[int, str]:
    m = DISC_RE.search(stem)
    if not m:
        return (9999, stem.lower())
    token = m.group(1)
    if token.isdigit():
        return (int(token), stem.lower())
    return (ord(token.lower()) - 96, stem.lower())

def main() -> None:
    cwd = Path(".")

    if DELETE_OLD_M3U:
        for m3u in cwd.glob("*.m3u"):
            m3u.unlink()

    groups: dict[str, list[Path]] = defaultdict(list)
    seen = set()

    for path in sorted(cwd.iterdir(), key=lambda p: p.name.lower()):
        if not path.is_file():
            continue
        if path.suffix.lower() not in EXTENSIONS:
            continue

        stem = path.stem

        if should_exclude_file(stem):
            continue
        if not has_real_disc_marker(stem):
            continue

        base = extract_base(stem)
        if not base or base == stem:
            continue
        if should_exclude_group(base):
            continue

        key = (base, path.name)
        if key in seen:
            continue
        seen.add(key)

        groups[base].append(path)

    generated = 0

    for base in sorted(groups, key=str.lower):
        files = sorted(groups[base], key=lambda p: disc_sort_key(p.stem))
        if len(files) < 2:
            continue

        outfile = cwd / f"{base}.m3u"
        outfile.write_text(
            "\n".join(p.name for p in files) + "\n",
            encoding="utf-8"
        )
        print(f"Créé : {outfile.name} ({len(files)} disques)")
        generated += 1

    print(f"\nTerminé. {generated} fichier(s) .m3u généré(s).")

if __name__ == "__main__":
    main()
# chmod +x make_m3u.py
# ./make_m3u.py
# rm make_m3u.py

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 .