Release v0.3.0

This commit is contained in:
Stein Magnus Jodal 2011-01-22 13:58:20 +01:00
commit df7ce7cf08
138 changed files with 3536 additions and 1157 deletions

View File

@ -1,6 +1,7 @@
include LICENSE pylintrc *.rst *.txt
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

@ -6,14 +6,15 @@ Mopidy is a music server which can play music from `Spotify
<http://www.spotify.com/>`_ or from your local hard drive. To search for music
in Spotify's vast archive, manage playlists, and play music, you can use most
`MPD clients <http://mpd.wikia.com/>`_. MPD clients are available for most
platforms, including Windows, Mac OS X, Linux, and iPhone and Android phones.
platforms, including Windows, Mac OS X, Linux, Android and iOS.
To install Mopidy, check out
`the installation docs <http://www.mopidy.com/docs/master/installation/>`_.
* `Documentation (latest release) <http://www.mopidy.com/docs/master/>`_
* `Documentation (development version) <http://www.mopidy.com/docs/develop/>`_
* `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/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_
- `Documentation for the latest release <http://www.mopidy.com/docs/master/>`_
- `Documentation for the development version
<http://www.mopidy.com/docs/develop/>`_
- `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/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_

0
bin/mopidy Normal file → Executable file
View File

31
bin/mopidy-scan Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env python
if __name__ == '__main__':
import sys
from mopidy import settings
from mopidy.scanner import Scanner, translator
from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format
tracks = []
def store(data):
track = translator(data)
tracks.append(track)
print >> sys.stderr, 'Added %s' % track.uri
def debug(uri, error):
print >> sys.stderr, 'Failed %s: %s' % (uri, error)
print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH
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 (u'%s' % a).encode('utf-8')
else:
print (u'%s: %s' % a).encode('utf-8')

10
data/mopidy.desktop Normal file
View File

@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Version=1.0
Name=Mopidy Music Server
Comment=MPD music server with Spotify support
Icon=audio-x-generic
TryExec=mopidy
Exec=mopidy
Terminal=true
Categories=AudioVideo;Audio;Player;ConsoleOnly;

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

BIN
docs/_static/mopidy.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

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

@ -1,106 +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:
:mod:`mopidy.backends.dummy` -- Dummy backend for testing
=========================================================
.. automodule:: mopidy.backends.dummy
:synopsis: Dummy backend used for testing
:members:
:mod:`mopidy.backends.libspotify` -- Libspotify backend
=======================================================
.. automodule:: mopidy.backends.libspotify
:synopsis: Spotify backend using the libspotify library
:members:
:mod:`mopidy.backends.local` -- Local backend
=====================================================
.. automodule:: mopidy.backends.local
:synopsis: Backend for playing music files on local storage
:members:

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

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

26
docs/api/frontends.rst Normal file
View File

@ -0,0 +1,26 @@
************
Frontend API
************
A frontend may do whatever it wants to, including creating threads, opening TCP
ports and exposing Mopidy for a type of clients.
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.
.. warning::
A stable frontend API is not available yet, as we've only implemented a
couple of frontend modules.
.. automodule:: mopidy.frontends.base
:synopsis: Base class for frontends
:members:
Frontend implementations
========================
* :mod:`mopidy.frontends.lastfm`
* :mod:`mopidy.frontends.mpd`

View File

@ -1,25 +0,0 @@
***********************
:mod:`mopidy.frontends`
***********************
A frontend is responsible for exposing Mopidy for a type of clients.
Frontend API
============
.. warning::
A stable frontend API is not available yet, as we've only implemented a
couple of frontend modules.
.. automodule:: mopidy.frontends.base
:synopsis: Base class for frontends
:members:
Frontends
=========
* :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

@ -5,6 +5,161 @@ Changes
This change log is used to track all major changes to Mopidy.
0.3.0 (2010-01-22)
==================
Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large
changes. The main features are support for high bitrate audio from Spotify, and
MPD password authentication.
Regarding the docs, we've improved the :ref:`installation instructions
<installation>` and done a bit of testing of the available :ref:`Android
<android_mpd_clients>` and :ref:`iOS clients <ios_mpd_clients>` for MPD.
Please note that 0.3.0 requires some updated dependencies, as listed under
*Important changes* below. Also, there is a known bug in the Spotify playlist
loading, as described below. As the bug will take some time to fix and has a
known workaround, we did not want to delay the release while waiting for a fix
to this problem.
.. warning:: Known bug in Spotify playlist loading
There is a known bug in the loading of Spotify playlists. This bug affects
both Mopidy 0.2.1 and 0.3.0, given that you use libspotify 0.0.6. To avoid
the bug, either use Mopidy 0.2.1 with libspotify 0.0.4, or use either
Mopidy version with libspotify 0.0.6 and follow the simple workaround
described at :issue:`59`.
**Important changes**
- 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/`.
- If you use the Last.fm frontend, you need to upgrade to pylast 0.5.7. Run
``sudp pip install --upgrade pylast`` or install Mopidy from APT.
**Changes**
- Spotify backend:
- Support high bitrate (320k) audio. Set the new setting
:attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` to :class:`True` to switch to
high bitrate audio.
- 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 (see :issue:`62`).
- Fix crash on failed lookup of track by URI. (Fixes: :issue:`60`)
- Local backend:
- Add :command:`mopidy-scan` command to generate ``tag_cache`` files without
any help from the original MPD server. See :ref:`generating_a_tag_cache`
for instructions on how to use it.
- Fix support for UTF-8 encoding in tag caches.
- MPD frontend:
- Add support for password authentication. See
:attr:`mopidy.settings.MPD_SERVER_PASSWORD` and
:ref:`use_mpd_on_a_network` for details on how to use it. (Fixes:
:issue:`41`)
- 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.
- Last.fm frontend:
- Update to use Last.fm's new Scrobbling 2.0 API, as the old Submissions
Protocol 1.2.1 is deprecated. (Fixes: :issue:`33`)
- Fix crash when track object does not contain all the expected meta data.
- Fix crash when response from Last.fm cannot be decoded as UTF-8. (Fixes:
:issue:`37`)
- Fix crash when response from Last.fm contains invalid XML.
- Fix crash when response from Last.fm has an invalid HTTP status line.
- Mixers:
- Support use of unicode strings for settings specific to
:mod:`mopidy.mixers.nad`.
- Settings:
- Automatically expand the "~" characted 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`
- Fix bug which made settings set to :class:`None` or 0 cause a
:exc:`mopidy.SettingsError` to be raised.
- Packaging and distribution:
- Setup APT repository and crate Debian packages of Mopidy. See
:ref:`installation` for instructions for how to install Mopidy, including
all dependencies, from APT.
- Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome
application menus.
- API:
- 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`.
- Prepare for multi-backend support (see :issue:`40`) by introducing 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`.
- 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)
==================

View File

@ -16,11 +16,14 @@ mpc
A command line client. Version 0.14 had some issues with Mopidy (see
:issue:`5`), but 0.16 seems to work nicely.
ncmpc
-----
A console client. Uses the ``idle`` command heavily, which Mopidy doesn't
support yet. If you want a console client, use ncmpcpp instead.
support yet (see :issue:`32`). If you want a console client, use ncmpcpp
instead.
ncmpcpp
-------
@ -40,59 +43,266 @@ If you run Ubuntu 10.04 or older, you can fetch an updated version of ncmpcpp
from `Launchpad <https://launchpad.net/ubuntu/+source/ncmpcpp>`_.
Graphical clients
=================
GMPC
----
A GTK+ client which works well with Mopidy, and is regularly used by Mopidy
developers.
`GMPC <http://gmpc.wikia.com>`_ is a graphical MPD client (GTK+) which works
well with Mopidy, and is regularly used by Mopidy developers.
GMPC may sometimes requests a lot of meta data of related albums, artists, etc.
This takes more time with Mopidy, which needs to query Spotify for the data,
than with a normal MPD server, which has a local cache of meta data. Thus, GMPC
may sometimes feel frozen, but usually you just need to give it a bit of slack
before it will catch up.
Sonata
------
A GTK+ client. Generally works well with Mopidy.
`Sonata <http://sonata.berlios.de/>`_ is a graphical MPD client (GTK+).
It generally works well with Mopidy, except for search.
Search does not work, because they do most of the search on the client side.
See :issue:`1` for details.
When you search in Sonata, it only sends the first to letters of the search
query to Mopidy, and then does the rest of the filtering itself on the client
side. Since Spotify has a collection of millions of tracks and they only return
the first 100 hits for any search query, searching for two-letter combinations
seldom returns any useful results. See :issue:`1` and the matching `Sonata
bug`_ for details.
.. _Sonata bug: http://developer.berlios.de/feature/?func=detailfeature&feature_id=5038&group_id=7323
Theremin
--------
`Theremin <http://theremin.sigterm.eu/>`_ is a graphical MPD client for OS X.
It generally works well with Mopidy.
.. _android_mpd_clients:
Android clients
===============
We've tested all six MPD clients we could find for Android with Mopidy 0.3 on a
HTC Hero with Android 2.1, using the following test procedure:
#. Connect to Mopidy
#. Search for ``foo``, with search type "any" if it can be selected
#. Add "The Pretender" from the search results to the current playlist
#. Start playback
#. Pause and resume playback
#. Adjust volume
#. Find a playlist and append it to the current playlist
#. Skip to next track
#. Skip to previous track
#. Select the last track from the current playlist
#. Turn on repeat mode
#. Seek to 10 seconds or so before the end of the track
#. Wait for the end of the track and confirm that playback continues at the
start of the playlist
#. Turn off repeat mode
#. Turn on random mode
#. Skip to next track and confirm that it random mode works
#. Turn off random mode
#. Stop playback
#. Check if the app got support for single mode and consume mode
#. Kill Mopidy and confirm that the app handles it without crashing
In summary:
- BitMPC lacks finishing touches on its user interface but supports all
features tested.
- Droid MPD Client works well, but got a couple of bugs one can live with and
does not expose stored playlist anywhere.
- IcyBeats is not usable yet.
- MPDroid is working well and looking good, but does not have search
functionality.
- PMix is just a lesser MPDroid, so use MPDroid instead.
- ThreeMPD is too buggy to even get connected to Mopidy.
Our recommendation:
- If you do not care about looks, use BitMPC.
- If you do not care about stored playlists, use Droid MPD Client.
- If you do not care about searching, use MPDroid.
BitMPC
------
Works well with Mopidy.
We tested version 1.0.0, which at the time had 1k-5k downloads, <100 ratings,
3.5 stars.
Droid MPD
---------
The user interface lacks some finishing touches. E.g. you can't enter a
hostname for the server. Only IPv4 addresses are allowed.
All features exercised in the test procedure works. BitMPC lacks support for
single mode and consume mode. BitMPC crashes if Mopidy is killed or crash.
Droid MPD Client
----------------
We tested version 0.4.0, which at the time had 5k-10k downloads, >200 ratings,
4 stars.
To find the search functionality, you have to select the menu, then "Playlist
manager", then the search tab. I do not understand why search is hidden inside
"Playlist manager".
The user interface have some French remnants, like "Rechercher" in the search
field.
When selecting the artist tab, it issues the ``list Artist`` command and
becomes stuck waiting for the results. Same thing happens for the album tab,
which issues ``list Album``, and the folder tab, which issues ``lsinfo``.
Mopidy returned zero hits immediately on all three commands. If Mopidy has
loaded your stored playlists and returns more than zero hits on these commands,
they artist and album tabs do not hang. The folder tab still freezes when
``lsinfo`` returns a list of stored playlists, though zero files. Thus, we've
discovered a couple of bugs in Droid MPD Client.
The volume control is very slick, with a turn knob, just like on an amplifier.
It lends itself to showing off to friends when combined with Mopidy's external
amplifier mixers. Everybody loves turning a knob on a touch screen and see the
physical knob on the amplifier turn as well ;-)
Even though ``lsinfo`` returns the stored playlists for the folder tab, they
are not displayed anywhere. Thus, we had to select an album in the album tab to
complete the test procedure.
At one point, I had problems turning off repeat mode. After I adjusted the
volume and tried again, it worked.
Droid MPD client does not support single mode or consume mode. It does not
detect that the server is killed/crashed. You'll only notice it by no actions
having any effect, e.g. you can't turn the volume knob any more.
In conclusion, some bugs and caveats, but most of the test procedure was
possible to perform.
IcyBeats
--------
We tested version 0.2, which at the time had 50-100 downloads, no ratings.
The app was still in beta when we tried it.
IcyBeats successfully connected to Mopidy and I was able to adjust volume. When
I was searching for some tracks, I could not figure out how to actually start
the search, as there was no search button and pressing enter in the input field
just added a new line. I was stuck. In other words, IcyBeats 0.2 is not usable
with Mopidy.
IcyBeats does have something going for it: IcyBeats uses IPv6 to connect to
Mopidy. The future is just around the corner!
Works well with Mopidy.
MPDroid
-------
Works well with Mopidy, and is regularly used by Mopidy developers.
We tested version 0.6.9, which at the time had 5k-10k downloads, <200 ratings,
4.5 stars. MPDroid started out as a fork of PMix.
First of all, MPDroid's user interface looks nice.
I couldn't find any search functionality, so I added the initial track using
another client. Other than the missing search functionality, everything in the
test procedure worked out flawlessly. Like all other Android clients, MPDroid
does not support single mode or consume mode. When Mopidy is killed, MPDroid
handles it gracefully and asks if you want to try to reconnect.
All in all, MPDroid is a good MPD client without search support.
PMix
----
Works well with Mopidy.
We tested version 0.4.0, which at the time had 10k-50k downloads, >200 ratings,
4 stars.
Add MPDroid is a fork from PMix, it is no surprise that PMix does not support
search either. In addition, I could not find stored playlists. Other than that,
I was able to complete the test procedure. PMix crashed once during testing,
but handled the killing of Mopidy just as nicely as MPDroid. It does not
support single mode or consume mode.
All in all, PMix works but can do less than MPDroid. Use MPDroid instead.
ThreeMPD
--------
Does not work well with Mopidy, because we haven't implemented ``listallinfo``
yet.
We tested version 0.3.0, which at the time had 1k-5k downloads, <25 ratings,
2.5 average. The developer request users to use MPDroid instead, due to limited
time for maintenance. Does not support password authentication.
ThreeMPD froze during startup, so we were not able to test it.
.. _ios_mpd_clients:
iPhone/iPod Touch clients
=========================
impdclient
----------
There's an open source MPD client for iOS called `impdclient
<http://code.google.com/p/impdclient/>`_ which has not seen any updates since
August 2008. So far, we've not heard of users trying it with Mopidy. Please
notify us of your successes and/or problems if you do try it out.
MPod
----
Works well with Mopidy as far as we've heard from users.
The `MPoD <http://www.katoemba.net/makesnosenseatall/mpod/>`_ client can be
installed from the `iTunes Store
<http://itunes.apple.com/us/app/mpod/id285063020>`_.
Users have reported varying success in using MPoD together with Mopidy. Thus,
we've tested a fresh install of MPoD 1.5.1 with Mopidy as of revision e7ed28d
(pre-0.3) on an iPod Touch 3rd generation. The following are our findings:
- **Works:** Playback control generally works, including stop, play, pause,
previous, next, repeat, random, seek, and volume control.
- **Bug:** Search does not work, neither in the artist, album, or song
tabs. Mopidy gets no requests at all from MPoD when executing searches. Seems
like MPoD only searches in local cache, even if "Use local cache" is turned
off in MPoD's settings. Until this is fixed by the MPoD developer, MPoD will
be much less useful with Mopidy.
- **Bug:** When adding another playlist to the current playlist in MPoD,
the currently playing track restarts at the beginning. I do not currently
know enough about this bug, because I'm not sure if MPoD was in the "add to
active playlist" or "replace active playlist" mode when I tested it. I only
later learned what that button was for. Anyway, what I experienced was:
#. I play a track
#. I select a new playlist
#. MPoD reconnects to Mopidy for unknown reason
#. MPoD issues MPD command ``load "a playlist name"``
#. MPoD issues MPD command ``play "-1"``
#. MPoD issues MPD command ``playlistinfo "-1"``
#. I hear that the currently playing tracks restarts playback
- **Tips:** MPoD seems to cache stored playlists, but they won't work if the
server hasn't loaded stored playlists from e.g. Spotify yet. A trick to force
refetching of playlists from Mopidy is to add a new empty playlist in MPoD.
- **Wishlist:** Modifying the current playlists is not supported by MPoD it
seems.
- **Wishlist:** MPoD supports playback of Last.fm radio streams through the MPD
server. Mopidy does not currently support this, but there is a wishlist bug
at :issue:`38`.
- **Wishlist:** MPoD supports autodetection/-configuration of MPD servers
through the use of Bonjour. Mopidy does not currently support this, but there
is a wishlist bug at :issue:`39`.

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
@ -43,7 +43,7 @@ master_doc = 'index'
# General information about the project.
project = u'Mopidy'
copyright = u'2010, Stein Magnus Jodal and contributors'
copyright = u'2010-2011, Stein Magnus Jodal and contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@ -116,7 +116,7 @@ html_theme_path = ['_themes']
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
html_logo = '_static/mopidy.png'
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@ -153,7 +153,7 @@ html_last_updated_fmt = '%b %d, %Y'
#html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the

View File

@ -2,84 +2,33 @@
Roadmap
*******
This is the current roadmap and collection of wild ideas for future Mopidy
development. This is intended to be a living document and may change at any
time.
We intend to have about one timeboxed release every month. Thus, the roadmap is
oriented around "soon" and "later" instead of mapping each feature to a future
release.
Release schedule
================
We intend to have about one timeboxed feature release every month
in periods of active development. The feature releases are numbered 0.x.0. The
features added is a mix of what we feel is most important/requested of the
missing features, and features we develop just because we find them fun to
make, even though they may be useful for very few users or for a limited use
case.
Bugfix releases, numbered 0.x.y, will be released whenever we discover bugs
that are too serious to wait for the next feature release. We will only release
bugfix releases for the last feature release. E.g. when 0.3.0 is released, we
will no longer provide bugfix releases for the 0.2 series. In other words,
there will be just a single supported release at any point in time.
Possible targets for the next version
=====================================
Feature wishlist
================
- 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.
- MPD frontend:
- ``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.
- Local backend:
- Better library support.
- A script for creating a tag cache.
- An alternative to tag cache for caching metadata, i.e. Sqlite.
- **[DONE]** Last.fm scrobbling.
Stuff we want to do, but not right now, and maybe never
=======================================================
- Packaging and distribution:
- **[PENDING]** 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.
- Compatability:
- Run frontend tests against a real MPD server to ensure we are in sync.
- Backends:
- `Last.fm <http://www.last.fm/api>`_
- `WIMP <http://twitter.com/wimp/status/8975885632>`_
- DNLA/UPnP so Mopidy can play music from other DNLA MediaServers.
- Frontends:
- 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
Mobile <http://jquerymobile.com/>`_.
- DNLA/UPnP so Mopidy can be controlled from i.e. TVs.
- `XMMS2 <http://www.xmms2.org/>`_
- LIRC frontend for controlling Mopidy with a remote.
- Mixers:
- LIRC mixer for controlling arbitrary amplifiers remotely.
- Audio streaming:
- Ogg Vorbis/MP3 audio stream over HTTP, to MPD clients, `Squeezeboxes
<http://www.logitechsqueezebox.com/>`_, etc.
- Feed audio to an `Icecast <http://www.icecast.org/>`_ server.
- Stream to AirPort Express using `RAOP
<http://en.wikipedia.org/wiki/Remote_Audio_Output_Protocol>`_.
We maintain our collection of sane or less sane ideas for future Mopidy
features as `issues <https://github.com/mopidy/mopidy/issues>`_ at GitHub
labeled with `the "wishlist" label
<https://github.com/mopidy/mopidy/issues/labels/wishlist>`_. Feel free to vote
up any feature you would love to see in Mopidy, but please refrain from adding
a comment just to say "I want this too!". You are of course free to add
comments if you have suggestions for how the feature should work or be
implemented, and you may add new wishlist issues if your ideas are not already
represented.

View File

