modules/UI/PeakDashboard.psm1
|
# Peak Dashboard Module # Shows real-time track insights in a Windows Form window # Uses standard Spotify API data (no extended quota required) function Show-PeakDashboard { <# .SYNOPSIS Display track insights dashboard for the currently playing track .DESCRIPTION Creates a non-blocking Windows Form that displays track and artist data (popularity, artist popularity, followers, genres, album info, duration) with progress bars. Automatically updates when the track changes. .EXAMPLE Show-PeakDashboard .EXAMPLE peak #> $scriptPath = $PSScriptRoot $modulePath = Split-Path -Parent $scriptPath $rootPath = Split-Path -Parent $modulePath # Get current environment variables for Spotify credentials $clientId = $env:SPOTIFY_CLIENT_ID $clientSecret = $env:SPOTIFY_CLIENT_SECRET # Create temp directory $tempDir = Join-Path $env:TEMP "SpotifyPeak" if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null } $backgroundScript = @" Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # Set credentials for token refresh `$env:SPOTIFY_CLIENT_ID = '$clientId' `$env:SPOTIFY_CLIENT_SECRET = '$clientSecret' # Import API client Import-Module '$rootPath\modules\Core\LegacyApiClient.psm1' -Force -ErrorAction SilentlyContinue # --- State --- `$script:currentTrackUri = `$null `$script:popHistory = [System.Collections.Generic.List[int]]::new() # --- Colors --- `$bgColor = [System.Drawing.ColorTranslator]::FromHtml('#1A1A1A') `$bgLight = [System.Drawing.ColorTranslator]::FromHtml('#2D2D2D') `$accent = [System.Drawing.ColorTranslator]::FromHtml('#4ECDC4') `$textWhite = [System.Drawing.Color]::White `$textGray = [System.Drawing.ColorTranslator]::FromHtml('#B3B3B3') `$goldColor = [System.Drawing.ColorTranslator]::FromHtml('#FFD700') `$pinkColor = [System.Drawing.ColorTranslator]::FromHtml('#FF69B4') # --- Form --- `$form = New-Object System.Windows.Forms.Form `$form.Text = "Peak Dashboard" `$form.Size = New-Object System.Drawing.Size(420, 530) `$form.StartPosition = 'CenterScreen' `$form.BackColor = `$bgColor `$form.ForeColor = `$textWhite `$form.FormBorderStyle = 'FixedToolWindow' `$form.TopMost = `$true # --- Header: Song + Artist --- `$lblSong = New-Object System.Windows.Forms.Label `$lblSong.Location = New-Object System.Drawing.Point(15, 12) `$lblSong.Size = New-Object System.Drawing.Size(375, 24) `$lblSong.Font = New-Object System.Drawing.Font('Segoe UI', 12, [System.Drawing.FontStyle]::Bold) `$lblSong.ForeColor = `$accent `$lblSong.Text = "Loading..." `$form.Controls.Add(`$lblSong) `$lblArtist = New-Object System.Windows.Forms.Label `$lblArtist.Location = New-Object System.Drawing.Point(15, 38) `$lblArtist.Size = New-Object System.Drawing.Size(375, 20) `$lblArtist.Font = New-Object System.Drawing.Font('Segoe UI', 9) `$lblArtist.ForeColor = `$textGray `$lblArtist.Text = "" `$form.Controls.Add(`$lblArtist) # --- Metric rows with progress bars --- `$metrics = @( @{ Name = "Track Pop."; Tag = "trackpop" }, @{ Name = "Artist Pop."; Tag = "artistpop" } ) `$script:bars = @{} `$script:valueLabels = @{} `$yStart = 72 `$rowHeight = 42 foreach (`$m in `$metrics) { `$y = `$yStart + (`$metrics.IndexOf(`$m) * `$rowHeight) `$nameLabel = New-Object System.Windows.Forms.Label `$nameLabel.Location = New-Object System.Drawing.Point(15, `$y) `$nameLabel.Size = New-Object System.Drawing.Size(90, 18) `$nameLabel.Font = New-Object System.Drawing.Font('Segoe UI', 9) `$nameLabel.ForeColor = `$textGray `$nameLabel.Text = `$m.Name `$form.Controls.Add(`$nameLabel) `$bar = New-Object System.Windows.Forms.ProgressBar `$bar.Location = New-Object System.Drawing.Point(110, `$y) `$bar.Size = New-Object System.Drawing.Size(210, 18) `$bar.Style = 'Continuous' `$bar.ForeColor = `$accent `$bar.BackColor = `$bgLight `$bar.Minimum = 0 `$bar.Maximum = 100 `$bar.Value = 0 `$form.Controls.Add(`$bar) `$script:bars[`$m.Tag] = `$bar `$valLabel = New-Object System.Windows.Forms.Label `$valLabel.Location = New-Object System.Drawing.Point(328, `$y) `$valLabel.Size = New-Object System.Drawing.Size(70, 18) `$valLabel.Font = New-Object System.Drawing.Font('Consolas', 9) `$valLabel.ForeColor = `$textWhite `$valLabel.Text = "---" `$valLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight `$form.Controls.Add(`$valLabel) `$script:valueLabels[`$m.Tag] = `$valLabel } # --- Info section --- `$yInfo = `$yStart + (`$metrics.Count * `$rowHeight) + 8 # Followers row `$lblFollowersTitle = New-Object System.Windows.Forms.Label `$lblFollowersTitle.Location = New-Object System.Drawing.Point(15, `$yInfo) `$lblFollowersTitle.Size = New-Object System.Drawing.Size(90, 18) `$lblFollowersTitle.Font = New-Object System.Drawing.Font('Segoe UI', 9) `$lblFollowersTitle.ForeColor = `$textGray `$lblFollowersTitle.Text = "Followers" `$form.Controls.Add(`$lblFollowersTitle) `$lblFollowers = New-Object System.Windows.Forms.Label `$lblFollowers.Location = New-Object System.Drawing.Point(110, `$yInfo) `$lblFollowers.Size = New-Object System.Drawing.Size(280, 18) `$lblFollowers.Font = New-Object System.Drawing.Font('Consolas', 9) `$lblFollowers.ForeColor = `$goldColor `$lblFollowers.Text = "---" `$form.Controls.Add(`$lblFollowers) # Duration row `$yDur = `$yInfo + 28 `$lblDurTitle = New-Object System.Windows.Forms.Label `$lblDurTitle.Location = New-Object System.Drawing.Point(15, `$yDur) `$lblDurTitle.Size = New-Object System.Drawing.Size(90, 18) `$lblDurTitle.Font = New-Object System.Drawing.Font('Segoe UI', 9) `$lblDurTitle.ForeColor = `$textGray `$lblDurTitle.Text = "Duration" `$form.Controls.Add(`$lblDurTitle) `$lblDuration = New-Object System.Windows.Forms.Label `$lblDuration.Location = New-Object System.Drawing.Point(110, `$yDur) `$lblDuration.Size = New-Object System.Drawing.Size(280, 18) `$lblDuration.Font = New-Object System.Drawing.Font('Consolas', 9) `$lblDuration.ForeColor = `$textWhite `$lblDuration.Text = "---" `$form.Controls.Add(`$lblDuration) # Album row `$yAlbum = `$yDur + 28 `$lblAlbumTitle = New-Object System.Windows.Forms.Label `$lblAlbumTitle.Location = New-Object System.Drawing.Point(15, `$yAlbum) `$lblAlbumTitle.Size = New-Object System.Drawing.Size(90, 18) `$lblAlbumTitle.Font = New-Object System.Drawing.Font('Segoe UI', 9) `$lblAlbumTitle.ForeColor = `$textGray `$lblAlbumTitle.Text = "Album" `$form.Controls.Add(`$lblAlbumTitle) `$lblAlbum = New-Object System.Windows.Forms.Label `$lblAlbum.Location = New-Object System.Drawing.Point(110, `$yAlbum) `$lblAlbum.Size = New-Object System.Drawing.Size(280, 18) `$lblAlbum.Font = New-Object System.Drawing.Font('Segoe UI', 9) `$lblAlbum.ForeColor = `$textWhite `$lblAlbum.Text = "---" `$form.Controls.Add(`$lblAlbum) # Release + Track number row `$yRelease = `$yAlbum + 22 `$lblRelease = New-Object System.Windows.Forms.Label `$lblRelease.Location = New-Object System.Drawing.Point(110, `$yRelease) `$lblRelease.Size = New-Object System.Drawing.Size(280, 18) `$lblRelease.Font = New-Object System.Drawing.Font('Segoe UI', 8) `$lblRelease.ForeColor = `$textGray `$lblRelease.Text = "" `$form.Controls.Add(`$lblRelease) # Explicit badge `$lblExplicit = New-Object System.Windows.Forms.Label `$lblExplicit.Location = New-Object System.Drawing.Point(15, `$yRelease) `$lblExplicit.Size = New-Object System.Drawing.Size(90, 18) `$lblExplicit.Font = New-Object System.Drawing.Font('Segoe UI', 8, [System.Drawing.FontStyle]::Bold) `$lblExplicit.ForeColor = `$pinkColor `$lblExplicit.Text = "" `$form.Controls.Add(`$lblExplicit) # --- Genres --- `$yGenres = `$yRelease + 28 `$lblGenresTitle = New-Object System.Windows.Forms.Label `$lblGenresTitle.Location = New-Object System.Drawing.Point(15, `$yGenres) `$lblGenresTitle.Size = New-Object System.Drawing.Size(90, 18) `$lblGenresTitle.Font = New-Object System.Drawing.Font('Segoe UI', 9) `$lblGenresTitle.ForeColor = `$textGray `$lblGenresTitle.Text = "Genres" `$form.Controls.Add(`$lblGenresTitle) `$lblGenres = New-Object System.Windows.Forms.Label `$lblGenres.Location = New-Object System.Drawing.Point(110, `$yGenres) `$lblGenres.Size = New-Object System.Drawing.Size(280, 36) `$lblGenres.Font = New-Object System.Drawing.Font('Segoe UI', 8) `$lblGenres.ForeColor = `$accent `$lblGenres.Text = "---" `$form.Controls.Add(`$lblGenres) # --- Mini-graph panel (popularity history) --- `$yGraph = `$yGenres + 44 `$graphPanel = New-Object System.Windows.Forms.Panel `$graphPanel.Location = New-Object System.Drawing.Point(15, `$yGraph) `$graphPanel.Size = New-Object System.Drawing.Size(375, 60) `$graphPanel.BackColor = `$bgLight `$form.Controls.Add(`$graphPanel) `$lblGraphTitle = New-Object System.Windows.Forms.Label `$lblGraphTitle.Location = New-Object System.Drawing.Point(5, 2) `$lblGraphTitle.Size = New-Object System.Drawing.Size(365, 16) `$lblGraphTitle.Font = New-Object System.Drawing.Font('Segoe UI', 8) `$lblGraphTitle.ForeColor = `$textGray `$lblGraphTitle.Text = "Popularity (last 10 tracks)" `$graphPanel.Controls.Add(`$lblGraphTitle) `$lblGraph = New-Object System.Windows.Forms.Label `$lblGraph.Location = New-Object System.Drawing.Point(5, 22) `$lblGraph.Size = New-Object System.Drawing.Size(365, 34) `$lblGraph.Font = New-Object System.Drawing.Font('Consolas', 18) `$lblGraph.ForeColor = `$accent `$lblGraph.Text = "" `$graphPanel.Controls.Add(`$lblGraph) # --- Status label --- `$yStatus = `$yGraph + 65 `$lblStatus = New-Object System.Windows.Forms.Label `$lblStatus.Location = New-Object System.Drawing.Point(15, `$yStatus) `$lblStatus.Size = New-Object System.Drawing.Size(375, 18) `$lblStatus.Font = New-Object System.Drawing.Font('Segoe UI', 8) `$lblStatus.ForeColor = `$textGray `$lblStatus.Text = "Waiting for track..." `$form.Controls.Add(`$lblStatus) # --- Helper: value to block char --- function Get-BlockChar { param([double]`$val) `$blocks = @([char]0x2581, [char]0x2582, [char]0x2583, [char]0x2584, [char]0x2585, [char]0x2586, [char]0x2587, [char]0x2588) `$idx = [Math]::Min(7, [Math]::Max(0, [int](`$val * 7.99))) return `$blocks[`$idx] } # --- Helper: format follower count --- function Format-Number { param([long]`$n) if (`$n -ge 1000000) { return "{0:N1}M" -f (`$n / 1000000) } if (`$n -ge 1000) { return "{0:N1}K" -f (`$n / 1000) } return `$n.ToString("N0") } # --- Helper: update dashboard from playback + artist data --- function Update-Dashboard { param(`$playback, `$artistData) `$item = `$playback.item # Track popularity (0-100) `$trackPop = [int]`$item.popularity `$script:bars['trackpop'].Value = [Math]::Min(100, [Math]::Max(0, `$trackPop)) `$script:valueLabels['trackpop'].Text = "`$trackPop/100" # Artist popularity (0-100) if (`$artistData) { `$artistPop = [int]`$artistData.popularity `$script:bars['artistpop'].Value = [Math]::Min(100, [Math]::Max(0, `$artistPop)) `$script:valueLabels['artistpop'].Text = "`$artistPop/100" # Followers `$followers = [long]`$artistData.followers.total `$lblFollowers.Text = Format-Number `$followers # Genres `$genres = `$artistData.genres if (`$genres -and `$genres.Count -gt 0) { `$lblGenres.Text = (`$genres | Select-Object -First 4) -join ", " } else { `$lblGenres.Text = "n/a" } } # Duration `$durMs = [long]`$item.duration_ms `$durMin = [int][Math]::Floor(`$durMs / 60000) `$durSec = [int][Math]::Floor((`$durMs % 60000) / 1000) `$lblDuration.Text = "{0}:{1:D2}" -f `$durMin, `$durSec # Album `$lblAlbum.Text = `$item.album.name `$releaseDate = `$item.album.release_date `$trackNum = `$item.track_number `$totalTracks = `$item.album.total_tracks `$lblRelease.Text = "Released: `$releaseDate | Track `$trackNum of `$totalTracks" # Explicit `$lblExplicit.Text = if (`$item.explicit) { "EXPLICIT" } else { "" } # Update popularity history graph `$script:popHistory.Add(`$trackPop) if (`$script:popHistory.Count -gt 10) { `$script:popHistory.RemoveAt(0) } `$graphChars = (`$script:popHistory | ForEach-Object { Get-BlockChar (`$_ / 100.0) }) -join " " `$lblGraph.Text = `$graphChars `$lblStatus.Text = "Updated: `$(Get-Date -Format 'HH:mm:ss')" } # --- Timer: poll every 2 seconds --- `$timer = New-Object System.Windows.Forms.Timer `$timer.Interval = 2000 `$timer.Add_Tick({ try { `$playback = Invoke-SpotifyApi -Method GET -Path '/me/player' -ErrorAction SilentlyContinue if (-not `$playback -or -not `$playback.item) { `$lblStatus.Text = "No track playing" return } `$trackUri = `$playback.item.uri `$isPlaying = `$playback.is_playing if (-not `$isPlaying) { `$lblStatus.Text = "Paused" } # Only update on track change if (`$trackUri -ne `$script:currentTrackUri) { `$script:currentTrackUri = `$trackUri # Update header `$songName = `$playback.item.name `$artists = (`$playback.item.artists | ForEach-Object { `$_.name }) -join ", " `$lblSong.Text = `$songName `$lblArtist.Text = `$artists `$form.Text = "Peak - `$songName" # Fetch artist details (popularity, followers, genres) `$artistData = `$null try { `$artistId = `$playback.item.artists[0].id `$artistData = Invoke-SpotifyApi -Method GET -Path "/artists/`$artistId" -ErrorAction SilentlyContinue } catch { } Update-Dashboard `$playback `$artistData } } catch { # Silently ignore errors } }.GetNewClosure()) `$timer.Start() # --- Do initial fetch immediately --- try { `$initPlayback = Invoke-SpotifyApi -Method GET -Path '/me/player' -ErrorAction SilentlyContinue if (`$initPlayback -and `$initPlayback.item) { `$script:currentTrackUri = `$initPlayback.item.uri `$lblSong.Text = `$initPlayback.item.name `$lblArtist.Text = (`$initPlayback.item.artists | ForEach-Object { `$_.name }) -join ", " `$form.Text = "Peak - `$(`$initPlayback.item.name)" `$artistData = `$null try { `$artistId = `$initPlayback.item.artists[0].id `$artistData = Invoke-SpotifyApi -Method GET -Path "/artists/`$artistId" -ErrorAction SilentlyContinue } catch { } Update-Dashboard `$initPlayback `$artistData } } catch { `$lblStatus.Text = "Init error: `$(`$_.Exception.Message)" } # --- Cleanup --- `$form.Add_FormClosing({ `$timer.Stop() `$timer.Dispose() }) `$form.Add_FormClosed({ Remove-Item '$scriptFile' -Force -ErrorAction SilentlyContinue }) [void]`$form.ShowDialog() "@ # Save script to temp file $scriptFile = Join-Path $tempDir "peak_dashboard_$(Get-Date -Format 'yyyyMMddHHmmss').ps1" $backgroundScript | Out-File -FilePath $scriptFile -Encoding UTF8 # Start background process Start-Process pwsh -ArgumentList @( '-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', $scriptFile ) Write-Host "Peak Dashboard launched!" -ForegroundColor Green Write-Host "Dashboard updates automatically when song changes" -ForegroundColor Cyan } Set-Alias -Name peak -Value Show-PeakDashboard -ErrorAction SilentlyContinue Export-ModuleMember -Function @('Show-PeakDashboard') -Alias @('peak') |