diff --git a/scripts/igir-report.ps1 b/scripts/igir-report.ps1 new file mode 100644 index 0000000..3b104fc --- /dev/null +++ b/scripts/igir-report.ps1 @@ -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 (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 diff --git a/scripts/igir-strip-snes-headers.sh b/scripts/igir-strip-snes-headers.sh new file mode 100644 index 0000000..065c616 --- /dev/null +++ b/scripts/igir-strip-snes-headers.sh @@ -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/-headered-.tar.gz \ +# -C /storage1/Emulation/roms/ +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"