forked from platypush/platypush
parent
e96eae73ec
commit
2066db463b
8 changed files with 301 additions and 232 deletions
|
@ -274,6 +274,10 @@ class Application:
|
||||||
backend.stop()
|
backend.stop()
|
||||||
|
|
||||||
for plugin in runnable_plugins:
|
for plugin in runnable_plugins:
|
||||||
|
# This is required because some plugins may redefine the `stop` method.
|
||||||
|
# In that case, at the very least the _should_stop event should be
|
||||||
|
# set to notify the plugin to stop.
|
||||||
|
plugin._should_stop.set() # pylint: disable=protected-access
|
||||||
plugin.stop()
|
plugin.stop()
|
||||||
|
|
||||||
for backend in backends:
|
for backend in backends:
|
||||||
|
|
|
@ -1,174 +0,0 @@
|
||||||
import time
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.context import get_plugin
|
|
||||||
from platypush.message.event.music import (
|
|
||||||
MusicPlayEvent,
|
|
||||||
MusicPauseEvent,
|
|
||||||
MusicStopEvent,
|
|
||||||
NewPlayingTrackEvent,
|
|
||||||
PlaylistChangeEvent,
|
|
||||||
VolumeChangeEvent,
|
|
||||||
PlaybackConsumeModeChangeEvent,
|
|
||||||
PlaybackSingleModeChangeEvent,
|
|
||||||
PlaybackRepeatModeChangeEvent,
|
|
||||||
PlaybackRandomModeChangeEvent,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MusicMpdBackend(Backend):
|
|
||||||
"""
|
|
||||||
This backend listens for events on a MPD/Mopidy music server.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* :class:`platypush.plugins.music.mpd.MusicMpdPlugin` configured
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, server='localhost', port=6600, poll_seconds=3, **kwargs):
|
|
||||||
"""
|
|
||||||
:param poll_seconds: Interval between queries to the server (default: 3 seconds)
|
|
||||||
:type poll_seconds: float
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
self.server = server
|
|
||||||
self.port = port
|
|
||||||
self.poll_seconds = poll_seconds
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
|
|
||||||
last_status = {}
|
|
||||||
last_state = None
|
|
||||||
last_track = None
|
|
||||||
last_playlist = None
|
|
||||||
|
|
||||||
while not self.should_stop():
|
|
||||||
success = False
|
|
||||||
state = None
|
|
||||||
status = None
|
|
||||||
playlist = None
|
|
||||||
track = None
|
|
||||||
|
|
||||||
while not success:
|
|
||||||
try:
|
|
||||||
plugin = get_plugin('music.mpd')
|
|
||||||
if not plugin:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
status = plugin.status().output
|
|
||||||
if not status or status.get('state') is None:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
track = plugin.currentsong().output
|
|
||||||
state = status['state'].lower()
|
|
||||||
playlist = status['playlist']
|
|
||||||
success = True
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.debug(e)
|
|
||||||
get_plugin('music.mpd', reload=True)
|
|
||||||
if not state:
|
|
||||||
state = last_state
|
|
||||||
if not playlist:
|
|
||||||
playlist = last_playlist
|
|
||||||
if not track:
|
|
||||||
track = last_track
|
|
||||||
finally:
|
|
||||||
time.sleep(self.poll_seconds)
|
|
||||||
|
|
||||||
if state != last_state:
|
|
||||||
if state == 'stop':
|
|
||||||
self.bus.post(
|
|
||||||
MusicStopEvent(
|
|
||||||
status=status, track=track, plugin_name='music.mpd'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif state == 'pause':
|
|
||||||
self.bus.post(
|
|
||||||
MusicPauseEvent(
|
|
||||||
status=status, track=track, plugin_name='music.mpd'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif state == 'play':
|
|
||||||
self.bus.post(
|
|
||||||
MusicPlayEvent(
|
|
||||||
status=status, track=track, plugin_name='music.mpd'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if playlist != last_playlist:
|
|
||||||
if last_playlist:
|
|
||||||
# XXX plchanges can become heavy with big playlists,
|
|
||||||
# PlaylistChangeEvent temporarily disabled
|
|
||||||
# changes = plugin.plchanges(last_playlist).output
|
|
||||||
# self.bus.post(PlaylistChangeEvent(changes=changes))
|
|
||||||
self.bus.post(PlaylistChangeEvent(plugin_name='music.mpd'))
|
|
||||||
last_playlist = playlist
|
|
||||||
|
|
||||||
if state == 'play' and track != last_track:
|
|
||||||
self.bus.post(
|
|
||||||
NewPlayingTrackEvent(
|
|
||||||
status=status, track=track, plugin_name='music.mpd'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if last_status.get('volume') != status['volume']:
|
|
||||||
self.bus.post(
|
|
||||||
VolumeChangeEvent(
|
|
||||||
volume=int(status['volume']),
|
|
||||||
status=status,
|
|
||||||
track=track,
|
|
||||||
plugin_name='music.mpd',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if last_status.get('random') != status['random']:
|
|
||||||
self.bus.post(
|
|
||||||
PlaybackRandomModeChangeEvent(
|
|
||||||
state=bool(int(status['random'])),
|
|
||||||
status=status,
|
|
||||||
track=track,
|
|
||||||
plugin_name='music.mpd',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if last_status.get('repeat') != status['repeat']:
|
|
||||||
self.bus.post(
|
|
||||||
PlaybackRepeatModeChangeEvent(
|
|
||||||
state=bool(int(status['repeat'])),
|
|
||||||
status=status,
|
|
||||||
track=track,
|
|
||||||
plugin_name='music.mpd',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if last_status.get('consume') != status['consume']:
|
|
||||||
self.bus.post(
|
|
||||||
PlaybackConsumeModeChangeEvent(
|
|
||||||
state=bool(int(status['consume'])),
|
|
||||||
status=status,
|
|
||||||
track=track,
|
|
||||||
plugin_name='music.mpd',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if last_status.get('single') != status['single']:
|
|
||||||
self.bus.post(
|
|
||||||
PlaybackSingleModeChangeEvent(
|
|
||||||
state=bool(int(status['single'])),
|
|
||||||
status=status,
|
|
||||||
track=track,
|
|
||||||
plugin_name='music.mpd',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
last_status = status
|
|
||||||
last_state = state
|
|
||||||
last_track = track
|
|
||||||
time.sleep(self.poll_seconds)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,22 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.music.MusicPauseEvent: if the playback state changed to
|
|
||||||
pause
|
|
||||||
platypush.message.event.music.MusicPlayEvent: if the playback state changed to
|
|
||||||
play
|
|
||||||
platypush.message.event.music.MusicStopEvent: if the playback state changed to
|
|
||||||
stop
|
|
||||||
platypush.message.event.music.NewPlayingTrackEvent: if a new track is being played
|
|
||||||
platypush.message.event.music.PlaylistChangeEvent: if the main playlist has changed
|
|
||||||
platypush.message.event.music.VolumeChangeEvent: if the main volume has changed
|
|
||||||
install:
|
|
||||||
apt:
|
|
||||||
- python3-mpd2
|
|
||||||
dnf:
|
|
||||||
- python-mpd2
|
|
||||||
pacman:
|
|
||||||
- python-mpd2
|
|
||||||
pip:
|
|
||||||
- python-mpd2
|
|
||||||
package: platypush.backend.music.mpd
|
|
||||||
type: backend
|
|
|
@ -320,6 +320,18 @@ backend.http:
|
||||||
# port: 6600
|
# port: 6600
|
||||||
###
|
###
|
||||||
|
|
||||||
|
###
|
||||||
|
# # Example last.fm scrobbler configuration, to synchronize your music
|
||||||
|
# # activities to your Last.fm profile. You'll need to register an application
|
||||||
|
# # with your account at https://www.last.fm/api.
|
||||||
|
#
|
||||||
|
# lastfm:
|
||||||
|
# api_key: <API_KEY>
|
||||||
|
# api_secret: <API_SECRET>
|
||||||
|
# username: <USERNAME>
|
||||||
|
# password: <PASSWORD>
|
||||||
|
###
|
||||||
|
|
||||||
###
|
###
|
||||||
# # Plugins with empty configuration can also be explicitly enabled by specifying
|
# # Plugins with empty configuration can also be explicitly enabled by specifying
|
||||||
# # `enabled: true` or `disabled: false`. An integration with no items will be
|
# # `enabled: true` or `disabled: false`. An integration with no items will be
|
||||||
|
@ -1100,6 +1112,23 @@ backend.http:
|
||||||
# source: ${msg["source"]}
|
# source: ${msg["source"]}
|
||||||
###
|
###
|
||||||
|
|
||||||
|
###
|
||||||
|
# # The example below is a hook that reacts when a `NewPlayingTrackEvent` event
|
||||||
|
# # is received and synchronize the listening activity to the users' Last.fm
|
||||||
|
# # profile (it requires the `lastfm` plugin and at least a music plugin
|
||||||
|
# # enabled, like `music.mpd`).
|
||||||
|
#
|
||||||
|
# event.hook.OnNewMusicActivity:
|
||||||
|
# if:
|
||||||
|
# type: platypush.message.event.music.NewPlayingTrackEvent
|
||||||
|
# then:
|
||||||
|
# - if ${track.get('artist') and track.get('title')}:
|
||||||
|
# - action: lastfm.scrobble
|
||||||
|
# args:
|
||||||
|
# artist: ${track['artist']}
|
||||||
|
# title: ${track['title']}
|
||||||
|
##
|
||||||
|
|
||||||
###
|
###
|
||||||
# # The example below plays the music on mpd/mopidy when your voice assistant
|
# # The example below plays the music on mpd/mopidy when your voice assistant
|
||||||
# # triggers a speech recognized event with "play the music" content.
|
# # triggers a speech recognized event with "play the music" content.
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
from typing import Collection, Optional, Union
|
from typing import Collection, Optional, Union
|
||||||
|
|
||||||
from platypush.plugins import action
|
from platypush.plugins import RunnablePlugin, action
|
||||||
from platypush.plugins.music import MusicPlugin
|
from platypush.plugins.music import MusicPlugin
|
||||||
|
|
||||||
|
from ._conf import MpdConfig
|
||||||
|
from ._listener import MpdListener
|
||||||
|
|
||||||
class MusicMpdPlugin(MusicPlugin):
|
|
||||||
|
class MusicMpdPlugin(MusicPlugin, RunnablePlugin):
|
||||||
"""
|
"""
|
||||||
This plugin allows you to interact with an MPD/Mopidy music server.
|
This plugin allows you to interact with an MPD/Mopidy music server.
|
||||||
|
|
||||||
|
@ -21,22 +23,29 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
|
|
||||||
.. note:: As of Mopidy 3.0 MPD is an optional interface provided by the
|
.. note:: As of Mopidy 3.0 MPD is an optional interface provided by the
|
||||||
``mopidy-mpd`` extension. Make sure that you have the extension
|
``mopidy-mpd`` extension. Make sure that you have the extension
|
||||||
installed and enabled on your instance to use this plugin with your
|
installed and enabled on your instance to use this plugin if you want to
|
||||||
server.
|
use it with Mopidy instead of MPD.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_client_lock = threading.RLock()
|
_client_lock = threading.RLock()
|
||||||
|
|
||||||
def __init__(self, host: str, port: int = 6600):
|
def __init__(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
port: int = 6600,
|
||||||
|
poll_interval: Optional[float] = 5.0,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param host: MPD IP/hostname
|
:param host: MPD IP/hostname.
|
||||||
:param port: MPD port (default: 6600)
|
:param port: MPD port (default: 6600).
|
||||||
|
:param poll_interval: Polling interval in seconds. If set, the plugin
|
||||||
|
will poll the MPD server for status updates and trigger change
|
||||||
|
events when required. Default: 5 seconds.
|
||||||
"""
|
"""
|
||||||
|
super().__init__(poll_interval=poll_interval, **kwargs)
|
||||||
super().__init__()
|
self.conf = MpdConfig(host=host, port=port)
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.client = None
|
self.client = None
|
||||||
|
|
||||||
def _connect(self, n_tries: int = 2):
|
def _connect(self, n_tries: int = 2):
|
||||||
|
@ -51,7 +60,7 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
try:
|
try:
|
||||||
n_tries -= 1
|
n_tries -= 1
|
||||||
self.client = mpd.MPDClient()
|
self.client = mpd.MPDClient()
|
||||||
self.client.connect(self.host, self.port)
|
self.client.connect(self.conf.host, self.conf.port)
|
||||||
return self.client
|
return self.client
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = e
|
error = e
|
||||||
|
@ -60,7 +69,7 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
e,
|
e,
|
||||||
(': Retrying' if n_tries > 0 else ''),
|
(': Retrying' if n_tries > 0 else ''),
|
||||||
)
|
)
|
||||||
time.sleep(0.5)
|
self.wait_stop(0.5)
|
||||||
|
|
||||||
self.client = None
|
self.client = None
|
||||||
if error:
|
if error:
|
||||||
|
@ -83,7 +92,8 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
response = getattr(self.client, method)(*args, **kwargs)
|
response = getattr(self.client, method)(*args, **kwargs)
|
||||||
|
|
||||||
if return_status:
|
if return_status:
|
||||||
return self.status().output
|
return self._status()
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = str(e)
|
error = str(e)
|
||||||
|
@ -145,7 +155,7 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
return self._exec('play') if status in ('pause', 'stop') else None
|
return self._exec('play') if status in ('pause', 'stop') else None
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def stop(self, *_, **__):
|
def stop(self, *_, **__): # type: ignore
|
||||||
"""Stop playback"""
|
"""Stop playback"""
|
||||||
return self._exec('stop')
|
return self._exec('stop')
|
||||||
|
|
||||||
|
@ -185,6 +195,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
|
|
||||||
:param vol: Volume value (range: 0-100).
|
:param vol: Volume value (range: 0-100).
|
||||||
"""
|
"""
|
||||||
|
self.logger.warning(
|
||||||
|
'music.mpd.setvol is deprecated, use music.mpd.set_volume instead'
|
||||||
|
)
|
||||||
return self.set_volume(vol)
|
return self.set_volume(vol)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -404,6 +417,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
:param value: Seek position in seconds, or delta string (e.g. '+15' or
|
:param value: Seek position in seconds, or delta string (e.g. '+15' or
|
||||||
'-15') to indicate a seek relative to the current position
|
'-15') to indicate a seek relative to the current position
|
||||||
"""
|
"""
|
||||||
|
self.logger.warning(
|
||||||
|
'music.mpd.seekcur is deprecated, use music.mpd.seek instead'
|
||||||
|
)
|
||||||
return self.seek(value)
|
return self.seek(value)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -472,6 +488,24 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
return self._status()
|
return self._status()
|
||||||
|
|
||||||
|
def _current_track(self):
|
||||||
|
track = self._exec('currentsong', return_status=False)
|
||||||
|
if not isinstance(track, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if 'title' in track and (
|
||||||
|
'artist' not in track
|
||||||
|
or not track['artist']
|
||||||
|
or re.search('^https?://', track['file'])
|
||||||
|
or re.search('^tunein:', track['file'])
|
||||||
|
):
|
||||||
|
m = re.match(r'^\s*(.+?)\s+-\s+(.*)\s*$', track['title'])
|
||||||
|
if m and m.group(1) and m.group(2):
|
||||||
|
track['artist'] = m.group(1)
|
||||||
|
track['title'] = m.group(2)
|
||||||
|
|
||||||
|
return track
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def currentsong(self):
|
def currentsong(self):
|
||||||
"""
|
"""
|
||||||
|
@ -500,23 +534,7 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"x-albumuri": "spotify:album:6q5KhDhf9BZkoob7uAnq19"
|
"x-albumuri": "spotify:album:6q5KhDhf9BZkoob7uAnq19"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
return self._current_track()
|
||||||
track = self._exec('currentsong', return_status=False)
|
|
||||||
if not isinstance(track, dict):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if 'title' in track and (
|
|
||||||
'artist' not in track
|
|
||||||
or not track['artist']
|
|
||||||
or re.search('^https?://', track['file'])
|
|
||||||
or re.search('^tunein:', track['file'])
|
|
||||||
):
|
|
||||||
m = re.match(r'^\s*(.+?)\s+-\s+(.*)\s*$', track['title'])
|
|
||||||
if m and m.group(1) and m.group(2):
|
|
||||||
track['artist'] = m.group(1)
|
|
||||||
track['title'] = m.group(2)
|
|
||||||
|
|
||||||
return track
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def playlistinfo(self):
|
def playlistinfo(self):
|
||||||
|
@ -589,6 +607,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.playlists`.
|
Deprecated alias for :meth:`.playlists`.
|
||||||
"""
|
"""
|
||||||
|
self.logger.warning(
|
||||||
|
'music.mpd.listplaylists is deprecated, use music.mpd.get_playlists instead'
|
||||||
|
)
|
||||||
return self.get_playlists()
|
return self.get_playlists()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -611,6 +632,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.playlist`.
|
Deprecated alias for :meth:`.playlist`.
|
||||||
"""
|
"""
|
||||||
|
self.logger.warning(
|
||||||
|
'music.mpd.listplaylist is deprecated, use music.mpd.get_playlist instead'
|
||||||
|
)
|
||||||
return self._exec('listplaylist', name, return_status=False)
|
return self._exec('listplaylist', name, return_status=False)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -618,10 +642,15 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.playlist` with ``with_tracks=True``.
|
Deprecated alias for :meth:`.playlist` with ``with_tracks=True``.
|
||||||
"""
|
"""
|
||||||
|
self.logger.warning(
|
||||||
|
'music.mpd.listplaylistinfo is deprecated, use music.mpd.get_playlist instead'
|
||||||
|
)
|
||||||
return self.get_playlist(name, with_tracks=True)
|
return self.get_playlist(name, with_tracks=True)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def add_to_playlist(self, playlist: str, resources: Union[str, Collection[str]]):
|
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.
|
||||||
|
|
||||||
|
@ -640,6 +669,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.add_to_playlist`.
|
Deprecated alias for :meth:`.add_to_playlist`.
|
||||||
"""
|
"""
|
||||||
|
self.logger.warning(
|
||||||
|
'music.mpd.playlistadd is deprecated, use music.mpd.add_to_playlist instead'
|
||||||
|
)
|
||||||
return self.add_to_playlist(name, uri)
|
return self.add_to_playlist(name, uri)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -677,6 +709,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.remove_from_playlist`.
|
Deprecated alias for :meth:`.remove_from_playlist`.
|
||||||
"""
|
"""
|
||||||
|
self.logger.warning(
|
||||||
|
'music.mpd.playlistdelete is deprecated, use music.mpd.remove_from_playlist instead'
|
||||||
|
)
|
||||||
return self.remove_from_playlist(name, pos)
|
return self.remove_from_playlist(name, pos)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -684,6 +719,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
"""
|
"""
|
||||||
Deprecated alias for :meth:`.playlist_move`.
|
Deprecated alias for :meth:`.playlist_move`.
|
||||||
"""
|
"""
|
||||||
|
self.logger.warning(
|
||||||
|
'music.mpd.playlistmove is deprecated, use music.mpd.playlist_move instead'
|
||||||
|
)
|
||||||
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
|
||||||
|
@ -816,7 +854,9 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def searchadd(self, filter: dict, *args, **kwargs):
|
def searchadd(
|
||||||
|
self, filter: dict, *args, **kwargs # pylint: disable=redefined-builtin
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Free search by filter and add the results to the current playlist.
|
Free search by filter and add the results to the current playlist.
|
||||||
|
|
||||||
|
@ -828,5 +868,16 @@ class MusicMpdPlugin(MusicPlugin):
|
||||||
'searchadd', *filter_list, *args, return_status=False, **kwargs
|
'searchadd', *filter_list, *args, return_status=False, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
listener = None
|
||||||
|
if self.poll_interval is not None:
|
||||||
|
listener = MpdListener(self)
|
||||||
|
listener.start()
|
||||||
|
|
||||||
|
self.wait_stop()
|
||||||
|
|
||||||
|
if listener:
|
||||||
|
listener.join()
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
12
platypush/plugins/music/mpd/_conf.py
Normal file
12
platypush/plugins/music/mpd/_conf.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MpdConfig:
|
||||||
|
"""
|
||||||
|
MPD configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
host: str = 'localhost'
|
||||||
|
port: int = 6600
|
||||||
|
password: str = ''
|
163
platypush/plugins/music/mpd/_listener.py
Normal file
163
platypush/plugins/music/mpd/_listener.py
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from threading import Thread
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from platypush.context import get_bus
|
||||||
|
from platypush.message.event.music import (
|
||||||
|
MusicPlayEvent,
|
||||||
|
MusicPauseEvent,
|
||||||
|
MusicStopEvent,
|
||||||
|
NewPlayingTrackEvent,
|
||||||
|
PlaylistChangeEvent,
|
||||||
|
VolumeChangeEvent,
|
||||||
|
PlaybackConsumeModeChangeEvent,
|
||||||
|
PlaybackSingleModeChangeEvent,
|
||||||
|
PlaybackRepeatModeChangeEvent,
|
||||||
|
PlaybackRandomModeChangeEvent,
|
||||||
|
)
|
||||||
|
from platypush.plugins.music import MusicPlugin
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MpdStatus:
|
||||||
|
"""
|
||||||
|
Data class for the MPD status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
state: Optional[str] = None
|
||||||
|
playlist: Optional[int] = None
|
||||||
|
volume: Optional[int] = None
|
||||||
|
random: Optional[bool] = None
|
||||||
|
repeat: Optional[bool] = None
|
||||||
|
consume: Optional[bool] = None
|
||||||
|
single: Optional[bool] = None
|
||||||
|
track: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class MpdListener(Thread):
|
||||||
|
"""
|
||||||
|
Thread that listens/polls for MPD events and posts them to the bus.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, plugin: MusicPlugin, *_, **__):
|
||||||
|
from . import MusicMpdPlugin
|
||||||
|
|
||||||
|
super().__init__(name='platypush:mpd:listener')
|
||||||
|
assert isinstance(plugin, MusicMpdPlugin)
|
||||||
|
self.plugin: MusicMpdPlugin = plugin
|
||||||
|
self._status = MpdStatus()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger(self):
|
||||||
|
return self.plugin.logger
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bus(self):
|
||||||
|
return get_bus()
|
||||||
|
|
||||||
|
def wait_stop(self, timeout=None):
|
||||||
|
self.plugin.wait_stop(timeout=timeout)
|
||||||
|
|
||||||
|
def _process_events(self, status: dict, track: Optional[dict] = None):
|
||||||
|
state = status.get('state', '').lower()
|
||||||
|
evt_args = {'status': status, 'track': track, 'plugin_name': 'music.mpd'}
|
||||||
|
|
||||||
|
if state != self._status.state:
|
||||||
|
if state == 'stop':
|
||||||
|
self.bus.post(MusicStopEvent(**evt_args))
|
||||||
|
elif state == 'pause':
|
||||||
|
self.bus.post(MusicPauseEvent(**evt_args))
|
||||||
|
elif state == 'play':
|
||||||
|
self.bus.post(MusicPlayEvent(**evt_args))
|
||||||
|
|
||||||
|
if status.get('playlist') != self._status.playlist and self._status.playlist:
|
||||||
|
# XXX plchanges can become heavy with big playlists,
|
||||||
|
# PlaylistChangeEvent temporarily disabled
|
||||||
|
# changes = plugin.plchanges(last_playlist).output
|
||||||
|
# self.bus.post(PlaylistChangeEvent(changes=changes))
|
||||||
|
self.bus.post(PlaylistChangeEvent(plugin_name='music.mpd'))
|
||||||
|
|
||||||
|
if state == 'play' and track != self._status.track:
|
||||||
|
self.bus.post(NewPlayingTrackEvent(**evt_args))
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.get('volume') is not None
|
||||||
|
and status.get('volume') != self._status.volume
|
||||||
|
):
|
||||||
|
self.bus.post(VolumeChangeEvent(volume=int(status['volume']), **evt_args))
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.get('random') is not None
|
||||||
|
and status.get('random') != self._status.random
|
||||||
|
):
|
||||||
|
self.bus.post(
|
||||||
|
PlaybackRandomModeChangeEvent(
|
||||||
|
state=bool(int(status['random'])), **evt_args
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.get('repeat') is not None
|
||||||
|
and status.get('repeat') != self._status.repeat
|
||||||
|
):
|
||||||
|
self.bus.post(
|
||||||
|
PlaybackRepeatModeChangeEvent(
|
||||||
|
state=bool(int(status['repeat'])), **evt_args
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.get('consume') is not None
|
||||||
|
and status.get('consume') != self._status.consume
|
||||||
|
):
|
||||||
|
self.bus.post(
|
||||||
|
PlaybackConsumeModeChangeEvent(
|
||||||
|
state=bool(int(status['consume'])), **evt_args
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.get('single') is not None
|
||||||
|
and status.get('single') != self._status.single
|
||||||
|
):
|
||||||
|
self.bus.post(
|
||||||
|
PlaybackSingleModeChangeEvent(
|
||||||
|
state=bool(int(status['single'])), **evt_args
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_status(self, status: dict, track: Optional[dict] = None):
|
||||||
|
self._status = MpdStatus(
|
||||||
|
state=status.get('state', '').lower(),
|
||||||
|
playlist=status.get('playlist'),
|
||||||
|
volume=status.get('volume'),
|
||||||
|
random=status.get('random'),
|
||||||
|
repeat=status.get('repeat'),
|
||||||
|
consume=status.get('consume'),
|
||||||
|
single=status.get('single'),
|
||||||
|
track=track or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
super().run()
|
||||||
|
|
||||||
|
while not self.plugin.should_stop():
|
||||||
|
try:
|
||||||
|
status = self.plugin._status() # pylint: disable=protected-access
|
||||||
|
assert status and status.get('state'), 'No status returned'
|
||||||
|
if not (status and status.get('state')):
|
||||||
|
self.wait_stop(self.plugin.poll_interval)
|
||||||
|
break
|
||||||
|
|
||||||
|
track = self.plugin._current_track() # pylint: disable=protected-access
|
||||||
|
self._process_events(status, track)
|
||||||
|
self._update_status(status, track)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'Could not retrieve the latest status: %s', e, exc_info=True
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self.wait_stop(self.plugin.poll_interval)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -1,5 +1,11 @@
|
||||||
manifest:
|
manifest:
|
||||||
events: {}
|
events:
|
||||||
|
- platypush.message.event.music.MusicPauseEvent
|
||||||
|
- platypush.message.event.music.MusicPlayEvent
|
||||||
|
- platypush.message.event.music.MusicStopEvent
|
||||||
|
- platypush.message.event.music.NewPlayingTrackEvent
|
||||||
|
- platypush.message.event.music.PlaylistChangeEvent
|
||||||
|
- platypush.message.event.music.VolumeChangeEvent
|
||||||
install:
|
install:
|
||||||
apt:
|
apt:
|
||||||
- python3-mpd
|
- python3-mpd
|
||||||
|
|
Loading…
Reference in a new issue