PSSonarr.psm1

function Add-SonarrSeries
{
    <#
        .SYNOPSIS
            Add a series to Sonarr by IMDB, TMDB, or TVDB ID.
 
        .SYNTAX
            Add-SonarrSeries -IMDBID <String> -QualityProfileId <Int32> [-MonitorOption <String>] [-Search] [<CommonParameters>]
 
            Add-SonarrSeries -TMDBID <String> -QualityProfileId <Int32> [-MonitorOption <String>] [-Search] [<CommonParameters>]
 
            Add-SonarrSeries -TVDBID <String> -QualityProfileId <Int32> [-MonitorOption <String>] [-Search] [<CommonParameters>]
 
        .DESCRIPTION
            Adds a series to Sonarr using external database IDs. The function will search for the series using the provided ID,
            then add it to Sonarr with the specified quality profile and monitoring options.
 
        .PARAMETER IMDBID
            The IMDB ID of the series to add. Can include or exclude the 'tt' prefix.
 
        .PARAMETER TMDBID
            The TMDB (The Movie Database) ID of the series to add.
 
        .PARAMETER TVDBID
            The TVDB (TheTVDB) ID of the series to add.
 
        .PARAMETER QualityProfileId
            The ID of the quality profile to assign to the series.
 
        .PARAMETER MonitorOption
            The monitoring option for the series. Valid values are: 'all', 'firstSeason', 'lastSeason', 'future', 'missing', 'existing', 'recent', 'pilot', 'monitorSpecials', 'unmonitorSpecials', 'none'. Defaults to 'all'.
 
        .PARAMETER Search
            If specified, initiates a search for missing episodes after adding the series.
 
        .EXAMPLE
            Add-SonarrSeries -IMDBID 'tt0944947' -QualityProfileId 1 -MonitorOption 'all' -Search
 
        .EXAMPLE
            Add-SonarrSeries -TVDBID '121361' -QualityProfileId 2 -MonitorOption 'future'
 
        .NOTES
            The series must exist in the external database (IMDB, TMDB, or TVDB) and be findable through Sonarr's lookup service.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'IMDB')]
        [ValidatePattern('^(tt)?\d{5,9}$')]
        [String]$IMDBID,

        [Parameter(Mandatory = $true, ParameterSetName = 'TMDB')]
        [ValidatePattern('^\d{1,9}$')]
        [String]$TMDBID,

        [Parameter(Mandatory = $true, ParameterSetName = 'TVDB')]
        [String]$TVDBID,

        [Parameter(Mandatory = $true)]
        [int]
        $QualityProfileId,

        [Parameter(Mandatory = $false)]
        [ValidateSet('all', 'firstSeason', 'lastSeason', 'future', 'missing', 'existing', 'recent', 'pilot', 'monitorSpecials', 'unmonitorSpecials', 'none')]
        $MonitorOption = 'all',

        [Parameter(Mandatory = $false)]
        [Switch]$Search
    )

    ####################################################################################################
    #Region Import configuration
    try
    {
        Import-Configuration -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    # If using IMDB, ensure the ID is in the correct format
    if($ParameterSetName -eq 'IMDBID' -and $IMDBID -notmatch '^tt')
    {
        $IMDBID = 'tt' + $IMDBID
    }


    ####################################################################################################
    #Region Check if already in Sonarr before attempting an addition
    Write-Verbose -Message "Checking if the series already exists"
    try
    {
        if($IMDBID)
        {
            $Series = Get-SonarrSeries -IMDBID $IMDBID -ErrorAction Stop
        }
        elseif($TMDBID)
        {
            $Series = Get-SonarrSeries -TMDBID $TMDBID -ErrorAction Stop
        }
        elseif($TVDBID)
        {
            $Series = Get-SonarrSeries -TVDBID $TVDBID -ErrorAction Stop
        }

        if($Series)
        {
            Write-Warning "Series already exists in Sonarr!"
            return
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region Define the path, parameters, headers and URI
    try
    {
        $Path = '/series/lookup'

        if($TVDBID)
        {
            $Params = @{
                term = "tvdb%3A$($TVDBID)"
            }
        }
        elseif($IMDBID)
        {
            $Params = @{
                term = "imdb%3A$($IMDBID)"
            }
        }

        # Generate the headers and URI
        $Headers = Get-Headers
        $Uri = Get-APIUri -RestEndpoint $Path -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region Search for the series (we need to get the data before adding it)
    Write-Verbose -Message "Using Sonarr lookup service to find the series"
    try
    {
        if($IMDBID)
        {
            $Series = Search-SonarrSeries -IMDBID $IMDBID -ErrorAction Stop
        }
        elseif($TMDBID)
        {
            $Series = Search-SonarrSeries -TMDBID $TMDBID -ErrorAction Stop
        }
        elseif($TVDBID)
        {
            $Series = Search-SonarrSeries -TVDBID $TVDBID -ErrorAction Stop
        }

        if(!$Series)
        {
            throw "Could not find the series to add"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion


    #############################################################################
    #Region Append data
    try
    {
        $Series | Add-Member -MemberType NoteProperty -Name 'qualityProfileId' -Value $QualityProfileId -Force
        $Series | Add-Member -MemberType NoteProperty -Name 'languageProfileId' -Value 1 -Force
        $Series | Add-Member -MemberType NoteProperty -Name 'seasonFolder' -Value $True -Force
        $Series | Add-Member -MemberType NoteProperty -Name 'monitored' -Value $True -Force
        $Series | Add-Member -MemberType NoteProperty -Name 'rootFolderPath' -Value $Config.RootFolderPath -Force

        if($Search)
        {
            $Series | Add-Member -MemberType NoteProperty -Name 'addOptions' -Value $(
                [PSCustomObject]@{
                    monitor                    = $MonitorOption
                    searchForMissingEpisodes   = $true
                    ignoreEpisodesWithFiles    = $false
                    ignoreEpisodesWithoutFiles = $false
                }
            ) -Force
        }
        else
        {
            $Series | Add-Member -MemberType NoteProperty -Name 'addOptions' -Value $(
                [PSCustomObject]@{
                    monitor                      = $MonitorOption
                    searchForMissingEpisodes     = $false
                    searchForCutoffUnmetEpisodes = $false
                    ignoreEpisodesWithFiles      = $false
                    ignoreEpisodesWithoutFiles   = $false
                }
            ) -Force
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region Define the path, parameters, headers and URI
    try
    {
        $Data = $Series | ConvertTo-Json -Depth 5
        $DataEncoded = ([System.Text.Encoding]::UTF8.GetBytes($Data))

        $Headers = Get-Headers
        $Path = '/series'
        $Uri = Get-APIUri -RestEndpoint $Path
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region make the main request
    Write-Verbose "Adding: $Uri"
    try
    {
        Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Post -ContentType "application/json" -Body $DataEncoded -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion
}
function Get-SonarrQualityProfile
{
    <#
        .SYNOPSIS
            Retrieves quality profiles from Sonarr.
 
        .SYNTAX
            Get-SonarrQualityProfile [<CommonParameters>]
 
            Get-SonarrQualityProfile -Id <String> [<CommonParameters>]
 
            Get-SonarrQualityProfile -Name <String> [<CommonParameters>]
 
        .DESCRIPTION
            Retrieves quality profile information from Sonarr. Can return all quality profiles or filter by specific criteria
            such as ID or name.
 
        .PARAMETER Id
            The quality profile ID to retrieve.
 
        .PARAMETER Name
            The name of the quality profile to retrieve.
 
        .EXAMPLE
            Get-SonarrQualityProfile
 
        .EXAMPLE
            Get-SonarrQualityProfile -Id '1'
 
        .EXAMPLE
            Get-SonarrQualityProfile -Name 'HD-1080p'
 
        .NOTES
            When no parameters are specified, all quality profiles in Sonarr are returned.
    #>


    [CmdletBinding(DefaultParameterSetName = 'All')]
    param(
        [Parameter(Mandatory = $false, ParameterSetName = 'Id')]
        [String]$Id,

        [Parameter(Mandatory = $false, ParameterSetName = 'Name')]
        [String]$Name
    )

    ####################################################################################################
    #Region Import configuration
    try
    {
        Import-Configuration -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region Define the path, parameters, headers and URI
    try
    {
        $Path = '/qualityprofile'
        if($PSCmdlet.ParameterSetName -eq 'Id' -and $Id)
        {
            $Path += "/$Id"
        }

        # Generate the headers and URI
        $Headers = Get-Headers
        $Uri = Get-APIUri -RestEndpoint $Path -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region make the main request
    Write-Verbose "Querying: $Uri"
    try
    {
        $Data = Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Get -ContentType 'application/json' -ErrorAction Stop
        if($Data)
        {
            if($Name)
            {
                $Data = $Data | Where-Object { $_.name -eq $Name }
            }

            return $Data
        }
    }
    catch
    {
        throw $_
    }

}
function Get-SonarrSeries
{
    <#
        .SYNOPSIS
            Retrieves series from Sonarr.
 
        .SYNTAX
            Get-SonarrSeries [<CommonParameters>]
 
            Get-SonarrSeries -Id <String> [<CommonParameters>]
 
            Get-SonarrSeries -Name <String> [<CommonParameters>]
 
            Get-SonarrSeries -IMDBID <String> [<CommonParameters>]
 
            Get-SonarrSeries -TMDBID <String> [<CommonParameters>]
 
            Get-SonarrSeries -TVDBID <String> [<CommonParameters>]
 
        .DESCRIPTION
            Retrieves series information from Sonarr. Can return all series or filter by specific criteria
            such as ID, name, or external database IDs.
 
        .PARAMETER Id
            The Sonarr series ID to retrieve.
 
        .PARAMETER Name
            The name or title of the series to retrieve. Searches both title and originalTitle fields.
 
        .PARAMETER IMDBID
            The IMDB ID of the series to retrieve. Can include or exclude the 'tt' prefix.
 
        .PARAMETER TMDBID
            The TMDB (The Movie Database) ID of the series to retrieve.
 
        .PARAMETER TVDBID
            The TVDB (TheTVDB) ID of the series to retrieve.
 
        .EXAMPLE
            Get-SonarrSeries
 
        .EXAMPLE
            Get-SonarrSeries -Id '1'
 
        .EXAMPLE
            Get-SonarrSeries -Name 'Game of Thrones'
 
        .EXAMPLE
            Get-SonarrSeries -IMDBID 'tt0944947'
 
        .NOTES
            When no parameters are specified, all series in Sonarr are returned.
    #>


    [CmdletBinding(DefaultParameterSetName = 'All')]
    param(
        [Parameter(Mandatory = $false, ParameterSetName = 'Id')]
        [String]$Id,

        [Parameter(Mandatory = $false, ParameterSetName = 'Name')]
        [Alias('Title')]
        [String]$Name,

        [Parameter(Mandatory = $false, ParameterSetName = 'IMDBID')]
        [ValidatePattern('^(tt)?\d{5,9}$')]
        [String]$IMDBID,

        [Parameter(Mandatory = $true, ParameterSetName = 'TMDBID')]
        [String]$TMDBID,

        [Parameter(Mandatory = $false, ParameterSetName = 'TVDBID')]
        [ValidatePattern('^\d{1,9}$')]
        [String]$TVDBID
    )

    ####################################################################################################
    #Region Import configuration
    try
    {
        Import-Configuration -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    # If using IMDB, ensure the ID is in the correct format
    if($ParameterSetName -eq 'IMDBID' -and $IMDBID -notmatch '^tt')
    {
        $IMDBID = 'tt' + $IMDBID
    }


    ####################################################################################################
    #Region Define the path, parameters, headers and URI
    try
    {
        $Path = '/series'
        if($PSCmdlet.ParameterSetName -eq 'Id' -and $Id)
        {
            $Path += "/$Id"
        }

        # Generate the headers and URI
        $Headers = Get-Headers
        $Uri = Get-APIUri -RestEndpoint $Path -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region make the main request
    Write-Verbose "Querying: $Uri"
    try
    {
        $Data = Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Get -ContentType 'application/json' -ErrorAction Stop
        if($Data)
        {
            # Filter results based on parameters if specified
            switch($PSCmdlet.ParameterSetName)
            {
                'Name'
                {
                    $Data = $Data | Where-Object { $_.title -eq $Name -or $_.originalTitle -eq $Name }
                }
                'IMDBID'
                {
                    if($IMDBID -notmatch '^tt')
                    {
                        $IMDBID = 'tt' + $IMDBID
                    }
                    $Data = $Data | Where-Object { $_.imdbId -eq "$IMDBID" }
                }
                'TMDBID'
                {
                    $Data = $Data | Where-Object { $_.tmdbId -eq $TMDBID }
                }
                'TVDBID'
                {
                    $Data = $Data | Where-Object { $_.tvdbId -eq $TVDBID }
                }
            }

            return $Data
        }
        else
        {
            Write-Verbose -Message 'No result found.'
            return
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion
}
function Remove-SonarrSeries
{
    <#
        .SYNOPSIS
            Removes a series from Sonarr.
 
        .SYNTAX
            Remove-SonarrSeries -Id <String> [-WhatIf] [-Confirm] [<CommonParameters>]
 
        .DESCRIPTION
            Removes a series from Sonarr using the series ID. This function supports WhatIf and Confirm parameters
            for safe execution.
 
        .PARAMETER Id
            The Sonarr series ID to remove. Accepts pipeline input by property name.
 
        .EXAMPLE
            Remove-SonarrSeries -Id '123'
 
        .EXAMPLE
            Get-SonarrSeries -Name 'Old Show' | Remove-SonarrSeries
 
        .EXAMPLE
            Remove-SonarrSeries -Id '123' -WhatIf
 
        .NOTES
            This function supports pipeline input and confirmation prompts for safe series removal.
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [String]$Id
    )

    begin
    {
        ####################################################################################################
        #Region Import configuration
        try
        {
            Import-Configuration -ErrorAction Stop
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    process
    {
        ####################################################################################################
        #Region Define the path, parameters, headers and URI
        try
        {
            $Path = '/series/' + $Id
            $Uri = Get-APIUri -RestEndpoint $Path
            $Headers = Get-Headers
        }
        catch
        {
            throw $_
        }
        #EndRegion

        ####################################################################################################
        #Region make the main request
        if($PSCmdlet.ShouldProcess("Series with ID: $Id", "Remove"))
        {
            Write-Verbose -Message "Removing series with ID $Id"
            try
            {
                Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Delete -ContentType 'application/json' -ErrorAction Stop
            }
            catch
            {
                Write-Error "Failed to remove series with ID $Id. Error: $($_.Exception.Message)"
            }
        }
        #EndRegion
    }
}
function Search-SonarrSeries
{
    <#
        .SYNOPSIS
            Search to find a series in order to add to Sonarr.
 
        .SYNTAX
            Search-SonarrSeries -Name <String> [-ExactMatch] [<CommonParameters>]
 
            Search-SonarrSeries -IMDBID <String> [<CommonParameters>]
 
            Search-SonarrSeries -TMDBID <String> [<CommonParameters>]
 
            Search-SonarrSeries -TVDBID <String> [<CommonParameters>]
 
        .DESCRIPTION
            This uses the lookup service within Sonarr to search for a series by name, TVDB ID, or IMDB ID.
            It does not search your local Sonarr library, but rather The Movie Database (TMDb).
 
        .PARAMETER Name
            The name of the series to search for.
 
        .PARAMETER TMDBID
            The TMDB ID of the series to search for.
 
        .PARAMETER TVDBID
            The TVDB ID of the series to search for.
 
        .PARAMETER IMDBID
            The IMDB ID of the series to search for.
 
        .EXAMPLE
            Search-SonarrSeries -Name "The Matrix"
 
        .NOTES
            If you have the IMDB ID or TVDB ID of a series, it's better to use this to search.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Name')]
        [String]$Name,

        [Parameter(Mandatory = $false, ParameterSetName = 'Name')]
        [Switch]$ExactMatch,

        [Parameter(Mandatory = $true, ParameterSetName = 'IMDBID')]
        [ValidatePattern('^(tt)?\d{5,9}$')]
        [String]$IMDBID,

        [Parameter(Mandatory = $true, ParameterSetName = 'TMDBID')]
        [String]$TMDBID,

        [Parameter(Mandatory = $true, ParameterSetName = 'TVDBID')]
        [String]$TVDBID
    )

    ####################################################################################################
    #Region Import configuration
    try
    {
        Import-Configuration -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    # If using IMDB, ensure the ID is in the correct format
    if($ParameterSetName -eq 'IMDBID' -and $IMDBID -notmatch '^tt')
    {
        $IMDBID = 'tt' + $IMDBID
    }


    ####################################################################################################
    #Region Define the path, parameters, headers and URI
    try
    {
        $Path = "/series/lookup"
        if($Name)
        {
            $Params = @{
                term = $Name
            }
        }
        elseif($IMDBID)
        {
            $Params = @{
                term = "imdb:$($IMDBID)"
            }
        }
        elseif($TMDBID)
        {
            $Params = @{
                term = "tmdb:$($TMDBID)"
            }
        }
        elseif($TVDBID)
        {
            $Params = @{
                term = "tvdb:$($TVDBID)"
            }
        }
        else
        {
            throw 'You must specify a name, TVDBID, or IMDBID.'
        }

        # Generate the headers and URI
        $Headers = Get-Headers
        $Uri = Get-APIUri -RestEndpoint $Path -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region make the main request
    Write-Verbose "Querying: $Uri"
    try
    {
        $Data = Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Get -ContentType 'application/json' -ErrorAction Stop
        if($Data)
        {
            # If ExactMatch is specified, filter the results to only include the exact match
            if($ExactMatch)
            {
                $Data = $Data | Where-Object { $_.title -eq $Name }
            }

            return $Data
        }
        else
        {
            Write-Warning -Message "No series found."
            return
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion
}
function Set-SonarrConfiguration
{
    <#
        .SYNOPSIS
            Sets the configuration for connecting to a Sonarr server instance.
 
        .SYNTAX
            Set-SonarrConfiguration -Server <String> -APIKey <String> -RootFolderPath <String> [-Port <Int32>] [-Protocol <String>] [-APIVersion <Int32>] [-Default <Boolean>] [<CommonParameters>]
 
        .DESCRIPTION
            Saves Sonarr server connection settings including server address, port, API key, and API version to a JSON configuration file.
            The configuration is stored in the user's home directory under .PSSonarr/PSSonarrConfig.json.
 
        .PARAMETER Server
            The URL or hostname of the Sonarr server (e.g. 'myserver.domain.com')
 
        .PARAMETER Port
            The port number that Sonarr is listening on. Defaults to 8989.
 
        .PARAMETER Protocol
            The protocol to use for connecting to the Sonarr server. Defaults to 'http'.
 
        .PARAMETER APIKey
            The API key from your Sonarr instance. Can be found in Sonarr under Settings > General.
 
        .PARAMETER APIVersion
            The version of the Sonarr API to use. Defaults to 3.
 
        .PARAMETER RootFolderPath
            The root folder path where movies are stored.
 
        .PARAMETER Default
            If set to true, marks this server configuration as the default instance for PSSonarr commands. Defaults to true.
 
        .EXAMPLE
            Set-SonarrConfiguration -Server 'myserver.domain.com' -APIKey 'myapikey' -RootFolderPath 'D:\Movies'
 
        .NOTES
            File: Set-SonarrConfiguration.ps1
            The configuration file will be created at $HOME/.PSSonarr/PSSonarrConfig.json
    #>


    param (
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^[a-zA-Z0-9.-]+$')]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [int]$Port = 8989,

        [Parameter(Mandatory = $false)]
        [ValidateSet("http", "https")]
        [string]$Protocol = "http",

        [Parameter(Mandatory = $true)]
        [string]$APIKey,

        [Parameter(Mandatory = $false)]
        [Int]$APIVersion = "3",

        [Parameter(Mandatory = $true)]
        [string]$RootFolderPath,

        [Parameter(Mandatory = $false)]
        [bool]$Default = $true
    )

    #############################################################################
    #Region Ensure required paths exist and load existing configuration
    $ConfigDir = Join-Path $HOME ".PSSonarr"
    if(-not (Test-Path -Path $ConfigDir -PathType Container))
    {
        try
        {
            New-Item -Path $ConfigDir -ItemType Directory -ErrorAction Stop | Out-Null
        }
        catch
        {
            throw $_
        }
    }

    # Path to the configuration file
    $ConfigPath = Join-Path $ConfigDir "PSSonarrConfig.json"

    # If the file exists, load existing data
    if(Test-Path -Path $ConfigPath -PathType Leaf)
    {
        [Array]$ConfigData = Get-Content -Path $ConfigPath | ConvertFrom-Json
    }
    else
    {
        $ConfigData = @()
    }
    #EndRegion

    #############################################################################
    # If the user has passed default as $False, but there is no existing default server (excluding itself) with
    # default set to $true, then we'll force this server to be the default.
    if($Default -eq $false -and ($ConfigData | Where-Object { $_.Default -eq $true -and $_.Server -ne $Server }).Count -eq 0)
    {
        Write-Warning -Message "No default server found. Forcing this server to be the default."
        $Default = $true
    }

    ####################################################################################################
    # Set all servers to Default = $false:
    $ConfigData | ForEach-Object ( { $_.Default = $false } )

    $Found = $false
    foreach($Entry in $ConfigData)
    {
        # If the server and port already exist in the configuration, update the rest of the data
        # that could have changed:
        if($Entry.Server -eq $Server -and $Entry.Port -eq $Port)
        {
            $Entry.Protocol = $Protocol
            $Entry.APIKey = $APIKey
            $Entry.APIVersion = $APIVersion
            $Entry.RootFolderPath = $RootFolderPath
            $Entry.Default = $Default
            $Found = $true
        }
    }

    # If we didn't find the server in the configuration, this would be a new entry:
    if($Found -eq $false)
    {
        #Construct an object with the data we want to save
        $ServerObject = [Ordered]@{
            "Server"         = $Server
            "Port"           = $Port
            "Protocol"       = $Protocol
            "APIKey"         = $APIKey
            "APIVersion"     = $APIVersion
            "RootFolderPath" = $RootFolderPath
            "Default"        = $Default
        }
        $ConfigData += $ServerObject
    }

    ####################################################################################################
    #Region Convert to JSON and save to file
    Write-Verbose -Message "Saving configuration to: $ConfigPath"
    try
    {
        # We want to make sure that $ConfigData is always an array before we export it, to ensure we can add
        # additional servers. Don't pipe $ConfigData directly to ConvertTo-Json, otherwise the first time around
        # it'll create an object instead of an array.
        ConvertTo-Json -InputObject $ConfigData -ErrorAction Stop | Set-Content -Path $ConfigPath -Force -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    Write-Verbose -Message "Configuration saved successfully to: $ConfigPath"
}

function Set-SonarrSeasonStatus
{
    <#
        .SYNOPSIS
            Sets the monitoring status of a specific season in Sonarr.
 
        .SYNTAX
            Set-SonarrSeasonStatus -Id <Int32> -SeasonNumber <Int32> -Monitored <Boolean> [<CommonParameters>]
 
        .DESCRIPTION
            Updates the monitoring status of a specific season for a series in Sonarr. When a season is monitored,
            Sonarr will automatically search for and download episodes from that season. When unmonitored,
            episodes from that season will not be automatically downloaded.
 
        .PARAMETER Id
            The Sonarr series ID containing the season to update.
 
        .PARAMETER SeasonNumber
            The season number to update (e.g., 1 for Season 1, 0 for Specials).
 
        .PARAMETER Monitored
            Boolean value indicating whether the season should be monitored (True) or unmonitored (False).
 
        .EXAMPLE
            Set-SonarrSeasonStatus -Id 123 -SeasonNumber 1 -Monitored $true
 
        .EXAMPLE
            Set-SonarrSeasonStatus -Id 456 -SeasonNumber 3 -Monitored $false
 
        .NOTES
            This function specifically modifies individual season monitoring status within a series.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $True)]
        [Int]$Id,

        [Parameter(Mandatory = $True)]
        [Int]$SeasonNumber,

        [Parameter(Mandatory = $True)]
        [Boolean]$Monitored
    )

    ####################################################################################################
    #Region Import configuration
    try
    {
        Import-Configuration -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region Get the series
    try
    {
        $Series = Get-SonarrSeries -Id $Id -ErrorAction Stop
        if(!$Series)
        {
            throw "Series with ID $Id not found."
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region Define the path, parameters, headers and URI
    try
    {
        if($Series.overview)
        {
            $Series.overview = Convert-SmartPunctuation -String $Series.overview
        }

        # Set the monitored status
        ($Series.seasons | Where-Object { $_.seasonNumber -eq $SeasonNumber }).monitored = [bool]$Monitored

        # Encode the body
        $BodyJSON = ($Series | ConvertTo-Json -Depth 5)
        $BodyEncoded = ([System.Text.Encoding]::UTF8.GetBytes($BodyJSON))

        $Path = '/series/' + "$Id"

        # Generate the headers and URI
        $Headers = Get-Headers
        $Uri = Get-APIUri -RestEndpoint $Path -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region make the main request
    Write-Verbose "Querying $Uri"
    try
    {
        Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Put -ContentType 'application/json' -Body $BodyEncoded -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion
}
function Set-SonarrSeriesStatus
{
    <#
        .SYNOPSIS
            Sets the monitoring status of a series in Sonarr.
 
        .SYNTAX
            Set-SonarrSeriesStatus -Id <Int32> -SeasonNumber <Int32> -Monitored <Boolean> [<CommonParameters>]
 
        .DESCRIPTION
            Updates the monitoring status of a series in Sonarr. When a series is monitored, Sonarr will automatically
            search for and download episodes. When unmonitored, episodes will not be automatically downloaded.
 
        .PARAMETER Id
            The Sonarr series ID to update.
 
        .PARAMETER SeasonNumber
            The season number (currently not used in this function - appears to be a parameter naming issue).
 
        .PARAMETER Monitored
            Boolean value indicating whether the series should be monitored (True) or unmonitored (False).
 
        .EXAMPLE
            Set-SonarrSeriesStatus -Id 123 -SeasonNumber 1 -Monitored $true
 
        .EXAMPLE
            Set-SonarrSeriesStatus -Id 456 -SeasonNumber 1 -Monitored $false
 
        .NOTES
            This function modifies the overall series monitoring status, not individual seasons.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $True)]
        [Int]$Id,

        [Parameter(Mandatory = $True)]
        [Int]$SeasonNumber,

        [Parameter(Mandatory = $True)]
        [Boolean]$Monitored
    )

    ####################################################################################################
    #Region Import configuration
    try
    {
        Import-Configuration -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion



    ####################################################################################################
    #Region Get the series
    try
    {
        $Series = Get-SonarrSeries -Id $Id -ErrorAction Stop
        if(!$Series)
        {
            throw "Series with ID $Id not found."
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region Define the path, parameters, headers and URI
    try
    {
        if($Series.overview)
        {
            $Series.overview = Convert-SmartPunctuation -String $Series.overview
        }

        # Set the monitored status
        $Series.monitored = [bool]$Monitored

        # Encode the body
        $BodyJSON = ($Series | ConvertTo-Json -Depth 5)
        $BodyEncoded = ([System.Text.Encoding]::UTF8.GetBytes($BodyJSON))

        $Path = '/series/' + "$Id"

        # Generate the headers and URI
        $Headers = Get-Headers
        $Uri = Get-APIUri -RestEndpoint $Path -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion


    ####################################################################################################
    #Region make the main request
    Write-Verbose "Querying $Uri"
    try
    {
        Invoke-RestMethod -Uri $Uri -Headers $Headers -Method Put -ContentType 'application/json' -Body $BodyEncoded -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion
}
function Convert-SmartPunctuation
{
    <#
        .SYNOPSIS
            Aims to replace some comment smart characters with their ASCII equivalents.
 
        .DESCRIPTION
            Aims to replace some comment smart characters with their ASCII equivalents.
            This file needs BOM encoding in order to correctly 'store' the smart characters.
 
        .PARAMETER String
            The string to clean
 
        .EXAMPLE
            Convert-SmartPunctuation "This is a ‘test’"
            Outputs: This is a 'test'
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [String]$String
    )

    Begin
    {
    }
    Process
    {
        $String = $String -replace "’", "'"
        $String = $String -replace "‘", "'"
        $String = $String -replace '“', '"'
        $String = $String -replace '”', '"'
        $String = $String -replace '–', '-'
        return $string
    }
    End
    {
    }
}
function Get-APIUri
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $RestEndpoint,

        [Parameter(Mandatory = $false)]
        [System.Collections.IDictionary]
        $Params
    )


    # If the endpoint starts with /, strip it off:
    if($RestEndpoint.StartsWith('/'))
    {
        $RestEndpoint = $RestEndpoint.Substring(1)
    }

    # Join the parameters as key=value pairs, and concatenate them with &
    if($Params.Count -gt 0)
    {
        [String]$ParamString = "?" + (($Params.GetEnumerator() | ForEach-Object { $_.Name + '=' + $_.Value }) -join '&')
    }
    else
    {
        [String]$ParamString = $Null
    }

    return "$($Config.Protocol)://$($Config.Server):$($Config.Port)/api/v$($Config.APIVersion)/$($RestEndpoint)$($ParamString)"
}
function Get-Headers
{
    [CmdletBinding()]
    param(
    )

    return @{
        'X-Api-Key' = $Config.APIKey
    }
}
function Import-Configuration
{
    [CmdletBinding()]
    param(
    )

    $FileName = 'PSSonarrConfig.json'
    $FilePath = "$HOME/.PSSonarr/$FileName"

    if(Test-Path $FilePath)
    {
        try
        {
            $Script:Config = Get-Content $FilePath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        }
        catch
        {
            throw $_
        }

        # Refine to our default server:
        $Script:Config = $Script:Config | Where-Object { $_.Default -eq $True }
        if(!$Script:Config)
        {
            throw "No default server found in $FilePath. Please run Set-SonarrConfiguration."
        }
    }
    else
    {
        throw "Config file not found at $FilePath. Please run Set-SonarrConfiguration."
    }
}