functions/Invoke-TuiMp3.ps1
|
using namespace Terminal.Gui # A sample MP3 player TUI function Invoke-TuiMp3 { [cmdletbinding()] [OutputType('None')] [Alias('tuimp3')] param( [parameter(Position = 0, HelpMessage = 'Specify the path to an MP3 file.')] [Alias('Path')] [ValidateNotNullOrEmpty()] [ValidatePattern('\.m((p3)|(4a))$', ErrorMessage = '{0} does not appear to be a .mp3 or .m4a file.')] [ValidateScript({ Test-Path -Path $_ }, ErrorMessage = 'Failed to find or validate {0}.')] [string]$FilePath, [Parameter(HelpMessage = 'Specify the window title.')] [validateNotNullOrEmpty()] [string]$Title = 'PSMusic Player', [Parameter(HelpMessage = 'Specify the default folder to open for MP3 files.')] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path -Path $_ }, ErrorMessage = 'Failed to find or validate {0}.')] [alias('Library')] [string]$DefaultLibrary = $HOME ) if ($IsLinux -or $IsMacOS) { Write-Warning 'This command requires a Windows platform.' return } #region initialize #26 February 2026 Setting the most recent list limit. This might become a parameter $mrpLimit = 10 #the path to store the most recently played $mrpHistory = Join-Path -Path $home -ChildPath .tuiMp3-most-recent.txt #9 March 2026 Moved code to stop playing to an internal helper function #so that it can be invoked from other actions function mediaStop { $script:isPlaying = $false $MediaPlayer.Stop() if ($null -ne $script:timeoutToken) { [Application]::MainLoop.RemoveTimeout($script:timeoutToken) $script:timeoutToken = $null } $progBar.Fraction = 0 $StatusBar.Items[0].Title = $(Get-Date -Format g) $StatusBar.Items[2].Title = 'Stopped' [Application]::Refresh() } #need this type for the media player Add-Type -AssemblyName PresentationCore $MediaPlayer = New-Object System.Windows.Media.MediaPlayer $ver = $((Get-Module PSTuiTools).version) #You MUST invoke Init() [Application]::Init() #I recommend setting a QuitKey [Application]::QuitKey = 'Esc' #endregion #region create the main window and status bar $window = [Window]@{Title = $Title } <# add an event handler to stop the player when the window closes if running #> $window.Add_Loaded({ $script:recentList = $mrp.children.foreach({ $_.Title.ToString() }) }) $window.Add_Unloaded({ if ($script:isPlaying) { $MediaPlayer.Stop() } }) <# Use this code if you want to customize the Window color scheme Valid colors: Black Blue BrightBlue BrightCyan BrightGreen BrightMagenta BrightRed BrightYellow Brown Cyan DarkGray Gray Green Magenta Red White New(Foreground,Background) $n = [Terminal.Gui.Attribute]::new('BrightYellow', 'Black') $cs = [ColorScheme]::new() $cs.normal = $n $Window.ColorScheme = $cs #> #Create a status bar at the bottom of the TUI $StatusBar = [StatusBar]::New( @( [StatusItem]::New('Unknown', $(Get-Date -Format g), {}), [StatusItem]::New('Unknown', "v$Ver", {}), [StatusItem]::New('Unknown', 'Ready', {}) ) ) #Add the control to the application [Application]::Top.Add($StatusBar) #endregion #region add menu #the menu bar actions are running private helper functions $MenuItem0 = [MenuItem]::New('_Open File', '', { $open = OpenFile -filter '.mp3', '.m4a' -path $DefaultLibrary if ($open) { #update the field if a file was selected $txtFile.Text = $open #reset progress $progBar.Fraction = 0 #if Playing then stop if ($script:isPlaying) { mediaStop } } }) #26 February 2026 Add a nested menu for recently played # test items #$mrp1 = [MenuItem]::New("D:\music\Get Back.mp3",'',{ $txtFile.Text = "D:\Music\Get Back.mp3";[Application]::Refresh}) #$mrp2 = [MenuItem]::New("D:\Music\01 Anthem.mp3",'',{ $txtFile.Text = "D:\Music\01 Anthem.mp3";[Application]::Refresh}) #load most recently played from an external source $recent = @() if (Test-Path $mrpHistory) { Get-Content $mrpHistory | ForEach-Object { #only add the file to the list if it still exists if (Test-Path -LiteralPath $_) { $cmd = "updateMP3Info ""$_"" ;[Application]::Refresh()" $action = [Scriptblock]::Create($cmd) $recent += [MenuItem]::New($_, '', $action) } } } $mrp = [MenuBarItem]::New('Recently Played', $recent) $MenuItem1 = [MenuItem]::New('_Quit', '', { quitMP3 }) $MenuBarItem0 = [MenuBarItem]::New('_File', @($MenuItem0, $mrp, $MenuItem1)) $MenuItem3 = [MenuItem]::New('_Documentation', '', { ShowMp3Help }) $MenuItem4 = [MenuItem]::New('_About', '', { ShowMp3About }) $MenuBarItem1 = [MenuBarItem]::New('_Help', @($MenuItem3, $MenuItem4)) $MenuBar = [MenuBar]::New(@($MenuBarItem0, $MenuBarItem1)) $Window.Add($MenuBar) #endregion #region add controls $txtFile = [TextField]@{ #X and Y are relative positions in the window X = 1 Y = 2 Width = [Dim]::Percent(80) Height = 1 ColorScheme = $window.ColorScheme Text = 'Select an MP3 file from the menu' } $n = [Terminal.Gui.Attribute]::new('White', 'Blue') $cs = [ColorScheme]::new() $cs.normal = $n $cs.Focus = $n $txtFile.ColorScheme = $cs $txtFile.Add_TextChanged({ UpdateMP3Info }) #add the control to the window $window.Add($txtFile) $progFrame = [FrameView]@{ X = 2 Y = 4 Width = [Dim]::Percent(50) Height = 5 Title = '' } $progBar = [ProgressBar]@{ X = 1 Y = 1 ProgressBarFormat = 'SimplePlusPercentage' ProgressBarStyle = 'Continuous' Fraction = 0 Width = [Dim]::Percent(98) } $progBar.Add_MouseClick({ param($m) $totalSeconds = $MediaPlayer.NaturalDuration.TimeSpan.TotalSeconds if ($totalSeconds -gt 0) { $clickX = $m.MouseEvent.X $fraction = $clickX / $progBar.Frame.Width $newPosition = [TimeSpan]::FromSeconds($fraction * $totalSeconds) $MediaPlayer.Position = $newPosition if ($script:isPlaying) { $MediaPlayer.Play() } $progBar.Fraction = $fraction [Application]::Refresh() } }.GetNewClosure()) $progFrame.Add($progBar) # use a timeout token instead $script:timeoutToken = $null # Define the update action as a scriptblock returning $true to repeat $updateProgress = { $totalSeconds = $MediaPlayer.NaturalDuration.TimeSpan.TotalSeconds if ($totalSeconds -gt 0) { $progBar.Fraction = $MediaPlayer.Position.TotalSeconds / $totalSeconds } else { $progBar.Fraction = 0 } $status = '{0} - {1:mm\:ss}' -f $(Split-Path $MediaPlayer.Source.LocalPath -Leaf), $MediaPlayer.Position $StatusBar.Items[2].Title = $status $StatusBar.Items[0].Title = ((Get-Date -Format g)) if ($MediaPlayer.Position.TotalSeconds -ge $totalSeconds) { $script:timeoutToken = $null $script:isPlaying = $false $MediaPlayer.Stop() # reset position so Play() restarts from beginning $progBar.Fraction = 0 $StatusBar.Items[2].Title = 'Finished' [Application]::Refresh() return $false # stop repeating } return $true # keep repeating }.GetNewClosure() $window.Add($progFrame) #28 Feb 2026 Added volume control $lblVol = [Label]@{ X = $progFrame.Frame.X Y = $progFrame.Frame.Bottom Text = 'Volume' } $window.Add($lblVol) $progVol = [ProgressBar]@{ X = $lblVol.Frame.Right + 1 Y = $lblVol.Y Fraction = 0 #$MediaPlayer.Volume ProgressBarFormat = 'SimplePlusPercentage' ProgressBarStyle = 'Continuous' Width = 50 Text = 'Volume' } $progVol.Add_MouseClick({ param($m) $clickX = $m.MouseEvent.X $fraction = $clickX / $progVol.Frame.Width $progVol.Fraction = $fraction $MediaPlayer.Volume = $progVol.Fraction [Application]::Refresh() }) $window.Add($progVol) $btnVolUp = [Button]@{ X = $progVol.Frame.Right + 1 Y = $progVol.Y Text = '+' Shortcut = 'CursorUp, CtrlMask' ShortcutAction = { $MediaPlayer.Volume += .05 $progVol.Fraction += .05 [Application]::Refresh() } } $btnVolUp.Add_Clicked({ $MediaPlayer.Volume += .05 $progVol.Fraction += .05 [Application]::Refresh() }) $window.Add($btnVolUp) $btnVolDown = [Button]@{ X = $btnVolUp.Frame.Right + 1 Y = $progVol.Y Text = '-' Shortcut = 'CursorDown, CtrlMask' ShortcutAction = { $MediaPlayer.Volume -= .05 $progVol.Fraction -= .05 [Application]::Refresh() } } $btnVolDown.Add_Clicked({ $MediaPlayer.Volume -= .05 $progVol.Fraction -= .05 [Application]::Refresh() }) $window.Add($btnVolDown) $tvInfo = [TextView]@{ X = [Pos]::right($progFrame) + 1 Y = [Pos]::Top($progFrame) - 1 Width = 60 Height = 10 Multiline = $true Visible = $False } $tvInfo.ColorScheme = $cs $window.add($tvInfo) $btnPlay = [Button]@{ X = 1 Y = $progVol.Frame.Bottom + 1 Text = '_Play' TabIndex = 0 Enabled = $False } #define an action when the button is clicked $btnPlay.Add_Clicked({ $script:isPlaying = $true $MediaPlayer.Play() $progVol.Fraction = $MediaPlayer.Volume $StatusBar.Items[0].Title = $(Get-Date -Format g) $StatusBar.Items[2].Title = "Playing $(Split-Path $MediaPlayer.Source.LocalPath -Leaf)" $progFrame.Title = $script:musicTitle # Register a 1-second repeating callback on the main loop if ($null -eq $script:timeoutToken) { $script:timeoutToken = [Application]::MainLoop.AddTimeout( [TimeSpan]::FromSeconds(1), $updateProgress) } #26 February 2026 Update recently played if ($script:recentList -notcontains $txtFile.Text.ToString()) { #This needs to persist outside the TUI $cmd = "updateMP3Info ""$($MediaPlayer.Source.LocalPath)"" ;[Application]::Refresh()" $action = [Scriptblock]::Create($cmd) $add = [MenuItem]::New($txtFile.Text.ToString(), '', $action) $mrp.Children += $add #trim if ($mrp.Children.count -gt $mrpLimit) { $mrp.children = $mrp.Children | Select-Object -Skip 1 } #update the list $script:recentList = $mrp.children.foreach({ $_.Title.ToString() }) #update the history Set-Content -Path $mrpHistory -Value $script:recentList } [Application]::Refresh() }) $window.Add($btnPlay) $btnPause = [Button]@{ X = $btnPlay.Frame.Right + 1 Y = $btnPlay.Y Text = 'Pa_use' TabIndex = 0 } $btnPause.Add_Clicked({ $script:isPlaying = $false $MediaPlayer.Pause() if ($null -ne $script:timeoutToken) { [Application]::MainLoop.RemoveTimeout($script:timeoutToken) $script:timeoutToken = $null } $StatusBar.Items[2].Title = "$script:MusicTitle - Paused" $StatusBar.Items[0].Title = $(Get-Date -Format g) [Application]::Refresh() }) $window.Add($btnPause) $btnStop = [Button]@{ X = $btnPause.Frame.Right + 1 Y = $btnPlay.Y Text = '_Stop' TabIndex = 0 } $btnStop.Add_Clicked({ if ($script:isPlaying) { mediaStop } }) $window.Add($btnStop) $btnQuit = [Button]@{ #set the position relative to the Copy button X = $btnStop.Frame.Right + 1 Y = $btnPlay.Y Text = '_Quit' } $btnQuit.Add_Clicked({ quitMP3 }) $window.Add($btnQuit) $txtLyrics = [TextView]@{ X = $btnQuit.X + 5 Y = $btnQuit.Frame.Bottom + 1 Width = [Dim]::Percent(95) Height = 25 Multiline = $True Visible = $false WordWrap = $True ReadOnly = $True Text = 'scooby dooby doo' } $txtLyrics.ColorScheme = $cs $window.Add($txtLyrics) #endregion #region display #Update the form if a file was specified if ($PSBoundParameters.ContainsKey('FilePath')) { $txtFile.Text = (Convert-Path $PSBoundParameters['FilePath']) UpdateMP3Info } #Add the Window and its nested controls to the TUI application [Application]::Top.Add($window) #Invoke the TUI [Application]::Run() #When the TUI ends it will shutdown [Application]::ShutDown() #endregion } #helper functions function ShowMp3Help { #define help information [CmdletBinding()] param() $title = 'TUI MP3 player Help' $help = @' If you didn't specify the path to an MP3 file when you launched this TUI, you can select one from the File menu. Use the file dialog to navigate to a folder and select a .mp3 or .m4a file. You will need to select the file extension from the dropdown. Selected properties from the file will be displayed. This is read-only information. Note that you can't select files with commas in the name. This is a Terminal.Gui limitation. However, you can specify such a file when starting the command. Invoke-TuiMp3 -filepath "c:\music\Train,Train.mp3" Use the buttons to play the file. You can also click in the progress bar to jump ahead and backwards. The highlighted letters are shortcut keys, i.e. Alt+P or Alt+Q. If the file has lyrics, they will be displayed in the TUI. Played files will be added to the recently played list. The maximum number of files in this list is 10. You can select one of the recently played to load it into the player. You can control the volume by clicking the volume bar, the +/- buttons or using Ctrl+Up or Ctrl+Down. Use the Quit button or menu choice to exit. '@ $dialog = [Dialog]@{ Title = $title TextAlignment = 'Left' Width = 75 Height = 30 Text = $help } $ok = [Terminal.Gui.Button]@{ Text = 'OK' } $ok.Add_Clicked({ $dialog.RequestStop() }) $dialog.AddButton($ok) [Application]::Run($dialog) } #get mp3 information function getMP3Info { [cmdletbinding()] param([string]$Path) $file = [TagLib.File]::Create( $Path) Write-Information $file [PSCustomObject]@{ Path = $file.Name Size = (Get-Item $File.Name).Length Duration = New-TimeSpan -Seconds $file.Properties.Duration.TotalSeconds Title = $file.Tag.Title Subtitle = $file.Tag.Subtitle Album = $file.Tag.Album Track = $file.Tag.Track Year = $file.Tag.Year Genre = $file.Tag.JoinedGenres Artist = $file.Tag.JoinedArtists Lyrics = $file.Tag.Lyrics } } #refresh MP3 info function updateMP3Info { param($mp3Path) if ($mp3Path) { $txtFile.text = $mp3Path } $script:FilePath = $txtFile.Text.toString() $script:musicTitle = Split-Path $script:FilePath -Leaf $ProgFrame.title = $script:musicTitle $MediaPlayer.Open($script:FilePath) $StatusBar.Items[2].Title = "Loaded $script:FilePath" #update details $script:mp3Info = getMP3Info $txtFile.Text.ToString() $tvInfo.Text = $script:mp3Info | Format-List Title, Subtitle, Album, Track, Year, Genre, Artist, Duration | Out-String $tvInfo.Visible = $True if ($script:mp3Info.Lyrics) { $txtLyrics.Visible = $True #expand lyrics as needed if ($script:mp3Info.Lyrics -notmatch "`n") { $txtLyrics.Text = $script:mp3Info.Lyrics -replace '\r', "`n" } else { $txtLyrics.Text = $script:mp3Info.Lyrics } } else { $txtLyrics.Visible = $False } #enable the Play button $btnPlay.Enabled = $True [Application]::Refresh() } function quitMP3 { if ($MediaPlayer.position.totalSeconds -ge 1) { $MediaPlayer.Stop() } if ($null -ne $script:timeoutToken) { [Application]::MainLoop.RemoveTimeout($script:timeoutToken) $script:timeoutToken = $null } [Application]::RequestStop() } |