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

This commit is contained in:
Thomas Adamcik 2010-02-07 03:52:26 +01:00
commit 7cb3e8e7da
11 changed files with 479 additions and 70 deletions

View File

@ -4,7 +4,7 @@ Mopidy
Mopidy is an `MPD <http://mpd.wikia.com/>`_ server with a Mopidy is an `MPD <http://mpd.wikia.com/>`_ server with a
`Spotify <http://www.spotify.com/>`_ backend. Using a standard MPD client you `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. play music from Spotify.
Mopidy is currently under development. Unless you want to contribute to the 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 { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
font-size: 100%; font-size: 100%;
background-color: #111; background-color: #111111;
color: #555; color: #555555;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
@ -22,7 +22,7 @@ div.documentwrapper {
} }
div.bodywrapper { div.bodywrapper {
margin: 0 0 0 230px; margin: 0 0 0 300px;
} }
hr{ hr{
@ -30,14 +30,14 @@ hr{
} }
div.document { div.document {
background-color: #eee; background-color: #fafafa;
} }
div.body { div.body {
background-color: #ffffff; background-color: #ffffff;
color: #3E4349; color: #3E4349;
padding: 0 30px 30px 30px; padding: 1em 30px 30px 30px;
font-size: 0.8em; font-size: 0.9em;
} }
div.footer { div.footer {
@ -49,25 +49,29 @@ div.footer {
} }
div.footer a { div.footer a {
color: #444; color: #444444;
text-decoration: underline;
} }
div.related { div.related {
background-color: #6BA81E; background-color: #6BA81E;
line-height: 32px; line-height: 36px;
color: #fff; color: #ffffff;
text-shadow: 0px 1px 0 #444; text-shadow: 0px 1px 0 #444444;
font-size: 0.80em; font-size: 1.1em;
} }
div.related a { div.related a {
color: #E2F3CC; color: #E2F3CC;
} }
div.related .right {
font-size: 0.9em;
}
div.sphinxsidebar { div.sphinxsidebar {
font-size: 0.75em; font-size: 0.9em;
line-height: 1.5em; line-height: 1.5em;
width: 300px
} }
div.sphinxsidebarwrapper{ div.sphinxsidebarwrapper{
@ -77,46 +81,46 @@ div.sphinxsidebarwrapper{
div.sphinxsidebar h3, div.sphinxsidebar h3,
div.sphinxsidebar h4 { div.sphinxsidebar h4 {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
color: #222; color: #222222;
font-size: 1.2em; font-size: 1.2em;
font-weight: normal; font-weight: bold;
margin: 0; margin: 0;
padding: 5px 10px; padding: 5px 10px;
background-color: #ddd;
text-shadow: 1px 1px 0 white text-shadow: 1px 1px 0 white
} }
div.sphinxsidebar h4{
font-size: 1.1em;
}
div.sphinxsidebar h3 a { div.sphinxsidebar h3 a {
color: #444; color: #444444;
} }
div.sphinxsidebar p { div.sphinxsidebar p {
color: #888; color: #888888;
padding: 5px 20px; padding: 5px 20px;
margin: 0.5em 0px;
} }
div.sphinxsidebar p.topless { div.sphinxsidebar p.topless {
} }
div.sphinxsidebar ul { div.sphinxsidebar ul {
margin: 10px 20px; margin: 10px 10px 10px 20px;
padding: 0; padding: 0;
color: #000; color: #000000;
} }
div.sphinxsidebar a { div.sphinxsidebar a {
color: #444; color: #444444;
} }
div.sphinxsidebar a:hover {
color: #E32E00;
}
div.sphinxsidebar input { div.sphinxsidebar input {
border: 1px solid #ccc; border: 1px solid #cccccc;
font-family: sans-serif; font-family: sans-serif;
font-size: 1em; font-size: 1.1em;
padding: 0.15em 0.3em;
} }
div.sphinxsidebar input[type=text]{ div.sphinxsidebar input[type=text]{
@ -132,7 +136,6 @@ a {
a:hover { a:hover {
color: #E32E00; color: #E32E00;
text-decoration: underline;
} }
div.body h1, div.body h1,
@ -142,20 +145,20 @@ div.body h4,
div.body h5, div.body h5,
div.body h6 { div.body h6 {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
background-color: #BED4EB;
font-weight: normal; font-weight: normal;
color: #212224; color: #212224;
margin: 30px 0px 10px 0px; margin: 30px 0px 10px 0px;
padding: 5px 0 5px 10px; padding: 5px 0 5px 0px;
text-shadow: 0px 1px 0 white 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 h1 { margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 150%; background-color: #C8D5E3; } div.body h2 { font-size: 150%; }
div.body h3 { font-size: 120%; background-color: #D8DEE3; } div.body h3 { font-size: 120%; }
div.body h4 { font-size: 110%; background-color: #D8DEE3; } div.body h4 { font-size: 110%; }
div.body h5 { font-size: 100%; background-color: #D8DEE3; } div.body h5 { font-size: 100%; }
div.body h6 { font-size: 100%; background-color: #D8DEE3; } div.body h6 { font-size: 100%; }
a.headerlink { a.headerlink {
color: #c60f0f; color: #c60f0f;
@ -170,7 +173,7 @@ a.headerlink:hover {
} }
div.body p, div.body dd, div.body li { div.body p, div.body dd, div.body li {
line-height: 1.5em; line-height: 1.8em;
} }
div.admonition p.admonition-title + p { div.admonition p.admonition-title + p {
@ -182,22 +185,23 @@ div.highlight{
} }
div.note { div.note {
background-color: #eee; background-color: #eeeeee;
border: 1px solid #ccc; border: 1px solid #cccccc;
} }
div.seealso { div.seealso {
background-color: #ffc; background-color: #ffffcc;
border: 1px solid #ff6; border: 1px solid #ffff66;
} }
div.topic { div.topic {
background-color: #eee; background-color: #fafafa;
border-width: 0;
} }
div.warning { div.warning {
background-color: #ffe4e4; background-color: #ffe4e4;
border: 1px solid #f66; border: 1px solid #ff6666;
} }
p.admonition-title { p.admonition-title {
@ -210,20 +214,23 @@ p.admonition-title:after {
pre { pre {
padding: 10px; padding: 10px;
background-color: White; background-color: #fafafa;
color: #222; color: #222222;
line-height: 1.2em; line-height: 1.5em;
border: 1px solid #C6C9CB; font-size: 1.1em;
font-size: 1.2em;
margin: 1.5em 0 1.5em 0; margin: 1.5em 0 1.5em 0;
-webkit-box-shadow: 1px 1px 1px #d8d8d8; -webkit-box-shadow: 0px 0px 4px #d8d8d8;
-moz-box-shadow: 1px 1px 1px #d8d8d8; -moz-box-shadow: 0px 0px 4px #d8d8d8;
box-shadow: 0px 0px 4px #d8d8d8;
} }
tt { tt {
background-color: #ecf0f3; color: #222222;
color: #222;
padding: 1px 2px; padding: 1px 2px;
font-size: 1.2em; font-size: 1.2em;
font-family: monospace; 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(position)
Remove the track at ``position`` from the current playlist.
:param position: position of track to remove
:type position: int
.. 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, # 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 # 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. # 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 ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # 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. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@ -147,7 +147,7 @@ html_static_path = ['_static']
#html_split_index = False #html_split_index = False
# If true, links to the reST sources are added to the pages. # 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 # 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 # 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/>`_. ``irc.freenode.net`` and through `GitHub <http://github.com/>`_.
API documentation
=================
.. toctree::
:glob:
api/*
Scope Scope
===== =====

View File

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

View File

@ -123,10 +123,6 @@ class BaseBackend(object):
return self.state return self.state
def status_time(self): 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()) return u'%s:%s' % (self._play_time_elapsed, self.status_time_total())
def status_time_total(self): def status_time_total(self):
@ -205,13 +201,13 @@ class BaseBackend(object):
# Current/single playlist methods # Current/single playlist methods
def playlist_changes_since(self, version):
return None
def playlist_load(self, name): def playlist_load(self, name):
self._current_song_pos = None
matches = filter(lambda p: p.name == name, self._playlists) matches = filter(lambda p: p.name == name, self._playlists)
if matches: if matches:
self._current_playlist = matches[0] self._current_playlist = matches[0]
if self.state == self.PLAY:
self.play(songpos=0)
else: else:
self._current_playlist = None self._current_playlist = None

View File

@ -120,4 +120,5 @@ class DespotifyBackend(BaseBackend):
def search(self, type, what): def search(self, type, what):
query = u'%s:%s' % (type, what) query = u'%s:%s' % (type, what)
result = self.spotify.search(query.encode(ENCODING)) 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 from copy import copy
class Artist(object): class Artist(object):
"""
:param uri: artist URI
:type uri: string
:param name: artist name
:type name: string
"""
def __init__(self, uri=None, name=None): def __init__(self, uri=None, name=None):
self._uri = None self._uri = None
self._name = name self._name = name
@property @property
def uri(self): def uri(self):
"""The artist URI. Read-only."""
return self._uri return self._uri
@property @property
def name(self): def name(self):
"""The artist name. Read-only."""
return self._name return self._name
class Album(object): 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): def __init__(self, uri=None, name=None, artists=None, num_tracks=0):
self._uri = uri self._uri = uri
self._name = name self._name = name
@ -23,24 +43,49 @@ class Album(object):
@property @property
def uri(self): def uri(self):
"""The album URI. Read-only."""
return self._uri return self._uri
@property @property
def name(self): def name(self):
"""The album name. Read-only."""
return self._name return self._name
@property @property
def artists(self): def artists(self):
"""List of :class:`Artist` elements. Read-only."""
return copy(self._artists) return copy(self._artists)
@property @property
def num_tracks(self): def num_tracks(self):
"""The number of tracks in the album. Read-only."""
return self._num_tracks return self._num_tracks
class Track(object): 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, 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._uri = uri
self._title = title self._title = title
self._artists = artists or [] self._artists = artists or []
@ -48,41 +93,62 @@ class Track(object):
self._track_no = track_no self._track_no = track_no
self._date = date self._date = date
self._length = length self._length = length
self._bitrate = bitrate
self._id = id self._id = id
@property @property
def uri(self): def uri(self):
"""The track URI. Read-only."""
return self._uri return self._uri
@property @property
def title(self): def title(self):
"""The track title. Read-only."""
return self._title return self._title
@property @property
def artists(self): def artists(self):
"""List of :class:`Artist`. Read-only."""
return copy(self._artists) return copy(self._artists)
@property @property
def album(self): def album(self):
"""The track :class:`Album`. Read-only."""
return self._album return self._album
@property @property
def track_no(self): def track_no(self):
"""The track number in album. Read-only."""
return self._track_no return self._track_no
@property @property
def date(self): def date(self):
"""The track release date. Read-only."""
return self._date return self._date
@property @property
def length(self): def length(self):
"""The track length in milliseconds. Read-only."""
return self._length return self._length
@property
def bitrate(self):
"""The track's bitrate in kbit/s. Read-only."""
return self._bitrate
@property @property
def id(self): def id(self):
"""The track ID. Read-only."""
return self._id return self._id
def mpd_format(self, position=0): 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 [ return [
('file', self.uri), ('file', self.uri),
('Time', self.length // 1000), ('Time', self.length // 1000),
@ -96,10 +162,24 @@ class Track(object):
] ]
def mpd_format_artists(self): 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]) return u', '.join([a.name for a in self.artists])
class Playlist(object): 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): def __init__(self, uri=None, name=None, tracks=None):
self._uri = uri self._uri = uri
self._name = name self._name = name
@ -107,21 +187,30 @@ class Playlist(object):
@property @property
def uri(self): def uri(self):
"""The playlist URI. Read-only."""
return self._uri return self._uri
@property @property
def name(self): def name(self):
"""The playlist name. Read-only."""
return self._name return self._name
@property @property
def tracks(self): def tracks(self):
"""List of :class:`Track` elements. Read-only."""
return copy(self._tracks) return copy(self._tracks)
@property @property
def length(self): def length(self):
"""The number of tracks in the playlist. Read-only."""
return len(self._tracks) return len(self._tracks)
def mpd_format(self, start=0, end=None): 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: if end is None:
end = self.length end = self.length
tracks = [] tracks = []