Plex – a quick introduction
As many of you may know, I’m a long time fan of the cross-platform media server, Plex. If you know what Plex is, the feel free to click this link to be taken straight to the PowerShell documentation and code!
As an ex-XBMC user (pun intended), I was initially drawn to Plex’s offering by the fact it looks polished ‘out of the box’ without having to author custom themes. When I discovered it could transcode to iOS devices, I made the leap wholesale, purchasing an Apple Mac Mini front end PC for my home theater downstairs and a Mac Mini Server for my office to run the back-end Media Server.
Of course, since then things have moved on a bit. It’s no longer an Apple-only affair…
Plex now offers native computer clients for Windows Desktop, Windows Modern UI and OSX, with Linux being catered for in the community.
A smorgasbord of mobile platforms are supported with iOS, Android and Windows Phone (horray!) being represented.
Connected device support is pretty much unparalleled with Samsung Smart TVs, Roku & Amazon Fire TV, GoogleTV and ChromeCast along with generic DNLA all being supported.
Finally, the Plex dev team have authored servers for computer operating systems (Windows, OSX, Linux & FreeBSD) and plugins for six NAS manufacturers.
But this post isn’t about the clients, or what servers you can run. It’s about…
Managing Plex Media Server
As a long time user of Plex, I have reasonably large library. On occasion, due to hardware failure or hardware upgrade, I’ve had to rebuild said library, and manually flag episode/series/show viewed statuses. More frequently, I’ve had cause to manage my files on the basis of their library metadata (i.e. delete ‘watched’ TV shows) rather than their file data. Batch operations using the mouse are always a pain, so my scripting orientated mind got to thinking…
For a long while now, Plex has supported management of it’s library via a webmin, punching commands over HTTP and supporting returns in the XML format.
For an even longer time, PowerShell has supported invoking HTTP and parsing XML.
Recently, a good buddy of mine, Richard Green, mentioned that Fiddler was a great way of intercepting browser traffic to work out what GET requests web based applications are posting
[See where I’m going here? 😉 ]
So, this weekend I put together a PowerShell script containing functions to enable me to parse and in a limited capacity manage by library. Say welcome to…
PoSH4PlexApp
PoSH4PlexApp is an in-development PowerShell module for querying and managing PlexApp libraries via Plex Media Server’s web listener. I’ve authored this code with an ethos that wherever possible, I will offload search activities to the Plex Media Server search service , rather than filtering arrays in memory. That said, there were a couple of fringe cases, such as searching for TV shows by ‘viewed’ status, where I had to bake my own, as the API doesn’t surface a method right now.
These functions are currently operational:
Get-PlexVideo
Queries the library of the Plex Media Server (PMS) specified for videos, returning metadata attributes. Supports filtering on the basis of ‘viewed’ status, video type (TV, Movies, All), series number, episode number, video name and/or show name
Parameters
PlexServer : IP address or Hostname of Plex Media Server listening on port 32400
VideoName : Name of video
VideoType : Type of video – Can be set to TV, Movie or All
ShowName : Name of TV Show. Will return no results when VideoType set to Movie (obviously)
ViewedStatus : Whether the video has been viewed or not – Can be set to Watched, Unwatched or All
EpisodeNumber : Episode number of video. Cannot be combined with VideoType parameter
SeriesNumber : Series number of video. Cannot be combined with VideoType parameter
Get PlexShow
Queries the TV sections of the PMS library for TV shows, returning metadata attributes. Supports filtering on the basis of show name and/or ‘viewed’ status
Parameters
PlexServer : IP address or Hostname of Plex Media Server listening on port 32400
ShowName : Name of TV Show
ViewedStatus : Whether the video has been viewed or not – Can be set to Watched, Part-Watched, Unwatched or All
Set-PlexViewedStatus
Sets the viewed status (watched/unwatched) of videos, series and shows in the PMS library. Accepts user-typed unique ID (ratingKey) of object from Plex library or pipe-lined input from the above two functions
Parameters
PlexServer : IP address or Hostname of Plex Media Server listening on port 32400
Key : Unique key of Plex library artefact
ViewedStatus : What you want to set the ‘Viewed’ status to be – Can be set to Watched or Unwatched
I also have the following functions planned for the near future:
Get-PlexSeries – Query library of the PMS for TV series
Set-PlexVideo – Update metadata attributes on Plex video objects
Set-PlexShow – Update metadata attributes on Plex show objects
Set-PlexSeries – Update metadata attributes on Plex series objects
… however, my first priority is to tidy up the commenting and refactor the current ‘Get’ functions to accept pipeline input. That and providing useful help text for each of the functions, something I typically neglect to do if not beaten into it!
The source code is available below and on CodePlex. Do feel free to reach out to me if you have any feedback or ideas, as they would be gratefully received.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
Function Get-PlexVideo { [CmdletBinding(DefaultParameterSetName="VideoMeta")] Param( [Parameter(Mandatory=$True,Position=1)] [string]$PlexServer, [Parameter(Mandatory=$False)] [ValidateSet('Watched','Unwatched','All')] [string]$ViewedStatus = 'All', [Parameter(Mandatory=$False)] [string]$VideoName, [Parameter(Mandatory=$False)] [string]$ShowName, [Parameter(Mandatory=$False, ParameterSetName="VideoMeta")] [ValidateSet('TV','Movies')] [string]$VideoType, [Parameter(Mandatory=$False, ParameterSetName="TVMeta")] [int]$SeriesNumber, [Parameter(Mandatory=$False, ParameterSetName="TVMeta")] [int]$EpisodeNumber ) If($PlexServer) #User specified their Plex Server { $SearchResult = @() Switch ($ViewedStatus.ToLower()) { 'all' {$moviesearchtrail = '/search?type=1&sort=titleSort:asc' ; $episodesearchtrail = '/all?type=4&sort=index:asc'} 'watched' {$moviesearchtrail = '/search?type=1unwatched=0&sort=titleSort:asc' ; $episodesearchtrail = '/all?type=4&unwatched=0&sort=index:asc'} 'unwatched' {$moviesearchtrail = '/all?type=1&unwatched=1&sort=titleSort:asc' ; $episodesearchtrail = '/all?type=4&unwatched=1&sort=index:asc'} } If ($EpisodeNumber) {$episodesearchtrail =$episodesearchtrail + "&index=" + $EpisodeNumber} If ($VideoName -and (!$EpisodeNumber -or !$SeriesNumber -or !$ShowName)) #Video Name specified as a parameter, no TVMeta parameters defined {$moviesearchtrail = $moviesearchtrail + "&title=" + ($VideoName -replace " ","%20"); $episodesearchtrail = $episodesearchtrail + "&title=" + ($VideoName -replace " ","%20")} $SectionsBaseURL = "http://" + $PlexServer + ":32400/library/sections" $Sections = New-Object System.Xml.XmlDocument $Sections.Load($SectionsBaseURL) $SectionsDirectories = $Sections.MediaContainer.Directory If (!$VideoType) {#No VideoType or VideoName specified - Global Search ForEach ($Directory in $SectionsDirectories) #Each directory in section listing... { If ($Directory.Type -eq 'movie') #... where it's a movie { $ChosenLibraryURL = $SectionsBaseURL+ "/" + $Directory.key + $moviesearchtrail #List 'movies' against the section $ChosenLibrary = New-Object System.Xml.XmlDocument $ChosenLibrary.Load($ChosenLibraryURL) ForEach ($Video in ($ChosenLibrary.MediaContainer.Video)) {$SearchResult += $Video} }#Close section type -eq Movie check if block ElseIf ($Directory.Type -eq 'show') #... where it's a show { $ChosenLibraryURL = $SectionsBaseURL+ "/" + $Directory.key + $episodesearchtrail #Perform an 'show' search against the section $ChosenLibrary = New-Object System.Xml.XmlDocument $ChosenLibrary.Load($ChosenLibraryURL) ForEach ($Video in ($ChosenLibrary.MediaContainer.Video)) {$SearchResult += $Video} } #Close section type -eq show check if block } #Close directory in section loop }#Close ElseIf No VideoType or VideoName specified Else #VideoType initialised { If ($VideoType.ToLower() -eq "movies") #No VideoName, VideoType -eq movie { ForEach ($Directory in $SectionsDirectories) #Each directory in section listing... { If ($Directory.Type -eq 'movie') #... where it's a movie { $ChosenLibraryURL = $SectionsBaseURL+ "/" + $Directory.key + $moviesearchtrail #List 'movies' against the section $ChosenLibrary = New-Object System.Xml.XmlDocument $ChosenLibrary.Load($ChosenLibraryURL) ForEach ($Video in ($ChosenLibrary.MediaContainer.Video)) {$SearchResult += $Video} }#Close section type -eq Movie check if block } #Close directory in section loop }# Close No VideoName, VideoType -eq Movie ElseIf block ElseIf ($VideoType.ToLower() -eq "tv") #No VideoName, VideoType -eq tv { ForEach ($Directory in $SectionsDirectories) #Each directory in section listing... { If ($Directory.Type -eq 'show') #... where it's a show { $ChosenLibraryURL = $SectionsBaseURL+ "/" + $Directory.key + $episodesearchtrail #Perform an 'show' search against the section $ChosenLibrary = New-Object System.Xml.XmlDocument $ChosenLibrary.Load($ChosenLibraryURL) ForEach ($Video in ($ChosenLibrary.MediaContainer.Video)) {$SearchResult += $Video} } #Close section type -eq show check if block } #Close directory in section loop } # Close No VideoName, VideoType -eq TV If block }#Close loop where VideoType initialised If ($SeriesNumber) {$SearchResult = $SearchResult | Where {$_.parentIndex -eq $SeriesNumber}} If ($VideoName -and ($EpisodeNumber -or $SeriesNumber -or $ShowName)){$SearchResult = $SearchResult | Where {$_.title -match $VideoName} } If ($ShowName) {$SearchResult = $SearchResult | Where {($_.grandparentTitle -match $ShowName) -and ($_.Type -eq 'episode')}} Return $SearchResult } Else {Write-Error -Message "No Plex Server specified"} #User did not specify their Plex Server } Function Get-PlexShow { Param( [Parameter(Mandatory=$True,Position=1)] [string]$PlexServer, [Parameter(Mandatory=$False)] [string]$ShowName, [Parameter(Mandatory=$False)] [ValidateSet('Part-Watched','Watched','Unwatched','All')] [string]$ViewedStatus = 'All' ) If($PlexServer) #User specified their Plex Server { $SearchResult = @() Switch ($ViewedStatus.ToLower()) { 'all' {$showsearchtrail = '/all?type=2&sort=titleSort:asc'} 'watched' {$showsearchtrail = '/all?type=2&sort=titleSort:asc'} 'part-watched' {$showsearchtrail = '/all?type=2&sort=titleSort:asc'} 'unwatched' {$showsearchtrail = '/all?type=2&unwatchedLeaves=1&sort=titleSort:asc'} } If ($ShowName) {$showsearchtrail = $showsearchtrail + "&title=" + ($ShowName -replace " ","%20")} $SectionsBaseURL = "http://" + $PlexServer + ":32400/library/sections" $Sections = New-Object System.Xml.XmlDocument $Sections.Load($SectionsBaseURL) $SectionsDirectories = $Sections.MediaContainer.Directory ForEach ($Directory in $SectionsDirectories) #Each directory in section listing... { If ($Directory.Type -eq 'show') #... where it's of type show { $ChosenLibraryURL = $SectionsBaseURL+ "/" + $Directory.key + $showsearchtrail #Perform an 'show' search against the section $ChosenLibrary = New-Object System.Xml.XmlDocument $ChosenLibrary.Load($ChosenLibraryURL) ForEach ($Show in ($ChosenLibrary.MediaContainer.Directory)) {$SearchResult += $Show} } #Close section type -eq show check if block } #Close directory in section loop If ($ViewedStatus.ToLower() -eq 'part-watched'){$SearchResult = $SearchResult | Where {($_.viewedLeafCount -ne 0) -and ($_.leafCount -ne $_.viewedLeafCount)} } ElseIf ($ViewedStatus.ToLower() -eq 'watched'){$SearchResult = $SearchResult | Where {$_.leafCount -eq $_.viewedLeafCount} } Return $SearchResult } Else {Write-Error -Message "No Plex Server specified"} #User did not specify their Plex Server } Function Set-PlexViewedStatus { Param( [Parameter(Mandatory=$True,Position=1)] [string]$PlexServer, [Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)] [PSobject[]]$Key, [Parameter(Mandatory=$True)] [ValidateSet('Watched','Unwatched')] [string]$ViewedStatus ) BEGIN { If ($ViewedStatus.ToLower() -eq "watched") {$ScrobbleAction = "scrobble"} Else {$ScrobbleAction = "unscrobble"} } PROCESS { ForEach ($ObjectKey in $Key) { If ($ObjectKey.GetType().Name -eq 'XmlElement') { If ($ObjectKey.ratingKey){$ResolvedObjectKey = $ObjectKey.ratingKey} } ElseIf ($ObjectKey.GetType().Name -eq 'String') { $ResolvedObjectKey = $ObjectKey } $ScrobbleURL = "http://"+ $PlexServer + ":32400/:/" + $ScrobbleAction + "?key=" + $ResolvedObjectKey + "&identifier=com.plexapp.plugins.library" $InvokeScrobbleAction = Invoke-WebRequest($ScrobbleURL) } } END {} } |
I’ll be doing some deep dive posts over the coming week on how I’m searching the library and handling the objects where I cannot get the Plex Media Server search service to do the lifting on my behalf. I may go into how I put the Cmdlets together, if any interest is expressed, but in all honesty the Hey, Scripting Guy blog does a pretty good job of those sort of tutorials 😉
Leave a Reply