@ -1,4 +1,31 @@
.. include:: ../README.rst
******
Mopidy
******
Mopidy is a music server which can play music from `Spotify
<http://www.spotify.com/>`_ or from your local hard drive. To search for music
in Spotify's vast archive, manage playlists, and play music, you can use most
`MPD clients <http://mpd.wikia.com/>`_. MPD clients are available for most
platforms, including Windows, Mac OS X, Linux, Android, and iOS.
To install Mopidy, start out by reading :ref:`installation`.
If you get stuck, we usually hang around at ``#mopidy`` at `irc.freenode.net
<http://freenode.net/>`_. If you stumble into a bug or got a feature request,
please create an issue in the `issue tracker
<http://github.com/mopidy/mopidy/issues>`_.
Project resources
=================
- `Documentation for the latest release <http://www.mopidy.com/docs/master/>`_
- `Documentation for the development version
<http://www.mopidy.com/docs/develop/>`_
- `Source code <http://github.com/mopidy/mopidy>`_
- `Issue tracker <http://github.com/mopidy/mopidy/issues>`_
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
User documentation
==================
@ -6,11 +33,11 @@ User documentation
.. toctree::
:maxdepth: 3
changes
installation/index
settings
running
clients/index
changes
authors
licenses
@ -21,6 +48,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

@ -1,11 +1,12 @@
.. _installation:
************
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,89 +18,182 @@ 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
======================
Install latest stable 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
From APT archive
----------------
To later upgrade to the latest release::
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.
sudo pip install -U Mopidy
#. Add the archive's GPG key::
If you for some reason can't use ``pip``, try ``easy_install``.
wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
Next, you need to set a couple of :doc:`settings </settings>`, and then you're
ready to :doc:`run Mopidy </running>`.
#. 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 Mopidy development closer, you may install the
development version of Mopidy::
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.
sudo aptitude install git-core # On Ubuntu/Debian
sudo brew install git # On OS X
git clone git://github.com/mopidy/mopidy.git
cd mopidy/
sudo python setup.py install
To later update to the very latest version::
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
sudo python setup.py install
For an introduction to ``git``, please visit `git-scm.com
<http://git-scm.com/>`_.
<http://git-scm.com/>`_. Also, please read our :doc:`developer documentation
</development/index>`.
Next, you need to set a couple of :doc:`settings </settings>`, and then you're
ready to :doc:`run Mopidy </running>`.
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,8 +19,25 @@ install libspotify and `pyspotify <http://github.com/winjer/pyspotify>`_.
Spotify Group.
Installing libspotify on Linux
==============================
Installing libspotify
=====================
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/.
@ -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,32 +63,51 @@ 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
-------------------------
Assuming that you've already set up http://apt.mopidy.com/ as a software
source, run::
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/pyspotify/
cd pyspotify/
sudo python setup.py install
It is important that you install pyspotify from the ``mopidy`` branch of the

View File

@ -8,7 +8,7 @@ contributed what, please refer to our git repository.
Source code license
===================
Copyright 2009-2010 Stein Magnus Jodal and contributors
Copyright 2009-2011 Stein Magnus Jodal and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -26,7 +26,7 @@ limitations under the License.
Documentation license
=====================
Copyright 2010 Stein Magnus Jodal and contributors
Copyright 2010-2011 Stein Magnus Jodal and contributors
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
Unported License. To view a copy of this license, visit

View File

@ -0,0 +1,7 @@
*********************************************************
:mod:`mopidy.backends.dummy` -- Dummy backend for testing
*********************************************************
.. automodule:: mopidy.backends.dummy
:synopsis: Dummy backend used for testing
:members:

View File

@ -0,0 +1,7 @@
*********************************************
:mod:`mopidy.backends.local` -- Local backend
*********************************************
.. automodule:: mopidy.backends.local
:synopsis: Backend for playing music files on local storage
:members:

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
@ -34,6 +52,45 @@ file::
You may also want to change some of the ``LOCAL_*`` settings. See
:mod:`mopidy.settings`, for a full list of available settings.
.. note::
Currently, Mopidy supports using Spotify *or* local storage as a music
source. We're working on using both sources simultaneously, and will
hopefully have support for this in the 0.3 release.
.. _generating_a_tag_cache:
Generating a tag cache
----------------------
Before Mopidy 0.3 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_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_PATH` points to where your
music is located. Check the current setting by running::
mopidy --list-settings
#. Scan your music library. Currently the command outputs the ``tag_cache`` to
``stdout``, which means that you will need to redirect the output to a file
yourself::
mopidy-scan > tag_cache
#. Move the ``tag_cache`` file to the location
: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!
.. _use_mpd_on_a_network:
Connecting from other machines on the network
=============================================
@ -42,6 +99,13 @@ As a secure default, Mopidy only accepts connections from ``localhost``. If you
want to open it for connections from other machines on your network, see
the documentation for :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`.
If you open up Mopidy for your local network, you should consider turning on
MPD password authentication by setting
:attr:`mopidy.settings.MPD_SERVER_PASSWORD` to the password you want to use.
If the password is set, Mopidy will require MPD clients to provide the password
before they can do anything else. Mopidy only supports a single password, and
do not support different permission schemes like the original MPD server.
Scrobbling tracks to Last.fm
============================
@ -53,3 +117,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

@ -3,7 +3,7 @@ if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
def get_version():
return u'0.2.1'
return u'0.3.0'
class MopidyException(Exception):
def __init__(self, message, *args, **kwargs):

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,12 +4,17 @@ 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
# Too many instance attributes
#: Constant representing the paused state.
PAUSED = u'paused'
@ -51,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
@ -62,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:
@ -130,6 +134,9 @@ class BasePlaybackController(object):
Not necessarily the same track as :attr:`cp_track_at_next`.
"""
# pylint: disable = R0911
# Too many return statements
cp_tracks = self.backend.current_playlist.cp_tracks
if not cp_tracks:
@ -149,10 +156,9 @@ class BasePlaybackController(object):
return cp_tracks[0]
if self.repeat and self.single:
return cp_tracks[
(self.current_playlist_position) % len(cp_tracks)]
return cp_tracks[self.current_playlist_position]
if self.repeat:
if self.repeat and not self.single:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
@ -325,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 = []
@ -348,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
@ -386,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)
@ -400,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:
@ -423,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.
@ -460,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):
"""
@ -484,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.
@ -527,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,61 +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/mopidy/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,7 +5,10 @@ import os
import shutil
from mopidy import settings
from mopidy.backends.base import *
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
@ -13,7 +16,7 @@ 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.
@ -21,50 +24,64 @@ class LocalBackend(BaseBackend):
**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):
@ -114,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
@ -134,18 +151,19 @@ 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):
tracks = parse_mpd_tag_cache(settings.LOCAL_TAG_CACHE,
settings.LOCAL_MUSIC_FOLDER)
tag_cache = settings.LOCAL_TAG_CACHE_FILE
music_folder = settings.LOCAL_MUSIC_PATH
logger.info('Loading songs in %s from %s',
settings.LOCAL_MUSIC_FOLDER, settings.LOCAL_TAG_CACHE)
tracks = parse_mpd_tag_cache(tag_cache, music_folder)
logger.info('Loading songs in %s from %s', music_folder, tag_cache)
for track in tracks:
self._uri_mapping[track.uri] = track

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,9 +20,9 @@ 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)
logger.debug(u'Failed to lookup "%s": %s', uri, e)
return None
def refresh(self, uri=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,25 +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')
class LibspotifySessionManager(SpotifySessionManager, BaseThread):
cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
# pylint: disable = R0901
# SpotifySessionManager: Too many ancestors (9/7)
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
@ -32,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):
@ -40,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"""
@ -65,6 +68,8 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
# pylint: disable = R0913
# Too many arguments (8/5)
assert sample_type == 0, u'Expects 16-bit signed integer samples'
capabilites = """
audio/x-raw-int,
@ -94,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

