2021-10-22 17:40:25 +11:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
The Plex Companion master python file
|
|
|
|
"""
|
|
|
|
from logging import getLogger
|
|
|
|
|
|
|
|
import xbmc
|
|
|
|
|
|
|
|
from ..plex_api import API
|
|
|
|
from .. import utils
|
|
|
|
from ..utils import cast
|
|
|
|
from .. import plex_functions as PF
|
|
|
|
from .. import playlist_func as PL
|
|
|
|
from .. import playback
|
|
|
|
from .. import json_rpc as js
|
|
|
|
from .. import variables as v
|
|
|
|
from .. import app
|
|
|
|
from .. import exceptions
|
|
|
|
|
|
|
|
|
|
|
|
log = getLogger('PLEX.companion.processing')
|
|
|
|
|
|
|
|
|
|
|
|
def update_playqueue_from_PMS(playqueue,
|
|
|
|
playqueue_id=None,
|
|
|
|
repeat=None,
|
|
|
|
offset=None,
|
|
|
|
transient_token=None,
|
|
|
|
start_plex_id=None):
|
|
|
|
"""
|
|
|
|
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
|
|
|
|
in playqueue_id if we need to fetch a new playqueue
|
|
|
|
|
|
|
|
repeat = 0, 1, 2
|
|
|
|
offset = time offset in Plextime (milliseconds)
|
|
|
|
"""
|
|
|
|
log.info('New playqueue %s received from Plex companion with offset '
|
|
|
|
'%s, repeat %s, start_plex_id %s',
|
|
|
|
playqueue_id, offset, repeat, start_plex_id)
|
|
|
|
# Safe transient token from being deleted
|
|
|
|
if transient_token is None:
|
|
|
|
transient_token = playqueue.plex_transient_token
|
|
|
|
with app.APP.lock_playqueues:
|
|
|
|
try:
|
|
|
|
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
|
|
|
except exceptions.PlaylistError:
|
|
|
|
log.error('Could now download playqueue %s', playqueue_id)
|
|
|
|
return
|
|
|
|
if playqueue.id == playqueue_id:
|
|
|
|
# This seems to be happening ONLY if a Plex Companion device
|
|
|
|
# reconnects and Kodi is already playing something - silly, really
|
|
|
|
# For all other cases, a new playqueue is generated by Plex
|
|
|
|
log.debug('Update for existing playqueue detected')
|
|
|
|
return
|
|
|
|
playqueue.clear()
|
|
|
|
# Get new metadata for the playqueue first
|
|
|
|
try:
|
|
|
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
|
|
|
except exceptions.PlaylistError:
|
|
|
|
log.error('Could not get playqueue ID %s', playqueue_id)
|
|
|
|
return
|
|
|
|
playqueue.repeat = 0 if not repeat else int(repeat)
|
|
|
|
playqueue.plex_transient_token = transient_token
|
|
|
|
playback.play_xml(playqueue,
|
|
|
|
xml,
|
|
|
|
offset=offset,
|
|
|
|
start_plex_id=start_plex_id)
|
|
|
|
|
|
|
|
|
|
|
|
def process_node(key, transient_token, offset):
|
|
|
|
"""
|
|
|
|
E.g. watch later initiated by Companion. Basically navigating Plex
|
|
|
|
"""
|
|
|
|
app.CONN.plex_transient_token = transient_token
|
|
|
|
params = {
|
|
|
|
'mode': 'plex_node',
|
|
|
|
'key': f'{{server}}{key}',
|
|
|
|
'offset': offset
|
|
|
|
}
|
|
|
|
handle = f'RunPlugin(plugin://{utils.extend_url(v.ADDON_ID, params)})'
|
|
|
|
xbmc.executebuiltin(handle)
|
|
|
|
|
|
|
|
|
|
|
|
def process_playlist(containerKey, typus, key, offset, token):
|
|
|
|
# Get the playqueue ID
|
|
|
|
_, container_key, query = PF.ParseContainerKey(containerKey)
|
|
|
|
try:
|
2021-10-31 20:44:27 +11:00
|
|
|
playqueue = app.PLAYQUEUES.from_plex_type(typus)
|
|
|
|
except ValueError:
|
2021-10-22 17:40:25 +11:00
|
|
|
# E.g. Plex web does not supply the media type
|
|
|
|
# Still need to figure out the type (video vs. music vs. pix)
|
|
|
|
xml = PF.GetPlexMetadata(key)
|
|
|
|
try:
|
|
|
|
xml[0].attrib
|
|
|
|
except (AttributeError, IndexError, TypeError):
|
|
|
|
log.error('Could not download Plex metadata')
|
|
|
|
return
|
|
|
|
api = API(xml[0])
|
2021-10-31 20:44:27 +11:00
|
|
|
playqueue = app.PLAYQUEUES.from_plex_type(api.plex_type)
|
2021-10-22 17:40:25 +11:00
|
|
|
if key:
|
|
|
|
_, key, _ = PF.ParseContainerKey(key)
|
|
|
|
update_playqueue_from_PMS(playqueue,
|
|
|
|
playqueue_id=container_key,
|
|
|
|
repeat=query.get('repeat'),
|
|
|
|
offset=utils.cast(int, offset),
|
|
|
|
transient_token=token,
|
|
|
|
start_plex_id=key)
|
|
|
|
|
|
|
|
|
2021-10-31 20:44:27 +11:00
|
|
|
def process_streams(plex_type, video_stream_id, audio_stream_id,
|
|
|
|
subtitle_stream_id):
|
2021-10-22 17:40:25 +11:00
|
|
|
"""
|
|
|
|
Plex Companion client adjusted audio or subtitle stream
|
|
|
|
"""
|
2021-10-31 20:44:27 +11:00
|
|
|
playqueue = app.PLAYQUEUES.from_plex_type(plex_type)
|
2021-10-22 17:40:25 +11:00
|
|
|
pos = js.get_position(playqueue.playlistid)
|
|
|
|
playqueue.items[pos].on_plex_stream_change(video_stream_id,
|
|
|
|
audio_stream_id,
|
|
|
|
subtitle_stream_id)
|
|
|
|
|
|
|
|
|
|
|
|
def process_refresh(playqueue_id):
|
|
|
|
"""
|
|
|
|
example data: {'playQueueID': '8475', 'commandID': '11'}
|
|
|
|
"""
|
|
|
|
xml = PL.get_pms_playqueue(playqueue_id)
|
|
|
|
if xml is None:
|
|
|
|
return
|
|
|
|
if len(xml) == 0:
|
|
|
|
log.debug('Empty playqueue received - clearing playqueue')
|
|
|
|
plex_type = PL.get_plextype_from_xml(xml)
|
|
|
|
if plex_type is None:
|
|
|
|
return
|
2021-10-31 20:44:27 +11:00
|
|
|
playqueue = app.PLAYQUEUES.from_plex_type(plex_type)
|
2021-10-22 17:40:25 +11:00
|
|
|
playqueue.clear()
|
|
|
|
return
|
2021-10-31 20:44:27 +11:00
|
|
|
playqueue = app.PLAYQUEUES.from_plex_type(xml[0].attrib['type'])
|
2021-10-22 17:40:25 +11:00
|
|
|
update_playqueue_from_PMS(playqueue, playqueue_id)
|
|
|
|
|
|
|
|
|
|
|
|
def skip_to(playqueue_item_id, key):
|
|
|
|
"""
|
|
|
|
Skip to a specific playlist position.
|
|
|
|
|
|
|
|
Does not seem to be implemented yet by Plex!
|
|
|
|
"""
|
|
|
|
_, plex_id = PF.GetPlexKeyNumber(key)
|
|
|
|
log.debug('Skipping to playQueueItemID %s, plex_id %s',
|
|
|
|
playqueue_item_id, plex_id)
|
|
|
|
found = True
|
|
|
|
for player in list(js.get_players().values()):
|
2021-10-31 20:44:27 +11:00
|
|
|
playqueue = app.PLAYQUEUES[player['playerid']]
|
2021-10-22 17:40:25 +11:00
|
|
|
for i, item in enumerate(playqueue.items):
|
|
|
|
if item.id == playqueue_item_id:
|
|
|
|
found = True
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
for i, item in enumerate(playqueue.items):
|
|
|
|
if item.plex_id == plex_id:
|
|
|
|
found = True
|
|
|
|
break
|
|
|
|
if found is True:
|
|
|
|
app.APP.player.play(playqueue.kodi_pl, None, False, i)
|
|
|
|
else:
|
|
|
|
log.error('Item not found to skip to')
|
|
|
|
|
|
|
|
|
2021-11-22 00:40:56 +11:00
|
|
|
def convert_xml_to_params(xml):
|
|
|
|
new_params = dict(xml.attrib)
|
|
|
|
for key in xml.attrib:
|
|
|
|
if key.startswith('query'):
|
|
|
|
new_params[key[5].lower() + key[6:]] = xml.get(key)
|
|
|
|
del new_params[key]
|
|
|
|
return new_params
|
|
|
|
|
|
|
|
|
|
|
|
def process_command(cmd=None, path=None, params=None):
|
2021-10-22 17:40:25 +11:00
|
|
|
"""cmd: a "Command" etree xml"""
|
2021-11-22 00:40:56 +11:00
|
|
|
path = cmd.get('path') if cmd is not None else path
|
|
|
|
if not path.startswith('/'):
|
|
|
|
path = '/' + path
|
|
|
|
if params is None:
|
|
|
|
params = convert_xml_to_params(cmd)
|
|
|
|
if path == '/player/playback/playMedia' and \
|
|
|
|
params.get('address') == 'node.plexapp.com':
|
2021-10-22 17:40:25 +11:00
|
|
|
process_node(cmd.get('queryKey'),
|
|
|
|
cmd.get('queryToken'),
|
|
|
|
cmd.get('queryOffset') or 0)
|
|
|
|
elif path == '/player/playback/playMedia':
|
|
|
|
with app.APP.lock_playqueues:
|
2021-11-22 00:40:56 +11:00
|
|
|
process_playlist(params.get('containerKey'),
|
|
|
|
params.get('type'),
|
|
|
|
params.get('key'),
|
|
|
|
params.get('offset'),
|
|
|
|
params.get('token'))
|
2021-10-22 17:40:25 +11:00
|
|
|
elif path == '/player/playback/refreshPlayQueue':
|
|
|
|
with app.APP.lock_playqueues:
|
2021-11-22 00:40:56 +11:00
|
|
|
process_refresh(params.get('playQueueID'))
|
2021-10-22 17:40:25 +11:00
|
|
|
elif path == '/player/playback/setParameters':
|
2021-11-22 00:40:56 +11:00
|
|
|
js.set_volume(int(params.get('volume')))
|
2021-10-22 17:40:25 +11:00
|
|
|
elif path == '/player/playback/play':
|
|
|
|
js.play()
|
|
|
|
elif path == '/player/playback/pause':
|
|
|
|
js.pause()
|
|
|
|
elif path == '/player/playback/stop':
|
|
|
|
js.stop()
|
|
|
|
elif path == '/player/playback/seekTo':
|
2021-11-22 00:40:56 +11:00
|
|
|
js.seek_to(float(params.get('offset', 0.0)) / 1000.0)
|
2021-10-22 17:40:25 +11:00
|
|
|
elif path == '/player/playback/stepForward':
|
|
|
|
js.smallforward()
|
|
|
|
elif path == '/player/playback/stepBack':
|
|
|
|
js.smallbackward()
|
|
|
|
elif path == '/player/playback/skipNext':
|
|
|
|
js.skipnext()
|
|
|
|
elif path == '/player/playback/skipPrevious':
|
|
|
|
js.skipprevious()
|
|
|
|
elif path == '/player/playback/skipTo':
|
2021-11-22 00:40:56 +11:00
|
|
|
skip_to(params.get('playQueueItemID'), params.get('key'))
|
2021-10-22 17:40:25 +11:00
|
|
|
elif path == '/player/navigation/moveUp':
|
|
|
|
js.input_up()
|
|
|
|
elif path == '/player/navigation/moveDown':
|
|
|
|
js.input_down()
|
|
|
|
elif path == '/player/navigation/moveLeft':
|
|
|
|
js.input_left()
|
|
|
|
elif path == '/player/navigation/moveRight':
|
|
|
|
js.input_right()
|
|
|
|
elif path == '/player/navigation/select':
|
|
|
|
js.input_select()
|
|
|
|
elif path == '/player/navigation/home':
|
|
|
|
js.input_home()
|
|
|
|
elif path == '/player/navigation/back':
|
|
|
|
js.input_back()
|
|
|
|
elif path == '/player/playback/setStreams':
|
2021-11-22 00:40:56 +11:00
|
|
|
process_streams(params.get('queryType'),
|
|
|
|
cast(int, params.get('videoStreamID')),
|
|
|
|
cast(int, params.get('audioStreamID')),
|
|
|
|
cast(int, params.get('subtitleStreamID')))
|
2021-10-22 17:40:25 +11:00
|
|
|
elif path == '/player/timeline/subscribe':
|
|
|
|
pass
|
|
|
|
elif path == '/player/timeline/unsubscribe':
|
|
|
|
pass
|
|
|
|
else:
|
2021-11-22 00:40:56 +11:00
|
|
|
if cmd is None:
|
|
|
|
log.error('Unknown request_path: %s with params %s', path, params)
|
|
|
|
else:
|
|
|
|
log.error('Unknown Plex companion path/command: %s: %s',
|
|
|
|
cmd.tag, cmd.attrib)
|
|
|
|
return False
|
2021-10-22 17:40:25 +11:00
|
|
|
return True
|