Merge branch 'master' of git://github.com/jodal/mopidy

This commit is contained in:
Thomas Adamcik 2010-02-13 20:56:42 +01:00
commit f03a8238b1
12 changed files with 1240 additions and 812 deletions

View File

@ -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>

View File

@ -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:

View File

@ -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::

View File

@ -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)

View File

@ -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()

View File

@ -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)]

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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
View 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)

View File

@ -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):