Add plexbmc.helper
This commit is contained in:
parent
accd8f4ac4
commit
e859a807bc
8 changed files with 1019 additions and 12 deletions
|
@ -1258,7 +1258,6 @@ class TVShows(Items):
|
||||||
kodicursor.execute(query, (title, plot, rating, writer, premieredate,
|
kodicursor.execute(query, (title, plot, rating, writer, premieredate,
|
||||||
runtime, director, season, episode, title, airsBeforeSeason,
|
runtime, director, season, episode, title, airsBeforeSeason,
|
||||||
airsBeforeEpisode, seasonid, episodeid))
|
airsBeforeEpisode, seasonid, episodeid))
|
||||||
self.logMsg("Checkpoint 1", 2)
|
|
||||||
else:
|
else:
|
||||||
query = ' '.join((
|
query = ' '.join((
|
||||||
|
|
||||||
|
@ -1272,10 +1271,8 @@ class TVShows(Items):
|
||||||
airsBeforeEpisode, episodeid))
|
airsBeforeEpisode, episodeid))
|
||||||
|
|
||||||
# Update the checksum in emby table
|
# Update the checksum in emby table
|
||||||
self.logMsg("Checkpoint 2", 2)
|
|
||||||
emby_db.updateReference(itemid, checksum)
|
emby_db.updateReference(itemid, checksum)
|
||||||
# Update parentid reference
|
# Update parentid reference
|
||||||
self.logMsg("Checkpoint 3", 2)
|
|
||||||
emby_db.updateParentId(itemid, seasonid)
|
emby_db.updateParentId(itemid, seasonid)
|
||||||
|
|
||||||
##### OR ADD THE EPISODE #####
|
##### OR ADD THE EPISODE #####
|
||||||
|
@ -1327,7 +1324,6 @@ class TVShows(Items):
|
||||||
"WHERE idPath = ?"
|
"WHERE idPath = ?"
|
||||||
))
|
))
|
||||||
kodicursor.execute(query, (path, None, None, 1, pathid))
|
kodicursor.execute(query, (path, None, None, 1, pathid))
|
||||||
self.logMsg("Checkpoint 4", 2)
|
|
||||||
# Update the file
|
# Update the file
|
||||||
query = ' '.join((
|
query = ' '.join((
|
||||||
|
|
||||||
|
@ -1336,24 +1332,17 @@ class TVShows(Items):
|
||||||
"WHERE idFile = ?"
|
"WHERE idFile = ?"
|
||||||
))
|
))
|
||||||
kodicursor.execute(query, (pathid, filename, dateadded, fileid))
|
kodicursor.execute(query, (pathid, filename, dateadded, fileid))
|
||||||
self.logMsg("Checkpoint 5", 2)
|
|
||||||
# Process cast
|
# Process cast
|
||||||
people = API.getPeopleList()
|
people = API.getPeopleList()
|
||||||
kodi_db.addPeople(episodeid, people, "episode")
|
kodi_db.addPeople(episodeid, people, "episode")
|
||||||
# Process artwork
|
# Process artwork
|
||||||
self.logMsg("Checkpoint 6", 2)
|
|
||||||
artworks = API.getAllArtwork()
|
artworks = API.getAllArtwork()
|
||||||
self.logMsg("Checkpoint 7", 2)
|
|
||||||
artwork.addOrUpdateArt(artworks['Primary'], episodeid, "episode", "thumb", kodicursor)
|
artwork.addOrUpdateArt(artworks['Primary'], episodeid, "episode", "thumb", kodicursor)
|
||||||
self.logMsg("Checkpoint 8", 2)
|
|
||||||
# Process stream details
|
# Process stream details
|
||||||
streams = API.getMediaStreams()
|
streams = API.getMediaStreams()
|
||||||
self.logMsg("Checkpoint 9", 2)
|
|
||||||
kodi_db.addStreams(fileid, streams, runtime)
|
kodi_db.addStreams(fileid, streams, runtime)
|
||||||
self.logMsg("Checkpoint 7", 2)
|
|
||||||
# Process playstates
|
# Process playstates
|
||||||
kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
|
kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
|
||||||
self.logMsg("Checkpoint 8", 2)
|
|
||||||
if not self.directpath and resume:
|
if not self.directpath and resume:
|
||||||
# Create additional entry for widgets. This is only required for plugin/episode.
|
# Create additional entry for widgets. This is only required for plugin/episode.
|
||||||
temppathid = kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/")
|
temppathid = kodi_db.getPath("plugin://plugin.video.plexkodiconnect.tvshows/")
|
||||||
|
@ -1364,7 +1353,6 @@ class TVShows(Items):
|
||||||
"SET idPath = ?, strFilename = ?, dateAdded = ?",
|
"SET idPath = ?, strFilename = ?, dateAdded = ?",
|
||||||
"WHERE idFile = ?"
|
"WHERE idFile = ?"
|
||||||
))
|
))
|
||||||
self.logMsg("Checkpoint 9", 2)
|
|
||||||
kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid))
|
kodicursor.execute(query, (temppathid, filename, dateadded, tempfileid))
|
||||||
kodi_db.addPlaystate(tempfileid, resume, runtime, playcount, dateplayed)
|
kodi_db.addPlaystate(tempfileid, resume, runtime, playcount, dateplayed)
|
||||||
|
|
||||||
|
|
1
resources/lib/plexbmc.helper/__init__.py
Normal file
1
resources/lib/plexbmc.helper/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Dummy file to make this directory a package.
|
195
resources/lib/plexbmc.helper/functions.py
Normal file
195
resources/lib/plexbmc.helper/functions.py
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
import base64
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import string
|
||||||
|
import traceback
|
||||||
|
import xbmc
|
||||||
|
from settings import settings
|
||||||
|
from httppersist import requests
|
||||||
|
|
||||||
|
def xbmc_photo():
|
||||||
|
return "photo"
|
||||||
|
def xbmc_video():
|
||||||
|
return "video"
|
||||||
|
def xbmc_audio():
|
||||||
|
return "audio"
|
||||||
|
|
||||||
|
def plex_photo():
|
||||||
|
return "photo"
|
||||||
|
def plex_video():
|
||||||
|
return "video"
|
||||||
|
def plex_audio():
|
||||||
|
return "music"
|
||||||
|
|
||||||
|
def xbmc_type(plex_type):
|
||||||
|
if plex_type == plex_photo():
|
||||||
|
return xbmc_photo()
|
||||||
|
elif plex_type == plex_video():
|
||||||
|
return xbmc_video()
|
||||||
|
elif plex_type == plex_audio():
|
||||||
|
return xbmc_audio()
|
||||||
|
|
||||||
|
def plex_type(xbmc_type):
|
||||||
|
if xbmc_type == xbmc_photo():
|
||||||
|
return plex_photo()
|
||||||
|
elif xbmc_type == xbmc_video():
|
||||||
|
return plex_video()
|
||||||
|
elif xbmc_type == xbmc_audio():
|
||||||
|
return plex_audio()
|
||||||
|
|
||||||
|
def getPlatform():
|
||||||
|
if xbmc.getCondVisibility('system.platform.osx'):
|
||||||
|
return "MacOSX"
|
||||||
|
elif xbmc.getCondVisibility('system.platform.atv2'):
|
||||||
|
return "AppleTV2"
|
||||||
|
elif xbmc.getCondVisibility('system.platform.ios'):
|
||||||
|
return "iOS"
|
||||||
|
elif xbmc.getCondVisibility('system.platform.windows'):
|
||||||
|
return "Windows"
|
||||||
|
elif xbmc.getCondVisibility('system.platform.raspberrypi'):
|
||||||
|
return "RaspberryPi"
|
||||||
|
elif xbmc.getCondVisibility('system.platform.linux'):
|
||||||
|
return "Linux"
|
||||||
|
elif xbmc.getCondVisibility('system.platform.android'):
|
||||||
|
return "Android"
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
def printDebug( msg, functionname=True ):
|
||||||
|
if settings['debug']:
|
||||||
|
if functionname is False:
|
||||||
|
print str(msg)
|
||||||
|
else:
|
||||||
|
print "PleXBMC Helper -> " + inspect.stack()[1][3] + ": " + str(msg)
|
||||||
|
|
||||||
|
""" communicate with XBMC """
|
||||||
|
def jsonrpc(action, arguments = {}):
|
||||||
|
""" put some JSON together for the JSON-RPC APIv6 """
|
||||||
|
if action.lower() == "sendkey":
|
||||||
|
request=json.dumps({ "jsonrpc" : "2.0" , "method" : "Input.SendText", "params" : { "text" : self.arguments[0], "done" : False }} )
|
||||||
|
elif action.lower() == "ping":
|
||||||
|
request=json.dumps({ "jsonrpc" : "2.0",
|
||||||
|
"id" : 1 ,
|
||||||
|
"method" : "JSONRPC.Ping" })
|
||||||
|
elif action.lower() == "playmedia":
|
||||||
|
fullurl=arguments[0]
|
||||||
|
resume=arguments[1]
|
||||||
|
xbmc.Player().play("plugin://plugin.video.plexbmc/?mode=5&force="+resume+"&url="+fullurl)
|
||||||
|
return True
|
||||||
|
elif arguments:
|
||||||
|
request=json.dumps({ "id" : 1,
|
||||||
|
"jsonrpc" : "2.0",
|
||||||
|
"method" : action,
|
||||||
|
"params" : arguments})
|
||||||
|
else:
|
||||||
|
request=json.dumps({ "id" : 1,
|
||||||
|
"jsonrpc" : "2.0",
|
||||||
|
"method" : action})
|
||||||
|
|
||||||
|
printDebug("Sending request to XBMC without network stack: %s" % request)
|
||||||
|
result = parseJSONRPC(xbmc.executeJSONRPC(request))
|
||||||
|
|
||||||
|
if not result and settings['webserver_enabled']:
|
||||||
|
# xbmc.executeJSONRPC appears to fail on the login screen, but going
|
||||||
|
# through the network stack works, so let's try the request again
|
||||||
|
result = parseJSONRPC(requests.post(
|
||||||
|
"127.0.0.1",
|
||||||
|
settings['port'],
|
||||||
|
"/jsonrpc",
|
||||||
|
request,
|
||||||
|
{ 'Content-Type' : 'application/json',
|
||||||
|
'Authorization' : 'Basic ' + string.strip(base64.encodestring(settings['user'] + ':' + settings['passwd'])) }))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def parseJSONRPC(jsonraw):
|
||||||
|
if not jsonraw:
|
||||||
|
printDebug("Empty response from XBMC")
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
printDebug("Response from XBMC: %s" % jsonraw)
|
||||||
|
parsed=json.loads(jsonraw)
|
||||||
|
if parsed.get('error', False):
|
||||||
|
print "XBMC returned an error: %s" % parsed.get('error')
|
||||||
|
return parsed.get('result', {})
|
||||||
|
|
||||||
|
def getXMLHeader():
|
||||||
|
return '<?xml version="1.0" encoding="utf-8"?>'+"\r\n"
|
||||||
|
|
||||||
|
def getOKMsg():
|
||||||
|
return getXMLHeader() + '<Response code="200" status="OK" />'
|
||||||
|
|
||||||
|
def getPlexHeaders():
|
||||||
|
h = {
|
||||||
|
"Content-type": "application/x-www-form-urlencoded",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"X-Plex-Version": settings['version'],
|
||||||
|
"X-Plex-Client-Identifier": settings['uuid'],
|
||||||
|
"X-Plex-Provides": "player",
|
||||||
|
"X-Plex-Product": "PleXBMC",
|
||||||
|
"X-Plex-Device-Name": settings['client_name'],
|
||||||
|
"X-Plex-Platform": "XBMC",
|
||||||
|
"X-Plex-Model": getPlatform(),
|
||||||
|
"X-Plex-Device": "PC",
|
||||||
|
}
|
||||||
|
if settings['myplex_user']:
|
||||||
|
h["X-Plex-Username"] = settings['myplex_user']
|
||||||
|
return h
|
||||||
|
|
||||||
|
def getServerByHost(host):
|
||||||
|
list = settings['serverList']
|
||||||
|
if len(list) == 1:
|
||||||
|
return list[0]
|
||||||
|
for server in list:
|
||||||
|
if server.get('serverName') in host or server.get('server') in host:
|
||||||
|
return server
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def getPlayers():
|
||||||
|
info = jsonrpc("Player.GetActivePlayers") or []
|
||||||
|
ret = {}
|
||||||
|
for player in info:
|
||||||
|
player['playerid'] = int(player['playerid'])
|
||||||
|
ret[player['type']] = player
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def getPlayerIds():
|
||||||
|
ret = []
|
||||||
|
for player in getPlayers().values():
|
||||||
|
ret.append(player['playerid'])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def getVideoPlayerId(players = False):
|
||||||
|
if players is None:
|
||||||
|
players = getPlayers()
|
||||||
|
return players.get(xbmc_video(), {}).get('playerid', None)
|
||||||
|
|
||||||
|
def getAudioPlayerId(players = False):
|
||||||
|
if players is None:
|
||||||
|
players = getPlayers()
|
||||||
|
return players.get(xbmc_audio(), {}).get('playerid', None)
|
||||||
|
|
||||||
|
def getPhotoPlayerId(players = False):
|
||||||
|
if players is None:
|
||||||
|
players = getPlayers()
|
||||||
|
return players.get(xbmc_photo(), {}).get('playerid', None)
|
||||||
|
|
||||||
|
def getVolume():
|
||||||
|
return str(jsonrpc('Application.GetProperties', { "properties": [ "volume" ] }).get('volume', 100))
|
||||||
|
|
||||||
|
def timeToMillis(time):
|
||||||
|
return (time['hours']*3600 + time['minutes']*60 + time['seconds'])*1000 + time['milliseconds']
|
||||||
|
|
||||||
|
def millisToTime(t):
|
||||||
|
millis = int(t)
|
||||||
|
seconds = millis / 1000
|
||||||
|
minutes = seconds / 60
|
||||||
|
hours = minutes / 60
|
||||||
|
seconds = seconds % 60
|
||||||
|
minutes = minutes % 60
|
||||||
|
millis = millis % 1000
|
||||||
|
return {'hours':hours,'minutes':minutes,'seconds':seconds,'milliseconds':millis}
|
||||||
|
|
||||||
|
def textFromXml(element):
|
||||||
|
return element.firstChild.data
|
76
resources/lib/plexbmc.helper/httppersist.py
Normal file
76
resources/lib/plexbmc.helper/httppersist.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import httplib
|
||||||
|
import traceback
|
||||||
|
import string
|
||||||
|
|
||||||
|
class RequestMgr:
|
||||||
|
def __init__(self):
|
||||||
|
self.conns = {}
|
||||||
|
|
||||||
|
def getConnection(self, protocol, host, port):
|
||||||
|
conn = self.conns.get(protocol+host+str(port), False)
|
||||||
|
if not conn:
|
||||||
|
if protocol=="https":
|
||||||
|
conn = httplib.HTTPSConnection(host, port)
|
||||||
|
else:
|
||||||
|
conn = httplib.HTTPConnection(host, port)
|
||||||
|
self.conns[protocol+host+str(port)] = conn
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def closeConnection(self, protocol, host, port):
|
||||||
|
conn = self.conns.get(protocol+host+str(port), False)
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
self.conns.pop(protocol+host+str(port), None)
|
||||||
|
|
||||||
|
def dumpConnections(self):
|
||||||
|
for conn in self.conns.values():
|
||||||
|
conn.close()
|
||||||
|
self.conns = {}
|
||||||
|
|
||||||
|
def post(self, host, port, path, body, header={}, protocol="http"):
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = self.getConnection(protocol, host, port)
|
||||||
|
header['Connection'] = "keep-alive"
|
||||||
|
conn.request("POST", path, body, header)
|
||||||
|
data = conn.getresponse()
|
||||||
|
if int(data.status) >= 400:
|
||||||
|
print "HTTP response error: " + str(data.status)
|
||||||
|
# this should return false, but I'm hacking it since iOS returns 404 no matter what
|
||||||
|
return data.read() or True
|
||||||
|
else:
|
||||||
|
return data.read() or True
|
||||||
|
except:
|
||||||
|
print "Unable to connect to %s\nReason:" % host
|
||||||
|
traceback.print_exc()
|
||||||
|
self.conns.pop(protocol+host+str(port), None)
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getwithparams(self, host, port, path, params, header={}, protocol="http"):
|
||||||
|
newpath = path + '?'
|
||||||
|
pairs = []
|
||||||
|
for key in params:
|
||||||
|
pairs.append(str(key)+'='+str(params[key]))
|
||||||
|
newpath += string.join(pairs, '&')
|
||||||
|
return self.get(host, port, newpath, header, protocol)
|
||||||
|
|
||||||
|
def get(self, host, port, path, header={}, protocol="http"):
|
||||||
|
try:
|
||||||
|
conn = self.getConnection(protocol, host, port)
|
||||||
|
header['Connection'] = "keep-alive"
|
||||||
|
conn.request("GET", path, headers=header)
|
||||||
|
data = conn.getresponse()
|
||||||
|
if int(data.status) >= 400:
|
||||||
|
print "HTTP response error: " + str(data.status)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return data.read() or True
|
||||||
|
except:
|
||||||
|
print "Unable to connect to %s\nReason: %s" % (host, traceback.print_exc())
|
||||||
|
self.conns.pop(protocol+host+str(port), None)
|
||||||
|
conn.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
requests = RequestMgr()
|
190
resources/lib/plexbmc.helper/listener.py
Normal file
190
resources/lib/plexbmc.helper/listener.py
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
import xbmc
|
||||||
|
from SocketServer import ThreadingMixIn
|
||||||
|
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
from urlparse import urlparse, parse_qs
|
||||||
|
from settings import settings
|
||||||
|
from functions import *
|
||||||
|
from subscribers import subMgr
|
||||||
|
|
||||||
|
class MyHandler(BaseHTTPRequestHandler):
|
||||||
|
protocol_version = 'HTTP/1.1'
|
||||||
|
def log_message(s, format, *args):
|
||||||
|
# I have my own logging, suppressing BaseHTTPRequestHandler's
|
||||||
|
#printDebug(format % args)
|
||||||
|
return True
|
||||||
|
def do_HEAD(s):
|
||||||
|
printDebug( "Serving HEAD request..." )
|
||||||
|
s.answer_request(0)
|
||||||
|
|
||||||
|
def do_GET(s):
|
||||||
|
printDebug( "Serving GET request..." )
|
||||||
|
s.answer_request(1)
|
||||||
|
|
||||||
|
def do_OPTIONS(s):
|
||||||
|
s.send_response(200)
|
||||||
|
s.send_header('Content-Length', '0')
|
||||||
|
s.send_header('X-Plex-Client-Identifier', settings['uuid'])
|
||||||
|
s.send_header('Content-Type', 'text/plain')
|
||||||
|
s.send_header('Connection', 'close')
|
||||||
|
s.send_header('Access-Control-Max-Age', '1209600')
|
||||||
|
s.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
s.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, DELETE, PUT, HEAD')
|
||||||
|
s.send_header('Access-Control-Allow-Headers', 'x-plex-version, x-plex-platform-version, x-plex-username, x-plex-client-identifier, x-plex-target-client-identifier, x-plex-device-name, x-plex-platform, x-plex-product, accept, x-plex-device')
|
||||||
|
s.end_headers()
|
||||||
|
s.wfile.close()
|
||||||
|
|
||||||
|
def response(s, body, headers = {}, code = 200):
|
||||||
|
try:
|
||||||
|
s.send_response(code)
|
||||||
|
for key in headers:
|
||||||
|
s.send_header(key, headers[key])
|
||||||
|
s.send_header('Content-Length', len(body))
|
||||||
|
s.send_header('Connection', "close")
|
||||||
|
s.end_headers()
|
||||||
|
s.wfile.write(body)
|
||||||
|
s.wfile.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def answer_request(s, sendData):
|
||||||
|
try:
|
||||||
|
request_path=s.path[1:]
|
||||||
|
request_path=re.sub(r"\?.*","",request_path)
|
||||||
|
url = urlparse(s.path)
|
||||||
|
paramarrays = parse_qs(url.query)
|
||||||
|
params = {}
|
||||||
|
for key in paramarrays:
|
||||||
|
params[key] = paramarrays[key][0]
|
||||||
|
printDebug ( "request path is: [%s]" % ( request_path,) )
|
||||||
|
printDebug ( "params are: %s" % params )
|
||||||
|
subMgr.updateCommandID(s.headers.get('X-Plex-Client-Identifier', s.client_address[0]), params.get('commandID', False))
|
||||||
|
if request_path=="version":
|
||||||
|
s.response("PleXBMC Helper Remote Redirector: Running\r\nVersion: %s" % settings['version'])
|
||||||
|
elif request_path=="verify":
|
||||||
|
result=jsonrpc("ping")
|
||||||
|
s.response("XBMC JSON connection test:\r\n"+result)
|
||||||
|
elif "resources" == request_path:
|
||||||
|
resp = getXMLHeader()
|
||||||
|
resp += "<MediaContainer>"
|
||||||
|
resp += "<Player"
|
||||||
|
resp += ' title="%s"' % settings['client_name']
|
||||||
|
resp += ' protocol="plex"'
|
||||||
|
resp += ' protocolVersion="1"'
|
||||||
|
resp += ' protocolCapabilities="navigation,playback,timeline"'
|
||||||
|
resp += ' machineIdentifier="%s"' % settings['uuid']
|
||||||
|
resp += ' product="PleXBMC"'
|
||||||
|
resp += ' platform="%s"' % getPlatform()
|
||||||
|
resp += ' platformVersion="%s"' % settings['plexbmc_version']
|
||||||
|
resp += ' deviceClass="pc"'
|
||||||
|
resp += "/>"
|
||||||
|
resp += "</MediaContainer>"
|
||||||
|
printDebug("crafted resources response: %s" % resp)
|
||||||
|
s.response(resp, getPlexHeaders())
|
||||||
|
elif "/subscribe" in request_path:
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
protocol = params.get('protocol', False)
|
||||||
|
host = s.client_address[0]
|
||||||
|
port = params.get('port', False)
|
||||||
|
uuid = s.headers.get('X-Plex-Client-Identifier', "")
|
||||||
|
commandID = params.get('commandID', 0)
|
||||||
|
subMgr.addSubscriber(protocol, host, port, uuid, commandID)
|
||||||
|
elif "/poll" in request_path:
|
||||||
|
if params.get('wait', False) == '1':
|
||||||
|
xbmc.sleep(950)
|
||||||
|
commandID = params.get('commandID', 0)
|
||||||
|
s.response(re.sub(r"INSERTCOMMANDID", str(commandID), subMgr.msg(getPlayers())), {
|
||||||
|
'X-Plex-Client-Identifier': settings['uuid'],
|
||||||
|
'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Type': 'text/xml'
|
||||||
|
})
|
||||||
|
elif "/unsubscribe" in request_path:
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
uuid = s.headers.get('X-Plex-Client-Identifier', False) or s.client_address[0]
|
||||||
|
subMgr.removeSubscriber(uuid)
|
||||||
|
elif request_path == "player/playback/setParameters":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
if 'volume' in params:
|
||||||
|
volume = int(params['volume'])
|
||||||
|
printDebug("adjusting the volume to %s%%" % volume)
|
||||||
|
jsonrpc("Application.SetVolume", {"volume": volume})
|
||||||
|
elif "/playMedia" in request_path:
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
resume = params.get('viewOffset', params.get('offset', "0"))
|
||||||
|
protocol = params.get('protocol', "http")
|
||||||
|
address = params.get('address', s.client_address[0])
|
||||||
|
server = getServerByHost(address)
|
||||||
|
port = params.get('port', server.get('port', '32400'))
|
||||||
|
fullurl = protocol+"://"+address+":"+port+params['key']
|
||||||
|
printDebug("playMedia command -> fullurl: %s" % fullurl)
|
||||||
|
jsonrpc("playmedia", [fullurl, resume])
|
||||||
|
subMgr.lastkey = params['key']
|
||||||
|
subMgr.server = server.get('server', 'localhost')
|
||||||
|
subMgr.port = port
|
||||||
|
subMgr.protocol = protocol
|
||||||
|
subMgr.notify()
|
||||||
|
elif request_path == "player/playback/play":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
for playerid in getPlayerIds():
|
||||||
|
jsonrpc("Player.PlayPause", {"playerid" : playerid, "play": True})
|
||||||
|
elif request_path == "player/playback/pause":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
for playerid in getPlayerIds():
|
||||||
|
jsonrpc("Player.PlayPause", {"playerid" : playerid, "play": False})
|
||||||
|
elif request_path == "player/playback/stop":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
for playerid in getPlayerIds():
|
||||||
|
jsonrpc("Player.Stop", {"playerid" : playerid})
|
||||||
|
elif request_path == "player/playback/seekTo":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
for playerid in getPlayerIds():
|
||||||
|
jsonrpc("Player.Seek", {"playerid":playerid, "value":millisToTime(params.get('offset', 0))})
|
||||||
|
subMgr.notify()
|
||||||
|
elif request_path == "player/playback/stepForward":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
for playerid in getPlayerIds():
|
||||||
|
jsonrpc("Player.Seek", {"playerid":playerid, "value":"smallforward"})
|
||||||
|
subMgr.notify()
|
||||||
|
elif request_path == "player/playback/stepBack":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
for playerid in getPlayerIds():
|
||||||
|
jsonrpc("Player.Seek", {"playerid":playerid, "value":"smallbackward"})
|
||||||
|
subMgr.notify()
|
||||||
|
elif request_path == "player/playback/skipNext":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
for playerid in getPlayerIds():
|
||||||
|
jsonrpc("Player.Seek", {"playerid":playerid, "value":"bigforward"})
|
||||||
|
subMgr.notify()
|
||||||
|
elif request_path == "player/playback/skipPrevious":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
for playerid in getPlayerIds():
|
||||||
|
jsonrpc("Player.Seek", {"playerid":playerid, "value":"bigbackward"})
|
||||||
|
subMgr.notify()
|
||||||
|
elif request_path == "player/navigation/moveUp":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
jsonrpc("Input.Up")
|
||||||
|
elif request_path == "player/navigation/moveDown":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
jsonrpc("Input.Down")
|
||||||
|
elif request_path == "player/navigation/moveLeft":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
jsonrpc("Input.Left")
|
||||||
|
elif request_path == "player/navigation/moveRight":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
jsonrpc("Input.Right")
|
||||||
|
elif request_path == "player/navigation/select":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
jsonrpc("Input.Select")
|
||||||
|
elif request_path == "player/navigation/home":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
jsonrpc("Input.Home")
|
||||||
|
elif request_path == "player/navigation/back":
|
||||||
|
s.response(getOKMsg(), getPlexHeaders())
|
||||||
|
jsonrpc("Input.Back")
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
||||||
|
daemon_threads = True
|
318
resources/lib/plexbmc.helper/plexgdm.py
Normal file
318
resources/lib/plexbmc.helper/plexgdm.py
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
"""
|
||||||
|
PlexGDM.py - Version 0.2
|
||||||
|
|
||||||
|
This class implements the Plex GDM (G'Day Mate) protocol to discover
|
||||||
|
local Plex Media Servers. Also allow client registration into all local
|
||||||
|
media servers.
|
||||||
|
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
MA 02110-1301, USA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = 'DHJ (hippojay) <plex@h-jay.com>'
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
class plexgdm:
|
||||||
|
|
||||||
|
def __init__(self, debug=False):
|
||||||
|
|
||||||
|
self.discover_message = 'M-SEARCH * HTTP/1.0'
|
||||||
|
self.client_header = '* HTTP/1.0'
|
||||||
|
self.client_data = None
|
||||||
|
self.client_id = None
|
||||||
|
|
||||||
|
self._multicast_address = '239.0.0.250'
|
||||||
|
self.discover_group = (self._multicast_address, 32414)
|
||||||
|
self.client_register_group = (self._multicast_address, 32413)
|
||||||
|
self.client_update_port = 32412
|
||||||
|
|
||||||
|
self.server_list = []
|
||||||
|
self.discovery_interval = 120
|
||||||
|
|
||||||
|
self._discovery_is_running = False
|
||||||
|
self._registration_is_running = False
|
||||||
|
|
||||||
|
self.discovery_complete = False
|
||||||
|
self.client_registered = False
|
||||||
|
self.debug = debug
|
||||||
|
|
||||||
|
def __printDebug(self, message, level=1):
|
||||||
|
if self.debug:
|
||||||
|
print "PlexGDM: %s" % message
|
||||||
|
|
||||||
|
def clientDetails(self, c_id, c_name, c_post, c_product, c_version):
|
||||||
|
self.client_data = "Content-Type: plex/media-player\r\nResource-Identifier: %s\r\nName: %s\r\nPort: %s\r\nProduct: %s\r\nVersion: %s\r\nProtocol: plex\r\nProtocol-Version: 1\r\nProtocol-Capabilities: navigation,playback,timeline\r\nDevice-Class: HTPC" % ( c_id, c_name, c_post, c_product, c_version )
|
||||||
|
self.client_id = c_id
|
||||||
|
|
||||||
|
def getClientDetails(self):
|
||||||
|
if not self.client_data:
|
||||||
|
self.__printDebug("Client data has not been initialised. Please use PlexGDM.clientDetails()")
|
||||||
|
|
||||||
|
return self.client_data
|
||||||
|
|
||||||
|
def client_update (self):
|
||||||
|
update_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||||
|
|
||||||
|
#Set socket reuse, may not work on all OSs.
|
||||||
|
try:
|
||||||
|
update_sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
#Attempt to bind to the socket to recieve and send data. If we can;t do this, then we cannot send registration
|
||||||
|
try:
|
||||||
|
update_sock.bind(('0.0.0.0',self.client_update_port))
|
||||||
|
except:
|
||||||
|
self.__printDebug( "Error: Unable to bind to port [%s] - client will not be registered" % self.client_update_port, 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
update_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
|
||||||
|
status = update_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(self._multicast_address) + socket.inet_aton('0.0.0.0'))
|
||||||
|
update_sock.setblocking(0)
|
||||||
|
self.__printDebug("Sending registration data: HELLO %s\r\n%s" % (self.client_header, self.client_data), 3)
|
||||||
|
|
||||||
|
#Send initial client registration
|
||||||
|
try:
|
||||||
|
update_sock.sendto("HELLO %s\r\n%s" % (self.client_header, self.client_data), self.client_register_group)
|
||||||
|
except:
|
||||||
|
self.__printDebug( "Error: Unable to send registeration message" , 0)
|
||||||
|
|
||||||
|
#Now, listen for client discovery reguests and respond.
|
||||||
|
while self._registration_is_running:
|
||||||
|
try:
|
||||||
|
data, addr = update_sock.recvfrom(1024)
|
||||||
|
self.__printDebug("Recieved UDP packet from [%s] containing [%s]" % (addr, data.strip()), 3)
|
||||||
|
except socket.error, e:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if "M-SEARCH * HTTP/1." in data:
|
||||||
|
self.__printDebug("Detected client discovery request from %s. Replying" % ( addr ,) , 2)
|
||||||
|
try:
|
||||||
|
update_sock.sendto("HTTP/1.0 200 OK\r\n%s" % self.client_data, addr)
|
||||||
|
except:
|
||||||
|
self.__printDebug( "Error: Unable to send client update message",0)
|
||||||
|
|
||||||
|
self.__printDebug("Sending registration data: HTTP/1.0 200 OK\r\n%s" % (self.client_data), 3)
|
||||||
|
self.client_registered = True
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
self.__printDebug("Client Update loop stopped",1)
|
||||||
|
|
||||||
|
#When we are finished, then send a final goodbye message to deregister cleanly.
|
||||||
|
self.__printDebug("Sending registration data: BYE %s\r\n%s" % (self.client_header, self.client_data), 3)
|
||||||
|
try:
|
||||||
|
update_sock.sendto("BYE %s\r\n%s" % (self.client_header, self.client_data), self.client_register_group)
|
||||||
|
except:
|
||||||
|
self.__printDebug( "Error: Unable to send client update message" ,0)
|
||||||
|
|
||||||
|
self.client_registered = False
|
||||||
|
|
||||||
|
def check_client_registration(self):
|
||||||
|
|
||||||
|
if self.client_registered and self.discovery_complete:
|
||||||
|
|
||||||
|
if not self.server_list:
|
||||||
|
self.__printDebug("Server list is empty. Unable to check",2)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
media_server=self.server_list[0]['server']
|
||||||
|
media_port=self.server_list[0]['port']
|
||||||
|
|
||||||
|
self.__printDebug("Checking server [%s] on port [%s]" % (media_server, media_port) ,2)
|
||||||
|
f = urllib2.urlopen('http://%s:%s/clients' % (media_server, media_port))
|
||||||
|
client_result = f.read()
|
||||||
|
if self.client_id in client_result:
|
||||||
|
self.__printDebug("Client registration successful",1)
|
||||||
|
self.__printDebug("Client data is: %s" % client_result, 3)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.__printDebug("Client registration not found",1)
|
||||||
|
self.__printDebug("Client data is: %s" % client_result, 3)
|
||||||
|
|
||||||
|
except:
|
||||||
|
self.__printDebug("Unable to check status")
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getServerList (self):
|
||||||
|
return self.server_list
|
||||||
|
|
||||||
|
def discover(self):
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|
||||||
|
# Set a timeout so the socket does not block indefinitely
|
||||||
|
sock.settimeout(0.6)
|
||||||
|
|
||||||
|
# Set the time-to-live for messages to 1 for local network
|
||||||
|
ttl = struct.pack('b', 1)
|
||||||
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
|
||||||
|
|
||||||
|
returnData = []
|
||||||
|
try:
|
||||||
|
# Send data to the multicast group
|
||||||
|
self.__printDebug("Sending discovery messages: %s" % self.discover_message, 2)
|
||||||
|
sent = sock.sendto(self.discover_message, self.discover_group)
|
||||||
|
|
||||||
|
# Look for responses from all recipients
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data, server = sock.recvfrom(1024)
|
||||||
|
self.__printDebug("Received data from %s, %s" % server, 3)
|
||||||
|
self.__printDebug("Data received is:\r\n %s" % data, 3)
|
||||||
|
returnData.append( { 'from' : server,
|
||||||
|
'data' : data } )
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
# if we can't send our discovery query, just abort and try again
|
||||||
|
# on the next loop
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
self.discovery_complete = True
|
||||||
|
|
||||||
|
discovered_servers = []
|
||||||
|
|
||||||
|
if returnData:
|
||||||
|
|
||||||
|
for response in returnData:
|
||||||
|
update = { 'server' : response.get('from')[0] }
|
||||||
|
|
||||||
|
#Check if we had a positive HTTP response
|
||||||
|
if "200 OK" in response.get('data'):
|
||||||
|
|
||||||
|
for each in response.get('data').split('\r\n'):
|
||||||
|
|
||||||
|
update['discovery'] = "auto"
|
||||||
|
update['owned']='1'
|
||||||
|
update['master']= 1
|
||||||
|
update['role']='master'
|
||||||
|
update['class']=None
|
||||||
|
|
||||||
|
if "Content-Type:" in each:
|
||||||
|
update['content-type'] = each.split(':')[1].strip()
|
||||||
|
elif "Resource-Identifier:" in each:
|
||||||
|
update['uuid'] = each.split(':')[1].strip()
|
||||||
|
elif "Name:" in each:
|
||||||
|
update['serverName'] = each.split(':')[1].strip()
|
||||||
|
elif "Port:" in each:
|
||||||
|
update['port'] = each.split(':')[1].strip()
|
||||||
|
elif "Updated-At:" in each:
|
||||||
|
update['updated'] = each.split(':')[1].strip()
|
||||||
|
elif "Version:" in each:
|
||||||
|
update['version'] = each.split(':')[1].strip()
|
||||||
|
elif "Server-Class:" in each:
|
||||||
|
update['class'] = each.split(':')[1].strip()
|
||||||
|
|
||||||
|
discovered_servers.append(update)
|
||||||
|
|
||||||
|
self.server_list = discovered_servers
|
||||||
|
|
||||||
|
if not self.server_list:
|
||||||
|
self.__printDebug("No servers have been discovered",1)
|
||||||
|
else:
|
||||||
|
self.__printDebug("Number of servers Discovered: %s" % len(self.server_list),1)
|
||||||
|
for items in self.server_list:
|
||||||
|
self.__printDebug("Server Discovered: %s" % items['serverName'] ,2)
|
||||||
|
|
||||||
|
|
||||||
|
def setInterval(self, interval):
|
||||||
|
self.discovery_interval = interval
|
||||||
|
|
||||||
|
def stop_all(self):
|
||||||
|
self.stop_discovery()
|
||||||
|
self.stop_registration()
|
||||||
|
|
||||||
|
def stop_discovery(self):
|
||||||
|
if self._discovery_is_running:
|
||||||
|
self.__printDebug("Discovery shutting down", 1)
|
||||||
|
self._discovery_is_running = False
|
||||||
|
self.discover_t.join()
|
||||||
|
del self.discover_t
|
||||||
|
else:
|
||||||
|
self.__printDebug("Discovery not running", 1)
|
||||||
|
|
||||||
|
def stop_registration(self):
|
||||||
|
if self._registration_is_running:
|
||||||
|
self.__printDebug("Registration shutting down", 1)
|
||||||
|
self._registration_is_running = False
|
||||||
|
self.register_t.join()
|
||||||
|
del self.register_t
|
||||||
|
else:
|
||||||
|
self.__printDebug("Registration not running", 1)
|
||||||
|
|
||||||
|
def run_discovery_loop(self):
|
||||||
|
#Run initial discovery
|
||||||
|
self.discover()
|
||||||
|
|
||||||
|
discovery_count=0
|
||||||
|
while self._discovery_is_running:
|
||||||
|
discovery_count+=1
|
||||||
|
if discovery_count > self.discovery_interval:
|
||||||
|
self.discover()
|
||||||
|
discovery_count=0
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def start_discovery(self, daemon = False):
|
||||||
|
if not self._discovery_is_running:
|
||||||
|
self.__printDebug("Discovery starting up", 1)
|
||||||
|
self._discovery_is_running = True
|
||||||
|
self.discover_t = threading.Thread(target=self.run_discovery_loop)
|
||||||
|
self.discover_t.setDaemon(daemon)
|
||||||
|
self.discover_t.start()
|
||||||
|
else:
|
||||||
|
self.__printDebug("Discovery already running", 1)
|
||||||
|
|
||||||
|
def start_registration(self, daemon = False):
|
||||||
|
if not self._registration_is_running:
|
||||||
|
self.__printDebug("Registration starting up", 1)
|
||||||
|
self._registration_is_running = True
|
||||||
|
self.register_t = threading.Thread(target=self.client_update)
|
||||||
|
self.register_t.setDaemon(daemon)
|
||||||
|
self.register_t.start()
|
||||||
|
else:
|
||||||
|
self.__printDebug("Registration already running", 1)
|
||||||
|
|
||||||
|
def start_all(self, daemon = False):
|
||||||
|
self.start_discovery(daemon)
|
||||||
|
self.start_registration(daemon)
|
||||||
|
|
||||||
|
|
||||||
|
#Example usage
|
||||||
|
if __name__ == '__main__':
|
||||||
|
client = plexgdm(debug=3)
|
||||||
|
client.clientDetails("Test-Name", "Test Client", "3003", "Test-App", "1.2.3")
|
||||||
|
client.start_all()
|
||||||
|
while not client.discovery_complete:
|
||||||
|
print "Waiting for results"
|
||||||
|
time.sleep(1)
|
||||||
|
time.sleep(20)
|
||||||
|
print client.getServerList()
|
||||||
|
if client.check_client_registration():
|
||||||
|
print "Successfully registered"
|
||||||
|
else:
|
||||||
|
print "Unsuccessfully registered"
|
||||||
|
client.stop_all()
|
42
resources/lib/plexbmc.helper/settings.py
Normal file
42
resources/lib/plexbmc.helper/settings.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import uuid
|
||||||
|
import xbmc
|
||||||
|
import xbmcaddon
|
||||||
|
from xml.dom.minidom import parse
|
||||||
|
|
||||||
|
settings = {}
|
||||||
|
try:
|
||||||
|
guidoc = parse(xbmc.translatePath('special://userdata/guisettings.xml'))
|
||||||
|
except:
|
||||||
|
print "Unable to read XBMC's guisettings.xml"
|
||||||
|
|
||||||
|
def getGUI(name):
|
||||||
|
global guidoc
|
||||||
|
if guidoc is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return guidoc.getElementsByTagName(name)[0].firstChild.nodeValue
|
||||||
|
except:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
addon = xbmcaddon.Addon()
|
||||||
|
plexbmc = xbmcaddon.Addon('plugin.video.plexbmc')
|
||||||
|
|
||||||
|
settings['debug'] = addon.getSetting('debug') == "true"
|
||||||
|
settings['gdm_debug'] = addon.getSetting('gdm_debug') == "true"
|
||||||
|
if addon.getSetting('use_xbmc_name') == "true":
|
||||||
|
settings['client_name'] = getGUI('devicename')
|
||||||
|
else:
|
||||||
|
settings['client_name'] = addon.getSetting('c_name')
|
||||||
|
# XBMC web server settings
|
||||||
|
settings['webserver_enabled'] = (getGUI('webserver') == "true")
|
||||||
|
settings['port'] = int(getGUI('webserverport'))
|
||||||
|
settings['user'] = getGUI('webserverusername')
|
||||||
|
settings['passwd'] = getGUI('webserverpassword')
|
||||||
|
|
||||||
|
settings['uuid'] = str(addon.getSetting('uuid')) or str(uuid.uuid4())
|
||||||
|
addon.setSetting('uuid', settings['uuid'])
|
||||||
|
settings['version'] = addon.getAddonInfo('version')
|
||||||
|
settings['plexbmc_version'] = plexbmc.getAddonInfo('version')
|
||||||
|
settings['myplex_user'] = plexbmc.getSetting('myplex_user')
|
||||||
|
settings['serverList'] = []
|
||||||
|
settings['myport'] = 3005
|
197
resources/lib/plexbmc.helper/subscribers.py
Normal file
197
resources/lib/plexbmc.helper/subscribers.py
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import xbmcgui
|
||||||
|
from xml.dom.minidom import parseString
|
||||||
|
from functions import *
|
||||||
|
from settings import settings
|
||||||
|
from httppersist import requests
|
||||||
|
|
||||||
|
class SubscriptionManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.subscribers = {}
|
||||||
|
self.info = {}
|
||||||
|
self.lastkey = ""
|
||||||
|
self.lastratingkey = ""
|
||||||
|
self.volume = 0
|
||||||
|
self.guid = ""
|
||||||
|
self.server = ""
|
||||||
|
self.protocol = "http"
|
||||||
|
self.port = ""
|
||||||
|
self.playerprops = {}
|
||||||
|
self.sentstopped = True
|
||||||
|
|
||||||
|
def getVolume(self):
|
||||||
|
self.volume = getVolume()
|
||||||
|
|
||||||
|
def msg(self, players):
|
||||||
|
msg = getXMLHeader()
|
||||||
|
msg += '<MediaContainer commandID="INSERTCOMMANDID"'
|
||||||
|
if players:
|
||||||
|
self.getVolume()
|
||||||
|
maintype = plex_audio()
|
||||||
|
for p in players.values():
|
||||||
|
if p.get('type') == xbmc_video():
|
||||||
|
maintype = plex_video()
|
||||||
|
elif p.get('type') == xbmc_photo():
|
||||||
|
maintype = plex_photo()
|
||||||
|
self.mainlocation = "fullScreen" + maintype[0:1].upper() + maintype[1:].lower()
|
||||||
|
else:
|
||||||
|
self.mainlocation = "navigation"
|
||||||
|
msg += ' location="%s">' % self.mainlocation
|
||||||
|
|
||||||
|
msg += self.getTimelineXML(getAudioPlayerId(players), plex_audio())
|
||||||
|
msg += self.getTimelineXML(getPhotoPlayerId(players), plex_photo())
|
||||||
|
msg += self.getTimelineXML(getVideoPlayerId(players), plex_video())
|
||||||
|
msg += "\r\n</MediaContainer>"
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def getTimelineXML(self, playerid, ptype):
|
||||||
|
if playerid is not None:
|
||||||
|
info = self.getPlayerProperties(playerid)
|
||||||
|
# save this info off so the server update can use it too
|
||||||
|
self.playerprops[playerid] = info;
|
||||||
|
state = info['state']
|
||||||
|
time = info['time']
|
||||||
|
else:
|
||||||
|
state = "stopped"
|
||||||
|
time = 0
|
||||||
|
ret = "\r\n"+'<Timeline location="%s" state="%s" time="%s" type="%s"' % (self.mainlocation, state, time, ptype)
|
||||||
|
if playerid is not None:
|
||||||
|
WINDOW = xbmcgui.Window(10000)
|
||||||
|
pbmc_server = str(WINDOW.getProperty('plexbmc.nowplaying.server'))
|
||||||
|
keyid = str(WINDOW.getProperty('plexbmc.nowplaying.id'))
|
||||||
|
if keyid:
|
||||||
|
self.lastkey = "/library/metadata/%s"%keyid
|
||||||
|
self.lastratingkey = keyid
|
||||||
|
if pbmc_server:
|
||||||
|
(self.server, self.port) = pbmc_server.split(':')
|
||||||
|
serv = getServerByHost(self.server)
|
||||||
|
ret += ' duration="%s"' % info['duration']
|
||||||
|
ret += ' seekRange="0-%s"' % info['duration']
|
||||||
|
ret += ' controllable="%s"' % self.controllable()
|
||||||
|
ret += ' machineIdentifier="%s"' % serv.get('uuid', "")
|
||||||
|
ret += ' protocol="%s"' % serv.get('protocol', "http")
|
||||||
|
ret += ' address="%s"' % serv.get('server', self.server)
|
||||||
|
ret += ' port="%s"' % serv.get('port', self.port)
|
||||||
|
ret += ' guid="%s"' % info['guid']
|
||||||
|
ret += ' containerKey="%s"' % (self.lastkey or "/library/metadata/900000")
|
||||||
|
ret += ' key="%s"' % (self.lastkey or "/library/metadata/900000")
|
||||||
|
ret += ' ratingKey="%s"' % (self.lastratingkey or "900000")
|
||||||
|
ret += ' volume="%s"' % info['volume']
|
||||||
|
ret += ' shuffle="%s"' % info['shuffle']
|
||||||
|
|
||||||
|
ret += '/>'
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def updateCommandID(self, uuid, commandID):
|
||||||
|
if commandID and self.subscribers.get(uuid, False):
|
||||||
|
self.subscribers[uuid].commandID = int(commandID)
|
||||||
|
|
||||||
|
def notify(self, event = False):
|
||||||
|
self.cleanup()
|
||||||
|
players = getPlayers()
|
||||||
|
# fetch the message, subscribers or not, since the server
|
||||||
|
# will need the info anyway
|
||||||
|
msg = self.msg(players)
|
||||||
|
if self.subscribers:
|
||||||
|
with threading.RLock():
|
||||||
|
for sub in self.subscribers.values():
|
||||||
|
sub.send_update(msg, len(players)==0)
|
||||||
|
self.notifyServer(players)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def notifyServer(self, players):
|
||||||
|
if not players and self.sentstopped: return True
|
||||||
|
params = {'state': 'stopped'}
|
||||||
|
for p in players.values():
|
||||||
|
info = self.playerprops[p.get('playerid')]
|
||||||
|
params = {}
|
||||||
|
params['containerKey'] = (self.lastkey or "/library/metadata/900000")
|
||||||
|
params['key'] = (self.lastkey or "/library/metadata/900000")
|
||||||
|
params['ratingKey'] = (self.lastratingkey or "900000")
|
||||||
|
params['state'] = info['state']
|
||||||
|
params['time'] = info['time']
|
||||||
|
params['duration'] = info['duration']
|
||||||
|
serv = getServerByHost(self.server)
|
||||||
|
requests.getwithparams(serv.get('server', 'localhost'), serv.get('port', 32400), "/:/timeline", params, getPlexHeaders(), serv.get('protocol', 'http'))
|
||||||
|
printDebug("sent server notification with state = %s" % params['state'])
|
||||||
|
WINDOW = xbmcgui.Window(10000)
|
||||||
|
WINDOW.setProperty('plexbmc.nowplaying.sent', '1')
|
||||||
|
if players:
|
||||||
|
self.sentstopped = False
|
||||||
|
else:
|
||||||
|
self.sentstopped = True
|
||||||
|
|
||||||
|
def controllable(self):
|
||||||
|
return "playPause,play,stop,skipPrevious,skipNext,volume,stepBack,stepForward,seekTo"
|
||||||
|
|
||||||
|
def addSubscriber(self, protocol, host, port, uuid, commandID):
|
||||||
|
sub = Subscriber(protocol, host, port, uuid, commandID)
|
||||||
|
with threading.RLock():
|
||||||
|
self.subscribers[sub.uuid] = sub
|
||||||
|
return sub
|
||||||
|
|
||||||
|
def removeSubscriber(self, uuid):
|
||||||
|
with threading.RLock():
|
||||||
|
for sub in self.subscribers.values():
|
||||||
|
if sub.uuid == uuid or sub.host == uuid:
|
||||||
|
sub.cleanup()
|
||||||
|
del self.subscribers[sub.uuid]
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
with threading.RLock():
|
||||||
|
for sub in self.subscribers.values():
|
||||||
|
if sub.age > 30:
|
||||||
|
sub.cleanup()
|
||||||
|
del self.subscribers[sub.uuid]
|
||||||
|
|
||||||
|
def getPlayerProperties(self, playerid):
|
||||||
|
info = {}
|
||||||
|
try:
|
||||||
|
# get info from the player
|
||||||
|
props = jsonrpc("Player.GetProperties", {"playerid": playerid, "properties": ["time", "totaltime", "speed", "shuffled"]})
|
||||||
|
printDebug(jsonrpc("Player.GetItem", {"playerid": playerid, "properties": ["file", "showlink", "episode", "season"]}))
|
||||||
|
info['time'] = timeToMillis(props['time'])
|
||||||
|
info['duration'] = timeToMillis(props['totaltime'])
|
||||||
|
info['state'] = ("paused", "playing")[int(props['speed'])]
|
||||||
|
info['shuffle'] = ("0","1")[props.get('shuffled', False)]
|
||||||
|
except:
|
||||||
|
info['time'] = 0
|
||||||
|
info['duration'] = 0
|
||||||
|
info['state'] = "stopped"
|
||||||
|
info['shuffle'] = False
|
||||||
|
# get the volume from the application
|
||||||
|
info['volume'] = self.volume
|
||||||
|
info['guid'] = self.guid
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
class Subscriber:
|
||||||
|
def __init__(self, protocol, host, port, uuid, commandID):
|
||||||
|
self.protocol = protocol or "http"
|
||||||
|
self.host = host
|
||||||
|
self.port = port or 32400
|
||||||
|
self.uuid = uuid or host
|
||||||
|
self.commandID = int(commandID) or 0
|
||||||
|
self.navlocationsent = False
|
||||||
|
self.age = 0
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.uuid == other.uuid
|
||||||
|
def tostr(self):
|
||||||
|
return "uuid=%s,commandID=%i" % (self.uuid, self.commandID)
|
||||||
|
def cleanup(self):
|
||||||
|
requests.closeConnection(self.protocol, self.host, self.port)
|
||||||
|
def send_update(self, msg, is_nav):
|
||||||
|
self.age += 1
|
||||||
|
if not is_nav:
|
||||||
|
self.navlocationsent = False
|
||||||
|
elif self.navlocationsent:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.navlocationsent = True
|
||||||
|
msg = re.sub(r"INSERTCOMMANDID", str(self.commandID), msg)
|
||||||
|
printDebug("sending xml to subscriber %s: %s" % (self.tostr(), msg))
|
||||||
|
if not requests.post(self.host, self.port, "/:/timeline", msg, getPlexHeaders(), self.protocol):
|
||||||
|
subMgr.removeSubscriber(self.uuid)
|
||||||
|
|
||||||
|
subMgr = SubscriptionManager()
|
Loading…
Reference in a new issue