316 lines
10 KiB
Python
316 lines
10 KiB
Python
from __future__ import absolute_import, unicode_literals
|
|
|
|
import pykka
|
|
|
|
from mopidy.core import PlaybackState
|
|
from mopidy.mpd import exceptions, protocol, translator
|
|
|
|
#: Subsystems that can be registered with idle command.
|
|
SUBSYSTEMS = [
|
|
'database', 'mixer', 'options', 'output', 'player', 'playlist',
|
|
'stored_playlist', 'update']
|
|
|
|
|
|
@protocol.commands.add('clearerror')
|
|
def clearerror(context):
|
|
"""
|
|
*musicpd.org, status section:*
|
|
|
|
``clearerror``
|
|
|
|
Clears the current error message in status (this is also
|
|
accomplished by any command that starts playback).
|
|
"""
|
|
raise exceptions.MpdNotImplemented # TODO
|
|
|
|
|
|
@protocol.commands.add('currentsong')
|
|
def currentsong(context):
|
|
"""
|
|
*musicpd.org, status section:*
|
|
|
|
``currentsong``
|
|
|
|
Displays the song info of the current song (same song that is
|
|
identified in status).
|
|
"""
|
|
tl_track = context.core.playback.get_current_tl_track().get()
|
|
stream_title = context.core.playback.get_stream_title().get()
|
|
if tl_track is not None:
|
|
position = context.core.tracklist.index(tl_track).get()
|
|
return translator.track_to_mpd_format(
|
|
tl_track, position=position, stream_title=stream_title)
|
|
|
|
|
|
@protocol.commands.add('idle', list_command=False)
|
|
def idle(context, *subsystems):
|
|
"""
|
|
*musicpd.org, status section:*
|
|
|
|
``idle [SUBSYSTEMS...]``
|
|
|
|
Waits until there is a noteworthy change in one or more of MPD's
|
|
subsystems. As soon as there is one, it lists all changed systems
|
|
in a line in the format ``changed: SUBSYSTEM``, where ``SUBSYSTEM``
|
|
is one of the following:
|
|
|
|
- ``database``: the song database has been modified after update.
|
|
- ``update``: a database update has started or finished. If the
|
|
database was modified during the update, the database event is
|
|
also emitted.
|
|
- ``stored_playlist``: a stored playlist has been modified,
|
|
renamed, created or deleted
|
|
- ``playlist``: the current playlist has been modified
|
|
- ``player``: the player has been started, stopped or seeked
|
|
- ``mixer``: the volume has been changed
|
|
- ``output``: an audio output has been enabled or disabled
|
|
- ``options``: options like repeat, random, crossfade, replay gain
|
|
|
|
While a client is waiting for idle results, the server disables
|
|
timeouts, allowing a client to wait for events as long as MPD runs.
|
|
The idle command can be canceled by sending the command ``noidle``
|
|
(no other commands are allowed). MPD will then leave idle mode and
|
|
print results immediately; might be empty at this time.
|
|
|
|
If the optional ``SUBSYSTEMS`` argument is used, MPD will only send
|
|
notifications when something changed in one of the specified
|
|
subsystems.
|
|
"""
|
|
# TODO: test against valid subsystems
|
|
|
|
if not subsystems:
|
|
subsystems = SUBSYSTEMS
|
|
|
|
for subsystem in subsystems:
|
|
context.subscriptions.add(subsystem)
|
|
|
|
active = context.subscriptions.intersection(context.events)
|
|
if not active:
|
|
context.session.prevent_timeout = True
|
|
return
|
|
|
|
response = []
|
|
context.events = set()
|
|
context.subscriptions = set()
|
|
|
|
for subsystem in active:
|
|
response.append('changed: %s' % subsystem)
|
|
return response
|
|
|
|
|
|
@protocol.commands.add('noidle', list_command=False)
|
|
def noidle(context):
|
|
"""See :meth:`_status_idle`."""
|
|
if not context.subscriptions:
|
|
return
|
|
context.subscriptions = set()
|
|
context.events = set()
|
|
context.session.prevent_timeout = False
|
|
|
|
|
|
@protocol.commands.add('stats')
|
|
def stats(context):
|
|
"""
|
|
*musicpd.org, status section:*
|
|
|
|
``stats``
|
|
|
|
Displays statistics.
|
|
|
|
- ``artists``: number of artists
|
|
- ``songs``: number of albums
|
|
- ``uptime``: daemon uptime in seconds
|
|
- ``db_playtime``: sum of all song times in the db
|
|
- ``db_update``: last db update in UNIX time
|
|
- ``playtime``: time length of music played
|
|
"""
|
|
return {
|
|
'artists': 0, # TODO
|
|
'albums': 0, # TODO
|
|
'songs': 0, # TODO
|
|
'uptime': 0, # TODO
|
|
'db_playtime': 0, # TODO
|
|
'db_update': 0, # TODO
|
|
'playtime': 0, # TODO
|
|
}
|
|
|
|
|
|
@protocol.commands.add('status')
|
|
def status(context):
|
|
"""
|
|
*musicpd.org, status section:*
|
|
|
|
``status``
|
|
|
|
Reports the current status of the player and the volume level.
|
|
|
|
- ``volume``: 0-100 or -1
|
|
- ``repeat``: 0 or 1
|
|
- ``single``: 0 or 1
|
|
- ``consume``: 0 or 1
|
|
- ``playlist``: 31-bit unsigned integer, the playlist version
|
|
number
|
|
- ``playlistlength``: integer, the length of the playlist
|
|
- ``state``: play, stop, or pause
|
|
- ``song``: playlist song number of the current song stopped on or
|
|
playing
|
|
- ``songid``: playlist songid of the current song stopped on or
|
|
playing
|
|
- ``nextsong``: playlist song number of the next song to be played
|
|
- ``nextsongid``: playlist songid of the next song to be played
|
|
- ``time``: total time elapsed (of current playing/paused song)
|
|
- ``elapsed``: Total time elapsed within the current song, but with
|
|
higher resolution.
|
|
- ``bitrate``: instantaneous bitrate in kbps
|
|
- ``xfade``: crossfade in seconds
|
|
- ``audio``: sampleRate``:bits``:channels
|
|
- ``updatings_db``: job id
|
|
- ``error``: if there is an error, returns message here
|
|
|
|
*Clarifications based on experience implementing*
|
|
- ``volume``: can also be -1 if no output is set.
|
|
- ``elapsed``: Higher resolution means time in seconds with three
|
|
decimal places for millisecond precision.
|
|
"""
|
|
tl_track = context.core.playback.get_current_tl_track()
|
|
next_tlid = context.core.tracklist.get_next_tlid()
|
|
|
|
futures = {
|
|
'tracklist.length': context.core.tracklist.get_length(),
|
|
'tracklist.version': context.core.tracklist.get_version(),
|
|
'mixer.volume': context.core.mixer.get_volume(),
|
|
'tracklist.consume': context.core.tracklist.get_consume(),
|
|
'tracklist.random': context.core.tracklist.get_random(),
|
|
'tracklist.repeat': context.core.tracklist.get_repeat(),
|
|
'tracklist.single': context.core.tracklist.get_single(),
|
|
'playback.state': context.core.playback.get_state(),
|
|
'playback.current_tl_track': tl_track,
|
|
'tracklist.index': context.core.tracklist.index(tl_track.get()),
|
|
'tracklist.next_tlid': next_tlid,
|
|
'tracklist.next_index': context.core.tracklist.index(
|
|
tlid=next_tlid.get()),
|
|
'playback.time_position': context.core.playback.get_time_position(),
|
|
}
|
|
pykka.get_all(futures.values())
|
|
result = [
|
|
('volume', _status_volume(futures)),
|
|
('repeat', _status_repeat(futures)),
|
|
('random', _status_random(futures)),
|
|
('single', _status_single(futures)),
|
|
('consume', _status_consume(futures)),
|
|
('playlist', _status_playlist_version(futures)),
|
|
('playlistlength', _status_playlist_length(futures)),
|
|
('xfade', _status_xfade(futures)),
|
|
('state', _status_state(futures)),
|
|
]
|
|
if futures['playback.current_tl_track'].get() is not None:
|
|
result.append(('song', _status_songpos(futures)))
|
|
result.append(('songid', _status_songid(futures)))
|
|
if futures['tracklist.next_tlid'].get() is not None:
|
|
result.append(('nextsong', _status_nextsongpos(futures)))
|
|
result.append(('nextsongid', _status_nextsongid(futures)))
|
|
if futures['playback.state'].get() in (
|
|
PlaybackState.PLAYING, PlaybackState.PAUSED):
|
|
result.append(('time', _status_time(futures)))
|
|
result.append(('elapsed', _status_time_elapsed(futures)))
|
|
result.append(('bitrate', _status_bitrate(futures)))
|
|
return result
|
|
|
|
|
|
def _status_bitrate(futures):
|
|
current_tl_track = futures['playback.current_tl_track'].get()
|
|
if current_tl_track is None:
|
|
return 0
|
|
if current_tl_track.track.bitrate is None:
|
|
return 0
|
|
return current_tl_track.track.bitrate
|
|
|
|
|
|
def _status_consume(futures):
|
|
if futures['tracklist.consume'].get():
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
|
|
def _status_playlist_length(futures):
|
|
return futures['tracklist.length'].get()
|
|
|
|
|
|
def _status_playlist_version(futures):
|
|
return futures['tracklist.version'].get()
|
|
|
|
|
|
def _status_random(futures):
|
|
return int(futures['tracklist.random'].get())
|
|
|
|
|
|
def _status_repeat(futures):
|
|
return int(futures['tracklist.repeat'].get())
|
|
|
|
|
|
def _status_single(futures):
|
|
return int(futures['tracklist.single'].get())
|
|
|
|
|
|
def _status_songid(futures):
|
|
current_tl_track = futures['playback.current_tl_track'].get()
|
|
if current_tl_track is not None:
|
|
return current_tl_track.tlid
|
|
else:
|
|
return _status_songpos(futures)
|
|
|
|
|
|
def _status_songpos(futures):
|
|
return futures['tracklist.index'].get()
|
|
|
|
|
|
def _status_nextsongid(futures):
|
|
return futures['tracklist.next_tlid'].get()
|
|
|
|
|
|
def _status_nextsongpos(futures):
|
|
return futures['tracklist.next_index'].get()
|
|
|
|
|
|
def _status_state(futures):
|
|
state = futures['playback.state'].get()
|
|
if state == PlaybackState.PLAYING:
|
|
return 'play'
|
|
elif state == PlaybackState.STOPPED:
|
|
return 'stop'
|
|
elif state == PlaybackState.PAUSED:
|
|
return 'pause'
|
|
|
|
|
|
def _status_time(futures):
|
|
return '%d:%d' % (
|
|
futures['playback.time_position'].get() // 1000,
|
|
_status_time_total(futures) // 1000)
|
|
|
|
|
|
def _status_time_elapsed(futures):
|
|
return '%.3f' % (futures['playback.time_position'].get() / 1000.0)
|
|
|
|
|
|
def _status_time_total(futures):
|
|
current_tl_track = futures['playback.current_tl_track'].get()
|
|
if current_tl_track is None:
|
|
return 0
|
|
elif current_tl_track.track.length is None:
|
|
return 0
|
|
else:
|
|
return current_tl_track.track.length
|
|
|
|
|
|
def _status_volume(futures):
|
|
volume = futures['mixer.volume'].get()
|
|
if volume is not None:
|
|
return volume
|
|
else:
|
|
return -1
|
|
|
|
|
|
def _status_xfade(futures):
|
|
return 0 # Not supported
|