platypush/platypush/plugins/music/mpd/__init__.py

512 lines
14 KiB
Python
Raw Normal View History

import mpd
import re
2018-11-07 18:04:37 +01:00
import time
from platypush.plugins import action
from platypush.plugins.music import MusicPlugin
class MusicMpdPlugin(MusicPlugin):
2018-06-25 00:49:45 +02:00
"""
This plugin allows you to interact with an MPD/Mopidy music server. MPD
(https://www.musicpd.org/) is a flexible server-side protocol/application
for handling music collections and playing music, mostly aimed to manage
local libraries. 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.).
Requires:
* **python-mpd2** (``pip install python-mpd2``)
"""
def __init__(self, host, port=6600):
2017-12-18 01:10:51 +01:00
"""
2018-06-25 00:49:45 +02:00
:param host: MPD IP/hostname
:type host: str
:param port: MPD port (default: 6600)
:type port: int
2017-12-18 01:10:51 +01:00
"""
super().__init__()
2017-12-18 01:10:51 +01:00
self.host = host
self.port = port
self.client = mpd.MPDClient(use_unicode=True)
2017-12-18 01:10:51 +01:00
self.client.connect(self.host, self.port)
def _exec(self, method, *args, **kwargs):
getattr(self.client, method)(*args, **kwargs)
return self.status().output
@action
def play(self, resource=None):
2018-06-25 00:49:45 +02:00
"""
Play a resource by path/URI
:param resource: Resource path/URI
:type resource: str
"""
if resource:
self.clear()
self.add(resource)
return self._exec('play')
@action
def play_pos(self, pos):
2018-06-25 00:49:45 +02:00
"""
Play a track in the current playlist by position number
:param pos: Position number
:type resource: int
"""
return self._exec('play', pos)
@action
def pause(self):
2018-06-25 00:49:45 +02:00
""" Pause playback """
2017-12-24 13:15:37 +01:00
status = self.status().output['state']
if status == 'play': return self._exec('pause')
else: return self._exec('play')
@action
def pause_if_playing(self):
2018-06-25 00:49:45 +02:00
""" Pause playback only if it's playing """
status = self.status().output['state']
if status == 'play':
return self._exec('pause')
@action
def play_if_paused(self):
2018-06-25 00:49:45 +02:00
""" Play only if it's paused (resume) """
status = self.status().output['state']
if status == 'pause':
return self._exec('play')
@action
def play_if_paused_or_stopped(self):
""" Play only if it's paused or stopped """
status = self.status().output['state']
if status == 'pause' or status == 'stop':
return self._exec('play')
@action
def stop(self):
2018-06-25 00:49:45 +02:00
""" Stop playback """
return self._exec('stop')
@action
2017-12-28 13:13:09 +01:00
def play_or_stop(self):
2018-06-25 00:49:45 +02:00
""" Play or stop (play state toggle) """
2017-12-28 13:13:09 +01:00
status = self.status().output['state']
if status == 'play':
return self._exec('stop')
else:
return self._exec('play')
2017-12-28 13:13:09 +01:00
@action
def playid(self, track_id):
2018-06-25 00:49:45 +02:00
"""
Play a track by ID
:param track_id: Track ID
:type track_id: str
"""
return self._exec('playid', track_id)
@action
2017-11-03 15:06:29 +01:00
def next(self):
2018-06-25 00:49:45 +02:00
""" Play the next track """
return self._exec('next')
2017-11-03 15:06:29 +01:00
@action
2017-11-03 15:06:29 +01:00
def previous(self):
2018-06-25 00:49:45 +02:00
""" Play the previous track """
return self._exec('previous')
2017-11-03 15:06:29 +01:00
@action
2017-11-03 15:06:29 +01:00
def setvol(self, vol):
2018-06-25 00:49:45 +02:00
"""
Set the volume
:param vol: Volume value (range: 0-100)
:type vol: int
"""
return self._exec('setvol', vol)
2017-11-03 15:06:29 +01:00
@action
2017-12-24 13:15:37 +01:00
def volup(self, delta=10):
2018-06-25 00:49:45 +02:00
"""
Turn up the volume
:param delta: Volume up delta (default: +10%)
:type delta: int
"""
2017-12-24 13:15:37 +01:00
volume = int(self.status().output['volume'])
2018-11-04 01:35:33 +01:00
new_volume = min(volume+delta, 100)
self.setvol(str(new_volume))
2017-12-24 13:15:37 +01:00
return self.status()
@action
2017-12-24 13:15:37 +01:00
def voldown(self, delta=10):
2018-06-25 00:49:45 +02:00
"""
Turn down the volume
:param delta: Volume down delta (default: -10%)
:type delta: int
"""
2017-12-24 13:15:37 +01:00
volume = int(self.status().output['volume'])
2018-11-04 01:35:33 +01:00
new_volume = max(volume-delta, 0)
self.setvol(str(new_volume))
2017-12-24 13:15:37 +01:00
return self.status()
@action
def random(self, value=None):
2018-06-25 00:49:45 +02:00
"""
2018-08-06 22:44:02 +02:00
Set random mode
2018-06-25 00:49:45 +02:00
2018-08-06 22:44:02 +02:00
:param value: If set, set the random state this value (true/false). Default: None (toggle current state)
2018-06-25 00:49:45 +02:00
:type value: bool
"""
if value is None:
value = int(self.status().output['random'])
value = 1 if value == 0 else 0
return self._exec('random', value)
@action
def repeat(self, value=None):
2018-06-25 00:49:45 +02:00
"""
Set repeat mode
:param value: If set, set the repeat state this value (true/false). Default: None (toggle current state)
:type value: bool
"""
if value is None:
value = int(self.status().output['repeat'])
value = 1 if value == 0 else 0
return self._exec('repeat', value)
2018-08-06 22:44:02 +02:00
@action
def shuffle(self):
"""
Shuffles the current playlist
"""
return self._exec('shuffle')
@action
def add(self, resource):
2018-06-25 00:49:45 +02:00
"""
Add a resource (track, album, artist, folder etc.) to the current playlist
:param resource: Resource path or URI
:type resource: str
"""
2018-09-25 09:42:46 +02:00
if isinstance(resource, list):
for r in resource:
2019-01-03 10:37:11 +01:00
r = self._parse_resource(r)
2018-09-25 09:42:46 +02:00
try:
self._exec('add', r)
except Exception as e:
self.logger.warning('Could not add {}: {}'.format(r, e))
return self.status().output
2019-01-03 10:37:11 +01:00
r = self._parse_resource(resource)
return self._exec('add', r)
@classmethod
def _parse_resource(cls, resource):
if resource and resource.startswith('spotify:playlist:'):
# Old Spotify URI format, convert it to new
m = re.match('^spotify:playlist:(.*)$', resource)
resource = 'spotify:user:spotify:playlist:' + m.group(1)
return resource
2017-11-03 15:06:29 +01:00
@action
2017-12-29 12:48:14 +01:00
def load(self, playlist):
2018-06-25 00:49:45 +02:00
"""
Load and play a playlist by name
:param playlist: Playlist name
:type playlist: str
"""
2017-12-29 12:48:14 +01:00
self._exec('load', playlist)
return self.play()
2017-11-03 15:06:29 +01:00
@action
2017-11-03 15:06:29 +01:00
def clear(self):
2018-06-25 00:49:45 +02:00
""" Clear the current playlist """
return self._exec('clear')
2017-11-03 15:06:29 +01:00
@action
def seekcur(self, value):
2018-06-25 00:49:45 +02:00
"""
Seek to the specified position
:param value: Seek position in seconds, or delta string (e.g. '+15' or '-15') to indicate a seek relative to the current position
:type value: int
"""
return self._exec('seekcur', value)
@action
def forward(self):
2018-06-25 00:49:45 +02:00
""" Go forward by 15 seconds """
return self._exec('seekcur', '+15')
@action
def back(self):
2018-06-25 00:49:45 +02:00
""" Go backward by 15 seconds """
return self._exec('seekcur', '-15')
@action
def status(self):
2018-06-25 00:49:45 +02:00
"""
:returns: The current state.
Example response::
output = {
"volume": "9",
"repeat": "0",
"random": "0",
"single": "0",
"consume": "0",
"playlist": "52",
"playlistlength": "14",
"xfade": "0",
"state": "play",
"song": "9",
"songid": "3061",
"nextsong": "10",
"nextsongid": "3062",
"time": "161:255",
"elapsed": "161.967",
"bitrate": "320"
}
"""
2018-11-07 18:04:37 +01:00
retries = 0
max_retries = 2
while retries < max_retries:
try:
return self.client.status()
except Exception as e:
self.logger.warning('Unable to parse mpd status: {}'.format(e))
retries += 1
time.sleep(1)
@action
2018-01-13 02:52:06 +01:00
def currentsong(self):
2018-06-25 00:49:45 +02:00
"""
:returns: The currently played track.
Example response::
output = {
"file": "spotify:track:7CO5ADlDN3DcR2pwlnB14P",
"time": "255",
"artist": "Elbow",
"album": "Little Fictions",
"title": "Kindling",
"date": "2017",
"track": "10",
"pos": "9",
"id": "3061",
"albumartist": "Elbow",
"x-albumuri": "spotify:album:6q5KhDhf9BZkoob7uAnq19"
}
"""
track = self.client.currentsong()
if 'title' in track and ('artist' not in track
or not track['artist']
or re.search('^tunein:', track['file'])):
m = re.match('^\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
2018-01-13 02:52:06 +01:00
@action
def playlistinfo(self):
2018-06-25 00:49:45 +02:00
"""
:returns: The tracks in the current playlist as a list of dicts.
Example output::
output = [
{
"file": "spotify:track:79VtgIoznishPUDWO7Kafu",
"time": "355",
"artist": "Elbow",
"album": "Little Fictions",
"title": "Trust the Sun",
"date": "2017",
"track": "3",
"pos": "10",
"id": "3062",
"albumartist": "Elbow",
"x-albumuri": "spotify:album:6q5KhDhf9BZkoob7uAnq19"
},
{
"file": "spotify:track:3EzTre0pxmoMYRuhJKMHj6",
"time": "219",
"artist": "Elbow",
"album": "Little Fictions",
"title": "Gentle Storm",
"date": "2017",
"track": "2",
"pos": "11",
"id": "3063",
"albumartist": "Elbow",
"x-albumuri": "spotify:album:6q5KhDhf9BZkoob7uAnq19"
},
]
"""
return self.client.playlistinfo()
@action
def listplaylists(self):
2018-06-25 00:49:45 +02:00
"""
:returns: The playlists available on the server as a list of dicts.
Example response::
output = [
{
"playlist": "Rock",
"last-modified": "2018-06-25T21:28:19Z"
},
{
"playlist": "Jazz",
"last-modified": "2018-06-24T22:28:29Z"
},
{
# ...
}
]
"""
return sorted(self.client.listplaylists(), key=lambda p: p['playlist'])
@action
def lsinfo(self, uri=None):
2018-06-25 00:49:45 +02:00
"""
Returns the list of playlists and directories on the server
"""
output = self.client.lsinfo(uri) if uri else self.client.lsinfo()
return output
@action
def plchanges(self, version):
2018-06-25 00:49:45 +02:00
"""
Show what has changed on the current playlist since a specified playlist
version number.
:param version: Version number
:type version: int
:returns: A list of dicts representing the songs being added since the specified version
"""
return self.client.plchanges(version)
@action
def searchaddplaylist(self, name):
2018-06-25 00:49:45 +02:00
"""
Search and add a playlist by (partial or full) name
:param name: Playlist name, can be partial
:type name: str
"""
playlists = list(map(lambda _: _['playlist'],
filter(lambda playlist:
name.lower() in playlist['playlist'].lower(),
self.client.listplaylists())))
if len(playlists):
self.client.clear()
self.client.load(playlists[0])
self.client.play()
return {'playlist': playlists[0]}
@action
def find(self, filter, *args, **kwargs):
2018-06-25 00:49:45 +02:00
"""
Find in the database/library by filter.
:param filter: Search filter. MPD treats it as a key-valued list (e.g. ``["artist", "Led Zeppelin", "album", "IV"]``)
:type filter: list[str]
:returns: list[dict]
"""
return self.client.find(*filter, *args, **kwargs)
@action
def findadd(self, filter, *args, **kwargs):
2018-06-25 00:49:45 +02:00
"""
Find in the database/library by filter and add to the current playlist.
:param filter: Search filter. MPD treats it as a key-valued list (e.g. ``["artist", "Led Zeppelin", "album", "IV"]``)
:type filter: list[str]
:returns: list[dict]
"""
return self.client.findadd(*filter, *args, **kwargs)
@action
def search(self, filter, *args, **kwargs):
2018-06-25 00:49:45 +02:00
"""
Free search by filter.
:param filter: Search filter. MPD treats it as a key-valued list (e.g. ``["artist", "Led Zeppelin", "album", "IV"]``)
:type filter: list[str]
:returns: list[dict]
"""
items = self.client.search(*filter, *args, **kwargs)
# Spotify results first
items = sorted(items, key=lambda item:
0 if item['file'].startswith('spotify:') else 1)
return items
@action
def searchadd(self, filter, *args, **kwargs):
2018-06-25 00:49:45 +02:00
"""
Free search by filter and add the results to the current playlist.
:param filter: Search filter. MPD treats it as a key-valued list (e.g. ``["artist", "Led Zeppelin", "album", "IV"]``)
:type filter: list[str]
:returns: list[dict]
"""
return self.client.searchadd(*filter, *args, **kwargs)
2018-01-13 02:52:06 +01:00
# vim:sw=4:ts=4:et: