PSPlex.psm1

function Add-PlexItemToPlaylist
{
    <#
        .SYNOPSIS
            Copies a single item to a playlist.
        .DESCRIPTION
            Copies a single item to a playlist.
        .PARAMETER PlaylistId
            The id of the playlist.
        .PARAMETER ItemId
            Id (ratingKey) of the Plex items to add. Can be a single item, comma separated list, or an array.
        .EXAMPLE
            # Add an item to a playlist on the default plex server
            Add-PlexItemToPlaylist -PlaylistId 12345 -ItemId 7204
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $PlaylistId,

        [Parameter(Mandatory = $true)]
        [String[]]
        $ItemId
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get machine identifier
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting list of Plex servers (to get machine identifier)"
    try
    {
        $CurrentPlexServer = Get-PlexServer -Name $DefaultPlexServer.PlexServer -ErrorAction Stop
        if(!$CurrentPlexServer)
        {
            throw "Could not find $CurrentPlexServer in $($Servers -join ', ')"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Construct Uri
    try
    {
        $Items = $ItemId -join ","
        $Params = [Ordered]@{
            uri = "server://$($CurrentPlexServer.machineIdentifier)/com.plexapp.plugins.library/library/metadata/$Items"
        }
        $DataUri = Get-PlexAPIUri -RestEndpoint "playlists/$PlaylistID/items" -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Make request
    if($PSCmdlet.ShouldProcess($PlaylistId, "Add item $ItemId to playlist"))
    {
        Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Adding item to playlist."
        try
        {
            Invoke-RestMethod -Uri $DataUri -Method PUT | Out-Null
        }
        catch
        {
            throw $_
        }
    }

    #EndRegion
}
function Add-PlexLabel
{
    <#
        .SYNOPSIS
            Adds a label to a Plex item (movie, show, or album).
        .DESCRIPTION
            Labels attached on movies, shows or albums are useful when sharing
            library content with others; you can choose to only show items with
            specific labels, or to hide items with specific labels.
        .PARAMETER Id
            Id of the item to add the label to.
        .PARAMETER Label
            The label to add.
        .EXAMPLE
            Add-PlexLabel -Id 12345 -Label 'FLAC'
        .NOTES
            Only movies, shows and albums support labels.
    #>


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

        [Parameter(Mandatory = $true)]
        [String]
        $Label
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get the item
    try
    {
        $Item = Get-PlexItem -Id $Id
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Simple checks:
    # If the item already has this label:
    if($Item.Label.Tag -contains $Label)
    {
        Write-Verbose -Message "Item already has label '$Label'"
        return
    }
    #EndRegion

    #############################################################################
    # Get the type id/value for this item:
    $Type = Get-PlexItemTypeId -Type $Item.Type

    #############################################################################
    #Region Construct Uri
    try
    {
        $Params = [Ordered]@{
            id                   = $Item.ratingKey
            type                 = $Type
            includeExternalMedia = 1
        }
        # Combine existing labels (if there are any, force casting to an array)
        # and the user specified label. Append to params.
        # Format: &label[0].tag.tag=MyLabel&label[1].tag.tag=AnotherLabel
        $Index = 0
        foreach($String in ([Array]$Item.Label.Tag + $Label))
        {
            $Params.Add("label[$($Index)].tag.tag", $String)
            $Index++
        }
        $DataUri = Get-PlexAPIUri -RestEndpoint "$($Item.librarySectionKey)/all" -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Make request
    if($PSCmdlet.ShouldProcess($Item.title, "Add label '$Label'"))
    {
        Write-Verbose -Message "Adding label '$Label' to item '$($Item.title)'"
        try
        {
            Invoke-RestMethod -Uri $DataUri -Method PUT
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
function Copy-PlexPlaylist
{
    <#
        .SYNOPSIS
            This function will copy a playlist from your account to another user account on your server.
            Note: If the destination user already has a playlist with this name, a second one will be created.
            To overwrite, use the -Force switch.
        .DESCRIPTION
            This function will copy a playlist from your account to another user account on your server.
            Note: If the destination user already has a playlist with this name, a second one will be created.
            To overwrite, use the -Force switch.
        .PARAMETER Id
            Id of the playlist you wish to copy. To get this, use 'Get-PlexPlaylist'.
        .PARAMETER NewPlaylistName
            Create the playlist with a different name.
        .PARAMETER Username
            The username for the account you wish to copy the playlist to.
        .PARAMETER Force
            Overwrite the contents of an existing playlist.
        .EXAMPLE
            Copy-PlexPlaylist -Id 12345 -User 'user@domain.com'
    #>


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

        [Parameter(Mandatory = $false)]
        [String]
        $NewPlaylistName,

        [Parameter(Mandatory = $true)]
        [String]
        $Username,

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

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion


    #############################################################################
    #Region Get the Playlist we want to copy
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting playlist $Id, including playlist items"
    try
    {
        # Get and filter:
        $Playlist = Get-PlexPlaylist -Id $Id -IncludeItems -ErrorAction Stop
        if(!$Playlist)
        {
            throw "Could not find playlist with id $Id."
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion


    #############################################################################
    #Region Get the target user along with user token for our server
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting user."
    try
    {
        $User = Get-PlexUser -User $Username -IncludeToken -ErrorAction Stop
        if(($Null -eq $User) -or ($User.count -eq 0))
        {
            throw "Could not get user: $Username"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion


    #############################################################################
    #Region Create a new variable to store the destination playlist name
    if($NewPlaylistName)
    {
        $PlaylistTitle = $NewPlaylistName
    }
    else
    {
        $PlaylistTitle = $Playlist.title
    }
    #EndRegion

    #############################################################################
    #Region Check whether the user already has a playlist by this name.
    # It's worth noting that you can have multiple playlists with the same name (sigh).
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Checking $Username account for existing playlist"
    try
    {
        [Array]$ExistingPlaylistsWithSameName = Get-PlexPlaylist -AlternativeToken $User.token -ErrorAction Stop | Where-Object { $_.title -eq $PlaylistTitle }
        if($ExistingPlaylistsWithSameName.Count -gt 1)
        {
            # If there is more than 1 playlist with the same name in the destination account, we
            # 1) wouldn't know which we wanted to overwrite and
            # 2) wouldn't want to remove them automatically when -Force is used, so warn and exit.
            Write-Warning -Message "Multiple playlists with the name '$PlaylistTitle' exist under the destination account $Username. You can review these with 'Get-PlexPlaylist' and remove with 'Remove-PlexPlaylist' (after obtaining a user token for $Username with 'Get-PlexUser -Username $Username -IncludeToken')."
            return
        }
        elseif($ExistingPlaylistsWithSameName.Count -eq 1)
        {
            if($Force)
            {
                Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Playlist already exists in destination account. Removing."
                foreach($PL in $ExistingPlaylistsWithSameName)
                {
                    try
                    {
                        Remove-PlexPlaylist -Id $PL.ratingKey -AlternativeToken $User.token -ErrorAction Stop | Out-Null
                    }
                    catch
                    {
                        Write-Warning -Message "Could not delete existing playlist."
                        throw $_
                    }
                }
            }
            else
            {
                Write-Warning -Message "The destination account already has a Playlist with the name '$PlaylistTitle'. To overwrite it, call this function with the -Force parameter."
                return
            }
        }
        else
        {
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion


    #############################################################################
    #Region Get the machine Id property for the current plex server we're working with:
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting list of Plex servers"
    try
    {
        $CurrentPlexServer = Get-PlexServer -Name $DefaultPlexServer.PlexServer -ErrorAction Stop
        if(!$CurrentPlexServer)
        {
            throw "Could not find $CurrentPlexServer in $($Servers -join ', ')"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    # Establish whether the playlist is smart or not; this will determine how we create it:
    # If playlist is not smart:
    if($Playlist.smart -eq 0)
    {
        Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Original playlist is NOT smart."

        # Create a new playlist on the server, under the user's account:
        if($PSCmdlet.ShouldProcess("Playlist: $PlaylistTitle", "Create playlist on server $($DefaultPlexServer.PlexServer) under user $Username"))
        {
            try
            {
                Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Creating playlist"
                $ItemsToAdd = $Playlist.Items.ratingKey -join ','
                $Params = [Ordered]@{
                    type           = $Playlist.playlistType
                    title          = $PlaylistTitle
                    smart          = 0
                    uri            = "server://$($CurrentPlexServer.machineIdentifier)/com.plexapp.plugins.library/library/metadata/$ItemsToAdd"
                    'X-Plex-Token' = $User.token
                }
                $DataUri = Get-PlexAPIUri -RestEndpoint "playlists" -Params $Params
                $Data = Invoke-RestMethod -Uri $DataUri -Method POST
                return $Data.MediaContainer.Playlist
            }
            catch
            {
                throw $_
            }
        }
    }
    elseif($Playlist.smart -eq 1)
    {
        Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Original playlist is smart."

        # Although we have the playlist object from Get-PlexPlaylist, this function makes a query for all playlists before returning based on a match
        # by the playlist name. With this, we're not given a property called .content which contains the data that defines *how* the playlist is smart.

        # Parse the data in the playlist to establish what parameters were used to create the smart playlist.
        # Split on the 'all?':
        $SmartPlaylistParams = ($Playlist.content -split 'all%3F')[1]

        if($PSCmdlet.ShouldProcess("Playlist: $PlaylistTitle", "Create playlist on server $($DefaultPlexServer.PlexServer) under user $Username"))
        {
            try
            {
                Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Creating playlist"
                $Params = [Ordered]@{
                    type           = $Playlist.playlistType
                    title          = $PlaylistTitle
                    smart          = 1
                    uri            = "server://$($CurrentPlexServer.machineIdentifier)/com.plexapp.plugins.library/library/sections/2/all?$($SmartPlaylistParams)"
                    'X-Plex-Token' = $User.token
                }
                $DataUri = Get-PlexAPIUri -RestEndpoint "playlists" -Params $Params
                $Data = Invoke-RestMethod -Uri $DataUri -Method POST
                return $Data.MediaContainer.Playlist
            }
            catch
            {
                throw $_
            }
        }
    }
    else
    {
        Write-Warning -Message "Function: $($MyInvocation.MyCommand): No work done."
    }
}
function Find-PlexItem
{
    <#
        .SYNOPSIS
            This function uses the search ability of Plex find items on your Plex server.
        .DESCRIPTION
            This function uses the search ability of Plex find items on your Plex server.
            As objects returned have different properties depending on the type, there is
            an option to refine this by type.
        .PARAMETER ItemName
            Name of what you wish to find.
        .PARAMETER ItemType
            Refines the output by type.
        .PARAMETER Year
            Refine by year.
        .PARAMETER ExactNameMatch
            Return only items matching exactly what is specified as ItemName.
        .EXAMPLE
            # Find only 'movies' from the Plex server that (fuzzy)match 'The Dark Knight'.
            Find-PlexItem -ItemName 'The Dark Knight' -ItemType 'movie'
        .EXAMPLE
            # Find items that match exactly 'The Dark Knight' from the library 'Films'.
            Find-PlexItem -ItemName 'The Dark Knight' -ExactNameMatch -LibraryTitle 'Films'
        .EXAMPLE
            # Find items that (fuzzy)match 'Spider' from the library 'TV'.
            Find-PlexItem -ItemName 'spider' -LibraryTitle 'TV'
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $ItemName,

        [Parameter(Mandatory = $false)]
        [ValidateSet('movie', 'episode', 'album')]
        [String]
        $ItemType,

        [Parameter(Mandatory = $false)]
        [String]
        $LibraryTitle,

        [Parameter(Mandatory = $false)]
        [Int]
        $Year,

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

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion


    #############################################################################
    #Region Construct Uri
    try
    {
        # URLEncode the title, otherwise we'll get '400 bad request' errors when searching for things like: Bill and Ted's ...
        $ItemNameEncoded = [System.Web.HttpUtility]::UrlEncode($ItemName)
        $Params = [Ordered]@{
            'includeCollections' = 0
            'sectionId'          = ''
            'query'              = $ItemNameEncoded
            'limit'              = 50
        }

        $DataUri = Get-PlexAPIUri -RestEndpoint "hubs/search" -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Make request
    Write-Verbose -Message "Searching for $ItemName."
    try
    {
        [Array]$Data = Invoke-RestMethod -Uri $DataUri -Method GET
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    # Refine by type:
    if($ItemType)
    {
        $Results = ($Data.MediaContainer.Hub | Where-Object { $_.type -eq $ItemType -and $_.Size -gt 0 }).Metadata
    }
    else
    {
        $Results = ($Data.MediaContainer.Hub | Where-Object { $_.Size -gt 0 }).Metadata
    }

    if($Results.Count -gt 0)
    {
        # Refine by the ItemName to attempt an exact match:
        if($ExactNameMatch)
        {
            [Array]$Results = $Results | Where-Object { $_.title -eq $ItemName }
            # There could still be more than one result with an exact title match due to the same item being in multiple libraries
            # or even in the same library!
            if($Results.count -gt 1)
            {
                Write-Warning -Message "Exact match was specified but there was more than 1 result for $ItemName."
            }
        }

        # Refine by library name:
        if($LibraryTitle)
        {
            # Sometimes they come back with 'reasonTitle'. Makes sense, not.
            Write-Verbose "Refining multiple results by library"
            [Array]$Results = $Results | Where-Object { $_.librarySectionTitle -eq $LibraryTitle -or $_.reasonTitle -eq $LibraryTitle }
        }

        if($Year)
        {
            #[Array]$Results = $Results | Where-Object { ($_.originallyAvailableAt.split('-')[0]) -match $Year }
            Write-Verbose "Refining results by Year: $Year"
            [Array]$Results = $Results | Where-Object { $_.year -eq $Year }
        }

        # Add datetime objects so we don't have to work with unixtimes...
        $Results | ForEach-Object {
            if($Null -ne $_.lastViewedAt) { $_ | Add-Member -NotePropertyName 'lastViewedAtDateTime' -NotePropertyValue (ConvertFrom-UnixTime $_.lastViewedAt) -Force }
            if($Null -ne $_.addedAt) { $_ | Add-Member -NotePropertyName 'addedAtDateTime' -NotePropertyValue (ConvertFrom-UnixTime $_.addedAt) -Force }
            if($Null -ne $_.updatedAt) { $_ | Add-Member -NotePropertyName 'updatedAtDateTime' -NotePropertyValue (ConvertFrom-UnixTime $_.updatedAt) -Force }
        }

        return $Results
    }
    else
    {
        Write-Verbose -Message "No result found."
        return
    }
}
function Get-PlexCollection
{
    <#
        .SYNOPSIS
            Gets collections.
        .DESCRIPTION
            Gets collections.
        .PARAMETER Id
            The id of the collection to get.
        .PARAMETER LibraryId
            The id of the library to get collections from.
        .PARAMETER IncludeItems
            If specified, the items in the collection are returned.
        .EXAMPLE
            Get-PlexCollection -LibraryId 1
        .EXAMPLE
            Get-PlexCollection -Id 723 -IncludeItems
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = "CollectionId")]
        [PSObject]
        $Id,

        [Parameter(Mandatory = $true, ParameterSetName = "LibraryId")]
        [PSObject]
        $LibraryId,

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

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Construct Uri
    if($Id)
    {
        $RestEndpoint = "library/collections/$($Id)"
    }

    if($LibraryId)
    {
        $RestEndpoint = "library/sections/$($LibraryId)/collections"
        #?includeCollections=1&includeExternalMedia=1&includeAdvanced=1&includeMeta=1"
        $Params = [Ordered]@{
            "includeCollections"   = 1
            "includeExternalMedia" = 0
            "includeAdvanced"      = 1
            "includeMeta"          = 1
        }
    }

    $DataUri = Get-PlexAPIUri -RestEndpoint $RestEndpoint -Params $Params
    #EndRegion

    #############################################################################
    #Region Get data
    try
    {
        $Data = Invoke-RestMethod -Uri $DataUri -Method GET
        if($Data.MediaContainer.metadata.count -eq 0)
        {
            return
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Get items
    if($IncludeItems)
    {
        if($Id)
        {
            Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Appending collection item(s) for collection $($Data.MediaContainer.metadata.Title)"
            try
            {
                $Params = [Ordered]@{
                    excludeAllLeaves = 1
                }
                $ItemsUri = Get-PlexAPIUri -RestEndpoint "library/collections/$($Id)/children" -Params $Params
                $Items = Invoke-RestMethod -Uri $ItemsUri -Method GET
                $Data.MediaContainer.metadata | Add-Member -NotePropertyName 'Items' -NotePropertyValue $Items.MediaContainer.metadata
            }
            catch
            {
                throw $_
            }
        }
        else
        {
            # Iterate over each collection, make a lookup for the items and append them:
            foreach($Collection in $Data.MediaContainer.Metadata)
            {
                Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Appending collection item(s) for collection $($Collection.title)"
                try
                {
                    $Params = [Ordered]@{
                        excludeAllLeaves = 1
                    }
                    $ItemsUri = Get-PlexAPIUri -RestEndpoint "library/collections/$($Collection.RatingKey)/children" -Params $Params
                    $Items = Invoke-RestMethod -Uri $ItemsUri -Method GET
                    $Collection | Add-Member -NotePropertyName 'Items' -NotePropertyValue $Items.MediaContainer.Metadata
                }
                catch
                {
                    throw $_
                }
            }
        }
    }
    #EndRegion

    return $Data.MediaContainer.Metadata
}
function Get-PlexItem
{
    <#
        .SYNOPSIS
            Get a specific item.
        .DESCRIPTION
            Get a specific item.
        .PARAMETER Id
            The id of the item.
        .PARAMETER IncludeTracks
            Only valid for albums. If specified, the tracks in the album are returned.
        .PARAMETER LibraryTitle
            Gets all items from a library with the specified title.
        .EXAMPLE
            # Get a single item by Id:
            Get-PlexItem -Id 204
        .EXAMPLE
            # Get all items from the library called 'Films'.
            # NOTE: Not all data for an item is returned this way.
            $Items = Get-PlexItem -LibraryTitle Films
            # Get all data for the above items:
            $AllData = $Items | % { Get-PlexItem -Id $_.ratingKey }
    #>


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

        [Parameter(Mandatory = $false, ParameterSetName = 'Id')]
        [Switch]
        $IncludeTracks,

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

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Construct Uri
    if($Id)
    {
        $DataUri = Get-PlexAPIUri -RestEndpoint "library/metadata/$Id"
    }
    elseif($LibraryTitle)
    {
        # Get the library to determine what type it is:
        $Library = Get-PlexLibrary | Where-Object { $_.title -eq $LibraryTitle }

        # If we were to support lookup of a library by Id we have to consider
        # that it returns with no TYPE attribute, so we couldn't construct params correctly.
        # or KEY (presented as librarySectionID).

        if(!$Library)
        {
            throw "No such library. Run Get-PlexLibrary to see a list."
        }
        else
        {
            if($Library.key)
            {
                $Key = $Library.key
            }
            elseif($Library.librarySectionID)
            {
                $Key = $Library.librarySectionID
            }
            else
            {
                throw "Unable to determine library key/id/sectionId"
            }

            $Params = [Ordered]@{
                sort                        = 'titleSort'
                includeGuids                = 1
                includeConcerts             = 0
                includeExtras               = 0
                includeOnDeck               = 0
                includePopularLeaves        = 0
                includePreferences          = 0
                includeReviews              = 0
                includeChapters             = 0
                includeStations             = 0
                includeExternalMedia        = 0
                asyncAugmentMetadata        = 0
                asyncCheckFiles             = 0
                asyncRefreshAnalysis        = 0
                asyncRefreshLocalMediaAgent = 0
            }
            $DataUri = Get-PlexAPIUri -RestEndpoint "library/sections/$Key/all" -Params $Params
        }
    }
    else {}
    #EndRegion

    #############################################################################
    #Region Get data
    try
    {
        $Data = Invoke-RestMethod -Uri $DataUri -Method GET

        # The metadata returned from Plex often contains duplicate values which breaks the (inherent) conversion into JSON, ending up as a string. Known cases:
        # guid and Guid
        # rating and Rating
        # The uppercase versions seem to be arrays of richer data, e.g. Guid contains IDs from various other metadata sources, as does Rating.

        # This isn't always the case however, so we need to check the object type:
        if($Data.gettype().Name -eq 'String')
        {
            # Let's go with renaming the lowercase keys. Using .Replace rather than -replace as it should be faster.
            $Data = $Data.toString().Replace('"guid"', '"_guid"').Replace('"rating"', '"_rating"')
            # Convert back into JSON:
            $Data = $Data | ConvertFrom-Json
        }
        else
        {
            # $Data should be JSON already.
        }

        # If this is an album, respect -IncludeTracks and get track data:
        if($Data.MediaContainer.Metadata.type -eq 'album' -and $IncludeTracks)
        {
            Write-Verbose -Message "Making additional lookup for album tracks"
            # $Data returned above has a key property on albums which equals: /library/metadata/{ratingKey}/children
            $TrackURi = Get-PlexAPIUri -RestEndpoint $Data.MediaContainer.Metadata.key
            $ChildData = Invoke-RestMethod -Uri $TrackURi -Method GET
            # Append:
            $Data.MediaContainer.Metadata | Add-Member -MemberType NoteProperty -Name 'Tracks' -Value $ChildData.MediaContainer.Metadata
        }

        # Return the required subproperty:
        return $Data.MediaContainer.Metadata
    }
    catch
    {
        throw $_
    }
    #EndRegion
}
function Get-PlexLibrary
{
    <#
        .SYNOPSIS
            By default, returns a list of libraries on a Plex server.
        .DESCRIPTION
            By default, returns a list of libraries on a Plex server.
            If -Id is specified, a single library is returned with
        .PARAMETER PlexServerHostname
            Fully qualified hostname for the Plex server (e.g. myserver.mydomain.com)
        .PARAMETER Protocol
            http or https
        .PARAMETER Port
            Parameter description
        .PARAMETER Id
            If specified, returns a specific library.
        .EXAMPLE
            Get-PlexLibrary
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [String]
        $Id
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Make request
    try
    {
        $DataUri = Get-PlexAPIUri -RestEndpoint "library/sections/$Id"
        [array]$Data = Invoke-RestMethod -Uri $DataUri -Method GET
        if($Id)
        {
            [array]$Results = $Data.MediaContainer
        }
        else
        {
            [array]$Results = $Data.MediaContainer.Directory
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    return $Results
}
function Get-PlexPlaylist
{
    <#
        .SYNOPSIS
            Gets playlists.
        .DESCRIPTION
            Gets playlists.
        .PARAMETER Id
            The id of the playlist to get.
        .PARAMETER IncludeItems
            If specified, the items in the playlist are returned.
        .PARAMETER AlternativeToken
            Alternative token to use for authentication. For example,
            when querying for playlists for a different user.
        .EXAMPLE
            Get-PlexPlaylist -Id 723 -IncludeItems
        .EXAMPLE
            $User = Get-PlexUser -Username "friendsusername"
            Get-PlexPlaylist -AlternativeToken $User.Token
    #>


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

        [Parameter(Mandatory = $false)]
        [Switch]
        $IncludeItems,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $AlternativeToken
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get data
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting playlist(s)"
    try
    {
        # Plex decided to automatically create Playlists with heart emojis in for music playlists.
        # When calling Invoke-RestMethod, PowerShell ends up converting these to squiggly a characters.
        # To work around this, we have to use Invoke-WebRequest and take the RawContentStream property
        # and use that.
        if($AlternativeToken)
        {
            $Params = @{'X-Plex-Token' = $AlternativeToken }
        }

        $DataUri = Get-PlexAPIUri -RestEndpoint "playlists/$Id" -Params $Params
        $Data = Invoke-WebRequest -Uri $DataUri -ErrorAction Stop
        if($Data)
        {
            $UTF8String = [system.Text.Encoding]::UTF8.GetString($Data.RawContentStream.ToArray())
            [array]$Results = ($UTF8String | ConvertFrom-Json).MediaContainer.Metadata
        }
        else
        {
            return
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Get items
    if($IncludeItems)
    {
        foreach($Playlist in $Results)
        {
            # If the playlist is smart skip it, as it doesn't have a static item list:
            if($Playlist.smart)
            {
                continue
            }

            # We don't need -AlternativeToken here as the playlists have unique IDs
            $ItemsUri = Get-PlexAPIUri -RestEndpoint "playlists/$($Playlist.ratingKey)/items"
            Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting and appending playlist item(s) for playlist $($playlist.title)"
            try
            {
                [array]$Items = Invoke-RestMethod -Uri $ItemsUri -ErrorAction Stop
                $Playlist | Add-Member -NotePropertyName 'Items' -NotePropertyValue $Items.MediaContainer.Metadata
            }
            catch
            {
                throw $_
            }
        }
    }
    #EndRegion

    #############################################################################
    # Append type and return results
    $Results | ForEach-Object { $_.psobject.TypeNames.Insert(0, "PSPlex.Playlist") }
    return $Results
}
function Get-PlexServer
{
    <#
        .SYNOPSIS
            Returns a list of online Plex Servers that you have access to.
        .DESCRIPTION
            Returns a list of online Plex Servers that you have access to.
        .EXAMPLE
            Get-PlexServer
        .OUTPUTS
            accessToken : abcd123456ABCDEFG
            name : thor
            address : 87.50.66.123
            port : 32400
            version : 1.16.0.1226-7eb2c8f6f
            scheme : http
            host : 87.50.66.123
            localAddresses : 172.18.0.2
            machineIdentifier : 8986j4286yl055szhtjx1bytgibsgpv93neb8yv4
            createdAt : 1550665837
            updatedAt : 1562328805
            owned : 1
            synced : 0
 
            accessToken : HIJKLMNO098765431
            name : friendserver
            address : 94.12.145.10
            port : 32400
            version : 1.16.1.1291-158e5b199
            scheme : http
            host : 94.12.145.10
            localAddresses :
            machineIdentifier : 534vgrzhrrp47oojircfdz9qxeqav4gkmqqnu1at
            createdAt : 1520613024
            updatedAt : 1562330172
            owned : 0
            synced : 0
            sourceTitle : username_of_friend
            ownerId : 6728195
            home : 0
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [String]
        $Name
    )

    #############################################################################
    #Region Import Plex Configuration
    try
    {
        Import-PlexConfiguration -WhatIf:$False
        $DefaultPlexServer = $PlexConfigData | Where-Object { $_.Default -eq $True }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Get Servers
    try
    {
        $Data = Invoke-RestMethod -Uri "https://plex.tv/api/servers`?`X-Plex-Token=$($DefaultPlexServer.Token)" -Method GET -UseBasicParsing
        if($Name)
        {
            [array]$Results = $Data.MediaContainer.Server | Where-Object { $_.name -eq $Name }
        }
        else
        {
            [array]$Results = $Data.MediaContainer.Server
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    return $Results
}
function Get-PlexSession
{
    <#
        .SYNOPSIS
            Gets a list of sessions (streams) on the Plex server.
        .DESCRIPTION
            Gets a list of sessions (streams) on the Plex server.
        .EXAMPLE
            Get-PlexSession
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get data
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting all sessions"
    try
    {
        $DataUri = Get-PlexAPIUri -RestEndpoint "status/sessions"
        $Data = Invoke-RestMethod -Uri $DataUri -Method GET -ErrorAction Stop
        if($Data.gettype().Name -eq 'String')
        {
            # Let's go with renaming the lowercase keys. Using .Replace rather than -replace as it should be faster.
            $Data = $Data.toString().Replace('"guid"', '"_guid"').Replace('"rating"', '"_rating"')
            # Convert back into JSON:
            $Data = $Data | ConvertFrom-Json
        }
        else
        {
            # $Data should be JSON already.
        }

        if($Data.MediaContainer.Size -eq 0)
        {
            return
        }

        $Results = $Data.MediaContainer.Metadata
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    # Append type and return results
    $Results | ForEach-Object { $_.psobject.TypeNames.Insert(0, "PSPlex.Session") }
    return $Results
}
function Get-PlexShare
{
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $false, ParameterSetName = 'username')]
        [String]
        $Username,

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

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get Server Details from Plex.tv (to obtain machine identifier)
    try
    {
        $Servers = Get-PlexServer -ErrorAction Stop
        if(!$Servers)
        {
            throw "No servers? This is odd..."
        }

        $Server = $Servers | Where-Object { $_.Name -eq $DefaultPlexServer.PlexServer }
        if(!$Server)
        {
            throw "Could not match the current default Plex server ($($DefaultPlexServer.PlexServer)) to those returned from plex.tv"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Get data
    try
    {
        $Data = Invoke-RestMethod -Uri "https://plex.tv/api/servers/$($Server.machineIdentifier)/shared_servers`?`X-Plex-Token=$($DefaultPlexServer.Token)" -Method GET -ErrorAction Stop
        if($Data.MediaContainer.Size -eq 0)
        {
            return
        }

        #############################################################################
        # Managed users have no username property - not sure how best to handle this.
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Filter by username or email
    if($Username -or $Email)
    {
        Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Filtering by $($PsCmdlet.ParameterSetName)"
        [array]$Results = $Data.MediaContainer.SharedServer | Where-Object { $_."$($PsCmdlet.ParameterSetName)" -eq $($PSBoundParameters[$PsCmdlet.ParameterSetName]) }
        if(!$Results)
        {
            Write-Verbose -Message "No results found for specified username"
            return
        }
    }
    else
    {
        [array]$Results = $Data.MediaContainer.SharedServer
    }
    #EndRegion

    #############################################################################
    # Append type and return results
    $Results | ForEach-Object { $_.psobject.TypeNames.Insert(0, "PSPlex.SharedLibrary") }
    return $Results
}
function Get-PlexUser
{
    <#
        .SYNOPSIS
            Gets a list of users associated with your account (e.g those you have shared with).
        .DESCRIPTION
            Gets a list of users associated with your account (e.g those you have shared with).
            This can include users who do not currently have access to libraries.
        .PARAMETER Username
            Refine by username (note: all users must be obtained from the Plex API first).
        .PARAMETER IncludeToken
            Get access token(s) that accounts use to access your server.
        .EXAMPLE
            Get-PlexUser
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $false)]
        [String]
        $Username,

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

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get data
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting all users"
    try
    {
        $Data = Invoke-RestMethod -Uri "https://plex.tv/api/users`?X-Plex-Token=$($DefaultPlexServer.Token)" -Method GET -ErrorAction Stop
        if($Data.MediaContainer.Size -eq 0)
        {
            return
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    # Managed users have no username property (only title). As this module uses 'username', copy title to username:
    $Data.MediaContainer.User | Where-Object { $null -eq $_.username } | ForEach-Object {
        $_ | Add-Member -NotePropertyName 'username' -NotePropertyValue $_.title -Force
    }

    #############################################################################
    # It seems that migrating to JSON requests for data results in objects not possessing 'lastSeenAt'
    # as a top level property for each user. It's nested away, so let's get it and add it as a new property.
    # Hashing out as this property seems to be the same across all users, and don't understand that just yet.
    #$Data.MediaContainer.User | ForEach-Object {
    # if($Null -ne $_.ChildNodes.lastSeenAt) { $_ | Add-Member -NotePropertyName 'lastSeenAt' -NotePropertyValue (ConvertFrom-UnixTime $_.ChildNodes.lastSeenAt) -Force }
    #}

    #############################################################################
    #Region Filtering
    if($Username)
    {
        Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Filtering by username"
        [array]$Results = $Data.MediaContainer.User | Where-Object { $_.username -eq $Username }
    }
    else
    {
        [array]$Results = $Data.MediaContainer.User
    }
    #EndRegion

    #############################################################################
    #Region Include access tokens
    if($IncludeToken)
    {
        Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting access token(s)"

        # There isn't a way to selective query, so just get all user tokens:
        try
        {
            # Make a request for server data to get the machine identifier:
            $PlexServer = Get-PlexServer -Name $DefaultPlexServer.PlexServer -ErrorAction Stop

            # Get all user tokens:
            $UserTokens = Get-PlexUserToken -MachineIdentifier $PlexServer.machineIdentifier -ErrorAction Stop

            # On the token objects, repeat the logic earlier by setting username to equal title if username is null.
            # Subtly different this time because the property exists on the object returned so we don't *create* a new
            # noteproperty but just populate the existing one:
            $UserTokens | Where-Object { $_.username -eq "" } | ForEach-Object {
                $_.username = $_.title
            }

            Write-Verbose -Message "$($UserTokens.count) user tokens received"
        }
        catch
        {
            throw $_
        }

        # Append (somewhat inefficient with the where clause, but this is in the order of ms here):
        foreach($User in $Results)
        {
            $User | Add-Member -MemberType NoteProperty -Name 'token' -Value $($UserTokens | Where-Object { $_.username -eq $User.username }).token
        }
    }
    #EndRegion

    #############################################################################
    # Append type and return results
    $Results | ForEach-Object { $_.psobject.TypeNames.Insert(0, "PSPlex.User") }
    return $Results
}
function New-PlexPlaylist
{
    <#
        .SYNOPSIS
            Creates a new playlist.
        .DESCRIPTION
            Creates a new playlist.
        .PARAMETER Name
            Name of the playlist.
        .PARAMETER Type
            Type of playlist. Currently only 'video' is supported.
        .PARAMETER Id
            Id (ratingKey) of the Plex items to add. Can be a single item, comma separated list, or an array.
        .EXAMPLE
            New-PlexPlaylist -Name "My Playlist" -Type video -Id 123,456,789
        .EXAMPLE
            $Item = Find-PlexItem -Name "Some Movie"
            New-PlexPlaylist -Name "My Playlist" -Type video -Id $Item.ratingKey
    #>


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

        [Parameter(Mandatory = $true)]
        [ValidateSet('video')]
        [String]
        $Type,

        [Parameter(Mandatory = $true)]
        [String[]]
        $ItemId
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Check if playlist already exists
    try
    {
        $Playlists = Get-PlexPlaylist
        if($Playlists.title -contains $Name)
        {
            throw "Playlist '$Name' already exists"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Get machine identifier
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting list of Plex servers (to get machine identifier)"
    try
    {
        $CurrentPlexServer = Get-PlexServer -Name $DefaultPlexServer.PlexServer -ErrorAction Stop
        if(!$CurrentPlexServer)
        {
            throw "Could not find $CurrentPlexServer in $($Servers -join ', ')"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Construct Uri
    try
    {
        $Items = $ItemId -join ","
        $Params = [Ordered]@{
            title = $Name
            type  = $Type
            smart = 0
            uri   = "server://$($CurrentPlexServer.machineIdentifier)/com.plexapp.plugins.library/library/metadata/$Items"
        }
        $DataUri = Get-PlexAPIUri -RestEndpoint "playlists" -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Make request
    if($PSCmdlet.ShouldProcess($Item.title, "Add label '$Label'"))
    {
        Write-Verbose -Message "Adding label '$Label' to item '$($Item.title)'"
        try
        {
            $Data = Invoke-RestMethod -Uri $DataUri -Method POST
            return $Data.mediacontainer.metadata
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
function Remove-PlexLabel
{
    <#
        .SYNOPSIS
            Removes a label from a Plex item (movie, show, or album).
        .DESCRIPTION
            Labels attached on movies, shows or albums are useful when sharing
            library content with others; you can choose to only show items with
            specific labels, or to hide items with specific labels.
        .PARAMETER Id
            Id of the item to remove the label from.
        .PARAMETER Label
            The label to remove.
        .EXAMPLE
            Remove-PlexLabel -Id 12345 -Label 'FLAC'
        .NOTES
            Only movies, shows and albums support labels.
    #>


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

        [Parameter(Mandatory = $true)]
        [String]
        $Label
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get the item
    try
    {
        $Item = Get-PlexItem -Id $Id
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Simple checks:
    # If the item has no labels:
    if(!$Item.Label.Tag)
    {
        Write-Verbose -Message "Item has no labels"
        return
    }

    # If the item doesn't have this label:
    if($Item.Label.Tag -notcontains $Label)
    {
        Write-Verbose -Message "Item does not have the label '$Label'"
        return
    }
    #EndRegion

    #############################################################################
    # Get the type id/value for this item:
    $Type = Get-PlexItemTypeId -Type $Item.Type

    #############################################################################
    #Region Construct Uri
    try
    {
        $Params = [Ordered]@{
            id                   = $Item.ratingKey
            type                 = $Type
            includeExternalMedia = 1
        }

        # Keep the existing labels (if there are any, force casting to an array) except
        # for the user specified label:
        $Index = 0
        foreach($String in ([Array]$Item.Label.Tag | Where-Object { $_ -ne $Label }))
        {
            $Params.Add("label[$($Index)].tag.tag", $String)
            $Index++
        }
        # Finally, to remove the label we need to add it like so:
        $Params.Add('label[].tag.tag-', $Label)

        $DataUri = Get-PlexAPIUri -RestEndpoint "$($Item.librarySectionKey)/all" -Params $Params
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Make request
    if($PSCmdlet.ShouldProcess($Item.title, "Remove label '$Label'"))
    {
        Write-Verbose -Message "Removing label '$Label' from item '$($Item.title)'"
        try
        {
            Invoke-RestMethod -Uri $DataUri -Method PUT
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
function Remove-PlexPlaylist
{
    <#
        .SYNOPSIS
            Remove a playlist from your or another user's account.
        .DESCRIPTION
            Remove a playlist from your or another user's account.
        .PARAMETER Id
            The Id of the playlist to remove.
        .PARAMETER AlternativeToken
            Alternative token to use for authentication. For example,
            when querying for playlists for a different user.
        .EXAMPLE
            Remove-PlexPlaylist -Id 12345
    #>


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

        [Parameter(Mandatory = $false)]
        [String]
        $AlternativeToken
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Make request
    if($PSCmdlet.ShouldProcess("Remove playlist with Id '$Id'"))
    {
        Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Removing playlist"
        try
        {
            if($AlternativeToken)
            {
                $Params = @{'X-Plex-Token' = $AlternativeToken }
            }
            $Uri = Get-PlexAPIUri -RestEndpoint "playlists/$Id" -Params $Params
            Invoke-RestMethod -Uri $Uri -Method DELETE -ErrorAction Stop | Out-Null
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
function Remove-PlexShare
{
    <#
        .SYNOPSIS
            Removes a shared library from a user.
        .DESCRIPTION
            Removes a shared library from a user.
        .PARAMETER Username
            The username to remove the shared library from.
        .PARAMETER LibraryTitle
            The name of the library to unshare.
        .PARAMETER LibraryId
            The id of the library to unshare.
        .EXAMPLE
            Remove-PlexShare -Username 'myfriend' -LibraryTitle 'Films'
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $Username,

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

        [Parameter(Mandatory = $true, ParameterSetName = 'LibraryId')]
        [Int]
        $LibraryId
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get User
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting user."
    try
    {
        $User = Get-PlexUser -User $Username -ErrorAction Stop
        if(($Null -eq $User) -or ($User.count -eq 0))
        {
            throw "Could not get the user '$Username'"
        }

        # When viewing the libraries shared to a user via the web client, it makes a request to
        # https://plex.tv/api/v2/shared_servers/$someid where $someid appears to be a unique Id
        # assigned to the user for the server in question. It's not the normal Plex user id.
        # Extract it from the Server property on the user object, making sure we match against the right
        # server of course.
        $UserIdOnServer = ($User.server | Where-Object { $_.name -eq $DefaultPlexServer.PlexServer }).Id
        if(!$UserIdOnServer)
        {
            throw "Could not determine user id on server: $($DefaultPlexServer.PlexServer). Are you sure the user '$Username' has access to any libraries?"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Confirm library access
    Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Checking User Access to Library"
    try
    {
        $DataForUser = Invoke-RestMethod -Uri "https://plex.tv/api/v2/shared_servers/$UserIdOnServer`?X-Plex-Token=$($DefaultPlexServer.Token)&X-Plex-Client-Identifier=PowerShell" -Method GET -ErrorAction Stop
        if(($Null -eq $DataForUser.libraries) -or ($DataForUser.libraries.count -eq 0))
        {
            throw "No shared libraries with user: $Username"
        }

        if($LibraryTitle)
        {
            if($Null -eq ($DataForUser.libraries | Where-Object { $_.title -eq $LibraryTitle }))
            {
                throw "The library '$LibraryTitle' is not shared with user: $Username"
            }

            if(($DataForUser.libraries | Where-Object { $_.title -eq $LibraryTitle }).Count -gt 1)
            {
                throw "Multiple libraries found called '$LibraryTitle'. Re-run this function using -LibraryId instead of -LibraryTitle to target a specific library."
            }

            # Grab the Id for the library:
            $LibraryId = ($DataForUser.libraries | Where-Object { $_.title -eq $LibraryTitle }).Id
        }
        elseif($LibraryId)
        {
            if($Null -eq ($DataForUser.libraries | Where-Object { $_.Id -eq $LibraryId }))
            {
                throw "The library with Id '$LibraryId' is not shared with user: $Username"
            }
        }
        else
        {
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    # At this point we know the user has access to the library. Remove:
    try
    {
        # If the result of this leaves the user with 0 libraries, we need to make a DELETE request.
        # If the result of this leaves the user with 1 or more libraries, we need to make a POST request.
        # So, if there's just 1 we need to delete everything, else post...
        if($DataForUser.libraries.count -eq 1)
        {
            $Method = 'DELETE'
            Write-Verbose -Message "Removing library with $($Method)"
            Invoke-RestMethod -Uri "https://plex.tv/api/v2/shared_servers/$UserIdOnServer`?X-Plex-Token=$($DefaultPlexServer.Token)&X-Plex-Client-Identifier=PowerShell" -Method $Method -ErrorAction Stop | Out-Null
            return
        }
        else
        {
            $Method = 'POST'

            # The body for this post needs to be a list of all library IDs excluding the one we're removing.
            $LibraryIdsToKeep = $DataForUser.libraries | Where-Object { $_.Id -ne $LibraryId } | Select-Object -Expand id

            # We have to construct the body object slightly differently depending on whether we're passing an array of library
            # IDs or just 1 Id, so that when converted to JSON it's the right format for Plex:
            if($LibraryIdsToKeep.Count -gt 1)
            {
                $Body = @{
                    machineIdentifier = "$($MatchingServer.machineIdentifier)"
                    librarySectionIds = $LibraryIdsToKeep
                } | ConvertTo-Json -Compress
            }
            else
            {
                $Body = @{
                    machineIdentifier = "$($MatchingServer.machineIdentifier)"
                    librarySectionIds = @($LibraryIdsToKeep)
                } | ConvertTo-Json -Compress
            }
            Write-Verbose -Message "Removing library with $($Method): $LibraryTitle"
            Invoke-RestMethod -Uri "https://plex.tv/api/v2/shared_servers/$UserIdOnServer`?X-Plex-Token=$($DefaultPlexServer.Token)&X-Plex-Client-Identifier=PowerShell" -Method $Method -ContentType "application/json" -Body $Body -ErrorAction Stop | Out-Null
        }
    }
    catch
    {
        throw $_
    }
}
function Set-PlexConfiguration
{
    <#
        .SYNOPSIS
        .DESCRIPTION
        .PARAMETER Credential
        .EXAMPLE
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [PSCredential]
        $Credential,

        [Parameter(Mandatory = $true)]
        [String]
        $DefaultServerName
    )


    #Region Get auth token:
    try
    {
        $Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password)))
        $Data = Invoke-RestMethod -Uri "https://plex.tv/users/sign_in.json" -Method POST -Headers @{
            'Authorization'            = ("Basic {0}" -f $Base64AuthInfo);
            'X-Plex-Client-Identifier' = "PowerShell-Test";
            'X-Plex-Product'           = 'PowerShell-Test';
            'X-Plex-Version'           = "V0.01";
            'X-Plex-Username'          = $Credential.GetNetworkCredential().UserName;
        } -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #Region Get online servers
    try
    {
        $ResourceData = Invoke-RestMethod -Uri "https://plex.tv/api/v2/resources?includeHttps=1&X-Plex-Token=$($Data.user.authentication_token)&X-Plex-Client-Identifier=PSPlex" -Method GET -UseBasicParsing -Headers @{"Accept" = "application/json, text/plain, */*" }
        if(!$ResourceData)
        {
            throw "Could not get resource data."
        }

        # Refine to only servers that are online and owned by the user:
        [Array]$OwnedAndOnline = $ResourceData | Where-Object { $_.product -eq 'Plex Media Server' -and $_.owned -eq 1 }
        if(!$OwnedAndOnline)
        {
            throw "No owned servers online."
        }

        # If in the owned and online servers, there's no match for $DefaultServerName, throw an error:
        if($OwnedAndOnline.Name -notcontains $DefaultServerName)
        {
            throw "The server name '$DefaultServerName' does not match any of the owned and online servers."
        }

        # Loop and construct a custom object to store in our configuration file.
        $ConfigurationData = [System.Collections.ArrayList]@()
        foreach($Server in $OwnedAndOnline)
        {
            # When storing the configuration data for each server, we need an accessible uri.

            # In the .connections property there may be an array of objects each with an 'address' property.
            # Find an address where it's a public IP address and 'uri' matches 'plex.direct':
            $Connection = $Server.connections | Where-Object { $_.address -notmatch '(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)' -and $_.uri -match "plex.direct" }
            if(!$Connection)
            {
                # We didn't find a suitable Plex.direct connection to use so skip this server
                continue
            }

            # If the current server name is equal to $DefaultServerName, set the 'Default' property to $true
            if($Server.name -eq $DefaultServerName)
            {
                $Default = $true
            }
            else
            {
                $Default = $false
            }

            $ConfigurationData.Add(
                [PSCustomObject]@{
                    PlexServer       = $Server.name
                    Port             = $Connection.port
                    PublicAddress    = $Server.publicAddress
                    ClientIdentifier = $Server.clientIdentifier
                    Token            = $Server.accessToken
                    Uri              = $Connection.uri
                    Default          = $Default
                }) | Out-Null
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #Region Save Configuration to disk
    try
    {
        $ConfigFile = Get-PlexConfigFileLocation -ErrorAction Stop

        # Create folder if it doesn't exist:
        if(-not (Test-Path (Split-Path $ConfigFile)))
        {
            New-Item -ItemType Directory -Path (Split-Path $ConfigFile) | Out-Null
        }

        # Write the configuration data to disk:
        ConvertTo-Json -InputObject @($ConfigurationData) -Depth 3 -ErrorAction Stop | Out-File -FilePath $ConfigFile -Force -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion
}
function Set-PlexItemEdition
{
    <#
        .SYNOPSIS
            Sets the edition on a Plex (movie) item.
        .DESCRIPTION
            Sets the edition on a Plex (movie) item.
        .PARAMETER Id
            The id of the item.
        .PARAMETER Edition
            Edition value.
        .EXAMPLE
            Set-PlexItemRating -Id 12345 -Edition "Director's Cut"
    #>


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

        [Parameter(Mandatory = $true)]
        [String]
        $Edition
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Get the Item
    try
    {
        $Item = Get-PlexItem -Id $Id -ErrorAction Stop
        if($Item.type -ne 'movie')
        {
            throw "Plex only supports setting the edition on movies. Item type is: $($Item.type)"
        }
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    #Region Submit rating
    if($PSCmdlet.ShouldProcess($Id, "Set edition to $Edition"))
    {
        Write-Verbose -Message "Setting edition"
        try
        {
            $RestEndpoint = "$($Item.librarySectionKey)/all"
            $Params = [Ordered]@{
                type                 = 1
                id                   = $Id
                "editionTitle.value" = $Edition
            }
            $Uri = Get-PlexAPIUri -RestEndpoint $RestEndpoint -Params $Params
            Invoke-RestMethod -Uri $Uri -Method Put
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
function Set-PlexItemRating
{
    <#
        .SYNOPSIS
            Sets the rating on a Plex item.
        .DESCRIPTION
            Sets the rating on a Plex item. Must be between 1-5.
        .PARAMETER Id
            The id of the item.
        .PARAMETER Rating
            Rating value.
        .EXAMPLE
            Set-PlexItemRating -Id 12345 -Rating 3
    #>


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

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, 5)]
        [Int]
        $Rating
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Submit rating
    if($PSCmdlet.ShouldProcess($Id, "Set rating to $Rating"))
    {
        Write-Verbose -Message "Submitting rating"
        try
        {
            $RestEndpoint = ":/rate"
            $Params = [Ordered]@{
                key        = $Id
                rating     = $($Rating * 2)
                identifier = 'com.plexapp.plugins.library'
            }
            $Uri = Get-PlexAPIUri -RestEndpoint $RestEndpoint -Params $Params
            Invoke-RestMethod -Uri $Uri -Method Put
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
function Set-PlexItemWatchStatus
{
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $Id,

        [Parameter(Mandatory = $true)]
        [ValidateSet('played', 'unplayed')]
        [String]
        $Status,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $AlternativeToken
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Construct Uri
    if($Status -eq 'played')
    {
        $RestEndpoint = ":/scrobble"
    }
    else
    {
        $RestEndpoint = ":/unscrobble"
    }

    $Params = [Ordered]@{
        identifier = 'com.plexapp.plugins.library'
        key        = $Id
    }

    if($AlternativeToken)
    {
        $Params.Add('X-Plex-Token', $AlternativeToken)
    }

    $DataUri = Get-PlexAPIUri -RestEndpoint $RestEndpoint -Params $Params
    #EndRegion

    #############################################################################
    #Region Make Request
    if($PSCmdlet.ShouldProcess("Set watch status for item Id $Id to $Status"))
    {
        Write-Verbose -Message "Setting watch status for item Id $Id to $Status"
        try
        {
            Invoke-RestMethod -Uri $DataUri -Method "GET" | Out-Null
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
function Set-PlexWebhook
{
    <#
        .SYNOPSIS
            Sets a URL for your Plex server to use for webhooks.
        .DESCRIPTION
            Sets a URL for your Plex server to use for webhooks.
        .PARAMETER Url
            The URL endpoint to receive your Plex webhooks
        .EXAMPLE
            Set-PlexWebhook -Url https://myserver.domain.com/plex
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param (
        [ValidateScript(
            {
                if($null -ne ([System.URI]$_).AbsoluteURI)
                {
                    $True
                }
                Else
                {
                    throw "$_ is not a valid Url"
                } })]
        [String]$Url
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Make request
    $string = "urls[]=" + $Url
    Add-Type -AssemblyName System.Web
    $stringencoded = [System.Web.HttpUtility]::UrlEncode($string)
    $stringencoded = $stringencoded -replace '%3d', '='

    $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
    $session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"

    if($PSCmdlet.ShouldProcess($DefaultPlexServer.PlexServer, "Set webhook to $Url"))
    {
        Invoke-WebRequest -UseBasicParsing -Uri "https://plex.tv/api/v2/user/webhooks?X-Plex-Token=$($DefaultPlexServer.Token)" `
            -Method POST `
            -WebSession $session `
            -ContentType "application/x-www-form-urlencoded; charset=UTF-8" `
            -Body $stringencoded
    }
    #EndRegion
}
function Stop-PlexSession
{
    <#
        .SYNOPSIS
            Stops a Plex session.
        .DESCRIPTION
            Stops a Plex session, either by id or by passing the results of Get-PlexSession
            to -SessionObject.
        .PARAMETER Id
            The session id to stop.
        .PARAMETER SessionObject
            The session object, if piping.
        .PARAMETER Reason
            Optional reason for stopping the session. Will be shown to the streamer.
        .EXAMPLE
            $Session = Get-Session (assumes only 1 stream)
            Stop-PlexSession -Id $Session.Session.id
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'SessionId')]
        [String]$Id,

        [Parameter(Mandatory = $true, ParameterSetName = 'SessionObject', ValueFromPipeline = $true)]
        $SessionObject,

        [Parameter(Mandatory = $false)]
        [String]$Reason = 'Message your Plex contact, or try again later!'
    )

    begin
    {

        #############################################################################
        #Region Import Plex Configuration
        if(!$script:PlexConfigData)
        {
            try
            {
                Import-PlexConfiguration -WhatIf:$False
            }
            catch
            {
                throw $_
            }
        }
        #EndRegion

        # If the user passed an Id, create an object using the same structure as the session object
        if($PSCmdlet.ParameterSetName -eq 'SessionId')
        {
            [Array]$SessionObject = [PSCustomObject]@{
                Session = @{
                    Id = $Id
                }
            }
        }
        else
        {
        }
    }
    process
    {
        foreach($Session in $SessionObject)
        {
            if($PSCmdlet.ShouldProcess($Session.Session.Id, 'Stop Plex Session'))
            {
                Write-Verbose -Message "Terminating session: $($Session.Id)"
                try
                {
                    $RestEndpoint = "status/sessions/terminate"
                    $Params = [Ordered]@{
                        reason    = $Reason
                        sessionId = $Session.Session.Id
                    }
                    $Uri = Get-PlexAPIUri -RestEndpoint $RestEndpoint -Params $Params

                    # A successful termination returns nothing from the API
                    Invoke-RestMethod -Uri $Uri -Method GET -ErrorAction Stop
                }
                catch
                {
                    throw $_
                }
            }
        }
    }
    end
    {
    }
}
function Update-PlexItemMetadata
{
    <#
        .SYNOPSIS
            Update the metadata for a Plex item.
        .DESCRIPTION
            Update the metadata for a Plex item.
        .PARAMETER Id
            The id of the item to update.
        .EXAMPLE
            Update-PlexItemMetadata -Id 54321
    #>


    [CmdletBinding(SupportsShouldProcess)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'False Positive')]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $Id
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Make request
    if($PSCmdlet.ShouldProcess("Update metadata for item $Id"))
    {
        Write-Verbose -Message "Initiating metadata refresh for item Id $Id"
        try
        {
            $Uri = Get-PlexAPIUri -RestEndpoint "library/metadata/$Id/refresh"
            Invoke-RestMethod -Uri $Uri -Method PUT | Out-Null
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
function Update-PlexLibrary
{
    <#
        .SYNOPSIS
            Initiates an update on a library.
        .DESCRIPTION
            Initiates an update on a library.
        .PARAMETER Id
            The Id of the library to update.
        .EXAMPLE
            Update-PlexLibrary -Id 123
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $Id
    )

    #############################################################################
    #Region Import Plex Configuration
    if(!$script:PlexConfigData)
    {
        try
        {
            Import-PlexConfiguration -WhatIf:$False
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion

    #############################################################################
    #Region Make request
    if($PSCmdlet.ShouldProcess("Update library $Id"))
    {
        Write-Verbose -Message "Initiating library update for library Id $Id"
        try
        {
            $Uri = Get-PlexAPIUri -RestEndpoint "library/sections/$Id/refresh"
            Invoke-RestMethod -Uri $Uri -Method GET -ErrorAction Stop
        }
        catch
        {
            throw $_
        }
    }
    #EndRegion
}
Function ConvertFrom-UnixTime($UnixDate)
{
    [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').AddSeconds($UnixDate))
}
function Get-PlexAPIUri
{
    <#
        .SYNOPSIS
            Returns a valid Uri for the Plex API.
        .DESCRIPTION
            Returns a valid Uri for the Plex API.
        .PARAMETER RestEndpoint
            The endpoint (the part after protocol://hostname:port/)
        .PARAMETER Params
            A hashtable/ordered hashtable of (URL) parameters
        .PARAMETER Token
            To make a request with another token (e.g. for a different user)
            use this parameter.
        .EXAMPLE
            $Params = @{
                sort = "titleSort"
            }
            $RestEndpoint = "library/sections/1/all"
            $DataUri = Get-PlexAPIUri -RestEndpoint $RestEndpoint -Params $Params
            Invoke-RestMethod -Uri $DataUri -Method GET
    #>


    [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)
    {
        # If the calling function hasn't passed a token as part of $Params, then add the default token to function as the default user:
        if($Null -eq $Params["X-Plex-Token"])
        {
            $Params.Add("X-Plex-Token", $Script:DefaultPlexServer.Token)
        }

        [String]$ExtraParamString = (($Params.GetEnumerator() | ForEach-Object { $_.Name + '=' + $_.Value }) -join '&') + "&"
    }
    else
    {
        [String]$ExtraParamString = "X-Plex-Token=$($Script:DefaultPlexServer.Token)"
    }

    return "$($Script:DefaultPlexServer.Uri)/$RestEndpoint`?$($ExtraParamString)"
}
function Get-PlexConfigFileLocation
{
    <#
        .SYNOPSIS
            Returns config file location.
        .DESCRIPTION
            Returns config file location.
    #>


    [CmdletBinding()]
    [OutputType([System.String])]
    param(
    )

    $FileName = 'PSPlexConfig.json'

    # PowerShell Core has IsWindows, IsLinux, IsMac, but previous versions do not:
    if($IsWindows -or ( [version]$PSVersionTable.PSVersion -lt [version]"5.99.0" ))
    {
        return "$env:appdata\PSPlex\$FileName"
    }
    elseif($IsLinux -or $IsMacOS)
    {
        return "$HOME/.PSPlex/$FileName"
    }
    else
    {
        throw "Unknown Platform"
    }
}
function Get-PlexItemTypeId
{
    <#
        .SYNOPSIS
            Some Plex API calls include a type key value pair. This provides the id for a 'type'.
        .DESCRIPTION
            Some Plex API calls include a type key value pair. This provides the id for a 'type'.
        .PARAMETER Type
            The type
        .EXAMPLE
            Get-PlexItemTypeId -Type 'movie'
    #>


    [CmdletBinding()]
    [OutputType([System.Int32])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('album', 'movie', 'show')]
        [String]
        $Type
    )

    <#
        Unsure how Plex defines the type value (as this isn't present on the metadata returned for an item) but handle
        known types here, at least:
        Movie = 1
        Show (not season) = 2
        Album = 9
    #>


    switch ($Type)
    {
        'album' { 9 }
        'movie' { 1 }
        'show' { 2 }
        default { throw "Unknown type for item. Are you tying to add a label to the wrong type of item? (must be: album, movie, show)" }
    }
}
function Get-PlexUserToken
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [String]
        $MachineIdentifier,

        [Parameter(Mandatory = $false)]
        [String]
        $Username
    )

    # Use the machine Id to get the Server Access Tokens for the users:
    try
    {
        Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Getting all access tokens for server $MachineIdentifier"
        $Data = Invoke-RestMethod -Uri "https://plex.tv/api/servers/$($MachineIdentifier)/access_tokens.xml?auth_token=$($DefaultPlexServer.Token)&includeProfiles=1&includeProviders=1" -ErrorAction Stop
        <#
            Example user object:
 
                token : their-server-access-token
                username : their-username
                thumb : https://plex.tv/users/jkh324jkh234jh23/avatar?c=4353453454
                title : their-email@domain.com
                id : 18658724
                owned : 0
                allow_sync : 0
                allow_camera_upload : 0
                allow_channels : 0
                allow_tuners : 0
                allow_subtitle_admin : 0
                filter_all :
                filter_movies :
                filter_music :
                filter_photos :
                filter_television :
                scrobble_types :
                profile_settings : profile_settings
                library_section : {library_section, library_section, library_section}
 
 
            Despite the singular example, the data can vary depending on how the user signed up.
            We may find that title/username is a typical username.
            We may find that title/username is the same as the email.
            We may find that for a managed user, they won't have a username or email but only 'title'.
 
                title username email
                ----- -------- -----
                mycleverusername mycleverusername someperson@hotmail.com
                anotheruser@gmail.com anotheruser@gmail.com anotheruser@gmail.com
                manageduser
 
            In order to cater to managed users, we will effectively use the title, as username.
 
            Note: It's entirely possible that there could be a managed user with the same username
            as a normal user. It's unlikely, but possible. This should be handled by downstream code.
 
        #>


        if($Username)
        {
            Write-Verbose -Message "Function: $($MyInvocation.MyCommand): Filtering by username/title"
            $Data.access_tokens.access_token | Where-Object { $_.username -eq $Username -or $_.title -eq $Username }
            return
        }
        else
        {
            return $Data.access_tokens.access_token
        }
    }
    catch
    {
        throw $_
    }
}
function Import-PlexConfiguration
{
    <#
        .SYNOPSIS
            Imports configuration from disk.
        .DESCRIPTION
            Imports configuration from disk.
            The aim of this function is to keep the config in a scoped variable for implicit use rather than expecting
            the user to pass details around. As such, nothing is explicitly returned from this function.
            It runs at the beginning of every function.
        .EXAMPLE
            Import-PlexConfiguration
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
    )

    #############################################################################
    # Set some defaults for all cmdlet calls
    $PSDefaultParameterValues["Import-PlexConfiguration:WhatIf"] = $false
    $PSDefaultParameterValues["Invoke-RestMethod:UseBasicParsing"] = $true
    $PSDefaultParameterValues["Invoke-RestMethod:Headers"] = @{"Accept" = "application/json, text/plain, */*" }
    $PSDefaultParameterValues["Invoke-RestMethod:ErrorAction"] = "Stop"
    $PSDefaultParameterValues["Invoke-WebRequest:UseBasicParsing"] = $true
    $PSDefaultParameterValues["Invoke-WebRequest:Headers"] = @{"Accept" = "application/json, text/plain, */*" }
    $PSDefaultParameterValues["Invoke-WebRequest:ErrorAction"] = "Stop"

    #############################################################################
    #Region Get path to the config file (varies by OS):
    try
    {
        $ConfigFile = Get-PlexConfigFileLocation -ErrorAction Stop
    }
    catch
    {
        throw $_
    }
    #EndRegion

    #############################################################################
    if(Test-Path -Path $ConfigFile)
    {
        Write-Verbose -Message "Importing Configuration from $ConfigFile"
        try
        {
            $script:PlexConfigData = Get-Content -Path $ConfigFile -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        }
        catch
        {
            throw $_
        }

        try
        {
            # Perform some file checking to be sure we can actually use it:

            # See if there is a default server set:
            $script:DefaultPlexServer = $script:PlexConfigData | Where-Object { $_.Default -eq $True }

            # If there's more than 1 default set then exit:
            if($script:DefaultPlexServer.Count -gt 1)
            {
                throw "You cannot have more than 1 default server. This shouldn't happen. Have you been manually editing the config file?: $ConfigFile"
            }

            # If there's no default server, and there's only 1 server in the config file set it as the default, save the file and then declare $script:DefaultPlexServer
            if(!$script:DefaultPlexServer -and $script:PlexConfigData.Count -eq 1)
            {
                $script:PlexConfigData.Default = $True
                Write-Warning -Message "Only 1 server defined in the configuration file. Default was set to false. Setting to true."
                ConvertTo-Json -InputObject @($script:PlexConfigData) -Depth 3 | Out-File -FilePath $ConfigFile -Force -ErrorAction Stop
                # Set the default server:
                $script:DefaultPlexServer = $script:PlexConfigData | Where-Object { $_.Default -eq $True }
            }

            # If there's no default server, and there's more than 1 server in the config file, exit:
            if(!$script:DefaultPlexServer -and $script:PlexConfigData.Count -gt 1)
            {
                throw "There are $($script:PlexConfigData.Count) servers configured but none are set to the default. This shouldn't happen. You can inspect the config file here: $ConfigFile"
            }
        }
        catch
        {
            throw $_
        }
    }
    else
    {
        throw 'No saved configuration information. Run Get-PlexAuthenticationToken, then Save-PlexConfiguration.'
    }
}