Public/Start-MusicForProgramming.ps1
|
function Start-MusicForProgramming { <# .SYNOPSIS Starts playback of a Music For Programming episode. .DESCRIPTION Plays a Music For Programming episode using the platform's default audio handler. Defaults to a random episode. Local files in the default music directory are always preferred over streaming. Falls back to streaming when a file is not available locally. Playback runs in the background by default so your shell stays usable. Use -Foreground if you want blocking playback. .PARAMETER EpisodeNumber Play a specific episode by number (1–78+). Cannot be combined with -Random. .PARAMETER Random Explicitly request a randomly chosen episode. This is the default behaviour when no parameter set is specified. .PARAMETER Path Directory containing locally downloaded MFP files. Local files are preferred over streaming. Defaults to ~/Music/MusicForProgramming. Override for all commands at once via: $PSDefaultParameterValues['*-MusicForProgramming:Path'] = 'D:\MyMusic' .PARAMETER Foreground Play in the foreground (blocking) instead of background. .EXAMPLE Start-MusicForProgramming Plays a random episode — local if available, otherwise streams. .EXAMPLE Start-MusicForProgramming -EpisodeNumber 42 Plays episode 42 — local if downloaded, otherwise streams. .EXAMPLE Start-MusicForProgramming -Random Picks a random episode. #> [CmdletBinding(DefaultParameterSetName = 'Random')] [OutputType([string])] param( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'ByNumber')] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) (Get-MusicForProgramming).EpisodeNumber | Where-Object { "$_" -like "$wordToComplete*" } | Sort-Object })] [int]$EpisodeNumber, [Parameter(ParameterSetName = 'Random')] [switch]$Random, [Parameter()] [string]$Path = $script:DefaultMusicPath, [Parameter()] [switch]$Foreground ) if ($PSCmdlet.ParameterSetName -eq 'ByNumber') { Write-Verbose "Looking up episode $EpisodeNumber (local-first)" $episode = Get-MusicForProgramming -EpisodeNumber $EpisodeNumber -Path $Path if (-not $episode) { throw "Episode $EpisodeNumber was not found." } } else { Write-Verbose "Selecting a random episode (local-first)" $episode = Get-MusicForProgramming -Path $Path | Get-Random } $displayTitle = if ($episode.PSObject.Properties['FullTitle']) { $episode.FullTitle } else { $episode.Title } $displayDuration = if ($episode.PSObject.Properties['Duration']) { $episode.Duration } else { '' } Write-Verbose "Selected: $displayTitle $displayDuration" # Episode objects from local collection have FilePath; remote ones have Url if ($episode.FilePath) { $playback = Invoke-MFPLocalPlay -FilePath $episode.FilePath -Foreground:$Foreground $result = "Playing local: $($episode.Title)" } else { $playback = Invoke-MFPStreamPlay -Url $episode.Url -Foreground:$Foreground $result = "Streaming: $($episode.Title)" } if ($playback -and $playback.Trackable) { $result = "$result (pid $($playback.ProcessId))" } elseif (-not $Foreground) { $result = "$result (untracked player)" } Write-Host $result return $result } # --------------------------------------------------------------------------- # Private helpers — not exported # --------------------------------------------------------------------------- function Invoke-MFPLocalPlay { <# Plays a local audio file using the best available method for the current platform and tracks background process state when possible. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$FilePath, [Parameter()] [switch]$Foreground ) if (-not (Test-Path $FilePath)) { throw "Audio file not found: $FilePath" } if ($IsMacOS) { Write-Verbose "Using 'afplay' on macOS" if ($Foreground) { try { & afplay $FilePath Clear-MFPPlaybackState return } catch { throw "Failed to play '$FilePath' on macOS: $_" } } try { $proc = Start-Process -FilePath 'afplay' -ArgumentList $FilePath -PassThru return Set-MFPPlaybackState -Process $proc -Target $FilePath -Mode 'local' } catch { throw "Failed to play '$FilePath' on macOS: $_" } } elseif ($IsLinux) { $player = foreach ($cmd in 'mpg123', 'mpv', 'ffplay', 'vlc') { if (Get-Command $cmd -ErrorAction SilentlyContinue) { $cmd; break } } if (-not $player) { if ($Foreground) { throw "No supported Linux player found (mpg123/mpv/ffplay/vlc)." } # Fall back to xdg-open (untracked) Write-Verbose "No CLI audio player found; using xdg-open (untracked)" xdg-open $FilePath Clear-MFPPlaybackState return [PSCustomObject]@{ Trackable = $false } } Write-Verbose "Using '$player' on Linux" if ($Foreground) { & $player $FilePath Clear-MFPPlaybackState return } $proc = Start-Process -FilePath $player -ArgumentList $FilePath -PassThru return Set-MFPPlaybackState -Process $proc -Target $FilePath -Mode 'local' } else { # Windows — open with the default associated application Write-Verbose "Using Start-Process on Windows" try { $proc = Start-Process -FilePath $FilePath -PassThru if ($Foreground -and $proc) { Wait-Process -Id $proc.Id Clear-MFPPlaybackState return } if ($proc) { return Set-MFPPlaybackState -Process $proc -Target $FilePath -Mode 'local' } Clear-MFPPlaybackState return [PSCustomObject]@{ Trackable = $false } } catch { throw "Failed to open '$FilePath' on Windows: $_" } } } function Invoke-MFPStreamPlay { <# Opens a streaming URL and tracks process state when possible. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Url, [Parameter()] [switch]$Foreground ) if ($IsMacOS) { # Prefer a player process we can track, then fall back to open $player = foreach ($cmd in 'mpv', 'vlc', 'ffplay') { if (Get-Command $cmd -ErrorAction SilentlyContinue) { $cmd; break } } if ($player) { Write-Verbose "Using '$player' to stream on macOS" if ($Foreground) { & $player $Url Clear-MFPPlaybackState return } $proc = Start-Process -FilePath $player -ArgumentList $Url -PassThru return Set-MFPPlaybackState -Process $proc -Target $Url -Mode 'stream' } Write-Verbose "Opening stream with 'open' on macOS (untracked)" try { open $Url Clear-MFPPlaybackState return [PSCustomObject]@{ Trackable = $false } } catch { throw "Failed to open stream on macOS: $_" } } elseif ($IsLinux) { # Prefer a player that can stream directly $player = foreach ($cmd in 'mpv', 'vlc', 'ffplay', 'mpg123') { if (Get-Command $cmd -ErrorAction SilentlyContinue) { $cmd; break } } if ($player) { Write-Verbose "Using '$player' to stream on Linux" if ($Foreground) { & $player $Url Clear-MFPPlaybackState return } $proc = Start-Process -FilePath $player -ArgumentList $Url -PassThru return Set-MFPPlaybackState -Process $proc -Target $Url -Mode 'stream' } if ($Foreground) { throw "No supported Linux streaming player found (mpv/vlc/ffplay/mpg123)." } Write-Verbose "No streaming player found; using xdg-open (untracked)" xdg-open $Url Clear-MFPPlaybackState return [PSCustomObject]@{ Trackable = $false } } else { # Windows — let the OS handle the URL (opens in default browser/media player) Write-Verbose "Using Start-Process on Windows" $proc = Start-Process $Url -PassThru if ($Foreground -and $proc) { Wait-Process -Id $proc.Id Clear-MFPPlaybackState return } if ($proc) { return Set-MFPPlaybackState -Process $proc -Target $Url -Mode 'stream' } Clear-MFPPlaybackState return [PSCustomObject]@{ Trackable = $false } } } function Set-MFPPlaybackState { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Diagnostics.Process]$Process, [Parameter(Mandatory)] [string]$Target, [Parameter(Mandatory)] [string]$Mode ) $state = [PSCustomObject]@{ Trackable = $true ProcessId = $Process.Id Mode = $Mode Target = $Target StartedAt = Get-Date } $script:CurrentPlayback = $state return $state } function Clear-MFPPlaybackState { [CmdletBinding()] param() $script:CurrentPlayback = $null } |