Merge branch 'develop' into feature/mpd-password
This commit is contained in:
commit
317492098a
@ -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 *
|
||||
|
||||
@ -13,7 +13,7 @@ To install Mopidy, check out
|
||||
|
||||
* `Documentation (latest release) <http://www.mopidy.com/docs/master/>`_
|
||||
* `Documentation (development version) <http://www.mopidy.com/docs/develop/>`_
|
||||
* `Source code <http://github.com/jodal/mopidy>`_
|
||||
* `Issue tracker <http://github.com/jodal/mopidy/issues>`_
|
||||
* `Source code <http://github.com/mopidy/mopidy>`_
|
||||
* `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
|
||||
* IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
* `Download development snapshot <http://github.com/jodal/mopidy/tarball/develop#egg=mopidy-dev>`_
|
||||
* `Download development snapshot <http://github.com/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_
|
||||
|
||||
0
bin/mopidy
Normal file → Executable file
0
bin/mopidy
Normal file → Executable file
@ -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')
|
||||
|
||||
@ -7,4 +7,4 @@ Icon=audio-x-generic
|
||||
TryExec=mopidy
|
||||
Exec=mopidy
|
||||
Terminal=true
|
||||
Categories=AudioVideo;Audio;Player;ConsoleOnly
|
||||
Categories=AudioVideo;Audio;Player;ConsoleOnly;
|
||||
|
||||
130
docs/Makefile
130
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 <target>' where <target> 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."
|
||||
|
||||
2
docs/_themes/nature/static/nature.css_t
vendored
2
docs/_themes/nature/static/nature.css_t
vendored
@ -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;
|
||||
|
||||
30
docs/api/backends/concepts.rst
Normal file
30
docs/api/backends/concepts.rst
Normal file
@ -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
|
||||
65
docs/api/backends/controllers.rst
Normal file
65
docs/api/backends/controllers.rst
Normal file
@ -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:
|
||||
@ -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`
|
||||
@ -1,7 +0,0 @@
|
||||
*******************************************************
|
||||
:mod:`mopidy.backends.libspotify` -- Libspotify backend
|
||||
*******************************************************
|
||||
|
||||
.. automodule:: mopidy.backends.libspotify
|
||||
:synopsis: Spotify backend using the libspotify library
|
||||
:members:
|
||||
41
docs/api/backends/providers.rst
Normal file
41
docs/api/backends/providers.rst
Normal file
@ -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`
|
||||
@ -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`
|
||||
@ -1,7 +0,0 @@
|
||||
******************************
|
||||
:mod:`mopidy.frontends.lastfm`
|
||||
******************************
|
||||
|
||||
.. automodule:: mopidy.frontends.lastfm
|
||||
:synopsis: Last.fm scrobbler frontend
|
||||
:members:
|
||||
@ -1,8 +1,11 @@
|
||||
*****************
|
||||
API documentation
|
||||
*****************
|
||||
*************
|
||||
API reference
|
||||
*************
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
**
|
||||
backends/concepts
|
||||
backends/controllers
|
||||
backends/providers
|
||||
*
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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:
|
||||
@ -10,13 +10,20 @@ Contributors to Mopidy in the order of appearance:
|
||||
- Kristian Klette <klette@klette.us>
|
||||
|
||||
|
||||
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 <http://pledgie.com/campaigns/12647>`_ 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 <https://flattr.com/thing/82288/Mopidy>`_, or `donate money
|
||||
<http://pledgie.com/campaigns/12647>`_ 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.
|
||||
|
||||
108
docs/changes.rst
108
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 <backend-concepts>`. Split the backend
|
||||
API into a :ref:`backend controller API <backend-controller-api>` (for
|
||||
frontend use) and a :ref:`backend provider API <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
|
||||
<http://github.com/jodal/mopidy/issues>`_. Thanks!
|
||||
<http://github.com/mopidy/mopidy/issues>`_. Thanks!
|
||||
|
||||
**Changes**
|
||||
|
||||
|
||||
@ -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-')}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <http://mxcl.github.com/homebrew/>`_
|
||||
- **[BLOCKED]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_
|
||||
recipies for all our dependencies and Mopidy itself to make OS X
|
||||
installation a breeze. See `Homebrew's issue #1612
|
||||
<http://github.com/mxcl/homebrew/issues/issue/1612>`_.
|
||||
- Create `Debian packages <http://www.debian.org/doc/maint-guide/>`_ 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
|
||||
<http://www.debian.org/doc/maint-guide/>`_ 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
|
||||
<http://en.wikipedia.org/wiki/Zeroconf>`_/Avahi.
|
||||
- D-Bus/`MPRIS <http://www.mpris.org/>`_
|
||||
- REST/JSON web service with a jQuery client as example application. Maybe
|
||||
based upon `Tornado <http://github.com/facebook/tornado>`_ and `jQuery
|
||||
- **[WIP: feature/mpris-frontend]** D-Bus/`MPRIS <http://www.mpris.org/>`_
|
||||
- **[WIP: feature/http-frontend]** REST/JSON web service with a jQuery client
|
||||
as example application. Maybe based upon `Tornado
|
||||
<http://github.com/facebook/tornado>`_ and `jQuery
|
||||
Mobile <http://jquerymobile.com/>`_.
|
||||
- DNLA/UPnP so Mopidy can be controlled from i.e. TVs.
|
||||
- `XMMS2 <http://www.xmms2.org/>`_
|
||||
|
||||
@ -21,6 +21,7 @@ Reference documentation
|
||||
:maxdepth: 3
|
||||
|
||||
api/index
|
||||
modules/index
|
||||
|
||||
Development documentation
|
||||
=========================
|
||||
|
||||
@ -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
|
||||
<http://github.com/mxcl/homebrew/issues/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.
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
Installation
|
||||
************
|
||||
|
||||
To get a basic version of Mopidy running, you need Python and the
|
||||
:doc:`GStreamer library <gstreamer>`. To use Spotify with Mopidy, you also need
|
||||
:doc:`libspotify and pyspotify <libspotify>`. 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 <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 <libspotify>`
|
||||
|
||||
- :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 </settings>`, and then you're
|
||||
ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
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 </settings>`, and then you're
|
||||
ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
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 </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
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 </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
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 </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
|
||||
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 </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
|
||||
<http://git-scm.com/>`_. Also, please read our :doc:`developer documentation
|
||||
</development/index>`.
|
||||
|
||||
|
||||
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 </settings>`, and then
|
||||
you're ready to :doc:`run Mopidy </running>`.
|
||||
|
||||
@ -5,11 +5,11 @@ libspotify installation
|
||||
Mopidy uses `libspotify
|
||||
<http://developer.spotify.com/en/libspotify/overview/>`_ for playing music from
|
||||
the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must
|
||||
install libspotify and `pyspotify <http://github.com/winjer/pyspotify>`_.
|
||||
install libspotify and `pyspotify <http://github.com/mopidy/pyspotify>`_.
|
||||
|
||||
.. warning::
|
||||
.. note::
|
||||
|
||||
This backend requires a `Spotify premium account
|
||||
This backend requires a paid `Spotify premium account
|
||||
<http://www.spotify.com/no/get-spotify/premium/>`_.
|
||||
|
||||
.. note::
|
||||
@ -19,17 +19,34 @@ install libspotify and `pyspotify <http://github.com/winjer/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 <http://developer.apple.com/tools/xcode/>`_ and
|
||||
`Homebrew <http://mxcl.github.com/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.
|
||||
|
||||
7
docs/modules/backends/spotify.rst
Normal file
7
docs/modules/backends/spotify.rst
Normal file
@ -0,0 +1,7 @@
|
||||
*************************************************
|
||||
:mod:`mopidy.backends.spotify` -- Spotify backend
|
||||
*************************************************
|
||||
|
||||
.. automodule:: mopidy.backends.spotify
|
||||
:synopsis: Backend for the Spotify music streaming service
|
||||
:members:
|
||||
7
docs/modules/frontends/lastfm.rst
Normal file
7
docs/modules/frontends/lastfm.rst
Normal file
@ -0,0 +1,7 @@
|
||||
***************************************************
|
||||
:mod:`mopidy.frontends.lastfm` -- Last.fm Scrobbler
|
||||
***************************************************
|
||||
|
||||
.. automodule:: mopidy.frontends.lastfm
|
||||
:synopsis: Last.fm scrobbler frontend
|
||||
:members:
|
||||
@ -1,6 +1,6 @@
|
||||
***************************
|
||||
:mod:`mopidy.frontends.mpd`
|
||||
***************************
|
||||
*****************************************
|
||||
:mod:`mopidy.frontends.mpd` -- MPD server
|
||||
*****************************************
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd
|
||||
:synopsis: MPD frontend
|
||||
8
docs/modules/index.rst
Normal file
8
docs/modules/index.rst
Normal file
@ -0,0 +1,8 @@
|
||||
****************
|
||||
Module reference
|
||||
****************
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
**
|
||||
9
docs/modules/mixers/alsa.rst
Normal file
9
docs/modules/mixers/alsa.rst
Normal file
@ -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:
|
||||
9
docs/modules/mixers/denon.rst
Normal file
9
docs/modules/mixers/denon.rst
Normal file
@ -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:
|
||||
9
docs/modules/mixers/dummy.rst
Normal file
9
docs/modules/mixers/dummy.rst
Normal file
@ -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:
|
||||
9
docs/modules/mixers/gstreamer_software.rst
Normal file
9
docs/modules/mixers/gstreamer_software.rst
Normal file
@ -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:
|
||||
9
docs/modules/mixers/nad.rst
Normal file
9
docs/modules/mixers/nad.rst
Normal file
@ -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:
|
||||
9
docs/modules/mixers/osa.rst
Normal file
9
docs/modules/mixers/osa.rst
Normal file
@ -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:
|
||||
9
docs/modules/outputs/gstreamer.rst
Normal file
9
docs/modules/outputs/gstreamer.rst
Normal file
@ -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:
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 <http://www.spotify.com/>`_ backend which uses the official
|
||||
`libspotify <http://developer.spotify.com/en/libspotify/overview/>`_
|
||||
library and the `pyspotify <http://github.com/winjer/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
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
74
mopidy/backends/spotify/__init__.py
Normal file
74
mopidy/backends/spotify/__init__.py
Normal file
@ -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 <http://www.spotify.com/>`_
|
||||
music streaming service. The backend uses the official `libspotify
|
||||
<http://developer.spotify.com/en/libspotify/overview/>`_ library and the
|
||||
`pyspotify <http://github.com/winjer/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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
@ -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 = []
|
||||
|
||||
@ -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
|
||||
"""
|
||||
|
||||
@ -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 <http://code.google.com/p/pylast/>`_ >= 0.4.30
|
||||
- `pylast <http://code.google.com/p/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)
|
||||
|
||||
@ -293,6 +293,7 @@ def replay_gain_status(frontend):
|
||||
"""
|
||||
return u'off' # TODO
|
||||
|
||||
@handle_pattern(r'^seek (?P<songpos>\d+) (?P<seconds>\d+)$')
|
||||
@handle_pattern(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\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<volume>[-+]*\d+)$')
|
||||
@handle_pattern(r'^setvol "(?P<volume>[-+]*\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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
@ -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`
|
||||
|
||||
55
mopidy/mixers/base.py
Normal file
55
mopidy/mixers/base.py
Normal file
@ -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
|
||||
@ -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()
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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)]
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -1 +0,0 @@
|
||||
pylast >= 0.4.30
|
||||
11
requirements/README.rst
Normal file
11
requirements/README.rst
Normal file
@ -0,0 +1,11 @@
|
||||
*********************
|
||||
pip requirement files
|
||||
*********************
|
||||
|
||||
The files found here are `requirement files
|
||||
<http://pip.openplans.org/requirement-format.html>`_ that may be used with `pip
|
||||
<http://pip.openplans.org/>`_.
|
||||
|
||||
To install the dependencies found in one of these files, simply run e.g.::
|
||||
|
||||
pip install -r requirements/tests.txt
|
||||
1
requirements/lastfm.txt
Normal file
1
requirements/lastfm.txt
Normal file
@ -0,0 +1 @@
|
||||
pylast >= 0.5
|
||||
2
setup.py
2
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'],
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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]),
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
16
tests/data/albumartist_tag_cache
Normal file
16
tests/data/albumartist_tag_cache
Normal file
@ -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
|
||||
20
tests/data/musicbrainz_tag_cache
Normal file
20
tests/data/musicbrainz_tag_cache
Normal file
@ -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
|
||||
@ -1 +1 @@
|
||||
../../sample.mp3
|
||||
../../../sample.mp3
|
||||
@ -1 +1 @@
|
||||
../../sample.mp3
|
||||
../../../sample.mp3
|
||||
13
tests/data/utf8_tag_cache
Normal file
13
tests/data/utf8_tag_cache
Normal file
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user