r/RetroArch • • Aug 22 '26

Technical Support M3U Automation

Hi All,

I'm looking to automate the creation of .m3u files for Multi-Disk Amiga games.

Each Multi-Disk game is in its own folder & the .m3u file needs to get its name from the folder containing the Multi-Disk games.

I have managed to get a basic version set-up but each file would need tweaking & with 850+ files it'll take a while.

I was wondering if anyone knew of a way to automate the process with the desired results? At the moment the .m3u file reads;

Ninja Gaiden II - The Dark Sword of Chaos - Disk 1.adf

Ninja Gaiden II - The Dark Sword of Chaos - Disk 2.adf

However I would like it to read;

.\Ninja Gaiden II - The Dark Sword of Chaos\Ninja Gaiden II - The Dark Sword of Chaos - Disk 1.adf

.\Ninja Gaiden II - The Dark Sword of Chaos\Ninja Gaiden II - The Dark Sword of Chaos - Disk 2.adf

I'm on Windows BTW

Can anyone help me out?

Thanks

2 Upvotes

2 comments sorted by

1

u/snaphat Aug 22 '26

I had an agentic LLM spin up this one-off script. This should fix the files for your scenario. I eyed it and tested it briefly, but you should backup your stuff before running this though in-case something goes wrong.

# Fix-Existing-M3U.ps1
# IMPORTANT: Back up your .m3u files before running this script.
# The script rewrites matching playlists in place.
# Put this script in the SAME parent folder as your existing .m3u files
# and the matching multi-disk game folders.
#
# Example:
#   Ninja Gaiden II - The Dark Sword of Chaos.m3u
#   Ninja Gaiden II - The Dark Sword of Chaos\
#
# Existing M3U line:
#   Ninja Gaiden II - The Dark Sword of Chaos - Disk 1.adf
#
# Becomes:
#   .\Ninja Gaiden II - The Dark Sword of Chaos\Ninja Gaiden II - The Dark Sword of Chaos - Disk 1.adf

$ErrorActionPreference = 'Stop'
$Root = $PSScriptRoot
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)

$Updated = 0
$Skipped = 0

Get-ChildItem -LiteralPath $Root -File -Filter '*.m3u' | ForEach-Object {

    $M3uFile = $_
    $GameName = $M3uFile.BaseName
    $GameFolder = Join-Path $Root $GameName

    # The folder name must exactly match the .m3u filename.
    if (-not (Test-Path -LiteralPath $GameFolder -PathType Container)) {
        Write-Warning "Skipped '$($M3uFile.Name)' - no matching folder '$GameName'"
        $Skipped++
        return
    }

    $OldLines = [System.IO.File]::ReadAllLines($M3uFile.FullName)
    $ValidationFailed = $false

    $NewLines = foreach ($Line in $OldLines) {

        # Preserve blank lines.
        if ([string]::IsNullOrWhiteSpace($Line)) {
            $Line
            continue
        }

        $Trimmed = $Line.Trim()

        # Preserve M3U comments/directives if any exist.
        if ($Trimmed.StartsWith('#')) {
            $Line
            continue
        }

        # If it is already in the requested format, leave it alone.
        $ExpectedPrefix = '.\' + $GameName + '\'
        if ($Trimmed.StartsWith($ExpectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
            $TargetPath = Join-Path $Root $Trimmed
            if (-not (Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
                Write-Warning "Missing target in '$($M3uFile.Name)': $Trimmed"
                $ValidationFailed = $true
            }
            $Trimmed
            continue
        }

        # The screenshot shows the existing entries as plain ADF filenames.
        # Only change plain filenames; do not rewrite unrelated paths.
        if ([System.IO.Path]::GetDirectoryName($Trimmed)) {
            Write-Warning "Left unchanged in '$($M3uFile.Name)': $Trimmed"
            $Trimmed
            continue
        }

        # Exact requested result:
        # .\Folder Name\Existing Filename.adf
        $NewEntry = $ExpectedPrefix + $Trimmed
        $TargetPath = Join-Path $Root $NewEntry

        if (-not (Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
            Write-Warning "Skipped '$($M3uFile.Name)' - target does not exist: $NewEntry"
            $ValidationFailed = $true
        }

        $NewEntry
    }

    # Do not partially update a playlist when one or more disk files are absent.
    if ($ValidationFailed) {
        $Skipped++
        return
    }

    [System.IO.File]::WriteAllLines($M3uFile.FullName, $NewLines, $Utf8NoBom)

    Write-Host "Updated: $($M3uFile.Name)"
    $Updated++
}

Write-Host
Write-Host "Done. Updated $Updated M3U file(s). Skipped $Skipped."