@ -7,7 +7,7 @@ from mopidy import get_version, settings, OptionalDependencyError
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
from mopidy.utils.process import BaseThread
from mopidy.utils.process import BaseThread, GObjectEventThread
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
@ -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 = []
@ -47,6 +48,7 @@ class CoreProcess(BaseThread):
def setup(self):
self.setup_logging()
self.setup_settings()
self.gobject_loop = self.setup_gobject_loop(self.core_queue)
self.output = self.setup_output(self.core_queue)
self.backend = self.setup_backend(self.core_queue, self.output)
self.frontends = self.setup_frontends(self.core_queue, self.backend)
@ -61,6 +63,11 @@ class CoreProcess(BaseThread):
get_or_create_file('~/.mopidy/settings.py')
settings.validate()
def setup_gobject_loop(self, core_queue):
gobject_loop = GObjectEventThread(core_queue)
gobject_loop.start()
return gobject_loop
def setup_output(self, core_queue):
output = get_class(settings.OUTPUT)(core_queue)
output.start()

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

@ -1,27 +1,21 @@
import logging
import multiprocessing
import socket
import time
try:
import pylast
except ImportError as e:
except ImportError as import_error:
from mopidy import OptionalDependencyError
raise OptionalDependencyError(e)
raise OptionalDependencyError(import_error)
from mopidy import get_version, settings, SettingsError
from mopidy import settings, SettingsError
from mopidy.frontends.base import BaseFrontend
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 +28,7 @@ class LastfmFrontend(BaseFrontend):
**Dependencies:**
- `pylast <http://code.google.com/p/pylast/>`_ >= 0.4.30
- `pylast <http://code.google.com/p/pylast/>`_ >= 0.5.7
**Settings:**
@ -64,12 +58,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 self.scrobbler is not None:
while self.lastfm is not None:
self.connection.poll(None)
message = self.connection.recv()
self.process_message(message)
@ -78,16 +71,16 @@ 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')
logger.debug(u'Last.fm settings error: %s', e)
except (pylast.WSError, socket.error) as e:
logger.error(u'Last.fm connection error: %s', e)
except (pylast.NetworkError, pylast.MalformedResponseError,
pylast.WSError) as e:
logger.error(u'Error during Last.fm setup: %s', e)
def process_message(self, message):
if message['command'] == 'started_playing':
@ -99,22 +92,24 @@ class LastfmFrontendThread(BaseThread):
def started_playing(self, track):
artists = ', '.join([a.name for a in track.artists])
duration = track.length // 1000
duration = track.length and track.length // 1000 or 0
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)
except (pylast.ScrobblingError, socket.error) as e:
logger.warning(u'Last.fm now playing error: %s', e)
self.lastfm.update_now_playing(
artists,
(track.name or ''),
album=(track.album and track.album.name or ''),
duration=str(duration),
track_number=str(track.track_no),
mbid=(track.musicbrainz_id or ''))
except (pylast.ScrobblingError, pylast.NetworkError,
pylast.MalformedResponseError, pylast.WSError) as e:
logger.warning(u'Error submitting playing track to Last.fm: %s', e)
def stopped_playing(self, track, stop_position):
artists = ', '.join([a.name for a in track.artists])
duration = track.length // 1000
duration = track.length and track.length // 1000 or 0
stop_position = stop_position // 1000
if duration < 30:
logger.debug(u'Track too short to scrobble. (30s)')
@ -127,14 +122,14 @@ 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)
except (pylast.ScrobblingError, socket.error) as e:
logger.warning(u'Last.fm scrobbling error: %s', e)
self.lastfm.scrobble(
artists,
(track.name or ''),
str(self.last_start_time),
album=(track.album and track.album.name or ''),
track_number=str(track.track_no),
duration=str(duration),
mbid=(track.musicbrainz_id or ''))
except (pylast.ScrobblingError, pylast.NetworkError,
pylast.MalformedResponseError, pylast.WSError) as e:
logger.warning(u'Error submitting played track to Last.fm: %s', e)

View File

