Merge branch 'master' of git://github.com/jodal/mopidy
This commit is contained in:
commit
f03a8238b1
@ -5,3 +5,4 @@ Contributors to Mopidy in the order of appearance:
|
||||
|
||||
* Stein Magnus Jodal <stein.magnus@jodal.no>
|
||||
* Johannes Knutsen <johannes@knutseninfo.no>
|
||||
* Thomas Adamcik <adamcik@samfundet.no>
|
||||
|
||||
@ -2,293 +2,7 @@
|
||||
:mod:`mopidy.backends` -- Backend API
|
||||
*************************************
|
||||
|
||||
.. warning::
|
||||
This is our *planned* backend API, and not the current API.
|
||||
|
||||
.. module:: mopidy.backends
|
||||
.. automodule:: mopidy.backends
|
||||
:synopsis: Interface between Mopidy and its various backends.
|
||||
|
||||
.. class:: BaseBackend()
|
||||
|
||||
.. attribute:: current_playlist
|
||||
|
||||
The current playlist controller. An instance of
|
||||
:class:`BaseCurrentPlaylistController`.
|
||||
|
||||
.. attribute:: library
|
||||
|
||||
The library controller. An instance of :class:`BaseLibraryController`.
|
||||
|
||||
.. attribute:: playback
|
||||
|
||||
The playback controller. An instance of :class:`BasePlaybackController`.
|
||||
|
||||
.. attribute:: stored_playlists
|
||||
|
||||
The stored playlists controller. An instance of
|
||||
:class:`BaseStoredPlaylistsController`.
|
||||
|
||||
.. attribute:: uri_handlers
|
||||
|
||||
List of URI prefixes this backend can handle.
|
||||
|
||||
|
||||
.. class:: BaseCurrentPlaylistController(backend)
|
||||
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
|
||||
.. method:: add(track, at_position=None)
|
||||
|
||||
Add the track to the end of, or at the given position in the current
|
||||
playlist.
|
||||
|
||||
:param track: track to add
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:param at_position: position in current playlist to add track
|
||||
:type at_position: int or :class:`None`
|
||||
|
||||
.. method:: clear()
|
||||
|
||||
Clear the current playlist.
|
||||
|
||||
.. method:: load(playlist)
|
||||
|
||||
Replace the current playlist with the given playlist.
|
||||
|
||||
:param playlist: playlist to load
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
|
||||
.. method:: move(start, end, to_position)
|
||||
|
||||
Move the tracks in the slice ``[start:end]`` to ``to_position``.
|
||||
|
||||
:param start: position of first track to move
|
||||
:type start: int
|
||||
:param end: position after last track to move
|
||||
:type end: int
|
||||
:param to_position: new position for the tracks
|
||||
:type to_position: int
|
||||
|
||||
.. attribute:: playlist
|
||||
|
||||
The currently loaded :class:`mopidy.models.Playlist`.
|
||||
|
||||
.. method:: remove(track)
|
||||
|
||||
Remove the track from the current playlist.
|
||||
|
||||
:param track: track to remove
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
|
||||
.. method:: shuffle(start=None, end=None)
|
||||
|
||||
Shuffles the entire playlist. If ``start`` and ``end`` is given only
|
||||
shuffles the slice ``[start:end]``.
|
||||
|
||||
:param start: position of first track to shuffle
|
||||
:type start: int or :class:`None`
|
||||
:param end: position after last track to shuffle
|
||||
:type end: int or :class:`None`
|
||||
|
||||
.. attribute:: version
|
||||
|
||||
The current playlist version. Integer which is increased every time the
|
||||
current playlist is changed.
|
||||
|
||||
|
||||
.. class:: BasePlaybackController(backend)
|
||||
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
|
||||
.. attribute:: consume
|
||||
|
||||
:class:`True`
|
||||
Tracks are removed from the playlist when they have been played.
|
||||
:class:`False`
|
||||
Tracks are not removed from the playlist.
|
||||
|
||||
.. attribute:: current_track
|
||||
|
||||
The currently playing or selected :class:`mopidy.models.Track`.
|
||||
|
||||
.. method:: next()
|
||||
|
||||
Play the next track.
|
||||
|
||||
.. method:: pause()
|
||||
|
||||
Pause playblack.
|
||||
|
||||
.. attribute:: PAUSED
|
||||
|
||||
Constant representing the paused state.
|
||||
|
||||
.. method:: play(track=None)
|
||||
|
||||
Play the given track or the currently active track.
|
||||
|
||||
:param track: track to play
|
||||
:type track: :class:`mopidy.models.Track` or :class:`None`
|
||||
|
||||
.. attribute:: PLAYING
|
||||
|
||||
Constant representing the playing state.
|
||||
|
||||
.. attribute:: playlist_position
|
||||
|
||||
The position in the current playlist.
|
||||
|
||||
.. method:: previous()
|
||||
|
||||
Play the previous track.
|
||||
|
||||
.. attribute:: random
|
||||
|
||||
:class:`True`
|
||||
Tracks are selected at random from the playlist.
|
||||
:class:`False`
|
||||
Tracks are played in the order of the playlist.
|
||||
|
||||
.. attribute:: repeat
|
||||
|
||||
:class:`True`
|
||||
The current track is played repeatedly.
|
||||
:class:`False`
|
||||
The current track is played once.
|
||||
|
||||
.. method:: resume()
|
||||
|
||||
If paused, resume playing the current track.
|
||||
|
||||
.. method:: seek(time_position)
|
||||
|
||||
Seeks to time position given in milliseconds.
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
|
||||
.. attribute:: state
|
||||
|
||||
The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
|
||||
:attr:`STOPPED`.
|
||||
|
||||
.. method:: stop()
|
||||
|
||||
Stop playing.
|
||||
|
||||
.. attribute:: STOPPED
|
||||
|
||||
Constant representing the stopped state.
|
||||
|
||||
.. attribute:: time_position
|
||||
|
||||
Time position in milliseconds.
|
||||
|
||||
.. attribute:: volume
|
||||
|
||||
The audio volume as an int in the range [0, 100]. :class:`None` if
|
||||
unknown.
|
||||
|
||||
|
||||
.. class:: BaseLibraryController(backend)
|
||||
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
|
||||
.. method:: find_exact(type, query)
|
||||
|
||||
Find tracks in the library where ``type`` matches ``query`` exactly.
|
||||
|
||||
:param type: 'title', 'artist', or 'album'
|
||||
:type type: string
|
||||
:param query: the search query
|
||||
:type query: string
|
||||
:rtype: list of :class:`mopidy.models.Track`
|
||||
|
||||
.. method:: lookup(uri)
|
||||
|
||||
Lookup track with given URI.
|
||||
|
||||
:param uri: track URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Track`
|
||||
|
||||
.. method:: refresh(uri=None)
|
||||
|
||||
Refresh library. Limit to URI and below if an URI is given.
|
||||
|
||||
:param uri: directory or track URI
|
||||
:type uri: string
|
||||
|
||||
.. method:: search(type, query)
|
||||
|
||||
Search the library for tracks where ``type`` contains ``query``.
|
||||
|
||||
:param type: 'title', 'artist', 'album', or 'uri'
|
||||
:type type: string
|
||||
:param query: the search query
|
||||
:type query: string
|
||||
:rtype: list of :class:`mopidy.models.Track`
|
||||
|
||||
|
||||
.. class:: BaseStoredPlaylistsController(backend)
|
||||
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
|
||||
.. method:: add(uri)
|
||||
|
||||
Add existing playlist with the given URI.
|
||||
|
||||
:param uri: URI of existing playlist
|
||||
:type uri: string
|
||||
|
||||
.. method:: create(name)
|
||||
|
||||
Create a new playlist.
|
||||
|
||||
:param name: name of the new playlist
|
||||
:type name: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
|
||||
.. attribute:: playlists
|
||||
|
||||
List of :class:`mopidy.models.Playlist`.
|
||||
|
||||
.. method:: delete(playlist)
|
||||
|
||||
Delete playlist.
|
||||
|
||||
:param playlist: the playlist to delete
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
|
||||
.. method:: lookup(uri)
|
||||
|
||||
Lookup playlist with given URI.
|
||||
|
||||
:param uri: playlist URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
|
||||
.. method:: refresh()
|
||||
|
||||
Refresh stored playlists.
|
||||
|
||||
.. method:: rename(playlist, new_name)
|
||||
|
||||
Rename playlist.
|
||||
|
||||
:param playlist: the playlist
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
:param new_name: the new name
|
||||
:type new_name: string
|
||||
|
||||
.. method:: search(query)
|
||||
|
||||
Search for playlists whose name contains ``query``.
|
||||
|
||||
:param query: query to search for
|
||||
:type query: string
|
||||
:rtype: list of :class:`mopidy.models.Playlist`
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
@ -126,7 +126,7 @@ libspotify backend, copy the Spotify application key to
|
||||
``mopidy/spotify_appkey.key``, and add the following to
|
||||
``mopidy/mopidy/local_settings.py``::
|
||||
|
||||
BACKEND=u'mopidy.backends.libspotify.LibspotifyBackend'
|
||||
BACKEND = u'mopidy.backends.libspotify.LibspotifyBackend'
|
||||
|
||||
To start Mopidy, go to the root of the Mopidy project, then simply run::
|
||||
|
||||
|
||||
@ -1,47 +1,315 @@
|
||||
from copy import copy
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
|
||||
from mopidy.exceptions import MpdNotImplemented
|
||||
from mopidy.models import Playlist
|
||||
|
||||
logger = logging.getLogger('backends.base')
|
||||
|
||||
class BaseBackend(object):
|
||||
PLAY = u'play'
|
||||
PAUSE = u'pause'
|
||||
STOP = u'stop'
|
||||
#: The current playlist controller. An instance of
|
||||
#: :class:`BaseCurrentPlaylistController`.
|
||||
current_playlist = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._state = self.STOP
|
||||
self._playlists = []
|
||||
self._x_current_playlist = Playlist()
|
||||
self._current_playlist_version = 0
|
||||
#: The library controller. An instance of :class:`BaseLibraryController`.
|
||||
library = None
|
||||
|
||||
# Backend state
|
||||
#: The playback controller. An instance of :class:`BasePlaybackController`.
|
||||
playback = None
|
||||
|
||||
#: The stored playlists controller. An instance of
|
||||
#: :class:`BaseStoredPlaylistsController`.
|
||||
stored_playlists = None
|
||||
|
||||
#: List of URI prefixes this backend can handle.
|
||||
uri_handlers = []
|
||||
|
||||
|
||||
class BaseCurrentPlaylistController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
#: The current playlist version. Integer which is increased every time the
|
||||
#: current playlist is changed. Is not reset before the MPD server is
|
||||
#: restarted.
|
||||
version = 0
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self.playlist = Playlist()
|
||||
|
||||
@property
|
||||
def playlist(self):
|
||||
"""The currently loaded :class:`mopidy.models.Playlist`."""
|
||||
return copy(self._playlist)
|
||||
|
||||
@playlist.setter
|
||||
def playlist(self, new_playlist):
|
||||
self._playlist = new_playlist
|
||||
self.version += 1
|
||||
|
||||
def add(self, track, at_position=None):
|
||||
"""
|
||||
Add the track to the end of, or at the given position in the current
|
||||
playlist.
|
||||
|
||||
:param track: track to add
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:param at_position: position in current playlist to add track
|
||||
:type at_position: int or :class:`None`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def clear(self):
|
||||
"""Clear the current playlist."""
|
||||
self.backend.playback.stop()
|
||||
self.playlist = Playlist()
|
||||
|
||||
def get_by_id(self, id):
|
||||
"""
|
||||
Get track by ID. Raises :class:`KeyError` if not found.
|
||||
|
||||
:param id: track ID
|
||||
:type id: int
|
||||
"""
|
||||
matches = filter(lambda t: t.id == id, self._playlist.tracks)
|
||||
if matches:
|
||||
return matches[0]
|
||||
else:
|
||||
raise KeyError('Track with ID "%s" not found' % id)
|
||||
|
||||
def get_by_uri(self, uri):
|
||||
"""
|
||||
Get track by URI. Raises :class:`KeyError` if not found.
|
||||
|
||||
:param uri: track URI
|
||||
:type uri: string
|
||||
"""
|
||||
matches = filter(lambda t: t.uri == uri, self._playlist.tracks)
|
||||
if matches:
|
||||
return matches[0]
|
||||
else:
|
||||
raise KeyError('Track with URI "%s" not found' % uri)
|
||||
|
||||
def load(self, playlist):
|
||||
"""
|
||||
Replace the current playlist with the given playlist.
|
||||
|
||||
:param playlist: playlist to load
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
self.playlist = playlist
|
||||
|
||||
def move(self, start, end, to_position):
|
||||
"""
|
||||
Move the tracks in the slice ``[start:end]`` to ``to_position``.
|
||||
|
||||
:param start: position of first track to move
|
||||
:type start: int
|
||||
:param end: position after last track to move
|
||||
:type end: int
|
||||
:param to_position: new position for the tracks
|
||||
:type to_position: int
|
||||
"""
|
||||
tracks = self.playlist.tracks
|
||||
new_tracks = tracks[:start] + tracks[end:]
|
||||
for track in tracks[start:end]:
|
||||
new_tracks.insert(to_position, track)
|
||||
to_position += 1
|
||||
self.playlist = self.playlist.with_(tracks=new_tracks)
|
||||
|
||||
def remove(self, track):
|
||||
"""
|
||||
Remove the track from the current playlist.
|
||||
|
||||
:param track: track to remove
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
"""
|
||||
tracks = self.playlist.tracks
|
||||
position = tracks.index(track)
|
||||
del tracks[position]
|
||||
self.playlist = self.playlist.with_(tracks=tracks)
|
||||
|
||||
def shuffle(self, start=None, end=None):
|
||||
"""
|
||||
Shuffles the entire playlist. If ``start`` and ``end`` is given only
|
||||
shuffles the slice ``[start:end]``.
|
||||
|
||||
:param start: position of first track to shuffle
|
||||
:type start: int or :class:`None`
|
||||
:param end: position after last track to shuffle
|
||||
:type end: int or :class:`None`
|
||||
"""
|
||||
tracks = self.playlist.tracks
|
||||
before = tracks[:start or 0]
|
||||
shuffled = tracks[start:end]
|
||||
after = tracks[end or len(tracks):]
|
||||
random.shuffle(shuffled)
|
||||
self.playlist = self.playlist.with_(tracks=before+shuffled+after)
|
||||
|
||||
|
||||
class BaseLibraryController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
|
||||
def find_exact(self, type, query):
|
||||
"""
|
||||
Find tracks in the library where ``type`` matches ``query`` exactly.
|
||||
|
||||
:param type: 'title', 'artist', or 'album'
|
||||
:type type: string
|
||||
:param query: the search query
|
||||
:type query: string
|
||||
:rtype: list of :class:`mopidy.models.Track`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
Lookup track with given URI.
|
||||
|
||||
:param uri: track URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Track`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self, uri=None):
|
||||
"""
|
||||
Refresh library. Limit to URI and below if an URI is given.
|
||||
|
||||
:param uri: directory or track URI
|
||||
:type uri: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, type, query):
|
||||
"""
|
||||
Search the library for tracks where ``type`` contains ``query``.
|
||||
|
||||
:param type: 'title', 'artist', 'album', or 'uri'
|
||||
:type type: string
|
||||
:param query: the search query
|
||||
:type query: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BasePlaybackController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
#: Constant representing the paused state.
|
||||
PAUSED = u'paused'
|
||||
|
||||
#: Constant representing the playing state.
|
||||
PLAYING = u'playing'
|
||||
|
||||
#: Constant representing the stopped state.
|
||||
STOPPED = u'stopped'
|
||||
|
||||
#: :class:`True`
|
||||
#: Tracks are removed from the playlist when they have been played.
|
||||
#: :class:`False`
|
||||
#: Tracks are not removed from the playlist.
|
||||
consume = False
|
||||
|
||||
#: The currently playing or selected :class:`mopidy.models.Track`.
|
||||
current_track = None
|
||||
|
||||
#: :class:`True`
|
||||
#: Tracks are selected at random from the playlist.
|
||||
#: :class:`False`
|
||||
#: Tracks are played in the order of the playlist.
|
||||
random = False
|
||||
|
||||
#: :class:`True`
|
||||
#: The current track is played repeatedly.
|
||||
#: :class:`False`
|
||||
#: The current track is played once.
|
||||
repeat = False
|
||||
|
||||
#: The audio volume as an int in the range [0, 100]. :class:`None` if
|
||||
#: unknown.
|
||||
volume = None
|
||||
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._state = self.STOPPED
|
||||
|
||||
@property
|
||||
def next_track(self):
|
||||
"""The next :class:`mopidy.models.Track` in the playlist."""
|
||||
if self.current_track is None:
|
||||
return None
|
||||
try:
|
||||
return self.backend.current_playlist.playlist.tracks[
|
||||
self.playlist_position + 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def playlist_position(self):
|
||||
"""The position in the current playlist."""
|
||||
if self.current_track is None:
|
||||
return None
|
||||
try:
|
||||
return self.backend.current_playlist.playlist.tracks.index(
|
||||
self.current_track)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def previous_track(self):
|
||||
"""The previous :class:`mopidy.models.Track` in the playlist."""
|
||||
if self.current_track is None:
|
||||
return None
|
||||
try:
|
||||
return self.backend.current_playlist.playlist.tracks[
|
||||
self.playlist_position - 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
|
||||
:attr:`STOPPED`.
|
||||
"""
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, new_state):
|
||||
(old_state, self._state) = (self.state, new_state)
|
||||
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
|
||||
if old_state in (self.PLAY, self.STOP) and new_state == self.PLAY:
|
||||
if (old_state in (self.PLAYING, self.STOPPED)
|
||||
and new_state == self.PLAYING):
|
||||
self._play_time_start()
|
||||
elif old_state == self.PLAY and new_state == self.PAUSE:
|
||||
elif old_state == self.PLAYING and new_state == self.PAUSED:
|
||||
self._play_time_pause()
|
||||
elif old_state == self.PAUSE and new_state == self.PLAY:
|
||||
elif old_state == self.PAUSED and new_state == self.PLAYING:
|
||||
self._play_time_resume()
|
||||
|
||||
@property
|
||||
def _play_time_elapsed(self):
|
||||
if self.state == self.PLAY:
|
||||
def time_position(self):
|
||||
"""Time position in milliseconds."""
|
||||
if self.state == self.PLAYING:
|
||||
time_since_started = int(time.time()) - self._play_time_started
|
||||
return self._play_time_accumulated + time_since_started
|
||||
elif self.state == self.PAUSE:
|
||||
elif self.state == self.PAUSED:
|
||||
return self._play_time_accumulated
|
||||
elif self.state == self.STOP:
|
||||
elif self.state == self.STOPPED:
|
||||
return 0
|
||||
|
||||
def _play_time_start(self):
|
||||
@ -55,184 +323,159 @@ class BaseBackend(object):
|
||||
def _play_time_resume(self):
|
||||
self._play_time_started = int(time.time())
|
||||
|
||||
@property
|
||||
def _current_playlist(self):
|
||||
return self._x_current_playlist
|
||||
|
||||
@_current_playlist.setter
|
||||
def _current_playlist(self, playlist):
|
||||
self._x_current_playlist = playlist
|
||||
self._current_playlist_version += 1
|
||||
|
||||
@property
|
||||
def _current_track(self):
|
||||
if self._current_song_pos is not None:
|
||||
return self._current_playlist.tracks[self._current_song_pos]
|
||||
|
||||
@property
|
||||
def _current_song_pos(self):
|
||||
if not hasattr(self, '_x_current_song_pos'):
|
||||
self._x_current_song_pos = None
|
||||
if (self._current_playlist is None
|
||||
or self._current_playlist.length == 0):
|
||||
self._x_current_song_pos = None
|
||||
elif self._x_current_song_pos < 0:
|
||||
self._x_current_song_pos = 0
|
||||
elif self._x_current_song_pos >= self._current_playlist.length:
|
||||
self._x_current_song_pos = self._current_playlist.length - 1
|
||||
return self._x_current_song_pos
|
||||
|
||||
@_current_song_pos.setter
|
||||
def _current_song_pos(self, songid):
|
||||
self._x_current_song_pos = songid
|
||||
|
||||
# Status methods
|
||||
|
||||
def current_song(self):
|
||||
if self.state is not self.STOP and self._current_track is not None:
|
||||
return self._current_track.mpd_format(self._current_song_pos)
|
||||
|
||||
def status_bitrate(self):
|
||||
return 0
|
||||
|
||||
def status_consume(self):
|
||||
return 0
|
||||
|
||||
def status_volume(self):
|
||||
return 0
|
||||
|
||||
def status_repeat(self):
|
||||
return 0
|
||||
|
||||
def status_random(self):
|
||||
return 0
|
||||
|
||||
def status_single(self):
|
||||
return 0
|
||||
|
||||
def status_song_id(self):
|
||||
return self._current_song_pos # Override if you got a better ID scheme
|
||||
|
||||
def status_playlist(self):
|
||||
return self._current_playlist_version
|
||||
|
||||
def status_playlist_length(self):
|
||||
return self._current_playlist.length
|
||||
|
||||
def status_state(self):
|
||||
return self.state
|
||||
|
||||
def status_time(self):
|
||||
return u'%s:%s' % (self._play_time_elapsed, self.status_time_total())
|
||||
|
||||
def status_time_total(self):
|
||||
if self._current_track is not None:
|
||||
return self._current_track.length // 1000
|
||||
else:
|
||||
return 0
|
||||
|
||||
def status_xfade(self):
|
||||
return 0
|
||||
|
||||
def url_handlers(self):
|
||||
return []
|
||||
|
||||
# Control methods
|
||||
|
||||
def end_of_track(self):
|
||||
self.next()
|
||||
def new_playlist_loaded_callback(self):
|
||||
"""Tell the playback controller that a new playlist has been loaded."""
|
||||
self.current_track = None
|
||||
if self.state == self.PLAYING:
|
||||
if self.backend.current_playlist.playlist.length > 0:
|
||||
self.play(self.backend.current_playlist.playlist.tracks[0])
|
||||
else:
|
||||
self.stop()
|
||||
|
||||
def next(self):
|
||||
self.stop()
|
||||
if self._next():
|
||||
self.state = self.PLAY
|
||||
"""Play the next track."""
|
||||
if self.next_track is not None and self._next(self.next_track):
|
||||
self.current_track = self.next_track
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _next(self):
|
||||
raise MpdNotImplemented
|
||||
def _next(self, track):
|
||||
return self._play(track)
|
||||
|
||||
def pause(self):
|
||||
if self.state == self.PLAY and self._pause():
|
||||
self.state = self.PAUSE
|
||||
"""Pause playback."""
|
||||
if self.state == self.PLAYING and self._pause():
|
||||
self.state = self.PAUSED
|
||||
|
||||
def _pause(self):
|
||||
raise MpdNotImplemented
|
||||
raise NotImplementedError
|
||||
|
||||
def play(self, songpos=None, songid=None):
|
||||
if self.state == self.PAUSE and songpos is None and songid is None:
|
||||
def play(self, track=None):
|
||||
"""
|
||||
Play the given track or the currently active track.
|
||||
|
||||
:param track: track to play
|
||||
:type track: :class:`mopidy.models.Track` or :class:`None`
|
||||
"""
|
||||
if self.state == self.PAUSED and track is None:
|
||||
return self.resume()
|
||||
self.stop()
|
||||
if songpos is not None and self._play_pos(songpos):
|
||||
self.state = self.PLAY
|
||||
elif songid is not None and self._play_id(songid):
|
||||
self.state = self.PLAY
|
||||
elif self._play():
|
||||
self.state = self.PLAY
|
||||
if track is not None and self._play(track):
|
||||
self.current_track = track
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _play(self):
|
||||
raise MpdNotImplemented
|
||||
|
||||
def _play_id(self, songid):
|
||||
raise MpdNotImplemented
|
||||
|
||||
def _play_pos(self, songpos):
|
||||
raise MpdNotImplemented
|
||||
def _play(self, track):
|
||||
raise NotImplementedError
|
||||
|
||||
def previous(self):
|
||||
self.stop()
|
||||
if self._previous():
|
||||
self.state = self.PLAY
|
||||
"""Play the previous track."""
|
||||
if (self.previous_track is not None
|
||||
and self._previous(self.previous_track)):
|
||||
self.current_track = self.previous_track
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _previous(self):
|
||||
raise MpdNotImplemented
|
||||
def _previous(self, track):
|
||||
return self._play(track)
|
||||
|
||||
def resume(self):
|
||||
if self.state == self.PAUSE and self._resume():
|
||||
self.state = self.PLAY
|
||||
"""If paused, resume playing the current track."""
|
||||
if self.state == self.PAUSED and self._resume():
|
||||
self.state = self.PLAYING
|
||||
|
||||
def _resume(self):
|
||||
raise MpdNotImplemented
|
||||
raise NotImplementedError
|
||||
|
||||
def seek(self, time_position):
|
||||
"""
|
||||
Seeks to time position given in milliseconds.
|
||||
|
||||
:param time_position: time position in milliseconds
|
||||
:type time_position: int
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
if self.state != self.STOP and self._stop():
|
||||
self.state = self.STOP
|
||||
"""Stop playing."""
|
||||
if self.state != self.STOPPED and self._stop():
|
||||
self.state = self.STOPPED
|
||||
|
||||
def _stop(self):
|
||||
raise MpdNotImplemented
|
||||
raise NotImplementedError
|
||||
|
||||
# Current/single playlist methods
|
||||
|
||||
def playlist_load(self, name):
|
||||
self._current_song_pos = None
|
||||
matches = filter(lambda p: p.name == name, self._playlists)
|
||||
if matches:
|
||||
self._current_playlist = matches[0]
|
||||
if self.state == self.PLAY:
|
||||
self.play(songpos=0)
|
||||
else:
|
||||
self._current_playlist = None
|
||||
class BaseStoredPlaylistsController(object):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
def playlist_changes_since(self, version='0'):
|
||||
if int(version) < self._current_playlist_version:
|
||||
return self._current_playlist.mpd_format()
|
||||
def __init__(self, backend):
|
||||
self.backend = backend
|
||||
self._playlists = []
|
||||
|
||||
def playlist_info(self, songpos=None, start=0, end=None):
|
||||
if songpos is not None:
|
||||
start = int(songpos)
|
||||
end = start + 1
|
||||
else:
|
||||
if start is None:
|
||||
start = 0
|
||||
start = int(start)
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
return self._current_playlist.mpd_format(start, end)
|
||||
@property
|
||||
def playlists(self):
|
||||
"""List of :class:`mopidy.models.Playlist`."""
|
||||
return copy(self._playlists)
|
||||
|
||||
# Stored playlist methods
|
||||
def create(self, name):
|
||||
"""
|
||||
Create a new playlist.
|
||||
|
||||
def playlists_list(self):
|
||||
return [u'playlist: %s' % p.name for p in self._playlists]
|
||||
:param name: name of the new playlist
|
||||
:type name: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# Music database methods
|
||||
def delete(self, playlist):
|
||||
"""
|
||||
Delete playlist.
|
||||
|
||||
def search(self, type, what):
|
||||
return None
|
||||
:param playlist: the playlist to delete
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lookup(self, uri):
|
||||
"""
|
||||
Lookup playlist with given URI in both the set of stored playlists and
|
||||
in any other playlist sources.
|
||||
|
||||
:param uri: playlist URI
|
||||
:type uri: string
|
||||
:rtype: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh stored playlists."""
|
||||
raise NotImplementedError
|
||||
|
||||
def rename(self, playlist, new_name):
|
||||
"""
|
||||
Rename playlist.
|
||||
|
||||
:param playlist: the playlist
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
:param new_name: the new name
|
||||
:type new_name: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, playlist):
|
||||
"""
|
||||
Save the playlist to the set of stored playlists.
|
||||
|
||||
:param playlist: the playlist
|
||||
:type playlist: :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, query):
|
||||
"""
|
||||
Search for playlists whose name contains ``query``.
|
||||
|
||||
:param query: query to search for
|
||||
:type query: string
|
||||
:rtype: list of :class:`mopidy.models.Playlist`
|
||||
"""
|
||||
return filter(lambda p: query in p.name, self._playlists)
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import datetime as dt
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import spytify
|
||||
|
||||
from mopidy import config
|
||||
from mopidy.backends import BaseBackend
|
||||
from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
|
||||
BaseLibraryController, BasePlaybackController,
|
||||
BaseStoredPlaylistsController)
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
|
||||
logger = logging.getLogger(u'backends.despotify')
|
||||
@ -13,37 +14,86 @@ logger = logging.getLogger(u'backends.despotify')
|
||||
ENCODING = 'utf-8'
|
||||
|
||||
class DespotifyBackend(BaseBackend):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DespotifyBackend, self).__init__(*args, **kwargs)
|
||||
logger.info(u'Connecting to Spotify')
|
||||
self.spotify = spytify.Spytify(
|
||||
config.SPOTIFY_USERNAME, config.SPOTIFY_PASSWORD)
|
||||
self.cache_stored_playlists()
|
||||
def __init__(self):
|
||||
self.current_playlist = DespotifyCurrentPlaylistController(backend=self)
|
||||
self.library = DespotifyLibraryController(backend=self)
|
||||
self.playback = DespotifyPlaybackController(backend=self)
|
||||
self.stored_playlists = DespotifyStoredPlaylistsController(backend=self)
|
||||
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
|
||||
self.translate = DespotifyTranslator()
|
||||
self.spotify = self._connect()
|
||||
self.stored_playlists.refresh()
|
||||
|
||||
def cache_stored_playlists(self):
|
||||
def _connect(self):
|
||||
logger.info(u'Connecting to Spotify')
|
||||
return spytify.Spytify(
|
||||
config.SPOTIFY_USERNAME, config.SPOTIFY_PASSWORD)
|
||||
|
||||
|
||||
class DespotifyCurrentPlaylistController(BaseCurrentPlaylistController):
|
||||
pass
|
||||
|
||||
|
||||
class DespotifyLibraryController(BaseLibraryController):
|
||||
def search(self, type, what):
|
||||
query = u'%s:%s' % (type, what)
|
||||
result = self.backend.spotify.search(query.encode(ENCODING))
|
||||
if result is None:
|
||||
return Playlist()
|
||||
return self.backend.translate.to_mopidy_playlist(result.playlist)
|
||||
|
||||
|
||||
class DespotifyPlaybackController(BasePlaybackController):
|
||||
def _pause(self):
|
||||
self.backend.spotify.pause()
|
||||
return True
|
||||
|
||||
def _play(self, track):
|
||||
self.backend.spotify.play(self.backend.spotify.lookup(track.uri))
|
||||
return True
|
||||
|
||||
def _resume(self):
|
||||
self.backend.spotify.resume()
|
||||
return True
|
||||
|
||||
def _stop(self):
|
||||
self.backend.spotify.stop()
|
||||
return True
|
||||
|
||||
|
||||
class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
def refresh(self):
|
||||
logger.info(u'Caching stored playlists')
|
||||
playlists = []
|
||||
for spotify_playlist in self.spotify.stored_playlists:
|
||||
playlists.append(self._to_mopidy_playlist(spotify_playlist))
|
||||
for spotify_playlist in self.backend.spotify.stored_playlists:
|
||||
playlists.append(
|
||||
self.backend.translate.to_mopidy_playlist(spotify_playlist))
|
||||
self._playlists = playlists
|
||||
logger.debug(u'Available playlists: %s',
|
||||
u', '.join([u'<%s>' % p.name for p in self._playlists]))
|
||||
u', '.join([u'<%s>' % p.name for p in self.playlists]))
|
||||
|
||||
# Model translation
|
||||
|
||||
def _to_mopidy_id(self, spotify_uri):
|
||||
return 0 # TODO
|
||||
class DespotifyTranslator(object):
|
||||
uri_to_id_map = {}
|
||||
next_id = 0
|
||||
|
||||
def _to_mopidy_artist(self, spotify_artist):
|
||||
def to_mopidy_id(self, spotify_uri):
|
||||
if spotify_uri not in self.uri_to_id_map:
|
||||
this_id = self.next_id
|
||||
self.next_id += 1
|
||||
self.uri_to_id_map[spotify_uri] = this_id
|
||||
return self.uri_to_id_map[spotify_uri]
|
||||
|
||||
def to_mopidy_artist(self, spotify_artist):
|
||||
return Artist(
|
||||
uri=spotify_artist.get_uri(),
|
||||
name=spotify_artist.name.decode(ENCODING)
|
||||
)
|
||||
|
||||
def _to_mopidy_album(self, spotify_album_name):
|
||||
def to_mopidy_album(self, spotify_album_name):
|
||||
return Album(name=spotify_album_name.decode(ENCODING))
|
||||
|
||||
def _to_mopidy_track(self, spotify_track):
|
||||
def to_mopidy_track(self, spotify_track):
|
||||
if dt.MINYEAR <= int(spotify_track.year) <= dt.MAXYEAR:
|
||||
date = dt.date(spotify_track.year, 1, 1)
|
||||
else:
|
||||
@ -51,74 +101,18 @@ class DespotifyBackend(BaseBackend):
|
||||
return Track(
|
||||
uri=spotify_track.get_uri(),
|
||||
title=spotify_track.title.decode(ENCODING),
|
||||
artists=[self._to_mopidy_artist(a) for a in spotify_track.artists],
|
||||
album=self._to_mopidy_album(spotify_track.album),
|
||||
artists=[self.to_mopidy_artist(a) for a in spotify_track.artists],
|
||||
album=self.to_mopidy_album(spotify_track.album),
|
||||
track_no=spotify_track.tracknumber,
|
||||
date=date,
|
||||
length=spotify_track.length,
|
||||
id=self._to_mopidy_id(spotify_track.get_uri()),
|
||||
bitrate=320,
|
||||
id=self.to_mopidy_id(spotify_track.get_uri()),
|
||||
)
|
||||
|
||||
def _to_mopidy_playlist(self, spotify_playlist):
|
||||
def to_mopidy_playlist(self, spotify_playlist):
|
||||
return Playlist(
|
||||
uri=spotify_playlist.get_uri(),
|
||||
name=spotify_playlist.name.decode(ENCODING),
|
||||
tracks=[self._to_mopidy_track(t) for t in spotify_playlist.tracks],
|
||||
tracks=[self.to_mopidy_track(t) for t in spotify_playlist.tracks],
|
||||
)
|
||||
|
||||
# Play control
|
||||
|
||||
def _next(self):
|
||||
self._current_song_pos += 1
|
||||
self.spotify.play(self.spotify.lookup(self._current_track.uri))
|
||||
return True
|
||||
|
||||
def _pause(self):
|
||||
self.spotify.pause()
|
||||
return True
|
||||
|
||||
def _play(self):
|
||||
if self._current_track is not None:
|
||||
self.spotify.play(self.spotify.lookup(self._current_track.uri))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _play_id(self, songid):
|
||||
self._current_song_pos = songid # XXX
|
||||
self.spotify.play(self.spotify.lookup(self._current_track.uri))
|
||||
return True
|
||||
|
||||
def _play_pos(self, songpos):
|
||||
self._current_song_pos = songpos
|
||||
self.spotify.play(self.spotify.lookup(self._current_track.uri))
|
||||
return True
|
||||
|
||||
def _previous(self):
|
||||
self._current_song_pos -= 1
|
||||
self.spotify.play(self.spotify.lookup(self._current_track.uri))
|
||||
return True
|
||||
|
||||
def _resume(self):
|
||||
self.spotify.resume()
|
||||
return True
|
||||
|
||||
def _stop(self):
|
||||
self.spotify.stop()
|
||||
return True
|
||||
|
||||
# Status querying
|
||||
|
||||
def status_bitrate(self):
|
||||
return 320
|
||||
|
||||
def url_handlers(self):
|
||||
return [u'spotify:', u'http://open.spotify.com/']
|
||||
|
||||
# Music database
|
||||
|
||||
def search(self, type, what):
|
||||
query = u'%s:%s' % (type, what)
|
||||
result = self.spotify.search(query.encode(ENCODING))
|
||||
if result is not None:
|
||||
return self._to_mopidy_playlist(result.playlist).mpd_format()
|
||||
|
||||
@ -1,25 +1,31 @@
|
||||
from mopidy.backends import BaseBackend
|
||||
from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
|
||||
BasePlaybackController, BaseLibraryController,
|
||||
BaseStoredPlaylistsController)
|
||||
from mopidy.models import Playlist
|
||||
|
||||
class DummyBackend(BaseBackend):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyBackend, self).__init__(*args, **kwargs)
|
||||
def __init__(self):
|
||||
self.current_playlist = DummyCurrentPlaylistController(backend=self)
|
||||
self.library = DummyLibraryController(backend=self)
|
||||
self.playback = DummyPlaybackController(backend=self)
|
||||
self.stored_playlists = DummyStoredPlaylistsController(backend=self)
|
||||
self.uri_handlers = [u'dummy:']
|
||||
|
||||
def url_handlers(self):
|
||||
return [u'dummy:']
|
||||
class DummyCurrentPlaylistController(BaseCurrentPlaylistController):
|
||||
pass
|
||||
|
||||
class DummyLibraryController(BaseLibraryController):
|
||||
def search(self, type, query):
|
||||
return Playlist()
|
||||
|
||||
class DummyPlaybackController(BasePlaybackController):
|
||||
def _next(self):
|
||||
return True
|
||||
|
||||
def _pause(self):
|
||||
return True
|
||||
|
||||
def _play(self):
|
||||
return True
|
||||
|
||||
def _play_id(self, songid):
|
||||
return True
|
||||
|
||||
def _play_pos(self, songpos):
|
||||
def _play(self, track):
|
||||
return True
|
||||
|
||||
def _previous(self):
|
||||
@ -27,3 +33,7 @@ class DummyBackend(BaseBackend):
|
||||
|
||||
def _resume(self):
|
||||
return True
|
||||
|
||||
class DummyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
def search(self, query):
|
||||
return [Playlist(name=query)]
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
from copy import deepcopy
|
||||
import datetime as dt
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
from spotify import Link
|
||||
from spotify.manager import SpotifySessionManager
|
||||
from spotify.alsahelper import AlsaController
|
||||
|
||||
from mopidy import config
|
||||
from mopidy.backends import BaseBackend
|
||||
from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController,
|
||||
BaseLibraryController, BasePlaybackController,
|
||||
BaseStoredPlaylistsController)
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
|
||||
logger = logging.getLogger(u'backends.libspotify')
|
||||
@ -16,106 +18,61 @@ logger = logging.getLogger(u'backends.libspotify')
|
||||
ENCODING = 'utf-8'
|
||||
|
||||
class LibspotifyBackend(BaseBackend):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LibspotifyBackend, self).__init__(*args, **kwargs)
|
||||
self._next_id = 0
|
||||
self._id_to_uri_map = {}
|
||||
self._uri_to_id_map = {}
|
||||
def __init__(self):
|
||||
self.current_playlist = LibspotifyCurrentPlaylistController(
|
||||
backend=self)
|
||||
self.library = LibspotifyLibraryController(backend=self)
|
||||
self.playback = LibspotifyPlaybackController(backend=self)
|
||||
self.stored_playlists = LibspotifyStoredPlaylistsController(
|
||||
backend=self)
|
||||
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
|
||||
self.translate = LibspotifyTranslator()
|
||||
self.spotify = self._connect()
|
||||
|
||||
def _connect(self):
|
||||
logger.info(u'Connecting to Spotify')
|
||||
self.spotify = LibspotifySessionManager(
|
||||
spotify = LibspotifySessionManager(
|
||||
config.SPOTIFY_USERNAME, config.SPOTIFY_PASSWORD, backend=self)
|
||||
self.spotify.start()
|
||||
spotify.start()
|
||||
return spotify
|
||||
|
||||
def update_stored_playlists(self):
|
||||
logger.info(u'Updating stored playlists')
|
||||
playlists = []
|
||||
for spotify_playlist in self.spotify.playlists:
|
||||
playlists.append(self._to_mopidy_playlist(spotify_playlist))
|
||||
self._playlists = playlists
|
||||
logger.debug(u'Available playlists: %s',
|
||||
u', '.join([u'<%s>' % p.name for p in self._playlists]))
|
||||
|
||||
# Model translation
|
||||
class LibspotifyCurrentPlaylistController(BaseCurrentPlaylistController):
|
||||
pass
|
||||
|
||||
def _to_mopidy_id(self, spotify_uri):
|
||||
if spotify_uri in self._uri_to_id_map:
|
||||
return self._uri_to_id_map[spotify_uri]
|
||||
else:
|
||||
id = self._next_id
|
||||
self._next_id += 1
|
||||
self._id_to_uri_map[id] = spotify_uri
|
||||
self._uri_to_id_map[spotify_uri] = id
|
||||
return id
|
||||
|
||||
def _to_mopidy_artist(self, spotify_artist):
|
||||
return Artist(
|
||||
uri=str(Link.from_artist(spotify_artist)),
|
||||
name=spotify_artist.name().decode(ENCODING),
|
||||
)
|
||||
class LibspotifyLibraryController(BaseLibraryController):
|
||||
search_results = False
|
||||
|
||||
def _to_mopidy_album(self, spotify_album):
|
||||
# TODO pyspotify got much more data on albums than this
|
||||
return Album(name=spotify_album.name().decode(ENCODING))
|
||||
def search(self, type, what):
|
||||
# XXX This is slow
|
||||
self.search_results = None
|
||||
def callback(results, userdata):
|
||||
logger.debug(u'Search results received')
|
||||
self.search_results = results
|
||||
query = u'%s:%s' % (type, what)
|
||||
self.backend.spotify.search(query.encode(ENCODING), callback)
|
||||
while self.search_results is None:
|
||||
time.sleep(0.01)
|
||||
result = Playlist(tracks=[self.backend.translate.to_mopidy_track(t)
|
||||
for t in self.search_results.tracks()])
|
||||
self.search_results = False
|
||||
return result
|
||||
|
||||
def _to_mopidy_track(self, spotify_track):
|
||||
return Track(
|
||||
uri=str(Link.from_track(spotify_track, 0)),
|
||||
title=spotify_track.name().decode(ENCODING),
|
||||
artists=[self._to_mopidy_artist(a)
|
||||
for a in spotify_track.artists()],
|
||||
album=self._to_mopidy_album(spotify_track.album()),
|
||||
track_no=spotify_track.index(),
|
||||
date=dt.date(spotify_track.album().year(), 1, 1),
|
||||
length=spotify_track.duration(),
|
||||
id=self._to_mopidy_id(str(Link.from_track(spotify_track, 0))),
|
||||
)
|
||||
|
||||
def _to_mopidy_playlist(self, spotify_playlist):
|
||||
return Playlist(
|
||||
uri=str(Link.from_playlist(spotify_playlist)),
|
||||
name=spotify_playlist.name().decode(ENCODING),
|
||||
tracks=[self._to_mopidy_track(t) for t in spotify_playlist],
|
||||
)
|
||||
# Playback control
|
||||
|
||||
def _play_current_track(self):
|
||||
self.spotify.session.load(
|
||||
Link.from_string(self._current_track.uri).as_track())
|
||||
self.spotify.session.play(1)
|
||||
|
||||
def _next(self):
|
||||
self._current_song_pos += 1
|
||||
self._play_current_track()
|
||||
return True
|
||||
|
||||
class LibspotifyPlaybackController(BasePlaybackController):
|
||||
def _pause(self):
|
||||
# TODO
|
||||
return False
|
||||
|
||||
def _play(self):
|
||||
if self._current_track is not None:
|
||||
self._play_current_track()
|
||||
return True
|
||||
else:
|
||||
def _play(self, track):
|
||||
if self.state == self.PLAYING:
|
||||
self.stop()
|
||||
if track.uri is None:
|
||||
return False
|
||||
|
||||
def _play_id(self, songid):
|
||||
matches = filter(lambda t: t.id == songid, self._current_playlist)
|
||||
if matches:
|
||||
self._current_song_pos = self._current_playlist.index(matches[0])
|
||||
self._play_current_track()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _play_pos(self, songpos):
|
||||
self._current_song_pos = songpos
|
||||
self._play_current_track()
|
||||
return True
|
||||
|
||||
def _previous(self):
|
||||
self._current_song_pos -= 1
|
||||
self._play_current_track()
|
||||
self.backend.spotify.session.load(
|
||||
Link.from_string(track.uri).as_track())
|
||||
self.backend.spotify.session.play(1)
|
||||
return True
|
||||
|
||||
def _resume(self):
|
||||
@ -123,16 +80,71 @@ class LibspotifyBackend(BaseBackend):
|
||||
return False
|
||||
|
||||
def _stop(self):
|
||||
self.spotify.session.play(0)
|
||||
self.backend.spotify.session.play(0)
|
||||
return True
|
||||
|
||||
# Status querying
|
||||
|
||||
def status_bitrate(self):
|
||||
return 320
|
||||
class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController):
|
||||
def refresh(self):
|
||||
logger.info(u'Refreshing stored playlists')
|
||||
playlists = []
|
||||
for spotify_playlist in self.backend.spotify.playlists:
|
||||
playlists.append(
|
||||
self.backend.translate.to_mopidy_playlist(spotify_playlist))
|
||||
self._playlists = playlists
|
||||
logger.debug(u'Available playlists: %s',
|
||||
u', '.join([u'<%s>' % p.name for p in self.playlists]))
|
||||
|
||||
def url_handlers(self):
|
||||
return [u'spotify:', u'http://open.spotify.com/']
|
||||
|
||||
class LibspotifyTranslator(object):
|
||||
uri_to_id_map = {}
|
||||
next_id = 0
|
||||
|
||||
def to_mopidy_id(self, spotify_uri):
|
||||
if spotify_uri not in self.uri_to_id_map:
|
||||
this_id = self.next_id
|
||||
self.next_id += 1
|
||||
self.uri_to_id_map[spotify_uri] = this_id
|
||||
return self.uri_to_id_map[spotify_uri]
|
||||
|
||||
def to_mopidy_artist(self, spotify_artist):
|
||||
if not spotify_artist.is_loaded():
|
||||
return Artist(name=u'[loading...]')
|
||||
return Artist(
|
||||
uri=str(Link.from_artist(spotify_artist)),
|
||||
name=spotify_artist.name().decode(ENCODING),
|
||||
)
|
||||
|
||||
def to_mopidy_album(self, spotify_album):
|
||||
if not spotify_album.is_loaded():
|
||||
return Album(name=u'[loading...]')
|
||||
# TODO pyspotify got much more data on albums than this
|
||||
return Album(name=spotify_album.name().decode(ENCODING))
|
||||
|
||||
def to_mopidy_track(self, spotify_track):
|
||||
if not spotify_track.is_loaded():
|
||||
return Track(title=u'[loading...]')
|
||||
uri = str(Link.from_track(spotify_track, 0))
|
||||
return Track(
|
||||
uri=uri,
|
||||
title=spotify_track.name().decode(ENCODING),
|
||||
artists=[self.to_mopidy_artist(a) for a in spotify_track.artists()],
|
||||
album=self.to_mopidy_album(spotify_track.album()),
|
||||
track_no=spotify_track.index(),
|
||||
date=dt.date(spotify_track.album().year(), 1, 1),
|
||||
length=spotify_track.duration(),
|
||||
bitrate=320,
|
||||
id=self.to_mopidy_id(uri),
|
||||
)
|
||||
|
||||
def to_mopidy_playlist(self, spotify_playlist):
|
||||
if not spotify_playlist.is_loaded():
|
||||
return Playlist(name=u'[loading...]')
|
||||
return Playlist(
|
||||
uri=str(Link.from_playlist(spotify_playlist)),
|
||||
name=spotify_playlist.name().decode(ENCODING),
|
||||
tracks=[self.to_mopidy_track(t) for t in spotify_playlist],
|
||||
)
|
||||
|
||||
|
||||
class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
|
||||
@ -141,6 +153,7 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
|
||||
threading.Thread.__init__(self)
|
||||
self.backend = backend
|
||||
self.audio = AlsaController()
|
||||
self.playlists = []
|
||||
|
||||
def run(self):
|
||||
self.connect()
|
||||
@ -159,7 +172,9 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
|
||||
|
||||
def metadata_updated(self, session):
|
||||
logger.debug('Metadata updated')
|
||||
self.backend.update_stored_playlists()
|
||||
# XXX This changes data "owned" by another thread, and leads to
|
||||
# segmentation fault. We should use locking and messaging here.
|
||||
self.backend.stored_playlists.refresh()
|
||||
|
||||
def connection_error(self, session, error):
|
||||
logger.error('Connection error: %s', error)
|
||||
@ -175,10 +190,14 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
|
||||
|
||||
def play_token_lost(self, session):
|
||||
logger.debug('Play token lost')
|
||||
self.backend.playback.stop()
|
||||
|
||||
def log_message(self, session, data):
|
||||
logger.debug(data)
|
||||
|
||||
def end_of_track(self, session):
|
||||
logger.debug('End of track')
|
||||
self.backend.playback.next()
|
||||
|
||||
def search(self, query, callback):
|
||||
self.session.search(query, callback)
|
||||
|
||||
215
mopidy/models.py
215
mopidy/models.py
@ -1,6 +1,15 @@
|
||||
from copy import copy
|
||||
|
||||
class Artist(object):
|
||||
class ImmutableObject(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name.startswith('_'):
|
||||
return super(ImmutableObject, self).__setattr__(name, value)
|
||||
raise AttributeError('Object is immutable.')
|
||||
|
||||
class Artist(ImmutableObject):
|
||||
"""
|
||||
:param uri: artist URI
|
||||
:type uri: string
|
||||
@ -8,22 +17,14 @@ class Artist(object):
|
||||
:type name: string
|
||||
"""
|
||||
|
||||
def __init__(self, uri=None, name=None):
|
||||
self._uri = None
|
||||
self._name = name
|
||||
#: The artist URI. Read-only.
|
||||
uri = None
|
||||
|
||||
@property
|
||||
def uri(self):
|
||||
"""The artist URI. Read-only."""
|
||||
return self._uri
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The artist name. Read-only."""
|
||||
return self._name
|
||||
#: The artist name. Read-only.
|
||||
name = None
|
||||
|
||||
|
||||
class Album(object):
|
||||
class Album(ImmutableObject):
|
||||
"""
|
||||
:param uri: album URI
|
||||
:type uri: string
|
||||
@ -35,34 +36,26 @@ class Album(object):
|
||||
:type num_tracks: integer
|
||||
"""
|
||||
|
||||
def __init__(self, uri=None, name=None, artists=None, num_tracks=0):
|
||||
self._uri = uri
|
||||
self._name = name
|
||||
self._artists = artists or []
|
||||
self._num_tracks = num_tracks
|
||||
#: The album URI. Read-only.
|
||||
uri = None
|
||||
|
||||
@property
|
||||
def uri(self):
|
||||
"""The album URI. Read-only."""
|
||||
return self._uri
|
||||
#: The album name. Read-only.
|
||||
name = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The album name. Read-only."""
|
||||
return self._name
|
||||
#: The number of tracks in the album. Read-only.
|
||||
num_tracks = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._artists = kwargs.pop('artists', [])
|
||||
super(Album, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def artists(self):
|
||||
"""List of :class:`Artist` elements. Read-only."""
|
||||
return copy(self._artists)
|
||||
|
||||
@property
|
||||
def num_tracks(self):
|
||||
"""The number of tracks in the album. Read-only."""
|
||||
return self._num_tracks
|
||||
|
||||
|
||||
class Track(object):
|
||||
class Track(ImmutableObject):
|
||||
"""
|
||||
:param uri: track URI
|
||||
:type uri: string
|
||||
@ -84,64 +77,40 @@ class Track(object):
|
||||
:type id: integer
|
||||
"""
|
||||
|
||||
def __init__(self, uri=None, title=None, artists=None, album=None,
|
||||
track_no=0, date=None, length=None, bitrate=None, id=None):
|
||||
self._uri = uri
|
||||
self._title = title
|
||||
self._artists = artists or []
|
||||
self._album = album
|
||||
self._track_no = track_no
|
||||
self._date = date
|
||||
self._length = length
|
||||
self._bitrate = bitrate
|
||||
self._id = id
|
||||
#: The track URI. Read-only.
|
||||
uri = None
|
||||
|
||||
@property
|
||||
def uri(self):
|
||||
"""The track URI. Read-only."""
|
||||
return self._uri
|
||||
#: The track title. Read-only.
|
||||
title = None
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""The track title. Read-only."""
|
||||
return self._title
|
||||
#: The track :class:`Album`. Read-only.
|
||||
album = None
|
||||
|
||||
#: The track number in album. Read-only.
|
||||
track_no = 0
|
||||
|
||||
#: The track release date. Read-only.
|
||||
date = None
|
||||
|
||||
#: The track length in milliseconds. Read-only.
|
||||
length = None
|
||||
|
||||
#: The track's bitrate in kbit/s. Read-only.
|
||||
bitrate = None
|
||||
|
||||
#: The track ID. Read-only.
|
||||
id = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._artists = kwargs.pop('artists', [])
|
||||
super(Track, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def artists(self):
|
||||
"""List of :class:`Artist`. Read-only."""
|
||||
return copy(self._artists)
|
||||
|
||||
@property
|
||||
def album(self):
|
||||
"""The track :class:`Album`. Read-only."""
|
||||
return self._album
|
||||
|
||||
@property
|
||||
def track_no(self):
|
||||
"""The track number in album. Read-only."""
|
||||
return self._track_no
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
"""The track release date. Read-only."""
|
||||
return self._date
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
"""The track length in milliseconds. Read-only."""
|
||||
return self._length
|
||||
|
||||
@property
|
||||
def bitrate(self):
|
||||
"""The track's bitrate in kbit/s. Read-only."""
|
||||
return self._bitrate
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""The track ID. Read-only."""
|
||||
return self._id
|
||||
|
||||
def mpd_format(self, position=0):
|
||||
def mpd_format(self, position=0, search_result=False):
|
||||
"""
|
||||
Format track for output to MPD client.
|
||||
|
||||
@ -149,17 +118,23 @@ class Track(object):
|
||||
:type position: integer
|
||||
:rtype: list of two-tuples
|
||||
"""
|
||||
return [
|
||||
('file', self.uri),
|
||||
('Time', self.length // 1000),
|
||||
result = [
|
||||
('file', self.uri or ''),
|
||||
('Time', self.length and (self.length // 1000) or 0),
|
||||
('Artist', self.mpd_format_artists()),
|
||||
('Title', self.title),
|
||||
('Album', self.album.name),
|
||||
('Track', '%d/%d' % (self.track_no, self.album.num_tracks)),
|
||||
('Date', self.date),
|
||||
('Pos', position),
|
||||
('Id', self.id),
|
||||
('Title', self.title or ''),
|
||||
('Album', self.album and self.album.name or ''),
|
||||
('Date', self.date or ''),
|
||||
]
|
||||
if self.album is not None and self.album.num_tracks != 0:
|
||||
result.append(('Track', '%d/%d' % (
|
||||
self.track_no, self.album.num_tracks)))
|
||||
else:
|
||||
result.append(('Track', self.track_no))
|
||||
if not search_result:
|
||||
result.append(('Pos', position))
|
||||
result.append(('Id', self.id or position))
|
||||
return result
|
||||
|
||||
def mpd_format_artists(self):
|
||||
"""
|
||||
@ -170,7 +145,7 @@ class Track(object):
|
||||
return u', '.join([a.name for a in self.artists])
|
||||
|
||||
|
||||
class Playlist(object):
|
||||
class Playlist(ImmutableObject):
|
||||
"""
|
||||
:param uri: playlist URI
|
||||
:type uri: string
|
||||
@ -180,20 +155,15 @@ class Playlist(object):
|
||||
:type tracks: list of :class:`Track` elements
|
||||
"""
|
||||
|
||||
def __init__(self, uri=None, name=None, tracks=None):
|
||||
self._uri = uri
|
||||
self._name = name
|
||||
self._tracks = tracks or []
|
||||
#: The playlist URI. Read-only.
|
||||
uri = None
|
||||
|
||||
@property
|
||||
def uri(self):
|
||||
"""The playlist URI. Read-only."""
|
||||
return self._uri
|
||||
#: The playlist name. Read-only.
|
||||
name = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The playlist name. Read-only."""
|
||||
return self._name
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._tracks = kwargs.pop('tracks', [])
|
||||
super(Playlist, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
@ -205,15 +175,44 @@ class Playlist(object):
|
||||
"""The number of tracks in the playlist. Read-only."""
|
||||
return len(self._tracks)
|
||||
|
||||
def mpd_format(self, start=0, end=None):
|
||||
def mpd_format(self, start=0, end=None, search_result=False):
|
||||
"""
|
||||
Format playlist for output to MPD client.
|
||||
|
||||
Optionally limit output to the slice ``[start:end]`` of the playlist.
|
||||
|
||||
:param start: position of first track to include in output
|
||||
:type start: int
|
||||
:param end: position after last track to include in output
|
||||
:type end: int or :class:`None` for end of list
|
||||
:rtype: list of lists of two-tuples
|
||||
"""
|
||||
if end is None:
|
||||
end = self.length
|
||||
tracks = []
|
||||
for track, position in zip(self.tracks, range(start, end)):
|
||||
tracks.append(track.mpd_format(position))
|
||||
for track, position in zip(self.tracks[start:end], range(start, end)):
|
||||
tracks.append(track.mpd_format(position, search_result))
|
||||
return tracks
|
||||
|
||||
def with_(self, uri=None, name=None, tracks=None):
|
||||
"""
|
||||
Create a new playlist object with the given values. The values that are
|
||||
not given are taken from the object the method is called on.
|
||||
|
||||
Does not change the object on which it is called.
|
||||
|
||||
:param uri: playlist URI
|
||||
:type uri: string
|
||||
:param name: playlist name
|
||||
:type name: string
|
||||
:param tracks: playlist's tracks
|
||||
:type tracks: list of :class:`Track` elements
|
||||
:rtype: :class:`Playlist`
|
||||
"""
|
||||
if uri is None:
|
||||
uri = self.uri
|
||||
if name is None:
|
||||
name = self.name
|
||||
if tracks is None:
|
||||
tracks = self.tracks
|
||||
return Playlist(uri=uri, name=name, tracks=tracks)
|
||||
|
||||
@ -132,7 +132,8 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^currentsong$')
|
||||
def _currentsong(self):
|
||||
return self.backend.current_song()
|
||||
if self.backend.playback.current_track is not None:
|
||||
return self.backend.playback.current_track.mpd_format()
|
||||
|
||||
@register(r'^delete "(?P<songpos>\d+)"$')
|
||||
@register(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
@ -189,11 +190,15 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^listplaylists$')
|
||||
def _listplaylists(self):
|
||||
return self.backend.playlists_list()
|
||||
return [u'playlist: %s' % p.name
|
||||
for p in self.backend.stored_playlists.playlists]
|
||||
|
||||
@register(r'^load "(?P<name>[^"]+)"$')
|
||||
def _load(self, name):
|
||||
return self.backend.playlist_load(name)
|
||||
matches = self.backend.stored_playlists.search(name)
|
||||
if matches:
|
||||
self.backend.current_playlist.load(matches[0])
|
||||
self.backend.playback.new_playlist_loaded_callback()
|
||||
|
||||
@register(r'^lsinfo$')
|
||||
@register(r'^lsinfo "(?P<uri>[^"]*)"$')
|
||||
@ -213,7 +218,15 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^next$')
|
||||
def _next(self):
|
||||
return self.backend.next()
|
||||
return self.backend.playback.next()
|
||||
|
||||
@register(r'^outputs$')
|
||||
def _outputs(self):
|
||||
return [
|
||||
('outputid', 0),
|
||||
('outputname', self.backend.__class__.__name__),
|
||||
('outputenabled', 1),
|
||||
]
|
||||
|
||||
@register(r'^password "(?P<password>[^"]+)"$')
|
||||
def _password(self, password):
|
||||
@ -222,9 +235,9 @@ class MpdHandler(object):
|
||||
@register(r'^pause "(?P<state>[01])"$')
|
||||
def _pause(self, state):
|
||||
if int(state):
|
||||
self.backend.pause()
|
||||
self.backend.playback.pause()
|
||||
else:
|
||||
self.backend.resume()
|
||||
self.backend.playback.resume()
|
||||
|
||||
@register(r'^ping$')
|
||||
def _ping(self):
|
||||
@ -232,15 +245,25 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^play$')
|
||||
def _play(self):
|
||||
return self.backend.play()
|
||||
return self.backend.playback.play()
|
||||
|
||||
@register(r'^play "(?P<songpos>\d+)"$')
|
||||
def _playpos(self, songpos):
|
||||
return self.backend.play(songpos=int(songpos))
|
||||
songpos = int(songpos)
|
||||
try:
|
||||
track = self.backend.current_playlist.playlist.tracks[songpos]
|
||||
return self.backend.playback.play(track)
|
||||
except IndexError:
|
||||
raise MpdAckError(u'Position out of bounds')
|
||||
|
||||
@register(r'^playid "(?P<songid>\d+)"$')
|
||||
def _playid(self, songid):
|
||||
return self.backend.play(songid=int(songid))
|
||||
songid = int(songid)
|
||||
try:
|
||||
track = self.backend.current_playlist.get_by_id(songid)
|
||||
return self.backend.playback.play(track)
|
||||
except KeyError, e:
|
||||
raise MpdAckError(unicode(e))
|
||||
|
||||
@register(r'^playlist$')
|
||||
def _playlist(self):
|
||||
@ -264,13 +287,23 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^playlistid( "(?P<songid>\S+)")*$')
|
||||
def _playlistid(self, songid=None):
|
||||
return self.backend.playlist_info(songid, None, None)
|
||||
return self.backend.current_playlist.playlist.mpd_format()
|
||||
|
||||
@register(r'^playlistinfo$')
|
||||
@register(r'^playlistinfo "(?P<songpos>\d+)"$')
|
||||
@register(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def _playlistinfo(self, songpos=None, start=None, end=None):
|
||||
return self.backend.playlist_info(songpos, start, end)
|
||||
if songpos is not None:
|
||||
songpos = int(songpos)
|
||||
return self.backend.current_playlist.playlist.mpd_format(
|
||||
songpos, songpos + 1)
|
||||
else:
|
||||
if start is None:
|
||||
start = 0
|
||||
start = int(start)
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
return self.backend.current_playlist.playlist.mpd_format(start, end)
|
||||
|
||||
@register(r'^playlistmove "(?P<name>[^"]+)" "(?P<songid>\d+)" "(?P<songpos>\d+)"$')
|
||||
def _playlistdelete(self, name, songid, songpos):
|
||||
@ -282,7 +315,8 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^plchanges "(?P<version>\d+)"$')
|
||||
def _plchanges(self, version):
|
||||
return self.backend.playlist_changes_since(version)
|
||||
if int(version) < self.backend.current_playlist.version:
|
||||
return self.backend.current_playlist.playlist.mpd_format()
|
||||
|
||||
@register(r'^plchangesposid "(?P<version>\d+)"$')
|
||||
def _plchangesposid(self, version):
|
||||
@ -290,7 +324,7 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^previous$')
|
||||
def _previous(self):
|
||||
return self.backend.previous()
|
||||
return self.backend.playback.previous()
|
||||
|
||||
@register(r'^rename "(?P<old_name>[^"]+)" "(?P<new_name>[^"]+)"$')
|
||||
def _rename(self, old_name, new_name):
|
||||
@ -334,7 +368,8 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^search "(?P<type>(album|artist|filename|title))" "(?P<what>[^"]+)"$')
|
||||
def _search(self, type, what):
|
||||
return self.backend.search(type, what)
|
||||
return self.backend.library.search(type, what).mpd_format(
|
||||
search_result=True)
|
||||
|
||||
@register(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\d+)"$')
|
||||
def _seek(self, songpos, seconds):
|
||||
@ -368,42 +403,114 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^stats$')
|
||||
def _stats(self):
|
||||
pass # TODO
|
||||
return {
|
||||
'artists': 0,
|
||||
'albums': 0,
|
||||
'songs': 0,
|
||||
'artists': 0, # TODO
|
||||
'albums': 0, # TODO
|
||||
'songs': 0, # TODO
|
||||
'uptime': self.session.stats_uptime(),
|
||||
'db_playtime': 0,
|
||||
'db_update': 0,
|
||||
'playtime': 0,
|
||||
'db_playtime': 0, # TODO
|
||||
'db_update': 0, # TODO
|
||||
'playtime': 0, # TODO
|
||||
}
|
||||
|
||||
@register(r'^stop$')
|
||||
def _stop(self):
|
||||
self.backend.stop()
|
||||
self.backend.playback.stop()
|
||||
|
||||
@register(r'^status$')
|
||||
def _status(self):
|
||||
result = [
|
||||
('volume', self.backend.status_volume()),
|
||||
('repeat', self.backend.status_repeat()),
|
||||
('random', self.backend.status_random()),
|
||||
('single', self.backend.status_single()),
|
||||
('consume', self.backend.status_consume()),
|
||||
('playlist', self.backend.status_playlist()),
|
||||
('playlistlength', self.backend.status_playlist_length()),
|
||||
('xfade', self.backend.status_xfade()),
|
||||
('state', self.backend.status_state()),
|
||||
('volume', self._status_volume()),
|
||||
('repeat', self._status_repeat()),
|
||||
('random', self._status_random()),
|
||||
('single', self._status_single()),
|
||||
('consume', self._status_consume()),
|
||||
('playlist', self._status_playlist_version()),
|
||||
('playlistlength', self._status_playlist_length()),
|
||||
('xfade', self._status_xfade()),
|
||||
('state', self._status_state()),
|
||||
]
|
||||
if self.backend.status_playlist_length() > 0:
|
||||
result.append(('song', self.backend.status_song_id()))
|
||||
result.append(('songid', self.backend.status_song_id()))
|
||||
if self.backend.state in (self.backend.PLAY, self.backend.PAUSE):
|
||||
result.append(('time', self.backend.status_time()))
|
||||
result.append(('bitrate', self.backend.status_bitrate()))
|
||||
if self.backend.playback.current_track is not None:
|
||||
result.append(('song', self._status_songpos()))
|
||||
result.append(('songid', self._status_songid()))
|
||||
if self.backend.playback.state in (
|
||||
self.backend.playback.PLAYING, self.backend.playback.PAUSED):
|
||||
result.append(('time', self._status_time()))
|
||||
result.append(('bitrate', self._status_bitrate()))
|
||||
return result
|
||||
|
||||
def _status_bitrate(self):
|
||||
if self.backend.playback.current_track is not None:
|
||||
return self.backend.playback.current_track.bitrate
|
||||
|
||||
def _status_consume(self):
|
||||
if self.backend.playback.consume:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _status_playlist_length(self):
|
||||
return self.backend.current_playlist.playlist.length
|
||||
|
||||
def _status_playlist_version(self):
|
||||
return self.backend.current_playlist.version
|
||||
|
||||
def _status_random(self):
|
||||
if self.backend.playback.random:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _status_repeat(self):
|
||||
if self.backend.playback.repeat:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _status_single(self):
|
||||
return 0 # TODO
|
||||
|
||||
def _status_songid(self):
|
||||
if self.backend.playback.current_track.id is not None:
|
||||
return self.backend.playback.current_track.id
|
||||
else:
|
||||
return self._status_songpos()
|
||||
|
||||
def _status_songpos(self):
|
||||
return self.backend.playback.playlist_position
|
||||
|
||||
def _status_state(self):
|
||||
if self.backend.playback.state == self.backend.playback.PLAYING:
|
||||
return u'play'
|
||||
elif self.backend.playback.state == self.backend.playback.STOPPED:
|
||||
return u'stop'
|
||||
elif self.backend.playback.state == self.backend.playback.PAUSED:
|
||||
return u'pause'
|
||||
|
||||
def _status_time(self):
|
||||
return u'%s:%s' % (
|
||||
self._status_time_elapsed(), self._status_time_total())
|
||||
|
||||
def _status_time_elapsed(self):
|
||||
return self.backend.playback.time_position
|
||||
|
||||
def _status_time_total(self):
|
||||
if self.backend.playback.current_track is None:
|
||||
return 0
|
||||
elif self.backend.playback.current_track.length is None:
|
||||
return 0
|
||||
else:
|
||||
return self.backend.playback.current_track.length // 1000
|
||||
|
||||
def _status_volume(self):
|
||||
if self.backend.playback.volume is not None:
|
||||
return self.backend.playback.volume
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _status_xfade(self):
|
||||
return 0 # TODO
|
||||
|
||||
@register(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
|
||||
def _swap(self, songpos1, songpos2):
|
||||
raise MpdNotImplemented # TODO
|
||||
@ -418,4 +525,4 @@ class MpdHandler(object):
|
||||
|
||||
@register(r'^urlhandlers$')
|
||||
def _urlhandlers(self):
|
||||
return self.backend.url_handlers()
|
||||
return self.backend.uri_handlers
|
||||
|
||||
@ -9,6 +9,7 @@ def main():
|
||||
sys.path.insert(0,
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
|
||||
r = CoverageTestRunner()
|
||||
r.add_pair('mopidy/models.py', 'tests/modelstest.py')
|
||||
r.add_pair('mopidy/mpd/handler.py', 'tests/mpd/handlertest.py')
|
||||
r.run()
|
||||
|
||||
|
||||
200
tests/modelstest.py
Normal file
200
tests/modelstest.py
Normal file
@ -0,0 +1,200 @@
|
||||
import datetime as dt
|
||||
import unittest
|
||||
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
|
||||
class ArtistTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = u'an_uri'
|
||||
artist = Artist(uri=uri)
|
||||
self.assertEqual(artist.uri, uri)
|
||||
self.assertRaises(AttributeError, setattr, artist, 'uri', None)
|
||||
|
||||
def test_name(self):
|
||||
name = u'a name'
|
||||
artist = Artist(name=name)
|
||||
self.assertEqual(artist.name, name)
|
||||
self.assertRaises(AttributeError, setattr, artist, 'name', None)
|
||||
|
||||
|
||||
class AlbumTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = u'an_uri'
|
||||
album = Album(uri=uri)
|
||||
self.assertEqual(album.uri, uri)
|
||||
self.assertRaises(AttributeError, setattr, album, 'uri', None)
|
||||
|
||||
def test_name(self):
|
||||
name = u'a name'
|
||||
album = Album(name=name)
|
||||
self.assertEqual(album.name, name)
|
||||
self.assertRaises(AttributeError, setattr, album, 'name', None)
|
||||
|
||||
def test_artists(self):
|
||||
artists = [Artist()]
|
||||
album = Album(artists=artists)
|
||||
self.assertEqual(album.artists, artists)
|
||||
self.assertRaises(AttributeError, setattr, album, 'artists', None)
|
||||
|
||||
def test_num_tracks(self):
|
||||
num_tracks = 11
|
||||
album = Album(num_tracks=11)
|
||||
self.assertEqual(album.num_tracks, num_tracks)
|
||||
self.assertRaises(AttributeError, setattr, album, 'num_tracks', None)
|
||||
|
||||
|
||||
class TrackTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = u'an_uri'
|
||||
track = Track(uri=uri)
|
||||
self.assertEqual(track.uri, uri)
|
||||
self.assertRaises(AttributeError, setattr, track, 'uri', None)
|
||||
|
||||
def test_title(self):
|
||||
title = u'a title'
|
||||
track = Track(title=title)
|
||||
self.assertEqual(track.title, title)
|
||||
self.assertRaises(AttributeError, setattr, track, 'title', None)
|
||||
|
||||
def test_artists(self):
|
||||
artists = [Artist(), Artist()]
|
||||
track = Track(artists=artists)
|
||||
self.assertEqual(track.artists, artists)
|
||||
self.assertRaises(AttributeError, setattr, track, 'artists', None)
|
||||
|
||||
def test_album(self):
|
||||
album = Album()
|
||||
track = Track(album=album)
|
||||
self.assertEqual(track.album, album)
|
||||
self.assertRaises(AttributeError, setattr, track, 'album', None)
|
||||
|
||||
def test_track_no(self):
|
||||
track_no = 7
|
||||
track = Track(track_no=track_no)
|
||||
self.assertEqual(track.track_no, track_no)
|
||||
self.assertRaises(AttributeError, setattr, track, 'track_no', None)
|
||||
|
||||
def test_date(self):
|
||||
date = dt.date(1977, 1, 1)
|
||||
track = Track(date=date)
|
||||
self.assertEqual(track.date, date)
|
||||
self.assertRaises(AttributeError, setattr, track, 'date', None)
|
||||
|
||||
def test_length(self):
|
||||
length = 137000
|
||||
track = Track(length=length)
|
||||
self.assertEqual(track.length, length)
|
||||
self.assertRaises(AttributeError, setattr, track, 'length', None)
|
||||
|
||||
def test_bitrate(self):
|
||||
bitrate = 160
|
||||
track = Track(bitrate=bitrate)
|
||||
self.assertEqual(track.bitrate, bitrate)
|
||||
self.assertRaises(AttributeError, setattr, track, 'bitrate', None)
|
||||
|
||||
def test_id(self):
|
||||
id = 17
|
||||
track = Track(id=id)
|
||||
self.assertEqual(track.id, id)
|
||||
self.assertRaises(AttributeError, setattr, track, 'id', None)
|
||||
|
||||
def test_mpd_format_for_empty_track(self):
|
||||
track = Track()
|
||||
result = track.mpd_format()
|
||||
self.assert_(('file', '') in result)
|
||||
self.assert_(('Time', 0) in result)
|
||||
self.assert_(('Artist', '') in result)
|
||||
self.assert_(('Title', '') in result)
|
||||
self.assert_(('Album', '') in result)
|
||||
self.assert_(('Track', 0) in result)
|
||||
self.assert_(('Date', '') in result)
|
||||
self.assert_(('Pos', 0) in result)
|
||||
self.assert_(('Id', 0) in result)
|
||||
|
||||
def test_mpd_format_for_nonempty_track(self):
|
||||
track = Track(
|
||||
uri=u'a uri',
|
||||
artists=[Artist(name=u'an artist')],
|
||||
title=u'a title',
|
||||
album=Album(name=u'an album', num_tracks=13),
|
||||
track_no=7,
|
||||
date=dt.date(1977, 1, 1),
|
||||
length=137000,
|
||||
id=122,
|
||||
)
|
||||
result = track.mpd_format(position=9)
|
||||
self.assert_(('file', 'a uri') in result)
|
||||
self.assert_(('Time', 137) in result)
|
||||
self.assert_(('Artist', 'an artist') in result)
|
||||
self.assert_(('Title', 'a title') in result)
|
||||
self.assert_(('Album', 'an album') in result)
|
||||
self.assert_(('Track', '7/13') in result)
|
||||
self.assert_(('Date', dt.date(1977, 1, 1)) in result)
|
||||
self.assert_(('Pos', 9) in result)
|
||||
self.assert_(('Id', 122) in result)
|
||||
|
||||
def test_mpd_format_artists(self):
|
||||
track = Track(artists=[Artist(name=u'ABBA'), Artist(name=u'Beatles')])
|
||||
self.assertEqual(track.mpd_format_artists(), u'ABBA, Beatles')
|
||||
|
||||
class PlaylistTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = u'an_uri'
|
||||
playlist = Playlist(uri=uri)
|
||||
self.assertEqual(playlist.uri, uri)
|
||||
self.assertRaises(AttributeError, setattr, playlist, 'uri', None)
|
||||
|
||||
def test_name(self):
|
||||
name = u'a name'
|
||||
playlist = Playlist(name=name)
|
||||
self.assertEqual(playlist.name, name)
|
||||
self.assertRaises(AttributeError, setattr, playlist, 'name', None)
|
||||
|
||||
def test_tracks(self):
|
||||
tracks = [Track(), Track(), Track()]
|
||||
playlist = Playlist(tracks=tracks)
|
||||
self.assertEqual(playlist.tracks, tracks)
|
||||
self.assertRaises(AttributeError, setattr, playlist, 'tracks', None)
|
||||
|
||||
def test_length(self):
|
||||
tracks = [Track(), Track(), Track()]
|
||||
playlist = Playlist(tracks=tracks)
|
||||
self.assertEqual(playlist.length, 3)
|
||||
|
||||
def test_mpd_format(self):
|
||||
playlist = Playlist(tracks=[
|
||||
Track(track_no=1), Track(track_no=2), Track(track_no=3)])
|
||||
result = playlist.mpd_format()
|
||||
self.assertEqual(len(result), 3)
|
||||
|
||||
def test_mpd_format_with_range(self):
|
||||
playlist = Playlist(tracks=[
|
||||
Track(track_no=1), Track(track_no=2), Track(track_no=3)])
|
||||
result = playlist.mpd_format(1, 2)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(dict(result[0])['Track'], 2)
|
||||
|
||||
def test_with_new_uri(self):
|
||||
tracks = [Track()]
|
||||
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks)
|
||||
new_playlist = playlist.with_(uri=u'another uri')
|
||||
self.assertEqual(new_playlist.uri, u'another uri')
|
||||
self.assertEqual(new_playlist.name, u'a name')
|
||||
self.assertEqual(new_playlist.tracks, tracks)
|
||||
|
||||
def test_with_new_name(self):
|
||||
tracks = [Track()]
|
||||
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks)
|
||||
new_playlist = playlist.with_(name=u'another name')
|
||||
self.assertEqual(new_playlist.uri, u'an uri')
|
||||
self.assertEqual(new_playlist.name, u'another name')
|
||||
self.assertEqual(new_playlist.tracks, tracks)
|
||||
|
||||
def test_with_new_tracks(self):
|
||||
tracks = [Track()]
|
||||
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks)
|
||||
new_tracks = [Track(), Track()]
|
||||
new_playlist = playlist.with_(tracks=new_tracks)
|
||||
self.assertEqual(new_playlist.uri, u'an uri')
|
||||
self.assertEqual(new_playlist.name, u'a name')
|
||||
self.assertEqual(new_playlist.tracks, new_tracks)
|
||||
@ -94,6 +94,20 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'ACK Not implemented' in result)
|
||||
|
||||
def test_currentsong(self):
|
||||
self.b.playback.current_track = Track()
|
||||
result = self.h.handle_request(u'currentsong')
|
||||
self.assert_(u'file: ' in result)
|
||||
self.assert_(u'Time: 0' in result)
|
||||
self.assert_(u'Artist: ' in result)
|
||||
self.assert_(u'Title: ' in result)
|
||||
self.assert_(u'Album: ' in result)
|
||||
self.assert_(u'Track: 0' in result)
|
||||
self.assert_(u'Date: ' in result)
|
||||
self.assert_(u'Pos: 0' in result)
|
||||
self.assert_(u'Id: 0' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_currentsong_without_song(self):
|
||||
result = self.h.handle_request(u'currentsong')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
@ -130,37 +144,117 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
result = self.h.handle_request(u'status')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_status_method(self):
|
||||
def test_status_method_contains_volume_which_defaults_to_0(self):
|
||||
self.b.playback.volume = None
|
||||
result = dict(self.h._status())
|
||||
self.assert_('volume' in result)
|
||||
self.assert_(int(result['volume']) in xrange(0, 101))
|
||||
self.assertEquals(int(result['volume']), 0)
|
||||
|
||||
def test_status_method_contains_volume(self):
|
||||
self.b.playback.volume = 17
|
||||
result = dict(self.h._status())
|
||||
self.assert_('volume' in result)
|
||||
self.assertEquals(int(result['volume']), 17)
|
||||
|
||||
def test_status_method_contains_repeat_is_0(self):
|
||||
result = dict(self.h._status())
|
||||
self.assert_('repeat' in result)
|
||||
self.assert_(int(result['repeat']) in (0, 1))
|
||||
self.assertEquals(int(result['repeat']), 0)
|
||||
|
||||
def test_status_method_contains_repeat_is_1(self):
|
||||
self.b.playback.repeat = 1
|
||||
result = dict(self.h._status())
|
||||
self.assert_('repeat' in result)
|
||||
self.assertEquals(int(result['repeat']), 1)
|
||||
|
||||
def test_status_method_contains_random_is_0(self):
|
||||
result = dict(self.h._status())
|
||||
self.assert_('random' in result)
|
||||
self.assert_(int(result['random']) in (0, 1))
|
||||
self.assertEquals(int(result['random']), 0)
|
||||
|
||||
def test_status_method_contains_random_is_1(self):
|
||||
self.b.playback.random = 1
|
||||
result = dict(self.h._status())
|
||||
self.assert_('random' in result)
|
||||
self.assertEquals(int(result['random']), 1)
|
||||
|
||||
def test_status_method_contains_single(self):
|
||||
result = dict(self.h._status())
|
||||
self.assert_('single' in result)
|
||||
self.assert_(int(result['single']) in (0, 1))
|
||||
|
||||
def test_status_method_contains_consume_is_0(self):
|
||||
result = dict(self.h._status())
|
||||
self.assert_('consume' in result)
|
||||
self.assert_(int(result['consume']) in (0, 1))
|
||||
self.assertEquals(int(result['consume']), 0)
|
||||
|
||||
def test_status_method_contains_consume_is_1(self):
|
||||
self.b.playback.consume = 1
|
||||
result = dict(self.h._status())
|
||||
self.assert_('consume' in result)
|
||||
self.assertEquals(int(result['consume']), 1)
|
||||
|
||||
def test_status_method_contains_playlist(self):
|
||||
result = dict(self.h._status())
|
||||
self.assert_('playlist' in result)
|
||||
self.assert_(int(result['playlist']) in xrange(0, 2**31))
|
||||
|
||||
def test_status_method_contains_playlistlength(self):
|
||||
result = dict(self.h._status())
|
||||
self.assert_('playlistlength' in result)
|
||||
self.assert_(int(result['playlistlength']) >= 0)
|
||||
|
||||
def test_status_method_contains_xfade(self):
|
||||
result = dict(self.h._status())
|
||||
self.assert_('xfade' in result)
|
||||
self.assert_(int(result['xfade']) >= 0)
|
||||
self.assert_('state' in result)
|
||||
self.assert_(result['state'] in ('play', 'stop', 'pause'))
|
||||
|
||||
def test_status_method_when_playlist_loaded(self):
|
||||
self.b._current_playlist = Playlist(tracks=[Track()])
|
||||
def test_status_method_contains_state_is_play(self):
|
||||
self.b.playback.state = self.b.playback.PLAYING
|
||||
result = dict(self.h._status())
|
||||
self.assert_('state' in result)
|
||||
self.assertEquals(result['state'], 'play')
|
||||
|
||||
def test_status_method_contains_state_is_stop(self):
|
||||
self.b.playback.state = self.b.playback.STOPPED
|
||||
result = dict(self.h._status())
|
||||
self.assert_('state' in result)
|
||||
self.assertEquals(result['state'], 'stop')
|
||||
|
||||
def test_status_method_contains_state_is_pause(self):
|
||||
self.b.playback.state = self.b.playback.PLAYING
|
||||
self.b.playback.state = self.b.playback.PAUSED
|
||||
result = dict(self.h._status())
|
||||
self.assert_('state' in result)
|
||||
self.assertEquals(result['state'], 'pause')
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_song(self):
|
||||
track = Track()
|
||||
self.b.current_playlist.load(Playlist(tracks=[track]))
|
||||
self.b.playback.current_track = track
|
||||
result = dict(self.h._status())
|
||||
self.assert_('song' in result)
|
||||
self.assert_(int(result['song']) >= 0)
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_pos_as_songid(self):
|
||||
track = Track()
|
||||
self.b.current_playlist.load(Playlist(tracks=[track]))
|
||||
self.b.playback.current_track = track
|
||||
result = dict(self.h._status())
|
||||
self.assert_('songid' in result)
|
||||
self.assert_(int(result['songid']) >= 0)
|
||||
|
||||
def test_status_method_when_playing(self):
|
||||
self.b.state = self.b.PLAY
|
||||
def test_status_method_when_playlist_loaded_contains_id_as_songid(self):
|
||||
track = Track(id=1)
|
||||
self.b.current_playlist.load(Playlist(tracks=[track]))
|
||||
self.b.playback.current_track = track
|
||||
result = dict(self.h._status())
|
||||
self.assert_('songid' in result)
|
||||
self.assertEquals(int(result['songid']), 1)
|
||||
|
||||
def test_status_method_when_playing_contains_time_with_no_length(self):
|
||||
self.b.playback.current_track = Track(length=None)
|
||||
self.b.playback.state = self.b.playback.PLAYING
|
||||
result = dict(self.h._status())
|
||||
self.assert_('time' in result)
|
||||
(position, total) = result['time'].split(':')
|
||||
@ -168,6 +262,23 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
total = int(total)
|
||||
self.assert_(position <= total)
|
||||
|
||||
def test_status_method_when_playing_contains_time_with_length(self):
|
||||
self.b.playback.current_track = Track(length=10000)
|
||||
self.b.playback.state = self.b.playback.PLAYING
|
||||
result = dict(self.h._status())
|
||||
self.assert_('time' in result)
|
||||
(position, total) = result['time'].split(':')
|
||||
position = int(position)
|
||||
total = int(total)
|
||||
self.assert_(position <= total)
|
||||
|
||||
def test_status_method_when_playing_contains_bitrate(self):
|
||||
self.b.playback.state = self.b.playback.PLAYING
|
||||
self.b.playback.current_track = Track(bitrate=320)
|
||||
result = dict(self.h._status())
|
||||
self.assert_('bitrate' in result)
|
||||
self.assertEquals(int(result['bitrate']), 320)
|
||||
|
||||
|
||||
class PlaybackOptionsHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -279,32 +390,55 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_pause_off(self):
|
||||
self.h.handle_request(u'play')
|
||||
track = Track()
|
||||
self.b.current_playlist.playlist = Playlist(tracks=[track])
|
||||
self.b.playback.current_track = track
|
||||
self.h.handle_request(u'play "0"')
|
||||
self.h.handle_request(u'pause "1"')
|
||||
result = self.h.handle_request(u'pause "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEquals(self.b.PLAY, self.b.state)
|
||||
self.assertEquals(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_pause_on(self):
|
||||
self.h.handle_request(u'play')
|
||||
track = Track()
|
||||
self.b.current_playlist.playlist = Playlist(tracks=[track])
|
||||
self.b.playback.current_track = track
|
||||
self.h.handle_request(u'play "0"')
|
||||
result = self.h.handle_request(u'pause "1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEquals(self.b.PAUSE, self.b.state)
|
||||
self.assertEquals(self.b.playback.PAUSED, self.b.playback.state)
|
||||
|
||||
def test_play_without_pos(self):
|
||||
track = Track()
|
||||
self.b.current_playlist.playlist = Playlist(tracks=[track])
|
||||
self.b.playback.current_track = track
|
||||
self.b.playback.state = self.b.playback.PAUSED
|
||||
result = self.h.handle_request(u'play')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEquals(self.b.PLAY, self.b.state)
|
||||
self.assertEquals(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_play_with_pos(self):
|
||||
self.b.current_playlist.load(Playlist(tracks=[Track()]))
|
||||
result = self.h.handle_request(u'play "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEquals(self.b.PLAY, self.b.state)
|
||||
self.assertEquals(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_play_with_pos_out_of_bounds(self):
|
||||
self.b.current_playlist.load(Playlist())
|
||||
result = self.h.handle_request(u'play "0"')
|
||||
self.assert_(u'ACK Position out of bounds' in result)
|
||||
self.assertEquals(self.b.playback.STOPPED, self.b.playback.state)
|
||||
|
||||
def test_playid(self):
|
||||
self.b.current_playlist.load(Playlist(tracks=[Track(id=0)]))
|
||||
result = self.h.handle_request(u'playid "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEquals(self.b.PLAY, self.b.state)
|
||||
self.assertEquals(self.b.playback.PLAYING, self.b.playback.state)
|
||||
|
||||
def test_playid_which_does_not_exist(self):
|
||||
self.b.current_playlist.load(Playlist(tracks=[Track(id=0)]))
|
||||
result = self.h.handle_request(u'playid "1"')
|
||||
self.assert_(u'ACK Track with ID "1" not found' in result)
|
||||
|
||||
def test_previous(self):
|
||||
result = self.h.handle_request(u'previous')
|
||||
@ -321,7 +455,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
def test_stop(self):
|
||||
result = self.h.handle_request(u'stop')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEquals(self.b.STOP, self.b.state)
|
||||
self.assertEquals(self.b.playback.STOPPED, self.b.playback.state)
|
||||
|
||||
|
||||
class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
@ -640,11 +774,17 @@ class ConnectionHandlerTest(unittest.TestCase):
|
||||
result = self.h.handle_request(u'ping')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
|
||||
class AudioOutputHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.h = handler.MpdHandler(backend=DummyBackend())
|
||||
|
||||
pass # TODO
|
||||
def test_outputs(self):
|
||||
result = self.h.handle_request(u'outputs')
|
||||
self.assert_(u'outputid: 0' in result)
|
||||
self.assert_(u'outputname: DummyBackend' in result)
|
||||
self.assert_(u'outputenabled: 1' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
|
||||
class ReflectionHandlerTest(unittest.TestCase):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user