From c78789e64451359383aec2c431b04c29fbd9b083 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sat, 29 Jun 2019 00:06:03 +0200 Subject: [PATCH] Added Kodi support to new media webplayer --- .../static/js/plugins/media/players/kodi.js | 54 +- platypush/backend/music/mopidy.py | 22 +- platypush/plugins/media/kodi.py | 464 +++++++++++++----- 3 files changed, 379 insertions(+), 161 deletions(-) diff --git a/platypush/backend/http/static/js/plugins/media/players/kodi.js b/platypush/backend/http/static/js/plugins/media/players/kodi.js index 5bc92408d0..66b02c5f67 100644 --- a/platypush/backend/http/static/js/plugins/media/players/kodi.js +++ b/platypush/backend/http/static/js/plugins/media/players/kodi.js @@ -9,7 +9,7 @@ MediaPlayers.kodi = Vue.extend({ type: Object, default: () => { return { - url: undefined, + host: undefined, }; }, }, @@ -18,6 +18,8 @@ MediaPlayers.kodi = Vue.extend({ type: Object, default: () => { return { + file: true, + generic: true, youtube: true, }; }, @@ -30,50 +32,54 @@ MediaPlayers.kodi = Vue.extend({ }, computed: { - host: function() { - if (!this.device.url) { - return; - } - - return this.device.url.match(/^https?:\/\/([^:]+):(\d+).*$/)[1]; - }, - name: function() { - return this.host; - }, - - port: function() { - if (!this.device.url) { - return; - } - - return parseInt(this.device.url.match(/^https?:\/\/([^:]+):(\d+).*$/)[2]); + return this.device.host; }, text: function() { - return 'Kodi '.concat('[', this.host, ']'); + return 'Kodi '.concat('[', this.device.host, ']'); }, }, methods: { scan: async function() { - if (!('media.kodi' in __plugins__)) { + const plugin = __plugins__['media.kodi']; + if (!plugin) { return []; } - return [ - { url: __plugins__['media.kodi'].url } - ]; + return [{ host: plugin.host }]; }, status: async function() { - return {}; + return await request('media.kodi.status'); }, play: async function(item) { + return await request('media.kodi.play', { + resource: item.url, + subtitles: item.subtitles_url, + }); + }, + + pause: async function() { + return await request('media.kodi.pause'); }, stop: async function() { + return await request('media.kodi.stop'); + }, + + seek: async function(position) { + return await request('media.kodi.set_position', { + position: position, + }); + }, + + setVolume: async function(volume) { + return await request('media.kodi.set_volume', { + volume: volume, + }); }, }, }); diff --git a/platypush/backend/music/mopidy.py b/platypush/backend/music/mopidy.py index 2e9d6fce70..4289a4f932 100644 --- a/platypush/backend/music/mopidy.py +++ b/platypush/backend/music/mopidy.py @@ -1,7 +1,5 @@ import json -import queue import re -import threading import time from platypush.backend import Backend @@ -12,6 +10,7 @@ from platypush.message.event.music import MusicPlayEvent, MusicPauseEvent, \ MuteChangeEvent, SeekChangeEvent +# noinspection PyUnusedLocal class MusicMopidyBackend(Backend): """ This backend listens for events on a Mopidy music server streaming port. @@ -33,7 +32,7 @@ class MusicMopidyBackend(Backend): * :class:`platypush.message.event.music.SeekChangeEvent` if a track seek event occurs Requires: - * **websockets** (``pip install websockets``) + * **websocket-client** (``pip install websocket-client``) * Mopidy installed and the HTTP service enabled """ @@ -52,7 +51,8 @@ class MusicMopidyBackend(Backend): except Exception as e: self.logger.warning('Unable to get mopidy status: {}'.format(str(e))) - def _parse_track(self, track, pos=None): + @staticmethod + def _parse_track(track, pos=None): if not track: return {} @@ -85,7 +85,6 @@ class MusicMopidyBackend(Backend): return conv_track - def _communicate(self, msg): import websocket @@ -103,7 +102,6 @@ class MusicMopidyBackend(Backend): ws.close() return response - def _get_tracklist_status(self): return { 'repeat': self._communicate({ @@ -139,8 +137,8 @@ class MusicMopidyBackend(Backend): return self.bus.post(MusicPlayEvent(status=status, track=track)) elif event == 'track_playback_ended' or ( - event == 'playback_state_changed' - and msg.get('new_state') == 'stopped'): + event == 'playback_state_changed' + and msg.get('new_state') == 'stopped'): status['state'] = 'stop' track = self._parse_track(track) self.bus.post(MusicStopEvent(status=status, track=track)) @@ -170,15 +168,15 @@ class MusicMopidyBackend(Backend): elif event == 'mute_changed': status['mute'] = msg.get('mute') self.bus.post(MuteChangeEvent(mute=status['mute'], - status=status, track=track)) + status=status, track=track)) elif event == 'seeked': status['position'] = msg.get('time_position')/1000 self.bus.post(SeekChangeEvent(position=status['position'], - status=status, track=track)) + status=status, track=track)) elif event == 'tracklist_changed': tracklist = [self._parse_track(t, pos=i) - for i, t in enumerate(self._communicate({ - 'method': 'core.tracklist.get_tl_tracks' }))] + for i, t in enumerate(self._communicate({ + 'method': 'core.tracklist.get_tl_tracks'}))] self.bus.post(PlaylistChangeEvent(changes=tracklist)) elif event == 'options_changed': diff --git a/platypush/plugins/media/kodi.py b/platypush/plugins/media/kodi.py index 0256da829b..6d4bcec2aa 100644 --- a/platypush/plugins/media/kodi.py +++ b/platypush/plugins/media/kodi.py @@ -1,35 +1,57 @@ +import json import re +import threading +import time -from platypush.plugins import Plugin, action +from platypush.context import get_bus +from platypush.plugins import action +from platypush.plugins.media import MediaPlugin, PlayerState +from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, MediaStopEvent, \ + MediaSeekEvent, MediaVolumeChangedEvent, NewPlayingMediaEvent -class MediaKodiPlugin(Plugin): +# noinspection PyUnusedLocal +class MediaKodiPlugin(MediaPlugin): """ Plugin to interact with a Kodi media player instance Requires: * **kodi-json** (``pip install kodi-json``) + * **websocket-client** (``pip install websocket-client``), optional, for player events support """ - def __init__(self, url, username=None, password=None, *args, **kwargs): + def __init__(self, host, http_port=8080, websocket_port=9090, username=None, password=None, **kwargs): """ - :param url: URL for the JSON-RPC calls to the Kodi system (example: http://localhost:8080/jsonrpc) - :type url: str + :param host: Kodi host name or IP + :type host: str + + :param http_port: Kodi JSON RPC web port. Remember to enable "Allow remote control via HTTP" + in Kodi service settings -> advanced configuration and "Allow remote control from applications" + on this system and, optionally, on other systems if the Kodi server is on another machine + :type http_port: int + + :param websocket_port: Kodi JSON RPC websocket port, used to receive player events + :type websocket_port: int :param username: Kodi username (optional) :type username: str :param password: Kodi password (optional) - :type username: str + :type password: str """ - super().__init__(*args, **kwargs) + super().__init__(**kwargs) - self.url = url + self.host = host + self.http_port = http_port + self.websocket_port = websocket_port + self.url = 'http://{}:{}/jsonrpc'.format(host, http_port) + self.websocket_url = 'ws://{}:{}/jsonrpc'.format(host, websocket_port) self.username = username self.password = password - + self._ws = None + threading.Thread(target=self._websocket_thread()).start() def _get_kodi(self): from kodijson import Kodi @@ -37,7 +59,8 @@ class MediaKodiPlugin(Plugin): args = [self.url] if self.username: args += [self.username] - if self.password: args += [self.password] + if self.password: + args += [self.password] return Kodi(*args) @@ -45,65 +68,91 @@ class MediaKodiPlugin(Plugin): kodi = self._get_kodi() players = kodi.Player.GetActivePlayers().get('result', []) if not players: - raise RuntimeError('No players found') + return None return players.pop().get('playerid') - @action - def get_active_players(self): + def _websocket_thread(self): """ - Get the list of active players + Initialize the websocket JSON RPC interface, if available, to receive player notifications """ - result = self._get_kodi().Player.GetActivePlayers() - return (result.get('result'), result.get('error')) + def thread_hndl(): + try: + import websocket + except ImportError: + self.logger.warning('websocket-client is not installed, Kodi events will be disabled') + return + + if not self._ws: + self._ws = websocket.WebSocketApp(self.websocket_url, + on_message=self._on_ws_msg(), + on_error=self._on_ws_error(), + on_close=self._on_ws_close()) + + self.logger.info('Kodi websocket interface for events started') + self._ws.run_forever() + + return thread_hndl + + def _post_event(self, evt_type, **evt): + bus = get_bus() + bus.post(evt_type(player=self.host, plugin='media.kodi', **evt)) + + def _on_ws_msg(self): + def hndl(ws, msg): + self.logger.info("Received Kodi message: {}".format(msg)) + msg = json.loads(msg) + method = msg.get('method') + + if method == 'Player.OnPlay': + item = msg.get('params', {}).get('data', {}).get('item', {}) + player = msg.get('params', {}).get('data', {}).get('player', {}) + self._post_event(MediaPlayEvent, player_id=player.get('playerid'), + title=item.get('title'), media_type=item.get('type')) + elif method == 'Player.OnPause': + item = msg.get('params', {}).get('data', {}).get('item', {}) + player = msg.get('params', {}).get('data', {}).get('player', {}) + self._post_event(MediaPauseEvent, player_id=player.get('playerid'), + title=item.get('title'), media_type=item.get('type')) + elif method == 'Player.OnStop': + player = msg.get('params', {}).get('data', {}).get('player', {}) + self._post_event(MediaStopEvent, player_id=player.get('playerid')) + elif method == 'Player.OnSeek': + player = msg.get('params', {}).get('data', {}).get('player', {}) + position = self._time_obj_to_pos(player.get('seekoffset')) + self._post_event(MediaSeekEvent, position=position, player_id=player.get('playerid')) + elif method == 'Application.OnVolumeChanged': + volume = msg.get('params', {}).get('data', {}).get('volume') + self._post_event(MediaVolumeChangedEvent, volume=volume) + + return hndl + + def _on_ws_error(self): + def hndl(ws, error): + self.logger.warning("Kodi websocket connection error: {}".format(error)) + return hndl + + def _on_ws_close(self): + def hndl(ws): + self._ws = None + self.logger.warning("Kodi websocket connection closed") + time.sleep(5) + self._websocket_thread() + + return hndl + + def _build_result(self, result): + status = self.status().output + status['result'] = result.get('result') + return status, result.get('error') @action - def get_movies(self, *args, **kwargs): - """ - Get the list of movies on the Kodi server - """ - - result = self._get_kodi().VideoLibrary.GetMovies() - return (result.get('result'), result.get('error')) - - @action - def play_pause(self, player_id=None, *args, **kwargs): - """ - Play/pause the current media - """ - - if not player_id: - player_id = self._get_player_id() - - result = self._get_kodi().Player.PlayPause(playerid=player_id) - return (result.get('result'), result.get('error')) - - @action - def stop(self, player_id=None, *args, **kwargs): - """ - Stop the current media - """ - - if not player_id: - player_id = self._get_player_id() - - result = self._get_kodi().Player.Stop(playerid=player_id) - return (result.get('result'), result.get('error')) - - @action - def notify(self, title, message, *args, **kwargs): - """ - Send a notification to the Kodi UI - """ - - result = self._get_kodi().GUI.ShowNotification(title=title, message=message) - return (result.get('result'), result.get('error')) - - @action - def open(self, resource, *args, **kwargs): + def play(self, resource, *args, **kwargs): """ Open and play the specified file or URL + + :param resource: URL or path to the media to be played """ if resource.startswith('youtube:video:') \ @@ -112,14 +161,74 @@ class MediaKodiPlugin(Plugin): m2 = re.match('https://www.youtube.com/watch?v=([^:?&#/]+)', resource) youtube_id = None - if m1: youtube_id = m1.group(1) - elif m2: youtube_id = m2.group(1) + if m1: + youtube_id = m1.group(1) + elif m2: + youtube_id = m2.group(1) if youtube_id: resource = 'plugin://plugin.video.youtube/?action=play_video&videoid=' + youtube_id + if resource.startswith('file://'): + resource = resource[7:] + result = self._get_kodi().Player.Open(item={'file': resource}) - return (result.get('result'), result.get('error')) + return self._build_result(result) + + @action + def pause(self, player_id=None, *args, **kwargs): + """ + Play/pause the current media + """ + + if player_id is None: + player_id = self._get_player_id() + if player_id is None: + return None, 'No active players found' + + result = self._get_kodi().Player.PlayPause(playerid=player_id) + return self._build_result(result) + + @action + def get_active_players(self): + """ + Get the list of active players + """ + + result = self._get_kodi().Player.GetActivePlayers() + return result.get('result'), result.get('error') + + @action + def get_movies(self, *args, **kwargs): + """ + Get the list of movies on the Kodi server + """ + + result = self._get_kodi().VideoLibrary.GetMovies() + return result.get('result'), result.get('error') + + @action + def stop(self, player_id=None, *args, **kwargs): + """ + Stop the current media + """ + + if player_id is None: + player_id = self._get_player_id() + if player_id is None: + return None, 'No active players found' + + result = self._get_kodi().Player.Stop(playerid=player_id) + return self._build_result(result) + + @action + def notify(self, title, message, *args, **kwargs): + """ + Send a notification to the Kodi UI + """ + + result = self._get_kodi().GUI.ShowNotification(title=title, message=message) + return result.get('result'), result.get('error') @action def left(self, *args, **kwargs): @@ -128,7 +237,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Input.Left() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def right(self, *args, **kwargs): @@ -137,7 +246,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Input.Right() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def up(self, *args, **kwargs): @@ -146,7 +255,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Input.Up() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def down(self, *args, **kwargs): @@ -155,7 +264,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Input.Down() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def back_btn(self, *args, **kwargs): @@ -164,7 +273,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Input.Back() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def select(self, *args, **kwargs): @@ -173,7 +282,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Input.Select() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def send_text(self, text, *args, **kwargs): @@ -185,44 +294,44 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Input.SendText(text=text) - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def get_volume(self, *args, **kwargs): result = self._get_kodi().Application.GetProperties( properties=['volume']) - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action - def volup(self, *args, **kwargs): - """ Volume up by 10% """ + def volup(self, step=10.0, *args, **kwargs): + """ Volume up (default: +10%) """ volume = self._get_kodi().Application.GetProperties( properties=['volume']).get('result', {}).get('volume') - result = self._get_kodi().Application.SetVolume(volume=min(volume+10, 100)) - return (result.get('result'), result.get('error')) + result = self._get_kodi().Application.SetVolume(volume=min(volume+step, 100)) + return self._build_result(result) @action - def voldown(self, *args, **kwargs): - """ Volume down by 10% """ + def voldown(self, step=10.0, *args, **kwargs): + """ Volume down (default: -10%) """ volume = self._get_kodi().Application.GetProperties( properties=['volume']).get('result', {}).get('volume') - result = self._get_kodi().Application.SetVolume(volume=max(volume-10, 0)) - return (result.get('result'), result.get('error')) + result = self._get_kodi().Application.SetVolume(volume=max(volume-step, 0)) + return self._build_result(result) @action def set_volume(self, volume, *args, **kwargs): """ Set the application volume - :param volume: Volume to set + :param volume: Volume to set between 0 and 100 :type volume: int """ result = self._get_kodi().Application.SetVolume(volume=volume) - return (result.get('result'), result.get('error')) + return self._build_result(result) @action def mute(self, *args, **kwargs): @@ -234,7 +343,7 @@ class MediaKodiPlugin(Plugin): properties=['muted']).get('result', {}).get('muted') result = self._get_kodi().Application.SetMute(mute=(not muted)) - return (result.get('result'), result.get('error')) + return self._build_result(result) @action def is_muted(self, *args, **kwargs): @@ -243,7 +352,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Application.GetProperties(properties=['muted']) - return (result.get('result'), result.get('error')) + return result.get('result') @action def scan_video_library(self, *args, **kwargs): @@ -252,7 +361,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().VideoLibrary.Scan() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def scan_audio_library(self, *args, **kwargs): @@ -261,7 +370,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().AudioLibrary.Scan() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def clean_video_library(self, *args, **kwargs): @@ -270,7 +379,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().VideoLibrary.Clean() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def clean_audio_library(self, *args, **kwargs): @@ -279,7 +388,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().AudioLibrary.Clean() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def quit(self, *args, **kwargs): @@ -288,7 +397,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Application.Quit() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def get_songs(self, *args, **kwargs): @@ -297,7 +406,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Application.GetSongs() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def get_artists(self, *args, **kwargs): @@ -306,7 +415,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Application.GetArtists() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def get_albums(self, *args, **kwargs): @@ -315,7 +424,7 @@ class MediaKodiPlugin(Plugin): """ result = self._get_kodi().Application.GetAlbums() - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def fullscreen(self, *args, **kwargs): @@ -327,7 +436,7 @@ class MediaKodiPlugin(Plugin): properties=['fullscreen']).get('result', {}).get('fullscreen') result = self._get_kodi().GUI.SetFullscreen(fullscreen=(not fullscreen)) - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def shuffle(self, player_id=None, shuffle=None, *args, **kwargs): @@ -335,8 +444,10 @@ class MediaKodiPlugin(Plugin): Set/unset shuffle mode """ - if not player_id: + if player_id is None: player_id = self._get_player_id() + if player_id is None: + return None, 'No active players found' if shuffle is None: shuffle = self._get_kodi().Player.GetProperties( @@ -345,7 +456,7 @@ class MediaKodiPlugin(Plugin): result = self._get_kodi().Player.SetShuffle( playerid=player_id, shuffle=(not shuffle)) - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') @action def repeat(self, player_id=None, repeat=None, *args, **kwargs): @@ -353,8 +464,10 @@ class MediaKodiPlugin(Plugin): Set/unset repeat mode """ - if not player_id: + if player_id is None: player_id = self._get_player_id() + if player_id is None: + return None, 'No active players found' if repeat is None: repeat = self._get_kodi().Player.GetProperties( @@ -365,74 +478,175 @@ class MediaKodiPlugin(Plugin): playerid=player_id, repeat='off' if repeat in ('one','all') else 'off') - return (result.get('result'), result.get('error')) + return result.get('result'), result.get('error') + + @staticmethod + def _time_pos_to_obj(t): + hours = int(t/3600) + minutes = int((t - hours*3600)/60) + seconds = t - hours*3600 - minutes*60 + milliseconds = t - int(t) + + return { + 'hours': hours, + 'minutes': minutes, + 'seconds': seconds, + 'milliseconds': milliseconds, + } + + @staticmethod + def _time_obj_to_pos(t): + return t.get('hours', 0) * 3600 + t.get('minutes', 0) * 60 + \ + t.get('seconds', 0) + t.get('milliseconds', 0)/1000 @action def seek(self, position, player_id=None, *args, **kwargs): """ - Move the cursor to the specified position in seconds + Move to the specified time position in seconds :param position: Seek time in seconds - :type position: int + :type position: float """ - if not player_id: + if player_id is None: player_id = self._get_player_id() + if player_id is None: + return None, 'No active players found' - hours = int(position/3600) - minutes = int((position - hours*3600)/60) - seconds = position - hours*3600 - minutes*60 - - position = { - 'hours': hours, - 'minutes': minutes, - 'seconds': seconds, - 'milliseconds': 0, - } - + position = self._time_pos_to_obj(position) result = self._get_kodi().Player.Seek(playerid=player_id, value=position) - return (result.get('result'), result.get('error')) + return self._build_result(result) @action - def back(self, delta_seconds=60, player_id=None, *args, **kwargs): + def set_position(self, position, player_id=None, *args, **kwargs): + """ + Move to the specified time position in seconds + + :param position: Seek time in seconds + :type position: float + """ + return self.seek(position=position, player_id=player_id, *args, **kwargs) + + @action + def back(self, offset=60, player_id=None, *args, **kwargs): """ Move the player execution backward by delta_seconds - :param delta_seconds: Backward seek duration (default: 60 seconds) - :type delta_seconds: int + :param offset: Backward seek duration (default: 60 seconds) + :type offset: float """ - if not player_id: + if player_id is None: player_id = self._get_player_id() + if player_id is None: + return None, 'No active players found' position = self._get_kodi().Player.GetProperties( playerid=player_id, properties=['time']).get('result', {}).get('time', {}) - position = position.get('hours', 0)*3600 + \ - position.get('minutes', 0)*60 + position.get('seconds', 0) - delta_seconds - + position = self._time_obj_to_pos(position) return self.seek(player_id=player_id, position=position) @action - def forward(self, delta_seconds=60, player_id=None, *args, **kwargs): + def forward(self, offset=60, player_id=None, *args, **kwargs): """ Move the player execution forward by delta_seconds - :param delta_seconds: Forward seek duration (default: 60 seconds) - :type delta_seconds: int + :param offset: Forward seek duration (default: 60 seconds) + :type offset: float """ - if not player_id: + if player_id is None: player_id = self._get_player_id() + if player_id is None: + return None, 'No active players found' position = self._get_kodi().Player.GetProperties( playerid=player_id, properties=['time']).get('result', {}).get('time', {}) - position = position.get('hours', 0)*3600 + \ - position.get('minutes', 0)*60 + position.get('seconds', 0) + delta_seconds - + position = self._time_obj_to_pos(position) return self.seek(player_id=player_id, position=position) + @action + def status(self, player_id=None): + media_props = { + 'album': 'album', + 'artist': 'artist', + 'duration': 'duration', + 'fanart': 'fanart', + 'file': 'file', + 'season': 'season', + 'showtitle': 'showtitle', + 'streamdetails': 'streamdetails', + 'thumbnail': 'thumbnail', + 'title': 'title', + 'tvshowid': 'tvshowid', + 'url': 'file', + } + + app_props = { + 'volume': 'volume', + 'mute': 'muted', + } + + player_props = { + "duration": "totaltime", + "position": "time", + "repeat": "repeat", + "seekable": "canseek", + 'speed': 'speed', + "subtitles": "subtitles", + } + + ret = {'state': PlayerState.IDLE.value} + + try: + kodi = self._get_kodi() + players = kodi.Player.GetActivePlayers().get('result', []) + except: + return ret + + ret['state'] = PlayerState.STOP.value + app = kodi.Application.GetProperties(properties=list(set(app_props.values()))).get('result', {}) + + for status_prop, kodi_prop in app_props.items(): + ret[status_prop] = app.get(kodi_prop) + + if not players: + return ret + + if player_id is None: + player_id = players.pop().get('playerid') + else: + for p in players: + if p['player_id'] == player_id: + player_id = p + break + + if player_id is None: + return ret + + media = kodi.Player.GetItem(playerid=player_id, + properties=list(set(media_props.values()))).get('result', {}).get('item', {}) + + for status_prop, kodi_prop in media_props.items(): + ret[status_prop] = media.get(kodi_prop) + + player_info = kodi.Player.GetProperties( + playerid=player_id, + properties=list(set(player_props.values()))).get('result', {}) + + for status_prop, kodi_prop in player_props.items(): + ret[status_prop] = player_info.get(kodi_prop) + + if ret['duration']: + ret['duration'] = self._time_obj_to_pos(ret['duration']) + + if ret['position']: + ret['position'] = self._time_obj_to_pos(ret['position']) + + ret['state'] = PlayerState.PAUSE.value if player_info.get('speed', 0) == 0 else PlayerState.PLAY.value + return ret + # vim:sw=4:ts=4:et: -