« ROMM » : différence entre les versions
Aucun résumé des modifications |
Aucun résumé des modifications Balise : Révocation manuelle |
||
| (18 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: | image: rommapp/romm:latest | ||
container_name: romm | container_name: romm | ||
restart: unless-stopped | restart: unless-stopped | ||
| Ligne 87 : | Ligne 87 : | ||
- SCAN_TIMEOUT=86400 # timeout de 24h | - SCAN_TIMEOUT=86400 # timeout de 24h | ||
- SCAN_WORKERS=4 # limite IGBD | - 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 250 : | 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 = | ||
{{Méta bandeau | {{Méta bandeau | ||
| Ligne 267 : | Ligne 314 : | ||
from pathlib import Path | from pathlib import Path | ||
ROOT = Path(" | 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 = | ADD_LAUNCH = True | ||
RETROARCH = "retroarch --fullscreen" | RETROARCH = "retroarch --fullscreen" | ||
CORE_DIR = "/home/retro/.config/retroarch/cores" | CORE_DIR = "/home/retro/.config/retroarch/cores" | ||
| Ligne 321 : | Ligne 368 : | ||
"n64": ("Nintendo 64", "n64"), | "n64": ("Nintendo 64", "n64"), | ||
"nds": ("Nintendo DS", "nds"), | "nds": ("Nintendo DS", "nds"), | ||
"ngc": ("Nintendo GameCube", " | "ngc": ("Nintendo GameCube", "gc"), | ||
"wii": ("Wii", "wii"), | "wii": ("Wii", "wii"), | ||
"lynx": ("Atari Lynx", "atarilynx"), | "lynx": ("Atari Lynx", "atarilynx"), | ||
| Ligne 346 : | 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"), | "g-and-w": ("Game & Watch", "gameandwatch"), | ||
} | } | ||
| Ligne 357 : | Ligne 408 : | ||
def launch_for_shortname(shortname: str): | def launch_for_shortname(shortname: str): | ||
launches = { | launches = { | ||
<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}}"', | ||
| Ligne 368 : | Ligne 419 : | ||
"megadrive": 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}}"', | ||
"sega32x": 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}}"', | "mastersystem": f'{RETROARCH} -L {CORE_DIR}/genesis_plus_gx_libretro.so "{{file.path}}"', | ||
| Ligne 378 : | Ligne 429 : | ||
"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}}"', | ||
"tg-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}/ | "supergrafx": f'{RETROARCH} -L {CORE_DIR}/mednafen_pce_libretro.so "{{file.path}}"', | ||
# Nintendo | # Nintendo | ||
| Ligne 389 : | Ligne 440 : | ||
"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}/melondsds_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": | "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}/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": | "psp": '/opt/emulator/ppsspp/PPSSPP-v1.20.3-anylinux-x86_64.AppImage "{file.path}"', | ||
# Sega Saturn / Dreamcast | # Sega Saturn / Dreamcast | ||
| Ligne 404 : | Ligne 456 : | ||
# Atari | # Atari | ||
"atarilynx": f'{RETROARCH} -L {CORE_DIR}/mednafen_lynx_libretro.so "{{file.path}}"', | "atarilynx": f'{RETROARCH} -L {CORE_DIR}/mednafen_lynx_libretro.so "{{file.path}}"', | ||
"atarijaguar": 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}}"', | #"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 422 : | 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}}"', | ||
"vic-20": f'{RETROARCH} -L {CORE_DIR}/vice_xvic_libretro.so "{{file.path}}"',</nowiki> | "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}}"',</nowiki> | |||
} | } | ||
return launches.get(shortname) | return launches.get(shortname) | ||
| Ligne 442 : | 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 463 : | 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 483 : | Ligne 597 : | ||
continue | continue | ||
# | rel_path = normalize_relpath(path) | ||
if | 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( | out.write(f"game: {name or Path(rel_path_posix).stem}\n") | ||
out.write(f"file: { | out.write(f"file: {rel_path_posix}\n") | ||
write_line(out, "description", game.findtext("desc")) | write_line(out, "description", game.findtext("desc")) | ||
| Ligne 522 : | Ligne 641 : | ||
if fanart: | if fanart: | ||
out.write(f"assets.background: {fanart}\n") | out.write(f"assets.background: {fanart}\n") | ||
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} ({ | print(f"[OK] {gamelist_path} -> {out_path} ({written_count} games, {skipped_count} skipped via m3u)") | ||
| Ligne 594 : | Ligne 712 : | ||
printf '%s' "${groups["$group"]}" | sed '/^$/d' | sort -V > "$group.m3u" | printf '%s' "${groups["$group"]}" | sed '/^$/d' | sort -V > "$group.m3u" | ||
done | 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 = | = 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 : | Si nécessaire il faut installer les outils de MAME pour avoir chdman : | ||
| Ligne 706 : | Ligne 1 029 : | ||
filters: | filters: | ||
On récupère le ficher .dat correspondant à la plateforme à trier [http://redump.org/downloads/ à cette adresse] puis on lance le trie : | 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 .