@ -14,6 +14,7 @@ class MpdFrontend(BaseFrontend):
**Settings:**
- :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`
- :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
- :attr:`mopidy.settings.MPD_SERVER_PORT`
"""

View File

@ -5,9 +5,11 @@ from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
# Do not remove the following import. The protocol modules must be imported to
# get them registered as request handlers.
# pylint: disable = W0611
from mopidy.frontends.mpd.protocol import (audio_output, command_list,
connection, current_playlist, empty, music_db, playback, reflection,
status, stickers, stored_playlists)
# pylint: enable = W0611
from mopidy.utils import flatten
class MpdDispatcher(object):

View File

@ -1,22 +1,20 @@
from mopidy import MopidyException
class MpdAckError(MopidyException):
"""
Available MPD error codes::
"""See fields on this class for available MPD error codes"""
ACK_ERROR_NOT_LIST = 1
ACK_ERROR_ARG = 2
ACK_ERROR_PASSWORD = 3
ACK_ERROR_PERMISSION = 4
ACK_ERROR_UNKNOWN = 5
ACK_ERROR_NO_EXIST = 50
ACK_ERROR_PLAYLIST_MAX = 51
ACK_ERROR_SYSTEM = 52
ACK_ERROR_PLAYLIST_LOAD = 53
ACK_ERROR_UPDATE_ALREADY = 54
ACK_ERROR_PLAYER_SYNC = 55
ACK_ERROR_EXIST = 56
"""
ACK_ERROR_NOT_LIST = 1
ACK_ERROR_ARG = 2
ACK_ERROR_PASSWORD = 3
ACK_ERROR_PERMISSION = 4
ACK_ERROR_UNKNOWN = 5
ACK_ERROR_NO_EXIST = 50
ACK_ERROR_PLAYLIST_MAX = 51
ACK_ERROR_SYSTEM = 52
ACK_ERROR_PLAYLIST_LOAD = 53
ACK_ERROR_UPDATE_ALREADY = 54
ACK_ERROR_PLAYER_SYNC = 55
ACK_ERROR_EXIST = 56
def __init__(self, message=u'', error_code=0, index=0, command=u''):
super(MpdAckError, self).__init__(message, error_code, index, command)
@ -37,19 +35,24 @@ class MpdAckError(MopidyException):
class MpdArgError(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdArgError, self).__init__(*args, **kwargs)
self.error_code = 2 # ACK_ERROR_ARG
self.error_code = MpdAckError.ACK_ERROR_ARG
class MpdPasswordError(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdPasswordError, self).__init__(*args, **kwargs)
self.error_code = MpdAckError.ACK_ERROR_PASSWORD
class MpdUnknownCommand(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdUnknownCommand, self).__init__(*args, **kwargs)
self.message = u'unknown command "%s"' % self.command
self.command = u''
self.error_code = 5 # ACK_ERROR_UNKNOWN
self.error_code = MpdAckError.ACK_ERROR_UNKNOWN
class MpdNoExistError(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdNoExistError, self).__init__(*args, **kwargs)
self.error_code = 50 # ACK_ERROR_NO_EXIST
self.error_code = MpdAckError.ACK_ERROR_NO_EXIST
class MpdNotImplemented(MpdAckError):
def __init__(self, *args, **kwargs):

View File

@ -13,7 +13,7 @@ implement our own MPD server which is compatible with the numerous existing
import re
#: The MPD protocol uses UTF-8 for encoding all data.
ENCODING = u'utf-8'
ENCODING = u'UTF-8'
#: The MPD protocol uses ``\n`` as line terminator.
LINE_TERMINATOR = u'\n'

View File

@ -1,5 +1,6 @@
from mopidy import settings
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
from mopidy.frontends.mpd.exceptions import MpdPasswordError
@handle_pattern(r'^close$')
def close(frontend):
@ -33,7 +34,11 @@ def password_(frontend, password):
This is used for authentication with the server. ``PASSWORD`` is
simply the plaintext password.
"""
raise MpdNotImplemented # TODO
# You will not get to this code without being authenticated. This is for
# when you are already authenticated, and are sending additional 'password'
# requests.
if settings.MPD_SERVER_PASSWORD != password:
raise MpdPasswordError(u'incorrect password', command=u'password')
@handle_pattern(r'^ping$')
def ping(frontend):

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

@ -9,9 +9,12 @@ def commands(frontend):
``commands``
Shows which commands the current user has access to.
As permissions is not implemented, any user has access to all commands.
"""
# FIXME When password auth is turned on and the client is not
# authenticated, 'commands' should list only the commands the client does
# have access to. To implement this we need access to the session object to
# check if the client is authenticated or not.
sorted_commands = sorted(list(mpd_commands))
# Not shown by MPD in its command list
@ -51,9 +54,11 @@ def notcommands(frontend):
``notcommands``
Shows which commands the current user does not have access to.
As permissions is not implemented, any user has access to all commands.
"""
# FIXME When password auth is turned on and the client is not
# authenticated, 'notcommands' should list all the commands the client does
# not have access to. To implement this we need access to the session
# object to check if the client is authenticated or not.
pass
@handle_pattern(r'^tagtypes$')

View File

@ -2,6 +2,7 @@ import asynchat
import logging
import multiprocessing
from mopidy import settings
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
from mopidy.utils.log import indent
from mopidy.utils.process import pickle_connection
@ -22,6 +23,7 @@ class MpdSession(asynchat.async_chat):
self.client_port = client_socket_address[1]
self.core_queue = core_queue
self.input_buffer = []
self.authenticated = False
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
def start(self):
@ -46,6 +48,11 @@ class MpdSession(asynchat.async_chat):
def handle_request(self, request):
"""Handle request by sending it to the MPD frontend."""
if not self.authenticated:
(self.authenticated, response) = self.check_password(request)
if response is not None:
self.send_response(response)
return
my_end, other_end = multiprocessing.Pipe()
self.core_queue.put({
'to': 'frontend',
@ -69,3 +76,26 @@ class MpdSession(asynchat.async_chat):
output = u'%s%s' % (output, LINE_TERMINATOR)
data = output.encode(ENCODING)
self.push(data)
def check_password(self, request):
"""
Takes any request and tries to authenticate the client using it.
:rtype: a two-tuple containing (is_authenticated, response_message). If
the response_message is :class:`None`, normal processing should
continue, even though the client may not be authenticated.
"""
if settings.MPD_SERVER_PASSWORD is None:
return (True, None)
command = request.split(' ')[0]
if command == 'password':
if request == 'password "%s"' % settings.MPD_SERVER_PASSWORD:
return (True, u'OK')
else:
return (False, u'ACK [3@0] {password} incorrect password')
if command in ('close', 'commands', 'notcommands', 'ping'):
return (False, None)
else:
return (False,
u'ACK [4@0] {%(c)s} you don\'t have permission for "%(c)s"' %
{'c': command})

View File

@ -1,3 +1,11 @@
import os
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 uri_to_path, split_path
def track_to_mpd_format(track, position=None, cpid=None):
"""
Format track for output to MPD client.
@ -8,12 +16,16 @@ def track_to_mpd_format(track, position=None, cpid=None):
:type position: integer
:param cpid: track's CPID (current playlist ID)
:type cpid: integer
:param key: if we should set key
:type key: boolean
:param mtime: if we should set mtime
:type mtime: boolean
:rtype: list of two-tuples
"""
result = [
('file', track.uri or ''),
('Time', track.length and (track.length // 1000) or 0),
('Artist', track_artists_to_mpd_format(track)),
('Artist', artists_to_mpd_format(track.artists)),
('Title', track.name or ''),
('Album', track.album and track.album.name or ''),
('Date', track.date or ''),
@ -23,20 +35,55 @@ def track_to_mpd_format(track, position=None, cpid=None):
track.track_no, track.album.num_tracks)))
else:
result.append(('Track', track.track_no))
if track.album is not None and track.album.artists:
artists = artists_to_mpd_format(track.album.artists)
result.append(('AlbumArtist', artists))
if position is not None and cpid is not None:
result.append(('Pos', position))
result.append(('Id', cpid))
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
def track_artists_to_mpd_format(track):
MPD_KEY_ORDER = '''
key file Time Artist AlbumArtist Title Album Track Date MUSICBRAINZ_ALBUMID
MUSICBRAINZ_ALBUMARTISTID MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime
'''.split()
def order_mpd_track_info(result):
"""
Order results from :func:`mopidy.frontends.mpd.translator.track_to_mpd_format`
so that it matches MPD's ordering. Simply a cosmetic fix for easier
diffing of tag_caches.
:param result: the track info
:type result: list of tuples
:rtype: list of tuples
"""
return sorted(result, key=lambda i: MPD_KEY_ORDER.index(i[0]))
def artists_to_mpd_format(artists):
"""
Format track artists for output to MPD client.
:param track: the track
:type track: :class:`mopidy.models.Track`
:param artists: the artists
:type track: array of :class:`mopidy.models.Artist`
:rtype: string
"""
artists = track.artists
artists.sort(key=lambda a: a.name)
return u', '.join([a.name for a in artists])
@ -72,3 +119,64 @@ def playlist_to_mpd_format(playlist, *args, **kwargs):
Arguments as for :func:`tracks_to_mpd_format`, except the first one.
"""
return tracks_to_mpd_format(playlist.tracks, *args, **kwargs)
def tracks_to_tag_cache_format(tracks):
"""
Format list of tracks for output to MPD tag cache
:param tracks: the tracks
:type tracks: list of :class:`mopidy.models.Track`
:rtype: list of lists of two-tuples
"""
result = [
('info_begin',),
('mpd_version', protocol.VERSION),
('fs_charset', protocol.ENCODING),
('info_end',)
]
tracks.sort(key=lambda t: t.uri)
_add_to_tag_cache(result, *tracks_to_directory_tree(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]
mtime = get_mtime(os.path.join(music_folder, path))
result.append(('directory', path))
result.append(('mtime', mtime))
result.append(('begin', name))
_add_to_tag_cache(result, *entry)
result.append(('end', name))
result.append(('songList begin',))
for track in files:
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',))
def tracks_to_directory_tree(tracks):
directories = ({}, [])
for track in tracks:
path = u''
current = directories
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)
for part in split_path(track_dir):
path = os.path.join(path, part)
if path not in current[0]:
current[0][path] = ({}, [])
current = current[0][path]
current[1].append(track)
return directories

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')
@ -40,7 +40,7 @@ class NadMixer(BaseMixer):
super(NadMixer, self).__init__(*args, **kwargs)
self._volume = None
self._pipe, other_end = Pipe()
NadTalker(pipe=other_end).start()
NadTalker(self.backend.core_queue, pipe=other_end).start()
def _get_volume(self):
return self._volume
@ -72,8 +72,9 @@ class NadTalker(BaseThread):
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
_nad_volume = None
def __init__(self, pipe=None):
super(NadTalker, self).__init__(name='NadTalker')
def __init__(self, core_queue, pipe=None):
super(NadTalker, self).__init__(core_queue)
self.name = u'NadTalker'
self.pipe = pipe
self._device = None
@ -146,6 +147,8 @@ class NadTalker(BaseThread):
return self._readline().replace('%s=' % key, '')
def _command_device(self, key, value):
if type(value) == unicode:
value = value.encode('utf-8')
self._write('%s=%s' % (key, value))
self._readline()

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,6 +5,9 @@ class DummyOutput(BaseOutput):
Audio output used for testing.
"""
# pylint: disable = R0902
# Too many instance attributes (9/7)
#: For testing. :class:`True` if :meth:`start` has been called.
start_called = False

View File

@ -1,6 +1,3 @@
import gobject
gobject.threads_init()
import pygst
pygst.require('0.10')
import gst
@ -28,20 +25,14 @@ class GStreamerOutput(BaseOutput):
def __init__(self, *args, **kwargs):
super(GStreamerOutput, self).__init__(*args, **kwargs)
# Start a helper thread that can run the gobject.MainLoop
self.messages_thread = GStreamerMessagesThread(self.core_queue)
# Start a helper thread that can process the output_queue
self.output_queue = multiprocessing.Queue()
self.player_thread = GStreamerPlayerThread(self.core_queue,
self.output_queue)
def start(self):
self.messages_thread.start()
self.player_thread.start()
def destroy(self):
self.messages_thread.destroy()
self.player_thread.destroy()
def process_message(self, message):
@ -78,7 +69,8 @@ class GStreamerOutput(BaseOutput):
return self._send_recv({'command': 'get_position'})
def set_position(self, position):
return self._send_recv({'command': 'set_position', 'position': position})
return self._send_recv({'command': 'set_position',
'position': position})
def set_state(self, state):
return self._send_recv({'command': 'set_state', 'state': state})
@ -90,21 +82,15 @@ class GStreamerOutput(BaseOutput):
return self._send_recv({'command': 'set_volume', 'volume': volume})
class GStreamerMessagesThread(BaseThread):
def __init__(self, core_queue):
super(GStreamerMessagesThread, self).__init__(core_queue)
self.name = u'GStreamerMessagesThread'
def run_inside_try(self):
gobject.MainLoop().run()
class GStreamerPlayerThread(BaseThread):
"""
A process for all work related to GStreamer.
The main loop processes events from both Mopidy and GStreamer.
This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be
running too. This is not enforced in any way by the code.
Make sure this subprocess is started by the MainThread in the top-most
parent process, and not some other thread. If not, we can get into the
problems described at

133
mopidy/scanner.py Normal file
View File

@ -0,0 +1,133 @@
import gobject
gobject.threads_init()
import pygst
pygst.require('0.10')
import gst
import datetime
from mopidy.utils.path import path_to_uri, find_files
from mopidy.models import Track, Artist, Album
def translator(data):
albumartist_kwargs = {}
album_kwargs = {}
artist_kwargs = {}
track_kwargs = {}
# FIXME replace with data.get('foo', None) ?
if 'album' in data:
album_kwargs['name'] = data['album']
if 'track-count' in data:
album_kwargs['num_tracks'] = data['track-count']
if 'artist' in data:
artist_kwargs['name'] = data['artist']
if 'date' in data:
date = data['date']
date = datetime.date(date.year, date.month, date.day)
track_kwargs['date'] = date
if 'title' in data:
track_kwargs['name'] = data['title']
if 'track-number' in data:
track_kwargs['track_no'] = data['track-number']
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)]
track_kwargs['uri'] = data['uri']
track_kwargs['length'] = data['duration']
track_kwargs['album'] = Album(**album_kwargs)
track_kwargs['artists'] = [Artist(**artist_kwargs)]
return Track(**track_kwargs)
class Scanner(object):
def __init__(self, folder, data_callback, error_callback=None):
self.uris = [path_to_uri(f) for f in find_files(folder)]
self.data_callback = data_callback
self.error_callback = error_callback
self.loop = gobject.MainLoop()
caps = gst.Caps('audio/x-raw-int')
fakesink = gst.element_factory_make('fakesink')
pad = fakesink.get_pad('sink')
self.uribin = gst.element_factory_make('uridecodebin')
self.uribin.connect('pad-added', self.process_new_pad, pad)
self.uribin.set_property('caps', caps)
self.pipe = gst.element_factory_make('pipeline')
self.pipe.add(fakesink)
self.pipe.add(self.uribin)
bus = self.pipe.get_bus()
bus.add_signal_watch()
bus.connect('message::tag', self.process_tags)
bus.connect('message::error', self.process_error)
def process_new_pad(self, source, pad, target_pad):
pad.link(target_pad)
def process_tags(self, bus, message):
data = message.parse_tag()
data = dict([(k, data[k]) for k in data.keys()])
data['uri'] = unicode(self.uribin.get_property('uri'))
data['duration'] = self.get_duration()
self.data_callback(data)
self.next_uri()
def process_error(self, bus, message):
if self.error_callback:
uri = self.uribin.get_property('uri')
errors = message.parse_error()
self.error_callback(uri, errors)
self.next_uri()
def get_duration(self):
self.pipe.get_state()
try:
return self.pipe.query_duration(
gst.FORMAT_TIME, None)[0] // gst.MSECOND
except gst.QueryError:
return None
def next_uri(self):
if not self.uris:
return self.stop()
self.pipe.set_state(gst.STATE_NULL)
self.uribin.set_property('uri', self.uris.pop())
self.pipe.set_state(gst.STATE_PAUSED)
def start(self):
if not self.uris:
return
self.next_uri()
self.loop.run()
def stop(self):
self.pipe.set_state(gst.STATE_NULL)
self.loop.quit()

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.
@ -77,8 +77,8 @@ LASTFM_PASSWORD = u''
#:
#: Default::
#:
#: LOCAL_MUSIC_FOLDER = u'~/music'
LOCAL_MUSIC_FOLDER = u'~/music'
#: LOCAL_MUSIC_PATH = u'~/music'
LOCAL_MUSIC_PATH = u'~/music'
#: Path to playlist folder with m3u files for local music.
#:
@ -86,8 +86,8 @@ LOCAL_MUSIC_FOLDER = u'~/music'
#:
#: Default::
#:
#: LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
#: LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
#: Path to tag cache for local music.
#:
@ -95,8 +95,8 @@ LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
#:
#: Default::
#:
#: LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
#: LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
#: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers.
#:
@ -164,22 +164,36 @@ OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
#: Listens on all interfaces, both IPv4 and IPv6.
MPD_SERVER_HOSTNAME = u'127.0.0.1'
#: The password required for connecting to the MPD server.
#:
#: Default: :class:`None`, which means no password required.
MPD_SERVER_PASSWORD = None
#: Which TCP port Mopidy's MPD server should listen to.
#:
#: 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

@ -1,6 +1,7 @@
import logging
import os
import sys
import re
import urllib
logger = logging.getLogger('mopidy.utils.path')
@ -21,8 +22,57 @@ 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)
return 'file://' + urllib.pathname2url(path)
def uri_to_path(uri):
if sys.platform == 'win32':
path = urllib.url2pathname(re.sub('^file:', '', uri))
else:
path = urllib.url2pathname(re.sub('^file://', '', uri))
return path.encode('latin1').decode('utf-8') # Undo double encoding
def split_path(path):
parts = []
while True:
path, part = os.path.split(path)
if part:
parts.insert(0, part)
if not path or path == '/':
break
return parts
# pylint: disable = W0612
# Unused variable 'dirnames'
def find_files(path):
if os.path.isfile(path):
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:
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):
self.fake = None
def __call__(self, path):
if self.fake is not None:
return self.fake
return int(os.stat(path).st_mtime)
def set_fake_time(self, time):
self.fake = time
def undo_fake(self):
self.fake = None
mtime = Mtime()

View File

@ -3,7 +3,9 @@ import multiprocessing
import multiprocessing.dummy
from multiprocessing.reduction import reduce_connection
import pickle
import sys
import gobject
gobject.threads_init()
from mopidy import SettingsError
@ -17,7 +19,6 @@ def unpickle_connection(pickled_connection):
(func, args) = pickle.loads(pickled_connection)
return func(*args)
class BaseProcess(multiprocessing.Process):
def __init__(self, core_queue):
super(BaseProcess, self).__init__()
@ -86,3 +87,25 @@ class BaseThread(multiprocessing.dummy.Process):
self.core_queue.put({'to': 'core', 'command': 'exit',
'status': status, 'reason': reason})
self.destroy()
class GObjectEventThread(BaseThread):
"""
A GObject event loop which is shared by all Mopidy components that uses
libraries that need a GObject event loop, like GStreamer and D-Bus.
Should be started by Mopidy's core and used by
:mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc.
"""
def __init__(self, core_queue):
super(GObjectEventThread, self).__init__(core_queue)
self.name = u'GObjectEventThread'
self.loop = None
def run_inside_try(self):
self.loop = gobject.MainLoop().run()
def destroy(self):
self.loop.quit()
super(GObjectEventThread, self).destroy()

View File

@ -23,7 +23,9 @@ class SettingsProxy(object):
if not os.path.isfile(settings_file):
return {}
sys.path.insert(0, dotdir)
# pylint: disable = F0401
import settings as local_settings_module
# pylint: enable = F0401
return self._get_settings_dict_from_module(local_settings_module)
def _get_settings_dict_from_module(self, module):
@ -47,8 +49,11 @@ class SettingsProxy(object):
if attr not in self.current:
raise SettingsError(u'Setting "%s" is not set.' % attr)
value = self.current[attr]
if type(value) != bool and not value:
if isinstance(value, basestring) and len(value) == 0:
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):
@ -92,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

@ -5,18 +5,19 @@
#
# C0103 - Invalid name "%s" (should match %s)
# C0111 - Missing docstring
# C0112 - Empty docstring
# E0102 - %s already defined line %s
# Does not understand @property getters and setters
# E0202 - An attribute inherited from %s hide this method
# Does not understand @property getters and setters
# E1101 - %s %r has no %r member
# Does not understand @property getters and setters
# R0201 - Method could be a function
# R0801 - Similar lines in %s files
# R0903 - Too few public methods (%s/%s)
# R0904 - Too many public methods (%s/%s)
# W0141 - Used builtin function %r
# R0921 - Abstract class not referenced
# W0141 - Used builtin function '%s'
# W0142 - Used * or ** magic
# W0401 - Wildcard import %s
# W0511 - TODO, FIXME and XXX in the code
# W0613 - Unused argument %r
#
disable-msg = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,W0141,W0142,W0401,W0511,W0613
disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0613

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

View File

@ -69,16 +69,18 @@ for dirpath, dirnames, filenames in os.walk(project_dir):
data_files.append([dirpath,
[os.path.join(dirpath, f) for f in filenames]])
data_files.append(('/usr/local/share/applications', ['data/mopidy.desktop']))
setup(
name='Mopidy',
version=get_version(),
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'],
scripts=['bin/mopidy', 'bin/mopidy-scan'],
url='http://www.mopidy.com/',
license='Apache License, Version 2.0',
description='MPD server with Spotify support',

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

Some files were not shown because too many files have changed in this diff Show More