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] ) ############################################################################# #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.' } } |