Public/Save-MusicForProgramming.ps1
|
function Save-MusicForProgramming { <# .SYNOPSIS Downloads Music For Programming episodes to a local directory. .DESCRIPTION Downloads one or more episodes from musicforprogramming.net with a live progress bar. Files are saved using the standard naming convention (music_for_programming_NN-artist.mp3) so they work seamlessly with Get-LocalMusicForProgramming and Start-MusicForProgramming -LocalPath. Accepts episode objects from Get-MusicForProgramming via the pipeline, or specify episodes by number with -EpisodeNumber, or use -All to grab everything. Skips files that already exist unless -Force is given. .PARAMETER InputObject Episode objects piped from Get-MusicForProgramming. .PARAMETER EpisodeNumber One or more episode numbers to download. .PARAMETER All Download every episode from the RSS feed. .PARAMETER Path Destination directory. Created if it does not exist. Defaults to the current directory. .PARAMETER Force Overwrite files that already exist locally. .PARAMETER PassThru Return a FileInfo object for each downloaded file. .EXAMPLE Save-MusicForProgramming -EpisodeNumber 78 -Path ~/Music/MFP Downloads episode 78 to ~/Music/MFP. .EXAMPLE Save-MusicForProgramming -EpisodeNumber 1, 2, 3 -Path ~/Music/MFP Downloads episodes 1, 2, and 3. .EXAMPLE Get-MusicForProgramming | Save-MusicForProgramming -Path ~/Music/MFP Downloads all episodes (piped from Get-MusicForProgramming). .EXAMPLE Get-MusicForProgramming | Where-Object Title -like '*Datassette*' | Save-MusicForProgramming -Path ~/Music/MFP Downloads only Datassette episodes. .EXAMPLE Save-MusicForProgramming -All -Path ~/Music/MFP -PassThru | Select-Object Name, Length Downloads everything and returns FileInfo objects for each saved file. #> [CmdletBinding(DefaultParameterSetName = 'ByEpisode', SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([System.IO.FileInfo])] param( [Parameter(ValueFromPipeline, ParameterSetName = 'ByEpisode')] [PSCustomObject[]]$InputObject, [Parameter(Mandatory, ParameterSetName = 'ByNumber')] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) (Get-MusicForProgramming).EpisodeNumber | Where-Object { "$_" -like "$wordToComplete*" } | Sort-Object })] [int[]]$EpisodeNumber, [Parameter(Mandatory, ParameterSetName = 'All')] [switch]$All, [Parameter()] [string]$Path = $script:DefaultMusicPath, [Parameter()] [switch]$Force, [Parameter()] [switch]$PassThru ) begin { # Ensure destination directory exists if (-not (Test-Path $Path -PathType Container)) { Write-Verbose "Creating directory: $Path" New-Item -ItemType Directory -Path $Path -Force | Out-Null } $resolvedPath = (Resolve-Path $Path).Path # For ByNumber / All: collect all episodes up front so we can show X/Y progress $queuedEpisodes = [System.Collections.Generic.List[PSCustomObject]]::new() if ($PSCmdlet.ParameterSetName -eq 'ByNumber') { foreach ($num in $EpisodeNumber) { $ep = Get-MusicForProgramming -EpisodeNumber $num if ($ep) { $queuedEpisodes.Add($ep) } else { Write-Warning "Episode $num not found in the RSS feed." } } } elseif ($PSCmdlet.ParameterSetName -eq 'All') { Write-Verbose "Fetching full episode list for -All" Get-MusicForProgramming | ForEach-Object { $queuedEpisodes.Add($_) } } } process { # Pipeline input goes into the queue too if ($PSCmdlet.ParameterSetName -eq 'ByEpisode' -and $InputObject) { foreach ($ep in $InputObject) { $queuedEpisodes.Add($ep) } } } end { if ($queuedEpisodes.Count -eq 0) { Write-Verbose "No episodes to download." return } $total = $queuedEpisodes.Count $index = 0 foreach ($episode in $queuedEpisodes) { $index++ $fileName = ($episode.Url -split '/')[-1] $outFile = Join-Path $resolvedPath $fileName $label = $episode.FullTitle # Outer progress bar — overall queue $outerStatus = "Episode $index of $total — $label" Write-Progress -Id 0 -Activity 'Music For Programming' ` -Status $outerStatus ` -PercentComplete ([int](($index - 1) / $total * 100)) if ((Test-Path $outFile) -and -not $Force) { Write-Verbose "Skipping (already exists): $outFile" Write-Host " [skip] $label" -ForegroundColor DarkGray if ($PassThru) { Get-Item $outFile } continue } if (-not $PSCmdlet.ShouldProcess($label, 'Download')) { continue } try { Invoke-MFPDownload -Url $episode.Url -OutFile $outFile -Label $label -OuterProgressId 0 Write-Host " [done] $label" -ForegroundColor Green if ($PassThru) { Get-Item $outFile } } catch { Write-Error "Failed to download '$label': $_" # Remove partial file if it exists if (Test-Path $outFile) { Remove-Item $outFile -Force -ErrorAction SilentlyContinue } } } # Clear outer progress Write-Progress -Id 0 -Activity 'Music For Programming' -Completed } } # --------------------------------------------------------------------------- # Private helper — not exported # --------------------------------------------------------------------------- function Invoke-MFPDownload { <# Streams a URL to disk with a Write-Progress inner progress bar. Uses HttpWebRequest for cross-platform compatibility in PS 5.1+. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Url, [Parameter(Mandatory)] [string]$OutFile, [Parameter(Mandatory)] [string]$Label, [Parameter()] [int] $OuterProgressId = -1 ) $innerId = $OuterProgressId + 1 $request = [System.Net.HttpWebRequest]::Create($Url) $request.Method = 'GET' $request.UserAgent = 'MusicForProgrammingPS/0.1' $response = $request.GetResponse() $totalBytes = $response.ContentLength # -1 if unknown $inStream = $response.GetResponseStream() $outStream = [System.IO.File]::Create($OutFile) $buffer = [byte[]]::new(81920) # 80 KB chunks $totalRead = 0L try { do { $read = $inStream.Read($buffer, 0, $buffer.Length) if ($read -gt 0) { $outStream.Write($buffer, 0, $read) $totalRead += $read if ($totalBytes -gt 0) { $pct = [int]($totalRead / $totalBytes * 100) $mbDone = [math]::Round($totalRead / 1MB, 1) $mbTotal = [math]::Round($totalBytes / 1MB, 1) $status = "$mbDone MB / $mbTotal MB" } else { $pct = -1 $status = "$([math]::Round($totalRead / 1MB, 1)) MB downloaded" } $progressParams = @{ Id = $innerId Activity = $Label Status = $status } if ($OuterProgressId -ge 0) { $progressParams['ParentId'] = $OuterProgressId } if ($pct -ge 0) { $progressParams['PercentComplete'] = $pct } Write-Progress @progressParams } } while ($read -gt 0) } finally { $outStream.Flush() $outStream.Dispose() $inStream.Dispose() $response.Dispose() Write-Progress -Id $innerId -Activity $Label -Completed } } |