Advertisement
martinezmp3

Plex Communicator

May 20th, 2021
1,334
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Groovy 18.96 KB | None | 0 0
  1. import groovy.json.JsonSlurper
  2. /**
  3.  *  Plex Communicator
  4.  *
  5.  *  Copyright 2018 Jake Tebbett
  6.  *  Credit To: Christian Hjelseth, iBeech & Ph4r as snippets of code taken and amended from their apps
  7.  *
  8.  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
  9.  *  in compliance with the License. You may obtain a copy of the License at:
  10.  *
  11.  *      http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
  14.  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
  15.  *  for the specific language governing permissions and limitations under the License.
  16.  *
  17.  * VERSION CONTROL
  18.  * ###############
  19.  *
  20.  *  v1.0 - Test Release
  21.  *  v2.0 - Too many changes to list them all (General improvements and fixes + added track description)
  22.  *
  23.  */
  24.  
  25. definition(
  26.     name: "Plex Communicator",
  27.     namespace: "jebbett",
  28.     author: "Jake Tebbett",
  29.     description: "Allow Your Hub and Plex to Communicate",
  30.     category: "My Apps",
  31.     iconUrl: "https://github.com/jebbett/Plex-Communicator/raw/master/icon.png",
  32.     iconX2Url: "https://github.com/jebbett/Plex-Communicator/raw/master/icon.png",
  33.     iconX3Url: "https://github.com/jebbett/Plex-Communicator/raw/master/icon.png",
  34.     oauth: [displayName: "PlexServer", displayLink: ""])
  35.  
  36.  
  37. def installed() {
  38.     initialize()
  39. }
  40.  
  41. def updated() {
  42.     unsubscribe()
  43.     initialize()
  44. }
  45.  
  46. def initialize() {
  47.     // sub to plex now playing response
  48.     subscribe(location, null, response, [filterEvents:false])
  49.     // Add New Devices
  50.     def storedDevices = state.plexClients
  51.     settings.devices.each {deviceId ->
  52.         try {
  53.             def existingDevice = getChildDevice(deviceId)
  54.             if(!existingDevice) {
  55.                 def theHub = location.hubs[0]
  56.                 if(logging){ log.debug "ADDING DEVICE ID:${deviceId} on HUB [${theHub.id}] : ${theHub}"}
  57.                 def childDevice = addChildDevice("jebbett", "Plex Communicator Device", deviceId, theHub.id, [name: deviceId, label: storedDevices."$deviceId".name, completedSetup: false])
  58.             }
  59.         } catch (e) { log.error "Error creating device: ${e}" }
  60.     }
  61.     // Clean up child devices
  62.     if(settings?.devices) {
  63.         getChildDevices().each { if(settings.devices.contains(it.deviceNetworkId)){}else{deleteChildDevice("${it.deviceNetworkId}")} }
  64.     }else{
  65.         getChildDevices().each { deleteChildDevice("${it.deviceNetworkId}") }
  66.     }
  67.     // Just in case plexPoller has gasped it's last breath (in case it's used)
  68.     if(settings?.stPoller){runEvery3Hours(plexPoller)}
  69. }
  70.  
  71. preferences {
  72.     page name: "mainMenu"
  73.     page name: "noAuthPage"
  74.     page name: "authPage"
  75.     page name: "authPage2"
  76.     page name: "clientPage"
  77.     page name: "clearClients"
  78.     page name: "mainPage"
  79.     page name: "ApiSettings"
  80. }
  81.  
  82. mappings {
  83.   path("/statechanged/:command")    { action: [GET: "plexExeHandler"] }
  84.   path("/p2stset")                  { action: [GET: "p2stset"]   }
  85.   path("/pwhset")                   { action: [GET: "pwhset"]   }
  86.   path("/pwh")                      { action: [POST: "plexWebHookHandler"] }
  87. }
  88.  
  89.  
  90. /***********************************************************
  91. ** Main Pages
  92. ************************************************************/
  93.  
  94. def mainMenu() {
  95.     // Get ST Token
  96.     try { if (!state.accessToken) {createAccessToken()} }
  97.     catch (Exception e) {
  98.         log.info "Unable to create access token, OAuth has probably not been enabled in IDE: $e"
  99.         return noAuthPage()
  100.     }
  101.  
  102.     if (state?.authenticationToken) { return mainPage() }
  103.     else { return authPage() }
  104. }
  105.  
  106. def noAuthPage() {
  107.    
  108.     return dynamicPage(name: "noAuthPage", uninstall: true, install: true) {
  109.         section("*Error* You have not enabled OAuth when installing the app code, please enable OAuth")
  110.     }
  111. }
  112.  
  113. def mainPage() {
  114.     return dynamicPage(name: "mainPage", uninstall: true, install: true) {
  115.         section(""){
  116.             paragraph "<h1><img src='https://github.com/jebbett/Plex-Communicator/raw/master/icon.png' width='64' height='64' />&nbsp;<strong><span style='color: #E8A60B;'>Plex Communicator</span></strong></h1>"
  117.         }
  118.         section("Main Menu") {
  119.             href "clientPage", title:"Select Your Devices", description: "Select the devices you want to monitor"
  120.             href "authPage", title:"Plex Account Details", description: "Update Plex Account Details"
  121.             href(name: "ApiSettings", title: "Connection Methods", required: false, page: "ApiSettings", description: "Select your method for connecting to Plex")
  122.             input "logging", "bool", title: "Turn on to enable debug logs", defaultValue:false, submitOnChange: true
  123.         }
  124.         section("If you want to control lighting scenes then the 'MediaScene' SmartApp is ideal for this purpose"){}
  125.     }
  126. }
  127.  
  128. /***********************************************************
  129. ** Interface Settings
  130. ************************************************************/
  131.  
  132. def ApiSettings() {
  133.     dynamicPage(name: "ApiSettings", title: "Select Your Connection Method", install: false, uninstall: false) {      
  134.         section("1. Plex Webhooks - Plex Pass Only (Best)") {
  135.             paragraph("Plex Webhooks is the best method for connecting Plex to your hub, however you will need an active Plex Pass Subscription to use it")
  136.             href url: "${getLocalApiServerUrl()}/${app.id}/pwhset?access_token=${state.accessToken}", style:"embedded", required:false, title:"Plex Webhooks Settings", description: ""        
  137.         }
  138.         section("2. Plex2Hub Program <B>*NOT WORKING PRESENTLY*</B>") {
  139.             paragraph("This involves running a program on an always on computer")
  140.             href url: "${getLocalApiServerUrl()}/${app.id}/p2stset?access_token=${state.accessToken}", style:"embedded", required:false, title:"Plex2Hub Program Settings", description: ""        
  141.         }
  142.         section("3. Hub Polling *Not Recommended*") {
  143.             paragraph("Your hub will poll every 10 seconds and request the status from Plex, however this method is unreliable and puts increased load on your hub and your network (Don't complain to me that it stops working occasionally)")
  144.             input "stPoller", "bool", title: "Enable - At your own risk", defaultValue:false, submitOnChange: true
  145.         }
  146.         if(settings?.stPoller){plexPoller()}
  147.     }
  148. }
  149.  
  150. def pwhset() {
  151.     def html = """<html><head><title>Plex Webhook Settings</title></head><body><h1>
  152.        ${getFullLocalApiServerUrl()}/pwh?access_token=${state.accessToken}<br />
  153.  
  154.    </h1></body></html>"""
  155.     render contentType: "text/html", data: html, status: 200
  156. }
  157.  
  158. def p2stset() {
  159.     def html = """
  160.    <!DOCTYPE html>
  161.    <html><head><title>Plex2Hub Program Settings</title></head><body><p>
  162.        &lt;!ENTITY accessToken '${state.accessToken}'><br />
  163.        &lt;!ENTITY appId '${app.id}'><br />
  164.        &lt;!ENTITY ide '${getFullLocalApiServerUrl()}'><br />
  165.        &lt;!ENTITY plexStatusUrl 'http://${settings.plexServerIP}:32400/status/sessions?X-Plex-Token=${state.authenticationToken}'>
  166.    </p></body></html>"""
  167.     render contentType: "text/html", data: html, status: 200
  168. }
  169.  
  170.  
  171.  
  172. /***********************************************************
  173. ** Plex Authentication
  174. ************************************************************/
  175.  
  176. def authPage() {
  177.     return dynamicPage(name: "authPage", nextPage: authPage2, install: false) {
  178.         def hub = location.hubs[0]
  179.         section("Plex Login Details") {
  180.             input "plexUserName", "text", "title": "Plex Username", multiple: false, required: true
  181.             input "plexPassword", "password", "title": "Plex Password", multiple: false, required: true
  182.             input "plexServerIP", "text", "title": "Server IP", multiple: false, required: true
  183.         }
  184.     }
  185. }
  186. def authPage2() {
  187.     getAuthenticationToken()
  188.     clientPage()
  189. }
  190.  
  191. def getAuthenticationToken() {
  192.     if(logging){ log.debug "Getting authentication token for Plex Server " + settings.plexServerIP }      
  193.     def paramsp = [
  194.         uri: "https://plex.tv/users/sign_in.json?user%5Blogin%5D=" + settings.plexUserName + "&user%5Bpassword%5D=" + URLEncoder.encode(settings.plexPassword),
  195.         requestContentType: "application/json",
  196.         headers: [
  197.             'X-Plex-Client-Identifier': 'PlexCommunicator',
  198.             'X-Plex-Product': 'Plex Communicator',
  199.             'X-Plex-Version': '1.0'
  200.         ]
  201.     ]    
  202.     try {    
  203.         httpPost(paramsp) { resp ->          
  204.             state.authenticationToken = resp.data.user.authentication_token;
  205.             if(logging){ log.debug "Congratulations Token recieved: " + state.authenticationToken + " & your Plex Pass status is " + resp.data.user.subscription.status } }
  206.     }
  207.     catch (Exception e) { log.warn "Hit Exception $e on $paramsp" }
  208. }
  209.  
  210. /***********************************************************
  211. ** CLIENTS
  212. ************************************************************/
  213.  
  214. def clientPage() {
  215.     getClients()
  216.     pause(2000)
  217.     def devs = getClientList()
  218.     return dynamicPage(name: "clientPage", title: "NOTE:", nextPage: mainPage, uninstall: false, install: true) {
  219.         section("If your device does not appear in the list"){}
  220.         section("Devices currently playing video will have a [►] icon next to them, this can be helpful when multiple devices share the same name, if a device is playing but not shown then press Done and come back to this screen"){
  221.             input "devices", "enum", title: "Select Your Devices", options: devs, multiple: true, required: false, submitOnChange: true
  222.         }
  223.         section("Use the below to clear and re-load the device list"){
  224.             href(name: "clearClients", title:"RESET Devices List", description: "", page: "clearClients", required: false)
  225.         }
  226.        
  227.     }
  228. }
  229.  
  230. def clearClients() {
  231.     state.plexClients = [:]
  232.     getClientsXML()
  233.     getNowPlayingXML()
  234.     pause(2000)
  235.     mainPage()
  236. }
  237.  
  238. def getClientList() {
  239.     def devList = [:]
  240.     state.plexClients.each { id, details -> devList << [ "$id": "${details.name}" ] }
  241.     state.playingClients.each { id, details -> devList << [ "$id": "[►] ${details.name}" ] }
  242.     return devList.sort { a, b -> a.value.toLowerCase() <=> b.value.toLowerCase() }
  243. }
  244.  
  245. def getClients(){
  246.     // set lists
  247.     def isMap = state.plexClients instanceof Map
  248.     if(!isMap){state.plexClients = [:]}
  249.     def isMap2 = state.playingClients instanceof Map
  250.     if(!isMap2){state.playingClients = [:]}
  251.     // Get devices.xml clients
  252.     getClientsXML()
  253.     // Request server:32400/status/sessions clients - Chromecast for example is not in devices.
  254.     getNowPlayingXML()
  255. }
  256.  
  257. def getNowPlayingXML() {    
  258.     def params = [
  259.         uri: "http://${settings.plexServerIP}:32400/status/sessions",
  260.         contentType: 'application/xml',
  261.         headers: [ 'X-Plex-Token': state.authenticationToken ]
  262.     ]
  263.    
  264.     try {    
  265.         httpGet(params) { resp ->
  266.            
  267.             def videos = resp.data.Video
  268.             def whatToCallMe = ""
  269.             def playingDevices = [:]
  270.            
  271.             videos.each { thing ->
  272.                 if(thing.Player.@title.text() != "")        {whatToCallMe = "${thing.Player.@title.text()}-${thing.Player.@product.text()}"}
  273.                 else if(thing.Player.@device.text()!="")    {whatToCallMe = "${thing.Player.@device.text()}-${thing.Player.@product.text()}"}
  274.                 playingDevices << [ (thing.Player.@machineIdentifier.text()): [name: "${whatToCallMe}", id: "${thing.Player.@machineIdentifier.text()}"]]
  275.            
  276.                 if(settings?.stPoller){
  277.                     def plexEvent = [:] << [ id: "${thing.Player.@machineIdentifier.text()}", type: "${thing.@type.text()}", status: "${thing.Player.@state.text()}", user: "${thing.User.@title.text()}", title: "${thing.@title.text()}" ]
  278.                     stillPlaying << "${thing.Player.@machineIdentifier.text()}"
  279.                     eventHandler(plexEvent)
  280.                 }
  281.             }
  282.             if(settings?.stPoller){
  283.                 //stop anything that's no long visible in the playing list but was playing before
  284.                 state.playingClients.each { id, data ->
  285.                     if(!stillPlaying.contains("$id")){
  286.                         def plexEvent2 = [:] << [ id: "${id}", type: "--", status: "stopped", user: "--", title: "--" ]
  287.                         eventHandler(plexEvent2)
  288.                     }
  289.                 }
  290.             }
  291.             state.plexClients << playingDevices
  292.             state.playingClients = playingDevices
  293.         }
  294.     } catch (e) {
  295.         log.error "something went wrong: $e"
  296.     }
  297. }
  298.  
  299. def getClientsXML() {
  300.     //getAuthenticationToken()
  301.     if(logging){ log.debug "Auth Token: $state.authenticationToken" }
  302.     def xmlDevices = [:]
  303.     // Get from Devices List
  304.     def paramsg = [
  305.         uri: "https://plex.tv/devices.xml",
  306.         contentType: 'application/xml',
  307.         headers: [ 'X-Plex-Token': state.authenticationToken ]
  308.     ]
  309.     httpGet(paramsg) { resp ->
  310.         if(logging){ log.debug "Parsing plex.tv/devices.xml" }
  311.         def devices = resp.data.Device
  312.         devices.each { thing ->        
  313.             // If not these things
  314.             if(thing.@name.text()!="Plex Communicator" && !thing.@provides.text().contains("server")){         
  315.                 //Define name based on name unless blank then use device name
  316.                 def whatToCallMe = "Unknown"
  317.                 if(thing.@name.text() != "")        {whatToCallMe = "${thing.@name.text()}-${thing.@product.text()}"}
  318.                 else if(thing.@device.text()!="")   {whatToCallMe = "${thing.@device.text()}-${thing.@product.text()}"}  
  319.                 xmlDevices << [ (thing.@clientIdentifier.text()): [name: "${whatToCallMe}", id: "${thing.@clientIdentifier.text()}"]]
  320.             }
  321.         }  
  322.     }
  323.     //Get from status
  324.     state.plexClients << xmlDevices
  325. }
  326.  
  327. /***********************************************************
  328. ** INPUT HANDLERS
  329. ************************************************************/
  330. def plexExeHandler() {
  331.     def status = params.command
  332.     def userName = params.user
  333.     def playerName = params.player
  334.     def playerIP = params.ipadd
  335.     def mediaType = params.type
  336.     def playerID = params.id
  337.     def title = "none"
  338.     if(logging){
  339.         log.debug "PLAYER_ID:$playerID / STATUS:$status / USERNAME:$userName / PLAYERNAME:$playerName / PLAYERIP:$playerIP / MEDIA_TYPE:$mediaType"
  340.     }
  341.     def plexEvent = [:] << [ id: "$playerID", type: "$mediaType", status: "$status", user: "$userName", title: "$title" ]
  342.     eventHandler(plexEvent)
  343.     return
  344. }
  345. def plexPoller(){
  346.     if(settings?.stPoller){
  347.         getNowPlayingXML()
  348.         if(logging){log.debug "Plex Poller Update Request"}
  349.         runOnce( new Date(now() + 10000L), plexPoller)
  350.     }
  351. }
  352. def plexWebHookHandler(){
  353.     def payloadStart = request.body.indexOf('application/json') + 17    
  354.     def newBody = request.body.substring(payloadStart)
  355.     def jsonSlurper = new JsonSlurper()
  356.     def plexJSON = jsonSlurper.parseText(newBody)
  357.     if(logging){
  358.         //log.debug "Metadata JSON: ${plexJSON.Metadata as String}"    //Only unhide if you want to see media data, cast etc..
  359.         log.debug "Media Type: ${plexJSON.Metadata.type as String}"
  360.         log.debug "Player JSON: ${plexJSON.Player as String}"
  361.         log.debug "Account JSON: ${plexJSON.Account}"
  362.         log.debug "Event JSON: ${plexJSON.event}"
  363.         log.info "## EVENT FROM WEBHOOKS BELOW ##"
  364.     }
  365.     //edition of original code from Jake Tebbett (05/18/21)
  366.     def plexEvent = [:] << [id: plexJSON.Player.uuid,
  367.                             type: plexJSON.Metadata.librarySectionType,
  368.                             status: plexJSON.event,
  369.                             user: plexJSON.Account.title,
  370.                             title: plexJSON.Metadata.title,
  371.                             rating: plexJSON.Metadata.contentRating,
  372.                             seasonNumber: plexJSON.Metadata.parentIndex,
  373.                             episode: plexJSON.Metadata.index,
  374.                             show: plexJSON.Metadata.grandparentTitle,
  375.                             live: plexJSON.Metadata.live
  376.                            ]
  377.     //end of edition
  378.     eventHandler(plexEvent)
  379. }
  380. /***********************************************************
  381. ** DTH OUTPUT
  382. ************************************************************/
  383. def eventHandler(event) {
  384.     def status = event.status as String
  385.     // change command to right format
  386.     switch(status) {
  387.         case ["media.play","media.resume","media.scrobble","onplay","play"]:    status = "playing"; break;
  388.         case ["media.pause","onpause","pause"]:                                 status = "paused";  break;
  389.         case ["media.stop","onstop","stop"]:                                    status = "stopped"; title = " "; break;
  390.     }
  391.     //**************edition of original code from Jake Tebbett (05/18/21)***************** in order to recive the extra atributes
  392.     pcd = getChildDevice(event.id)
  393.     if (!pcd) {
  394.         log.info "child not beeing monitor"
  395.     } else {
  396.         pcd.sendEvent(name: "trackDescription", value: event.title)
  397.         pcd.sendEvent(name: "status", value: status)
  398.         if (event.type){
  399.             pcd.sendEvent(name: "playbackType", value: event.type)
  400.         } else {
  401.             pcd.sendEvent(name: "playbackType", value: "No Type Found" )
  402.         }
  403.         if (event.rating){
  404.             pcd.sendEvent(name: "Rating", value: event.rating)
  405.         } else {
  406.             pcd.sendEvent(name: "Rating", value: "No Rating Found")
  407.         }
  408.         if (event.seasonNumber){
  409.             pcd.sendEvent(name: "Season", value: event.seasonNumber)
  410.         } else {
  411.             pcd.sendEvent(name: "Season", value: "Not a Show")
  412.         }
  413.         if (event.episode){
  414.             pcd.sendEvent(name: "Episode", value: event.episode)
  415.         } else {
  416.             pcd.sendEvent(name: "Episode", value: "Not a Show")
  417.         }
  418.         if (event.show){
  419.             pcd.sendEvent(name: "Show", value: event.show)
  420.         } else {
  421.             pcd.sendEvent(name: "Show", value: "Not a Show")
  422.         }
  423.         if (event.live){
  424.             pcd.sendEvent(name: "Live", value: true)
  425.         } else {
  426.             pcd.sendEvent(name: "Live", value: false)
  427.         }
  428.     }
  429.     ////**************end of edition**************
  430. }
  431. //**************edition of original code from Jake Tebbett (05/20/21)***************** in order to control players from the hub
  432. def sendCommand (XPlexTargetClientIdentifier, command) {
  433.  
  434.     def Params = [
  435.         uri: "http://${settings.plexServerIP}:32400${command}",
  436.         contentType: 'application/xml',
  437.         headers: [
  438.             'X-Plex-Token': state.authenticationToken,
  439.             'X-Plex-Target-Client-Identifier':XPlexTargetClientIdentifier
  440.         ]
  441.     ]
  442.     asynchttpGet("processCallBack",Params)
  443. }    
  444. def processCallBack(response, data) {
  445.     if (logEnable) log.debug "processCallBack"
  446.     if (response.hasError()){
  447.         log.error "got error # ${response.getStatus()} ${response.getErrorMessage()}"
  448.     }
  449.     if (!response.hasError()){
  450.         if(logging){
  451.             log.debug "no error responce data = ${response.getData()} satus =  ${response.status}"
  452.         }
  453.     }
  454. }
  455. ////**************end of edition**************
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement