Major Plex Companion overhaul, part 2
This commit is contained in:
parent
cc347d5654
commit
9cac51d5c9
7 changed files with 258 additions and 217 deletions
|
@ -2690,7 +2690,8 @@ class API():
|
||||||
plexitem = "plex_%s" % playurl
|
plexitem = "plex_%s" % playurl
|
||||||
window('%s.runtime' % plexitem, value=str(userdata['Runtime']))
|
window('%s.runtime' % plexitem, value=str(userdata['Runtime']))
|
||||||
window('%s.type' % plexitem, value=itemtype)
|
window('%s.type' % plexitem, value=itemtype)
|
||||||
window('%s.itemid' % plexitem, value=self.getRatingKey())
|
state.PLEX_IDS[tryDecode(playurl)] = self.getRatingKey()
|
||||||
|
# window('%s.itemid' % plexitem, value=self.getRatingKey())
|
||||||
window('%s.playcount' % plexitem, value=str(userdata['PlayCount']))
|
window('%s.playcount' % plexitem, value=str(userdata['PlayCount']))
|
||||||
|
|
||||||
if itemtype == v.PLEX_TYPE_EPISODE:
|
if itemtype == v.PLEX_TYPE_EPISODE:
|
||||||
|
|
|
@ -362,6 +362,7 @@ def get_episodes(params):
|
||||||
|
|
||||||
def get_item(playerid):
|
def get_item(playerid):
|
||||||
"""
|
"""
|
||||||
|
UNRELIABLE on playback startup! (as other JSON and Python Kodi functions)
|
||||||
Returns the following for the currently playing item:
|
Returns the following for the currently playing item:
|
||||||
{
|
{
|
||||||
u'title': u'Okja',
|
u'title': u'Okja',
|
||||||
|
|
|
@ -808,7 +808,7 @@ class Kodidb_Functions():
|
||||||
ids.append(row[0])
|
ids.append(row[0])
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def getIdFromFilename(self, filename, path):
|
def video_id_from_filename(self, filename, path):
|
||||||
"""
|
"""
|
||||||
Returns the tuple (itemId, type) where
|
Returns the tuple (itemId, type) where
|
||||||
itemId: Kodi DB unique Id for either movie or episode
|
itemId: Kodi DB unique Id for either movie or episode
|
||||||
|
@ -884,6 +884,34 @@ class Kodidb_Functions():
|
||||||
return
|
return
|
||||||
return itemId, typus
|
return itemId, typus
|
||||||
|
|
||||||
|
def music_id_from_filename(self, filename, path):
|
||||||
|
"""
|
||||||
|
Returns the Kodi song_id from the Kodi music database or None if not
|
||||||
|
found OR something went wrong.
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT idPath
|
||||||
|
FROM path
|
||||||
|
WHERE strPath = ?
|
||||||
|
'''
|
||||||
|
self.cursor.execute(query, (path,))
|
||||||
|
path_id = self.cursor.fetchall()
|
||||||
|
if len(path_id) != 1:
|
||||||
|
log.error('Found wrong number of path ids: %s for path %s, abort',
|
||||||
|
path_id, path)
|
||||||
|
return
|
||||||
|
query = '''
|
||||||
|
SELECT idSong
|
||||||
|
FROM song
|
||||||
|
WHERE strFileName = ? AND idPath = ?
|
||||||
|
'''
|
||||||
|
self.cursor.execute(query, (filename, path_id[0]))
|
||||||
|
song_id = self.cursor.fetchall()
|
||||||
|
if len(song_id) != 1:
|
||||||
|
log.info('Found wrong number of songs %s, abort', song_id)
|
||||||
|
return
|
||||||
|
return song_id[0]
|
||||||
|
|
||||||
def getUnplayedItems(self):
|
def getUnplayedItems(self):
|
||||||
"""
|
"""
|
||||||
VIDEOS
|
VIDEOS
|
||||||
|
@ -1522,24 +1550,29 @@ class Kodidb_Functions():
|
||||||
self.cursor.execute(query, (kodi_id, kodi_type))
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
||||||
|
|
||||||
|
|
||||||
def get_kodiid_from_filename(file):
|
def kodiid_from_filename(path, kodi_type):
|
||||||
"""
|
"""
|
||||||
Returns the tuple (kodiid, type) if we have a video in the database with
|
Returns kodi_id if we have an item in the Kodi video or audio database with
|
||||||
said filename, or (None, None)
|
said path. Feed with the Kodi itemtype, e.v. 'movie', 'song'
|
||||||
|
Returns None if not possible
|
||||||
"""
|
"""
|
||||||
kodiid = None
|
kodi_id = None
|
||||||
typus = None
|
|
||||||
try:
|
try:
|
||||||
filename = file.rsplit('/', 1)[1]
|
filename = path.rsplit('/', 1)[1]
|
||||||
path = file.rsplit('/', 1)[0] + '/'
|
path = path.rsplit('/', 1)[0] + '/'
|
||||||
except IndexError:
|
except IndexError:
|
||||||
filename = file.rsplit('\\', 1)[1]
|
filename = path.rsplit('\\', 1)[1]
|
||||||
path = file.rsplit('\\', 1)[0] + '\\'
|
path = path.rsplit('\\', 1)[0] + '\\'
|
||||||
log.debug('Trying to figure out playing item from filename: %s '
|
if kodi_type == v.KODI_TYPE_SONG:
|
||||||
'and path: %s' % (filename, path))
|
with GetKodiDB('music') as kodi_db:
|
||||||
with GetKodiDB('video') as kodi_db:
|
try:
|
||||||
try:
|
kodi_id, _ = kodi_db.music_id_from_filename(filename, path)
|
||||||
kodiid, typus = kodi_db.getIdFromFilename(filename, path)
|
except TypeError:
|
||||||
except TypeError:
|
log.info('No Kodi audio db element found for path %s', path)
|
||||||
log.info('No kodi video element found with filename %s' % filename)
|
else:
|
||||||
return (kodiid, typus)
|
with GetKodiDB('video') as kodi_db:
|
||||||
|
try:
|
||||||
|
kodi_id, _ = kodi_db.video_id_from_filename(filename, path)
|
||||||
|
except TypeError:
|
||||||
|
log.info('No kodi video db element found for path %s', path)
|
||||||
|
return kodi_id
|
||||||
|
|
|
@ -11,7 +11,7 @@ import plexdb_functions as plexdb
|
||||||
from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \
|
from utils import window, settings, CatchExceptions, tryDecode, tryEncode, \
|
||||||
plex_command
|
plex_command
|
||||||
from PlexFunctions import scrobble
|
from PlexFunctions import scrobble
|
||||||
from kodidb_functions import get_kodiid_from_filename
|
from kodidb_functions import kodiid_from_filename
|
||||||
from PlexAPI import API
|
from PlexAPI import API
|
||||||
import json_rpc as js
|
import json_rpc as js
|
||||||
import state
|
import state
|
||||||
|
@ -185,68 +185,61 @@ class KodiMonitor(Monitor):
|
||||||
u'item': {u'type': u'movie', u'title': u''},
|
u'item': {u'type': u'movie', u'title': u''},
|
||||||
u'player': {u'playerid': 1, u'speed': 1}
|
u'player': {u'playerid': 1, u'speed': 1}
|
||||||
}
|
}
|
||||||
|
Unfortunately VERY random inputs!
|
||||||
|
E.g. when using Widgets, Kodi doesn't tell us shit
|
||||||
"""
|
"""
|
||||||
log.debug('PlayBackStart called with: %s', data)
|
|
||||||
# Get the type of media we're playing
|
# Get the type of media we're playing
|
||||||
try:
|
try:
|
||||||
kodi_type = data['item']['type']
|
kodi_type = data['item']['type']
|
||||||
playerid = data['player']['playerid']
|
playerid = data['player']['playerid']
|
||||||
json_data = js.get_item(playerid)
|
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
log.info('Aborting playback report - item is invalid for updates')
|
log.info('Aborting playback report - item invalid for updates %s',
|
||||||
|
data)
|
||||||
return
|
return
|
||||||
|
json_data = js.get_item(playerid)
|
||||||
|
path = json_data.get('file')
|
||||||
|
kodi_id = json_data.get('id')
|
||||||
|
if not path and not kodi_id:
|
||||||
|
log.info('Aborting playback report - no Kodi id or file for %s',
|
||||||
|
json_data)
|
||||||
|
return
|
||||||
|
# Plex id will NOT be set with direct paths
|
||||||
|
plex_id = state.PLEX_IDS.get(path)
|
||||||
try:
|
try:
|
||||||
kodi_id = json_data['id']
|
plex_type = v.PLEX_TYPE_FROM_KODI_TYPE[kodi_type]
|
||||||
kodi_type = json_data['type']
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.info('Aborting playback report - no Kodi id for %s', json_data)
|
|
||||||
return
|
|
||||||
# Get Plex' item id
|
|
||||||
with plexdb.Get_Plex_DB() as plex_db:
|
|
||||||
plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type)
|
|
||||||
try:
|
|
||||||
plex_id = plex_dbitem[0]
|
|
||||||
plex_type = plex_dbitem[2]
|
|
||||||
except TypeError:
|
|
||||||
# No plex id, hence item not in the library. E.g. clips
|
|
||||||
plex_id = None
|
|
||||||
plex_type = None
|
plex_type = None
|
||||||
|
# No Kodi id returned by Kodi, even if there is one. Ex: Widgets
|
||||||
|
if plex_id and not kodi_id:
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plex_dbitem = plex_db.getItem_byId(plex_id)
|
||||||
|
try:
|
||||||
|
kodi_id = plex_dbitem[0]
|
||||||
|
except TypeError:
|
||||||
|
kodi_id = None
|
||||||
|
# If using direct paths and starting playback from a widget
|
||||||
|
if not path.startswith('http'):
|
||||||
|
if not kodi_id:
|
||||||
|
kodi_id = kodiid_from_filename(path, kodi_type)
|
||||||
|
if not plex_id and kodi_id:
|
||||||
|
with plexdb.Get_Plex_DB() as plex_db:
|
||||||
|
plex_dbitem = plex_db.getItem_byKodiId(kodi_id, kodi_type)
|
||||||
|
try:
|
||||||
|
plex_id = plex_dbitem[0]
|
||||||
|
plex_type = plex_dbitem[2]
|
||||||
|
except TypeError:
|
||||||
|
# No plex id, hence item not in the library. E.g. clips
|
||||||
|
pass
|
||||||
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
|
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
|
||||||
state.PLAYER_STATES[playerid]['file'] = json_data['file']
|
state.PLAYER_STATES[playerid]['file'] = json_data['file']
|
||||||
state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id
|
state.PLAYER_STATES[playerid]['kodi_id'] = kodi_id
|
||||||
state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type
|
state.PLAYER_STATES[playerid]['kodi_type'] = kodi_type
|
||||||
state.PLAYER_STATES[playerid]['plex_id'] = plex_id
|
state.PLAYER_STATES[playerid]['plex_id'] = plex_id
|
||||||
state.PLAYER_STATES[playerid]['plex_type'] = plex_type
|
state.PLAYER_STATES[playerid]['plex_type'] = plex_type
|
||||||
log.debug('Set the player state %s', state.PLAYER_STATES[playerid])
|
|
||||||
# Set other stuff like volume
|
# Set other stuff like volume
|
||||||
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
|
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
|
||||||
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
|
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
|
||||||
return
|
log.debug('Set the player state: %s', state.PLAYER_STATES[playerid])
|
||||||
|
|
||||||
# Switch subtitle tracks if applicable
|
|
||||||
subtitle = window('plex_%s.subtitle' % tryEncode(currentFile))
|
|
||||||
if window(tryEncode('plex_%s.playmethod' % currentFile)) \
|
|
||||||
== 'Transcode' and subtitle:
|
|
||||||
if window('plex_%s.subtitle' % currentFile) == 'None':
|
|
||||||
self.xbmcplayer.showSubtitles(False)
|
|
||||||
else:
|
|
||||||
self.xbmcplayer.setSubtitleStream(int(subtitle))
|
|
||||||
|
|
||||||
# Set some stuff if Kodi initiated playback
|
|
||||||
if ((settings('useDirectPaths') == "1" and not typus == "song")
|
|
||||||
or
|
|
||||||
(typus == "song" and settings('enableMusic') == "true")):
|
|
||||||
if self.StartDirectPath(plex_id,
|
|
||||||
typus,
|
|
||||||
tryEncode(currentFile)) is False:
|
|
||||||
log.error('Could not initiate monitoring; aborting')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Save currentFile for cleanup later and to be able to access refs
|
|
||||||
window('plex_lastPlayedFiled', value=currentFile)
|
|
||||||
window('plex_currently_playing_itemid', value=plex_id)
|
|
||||||
window("plex_%s.itemid" % tryEncode(currentFile), value=plex_id)
|
|
||||||
log.info('Finish playback startup')
|
|
||||||
|
|
||||||
def StartDirectPath(self, plex_id, type, currentFile):
|
def StartDirectPath(self, plex_id, type, currentFile):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,56 +1,68 @@
|
||||||
import logging
|
"""
|
||||||
import re
|
Manages getting playstate from Kodi and sending it to the PMS as well as
|
||||||
import threading
|
subscribed Plex Companion clients.
|
||||||
|
"""
|
||||||
from xbmc import sleep
|
from logging import getLogger
|
||||||
|
from re import sub
|
||||||
|
from threading import Thread, RLock
|
||||||
|
|
||||||
import downloadutils
|
import downloadutils
|
||||||
from clientinfo import getXArgsDeviceInfo
|
|
||||||
from utils import window, kodi_time_to_millis
|
from utils import window, kodi_time_to_millis
|
||||||
import PlexFunctions as pf
|
|
||||||
import state
|
import state
|
||||||
import variables as v
|
import variables as v
|
||||||
import json_rpc as js
|
import json_rpc as js
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
log = logging.getLogger("PLEX."+__name__)
|
LOG = getLogger("PLEX." + __name__)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# What is Companion controllable?
|
# What is Companion controllable?
|
||||||
CONTROLLABLE = {
|
CONTROLLABLE = {
|
||||||
v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop',
|
v.PLEX_TYPE_PHOTO: 'skipPrevious,skipNext,stop',
|
||||||
v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,' \
|
v.PLEX_TYPE_AUDIO: 'playPause,stop,volume,shuffle,repeat,seekTo,'
|
||||||
'skipPrevious,skipNext,stepBack,stepForward',
|
'skipPrevious,skipNext,stepBack,stepForward',
|
||||||
v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,audioStream,subtitleStream,' \
|
v.PLEX_TYPE_VIDEO: 'playPause,stop,volume,shuffle,audioStream,'
|
||||||
'seekTo,skipPrevious,skipNext,stepBack,stepForward'
|
'subtitleStream,seekTo,skipPrevious,skipNext,stepBack,stepForward'
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubscriptionManager:
|
class SubscriptionManager:
|
||||||
|
"""
|
||||||
|
Manages Plex companion subscriptions
|
||||||
|
"""
|
||||||
def __init__(self, RequestMgr, player, mgr):
|
def __init__(self, RequestMgr, player, mgr):
|
||||||
self.serverlist = []
|
self.serverlist = []
|
||||||
self.subscribers = {}
|
self.subscribers = {}
|
||||||
self.info = {}
|
self.info = {}
|
||||||
self.lastkey = ""
|
self.containerKey = None
|
||||||
self.containerKey = ""
|
self.ratingkey = None
|
||||||
self.ratingkey = ""
|
|
||||||
self.lastplayers = {}
|
|
||||||
self.lastinfo = {
|
|
||||||
'video': {},
|
|
||||||
'audio': {},
|
|
||||||
'picture': {}
|
|
||||||
}
|
|
||||||
self.server = ""
|
self.server = ""
|
||||||
self.protocol = "http"
|
self.protocol = "http"
|
||||||
self.port = ""
|
self.port = ""
|
||||||
self.playerprops = {}
|
# In order to be able to signal a stop at the end
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
self.last_params = {}
|
||||||
|
self.lastplayers = {}
|
||||||
|
|
||||||
|
self.doUtils = downloadutils.DownloadUtils
|
||||||
self.xbmcplayer = player
|
self.xbmcplayer = player
|
||||||
self.playqueue = mgr.playqueue
|
self.playqueue = mgr.playqueue
|
||||||
|
|
||||||
self.RequestMgr = RequestMgr
|
self.RequestMgr = RequestMgr
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _headers():
|
||||||
|
"""
|
||||||
|
Headers are different for Plex Companion!
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'Content-type': 'text/plain',
|
||||||
|
'Connection': 'Keep-Alive',
|
||||||
|
'Keep-Alive': 'timeout=20',
|
||||||
|
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
|
||||||
|
'Access-Control-Expose-Headers': 'X-Plex-Client-Identifier',
|
||||||
|
'X-Plex-Protocol': "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
def getServerByHost(self, host):
|
def getServerByHost(self, host):
|
||||||
if len(self.serverlist) == 1:
|
if len(self.serverlist) == 1:
|
||||||
return self.serverlist[0]
|
return self.serverlist[0]
|
||||||
|
@ -61,64 +73,80 @@ class SubscriptionManager:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def msg(self, players):
|
def msg(self, players):
|
||||||
log.debug('players: %s', players)
|
LOG.debug('players: %s', players)
|
||||||
msg = v.XML_HEADER
|
msg = v.XML_HEADER
|
||||||
msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"'
|
msg += '<MediaContainer size="3" commandID="INSERTCOMMANDID"'
|
||||||
msg += ' machineIdentifier="%s">' % v.PKC_MACHINE_IDENTIFIER
|
msg += ' machineIdentifier="%s">\n' % v.PKC_MACHINE_IDENTIFIER
|
||||||
msg += self.getTimelineXML(players.get(v.KODI_TYPE_AUDIO),
|
msg += self.get_timeline_xml(players.get(v.KODI_TYPE_AUDIO),
|
||||||
v.PLEX_TYPE_AUDIO)
|
v.PLEX_TYPE_AUDIO)
|
||||||
msg += self.getTimelineXML(players.get(v.KODI_TYPE_PHOTO),
|
msg += self.get_timeline_xml(players.get(v.KODI_TYPE_PHOTO),
|
||||||
v.PLEX_TYPE_PHOTO)
|
v.PLEX_TYPE_PHOTO)
|
||||||
msg += self.getTimelineXML(players.get(v.KODI_TYPE_VIDEO),
|
msg += self.get_timeline_xml(players.get(v.KODI_TYPE_VIDEO),
|
||||||
v.PLEX_TYPE_VIDEO)
|
v.PLEX_TYPE_VIDEO)
|
||||||
msg += "\n</MediaContainer>"
|
msg += "</MediaContainer>"
|
||||||
log.debug('msg is: %s', msg)
|
LOG.debug('msg is: %s', msg)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def getTimelineXML(self, player, ptype):
|
def _get_container_key(self, playerid):
|
||||||
if player is None:
|
key = None
|
||||||
status = 'stopped'
|
playlistid = state.PLAYER_STATES[playerid]['playlistid']
|
||||||
|
LOG.debug('type: %s, playlistid: %s', type(playlistid), playlistid)
|
||||||
|
if playlistid != -1:
|
||||||
|
# -1 is Kodi's answer if there is no playlist
|
||||||
|
try:
|
||||||
|
key = self.playqueue.playqueues[playlistid].id
|
||||||
|
except (KeyError, IndexError, TypeError):
|
||||||
|
pass
|
||||||
|
if key is not None:
|
||||||
|
key = '/playQueues/%s' % key
|
||||||
else:
|
else:
|
||||||
playerid = player['playerid']
|
if state.PLAYER_STATES[playerid]['plex_id']:
|
||||||
info = state.PLAYER_STATES[playerid]
|
key = '/library/metadata/%s' % \
|
||||||
# save this info off so the server update can use it too
|
state.PLAYER_STATES[playerid]['plex_id']
|
||||||
# self.playerprops[playerid] = info
|
return key
|
||||||
status = ("paused", "playing")[info['speed']]
|
|
||||||
ret = ('\n <Timeline state="%s" controllable="%s" type="%s" '
|
|
||||||
'itemType="%s"' % (status, CONTROLLABLE[ptype], ptype, ptype))
|
|
||||||
if player is None:
|
|
||||||
ret += ' />'
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
def get_timeline_xml(self, player, ptype):
|
||||||
|
if player is None:
|
||||||
|
return ' <Timeline state="stopped" controllable="%s" type="%s" ' \
|
||||||
|
'itemType="%s" />\n' % (CONTROLLABLE[ptype], ptype, ptype)
|
||||||
|
playerid = player['playerid']
|
||||||
|
info = state.PLAYER_STATES[playerid]
|
||||||
|
status = 'paused' if info['speed'] == '0' else 'playing'
|
||||||
|
ret = ' <Timeline state="%s"' % status
|
||||||
|
ret += ' controllable="%s"' % CONTROLLABLE[ptype]
|
||||||
|
ret += ' type="%s" itemType="%s"' % (ptype, ptype)
|
||||||
ret += ' time="%s"' % kodi_time_to_millis(info['time'])
|
ret += ' time="%s"' % kodi_time_to_millis(info['time'])
|
||||||
ret += ' duration="%s"' % kodi_time_to_millis(info['totaltime'])
|
ret += ' duration="%s"' % kodi_time_to_millis(info['totaltime'])
|
||||||
ret += ' shuffle="%s"' % ("0", "1")[info['shuffled']]
|
shuffled = '1' if info['shuffled'] else '0'
|
||||||
|
ret += ' shuffle="%s"' % shuffled
|
||||||
ret += ' repeat="%s"' % v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']]
|
ret += ' repeat="%s"' % v.PLEX_REPEAT_FROM_KODI_REPEAT[info['repeat']]
|
||||||
if ptype != v.KODI_TYPE_PHOTO:
|
if ptype != v.KODI_TYPE_PHOTO:
|
||||||
ret += ' volume="%s"' % info['volume']
|
ret += ' volume="%s"' % info['volume']
|
||||||
ret += ' mute="%s"' % ("0", "1")[info['muted']]
|
muted = '1' if info['muted'] is True else '0'
|
||||||
|
ret += ' mute="%s"' % muted
|
||||||
pbmc_server = window('pms_server')
|
pbmc_server = window('pms_server')
|
||||||
server = self.getServerByHost(self.server)
|
server = self.getServerByHost(self.server)
|
||||||
if pbmc_server:
|
if pbmc_server:
|
||||||
(self.protocol, self.server, self.port) = pbmc_server.split(':')
|
(self.protocol, self.server, self.port) = pbmc_server.split(':')
|
||||||
self.server = self.server.replace('/', '')
|
self.server = self.server.replace('/', '')
|
||||||
if info['plex_id']:
|
if info['plex_id']:
|
||||||
self.lastkey = "/library/metadata/%s" % info['plex_id']
|
|
||||||
self.ratingkey = info['plex_id']
|
self.ratingkey = info['plex_id']
|
||||||
ret += ' key="/library/metadata/%s"' % info['plex_id']
|
ret += ' key="/library/metadata/%s"' % info['plex_id']
|
||||||
ret += ' ratingKey="%s"' % info['plex_id']
|
ret += ' ratingKey="%s"' % info['plex_id']
|
||||||
# PlayQueue stuff
|
# PlayQueue stuff
|
||||||
playqueue = self.playqueue.playqueues[playerid]
|
playqueue = self.playqueue.playqueues[playerid]
|
||||||
pos = info['position']
|
key = self._get_container_key(playerid)
|
||||||
try:
|
if key is not None and key.startswith('/playQueues'):
|
||||||
ret += ' playQueueItemID="%s"' % playqueue.items[pos].ID or 'null'
|
self.containerKey = key
|
||||||
self.containerKey = "/playQueues/%s" % playqueue.ID or 'null'
|
|
||||||
ret += ' playQueueID="%s"' % playqueue.ID or 'null'
|
|
||||||
ret += ' playQueueVersion="%s"' % playqueue.version or 'null'
|
|
||||||
ret += ' containerKey="%s"' % self.containerKey
|
ret += ' containerKey="%s"' % self.containerKey
|
||||||
|
pos = info['position']
|
||||||
|
ret += ' playQueueItemID="%s"' % playqueue.items[pos].id or 'null'
|
||||||
|
ret += ' playQueueID="%s"' % playqueue.id or 'null'
|
||||||
|
ret += ' playQueueVersion="%s"' % playqueue.version or 'null'
|
||||||
ret += ' guid="%s"' % playqueue.items[pos].guid or 'null'
|
ret += ' guid="%s"' % playqueue.items[pos].guid or 'null'
|
||||||
except IndexError:
|
elif key:
|
||||||
pass
|
self.containerKey = key
|
||||||
|
ret += ' containerKey="%s"' % self.containerKey
|
||||||
ret += ' machineIdentifier="%s"' % server.get('uuid', "")
|
ret += ' machineIdentifier="%s"' % server.get('uuid', "")
|
||||||
ret += ' protocol="%s"' % server.get('protocol', 'http')
|
ret += ' protocol="%s"' % server.get('protocol', 'http')
|
||||||
ret += ' address="%s"' % server.get('server', self.server)
|
ret += ' address="%s"' % server.get('server', self.server)
|
||||||
|
@ -132,8 +160,7 @@ class SubscriptionManager:
|
||||||
if ptype == 'video':
|
if ptype == 'video':
|
||||||
ret += ' subtitleStreamID="-1"'
|
ret += ' subtitleStreamID="-1"'
|
||||||
ret += ' audioStreamID="-1"'
|
ret += ' audioStreamID="-1"'
|
||||||
|
ret += '/>\n'
|
||||||
ret += '/>'
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def updateCommandID(self, uuid, commandID):
|
def updateCommandID(self, uuid, commandID):
|
||||||
|
@ -142,120 +169,92 @@ class SubscriptionManager:
|
||||||
|
|
||||||
def notify(self, event=False):
|
def notify(self, event=False):
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
# Don't tell anyone if we don't know a Plex ID and are still playing
|
# Do we need a check to NOT tell about e.g. PVR/TV and Addon playback?
|
||||||
# (e.g. no stop called). Used for e.g. PVR/TV without PKC usage
|
|
||||||
if (not window('plex_currently_playing_itemid')
|
|
||||||
and not self.lastplayers):
|
|
||||||
return True
|
|
||||||
players = js.get_players()
|
players = js.get_players()
|
||||||
# fetch the message, subscribers or not, since the server
|
# fetch the message, subscribers or not, since the server
|
||||||
# will need the info anyway
|
# will need the info anyway
|
||||||
msg = self.msg(players)
|
msg = self.msg(players)
|
||||||
if self.subscribers:
|
if self.subscribers:
|
||||||
with threading.RLock():
|
with RLock():
|
||||||
for sub in self.subscribers.values():
|
for subscriber in self.subscribers.values():
|
||||||
sub.send_update(msg, len(players) == 0)
|
subscriber.send_update(msg, len(players) == 0)
|
||||||
self.notifyServer(players)
|
self.notifyServer(players)
|
||||||
self.lastplayers = players
|
self.lastplayers = players
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def notifyServer(self, players):
|
def notifyServer(self, players):
|
||||||
for typus, p in players.iteritems():
|
for typus, player in players.iteritems():
|
||||||
info = self.playerprops[p.get('playerid')]
|
self._send_pms_notification(
|
||||||
self._sendNotification(info, int(p['playerid']))
|
player['playerid'], self._get_pms_params(player['playerid']))
|
||||||
self.lastinfo[typus] = info
|
|
||||||
# Cross the one of the list
|
|
||||||
try:
|
try:
|
||||||
del self.lastplayers[typus]
|
del self.lastplayers[typus]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
# Process the players we have left (to signal a stop)
|
# Process the players we have left (to signal a stop)
|
||||||
for typus, p in self.lastplayers.iteritems():
|
for typus, player in self.lastplayers.iteritems():
|
||||||
self.lastinfo[typus]['state'] = 'stopped'
|
self.last_params['state'] = 'stopped'
|
||||||
self._sendNotification(self.lastinfo[typus], int(p['playerid']))
|
self._send_pms_notification(player['playerid'], self.last_params)
|
||||||
|
|
||||||
def _sendNotification(self, info, playerid):
|
def _get_pms_params(self, playerid):
|
||||||
playqueue = self.playqueue.playqueues[playerid]
|
info = state.PLAYER_STATES[playerid]
|
||||||
xargs = getXArgsDeviceInfo(include_token=False)
|
status = 'paused' if info['speed'] == '0' else 'playing'
|
||||||
params = {
|
params = {'state': status,
|
||||||
'containerKey': self.containerKey or "/library/metadata/900000",
|
'ratingKey': self.ratingkey,
|
||||||
'key': self.lastkey or "/library/metadata/900000",
|
'key': '/library/metadata/%s' % self.ratingkey,
|
||||||
'ratingKey': self.ratingkey or "900000",
|
'time': kodi_time_to_millis(info['time']),
|
||||||
'state': info['state'],
|
'duration': kodi_time_to_millis(info['totaltime'])
|
||||||
'time': info['time'],
|
|
||||||
'duration': info['duration']
|
|
||||||
}
|
}
|
||||||
|
if self.containerKey:
|
||||||
|
params['containerKey'] = self.containerKey
|
||||||
|
if self.containerKey is not None and \
|
||||||
|
self.containerKey.startswith('/playQueues/'):
|
||||||
|
params['playQueueVersion'] = info['playQueueVersion']
|
||||||
|
params['playQueueItemID'] = info['playQueueItemID']
|
||||||
|
self.last_params = params
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _send_pms_notification(self, playerid, params):
|
||||||
|
serv = self.getServerByHost(self.server)
|
||||||
|
xargs = self._headers()
|
||||||
|
playqueue = self.playqueue.playqueues[playerid]
|
||||||
if state.PLEX_TRANSIENT_TOKEN:
|
if state.PLEX_TRANSIENT_TOKEN:
|
||||||
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
|
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
|
||||||
elif playqueue.plex_transient_token:
|
elif playqueue.plex_transient_token:
|
||||||
xargs['X-Plex-Token'] = playqueue.plex_transient_token
|
xargs['X-Plex-Token'] = playqueue.plex_transient_token
|
||||||
if info.get('playQueueID'):
|
|
||||||
params['containerKey'] = '/playQueues/%s' % info['playQueueID']
|
|
||||||
params['playQueueVersion'] = info['playQueueVersion']
|
|
||||||
params['playQueueItemID'] = info['playQueueItemID']
|
|
||||||
serv = self.getServerByHost(self.server)
|
|
||||||
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
|
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
|
||||||
serv.get('server', 'localhost'),
|
serv.get('server', 'localhost'),
|
||||||
serv.get('port', '32400'))
|
serv.get('port', '32400'))
|
||||||
self.doUtils(url, parameters=params, headerOptions=xargs)
|
self.doUtils().downloadUrl(
|
||||||
log.debug("Sent server notification with parameters: %s to %s"
|
url, parameters=params, headerOptions=xargs)
|
||||||
% (params, url))
|
# Save to be able to signal a stop at the end
|
||||||
|
LOG.debug("Sent server notification with parameters: %s to %s",
|
||||||
|
params, url)
|
||||||
|
|
||||||
def addSubscriber(self, protocol, host, port, uuid, commandID):
|
def addSubscriber(self, protocol, host, port, uuid, commandID):
|
||||||
sub = Subscriber(protocol,
|
subscriber = Subscriber(protocol,
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
uuid,
|
uuid,
|
||||||
commandID,
|
commandID,
|
||||||
self,
|
self,
|
||||||
self.RequestMgr)
|
self.RequestMgr)
|
||||||
with threading.RLock():
|
with RLock():
|
||||||
self.subscribers[sub.uuid] = sub
|
self.subscribers[subscriber.uuid] = subscriber
|
||||||
return sub
|
return subscriber
|
||||||
|
|
||||||
def removeSubscriber(self, uuid):
|
def removeSubscriber(self, uuid):
|
||||||
with threading.RLock():
|
with RLock():
|
||||||
for sub in self.subscribers.values():
|
for subscriber in self.subscribers.values():
|
||||||
if sub.uuid == uuid or sub.host == uuid:
|
if subscriber.uuid == uuid or subscriber.host == uuid:
|
||||||
sub.cleanup()
|
subscriber.cleanup()
|
||||||
del self.subscribers[sub.uuid]
|
del self.subscribers[subscriber.uuid]
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
with threading.RLock():
|
with RLock():
|
||||||
for sub in self.subscribers.values():
|
for subscriber in self.subscribers.values():
|
||||||
if sub.age > 30:
|
if subscriber.age > 30:
|
||||||
sub.cleanup()
|
subscriber.cleanup()
|
||||||
del self.subscribers[sub.uuid]
|
del self.subscribers[subscriber.uuid]
|
||||||
|
|
||||||
def getPlayerProperties(self, playerid):
|
|
||||||
# Get the playqueue
|
|
||||||
playqueue = self.playqueue.playqueues[playerid]
|
|
||||||
# get info from the player
|
|
||||||
props = state.PLAYER_STATES[playerid]
|
|
||||||
info = {
|
|
||||||
'time': kodi_time_to_millis(props['time']),
|
|
||||||
'duration': kodi_time_to_millis(props['totaltime']),
|
|
||||||
'state': ("paused", "playing")[int(props['speed'])],
|
|
||||||
'shuffle': ("0", "1")[props.get('shuffled', False)],
|
|
||||||
'repeat': v.PLEX_REPEAT_FROM_KODI_REPEAT[props.get('repeat')]
|
|
||||||
}
|
|
||||||
pos = props['position']
|
|
||||||
try:
|
|
||||||
info['playQueueItemID'] = playqueue.items[pos].ID or 'null'
|
|
||||||
info['guid'] = playqueue.items[pos].guid or 'null'
|
|
||||||
info['playQueueID'] = playqueue.ID or 'null'
|
|
||||||
info['playQueueVersion'] = playqueue.version or 'null'
|
|
||||||
info['itemType'] = playqueue.items[pos].plex_type or 'null'
|
|
||||||
except:
|
|
||||||
info['itemType'] = props.get('type') or 'null'
|
|
||||||
|
|
||||||
# get the volume from the application
|
|
||||||
info['volume'] = js.get_volume()
|
|
||||||
info['mute'] = js.get_muted()
|
|
||||||
|
|
||||||
info['plex_transient_token'] = playqueue.plex_transient_token
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
class Subscriber:
|
class Subscriber:
|
||||||
|
@ -268,16 +267,13 @@ class Subscriber:
|
||||||
self.commandID = int(commandID) or 0
|
self.commandID = int(commandID) or 0
|
||||||
self.navlocationsent = False
|
self.navlocationsent = False
|
||||||
self.age = 0
|
self.age = 0
|
||||||
self.doUtils = downloadutils.DownloadUtils().downloadUrl
|
self.doUtils = downloadutils.DownloadUtils
|
||||||
self.subMgr = subMgr
|
self.subMgr = subMgr
|
||||||
self.RequestMgr = RequestMgr
|
self.RequestMgr = RequestMgr
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.uuid == other.uuid
|
return self.uuid == other.uuid
|
||||||
|
|
||||||
def tostr(self):
|
|
||||||
return "uuid=%s,commandID=%i" % (self.uuid, self.commandID)
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.RequestMgr.closeConnection(self.protocol, self.host, self.port)
|
self.RequestMgr.closeConnection(self.protocol, self.host, self.port)
|
||||||
|
|
||||||
|
@ -289,11 +285,12 @@ class Subscriber:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.navlocationsent = True
|
self.navlocationsent = True
|
||||||
msg = re.sub(r"INSERTCOMMANDID", str(self.commandID), msg)
|
msg = sub(r"INSERTCOMMANDID", str(self.commandID), msg)
|
||||||
log.debug("sending xml to subscriber %s:\n%s" % (self.tostr(), msg))
|
LOG.debug("sending xml to subscriber uuid=%s,commandID=%i:\n%s",
|
||||||
|
self.uuid, self.commandID, msg)
|
||||||
url = self.protocol + '://' + self.host + ':' + self.port \
|
url = self.protocol + '://' + self.host + ':' + self.port \
|
||||||
+ "/:/timeline"
|
+ "/:/timeline"
|
||||||
t = threading.Thread(target=self.threadedSend, args=(url, msg))
|
t = Thread(target=self.threadedSend, args=(url, msg))
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def threadedSend(self, url, msg):
|
def threadedSend(self, url, msg):
|
||||||
|
@ -301,9 +298,8 @@ class Subscriber:
|
||||||
Threaded POST request, because they stall due to PMS response missing
|
Threaded POST request, because they stall due to PMS response missing
|
||||||
the Content-Length header :-(
|
the Content-Length header :-(
|
||||||
"""
|
"""
|
||||||
response = self.doUtils(url,
|
response = self.doUtils().downloadUrl(url,
|
||||||
postBody=msg,
|
postBody=msg,
|
||||||
action_type="POST")
|
action_type="POST")
|
||||||
log.debug('response is: %s', response)
|
|
||||||
if response in [False, None, 401]:
|
if response in [False, None, 401]:
|
||||||
self.subMgr.removeSubscriber(self.uuid)
|
self.subMgr.removeSubscriber(self.uuid)
|
||||||
|
|
|
@ -112,6 +112,9 @@ PLAYER_STATES = {
|
||||||
2: {},
|
2: {},
|
||||||
3: {}
|
3: {}
|
||||||
}
|
}
|
||||||
|
# Dict containing all filenames as keys with plex id as values - used for addon
|
||||||
|
# paths for playback (since we're not receiving a Kodi id)
|
||||||
|
PLEX_IDS = {}
|
||||||
PLAYED_INFO = {}
|
PLAYED_INFO = {}
|
||||||
|
|
||||||
# Kodi webserver details
|
# Kodi webserver details
|
||||||
|
|
|
@ -199,6 +199,20 @@ KODITYPE_FROM_PLEXTYPE = {
|
||||||
'XXXXXXX': 'genre'
|
'XXXXXXX': 'genre'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PLEX_TYPE_FROM_KODI_TYPE = {
|
||||||
|
KODI_TYPE_VIDEO: PLEX_TYPE_VIDEO,
|
||||||
|
KODI_TYPE_MOVIE: PLEX_TYPE_MOVIE,
|
||||||
|
KODI_TYPE_EPISODE: PLEX_TYPE_EPISODE,
|
||||||
|
KODI_TYPE_SEASON: PLEX_TYPE_SEASON,
|
||||||
|
KODI_TYPE_SHOW: PLEX_TYPE_SHOW,
|
||||||
|
KODI_TYPE_CLIP: PLEX_TYPE_CLIP,
|
||||||
|
KODI_TYPE_ARTIST: PLEX_TYPE_ARTIST,
|
||||||
|
KODI_TYPE_ALBUM: PLEX_TYPE_ALBUM,
|
||||||
|
KODI_TYPE_SONG: PLEX_TYPE_SONG,
|
||||||
|
KODI_TYPE_AUDIO: PLEX_TYPE_AUDIO,
|
||||||
|
KODI_TYPE_PHOTO: PLEX_TYPE_PHOTO
|
||||||
|
}
|
||||||
|
|
||||||
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
|
KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
|
||||||
PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO,
|
PLEX_TYPE_VIDEO: KODI_TYPE_VIDEO,
|
||||||
PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO,
|
PLEX_TYPE_MOVIE: KODI_TYPE_VIDEO,
|
||||||
|
|
Loading…
Reference in a new issue