forked from platypush/platypush
LINT+Black+stability fixes for some plugins that hadn't been touched in a while.
- media.mplayer - media.omxplayer - media.vlc - music.mpd - music.snapcast
This commit is contained in:
parent
2aefc4e5c8
commit
3086dd86fc
6 changed files with 711 additions and 702 deletions
|
@ -4,6 +4,7 @@ import select
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from typing import Any, Collection, Dict, List, Optional
|
||||||
|
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
from platypush.message.response import Response
|
from platypush.message.response import Response
|
||||||
|
@ -39,10 +40,9 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mplayer_bin=None,
|
mplayer_bin: Optional[str] = None,
|
||||||
mplayer_timeout=_mplayer_default_communicate_timeout,
|
mplayer_timeout: float = _mplayer_default_communicate_timeout,
|
||||||
args=None,
|
args: Optional[Collection[str]] = None,
|
||||||
*argv,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -52,21 +52,13 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
:param mplayer_bin: Path to the MPlayer executable (default: search for
|
:param mplayer_bin: Path to the MPlayer executable (default: search for
|
||||||
the first occurrence in your system PATH environment variable)
|
the first occurrence in your system PATH environment variable)
|
||||||
:type mplayer_bin: str
|
|
||||||
|
|
||||||
:param mplayer_timeout: Timeout in seconds to wait for more data
|
:param mplayer_timeout: Timeout in seconds to wait for more data
|
||||||
from MPlayer before considering a response ready (default: 0.5 seconds)
|
from MPlayer before considering a response ready (default: 0.5 seconds)
|
||||||
:type mplayer_timeout: float
|
|
||||||
|
|
||||||
:param subtitles: Path to the subtitles file
|
|
||||||
:type subtitles: str
|
|
||||||
|
|
||||||
:param args: Default arguments that will be passed to the MPlayer
|
:param args: Default arguments that will be passed to the MPlayer
|
||||||
executable
|
executable
|
||||||
:type args: list
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(*argv, **kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.args = args or []
|
self.args = args or []
|
||||||
self._init_mplayer_bin(mplayer_bin=mplayer_bin)
|
self._init_mplayer_bin(mplayer_bin=mplayer_bin)
|
||||||
|
@ -106,17 +98,15 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
try:
|
try:
|
||||||
self._player.terminate()
|
self._player.terminate()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(
|
self.logger.debug('Failed to quit mplayer before _exec: %s', e)
|
||||||
'Failed to quit mplayer before _exec: {}'.format(str(e))
|
|
||||||
)
|
|
||||||
|
|
||||||
mplayer_args = mplayer_args or []
|
m_args = mplayer_args or []
|
||||||
args = [self.mplayer_bin] + self._mplayer_bin_default_args
|
args = [self.mplayer_bin] + self._mplayer_bin_default_args
|
||||||
for arg in self.args + mplayer_args:
|
for arg in (*self.args, *m_args):
|
||||||
if arg not in args:
|
if arg not in args:
|
||||||
args.append(arg)
|
args.append(arg)
|
||||||
|
|
||||||
popen_args = {
|
popen_args: Dict[str, Any] = {
|
||||||
'stdin': subprocess.PIPE,
|
'stdin': subprocess.PIPE,
|
||||||
'stdout': subprocess.PIPE,
|
'stdout': subprocess.PIPE,
|
||||||
}
|
}
|
||||||
|
@ -140,10 +130,13 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
def args_pprint(txt):
|
def args_pprint(txt):
|
||||||
lc = txt.lower()
|
lc = txt.lower()
|
||||||
if lc[0] == '[':
|
if lc[0] == '[':
|
||||||
return '%s=None' % lc[1:-1]
|
return f'{lc[1:-1]}=None'
|
||||||
return lc
|
return lc
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
if not mplayer.stdout:
|
||||||
|
break
|
||||||
|
|
||||||
line = mplayer.stdout.readline()
|
line = mplayer.stdout.readline()
|
||||||
if not line:
|
if not line:
|
||||||
break
|
break
|
||||||
|
@ -153,7 +146,7 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
args = line.split()
|
args = line.split()
|
||||||
cmd_name = args.pop(0)
|
cmd_name = args.pop(0)
|
||||||
arguments = ', '.join([args_pprint(a) for a in args])
|
arguments = ', '.join([args_pprint(a) for a in args])
|
||||||
self._actions[cmd_name] = '{}({})'.format(cmd_name, arguments)
|
self._actions[cmd_name] = f'{cmd_name}({arguments})'
|
||||||
|
|
||||||
def _exec(
|
def _exec(
|
||||||
self, cmd, *args, mplayer_args=None, prefix=None, wait_for_response=False
|
self, cmd, *args, mplayer_args=None, prefix=None, wait_for_response=False
|
||||||
|
@ -161,22 +154,27 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
cmd_name = cmd
|
cmd_name = cmd
|
||||||
response = None
|
response = None
|
||||||
|
|
||||||
if cmd_name == 'loadfile' or cmd_name == 'loadlist':
|
if cmd_name in {'loadfile', 'loadlist'}:
|
||||||
self._init_mplayer(mplayer_args)
|
self._init_mplayer(mplayer_args)
|
||||||
else:
|
else:
|
||||||
if not self._player:
|
if not self._player:
|
||||||
self.logger.warning('MPlayer is not running')
|
self.logger.warning('MPlayer is not running')
|
||||||
|
|
||||||
cmd = '{}{}{}{}\n'.format(
|
cmd = (
|
||||||
prefix + ' ' if prefix else '',
|
f'{prefix + " " if prefix else ""}'
|
||||||
cmd_name,
|
+ cmd_name
|
||||||
' ' if args else '',
|
+ (" " if args else "")
|
||||||
' '.join(repr(a) for a in args),
|
+ " ".join(repr(a) for a in args)
|
||||||
|
+ '\n'
|
||||||
).encode()
|
).encode()
|
||||||
|
|
||||||
if not self._player:
|
if not self._player:
|
||||||
|
self.logger.warning('Cannot send command %s: player unavailable', cmd)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._player.stdin:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Cannot send command {}: player unavailable'.format(cmd)
|
'Could not communicate with the mplayer process: the stdin is closed'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -199,6 +197,12 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
if not wait_for_response:
|
if not wait_for_response:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not (self._player and self._player.stdout):
|
||||||
|
self.logger.warning(
|
||||||
|
'Could not communicate with the mplayer process: the stdout is closed'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
poll = select.poll()
|
poll = select.poll()
|
||||||
poll.register(self._player.stdout, select.POLLIN)
|
poll.register(self._player.stdout, select.POLLIN)
|
||||||
last_read_time = time.time()
|
last_read_time = time.time()
|
||||||
|
@ -209,11 +213,16 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
if not self._player:
|
if not self._player:
|
||||||
break
|
break
|
||||||
|
|
||||||
line = self._player.stdout.readline().decode()
|
buf = self._player.stdout.readline()
|
||||||
|
line = buf.decode() if isinstance(buf, bytes) else buf
|
||||||
last_read_time = time.time()
|
last_read_time = time.time()
|
||||||
|
|
||||||
if line.startswith('ANS_'):
|
if line.startswith('ANS_'):
|
||||||
m = re.match('^([^=]+)=(.*)$', line[4:])
|
m = re.match('^([^=]+)=(.*)$', line[4:])
|
||||||
|
if not m:
|
||||||
|
self.logger.warning('Unexpected response: %s', line)
|
||||||
|
break
|
||||||
|
|
||||||
k, v = m.group(1), m.group(2)
|
k, v = m.group(1), m.group(2)
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if v == 'yes':
|
if v == 'yes':
|
||||||
|
@ -222,7 +231,8 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
v = False
|
v = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
v = eval(v)
|
if isinstance(v, str):
|
||||||
|
v = eval(v) # pylint: disable=eval-used
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -272,25 +282,26 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
bus.post(evt_type(player='local', plugin='media.mplayer', **evt))
|
bus.post(evt_type(player='local', plugin='media.mplayer', **evt))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource, subtitles=None, mplayer_args=None):
|
def play(
|
||||||
|
self,
|
||||||
|
resource: str,
|
||||||
|
subtitles: Optional[str] = None,
|
||||||
|
mplayer_args: Optional[List[str]] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Play a resource.
|
Play a resource.
|
||||||
|
|
||||||
:param resource: Resource to play - can be a local file or a remote URL
|
:param resource: Resource to play - can be a local file or a remote URL
|
||||||
:type resource: str
|
|
||||||
|
|
||||||
:param subtitles: Path to optional subtitle file
|
:param subtitles: Path to optional subtitle file
|
||||||
:type subtitles: str
|
|
||||||
|
|
||||||
:param mplayer_args: Extra runtime arguments that will be passed to the
|
:param mplayer_args: Extra runtime arguments that will be passed to the
|
||||||
MPlayer executable
|
MPlayer executable
|
||||||
:type mplayer_args: list[str]
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._post_event(MediaPlayRequestEvent, resource=resource)
|
self._post_event(MediaPlayRequestEvent, resource=resource)
|
||||||
if subtitles:
|
if subtitles:
|
||||||
mplayer_args = mplayer_args or []
|
subs = self.get_subtitles_file(subtitles)
|
||||||
mplayer_args += ['-sub', self.get_subtitles_file(subtitles)]
|
if subs:
|
||||||
|
mplayer_args = list(mplayer_args or []) + ['-sub', subs]
|
||||||
|
|
||||||
resource = self._get_resource(resource)
|
resource = self._get_resource(resource)
|
||||||
if resource.startswith('file://'):
|
if resource.startswith('file://'):
|
||||||
|
@ -305,67 +316,78 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def pause(self):
|
def pause(self, *_, **__):
|
||||||
"""Toggle the paused state"""
|
"""Toggle the paused state"""
|
||||||
self._exec('pause')
|
self._exec('pause')
|
||||||
self._post_event(MediaPauseEvent)
|
self._post_event(MediaPauseEvent)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def stop(self):
|
def stop(self, *_, **__):
|
||||||
"""Stop the playback"""
|
"""Stop the playback"""
|
||||||
# return self._exec('stop')
|
# return self._exec('stop')
|
||||||
self.quit()
|
self.quit()
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def quit(self):
|
def quit(self, *_, **__):
|
||||||
"""Quit the player"""
|
"""Quit the player"""
|
||||||
self._exec('quit')
|
self._exec('quit')
|
||||||
self._post_event(MediaStopEvent)
|
self._post_event(MediaStopEvent)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def voldown(self, step=10.0):
|
def voldown(self, *_, step=10.0, **__):
|
||||||
"""Volume down by (default: 10)%"""
|
"""Volume down by (default: 10)%"""
|
||||||
self._exec('volume', -step * 10)
|
self._exec('volume', -step * 10)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def volup(self, step=10.0):
|
def volup(self, *_, step=10.0, **__):
|
||||||
"""Volume up by (default: 10)%"""
|
"""Volume up by (default: 10)%"""
|
||||||
self._exec('volume', step * 10)
|
self._exec('volume', step * 10)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def back(self, offset=30.0):
|
def back(self, *_, offset=30.0, **__):
|
||||||
"""Back by (default: 30) seconds"""
|
"""Back by (default: 30) seconds"""
|
||||||
self.step_property('time_pos', -offset)
|
self.step_property('time_pos', -offset)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def forward(self, offset=30.0):
|
def forward(self, *_, offset=30.0, **__):
|
||||||
"""Forward by (default: 30) seconds"""
|
"""Forward by (default: 30) seconds"""
|
||||||
self.step_property('time_pos', offset)
|
self.step_property('time_pos', offset)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def toggle_subtitles(self):
|
def toggle_subtitles(self, *_, **__):
|
||||||
"""Toggle the subtitles visibility"""
|
"""Toggle the subtitles visibility"""
|
||||||
subs = self.get_property('sub_visibility').output.get('sub_visibility')
|
response: dict = (
|
||||||
|
self.get_property('sub_visibility').output or {} # type: ignore
|
||||||
|
)
|
||||||
|
subs = response.get('sub_visibility')
|
||||||
self._exec('sub_visibility', int(not subs))
|
self._exec('sub_visibility', int(not subs))
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def add_subtitles(self, filename, **__):
|
def add_subtitles(self, filename: str, **__):
|
||||||
"""Sets media subtitles from filename"""
|
"""
|
||||||
|
Sets media subtitles from filename
|
||||||
|
|
||||||
|
:param filename: Subtitles file.
|
||||||
|
"""
|
||||||
self._exec('sub_visibility', 1)
|
self._exec('sub_visibility', 1)
|
||||||
self._exec('sub_load', filename)
|
self._exec('sub_load', filename)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def remove_subtitles(self, index=None):
|
def remove_subtitles(self, *_, index: Optional[int] = None, **__):
|
||||||
"""Removes the subtitle specified by the index (default: all)"""
|
"""
|
||||||
|
Removes the subtitle specified by the index (default: all)
|
||||||
|
|
||||||
|
:param index: (1-based) index of the subtitles track to remove.
|
||||||
|
"""
|
||||||
if index is None:
|
if index is None:
|
||||||
self._exec('sub_remove')
|
self._exec('sub_remove')
|
||||||
else:
|
else:
|
||||||
|
@ -374,14 +396,15 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def is_playing(self):
|
def is_playing(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
:returns: True if it's playing, False otherwise
|
:returns: True if it's playing, False otherwise
|
||||||
"""
|
"""
|
||||||
return self.get_property('pause').output.get('pause') is False
|
response: dict = self.get_property('pause').output or {} # type: ignore
|
||||||
|
return response.get('pause') is False
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def load(self, resource, mplayer_args=None, **kwargs):
|
def load(self, resource, *_, mplayer_args: Optional[Collection[str]] = None, **__):
|
||||||
"""
|
"""
|
||||||
Load a resource/video in the player.
|
Load a resource/video in the player.
|
||||||
"""
|
"""
|
||||||
|
@ -390,13 +413,13 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
return self.play(resource, mplayer_args=mplayer_args)
|
return self.play(resource, mplayer_args=mplayer_args)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def mute(self):
|
def mute(self, *_, **__):
|
||||||
"""Toggle mute state"""
|
"""Toggle mute state"""
|
||||||
self._exec('mute')
|
self._exec('mute')
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seek(self, position):
|
def seek(self, position: float, *_, **__):
|
||||||
"""
|
"""
|
||||||
Seek backward/forward by the specified number of seconds
|
Seek backward/forward by the specified number of seconds
|
||||||
|
|
||||||
|
@ -407,7 +430,7 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_position(self, position):
|
def set_position(self, position: float, *_, **__):
|
||||||
"""
|
"""
|
||||||
Seek backward/forward to the specified absolute position
|
Seek backward/forward to the specified absolute position
|
||||||
|
|
||||||
|
@ -418,7 +441,7 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume: float, *_, **__):
|
||||||
"""
|
"""
|
||||||
Set the volume
|
Set the volume
|
||||||
|
|
||||||
|
@ -463,7 +486,7 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
with self._status_lock:
|
with self._status_lock:
|
||||||
for prop, player_prop in props.items():
|
for prop, player_prop in props.items():
|
||||||
value = self.get_property(player_prop).output
|
value = self.get_property(player_prop).output
|
||||||
if value is not None:
|
if isinstance(value, dict):
|
||||||
status[prop] = value.get(player_prop)
|
status[prop] = value.get(player_prop)
|
||||||
|
|
||||||
status['seekable'] = bool(status['duration'])
|
status['seekable'] = bool(status['duration'])
|
||||||
|
@ -480,7 +503,11 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
return status
|
return status
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_property(self, property, args=None):
|
def get_property(
|
||||||
|
self,
|
||||||
|
property: str, # pylint: disable=redefined-builtin
|
||||||
|
args: Optional[Collection[str]] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Get a player property (e.g. pause, fullscreen etc.). See
|
Get a player property (e.g. pause, fullscreen etc.). See
|
||||||
https://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the
|
https://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the
|
||||||
|
@ -503,14 +530,23 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
for k, v in result.items():
|
for k, v in result.items():
|
||||||
if k == 'ERROR' and v not in response.errors:
|
if k == 'ERROR' and v not in response.errors:
|
||||||
response.errors.append('{}{}: {}'.format(property, args, v))
|
if not isinstance(response.errors, list):
|
||||||
|
response.errors = []
|
||||||
|
response.errors.append(f'{property}{args}: {v}')
|
||||||
else:
|
else:
|
||||||
|
if not isinstance(response.output, dict):
|
||||||
|
response.output = {}
|
||||||
response.output[k] = v
|
response.output[k] = v
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_property(self, property, value, args=None):
|
def set_property(
|
||||||
|
self,
|
||||||
|
property: str, # pylint: disable=redefined-builtin
|
||||||
|
value: Any,
|
||||||
|
args: Optional[Collection[str]] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Set a player property (e.g. pause, fullscreen etc.). See
|
Set a player property (e.g. pause, fullscreen etc.). See
|
||||||
https://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the
|
https://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the
|
||||||
|
@ -534,14 +570,25 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
for k, v in result.items():
|
for k, v in result.items():
|
||||||
if k == 'ERROR' and v not in response.errors:
|
if k == 'ERROR' and v not in response.errors:
|
||||||
response.errors.append('{} {}{}: {}'.format(property, value, args, v))
|
if not isinstance(response.errors, list):
|
||||||
|
response.errors = []
|
||||||
|
response.errors.append(f'{property} {value}{args}: {v}')
|
||||||
else:
|
else:
|
||||||
|
if not isinstance(response.output, dict):
|
||||||
|
response.output = {}
|
||||||
response.output[k] = v
|
response.output[k] = v
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def step_property(self, property, value, args=None):
|
def step_property(
|
||||||
|
self,
|
||||||
|
property: str, # pylint: disable=redefined-builtin
|
||||||
|
value: Any,
|
||||||
|
*_,
|
||||||
|
args: Optional[Collection[str]] = None,
|
||||||
|
**__,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Step a player property (e.g. volume, time_pos etc.). See
|
Step a player property (e.g. volume, time_pos etc.). See
|
||||||
https://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the
|
https://www.mplayerhq.hu/DOCS/tech/slave.txt for a full list of the
|
||||||
|
@ -565,13 +612,18 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
for k, v in result.items():
|
for k, v in result.items():
|
||||||
if k == 'ERROR' and v not in response.errors:
|
if k == 'ERROR' and v not in response.errors:
|
||||||
response.errors.append('{} {}{}: {}'.format(property, value, args, v))
|
if not isinstance(response.errors, list):
|
||||||
|
response.errors = []
|
||||||
|
response.errors.append(f'{property} {value}{args}: {v}')
|
||||||
else:
|
else:
|
||||||
|
if not isinstance(response.output, dict):
|
||||||
|
response.output = {}
|
||||||
response.output[k] = v
|
response.output[k] = v
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def set_subtitles(self, filename, *args, **kwargs):
|
def set_subtitles(self, filename: str, *_, **__):
|
||||||
|
self.logger.debug('set_subtitles called with filename=%s', filename)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import enum
|
import enum
|
||||||
import threading
|
import threading
|
||||||
|
from typing import Collection, Optional
|
||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
|
@ -16,6 +18,10 @@ from platypush.plugins import action
|
||||||
|
|
||||||
|
|
||||||
class PlayerEvent(enum.Enum):
|
class PlayerEvent(enum.Enum):
|
||||||
|
"""
|
||||||
|
Supported player events.
|
||||||
|
"""
|
||||||
|
|
||||||
STOP = 'stop'
|
STOP = 'stop'
|
||||||
PLAY = 'play'
|
PLAY = 'play'
|
||||||
PAUSE = 'pause'
|
PAUSE = 'pause'
|
||||||
|
@ -26,17 +32,18 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
Plugin to control video and media playback using OMXPlayer.
|
Plugin to control video and media playback using OMXPlayer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args=None, *argv, timeout: float = 20.0, **kwargs):
|
def __init__(
|
||||||
|
self, args: Optional[Collection[str]] = None, timeout: float = 20.0, **kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param args: Arguments that will be passed to the OMXPlayer constructor
|
:param args: Arguments that will be passed to the OMXPlayer constructor
|
||||||
(e.g. subtitles, volume, start position, window size etc.) see
|
(e.g. subtitles, volume, start position, window size etc.) see
|
||||||
https://github.com/popcornmix/omxplayer#synopsis and
|
https://github.com/popcornmix/omxplayer#synopsis and
|
||||||
https://python-omxplayer-wrapper.readthedocs.io/en/latest/omxplayer/#omxplayer.player.OMXPlayer
|
https://python-omxplayer-wrapper.readthedocs.io/en/latest/omxplayer/#omxplayer.player.OMXPlayer
|
||||||
:type args: list
|
|
||||||
:param timeout: How long the plugin should wait for a video to start upon play request (default: 20 seconds).
|
:param timeout: How long the plugin should wait for a video to start upon play request (default: 20 seconds).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(*argv, **kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
if args is None:
|
if args is None:
|
||||||
args = []
|
args = []
|
||||||
|
@ -48,7 +55,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
self._play_started = threading.Event()
|
self._play_started = threading.Event()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource=None, subtitles=None, *args, **kwargs):
|
def play(self, *args, resource=None, subtitles=None, **_):
|
||||||
"""
|
"""
|
||||||
Play or resume playing a resource.
|
Play or resume playing a resource.
|
||||||
|
|
||||||
|
@ -68,9 +75,8 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
self._player.play()
|
self._player.play()
|
||||||
|
|
||||||
return self.status()
|
return self.status()
|
||||||
else:
|
|
||||||
self._play_started.clear()
|
|
||||||
|
|
||||||
|
self._play_started.clear()
|
||||||
self._post_event(MediaPlayRequestEvent, resource=resource)
|
self._post_event(MediaPlayRequestEvent, resource=resource)
|
||||||
|
|
||||||
if subtitles:
|
if subtitles:
|
||||||
|
@ -141,7 +147,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
return {'status': 'stop'}
|
return {'status': 'stop'}
|
||||||
|
|
||||||
def get_volume(self) -> float:
|
def get_volume(self) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
:return: The player volume in percentage [0, 100].
|
:return: The player volume in percentage [0, 100].
|
||||||
"""
|
"""
|
||||||
|
@ -157,7 +163,9 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
:type step: float
|
:type step: float
|
||||||
"""
|
"""
|
||||||
if self._player:
|
if self._player:
|
||||||
self.set_volume(max(0, self.get_volume() - step))
|
vol = self.get_volume()
|
||||||
|
if vol is not None:
|
||||||
|
self.set_volume(max(0, vol - step))
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -169,7 +177,9 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
:type step: float
|
:type step: float
|
||||||
"""
|
"""
|
||||||
if self._player:
|
if self._player:
|
||||||
self.set_volume(min(100, self.get_volume() + step))
|
vol = self.get_volume()
|
||||||
|
if vol is not None:
|
||||||
|
self.set_volume(min(100, vol + step))
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -213,23 +223,19 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def is_playing(self):
|
def is_playing(self, *_, **__) -> bool:
|
||||||
"""
|
"""
|
||||||
:returns: True if it's playing, False otherwise
|
:returns: True if it's playing, False otherwise
|
||||||
"""
|
"""
|
||||||
|
return self._player.is_playing() if self._player else False
|
||||||
return self._player.is_playing()
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def load(self, resource, pause=False, **kwargs):
|
def load(self, resource: str, *_, pause: bool = False, **__):
|
||||||
"""
|
"""
|
||||||
Load a resource/video in the player.
|
Load a resource/video in the player.
|
||||||
|
|
||||||
:param resource: URL or filename to load
|
:param resource: URL or filename to load
|
||||||
:type resource: str
|
|
||||||
|
|
||||||
:param pause: If set, load the video in paused mode (default: False)
|
:param pause: If set, load the video in paused mode (default: False)
|
||||||
:type pause: bool
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._player:
|
if self._player:
|
||||||
|
@ -244,48 +250,45 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def mute(self):
|
def mute(self, *_, **__):
|
||||||
"""Mute the player"""
|
"""Mute the player"""
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.mute()
|
self._player.mute()
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def unmute(self):
|
def unmute(self, *_, **__):
|
||||||
"""Unmute the player"""
|
"""Unmute the player"""
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.unmute()
|
self._player.unmute()
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seek(self, position):
|
def seek(self, position: float, **__):
|
||||||
"""
|
"""
|
||||||
Seek to the specified number of seconds from the start.
|
Seek to the specified number of seconds from the start.
|
||||||
|
|
||||||
:param position: Number of seconds from the start
|
:param position: Number of seconds from the start
|
||||||
:type position: float
|
|
||||||
"""
|
"""
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.set_position(position)
|
self._player.set_position(position)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_position(self, position):
|
def set_position(self, position: float, **__):
|
||||||
"""
|
"""
|
||||||
Seek to the specified number of seconds from the start (same as :meth:`.seek`).
|
Seek to the specified number of seconds from the start (same as :meth:`.seek`).
|
||||||
|
|
||||||
:param position: Number of seconds from the start
|
:param position: Number of seconds from the start
|
||||||
:type position: float
|
|
||||||
"""
|
"""
|
||||||
return self.seek(position)
|
return self.seek(position)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume: float, *_, **__):
|
||||||
"""
|
"""
|
||||||
Set the volume
|
Set the volume
|
||||||
|
|
||||||
:param volume: Volume value between 0 and 100
|
:param volume: Volume value between 0 and 100
|
||||||
:type volume: float
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._player:
|
if self._player:
|
||||||
|
@ -327,7 +330,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
try:
|
try:
|
||||||
state = self._player.playback_status().lower()
|
state = self._player.playback_status().lower()
|
||||||
except (OMXPlayerDeadError, DBusException) as e:
|
except (OMXPlayerDeadError, DBusException) as e:
|
||||||
self.logger.warning(f'Could not retrieve player status: {e}')
|
self.logger.warning('Could not retrieve player status: %s', e)
|
||||||
if isinstance(e, OMXPlayerDeadError):
|
if isinstance(e, OMXPlayerDeadError):
|
||||||
self._player = None
|
self._player = None
|
||||||
|
|
||||||
|
@ -362,9 +365,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
def add_handler(self, event_type, callback):
|
def add_handler(self, event_type, callback):
|
||||||
if event_type not in self._handlers.keys():
|
if event_type not in self._handlers.keys():
|
||||||
raise AttributeError(
|
raise AttributeError(f'{event_type} is not a valid PlayerEvent type')
|
||||||
'{} is not a valid PlayerEvent type'.format(event_type)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._handlers[event_type].append(callback)
|
self._handlers[event_type].append(callback)
|
||||||
|
|
||||||
|
@ -420,13 +421,13 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
self._player.positionEvent += self.on_seek()
|
self._player.positionEvent += self.on_seek()
|
||||||
self._player.seekEvent += self.on_seek()
|
self._player.seekEvent += self.on_seek()
|
||||||
|
|
||||||
def toggle_subtitles(self, *args, **kwargs):
|
def toggle_subtitles(self, *_, **__):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def set_subtitles(self, filename, *args, **kwargs):
|
def set_subtitles(self, *_, **__):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def remove_subtitles(self, *args, **kwargs):
|
def remove_subtitles(self, *_, **__):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Optional
|
from typing import Collection, Optional
|
||||||
|
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
from platypush.plugins.media import PlayerState, MediaPlugin
|
from platypush.plugins.media import PlayerState, MediaPlugin
|
||||||
|
@ -24,23 +24,22 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
Plugin to control VLC instances.
|
Plugin to control VLC instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args=None, fullscreen=False, volume=100, *argv, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
args: Optional[Collection[str]] = None,
|
||||||
|
fullscreen: bool = False,
|
||||||
|
volume: int = 100,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Create the vlc wrapper.
|
|
||||||
|
|
||||||
:param args: List of extra arguments to pass to the VLC executable (e.g.
|
:param args: List of extra arguments to pass to the VLC executable (e.g.
|
||||||
``['--sub-language=en', '--snapshot-path=/mnt/snapshots']``)
|
``['--sub-language=en', '--snapshot-path=/mnt/snapshots']``)
|
||||||
:type args: list[str]
|
|
||||||
|
|
||||||
:param fullscreen: Set to True if you want media files to be opened in
|
:param fullscreen: Set to True if you want media files to be opened in
|
||||||
fullscreen by default (can be overridden by `.play()`) (default: False)
|
fullscreen by default (can be overridden by `.play()`) (default: False)
|
||||||
:type fullscreen: bool
|
|
||||||
|
|
||||||
:param volume: Default media volume (default: 100)
|
:param volume: Default media volume (default: 100)
|
||||||
:type volume: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(*argv, **kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self._args = args or []
|
self._args = args or []
|
||||||
self._instance = None
|
self._instance = None
|
||||||
|
@ -98,6 +97,7 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
self._monitor_thread = threading.Thread(target=self._player_monitor)
|
self._monitor_thread = threading.Thread(target=self._player_monitor)
|
||||||
self._monitor_thread.start()
|
self._monitor_thread.start()
|
||||||
self._instance = vlc.Instance(*self._args)
|
self._instance = vlc.Instance(*self._args)
|
||||||
|
assert self._instance, 'Could not create a VLC instance'
|
||||||
self._player = self._instance.media_player_new(resource)
|
self._player = self._instance.media_player_new(resource)
|
||||||
|
|
||||||
for evt in self._watched_event_types():
|
for evt in self._watched_event_types():
|
||||||
|
@ -136,65 +136,67 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
def callback(event):
|
def callback(event):
|
||||||
from vlc import EventType
|
from vlc import EventType
|
||||||
|
|
||||||
self.logger.debug('Received vlc event: {}'.format(event))
|
self.logger.debug('Received vlc event: %s', event)
|
||||||
|
if event.type == EventType.MediaPlayerPlaying: # type: ignore
|
||||||
if event.type == EventType.MediaPlayerPlaying:
|
|
||||||
self._post_event(MediaPlayEvent, resource=self._get_current_resource())
|
self._post_event(MediaPlayEvent, resource=self._get_current_resource())
|
||||||
elif event.type == EventType.MediaPlayerPaused:
|
elif event.type == EventType.MediaPlayerPaused: # type: ignore
|
||||||
self._post_event(MediaPauseEvent)
|
self._post_event(MediaPauseEvent)
|
||||||
elif (
|
elif (
|
||||||
event.type == EventType.MediaPlayerStopped
|
event.type == EventType.MediaPlayerStopped # type: ignore
|
||||||
or event.type == EventType.MediaPlayerEndReached
|
or event.type == EventType.MediaPlayerEndReached # type: ignore
|
||||||
):
|
):
|
||||||
self._on_stop_event.set()
|
self._on_stop_event.set()
|
||||||
self._post_event(MediaStopEvent)
|
self._post_event(MediaStopEvent)
|
||||||
for cbk in self._on_stop_callbacks:
|
for cbk in self._on_stop_callbacks:
|
||||||
cbk()
|
cbk()
|
||||||
elif (
|
elif self._player and (
|
||||||
event.type == EventType.MediaPlayerTitleChanged
|
event.type
|
||||||
or event.type == EventType.MediaPlayerMediaChanged
|
in (
|
||||||
|
EventType.MediaPlayerTitleChanged, # type: ignore
|
||||||
|
EventType.MediaPlayerMediaChanged, # type: ignore
|
||||||
|
)
|
||||||
):
|
):
|
||||||
self._title = self._player.get_title() or self._filename
|
self._title = self._player.get_title() or self._filename
|
||||||
if event.type == EventType.MediaPlayerMediaChanged:
|
if event.type == EventType.MediaPlayerMediaChanged: # type: ignore
|
||||||
self._post_event(NewPlayingMediaEvent, resource=self._title)
|
self._post_event(NewPlayingMediaEvent, resource=self._title)
|
||||||
elif event.type == EventType.MediaPlayerLengthChanged:
|
elif event.type == EventType.MediaPlayerLengthChanged: # type: ignore
|
||||||
self._post_event(
|
self._post_event(
|
||||||
NewPlayingMediaEvent, resource=self._get_current_resource()
|
NewPlayingMediaEvent, resource=self._get_current_resource()
|
||||||
)
|
)
|
||||||
elif event.type == EventType.MediaPlayerTimeChanged:
|
elif self._player and event.type == EventType.MediaPlayerTimeChanged: # type: ignore
|
||||||
pos = float(self._player.get_time() / 1000)
|
pos = float(self._player.get_time() / 1000)
|
||||||
if self._latest_seek is None or abs(pos - self._latest_seek) > 5:
|
if self._latest_seek is None or abs(pos - self._latest_seek) > 5:
|
||||||
self._post_event(MediaSeekEvent, position=pos)
|
self._post_event(MediaSeekEvent, position=pos)
|
||||||
self._latest_seek = pos
|
self._latest_seek = pos
|
||||||
elif event.type == EventType.MediaPlayerAudioVolume:
|
elif self._player and event.type == EventType.MediaPlayerAudioVolume: # type: ignore
|
||||||
self._post_event(
|
self._post_event(
|
||||||
MediaVolumeChangedEvent, volume=self._player.audio_get_volume()
|
MediaVolumeChangedEvent, volume=self._player.audio_get_volume()
|
||||||
)
|
)
|
||||||
elif event.type == EventType.MediaPlayerMuted:
|
elif event.type == EventType.MediaPlayerMuted: # type: ignore
|
||||||
self._post_event(MediaMuteChangedEvent, mute=True)
|
self._post_event(MediaMuteChangedEvent, mute=True)
|
||||||
elif event.type == EventType.MediaPlayerUnmuted:
|
elif event.type == EventType.MediaPlayerUnmuted: # type: ignore
|
||||||
self._post_event(MediaMuteChangedEvent, mute=False)
|
self._post_event(MediaMuteChangedEvent, mute=False)
|
||||||
|
|
||||||
return callback
|
return callback
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource=None, subtitles=None, fullscreen=None, volume=None):
|
def play(
|
||||||
|
self,
|
||||||
|
resource: Optional[str] = None,
|
||||||
|
subtitles: Optional[str] = None,
|
||||||
|
fullscreen: Optional[bool] = None,
|
||||||
|
volume: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Play a resource.
|
Play a resource.
|
||||||
|
|
||||||
:param resource: Resource to play - can be a local file or a remote URL (default: None == toggle play).
|
:param resource: Resource to play - can be a local file or a remote URL
|
||||||
:type resource: str
|
(default: None == toggle play).
|
||||||
|
|
||||||
:param subtitles: Path to optional subtitle file
|
:param subtitles: Path to optional subtitle file
|
||||||
:type subtitles: str
|
|
||||||
|
|
||||||
:param fullscreen: Set to explicitly enable/disable fullscreen (default:
|
:param fullscreen: Set to explicitly enable/disable fullscreen (default:
|
||||||
`fullscreen` configured value or False)
|
`fullscreen` configured value or False)
|
||||||
:type fullscreen: bool
|
|
||||||
|
|
||||||
:param volume: Set to explicitly set the playback volume (default:
|
:param volume: Set to explicitly set the playback volume (default:
|
||||||
`volume` configured value or 100)
|
`volume` configured value or 100)
|
||||||
:type fullscreen: bool
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not resource:
|
if not resource:
|
||||||
|
@ -208,12 +210,14 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
|
|
||||||
self._filename = resource
|
self._filename = resource
|
||||||
self._init_vlc(resource)
|
self._init_vlc(resource)
|
||||||
if subtitles:
|
if subtitles and self._player:
|
||||||
if subtitles.startswith('file://'):
|
if subtitles.startswith('file://'):
|
||||||
subtitles = subtitles[len('file://') :]
|
subtitles = subtitles[len('file://') :]
|
||||||
self._player.video_set_subtitle_file(subtitles)
|
self._player.video_set_subtitle_file(subtitles)
|
||||||
|
|
||||||
self._player.play()
|
if self._player:
|
||||||
|
self._player.play()
|
||||||
|
|
||||||
if self.volume:
|
if self.volume:
|
||||||
self.set_volume(volume=self.volume)
|
self.set_volume(volume=self.volume)
|
||||||
|
|
||||||
|
@ -226,71 +230,60 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def pause(self):
|
def pause(self, *_, **__):
|
||||||
"""Toggle the paused state"""
|
"""Toggle the paused state"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
assert self._player.can_pause(), 'The specified media type cannot be paused'
|
||||||
if not self._player.can_pause():
|
|
||||||
return None, 'The specified media type cannot be paused'
|
|
||||||
|
|
||||||
self._player.pause()
|
self._player.pause()
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def quit(self):
|
def quit(self, *_, **__):
|
||||||
"""Quit the player (same as `stop`)"""
|
"""Quit the player (same as `stop`)"""
|
||||||
with self._stop_lock:
|
with self._stop_lock:
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
|
|
||||||
self._player.stop()
|
self._player.stop()
|
||||||
self._on_stop_event.wait(timeout=5)
|
self._on_stop_event.wait(timeout=5)
|
||||||
self._reset_state()
|
self._reset_state()
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def stop(self):
|
def stop(self, *_, **__):
|
||||||
"""Stop the application (same as `quit`)"""
|
"""Stop the application (same as `quit`)"""
|
||||||
return self.quit()
|
return self.quit()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def voldown(self, step=10.0):
|
def voldown(self, *_, step: float = 10.0, **__):
|
||||||
"""Volume down by (default: 10)%"""
|
"""Volume down by (default: 10)%"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
return self.set_volume(int(max(0, self._player.audio_get_volume() - step)))
|
return self.set_volume(int(max(0, self._player.audio_get_volume() - step)))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def volup(self, step=10.0):
|
def volup(self, *_, step: float = 10.0, **__):
|
||||||
"""Volume up by (default: 10)%"""
|
"""Volume up by (default: 10)%"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
return self.set_volume(int(min(100, self._player.audio_get_volume() + step)))
|
return self.set_volume(int(min(100, self._player.audio_get_volume() + step)))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume: int):
|
||||||
"""
|
"""
|
||||||
Set the volume
|
Set the volume
|
||||||
|
|
||||||
:param volume: Volume value between 0 and 100
|
:param volume: Volume value between 0 and 100
|
||||||
:type volume: float
|
|
||||||
"""
|
"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
|
|
||||||
volume = max(0, min([100, volume]))
|
volume = max(0, min([100, volume]))
|
||||||
self._player.audio_set_volume(volume)
|
self._player.audio_set_volume(volume)
|
||||||
status = self.status().output
|
status: dict = self.status().output # type: ignore
|
||||||
status['volume'] = volume
|
status['volume'] = volume
|
||||||
return status
|
return status
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seek(self, position):
|
def seek(self, position: float):
|
||||||
"""
|
"""
|
||||||
Seek backward/forward by the specified number of seconds
|
Seek backward/forward by the specified number of seconds
|
||||||
|
|
||||||
:param position: Number of seconds relative to the current cursor
|
:param position: Number of seconds relative to the current cursor
|
||||||
:type position: int
|
|
||||||
"""
|
"""
|
||||||
if not self._player:
|
if not self._player:
|
||||||
return None, 'No vlc instance is running'
|
return None, 'No vlc instance is running'
|
||||||
|
@ -306,7 +299,7 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def back(self, offset=30.0):
|
def back(self, *_, offset: float = 30.0, **__):
|
||||||
"""Back by (default: 30) seconds"""
|
"""Back by (default: 30) seconds"""
|
||||||
if not self._player:
|
if not self._player:
|
||||||
return None, 'No vlc instance is running'
|
return None, 'No vlc instance is running'
|
||||||
|
@ -319,7 +312,7 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
return self.seek(pos)
|
return self.seek(pos)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def forward(self, offset=30.0):
|
def forward(self, *_, offset: float = 30.0, **__):
|
||||||
"""Forward by (default: 30) seconds"""
|
"""Forward by (default: 30) seconds"""
|
||||||
if not self._player:
|
if not self._player:
|
||||||
return None, 'No vlc instance is running'
|
return None, 'No vlc instance is running'
|
||||||
|
@ -334,13 +327,12 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
return self.seek(pos)
|
return self.seek(pos)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def toggle_subtitles(self, visibile=None):
|
def toggle_subtitles(self, *_, **__):
|
||||||
"""Toggle the subtitles visibility"""
|
"""Toggle the subtitles visibility"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
assert (
|
||||||
|
self._player.video_get_spu_count() > 0
|
||||||
if self._player.video_get_spu_count() == 0:
|
), 'The media file has no subtitles set'
|
||||||
return None, 'The media file has no subtitles set'
|
|
||||||
|
|
||||||
if self._player.video_get_spu() is None or self._player.video_get_spu() == -1:
|
if self._player.video_get_spu() is None or self._player.video_get_spu() == -1:
|
||||||
self._player.video_set_spu(0)
|
self._player.video_set_spu(0)
|
||||||
|
@ -350,36 +342,32 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
@action
|
@action
|
||||||
def toggle_fullscreen(self):
|
def toggle_fullscreen(self):
|
||||||
"""Toggle the fullscreen mode"""
|
"""Toggle the fullscreen mode"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
self._player.toggle_fullscreen()
|
self._player.toggle_fullscreen()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_fullscreen(self, fullscreen=True):
|
def set_fullscreen(self, fullscreen: bool = True):
|
||||||
"""Set fullscreen mode"""
|
"""Set fullscreen mode"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
self._player.set_fullscreen(fullscreen)
|
self._player.set_fullscreen(fullscreen)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_subtitles(self, filename, **args):
|
def set_subtitles(self, filename: str, *_, **__):
|
||||||
"""Sets media subtitles from filename"""
|
"""Sets media subtitles from filename"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
if filename.startswith('file://'):
|
if filename.startswith('file://'):
|
||||||
filename = filename[len('file://') :]
|
filename = filename[len('file://') :]
|
||||||
|
|
||||||
self._player.video_set_subtitle_file(filename)
|
self._player.video_set_subtitle_file(filename)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def remove_subtitles(self):
|
def remove_subtitles(self, *_, **__):
|
||||||
"""Removes (hides) the subtitles"""
|
"""Removes (hides) the subtitles"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
self._player.video_set_spu(-1)
|
self._player.video_set_spu(-1)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def is_playing(self):
|
def is_playing(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
:returns: True if it's playing, False otherwise
|
:returns: True if it's playing, False otherwise
|
||||||
"""
|
"""
|
||||||
|
@ -388,7 +376,7 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
return self._player.is_playing()
|
return self._player.is_playing()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def load(self, resource, **args):
|
def load(self, resource, *_, **args):
|
||||||
"""
|
"""
|
||||||
Load/queue a resource/video to the player
|
Load/queue a resource/video to the player
|
||||||
"""
|
"""
|
||||||
|
@ -398,14 +386,13 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def mute(self):
|
def mute(self, *_, **__):
|
||||||
"""Toggle mute state"""
|
"""Toggle mute state"""
|
||||||
if not self._player:
|
assert self._player, 'No vlc instance is running'
|
||||||
return None, 'No vlc instance is running'
|
|
||||||
self._player.audio_toggle_mute()
|
self._player.audio_toggle_mute()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_position(self, position):
|
def set_position(self, position: float, **_):
|
||||||
"""
|
"""
|
||||||
Seek backward/forward to the specified absolute position (same as ``seek``)
|
Seek backward/forward to the specified absolute position (same as ``seek``)
|
||||||
"""
|
"""
|
||||||
|
@ -434,9 +421,9 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
status = {}
|
status = {}
|
||||||
vlc_state = self._player.get_state()
|
vlc_state = self._player.get_state()
|
||||||
|
|
||||||
if vlc_state == vlc.State.Playing:
|
if vlc_state == vlc.State.Playing: # type: ignore
|
||||||
status['state'] = PlayerState.PLAY.value
|
status['state'] = PlayerState.PLAY.value
|
||||||
elif vlc_state == vlc.State.Paused:
|
elif vlc_state == vlc.State.Paused: # type: ignore
|
||||||
status['state'] = PlayerState.PAUSE.value
|
status['state'] = PlayerState.PAUSE.value
|
||||||
else:
|
else:
|
||||||
status['state'] = PlayerState.STOP.value
|
status['state'] = PlayerState.STOP.value
|
||||||
|
@ -446,6 +433,7 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
if self._player.get_media()
|
if self._player.get_media()
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
status['position'] = (
|
status['position'] = (
|
||||||
float(self._player.get_time() / 1000)
|
float(self._player.get_time() / 1000)
|
||||||
if self._player.get_time() is not None
|
if self._player.get_time() is not None
|
||||||
|
@ -477,7 +465,7 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
|
|
||||||
def _get_current_resource(self):
|
def _get_current_resource(self):
|
||||||
if not self._player or not self._player.get_media():
|
if not self._player or not self._player.get_media():
|
||||||
return
|
return None
|
||||||
return self._player.get_media().get_mrl()
|
return self._player.get_media().get_mrl()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Union
|
from typing import Collection, Optional, Union
|
||||||
|
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
from platypush.plugins.music import MusicPlugin
|
from platypush.plugins.music import MusicPlugin
|
||||||
|
@ -9,27 +9,29 @@ from platypush.plugins.music import MusicPlugin
|
||||||
|
|
||||||
class MusicMpdPlugin(MusicPlugin):
|
class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
This plugin allows you to interact with an MPD/Mopidy music server. MPD
|
This plugin allows you to interact with an MPD/Mopidy music server.
|
||||||
(https://www.musicpd.org/) is a flexible server-side protocol/application
|
|
||||||
for handling music collections and playing music, mostly aimed to manage
|
`MPD <https://www.musicpd.org/>`_ is a flexible server-side
|
||||||
local libraries. Mopidy (https://www.mopidy.com/) is an evolution of MPD,
|
protocol/application for handling music collections and playing music,
|
||||||
compatible with the original protocol and with support for multiple music
|
mostly aimed to manage local libraries.
|
||||||
sources through plugins (e.g. Spotify, TuneIn, Soundcloud, local files
|
|
||||||
etc.).
|
`Mopidy <https://www.mopidy.com/>`_ is an evolution of MPD, compatible with
|
||||||
|
the original protocol and with support for multiple music sources through
|
||||||
|
plugins (e.g. Spotify, TuneIn, Soundcloud, local files etc.).
|
||||||
|
|
||||||
|
.. note:: As of Mopidy 3.0 MPD is an optional interface provided by the
|
||||||
|
``mopidy-mpd`` extension. Make sure that you have the extension
|
||||||
|
installed and enabled on your instance to use this plugin with your
|
||||||
|
server.
|
||||||
|
|
||||||
**NOTE**: As of Mopidy 3.0 MPD is an optional interface provided by the ``mopidy-mpd`` extension. Make sure that you
|
|
||||||
have the extension installed and enabled on your instance to use this plugin with your server.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_client_lock = threading.RLock()
|
_client_lock = threading.RLock()
|
||||||
|
|
||||||
def __init__(self, host, port=6600):
|
def __init__(self, host: str, port: int = 6600):
|
||||||
"""
|
"""
|
||||||
:param host: MPD IP/hostname
|
:param host: MPD IP/hostname
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: MPD port (default: 6600)
|
:param port: MPD port (default: 6600)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -37,12 +39,12 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.client = None
|
self.client = None
|
||||||
|
|
||||||
def _connect(self, n_tries=2):
|
def _connect(self, n_tries: int = 2):
|
||||||
import mpd
|
import mpd
|
||||||
|
|
||||||
with self._client_lock:
|
with self._client_lock:
|
||||||
if self.client:
|
if self.client:
|
||||||
return
|
return self.client
|
||||||
|
|
||||||
error = None
|
error = None
|
||||||
while n_tries > 0:
|
while n_tries > 0:
|
||||||
|
@ -54,9 +56,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = e
|
error = e
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Connection exception: {}{}'.format(
|
'Connection exception: %s%s',
|
||||||
str(e), (': Retrying' if n_tries > 0 else '')
|
e,
|
||||||
)
|
(': Retrying' if n_tries > 0 else ''),
|
||||||
)
|
)
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
@ -64,7 +66,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
if error:
|
if error:
|
||||||
raise error
|
raise error
|
||||||
|
|
||||||
def _exec(self, method, *args, **kwargs):
|
return self.client
|
||||||
|
|
||||||
|
def _exec(self, method: str, *args, **kwargs):
|
||||||
error = None
|
error = None
|
||||||
n_tries = int(kwargs.pop('n_tries')) if 'n_tries' in kwargs else 2
|
n_tries = int(kwargs.pop('n_tries')) if 'n_tries' in kwargs else 2
|
||||||
return_status = (
|
return_status = (
|
||||||
|
@ -84,16 +88,16 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = str(e)
|
error = str(e)
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Exception while executing MPD method {}: {}'.format(method, error)
|
'Exception while executing MPD method %s: %s', method, error
|
||||||
)
|
)
|
||||||
self.client = None
|
self.client = None
|
||||||
|
|
||||||
return None, error
|
return None, error
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource=None):
|
def play(self, resource: Optional[str] = None, **__):
|
||||||
"""
|
"""
|
||||||
Play a resource by path/URI
|
Play a resource by path/URI.
|
||||||
|
|
||||||
:param resource: Resource path/URI
|
:param resource: Resource path/URI
|
||||||
:type resource: str
|
:type resource: str
|
||||||
|
@ -106,213 +110,184 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
return self._exec('play')
|
return self._exec('play')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play_pos(self, pos):
|
def play_pos(self, pos: int):
|
||||||
"""
|
"""
|
||||||
Play a track in the current playlist by position number
|
Play a track in the current playlist by position number.
|
||||||
|
|
||||||
:param pos: Position number
|
:param pos: Position number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._exec('play', pos)
|
return self._exec('play', pos)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def pause(self):
|
def pause(self, *_, **__):
|
||||||
"""Pause playback"""
|
"""Pause playback"""
|
||||||
|
|
||||||
status = self.status().output['state']
|
status = self._status()['state']
|
||||||
if status == 'play':
|
return self._exec('pause') if status == 'play' else self._exec('play')
|
||||||
return self._exec('pause')
|
|
||||||
else:
|
|
||||||
return self._exec('play')
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def pause_if_playing(self):
|
def pause_if_playing(self):
|
||||||
"""Pause playback only if it's playing"""
|
"""Pause playback only if it's playing"""
|
||||||
|
status = self._status()['state']
|
||||||
status = self.status().output['state']
|
return self._exec('pause') if status == 'play' else None
|
||||||
if status == 'play':
|
|
||||||
return self._exec('pause')
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play_if_paused(self):
|
def play_if_paused(self):
|
||||||
"""Play only if it's paused (resume)"""
|
"""Play only if it's paused (resume)"""
|
||||||
|
status = self._status()['state']
|
||||||
status = self.status().output['state']
|
return self._exec('play') if status == 'pause' else None
|
||||||
if status == 'pause':
|
|
||||||
return self._exec('play')
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play_if_paused_or_stopped(self):
|
def play_if_paused_or_stopped(self):
|
||||||
"""Play only if it's paused or stopped"""
|
"""Play only if it's paused or stopped"""
|
||||||
|
status = self._status()['state']
|
||||||
status = self.status().output['state']
|
return self._exec('play') if status in ('pause', 'stop') else None
|
||||||
if status == 'pause' or status == 'stop':
|
|
||||||
return self._exec('play')
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def stop(self):
|
def stop(self, *_, **__):
|
||||||
"""Stop playback"""
|
"""Stop playback"""
|
||||||
return self._exec('stop')
|
return self._exec('stop')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play_or_stop(self):
|
def play_or_stop(self):
|
||||||
"""Play or stop (play state toggle)"""
|
"""Play or stop (play state toggle)"""
|
||||||
status = self.status().output['state']
|
status = self._status()['state']
|
||||||
if status == 'play':
|
if status == 'play':
|
||||||
return self._exec('stop')
|
return self._exec('stop')
|
||||||
else:
|
return self._exec('play')
|
||||||
return self._exec('play')
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def playid(self, track_id):
|
def playid(self, track_id: str):
|
||||||
"""
|
"""
|
||||||
Play a track by ID
|
Play a track by ID.
|
||||||
|
|
||||||
:param track_id: Track ID
|
:param track_id: Track ID.
|
||||||
:type track_id: str
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._exec('playid', track_id)
|
return self._exec('playid', track_id)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def next(self):
|
def next(self, *_, **__):
|
||||||
"""Play the next track"""
|
"""Play the next track"""
|
||||||
return self._exec('next')
|
return self._exec('next')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def previous(self):
|
def previous(self, *_, **__):
|
||||||
"""Play the previous track"""
|
"""Play the previous track"""
|
||||||
return self._exec('previous')
|
return self._exec('previous')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def setvol(self, vol):
|
def setvol(self, vol: int):
|
||||||
"""
|
"""
|
||||||
Set the volume (DEPRECATED, use :meth:`.set_volume` instead).
|
Set the volume.
|
||||||
|
|
||||||
:param vol: Volume value (range: 0-100)
|
..warning :: **DEPRECATED**, use :meth:`.set_volume` instead.
|
||||||
:type vol: int
|
|
||||||
|
:param vol: Volume value (range: 0-100).
|
||||||
"""
|
"""
|
||||||
return self.set_volume(vol)
|
return self.set_volume(vol)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume: int, *_, **__):
|
||||||
"""
|
"""
|
||||||
Set the volume.
|
Set the volume.
|
||||||
|
|
||||||
:param volume: Volume value (range: 0-100)
|
:param volume: Volume value (range: 0-100).
|
||||||
:type volume: int
|
|
||||||
"""
|
"""
|
||||||
return self._exec('setvol', str(volume))
|
return self._exec('setvol', str(volume))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def volup(self, delta=10):
|
def volup(self, *_, delta: int = 10, **__):
|
||||||
"""
|
"""
|
||||||
Turn up the volume
|
Turn up the volume.
|
||||||
|
|
||||||
:param delta: Volume up delta (default: +10%)
|
:param delta: Volume up delta (default: +10%).
|
||||||
:type delta: int
|
|
||||||
"""
|
"""
|
||||||
|
volume = int(self._status()['volume'])
|
||||||
volume = int(self.status().output['volume'])
|
|
||||||
new_volume = min(volume + delta, 100)
|
new_volume = min(volume + delta, 100)
|
||||||
return self.setvol(new_volume)
|
return self.setvol(new_volume)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def voldown(self, delta=10):
|
def voldown(self, *_, delta: int = 10, **__):
|
||||||
"""
|
"""
|
||||||
Turn down the volume
|
Turn down the volume.
|
||||||
|
|
||||||
:param delta: Volume down delta (default: -10%)
|
:param delta: Volume down delta (default: -10%).
|
||||||
:type delta: int
|
|
||||||
"""
|
"""
|
||||||
|
volume = int(self._status()['volume'])
|
||||||
volume = int(self.status().output['volume'])
|
|
||||||
new_volume = max(volume - delta, 0)
|
new_volume = max(volume - delta, 0)
|
||||||
return self.setvol(new_volume)
|
return self.setvol(new_volume)
|
||||||
|
|
||||||
@action
|
def _toggle(self, key: str, value: Optional[bool] = None):
|
||||||
def random(self, value=None):
|
|
||||||
"""
|
|
||||||
Set random mode
|
|
||||||
|
|
||||||
:param value: If set, set the random state this value (true/false). Default: None (toggle current state)
|
|
||||||
:type value: bool
|
|
||||||
"""
|
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
value = int(self.status().output['random'])
|
value = bool(self._status()[key])
|
||||||
value = 1 if value == 0 else 0
|
return self._exec(key, int(value))
|
||||||
return self._exec('random', value)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def consume(self, value=None):
|
def random(self, value: Optional[bool] = None):
|
||||||
"""
|
"""
|
||||||
Set consume mode
|
Set random mode.
|
||||||
|
|
||||||
:param value: If set, set the consume state this value (true/false). Default: None (toggle current state)
|
:param value: If set, set the random state this value (true/false).
|
||||||
:type value: bool
|
Default: None (toggle current state).
|
||||||
"""
|
"""
|
||||||
|
return self._toggle('random', value)
|
||||||
if value is None:
|
|
||||||
value = int(self.status().output['consume'])
|
|
||||||
value = 1 if value == 0 else 0
|
|
||||||
return self._exec('consume', value)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def single(self, value=None):
|
def consume(self, value: Optional[bool] = None):
|
||||||
"""
|
"""
|
||||||
Set single mode
|
Set consume mode.
|
||||||
|
|
||||||
:param value: If set, set the consume state this value (true/false). Default: None (toggle current state)
|
:param value: If set, set the consume state this value (true/false).
|
||||||
:type value: bool
|
Default: None (toggle current state)
|
||||||
"""
|
"""
|
||||||
|
return self._toggle('consume', value)
|
||||||
if value is None:
|
|
||||||
value = int(self.status().output['single'])
|
|
||||||
value = 1 if value == 0 else 0
|
|
||||||
return self._exec('single', value)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def repeat(self, value=None):
|
def single(self, value: Optional[bool] = None):
|
||||||
"""
|
"""
|
||||||
Set repeat mode
|
Set single mode.
|
||||||
|
|
||||||
:param value: If set, set the repeat state this value (true/false). Default: None (toggle current state)
|
:param value: If set, set the consume state this value (true/false).
|
||||||
:type value: bool
|
Default: None (toggle current state)
|
||||||
"""
|
"""
|
||||||
|
return self._toggle('single', value)
|
||||||
|
|
||||||
if value is None:
|
@action
|
||||||
value = int(self.status().output['repeat'])
|
def repeat(self, value: Optional[bool] = None):
|
||||||
value = 1 if value == 0 else 0
|
"""
|
||||||
return self._exec('repeat', value)
|
Set repeat mode.
|
||||||
|
|
||||||
|
:param value: If set, set the repeat state this value (true/false).
|
||||||
|
Default: None (toggle current state)
|
||||||
|
"""
|
||||||
|
return self._toggle('repeat', value)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def shuffle(self):
|
def shuffle(self):
|
||||||
"""
|
"""
|
||||||
Shuffles the current playlist
|
Shuffles the current playlist.
|
||||||
"""
|
"""
|
||||||
return self._exec('shuffle')
|
return self._exec('shuffle')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def save(self, name):
|
def save(self, name: str):
|
||||||
"""
|
"""
|
||||||
Save the current tracklist to a new playlist with the specified name
|
Save the current tracklist to a new playlist with the specified name.
|
||||||
|
|
||||||
:param name: Name of the playlist
|
:param name: Name of the playlist
|
||||||
:type name: str
|
|
||||||
"""
|
"""
|
||||||
return self._exec('save', name)
|
return self._exec('save', name)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def add(self, resource, position=None):
|
def add(self, resource: str, *_, position: Optional[int] = None, **__):
|
||||||
"""
|
"""
|
||||||
Add a resource (track, album, artist, folder etc.) to the current playlist
|
Add a resource (track, album, artist, folder etc.) to the current
|
||||||
|
playlist.
|
||||||
|
|
||||||
:param resource: Resource path or URI
|
:param resource: Resource path or URI.
|
||||||
:type resource: str
|
:param position: Position where the track(s) will be inserted (default:
|
||||||
|
end of the playlist).
|
||||||
:param position: Position where the track(s) will be inserted (default: end of the playlist)
|
|
||||||
:type position: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(resource, list):
|
if isinstance(resource, list):
|
||||||
|
@ -324,7 +299,7 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
else:
|
else:
|
||||||
self._exec('addid', r, position)
|
self._exec('addid', r, position)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Could not add {}: {}'.format(r, e))
|
self.logger.warning('Could not add %s: %s', r, e)
|
||||||
|
|
||||||
return self.status().output
|
return self.status().output
|
||||||
|
|
||||||
|
@ -361,7 +336,7 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
if isinstance(playlist, str):
|
if isinstance(playlist, str):
|
||||||
playlist = [playlist]
|
playlist = [playlist]
|
||||||
elif not isinstance(playlist, list):
|
elif not isinstance(playlist, list):
|
||||||
raise RuntimeError('Invalid type for playlist: {}'.format(type(playlist)))
|
raise RuntimeError(f'Invalid type for playlist: {type(playlist)}')
|
||||||
|
|
||||||
for p in playlist:
|
for p in playlist:
|
||||||
self._exec('rm', p)
|
self._exec('rm', p)
|
||||||
|
@ -382,11 +357,11 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse_resource(cls, resource):
|
def _parse_resource(cls, resource):
|
||||||
if not resource:
|
if not resource:
|
||||||
return
|
return None
|
||||||
|
|
||||||
m = re.search(r'^https?://open\.spotify\.com/([^?]+)', resource)
|
m = re.search(r'^https?://open\.spotify\.com/([^?]+)', resource)
|
||||||
if m:
|
if m:
|
||||||
resource = 'spotify:{}'.format(m.group(1).replace('/', ':'))
|
resource = 'spotify:' + m.group(1).replace('/', ':')
|
||||||
|
|
||||||
if resource.startswith('spotify:'):
|
if resource.startswith('spotify:'):
|
||||||
resource = resource.split('?')[0]
|
resource = resource.split('?')[0]
|
||||||
|
@ -415,46 +390,59 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def clear(self):
|
def clear(self, *_, **__):
|
||||||
"""Clear the current playlist"""
|
"""Clear the current playlist"""
|
||||||
return self._exec('clear')
|
return self._exec('clear')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seekcur(self, value):
|
def seekcur(self, value: float):
|
||||||
"""
|
"""
|
||||||
Seek to the specified position (DEPRECATED, use :meth:`.seek` instead).
|
Seek to the specified position (DEPRECATED, use :meth:`.seek` instead).
|
||||||
|
|
||||||
:param value: Seek position in seconds, or delta string (e.g. '+15' or '-15') to indicate a seek relative to
|
:param value: Seek position in seconds, or delta string (e.g. '+15' or
|
||||||
the current position :type value: int
|
'-15') to indicate a seek relative to the current position
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.seek(value)
|
return self.seek(value)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seek(self, position):
|
def seek(self, position: float, *_, **__):
|
||||||
"""
|
"""
|
||||||
Seek to the specified position
|
Seek to the specified position
|
||||||
|
|
||||||
:param position: Seek position in seconds, or delta string (e.g. '+15' or '-15') to indicate a seek relative
|
:param position: Seek position in seconds, or delta string (e.g. '+15'
|
||||||
to the current position :type position: int
|
or '-15') to indicate a seek relative to the current position
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._exec('seekcur', position)
|
return self._exec('seekcur', position)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def forward(self):
|
def forward(self):
|
||||||
"""Go forward by 15 seconds"""
|
"""Go forward by 15 seconds"""
|
||||||
|
|
||||||
return self._exec('seekcur', '+15')
|
return self._exec('seekcur', '+15')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def back(self):
|
def back(self):
|
||||||
"""Go backward by 15 seconds"""
|
"""Go backward by 15 seconds"""
|
||||||
|
|
||||||
return self._exec('seekcur', '-15')
|
return self._exec('seekcur', '-15')
|
||||||
|
|
||||||
|
def _status(self) -> dict:
|
||||||
|
n_tries = 2
|
||||||
|
error = None
|
||||||
|
|
||||||
|
while n_tries > 0:
|
||||||
|
try:
|
||||||
|
n_tries -= 1
|
||||||
|
self._connect()
|
||||||
|
if self.client:
|
||||||
|
return self.client.status() # type: ignore
|
||||||
|
except Exception as e:
|
||||||
|
error = e
|
||||||
|
self.logger.warning('Exception while getting MPD status: %s', e)
|
||||||
|
self.client = None
|
||||||
|
|
||||||
|
raise AssertionError(str(error))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def status(self):
|
def status(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
:returns: The current state.
|
:returns: The current state.
|
||||||
|
|
||||||
|
@ -480,24 +468,7 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
return self._status()
|
||||||
n_tries = 2
|
|
||||||
error = None
|
|
||||||
|
|
||||||
while n_tries > 0:
|
|
||||||
try:
|
|
||||||
n_tries -= 1
|
|
||||||
self._connect()
|
|
||||||
if self.client:
|
|
||||||
return self.client.status()
|
|
||||||
except Exception as e:
|
|
||||||
error = e
|
|
||||||
self.logger.warning(
|
|
||||||
'Exception while getting MPD status: {}'.format(str(e))
|
|
||||||
)
|
|
||||||
self.client = None
|
|
||||||
|
|
||||||
return None, error
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def currentsong(self):
|
def currentsong(self):
|
||||||
|
@ -506,9 +477,8 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
return self.current_track()
|
return self.current_track()
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
@action
|
@action
|
||||||
def current_track(self):
|
def current_track(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
:returns: The currently played track.
|
:returns: The currently played track.
|
||||||
|
|
||||||
|
@ -530,6 +500,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
track = self._exec('currentsong', return_status=False)
|
track = self._exec('currentsong', return_status=False)
|
||||||
|
if not isinstance(track, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
if 'title' in track and (
|
if 'title' in track and (
|
||||||
'artist' not in track
|
'artist' not in track
|
||||||
or not track['artist']
|
or not track['artist']
|
||||||
|
@ -583,7 +556,7 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
return self._exec('playlistinfo', return_status=False)
|
return self._exec('playlistinfo', return_status=False)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_playlists(self):
|
def get_playlists(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
:returns: The playlists available on the server as a list of dicts.
|
:returns: The playlists available on the server as a list of dicts.
|
||||||
|
|
||||||
|
@ -602,11 +575,12 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
# ...
|
# ...
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return sorted(
|
playlists: list = self._exec( # type: ignore
|
||||||
self._exec('listplaylists', return_status=False),
|
'listplaylists', return_status=False
|
||||||
key=lambda p: p['playlist'],
|
|
||||||
)
|
)
|
||||||
|
return sorted(playlists, key=lambda p: p['playlist'])
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def listplaylists(self):
|
def listplaylists(self):
|
||||||
|
@ -616,14 +590,13 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
return self.get_playlists()
|
return self.get_playlists()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_playlist(self, playlist, with_tracks=False):
|
def get_playlist(self, playlist: str, *_, with_tracks: bool = False, **__):
|
||||||
"""
|
"""
|
||||||
List the items in the specified playlist.
|
List the items in the specified playlist.
|
||||||
|
|
||||||
:param playlist: Name of the playlist
|
:param playlist: Name of the playlist
|
||||||
:type playlist: str
|
:param with_tracks: If True then the list of tracks in the playlist will
|
||||||
:param with_tracks: If True then the list of tracks in the playlist will be returned as well (default: False).
|
be returned as well (default: False).
|
||||||
:type with_tracks: bool
|
|
||||||
"""
|
"""
|
||||||
return self._exec(
|
return self._exec(
|
||||||
'listplaylistinfo' if with_tracks else 'listplaylist',
|
'listplaylistinfo' if with_tracks else 'listplaylist',
|
||||||
|
@ -632,29 +605,26 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def listplaylist(self, name):
|
def listplaylist(self, name: str):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.playlist`.
|
Deprecated alias for :meth:`.playlist`.
|
||||||
"""
|
"""
|
||||||
return self._exec('listplaylist', name, return_status=False)
|
return self._exec('listplaylist', name, return_status=False)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def listplaylistinfo(self, name):
|
def listplaylistinfo(self, name: str):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.playlist` with `with_tracks=True`.
|
Deprecated alias for :meth:`.playlist` with ``with_tracks=True``.
|
||||||
"""
|
"""
|
||||||
return self.get_playlist(name, with_tracks=True)
|
return self.get_playlist(name, with_tracks=True)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def add_to_playlist(self, playlist, resources):
|
def add_to_playlist(self, playlist: str, resources: Union[str, Collection[str]]):
|
||||||
"""
|
"""
|
||||||
Add one or multiple resources to a playlist.
|
Add one or multiple resources to a playlist.
|
||||||
|
|
||||||
:param playlist: Playlist name
|
:param playlist: Playlist name
|
||||||
:type playlist: str
|
|
||||||
|
|
||||||
:param resources: URI or path of the resource(s) to be added
|
:param resources: URI or path of the resource(s) to be added
|
||||||
:type resources: str or list[str]
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(resources, str):
|
if isinstance(resources, str):
|
||||||
|
@ -664,22 +634,21 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
self._exec('playlistadd', playlist, res)
|
self._exec('playlistadd', playlist, res)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def playlistadd(self, name, uri):
|
def playlistadd(self, name: str, uri: str):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.add_to_playlist`.
|
Deprecated alias for :meth:`.add_to_playlist`.
|
||||||
"""
|
"""
|
||||||
return self.add_to_playlist(name, uri)
|
return self.add_to_playlist(name, uri)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def remove_from_playlist(self, playlist, resources):
|
def remove_from_playlist(
|
||||||
|
self, playlist: str, resources: Union[int, Collection[int]], *_, **__
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Remove one or multiple tracks from a playlist.
|
Remove one or multiple tracks from a playlist.
|
||||||
|
|
||||||
:param playlist: Playlist name
|
:param playlist: Playlist name
|
||||||
:type playlist: str
|
|
||||||
|
|
||||||
:param resources: Position or list of positions to remove
|
:param resources: Position or list of positions to remove
|
||||||
:type resources: int or list[int]
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(resources, str):
|
if isinstance(resources, str):
|
||||||
|
@ -691,62 +660,53 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
self._exec('playlistdelete', playlist, p)
|
self._exec('playlistdelete', playlist, p)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def playlist_move(self, playlist, from_pos, to_pos):
|
def playlist_move(self, playlist: str, from_pos: int, to_pos: int, *_, **__):
|
||||||
"""
|
"""
|
||||||
Change the position of a track in the specified playlist.
|
Change the position of a track in the specified playlist.
|
||||||
|
|
||||||
:param playlist: Playlist name
|
:param playlist: Playlist name
|
||||||
:type playlist: str
|
|
||||||
|
|
||||||
:param from_pos: Original track position
|
:param from_pos: Original track position
|
||||||
:type from_pos: int
|
|
||||||
|
|
||||||
:param to_pos: New track position
|
:param to_pos: New track position
|
||||||
:type to_pos: int
|
|
||||||
"""
|
"""
|
||||||
self._exec('playlistmove', playlist, from_pos, to_pos)
|
self._exec('playlistmove', playlist, from_pos, to_pos)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def playlistdelete(self, name, pos):
|
def playlistdelete(self, name: str, pos: int):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.remove_from_playlist`.
|
Deprecated alias for :meth:`.remove_from_playlist`.
|
||||||
"""
|
"""
|
||||||
return self.remove_from_playlist(name, pos)
|
return self.remove_from_playlist(name, pos)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def playlistmove(self, name, from_pos, to_pos):
|
def playlistmove(self, name: str, from_pos: int, to_pos: int):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.playlist_move`.
|
Deprecated alias for :meth:`.playlist_move`.
|
||||||
"""
|
"""
|
||||||
return self.playlist_move(name, from_pos=from_pos, to_pos=to_pos)
|
return self.playlist_move(name, from_pos=from_pos, to_pos=to_pos)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def playlistclear(self, name):
|
def playlistclear(self, name: str):
|
||||||
"""
|
"""
|
||||||
Clears all the elements from the specified playlist
|
Clears all the elements from the specified playlist.
|
||||||
|
|
||||||
:param name: Playlist name
|
:param name: Playlist name.
|
||||||
:type name: str
|
|
||||||
"""
|
"""
|
||||||
self._exec('playlistclear', name)
|
self._exec('playlistclear', name)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def rename(self, name, new_name):
|
def rename(self, name: str, new_name: str):
|
||||||
"""
|
"""
|
||||||
Rename a playlist
|
Rename a playlist.
|
||||||
|
|
||||||
:param name: Original playlist name
|
:param name: Original playlist name
|
||||||
:type name: str
|
|
||||||
|
|
||||||
:param new_name: New playlist name
|
:param new_name: New playlist name
|
||||||
:type name: str
|
|
||||||
"""
|
"""
|
||||||
self._exec('rename', name, new_name)
|
self._exec('rename', name, new_name)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def lsinfo(self, uri=None):
|
def lsinfo(self, uri: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
Returns the list of playlists and directories on the server
|
Returns the list of playlists and directories on the server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -756,41 +716,36 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def plchanges(self, version):
|
def plchanges(self, version: int):
|
||||||
"""
|
"""
|
||||||
Show what has changed on the current playlist since a specified playlist
|
Show what has changed on the current playlist since a specified playlist
|
||||||
version number.
|
version number.
|
||||||
|
|
||||||
:param version: Version number
|
:param version: Version number
|
||||||
:type version: int
|
|
||||||
|
|
||||||
:returns: A list of dicts representing the songs being added since the specified version
|
:returns: A list of dicts representing the songs being added since the specified version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._exec('plchanges', version, return_status=False)
|
return self._exec('plchanges', version, return_status=False)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def searchaddplaylist(self, name):
|
def searchaddplaylist(self, name: str):
|
||||||
"""
|
"""
|
||||||
Search and add a playlist by (partial or full) name
|
Search and add a playlist by (partial or full) name.
|
||||||
|
|
||||||
:param name: Playlist name, can be partial
|
:param name: Playlist name, can be partial.
|
||||||
:type name: str
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
resp: list = self._exec('listplaylists', return_status=False) # type: ignore
|
||||||
playlists = [
|
playlists = [
|
||||||
pl['playlist']
|
pl['playlist'] for pl in resp if name.lower() in pl['playlist'].lower()
|
||||||
for pl in filter(
|
|
||||||
lambda playlist: name.lower() in playlist['playlist'].lower(),
|
|
||||||
self._exec('listplaylists', return_status=False),
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if len(playlists):
|
if not playlists:
|
||||||
self._exec('clear')
|
return None
|
||||||
self._exec('load', playlists[0])
|
|
||||||
self._exec('play')
|
self._exec('clear')
|
||||||
return {'playlist': playlists[0]}
|
self._exec('load', playlists[0])
|
||||||
|
self._exec('play')
|
||||||
|
return {'playlist': playlists[0]}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _make_filter(f: dict) -> list:
|
def _make_filter(f: dict) -> list:
|
||||||
|
@ -799,40 +754,37 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
ll.extend([k, v])
|
ll.extend([k, v])
|
||||||
return ll
|
return ll
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
|
||||||
@action
|
@action
|
||||||
def find(self, filter: dict, *args, **kwargs):
|
def find(self, filter: dict, *args, **kwargs): # pylint: disable=redefined-builtin
|
||||||
"""
|
"""
|
||||||
Find in the database/library by filter.
|
Find in the database/library by filter.
|
||||||
|
|
||||||
:param filter: Search filter (e.g. ``{"artist": "Led Zeppelin", "album": "IV"}``)
|
:param filter: Search filter (e.g. ``{"artist": "Led Zeppelin", "album": "IV"}``)
|
||||||
:returns: list[dict]
|
:returns: list[dict]
|
||||||
"""
|
"""
|
||||||
|
filter_list = self._make_filter(filter)
|
||||||
|
return self._exec('find', *filter_list, *args, return_status=False, **kwargs)
|
||||||
|
|
||||||
filter = self._make_filter(filter)
|
|
||||||
return self._exec('find', *filter, *args, return_status=False, **kwargs)
|
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
|
||||||
@action
|
@action
|
||||||
def findadd(self, filter: dict, *args, **kwargs):
|
def findadd(
|
||||||
|
self, filter: dict, *args, **kwargs # pylint: disable=redefined-builtin
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Find in the database/library by filter and add to the current playlist.
|
Find in the database/library by filter and add to the current playlist.
|
||||||
|
|
||||||
:param filter: Search filter (e.g. ``{"artist": "Led Zeppelin", "album": "IV"}``)
|
:param filter: Search filter (e.g. ``{"artist": "Led Zeppelin", "album": "IV"}``)
|
||||||
:returns: list[dict]
|
:returns: list[dict]
|
||||||
"""
|
"""
|
||||||
|
filter_list = self._make_filter(filter)
|
||||||
|
return self._exec('findadd', *filter_list, *args, return_status=False, **kwargs)
|
||||||
|
|
||||||
filter = self._make_filter(filter)
|
|
||||||
return self._exec('findadd', *filter, *args, return_status=False, **kwargs)
|
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
|
||||||
@action
|
@action
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
query: Optional[Union[str, dict]] = None,
|
|
||||||
filter: Optional[dict] = None,
|
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
query: Optional[Union[str, dict]] = None,
|
||||||
|
filter: Optional[dict] = None, # pylint: disable=redefined-builtin
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Free search by filter.
|
Free search by filter.
|
||||||
|
@ -842,26 +794,37 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
``query``, it's still here for back-compatibility reasons.
|
``query``, it's still here for back-compatibility reasons.
|
||||||
:returns: list[dict]
|
:returns: list[dict]
|
||||||
"""
|
"""
|
||||||
filter = self._make_filter(query or filter)
|
assert query or filter, 'Specify either `query` or `filter`'
|
||||||
items = self._exec('search', *filter, *args, return_status=False, **kwargs)
|
|
||||||
|
filt = filter
|
||||||
|
if isinstance(query, str):
|
||||||
|
filt = query
|
||||||
|
elif isinstance(query, dict):
|
||||||
|
filt = {**(filter or {}), **query}
|
||||||
|
|
||||||
|
filter_list = self._make_filter(filt) if isinstance(filt, dict) else [query]
|
||||||
|
|
||||||
|
items: list = self._exec( # type: ignore
|
||||||
|
'search', *filter_list, *args, return_status=False, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# Spotify results first
|
# Spotify results first
|
||||||
return sorted(
|
return sorted(
|
||||||
items, key=lambda item: 0 if item['file'].startswith('spotify:') else 1
|
items, key=lambda item: 0 if item['file'].startswith('spotify:') else 1
|
||||||
)
|
)
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
|
||||||
@action
|
@action
|
||||||
def searchadd(self, filter, *args, **kwargs):
|
def searchadd(self, filter: dict, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Free search by filter and add the results to the current playlist.
|
Free search by filter and add the results to the current playlist.
|
||||||
|
|
||||||
:param filter: Search filter (e.g. ``{"artist": "Led Zeppelin", "album": "IV"}``)
|
:param filter: Search filter (e.g. ``{"artist": "Led Zeppelin", "album": "IV"}``)
|
||||||
:returns: list[dict]
|
:returns: list[dict]
|
||||||
"""
|
"""
|
||||||
|
filter_list = self._make_filter(filter)
|
||||||
filter = self._make_filter(filter)
|
return self._exec(
|
||||||
return self._exec('searchadd', *filter, *args, return_status=False, **kwargs)
|
'searchadd', *filter_list, *args, return_status=False, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
from typing import Collection, Optional
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.context import get_backend
|
from platypush.context import get_backend
|
||||||
|
@ -9,7 +10,7 @@ from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
class MusicSnapcastPlugin(Plugin):
|
class MusicSnapcastPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Plugin to interact with a [Snapcast](https://github.com/badaix/snapcast)
|
Plugin to interact with a `Snapcast <https://github.com/badaix/snapcast>`_
|
||||||
instance, control clients mute status, volume, playback etc.
|
instance, control clients mute status, volume, playback etc.
|
||||||
|
|
||||||
See https://github.com/badaix/snapcast/blob/master/doc/json_rpc_api/v2_0_0.md
|
See https://github.com/badaix/snapcast/blob/master/doc/json_rpc_api/v2_0_0.md
|
||||||
|
@ -19,15 +20,13 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
_DEFAULT_SNAPCAST_PORT = 1705
|
_DEFAULT_SNAPCAST_PORT = 1705
|
||||||
_SOCKET_EOL = '\r\n'.encode()
|
_SOCKET_EOL = '\r\n'.encode()
|
||||||
|
|
||||||
def __init__(self, host='localhost', port=_DEFAULT_SNAPCAST_PORT, **kwargs):
|
def __init__(
|
||||||
|
self, host: str = 'localhost', port: int = _DEFAULT_SNAPCAST_PORT, **kwargs
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param host: Default Snapcast server host (default: localhost)
|
:param host: Default Snapcast server host (default: localhost)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Default Snapcast server control port (default: 1705)
|
:param port: Default Snapcast server control port (default: 1705)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.host = host
|
self.host = host
|
||||||
|
@ -46,23 +45,24 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
self._latest_req_id += 1
|
self._latest_req_id += 1
|
||||||
return self._latest_req_id
|
return self._latest_req_id
|
||||||
|
|
||||||
def _connect(self, host=None, port=None):
|
def _connect(self, host: Optional[str] = None, port: Optional[int] = None):
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.logger.info('Connecting to Snapcast host {}:{}'.format(host, port))
|
self.logger.info('Connecting to Snapcast host %s:%d', host, port)
|
||||||
sock.connect((host or self.host, port or self.port))
|
sock.connect((host or self.host, port or self.port))
|
||||||
return sock
|
return sock
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _send(cls, sock, req):
|
def _send(cls, sock: socket.socket, req: dict):
|
||||||
if isinstance(req, dict):
|
if isinstance(req, dict):
|
||||||
req = json.dumps(req)
|
r = json.dumps(req)
|
||||||
if isinstance(req, str):
|
if isinstance(req, str):
|
||||||
req = req.encode()
|
r = req.encode()
|
||||||
if not isinstance(req, bytes):
|
if not isinstance(r, bytes):
|
||||||
raise RuntimeError('Unsupported type {} for Snapcast request: {}'.
|
raise RuntimeError(
|
||||||
format(type(req), req))
|
f'Unsupported type {type(req)} for Snapcast request: {req}'
|
||||||
|
)
|
||||||
|
|
||||||
sock.send(req + cls._SOCKET_EOL)
|
sock.send(r + cls._SOCKET_EOL)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _recv(cls, sock):
|
def _recv(cls, sock):
|
||||||
|
@ -71,133 +71,137 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
buf += sock.recv(1)
|
buf += sock.recv(1)
|
||||||
return json.loads(buf.decode().strip()).get('result')
|
return json.loads(buf.decode().strip()).get('result')
|
||||||
|
|
||||||
def _get_group(self, sock, group):
|
def _get_group(self, sock: socket.socket, group: str):
|
||||||
for g in self._status(sock).get('groups', []):
|
for g in self._status(sock).get('groups', []):
|
||||||
if group == g.get('id') or group == g.get('name'):
|
if group == g.get('id') or group == g.get('name'):
|
||||||
return g
|
return g
|
||||||
|
|
||||||
def _get_client(self, sock, client):
|
return None
|
||||||
|
|
||||||
|
def _get_client(self, sock: socket.socket, client: str):
|
||||||
for g in self._status(sock).get('groups', []):
|
for g in self._status(sock).get('groups', []):
|
||||||
clients = g.get('clients', [])
|
clients = g.get('clients', [])
|
||||||
|
|
||||||
for c in clients:
|
for c in clients:
|
||||||
if client == c.get('id') or \
|
if (
|
||||||
client == c.get('name') or \
|
client == c.get('id')
|
||||||
client == c.get('host', {}).get('name') or \
|
or client == c.get('name')
|
||||||
client == c.get('host', {}).get('ip'):
|
or client == c.get('host', {}).get('name')
|
||||||
|
or client == c.get('host', {}).get('ip')
|
||||||
|
):
|
||||||
c['group_id'] = g.get('id')
|
c['group_id'] = g.get('id')
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def _status(self, sock):
|
return None
|
||||||
|
|
||||||
|
def _status(self, sock: socket.socket):
|
||||||
request = {
|
request = {
|
||||||
'id': self._get_req_id(),
|
'id': self._get_req_id(),
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'Server.GetStatus'
|
'method': 'Server.GetStatus',
|
||||||
}
|
}
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return (self._recv(sock) or {}).get('server', {})
|
return (self._recv(sock) or {}).get('server', {})
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def status(self, host=None, port=None, client=None, group=None):
|
def status(
|
||||||
|
self,
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
client: Optional[str] = None,
|
||||||
|
group: Optional[str] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Get the status either of a Snapcast server, client or group
|
Get the status either of a Snapcast server, client or group
|
||||||
|
|
||||||
:param host: Snapcast server to query (default: default configured host)
|
:param host: Snapcast server to query (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
|
|
||||||
:param client: Client ID or name (default: None)
|
:param client: Client ID or name (default: None)
|
||||||
:type client: str
|
|
||||||
|
|
||||||
:param group: Group ID or name (default: None)
|
:param group: Group ID or name (default: None)
|
||||||
:type group: str
|
|
||||||
|
|
||||||
:returns: dict.
|
:returns: dict. Example:
|
||||||
|
|
||||||
Example::
|
.. code-block:: json
|
||||||
|
|
||||||
"output": {
|
"output": {
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
|
||||||
"clients": [
|
|
||||||
{
|
{
|
||||||
"config": {
|
"clients": [
|
||||||
"instance": 1,
|
{
|
||||||
"latency": 0,
|
"config": {
|
||||||
"name": "",
|
"instance": 1,
|
||||||
"volume": {
|
"latency": 0,
|
||||||
"muted": false,
|
"name": "",
|
||||||
"percent": 96
|
"volume": {
|
||||||
|
"muted": false,
|
||||||
|
"percent": 96
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connected": true,
|
||||||
|
"host": {
|
||||||
|
"arch": "x86_64",
|
||||||
|
"ip": "YOUR_IP",
|
||||||
|
"mac": "YOUR_MAC",
|
||||||
|
"name": "YOUR_NAME",
|
||||||
|
"os": "YOUR_OS"
|
||||||
|
},
|
||||||
|
"id": "YOUR_ID",
|
||||||
|
"lastSeen": {
|
||||||
|
"sec": 1546648311,
|
||||||
|
"usec": 86011
|
||||||
|
},
|
||||||
|
"snapclient": {
|
||||||
|
"name": "Snapclient",
|
||||||
|
"protocolVersion": 2,
|
||||||
|
"version": "0.15.0"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"connected": true,
|
],
|
||||||
"host": {
|
|
||||||
"arch": "x86_64",
|
|
||||||
"ip": "YOUR_IP",
|
|
||||||
"mac": "YOUR_MAC",
|
|
||||||
"name": "YOUR_NAME",
|
|
||||||
"os": "YOUR_OS"
|
|
||||||
},
|
|
||||||
"id": "YOUR_ID",
|
"id": "YOUR_ID",
|
||||||
"lastSeen": {
|
"muted": false,
|
||||||
"sec": 1546648311,
|
"name": "",
|
||||||
"usec": 86011
|
"stream_id": "mopidy"
|
||||||
},
|
|
||||||
"snapclient": {
|
|
||||||
"name": "Snapclient",
|
|
||||||
"protocolVersion": 2,
|
|
||||||
"version": "0.15.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"id": "YOUR_ID",
|
"server": {
|
||||||
"muted": false,
|
"host": {
|
||||||
"name": "",
|
"arch": "armv7l",
|
||||||
"stream_id": "mopidy"
|
"ip": "",
|
||||||
}
|
"mac": "",
|
||||||
],
|
"name": "YOUR_NAME",
|
||||||
"server": {
|
"os": "YOUR_OS"
|
||||||
"host": {
|
|
||||||
"arch": "armv7l",
|
|
||||||
"ip": "",
|
|
||||||
"mac": "",
|
|
||||||
"name": "YOUR_NAME",
|
|
||||||
"os": "YOUR_OS"
|
|
||||||
},
|
|
||||||
"snapserver": {
|
|
||||||
"controlProtocolVersion": 1,
|
|
||||||
"name": "Snapserver",
|
|
||||||
"protocolVersion": 1,
|
|
||||||
"version": "0.15.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"streams": [
|
|
||||||
{
|
|
||||||
"id": "mopidy",
|
|
||||||
"meta": {
|
|
||||||
"STREAM": "mopidy"
|
|
||||||
},
|
},
|
||||||
"status": "playing",
|
"snapserver": {
|
||||||
"uri": {
|
"controlProtocolVersion": 1,
|
||||||
"fragment": "",
|
"name": "Snapserver",
|
||||||
"host": "",
|
"protocolVersion": 1,
|
||||||
"path": "/tmp/snapfifo",
|
"version": "0.15.0"
|
||||||
"query": {
|
|
||||||
"buffer_ms": "20",
|
|
||||||
"codec": "pcm",
|
|
||||||
"name": "mopidy",
|
|
||||||
"sampleformat": "48000:16:2"
|
|
||||||
},
|
|
||||||
"raw": "pipe:////tmp/snapfifo?buffer_ms=20&codec=pcm&name=mopidy&sampleformat=48000:16:2",
|
|
||||||
"scheme": "pipe"
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
"streams": [
|
||||||
}
|
{
|
||||||
|
"id": "mopidy",
|
||||||
|
"meta": {
|
||||||
|
"STREAM": "mopidy"
|
||||||
|
},
|
||||||
|
"status": "playing",
|
||||||
|
"uri": {
|
||||||
|
"fragment": "",
|
||||||
|
"host": "",
|
||||||
|
"path": "/tmp/snapfifo",
|
||||||
|
"query": {
|
||||||
|
"buffer_ms": "20",
|
||||||
|
"codec": "pcm",
|
||||||
|
"name": "mopidy",
|
||||||
|
"sampleformat": "48000:16:2"
|
||||||
|
},
|
||||||
|
"raw": "pipe:////tmp/fifo?buffer_ms=20&codec=pcm&name=mopidy&sampleformat=48000:16:2",
|
||||||
|
"scheme": "pipe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -213,33 +217,32 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
return self._status(sock)
|
return self._status(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f'Error on socket close: {e}')
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def mute(self, client=None, group=None, mute=None, host=None, port=None):
|
def mute(
|
||||||
|
self,
|
||||||
|
client: Optional[str] = None,
|
||||||
|
group: Optional[str] = None,
|
||||||
|
mute: Optional[bool] = None,
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Set the mute status of a connected client or group
|
Set the mute status of a connected client or group
|
||||||
|
|
||||||
:param client: Client name or ID to mute
|
:param client: Client name or ID to mute
|
||||||
:type client: str
|
|
||||||
|
|
||||||
:param group: Group ID to mute
|
:param group: Group ID to mute
|
||||||
:type group: str
|
|
||||||
|
|
||||||
:param mute: Mute status. If not set, the mute status of the selected
|
:param mute: Mute status. If not set, the mute status of the selected
|
||||||
client/group will be toggled.
|
client/group will be toggled.
|
||||||
:type mute: bool
|
|
||||||
|
|
||||||
:param host: Snapcast server to query (default: default configured host)
|
:param host: Snapcast server to query (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not client and not group:
|
if not (client and group):
|
||||||
raise RuntimeError('Please specify either a client or a group')
|
raise RuntimeError('Please specify either a client or a group')
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
|
@ -250,59 +253,62 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
'id': self._get_req_id(),
|
'id': self._get_req_id(),
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'Group.SetMute' if group else 'Client.SetVolume',
|
'method': 'Group.SetMute' if group else 'Client.SetVolume',
|
||||||
'params': {}
|
'params': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if group:
|
if group:
|
||||||
group = self._get_group(sock, group)
|
g = self._get_group(sock, group)
|
||||||
cur_muted = group['muted']
|
assert g, f'No such group: {group}'
|
||||||
request['params']['id'] = group['id']
|
cur_muted = g['muted']
|
||||||
|
request['params']['id'] = g['id']
|
||||||
request['params']['mute'] = not cur_muted if mute is None else mute
|
request['params']['mute'] = not cur_muted if mute is None else mute
|
||||||
else:
|
elif client:
|
||||||
client = self._get_client(sock, client)
|
c = self._get_client(sock, client)
|
||||||
cur_muted = client['config']['volume']['muted']
|
assert c, f'No such client: {client}'
|
||||||
request['params']['id'] = client['id']
|
cur_muted = c['config']['volume']['muted']
|
||||||
|
request['params']['id'] = c['id']
|
||||||
request['params']['volume'] = {}
|
request['params']['volume'] = {}
|
||||||
request['params']['volume']['percent'] = client['config']['volume']['percent']
|
request['params']['volume']['percent'] = c['config']['volume'][
|
||||||
request['params']['volume']['muted'] = not cur_muted if mute is None else mute
|
'percent'
|
||||||
|
]
|
||||||
|
request['params']['volume']['muted'] = (
|
||||||
|
not cur_muted if mute is None else mute
|
||||||
|
)
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return self._recv(sock)
|
return self._recv(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on socket close', e)
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def volume(self, client, volume=None, delta=None, mute=None, host=None,
|
def volume(
|
||||||
port=None):
|
self,
|
||||||
|
client: str,
|
||||||
|
volume: Optional[int] = None,
|
||||||
|
delta: Optional[int] = None,
|
||||||
|
mute: Optional[bool] = None,
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Set the volume of a connected client
|
Set the volume of a connected client.
|
||||||
|
|
||||||
:param client: Client name or ID
|
:param client: Client name or ID
|
||||||
:type client: str
|
|
||||||
|
|
||||||
:param volume: Absolute volume to set between 0 and 100
|
:param volume: Absolute volume to set between 0 and 100
|
||||||
:type volume: int
|
|
||||||
|
|
||||||
:param delta: Relative volume change in percentage (e.g. +10 or -10)
|
:param delta: Relative volume change in percentage (e.g. +10 or -10)
|
||||||
:type delta: int
|
|
||||||
|
|
||||||
:param mute: Set to true or false if you want to toggle the muted status
|
:param mute: Set to true or false if you want to toggle the muted status
|
||||||
:type mute: bool
|
|
||||||
|
|
||||||
:param host: Snapcast server (default: default configured host)
|
:param host: Snapcast server (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if volume is None and delta is None and mute is None:
|
if volume is None and delta is None and mute is None:
|
||||||
raise RuntimeError('Please specify either an absolute volume or ' +
|
raise RuntimeError(
|
||||||
'relative delta')
|
'Please specify either an absolute volume or ' + 'relative delta'
|
||||||
|
)
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
|
|
||||||
|
@ -312,56 +318,51 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
'id': self._get_req_id(),
|
'id': self._get_req_id(),
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'Client.SetVolume',
|
'method': 'Client.SetVolume',
|
||||||
'params': {}
|
'params': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
client = self._get_client(sock, client)
|
c = self._get_client(sock, client)
|
||||||
cur_volume = int(client['config']['volume']['percent'])
|
assert c, f'No such client: {client}'
|
||||||
cur_mute = bool(client['config']['volume']['muted'])
|
cur_volume = int(c['config']['volume']['percent'])
|
||||||
|
cur_mute = bool(c['config']['volume']['muted'])
|
||||||
|
|
||||||
if volume is not None:
|
if volume is not None:
|
||||||
volume = int(volume)
|
volume = int(volume)
|
||||||
elif delta is not None:
|
elif delta is not None:
|
||||||
volume = cur_volume + int(delta)
|
volume = cur_volume + int(delta)
|
||||||
|
|
||||||
if volume is not None:
|
volume = max(0, min(100, volume)) if volume is not None else cur_volume
|
||||||
if volume > 100: volume = 100
|
|
||||||
if volume < 0: volume = 0
|
|
||||||
else:
|
|
||||||
volume = cur_volume
|
|
||||||
|
|
||||||
if mute is None:
|
if mute is None:
|
||||||
mute = cur_mute
|
mute = cur_mute
|
||||||
|
|
||||||
request['params']['id'] = client['id']
|
request['params']['id'] = c['id']
|
||||||
request['params']['volume'] = {}
|
request['params']['volume'] = {}
|
||||||
request['params']['volume']['percent'] = volume
|
request['params']['volume']['percent'] = volume
|
||||||
request['params']['volume']['muted'] = mute
|
request['params']['volume']['muted'] = mute
|
||||||
# noinspection PyTypeChecker
|
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return self._recv(sock)
|
return self._recv(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on socket close', e)
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_client_name(self, client, name, host=None, port=None):
|
def set_client_name(
|
||||||
|
self,
|
||||||
|
client: str,
|
||||||
|
name: str,
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Set/change the name of a connected client
|
Set/change the name of a connected client
|
||||||
|
|
||||||
:param client: Current client name or ID to rename
|
:param client: Current client name or ID to rename
|
||||||
:type client: str
|
|
||||||
|
|
||||||
:param name: New name
|
:param name: New name
|
||||||
:type name: str
|
|
||||||
|
|
||||||
:param host: Snapcast server (default: default configured host)
|
:param host: Snapcast server (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
|
@ -372,37 +373,37 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
'id': self._get_req_id(),
|
'id': self._get_req_id(),
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'Client.SetName',
|
'method': 'Client.SetName',
|
||||||
'params': {}
|
'params': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
client = self._get_client(sock, client)
|
c = self._get_client(sock, client)
|
||||||
request['params']['id'] = client['id']
|
assert c, f'No such client: {client}'
|
||||||
|
request['params']['id'] = c['id']
|
||||||
request['params']['name'] = name
|
request['params']['name'] = name
|
||||||
# noinspection PyTypeChecker
|
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return self._recv(sock)
|
return self._recv(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on socket close', e)
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_group_name(self, group, name, host=None, port=None):
|
def set_group_name(
|
||||||
|
self,
|
||||||
|
group: str,
|
||||||
|
name: str,
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Set/change the name of a group
|
Set/change the name of a group
|
||||||
|
|
||||||
:param group: Group ID to rename
|
:param group: Group ID to rename
|
||||||
:type group: str
|
|
||||||
|
|
||||||
:param name: New name
|
:param name: New name
|
||||||
:type name: str
|
|
||||||
|
|
||||||
:param host: Snapcast server (default: default configured host)
|
:param host: Snapcast server (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
|
@ -416,34 +417,33 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
'params': {
|
'params': {
|
||||||
'id': group,
|
'id': group,
|
||||||
'name': name,
|
'name': name,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return self._recv(sock)
|
return self._recv(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on socket close', e)
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_latency(self, client, latency, host=None, port=None):
|
def set_latency(
|
||||||
|
self,
|
||||||
|
client: str,
|
||||||
|
latency: float,
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Set/change the latency of a connected client
|
Set/change the latency of a connected client
|
||||||
|
|
||||||
:param client: Client name or ID
|
:param client: Client name or ID
|
||||||
:type client: str
|
|
||||||
|
|
||||||
:param latency: New latency in milliseconds
|
:param latency: New latency in milliseconds
|
||||||
:type latency: float
|
|
||||||
|
|
||||||
:param host: Snapcast server (default: default configured host)
|
:param host: Snapcast server (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
|
@ -454,35 +454,31 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
'id': self._get_req_id(),
|
'id': self._get_req_id(),
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'Client.SetLatency',
|
'method': 'Client.SetLatency',
|
||||||
'params': {
|
'params': {'latency': latency},
|
||||||
'latency': latency
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client = self._get_client(sock, client)
|
c = self._get_client(sock, client)
|
||||||
request['params']['id'] = client['id']
|
assert c, f'No such client: {client}'
|
||||||
# noinspection PyTypeChecker
|
request['params']['id'] = c['id']
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return self._recv(sock)
|
return self._recv(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on socket close', e)
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def delete_client(self, client, host=None, port=None):
|
def delete_client(
|
||||||
|
self, client: str, host: Optional[str] = None, port: Optional[int] = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Delete a client from the Snapcast server
|
Delete a client from the Snapcast server
|
||||||
|
|
||||||
:param client: Client name or ID
|
:param client: Client name or ID
|
||||||
:type client: str
|
|
||||||
|
|
||||||
:param host: Snapcast server (default: default configured host)
|
:param host: Snapcast server (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
|
@ -493,176 +489,188 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
'id': self._get_req_id(),
|
'id': self._get_req_id(),
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'Server.DeleteClient',
|
'method': 'Server.DeleteClient',
|
||||||
'params': {}
|
'params': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
client = self._get_client(sock, client)
|
c = self._get_client(sock, client)
|
||||||
request['params']['id'] = client['id']
|
assert c, f'No such client: {client}'
|
||||||
# noinspection PyTypeChecker
|
request['params']['id'] = c['id']
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return self._recv(sock)
|
return self._recv(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on socket close', e)
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def group_set_clients(self, group, clients, host=None, port=None):
|
def group_set_clients(
|
||||||
|
self,
|
||||||
|
group: str,
|
||||||
|
clients: Collection[str],
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Sets the clients for a group on a Snapcast server
|
Sets the clients for a group on a Snapcast server
|
||||||
|
|
||||||
:param group: Group name or ID
|
:param group: Group name or ID
|
||||||
:type group: str
|
|
||||||
|
|
||||||
:param clients: List of client names or IDs
|
:param clients: List of client names or IDs
|
||||||
:type clients: list[str]
|
|
||||||
|
|
||||||
:param host: Snapcast server (default: default configured host)
|
:param host: Snapcast server (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock = self._connect(host or self.host, port or self.port)
|
sock = self._connect(host or self.host, port or self.port)
|
||||||
group = self._get_group(sock, group)
|
g = self._get_group(sock, group)
|
||||||
|
assert g, f'No such group: {group}'
|
||||||
request = {
|
request = {
|
||||||
'id': self._get_req_id(),
|
'id': self._get_req_id(),
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'Group.SetClients',
|
'method': 'Group.SetClients',
|
||||||
'params': {
|
'params': {'id': g['id'], 'clients': []},
|
||||||
'id': group['id'],
|
|
||||||
'clients': []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for client in clients:
|
for client in clients:
|
||||||
client = self._get_client(sock, client)
|
c = self._get_client(sock, client)
|
||||||
request['params']['clients'].append(client['id'])
|
assert c, f'No such client: {client}'
|
||||||
|
request['params']['clients'].append(c['id'])
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return self._recv(sock)
|
return self._recv(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on socket close', e)
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def group_set_stream(self, group, stream_id, host=None, port=None):
|
def group_set_stream(
|
||||||
|
self,
|
||||||
|
group: str,
|
||||||
|
stream_id: str,
|
||||||
|
host: Optional[str] = None,
|
||||||
|
port: Optional[int] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Sets the active stream for a group.
|
Sets the active stream for a group.
|
||||||
|
|
||||||
:param group: Group name or ID
|
:param group: Group name or ID
|
||||||
:type group: str
|
|
||||||
|
|
||||||
:param stream_id: Stream ID
|
:param stream_id: Stream ID
|
||||||
:type stream_id: str
|
|
||||||
|
|
||||||
:param host: Snapcast server (default: default configured host)
|
:param host: Snapcast server (default: default configured host)
|
||||||
:type host: str
|
|
||||||
|
|
||||||
:param port: Snapcast server port (default: default configured port)
|
:param port: Snapcast server port (default: default configured port)
|
||||||
:type port: int
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sock = None
|
sock = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sock = self._connect(host or self.host, port or self.port)
|
sock = self._connect(host or self.host, port or self.port)
|
||||||
group = self._get_group(sock, group)
|
g = self._get_group(sock, group)
|
||||||
|
assert g, f'No such group: {group}'
|
||||||
request = {
|
request = {
|
||||||
'id': self._get_req_id(),
|
'id': self._get_req_id(),
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'Group.SetStream',
|
'method': 'Group.SetStream',
|
||||||
'params': {
|
'params': {
|
||||||
'id': group['id'],
|
'id': g['id'],
|
||||||
'stream_id': stream_id,
|
'stream_id': stream_id,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
self._send(sock, request)
|
self._send(sock, request)
|
||||||
return self._recv(sock)
|
return self._recv(sock)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
sock.close()
|
if sock:
|
||||||
|
sock.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Error on socket close', e)
|
self.logger.warning('Error on socket close: %s', e)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_backend_hosts(self):
|
def get_backend_hosts(self):
|
||||||
"""
|
"""
|
||||||
:return: A dict with the Snapcast hosts configured on the backend
|
:return: A dict with the Snapcast hosts configured on the backend
|
||||||
in the format host -> port
|
in the format ``host -> port``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
hosts = {}
|
return {
|
||||||
for i in range(len(self.backend_hosts)):
|
host: self.backend_ports[i] for i, host in enumerate(self.backend_hosts)
|
||||||
hosts[self.backend_hosts[i]] = self.backend_ports[i]
|
}
|
||||||
return hosts
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_playing_streams(self, exclude_local=False):
|
def get_playing_streams(self, exclude_local: bool = False):
|
||||||
"""
|
"""
|
||||||
Returns the remote streams configured in the `music.snapcast` backend
|
Returns the remote streams configured in the `music.snapcast` backend
|
||||||
that are currently active and unmuted.
|
that are currently active and unmuted.
|
||||||
|
|
||||||
:param exclude_local: Exclude localhost connections (default: False)
|
:param exclude_local: Exclude localhost connections (default: False)
|
||||||
:type exclude_local: bool
|
|
||||||
|
|
||||||
:returns: dict with the host->port mapping.
|
:returns: dict with the host->port mapping. Example:
|
||||||
|
|
||||||
Example::
|
.. code-block:: json
|
||||||
|
|
||||||
{
|
{
|
||||||
"hosts": {
|
"hosts": {
|
||||||
"server_1": 1705,
|
"server_1": 1705,
|
||||||
"server_2": 1705,
|
"server_2": 1705,
|
||||||
"server_3": 1705
|
"server_3": 1705
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
backend_hosts = self.get_backend_hosts().output
|
backend_hosts: dict = self.get_backend_hosts().output # type: ignore
|
||||||
playing_hosts = {}
|
playing_hosts = {}
|
||||||
|
|
||||||
def _worker(host, port):
|
def _worker(host, port):
|
||||||
try:
|
try:
|
||||||
if exclude_local and (host == 'localhost'
|
if exclude_local and (
|
||||||
or host == Config.get('device_id')):
|
host == 'localhost' or host == Config.get('device_id')
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
server_status = self.status(host=host, port=port).output
|
server_status: dict = self.status(host=host, port=port).output # type: ignore
|
||||||
client_status = self.status(host=host, port=port,
|
client_status: dict = self.status( # type: ignore
|
||||||
client=Config.get('device_id')).output
|
host=host, port=port, client=Config.get('device_id')
|
||||||
|
).output
|
||||||
|
|
||||||
if client_status.get('config', {}).get('volume', {}).get('muted'):
|
if client_status.get('config', {}).get('volume', {}).get('muted'):
|
||||||
return
|
return
|
||||||
|
|
||||||
group = [g for g in server_status.get('groups', {})
|
group = next(
|
||||||
if g.get('id') == client_status.get('group_id')].pop(0)
|
iter(
|
||||||
|
g
|
||||||
|
for g in server_status.get('groups', {})
|
||||||
|
if g.get('id') == client_status.get('group_id')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if group.get('muted'):
|
if group.get('muted'):
|
||||||
return
|
return
|
||||||
|
|
||||||
stream = [s for s in server_status.get('streams')
|
stream = next(
|
||||||
if s.get('id') == group.get('stream_id')].pop(0)
|
iter(
|
||||||
|
s
|
||||||
|
for s in server_status.get('streams', {})
|
||||||
|
if s.get('id') == group.get('stream_id')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if stream.get('status') != 'playing':
|
if stream.get('status') != 'playing':
|
||||||
return
|
return
|
||||||
|
|
||||||
playing_hosts[host] = port
|
playing_hosts[host] = port
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(('Error while retrieving the status of ' +
|
self.logger.warning(
|
||||||
'Snapcast host at {}:{}: {}').format(
|
'Error while retrieving the status of Snapcast host at %s:%d: %s',
|
||||||
host, port, str(e)))
|
host,
|
||||||
|
port,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
workers = []
|
workers = []
|
||||||
|
|
||||||
|
@ -677,4 +685,5 @@ class MusicSnapcastPlugin(Plugin):
|
||||||
|
|
||||||
return {'hosts': playing_hosts}
|
return {'hosts': playing_hosts}
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -79,10 +79,6 @@ class ZwaveMqttPlugin(
|
||||||
This plugin allows you to manage a Z-Wave network over MQTT through
|
This plugin allows you to manage a Z-Wave network over MQTT through
|
||||||
`zwave-js-ui <https://github.com/zwave-js/zwave-js-ui>`_.
|
`zwave-js-ui <https://github.com/zwave-js/zwave-js-ui>`_.
|
||||||
|
|
||||||
For historical reasons, it is advised to enable this plugin together
|
|
||||||
with the ``zwave.mqtt`` backend, or you may lose the ability to listen
|
|
||||||
to asynchronous events.
|
|
||||||
|
|
||||||
Configuration required on the zwave-js-ui gateway:
|
Configuration required on the zwave-js-ui gateway:
|
||||||
|
|
||||||
* Install the gateway following the instructions reported
|
* Install the gateway following the instructions reported
|
||||||
|
|
Loading…
Reference in a new issue