Merge branch 'master' into gstreamer

This commit is contained in:
Thomas Adamcik 2010-02-07 04:41:09 +01:00
commit 4552784637
12 changed files with 527 additions and 70 deletions

View File

@ -4,7 +4,7 @@ Mopidy
Mopidy is an `MPD <http://mpd.wikia.com/>`_ server with a
`Spotify <http://www.spotify.com/>`_ backend. Using a standard MPD client you
can search for music in Spotify's wast archive, manage Spotify play lists and
can search for music in Spotify's vast archive, manage Spotify play lists and
play music from Spotify.
Mopidy is currently under development. Unless you want to contribute to the

0
docs/_static/.placeholder vendored Normal file
View File

View File

@ -10,8 +10,8 @@
body {
font-family: Arial, sans-serif;
font-size: 100%;
background-color: #111;
color: #555;
background-color: #111111;
color: #555555;
margin: 0;
padding: 0;
}
@ -22,7 +22,7 @@ div.documentwrapper {
}
div.bodywrapper {
margin: 0 0 0 230px;
margin: 0 0 0 300px;
}
hr{
@ -30,14 +30,14 @@ hr{
}
div.document {
background-color: #eee;
background-color: #fafafa;
}
div.body {
background-color: #ffffff;
color: #3E4349;
padding: 0 30px 30px 30px;
font-size: 0.8em;
padding: 1em 30px 30px 30px;
font-size: 0.9em;
}
div.footer {
@ -49,25 +49,29 @@ div.footer {
}
div.footer a {
color: #444;
text-decoration: underline;
color: #444444;
}
div.related {
background-color: #6BA81E;
line-height: 32px;
color: #fff;
text-shadow: 0px 1px 0 #444;
font-size: 0.80em;
line-height: 36px;
color: #ffffff;
text-shadow: 0px 1px 0 #444444;
font-size: 1.1em;
}
div.related a {
color: #E2F3CC;
}
div.related .right {
font-size: 0.9em;
}
div.sphinxsidebar {
font-size: 0.75em;
font-size: 0.9em;
line-height: 1.5em;
width: 300px
}
div.sphinxsidebarwrapper{
@ -77,46 +81,46 @@ div.sphinxsidebarwrapper{
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: Arial, sans-serif;
color: #222;
color: #222222;
font-size: 1.2em;
font-weight: normal;
font-weight: bold;
margin: 0;
padding: 5px 10px;
background-color: #ddd;
text-shadow: 1px 1px 0 white
}
div.sphinxsidebar h4{
font-size: 1.1em;
}
div.sphinxsidebar h3 a {
color: #444;
color: #444444;
}
div.sphinxsidebar p {
color: #888;
color: #888888;
padding: 5px 20px;
margin: 0.5em 0px;
}
div.sphinxsidebar p.topless {
}
div.sphinxsidebar ul {
margin: 10px 20px;
margin: 10px 10px 10px 20px;
padding: 0;
color: #000;
color: #000000;
}
div.sphinxsidebar a {
color: #444;
color: #444444;
}
div.sphinxsidebar a:hover {
color: #E32E00;
}
div.sphinxsidebar input {
border: 1px solid #ccc;
border: 1px solid #cccccc;
font-family: sans-serif;
font-size: 1em;
font-size: 1.1em;
padding: 0.15em 0.3em;
}
div.sphinxsidebar input[type=text]{
@ -132,7 +136,6 @@ a {
a:hover {
color: #E32E00;
text-decoration: underline;
}
div.body h1,
@ -142,20 +145,20 @@ div.body h4,
div.body h5,
div.body h6 {
font-family: Arial, sans-serif;
background-color: #BED4EB;
font-weight: normal;
color: #212224;
margin: 30px 0px 10px 0px;
padding: 5px 0 5px 10px;
text-shadow: 0px 1px 0 white
padding: 5px 0 5px 0px;
text-shadow: 0px 1px 0 white;
border-bottom: 1px solid #C8D5E3;
}
div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 150%; background-color: #C8D5E3; }
div.body h3 { font-size: 120%; background-color: #D8DEE3; }
div.body h4 { font-size: 110%; background-color: #D8DEE3; }
div.body h5 { font-size: 100%; background-color: #D8DEE3; }
div.body h6 { font-size: 100%; background-color: #D8DEE3; }
div.body h1 { margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 150%; }
div.body h3 { font-size: 120%; }
div.body h4 { font-size: 110%; }
div.body h5 { font-size: 100%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: #c60f0f;
@ -170,7 +173,7 @@ a.headerlink:hover {
}
div.body p, div.body dd, div.body li {
line-height: 1.5em;
line-height: 1.8em;
}
div.admonition p.admonition-title + p {
@ -182,22 +185,23 @@ div.highlight{
}
div.note {
background-color: #eee;
border: 1px solid #ccc;
background-color: #eeeeee;
border: 1px solid #cccccc;
}
div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
background-color: #ffffcc;
border: 1px solid #ffff66;
}
div.topic {
background-color: #eee;
background-color: #fafafa;
border-width: 0;
}
div.warning {
background-color: #ffe4e4;
border: 1px solid #f66;
border: 1px solid #ff6666;
}
p.admonition-title {
@ -210,20 +214,23 @@ p.admonition-title:after {
pre {
padding: 10px;
background-color: White;
color: #222;
line-height: 1.2em;
border: 1px solid #C6C9CB;
font-size: 1.2em;
background-color: #fafafa;
color: #222222;
line-height: 1.5em;
font-size: 1.1em;
margin: 1.5em 0 1.5em 0;
-webkit-box-shadow: 1px 1px 1px #d8d8d8;
-moz-box-shadow: 1px 1px 1px #d8d8d8;
-webkit-box-shadow: 0px 0px 4px #d8d8d8;
-moz-box-shadow: 0px 0px 4px #d8d8d8;
box-shadow: 0px 0px 4px #d8d8d8;
}
tt {
background-color: #ecf0f3;
color: #222;
color: #222222;
padding: 1px 2px;
font-size: 1.2em;
font-family: monospace;
}
#table-of-contents ul {
padding-left: 2em;
}

298
docs/api/backends.rst Normal file
View File

@ -0,0 +1,298 @@
*************************************
:mod:`mopidy.backends` -- Backend API
*************************************
.. warning::
This is our *planned* backend API, and not the current API.
.. module:: 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 at positions in [``start``, ``end``] to
``to_position``.
:param start: position of first track to move
:type start: int
:param end: position of 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 playlist, optionally a part of the playlist given by
``start`` and ``end``.
:param start: position of first track to shuffle
:type start: int or :class:`None`
:param end: position of 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(id=None, position=None)
Play either the track with the given ID, the given position, or the
currently active track.
:param id: ID of track to play
:type id: int
:param position: position in current playlist of track to play
:type position: int
.. 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`

8
docs/api/models.rst Normal file
View File

@ -0,0 +1,8 @@
*********************************************
:mod:`mopidy.models` -- Immutable data models
*********************************************
.. automodule:: mopidy.models
:synopsis: Immutable data models.
:members:
:undoc-members:

View File

@ -16,13 +16,13 @@ import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.'))
sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/../'))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@ -147,7 +147,7 @@ html_static_path = ['_static']
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
html_show_sourcelink = False
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the

View File

@ -6,6 +6,15 @@ Development of Mopidy is coordinated through the IRC channel ``#mopidy`` at
``irc.freenode.net`` and through `GitHub <http://github.com/>`_.
API documentation
=================
.. toctree::
:glob:
api/*
Scope
=====

View File

@ -14,5 +14,6 @@ Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -123,10 +123,6 @@ class BaseBackend(object):
return self.state
def status_time(self):
# XXX This is only called when a client is connected, and is thus not a
# complete solution
if self._play_time_elapsed >= self.status_time_total() > 0:
self.end_of_track()
return u'%s:%s' % (self._play_time_elapsed, self.status_time_total())
def status_time_total(self):
@ -208,13 +204,13 @@ class BaseBackend(object):
# Current/single playlist methods
def playlist_changes_since(self, version):
return None
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

View File

@ -120,4 +120,5 @@ class DespotifyBackend(BaseBackend):
def search(self, type, what):
query = u'%s:%s' % (type, what)
result = self.spotify.search(query.encode(ENCODING))
return self._to_mopidy_playlist(result.playlist).mpd_format()
if result is not None:
return self._to_mopidy_playlist(result.playlist).mpd_format()

View File

@ -1,20 +1,40 @@
from copy import copy
class Artist(object):
"""
:param uri: artist URI
:type uri: string
:param name: artist name
:type name: string
"""
def __init__(self, uri=None, name=None):
self._uri = None
self._name = name
@property
def uri(self):
"""The artist URI. Read-only."""
return self._uri
@property
def name(self):
"""The artist name. Read-only."""
return self._name
class Album(object):
"""
:param uri: album URI
:type uri: string
:param name: album name
:type name: string
:param artists: album artists
:type artists: list of :class:`Artist`
:param num_tracks: number of tracks in album
:type num_tracks: integer
"""
def __init__(self, uri=None, name=None, artists=None, num_tracks=0):
self._uri = uri
self._name = name
@ -23,24 +43,49 @@ class Album(object):
@property
def uri(self):
"""The album URI. Read-only."""
return self._uri
@property
def name(self):
"""The album name. Read-only."""
return self._name
@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):
"""
:param uri: track URI
:type uri: string
:param title: track title
:type title: string
:param artists: track artists
:type artists: list of :class:`Artist`
:param album: track album
:type album: :class:`Album`
:param track_no: track number in album
:type track_no: integer
:param date: track release date
:type date: :class:`datetime.date`
:param length: track length in milliseconds
:type length: integer
:param bitrate: bitrate in kbit/s
:type bitrate: integer
:param id: track ID (unique and non-changing as long as the process lives)
:type id: integer
"""
def __init__(self, uri=None, title=None, artists=None, album=None,
track_no=0, date=None, length=None, id=None):
track_no=0, date=None, length=None, bitrate=None, id=None):
self._uri = uri
self._title = title
self._artists = artists or []
@ -48,41 +93,62 @@ class Track(object):
self._track_no = track_no
self._date = date
self._length = length
self._bitrate = bitrate
self._id = id
@property
def uri(self):
"""The track URI. Read-only."""
return self._uri
@property
def title(self):
"""The track title. Read-only."""
return self._title
@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):
"""
Format track for output to MPD client.
:param position: track's position in playlist
:type position: integer
:rtype: list of two-tuples
"""
return [
('file', self.uri),
('Time', self.length // 1000),
@ -96,10 +162,24 @@ class Track(object):
]
def mpd_format_artists(self):
"""
Format track artists for output to MPD client.
:rtype: string
"""
return u', '.join([a.name for a in self.artists])
class Playlist(object):
"""
: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
"""
def __init__(self, uri=None, name=None, tracks=None):
self._uri = uri
self._name = name
@ -107,21 +187,30 @@ class Playlist(object):
@property
def uri(self):
"""The playlist URI. Read-only."""
return self._uri
@property
def name(self):
"""The playlist name. Read-only."""
return self._name
@property
def tracks(self):
"""List of :class:`Track` elements. Read-only."""
return copy(self._tracks)
@property
def length(self):
"""The number of tracks in the playlist. Read-only."""
return len(self._tracks)
def mpd_format(self, start=0, end=None):
"""
Format playlist for output to MPD client.
:rtype: list of lists of two-tuples
"""
if end is None:
end = self.length
tracks = []

View File

@ -0,0 +1,48 @@
from mopidy.models import Track
class BaseCurrentPlaylistControllerTest(object):
uris = []
backend_class = None
def setUp(self):
self.backend = self.backend_class()
def test_add(self):
playlist = self.backend.current_playlist
for uri in self.uris:
playlist.add(uri)
self.assertEqual(uri, playlist.tracks[-1].uri)
def test_add_at_position(self):
playlist = self.backend.current_playlist
for uri in self.uris:
playlist.add(uri, 0)
self.assertEqual(uri, playlist.tracks[0].uri)
# FIXME test other placements
class BasePlaybackControllerTest(object):
backend_class = None
def setUp(self):
self.backend = self.backend_class()
def test_play(self):
playback = self.backend.playback
self.assertEqual(playback.state, playback.STOPPED)
playback.play()
self.assertEqual(playback.state, playback.PLAYING)
def test_next(self):
playback = self.backend.playback
current_song = playback.playlist_position
playback.next()
self.assertEqual(playback.playlist_position, current_song+1)