#!/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: playqueue = app.PLAYQUEUES.from_plex_type(typus) except ValueError: # 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]) playqueue = app.PLAYQUEUES.from_plex_type(api.plex_type) 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) def process_streams(plex_type, video_stream_id, audio_stream_id, subtitle_stream_id): """ Plex Companion client adjusted audio or subtitle stream """ playqueue = app.PLAYQUEUES.from_plex_type(plex_type) 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 playqueue = app.PLAYQUEUES.from_plex_type(plex_type) playqueue.clear() return playqueue = app.PLAYQUEUES.from_plex_type(xml[0].attrib['type']) 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()): playqueue = app.PLAYQUEUES[player['playerid']] 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') 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): """cmd: a "Command" etree xml""" 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': process_node(cmd.get('queryKey'), cmd.get('queryToken'), cmd.get('queryOffset') or 0) elif path == '/player/playback/playMedia': with app.APP.lock_playqueues: process_playlist(params.get('containerKey'), params.get('type'), params.get('key'), params.get('offset'), params.get('token')) elif path == '/player/playback/refreshPlayQueue': with app.APP.lock_playqueues: process_refresh(params.get('playQueueID')) elif path == '/player/playback/setParameters': js.set_volume(int(params.get('volume'))) 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': js.seek_to(float(params.get('offset', 0.0)) / 1000.0) 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': skip_to(params.get('playQueueItemID'), params.get('key')) 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': process_streams(params.get('queryType'), cast(int, params.get('videoStreamID')), cast(int, params.get('audioStreamID')), cast(int, params.get('subtitleStreamID'))) elif path == '/player/timeline/subscribe': pass elif path == '/player/timeline/unsubscribe': pass else: 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 return True