feat(roms): add igir report + SNES header-strip ops scripts
igir-report.ps1: read-only igir report over the EmuDeck tree on valhalla, matching ROMs by checksum against libretro's No-Intro + Redump DAT sets (sparse-cloned + cached). Scoped (-Platforms) or whole-library (-All); CSV per run under /storage1/igir/reports. Uses node:lts (alpine breaks igir on musl) and metadat/no-intro (not the curated dat/ folder). igir-strip-snes-headers.sh: reversible backup-then-strip of 512-byte SNES copier headers (size%1024==512) that igir can't auto-detect, renaming to .sfc so headered ROMs match headerless No-Intro.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# scripts/igir-report.ps1
|
||||
#
|
||||
# Run the igir ROM collection manager in REPORT mode (read-only) against the
|
||||
# EmuDeck tree at /storage1/Emulation/roms on valhalla, matching ROMs by
|
||||
# checksum against the full No-Intro + Redump DAT sets. Produces a CSV per run
|
||||
# under /storage1/igir/reports listing FOUND / MISSING / UNUSED per system.
|
||||
#
|
||||
# Nothing is moved, renamed, or deleted — the ROM tree is mounted read-only.
|
||||
#
|
||||
# ./scripts/igir-report.ps1 -Platforms gba,snes,n64,psx # scoped run (fast)
|
||||
# ./scripts/igir-report.ps1 -All # whole library (SLOW: hashes everything)
|
||||
# ./scripts/igir-report.ps1 -Platforms snes -UpdateDats # refresh the cached DATs first
|
||||
#
|
||||
# Gotchas baked in (all learned the hard way — see memory igir-roms):
|
||||
# * Uses the Debian `node:lts` image, NOT node:lts-alpine (igir silently
|
||||
# dies on musl).
|
||||
# * DATs come from libretro-database `metadat/no-intro` + `metadat/redump`
|
||||
# (full sets with checksums), NOT the tiny curated top-level `dat/` folder.
|
||||
# * SNES `.smc` copier headers (size%1024==512) are NOT auto-detected by igir
|
||||
# and won't match headerless No-Intro until physically stripped. The `snes`
|
||||
# folder was stripped 2026-06-06; if you add more headered SNES ROMs, strip
|
||||
# them first (see scripts/igir-strip-snes-headers.sh) or they'll show UNUSED.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Platforms,
|
||||
[switch]$All,
|
||||
[switch]$UpdateDats,
|
||||
[string]$Hostname = 'valhalla'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PSNativeCommandUseErrorActionPreference = $true
|
||||
$server = "ginnoir@$Hostname"
|
||||
|
||||
if (-not $All -and (-not $Platforms -or $Platforms.Count -eq 0)) {
|
||||
Write-Error "Specify -Platforms <name,name,...> (e.g. -Platforms gba,snes) or -All for the whole library."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build the igir -i input args. -All scans the entire roms tree (heavy);
|
||||
# otherwise one -i per named platform folder.
|
||||
if ($All) {
|
||||
$inputArgs = '-i /input'
|
||||
} else {
|
||||
$inputArgs = ($Platforms | ForEach-Object { "-i /input/$_" }) -join ' '
|
||||
}
|
||||
$doPull = if ($UpdateDats) { '1' } else { '0' }
|
||||
|
||||
# Remote logic runs on valhalla. Single-quoted here-string so PowerShell leaves
|
||||
# $vars for bash; INPUT_ARGS / DOPULL are injected below.
|
||||
$remote = @'
|
||||
set -uo pipefail
|
||||
DATS=/storage1/igir/dats/libretro-database
|
||||
REPORTS=/storage1/igir/reports
|
||||
mkdir -p "$REPORTS" /storage1/igir/dats
|
||||
|
||||
# Sparse, blobless clone of just the No-Intro + Redump DAT dirs (cached & reused).
|
||||
if [ ! -d "$DATS/.git" ]; then
|
||||
echo ">> Fetching libretro-database DATs (one-time sparse clone)..."
|
||||
docker run --rm -v /storage1/igir:/w -w /w node:lts sh -c '
|
||||
git clone --filter=blob:none --sparse --depth 1 \
|
||||
https://github.com/libretro/libretro-database dats/libretro-database &&
|
||||
cd dats/libretro-database &&
|
||||
git sparse-checkout set metadat/no-intro metadat/redump'
|
||||
elif [ "__DOPULL__" = "1" ]; then
|
||||
echo ">> Updating cached DATs..."
|
||||
docker run --rm -v /storage1/igir:/w -w /w/dats/libretro-database node:lts git pull -q
|
||||
fi
|
||||
|
||||
NI=$(find "$DATS/metadat/no-intro" -name '*.dat' | wc -l)
|
||||
RD=$(find "$DATS/metadat/redump" -name '*.dat' | wc -l)
|
||||
echo ">> DATs available: $NI No-Intro, $RD Redump"
|
||||
|
||||
STAMP=$(date +%Y-%m-%dT%H-%M-%S)
|
||||
OUT="igir-report-$STAMP.csv"
|
||||
echo ">> Running igir report -> $REPORTS/$OUT"
|
||||
docker run --rm \
|
||||
-v /storage1/Emulation/roms:/input:ro \
|
||||
-v "$DATS":/dats:ro \
|
||||
-v "$REPORTS":/output \
|
||||
node:lts npx -y igir@latest report \
|
||||
-d /dats/metadat/no-intro -d /dats/metadat/redump \
|
||||
__INPUT_ARGS__ \
|
||||
--report-output "/output/$OUT" 2>&1 | grep -vE 'npm notice|npm warn' || true
|
||||
echo ">> Done. CSV: /storage1/igir/reports/$OUT"
|
||||
'@
|
||||
|
||||
$remote = $remote.Replace('__INPUT_ARGS__', $inputArgs).Replace('__DOPULL__', $doPull)
|
||||
# Strip CR so the remote bash never sees stray \r (this file may be CRLF on disk).
|
||||
$remote = $remote.Replace("`r", '')
|
||||
|
||||
Write-Host "igir report on $server :: $(if ($All) {'ALL platforms'} else {$Platforms -join ', '})"
|
||||
ssh $server $remote
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/igir-strip-snes-headers.sh (runs ON valhalla)
|
||||
#
|
||||
# SNES copier ROMs carry a 512-byte header (file size % 1024 == 512). igir
|
||||
# detects SNES headers by content *signature*, which these copier headers lack,
|
||||
# so it can't strip them and they never match headerless No-Intro DATs — even
|
||||
# though their headerless checksum IS in No-Intro. This script blind-strips that
|
||||
# 512-byte header (the universally-correct fix for SNES) and renames to .sfc.
|
||||
#
|
||||
# It is a MUTATION of the live EmuDeck tree (RomM + EmuDeck read it). Every
|
||||
# touched file is tar'd to a backup first, so the change is fully reversible.
|
||||
#
|
||||
# Usage (default folder = snes):
|
||||
# ssh ginnoir@valhalla 'bash -s' < scripts/igir-strip-snes-headers.sh
|
||||
# ssh ginnoir@valhalla 'bash -s -- sfc' < scripts/igir-strip-snes-headers.sh
|
||||
# ssh ginnoir@valhalla 'bash -s -- snes 1' < scripts/igir-strip-snes-headers.sh # 2nd arg=1 => dry run
|
||||
#
|
||||
# Restore from a backup:
|
||||
# tar xzf /storage1/igir/backup/<folder>-headered-<stamp>.tar.gz \
|
||||
# -C /storage1/Emulation/roms/<folder>
|
||||
set -uo pipefail
|
||||
|
||||
FOLDER="${1:-snes}"
|
||||
DRYRUN="${2:-0}"
|
||||
ROOT="/storage1/Emulation/roms/$FOLDER"
|
||||
BK="/storage1/igir/backup"
|
||||
STAMP="$(date +%Y-%m-%d)"
|
||||
|
||||
[ -d "$ROOT" ] || { echo "No such folder: $ROOT"; exit 1; }
|
||||
mkdir -p "$BK"
|
||||
cd "$ROOT" || exit 1
|
||||
shopt -s nullglob nocaseglob
|
||||
|
||||
# Collect headered SNES ROMs (size % 1024 == 512).
|
||||
list="$(mktemp)"; : > "$list"
|
||||
for f in *.smc *.sfc *.fig *.swc; do
|
||||
[ -f "$f" ] || continue
|
||||
s=$(stat -c %s "$f")
|
||||
[ $((s % 1024)) -eq 512 ] && printf '%s\0' "$f" >> "$list"
|
||||
done
|
||||
n=$(grep -zc . "$list" || true)
|
||||
echo "Headered files in '$FOLDER': $n"
|
||||
if [ "$n" -eq 0 ]; then echo "Nothing to strip."; rm -f "$list"; exit 0; fi
|
||||
|
||||
if [ "$DRYRUN" = "1" ]; then
|
||||
echo "(dry run) would back up to $BK/$FOLDER-headered-$STAMP.tar.gz and strip $n files."
|
||||
rm -f "$list"; exit 0
|
||||
fi
|
||||
|
||||
# Back up originals before mutating.
|
||||
bkfile="$BK/$FOLDER-headered-$STAMP.tar.gz"
|
||||
tar --null -czf "$bkfile" -T "$list"
|
||||
echo "Backup: $bkfile ($(stat -c %s "$bkfile") bytes, $(tar tzf "$bkfile" | wc -l) files)"
|
||||
|
||||
# Strip: drop first 512 bytes, verify new size, write as .sfc, remove old.
|
||||
stripped=0; errors=0
|
||||
while IFS= read -r -d '' f; do
|
||||
s=$(stat -c %s "$f")
|
||||
base="${f%.*}"; target="$base.sfc"; tmp="$f.hl.tmp"
|
||||
tail -c +513 "$f" > "$tmp"
|
||||
ns=$(stat -c %s "$tmp")
|
||||
if [ "$ns" -ne $((s - 512)) ]; then
|
||||
echo "SIZE MISMATCH, skipped: $f ($s -> $ns)"; rm -f "$tmp"; errors=$((errors+1)); continue
|
||||
fi
|
||||
if [ "$target" != "$f" ] && [ -e "$target" ]; then target="$base (headerless).sfc"; fi
|
||||
mv -f "$tmp" "$target"
|
||||
[ "$target" != "$f" ] && rm -f "$f"
|
||||
stripped=$((stripped+1))
|
||||
done < "$list"
|
||||
rm -f "$list"
|
||||
|
||||
# Verify none remain.
|
||||
left=0
|
||||
for f in *.smc *.sfc *.fig *.swc; do
|
||||
[ -f "$f" ] || continue
|
||||
s=$(stat -c %s "$f")
|
||||
[ $((s % 1024)) -eq 512 ] && left=$((left+1))
|
||||
done
|
||||
echo "RESULT stripped=$stripped errors=$errors remaining_headered=$left"
|
||||
Reference in New Issue
Block a user