Last active
December 30, 2025 18:04
-
-
Save Solessfir/cf0a22b30c6459f9d7436f6678bb8ad4 to your computer and use it in GitHub Desktop.
PowerShell script to batch convert FLAC to MP3 using FFmpeg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Recursively converts FLAC tracks to MP3 320 kbps using FFmpeg. | |
| Usage: | |
| .\convert.ps1 | Converts all FLACs in the current directory to 320k MP3 in a "_Converted" subfolder. | |
| .\convert.ps1 -DestinationRoot "D:\Music\MP3" -Bitrate 256k | Converts files to the specified path at 256kbps. | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Alias("h", "help")] | |
| [switch]$ShowHelp, | |
| [Parameter(Position=0)] | |
| [string]$DestinationRoot, | |
| [Parameter(Position=1)] | |
| [ValidatePattern("^\d+k$")] | |
| [string]$Bitrate = "320k" | |
| ) | |
| if ($ShowHelp) { | |
| Get-Help $PSCommandPath | |
| exit | |
| } | |
| if (!(Get-Command ffmpeg -ErrorAction SilentlyContinue)) { | |
| Write-Host "Error: FFmpeg was not found in your system path." -ForegroundColor Red | |
| Write-Host "Please install it using the following command (Admin PowerShell):" -ForegroundColor Yellow | |
| Write-Host "`n winget install --id Gyan.FFmpeg`n" -ForegroundColor Cyan | |
| return | |
| } | |
| $sourceRoot = (Get-Location).Path | |
| if ([string]::IsNullOrWhiteSpace($DestinationRoot)) { | |
| $DestinationRoot = Join-Path $sourceRoot "_Converted" | |
| } | |
| if (!(Test-Path $DestinationRoot)) { | |
| New-Item -ItemType Directory -Path $DestinationRoot -Force | Out-Null | |
| } | |
| Write-Host "--- Audio Conversion Started ---" -ForegroundColor Cyan | |
| Write-Host "Source: $sourceRoot" | |
| Write-Host "Destination: $DestinationRoot" | |
| Write-Host "Bitrate: $Bitrate" -ForegroundColor Cyan | |
| # Get all unique folders containing FLAC files | |
| $albumDirs = Get-ChildItem -Path $sourceRoot -Recurse -File -Filter *.flac | | |
| Select-Object -ExpandProperty DirectoryName -Unique | | |
| Where-Object { $_ -notlike "*$DestinationRoot*" } | |
| foreach ($dir in $albumDirs) { | |
| # 1. Path Calculation and Folder Renaming | |
| $relativeDir = "" | |
| if ($dir.Length -gt $sourceRoot.Length) { | |
| $relativeDir = $dir.Substring($sourceRoot.Length).TrimStart([IO.Path]::DirectorySeparatorChar) | |
| } | |
| # "Year Album" -> "Year - Album" | |
| $pathSegments = $relativeDir.Split([IO.Path]::DirectorySeparatorChar) | |
| $renamedSegments = $pathSegments | ForEach-Object { | |
| $_ -replace '^(\d{4})\s+(?!-\s)', '$1 - ' | |
| } | |
| $newRelativeDir = $renamedSegments -join [IO.Path]::DirectorySeparatorChar | |
| $targetDir = Join-Path $DestinationRoot $newRelativeDir | |
| if (!(Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null } | |
| Write-Host "`nFolder: $relativeDir" -ForegroundColor Magenta | |
| # 2. Cover Art Handling (.jpg, .jpeg, .png) | |
| $artFile = Get-ChildItem -Path $dir -File | Where-Object { | |
| $_.BaseName -eq "Cover" -and ($_.Extension -match '^\.(jpg|jpeg|png)$') | |
| } | Select-Object -First 1 | |
| if ($artFile) { | |
| Copy-Item -Path $artFile.FullName -Destination (Join-Path $targetDir $artFile.Name) -Force | |
| Write-Host " [OK] Cover art: $($artFile.Name)" -ForegroundColor Yellow | |
| } | |
| # 3. Process FLAC files | |
| $flacsInDir = Get-ChildItem -Path $dir -Filter *.flac | |
| foreach ($flac in $flacsInDir) { | |
| $matchingCue = Join-Path $dir ($flac.BaseName + ".cue") | |
| if (Test-Path $matchingCue) { | |
| # --- SCENARIO: MONOLITHIC FLAC --- | |
| Write-Host " [CUE] Splitting image: $($flac.Name)" -ForegroundColor Cyan | |
| $globalMetadata = @{ PERFORMER = "Unknown Artist"; TITLE = $flac.BaseName; DATE = "" } | |
| $tracks = @() | |
| $currentTrack = $null | |
| Get-Content $matchingCue -Encoding UTF8 | ForEach-Object { | |
| $line = $_.Trim() | |
| if ($line -match '^REM DATE (\d+)') { $globalMetadata.DATE = $matches[1] } | |
| elseif ($line -match '^PERFORMER "(.*)"') { if ($currentTrack) { $currentTrack.Artist = $matches[1] } else { $globalMetadata.PERFORMER = $matches[1] } } | |
| elseif ($line -match '^TITLE "(.*)"') { if ($currentTrack) { $currentTrack.Title = $matches[1] } else { $globalMetadata.TITLE = $matches[1] } } | |
| elseif ($line -match '^TRACK (\d+) AUDIO') { | |
| if ($currentTrack) { $tracks += $currentTrack } | |
| $currentTrack = [ordered]@{ Number = $matches[1]; Title = "Track $($matches[1])"; Artist = $globalMetadata.PERFORMER; Start = 0 } | |
| } | |
| elseif ($line -match 'INDEX 01 (\d+):(\d+):(\d+)') { | |
| $currentTrack.Start = ([int]$matches[1] * 60) + [int]$matches[2] + ([int]$matches[3] / 75.0) | |
| } | |
| } | |
| if ($currentTrack) { $tracks += $currentTrack } | |
| for ($i = 0; $i -lt $tracks.Count; $i++) { | |
| $t = $tracks[$i] | |
| $startStr = $t.Start.ToString("0.000", [System.Globalization.CultureInfo]::InvariantCulture) | |
| $duration = if ($i -lt $tracks.Count - 1) { $tracks[$i+1].Start - $t.Start } else { $null } | |
| $outName = "$($t.Number). $($t.Title -replace '[\\/:*?"<>|]', '_').mp3" | |
| $outPath = Join-Path $targetDir $outName | |
| $args = @("-ss", $startStr, "-i", "`"$($flac.FullName)`"") | |
| if ($artFile) { $args += @("-i", "`"$($artFile.FullName)`"") } | |
| $args += @("-map", "0:a") | |
| if ($artFile) { $args += @("-map", "1:v", "-c:v", "copy", "-disposition:v", "attached_pic") } | |
| $args += @("-ab", $Bitrate, "-id3v2_version", "3", "-metadata", "title=$($t.Title)", "-metadata", "artist=$($t.Artist)", "-metadata", "album=$($globalMetadata.TITLE)", "-metadata", "date=$($globalMetadata.DATE)", "-metadata", "track=$($t.Number)/$($tracks.Count)") | |
| if ($duration) { $args += @("-t", $duration.ToString("0.000", [System.Globalization.CultureInfo]::InvariantCulture)) } | |
| $args += "`"$outPath`"" | |
| cmd /c "ffmpeg -hide_banner -loglevel error -y $args" | |
| } | |
| } | |
| else { | |
| # --- SCENARIO: SEPARATE TRACKS --- | |
| Write-Host " [FILE] Converting: $($flac.Name)" -ForegroundColor Green | |
| $outPath = Join-Path $targetDir ($flac.BaseName + ".mp3") | |
| $args = @("-i", "`"$($flac.FullName)`"") | |
| if ($artFile) { $args += @("-i", "`"$($artFile.FullName)`"") } | |
| $args += @("-map", "0:a") | |
| if ($artFile) { $args += @("-map", "1:v", "-c:v", "copy", "-disposition:v", "attached_pic") } | |
| $args += @("-ab", $Bitrate, "-map_metadata", "0", "-id3v2_version", "3", "-y", "`"$outPath`"") | |
| cmd /c "ffmpeg -hide_banner -loglevel error $args" | |
| } | |
| } | |
| } | |
| Write-Host "`nConversion Complete!" -ForegroundColor Green |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment