Public/Get-MusicForProgramming.ps1

function Get-MusicForProgramming {
    <#
    .SYNOPSIS
        Returns Music For Programming episodes — local collection first, then online.

    .DESCRIPTION
        By default, checks the local music directory for downloaded episodes and returns
        those. If no local files are found (or the directory does not exist), fetches
        the full episode list from the RSS feed at musicforprogramming.net.

        Use -Local to restrict results to only locally downloaded files without
        querying the internet at all.

        Output objects always have EpisodeNumber and Title. Local episodes include
        FilePath; online episodes include Url, Duration, and PublishedDate.

    .PARAMETER EpisodeNumber
        Filter to a specific episode number. If the episode is not in the local
        collection, it is fetched from the RSS feed automatically (unless -Local).

    .PARAMETER Path
        Directory to scan for locally downloaded MFP files.
        Defaults to ~/Music/MusicForProgramming.
        Override for all commands at once via:
            $PSDefaultParameterValues['*-MusicForProgramming:Path'] = 'D:\MyMusic'

    .PARAMETER Local
        Return only locally downloaded episodes. Does not query the RSS feed even
        if no local files are found.

    .EXAMPLE
        Get-MusicForProgramming

        Returns local episodes if any are downloaded, otherwise lists all episodes
        available online.

    .EXAMPLE
        Get-MusicForProgramming | Format-Table EpisodeNumber, Title, Duration

        Browse all available episodes in a table.

    .EXAMPLE
        Get-MusicForProgramming -EpisodeNumber 42

        Returns episode 42 — from local collection if present, otherwise from the feed.

    .EXAMPLE
        Get-MusicForProgramming -Local

        Returns only locally downloaded episodes, no network request.

    .EXAMPLE
        Get-MusicForProgramming | Where-Object Title -like '*Datassette*'

        Finds all Datassette episodes.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Position = 0)]
        [int]$EpisodeNumber,

        [Parameter()]
        [string]$Path = $script:DefaultMusicPath,

        [Parameter()]
        [switch]$Local
    )

    # Check for a local collection
    $localEpisodes = $null
    if (Test-Path $Path -PathType Container) {
        $localEpisodes = Get-LocalMusicForProgramming -Path $Path
    }

    if ($Local) {
        # Caller explicitly wants only local files
        $results = $localEpisodes
    }
    elseif ($localEpisodes) {
        Write-Verbose "Returning $($localEpisodes.Count) local episode(s) from '$Path'"
        $results = $localEpisodes
    }
    else {
        Write-Verbose "No local files found in '$Path'; fetching RSS feed"
        $results = Get-MFPFeedEpisodes
    }

    if ($PSBoundParameters.ContainsKey('EpisodeNumber')) {
        $filtered = $results | Where-Object { $_.EpisodeNumber -eq $EpisodeNumber }

        # If a specific episode was requested but not found locally, fall back to RSS
        if (-not $filtered -and -not $Local -and $localEpisodes) {
            Write-Verbose "Episode $EpisodeNumber not in local collection; fetching from RSS"
            $filtered = Get-MFPFeedEpisodes | Where-Object { $_.EpisodeNumber -eq $EpisodeNumber }
        }

        if (-not $filtered) {
            Write-Warning "Episode $EpisodeNumber was not found."
        }
        return $filtered
    }

    $results
}

# ---------------------------------------------------------------------------
# Private helper — not exported
# ---------------------------------------------------------------------------

function Get-MFPFeedEpisodes {
    <# Fetches and parses the MFP RSS feed into episode objects. #>
    [CmdletBinding()]
    param()

    $rssUrl = 'https://musicforprogramming.net/rss.xml'
    Write-Verbose "Fetching RSS feed from $rssUrl"

    try {
        [xml]$rss = (Invoke-WebRequest -Uri $rssUrl -UseBasicParsing).Content
    }
    catch {
        throw "Failed to fetch RSS feed from ${rssUrl}: $_"
    }

    $nsManager = New-Object System.Xml.XmlNamespaceManager($rss.NameTable)
    $nsManager.AddNamespace('itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd')

    foreach ($item in $rss.rss.channel.item) {
        $episodeNum = 0
        $artist     = $item.title
        if ($item.title -match 'Episode (\d+): (.+)') {
            $episodeNum = [int]$Matches[1]
            $artist     = $Matches[2].Trim()
        }

        $durationNode = $item.SelectSingleNode('itunes:duration', $nsManager)
        $duration = if ($durationNode) { $durationNode.InnerText } else { '' }

        [PSCustomObject]@{
            PSTypeName    = 'MusicForProgramming.Episode'
            EpisodeNumber = $episodeNum
            Title         = $artist
            FullTitle     = $item.title
            Url           = $item.enclosure.url
            Duration      = $duration
            PublishedDate = [datetime]$item.pubDate
            PageUrl       = $item.link
        }
    }
}