diff --git a/README.rst b/README.rst index 766d5354..9bca444e 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Mopidy Mopidy is an `MPD `_ server with a `Spotify `_ 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 diff --git a/docs/_static/.placeholder b/docs/_static/.placeholder new file mode 100644 index 00000000..e69de29b diff --git a/docs/_themes/nature/static/nature.css_t b/docs/_themes/nature/static/nature.css_t index 03b0379d..8762e019 100644 --- a/docs/_themes/nature/static/nature.css_t +++ b/docs/_themes/nature/static/nature.css_t @@ -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; +} diff --git a/docs/api/backends.rst b/docs/api/backends.rst new file mode 100644 index 00000000..365c6d85 --- /dev/null +++ b/docs/api/backends.rst @@ -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` diff --git a/docs/api/models.rst b/docs/api/models.rst new file mode 100644 index 00000000..75f9ab02 --- /dev/null +++ b/docs/api/models.rst @@ -0,0 +1,8 @@ +********************************************* +:mod:`mopidy.models` -- Immutable data models +********************************************* + +.. automodule:: mopidy.models + :synopsis: Immutable data models. + :members: + :undoc-members: diff --git a/docs/conf.py b/docs/conf.py index f0d29003..8cb63290 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 tag referring to it. The value of this option must be the diff --git a/docs/development.rst b/docs/development.rst index 68239393..959dcd58 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -6,6 +6,15 @@ Development of Mopidy is coordinated through the IRC channel ``#mopidy`` at ``irc.freenode.net`` and through `GitHub `_. +API documentation +================= + +.. toctree:: + :glob: + + api/* + + Scope ===== diff --git a/docs/index.rst b/docs/index.rst index 08a9dacd..7c618dc3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,5 +14,6 @@ Indices and tables ================== * :ref:`genindex` +* :ref:`modindex` * :ref:`search` diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py index b7d761ff..af3e1b23 100644 --- a/mopidy/backends/__init__.py +++ b/mopidy/backends/__init__.py @@ -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): @@ -205,13 +201,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 diff --git a/mopidy/backends/despotify.py b/mopidy/backends/despotify.py index 646228c8..a60e2aac 100644 --- a/mopidy/backends/despotify.py +++ b/mopidy/backends/despotify.py @@ -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() diff --git a/mopidy/models.py b/mopidy/models.py index bd47c1ef..39212c8e 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -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 = []