From 343f892029b9eb949326e9d05386475b2c6a1a7b Mon Sep 17 00:00:00 2001 From: Relationship <139162359+Relationship-OctoWoW@users.noreply.github.com> Date: Sun, 5 Jul 2026 08:25:40 +0100 Subject: [PATCH] Delete GeneratePlaylist.ps1 --- GeneratePlaylist.ps1 | 1357 ------------------------------------------ 1 file changed, 1357 deletions(-) delete mode 100644 GeneratePlaylist.ps1 diff --git a/GeneratePlaylist.ps1 b/GeneratePlaylist.ps1 deleted file mode 100644 index 880aa25..0000000 --- a/GeneratePlaylist.ps1 +++ /dev/null @@ -1,1357 +0,0 @@ -param( - [string]$RootPath = '', - [switch]$UseWinRAR = $false -) - -$ErrorActionPreference = 'Continue' - -# --------------------------------------------------------------------------- -# Download with progress bar -# --------------------------------------------------------------------------- -# Uses System.Net.WebClient for chunk-based downloading with a real-time -# console progress bar showing percentage, speed, and ETA. Falls back to -# chunked HttpWebRequest if the WebClient approach fails for any reason. -# --------------------------------------------------------------------------- - -function Download-FileWithProgress { - param( - [Parameter(Mandatory=$true)] - [string]$Url, - - [Parameter(Mandatory=$true)] - [string]$OutFile - ) - - # Ensure TLS 1.2 for modern HTTPS endpoints - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - - # Pre-define format strings as variables to avoid Windows PowerShell 5.1 - # parser errors caused by curly braces inside inline -f expressions. - $fmtSizeMB = ' Downloading {0:N1} MB...' - $fmtBar = ' [{0}] {1,3}% {2:N1}/{3:N1} MB {4:N1} MB/s ETA {5}' - $fmtCarriage = "`r{0}" - $fmtEtaMin = '{0}m {1}s' - $fmtEtaSec = '{0}s' - $fmtUnknownDl = "`r Downloaded {0:N1} MB {1:N1} MB/s " - $fmtUnknownFin = ' Download complete ({0:N1} MB).' - - # Try the chunked WebClient approach with progress bar - $useWebClient = $true - - try { - $webClient = New-Object System.Net.WebClient - - # First, get the file size via a HEAD request so we can show percentage - $totalBytes = 0L - try { - $req = [System.Net.HttpWebRequest]::Create($Url) - $req.Method = 'HEAD' - $req.Timeout = 15000 - $req.AllowAutoRedirect = $true - $resp = $req.GetResponse() - $totalBytes = $resp.ContentLength - $resp.Close() - } catch { - # HEAD not supported or failed -- we'll still download, just without total size - $totalBytes = 0L - } - - if ($totalBytes -gt 0) { - # --- We know the total size: show a percentage progress bar --- - $sizeMB = $totalBytes / 1MB - Write-Host ($fmtSizeMB -f $sizeMB) - - $chunkSize = 256KB - $downloaded = 0L - $sw = [System.Diagnostics.Stopwatch]::StartNew() - - # Open the remote stream - $remoteStream = $webClient.OpenRead($Url) - # Create the local file stream - $fileStream = [System.IO.File]::Create($OutFile) - - $buffer = New-Object byte[] $chunkSize - $lastPct = -1 - - while ($true) { - $read = $remoteStream.Read($buffer, 0, $buffer.Length) - if ($read -le 0) { break } - $fileStream.Write($buffer, 0, $read) - $downloaded += $read - - $pct = [int](($downloaded * 100) / $totalBytes) - if ($pct -ne $lastPct) { - $lastPct = $pct - - # Build the visual progress bar using string multiplication - # NOTE: PowerShell 5.1 does NOT support [char] * [int], so - # we use string constructor instead (String([char], count)). - $barWidth = 30 - $filled = [int]($barWidth * $pct / 100) - $empty = $barWidth - $filled - $blockChar = [string][char]0x2588 # full block - $lightChar = [string][char]0x2591 # light shade - $barStr = ($blockChar * $filled) + ($lightChar * $empty) - - # Calculate speed and ETA - $elapsed = $sw.Elapsed.TotalSeconds - $speedMB = 0 - if ($elapsed -gt 0) { $speedMB = ($downloaded / 1MB) / $elapsed } - $etaSec = 0 - if ($speedMB -gt 0) { $etaSec = (($totalBytes - $downloaded) / 1MB) / $speedMB } - - # Format ETA - $etaStr = '' - if ($etaSec -ge 60) { - $etaMin = [int]($etaSec / 60) - $etaRem = [int]($etaSec % 60) - $etaStr = $fmtEtaMin -f $etaMin, $etaRem - } else { - $etaStr = $fmtEtaSec -f [int]$etaSec - } - - # Downloaded so far in MB - $dlMB = $downloaded / 1MB - $totalMB = $totalBytes / 1MB - - # Write the progress line (overwrites itself using carriage return) - $progressLine = $fmtBar -f $barStr, $pct, $dlMB, $totalMB, $speedMB, $etaStr - Write-Host -NoNewline ($fmtCarriage -f $progressLine) - } - } - - Write-Host '' # newline after the progress bar - - $fileStream.Close() - $remoteStream.Close() - $sw.Stop() - - # Verify file was written - if ((Test-Path -LiteralPath $OutFile -PathType Leaf) -and ((Get-Item -LiteralPath $OutFile).Length -gt 0)) { - Write-Host ' Download complete.' - return $true - } else { - return $false - } - - } else { - # --- Unknown total size: show bytes downloaded with speed --- - Write-Host ' Downloading (unknown size)...' - - $chunkSize = 256KB - $downloaded = 0L - $sw = [System.Diagnostics.Stopwatch]::StartNew() - - $remoteStream = $webClient.OpenRead($Url) - $fileStream = [System.IO.File]::Create($OutFile) - - $buffer = New-Object byte[] $chunkSize - $lastPrint = [DateTime]::MinValue - - while ($true) { - $read = $remoteStream.Read($buffer, 0, $buffer.Length) - if ($read -le 0) { break } - $fileStream.Write($buffer, 0, $read) - $downloaded += $read - - # Update progress every 0.5 seconds - $now = [DateTime]::UtcNow - if (($now - $lastPrint).TotalSeconds -ge 0.5) { - $lastPrint = $now - $elapsed = $sw.Elapsed.TotalSeconds - $speedMB = 0 - if ($elapsed -gt 0) { $speedMB = ($downloaded / 1MB) / $elapsed } - $dlMB = $downloaded / 1MB - Write-Host -NoNewline ($fmtUnknownDl -f $dlMB, $speedMB) - } - } - - Write-Host '' # newline after the progress line - - $fileStream.Close() - $remoteStream.Close() - $sw.Stop() - - if ((Test-Path -LiteralPath $OutFile -PathType Leaf) -and ((Get-Item -LiteralPath $OutFile).Length -gt 0)) { - $finalMB = [Math]::Round($downloaded / 1MB, 1) - Write-Host ($fmtUnknownFin -f $finalMB) - return $true - } else { - return $false - } - } - - } catch { - # WebClient approach failed -- fall back to chunked HttpWebRequest download - Write-Host ' WebClient download failed, falling back to chunked HTTP download...' - Write-Host " Error: $($_.Exception.Message)" - $useWebClient = $false - } - - # Fallback: use manual chunked HTTP download with byte/MB progress display - if (-not $useWebClient) { - try { - Write-Host ' Downloading (fallback mode - showing bytes downloaded)...' - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - - # Use HttpWebRequest directly so we get the response stream and can - # read chunks while showing progress (Invoke-WebRequest has no chunk API) - $req = [System.Net.HttpWebRequest]::Create($Url) - $req.Method = 'GET' - $req.Timeout = 120000 - $req.AllowAutoRedirect = $true - $resp = $req.GetResponse() - - $totalBytes = $resp.ContentLength - $respStream = $resp.GetResponseStream() - $fileStream = [System.IO.File]::Create($OutFile) - - $buffer = New-Object byte[] 65536 - $totalDownloaded = 0L - $sw = [System.Diagnostics.Stopwatch]::StartNew() - $lastPrint = [DateTime]::MinValue - - if ($totalBytes -gt 0) { - # Known size - show percentage-based progress - $sizeMB = $totalBytes / 1MB - Write-Host " File size: $([Math]::Round($sizeMB, 1)) MB" - - $blockChar = [string][char]0x2588 - $lightChar = [string][char]0x2591 - $lastPct = -1 - - while ($true) { - $read = $respStream.Read($buffer, 0, $buffer.Length) - if ($read -le 0) { break } - $fileStream.Write($buffer, 0, $read) - $totalDownloaded += $read - - $pct = [int](($totalDownloaded * 100) / $totalBytes) - if ($pct -ne $lastPct) { - $lastPct = $pct - $barWidth = 30 - $filled = [int]($barWidth * $pct / 100) - $empty = $barWidth - $filled - $barStr = ($blockChar * $filled) + ($lightChar * $empty) - - $elapsed = $sw.Elapsed.TotalSeconds - $speedMB = 0 - if ($elapsed -gt 0) { $speedMB = ($totalDownloaded / 1MB) / $elapsed } - $etaSec = 0 - if ($speedMB -gt 0) { $etaSec = (($totalBytes - $totalDownloaded) / 1MB) / $speedMB } - - $etaStr = '' - if ($etaSec -ge 60) { - $etaStr = '{0}m {1}s' -f [int]($etaSec / 60), [int]($etaSec % 60) - } else { - $etaStr = '{0}s' -f [int]$etaSec - } - - $dlMB = $totalDownloaded / 1MB - $totalMB = $totalBytes / 1MB - $progressLine = ' [{0}] {1,3}% {2:N1}/{3:N1} MB {4:N1} MB/s ETA {5}' -f $barStr, $pct, $dlMB, $totalMB, $speedMB, $etaStr - Write-Host -NoNewline ("`r{0}" -f $progressLine) - } - } - } else { - # Unknown size - show MB/bytes downloaded with speed - while ($true) { - $read = $respStream.Read($buffer, 0, $buffer.Length) - if ($read -le 0) { break } - $fileStream.Write($buffer, 0, $read) - $totalDownloaded += $read - - $now = [DateTime]::UtcNow - if (($now - $lastPrint).TotalSeconds -ge 0.5) { - $lastPrint = $now - $elapsed = $sw.Elapsed.TotalSeconds - $speedMB = 0 - if ($elapsed -gt 0) { $speedMB = ($totalDownloaded / 1MB) / $elapsed } - $dlMB = $totalDownloaded / 1MB - Write-Host -NoNewline ("`r Downloaded {0:N1} MB {1:N1} MB/s " -f $dlMB, $speedMB) - } - } - } - - Write-Host '' # newline after the progress line - - $fileStream.Close() - $respStream.Close() - $resp.Close() - $sw.Stop() - - if ((Test-Path -LiteralPath $OutFile -PathType Leaf) -and ((Get-Item -LiteralPath $OutFile).Length -gt 0)) { - $finalMB = [Math]::Round($totalDownloaded / 1MB, 1) - Write-Host " Download complete ($finalMB MB)." - return $true - } - } catch { - Write-Host " Download failed: $($_.Exception.Message)" - } - } - - return $false -} - -# -- Resolve the addon root directory ---------------------------------------- -# $PSScriptRoot can be empty when invoked via powershell -File from a .bat -if ($RootPath -eq '') { - $RootPath = $PSScriptRoot -} -if ($RootPath -eq '') { - $RootPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -} -if ($RootPath -eq '') { - # Last resort: use the current working directory - $RootPath = (Get-Location -PSProvider FileSystem).ProviderPath -} - -# Strip any trailing slashes or quotes that cause "Illegal characters in path" -$RootPath = $RootPath.TrimEnd('\', '/').Trim('"', "'") - -# Normalize to a full absolute path - use GetFullPath on the cleaned string -$Root = [System.IO.Path]::GetFullPath($RootPath) - -$Media = Join-Path $Root 'Media' -$Out = Join-Path $Root 'AutoPlaylist.lua' - -# WoW reads paths with backslashes; inside Lua strings a single backslash -# must be written as \\ (two characters in the source file). Escape-LuaString -# will double every \ it sees, so we store the prefix with SINGLE backslashes -# here -- after escaping they become \\ in the Lua source, which Lua reads as \. -$AddonPrefix = 'Interface\AddOns\RelationshipsJukeBox\' - -# --------------------------------------------------------------------------- -# Auto-download ffmpeg into the RelationshipsJukeBox folder if not found -# --------------------------------------------------------------------------- -# We look for ffmpeg/ffprobe on PATH first, then in a local "ffmpeg" subfolder -# inside the addon directory. If neither exists, we download a static build -# from gyan.dev (the official ffmpeg.org-linked Windows builds) or from -# BtbN/FFmpeg-Builds on GitHub, extract it, and place ffmpeg.exe + ffprobe.exe -# into a "ffmpeg" folder next to this script. -# -# IMPORTANT: gyan.dev distributes .7z archives (smaller). We need 7-Zip or -# WinRAR to extract them. If neither is available, we fall back to .zip -# archives from gyan.dev or GitHub (BtbN) which PowerShell can extract natively. -# --------------------------------------------------------------------------- - -$FfmpegDir = Join-Path $Root 'ffmpeg' - -function Find-Ffmpeg { - param([string]$Name) - # 1) On PATH already? - $found = Get-Command $Name -ErrorAction SilentlyContinue - if ($found) { return $found.Source } - # 2) In our local ffmpeg subfolder? - $local = Join-Path $FfmpegDir "$Name.exe" - if (Test-Path -LiteralPath $local -PathType Leaf) { return $local } - return $null -} - -function Find-SevenZip { - # 1) On PATH? - $found = Get-Command '7z' -ErrorAction SilentlyContinue - if ($found) { return $found.Source } - # 2) Default install location (7-Zip installs here on 64-bit) - $default64 = Join-Path $env:ProgramFiles '7-Zip\7z.exe' - if (Test-Path -LiteralPath $default64 -PathType Leaf) { return $default64 } - # 3) 32-bit on 64-bit system - $default32 = Join-Path ${env:ProgramFiles(x86)} '7-Zip\7z.exe' - if (Test-Path -LiteralPath $default32 -PathType Leaf) { return $default32 } - return $null -} - -function Find-WinRAR { - # 1) On PATH? - $found = Get-Command 'winrar' -ErrorAction SilentlyContinue - if ($found) { return $found.Source } - # 2) On PATH as rar.exe (command-line variant)? - $foundRar = Get-Command 'rar' -ErrorAction SilentlyContinue - if ($foundRar) { - # rar.exe is typically in the WinRAR install folder alongside WinRAR.exe - $rarDir = Split-Path -Parent $foundRar.Source - $winrarExe = Join-Path $rarDir 'WinRAR.exe' - if (Test-Path -LiteralPath $winrarExe -PathType Leaf) { return $winrarExe } - # unrar.exe can also extract - return $foundRar.Source - } - # 3) On PATH as unrar.exe (extraction-only variant)? - $foundUnrar = Get-Command 'unrar' -ErrorAction SilentlyContinue - if ($foundUnrar) { return $foundUnrar.Source } - # 4) Default 64-bit install location - $default64 = Join-Path $env:ProgramFiles 'WinRAR\WinRAR.exe' - if (Test-Path -LiteralPath $default64 -PathType Leaf) { return $default64 } - # 5) Default 32-bit install on 64-bit system - $default32 = Join-Path ${env:ProgramFiles(x86)} 'WinRAR\WinRAR.exe' - if (Test-Path -LiteralPath $default32 -PathType Leaf) { return $default32 } - # 6) Try common non-ProgramFiles locations - $commonPaths = @( - 'C:\WinRAR\WinRAR.exe', - 'D:\WinRAR\WinRAR.exe', - (Join-Path $env:ProgramFiles 'RAR\WinRAR.exe'), - (Join-Path ${env:ProgramFiles(x86)} 'RAR\WinRAR.exe') - ) - foreach ($p in $commonPaths) { - if (Test-Path -LiteralPath $p -PathType Leaf) { return $p } - } - # 7) Try to find unrar.exe in WinRAR folder as fallback - $unrar64 = Join-Path $env:ProgramFiles 'WinRAR\UnRAR.exe' - if (Test-Path -LiteralPath $unrar64 -PathType Leaf) { return $unrar64 } - $unrar32 = Join-Path ${env:ProgramFiles(x86)} 'WinRAR\UnRAR.exe' - if (Test-Path -LiteralPath $unrar32 -PathType Leaf) { return $unrar32 } - return $null -} - -# -UseWinRAR flag is set via the script parameter above - -function Ensure-FfmpegInstalled { - $ffmpegExe = Find-Ffmpeg -Name 'ffmpeg' - $ffprobeExe = Find-Ffmpeg -Name 'ffprobe' - - if ($ffmpegExe -and $ffprobeExe) { - Write-Host "ffmpeg found: $ffmpegExe" - Write-Host "ffprobe found: $ffprobeExe" - return - } - - Write-Host "" - Write-Host "============================================" - Write-Host " ffmpeg not found -- auto-downloading..." - Write-Host "============================================" - Write-Host "" - - # Check if we have 7-Zip and/or WinRAR available for .7z extraction - $sevenZipExe = Find-SevenZip - $winRarExe = Find-WinRAR - - # Determine extraction preference order based on -UseWinRAR flag - # When -UseWinRAR is specified, WinRAR is preferred over 7-Zip for all formats - $preferWinRAR = $UseWinRAR -and $winRarExe - - $downloadDir = Join-Path $Root 'ffmpeg_download' - if (-not (Test-Path -LiteralPath $downloadDir -PathType Container)) { - New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null - } - - $downloaded = $false - $downloadedFile = $null - - # We can extract .7z if we have 7-Zip OR WinRAR - $canExtract7z = ($sevenZipExe -ne $null) -or ($winRarExe -ne $null) - - if ($canExtract7z) { - # We have an extraction tool -- download the smaller .7z release - if ($preferWinRAR) { - Write-Host "WinRAR found (preferred): $winRarExe" - } elseif ($sevenZipExe) { - Write-Host "7-Zip found: $sevenZipExe" - } - if ($winRarExe -and -not $preferWinRAR) { - Write-Host "WinRAR also found: $winRarExe" - } - Write-Host "Downloading ffmpeg .7z release (smaller archive)..." - - # gyan.dev URL scheme (as of 2026-07): - # - Short alias URLs that 303-redirect to dated packages under /packages/ - # - ffmpeg-release-essentials.7z -> /packages/ffmpeg-X.Y.Z-essentials_build.7z - # - ffmpeg-git-full.7z -> /packages/ffmpeg-YYYY-MM-DD-git-HASH-full_build.7z - # - ffmpeg-release-full.7z -> /packages/ffmpeg-X.Y.Z-full_build.7z - # - ffmpeg-git-essentials.7z -> /packages/ffmpeg-YYYY-MM-DD-git-HASH-essentials_build.7z - # NOTE: ffmpeg-release-full.7z exists but there is no .zip for the full build. - # There is also NO /packages/ffmpeg-release-full.7z direct URL (404). - # Use the short alias URLs -- they redirect (303) to the correct dated file. - $urls7z = @( - # Release essentials (smaller, ~32 MB) -- has everything we need - "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z", - # Git master essentials (latest dev build) - "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-essentials.7z", - # Release full build (more codecs, ~70 MB) - "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z", - # Git master full build (latest dev build, full) - "https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z" - ) - - foreach ($tryUrl in $urls7z) { - $archName = [System.IO.Path]::GetFileName($tryUrl) - $savePath = Join-Path $downloadDir $archName - Write-Host "Attempting: $tryUrl" - try { - $dlOk = Download-FileWithProgress -Url $tryUrl -OutFile $savePath - if ($dlOk) { - $downloaded = $true - $downloadedFile = $savePath - Write-Host "Download complete ($archName)." - break - } - } catch { - Write-Host "Failed: $($_.Exception.Message)" - if (Test-Path -LiteralPath $savePath) { - Remove-Item -LiteralPath $savePath -Force -ErrorAction SilentlyContinue - } - } - } - } - - if (-not $downloaded) { - # .7z download failed or no extraction tool -- try .zip archives - # PowerShell can extract .zip natively with Expand-Archive - Write-Host "" - if (-not $canExtract7z) { - Write-Host "Neither 7-Zip nor WinRAR found -- cannot extract .7z archives." - Write-Host "Downloading .zip release instead (larger but no extra tools needed)..." - } else { - Write-Host ".7z downloads failed. Trying .zip archives..." - } - - # gyan.dev .zip files: - # - ffmpeg-release-essentials.zip (~104 MB, PowerShell-native extraction) - # - ffmpeg-8.1.2-essentials_build.zip (versioned, under /packages/) - # NOTE: There is NO ffmpeg-release-full.zip on gyan.dev (404). - # So we also try BtbN/FFmpeg-Builds on GitHub which provides .zip archives. - $urlsZip = @( - # gyan.dev release essentials .zip (native PS extraction, ~104 MB) - "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip", - # BtbN GitHub - GPL shared build (has ffmpeg.exe + ffprobe.exe + DLLs, ~80 MB) - "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl-shared.zip", - # BtbN GitHub - GPL static build (single ffmpeg.exe, no DLLs, ~100 MB) - "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" - ) - - foreach ($tryUrl in $urlsZip) { - $archName = [System.IO.Path]::GetFileName($tryUrl) - $savePath = Join-Path $downloadDir $archName - Write-Host "Attempting: $tryUrl" - try { - $dlOk = Download-FileWithProgress -Url $tryUrl -OutFile $savePath - if ($dlOk) { - $downloaded = $true - $downloadedFile = $savePath - Write-Host "Download complete ($archName)." - break - } - } catch { - Write-Host "Failed: $($_.Exception.Message)" - if (Test-Path -LiteralPath $savePath) { - Remove-Item -LiteralPath $savePath -Force -ErrorAction SilentlyContinue - } - } - } - } - - if (-not $downloaded) { - Write-Host "" - Write-Host "============================================================" - Write-Host " AUTOMATIC DOWNLOAD FAILED" - Write-Host "============================================================" - Write-Host "" - Write-Host "Please install ffmpeg manually using ONE of these methods:" - Write-Host "" - Write-Host " Option 1 - Download and extract into the addon folder:" - Write-Host " 1. Go to https://www.gyan.dev/ffmpeg/builds/" - Write-Host " 2. Download 'ffmpeg-release-essentials.zip' (or .7z if you have 7-Zip)" - Write-Host " 3. Extract the archive" - Write-Host " 4. Find ffmpeg.exe and ffprobe.exe inside the bin/ folder" - Write-Host " 5. Create a folder called 'ffmpeg' next to this script" - Write-Host " 6. Copy ffmpeg.exe and ffprobe.exe into that 'ffmpeg' folder" - Write-Host "" - Write-Host " Option 2 - Download from GitHub BtbN/FFmpeg-Builds:" - Write-Host " 1. Go to https://github.com/BtbN/FFmpeg-Builds/releases" - Write-Host " 2. Download 'ffmpeg-master-latest-win64-gpl-shared.zip'" - Write-Host " 3. Extract the archive" - Write-Host " 4. Find ffmpeg.exe and ffprobe.exe inside the bin/ folder" - Write-Host " 5. Create a folder called 'ffmpeg' next to this script" - Write-Host " 6. Copy ffmpeg.exe and ffprobe.exe into that 'ffmpeg' folder" - Write-Host "" - Write-Host " Option 3 - Install via Chocolatey (run as Admin):" - Write-Host " choco install ffmpeg" - Write-Host "" - Write-Host " Option 4 - Install via Winget:" - Write-Host " winget install ffmpeg" - Write-Host "" - Write-Host " Option 5 - Install via Scoop:" - Write-Host " scoop install ffmpeg-essentials" - Write-Host "" - Write-Host " Option 6 - Add ffmpeg to your system PATH:" - Write-Host " Download from https://ffmpeg.org/download.html" - Write-Host " Extract and add the bin folder to your PATH environment variable" - Write-Host "" - Write-Host "NOTE: gyan.dev distributes ffmpeg as .7z files (smaller)." - Write-Host "Install 7-Zip from https://www.7-zip.org/ for .7z support." - Write-Host "WinRAR from https://www.win-rar.com/ can also extract .7z files." - Write-Host "You can also run this script with -UseWinRAR to prefer WinRAR over 7-Zip." - Write-Host "A .zip release is also available from gyan.dev or GitHub but is larger." - Write-Host "" - Write-Host "The script will continue WITHOUT ffmpeg -- MP3 sanitization" - Write-Host "and accurate duration detection will be skipped." - Write-Host "============================================================" - Write-Host "" - return - } - - # Extract the archive - Write-Host "Extracting ffmpeg..." - try { - # Create ffmpeg output directory - if (-not (Test-Path -LiteralPath $FfmpegDir -PathType Container)) { - New-Item -ItemType Directory -Path $FfmpegDir -Force | Out-Null - } - - $extractBase = Join-Path $downloadDir 'extracted' - if (Test-Path -LiteralPath $extractBase) { - Remove-Item -LiteralPath $extractBase -Recurse -Force - } - - $is7z = $downloadedFile.EndsWith('.7z') - - if ($is7z) { - # Extract .7z -- prefer WinRAR if -UseWinRAR was specified, then 7-Zip, then WinRAR as fallback - if ($preferWinRAR -and $winRarExe) { - Write-Host "Using WinRAR to extract .7z archive..." - & $winRarExe x -y $downloadedFile "$extractBase\" 2>$null | Out-Null - } elseif ($sevenZipExe) { - Write-Host "Using 7-Zip to extract .7z archive..." - & $sevenZipExe x $downloadedFile -o"$extractBase" -y 2>$null | Out-Null - } elseif ($winRarExe) { - Write-Host "Using WinRAR to extract .7z archive..." - & $winRarExe x -y $downloadedFile "$extractBase\" 2>$null | Out-Null - } else { - Write-Host "ERROR: No tool available to extract .7z archive (need 7-Zip or WinRAR)." - throw "Cannot extract .7z file -- install 7-Zip or WinRAR, or re-run without .7z download" - } - } else { - # Extract .zip -- prefer WinRAR if -UseWinRAR, then 7-Zip, then PowerShell native - if ($preferWinRAR -and $winRarExe) { - Write-Host "Using WinRAR to extract .zip archive..." - & $winRarExe x -y $downloadedFile "$extractBase\" 2>$null | Out-Null - } elseif ($sevenZipExe) { - Write-Host "Using 7-Zip to extract .zip archive..." - & $sevenZipExe x $downloadedFile -o"$extractBase" -y 2>$null | Out-Null - } else { - Write-Host "Using PowerShell to extract .zip archive..." - Expand-Archive -LiteralPath $downloadedFile -DestinationPath $extractBase -Force - } - } - - # Find the bin folder inside the extracted directory - # gyan.dev archives contain a folder like ffmpeg-8.1.2-essentials_build/ or - # ffmpeg-2026-06-29-git-de6bcf5c05-full_build/ with a bin/ subfolder inside. - # BtbN GitHub archives contain a folder like ffmpeg-master-latest-win64-gpl-shared/ - # with a bin/ subfolder inside. - $binDir = Get-ChildItem -LiteralPath $extractBase -Recurse -Directory -Filter 'bin' | - Select-Object -First 1 - - if ($binDir) { - # Copy ffmpeg.exe and ffprobe.exe to our local ffmpeg folder - foreach ($exe in @('ffmpeg.exe', 'ffprobe.exe')) { - $src = Join-Path $binDir.FullName $exe - $dst = Join-Path $FfmpegDir $exe - if (Test-Path -LiteralPath $src -PathType Leaf) { - Copy-Item -LiteralPath $src -Destination $dst -Force - Write-Host "Installed: $exe -> $dst" - } - } - } else { - # Try to find ffmpeg.exe directly in extracted folder - $ffmpegFiles = Get-ChildItem -LiteralPath $extractBase -Recurse -File -Filter 'ffmpeg.exe' - $ffprobeFiles = Get-ChildItem -LiteralPath $extractBase -Recurse -File -Filter 'ffprobe.exe' - foreach ($f in $ffmpegFiles) { - Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $FfmpegDir 'ffmpeg.exe') -Force - Write-Host "Installed: ffmpeg.exe" - } - foreach ($f in $ffprobeFiles) { - Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $FfmpegDir 'ffprobe.exe') -Force - Write-Host "Installed: ffprobe.exe" - } - } - - # Clean up download artifacts - Remove-Item -LiteralPath $extractBase -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -LiteralPath $downloadedFile -Force -ErrorAction SilentlyContinue - # Remove download dir if empty - $remaining = Get-ChildItem -LiteralPath $downloadDir -ErrorAction SilentlyContinue - if (-not $remaining) { - Remove-Item -LiteralPath $downloadDir -Force -ErrorAction SilentlyContinue - } - - # Verify installation - $ffTest = Join-Path $FfmpegDir 'ffmpeg.exe' - $fpTest = Join-Path $FfmpegDir 'ffprobe.exe' - if ((Test-Path -LiteralPath $ffTest) -and (Test-Path -LiteralPath $fpTest)) { - Write-Host "" - Write-Host "ffmpeg installed successfully in: $FfmpegDir" - Write-Host "" - } else { - Write-Host "" - Write-Host "WARNING: ffmpeg/ffprobe not found after extraction." - Write-Host "Please install manually (see instructions below)." - Write-Host "" - } - - } catch { - Write-Host "ERROR: Failed to extract ffmpeg: $($_.Exception.Message)" - Write-Host "Please install ffmpeg manually (see instructions in the script output)." - } -} - -# Run the auto-installer -Ensure-FfmpegInstalled - -# --------------------------------------------------------------------------- -# Now that ffmpeg may be available, set up a helper to call it from the -# local ffmpeg folder when the system PATH doesn't have it. -# --------------------------------------------------------------------------- - -# Override ffprobe/ffmpeg calls to use the local copy if needed -$localFfmpeg = Join-Path $FfmpegDir 'ffmpeg.exe' -$localFfprobe = Join-Path $FfmpegDir 'ffprobe.exe' - -# We can't easily override the & operator, but we can define helper functions -function Invoke-LocalFfprobe { - param([string[]]$Arguments) - $exe = Find-Ffmpeg -Name 'ffprobe' - if ($exe) { - return & $exe @Arguments - } - return $null -} - -function Invoke-LocalFfmpeg { - param([string[]]$Arguments) - $exe = Find-Ffmpeg -Name 'ffmpeg' - if ($exe) { - return & $exe @Arguments - } - return $null -} - -if (-not (Test-Path -LiteralPath $Media -PathType Container)) { - throw "Media folder not found: $Media" -} - -# -- Lua string escaper ------------------------------------------------------ -function Escape-LuaString { - param([AllowNull()][string]$Value) - - if ($null -eq $Value) { return '' } - - $escaped = $Value.Replace('\', '\\') - $escaped = $escaped.Replace('"', '\"') - $escaped = $escaped.Replace("`n", '\n') - $escaped = $escaped.Replace("`r", '\r') - return $escaped -} - -# -- Duration helpers (optional -- graceful fallback) ------------------------- -$shell = $null -$wmp = $null -try { $shell = New-Object -ComObject Shell.Application } catch { $shell = $null } -try { $wmp = New-Object -ComObject WMPlayer.OCX.7 } catch { $wmp = $null } - -# Default duration assigned when we cannot read metadata (seconds). -# This lets autoplay/shuffle work without metadata -- the Lua side uses -# this as the wait time before advancing to the next track. -$DEFAULT_DURATION = 180 # 3 minutes -- reasonable for most songs - -function Get-TrackDurationSeconds { - param([System.IO.FileInfo]$File) - - # --- Try Shell.Application (works for mp3 with Windows property handlers) --- - if ($shell) { - try { - $folder = $shell.Namespace($File.DirectoryName) - if ($folder) { - $item = $folder.ParseName($File.Name) - if ($item) { - # 27 = Duration column in Shell folder view (HH:MM:SS string) - $raw = $item.ExtendedProperty('System.Media.Duration') - $ticks = 0L - if ($raw -is [ValueType]) { - $ticks = [int64]$raw - } elseif ($raw) { - [void][int64]::TryParse([string]$raw, [ref]$ticks) - } - - if ($ticks -gt 0) { - return [int][Math]::Ceiling($ticks / 10000000.0) - } - - # Fallback: try the Duration column (index 27) as a string - $durStr = $folder.GetDetailsOf($item, 27) - if ($durStr -and $durStr.Trim() -ne '') { - # Parse HH:MM:SS or MM:SS - $parts = $durStr.Trim() -split ':' - $secs = 0 - if ($parts.Count -eq 3) { - $secs = [int]$parts[0]*3600 + [int]$parts[1]*60 + [int]$parts[2] - } elseif ($parts.Count -eq 2) { - $secs = [int]$parts[0]*60 + [int]$parts[1] - } - if ($secs -gt 0) { return $secs } - } - } - } - } catch { - # Silently skip -- special characters in filename can break COM - } - } - - # --- Try Windows Media Player COM --- - if ($wmp) { - try { - $mediaItem = $wmp.newMedia($File.FullName) - if ($mediaItem) { - $duration = [double]$mediaItem.duration - if ($duration -gt 0) { - return [int][Math]::Ceiling($duration) - } - } - } catch { - # Silently skip -- special characters in filename can break COM - } - } - - # --- Try ffprobe if available --- - try { - $ffprobeExe = Find-Ffmpeg -Name 'ffprobe' - if ($ffprobeExe) { - # Use Start-Process to avoid crashes from ffprobe stderr/non-zero exit - $ffOutFile = [System.IO.Path]::GetTempFileName() - $ffErrFile = [System.IO.Path]::GetTempFileName() - $ffProc = Start-Process -FilePath $ffprobeExe -ArgumentList @('-v', 'quiet', '-print_format', 'json', '-show_format', $File.FullName) -NoNewWindow -Wait -PassThru -RedirectStandardOutput $ffOutFile -RedirectStandardError $ffErrFile - Remove-Item -LiteralPath $ffErrFile -Force -ErrorAction SilentlyContinue - if ($ffProc.ExitCode -eq 0 -and (Test-Path -LiteralPath $ffOutFile -PathType Leaf)) { - $ffOut = Get-Content -LiteralPath $ffOutFile -Raw - Remove-Item -LiteralPath $ffOutFile -Force -ErrorAction SilentlyContinue - if ($ffOut) { - $ffJson = $ffOut | ConvertFrom-Json - if ($ffJson.format.duration) { - $dur = [double]$ffJson.format.duration - if ($dur -gt 0) { return [int][Math]::Ceiling($dur) } - } - } - } else { - Remove-Item -LiteralPath $ffOutFile -Force -ErrorAction SilentlyContinue - } - } - } catch { - # ffprobe not installed or failed, skip - } - - # --- Fallback: estimate from file size (very rough) --- - # For mp3 @ ~128kbps: ~1KB per second. For WAV: ~172KB per second (16bit/44.1k/stereo) - if ($File.Extension -match '^(?i)\.mp3$') { - $estimate = [int][Math]::Ceiling($File.Length / 16000.0) # ~128kbps - if ($estimate -gt 10) { return $estimate } - } elseif ($File.Extension -match '^(?i)\.wav$') { - $estimate = [int][Math]::Ceiling($File.Length / 176000.0) # ~16bit/44.1k/stereo - if ($estimate -gt 10) { return $estimate } - } elseif ($File.Extension -match '^(?i)\.ogg$') { - $estimate = [int][Math]::Ceiling($File.Length / 12000.0) # ~112kbps vorbis - if ($estimate -gt 10) { return $estimate } - } - - # --- Ultimate fallback: use default duration --- - return $DEFAULT_DURATION -} - -# -- Sanitize MP3 files for WoW 1.12 compatibility --------------------------- -# WoW 1.12's FMOD 3.x engine is very picky about MP3 files. Many tracks that -# play fine in modern players will silently fail in-game. Known issues include: -# -# 1. CRC protection bit set (protection_bit = 0 in MPEG frame header) -# 2. Embedded album art / ID3v2 APIC frames (FMOD may misparse these as video) -# 3. Variable bitrate (VBR) encoding with Xing/Info headers -# 4. Non-standard MPEG version/layer combinations (MPEG 2.0/2.5, Layer I/II) -# 5. Unusual channel modes (dual channel, joint stereo variants) -# 6. Very high bitrates (>256kbps) that FMOD 3.x sometimes chokes on -# 7. LAME-encoded files with non-standard delay/padding info tags -# 8. ID3v2 extended headers or unsynchronization that confuse FMOD -# 9. Files with large ID3v2 tags where FMOD fails to find the audio start -# 10. Files encoded with encoders that produce MPEG frame padding inconsistencies -# -# The ONLY reliable fix is to re-encode with ffmpeg using settings known to -# produce FMOD 3.x-compatible output. This script checks each MP3 for any -# of these issues and re-encodes only those that need it. If ffmpeg is not -# available, it tries a binary patch to at least fix CRC protection. -# -# Re-encoded files use: CBR 256kbps, MPEG1 Layer3, Simple Stereo, no CRC, -# no embedded art, stripped tags, standard LAME encoding. - -function Test-Mp3NeedsReencode { - param([string]$FilePath) - - try { - $bytes = [System.IO.File]::ReadAllBytes($FilePath) - if ($bytes.Length -lt 10) { return $false, 'File too small' } - - # --- Check for ID3v2 tag and find audio start --- - $audioOffset = 0 - $hasId3v2 = $false - $hasExtendedHeader = $false - $hasUnsynchronization = $false - $id3v2Size = 0 - - if ($bytes[0] -eq 0x49 -and $bytes[1] -eq 0x44 -and $bytes[2] -eq 0x33) { - # ID3v2 tag present - $hasId3v2 = $true - $id3v2Flags = $bytes[5] - $hasUnsynchronization = ($id3v2Flags -band 0x80) -ne 0 - $hasExtendedHeader = ($id3v2Flags -band 0x40) -ne 0 - $id3v2Size = ($bytes[6] -shl 21) -bor ($bytes[7] -shl 14) -bor ($bytes[8] -shl 7) -bor $bytes[9] - $audioOffset = $id3v2Size + 10 - - # Check for embedded album art (APIC frame) in ID3v2 - # Scan the ID3v2 tag for 'APIC' frame ID - $tagEnd = [Math]::Min($id3v2Size + 10, $bytes.Length) - for ($j = 10; $j -lt $tagEnd - 4; $j++) { - if ($bytes[$j] -eq 0x41 -and $bytes[$j+1] -eq 0x50 -and $bytes[$j+2] -eq 0x49 -and $bytes[$j+3] -eq 0x43) { - # APIC frame found = embedded album art - return $true, 'Embedded album art (APIC frame in ID3v2 tag)' - } - # Also check for 'PIC ' (ID3v2.2 picture frame) - if ($bytes[$j] -eq 0x50 -and $bytes[$j+1] -eq 0x49 -and $bytes[$j+2] -eq 0x43 -and $bytes[$j+3] -eq 0x20) { - return $true, 'Embedded album art (PIC frame in ID3v2 tag)' - } - } - } - - if ($hasUnsynchronization) { - return $true, 'ID3v2 unsynchronization flag set (can confuse FMOD 3.x)' - } - - if ($hasExtendedHeader) { - return $true, 'ID3v2 extended header present (can confuse FMOD 3.x)' - } - - # --- Scan MPEG audio frames --- - if ($audioOffset -ge $bytes.Length - 4) { return $false, 'No audio data found' } - - $frameCount = 0 - $maxFramesToCheck = 50 - $foundXing = $false - $foundInfo = $false - $hasCrc = $false - $hasVbr = $false - $hasHighBitrate = $false - $hasNonStandardMode = $false - $hasMpeg2 = $false - $prevBitrateIdx = -1 - $bitrateChanges = 0 - - # Bitrate table for MPEG1 Layer III (index 0 = free, 15 = bad) - $bitrateTable1L3 = @(0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0) - - for ($i = $audioOffset; $i -lt [Math]::Min($bytes.Length - 4, $audioOffset + 65536); $i++) { - # Look for MPEG sync word - if ($bytes[$i] -ne 0xFF -or ($bytes[$i+1] -band 0xE0) -ne 0xE0) { - continue - } - - $b1 = $bytes[$i+1] - $b2 = $bytes[$i+2] - - # Decode MPEG header - $mpegVersion = ($b1 -shr 3) -band 0x03 # 0=2.5, 2=2.0, 3=1.0 - $layer = ($b1 -shr 1) -band 0x03 # 1=Layer3, 2=Layer2, 3=Layer1 - $protection = $b1 -band 0x01 # 0=CRC present, 1=no CRC - $bitrateIdx = ($b2 -shr 4) -band 0x0F - $sampleRateIdx = ($b2 -shr 2) -band 0x03 - $channelMode = ($bytes[$i+3] -shr 6) -band 0x03 # 0=Stereo, 1=Joint, 2=Dual, 3=Mono - - # Check for MPEG 2.0 or 2.5 (WoW 1.12 FMOD may not handle these) - if ($mpegVersion -eq 0 -or $mpegVersion -eq 2) { - $hasMpeg2 = $true - } - - # Check for non-Layer3 (WoW 1.12 FMOD may not handle Layer I/II) - if ($layer -ne 1) { - $hasNonStandardMode = $true - } - - # Check CRC protection - if ($protection -eq 0) { - $hasCrc = $true - } - - # Check bitrate - if ($mpegVersion -eq 3 -and $layer -eq 1) { - # MPEG1 Layer3 - $br = $bitrateTable1L3[$bitrateIdx] - if ($br -ge 320) { - $hasHighBitrate = $true # 320kbps sometimes causes FMOD issues - } - # Track bitrate changes for VBR detection - if ($prevBitrateIdx -ge 0 -and $bitrateIdx -ne $prevBitrateIdx) { - $bitrateChanges++ - } - $prevBitrateIdx = $bitrateIdx - } - - # Check for Xing/Info header (VBR indicator) - usually at offset 36 from frame start - # for MPEG1 Layer3 stereo: after 32-byte side information - if ($i + 72 -lt $bytes.Length) { - $xingOffset = $i + 36 - if ($bytes[$xingOffset] -eq 0x58 -and $bytes[$xingOffset+1] -eq 0x69 -and $bytes[$xingOffset+2] -eq 0x6E -and $bytes[$xingOffset+3] -eq 0x67) { - $foundXing = $true - $hasVbr = $true - } - if ($bytes[$xingOffset] -eq 0x49 -and $bytes[$xingOffset+1] -eq 0x6E -and $bytes[$xingOffset+2] -eq 0x66 -and $bytes[$xingOffset+3] -eq 0x6F) { - $foundInfo = $true - # Info header can indicate CBR but still has LAME tag that may cause issues - } - } - - $frameCount++ - if ($frameCount -ge $maxFramesToCheck) { break } - - # Calculate frame size to skip to next frame - if ($mpegVersion -eq 3 -and $layer -eq 1) { - $br = $bitrateTable1L3[$bitrateIdx] - if ($br -gt 0) { - $padding = ($b2 -shr 1) -band 0x01 - $frameSize = [int]([Math]::Floor(144.0 * $br * 1000 / 44100)) + $padding - $i += $frameSize - 1 # -1 because the for loop does $i++ - } else { - $i += 417 - } - } else { - # Non-standard frame, skip forward conservatively - $i += 417 - } - } - - # --- Return findings --- - # VBR detection: Xing header or multiple bitrate changes - if ($foundXing) { - return $true, 'VBR encoding (Xing header found - FMOD 3.x often fails on VBR)' - } - if ($bitrateChanges -ge 3) { - return $true, 'VBR encoding (bitrate changes detected across frames)' - } - - # CRC protection - if ($hasCrc) { - return $true, 'CRC protection bit set (FMOD 3.x cannot play CRC-protected MP3)' - } - - # Embedded art was already checked above, but also check via file size heuristic - # If there's an ID3v2 tag larger than 100KB, it likely has embedded art - if ($hasId3v2 -and $id3v2Size -gt 102400) { - return $true, "Large ID3v2 tag ($([int]($id3v2Size/1024))KB - likely contains embedded art)" - } - - # High bitrate (320kbps) with CRC or other issues - if ($hasHighBitrate -and $hasCrc) { - return $true, '320kbps with CRC protection (known to fail in FMOD 3.x)' - } - - # Non-standard MPEG version/layer - if ($hasMpeg2) { - return $true, 'MPEG 2.0/2.5 audio detected (FMOD 3.x may not support)' - } - if ($hasNonStandardMode) { - return $true, 'Non-Layer3 MPEG audio detected (FMOD 3.x may not support)' - } - - # Dual channel mode (rare, sometimes causes FMOD issues) - if ($channelMode -eq 2 -and $frameCount -gt 0) { - return $true, 'Dual channel mode detected (FMOD 3.x may not handle correctly)' - } - - # Check for video/image streams embedded in the MP3 file - # Some MP3 files have embedded JPEG/PNG album art as a separate "stream" - # FMOD may try to play these as video and fail - try { - $ffprobeExe = Find-Ffmpeg -Name 'ffprobe' - if ($ffprobeExe) { - $ffOutFile2 = [System.IO.Path]::GetTempFileName() - $ffErrFile2 = [System.IO.Path]::GetTempFileName() - $ffProc2 = Start-Process -FilePath $ffprobeExe -ArgumentList @('-v', 'quiet', '-print_format', 'json', '-show_streams', $FilePath) -NoNewWindow -Wait -PassThru -RedirectStandardOutput $ffOutFile2 -RedirectStandardError $ffErrFile2 - Remove-Item -LiteralPath $ffErrFile2 -Force -ErrorAction SilentlyContinue - if ($ffProc2.ExitCode -eq 0 -and (Test-Path -LiteralPath $ffOutFile2 -PathType Leaf)) { - $ffProbeOut = Get-Content -LiteralPath $ffOutFile2 -Raw - Remove-Item -LiteralPath $ffOutFile2 -Force -ErrorAction SilentlyContinue - if ($ffProbeOut) { - $ffJson = $ffProbeOut | ConvertFrom-Json - foreach ($stream in $ffJson.streams) { - if ($stream.codec_type -eq 'video') { - return $true, "Embedded video/image stream (codec: $($stream.codec_name)) - FMOD 3.x cannot handle" - } - } - } - } else { - Remove-Item -LiteralPath $ffOutFile2 -Force -ErrorAction SilentlyContinue - } - } - } catch { - # ffprobe not available or failed, skip this check - } - - return $false, 'Compatible' - } - catch { - return $true, "Error analyzing file: $($_.Exception.Message)" - } -} - -function Sanitize-Mp3File { - param([System.IO.FileInfo]$File) - - $tempFile = $File.FullName + '.sanitize.tmp' - - # Re-encode with ffmpeg: strip video/art, no CRC, CBR 256kbps, simple stereo, - # no tags (stripped), no VBR, standard MPEG1 Layer3 - # Using 256kbps instead of 320kbps because FMOD 3.x is more reliable at this bitrate - $ffmpegExe = Find-Ffmpeg -Name 'ffmpeg' - $result = $null - try { - if ($ffmpegExe) { - # Capture stderr to prevent it from interfering with the console - # and check the exit code manually rather than relying on ErrorActionPreference - $stderrFile = [System.IO.Path]::GetTempFileName() - $proc = Start-Process -FilePath $ffmpegExe -ArgumentList @('-y', '-i', $File.FullName, '-vn', '-c:a', 'libmp3lame', '-b:a', '256k', '-ac', '2', '-ar', '44100', '-joint_stereo', '0', '-map_metadata', '-1', $tempFile) -NoNewWindow -Wait -PassThru -RedirectStandardError $stderrFile - Remove-Item -LiteralPath $stderrFile -Force -ErrorAction SilentlyContinue - if ($proc.ExitCode -ne 0) { - Write-Host " ffmpeg exited with code $($proc.ExitCode) for $($File.Name), trying alternate settings..." - } - } - } catch { - # ffmpeg failed -- cannot sanitize, continue to next method - Write-Host " ffmpeg re-encode failed for $($File.Name): $($_.Exception.Message)" - } - - if ((Test-Path -LiteralPath $tempFile -PathType Leaf) -and ((Get-Item -LiteralPath $tempFile).Length -gt 0)) { - # Verify the sanitized file passes all checks - $needsMore, $reason = Test-Mp3NeedsReencode -FilePath $tempFile - if (-not $needsMore) { - Move-Item -LiteralPath $tempFile -Destination $File.FullName -Force - Write-Host " Sanitized (re-encoded for WoW 1.12): $($File.Name)" - return $true - } else { - # Even after re-encode, still has issues - try one more time with different settings - Remove-Item -LiteralPath $tempFile -Force - $tempFile2 = $File.FullName + '.sanitize2.tmp' - try { - $ffmpegExe2 = Find-Ffmpeg -Name 'ffmpeg' - if ($ffmpegExe2) { - $stderrFile2 = [System.IO.Path]::GetTempFileName() - $proc2 = Start-Process -FilePath $ffmpegExe2 -ArgumentList @('-y', '-i', $File.FullName, '-vn', '-c:a', 'libmp3lame', '-b:a', '192k', '-ac', '2', '-ar', '44100', '-joint_stereo', '0', '-map_metadata', '-1', $tempFile2) -NoNewWindow -Wait -PassThru -RedirectStandardError $stderrFile2 - Remove-Item -LiteralPath $stderrFile2 -Force -ErrorAction SilentlyContinue - } - if ((Test-Path -LiteralPath $tempFile2 -PathType Leaf) -and ((Get-Item -LiteralPath $tempFile2).Length -gt 0)) { - Move-Item -LiteralPath $tempFile2 -Destination $File.FullName -Force - Write-Host " Sanitized (re-encoded at 192kbps for WoW 1.12): $($File.Name)" - return $true - } - } catch {} - if (Test-Path -LiteralPath $tempFile2) { Remove-Item -LiteralPath $tempFile2 -Force } - } - } elseif (Test-Path -LiteralPath $tempFile) { - Remove-Item -LiteralPath $tempFile -Force - } - - # If ffmpeg is not available, try binary patch for CRC-only fix - $needsReencode, $reason = Test-Mp3NeedsReencode -FilePath $File.FullName - if ($needsReencode -and $reason -match 'CRC') { - # Binary patch: flip the protection bit from 0 to 1 in each MPEG frame - try { - $bytes = [System.IO.File]::ReadAllBytes($File.FullName) - $patched = 0 - - # Find ID3v2 tag end - $audioOffset = 0 - if ($bytes[0] -eq 0x49 -and $bytes[1] -eq 0x44 -and $bytes[2] -eq 0x33) { - $tagSize = ($bytes[6] -shl 21) -bor ($bytes[7] -shl 14) -bor ($bytes[8] -shl 7) -bor $bytes[9] - $audioOffset = $tagSize + 10 - } - - for ($i = $audioOffset; $i -lt $bytes.Length - 4; $i++) { - if ($bytes[$i] -eq 0xFF -and ($bytes[$i+1] -band 0xE0) -eq 0xE0) { - if (($bytes[$i+1] -band 0x01) -eq 0) { - # CRC bit is 0, flip it to 1 (no CRC) - $bytes[$i+1] = $bytes[$i+1] -bor 0x01 - $patched++ - } - # Skip past this frame - $b2 = $bytes[$i+2] - $bitrateIdx = ($b2 -shr 4) -band 0x0F - $br = $bitrateTable1L3[$bitrateIdx] - if ($br -gt 0) { - $padding = ($b2 -shr 1) -band 0x01 - # Approximate frame skip (note: if CRC was present, frame had 2 extra bytes) - $frameSize = [int]([Math]::Floor(144.0 * $br * 1000 / 44100)) + $padding - # After removing CRC, each frame is 2 bytes shorter, but we're patching in-place - # so we just skip the original frame size minus the CRC - $i += $frameSize - 1 - } else { - $i += 417 - } - } - } - - if ($patched -gt 0) { - [System.IO.File]::WriteAllBytes($File.FullName, $bytes) - Write-Host " Patched (CRC flag removed in $patched frames): $($File.Name)" - return $true - } - } catch { - # Binary patch failed - } - } - - return $false -} - -# Check and sanitize MP3 files for WoW 1.12 compatibility -Write-Host "" -Write-Host "Scanning MP3 files for WoW 1.12 compatibility..." -$mp3Files = @(Get-ChildItem -LiteralPath $Media -Recurse -File | - Where-Object { $_.Extension -match '^(?i)\.mp3$' }) - -$sanitized = 0 -$skipped = 0 -$scanErrors = 0 -$compatOk = 0 - -if ($mp3Files.Count -eq 0) { - Write-Host "No MP3 files found in Media folder." -} else { - Write-Host "Found $($mp3Files.Count) MP3 file(s) to check..." - foreach ($mp3 in $mp3Files) { - try { - $needsReencode, $reason = Test-Mp3NeedsReencode -FilePath $mp3.FullName - if ($needsReencode) { - Write-Host " Issue found: $($mp3.Name) - $reason" - try { - $fixed = Sanitize-Mp3File -File $mp3 - if ($fixed) { $sanitized++ } else { $skipped++ } - } catch { - Write-Host " WARNING: Could not sanitize $($mp3.Name): $($_.Exception.Message)" - $skipped++ - } - } else { - $compatOk++ - } - } catch { - Write-Host " WARNING: Could not scan $($mp3.Name): $($_.Exception.Message)" - $scanErrors++ - } - } - Write-Host "" - Write-Host " Compatible: $compatOk Sanitized: $sanitized Skipped: $skipped Errors: $scanErrors" -} - -if ($sanitized -gt 0) { - Write-Host "" - Write-Host "Sanitized $sanitized MP3 file(s) for WoW 1.12 compatibility." - Write-Host "These files were re-encoded to work with the in-game FMOD 3.x music engine." - Write-Host "" -} -if ($skipped -gt 0) { - Write-Host "" - Write-Host "WARNING: $skipped MP3 file(s) could not be sanitized (ffmpeg not available?)." - Write-Host "These tracks may not play correctly in-game. Install ffmpeg and re-run." - Write-Host "" -} -if ($sanitized -eq 0 -and $skipped -eq 0 -and $mp3Files.Count -gt 0) { - Write-Host "" - Write-Host "All $($mp3Files.Count) MP3 file(s) passed compatibility check." - Write-Host "" -} - -# -- Rename files with special characters for WoW 1.12 compatibility ---------- -# WoW 1.12's FMOD engine may fail to play files whose names contain characters -# like apostrophes ('), parentheses, or other special symbols. This step -# renames such files by replacing problematic characters with underscores. -# Only the physical filename is changed; the track name in the playlist -# preserves the original display name. - -$problematicChars = @("'", '"', '(', ')', '[', ']', '{', '}', '!', '&', '#', '%', '@', '+', '=', ',', ';') -$renamed = 0 -$allMediaFiles = @(Get-ChildItem -LiteralPath $Media -Recurse -File | - Where-Object { $_.Extension -match '^(?i)\.(mp3|wav|ogg)$' }) - -foreach ($mf in $allMediaFiles) { - $needsRename = $false - foreach ($ch in $problematicChars) { - if ($mf.Name.Contains($ch)) { - $needsRename = $true - break - } - } - - if ($needsRename) { - $newName = $mf.Name - foreach ($ch in $problematicChars) { - $newName = $newName.Replace($ch, '_') - } - # Remove double underscores and trailing/leading underscores - while ($newName.Contains('__')) { $newName = $newName.Replace('__', '_') } - - $newPath = Join-Path $mf.DirectoryName $newName - if ($newPath -ne $mf.FullName -and -not (Test-Path -LiteralPath $newPath)) { - try { - Rename-Item -LiteralPath $mf.FullName -NewName $newName -Force - Write-Host " Renamed: $($mf.Name) -> $newName" - $renamed++ - } catch { - Write-Host " WARNING: Could not rename $($mf.Name): $_" - } - } - } -} - -if ($renamed -gt 0) { - Write-Host "" - Write-Host "Renamed $renamed file(s) -- replaced special characters for WoW 1.12 path compatibility." - Write-Host "" -} - -# -- Scan Media folder ------------------------------------------------------- -$files = @(Get-ChildItem -LiteralPath $Media -Recurse -File | - Where-Object { $_.Extension -match '^(?i)\.(mp3|wav|ogg)$' } | - Sort-Object -Property Name) - -$lines = New-Object System.Collections.Generic.List[string] -$lines.Add('RelationshipsJukeboxAutoPlaylist = {}') | Out-Null -$lines.Add('RelationshipsJukeboxAutoPlaylist.tracks = {') | Out-Null - -$defaultCount = 0 -foreach ($file in $files) { - # Build the WoW-relative path (always backslashes for consistency) - $relative = $file.FullName.Substring($Root.Length).TrimStart('\', '/') - $relative = $relative.Replace('/', '\') - $luaPath = Escape-LuaString(($AddonPrefix + $relative)) - $name = Escape-LuaString($file.BaseName) - $duration = Get-TrackDurationSeconds -File $file - - if ($duration -eq $DEFAULT_DURATION) { - $defaultCount++ - } - - $fmtLuaEntry = ' {{ path = "{0}", name = "{1}", duration = {2} }},' - $lines.Add(($fmtLuaEntry -f $luaPath, $name, $duration)) | Out-Null -} - -$lines.Add('}') | Out-Null - -# -- Write AutoPlaylist.lua (UTF-8 no BOM) ----------------------------------- -$utf8NoBom = New-Object System.Text.UTF8Encoding($false) -[System.IO.File]::WriteAllLines($Out, $lines, $utf8NoBom) - -# -- Summary ----------------------------------------------------------------- -Write-Host "Generated: $Out" -Write-Host "Tracks: $($files.Count)" -if ($defaultCount -gt 0) { - Write-Host "" - Write-Host "$defaultCount track(s) using estimated/default duration $($DEFAULT_DURATION)s." - Write-Host "Autoplay and Shuffle will still work -- the addon advances after the estimated time." -} else { - Write-Host 'All track durations were captured from metadata successfully.' -} -Write-Host '' -Write-Host 'Next step: type /reload in game or restart the client.' - -# -- Cleanup COM objects ----------------------------------------------------- -if ($wmp) { try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($wmp) } catch {} } -if ($shell) { try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) } catch {} }