forked from platypush/platypush
Added Kodi support to new media webplayer
This commit is contained in:
parent
f86eeef549
commit
c78789e644
3 changed files with 379 additions and 161 deletions
|
@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
Loading…
Reference in a new issue