Merge branch 'develop' into feature/mpris-frontend

Conflicts:
	data/mopidy.desktop
This commit is contained in:
Stein Magnus Jodal 2011-01-11 22:11:56 +01:00
commit 18854c6f3b
108 changed files with 1996 additions and 1054 deletions

View File

@ -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 *

View File

@ -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
View File

View 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')

View File

@ -7,5 +7,5 @@ Icon=audio-x-generic
TryExec=mopidy
Exec=mopidy
Terminal=true
Categories=AudioVideo;Audio;Player;ConsoleOnly
Categories=AudioVideo;Audio;Player;ConsoleOnly;
StartupNotify=true

View File

@ -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."

View File

@ -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;

View 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

View 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:

View File

@ -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`

View File

@ -1,7 +0,0 @@
*******************************************************
:mod:`mopidy.backends.libspotify` -- Libspotify backend
*******************************************************
.. automodule:: mopidy.backends.libspotify
:synopsis: Spotify backend using the libspotify library
:members:

View 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`

View File

@ -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`

View File

@ -1,7 +0,0 @@
******************************
:mod:`mopidy.frontends.lastfm`
******************************
.. automodule:: mopidy.frontends.lastfm
:synopsis: Last.fm scrobbler frontend
:members:

View File

@ -1,8 +1,11 @@
*****************
API documentation
*****************
*************
API reference
*************
.. toctree::
:glob:
**
backends/concepts
backends/controllers
backends/providers
*

View File

@ -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`

View File

@ -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

View File

@ -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`

View File

@ -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:

View File

@ -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.

View File

@ -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**

View File

@ -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-')}

View File

@ -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

View File

@ -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/>`_

View File

@ -21,6 +21,7 @@ Reference documentation
:maxdepth: 3
api/index
modules/index
Development documentation
=========================

View File

@ -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.

View 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>`.

View File

@ -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.

View File

@ -0,0 +1,7 @@
*************************************************
:mod:`mopidy.backends.spotify` -- Spotify backend
*************************************************
.. automodule:: mopidy.backends.spotify
:synopsis: Backend for the Spotify music streaming service
:members:

View File

@ -0,0 +1,7 @@
***************************************************
:mod:`mopidy.frontends.lastfm` -- Last.fm Scrobbler
***************************************************
.. automodule:: mopidy.frontends.lastfm
:synopsis: Last.fm scrobbler frontend
:members:

View File

@ -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
View File

@ -0,0 +1,8 @@
****************
Module reference
****************
.. toctree::
:glob:
**

View 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:

View 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:

View 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:

View 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:

View 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:

View 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:

View 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:

View File

@ -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:

View File

@ -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.

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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 = []

View File

@ -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
"""

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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
View 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

View File

@ -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()

View File

@ -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."""

View File

@ -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."""

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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`

View File

@ -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)]

View File

@ -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.
@ -78,8 +78,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.
#:
@ -87,8 +87,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.
#:
@ -96,8 +96,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.
#:
@ -170,17 +170,26 @@ MPD_SERVER_HOSTNAME = u'127.0.0.1'
#: 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

View File

@ -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):

View File

@ -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():

View File

@ -1 +0,0 @@
pylast >= 0.4.30

11
requirements/README.rst Normal file
View 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
View File

@ -0,0 +1 @@
pylast >= 0.5

View File

@ -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'],

View File

@ -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):

View File

@ -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]),

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)]

View File

@ -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)

View File

@ -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])

View 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

View 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

View File

@ -1 +1 @@
../../sample.mp3
../../../sample.mp3

View File

@ -1 +1 @@
../../sample.mp3
../../../sample.mp3

13
tests/data/utf8_tag_cache Normal file
View 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