diff --git a/MANIFEST.in b/MANIFEST.in
index 33d7dc71..f629bcc7 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,7 @@
-include LICENSE pylintrc *.rst *.txt data/mopidy.desktop
+include LICENSE pylintrc *.rst data/mopidy.desktop
include mopidy/backends/libspotify/spotify_appkey.key
recursive-include docs *
prune docs/_build
+recursive-include requirements *
recursive-include tests *.py
recursive-include tests/data *
diff --git a/README.rst b/README.rst
index c6187119..4f31fb59 100644
--- a/README.rst
+++ b/README.rst
@@ -13,7 +13,7 @@ To install Mopidy, check out
* `Documentation (latest release) `_
* `Documentation (development version) `_
-* `Source code `_
-* `Issue tracker `_
+* `Source code `_
+* `Issue tracker `_
* IRC: ``#mopidy`` at `irc.freenode.net `_
-* `Download development snapshot `_
+* `Download development snapshot `_
diff --git a/bin/mopidy b/bin/mopidy
old mode 100644
new mode 100755
diff --git a/bin/mopidy-scan b/bin/mopidy-scan
index 8534372c..84cfee57 100755
--- a/bin/mopidy-scan
+++ b/bin/mopidy-scan
@@ -17,15 +17,15 @@ if __name__ == '__main__':
def debug(uri, error):
print >> sys.stderr, 'Failed %s: %s' % (uri, error)
- print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_FOLDER
+ print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH
- scanner = Scanner(settings.LOCAL_MUSIC_FOLDER, store, debug)
+ scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
scanner.start()
print >> sys.stderr, 'Done'
for a in tracks_to_tag_cache_format(tracks):
if len(a) == 1:
- print a[0]
+ print (u'%s' % a).encode('utf-8')
else:
- print u': '.join([unicode(b) for b in a]).encode('utf-8')
+ print (u'%s: %s' % a).encode('utf-8')
diff --git a/data/mopidy.desktop b/data/mopidy.desktop
index f5ca43bb..70257d58 100644
--- a/data/mopidy.desktop
+++ b/data/mopidy.desktop
@@ -7,4 +7,4 @@ Icon=audio-x-generic
TryExec=mopidy
Exec=mopidy
Terminal=true
-Categories=AudioVideo;Audio;Player;ConsoleOnly
+Categories=AudioVideo;Audio;Player;ConsoleOnly;
diff --git a/docs/Makefile b/docs/Makefile
index 4ad8691e..6a3272f4 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -4,101 +4,127 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
-PAPER =
+PAPER =
+BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make ' where is one of"
- @echo " html to make standalone HTML files"
- @echo " dirhtml to make HTML files named index.html in directories"
- @echo " pickle to make pickle files"
- @echo " json to make JSON files"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " qthelp to make HTML files and a qthelp project"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " changes to make an overview of all changed/added/deprecated items"
- @echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
- -rm -rf _build/*
+ -rm -rf $(BUILDDIR)/*
html:
- $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
- @echo "Build finished. The HTML pages are in _build/html."
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
- $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
- @echo "Build finished. The HTML pages are in _build/dirhtml."
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
- $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
- $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
- $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in _build/htmlhelp."
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
- $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
- ".qhcp project file in _build/qthelp, like this:"
- @echo "# qcollectiongenerator _build/qthelp/Mopidy.qhcp"
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Mopidy.qhcp"
@echo "To view the help file:"
- @echo "# assistant -collectionFile _build/qthelp/Mopidy.qhc"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Mopidy.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/Mopidy"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Mopidy"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
- @echo "Build finished; the LaTeX files are in _build/latex."
- @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
- "run these through (pdf)latex."
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
- $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
- @echo "The overview file is in _build/changes."
+ @echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
- $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
- "or in _build/linkcheck/output.txt."
+ "or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
- $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
- "results in _build/doctest/output.txt."
-
-public: clean dirhtml
- rm -rf /tmp/mopidy-html && cp -r _build/dirhtml /tmp/mopidy-html
- git stash save
- cd .. && \
- git checkout gh-pages && \
- git pull && \
- rm -r * && \
- cp -r /tmp/mopidy-html/* . && \
- mv _sources sources && \
- (find . -type f | xargs sed -i -e 's/_sources/sources/g') && \
- mv _static static && \
- (find . -type f | xargs sed -i -e 's/_static/static/g') && \
- if [ -d _images ]; then mv _images images; fi && \
- (find . -type f | xargs sed -i -e 's/_images/images/g') && \
- git add *
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/_themes/nature/static/nature.css_t b/docs/_themes/nature/static/nature.css_t
index 63ef80d6..b6c0f22e 100644
--- a/docs/_themes/nature/static/nature.css_t
+++ b/docs/_themes/nature/static/nature.css_t
@@ -214,7 +214,7 @@ p.admonition-title:after {
pre {
padding: 10px;
- background-color: #fafafa;
+ background-color: #eeeeee;
color: #222222;
line-height: 1.5em;
font-size: 1.1em;
diff --git a/docs/api/backends/concepts.rst b/docs/api/backends/concepts.rst
new file mode 100644
index 00000000..0d476213
--- /dev/null
+++ b/docs/api/backends/concepts.rst
@@ -0,0 +1,30 @@
+.. _backend-concepts:
+
+**********************************************
+The backend, controller, and provider concepts
+**********************************************
+
+Backend:
+ The backend is mostly for convenience. It is a container that holds
+ references to all the controllers.
+Controllers:
+ Each controller has responsibility for a given part of the backend
+ functionality. Most, but not all, controllers delegates some work to one or
+ more providers. The controllers are responsible for choosing the right
+ provider for any given task based upon i.e. the track's URI. See
+ :ref:`backend-controller-api` for more details.
+Providers:
+ Anything specific to i.e. Spotify integration or local storage is contained
+ in the providers. To integrate with new music sources, you just add new
+ providers. See :ref:`backend-provider-api` for more details.
+
+.. digraph:: backend_relations
+
+ Backend -> "Current\nplaylist\ncontroller"
+ Backend -> "Library\ncontroller"
+ "Library\ncontroller" -> "Library\nproviders"
+ Backend -> "Playback\ncontroller"
+ "Playback\ncontroller" -> "Playback\nproviders"
+ Backend -> "Stored\nplaylists\ncontroller"
+ "Stored\nplaylists\ncontroller" -> "Stored\nplaylist\nproviders"
+ Backend -> Mixer
diff --git a/docs/api/backends/controllers.rst b/docs/api/backends/controllers.rst
new file mode 100644
index 00000000..28112cf7
--- /dev/null
+++ b/docs/api/backends/controllers.rst
@@ -0,0 +1,65 @@
+.. _backend-controller-api:
+
+**********************
+Backend controller API
+**********************
+
+
+The backend controller API is the interface that is used by frontends like
+:mod:`mopidy.frontends.mpd`. If you want to implement your own backend, see the
+:ref:`backend-provider-api`.
+
+
+The backend
+===========
+
+.. autoclass:: mopidy.backends.base.Backend
+ :members:
+ :undoc-members:
+
+
+Playback controller
+===================
+
+Manages playback, with actions like play, pause, stop, next, previous, and
+seek.
+
+.. autoclass:: mopidy.backends.base.PlaybackController
+ :members:
+ :undoc-members:
+
+
+Mixer controller
+================
+
+Manages volume. See :class:`mopidy.mixers.base.BaseMixer`.
+
+
+Current playlist controller
+===========================
+
+Manages everything related to the currently loaded playlist.
+
+.. autoclass:: mopidy.backends.base.CurrentPlaylistController
+ :members:
+ :undoc-members:
+
+
+Stored playlists controller
+===========================
+
+Manages stored playlist.
+
+.. autoclass:: mopidy.backends.base.StoredPlaylistsController
+ :members:
+ :undoc-members:
+
+
+Library controller
+==================
+
+Manages the music library, e.g. searching for tracks to be added to a playlist.
+
+.. autoclass:: mopidy.backends.base.LibraryController
+ :members:
+ :undoc-members:
diff --git a/docs/api/backends/index.rst b/docs/api/backends/index.rst
deleted file mode 100644
index 100f6f0d..00000000
--- a/docs/api/backends/index.rst
+++ /dev/null
@@ -1,90 +0,0 @@
-**********************
-:mod:`mopidy.backends`
-**********************
-
-.. automodule:: mopidy.backends
- :synopsis: Backend API
-
-
-The backend and its controllers
-===============================
-
-.. graph:: backend_relations
-
- backend -- current_playlist
- backend -- library
- backend -- playback
- backend -- stored_playlists
-
-
-Backend API
-===========
-
-.. note::
-
- Currently this only documents the API that is available for use by
- frontends like :mod:`mopidy.frontends.mpd`, and not what is required to
- implement your own backend. :class:`mopidy.backends.base.BaseBackend` and
- its controllers implements many of these methods in a matter that should be
- independent of most concrete backend implementations, so you should
- generally just implement or override a few of these methods yourself to
- create a new backend with a complete feature set.
-
-.. autoclass:: mopidy.backends.base.BaseBackend
- :members:
- :undoc-members:
-
-
-Playback controller
--------------------
-
-Manages playback, with actions like play, pause, stop, next, previous, and
-seek.
-
-.. autoclass:: mopidy.backends.base.BasePlaybackController
- :members:
- :undoc-members:
-
-
-Mixer controller
-----------------
-
-Manages volume. See :class:`mopidy.mixers.BaseMixer`.
-
-
-Current playlist controller
----------------------------
-
-Manages everything related to the currently loaded playlist.
-
-.. autoclass:: mopidy.backends.base.BaseCurrentPlaylistController
- :members:
- :undoc-members:
-
-
-Stored playlists controller
----------------------------
-
-Manages stored playlist.
-
-.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsController
- :members:
- :undoc-members:
-
-
-Library controller
-------------------
-
-Manages the music library, e.g. searching for tracks to be added to a playlist.
-
-.. autoclass:: mopidy.backends.base.BaseLibraryController
- :members:
- :undoc-members:
-
-
-Backends
-========
-
-* :mod:`mopidy.backends.dummy`
-* :mod:`mopidy.backends.libspotify`
-* :mod:`mopidy.backends.local`
diff --git a/docs/api/backends/libspotify.rst b/docs/api/backends/libspotify.rst
deleted file mode 100644
index e7528757..00000000
--- a/docs/api/backends/libspotify.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-*******************************************************
-:mod:`mopidy.backends.libspotify` -- Libspotify backend
-*******************************************************
-
-.. automodule:: mopidy.backends.libspotify
- :synopsis: Spotify backend using the libspotify library
- :members:
diff --git a/docs/api/backends/providers.rst b/docs/api/backends/providers.rst
new file mode 100644
index 00000000..903e220b
--- /dev/null
+++ b/docs/api/backends/providers.rst
@@ -0,0 +1,41 @@
+.. _backend-provider-api:
+
+********************
+Backend provider API
+********************
+
+The backend provider API is the interface that must be implemented when you
+create a backend. If you are working on a frontend and need to access the
+backend, see the :ref:`backend-controller-api`.
+
+
+Playback provider
+=================
+
+.. autoclass:: mopidy.backends.base.BasePlaybackProvider
+ :members:
+ :undoc-members:
+
+
+Stored playlists provider
+=========================
+
+.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider
+ :members:
+ :undoc-members:
+
+
+Library provider
+================
+
+.. autoclass:: mopidy.backends.base.BaseLibraryProvider
+ :members:
+ :undoc-members:
+
+
+Backend provider implementations
+================================
+
+* :mod:`mopidy.backends.dummy`
+* :mod:`mopidy.backends.spotify`
+* :mod:`mopidy.backends.local`
diff --git a/docs/api/frontends/index.rst b/docs/api/frontends.rst
similarity index 78%
rename from docs/api/frontends/index.rst
rename to docs/api/frontends.rst
index b01bac3d..0c1e32a3 100644
--- a/docs/api/frontends/index.rst
+++ b/docs/api/frontends.rst
@@ -1,6 +1,6 @@
-***********************
-:mod:`mopidy.frontends`
-***********************
+************
+Frontend API
+************
A frontend may do whatever it wants to, including creating threads, opening TCP
ports and exposing Mopidy for a type of clients.
@@ -9,14 +9,6 @@ Frontends got one main limitation: they are restricted to passing messages
through the ``core_queue`` for all communication with the rest of Mopidy. Thus,
the frontend API is very small and reveals little of what a frontend may do.
-.. automodule:: mopidy.frontends
- :synopsis: Frontend API
- :members:
-
-
-Frontend API
-============
-
.. warning::
A stable frontend API is not available yet, as we've only implemented a
@@ -27,8 +19,8 @@ Frontend API
:members:
-Frontends
-=========
+Frontend implementations
+========================
* :mod:`mopidy.frontends.lastfm`
* :mod:`mopidy.frontends.mpd`
diff --git a/docs/api/frontends/lastfm.rst b/docs/api/frontends/lastfm.rst
deleted file mode 100644
index bd3e218e..00000000
--- a/docs/api/frontends/lastfm.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-******************************
-:mod:`mopidy.frontends.lastfm`
-******************************
-
-.. automodule:: mopidy.frontends.lastfm
- :synopsis: Last.fm scrobbler frontend
- :members:
diff --git a/docs/api/index.rst b/docs/api/index.rst
index 86f4e06e..1f37e9ff 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -1,8 +1,11 @@
-*****************
-API documentation
-*****************
+*************
+API reference
+*************
.. toctree::
:glob:
- **
+ backends/concepts
+ backends/controllers
+ backends/providers
+ *
diff --git a/docs/api/mixers.rst b/docs/api/mixers.rst
index edaea306..6daa7a4e 100644
--- a/docs/api/mixers.rst
+++ b/docs/api/mixers.rst
@@ -1,6 +1,6 @@
-********************
-:mod:`mopidy.mixers`
-********************
+*********
+Mixer API
+*********
Mixers are responsible for controlling volume. Clients of the mixers will
simply instantiate a mixer and read/write to the ``volume`` attribute::
@@ -24,74 +24,21 @@ enable one of the hardware device mixers, you must the set
:attr:`mopidy.settings.MIXER` setting to point to one of the classes found
below, and possibly add some extra settings required by the mixer you choose.
-
-Mixer API
-=========
-
-All mixers should subclass :class:`mopidy.mixers.BaseMixer` and override
+All mixers should subclass :class:`mopidy.mixers.base.BaseMixer` and override
methods as described below.
-.. automodule:: mopidy.mixers
+.. automodule:: mopidy.mixers.base
:synopsis: Mixer API
:members:
:undoc-members:
-:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux
-=================================================
+Mixer implementations
+=====================
-.. inheritance-diagram:: mopidy.mixers.alsa
-
-.. automodule:: mopidy.mixers.alsa
- :synopsis: ALSA mixer for Linux
- :members:
-
-
-:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers
-=================================================================
-
-.. inheritance-diagram:: mopidy.mixers.denon
-
-.. automodule:: mopidy.mixers.denon
- :synopsis: Hardware mixer for Denon amplifiers
- :members:
-
-
-:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing
-=====================================================
-
-.. inheritance-diagram:: mopidy.mixers.dummy
-
-.. automodule:: mopidy.mixers.dummy
- :synopsis: Dummy mixer for testing
- :members:
-
-
-:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms
-===========================================================================
-
-.. inheritance-diagram:: mopidy.mixers.gstreamer_software
-
-.. automodule:: mopidy.mixers.gstreamer_software
- :synopsis: Software mixer for all platforms
- :members:
-
-
-:mod:`mopidy.mixers.osa` -- Osa mixer for OS X
-==============================================
-
-.. inheritance-diagram:: mopidy.mixers.osa
-
-.. automodule:: mopidy.mixers.osa
- :synopsis: Osa mixer for OS X
- :members:
-
-
-:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers
-=============================================================
-
-.. inheritance-diagram:: mopidy.mixers.nad
-
-.. automodule:: mopidy.mixers.nad
- :synopsis: Hardware mixer for NAD amplifiers
- :members:
+* :mod:`mopidy.mixers.alsa`
+* :mod:`mopidy.mixers.denon`
+* :mod:`mopidy.mixers.dummy`
+* :mod:`mopidy.mixers.gstreamer_software`
+* :mod:`mopidy.mixers.osa`
+* :mod:`mopidy.mixers.nad`
diff --git a/docs/api/models.rst b/docs/api/models.rst
index 62e6f75a..ef11547e 100644
--- a/docs/api/models.rst
+++ b/docs/api/models.rst
@@ -1,6 +1,6 @@
-********************
-:mod:`mopidy.models`
-********************
+***********
+Data models
+***********
These immutable data models are used for all data transfer within the Mopidy
backends and between the backends and the MPD frontend. All fields are optional
diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst
index 8f4e33c0..5ef1606d 100644
--- a/docs/api/outputs.rst
+++ b/docs/api/outputs.rst
@@ -1,22 +1,20 @@
-*********************
-:mod:`mopidy.outputs`
-*********************
+**********
+Output API
+**********
Outputs are responsible for playing audio.
+.. warning::
-Output API
-==========
+ A stable output API is not available yet, as we've only implemented a
+ single output module.
-A stable output API is not available yet, as we've only implemented a single
-output module.
-
-
-:mod:`mopidy.outputs.gstreamer` -- GStreamer output for all platforms
-=====================================================================
-
-.. inheritance-diagram:: mopidy.outputs.gstreamer
-
-.. automodule:: mopidy.outputs.gstreamer
- :synopsis: GStreamer output for all platforms
+.. automodule:: mopidy.outputs.base
+ :synopsis: Base class for outputs
:members:
+
+
+Output implementations
+======================
+
+* :mod:`mopidy.outputs.gstreamer`
diff --git a/docs/api/settings.rst b/docs/api/settings.rst
deleted file mode 100644
index cfc270d6..00000000
--- a/docs/api/settings.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-**********************
-:mod:`mopidy.settings`
-**********************
-
-
-Changing settings
-=================
-
-For any Mopidy installation you will need to change at least a couple of
-settings. To do this, create a new file in the ``~/.mopidy/`` directory
-named ``settings.py`` and add settings you need to change from their defaults
-there.
-
-A complete ``~/.mopidy/settings.py`` may look like this::
-
- MPD_SERVER_HOSTNAME = u'::'
- SPOTIFY_USERNAME = u'alice'
- SPOTIFY_PASSWORD = u'mysecret'
-
-
-Available settings
-==================
-
-.. automodule:: mopidy.settings
- :synopsis: Available settings and their default values
- :members:
- :undoc-members:
diff --git a/docs/authors.rst b/docs/authors.rst
index f56242a5..01e810e4 100644
--- a/docs/authors.rst
+++ b/docs/authors.rst
@@ -10,13 +10,20 @@ Contributors to Mopidy in the order of appearance:
- Kristian Klette
-Donations
-=========
+Showing your appreciation
+=========================
If you already enjoy Mopidy, or don't enjoy it and want to help us making
-Mopidy better, you can `donate money `_ to
-Mopidy's development.
+Mopidy better, the best way to do so is to contribute back to the community.
+You can contribute code, documentation, tests, bug reports, or help other
+users, spreading the word, etc.
+
+If you want to show your appreciation in a less time consuming way, you can
+`flattr us `_, or `donate money
+`_ to Mopidy's development.
+
+We promise that any money donated -- to Pledgie, not Flattr, due to the size of
+the amounts -- will be used to cover costs related to Mopidy development, like
+service subscriptions (Spotify, Last.fm, etc.) and hardware devices like an
+used iPod Touch for testing Mopidy with MPod.
-Any donated money will be used to cover service subscriptions (e.g. Spotify
-and Last.fm) and hardware devices (e.g. an used iPod Touch for testing Mopidy
-with MPod) needed for developing Mopidy.
diff --git a/docs/changes.rst b/docs/changes.rst
index c3df7d85..ddb46bb8 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -10,12 +10,110 @@ This change log is used to track all major changes to Mopidy.
No description yet.
+**Important changes**
+
+- Spotify backend:
+
+ - If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and
+ the latest pyspotify from the Mopidy developers. Follow the instructions at
+ :doc:`/installation/libspotify/`.
+
+ - Support high bitrate (320k) audio. See
+ :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details.
+
+ - Rename :mod:`mopidy.backends.libspotify` to :mod:`mopidy.backends.spotify`.
+ If you have set :attr:`mopidy.settings.BACKENDS` explicitly, you may need
+ to update the setting's value.
+
+ - Catch and log error caused by playlist folder boundaries being threated as
+ normal playlists. More permanent fix requires support for checking playlist
+ types in pyspotify.
+
+- Last.fm frontend:
+
+ - If you use the Last.fm frontend, you need to upgrade to pylast 0.5.
+
+ - Update to use Last.fm's new Scrobbling 2.0 API, as the old Submissions
+ Protocol 1.2.1 is deprecated. (Fixes: :issue:`33`)
+
+
**Changes**
-- Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome
- application menus.
-- Add :command:`mopidy-scan` command to generate ``tag_cache`` files without
- any help from the original MPD server.
+- Settings:
+
+ - Automatically expand ``~`` to the user's home directory and make the path
+ absolute for settings with names ending in ``_PATH`` or ``_FILE``.
+ - Rename the following settings. The settings validator will warn you if you
+ need to change your local settings.
+
+ - ``LOCAL_MUSIC_FOLDER`` to :attr:`mopidy.settings.LOCAL_MUSIC_PATH`
+ - ``LOCAL_PLAYLIST_FOLDER`` to
+ :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH`
+ - ``LOCAL_TAG_CACHE`` to :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE`
+ - ``SPOTIFY_LIB_CACHE`` to :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`
+
+- Packaging and distribution:
+
+ - Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome
+ application menus.
+ - Create infrastructure for creating Debian packages of Mopidy.
+
+- MPD frontend:
+
+ - Support ``setvol 50`` without quotes around the argument. Fixes volume
+ control in Droid MPD.
+ - Support ``seek 1 120`` without quotes around the arguments. Fixes seek in
+ Droid MPD.
+
+- Local backend:
+
+ - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without
+ any help from the original MPD server.
+ - Support UTF-8 encoded tag caches with non-ASCII characters.
+
+- Models:
+
+ - Rename and generalize ``Playlist._with(**kwargs)`` to
+ :meth:`mopidy.models.ImmutableObject.copy`.
+ - Add ``musicbrainz_id`` field to :class:`mopidy.models.Artist`,
+ :class:`mopidy.models.Album`, and :class:`mopidy.models.Track`.
+
+- Introduce the :ref:`provider concept `. Split the backend
+ API into a :ref:`backend controller API ` (for
+ frontend use) and a :ref:`backend provider API ` (for
+ backend implementation use), which includes the following changes:
+
+ - Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`.
+ - Rename ``BaseCurrentPlaylistController`` to
+ :class:`mopidy.backends.base.CurrentPlaylistController`.
+ - Split ``BaseLibraryController`` to
+ :class:`mopidy.backends.base.LibraryController` and
+ :class:`mopidy.backends.base.BaseLibraryProvider`.
+ - Split ``BasePlaybackController`` to
+ :class:`mopidy.backends.base.PlaybackController` and
+ :class:`mopidy.backends.base.BasePlaybackProvider`.
+ - Split ``BaseStoredPlaylistsController`` to
+ :class:`mopidy.backends.base.StoredPlaylistsController` and
+ :class:`mopidy.backends.base.BaseStoredPlaylistsProvider`.
+
+- Other API and package structure cleaning:
+
+ - Move ``BaseMixer`` to :class:`mopidy.mixers.base.BaseMixer`.
+ - Add docs for the current non-stable output API,
+ :class:`mopidy.outputs.base.BaseOutput`.
+
+
+0.2.1 (2011-01-07)
+==================
+
+This is a maintenance release without any new features.
+
+**Bugfixes**
+
+- Fix crash in :mod:`mopidy.frontends.lastfm` which occurred at playback if
+ either :mod:`pylast` was not installed or the Last.fm scrobbling was not
+ correctly configured. The scrobbling thread now shuts properly down at
+ failure.
0.2.0 (2010-10-24)
@@ -364,7 +462,7 @@ Mopidy is working and usable. 0.1.0a0 is an alpha release, which basicly means
we will still change APIs, add features, etc. before the final 0.1.0 release.
But the software is usable as is, so we release it. Please give it a try and
give us feedback, either at our IRC channel or through the `issue tracker
-`_. Thanks!
+`_. Thanks!
**Changes**
diff --git a/docs/conf.py b/docs/conf.py
index d0d8f3af..9e7ff1fb 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -16,8 +16,8 @@ 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(os.path.dirname(__file__)))
-sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/../'))
+sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
+sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../'))
import mopidy
@@ -202,4 +202,4 @@ latex_documents = [
needs_sphinx = '1.0'
-extlinks = {'issue': ('http://github.com/jodal/mopidy/issues#issue/%s', 'GH-')}
+extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues#issue/%s', 'GH-')}
diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst
index 4adde637..a9cd8dc3 100644
--- a/docs/development/contributing.rst
+++ b/docs/development/contributing.rst
@@ -137,7 +137,7 @@ Then, to generate docs::
.. note::
The documentation at http://www.mopidy.com/ is automatically updated when a
- documentation update is pushed to ``jodal/mopidy`` at GitHub.
+ documentation update is pushed to ``mopidy/mopidy`` at GitHub.
Documentation generated from the ``master`` branch is published at
http://www.mopidy.com/docs/master/, and will always be valid for the latest
diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst
index 645cbd30..9db74a4d 100644
--- a/docs/development/roadmap.rst
+++ b/docs/development/roadmap.rst
@@ -14,26 +14,28 @@ release.
Possible targets for the next version
=====================================
-- Reintroduce support for OS X. See :issue:`14` for details.
-- Support for using multiple Mopidy backends simultaneously. Should make it
- possible to have both Spotify tracks and local tracks in the same playlist.
+- Reintroduce support for OS X. See :issue:`25` for details.
+- **[WIP: feature/multi-backend]** Support for using multiple Mopidy backends
+ simultaneously. Should make it possible to have both Spotify tracks and local
+ tracks in the same playlist.
- MPD frontend:
+ - **[WIP: feature/mpd-password]** Password authentication.
- ``idle`` support.
- Spotify backend:
- Write-support for Spotify, i.e. playlist management.
- Virtual directories with e.g. starred tracks from Spotify.
- - Support for 320 kbps audio.
+ - **[DONE: v0.3]** Support for 320 kbps audio.
- Local backend:
- - Better library support.
- - A script for creating a tag cache.
+ - Better music library support.
+ - **[DONE: v0.3]** A script for creating a tag cache.
- An alternative to tag cache for caching metadata, i.e. Sqlite.
-- **[DONE]** Last.fm scrobbling.
+- **[DONE: v0.2]** Last.fm scrobbling.
Stuff we want to do, but not right now, and maybe never
@@ -41,18 +43,19 @@ Stuff we want to do, but not right now, and maybe never
- Packaging and distribution:
- - **[PENDING]** Create `Homebrew `_
+ - **[BLOCKED]** Create `Homebrew `_
recipies for all our dependencies and Mopidy itself to make OS X
installation a breeze. See `Homebrew's issue #1612
`_.
- - Create `Debian packages `_ of all
- our dependencies and Mopidy itself (hosted in our own Debian repo until we
- get stuff into the various distros) to make Debian/Ubuntu installation a
- breeze.
+ - **[DONE]** Create `Debian packages
+ `_ of all our dependencies and
+ Mopidy itself (hosted in our own Debian repo until we get stuff into the
+ various distros) to make Debian/Ubuntu installation a breeze.
- Compatability:
- - Run frontend tests against a real MPD server to ensure we are in sync.
+ - **[WIP: feature/blackbox-testing]** Run frontend tests against a real MPD
+ server to ensure we are in sync.
- Backends:
@@ -64,9 +67,10 @@ Stuff we want to do, but not right now, and maybe never
- Publish the server's presence to the network using `Zeroconf
`_/Avahi.
- - D-Bus/`MPRIS `_
- - REST/JSON web service with a jQuery client as example application. Maybe
- based upon `Tornado `_ and `jQuery
+ - **[WIP: feature/mpris-frontend]** D-Bus/`MPRIS `_
+ - **[WIP: feature/http-frontend]** REST/JSON web service with a jQuery client
+ as example application. Maybe based upon `Tornado
+ `_ and `jQuery
Mobile `_.
- DNLA/UPnP so Mopidy can be controlled from i.e. TVs.
- `XMMS2 `_
diff --git a/docs/index.rst b/docs/index.rst
index f53373dc..09029a4f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -21,6 +21,7 @@ Reference documentation
:maxdepth: 3
api/index
+ modules/index
Development documentation
=========================
diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst
index ef66c673..72d55908 100644
--- a/docs/installation/gstreamer.rst
+++ b/docs/installation/gstreamer.rst
@@ -5,23 +5,32 @@ GStreamer installation
To use the Mopidy, you first need to install GStreamer and its Python bindings.
-Installing GStreamer on Linux
-=============================
+Installing GStreamer
+====================
-GStreamer is packaged for most popular Linux distributions. If you use
-Debian/Ubuntu you can install GStreamer with Aptitude::
+On Linux
+--------
- sudo aptitude install python-gst0.10 gstreamer0.10-plugins-good \
+GStreamer is packaged for most popular Linux distributions. Search for
+GStreamer in your package manager, and make sure to install the Python
+bindings, and the "good" and "ugly" plugin sets.
+
+If you use Debian/Ubuntu you can install GStreamer like this::
+
+ sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
gstreamer0.10-plugins-ugly
+If you install Mopidy from our APT archive, you don't need to install GStreamer
+yourself. The Mopidy Debian package will handle it for you.
-Installing GStreamer on OS X
-============================
+
+On OS X from Homebrew
+---------------------
.. note::
We have created GStreamer formulas for Homebrew to make the GStreamer
- installation easy for you, but our formulas has not been merged into
+ installation easy for you, but not all our formulas have been merged into
Homebrew's master branch yet. You should either fetch the formula files
from `Homebrew's issue #1612
`_ yourself, or fall
@@ -31,6 +40,10 @@ To install GStreamer on OS X using Homebrew::
brew install gst-python gst-plugins-good gst-plugins-ugly
+
+On OS X from MacPorts
+---------------------
+
To install GStreamer on OS X using MacPorts::
sudo port install py26-gst-python gstreamer-plugins-good \
@@ -46,3 +59,19 @@ you should see a long listing of installed plugins, ending in a summary line::
$ gst-inspect-0.10
... long list of installed plugins ...
Total count: 218 plugins (1 blacklist entry not shown), 1031 features
+
+You should be able to produce a audible tone by running::
+
+ gst-launch-0.10 audiotestsrc ! autoaudiosink
+
+If you cannot hear any sound when running this command, you won't hear any
+sound from Mopidy either, as Mopidy uses GStreamer's ``autoaudiosink`` to play
+audio. Thus, make this work before you continue installing Mopidy.
+
+
+Using a custom audio sink
+=========================
+
+If you for some reason want to use some other GStreamer audio sink than
+``autoaudiosink``, you can change :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
+in your ``settings.py`` file.
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index 580ecd6d..26b50994 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -2,10 +2,9 @@
Installation
************
-To get a basic version of Mopidy running, you need Python and the
-:doc:`GStreamer library `. To use Spotify with Mopidy, you also need
-:doc:`libspotify and pyspotify `. Mopidy itself can either be
-installed from the Python package index, PyPI, or from git.
+There are several ways to install Mopidy. What way is best depends upon your
+setup and whether you want to use stable releases or less stable development
+versions.
Install dependencies
@@ -17,97 +16,155 @@ Install dependencies
gstreamer
libspotify
-Make sure you got the required dependencies installed.
+If you install Mopidy from the APT archive, as described below, you can skip
+the dependency installation part.
+
+Otherwise, make sure you got the required dependencies installed.
- Python >= 2.6, < 3
-- :doc:`GStreamer ` >= 0.10, with Python bindings
-- Dependencies for at least one Mopidy mixer:
- - :mod:`mopidy.mixers.alsa` (Linux only)
+- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
- - pyalsaaudio >= 0.2 (Debian/Ubuntu package: python-alsaaudio)
-
- - :mod:`mopidy.mixers.denon` (Linux, OS X, and Windows)
-
- - pyserial (Debian/Ubuntu package: python-serial)
-
- - *Default:* :mod:`mopidy.mixers.gstreamer_software` (Linux, OS X, and
- Windows)
-
- - No additional dependencies.
-
- - :mod:`mopidy.mixers.nad` (Linux, OS X, and Windows)
-
- - pyserial (Debian/Ubuntu package: python-serial)
-
- - :mod:`mopidy.mixers.osa` (OS X only)
-
- - No additional dependencies.
+- Mixer dependencies: The default mixer does not require any additional
+ dependencies. If you use another mixer, see the mixer's docs for any
+ additional requirements.
- Dependencies for at least one Mopidy backend:
- - *Default:* :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows)
+ - The default backend, :mod:`mopidy.backends.spotify`, requires libspotify
+ and pyspotify. See :doc:`libspotify`.
- - :doc:`libspotify and pyspotify `
-
- - :mod:`mopidy.backends.local` (Linux, OS X, and Windows)
-
- - No additional dependencies.
+ - The local backend, :mod:`mopidy.backends.local`, requires no additional
+ dependencies.
- Optional dependencies:
- - :mod:`mopidy.frontends.lastfm`
-
- - pylast >= 4.3.0
+ - To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for
+ additional requirements.
-Install latest release
-======================
-
-To install the currently latest release of Mopidy using ``pip``::
-
- sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
- sudo brew install pip # On OS X
- sudo pip install mopidy
-
-To later upgrade to the latest release::
-
- sudo pip install -U mopidy
-
-If you for some reason can't use ``pip``, try ``easy_install``.
-
-Next, you need to set a couple of :doc:`settings `, and then you're
-ready to :doc:`run Mopidy `.
-
-
-Install development snapshot
-============================
-
-If you want to follow Mopidy development closer, you may install a snapshot of
-Mopidy's ``develop`` branch::
-
- sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
- sudo brew install pip # On OS X
- sudo pip install mopidy==dev
-
-Next, you need to set a couple of :doc:`settings `, and then you're
-ready to :doc:`run Mopidy `.
-
-
-Run from source code checkout
+Install latest stable release
=============================
-If you may want to contribute to Mopidy, and want access to other branches as
-well, you can checkout the Mopidy source from Git and run it directly from the
-ckeckout::
- sudo aptitude install git-core # On Ubuntu/Debian
- sudo brew install git # On OS X
- git clone git://github.com/jodal/mopidy.git
- cd mopidy/
- python mopidy # Yes, 'mopidy' is a dir
+From APT archive
+----------------
-To later update to the very latest version::
+If you run a Debian based Linux distribution, like Ubuntu, the easiest way to
+install Mopidy is from the Mopidy APT archive. When installing from the APT
+archive, you will automatically get updates to Mopidy in the same way as you
+get updates to the rest of your distribution.
+
+#. Add the archive's GPG key::
+
+ wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
+
+#. Add the following to ``/etc/apt/sources.list``, or if you have the directory
+ ``/etc/apt/sources.list.d/``, add it to a file called ``mopidy.list`` in
+ that directory::
+
+ # Mopidy APT archive
+ deb http://apt.mopidy.com/ stable main contrib non-free
+ deb-src http://apt.mopidy.com/ stable main contrib non-free
+
+#. Install Mopidy and all dependencies::
+
+ sudo apt-get update
+ sudo apt-get install mopidy
+
+#. Next, you need to set a couple of :doc:`settings `, and then
+ you're ready to :doc:`run Mopidy `.
+
+When a new release is out, and you can't wait for you system to figure it out
+for itself, run the following to force an upgrade::
+
+ sudo apt-get update
+ sudo apt-get dist-upgrade
+
+
+From PyPI using Pip
+-------------------
+
+If you are on OS X or on Linux, but can't install from the APT archive, you can
+install Mopidy from PyPI using Pip.
+
+#. When you install using Pip, you first need to ensure that all of Mopidy's
+ dependencies have been installed. See the section on dependencies above.
+
+#. Then, you need to install Pip::
+
+ sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
+ sudo brew install pip # On OS X
+
+#. To install the currently latest stable release of Mopidy::
+
+ sudo pip install -U Mopidy
+
+ To upgrade Mopidy to future releases, just rerun this command.
+
+#. Next, you need to set a couple of :doc:`settings `, and then
+ you're ready to :doc:`run Mopidy `.
+
+If you for some reason can't use Pip, try ``easy_install`` instead.
+
+
+Install development version
+===========================
+
+If you want to follow the development of Mopidy closer, you may install a
+development version of Mopidy. These are not as stable as the releases, but
+you'll get access to new features earlier and may help us by reporting issues.
+
+
+From snapshot using Pip
+-----------------------
+
+If you want to follow Mopidy development closer, you may install a snapshot of
+Mopidy's ``develop`` branch.
+
+#. When you install using Pip, you first need to ensure that all of Mopidy's
+ dependencies have been installed. See the section on dependencies above.
+
+#. Then, you need to install Pip::
+
+ sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
+ sudo brew install pip # On OS X
+
+#. To install the latest snapshot of Mopidy, run::
+
+ sudo pip install mopidy==dev
+
+ To upgrade Mopidy to future releases, just rerun this command.
+
+#. Next, you need to set a couple of :doc:`settings `, and then
+ you're ready to :doc:`run Mopidy `.
+
+
+From Git
+--------
+
+If you want to contribute to Mopidy, you should install Mopidy using Git.
+
+#. When you install from Git, you first need to ensure that all of Mopidy's
+ dependencies have been installed. See the section on dependencies above.
+
+#. Then install Git, if haven't already::
+
+ sudo aptitude install git-core # On Ubuntu/Debian
+ sudo brew install git # On OS X
+
+#. Clone the official Mopidy repository, or your own fork of it::
+
+ git clone git://github.com/mopidy/mopidy.git
+
+#. Next, you need to set a couple of :doc:`settings `.
+
+#. You can then run Mopidy directly from the Git repository::
+
+ cd mopidy/ # Move into the Git repo dir
+ python mopidy # Run python on the mopidy source code dir
+
+#. Later, to get the latest changes to Mopidy::
cd mopidy/
git pull
@@ -115,3 +172,26 @@ To later update to the very latest version::
For an introduction to ``git``, please visit `git-scm.com
`_. Also, please read our :doc:`developer documentation
`.
+
+
+From AUR on ArchLinux
+---------------------
+
+If you are running ArchLinux, you can install a development snapshot of Mopidy
+using the package found at http://aur.archlinux.org/packages.php?ID=44026.
+
+#. First, you should consider installing any optional dependencies not included
+ by the AUR package, like required for e.g. Last.fm scrobbling.
+
+#. To install Mopidy with GStreamer, libspotify and pyspotify, you can use
+ ``packer``, ``yaourt``, or do it by hand like this::
+
+ wget http://aur.archlinux.org/packages/mopidy-git/mopidy-git.tar.gz
+ tar xf mopidy-git.tar.gz
+ cd mopidy-git/
+ makepkg -si
+
+ To upgrade Mopidy to future releases, just rerun ``makepkg``.
+
+#. Next, you need to set a couple of :doc:`settings `, and then
+ you're ready to :doc:`run Mopidy `.
diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst
index b3ea06fa..5d278fe2 100644
--- a/docs/installation/libspotify.rst
+++ b/docs/installation/libspotify.rst
@@ -5,11 +5,11 @@ libspotify installation
Mopidy uses `libspotify
`_ for playing music from
the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must
-install libspotify and `pyspotify `_.
+install libspotify and `pyspotify `_.
-.. warning::
+.. note::
- This backend requires a `Spotify premium account
+ This backend requires a paid `Spotify premium account
`_.
.. note::
@@ -19,17 +19,34 @@ install libspotify and `pyspotify `_.
Spotify Group.
-Installing libspotify on Linux
-==============================
+Installing libspotify
+=====================
-Download and install libspotify 0.0.4 for your OS and CPU architecture from
+
+On Linux from APT archive
+-------------------------
+
+If you run a Debian based Linux distribution, like Ubuntu, see
+http://apt.mopidy.com/ for how to the Mopidy APT archive as a software source
+on your installation. Then, simply run::
+
+ sudo apt-get install libspotify6
+
+When libspotify has been installed, continue with
+:ref:`pyspotify_installation`.
+
+
+On Linux from source
+--------------------
+
+Download and install libspotify 0.0.6 for your OS and CPU architecture from
https://developer.spotify.com/en/libspotify/.
For 64-bit Linux the process is as follows::
- wget http://developer.spotify.com/download/libspotify/libspotify-0.0.4-linux6-x86_64.tar.gz
- tar zxfv libspotify-0.0.4-linux6-x86_64.tar.gz
- cd libspotify-0.0.4-linux6-x86_64/
+ wget http://developer.spotify.com/download/libspotify/libspotify-0.0.6-linux6-x86_64.tar.gz
+ tar zxfv libspotify-0.0.6-linux6-x86_64.tar.gz
+ cd libspotify-0.0.6-linux6-x86_64/
sudo make install prefix=/usr/local
sudo ldconfig
@@ -37,8 +54,8 @@ When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
-Installing libspotify on OS X
-=============================
+On OS X from Homebrew
+---------------------
In OS X you need to have `XCode `_ and
`Homebrew `_ installed. Then, to install
@@ -46,37 +63,54 @@ libspotify::
brew install libspotify
+To update your existing libspotify installation using Homebrew::
+
+ brew update
+ brew install `brew outdated`
+
When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
-Install libspotify on Windows
-=============================
-
-**TODO** Test and document installation on Windows.
-
-
.. _pyspotify_installation:
Installing pyspotify
====================
-Install pyspotify's dependencies. At Debian/Ubuntu systems::
+When you've installed libspotify, it's time for making it available from Python
+by installing pyspotify.
- sudo aptitude install python-dev
-In OS X no additional dependencies are needed.
+On Linux from APT archive
+-------------------------
-Check out the pyspotify code, and install it::
+Assuming that you've already set up http://apt.mopidy.com/ as a software
+source, run::
- git clone git://github.com/jodal/pyspotify.git
- cd pyspotify/pyspotify/
- sudo rm -rf build/ # If you are upgrading pyspotify
+ sudo apt-get install python-spotify
+
+If you haven't already installed libspotify, this command will install both
+libspotify and pyspotify for you.
+
+
+On Linux/OS X from source
+-------------------------
+
+On Linux, you need to get the Python development files installed. On
+Debian/Ubuntu systems run::
+
+ sudo apt-get install python-dev
+
+On OS X no additional dependencies are needed.
+
+Get the pyspotify code, and install it::
+
+ wget --no-check-certificate -O pyspotify.tar.gz https://github.com/mopidy/pyspotify/tarball/mopidy
+ tar zxfv pyspotify.tar.gz
+ cd pyspotify/
sudo python setup.py install
-.. note::
-
- The ``sudo rm -rf build/`` step is needed if you are upgrading pyspotify.
- Simply running ``python setup.py clean`` will *not* clean out the C parts
- of the ``build/`` directory, and you will thus not get any changes to the C
- code included in your installation.
+It is important that you install pyspotify from the ``mopidy`` branch of the
+``mopidy/pyspotify`` repository, as the upstream repository at
+``winjer/pyspotify`` is not updated with changes needed to support e.g.
+libspotify 0.0.6 and high bitrate audio.
diff --git a/docs/api/backends/dummy.rst b/docs/modules/backends/dummy.rst
similarity index 100%
rename from docs/api/backends/dummy.rst
rename to docs/modules/backends/dummy.rst
diff --git a/docs/api/backends/local.rst b/docs/modules/backends/local.rst
similarity index 100%
rename from docs/api/backends/local.rst
rename to docs/modules/backends/local.rst
diff --git a/docs/modules/backends/spotify.rst b/docs/modules/backends/spotify.rst
new file mode 100644
index 00000000..938d6337
--- /dev/null
+++ b/docs/modules/backends/spotify.rst
@@ -0,0 +1,7 @@
+*************************************************
+:mod:`mopidy.backends.spotify` -- Spotify backend
+*************************************************
+
+.. automodule:: mopidy.backends.spotify
+ :synopsis: Backend for the Spotify music streaming service
+ :members:
diff --git a/docs/modules/frontends/lastfm.rst b/docs/modules/frontends/lastfm.rst
new file mode 100644
index 00000000..a726f4a2
--- /dev/null
+++ b/docs/modules/frontends/lastfm.rst
@@ -0,0 +1,7 @@
+***************************************************
+:mod:`mopidy.frontends.lastfm` -- Last.fm Scrobbler
+***************************************************
+
+.. automodule:: mopidy.frontends.lastfm
+ :synopsis: Last.fm scrobbler frontend
+ :members:
diff --git a/docs/api/frontends/mpd.rst b/docs/modules/frontends/mpd.rst
similarity index 93%
rename from docs/api/frontends/mpd.rst
rename to docs/modules/frontends/mpd.rst
index 6361e909..35128e70 100644
--- a/docs/api/frontends/mpd.rst
+++ b/docs/modules/frontends/mpd.rst
@@ -1,6 +1,6 @@
-***************************
-:mod:`mopidy.frontends.mpd`
-***************************
+*****************************************
+:mod:`mopidy.frontends.mpd` -- MPD server
+*****************************************
.. automodule:: mopidy.frontends.mpd
:synopsis: MPD frontend
diff --git a/docs/modules/index.rst b/docs/modules/index.rst
new file mode 100644
index 00000000..44da0028
--- /dev/null
+++ b/docs/modules/index.rst
@@ -0,0 +1,8 @@
+****************
+Module reference
+****************
+
+.. toctree::
+ :glob:
+
+ **
diff --git a/docs/modules/mixers/alsa.rst b/docs/modules/mixers/alsa.rst
new file mode 100644
index 00000000..05f429eb
--- /dev/null
+++ b/docs/modules/mixers/alsa.rst
@@ -0,0 +1,9 @@
+*************************************************
+:mod:`mopidy.mixers.alsa` -- ALSA mixer for Linux
+*************************************************
+
+.. inheritance-diagram:: mopidy.mixers.alsa
+
+.. automodule:: mopidy.mixers.alsa
+ :synopsis: ALSA mixer for Linux
+ :members:
diff --git a/docs/modules/mixers/denon.rst b/docs/modules/mixers/denon.rst
new file mode 100644
index 00000000..ac944ccc
--- /dev/null
+++ b/docs/modules/mixers/denon.rst
@@ -0,0 +1,9 @@
+*****************************************************************
+:mod:`mopidy.mixers.denon` -- Hardware mixer for Denon amplifiers
+*****************************************************************
+
+.. inheritance-diagram:: mopidy.mixers.denon
+
+.. automodule:: mopidy.mixers.denon
+ :synopsis: Hardware mixer for Denon amplifiers
+ :members:
diff --git a/docs/modules/mixers/dummy.rst b/docs/modules/mixers/dummy.rst
new file mode 100644
index 00000000..6665f949
--- /dev/null
+++ b/docs/modules/mixers/dummy.rst
@@ -0,0 +1,9 @@
+*****************************************************
+:mod:`mopidy.mixers.dummy` -- Dummy mixer for testing
+*****************************************************
+
+.. inheritance-diagram:: mopidy.mixers.dummy
+
+.. automodule:: mopidy.mixers.dummy
+ :synopsis: Dummy mixer for testing
+ :members:
diff --git a/docs/modules/mixers/gstreamer_software.rst b/docs/modules/mixers/gstreamer_software.rst
new file mode 100644
index 00000000..ef8cc310
--- /dev/null
+++ b/docs/modules/mixers/gstreamer_software.rst
@@ -0,0 +1,9 @@
+***************************************************************************
+:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms
+***************************************************************************
+
+.. inheritance-diagram:: mopidy.mixers.gstreamer_software
+
+.. automodule:: mopidy.mixers.gstreamer_software
+ :synopsis: Software mixer for all platforms
+ :members:
diff --git a/docs/modules/mixers/nad.rst b/docs/modules/mixers/nad.rst
new file mode 100644
index 00000000..d441b3fd
--- /dev/null
+++ b/docs/modules/mixers/nad.rst
@@ -0,0 +1,9 @@
+*************************************************************
+:mod:`mopidy.mixers.nad` -- Hardware mixer for NAD amplifiers
+*************************************************************
+
+.. inheritance-diagram:: mopidy.mixers.nad
+
+.. automodule:: mopidy.mixers.nad
+ :synopsis: Hardware mixer for NAD amplifiers
+ :members:
diff --git a/docs/modules/mixers/osa.rst b/docs/modules/mixers/osa.rst
new file mode 100644
index 00000000..14bf9a49
--- /dev/null
+++ b/docs/modules/mixers/osa.rst
@@ -0,0 +1,9 @@
+**********************************************
+:mod:`mopidy.mixers.osa` -- Osa mixer for OS X
+**********************************************
+
+.. inheritance-diagram:: mopidy.mixers.osa
+
+.. automodule:: mopidy.mixers.osa
+ :synopsis: Osa mixer for OS X
+ :members:
diff --git a/docs/modules/outputs/gstreamer.rst b/docs/modules/outputs/gstreamer.rst
new file mode 100644
index 00000000..69c77dad
--- /dev/null
+++ b/docs/modules/outputs/gstreamer.rst
@@ -0,0 +1,9 @@
+*********************************************************************
+:mod:`mopidy.outputs.gstreamer` -- GStreamer output for all platforms
+*********************************************************************
+
+.. inheritance-diagram:: mopidy.outputs.gstreamer
+
+.. automodule:: mopidy.outputs.gstreamer
+ :synopsis: GStreamer output for all platforms
+ :members:
diff --git a/docs/settings.rst b/docs/settings.rst
index a7638b4e..532f52cf 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -2,13 +2,31 @@
Settings
********
+Mopidy has lots of settings. Luckily, you only need to change a few, and stay
+ignorant of the rest. Below you can find guides for typical configuration
+changes you may want to do, and a complete listing of available settings.
+
+
+Changing settings
+=================
+
Mopidy reads settings from the file ``~/.mopidy/settings.py``, where ``~``
means your *home directory*. If your username is ``alice`` and you are running
Linux, the settings file should probably be at
``/home/alice/.mopidy/settings.py``.
-You can either create this file yourself, or run the ``mopidy`` command, and it
-will create an empty settings file for you.
+You can either create the settings file yourself, or run the ``mopidy``
+command, and it will create an empty settings file for you.
+
+When you have created the settings file, open it in a text editor, and add
+settings you want to change. If you want to keep the default value for setting,
+you should *not* redefine it in your own settings file.
+
+A complete ``~/.mopidy/settings.py`` may look as simple as this::
+
+ MPD_SERVER_HOSTNAME = u'::'
+ SPOTIFY_USERNAME = u'alice'
+ SPOTIFY_PASSWORD = u'mysecret'
Music from Spotify
@@ -47,12 +65,12 @@ Generating a tag cache
Previously the local storage backend relied purely on ``tag_cache`` files
generated by the original MPD server. To remedy this the command
:command:`mopidy-scan` has been created. The program will scan your current
-:attr:`mopidy.settings.LOCAL_MUSIC_FOLDER` and build a MPD compatible
+:attr:`mopidy.settings.LOCAL_MUSIC_PATH` and build a MPD compatible
``tag_cache``.
To make a ``tag_cache`` of your local music available for Mopidy:
-#. Ensure that :attr:`mopidy.settings.LOCAL_MUSIC_FOLDER` points to where your
+#. Ensure that :attr:`mopidy.settings.LOCAL_MUSIC_PATH` points to where your
music is located. Check the current setting by running::
mopidy --list-settings
@@ -64,7 +82,7 @@ To make a ``tag_cache`` of your local music available for Mopidy:
mopidy-scan > tag_cache
#. Move the ``tag_cache`` file to the location
- :attr:`mopidy.settings.LOCAL_TAG_CACHE` is set to, or change the setting to
+ :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` is set to, or change the setting to
point to where your ``tag_cache`` file is.
#. Start Mopidy, find the music library in a client, and play some local music!
@@ -88,3 +106,12 @@ file::
LASTFM_USERNAME = u'myusername'
LASTFM_PASSWORD = u'mysecret'
+
+
+Available settings
+==================
+
+.. automodule:: mopidy.settings
+ :synopsis: Available settings and their default values
+ :members:
+ :undoc-members:
diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py
index 491c5b73..096a433f 100644
--- a/mopidy/backends/base/__init__.py
+++ b/mopidy/backends/base/__init__.py
@@ -4,21 +4,19 @@ import random
import time
from mopidy import settings
-from mopidy.backends.base.current_playlist import BaseCurrentPlaylistController
-from mopidy.backends.base.library import BaseLibraryController
-from mopidy.backends.base.playback import BasePlaybackController
-from mopidy.backends.base.stored_playlists import BaseStoredPlaylistsController
from mopidy.frontends.mpd import translator
from mopidy.models import Playlist
from mopidy.utils import get_class
+from .current_playlist import CurrentPlaylistController
+from .library import LibraryController, BaseLibraryProvider
+from .playback import PlaybackController, BasePlaybackProvider
+from .stored_playlists import (StoredPlaylistsController,
+ BaseStoredPlaylistsProvider)
+
logger = logging.getLogger('mopidy.backends.base')
-__all__ = ['BaseBackend', 'BasePlaybackController',
- 'BaseCurrentPlaylistController', 'BaseStoredPlaylistsController',
- 'BaseLibraryController']
-
-class BaseBackend(object):
+class Backend(object):
"""
:param core_queue: a queue for sending messages to
:class:`mopidy.process.CoreProcess`
@@ -44,22 +42,22 @@ class BaseBackend(object):
core_queue = None
#: The current playlist controller. An instance of
- #: :class:`mopidy.backends.base.BaseCurrentPlaylistController`.
+ #: :class:`mopidy.backends.base.CurrentPlaylistController`.
current_playlist = None
#: The library controller. An instance of
- # :class:`mopidy.backends.base.BaseLibraryController`.
+ # :class:`mopidy.backends.base.LibraryController`.
library = None
#: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`.
mixer = None
#: The playback controller. An instance of
- #: :class:`mopidy.backends.base.BasePlaybackController`.
+ #: :class:`mopidy.backends.base.PlaybackController`.
playback = None
#: The stored playlists controller. An instance of
- #: :class:`mopidy.backends.base.BaseStoredPlaylistsController`.
+ #: :class:`mopidy.backends.base.StoredPlaylistsController`.
stored_playlists = None
#: List of URI prefixes this backend can handle.
diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py
index 34a16369..fe7d1de9 100644
--- a/mopidy/backends/base/current_playlist.py
+++ b/mopidy/backends/base/current_playlist.py
@@ -6,10 +6,10 @@ from mopidy.frontends.mpd import translator
logger = logging.getLogger('mopidy.backends.base')
-class BaseCurrentPlaylistController(object):
+class CurrentPlaylistController(object):
"""
:param backend: backend the controller is a part of
- :type backend: :class:`BaseBackend`
+ :type backend: :class:`mopidy.backends.base.Backend`
"""
def __init__(self, backend):
diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py
index 94f40863..fd018b5f 100644
--- a/mopidy/backends/base/library.py
+++ b/mopidy/backends/base/library.py
@@ -2,18 +2,21 @@ import logging
logger = logging.getLogger('mopidy.backends.base')
-class BaseLibraryController(object):
+class LibraryController(object):
"""
:param backend: backend the controller is a part of
- :type backend: :class:`BaseBackend`
+ :type backend: :class:`mopidy.backends.base.Backend`
+ :param provider: provider the controller should use
+ :type provider: instance of :class:`BaseLibraryProvider`
"""
- def __init__(self, backend):
+ def __init__(self, backend, provider):
self.backend = backend
+ self.provider = provider
def destroy(self):
"""Cleanup after component."""
- pass
+ self.provider.destroy()
def find_exact(self, **query):
"""
@@ -32,7 +35,7 @@ class BaseLibraryController(object):
:type query: dict
:rtype: :class:`mopidy.models.Playlist`
"""
- raise NotImplementedError
+ return self.provider.find_exact(**query)
def lookup(self, uri):
"""
@@ -42,7 +45,7 @@ class BaseLibraryController(object):
:type uri: string
:rtype: :class:`mopidy.models.Track` or :class:`None`
"""
- raise NotImplementedError
+ return self.provider.lookup(uri)
def refresh(self, uri=None):
"""
@@ -51,7 +54,7 @@ class BaseLibraryController(object):
:param uri: directory or track URI
:type uri: string
"""
- raise NotImplementedError
+ return self.provider.refresh(uri)
def search(self, **query):
"""
@@ -70,4 +73,54 @@ class BaseLibraryController(object):
:type query: dict
:rtype: :class:`mopidy.models.Playlist`
"""
+ return self.provider.search(**query)
+
+
+class BaseLibraryProvider(object):
+ """
+ :param backend: backend the controller is a part of
+ :type backend: :class:`mopidy.backends.base.Backend`
+ """
+
+ def __init__(self, backend):
+ self.backend = backend
+
+ def destroy(self):
+ """
+ Cleanup after component.
+
+ *MAY be implemented by subclasses.*
+ """
+ pass
+
+ def find_exact(self, **query):
+ """
+ See :meth:`mopidy.backends.base.LibraryController.find_exact`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def lookup(self, uri):
+ """
+ See :meth:`mopidy.backends.base.LibraryController.lookup`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def refresh(self, uri=None):
+ """
+ See :meth:`mopidy.backends.base.LibraryController.refresh`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def search(self, **query):
+ """
+ See :meth:`mopidy.backends.base.LibraryController.search`.
+
+ *MUST be implemented by subclass.*
+ """
raise NotImplementedError
diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py
index b7ceeee2..8a3eeee5 100644
--- a/mopidy/backends/base/playback.py
+++ b/mopidy/backends/base/playback.py
@@ -4,10 +4,12 @@ import time
logger = logging.getLogger('mopidy.backends.base')
-class BasePlaybackController(object):
+class PlaybackController(object):
"""
- :param backend: backend the controller is a part of
- :type backend: :class:`BaseBackend`
+ :param backend: the backend
+ :type backend: :class:`mopidy.backends.base.Backend`
+ :param provider: provider the controller should use
+ :type provider: instance of :class:`BasePlaybackProvider`
"""
# pylint: disable = R0902
@@ -54,8 +56,9 @@ class BasePlaybackController(object):
#: Playback continues after current song.
single = False
- def __init__(self, backend):
+ def __init__(self, backend, provider):
self.backend = backend
+ self.provider = provider
self._state = self.STOPPED
self._shuffled = []
self._first_shuffle = True
@@ -65,10 +68,8 @@ class BasePlaybackController(object):
def destroy(self):
"""
Cleanup after component.
-
- May be overridden by subclasses.
"""
- pass
+ self.provider.destroy()
def _get_cpid(self, cp_track):
if cp_track is None:
@@ -330,7 +331,7 @@ class BasePlaybackController(object):
"""
Tell the playback controller that the current playlist has changed.
- Used by :class:`mopidy.backends.base.BaseCurrentPlaylistController`.
+ Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
"""
self._first_shuffle = True
self._shuffled = []
@@ -353,18 +354,9 @@ class BasePlaybackController(object):
def pause(self):
"""Pause playback."""
- if self.state == self.PLAYING and self._pause():
+ if self.state == self.PLAYING and self.provider.pause():
self.state = self.PAUSED
- def _pause(self):
- """
- To be overridden by subclass. Implement your backend's pause
- functionality here.
-
- :rtype: :class:`True` if successful, else :class:`False`
- """
- raise NotImplementedError
-
def play(self, cp_track=None, on_error_step=1):
"""
Play the given track, or if the given track is :class:`None`, play the
@@ -391,7 +383,7 @@ class BasePlaybackController(object):
self.state = self.STOPPED
self.current_cp_track = cp_track
self.state = self.PLAYING
- if not self._play(cp_track[1]):
+ if not self.provider.play(cp_track[1]):
# Track is not playable
if self.random and self._shuffled:
self._shuffled.remove(cp_track)
@@ -405,18 +397,6 @@ class BasePlaybackController(object):
self._trigger_started_playing_event()
- def _play(self, track):
- """
- To be overridden by subclass. Implement your backend's play
- functionality here.
-
- :param track: the track to play
- :type track: :class:`mopidy.models.Track`
- :rtype: :class:`True` if successful, else :class:`False`
- """
-
- raise NotImplementedError
-
def previous(self):
"""Play the previous track."""
if self.cp_track_at_previous is None:
@@ -428,18 +408,9 @@ class BasePlaybackController(object):
def resume(self):
"""If paused, resume playing the current track."""
- if self.state == self.PAUSED and self._resume():
+ if self.state == self.PAUSED and self.provider.resume():
self.state = self.PLAYING
- def _resume(self):
- """
- To be overridden by subclass. Implement your backend's resume
- functionality here.
-
- :rtype: :class:`True` if successful, else :class:`False`
- """
- raise NotImplementedError
-
def seek(self, time_position):
"""
Seeks to time position given in milliseconds.
@@ -465,18 +436,7 @@ class BasePlaybackController(object):
self._play_time_started = self._current_wall_time
self._play_time_accumulated = time_position
- return self._seek(time_position)
-
- def _seek(self, time_position):
- """
- To be overridden by subclass. Implement your backend's seek
- functionality here.
-
- :param time_position: time position in milliseconds
- :type time_position: int
- :rtype: :class:`True` if successful, else :class:`False`
- """
- raise NotImplementedError
+ return self.provider.seek(time_position)
def stop(self, clear_current_track=False):
"""
@@ -489,20 +449,11 @@ class BasePlaybackController(object):
if self.state == self.STOPPED:
return
self._trigger_stopped_playing_event()
- if self._stop():
+ if self.provider.stop():
self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
- def _stop(self):
- """
- To be overridden by subclass. Implement your backend's stop
- functionality here.
-
- :rtype: :class:`True` if successful, else :class:`False`
- """
- raise NotImplementedError
-
def _trigger_started_playing_event(self):
"""
Notifies frontends that a track has started playing.
@@ -532,3 +483,75 @@ class BasePlaybackController(object):
'track': self.current_track,
'stop_position': self.time_position,
})
+
+
+class BasePlaybackProvider(object):
+ """
+ :param backend: the backend
+ :type backend: :class:`mopidy.backends.base.Backend`
+ """
+
+ def __init__(self, backend):
+ self.backend = backend
+
+ def destroy(self):
+ """
+ Cleanup after component.
+
+ *MAY be implemented by subclasses.*
+ """
+ pass
+
+ def pause(self):
+ """
+ Pause playback.
+
+ *MUST be implemented by subclass.*
+
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ raise NotImplementedError
+
+ def play(self, track):
+ """
+ Play given track.
+
+ *MUST be implemented by subclass.*
+
+ :param track: the track to play
+ :type track: :class:`mopidy.models.Track`
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ raise NotImplementedError
+
+ def resume(self):
+ """
+ Resume playback at the same time position playback was paused.
+
+ *MUST be implemented by subclass.*
+
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ raise NotImplementedError
+
+ def seek(self, time_position):
+ """
+ Seek to a given time position.
+
+ *MUST be implemented by subclass.*
+
+ :param time_position: time position in milliseconds
+ :type time_position: int
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ raise NotImplementedError
+
+ def stop(self):
+ """
+ Stop playback.
+
+ *MUST be implemented by subclass.*
+
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ raise NotImplementedError
diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py
index 61722c81..6578c046 100644
--- a/mopidy/backends/base/stored_playlists.py
+++ b/mopidy/backends/base/stored_playlists.py
@@ -3,28 +3,34 @@ import logging
logger = logging.getLogger('mopidy.backends.base')
-class BaseStoredPlaylistsController(object):
+class StoredPlaylistsController(object):
"""
:param backend: backend the controller is a part of
- :type backend: :class:`BaseBackend`
+ :type backend: :class:`mopidy.backends.base.Backend`
+ :param provider: provider the controller should use
+ :type provider: instance of :class:`BaseStoredPlaylistsProvider`
"""
- def __init__(self, backend):
+ def __init__(self, backend, provider):
self.backend = backend
- self._playlists = []
+ self.provider = provider
def destroy(self):
"""Cleanup after component."""
- pass
+ self.provider.destroy()
@property
def playlists(self):
- """List of :class:`mopidy.models.Playlist`."""
- return copy(self._playlists)
+ """
+ Currently stored playlists.
+
+ Read/write. List of :class:`mopidy.models.Playlist`.
+ """
+ return self.provider.playlists
@playlists.setter
def playlists(self, playlists):
- self._playlists = playlists
+ self.provider.playlists = playlists
def create(self, name):
"""
@@ -34,7 +40,7 @@ class BaseStoredPlaylistsController(object):
:type name: string
:rtype: :class:`mopidy.models.Playlist`
"""
- raise NotImplementedError
+ return self.provider.create(name)
def delete(self, playlist):
"""
@@ -43,7 +49,7 @@ class BaseStoredPlaylistsController(object):
:param playlist: the playlist to delete
:type playlist: :class:`mopidy.models.Playlist`
"""
- raise NotImplementedError
+ return self.provider.delete(playlist)
def get(self, **criteria):
"""
@@ -55,13 +61,14 @@ class BaseStoredPlaylistsController(object):
get(name='a') # Returns track with name 'a'
get(uri='xyz') # Returns track with URI 'xyz'
- get(name='a', uri='xyz') # Returns track with name 'a' and URI 'xyz'
+ get(name='a', uri='xyz') # Returns track with name 'a' and URI
+ # 'xyz'
:param criteria: one or more criteria to match by
:type criteria: dict
:rtype: :class:`mopidy.models.Playlist`
"""
- matches = self._playlists
+ matches = self.playlists
for (key, value) in criteria.iteritems():
matches = filter(lambda p: getattr(p, key) == value, matches)
if len(matches) == 1:
@@ -82,11 +89,14 @@ class BaseStoredPlaylistsController(object):
:type uri: string
:rtype: :class:`mopidy.models.Playlist`
"""
- raise NotImplementedError
+ return self.provider.lookup(uri)
def refresh(self):
- """Refresh stored playlists."""
- raise NotImplementedError
+ """
+ Refresh the stored playlists in
+ :attr:`mopidy.backends.base.StoredPlaylistsController.playlists`.
+ """
+ return self.provider.refresh()
def rename(self, playlist, new_name):
"""
@@ -97,7 +107,7 @@ class BaseStoredPlaylistsController(object):
:param new_name: the new name
:type new_name: string
"""
- raise NotImplementedError
+ return self.provider.rename(playlist, new_name)
def save(self, playlist):
"""
@@ -106,4 +116,85 @@ class BaseStoredPlaylistsController(object):
:param playlist: the playlist
:type playlist: :class:`mopidy.models.Playlist`
"""
+ return self.provider.save(playlist)
+
+
+class BaseStoredPlaylistsProvider(object):
+ """
+ :param backend: backend the controller is a part of
+ :type backend: :class:`mopidy.backends.base.Backend`
+ """
+
+ def __init__(self, backend):
+ self.backend = backend
+ self._playlists = []
+
+ def destroy(self):
+ """
+ Cleanup after component.
+
+ *MAY be implemented by subclass.*
+ """
+ pass
+
+ @property
+ def playlists(self):
+ """
+ Currently stored playlists.
+
+ Read/write. List of :class:`mopidy.models.Playlist`.
+ """
+ return copy(self._playlists)
+
+ @playlists.setter
+ def playlists(self, playlists):
+ self._playlists = playlists
+
+ def create(self, name):
+ """
+ See :meth:`mopidy.backends.base.StoredPlaylistsController.create`.
+
+ *MUST be implemented by subclass.*
+ """
raise NotImplementedError
+
+ def delete(self, playlist):
+ """
+ See :meth:`mopidy.backends.base.StoredPlaylistsController.delete`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def lookup(self, uri):
+ """
+ See :meth:`mopidy.backends.base.StoredPlaylistsController.lookup`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def refresh(self):
+ """
+ See :meth:`mopidy.backends.base.StoredPlaylistsController.refresh`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def rename(self, playlist, new_name):
+ """
+ See :meth:`mopidy.backends.base.StoredPlaylistsController.rename`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def save(self, playlist):
+ """
+ See :meth:`mopidy.backends.base.StoredPlaylistsController.save`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py
index 62cbd7e2..9c6885bc 100644
--- a/mopidy/backends/dummy/__init__.py
+++ b/mopidy/backends/dummy/__init__.py
@@ -1,9 +1,19 @@
-from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
- BasePlaybackController, BaseLibraryController,
- BaseStoredPlaylistsController)
+from mopidy.backends.base import (Backend, CurrentPlaylistController,
+ PlaybackController, BasePlaybackProvider, LibraryController,
+ BaseLibraryProvider, StoredPlaylistsController,
+ BaseStoredPlaylistsProvider)
from mopidy.models import Playlist
-class DummyBackend(BaseBackend):
+
+class DummyQueue(object):
+ def __init__(self):
+ self.received_messages = []
+
+ def put(self, message):
+ self.received_messages.append(message)
+
+
+class DummyBackend(Backend):
"""
A backend which implements the backend API in the simplest way possible.
Used in tests of the frontends.
@@ -13,19 +23,30 @@ class DummyBackend(BaseBackend):
def __init__(self, *args, **kwargs):
super(DummyBackend, self).__init__(*args, **kwargs)
- self.current_playlist = DummyCurrentPlaylistController(backend=self)
- self.library = DummyLibraryController(backend=self)
- self.playback = DummyPlaybackController(backend=self)
- self.stored_playlists = DummyStoredPlaylistsController(backend=self)
+
+ self.core_queue = DummyQueue()
+
+ self.current_playlist = CurrentPlaylistController(backend=self)
+
+ library_provider = DummyLibraryProvider(backend=self)
+ self.library = LibraryController(backend=self,
+ provider=library_provider)
+
+ playback_provider = DummyPlaybackProvider(backend=self)
+ self.playback = PlaybackController(backend=self,
+ provider=playback_provider)
+
+ stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self)
+ self.stored_playlists = StoredPlaylistsController(backend=self,
+ provider=stored_playlists_provider)
+
self.uri_handlers = [u'dummy:']
-class DummyCurrentPlaylistController(BaseCurrentPlaylistController):
- pass
-
-
-class DummyLibraryController(BaseLibraryController):
- _library = []
+class DummyLibraryProvider(BaseLibraryProvider):
+ def __init__(self, *args, **kwargs):
+ super(DummyLibraryProvider, self).__init__(*args, **kwargs)
+ self._library = []
def find_exact(self, **query):
return Playlist()
@@ -42,41 +63,25 @@ class DummyLibraryController(BaseLibraryController):
return Playlist()
-class DummyPlaybackController(BasePlaybackController):
- def _next(self, track):
+class DummyPlaybackProvider(BasePlaybackProvider):
+ def pause(self):
+ return True
+
+ def play(self, track):
"""Pass None as track to force failure"""
return track is not None
- def _pause(self):
+ def resume(self):
return True
- def _play(self, track):
- """Pass None as track to force failure"""
- return track is not None
-
- def _previous(self, track):
- """Pass None as track to force failure"""
- return track is not None
-
- def _resume(self):
+ def seek(self, time_position):
return True
- def _seek(self, time_position):
+ def stop(self):
return True
- def _stop(self):
- return True
-
- def _trigger_started_playing_event(self):
- pass # noop
-
- def _trigger_stopped_playing_event(self):
- pass # noop
-
-
-class DummyStoredPlaylistsController(BaseStoredPlaylistsController):
- _playlists = []
+class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
def create(self, name):
playlist = Playlist(name=name)
self._playlists.append(playlist)
@@ -93,7 +98,7 @@ class DummyStoredPlaylistsController(BaseStoredPlaylistsController):
def rename(self, playlist, new_name):
self._playlists[self._playlists.index(playlist)] = \
- playlist.with_(name=new_name)
+ playlist.copy(name=new_name)
def save(self, playlist):
self._playlists.append(playlist)
diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py
deleted file mode 100644
index 223d9968..00000000
--- a/mopidy/backends/libspotify/__init__.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import logging
-
-from mopidy import settings
-from mopidy.backends.base import BaseBackend, BaseCurrentPlaylistController
-
-logger = logging.getLogger('mopidy.backends.libspotify')
-
-ENCODING = 'utf-8'
-
-class LibspotifyBackend(BaseBackend):
- """
- A `Spotify `_ backend which uses the official
- `libspotify `_
- library and the `pyspotify `_ Python
- bindings for libspotify.
-
- **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify
-
- **Settings:**
-
- - :attr:`mopidy.settings.SPOTIFY_LIB_CACHE`
- - :attr:`mopidy.settings.SPOTIFY_USERNAME`
- - :attr:`mopidy.settings.SPOTIFY_PASSWORD`
-
- .. note::
-
- This product uses SPOTIFY(R) CORE but is not endorsed, certified or
- otherwise approved in any way by Spotify. Spotify is the registered
- trade mark of the Spotify Group.
- """
-
- # Imports inside methods are to prevent loading of __init__.py to fail on
- # missing spotify dependencies.
-
- def __init__(self, *args, **kwargs):
- from .library import LibspotifyLibraryController
- from .playback import LibspotifyPlaybackController
- from .stored_playlists import LibspotifyStoredPlaylistsController
-
- super(LibspotifyBackend, self).__init__(*args, **kwargs)
-
- self.current_playlist = BaseCurrentPlaylistController(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.spotify = self._connect()
-
- def _connect(self):
- from .session_manager import LibspotifySessionManager
-
- logger.info(u'Mopidy uses SPOTIFY(R) CORE')
- logger.debug(u'Connecting to Spotify')
- spotify = LibspotifySessionManager(
- settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,
- core_queue=self.core_queue,
- output=self.output)
- spotify.start()
- return spotify
diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py
index efcc3bbd..e3e1d5dc 100644
--- a/mopidy/backends/local/__init__.py
+++ b/mopidy/backends/local/__init__.py
@@ -5,9 +5,10 @@ import os
import shutil
from mopidy import settings
-from mopidy.backends.base import (BaseBackend, BaseLibraryController,
- BaseStoredPlaylistsController, BaseCurrentPlaylistController,
- BasePlaybackController)
+from mopidy.backends.base import (Backend, CurrentPlaylistController,
+ LibraryController, BaseLibraryProvider, PlaybackController,
+ BasePlaybackProvider, StoredPlaylistsController,
+ BaseStoredPlaylistsProvider)
from mopidy.models import Playlist, Track, Album
from mopidy.utils.process import pickle_connection
@@ -15,58 +16,72 @@ from .translator import parse_m3u, parse_mpd_tag_cache
logger = logging.getLogger(u'mopidy.backends.local')
-class LocalBackend(BaseBackend):
+class LocalBackend(Backend):
"""
A backend for playing music from a local music archive.
- **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-local
+ **Issues:** http://github.com/mopidy/mopidy/issues/labels/backend-local
**Settings:**
- - :attr:`mopidy.settings.LOCAL_MUSIC_FOLDER`
- - :attr:`mopidy.settings.LOCAL_PLAYLIST_FOLDER`
- - :attr:`mopidy.settings.LOCAL_TAG_CACHE`
+ - :attr:`mopidy.settings.LOCAL_MUSIC_PATH`
+ - :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH`
+ - :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE`
"""
def __init__(self, *args, **kwargs):
super(LocalBackend, self).__init__(*args, **kwargs)
- self.library = LocalLibraryController(self)
- self.stored_playlists = LocalStoredPlaylistsController(self)
- self.current_playlist = BaseCurrentPlaylistController(self)
- self.playback = LocalPlaybackController(self)
+ self.current_playlist = CurrentPlaylistController(backend=self)
+
+ library_provider = LocalLibraryProvider(backend=self)
+ self.library = LibraryController(backend=self,
+ provider=library_provider)
+
+ playback_provider = LocalPlaybackProvider(backend=self)
+ self.playback = LocalPlaybackController(backend=self,
+ provider=playback_provider)
+
+ stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self)
+ self.stored_playlists = StoredPlaylistsController(backend=self,
+ provider=stored_playlists_provider)
+
self.uri_handlers = [u'file://']
-class LocalPlaybackController(BasePlaybackController):
- def __init__(self, backend):
- super(LocalPlaybackController, self).__init__(backend)
+class LocalPlaybackController(PlaybackController):
+ def __init__(self, *args, **kwargs):
+ super(LocalPlaybackController, self).__init__(*args, **kwargs)
+
+ # XXX Why do we call stop()? Is it to set GStreamer state to 'READY'?
self.stop()
- def _play(self, track):
- return self.backend.output.play_uri(track.uri)
-
- def _stop(self):
- return self.backend.output.set_state('READY')
-
- def _pause(self):
- return self.backend.output.set_state('PAUSED')
-
- def _resume(self):
- return self.backend.output.set_state('PLAYING')
-
- def _seek(self, time_position):
- return self.backend.output.set_position(time_position)
-
@property
def time_position(self):
return self.backend.output.get_position()
-class LocalStoredPlaylistsController(BaseStoredPlaylistsController):
- def __init__(self, *args):
- super(LocalStoredPlaylistsController, self).__init__(*args)
- self._folder = os.path.expanduser(settings.LOCAL_PLAYLIST_FOLDER)
+class LocalPlaybackProvider(BasePlaybackProvider):
+ def pause(self):
+ return self.backend.output.set_state('PAUSED')
+
+ def play(self, track):
+ return self.backend.output.play_uri(track.uri)
+
+ def resume(self):
+ return self.backend.output.set_state('PLAYING')
+
+ def seek(self, time_position):
+ return self.backend.output.set_position(time_position)
+
+ def stop(self):
+ return self.backend.output.set_state('READY')
+
+
+class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
+ def __init__(self, *args, **kwargs):
+ super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
+ self._folder = settings.LOCAL_PLAYLIST_PATH
self.refresh()
def lookup(self, uri):
@@ -116,7 +131,7 @@ class LocalStoredPlaylistsController(BaseStoredPlaylistsController):
src = os.path.join(self._folder, playlist.name + '.m3u')
dst = os.path.join(self._folder, name + '.m3u')
- renamed = playlist.with_(name=name)
+ renamed = playlist.copy(name=name)
index = self._playlists.index(playlist)
self._playlists[index] = renamed
@@ -136,15 +151,15 @@ class LocalStoredPlaylistsController(BaseStoredPlaylistsController):
self._playlists.append(playlist)
-class LocalLibraryController(BaseLibraryController):
- def __init__(self, backend):
- super(LocalLibraryController, self).__init__(backend)
+class LocalLibraryProvider(BaseLibraryProvider):
+ def __init__(self, *args, **kwargs):
+ super(LocalLibraryProvider, self).__init__(*args, **kwargs)
self._uri_mapping = {}
self.refresh()
def refresh(self, uri=None):
- tag_cache = os.path.expanduser(settings.LOCAL_TAG_CACHE)
- music_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER)
+ tag_cache = settings.LOCAL_TAG_CACHE_FILE
+ music_folder = settings.LOCAL_MUSIC_PATH
tracks = parse_mpd_tag_cache(tag_cache, music_folder)
diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py
index 87ea15df..51522ead 100644
--- a/mopidy/backends/local/translator.py
+++ b/mopidy/backends/local/translator.py
@@ -84,7 +84,7 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
_convert_mpd_data(current, tracks, music_dir)
current.clear()
- current[key.lower()] = value
+ current[key.lower()] = value.decode('utf-8')
_convert_mpd_data(current, tracks, music_dir)
@@ -96,29 +96,55 @@ def _convert_mpd_data(data, tracks, music_dir):
track_kwargs = {}
album_kwargs = {}
+ artist_kwargs = {}
+ albumartist_kwargs = {}
if 'track' in data:
album_kwargs['num_tracks'] = int(data['track'].split('/')[1])
track_kwargs['track_no'] = int(data['track'].split('/')[0])
if 'artist' in data:
- artist = Artist(name=data['artist'])
- track_kwargs['artists'] = [artist]
- album_kwargs['artists'] = [artist]
+ artist_kwargs['name'] = data['artist']
+ albumartist_kwargs['name'] = data['artist']
+
+ if 'albumartist' in data:
+ albumartist_kwargs['name'] = data['albumartist']
if 'album' in data:
album_kwargs['name'] = data['album']
- album = Album(**album_kwargs)
- track_kwargs['album'] = album
if 'title' in data:
track_kwargs['name'] = data['title']
+ if 'musicbrainz_trackid' in data:
+ track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid']
+
+ if 'musicbrainz_albumid' in data:
+ album_kwargs['musicbrainz_id'] = data['musicbrainz_albumid']
+
+ if 'musicbrainz_artistid' in data:
+ artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid']
+
+ if 'musicbrainz_albumartistid' in data:
+ albumartist_kwargs['musicbrainz_id'] = data['musicbrainz_albumartistid']
+
if data['file'][0] == '/':
path = data['file'][1:]
else:
path = data['file']
+ if artist_kwargs:
+ artist = Artist(**artist_kwargs)
+ track_kwargs['artists'] = [artist]
+
+ if albumartist_kwargs:
+ albumartist = Artist(**albumartist_kwargs)
+ album_kwargs['artists'] = [albumartist]
+
+ if album_kwargs:
+ album = Album(**album_kwargs)
+ track_kwargs['album'] = album
+
track_kwargs['uri'] = path_to_uri(music_dir, path)
track_kwargs['length'] = int(data.get('time', 0)) * 1000
diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py
new file mode 100644
index 00000000..d36f6250
--- /dev/null
+++ b/mopidy/backends/spotify/__init__.py
@@ -0,0 +1,74 @@
+import logging
+
+from mopidy import settings
+from mopidy.backends.base import (Backend, CurrentPlaylistController,
+ LibraryController, PlaybackController, StoredPlaylistsController)
+
+logger = logging.getLogger('mopidy.backends.spotify')
+
+ENCODING = 'utf-8'
+
+class SpotifyBackend(Backend):
+ """
+ A backend for playing music from the `Spotify `_
+ music streaming service. The backend uses the official `libspotify
+ `_ library and the
+ `pyspotify `_ Python bindings for
+ libspotify.
+
+ .. note::
+
+ This product uses SPOTIFY(R) CORE but is not endorsed, certified or
+ otherwise approved in any way by Spotify. Spotify is the registered
+ trade mark of the Spotify Group.
+
+ **Issues:**
+ http://github.com/mopidy/mopidy/issues/labels/backend-spotify
+
+ **Settings:**
+
+ - :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`
+ - :attr:`mopidy.settings.SPOTIFY_USERNAME`
+ - :attr:`mopidy.settings.SPOTIFY_PASSWORD`
+ """
+
+ # Imports inside methods are to prevent loading of __init__.py to fail on
+ # missing spotify dependencies.
+
+ def __init__(self, *args, **kwargs):
+ from .library import SpotifyLibraryProvider
+ from .playback import SpotifyPlaybackProvider
+ from .stored_playlists import SpotifyStoredPlaylistsProvider
+
+ super(SpotifyBackend, self).__init__(*args, **kwargs)
+
+ self.current_playlist = CurrentPlaylistController(backend=self)
+
+ library_provider = SpotifyLibraryProvider(backend=self)
+ self.library = LibraryController(backend=self,
+ provider=library_provider)
+
+ playback_provider = SpotifyPlaybackProvider(backend=self)
+ self.playback = PlaybackController(backend=self,
+ provider=playback_provider)
+
+ stored_playlists_provider = SpotifyStoredPlaylistsProvider(
+ backend=self)
+ self.stored_playlists = StoredPlaylistsController(backend=self,
+ provider=stored_playlists_provider)
+
+ self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
+
+ self.spotify = self._connect()
+
+ def _connect(self):
+ from .session_manager import SpotifySessionManager
+
+ logger.info(u'Mopidy uses SPOTIFY(R) CORE')
+ logger.debug(u'Connecting to Spotify')
+ spotify = SpotifySessionManager(
+ settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,
+ core_queue=self.core_queue,
+ output=self.output)
+ spotify.start()
+ return spotify
diff --git a/mopidy/backends/libspotify/library.py b/mopidy/backends/spotify/library.py
similarity index 84%
rename from mopidy/backends/libspotify/library.py
rename to mopidy/backends/spotify/library.py
index 972eaf03..16391473 100644
--- a/mopidy/backends/libspotify/library.py
+++ b/mopidy/backends/spotify/library.py
@@ -3,14 +3,14 @@ import multiprocessing
from spotify import Link, SpotifyError
-from mopidy.backends.base import BaseLibraryController
-from mopidy.backends.libspotify import ENCODING
-from mopidy.backends.libspotify.translator import LibspotifyTranslator
+from mopidy.backends.base import BaseLibraryProvider
+from mopidy.backends.spotify import ENCODING
+from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist
-logger = logging.getLogger('mopidy.backends.libspotify.library')
+logger = logging.getLogger('mopidy.backends.spotify.library')
-class LibspotifyLibraryController(BaseLibraryController):
+class SpotifyLibraryProvider(BaseLibraryProvider):
def find_exact(self, **query):
return self.search(**query)
@@ -20,7 +20,7 @@ class LibspotifyLibraryController(BaseLibraryController):
# TODO Block until metadata_updated callback is called. Before that
# the track will be unloaded, unless it's already in the stored
# playlists.
- return LibspotifyTranslator.to_mopidy_track(spotify_track)
+ return SpotifyTranslator.to_mopidy_track(spotify_track)
except SpotifyError as e:
logger.warning(u'Failed to lookup: %s', uri, e)
return None
diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/spotify/playback.py
similarity index 68%
rename from mopidy/backends/libspotify/playback.py
rename to mopidy/backends/spotify/playback.py
index 39c56bf6..a066d90e 100644
--- a/mopidy/backends/libspotify/playback.py
+++ b/mopidy/backends/spotify/playback.py
@@ -2,17 +2,17 @@ import logging
from spotify import Link, SpotifyError
-from mopidy.backends.base import BasePlaybackController
+from mopidy.backends.base import BasePlaybackProvider
-logger = logging.getLogger('mopidy.backends.libspotify.playback')
+logger = logging.getLogger('mopidy.backends.spotify.playback')
-class LibspotifyPlaybackController(BasePlaybackController):
- def _pause(self):
+class SpotifyPlaybackProvider(BasePlaybackProvider):
+ def pause(self):
return self.backend.output.set_state('PAUSED')
- def _play(self, track):
+ def play(self, track):
self.backend.output.set_state('READY')
- if self.state == self.PLAYING:
+ if self.backend.playback.state == self.backend.playback.PLAYING:
self.backend.spotify.session.play(0)
if track.uri is None:
return False
@@ -26,16 +26,16 @@ class LibspotifyPlaybackController(BasePlaybackController):
logger.warning('Play %s failed: %s', track.uri, e)
return False
- def _resume(self):
- return self._seek(self.time_position)
+ def resume(self):
+ return self.seek(self.backend.playback.time_position)
- def _seek(self, time_position):
+ def seek(self, time_position):
self.backend.output.set_state('READY')
self.backend.spotify.session.seek(time_position)
self.backend.output.set_state('PLAYING')
return True
- def _stop(self):
+ def stop(self):
result = self.backend.output.set_state('READY')
self.backend.spotify.session.play(0)
return result
diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/spotify/session_manager.py
similarity index 70%
rename from mopidy/backends/libspotify/session_manager.py
rename to mopidy/backends/spotify/session_manager.py
index 45841350..9736f2eb 100644
--- a/mopidy/backends/libspotify/session_manager.py
+++ b/mopidy/backends/spotify/session_manager.py
@@ -2,28 +2,29 @@ import logging
import os
import threading
-from spotify.manager import SpotifySessionManager
+import spotify.manager
from mopidy import get_version, settings
-from mopidy.backends.libspotify.translator import LibspotifyTranslator
+from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist
from mopidy.utils.process import BaseThread
-logger = logging.getLogger('mopidy.backends.libspotify.session_manager')
+logger = logging.getLogger('mopidy.backends.spotify.session_manager')
# pylint: disable = R0901
-# LibspotifySessionManager: Too many ancestors (9/7)
+# SpotifySessionManager: Too many ancestors (9/7)
-class LibspotifySessionManager(SpotifySessionManager, BaseThread):
- cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
- settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
+class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
+ cache_location = settings.SPOTIFY_CACHE_PATH
+ settings_location = settings.SPOTIFY_CACHE_PATH
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
user_agent = 'Mopidy %s' % get_version()
def __init__(self, username, password, core_queue, output):
- SpotifySessionManager.__init__(self, username, password)
+ spotify.manager.SpotifySessionManager.__init__(
+ self, username, password)
BaseThread.__init__(self, core_queue)
- self.name = 'LibspotifySMThread'
+ self.name = 'SpotifySMThread'
self.output = output
self.connected = threading.Event()
self.session = None
@@ -35,6 +36,12 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
"""Callback used by pyspotify"""
logger.info(u'Connected to Spotify')
self.session = session
+ if settings.SPOTIFY_HIGH_BITRATE:
+ logger.debug(u'Preferring high bitrate from Spotify')
+ self.session.set_preferred_bitrate(1)
+ else:
+ logger.debug(u'Preferring normal bitrate from Spotify')
+ self.session.set_preferred_bitrate(0)
self.connected.set()
def logged_out(self, session):
@@ -43,15 +50,8 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
def metadata_updated(self, session):
"""Callback used by pyspotify"""
- logger.debug(u'Metadata updated, refreshing stored playlists')
- playlists = []
- for spotify_playlist in session.playlist_container():
- playlists.append(
- LibspotifyTranslator.to_mopidy_playlist(spotify_playlist))
- self.core_queue.put({
- 'command': 'set_stored_playlists',
- 'playlists': playlists,
- })
+ logger.debug(u'Metadata updated')
+ self.refresh_stored_playlists()
def connection_error(self, session, error):
"""Callback used by pyspotify"""
@@ -99,12 +99,26 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
logger.debug(u'End of data stream reached')
self.output.end_of_data_stream()
+ def refresh_stored_playlists(self):
+ """Refresh the stored playlists in the backend with fresh meta data
+ from Spotify"""
+ playlists = []
+ for spotify_playlist in self.session.playlist_container():
+ playlists.append(
+ SpotifyTranslator.to_mopidy_playlist(spotify_playlist))
+ playlists = filter(None, playlists)
+ self.core_queue.put({
+ 'command': 'set_stored_playlists',
+ 'playlists': playlists,
+ })
+ logger.debug(u'Refreshed %d stored playlist(s)', len(playlists))
+
def search(self, query, connection):
"""Search method used by Mopidy backend"""
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
playlist = Playlist(tracks=[
- LibspotifyTranslator.to_mopidy_track(t)
+ SpotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
connection.send(playlist)
self.connected.wait()
diff --git a/mopidy/backends/libspotify/spotify_appkey.key b/mopidy/backends/spotify/spotify_appkey.key
similarity index 100%
rename from mopidy/backends/libspotify/spotify_appkey.key
rename to mopidy/backends/spotify/spotify_appkey.key
diff --git a/mopidy/backends/libspotify/stored_playlists.py b/mopidy/backends/spotify/stored_playlists.py
similarity index 69%
rename from mopidy/backends/libspotify/stored_playlists.py
rename to mopidy/backends/spotify/stored_playlists.py
index 3339578c..054e2bd1 100644
--- a/mopidy/backends/libspotify/stored_playlists.py
+++ b/mopidy/backends/spotify/stored_playlists.py
@@ -1,6 +1,6 @@
-from mopidy.backends.base import BaseStoredPlaylistsController
+from mopidy.backends.base import BaseStoredPlaylistsProvider
-class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController):
+class SpotifyStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
def create(self, name):
pass # TODO
diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/spotify/translator.py
similarity index 62%
rename from mopidy/backends/libspotify/translator.py
rename to mopidy/backends/spotify/translator.py
index ff8f3c5c..50ee07d1 100644
--- a/mopidy/backends/libspotify/translator.py
+++ b/mopidy/backends/spotify/translator.py
@@ -1,11 +1,15 @@
import datetime as dt
+import logging
-from spotify import Link
+from spotify import Link, SpotifyError
+from mopidy import settings
+from mopidy.backends.spotify import ENCODING
from mopidy.models import Artist, Album, Track, Playlist
-from mopidy.backends.libspotify import ENCODING
-class LibspotifyTranslator(object):
+logger = logging.getLogger('mopidy.backends.spotify.translator')
+
+class SpotifyTranslator(object):
@classmethod
def to_mopidy_artist(cls, spotify_artist):
if not spotify_artist.is_loaded():
@@ -39,15 +43,22 @@ class LibspotifyTranslator(object):
track_no=spotify_track.index(),
date=date,
length=spotify_track.duration(),
- bitrate=160,
+ bitrate=(settings.SPOTIFY_HIGH_BITRATE and 320 or 160),
)
@classmethod
def to_mopidy_playlist(cls, 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=[cls.to_mopidy_track(t) for t in spotify_playlist],
- )
+ # FIXME Replace this try-except with a check on the playlist type,
+ # which is currently not supported by pyspotify, to avoid handling
+ # playlist folder boundaries like normal playlists.
+ try:
+ return Playlist(
+ uri=str(Link.from_playlist(spotify_playlist)),
+ name=spotify_playlist.name().decode(ENCODING),
+ tracks=[cls.to_mopidy_track(t) for t in spotify_playlist],
+ )
+ except SpotifyError, e:
+ logger.warning(u'Failed translating Spotify playlist '
+ '(probably a playlist folder boundary): %s', e)
diff --git a/mopidy/core.py b/mopidy/core.py
index 0be6b96f..1a4ed7cc 100644
--- a/mopidy/core.py
+++ b/mopidy/core.py
@@ -18,6 +18,7 @@ class CoreProcess(BaseThread):
super(CoreProcess, self).__init__(self.core_queue)
self.name = 'CoreProcess'
self.options = self.parse_options()
+ self.gobject_loop = None
self.output = None
self.backend = None
self.frontends = []
diff --git a/mopidy/frontends/base.py b/mopidy/frontends/base.py
index 92545b73..bf1c9bda 100644
--- a/mopidy/frontends/base.py
+++ b/mopidy/frontends/base.py
@@ -5,7 +5,7 @@ class BaseFrontend(object):
:param core_queue: queue for messaging the core
:type core_queue: :class:`multiprocessing.Queue`
:param backend: the backend
- :type backend: :class:`mopidy.backends.base.BaseBackend`
+ :type backend: :class:`mopidy.backends.base.Backend`
"""
def __init__(self, core_queue, backend):
@@ -13,17 +13,27 @@ class BaseFrontend(object):
self.backend = backend
def start(self):
- """Start the frontend."""
+ """
+ Start the frontend.
+
+ *MAY be implemented by subclass.*
+ """
pass
def destroy(self):
- """Destroy the frontend."""
+ """
+ Destroy the frontend.
+
+ *MAY be implemented by subclass.*
+ """
pass
def process_message(self, message):
"""
Process messages for the frontend.
+ *MUST be implemented by subclass.*
+
:param message: the message
:type message: dict
"""
diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py
index 0cf534af..60c2d708 100644
--- a/mopidy/frontends/lastfm.py
+++ b/mopidy/frontends/lastfm.py
@@ -15,13 +15,8 @@ from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.frontends.lastfm')
-CLIENT_ID = u'mop'
-CLIENT_VERSION = get_version()
-
-# pylast raises UnicodeEncodeError on conversion from unicode objects to
-# ascii-encoded bytestrings, so we explicitly encode as utf-8 before passing
-# strings to pylast.
-ENCODING = u'utf-8'
+API_KEY = '2236babefa8ebb3d93ea467560d00d04'
+API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
class LastfmFrontend(BaseFrontend):
"""
@@ -34,7 +29,7 @@ class LastfmFrontend(BaseFrontend):
**Dependencies:**
- - `pylast `_ >= 0.4.30
+ - `pylast `_ >= 0.5
**Settings:**
@@ -54,7 +49,8 @@ class LastfmFrontend(BaseFrontend):
self.thread.destroy()
def process_message(self, message):
- self.connection.send(message)
+ if self.thread.is_alive():
+ self.connection.send(message)
class LastfmFrontendThread(BaseThread):
@@ -63,12 +59,11 @@ class LastfmFrontendThread(BaseThread):
self.name = u'LastfmFrontendThread'
self.connection = connection
self.lastfm = None
- self.scrobbler = None
self.last_start_time = None
def run_inside_try(self):
self.setup()
- while True:
+ while self.lastfm is not None:
self.connection.poll(None)
message = self.connection.recv()
self.process_message(message)
@@ -77,10 +72,9 @@ class LastfmFrontendThread(BaseThread):
try:
username = settings.LASTFM_USERNAME
password_hash = pylast.md5(settings.LASTFM_PASSWORD)
- self.lastfm = pylast.get_lastfm_network(
+ self.lastfm = pylast.LastFMNetwork(
+ api_key=API_KEY, api_secret=API_SECRET,
username=username, password_hash=password_hash)
- self.scrobbler = self.lastfm.get_scrobbler(
- CLIENT_ID, CLIENT_VERSION)
logger.info(u'Connected to Last.fm')
except SettingsError as e:
logger.info(u'Last.fm scrobbler not started')
@@ -102,12 +96,13 @@ class LastfmFrontendThread(BaseThread):
self.last_start_time = int(time.time())
logger.debug(u'Now playing track: %s - %s', artists, track.name)
try:
- self.scrobbler.report_now_playing(
- artists.encode(ENCODING),
- track.name.encode(ENCODING),
- album=track.album.name.encode(ENCODING),
- duration=duration,
- track_number=track.track_no)
+ self.lastfm.update_now_playing(
+ artists,
+ track.name,
+ album=track.album.name,
+ duration=str(duration),
+ track_number=str(track.track_no),
+ mbid=(track.musicbrainz_id or ''))
except (pylast.ScrobblingError, socket.error) as e:
logger.warning(u'Last.fm now playing error: %s', e)
@@ -126,14 +121,13 @@ class LastfmFrontendThread(BaseThread):
self.last_start_time = int(time.time()) - duration
logger.debug(u'Scrobbling track: %s - %s', artists, track.name)
try:
- self.scrobbler.scrobble(
- artists.encode(ENCODING),
- track.name.encode(ENCODING),
- time_started=self.last_start_time,
- source=pylast.SCROBBLE_SOURCE_USER,
- mode=pylast.SCROBBLE_MODE_PLAYED,
- duration=duration,
- album=track.album.name.encode(ENCODING),
- track_number=track.track_no)
+ self.lastfm.scrobble(
+ artists,
+ track.name,
+ str(self.last_start_time),
+ album=track.album.name,
+ track_number=str(track.track_no),
+ duration=str(duration),
+ mbid=(track.musicbrainz_id or ''))
except (pylast.ScrobblingError, socket.error) as e:
logger.warning(u'Last.fm scrobbling error: %s', e)
diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py
index 2f5dd29e..19922bc3 100644
--- a/mopidy/frontends/mpd/protocol/playback.py
+++ b/mopidy/frontends/mpd/protocol/playback.py
@@ -293,6 +293,7 @@ def replay_gain_status(frontend):
"""
return u'off' # TODO
+@handle_pattern(r'^seek (?P\d+) (?P\d+)$')
@handle_pattern(r'^seek "(?P\d+)" "(?P\d+)"$')
def seek(frontend, songpos, seconds):
"""
@@ -302,6 +303,10 @@ def seek(frontend, songpos, seconds):
Seeks to the position ``TIME`` (in seconds) of entry ``SONGPOS`` in
the playlist.
+
+ *Droid MPD:*
+
+ - issues ``seek 1 120`` without quotes around the arguments.
"""
if frontend.backend.playback.current_playlist_position != songpos:
playpos(frontend, songpos)
@@ -320,6 +325,7 @@ def seekid(frontend, cpid, seconds):
playid(frontend, cpid)
frontend.backend.playback.seek(int(seconds) * 1000)
+@handle_pattern(r'^setvol (?P[-+]*\d+)$')
@handle_pattern(r'^setvol "(?P[-+]*\d+)"$')
def setvol(frontend, volume):
"""
@@ -328,6 +334,10 @@ def setvol(frontend, volume):
``setvol {VOL}``
Sets volume to ``VOL``, the range of volume is 0-100.
+
+ *Droid MPD:*
+
+ - issues ``setvol 50`` without quotes around the argument.
"""
volume = int(volume)
if volume < 0:
diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py
index 2b1adf50..3ead23c7 100644
--- a/mopidy/frontends/mpd/translator.py
+++ b/mopidy/frontends/mpd/translator.py
@@ -4,9 +4,9 @@ import re
from mopidy import settings
from mopidy.utils.path import mtime as get_mtime
from mopidy.frontends.mpd import protocol
-from mopidy.utils.path import path_to_uri, uri_to_path, split_path
+from mopidy.utils.path import uri_to_path, split_path
-def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False):
+def track_to_mpd_format(track, position=None, cpid=None):
"""
Format track for output to MPD client.
@@ -41,10 +41,22 @@ def track_to_mpd_format(track, position=None, cpid=None, key=False, mtime=False)
if position is not None and cpid is not None:
result.append(('Pos', position))
result.append(('Id', cpid))
- if key and track.uri:
- result.insert(0, ('key', os.path.basename(uri_to_path(track.uri))))
- if mtime and track.uri:
- result.append(('mtime', get_mtime(uri_to_path(track.uri))))
+ if track.album is not None and track.album.musicbrainz_id is not None:
+ result.append(('MUSICBRAINZ_ALBUMID', track.album.musicbrainz_id))
+ # FIXME don't use first and best artist?
+ # FIXME don't duplicate following code?
+ if track.album is not None and track.album.artists:
+ artists = filter(lambda a: a.musicbrainz_id is not None,
+ track.album.artists)
+ if artists:
+ result.append(
+ ('MUSICBRAINZ_ALBUMARTISTID', artists[0].musicbrainz_id))
+ if track.artists:
+ artists = filter(lambda a: a.musicbrainz_id is not None, track.artists)
+ if artists:
+ result.append(('MUSICBRAINZ_ARTISTID', artists[0].musicbrainz_id))
+ if track.musicbrainz_id is not None:
+ result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id))
return result
MPD_KEY_ORDER = '''
@@ -127,9 +139,11 @@ def tracks_to_tag_cache_format(tracks):
return result
def _add_to_tag_cache(result, folders, files):
+ music_folder = settings.LOCAL_MUSIC_PATH
+ regexp = '^' + re.escape(music_folder).rstrip('/') + '/?'
+
for path, entry in folders.items():
name = os.path.split(path)[1]
- music_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER)
mtime = get_mtime(os.path.join(music_folder, path))
result.append(('directory', path))
result.append(('mtime', mtime))
@@ -139,8 +153,12 @@ def _add_to_tag_cache(result, folders, files):
result.append(('songList begin',))
for track in files:
- track_result = track_to_mpd_format(track, key=True, mtime=True)
- track_result = order_mpd_track_info(track_result)
+ track_result = dict(track_to_mpd_format(track))
+ path = uri_to_path(track_result['file'])
+ track_result['mtime'] = get_mtime(path)
+ track_result['file'] = re.sub(regexp, '', path)
+ track_result['key'] = os.path.basename(track_result['file'])
+ track_result = order_mpd_track_info(track_result.items())
result.extend(track_result)
result.append(('songList end',))
@@ -150,7 +168,7 @@ def tracks_to_directory_tree(tracks):
path = u''
current = directories
- local_folder = os.path.expanduser(settings.LOCAL_MUSIC_FOLDER)
+ local_folder = settings.LOCAL_MUSIC_PATH
track_path = uri_to_path(track.uri)
track_path = re.sub('^' + re.escape(local_folder), '', track_path)
track_dir = os.path.dirname(track_path)
diff --git a/mopidy/mixers/__init__.py b/mopidy/mixers/__init__.py
index 332718a6..e69de29b 100644
--- a/mopidy/mixers/__init__.py
+++ b/mopidy/mixers/__init__.py
@@ -1,55 +0,0 @@
-from mopidy import settings
-
-class BaseMixer(object):
- """
- :param backend: a backend instance
- :type mixer: :class:`mopidy.backends.base.BaseBackend`
-
- **Settings:**
-
- - :attr:`mopidy.settings.MIXER_MAX_VOLUME`
- """
-
- def __init__(self, backend, *args, **kwargs):
- self.backend = backend
- self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
-
- @property
- def volume(self):
- """
- The audio volume
-
- Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
- equal to 0. Values above 100 is equal to 100.
- """
- if self._get_volume() is None:
- return None
- return int(self._get_volume() / self.amplification_factor)
-
- @volume.setter
- def volume(self, volume):
- volume = int(int(volume) * self.amplification_factor)
- if volume < 0:
- volume = 0
- elif volume > 100:
- volume = 100
- self._set_volume(volume)
-
- def destroy(self):
- pass
-
- def _get_volume(self):
- """
- Return volume as integer in range [0, 100]. :class:`None` if unknown.
-
- *Must be implemented by subclass.*
- """
- raise NotImplementedError
-
- def _set_volume(self, volume):
- """
- Set volume as integer in range [0, 100].
-
- *Must be implemented by subclass.*
- """
- raise NotImplementedError
diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py
index 6eef6da4..4aa5952f 100644
--- a/mopidy/mixers/alsa.py
+++ b/mopidy/mixers/alsa.py
@@ -2,7 +2,7 @@ import alsaaudio
import logging
from mopidy import settings
-from mopidy.mixers import BaseMixer
+from mopidy.mixers.base import BaseMixer
logger = logging.getLogger('mopidy.mixers.alsa')
@@ -11,6 +11,10 @@ class AlsaMixer(BaseMixer):
Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control
volume.
+ **Dependencies:**
+
+ - pyalsaaudio >= 0.2 (python-alsaaudio on Debian/Ubuntu)
+
**Settings:**
- :attr:`mopidy.settings.MIXER_ALSA_CONTROL`
diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py
new file mode 100644
index 00000000..f7f9525c
--- /dev/null
+++ b/mopidy/mixers/base.py
@@ -0,0 +1,55 @@
+from mopidy import settings
+
+class BaseMixer(object):
+ """
+ :param backend: a backend instance
+ :type backend: :class:`mopidy.backends.base.Backend`
+
+ **Settings:**
+
+ - :attr:`mopidy.settings.MIXER_MAX_VOLUME`
+ """
+
+ def __init__(self, backend, *args, **kwargs):
+ self.backend = backend
+ self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
+
+ @property
+ def volume(self):
+ """
+ The audio volume
+
+ Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
+ equal to 0. Values above 100 is equal to 100.
+ """
+ if self._get_volume() is None:
+ return None
+ return int(self._get_volume() / self.amplification_factor)
+
+ @volume.setter
+ def volume(self, volume):
+ volume = int(int(volume) * self.amplification_factor)
+ if volume < 0:
+ volume = 0
+ elif volume > 100:
+ volume = 100
+ self._set_volume(volume)
+
+ def destroy(self):
+ pass
+
+ def _get_volume(self):
+ """
+ Return volume as integer in range [0, 100]. :class:`None` if unknown.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def _set_volume(self, volume):
+ """
+ Set volume as integer in range [0, 100].
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py
index 32750f60..f0712f95 100644
--- a/mopidy/mixers/denon.py
+++ b/mopidy/mixers/denon.py
@@ -1,10 +1,8 @@
import logging
from threading import Lock
-from serial import Serial
-
from mopidy import settings
-from mopidy.mixers import BaseMixer
+from mopidy.mixers.base import BaseMixer
logger = logging.getLogger(u'mopidy.mixers.denon')
@@ -33,8 +31,11 @@ class DenonMixer(BaseMixer):
"""
super(DenonMixer, self).__init__(*args, **kwargs)
device = kwargs.get('device', None)
- self._device = device or Serial(port=settings.MIXER_EXT_PORT,
- timeout=0.2)
+ if device:
+ self._device = device
+ else:
+ from serial import Serial
+ self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
self._volume = 0
self._lock = Lock()
diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py
index b0ea0e47..12a8137e 100644
--- a/mopidy/mixers/dummy.py
+++ b/mopidy/mixers/dummy.py
@@ -1,4 +1,4 @@
-from mopidy.mixers import BaseMixer
+from mopidy.mixers.base import BaseMixer
class DummyMixer(BaseMixer):
"""Mixer which just stores and reports the chosen volume."""
diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py
index 333690ea..9dca3690 100644
--- a/mopidy/mixers/gstreamer_software.py
+++ b/mopidy/mixers/gstreamer_software.py
@@ -1,4 +1,4 @@
-from mopidy.mixers import BaseMixer
+from mopidy.mixers.base import BaseMixer
class GStreamerSoftwareMixer(BaseMixer):
"""Mixer which uses GStreamer to control volume in software."""
diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py
index 8caa9700..3215a761 100644
--- a/mopidy/mixers/nad.py
+++ b/mopidy/mixers/nad.py
@@ -3,7 +3,7 @@ from serial import Serial
from multiprocessing import Pipe
from mopidy import settings
-from mopidy.mixers import BaseMixer
+from mopidy.mixers.base import BaseMixer
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.mixers.nad')
diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py
index 3aeaed5c..2ea04cf2 100644
--- a/mopidy/mixers/osa.py
+++ b/mopidy/mixers/osa.py
@@ -1,10 +1,21 @@
from subprocess import Popen, PIPE
import time
-from mopidy.mixers import BaseMixer
+from mopidy.mixers.base import BaseMixer
class OsaMixer(BaseMixer):
- """Mixer which uses ``osascript`` on OS X to control volume."""
+ """
+ Mixer which uses ``osascript`` on OS X to control volume.
+
+ **Dependencies:**
+
+ - None
+
+ **Settings:**
+
+ - None
+
+ """
CACHE_TTL = 30
diff --git a/mopidy/models.py b/mopidy/models.py
index c5877657..8e7585f1 100644
--- a/mopidy/models.py
+++ b/mopidy/models.py
@@ -38,6 +38,32 @@ class ImmutableObject(object):
def __ne__(self, other):
return not self.__eq__(other)
+ def copy(self, **values):
+ """
+ Copy the model with ``field`` updated to new value.
+
+ Examples::
+
+ # Returns a track with a new name
+ Track(name='foo').copy(name='bar')
+ # Return an album with a new number of tracks
+ Album(num_tracks=2).copy(num_tracks=5)
+
+ :param values: the model fields to modify
+ :type values: dict
+ :rtype: new instance of the model being copied
+ """
+ data = {}
+ for key in self.__dict__.keys():
+ public_key = key.lstrip('_')
+ data[public_key] = values.pop(public_key, self.__dict__[key])
+ for key in values.keys():
+ if hasattr(self, key):
+ data[key] = values.pop(key)
+ if values:
+ raise TypeError("copy() got an unexpected keyword argument '%s'"
+ % key)
+ return self.__class__(**data)
class Artist(ImmutableObject):
"""
@@ -45,6 +71,8 @@ class Artist(ImmutableObject):
:type uri: string
:param name: artist name
:type name: string
+ :param musicbrainz_id: MusicBrainz ID
+ :type musicbrainz_id: string
"""
#: The artist URI. Read-only.
@@ -53,6 +81,9 @@ class Artist(ImmutableObject):
#: The artist name. Read-only.
name = None
+ #: The MusicBrainz ID of the artist. Read-only.
+ musicbrainz_id = None
+
class Album(ImmutableObject):
"""
@@ -64,6 +95,8 @@ class Album(ImmutableObject):
:type artists: list of :class:`Artist`
:param num_tracks: number of tracks in album
:type num_tracks: integer
+ :param musicbrainz_id: MusicBrainz ID
+ :type musicbrainz_id: string
"""
#: The album URI. Read-only.
@@ -75,6 +108,9 @@ class Album(ImmutableObject):
#: The number of tracks in the album. Read-only.
num_tracks = 0
+ #: The MusicBrainz ID of the album. Read-only.
+ musicbrainz_id = None
+
def __init__(self, *args, **kwargs):
self._artists = frozenset(kwargs.pop('artists', []))
super(Album, self).__init__(*args, **kwargs)
@@ -103,6 +139,8 @@ class Track(ImmutableObject):
:type length: integer
:param bitrate: bitrate in kbit/s
:type bitrate: integer
+ :param musicbrainz_id: MusicBrainz ID
+ :type musicbrainz_id: string
"""
#: The track URI. Read-only.
@@ -126,6 +164,9 @@ class Track(ImmutableObject):
#: The track's bitrate in kbit/s. Read-only.
bitrate = None
+ #: The MusicBrainz ID of the track. Read-only.
+ musicbrainz_id = None
+
def __init__(self, *args, **kwargs):
self._artists = frozenset(kwargs.pop('artists', []))
super(Track, self).__init__(*args, **kwargs)
@@ -178,31 +219,3 @@ class Playlist(ImmutableObject):
def mpd_format(self, *args, **kwargs):
return translator.playlist_to_mpd_format(self, *args, **kwargs)
-
- def with_(self, uri=None, name=None, tracks=None, last_modified=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
- :param last_modified: playlist's modification time
- :type last_modified: :class:`datetime.datetime`
- :rtype: :class:`Playlist`
- """
- if uri is None:
- uri = self.uri
- if name is None:
- name = self.name
- if tracks is None:
- tracks = self.tracks
- if last_modified is None:
- last_modified = self.last_modified
- return Playlist(uri=uri, name=name, tracks=tracks,
- last_modified=last_modified)
diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py
index bb312323..372d7d70 100644
--- a/mopidy/outputs/base.py
+++ b/mopidy/outputs/base.py
@@ -7,21 +7,35 @@ class BaseOutput(object):
self.core_queue = core_queue
def start(self):
- """Start the output."""
+ """
+ Start the output.
+
+ *MAY be implemented by subclasses.*
+ """
pass
def destroy(self):
- """Destroy the output."""
+ """
+ Destroy the output.
+
+ *MAY be implemented by subclasses.*
+ """
pass
def process_message(self, message):
- """Process messages with the output as destination."""
+ """
+ Process messages with the output as destination.
+
+ *MUST be implemented by subclass.*
+ """
raise NotImplementedError
def play_uri(self, uri):
"""
Play URI.
+ *MUST be implemented by subclass.*
+
:param uri: the URI to play
:type uri: string
:rtype: :class:`True` if successful, else :class:`False`
@@ -32,19 +46,27 @@ class BaseOutput(object):
"""
Deliver audio data to be played.
+ *MUST be implemented by subclass.*
+
:param capabilities: a GStreamer capabilities string
:type capabilities: string
"""
raise NotImplementedError
def end_of_data_stream(self):
- """Signal that the last audio data has been delivered."""
+ """
+ Signal that the last audio data has been delivered.
+
+ *MUST be implemented by subclass.*
+ """
raise NotImplementedError
def get_position(self):
"""
Get position in milliseconds.
+ *MUST be implemented by subclass.*
+
:rtype: int
"""
raise NotImplementedError
@@ -53,6 +75,8 @@ class BaseOutput(object):
"""
Set position in milliseconds.
+ *MUST be implemented by subclass.*
+
:param position: the position in milliseconds
:type volume: int
:rtype: :class:`True` if successful, else :class:`False`
@@ -63,6 +87,8 @@ class BaseOutput(object):
"""
Set playback state.
+ *MUST be implemented by subclass.*
+
:param state: the state
:type state: string
:rtype: :class:`True` if successful, else :class:`False`
@@ -73,6 +99,8 @@ class BaseOutput(object):
"""
Get volume level for software mixer.
+ *MUST be implemented by subclass.*
+
:rtype: int in range [0..100]
"""
raise NotImplementedError
@@ -81,6 +109,8 @@ class BaseOutput(object):
"""
Set volume level for software mixer.
+ *MUST be implemented by subclass.*
+
:param volume: the volume in the range [0..100]
:type volume: int
:rtype: :class:`True` if successful, else :class:`False`
diff --git a/mopidy/scanner.py b/mopidy/scanner.py
index 436598bd..93224331 100644
--- a/mopidy/scanner.py
+++ b/mopidy/scanner.py
@@ -5,10 +5,7 @@ import pygst
pygst.require('0.10')
import gst
-from os.path import abspath
import datetime
-import sys
-import threading
from mopidy.utils.path import path_to_uri, find_files
from mopidy.models import Track, Artist, Album
@@ -19,6 +16,8 @@ def translator(data):
artist_kwargs = {}
track_kwargs = {}
+ # FIXME replace with data.get('foo', None) ?
+
if 'album' in data:
album_kwargs['name'] = data['album']
@@ -26,7 +25,7 @@ def translator(data):
album_kwargs['num_tracks'] = data['track-count']
if 'artist' in data:
- artist_kwargs['name'] =data['artist']
+ artist_kwargs['name'] = data['artist']
if 'date' in data:
date = data['date']
@@ -42,6 +41,18 @@ def translator(data):
if 'album-artist' in data:
albumartist_kwargs['name'] = data['album-artist']
+ if 'musicbrainz-trackid' in data:
+ track_kwargs['musicbrainz_id'] = data['musicbrainz-trackid']
+
+ if 'musicbrainz-artistid' in data:
+ artist_kwargs['musicbrainz_id'] = data['musicbrainz-artistid']
+
+ if 'musicbrainz-albumid' in data:
+ album_kwargs['musicbrainz_id'] = data['musicbrainz-albumid']
+
+ if 'musicbrainz-albumartistid' in data:
+ albumartist_kwargs['musicbrainz_id'] = data['musicbrainz-albumartistid']
+
if albumartist_kwargs:
album_kwargs['artists'] = [Artist(**albumartist_kwargs)]
diff --git a/mopidy/settings.py b/mopidy/settings.py
index da08584e..b2ee5b12 100644
--- a/mopidy/settings.py
+++ b/mopidy/settings.py
@@ -12,12 +12,12 @@ Available settings and their default values.
#:
#: Default::
#:
-#: BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',)
+#: BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',)
#:
#: .. note::
#: Currently only the first backend in the list is used.
BACKENDS = (
- u'mopidy.backends.libspotify.LibspotifyBackend',
+ u'mopidy.backends.spotify.SpotifyBackend',
)
#: The log format used for informational logging.
@@ -77,8 +77,8 @@ LASTFM_PASSWORD = u''
#:
#: Default::
#:
-#: LOCAL_MUSIC_FOLDER = u'~/music'
-LOCAL_MUSIC_FOLDER = u'~/music'
+#: LOCAL_MUSIC_PATH = u'~/music'
+LOCAL_MUSIC_PATH = u'~/music'
#: Path to playlist folder with m3u files for local music.
#:
@@ -86,8 +86,8 @@ LOCAL_MUSIC_FOLDER = u'~/music'
#:
#: Default::
#:
-#: LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
-LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
+#: LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
+LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
#: Path to tag cache for local music.
#:
@@ -95,8 +95,8 @@ LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
#:
#: Default::
#:
-#: LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
-LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
+#: LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
+LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
#: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers.
#:
@@ -174,17 +174,26 @@ MPD_SERVER_PASSWORD = False
#: Default: 6600
MPD_SERVER_PORT = 6600
-#: Path to the libspotify cache.
+#: Path to the Spotify cache.
#:
-#: Used by :mod:`mopidy.backends.libspotify`.
-SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache'
+#: Used by :mod:`mopidy.backends.spotify`.
+SPOTIFY_CACHE_PATH = u'~/.mopidy/spotify_cache'
#: Your Spotify Premium username.
#:
-#: Used by :mod:`mopidy.backends.libspotify`.
+#: Used by :mod:`mopidy.backends.spotify`.
SPOTIFY_USERNAME = u''
#: Your Spotify Premium password.
#:
-#: Used by :mod:`mopidy.backends.libspotify`.
+#: Used by :mod:`mopidy.backends.spotify`.
SPOTIFY_PASSWORD = u''
+
+#: Do you prefer high bitrate (320k)?
+#:
+#: Used by :mod:`mopidy.backends.spotify`.
+#
+#: Default::
+#:
+#: SPOTIFY_HIGH_BITRATE = False # 160k
+SPOTIFY_HIGH_BITRATE = False
diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py
index b3669e38..540cb4fa 100644
--- a/mopidy/utils/path.py
+++ b/mopidy/utils/path.py
@@ -22,7 +22,6 @@ def get_or_create_file(filename):
def path_to_uri(*paths):
path = os.path.join(*paths)
- #path = os.path.expanduser(path) # FIXME Waiting for test case?
path = path.encode('utf-8')
if sys.platform == 'win32':
return 'file:' + urllib.pathname2url(path)
@@ -45,21 +44,21 @@ def split_path(path):
break
return parts
+# pylint: disable = W0612
+# Unused variable 'dirnames'
def find_files(path):
- path = os.path.expanduser(path)
if os.path.isfile(path):
- filename = os.path.abspath(path)
- if not isinstance(filename, unicode):
- filename = filename.decode('utf-8')
- yield filename
+ if not isinstance(path, unicode):
+ path = path.decode('utf-8')
+ yield path
else:
for dirpath, dirnames, filenames in os.walk(path):
for filename in filenames:
- dirpath = os.path.abspath(dirpath)
filename = os.path.join(dirpath, filename)
if not isinstance(filename, unicode):
filename = filename.decode('utf-8')
yield filename
+# pylint: enable = W0612
class Mtime(object):
def __init__(self):
diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py
index ac75cb70..2ec0f716 100644
--- a/mopidy/utils/settings.py
+++ b/mopidy/utils/settings.py
@@ -51,6 +51,9 @@ class SettingsProxy(object):
value = self.current[attr]
if type(value) != bool and not value:
raise SettingsError(u'Setting "%s" is empty.' % attr)
+ if attr.endswith('_PATH') or attr.endswith('_FILE'):
+ value = os.path.expanduser(value)
+ value = os.path.abspath(value)
return value
def __setattr__(self, attr, value):
@@ -94,10 +97,14 @@ def validate_settings(defaults, settings):
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
'FRONTEND': 'FRONTENDS',
+ 'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH',
+ 'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
+ 'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
'SERVER': None,
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
'SERVER_PORT': 'MPD_SERVER_PORT',
'SPOTIFY_LIB_APPKEY': None,
+ 'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH',
}
for setting, value in settings.iteritems():
diff --git a/requirements-lastfm.txt b/requirements-lastfm.txt
deleted file mode 100644
index 642735be..00000000
--- a/requirements-lastfm.txt
+++ /dev/null
@@ -1 +0,0 @@
-pylast >= 0.4.30
diff --git a/requirements/README.rst b/requirements/README.rst
new file mode 100644
index 00000000..cc061a7b
--- /dev/null
+++ b/requirements/README.rst
@@ -0,0 +1,11 @@
+*********************
+pip requirement files
+*********************
+
+The files found here are `requirement files
+`_ that may be used with `pip
+`_.
+
+To install the dependencies found in one of these files, simply run e.g.::
+
+ pip install -r requirements/tests.txt
diff --git a/requirements-docs.txt b/requirements/docs.txt
similarity index 100%
rename from requirements-docs.txt
rename to requirements/docs.txt
diff --git a/requirements-external-mixers.txt b/requirements/external_mixers.txt
similarity index 100%
rename from requirements-external-mixers.txt
rename to requirements/external_mixers.txt
diff --git a/requirements/lastfm.txt b/requirements/lastfm.txt
new file mode 100644
index 00000000..887a0f0d
--- /dev/null
+++ b/requirements/lastfm.txt
@@ -0,0 +1 @@
+pylast >= 0.5
diff --git a/requirements-tests.txt b/requirements/tests.txt
similarity index 100%
rename from requirements-tests.txt
rename to requirements/tests.txt
diff --git a/setup.py b/setup.py
index d77be3cd..d9d6af42 100644
--- a/setup.py
+++ b/setup.py
@@ -77,7 +77,7 @@ setup(
author='Stein Magnus Jodal',
author_email='stein.magnus@jodal.no',
packages=packages,
- package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']},
+ package_data={'mopidy': ['backends/spotify/spotify_appkey.key']},
cmdclass=cmdclasses,
data_files=data_files,
scripts=['bin/mopidy', 'bin/mopidy-scan'],
diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py
index 05f08e18..2b6cb84e 100644
--- a/tests/backends/base/current_playlist.py
+++ b/tests/backends/base/current_playlist.py
@@ -9,7 +9,7 @@ from mopidy.utils import get_class
from tests.backends.base import populate_playlist
-class BaseCurrentPlaylistControllerTest(object):
+class CurrentPlaylistControllerTest(object):
tracks = []
def setUp(self):
diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py
index 1239bd08..71f62147 100644
--- a/tests/backends/base/library.py
+++ b/tests/backends/base/library.py
@@ -3,7 +3,7 @@ from mopidy.models import Playlist, Track, Album, Artist
from tests import SkipTest, data_folder
-class BaseLibraryControllerTest(object):
+class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
albums = [Album(name='album1', artists=artists[:1]),
Album(name='album2', artists=artists[1:2]),
diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py
index 4caaf44b..26662f96 100644
--- a/tests/backends/base/playback.py
+++ b/tests/backends/base/playback.py
@@ -13,7 +13,7 @@ from tests.backends.base import populate_playlist
# TODO Test 'playlist repeat', e.g. repeat=1,single=0
-class BasePlaybackControllerTest(object):
+class PlaybackControllerTest(object):
tracks = []
def setUp(self):
@@ -104,8 +104,8 @@ class BasePlaybackControllerTest(object):
@populate_playlist
def test_play_skips_to_next_track_on_failure(self):
- # If _play() returns False, it is a failure.
- self.playback._play = lambda track: track != self.tracks[0]
+ # If provider.play() returns False, it is a failure.
+ self.playback.provider.play = lambda track: track != self.tracks[0]
self.playback.play()
self.assertNotEqual(self.playback.current_track, self.tracks[0])
self.assertEqual(self.playback.current_track, self.tracks[1])
@@ -164,8 +164,8 @@ class BasePlaybackControllerTest(object):
@populate_playlist
def test_previous_skips_to_previous_track_on_failure(self):
- # If _play() returns False, it is a failure.
- self.playback._play = lambda track: track != self.tracks[1]
+ # If provider.play() returns False, it is a failure.
+ self.playback.provider.play = lambda track: track != self.tracks[1]
self.playback.play(self.current_playlist.cp_tracks[2])
self.assertEqual(self.playback.current_track, self.tracks[2])
self.playback.previous()
@@ -228,8 +228,8 @@ class BasePlaybackControllerTest(object):
@populate_playlist
def test_next_skips_to_next_track_on_failure(self):
- # If _play() returns False, it is a failure.
- self.playback._play = lambda track: track != self.tracks[1]
+ # If provider.play() returns False, it is a failure.
+ self.playback.provider.play = lambda track: track != self.tracks[1]
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
self.playback.next()
@@ -364,8 +364,8 @@ class BasePlaybackControllerTest(object):
@populate_playlist
def test_end_of_track_skips_to_next_track_on_failure(self):
- # If _play() returns False, it is a failure.
- self.playback._play = lambda track: track != self.tracks[1]
+ # If provider.play() returns False, it is a failure.
+ self.playback.provider.play = lambda track: track != self.tracks[1]
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
self.playback.on_end_of_track()
diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py
index ef5806ef..0ac0b167 100644
--- a/tests/backends/base/stored_playlists.py
+++ b/tests/backends/base/stored_playlists.py
@@ -8,11 +8,11 @@ from mopidy.models import Playlist
from tests import SkipTest, data_folder
-class BaseStoredPlaylistsControllerTest(object):
+class StoredPlaylistsControllerTest(object):
def setUp(self):
- settings.LOCAL_PLAYLIST_FOLDER = tempfile.mkdtemp()
- settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache')
- settings.LOCAL_MUSIC_FOLDER = data_folder('')
+ settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
+ settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
+ settings.LOCAL_MUSIC_PATH = data_folder('')
self.backend = self.backend_class(mixer_class=DummyMixer)
self.stored = self.backend.stored_playlists
@@ -20,8 +20,8 @@ class BaseStoredPlaylistsControllerTest(object):
def tearDown(self):
self.backend.destroy()
- if os.path.exists(settings.LOCAL_PLAYLIST_FOLDER):
- shutil.rmtree(settings.LOCAL_PLAYLIST_FOLDER)
+ if os.path.exists(settings.LOCAL_PLAYLIST_PATH):
+ shutil.rmtree(settings.LOCAL_PLAYLIST_PATH)
settings.runtime.clear()
diff --git a/tests/backends/libspotify/__init__.py b/tests/backends/libspotify/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/backends/libspotify/backend_integrationtest.py b/tests/backends/libspotify/backend_integrationtest.py
deleted file mode 100644
index 8d1f0b0e..00000000
--- a/tests/backends/libspotify/backend_integrationtest.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# TODO This integration test is work in progress.
-
-import unittest
-
-from mopidy.backends.libspotify import LibspotifyBackend
-from mopidy.models import Track
-
-from tests.backends.base.current_playlist import \
- BaseCurrentPlaylistControllerTest
-from tests.backends.base.library import BaseLibraryControllerTest
-from tests.backends.base.playback import BasePlaybackControllerTest
-from tests.backends.base.stored_playlists import \
- BaseStoredPlaylistsControllerTest
-
-uris = [
- 'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt',
- 'spotify:track:111sulhaZqgsnypz3MkiaW',
- 'spotify:track:7t8oznvbeiAPMDRuK0R5ZT',
-]
-
-class LibspotifyCurrentPlaylistControllerTest(
- BaseCurrentPlaylistControllerTest, unittest.TestCase):
-
- backend_class = LibspotifyBackend
- tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
-
-
-class LibspotifyPlaybackControllerTest(
- BasePlaybackControllerTest, unittest.TestCase):
-
- backend_class = LibspotifyBackend
- tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
-
-
-class LibspotifyStoredPlaylistsControllerTest(
- BaseStoredPlaylistsControllerTest, unittest.TestCase):
-
- backend_class = LibspotifyBackend
-
-
-class LibspotifyLibraryControllerTest(
- BaseLibraryControllerTest, unittest.TestCase):
-
- backend_class = LibspotifyBackend
diff --git a/tests/backends/local/current_playlist_test.py b/tests/backends/local/current_playlist_test.py
index 3895497a..6f72d7d5 100644
--- a/tests/backends/local/current_playlist_test.py
+++ b/tests/backends/local/current_playlist_test.py
@@ -10,11 +10,10 @@ from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.models import Track
-from tests.backends.base.current_playlist import \
- BaseCurrentPlaylistControllerTest
+from tests.backends.base.current_playlist import CurrentPlaylistControllerTest
from tests.backends.local import generate_song
-class LocalCurrentPlaylistControllerTest(BaseCurrentPlaylistControllerTest,
+class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest,
unittest.TestCase):
backend_class = LocalBackend
diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py
index c0605ef2..0c44924a 100644
--- a/tests/backends/local/library_test.py
+++ b/tests/backends/local/library_test.py
@@ -10,15 +10,15 @@ from mopidy import settings
from mopidy.backends.local import LocalBackend
from tests import data_folder
-from tests.backends.base.library import BaseLibraryControllerTest
+from tests.backends.base.library import LibraryControllerTest
-class LocalLibraryControllerTest(BaseLibraryControllerTest, unittest.TestCase):
+class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend
def setUp(self):
- settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache')
- settings.LOCAL_MUSIC_FOLDER = data_folder('')
+ settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
+ settings.LOCAL_MUSIC_PATH = data_folder('')
super(LocalLibraryControllerTest, self).setUp()
diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py
index a84dfcde..2007cff8 100644
--- a/tests/backends/local/playback_test.py
+++ b/tests/backends/local/playback_test.py
@@ -12,12 +12,10 @@ from mopidy.models import Track
from mopidy.utils.path import path_to_uri
from tests import data_folder
-from tests.backends.base.playback import BasePlaybackControllerTest
+from tests.backends.base.playback import PlaybackControllerTest
from tests.backends.local import generate_song
-class LocalPlaybackControllerTest(BasePlaybackControllerTest,
- unittest.TestCase):
-
+class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
backend_class = LocalBackend
tracks = [Track(uri=generate_song(i), length=4464)
for i in range(1, 4)]
diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py
index bb03f997..a7d9043f 100644
--- a/tests/backends/local/stored_playlists_test.py
+++ b/tests/backends/local/stored_playlists_test.py
@@ -16,22 +16,22 @@ from mopidy.utils.path import path_to_uri
from tests import data_folder
from tests.backends.base.stored_playlists import \
- BaseStoredPlaylistsControllerTest
+ StoredPlaylistsControllerTest
from tests.backends.local import generate_song
-class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest,
+class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
unittest.TestCase):
backend_class = LocalBackend
def test_created_playlist_is_persisted(self):
- path = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test.m3u')
+ path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
self.assert_(not os.path.exists(path))
self.stored.create('test')
self.assert_(os.path.exists(path))
def test_saved_playlist_is_persisted(self):
- path = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test2.m3u')
+ path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2.m3u')
self.assert_(not os.path.exists(path))
self.stored.save(Playlist(name='test2'))
self.assert_(os.path.exists(path))
@@ -39,13 +39,13 @@ class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest,
def test_deleted_playlist_get_removed(self):
playlist = self.stored.create('test')
self.stored.delete(playlist)
- path = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test.m3u')
+ path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
self.assert_(not os.path.exists(path))
def test_renamed_playlist_gets_moved(self):
playlist = self.stored.create('test')
- file1 = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test.m3u')
- file2 = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test2.m3u')
+ file1 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
+ file2 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2.m3u')
self.assert_(not os.path.exists(file2))
self.stored.rename(playlist, 'test2')
self.assert_(not os.path.exists(file1))
@@ -55,7 +55,7 @@ class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest,
track = Track(uri=generate_song(1))
uri = track.uri[len('file://'):]
playlist = Playlist(tracks=[track], name='test')
- path = os.path.join(settings.LOCAL_PLAYLIST_FOLDER, 'test.m3u')
+ path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
self.stored.save(playlist)
diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py
index a9fe58d8..b7fd212c 100644
--- a/tests/backends/local/translator_test.py
+++ b/tests/backends/local/translator_test.py
@@ -116,7 +116,16 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
self.assertEqual(set(expected_tracks), tracks)
def test_unicode_cache(self):
- raise SkipTest
+ tracks = parse_mpd_tag_cache(data_folder('utf8_tag_cache'),
+ data_folder(''))
+
+ uri = path_to_uri(data_folder('song1.mp3'))
+ artists = [Artist(name=u'æøå')]
+ album = Album(name=u'æøå', artists=artists)
+ track = Track(uri=uri, name=u'æøå', artists=artists,
+ album=album, length=4000)
+
+ self.assertEqual(track, list(tracks)[0])
def test_misencoded_cache(self):
# FIXME not sure if this can happen
@@ -127,3 +136,28 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
data_folder(''))
uri = path_to_uri(data_folder('song1.mp3'))
self.assertEqual(set([Track(uri=uri, length=4000)]), tracks)
+
+ def test_musicbrainz_tagcache(self):
+ tracks = parse_mpd_tag_cache(data_folder('musicbrainz_tag_cache'),
+ data_folder(''))
+ artist = list(expected_tracks[0].artists)[0].copy(
+ musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897')
+ albumartist = list(expected_tracks[0].artists)[0].copy(
+ name='albumartistname',
+ musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897')
+ album = expected_tracks[0].album.copy(artists=[albumartist],
+ musicbrainz_id='cb5f1603-d314-4c9c-91e5-e295cfb125d2')
+ track = expected_tracks[0].copy(artists=[artist], album=album,
+ musicbrainz_id='90488461-8c1f-4a4e-826b-4c6dc70801f0')
+
+ self.assertEqual(track, list(tracks)[0])
+
+ def test_albumartist_tag_cache(self):
+ tracks = parse_mpd_tag_cache(data_folder('albumartist_tag_cache'),
+ data_folder(''))
+ uri = path_to_uri(data_folder('song1.mp3'))
+ artist = Artist(name='albumartistname')
+ album = expected_albums[0].copy(artists=[artist])
+ track = Track(name='trackname', artists=expected_artists, track_no=1,
+ album=album, length=4000, uri=uri)
+ self.assertEqual(track, list(tracks)[0])
diff --git a/tests/data/albumartist_tag_cache b/tests/data/albumartist_tag_cache
new file mode 100644
index 00000000..29942a75
--- /dev/null
+++ b/tests/data/albumartist_tag_cache
@@ -0,0 +1,16 @@
+info_begin
+mpd_version: 0.14.2
+fs_charset: UTF-8
+info_end
+songList begin
+key: song1.mp3
+file: /song1.mp3
+Time: 4
+Artist: name
+Title: trackname
+Album: albumname
+AlbumArtist: albumartistname
+Track: 1/2
+Date: 2006
+mtime: 1272319626
+songList end
diff --git a/tests/data/musicbrainz_tag_cache b/tests/data/musicbrainz_tag_cache
new file mode 100644
index 00000000..0e9dca46
--- /dev/null
+++ b/tests/data/musicbrainz_tag_cache
@@ -0,0 +1,20 @@
+info_begin
+mpd_version: 0.16.0
+fs_charset: UTF-8
+info_end
+songList begin
+key: song1.mp3
+file: /song1.mp3
+Time: 4
+Artist: name
+Title: trackname
+Album: albumname
+AlbumArtist: albumartistname
+Track: 1/2
+Date: 2006
+MUSICBRAINZ_ALBUMID: cb5f1603-d314-4c9c-91e5-e295cfb125d2
+MUSICBRAINZ_ALBUMARTISTID: 7364dea6-ca9a-48e3-be01-b44ad0d19897
+MUSICBRAINZ_ARTISTID: 7364dea6-ca9a-48e3-be01-b44ad0d19897
+MUSICBRAINZ_TRACKID: 90488461-8c1f-4a4e-826b-4c6dc70801f0
+mtime: 1272319626
+songList end
diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3
index 45812ac5..e84bdc24 120000
--- a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3
+++ b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3
@@ -1 +1 @@
-../../sample.mp3
\ No newline at end of file
+../../../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3
index 45812ac5..e84bdc24 120000
--- a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3
+++ b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3
@@ -1 +1 @@
-../../sample.mp3
\ No newline at end of file
+../../../sample.mp3
\ No newline at end of file
diff --git a/tests/data/utf8_tag_cache b/tests/data/utf8_tag_cache
new file mode 100644
index 00000000..6642ec77
--- /dev/null
+++ b/tests/data/utf8_tag_cache
@@ -0,0 +1,13 @@
+info_begin
+mpd_version: 0.14.2
+fs_charset: UTF-8
+info_end
+songList begin
+key: song1.mp3
+file: /song1.mp3
+Time: 4
+Artist: æøå
+Title: æøå
+Album: æøå
+mtime: 1272319626
+songList end
diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py
index 8a4b9ab5..a4179637 100644
--- a/tests/frontends/mpd/current_playlist_test.py
+++ b/tests/frontends/mpd/current_playlist_test.py
@@ -12,7 +12,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_add(self):
needle = Track(uri='dummy://foo')
- self.b.library._library = [Track(), Track(), needle, Track()]
+ self.b.library.provider._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
@@ -40,7 +40,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo')
- self.b.library._library = [Track(), Track(), needle, Track()]
+ self.b.library.provider._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
@@ -58,7 +58,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo')
- self.b.library._library = [Track(), Track(), needle, Track()]
+ self.b.library.provider._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
@@ -71,7 +71,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo')
- self.b.library._library = [Track(), Track(), needle, Track()]
+ self.b.library.provider._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py
index 4e60546d..43614173 100644
--- a/tests/frontends/mpd/playback_test.py
+++ b/tests/frontends/mpd/playback_test.py
@@ -104,6 +104,11 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
self.assertEqual(10, self.b.mixer.volume)
+ def test_setvol_without_quotes(self):
+ result = self.h.handle_request(u'setvol 50')
+ self.assert_(u'OK' in result)
+ self.assertEqual(50, self.b.mixer.volume)
+
def test_single_off(self):
result = self.h.handle_request(u'single "0"')
self.assertFalse(self.b.playback.single)
@@ -320,6 +325,13 @@ class PlaybackControlHandlerTest(unittest.TestCase):
result = self.h.handle_request(u'seek "1" "30"')
self.assertEqual(self.b.playback.current_track, seek_track)
+ def test_seek_without_quotes(self):
+ self.b.current_playlist.append([Track(length=40000)])
+ self.h.handle_request(u'seek 0')
+ result = self.h.handle_request(u'seek 0 30')
+ self.assert_(u'OK' in result)
+ self.assert_(self.b.playback.time_position >= 30000)
+
def test_seekid(self):
self.b.current_playlist.append([Track(length=40000)])
result = self.h.handle_request(u'seekid "0" "30"')
diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py
index 3cfdb855..7e7163d8 100644
--- a/tests/frontends/mpd/regression_test.py
+++ b/tests/frontends/mpd/regression_test.py
@@ -8,7 +8,7 @@ from mopidy.models import Track
class IssueGH17RegressionTest(unittest.TestCase):
"""
- The issue: http://github.com/jodal/mopidy/issues#issue/17
+ The issue: http://github.com/mopidy/mopidy/issues#issue/17
How to reproduce:
@@ -42,7 +42,7 @@ class IssueGH17RegressionTest(unittest.TestCase):
class IssueGH18RegressionTest(unittest.TestCase):
"""
- The issue: http://github.com/jodal/mopidy/issues#issue/18
+ The issue: http://github.com/mopidy/mopidy/issues#issue/18
How to reproduce:
@@ -79,7 +79,7 @@ class IssueGH18RegressionTest(unittest.TestCase):
class IssueGH22RegressionTest(unittest.TestCase):
"""
- The issue: http://github.com/jodal/mopidy/issues/#issue/22
+ The issue: http://github.com/mopidy/mopidy/issues/#issue/22
How to reproduce:
diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py
index 8e8a5d21..7e4500ea 100644
--- a/tests/frontends/mpd/serializer_test.py
+++ b/tests/frontends/mpd/serializer_test.py
@@ -3,15 +3,26 @@ import os
import unittest
from mopidy import settings
-from mopidy.utils.path import mtime
+from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol
from mopidy.models import Album, Artist, Playlist, Track
from tests import data_folder, SkipTest
class TrackMpdFormatTest(unittest.TestCase):
+ track = Track(
+ uri=u'a uri',
+ artists=[Artist(name=u'an artist')],
+ name=u'a name',
+ album=Album(name=u'an album', num_tracks=13,
+ artists=[Artist(name=u'an other artist')]),
+ track_no=7,
+ date=dt.date(1977, 1, 1),
+ length=137000,
+ )
+
def setUp(self):
- settings.LOCAL_MUSIC_FOLDER = '/dir/subdir'
+ settings.LOCAL_MUSIC_PATH = '/dir/subdir'
mtime.set_fake_time(1234567)
def tearDown(self):
@@ -42,33 +53,8 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Pos', 1) in result)
self.assert_(('Id', 2) in result)
- def test_track_to_mpd_format_with_key(self):
- track = Track(uri='file:///dir/subdir/file.mp3')
- result = translator.track_to_mpd_format(track, key=True)
- self.assert_(('key', 'file.mp3') in result)
-
- def test_track_to_mpd_format_with_key_not_uri_encoded(self):
- track = Track(uri='file:///dir/subdir/file%20test.mp3')
- result = translator.track_to_mpd_format(track, key=True)
- self.assert_(('key', 'file test.mp3') in result)
-
- def test_track_to_mpd_format_with_mtime(self):
- uri = translator.path_to_uri(data_folder('blank.mp3'))
- result = translator.track_to_mpd_format(Track(uri=uri), mtime=True)
- self.assert_(('mtime', 1234567) in result)
-
def test_track_to_mpd_format_for_nonempty_track(self):
- track = Track(
- uri=u'a uri',
- artists=[Artist(name=u'an artist')],
- name=u'a name',
- album=Album(name=u'an album', num_tracks=13,
- artists=[Artist(name=u'an other artist')]),
- track_no=7,
- date=dt.date(1977, 1, 1),
- length=137000,
- )
- result = translator.track_to_mpd_format(track, position=9, cpid=122)
+ result = translator.track_to_mpd_format(self.track, position=9, cpid=122)
self.assert_(('file', 'a uri') in result)
self.assert_(('Time', 137) in result)
self.assert_(('Artist', 'an artist') in result)
@@ -81,6 +67,30 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Id', 122) in result)
self.assertEqual(len(result), 10)
+ def test_track_to_mpd_format_musicbrainz_trackid(self):
+ track = self.track.copy(musicbrainz_id='foo')
+ result = translator.track_to_mpd_format(track)
+ self.assert_(('MUSICBRAINZ_TRACKID', 'foo') in result)
+
+ def test_track_to_mpd_format_musicbrainz_albumid(self):
+ album = self.track.album.copy(musicbrainz_id='foo')
+ track = self.track.copy(album=album)
+ result = translator.track_to_mpd_format(track)
+ self.assert_(('MUSICBRAINZ_ALBUMID', 'foo') in result)
+
+ def test_track_to_mpd_format_musicbrainz_albumid(self):
+ artist = list(self.track.artists)[0].copy(musicbrainz_id='foo')
+ album = self.track.album.copy(artists=[artist])
+ track = self.track.copy(album=album)
+ result = translator.track_to_mpd_format(track)
+ self.assert_(('MUSICBRAINZ_ALBUMARTISTID', 'foo') in result)
+
+ def test_track_to_mpd_format_musicbrainz_artistid(self):
+ artist = list(self.track.artists)[0].copy(musicbrainz_id='foo')
+ track = self.track.copy(artists=[artist])
+ result = translator.track_to_mpd_format(track)
+ self.assert_(('MUSICBRAINZ_ARTISTID', 'foo') in result)
+
def test_artists_to_mpd_format(self):
artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')]
translated = translator.artists_to_mpd_format(artists)
@@ -104,7 +114,7 @@ class PlaylistMpdFormatTest(unittest.TestCase):
class TracksToTagCacheFormatTest(unittest.TestCase):
def setUp(self):
- settings.LOCAL_MUSIC_FOLDER = '/dir/subdir'
+ settings.LOCAL_MUSIC_PATH = '/dir/subdir'
mtime.set_fake_time(1234567)
def tearDown(self):
@@ -112,8 +122,13 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
mtime.undo_fake()
def translate(self, track):
- result = translator.track_to_mpd_format(track, key=True, mtime=True)
- return translator.order_mpd_track_info(result)
+ folder = settings.LOCAL_MUSIC_PATH
+ result = dict(translator.track_to_mpd_format(track))
+ result['file'] = uri_to_path(result['file'])
+ result['file'] = result['file'][len(folder)+1:]
+ result['key'] = os.path.basename(result['file'])
+ result['mtime'] = mtime('')
+ return translator.order_mpd_track_info(result.items())
def consume_headers(self, result):
self.assertEqual(('info_begin',), result[0])
@@ -279,7 +294,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
class TracksToDirectoryTreeTest(unittest.TestCase):
def setUp(self):
- settings.LOCAL_MUSIC_FOLDER = '/root/'
+ settings.LOCAL_MUSIC_PATH = '/root/'
def tearDown(self):
settings.runtime.clear()
diff --git a/tests/models_test.py b/tests/models_test.py
index ab7bc793..0b44f337 100644
--- a/tests/models_test.py
+++ b/tests/models_test.py
@@ -5,6 +5,50 @@ from mopidy.models import Artist, Album, Track, Playlist
from tests import SkipTest
+class GenericCopyTets(unittest.TestCase):
+ def compare(self, orig, other):
+ self.assertEqual(orig, other)
+ self.assertNotEqual(id(orig), id(other))
+
+ def test_copying_track(self):
+ track = Track()
+ self.compare(track, track.copy())
+
+ def test_copying_artist(self):
+ artist = Artist()
+ self.compare(artist, artist.copy())
+
+ def test_copying_album(self):
+ album = Album()
+ self.compare(album, album.copy())
+
+ def test_copying_playlist(self):
+ playlist = Playlist()
+ self.compare(playlist, playlist.copy())
+
+ def test_copying_track_with_basic_values(self):
+ track = Track(name='foo', uri='bar')
+ copy = track.copy(name='baz')
+ self.assertEqual('baz', copy.name)
+ self.assertEqual('bar', copy.uri)
+
+ def test_copying_track_with_missing_values(self):
+ track = Track(uri='bar')
+ copy = track.copy(name='baz')
+ self.assertEqual('baz', copy.name)
+ self.assertEqual('bar', copy.uri)
+
+ def test_copying_track_with_private_internal_value(self):
+ artists1 = [Artist(name='foo')]
+ artists2 = [Artist(name='bar')]
+ track = Track(artists=artists1)
+ copy = track.copy(artists=artists2)
+ self.assertEqual(copy.artists, artists2)
+
+ def test_copying_track_with_invalid_key(self):
+ test = lambda: Track().copy(invalid_key=True)
+ self.assertRaises(TypeError, test)
+
class ArtistTest(unittest.TestCase):
def test_uri(self):
uri = u'an_uri'
@@ -18,6 +62,13 @@ class ArtistTest(unittest.TestCase):
self.assertEqual(artist.name, name)
self.assertRaises(AttributeError, setattr, artist, 'name', None)
+ def test_musicbrainz_id(self):
+ mb_id = u'mb-id'
+ artist = Artist(musicbrainz_id=mb_id)
+ self.assertEqual(artist.musicbrainz_id, mb_id)
+ self.assertRaises(AttributeError, setattr, artist,
+ 'musicbrainz_id', None)
+
def test_invalid_kwarg(self):
test = lambda: Artist(foo='baz')
self.assertRaises(TypeError, test)
@@ -34,9 +85,15 @@ class ArtistTest(unittest.TestCase):
self.assertEqual(artist1, artist2)
self.assertEqual(hash(artist1), hash(artist2))
+ def test_eq_musibrainz_id(self):
+ artist1 = Artist(musicbrainz_id=u'id')
+ artist2 = Artist(musicbrainz_id=u'id')
+ self.assertEqual(artist1, artist2)
+ self.assertEqual(hash(artist1), hash(artist2))
+
def test_eq(self):
- artist1 = Artist(uri=u'uri', name=u'name')
- artist2 = Artist(uri=u'uri', name=u'name')
+ artist1 = Artist(uri=u'uri', name=u'name', musicbrainz_id='id')
+ artist2 = Artist(uri=u'uri', name=u'name', musicbrainz_id='id')
self.assertEqual(artist1, artist2)
self.assertEqual(hash(artist1), hash(artist2))
@@ -58,9 +115,15 @@ class ArtistTest(unittest.TestCase):
self.assertNotEqual(artist1, artist2)
self.assertNotEqual(hash(artist1), hash(artist2))
+ def test_ne_musicbrainz_id(self):
+ artist1 = Artist(musicbrainz_id=u'id1')
+ artist2 = Artist(musicbrainz_id=u'id2')
+ self.assertNotEqual(artist1, artist2)
+ self.assertNotEqual(hash(artist1), hash(artist2))
+
def test_ne(self):
- artist1 = Artist(uri=u'uri1', name=u'name1')
- artist2 = Artist(uri=u'uri2', name=u'name2')
+ artist1 = Artist(uri=u'uri1', name=u'name1', musicbrainz_id='id1')
+ artist2 = Artist(uri=u'uri2', name=u'name2', musicbrainz_id='id2')
self.assertNotEqual(artist1, artist2)
self.assertNotEqual(hash(artist1), hash(artist2))
@@ -90,6 +153,13 @@ class AlbumTest(unittest.TestCase):
self.assertEqual(album.num_tracks, num_tracks)
self.assertRaises(AttributeError, setattr, album, 'num_tracks', None)
+ def test_musicbrainz_id(self):
+ mb_id = u'mb-id'
+ album = Album(musicbrainz_id=mb_id)
+ self.assertEqual(album.musicbrainz_id, mb_id)
+ self.assertRaises(AttributeError, setattr, album,
+ 'musicbrainz_id', None)
+
def test_invalid_kwarg(self):
test = lambda: Album(foo='baz')
self.assertRaises(TypeError, test)
@@ -127,10 +197,16 @@ class AlbumTest(unittest.TestCase):
self.assertEqual(album1, album2)
self.assertEqual(hash(album1), hash(album2))
+ def test_eq_musibrainz_id(self):
+ album1 = Album(musicbrainz_id=u'id')
+ album2 = Album(musicbrainz_id=u'id')
+ self.assertEqual(album1, album2)
+ self.assertEqual(hash(album1), hash(album2))
+
def test_eq(self):
artists = [Artist()]
- album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2)
- album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2)
+ album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id')
+ album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id')
self.assertEqual(album1, album2)
self.assertEqual(hash(album1), hash(album2))
@@ -164,11 +240,19 @@ class AlbumTest(unittest.TestCase):
self.assertNotEqual(album1, album2)
self.assertNotEqual(hash(album1), hash(album2))
+ def test_ne_musicbrainz_id(self):
+ album1 = Album(musicbrainz_id=u'id1')
+ album2 = Album(musicbrainz_id=u'id2')
+ self.assertNotEqual(album1, album2)
+ self.assertNotEqual(hash(album1), hash(album2))
+
def test_ne(self):
album1 = Album(name=u'name1', uri=u'uri1',
- artists=[Artist(name=u'name1')], num_tracks=1)
+ artists=[Artist(name=u'name1')], num_tracks=1,
+ musicbrainz_id='id1')
album2 = Album(name=u'name2', uri=u'uri2',
- artists=[Artist(name=u'name2')], num_tracks=2)
+ artists=[Artist(name=u'name2')], num_tracks=2,
+ musicbrainz_id='id2')
self.assertNotEqual(album1, album2)
self.assertNotEqual(hash(album1), hash(album2))
@@ -222,6 +306,13 @@ class TrackTest(unittest.TestCase):
self.assertEqual(track.bitrate, bitrate)
self.assertRaises(AttributeError, setattr, track, 'bitrate', None)
+ def test_musicbrainz_id(self):
+ mb_id = u'mb-id'
+ track = Track(musicbrainz_id=mb_id)
+ self.assertEqual(track.musicbrainz_id, mb_id)
+ self.assertRaises(AttributeError, setattr, track,
+ 'musicbrainz_id', None)
+
def test_invalid_kwarg(self):
test = lambda: Track(foo='baz')
self.assertRaises(TypeError, test)
@@ -285,14 +376,22 @@ class TrackTest(unittest.TestCase):
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
+ def test_eq_musibrainz_id(self):
+ track1 = Track(musicbrainz_id=u'id')
+ track2 = Track(musicbrainz_id=u'id')
+ self.assertEqual(track1, track2)
+ self.assertEqual(hash(track1), hash(track2))
+
def test_eq(self):
date = dt.date.today()
artists = [Artist()]
album = Album()
track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
- track_no=1, date=date, length=100, bitrate=100)
+ track_no=1, date=date, length=100, bitrate=100,
+ musicbrainz_id='id')
track2 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
- track_no=1, date=date, length=100, bitrate=100)
+ track_no=1, date=date, length=100, bitrate=100,
+ musicbrainz_id='id')
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
@@ -350,14 +449,21 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
+ def test_ne_musicbrainz_id(self):
+ track1 = Track(musicbrainz_id=u'id1')
+ track2 = Track(musicbrainz_id=u'id2')
+ self.assertNotEqual(track1, track2)
+ self.assertNotEqual(hash(track1), hash(track2))
+
def test_ne(self):
track1 = Track(uri=u'uri1', name=u'name1',
artists=[Artist(name=u'name1')], album=Album(name=u'name1'),
- track_no=1, date=dt.date.today(), length=100, bitrate=100)
+ track_no=1, date=dt.date.today(), length=100, bitrate=100,
+ musicbrainz_id='id1')
track2 = Track(uri=u'uri2', name=u'name2',
artists=[Artist(name=u'name2')], album=Album(name=u'name2'),
track_no=2, date=dt.date.today()-dt.timedelta(days=1),
- length=200, bitrate=200)
+ length=200, bitrate=200, musicbrainz_id='id2')
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
@@ -398,7 +504,7 @@ class PlaylistTest(unittest.TestCase):
last_modified = dt.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
- new_playlist = playlist.with_(uri=u'another uri')
+ new_playlist = playlist.copy(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)
@@ -409,7 +515,7 @@ class PlaylistTest(unittest.TestCase):
last_modified = dt.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
- new_playlist = playlist.with_(name=u'another name')
+ new_playlist = playlist.copy(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)
@@ -421,7 +527,7 @@ class PlaylistTest(unittest.TestCase):
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_tracks = [Track(), Track()]
- new_playlist = playlist.with_(tracks=new_tracks)
+ new_playlist = playlist.copy(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)
@@ -433,7 +539,7 @@ class PlaylistTest(unittest.TestCase):
new_last_modified = last_modified + dt.timedelta(1)
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
- new_playlist = playlist.with_(last_modified=new_last_modified)
+ new_playlist = playlist.copy(last_modified=new_last_modified)
self.assertEqual(new_playlist.uri, u'an uri')
self.assertEqual(new_playlist.name, u'a name')
self.assertEqual(new_playlist.tracks, tracks)
diff --git a/tests/scanner_test.py b/tests/scanner_test.py
index 141f2ceb..a1b53bcf 100644
--- a/tests/scanner_test.py
+++ b/tests/scanner_test.py
@@ -25,19 +25,26 @@ class TranslatorTest(unittest.TestCase):
'date': FakeGstDate(2006, 1, 1,),
'container-format': u'ID3 tag',
'duration': 4531,
+ 'musicbrainz-trackid': 'mbtrackid',
+ 'musicbrainz-albumid': 'mbalbumid',
+ 'musicbrainz-artistid': 'mbartistid',
+ 'musicbrainz-albumartistid': 'mbalbumartistid',
}
self.album = {
'name': 'albumname',
'num_tracks': 2,
+ 'musicbrainz_id': 'mbalbumid',
}
self.artist = {
'name': 'name',
+ 'musicbrainz_id': 'mbartistid',
}
self.albumartist = {
'name': 'albumartistname',
+ 'musicbrainz_id': 'mbalbumartistid',
}
self.track = {
@@ -46,6 +53,7 @@ class TranslatorTest(unittest.TestCase):
'date': date(2006, 1, 1),
'track_no': 1,
'length': 4531,
+ 'musicbrainz_id': 'mbtrackid',
}
def build_track(self):
@@ -78,21 +86,41 @@ class TranslatorTest(unittest.TestCase):
del self.track['name']
self.check()
+ def test_missing_track_musicbrainz_id(self):
+ del self.data['musicbrainz-trackid']
+ del self.track['musicbrainz_id']
+ self.check()
+
def test_missing_album_name(self):
del self.data['album']
del self.album['name']
self.check()
+ def test_missing_album_musicbrainz_id(self):
+ del self.data['musicbrainz-albumid']
+ del self.album['musicbrainz_id']
+ self.check()
+
def test_missing_artist_name(self):
del self.data['artist']
del self.artist['name']
self.check()
+ def test_missing_artist_musicbrainz_id(self):
+ del self.data['musicbrainz-artistid']
+ del self.artist['musicbrainz_id']
+ self.check()
+
def test_missing_album_artist(self):
del self.data['album-artist']
del self.albumartist['name']
self.check()
+ def test_missing_album_artist_musicbrainz_id(self):
+ del self.data['musicbrainz-albumartistid']
+ del self.albumartist['musicbrainz_id']
+ self.check()
+
def test_missing_date(self):
del self.data['date']
del self.track['date']
diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py
index 758a09ab..4366305c 100644
--- a/tests/utils/path_test.py
+++ b/tests/utils/path_test.py
@@ -34,9 +34,6 @@ class GetOrCreateFolderTest(unittest.TestCase):
self.assert_(os.path.isdir(self.parent))
self.assertEqual(created, self.parent)
- def test_that_userfolder_is_expanded(self):
- raise SkipTest # Not sure how to safely test this
-
class PathToFileURITest(unittest.TestCase):
def test_simple_path(self):
@@ -139,9 +136,6 @@ class FindFilesTest(unittest.TestCase):
self.assert_(is_unicode(name),
'%s is not unicode object' % repr(name))
- def test_expanduser(self):
- raise SkipTest
-
class MtimeTest(unittest.TestCase):
def tearDown(self):
diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py
index 0c06ae5c..cef0069d 100644
--- a/tests/utils/settings_test.py
+++ b/tests/utils/settings_test.py
@@ -1,3 +1,4 @@
+import os
import unittest
from mopidy import settings as default_settings_module
@@ -65,3 +66,37 @@ class SettingsProxyTest(unittest.TestCase):
def test_runtime_value_included_in_current(self):
self.settings.TEST = 'test'
self.assertEqual(self.settings.current['TEST'], 'test')
+
+ def test_value_ending_in_path_is_expanded(self):
+ self.settings.TEST_PATH = '~/test'
+ acctual = self.settings.TEST_PATH
+ expected = os.path.expanduser('~/test')
+ self.assertEqual(acctual, expected)
+
+ def test_value_ending_in_path_is_absolute(self):
+ self.settings.TEST_PATH = './test'
+ acctual = self.settings.TEST_PATH
+ expected = os.path.abspath('./test')
+ self.assertEqual(acctual, expected)
+
+ def test_value_ending_in_file_is_expanded(self):
+ self.settings.TEST_FILE = '~/test'
+ acctual = self.settings.TEST_FILE
+ expected = os.path.expanduser('~/test')
+ self.assertEqual(acctual, expected)
+
+ def test_value_ending_in_file_is_absolute(self):
+ self.settings.TEST_FILE = './test'
+ acctual = self.settings.TEST_FILE
+ expected = os.path.abspath('./test')
+ self.assertEqual(acctual, expected)
+
+ def test_value_not_ending_in_path_or_file_is_not_expanded(self):
+ self.settings.TEST = '~/test'
+ acctual = self.settings.TEST
+ self.assertEqual(acctual, '~/test')
+
+ def test_value_not_ending_in_path_or_file_is_not_absolute(self):
+ self.settings.TEST = './test'
+ acctual = self.settings.TEST
+ self.assertEqual(acctual, './test')