diff --git a/Core.lua b/Core.lua index b7be4ba..b93f92c 100644 --- a/Core.lua +++ b/Core.lua @@ -81,6 +81,47 @@ local function Count(t) return n end +----------------------------------------------------------------------- +-- Sound-CVar snapshot / restore. +-- +-- Every CVar this addon writes to (MasterSoundEffects, EnableMusic, +-- MasterVolume, ...) is snapshotted into SavedVariables the FIRST time +-- we change it in a session, and restored on PLAYER_LOGOUT. +-- +-- Why SavedVariables and not just an in-memory table: +-- KillAllSounds() sets MasterSoundEffects="0" and re-enables it ~50ms +-- later from the ticker. If the player /reloads, logs out, or the +-- client crashes inside that window, "0" is what WoW persists to +-- Config.wtf and every sound effect is muted on the next login with +-- no obvious cause. Persisting the pre-mute value lets us recover +-- on the very next VARIABLES_LOADED even after a hard crash. +----------------------------------------------------------------------- +local function SnapshotCVar(name) + if type(RelationshipsJukeboxDB) ~= "table" then RelationshipsJukeboxDB = {} end + if type(RelationshipsJukeboxDB.savedCVars) ~= "table" then + RelationshipsJukeboxDB.savedCVars = {} + end + if RelationshipsJukeboxDB.savedCVars[name] ~= nil then + return -- already snapshotted this session (or from a previous crashed session) + end + local cv = GetCVar or function() return nil end + local ok, val = pcall(cv, name) + if ok and val ~= nil then + RelationshipsJukeboxDB.savedCVars[name] = tostring(val) + end +end + +local function RestoreSavedCVars() + if type(RelationshipsJukeboxDB) ~= "table" then return end + local saved = RelationshipsJukeboxDB.savedCVars + if type(saved) ~= "table" then return end + local sc = SetCVar or function() end + for name, value in pairs(saved) do + pcall(sc, name, value) + end + RelationshipsJukeboxDB.savedCVars = nil +end + local function Trim(s) if not s then return "" end return (string.gsub(s, "^%s*(.-)%s*$", "%1")) @@ -366,6 +407,10 @@ end local function KillAllSounds(afterReenableCallback) local sc = SetCVar or function() end + -- Snapshot MasterSoundEffects BEFORE muting, so if the player /reloads + -- or crashes before the ticker re-enables it, we can restore it on + -- the next login instead of leaving Config.wtf with SFX muted. + SnapshotCVar("MasterSoundEffects") -- Kill ALL currently playing sounds by disabling the master sound CVar. pcall(sc, "MasterSoundEffects", "0") -- Also stop any PlayMusic track as a fallback. @@ -1352,9 +1397,12 @@ local function CreateUI() slider:SetScript("OnValueChanged", function() local pct = this:GetValue() local sc = SetCVar or function() end + -- Snapshot MasterVolume once so we can restore the player's + -- original master output level on logout. + SnapshotCVar("MasterVolume") -- MasterVolume is 0..1, slider is 0..100 pcall(sc, "MasterVolume", string.format("%.2f", pct / 100)) - -- Persist to SavedVariables + -- Persist slider position (not applied at login — see VARIABLES_LOADED) RelationshipsJukeboxDB.volume = pct end) end) @@ -1438,6 +1486,7 @@ local function CreateUI() -- Reset volume slider to default (100%) do local sc = SetCVar or function() end + SnapshotCVar("MasterVolume") pcall(sc, "MasterVolume", "1.0") RelationshipsJukeboxDB.volume = 100 if RJ.volumeSlider then @@ -1911,16 +1960,18 @@ logoutHandler:SetScript("OnEvent", function() RJ.pausedElapsed = nil RJ.currentDuration = 0 end - -- Always restore the player's original EnableMusic on logout, - -- even if no track was playing. - if RJ._originalEnableMusic then - local sc = SetCVar or function() end - pcall(sc, "EnableMusic", RJ._originalEnableMusic) - RJ._originalEnableMusic = nil - end - -- Suppress any remaining pending state + -- Suppress the pending MasterSoundEffects re-enable — we're about to + -- restore it explicitly from the snapshot below, and the ticker will + -- not fire again this session. RJ._pendingReenableAt = nil RJ._pendingReenableCallback = nil + -- Clear legacy in-memory copy (superseded by SavedVariables snapshot). + RJ._originalEnableMusic = nil + -- Restore every sound CVar the addon ever changed this session + -- (EnableMusic, MasterSoundEffects, MasterVolume, ...) back to the + -- value the player had before the addon loaded, and clear the + -- snapshot so the next login starts clean. + RestoreSavedCVars() end) ----------------------------------------------------------------------- @@ -2085,21 +2136,32 @@ loader:SetScript("OnEvent", function() EnsureDB() + -- CRASH-RECOVERY: if a previous session ended (logout, /reload, or crash) + -- without our PLAYER_LOGOUT handler firing — for example inside the ~50ms + -- window where KillAllSounds() has MasterSoundEffects="0" — restore the + -- pre-mute values from SavedVariables FIRST, before we do anything else. + -- This guarantees the player logs in with their real sound settings even + -- after a hard crash mid-mute. + RestoreSavedCVars() + -- Save the player's original EnableMusic value and disable game engine - -- music so it does not overlap with jukebox playback. This is restored - -- on logout or when the Reset button is pressed. + -- music so it does not overlap with jukebox playback. Restored on + -- PLAYER_LOGOUT via RestoreSavedCVars() (and on Reset). + -- + -- IMPORTANT: We deliberately do NOT touch MasterVolume here. MasterVolume + -- is the global master output CVar and controls ALL game audio (SFX, + -- ambience, jukebox, everything). A previous version applied the saved + -- jukebox slider value to MasterVolume at login, which muted every game + -- sound whenever the slider had been dragged down. The only thing that + -- should be silenced when this addon loads is the in-game music ("the + -- music box"), which is what EnableMusic="0" does. do local cv = GetCVar or function() return "1" end local sc = SetCVar or function() end + SnapshotCVar("EnableMusic") local ok, val = pcall(cv, "EnableMusic") RJ._originalEnableMusic = (ok and val) or "1" pcall(sc, "EnableMusic", "0") - -- Apply saved MasterVolume (master output volume). If the - -- player has never set it, the CVar default (1.0) remains. - if RelationshipsJukeboxDB.volume then - pcall(sc, "MasterVolume", - string.format("%.2f", RelationshipsJukeboxDB.volume / 100)) - end end imported, updated = MergeAutoPlaylist() diff --git a/GeneratePlaylist.ps1 b/GeneratePlaylist.ps1 index d6de9d2..6e303d6 100644 --- a/GeneratePlaylist.ps1 +++ b/GeneratePlaylist.ps1 @@ -771,6 +771,106 @@ if ($renameSkipped -gt 0) { Write-Host "" } +# -- Rename numeric-only MP3 filenames using embedded ID3 title -------------- +# Some MP3s have filenames that are just numbers (e.g. "01.mp3", "1234.mp3", +# "01 02 03.mp3"). These are useless in the playlist UI. When we detect such +# a file, read the embedded ID3 title tag via Shell.Application and rename the +# file to ".mp3" (sanitized for WoW 1.12 path compatibility). + +function Get-Mp3EmbeddedTitle { + param([Parameter(Mandatory=$true)][string]$FilePath) + try { + $shell = New-Object -ComObject Shell.Application + $folder = $shell.Namespace((Split-Path -Parent $FilePath)) + if ($null -eq $folder) { return $null } + $item = $folder.ParseName((Split-Path -Leaf $FilePath)) + if ($null -eq $item) { return $null } + # Property index 21 = Title on Windows; fall back to scanning if empty. + $title = $folder.GetDetailsOf($item, 21) + if ([string]::IsNullOrWhiteSpace($title)) { + for ($i = 0; $i -lt 400; $i++) { + $header = $folder.GetDetailsOf($null, $i) + if ($header -eq 'Title') { + $title = $folder.GetDetailsOf($item, $i) + break + } + } + } + if ([string]::IsNullOrWhiteSpace($title)) { return $null } + return $title.Trim() + } catch { + return $null + } +} + +$numericRenamed = 0 +$numericSkipped = 0 +$mp3Files = @(Get-ChildItem -LiteralPath $Media -Recurse -File | + Where-Object { $_.Extension -match '^(?i)\.mp3$' -and $_.Name -notmatch '\.sanitize\.tmp\.' }) + +foreach ($mf in $mp3Files) { + $base = [System.IO.Path]::GetFileNameWithoutExtension($mf.Name) + # "Only numbers": digits, optionally separated by spaces / dots / dashes / underscores. + if ($base -notmatch '^[\d\s\.\-_]+$' -or $base -notmatch '\d') { continue } + + $title = Get-Mp3EmbeddedTitle -FilePath $mf.FullName + if ([string]::IsNullOrWhiteSpace($title)) { + Write-Host " Skipped (no embedded title): $($mf.Name)" + $numericSkipped++ + continue + } + + # Sanitize the title for filesystem + WoW 1.12 compatibility. + $safeTitle = $title + foreach ($ch in $problematicChars) { $safeTitle = $safeTitle.Replace($ch, '_') } + foreach ($ch in [System.IO.Path]::GetInvalidFileNameChars()) { + $safeTitle = $safeTitle.Replace([string]$ch, '_') + } + while ($safeTitle.Contains('__')) { $safeTitle = $safeTitle.Replace('__', '_') } + $safeTitle = $safeTitle.Trim(' ', '_', '.') + + if ([string]::IsNullOrWhiteSpace($safeTitle)) { + Write-Host " Skipped (title sanitized to empty): $($mf.Name)" + $numericSkipped++ + continue + } + + $newName = "$safeTitle.mp3" + $newPath = Join-Path $mf.DirectoryName $newName + if ($newPath -eq $mf.FullName) { continue } + + # Avoid collisions by appending a numeric suffix. + if (Test-Path -LiteralPath $newPath) { + $n = 2 + do { + $candidate = Join-Path $mf.DirectoryName ("$safeTitle ($n).mp3") + $n++ + } while (Test-Path -LiteralPath $candidate) + $newPath = $candidate + $newName = Split-Path -Leaf $candidate + } + + try { + Rename-Item -LiteralPath $mf.FullName -NewName $newName -Force + Write-Host " Renamed (numeric -> title): $($mf.Name) -> $newName" + $numericRenamed++ + } catch { + Write-Host " WARNING: Could not rename $($mf.Name): $_" + $numericSkipped++ + } +} + +if ($numericRenamed -gt 0) { + Write-Host "" + Write-Host "Renamed $numericRenamed numeric-only MP3 file(s) using embedded ID3 title." + Write-Host "" +} +if ($numericSkipped -gt 0) { + Write-Host "" + Write-Host "$numericSkipped numeric-only MP3 file(s) skipped (no title / collision / error)." + Write-Host "" +} + # --------------------------------------------------------------------------- # 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. diff --git a/RelationshipsJukeBox.toc b/RelationshipsJukeBox.toc index 5032f43..e1bfb01 100644 --- a/RelationshipsJukeBox.toc +++ b/RelationshipsJukeBox.toc @@ -2,7 +2,7 @@ ## Title: Relationships Jukebox ## Notes: Simple in-game MP3/WAV/OGG player with GUI. ## Author: Relationship -## Version: 1.3 +## Version: 1.3.2 ## SavedVariables: RelationshipsJukeboxDB ## X-Website: https://github.com/Relationship-OctoWoW/RelationshipsJukeBox ## X-GitHub: https://github.com/Relationship-OctoWoW/RelationshipsJukeBox