diff --git a/.gitignore b/.gitignore
index e0026170..3fed7452 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,11 +2,12 @@
*.swp
.coverage
.noseids
+.tox
MANIFEST
build/
cover/
coverage.xml
dist/
docs/_build/
-mopidy.log
+mopidy.log*
nosetests.xml
diff --git a/MANIFEST.in b/MANIFEST.in
index f629bcc7..1c126f85 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,5 @@
include LICENSE pylintrc *.rst data/mopidy.desktop
-include mopidy/backends/libspotify/spotify_appkey.key
+include mopidy/backends/spotify/spotify_appkey.key
recursive-include docs *
prune docs/_build
recursive-include requirements *
diff --git a/README.rst b/README.rst
index 4f31fb59..c063de79 100644
--- a/README.rst
+++ b/README.rst
@@ -6,14 +6,15 @@ Mopidy is a music server which can play music from `Spotify
`_ 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 `_. 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 `_.
-* `Documentation (latest release) `_
-* `Documentation (development version) `_
-* `Source code `_
-* `Issue tracker `_
-* IRC: ``#mopidy`` at `irc.freenode.net `_
-* `Download development snapshot `_
+- `Documentation for the latest release `_
+- `Documentation for the development version
+ `_
+- `Source code `_
+- `Issue tracker `_
+- IRC: ``#mopidy`` at `irc.freenode.net `_
+- `Download development snapshot `_
diff --git a/bin/mopidy b/bin/mopidy
index 0472518e..aabf21d3 100755
--- a/bin/mopidy
+++ b/bin/mopidy
@@ -1,5 +1,5 @@
#! /usr/bin/env python
if __name__ == '__main__':
- from mopidy.__main__ import main
+ from mopidy.core import main
main()
diff --git a/docs/_static/mopidy.png b/docs/_static/mopidy.png
new file mode 100644
index 00000000..7d6ce5af
Binary files /dev/null and b/docs/_static/mopidy.png differ
diff --git a/docs/changes.rst b/docs/changes.rst
index ddb46bb8..fe7b9927 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -5,21 +5,134 @@ Changes
This change log is used to track all major changes to Mopidy.
-0.3.0 (in development)
+0.4.0 (in development)
======================
No description yet.
+
**Important changes**
+- Mopidy now depends on `Pykka `_ >=0.12. If you
+ install from APT, Pykka will automatically be installed. If you are not
+ installing from APT, you may install Pykka from PyPI::
+
+ sudo pip install -U Pykka
+
+- If you use the Spotify backend, you *should* upgrade to libspotify 0.0.7 and
+ the latest pyspotify from the Mopidy developers. If you install from APT,
+ libspotify and pyspotify will automatically be upgraded. If you are not
+ installing from APT, follow the instructions at
+ :doc:`/installation/libspotify/`.
+
+
+**Changes**
+
+- Mopidy now use Pykka actors for thread management and inter-thread
+ communication. The immediate advantage of this is that Mopidy now works on
+ Python 2.7. (Fixes: :issue:`66`)
+
- Spotify backend:
- - If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and
- the latest pyspotify from the Mopidy developers. Follow the instructions at
- :doc:`/installation/libspotify/`.
+ - Fixed multiple segmentation faults due to bugs in Pyspotify. Thanks to
+ Antoine Pierlot-Garcin and Jamie Kirkpatrick for patches to Pyspotify.
- - Support high bitrate (320k) audio. See
- :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` for details.
+ - Better error messages on wrong login or network problems. Thanks to Antoine
+ Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :issue:`77`)
+
+ - Reduce log level for trivial log messages from warning to info. (Fixes:
+ :issue:`71`)
+
+- Local backend:
+
+ - Fix crash in :command:`mopidy-scan` if a track has no artist name. Thanks
+ to Martins Grunskis for test and patch and "octe" for patch.
+
+ - Fix crash in `tag_cache` parsing if a track has no total number of tracks
+ in the album. Thanks to Martins Grunskis for the patch.
+
+- MPD frontend:
+
+ - Add support for "date" queries to both the ``find`` and ``search``
+ commands. This makes media library browsing in ncmpcpp work, though very
+ slow due to all the meta data requests to Spotify.
+
+ - Add support for ``play "-1"`` when in playing or paused state, which fixes
+ resume and addition of tracks to the current playlist while playing for the
+ MPoD client.
+
+ - Fix bug where ``status`` returned ``song: None``, which caused MPDroid to
+ crash. (Fixes: :issue:`69`)
+
+- Settings:
+
+ - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins
+ Grunskis for the bug report and patch. (Fixes: :issue:`63`)
+
+- Packaging:
+
+ - Replace test data symlinks with real files to avoid symlink issues when
+ installing with pip. (Fixes: :issue:`68`)
+
+
+0.3.1 (2010-01-22)
+==================
+
+A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
+
+**Bugfixes**
+
+- The Spotify application key was missing from the Python package.
+
+- Installation of the Python package as a normal user failed because it did not
+ have permissions to install ``mopidy.desktop``. The file is now only
+ installed if the installation is executed as the root user.
+
+
+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
+` and done a bit of testing of the available :ref:`Android
+` and :ref:`iOS 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
+ ``sudo 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
@@ -27,78 +140,106 @@ No description yet.
- 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.
+ types in pyspotify (see :issue:`62`).
-- Last.fm frontend:
-
- - If you use the Last.fm frontend, you need to upgrade to pylast 0.5.
-
- - Update to use Last.fm's new Scrobbling 2.0 API, as the old Submissions
- Protocol 1.2.1 is deprecated. (Fixes: :issue:`33`)
-
-
-**Changes**
-
-- Settings:
-
- - Automatically expand ``~`` to the user's home directory and make the path
- absolute for settings with names ending in ``_PATH`` or ``_FILE``.
- - Rename the following settings. The settings validator will warn you if you
- need to change your local settings.
-
- - ``LOCAL_MUSIC_FOLDER`` to :attr:`mopidy.settings.LOCAL_MUSIC_PATH`
- - ``LOCAL_PLAYLIST_FOLDER`` to
- :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH`
- - ``LOCAL_TAG_CACHE`` to :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE`
- - ``SPOTIFY_LIB_CACHE`` to :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`
-
-- Packaging and distribution:
-
- - Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome
- application menus.
- - Create infrastructure for creating Debian packages of Mopidy.
-
-- MPD frontend:
-
- - Support ``setvol 50`` without quotes around the argument. Fixes volume
- control in Droid MPD.
- - Support ``seek 1 120`` without quotes around the arguments. Fixes seek in
- Droid MPD.
+ - 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.
- - Support UTF-8 encoded tag caches with non-ASCII characters.
+ any help from the original MPD server. See :ref:`generating_a_tag_cache`
+ for instructions on how to use it.
-- Models:
+ - 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`.
-- Introduce the :ref:`provider concept `. Split the backend
- API into a :ref:`backend controller API ` (for
- frontend use) and a :ref:`backend provider API ` (for
- backend implementation use), which includes the following changes:
+ - Prepare for multi-backend support (see :issue:`40`) by introducing the
+ :ref:`provider concept `. Split the backend API into a
+ :ref:`backend controller API ` (for frontend use)
+ and a :ref:`backend provider API ` (for backend
+ implementation use), which includes the following changes:
- - Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`.
- - Rename ``BaseCurrentPlaylistController`` to
- :class:`mopidy.backends.base.CurrentPlaylistController`.
- - Split ``BaseLibraryController`` to
- :class:`mopidy.backends.base.LibraryController` and
- :class:`mopidy.backends.base.BaseLibraryProvider`.
- - Split ``BasePlaybackController`` to
- :class:`mopidy.backends.base.PlaybackController` and
- :class:`mopidy.backends.base.BasePlaybackProvider`.
- - Split ``BaseStoredPlaylistsController`` to
- :class:`mopidy.backends.base.StoredPlaylistsController` and
- :class:`mopidy.backends.base.BaseStoredPlaylistsProvider`.
-
-- Other API and package structure cleaning:
+ - 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`.
diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst
index de54dfcb..f5066210 100644
--- a/docs/clients/mpd.rst
+++ b/docs/clients/mpd.rst
@@ -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
-------
@@ -28,6 +31,9 @@ ncmpcpp
A console client that generally works well with Mopidy, and is regularly used
by Mopidy developers.
+Search
+^^^^^^
+
Search only works for ncmpcpp versions 0.5.1 and higher, and in two of the
three search modes:
@@ -39,6 +45,18 @@ three search modes:
If you run Ubuntu 10.04 or older, you can fetch an updated version of ncmpcpp
from `Launchpad `_.
+Communication mode
+^^^^^^^^^^^^^^^^^^
+
+In newer versions of ncmpcpp, like 0.5.5 shipped with Ubuntu 11.04, ncmcpp
+defaults to "notifications" mode for MPD communications, which Mopidy currently
+does not support. To workaround this limitation in Mopidy, edit the ncmpcpp
+configuration file at ``~/.ncmpcpp/config`` and add the following setting::
+
+ mpd_communication_mode = "polling"
+
+You can track the development of "notifications" mode support in Mopidy in
+:issue:`32`.
Graphical clients
@@ -47,52 +65,260 @@ Graphical clients
GMPC
----
-A GTK+ client which works well with Mopidy, and is regularly used by Mopidy
-developers.
+`GMPC `_ 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 `_ 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 `_ 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
+`_ 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 `_ client can be
+installed from the `iTunes Store
+`_.
+
+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`.
diff --git a/docs/conf.py b/docs/conf.py
index 9e7ff1fb..7ae3c126 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -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 tag referring to it. The value of this option must be the
@@ -202,4 +202,4 @@ latex_documents = [
needs_sphinx = '1.0'
-extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues#issue/%s', 'GH-')}
+extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues/%s', 'GH-')}
diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst
index a9cd8dc3..9ea3533f 100644
--- a/docs/development/contributing.rst
+++ b/docs/development/contributing.rst
@@ -74,11 +74,11 @@ Running tests
To run tests, you need a couple of dependencies. They can be installed through
Debian/Ubuntu package management::
- sudo aptitude install python-coverage python-nose
+ sudo aptitude install python-coverage python-mock python-nose
Or, they can be installed using ``pip``::
- sudo pip install -r requirements-tests.txt
+ sudo pip install -r requirements/tests.txt
Then, to run all tests, go to the project directory and run::
@@ -107,14 +107,14 @@ For more documentation on testing, check out the `nose documentation
Continuous integration server
=============================
-We run a continuous integration server called Hudson at
-http://hudson.mopidy.com/ that runs all test on multiple platforms (Ubuntu, OS
-X, etc.) for every commit we push to GitHub.
+We run a continuous integration (CI) server at http://ci.mopidy.com/ that runs
+all test on multiple platforms (Ubuntu, OS X, etc.) for every commit we push to
+GitHub.
-In addition to running tests, Hudson also does coverage statistics and uses
-pylint to check for errors and possible improvements in our code. So, if you're
-out of work, the code coverage and pylint data in Hudson should give you a
-place to start.
+In addition to running tests, the CI server also gathers coverage statistics
+and uses pylint to check for errors and possible improvements in our code. So,
+if you're out of work, the code coverage and pylint data at the CI server
+should give you a place to start.
Writing documentation
diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst
index 9db74a4d..cec8e9c7 100644
--- a/docs/development/roadmap.rst
+++ b/docs/development/roadmap.rst
@@ -2,88 +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:`25` for details.
-- **[WIP: feature/multi-backend]** Support for using multiple Mopidy backends
- simultaneously. Should make it possible to have both Spotify tracks and local
- tracks in the same playlist.
-- MPD frontend:
-
- - **[WIP: feature/mpd-password]** Password authentication.
- - ``idle`` support.
-
-- Spotify backend:
-
- - Write-support for Spotify, i.e. playlist management.
- - Virtual directories with e.g. starred tracks from Spotify.
- - **[DONE: v0.3]** Support for 320 kbps audio.
-
-- Local backend:
-
- - Better music library support.
- - **[DONE: v0.3]** A script for creating a tag cache.
- - An alternative to tag cache for caching metadata, i.e. Sqlite.
-
-- **[DONE: v0.2]** Last.fm scrobbling.
-
-
-Stuff we want to do, but not right now, and maybe never
-=======================================================
-
-- Packaging and distribution:
-
- - **[BLOCKED]** Create `Homebrew `_
- recipies for all our dependencies and Mopidy itself to make OS X
- installation a breeze. See `Homebrew's issue #1612
- `_.
- - **[DONE]** Create `Debian packages
- `_ of all our dependencies and
- Mopidy itself (hosted in our own Debian repo until we get stuff into the
- various distros) to make Debian/Ubuntu installation a breeze.
-
-- Compatability:
-
- - **[WIP: feature/blackbox-testing]** Run frontend tests against a real MPD
- server to ensure we are in sync.
-
-- Backends:
-
- - `Last.fm `_
- - `WIMP `_
- - DNLA/UPnP so Mopidy can play music from other DNLA MediaServers.
-
-- Frontends:
-
- - Publish the server's presence to the network using `Zeroconf
- `_/Avahi.
- - **[WIP: feature/mpris-frontend]** D-Bus/`MPRIS `_
- - **[WIP: feature/http-frontend]** REST/JSON web service with a jQuery client
- as example application. Maybe based upon `Tornado
- `_ and `jQuery
- Mobile `_.
- - DNLA/UPnP so Mopidy can be controlled from i.e. TVs.
- - `XMMS2 `_
- - 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
- `_, etc.
- - Feed audio to an `Icecast `_ server.
- - Stream to AirPort Express using `RAOP
- `_.
+We maintain our collection of sane or less sane ideas for future Mopidy
+features as `issues `_ at GitHub
+labeled with `the "wishlist" label
+`_. 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.
diff --git a/docs/index.rst b/docs/index.rst
index 09029a4f..0af45835 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,4 +1,31 @@
-.. include:: ../README.rst
+******
+Mopidy
+******
+
+Mopidy is a music server which can play music from `Spotify
+`_ 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 `_. 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
+`_. If you stumble into a bug or got a feature request,
+please create an issue in the `issue tracker
+`_.
+
+
+Project resources
+=================
+
+- `Documentation for the latest release `_
+- `Documentation for the development version
+ `_
+- `Source code `_
+- `Issue tracker `_
+- IRC: ``#mopidy`` at `irc.freenode.net `_
+
User documentation
==================
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index 26b50994..d1fbd0f6 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -1,3 +1,5 @@
+.. _installation:
+
************
Installation
************
@@ -23,6 +25,8 @@ Otherwise, make sure you got the required dependencies installed.
- Python >= 2.6, < 3
+- `Pykka `_ >= 0.12
+
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
- Mixer dependencies: The default mixer does not require any additional
diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst
index 5d278fe2..ca0ad87d 100644
--- a/docs/installation/libspotify.rst
+++ b/docs/installation/libspotify.rst
@@ -30,7 +30,7 @@ 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
+ sudo apt-get install libspotify7
When libspotify has been installed, continue with
:ref:`pyspotify_installation`.
@@ -39,14 +39,14 @@ When libspotify has been installed, continue with
On Linux from source
--------------------
-Download and install libspotify 0.0.6 for your OS and CPU architecture from
+Download and install libspotify 0.0.7 for your OS and CPU architecture from
https://developer.spotify.com/en/libspotify/.
For 64-bit Linux the process is as follows::
- wget http://developer.spotify.com/download/libspotify/libspotify-0.0.6-linux6-x86_64.tar.gz
- tar zxfv libspotify-0.0.6-linux6-x86_64.tar.gz
- cd libspotify-0.0.6-linux6-x86_64/
+ wget http://developer.spotify.com/download/libspotify/libspotify-0.0.7-linux6-x86_64.tar.gz
+ tar zxfv libspotify-0.0.7-linux6-x86_64.tar.gz
+ cd libspotify-0.0.7-linux6-x86_64/
sudo make install prefix=/usr/local
sudo ldconfig
@@ -113,4 +113,4 @@ Get the pyspotify code, and install it::
It is important that you install pyspotify from the ``mopidy`` branch of the
``mopidy/pyspotify`` repository, as the upstream repository at
``winjer/pyspotify`` is not updated with changes needed to support e.g.
-libspotify 0.0.6 and high bitrate audio.
+libspotify 0.0.7 and high bitrate audio.
diff --git a/docs/licenses.rst b/docs/licenses.rst
index c3a13904..7f4ed0ce 100644
--- a/docs/licenses.rst
+++ b/docs/licenses.rst
@@ -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
diff --git a/docs/settings.rst b/docs/settings.rst
index 532f52cf..1d4a4972 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -59,11 +59,13 @@ You may also want to change some of the ``LOCAL_*`` settings. See
hopefully have support for this in the 0.3 release.
+.. _generating_a_tag_cache:
+
Generating a tag cache
----------------------
-Previously the local storage backend relied purely on ``tag_cache`` files
-generated by the original MPD server. To remedy this the command
+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``.
@@ -82,12 +84,14 @@ To make a ``tag_cache`` of your local music available for Mopidy:
mopidy-scan > tag_cache
#. Move the ``tag_cache`` file to the location
- :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` is set to, or change the setting to
- point to where your ``tag_cache`` file is.
+ :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
=============================================
@@ -95,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
============================
diff --git a/mopidy/__init__.py b/mopidy/__init__.py
index fffa25c7..e9ced3ae 100644
--- a/mopidy/__init__.py
+++ b/mopidy/__init__.py
@@ -2,8 +2,27 @@ import sys
if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
+from subprocess import PIPE, Popen
+
+VERSION = (0, 4, 0)
+
+def get_git_version():
+ process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE)
+ if process.wait() != 0:
+ raise EnvironmentError('Execution of "git describe" failed')
+ version = process.stdout.read().strip()
+ if version.startswith('v'):
+ version = version[1:]
+ return version
+
+def get_plain_version():
+ return '.'.join(map(str, VERSION))
+
def get_version():
- return u'0.3.0'
+ try:
+ return get_git_version()
+ except EnvironmentError:
+ return get_plain_version()
class MopidyException(Exception):
def __init__(self, message, *args, **kwargs):
diff --git a/mopidy/__main__.py b/mopidy/__main__.py
index 20e78f5a..169c2754 100644
--- a/mopidy/__main__.py
+++ b/mopidy/__main__.py
@@ -1,17 +1,10 @@
-import os
-import sys
-
# Add ../ to the path so we can run Mopidy from a Git checkout without
# installing it on the system.
+import os
+import sys
sys.path.insert(0,
os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
-from mopidy.core import CoreProcess
-
-def main():
- # Explictly call run() instead of start(), since we don't need to start
- # another process.
- CoreProcess().run()
-
if __name__ == '__main__':
+ from mopidy.core import main
main()
diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py
index 096a433f..038e2d7b 100644
--- a/mopidy/backends/base/__init__.py
+++ b/mopidy/backends/base/__init__.py
@@ -1,12 +1,4 @@
-from copy import copy
import logging
-import random
-import time
-
-from mopidy import settings
-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
@@ -17,30 +9,6 @@ from .stored_playlists import (StoredPlaylistsController,
logger = logging.getLogger('mopidy.backends.base')
class Backend(object):
- """
- :param core_queue: a queue for sending messages to
- :class:`mopidy.process.CoreProcess`
- :type core_queue: :class:`multiprocessing.Queue`
- :param output: the audio output
- :type output: :class:`mopidy.outputs.gstreamer.GStreamerOutput` or similar
- :param mixer_class: either a mixer class, or :class:`None` to use the mixer
- defined in settings
- :type mixer_class: a subclass of :class:`mopidy.mixers.BaseMixer` or
- :class:`None`
- """
-
- def __init__(self, core_queue=None, output=None, mixer_class=None):
- self.core_queue = core_queue
- self.output = output
- if mixer_class is None:
- mixer_class = get_class(settings.MIXER)
- self.mixer = mixer_class(self)
-
- #: A :class:`multiprocessing.Queue` which can be used by e.g. library
- #: callbacks executing in other threads to send messages to the core
- #: thread, so that action may be taken in the correct thread.
- core_queue = None
-
#: The current playlist controller. An instance of
#: :class:`mopidy.backends.base.CurrentPlaylistController`.
current_playlist = None
@@ -49,9 +17,6 @@ class Backend(object):
# :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.PlaybackController`.
playback = None
@@ -62,24 +27,3 @@ class Backend(object):
#: List of URI prefixes this backend can handle.
uri_handlers = []
-
- def destroy(self):
- """
- Call destroy on all sub-components in backend so that they can cleanup
- after themselves.
- """
-
- if self.current_playlist:
- self.current_playlist.destroy()
-
- if self.library:
- self.library.destroy()
-
- if self.mixer:
- self.mixer.destroy()
-
- if self.playback:
- self.playback.destroy()
-
- if self.stored_playlists:
- self.stored_playlists.destroy()
diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py
index fe7d1de9..ffdce176 100644
--- a/mopidy/backends/base/current_playlist.py
+++ b/mopidy/backends/base/current_playlist.py
@@ -2,8 +2,6 @@ from copy import copy
import logging
import random
-from mopidy.frontends.mpd import translator
-
logger = logging.getLogger('mopidy.backends.base')
class CurrentPlaylistController(object):
@@ -12,6 +10,8 @@ class CurrentPlaylistController(object):
:type backend: :class:`mopidy.backends.base.Backend`
"""
+ pykka_traversable = True
+
def __init__(self, backend):
self.backend = backend
self._cp_tracks = []
@@ -197,8 +197,3 @@ class CurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
-
- def mpd_format(self, *args, **kwargs):
- """Not a part of the generic backend API."""
- kwargs['cpids'] = [ct[0] for ct in self._cp_tracks]
- return translator.tracks_to_mpd_format(self.tracks, *args, **kwargs)
diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py
index fd018b5f..a30ed412 100644
--- a/mopidy/backends/base/library.py
+++ b/mopidy/backends/base/library.py
@@ -10,6 +10,8 @@ class LibraryController(object):
:type provider: instance of :class:`BaseLibraryProvider`
"""
+ pykka_traversable = True
+
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
@@ -82,6 +84,8 @@ class BaseLibraryProvider(object):
:type backend: :class:`mopidy.backends.base.Backend`
"""
+ pykka_traversable = True
+
def __init__(self, backend):
self.backend = backend
diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py
index 8ab60470..88aa0877 100644
--- a/mopidy/backends/base/playback.py
+++ b/mopidy/backends/base/playback.py
@@ -2,6 +2,11 @@ import logging
import random
import time
+from pykka.registry import ActorRegistry
+
+from mopidy.frontends.base import BaseFrontend
+from mopidy.outputs.base import BaseOutput
+
logger = logging.getLogger('mopidy.backends.base')
class PlaybackController(object):
@@ -15,6 +20,8 @@ class PlaybackController(object):
# pylint: disable = R0902
# Too many instance attributes
+ pykka_traversable = True
+
#: Constant representing the paused state.
PAUSED = u'paused'
@@ -62,8 +69,8 @@ class PlaybackController(object):
self._state = self.STOPPED
self._shuffled = []
self._first_shuffle = True
- self._play_time_accumulated = 0
- self._play_time_started = None
+ self.play_time_accumulated = 0
+ self.play_time_started = None
def destroy(self):
"""
@@ -240,7 +247,7 @@ class PlaybackController(object):
if self.repeat or self.consume or self.random:
return self.current_cp_track
- if self.current_cp_track is None or self.current_playlist_position == 0:
+ if self.current_playlist_position in (None, 0):
return None
return self.backend.current_playlist.cp_tracks[
@@ -269,7 +276,7 @@ class PlaybackController(object):
def state(self, new_state):
(old_state, self._state) = (self.state, new_state)
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
- # FIXME _play_time stuff assumes backend does not have a better way of
+ # FIXME play_time stuff assumes backend does not have a better way of
# handeling this stuff :/
if (old_state in (self.PLAYING, self.STOPPED)
and new_state == self.PLAYING):
@@ -282,28 +289,35 @@ class PlaybackController(object):
@property
def time_position(self):
"""Time position in milliseconds."""
- output_position = self.backend.output.get_position()
+ output_position = self._time_position_from_output()
if output_position is not None:
return output_position
if self.state == self.PLAYING:
time_since_started = (self._current_wall_time -
- self._play_time_started)
- return self._play_time_accumulated + time_since_started
+ self.play_time_started)
+ return self.play_time_accumulated + time_since_started
elif self.state == self.PAUSED:
- return self._play_time_accumulated
+ return self.play_time_accumulated
elif self.state == self.STOPPED:
return 0
+ def _time_position_from_output(self):
+ output_refs = ActorRegistry.get_by_class(BaseOutput)
+ if not output_refs:
+ return None
+ output = output_refs[0].proxy()
+ return output.get_position()
+
def _play_time_start(self):
- self._play_time_accumulated = 0
- self._play_time_started = self._current_wall_time
+ self.play_time_accumulated = 0
+ self.play_time_started = self._current_wall_time
def _play_time_pause(self):
- time_since_started = self._current_wall_time - self._play_time_started
- self._play_time_accumulated += time_since_started
+ time_since_started = self._current_wall_time - self.play_time_started
+ self.play_time_accumulated += time_since_started
def _play_time_resume(self):
- self._play_time_started = self._current_wall_time
+ self.play_time_started = self._current_wall_time
@property
def _current_wall_time(self):
@@ -436,8 +450,8 @@ class PlaybackController(object):
self.next()
return True
- self._play_time_started = self._current_wall_time
- self._play_time_accumulated = time_position
+ self.play_time_started = self._current_wall_time
+ self.play_time_accumulated = time_position
return self.provider.seek(time_position)
@@ -449,11 +463,10 @@ class PlaybackController(object):
stopping
:type clear_current_track: boolean
"""
- if self.state == self.STOPPED:
- return
- self._trigger_stopped_playing_event()
- if self.provider.stop():
- self.state = self.STOPPED
+ if self.state != self.STOPPED:
+ self._trigger_stopped_playing_event()
+ if self.provider.stop():
+ self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
@@ -464,9 +477,11 @@ class PlaybackController(object):
For internal use only. Should be called by the backend directly after a
track has started playing.
"""
- if self.current_track is not None:
- self.backend.core_queue.put({
- 'to': 'frontend',
+ if self.current_track is None:
+ return
+ frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
+ for frontend_ref in frontend_refs:
+ frontend_ref.send_one_way({
'command': 'started_playing',
'track': self.current_track,
})
@@ -479,9 +494,11 @@ class PlaybackController(object):
is stopped playing, e.g. at the next, previous, and stop actions and at
end-of-track.
"""
- if self.current_track is not None:
- self.backend.core_queue.put({
- 'to': 'frontend',
+ if self.current_track is None:
+ return
+ frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
+ for frontend_ref in frontend_refs:
+ frontend_ref.send_one_way({
'command': 'stopped_playing',
'track': self.current_track,
'stop_position': self.time_position,
@@ -494,6 +511,8 @@ class BasePlaybackProvider(object):
:type backend: :class:`mopidy.backends.base.Backend`
"""
+ pykka_traversable = True
+
def __init__(self, backend):
self.backend = backend
diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py
index 6578c046..aca78a8c 100644
--- a/mopidy/backends/base/stored_playlists.py
+++ b/mopidy/backends/base/stored_playlists.py
@@ -11,6 +11,8 @@ class StoredPlaylistsController(object):
:type provider: instance of :class:`BaseStoredPlaylistsProvider`
"""
+ pykka_traversable = True
+
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
@@ -125,6 +127,8 @@ class BaseStoredPlaylistsProvider(object):
:type backend: :class:`mopidy.backends.base.Backend`
"""
+ pykka_traversable = True
+
def __init__(self, backend):
self.backend = backend
self._playlists = []
diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py
index 2d72ec8a..90c87dac 100644
--- a/mopidy/backends/dummy/__init__.py
+++ b/mopidy/backends/dummy/__init__.py
@@ -1,21 +1,13 @@
+from pykka.actor import ThreadingActor
+
from mopidy.backends.base import (Backend, CurrentPlaylistController,
PlaybackController, BasePlaybackProvider, LibraryController,
BaseLibraryProvider, StoredPlaylistsController,
BaseStoredPlaylistsProvider)
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist
-from mopidy.outputs.dummy import DummyOutput
-class DummyQueue(object):
- def __init__(self):
- self.received_messages = []
-
- def put(self, message):
- self.received_messages.append(message)
-
-
-class DummyBackend(Backend):
+class DummyBackend(ThreadingActor, Backend):
"""
A backend which implements the backend API in the simplest way possible.
Used in tests of the frontends.
@@ -24,9 +16,6 @@ class DummyBackend(Backend):
"""
def __init__(self, *args, **kwargs):
- kwargs['core_queue'] = DummyQueue()
- kwargs['output'] = DummyOutput(core_queue=DummyQueue())
- kwargs['mixer_class'] = DummyMixer
super(DummyBackend, self).__init__(*args, **kwargs)
self.current_playlist = CurrentPlaylistController(backend=self)
@@ -49,13 +38,13 @@ class DummyBackend(Backend):
class DummyLibraryProvider(BaseLibraryProvider):
def __init__(self, *args, **kwargs):
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
- self._library = []
+ self.dummy_library = []
def find_exact(self, **query):
return Playlist()
def lookup(self, uri):
- matches = filter(lambda t: uri == t.uri, self._library)
+ matches = filter(lambda t: uri == t.uri, self.dummy_library)
if matches:
return matches[0]
diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py
index bff5cb61..fc7f170c 100644
--- a/mopidy/backends/local/__init__.py
+++ b/mopidy/backends/local/__init__.py
@@ -1,22 +1,24 @@
import glob
import logging
-import multiprocessing
import os
import shutil
+from pykka.actor import ThreadingActor
+from pykka.registry import ActorRegistry
+
from mopidy import settings
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
+from mopidy.outputs.base import BaseOutput
from .translator import parse_m3u, parse_mpd_tag_cache
logger = logging.getLogger(u'mopidy.backends.local')
-class LocalBackend(Backend):
+class LocalBackend(ThreadingActor, Backend):
"""
A backend for playing music from a local music archive.
@@ -48,22 +50,29 @@ class LocalBackend(Backend):
self.uri_handlers = [u'file://']
+ self.output = None
+
+ def on_start(self):
+ output_refs = ActorRegistry.get_by_class(BaseOutput)
+ assert len(output_refs) == 1, 'Expected exactly one running output.'
+ self.output = output_refs[0].proxy()
+
class LocalPlaybackProvider(BasePlaybackProvider):
def pause(self):
- return self.backend.output.set_state('PAUSED')
+ return self.backend.output.set_state('PAUSED').get()
def play(self, track):
- return self.backend.output.play_uri(track.uri)
+ return self.backend.output.play_uri(track.uri).get()
def resume(self):
- return self.backend.output.set_state('PLAYING')
+ return self.backend.output.set_state('PLAYING').get()
def seek(self, time_position):
- return self.backend.output.set_position(time_position)
+ return self.backend.output.set_position(time_position).get()
def stop(self):
- return self.backend.output.set_state('READY')
+ return self.backend.output.set_state('READY').get()
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py
index 51522ead..be7ab8a8 100644
--- a/mopidy/backends/local/translator.py
+++ b/mopidy/backends/local/translator.py
@@ -100,8 +100,11 @@ def _convert_mpd_data(data, tracks, music_dir):
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 '/' in data['track']:
+ album_kwargs['num_tracks'] = int(data['track'].split('/')[1])
+ track_kwargs['track_no'] = int(data['track'].split('/')[0])
+ else:
+ track_kwargs['track_no'] = int(data['track'])
if 'artist' in data:
artist_kwargs['name'] = data['artist']
diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py
index d36f6250..1ac5f0be 100644
--- a/mopidy/backends/spotify/__init__.py
+++ b/mopidy/backends/spotify/__init__.py
@@ -1,14 +1,18 @@
import logging
+from pykka.actor import ThreadingActor
+from pykka.registry import ActorRegistry
+
from mopidy import settings
from mopidy.backends.base import (Backend, CurrentPlaylistController,
LibraryController, PlaybackController, StoredPlaylistsController)
+from mopidy.outputs.base import BaseOutput
logger = logging.getLogger('mopidy.backends.spotify')
ENCODING = 'utf-8'
-class SpotifyBackend(Backend):
+class SpotifyBackend(ThreadingActor, Backend):
"""
A backend for playing music from the `Spotify `_
music streaming service. The backend uses the official `libspotify
@@ -59,6 +63,14 @@ class SpotifyBackend(Backend):
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
+ self.output = None
+ self.spotify = None
+
+ def on_start(self):
+ output_refs = ActorRegistry.get_by_class(BaseOutput)
+ assert len(output_refs) == 1, 'Expected exactly one running output.'
+ self.output = output_refs[0].proxy()
+
self.spotify = self._connect()
def _connect(self):
@@ -67,8 +79,6 @@ class SpotifyBackend(Backend):
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)
+ settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD)
spotify.start()
return spotify
diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py
index 16391473..40d4a099 100644
--- a/mopidy/backends/spotify/library.py
+++ b/mopidy/backends/spotify/library.py
@@ -1,5 +1,5 @@
import logging
-import multiprocessing
+import Queue
from spotify import Link, SpotifyError
@@ -22,7 +22,7 @@ class SpotifyLibraryProvider(BaseLibraryProvider):
# playlists.
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):
@@ -54,8 +54,9 @@ class SpotifyLibraryProvider(BaseLibraryProvider):
spotify_query.append(u'%s:"%s"' % (field, value))
spotify_query = u' '.join(spotify_query)
logger.debug(u'Spotify search query: %s' % spotify_query)
- my_end, other_end = multiprocessing.Pipe()
- self.backend.spotify.search(spotify_query.encode(ENCODING), other_end)
- my_end.poll(None)
- playlist = my_end.recv()
- return playlist
+ queue = Queue.Queue()
+ self.backend.spotify.search(spotify_query.encode(ENCODING), queue)
+ try:
+ return queue.get(timeout=3) # XXX What is an reasonable timeout?
+ except Queue.Empty:
+ return Playlist(tracks=[])
diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py
index a066d90e..69050eb8 100644
--- a/mopidy/backends/spotify/playback.py
+++ b/mopidy/backends/spotify/playback.py
@@ -23,7 +23,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
self.backend.output.set_state('PLAYING')
return True
except SpotifyError as e:
- logger.warning('Play %s failed: %s', track.uri, e)
+ logger.info('Playback of %s failed: %s', track.uri, e)
return False
def resume(self):
diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py
index 9736f2eb..e92fe89e 100644
--- a/mopidy/backends/spotify/session_manager.py
+++ b/mopidy/backends/spotify/session_manager.py
@@ -2,11 +2,15 @@ import logging
import os
import threading
-import spotify.manager
+from spotify.manager import SpotifySessionManager as PyspotifySessionManager
+
+from pykka.registry import ActorRegistry
from mopidy import get_version, settings
+from mopidy.backends.base import Backend
from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist
+from mopidy.outputs.base import BaseOutput
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.backends.spotify.session_manager')
@@ -14,26 +18,41 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager')
# pylint: disable = R0901
# SpotifySessionManager: Too many ancestors (9/7)
-class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
+class SpotifySessionManager(BaseThread, PyspotifySessionManager):
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):
- spotify.manager.SpotifySessionManager.__init__(
- self, username, password)
- BaseThread.__init__(self, core_queue)
+ def __init__(self, username, password):
+ PyspotifySessionManager.__init__(self, username, password)
+ BaseThread.__init__(self)
self.name = 'SpotifySMThread'
- self.output = output
+
+ self.output = None
+ self.backend = None
+
self.connected = threading.Event()
self.session = None
def run_inside_try(self):
+ self.setup()
self.connect()
+ def setup(self):
+ output_refs = ActorRegistry.get_by_class(BaseOutput)
+ assert len(output_refs) == 1, 'Expected exactly one running output.'
+ self.output = output_refs[0].proxy()
+
+ backend_refs = ActorRegistry.get_by_class(Backend)
+ assert len(backend_refs) == 1, 'Expected exactly one running backend.'
+ self.backend = backend_refs[0].proxy()
+
def logged_in(self, session, error):
"""Callback used by pyspotify"""
+ if error:
+ logger.error(u'Spotify login error: %s', error)
+ return
logger.info(u'Connected to Spotify')
self.session = session
if settings.SPOTIFY_HIGH_BITRATE:
@@ -55,7 +74,7 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
def connection_error(self, session, error):
"""Callback used by pyspotify"""
- logger.error(u'Connection error: %s', error)
+ logger.error(u'Spotify connection error: %s', error)
def message_to_user(self, session, message):
"""Callback used by pyspotify"""
@@ -88,7 +107,7 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
def play_token_lost(self, session):
"""Callback used by pyspotify"""
logger.debug(u'Play token lost')
- self.core_queue.put({'command': 'stop_playback'})
+ self.backend.playback.pause()
def log_message(self, session, data):
"""Callback used by pyspotify"""
@@ -107,19 +126,16 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread):
playlists.append(
SpotifyTranslator.to_mopidy_playlist(spotify_playlist))
playlists = filter(None, playlists)
- self.core_queue.put({
- 'command': 'set_stored_playlists',
- 'playlists': playlists,
- })
+ self.backend.stored_playlists.playlists = playlists
logger.debug(u'Refreshed %d stored playlist(s)', len(playlists))
- def search(self, query, connection):
+ def search(self, query, queue):
"""Search method used by Mopidy backend"""
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
playlist = Playlist(tracks=[
SpotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
- connection.send(playlist)
+ queue.put(playlist)
self.connected.wait()
self.session.search(query, callback)
diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py
index 50ee07d1..651154f8 100644
--- a/mopidy/backends/spotify/translator.py
+++ b/mopidy/backends/spotify/translator.py
@@ -28,9 +28,9 @@ class SpotifyTranslator(object):
@classmethod
def to_mopidy_track(cls, spotify_track):
- if not spotify_track.is_loaded():
- return Track(name=u'[loading...]')
uri = str(Link.from_track(spotify_track, 0))
+ if not spotify_track.is_loaded():
+ return Track(uri=uri, name=u'[loading...]')
if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
date = dt.date(spotify_track.album().year(), 1, 1)
else:
@@ -60,5 +60,5 @@ class SpotifyTranslator(object):
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist],
)
except SpotifyError, e:
- logger.warning(u'Failed translating Spotify playlist '
+ logger.info(u'Failed translating Spotify playlist '
'(probably a playlist folder boundary): %s', e)
diff --git a/mopidy/core.py b/mopidy/core.py
index 1a4ed7cc..5a83b1e6 100644
--- a/mopidy/core.py
+++ b/mopidy/core.py
@@ -1,114 +1,76 @@
import logging
-import multiprocessing
import optparse
-import sys
+import time
+
+from pykka.registry import ActorRegistry
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, GObjectEventThread
+from mopidy.utils.process import GObjectEventThread
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
-class CoreProcess(BaseThread):
- def __init__(self):
- self.core_queue = multiprocessing.Queue()
- 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 = []
+def main():
+ options = parse_options()
+ setup_logging(options.verbosity_level, options.save_debug_log)
+ setup_settings()
+ setup_gobject_loop()
+ setup_output()
+ setup_mixer()
+ setup_backend()
+ setup_frontends()
+ try:
+ while ActorRegistry.get_all():
+ time.sleep(1)
+ logger.info(u'No actors left. Exiting...')
+ except KeyboardInterrupt:
+ logger.info(u'User interrupt. Exiting...')
+ ActorRegistry.stop_all()
- def parse_options(self):
- parser = optparse.OptionParser(version='Mopidy %s' % get_version())
- parser.add_option('-q', '--quiet',
- action='store_const', const=0, dest='verbosity_level',
- help='less output (warning level)')
- parser.add_option('-v', '--verbose',
- action='store_const', const=2, dest='verbosity_level',
- help='more output (debug level)')
- parser.add_option('--save-debug-log',
- action='store_true', dest='save_debug_log',
- help='save debug log to "./mopidy.log"')
- parser.add_option('--list-settings',
- action='callback', callback=list_settings_optparse_callback,
- help='list current settings')
- return parser.parse_args()[0]
+def parse_options():
+ parser = optparse.OptionParser(version='Mopidy %s' % get_version())
+ parser.add_option('-q', '--quiet',
+ action='store_const', const=0, dest='verbosity_level',
+ help='less output (warning level)')
+ parser.add_option('-v', '--verbose',
+ action='store_const', const=2, dest='verbosity_level',
+ help='more output (debug level)')
+ parser.add_option('--save-debug-log',
+ action='store_true', dest='save_debug_log',
+ help='save debug log to "./mopidy.log"')
+ parser.add_option('--list-settings',
+ action='callback', callback=list_settings_optparse_callback,
+ help='list current settings')
+ return parser.parse_args()[0]
- def run_inside_try(self):
- self.setup()
- while True:
- message = self.core_queue.get()
- self.process_message(message)
+def setup_settings():
+ get_or_create_folder('~/.mopidy/')
+ get_or_create_file('~/.mopidy/settings.py')
+ settings.validate()
- 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)
+def setup_gobject_loop():
+ gobject_loop = GObjectEventThread()
+ gobject_loop.start()
+ return gobject_loop
- def setup_logging(self):
- setup_logging(self.options.verbosity_level,
- self.options.save_debug_log)
- logger.info(u'-- Starting Mopidy --')
+def setup_output():
+ return get_class(settings.OUTPUT).start().proxy()
- def setup_settings(self):
- get_or_create_folder('~/.mopidy/')
- get_or_create_file('~/.mopidy/settings.py')
- settings.validate()
+def setup_mixer():
+ return get_class(settings.MIXER).start().proxy()
- def setup_gobject_loop(self, core_queue):
- gobject_loop = GObjectEventThread(core_queue)
- gobject_loop.start()
- return gobject_loop
+def setup_backend():
+ return get_class(settings.BACKENDS[0]).start().proxy()
- def setup_output(self, core_queue):
- output = get_class(settings.OUTPUT)(core_queue)
- output.start()
- return output
-
- def setup_backend(self, core_queue, output):
- return get_class(settings.BACKENDS[0])(core_queue, output)
-
- def setup_frontends(self, core_queue, backend):
- frontends = []
- for frontend_class_name in settings.FRONTENDS:
- try:
- frontend = get_class(frontend_class_name)(core_queue, backend)
- frontend.start()
- frontends.append(frontend)
- except OptionalDependencyError as e:
- logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
- return frontends
-
- def process_message(self, message):
- if message.get('to') == 'core':
- self.process_message_to_core(message)
- elif message.get('to') == 'output':
- self.output.process_message(message)
- elif message.get('to') == 'frontend':
- for frontend in self.frontends:
- frontend.process_message(message)
- elif message['command'] == 'end_of_track':
- self.backend.playback.on_end_of_track()
- elif message['command'] == 'stop_playback':
- self.backend.playback.stop()
- elif message['command'] == 'set_stored_playlists':
- self.backend.stored_playlists.playlists = message['playlists']
- else:
- logger.warning(u'Cannot handle message: %s', message)
-
- def process_message_to_core(self, message):
- assert message['to'] == 'core', u'Message recipient must be "core".'
- if message['command'] == 'exit':
- if message['reason'] is not None:
- logger.info(u'Exiting (%s)', message['reason'])
- sys.exit(message['status'])
- else:
- logger.warning(u'Cannot handle message: %s', message)
+def setup_frontends():
+ frontends = []
+ for frontend_class_name in settings.FRONTENDS:
+ try:
+ frontend = get_class(frontend_class_name).start().proxy()
+ frontends.append(frontend)
+ except OptionalDependencyError as e:
+ logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
+ return frontends
diff --git a/mopidy/frontends/base.py b/mopidy/frontends/base.py
index bf1c9bda..811644b1 100644
--- a/mopidy/frontends/base.py
+++ b/mopidy/frontends/base.py
@@ -1,40 +1,5 @@
class BaseFrontend(object):
"""
Base class for frontends.
-
- :param core_queue: queue for messaging the core
- :type core_queue: :class:`multiprocessing.Queue`
- :param backend: the backend
- :type backend: :class:`mopidy.backends.base.Backend`
"""
-
- def __init__(self, core_queue, backend):
- self.core_queue = core_queue
- self.backend = backend
-
- def start(self):
- """
- Start the frontend.
-
- *MAY be implemented by subclass.*
- """
- pass
-
- def destroy(self):
- """
- 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
- """
- raise NotImplementedError
+ pass
diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py
index 60c2d708..04716c61 100644
--- a/mopidy/frontends/lastfm.py
+++ b/mopidy/frontends/lastfm.py
@@ -1,6 +1,4 @@
import logging
-import multiprocessing
-import socket
import time
try:
@@ -9,16 +7,17 @@ except ImportError as import_error:
from mopidy import OptionalDependencyError
raise OptionalDependencyError(import_error)
-from mopidy import get_version, settings, SettingsError
+from pykka.actor import ThreadingActor
+
+from mopidy import settings, SettingsError
from mopidy.frontends.base import BaseFrontend
-from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.frontends.lastfm')
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
-class LastfmFrontend(BaseFrontend):
+class LastfmFrontend(ThreadingActor, BaseFrontend):
"""
Frontend which scrobbles the music you play to your `Last.fm
`_ profile.
@@ -29,7 +28,7 @@ class LastfmFrontend(BaseFrontend):
**Dependencies:**
- - `pylast `_ >= 0.5
+ - `pylast `_ >= 0.5.7
**Settings:**
@@ -37,38 +36,11 @@ class LastfmFrontend(BaseFrontend):
- :attr:`mopidy.settings.LASTFM_PASSWORD`
"""
- def __init__(self, *args, **kwargs):
- super(LastfmFrontend, self).__init__(*args, **kwargs)
- (self.connection, other_end) = multiprocessing.Pipe()
- self.thread = LastfmFrontendThread(self.core_queue, other_end)
-
- def start(self):
- self.thread.start()
-
- def destroy(self):
- self.thread.destroy()
-
- def process_message(self, message):
- if self.thread.is_alive():
- self.connection.send(message)
-
-
-class LastfmFrontendThread(BaseThread):
- def __init__(self, core_queue, connection):
- super(LastfmFrontendThread, self).__init__(core_queue)
- self.name = u'LastfmFrontendThread'
- self.connection = connection
+ def __init__(self):
self.lastfm = None
self.last_start_time = None
- def run_inside_try(self):
- self.setup()
- while self.lastfm is not None:
- self.connection.poll(None)
- message = self.connection.recv()
- self.process_message(message)
-
- def setup(self):
+ def on_start(self):
try:
username = settings.LASTFM_USERNAME
password_hash = pylast.md5(settings.LASTFM_PASSWORD)
@@ -79,36 +51,40 @@ class LastfmFrontendThread(BaseThread):
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)
+ self.stop()
+ except (pylast.NetworkError, pylast.MalformedResponseError,
+ pylast.WSError) as e:
+ logger.error(u'Error during Last.fm setup: %s', e)
+ self.stop()
- def process_message(self, message):
- if message['command'] == 'started_playing':
+ def on_receive(self, message):
+ if message.get('command') == 'started_playing':
self.started_playing(message['track'])
- elif message['command'] == 'stopped_playing':
+ elif message.get('command') == 'stopped_playing':
self.stopped_playing(message['track'], message['stop_position'])
else:
- pass # Ignore commands for other frontends
+ pass # Ignore any other messages
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.lastfm.update_now_playing(
artists,
- track.name,
- album=track.album.name,
+ (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, socket.error) as e:
- logger.warning(u'Last.fm now playing error: %s', e)
+ 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)')
@@ -123,11 +99,12 @@ class LastfmFrontendThread(BaseThread):
try:
self.lastfm.scrobble(
artists,
- track.name,
+ (track.name or ''),
str(self.last_start_time),
- album=track.album.name,
+ 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, socket.error) as e:
- logger.warning(u'Last.fm scrobbling error: %s', e)
+ except (pylast.ScrobblingError, pylast.NetworkError,
+ pylast.MalformedResponseError, pylast.WSError) as e:
+ logger.warning(u'Error submitting played track to Last.fm: %s', e)
diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py
index ce9abc6d..24c21c38 100644
--- a/mopidy/frontends/mpd/__init__.py
+++ b/mopidy/frontends/mpd/__init__.py
@@ -1,48 +1,43 @@
+import asyncore
import logging
+from pykka.actor import ThreadingActor
+
from mopidy.frontends.base import BaseFrontend
-from mopidy.frontends.mpd.dispatcher import MpdDispatcher
-from mopidy.frontends.mpd.thread import MpdThread
-from mopidy.utils.process import unpickle_connection
+from mopidy.frontends.mpd.server import MpdServer
+from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.frontends.mpd')
-class MpdFrontend(BaseFrontend):
+class MpdFrontend(ThreadingActor, BaseFrontend):
"""
The MPD frontend.
**Settings:**
- :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`
+ - :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
- :attr:`mopidy.settings.MPD_SERVER_PORT`
"""
- def __init__(self, *args, **kwargs):
- super(MpdFrontend, self).__init__(*args, **kwargs)
- self.thread = None
- self.dispatcher = MpdDispatcher(self.backend)
+ def __init__(self):
+ self._thread = None
- def start(self):
- """Starts the MPD server."""
- self.thread = MpdThread(self.core_queue)
- self.thread.start()
+ def on_start(self):
+ self._thread = MpdThread()
+ self._thread.start()
- def destroy(self):
- """Destroys the MPD server."""
- self.thread.destroy()
+ def on_receive(self, message):
+ pass # Ignore any messages
- def process_message(self, message):
- """
- Processes messages with the MPD frontend as destination.
- :param message: the message
- :type message: dict
- """
- assert message['to'] == 'frontend', \
- u'Message recipient must be "frontend".'
- if message['command'] == 'mpd_request':
- response = self.dispatcher.handle_request(message['request'])
- connection = unpickle_connection(message['reply_to'])
- connection.send(response)
- else:
- pass # Ignore messages for other frontends
+class MpdThread(BaseThread):
+ def __init__(self):
+ super(MpdThread, self).__init__()
+ self.name = u'MpdThread'
+
+ def run_inside_try(self):
+ logger.debug(u'Starting MPD server thread')
+ server = MpdServer()
+ server.start()
+ asyncore.loop()
diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py
index ab5f2e8c..f5c30b23 100644
--- a/mopidy/frontends/mpd/dispatcher.py
+++ b/mopidy/frontends/mpd/dispatcher.py
@@ -1,5 +1,8 @@
import re
+from pykka.registry import ActorRegistry
+
+from mopidy.backends.base import Backend
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
MpdUnknownCommand)
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
@@ -10,15 +13,27 @@ 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.mixers.base import BaseMixer
from mopidy.utils import flatten
class MpdDispatcher(object):
"""
- Dispatches MPD requests to the correct handler.
+ The MPD session feeds the MPD dispatcher with requests. The dispatcher
+ finds the correct handler, processes the request and sends the response
+ back to the MPD session.
"""
- def __init__(self, backend=None):
- self.backend = backend
+ # XXX Consider merging MpdDispatcher into MpdSession
+
+ def __init__(self):
+ backend_refs = ActorRegistry.get_by_class(Backend)
+ assert len(backend_refs) == 1, 'Expected exactly one running backend.'
+ self.backend = backend_refs[0].proxy()
+
+ mixer_refs = ActorRegistry.get_by_class(BaseMixer)
+ assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
+ self.mixer = mixer_refs[0].proxy()
+
self.command_list = False
self.command_list_ok = False
diff --git a/mopidy/frontends/mpd/exceptions.py b/mopidy/frontends/mpd/exceptions.py
index 2a18b2f3..faf4ce2f 100644
--- a/mopidy/frontends/mpd/exceptions.py
+++ b/mopidy/frontends/mpd/exceptions.py
@@ -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):
diff --git a/mopidy/frontends/mpd/protocol/audio_output.py b/mopidy/frontends/mpd/protocol/audio_output.py
index d25fc118..98c1d645 100644
--- a/mopidy/frontends/mpd/protocol/audio_output.py
+++ b/mopidy/frontends/mpd/protocol/audio_output.py
@@ -34,6 +34,6 @@ def outputs(frontend):
"""
return [
('outputid', 0),
- ('outputname', frontend.backend.__class__.__name__),
+ ('outputname', None),
('outputenabled', 1),
]
diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py
index 0ce3ef51..65811d09 100644
--- a/mopidy/frontends/mpd/protocol/connection.py
+++ b/mopidy/frontends/mpd/protocol/connection.py
@@ -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):
diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py
index 2f0a9f8f..8ef5e026 100644
--- a/mopidy/frontends/mpd/protocol/current_playlist.py
+++ b/mopidy/frontends/mpd/protocol/current_playlist.py
@@ -1,6 +1,7 @@
-from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
+from mopidy.frontends.mpd.protocol import handle_pattern
+from mopidy.frontends.mpd.translator import tracks_to_mpd_format
@handle_pattern(r'^add "(?P[^"]*)"$')
def add(frontend, uri):
@@ -18,9 +19,9 @@ def add(frontend, uri):
"""
if not uri:
return
- for handler_prefix in frontend.backend.uri_handlers:
+ for handler_prefix in frontend.backend.uri_handlers.get():
if uri.startswith(handler_prefix):
- track = frontend.backend.library.lookup(uri)
+ track = frontend.backend.library.lookup(uri).get()
if track is not None:
frontend.backend.current_playlist.add(track)
return
@@ -50,13 +51,14 @@ def addid(frontend, uri, songpos=None):
raise MpdNoExistError(u'No such song', command=u'addid')
if songpos is not None:
songpos = int(songpos)
- track = frontend.backend.library.lookup(uri)
+ track = frontend.backend.library.lookup(uri).get()
if track is None:
raise MpdNoExistError(u'No such song', command=u'addid')
- if songpos and songpos > len(frontend.backend.current_playlist.tracks):
+ if songpos and songpos > len(
+ frontend.backend.current_playlist.tracks.get()):
raise MpdArgError(u'Bad song index', command=u'addid')
cp_track = frontend.backend.current_playlist.add(track,
- at_position=songpos)
+ at_position=songpos).get()
return ('Id', cp_track[0])
@handle_pattern(r'^delete "(?P\d+):(?P\d+)*"$')
@@ -72,8 +74,8 @@ def delete_range(frontend, start, end=None):
if end is not None:
end = int(end)
else:
- end = len(frontend.backend.current_playlist.tracks)
- cp_tracks = frontend.backend.current_playlist.cp_tracks[start:end]
+ end = len(frontend.backend.current_playlist.tracks.get())
+ cp_tracks = frontend.backend.current_playlist.cp_tracks.get()[start:end]
if not cp_tracks:
raise MpdArgError(u'Bad song index', command=u'delete')
for (cpid, _) in cp_tracks:
@@ -84,7 +86,7 @@ def delete_songpos(frontend, songpos):
"""See :meth:`delete_range`"""
try:
songpos = int(songpos)
- (cpid, _) = frontend.backend.current_playlist.cp_tracks[songpos]
+ (cpid, _) = frontend.backend.current_playlist.cp_tracks.get()[songpos]
frontend.backend.current_playlist.remove(cpid=cpid)
except IndexError:
raise MpdArgError(u'Bad song index', command=u'delete')
@@ -100,9 +102,9 @@ def deleteid(frontend, cpid):
"""
try:
cpid = int(cpid)
- if frontend.backend.playback.current_cpid == cpid:
+ if frontend.backend.playback.current_cpid.get() == cpid:
frontend.backend.playback.next()
- return frontend.backend.current_playlist.remove(cpid=cpid)
+ return frontend.backend.current_playlist.remove(cpid=cpid).get()
except LookupError:
raise MpdNoExistError(u'No such song', command=u'deleteid')
@@ -128,7 +130,7 @@ def move_range(frontend, start, to, end=None):
``TO`` in the playlist.
"""
if end is None:
- end = len(frontend.backend.current_playlist.tracks)
+ end = len(frontend.backend.current_playlist.tracks.get())
start = int(start)
end = int(end)
to = int(to)
@@ -154,8 +156,9 @@ def moveid(frontend, cpid, to):
"""
cpid = int(cpid)
to = int(to)
- cp_track = frontend.backend.current_playlist.get(cpid=cpid)
- position = frontend.backend.current_playlist.cp_tracks.index(cp_track)
+ cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
+ position = frontend.backend.current_playlist.cp_tracks.get().index(
+ cp_track)
frontend.backend.current_playlist.move(position, position + 1, to)
@handle_pattern(r'^playlist$')
@@ -189,9 +192,9 @@ def playlistfind(frontend, tag, needle):
"""
if tag == 'filename':
try:
- cp_track = frontend.backend.current_playlist.get(uri=needle)
+ cp_track = frontend.backend.current_playlist.get(uri=needle).get()
(cpid, track) = cp_track
- position = frontend.backend.current_playlist.cp_tracks.index(
+ position = frontend.backend.current_playlist.cp_tracks.get().index(
cp_track)
return track.mpd_format(cpid=cpid, position=position)
except LookupError:
@@ -211,14 +214,17 @@ def playlistid(frontend, cpid=None):
if cpid is not None:
try:
cpid = int(cpid)
- cp_track = frontend.backend.current_playlist.get(cpid=cpid)
- position = frontend.backend.current_playlist.cp_tracks.index(
+ cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
+ position = frontend.backend.current_playlist.cp_tracks.get().index(
cp_track)
return cp_track[1].mpd_format(position=position, cpid=cpid)
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playlistid')
else:
- return frontend.backend.current_playlist.mpd_format()
+ cpids = [ct[0] for ct in
+ frontend.backend.current_playlist.cp_tracks.get()]
+ return tracks_to_mpd_format(
+ frontend.backend.current_playlist.tracks.get(), cpids=cpids)
@handle_pattern(r'^playlistinfo$')
@handle_pattern(r'^playlistinfo "(?P-?\d+)"$')
@@ -248,18 +254,27 @@ def playlistinfo(frontend, songpos=None,
end = songpos + 1
if start == -1:
end = None
- return frontend.backend.current_playlist.mpd_format(start, end)
+ cpids = [ct[0] for ct in
+ frontend.backend.current_playlist.cp_tracks.get()]
+ return tracks_to_mpd_format(
+ frontend.backend.current_playlist.tracks.get(),
+ start, end, cpids=cpids)
else:
if start is None:
start = 0
start = int(start)
- if not (0 <= start <= len(frontend.backend.current_playlist.tracks)):
+ if not (0 <= start <= len(
+ frontend.backend.current_playlist.tracks.get())):
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
if end is not None:
end = int(end)
- if end > len(frontend.backend.current_playlist.tracks):
+ if end > len(frontend.backend.current_playlist.tracks.get()):
end = None
- return frontend.backend.current_playlist.mpd_format(start, end)
+ cpids = [ct[0] for ct in
+ frontend.backend.current_playlist.cp_tracks.get()]
+ return tracks_to_mpd_format(
+ frontend.backend.current_playlist.tracks.get(),
+ start, end, cpids=cpids)
@handle_pattern(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$')
@handle_pattern(r'^playlistsearch (?P\S+) "(?P[^"]+)"$')
@@ -298,7 +313,10 @@ def plchanges(frontend, version):
"""
# XXX Naive implementation that returns all tracks as changed
if int(version) < frontend.backend.current_playlist.version:
- return frontend.backend.current_playlist.mpd_format()
+ cpids = [ct[0] for ct in
+ frontend.backend.current_playlist.cp_tracks.get()]
+ return tracks_to_mpd_format(
+ frontend.backend.current_playlist.tracks.get(), cpids=cpids)
@handle_pattern(r'^plchangesposid "(?P\d+)"$')
def plchangesposid(frontend, version):
@@ -315,10 +333,10 @@ def plchangesposid(frontend, version):
``playlistlength`` returned by status command.
"""
# XXX Naive implementation that returns all tracks as changed
- if int(version) != frontend.backend.current_playlist.version:
+ if int(version) != frontend.backend.current_playlist.version.get():
result = []
for (position, (cpid, _)) in enumerate(
- frontend.backend.current_playlist.cp_tracks):
+ frontend.backend.current_playlist.cp_tracks.get()):
result.append((u'cpos', position))
result.append((u'Id', cpid))
return result
@@ -351,7 +369,7 @@ def swap(frontend, songpos1, songpos2):
"""
songpos1 = int(songpos1)
songpos2 = int(songpos2)
- tracks = frontend.backend.current_playlist.tracks
+ tracks = frontend.backend.current_playlist.tracks.get()
song1 = tracks[songpos1]
song2 = tracks[songpos2]
del tracks[songpos1]
@@ -372,8 +390,9 @@ def swapid(frontend, cpid1, cpid2):
"""
cpid1 = int(cpid1)
cpid2 = int(cpid2)
- cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1)
- cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2)
- position1 = frontend.backend.current_playlist.cp_tracks.index(cp_track1)
- position2 = frontend.backend.current_playlist.cp_tracks.index(cp_track2)
+ cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1).get()
+ cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2).get()
+ cp_tracks = frontend.backend.current_playlist.cp_tracks.get()
+ position1 = cp_tracks.index(cp_track1)
+ position2 = cp_tracks.index(cp_track2)
swap(frontend, position1, position2)
diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py
index fb3a3a09..a6836533 100644
--- a/mopidy/frontends/mpd/protocol/music_db.py
+++ b/mopidy/frontends/mpd/protocol/music_db.py
@@ -41,8 +41,8 @@ def count(frontend, tag, needle):
return [('songs', 0), ('playtime', 0)] # TODO
@handle_pattern(r'^find '
- r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
- ' "[^"]+"\s?)+)$')
+ r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
+ r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
def find(frontend, mpd_query):
"""
*musicpd.org, music database section:*
@@ -62,9 +62,13 @@ def find(frontend, mpd_query):
- does not add quotes around the field argument.
- capitalizes the type argument.
+
+ *ncmpcpp:*
+
+ - also uses the search type "date".
"""
query = _build_query(mpd_query)
- return frontend.backend.library.find_exact(**query).mpd_format()
+ return frontend.backend.library.find_exact(**query).get().mpd_format()
@handle_pattern(r'^findadd '
r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
@@ -211,7 +215,7 @@ def _list_build_query(field, mpd_query):
def _list_artist(frontend, query):
artists = set()
- playlist = frontend.backend.library.find_exact(**query)
+ playlist = frontend.backend.library.find_exact(**query).get()
for track in playlist.tracks:
for artist in track.artists:
artists.add((u'Artist', artist.name))
@@ -219,7 +223,7 @@ def _list_artist(frontend, query):
def _list_album(frontend, query):
albums = set()
- playlist = frontend.backend.library.find_exact(**query)
+ playlist = frontend.backend.library.find_exact(**query).get()
for track in playlist.tracks:
if track.album is not None:
albums.add((u'Album', track.album.name))
@@ -227,7 +231,7 @@ def _list_album(frontend, query):
def _list_date(frontend, query):
dates = set()
- playlist = frontend.backend.library.find_exact(**query)
+ playlist = frontend.backend.library.find_exact(**query).get()
for track in playlist.tracks:
if track.date is not None:
dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
@@ -290,8 +294,8 @@ def rescan(frontend, uri=None):
return update(frontend, uri, rescan_unmodified_files=True)
@handle_pattern(r'^search '
- r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?'
- ' "[^"]+"\s?)+)$')
+ r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
+ r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
def search(frontend, mpd_query):
"""
*musicpd.org, music database section:*
@@ -314,9 +318,13 @@ def search(frontend, mpd_query):
- does not add quotes around the field argument.
- capitalizes the field argument.
+
+ *ncmpcpp:*
+
+ - also uses the search type "date".
"""
query = _build_query(mpd_query)
- return frontend.backend.library.search(**query).mpd_format()
+ return frontend.backend.library.search(**query).get().mpd_format()
@handle_pattern(r'^update( "(?P[^"]+)")*$')
def update(frontend, uri=None, rescan_unmodified_files=False):
diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py
index 19922bc3..65282f42 100644
--- a/mopidy/frontends/mpd/protocol/playback.py
+++ b/mopidy/frontends/mpd/protocol/playback.py
@@ -1,3 +1,4 @@
+from mopidy.backends.base import PlaybackController
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
@@ -86,7 +87,7 @@ def next_(frontend):
order as the first time.
"""
- return frontend.backend.playback.next()
+ return frontend.backend.playback.next().get()
@handle_pattern(r'^pause$')
@handle_pattern(r'^pause "(?P[01])"$')
@@ -103,11 +104,11 @@ def pause(frontend, state=None):
- Calls ``pause`` without any arguments to toogle pause.
"""
if state is None:
- if (frontend.backend.playback.state ==
- frontend.backend.playback.PLAYING):
+ if (frontend.backend.playback.state.get() ==
+ PlaybackController.PLAYING):
frontend.backend.playback.pause()
- elif (frontend.backend.playback.state ==
- frontend.backend.playback.PAUSED):
+ elif (frontend.backend.playback.state.get() ==
+ PlaybackController.PAUSED):
frontend.backend.playback.resume()
elif int(state):
frontend.backend.playback.pause()
@@ -120,7 +121,7 @@ def play(frontend):
The original MPD server resumes from the paused state on ``play``
without arguments.
"""
- return frontend.backend.playback.play()
+ return frontend.backend.playback.play().get()
@handle_pattern(r'^playid "(?P\d+)"$')
@handle_pattern(r'^playid "(?P-1)"$')
@@ -132,22 +133,21 @@ def playid(frontend, cpid):
Begins playing the playlist at song ``SONGID``.
- *GMPC:*
+ *Clarifications:*
- - issues ``playid "-1"`` after playlist replacement to start playback
- at the first track.
+ - ``playid "-1"`` when playing is ignored.
+ - ``playid "-1"`` when paused resumes playback.
+ - ``playid "-1"`` when stopped with a current track starts playback at the
+ current track.
+ - ``playid "-1"`` when stopped without a current track, e.g. after playlist
+ replacement, starts playback at the first track.
"""
cpid = int(cpid)
- paused = (frontend.backend.playback.state ==
- frontend.backend.playback.PAUSED)
- if cpid == -1 and paused:
- return frontend.backend.playback.resume()
+ if cpid == -1:
+ return _play_minus_one(frontend)
try:
- if cpid == -1:
- cp_track = _get_cp_track_for_play_minus_one(frontend)
- else:
- cp_track = frontend.backend.current_playlist.get(cpid=cpid)
- return frontend.backend.playback.play(cp_track)
+ cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
+ return frontend.backend.playback.play(cp_track).get()
except LookupError:
raise MpdNoExistError(u'No such song', command=u'playid')
@@ -161,33 +161,41 @@ def playpos(frontend, songpos):
Begins playing the playlist at song number ``SONGPOS``.
- *Many clients:*
+ *Clarifications:*
- - issue ``play "-1"`` after playlist replacement to start the current
- track. If the current track is not set, start playback at the first
- track.
+ - ``playid "-1"`` when playing is ignored.
+ - ``playid "-1"`` when paused resumes playback.
+ - ``playid "-1"`` when stopped with a current track starts playback at the
+ current track.
+ - ``playid "-1"`` when stopped without a current track, e.g. after playlist
+ replacement, starts playback at the first track.
*BitMPC:*
- issues ``play 6`` without quotes around the argument.
"""
songpos = int(songpos)
+ if songpos == -1:
+ return _play_minus_one(frontend)
try:
- if songpos == -1:
- cp_track = _get_cp_track_for_play_minus_one(frontend)
- else:
- cp_track = frontend.backend.current_playlist.cp_tracks[songpos]
- return frontend.backend.playback.play(cp_track)
+ cp_track = frontend.backend.current_playlist.cp_tracks.get()[songpos]
+ return frontend.backend.playback.play(cp_track).get()
except IndexError:
raise MpdArgError(u'Bad song index', command=u'play')
-def _get_cp_track_for_play_minus_one(frontend):
- if not frontend.backend.current_playlist.cp_tracks:
+def _play_minus_one(frontend):
+ if (frontend.backend.playback.state.get() == PlaybackController.PLAYING):
+ return # Nothing to do
+ elif (frontend.backend.playback.state.get() == PlaybackController.PAUSED):
+ return frontend.backend.playback.resume().get()
+ elif frontend.backend.playback.current_cp_track.get() is not None:
+ cp_track = frontend.backend.playback.current_cp_track.get()
+ return frontend.backend.playback.play(cp_track).get()
+ elif frontend.backend.current_playlist.cp_tracks.get():
+ cp_track = frontend.backend.current_playlist.cp_tracks.get()[0]
+ return frontend.backend.playback.play(cp_track).get()
+ else:
return # Fail silently
- cp_track = frontend.backend.playback.current_cp_track
- if cp_track is None:
- cp_track = frontend.backend.current_playlist.cp_tracks[0]
- return cp_track
@handle_pattern(r'^previous$')
def previous(frontend):
@@ -233,7 +241,7 @@ def previous(frontend):
``previous`` should do a seek to time position 0.
"""
- return frontend.backend.playback.previous()
+ return frontend.backend.playback.previous().get()
@handle_pattern(r'^random (?P[01])$')
@handle_pattern(r'^random "(?P[01])"$')
@@ -344,7 +352,7 @@ def setvol(frontend, volume):
volume = 0
if volume > 100:
volume = 100
- frontend.backend.mixer.volume = volume
+ frontend.mixer.volume = volume
@handle_pattern(r'^single (?P[01])$')
@handle_pattern(r'^single "(?P[01])"$')
diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py
index d2c9c599..ab782440 100644
--- a/mopidy/frontends/mpd/protocol/reflection.py
+++ b/mopidy/frontends/mpd/protocol/reflection.py
@@ -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$')
@@ -76,4 +81,4 @@ def urlhandlers(frontend):
Gets a list of available URL handlers.
"""
- return [(u'handler', uri) for uri in frontend.backend.uri_handlers]
+ return [(u'handler', uri) for uri in frontend.backend.uri_handlers.get()]
diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py
index e18f1ea4..a78efc0a 100644
--- a/mopidy/frontends/mpd/protocol/status.py
+++ b/mopidy/frontends/mpd/protocol/status.py
@@ -1,3 +1,4 @@
+from mopidy.backends.base import PlaybackController
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@@ -23,10 +24,11 @@ def currentsong(frontend):
Displays the song info of the current song (same song that is
identified in status).
"""
- if frontend.backend.playback.current_track is not None:
- return frontend.backend.playback.current_track.mpd_format(
- position=frontend.backend.playback.current_playlist_position,
- cpid=frontend.backend.playback.current_cpid)
+ current_cp_track = frontend.backend.playback.current_cp_track.get()
+ if current_cp_track is not None:
+ return current_cp_track[1].mpd_format(
+ position=frontend.backend.playback.current_playlist_position.get(),
+ cpid=current_cp_track[0])
@handle_pattern(r'^idle$')
@handle_pattern(r'^idle (?P.+)$')
@@ -90,8 +92,7 @@ def stats(frontend):
'artists': 0, # TODO
'albums': 0, # TODO
'songs': 0, # TODO
- # TODO Does not work after multiprocessing branch merge
- 'uptime': 0, # frontend.session.stats_uptime(),
+ 'uptime': 0, # TODO
'db_playtime': 0, # TODO
'db_update': 0, # TODO
'playtime': 0, # TODO
@@ -140,56 +141,59 @@ def status(frontend):
('xfade', _status_xfade(frontend)),
('state', _status_state(frontend)),
]
- if frontend.backend.playback.current_track is not None:
+ if frontend.backend.playback.current_track.get() is not None:
result.append(('song', _status_songpos(frontend)))
result.append(('songid', _status_songid(frontend)))
- if frontend.backend.playback.state in (frontend.backend.playback.PLAYING,
- frontend.backend.playback.PAUSED):
+ if frontend.backend.playback.state.get() in (PlaybackController.PLAYING,
+ PlaybackController.PAUSED):
result.append(('time', _status_time(frontend)))
result.append(('elapsed', _status_time_elapsed(frontend)))
result.append(('bitrate', _status_bitrate(frontend)))
return result
def _status_bitrate(frontend):
- if frontend.backend.playback.current_track is not None:
- return frontend.backend.playback.current_track.bitrate
+ current_track = frontend.backend.playback.current_track.get()
+ if current_track is not None:
+ return current_track.bitrate
def _status_consume(frontend):
- if frontend.backend.playback.consume:
+ if frontend.backend.playback.consume.get():
return 1
else:
return 0
def _status_playlist_length(frontend):
- return len(frontend.backend.current_playlist.tracks)
+ return len(frontend.backend.current_playlist.tracks.get())
def _status_playlist_version(frontend):
- return frontend.backend.current_playlist.version
+ return frontend.backend.current_playlist.version.get()
def _status_random(frontend):
- return int(frontend.backend.playback.random)
+ return int(frontend.backend.playback.random.get())
def _status_repeat(frontend):
- return int(frontend.backend.playback.repeat)
+ return int(frontend.backend.playback.repeat.get())
def _status_single(frontend):
- return int(frontend.backend.playback.single)
+ return int(frontend.backend.playback.single.get())
def _status_songid(frontend):
- if frontend.backend.playback.current_cpid is not None:
- return frontend.backend.playback.current_cpid
+ current_cpid = frontend.backend.playback.current_cpid.get()
+ if current_cpid is not None:
+ return current_cpid
else:
return _status_songpos(frontend)
def _status_songpos(frontend):
- return frontend.backend.playback.current_playlist_position
+ return frontend.backend.playback.current_playlist_position.get()
def _status_state(frontend):
- if frontend.backend.playback.state == frontend.backend.playback.PLAYING:
+ state = frontend.backend.playback.state.get()
+ if state == PlaybackController.PLAYING:
return u'play'
- elif frontend.backend.playback.state == frontend.backend.playback.STOPPED:
+ elif state == PlaybackController.STOPPED:
return u'stop'
- elif frontend.backend.playback.state == frontend.backend.playback.PAUSED:
+ elif state == PlaybackController.PAUSED:
return u'pause'
def _status_time(frontend):
@@ -197,19 +201,21 @@ def _status_time(frontend):
_status_time_total(frontend) // 1000)
def _status_time_elapsed(frontend):
- return frontend.backend.playback.time_position
+ return frontend.backend.playback.time_position.get()
def _status_time_total(frontend):
- if frontend.backend.playback.current_track is None:
+ current_track = frontend.backend.playback.current_track.get()
+ if current_track is None:
return 0
- elif frontend.backend.playback.current_track.length is None:
+ elif current_track.length is None:
return 0
else:
- return frontend.backend.playback.current_track.length
+ return current_track.length
def _status_volume(frontend):
- if frontend.backend.mixer.volume is not None:
- return frontend.backend.mixer.volume
+ volume = frontend.mixer.volume.get()
+ if volume is not None:
+ return volume
else:
return 0
diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py
index c34b1676..6eccffac 100644
--- a/mopidy/frontends/mpd/protocol/stored_playlists.py
+++ b/mopidy/frontends/mpd/protocol/stored_playlists.py
@@ -19,8 +19,8 @@ def listplaylist(frontend, name):
file: relative/path/to/file3.mp3
"""
try:
- return ['file: %s' % t.uri
- for t in frontend.backend.stored_playlists.get(name=name).tracks]
+ playlist = frontend.backend.stored_playlists.get(name=name).get()
+ return ['file: %s' % t.uri for t in playlist.tracks]
except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
@@ -39,7 +39,8 @@ def listplaylistinfo(frontend, name):
Album, Artist, Track
"""
try:
- return frontend.backend.stored_playlists.get(name=name).mpd_format()
+ playlist = frontend.backend.stored_playlists.get(name=name).get()
+ return playlist.mpd_format()
except LookupError:
raise MpdNoExistError(
u'No such playlist', command=u'listplaylistinfo')
@@ -66,7 +67,7 @@ def listplaylists(frontend):
Last-Modified: 2010-02-06T02:11:08Z
"""
result = []
- for playlist in frontend.backend.stored_playlists.playlists:
+ for playlist in frontend.backend.stored_playlists.playlists.get():
result.append((u'playlist', playlist.name))
last_modified = (playlist.last_modified or
dt.datetime.now()).isoformat()
@@ -92,7 +93,7 @@ def load(frontend, name):
- ``load`` appends the given playlist to the current playlist.
"""
try:
- playlist = frontend.backend.stored_playlists.get(name=name)
+ playlist = frontend.backend.stored_playlists.get(name=name).get()
frontend.backend.current_playlist.append(playlist.tracks)
except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'load')
diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py
index 7caf21f9..231bdf40 100644
--- a/mopidy/frontends/mpd/server.py
+++ b/mopidy/frontends/mpd/server.py
@@ -15,9 +15,8 @@ class MpdServer(asyncore.dispatcher):
for each client connection.
"""
- def __init__(self, core_queue):
+ def __init__(self):
asyncore.dispatcher.__init__(self)
- self.core_queue = core_queue
def start(self):
"""Start MPD server."""
@@ -47,8 +46,7 @@ class MpdServer(asyncore.dispatcher):
(client_socket, client_socket_address) = self.accept()
logger.info(u'MPD client connection from [%s]:%s',
client_socket_address[0], client_socket_address[1])
- MpdSession(self, client_socket, client_socket_address,
- self.core_queue).start()
+ MpdSession(self, client_socket, client_socket_address).start()
def handle_close(self):
"""Handle end of client connection."""
diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py
index 580b5905..5a473eca 100644
--- a/mopidy/frontends/mpd/session.py
+++ b/mopidy/frontends/mpd/session.py
@@ -1,28 +1,28 @@
import asynchat
import logging
-import multiprocessing
+from mopidy import settings
+from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
from mopidy.utils.log import indent
-from mopidy.utils.process import pickle_connection
logger = logging.getLogger('mopidy.frontends.mpd.session')
class MpdSession(asynchat.async_chat):
"""
- The MPD client session. Keeps track of a single client and passes its
- MPD requests to the dispatcher.
+ The MPD client session. Keeps track of a single client session. Any
+ requests from the client is passed on to the MPD request dispatcher.
"""
- def __init__(self, server, client_socket, client_socket_address,
- core_queue):
+ def __init__(self, server, client_socket, client_socket_address):
asynchat.async_chat.__init__(self, sock=client_socket)
self.server = server
self.client_address = client_socket_address[0]
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))
+ self.dispatcher = MpdDispatcher()
def start(self):
"""Start a new client session."""
@@ -46,15 +46,12 @@ class MpdSession(asynchat.async_chat):
def handle_request(self, request):
"""Handle request by sending it to the MPD frontend."""
- my_end, other_end = multiprocessing.Pipe()
- self.core_queue.put({
- 'to': 'frontend',
- 'command': 'mpd_request',
- 'request': request,
- 'reply_to': pickle_connection(other_end),
- })
- my_end.poll(None)
- response = my_end.recv()
+ if not self.authenticated:
+ (self.authenticated, response) = self.check_password(request)
+ if response is not None:
+ self.send_response(response)
+ return
+ response = self.dispatcher.handle_request(request)
if response is not None:
self.handle_response(response)
@@ -69,3 +66,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})
diff --git a/mopidy/frontends/mpd/thread.py b/mopidy/frontends/mpd/thread.py
deleted file mode 100644
index 0ad5ee68..00000000
--- a/mopidy/frontends/mpd/thread.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import asyncore
-import logging
-
-from mopidy.frontends.mpd.server import MpdServer
-from mopidy.utils.process import BaseThread
-
-logger = logging.getLogger('mopidy.frontends.mpd.thread')
-
-class MpdThread(BaseThread):
- def __init__(self, core_queue):
- super(MpdThread, self).__init__(core_queue)
- self.name = u'MpdThread'
-
- def run_inside_try(self):
- logger.debug(u'Starting MPD server thread')
- server = MpdServer(self.core_queue)
- server.start()
- asyncore.loop()
diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py
index 3ead23c7..562b2d2d 100644
--- a/mopidy/frontends/mpd/translator.py
+++ b/mopidy/frontends/mpd/translator.py
@@ -84,8 +84,9 @@ def artists_to_mpd_format(artists):
:type track: array of :class:`mopidy.models.Artist`
:rtype: string
"""
+ artists = list(artists)
artists.sort(key=lambda a: a.name)
- return u', '.join([a.name for a in artists])
+ return u', '.join([a.name for a in artists if a.name])
def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None):
"""
diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py
index 4aa5952f..6329bbbb 100644
--- a/mopidy/mixers/alsa.py
+++ b/mopidy/mixers/alsa.py
@@ -1,12 +1,14 @@
import alsaaudio
import logging
+from pykka.actor import ThreadingActor
+
from mopidy import settings
from mopidy.mixers.base import BaseMixer
logger = logging.getLogger('mopidy.mixers.alsa')
-class AlsaMixer(BaseMixer):
+class AlsaMixer(ThreadingActor, BaseMixer):
"""
Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control
volume.
@@ -20,8 +22,10 @@ class AlsaMixer(BaseMixer):
- :attr:`mopidy.settings.MIXER_ALSA_CONTROL`
"""
- def __init__(self, *args, **kwargs):
- super(AlsaMixer, self).__init__(*args, **kwargs)
+ def __init__(self):
+ self._mixer = None
+
+ def on_start(self):
self._mixer = alsaaudio.Mixer(self._get_mixer_control())
assert self._mixer is not None
diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py
index f7f9525c..74996cb6 100644
--- a/mopidy/mixers/base.py
+++ b/mopidy/mixers/base.py
@@ -2,17 +2,12 @@ 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
+ amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
@property
def volume(self):
@@ -35,9 +30,6 @@ class BaseMixer(object):
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.
diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py
index f0712f95..3922d20a 100644
--- a/mopidy/mixers/denon.py
+++ b/mopidy/mixers/denon.py
@@ -1,12 +1,13 @@
import logging
-from threading import Lock
+
+from pykka.actor import ThreadingActor
from mopidy import settings
from mopidy.mixers.base import BaseMixer
logger = logging.getLogger(u'mopidy.mixers.denon')
-class DenonMixer(BaseMixer):
+class DenonMixer(ThreadingActor, BaseMixer):
"""
Mixer for controlling Denon amplifiers and receivers using the RS-232
protocol.
@@ -25,27 +26,19 @@ class DenonMixer(BaseMixer):
"""
def __init__(self, *args, **kwargs):
- """
- Connects using the serial specifications from Denon's RS-232 Protocol
- specification: 9600bps 8N1.
- """
- super(DenonMixer, self).__init__(*args, **kwargs)
- device = kwargs.get('device', None)
- if device:
- self._device = device
- else:
- from serial import Serial
- self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
+ self._device = kwargs.get('device', None)
self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)]
self._volume = 0
- self._lock = Lock()
+
+ def on_start(self):
+ if self._device is None:
+ from serial import Serial
+ self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
def _get_volume(self):
- self._lock.acquire()
- self.ensure_open_device()
+ self._ensure_open_device()
self._device.write('MV?\r')
vol = str(self._device.readline()[2:4])
- self._lock.release()
logger.debug(u'_get_volume() = %s' % vol)
return self._levels.index(vol)
@@ -53,14 +46,12 @@ class DenonMixer(BaseMixer):
# Clamp according to Denon-spec
if volume > 99:
volume = 99
- self._lock.acquire()
- self.ensure_open_device()
+ self._ensure_open_device()
self._device.write('MV%s\r'% self._levels[volume])
vol = self._device.readline()[2:4]
- self._lock.release()
self._volume = self._levels.index(vol)
- def ensure_open_device(self):
+ def _ensure_open_device(self):
if not self._device.isOpen():
logger.debug(u'(re)connecting to Denon device')
self._device.open()
diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py
index 12a8137e..186bc7aa 100644
--- a/mopidy/mixers/dummy.py
+++ b/mopidy/mixers/dummy.py
@@ -1,10 +1,11 @@
+from pykka.actor import ThreadingActor
+
from mopidy.mixers.base import BaseMixer
-class DummyMixer(BaseMixer):
+class DummyMixer(ThreadingActor, BaseMixer):
"""Mixer which just stores and reports the chosen volume."""
- def __init__(self, *args, **kwargs):
- super(DummyMixer, self).__init__(*args, **kwargs)
+ def __init__(self):
self._volume = None
def _get_volume(self):
diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py
index 9dca3690..d6365b4b 100644
--- a/mopidy/mixers/gstreamer_software.py
+++ b/mopidy/mixers/gstreamer_software.py
@@ -1,13 +1,22 @@
-from mopidy.mixers.base import BaseMixer
+from pykka.actor import ThreadingActor
+from pykka.registry import ActorRegistry
-class GStreamerSoftwareMixer(BaseMixer):
+from mopidy.mixers.base import BaseMixer
+from mopidy.outputs.base import BaseOutput
+
+class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
"""Mixer which uses GStreamer to control volume in software."""
- def __init__(self, *args, **kwargs):
- super(GStreamerSoftwareMixer, self).__init__(*args, **kwargs)
+ def __init__(self):
+ self.output = None
+
+ def on_start(self):
+ output_refs = ActorRegistry.get_by_class(BaseOutput)
+ assert len(output_refs) == 1, 'Expected exactly one running output.'
+ self.output = output_refs[0].proxy()
def _get_volume(self):
- return self.backend.output.get_volume()
+ return self.output.get_volume().get()
def _set_volume(self, volume):
- self.backend.output.set_volume(volume)
+ self.output.set_volume(volume).get()
diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py
index 3215a761..bd53376e 100644
--- a/mopidy/mixers/nad.py
+++ b/mopidy/mixers/nad.py
@@ -1,14 +1,14 @@
import logging
-from serial import Serial
-from multiprocessing import Pipe
+import serial
+
+from pykka.actor import ThreadingActor
from mopidy import settings
from mopidy.mixers.base import BaseMixer
-from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.mixers.nad')
-class NadMixer(BaseMixer):
+class NadMixer(ThreadingActor, BaseMixer):
"""
Mixer for controlling NAD amplifiers and receivers using the NAD RS-232
protocol.
@@ -36,21 +36,19 @@ class NadMixer(BaseMixer):
"""
- def __init__(self, *args, **kwargs):
- super(NadMixer, self).__init__(*args, **kwargs)
- self._volume = None
- self._pipe, other_end = Pipe()
- NadTalker(self.backend.core_queue, pipe=other_end).start()
+ def __init__(self):
+ self._volume_cache = None
+ self._nad_talker = NadTalker.start().proxy()
def _get_volume(self):
- return self._volume
+ return self._volume_cache
def _set_volume(self, volume):
- self._volume = volume
- self._pipe.send({'command': 'set_volume', 'volume': volume})
+ self._volume_cache = volume
+ self._nad_talker.set_volume(volume)
-class NadTalker(BaseThread):
+class NadTalker(ThreadingActor):
"""
Independent process which does the communication with the NAD device.
@@ -72,29 +70,20 @@ class NadTalker(BaseThread):
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
_nad_volume = None
- def __init__(self, core_queue, pipe=None):
- super(NadTalker, self).__init__(core_queue)
- self.name = u'NadTalker'
- self.pipe = pipe
+ def __init__(self):
self._device = None
- def run_inside_try(self):
+ def on_start(self):
self._open_connection()
self._set_device_to_known_state()
- while self.pipe.poll(None):
- message = self.pipe.recv()
- if message['command'] == 'set_volume':
- self._set_volume(message['volume'])
- elif message['command'] == 'reset_device':
- self._set_device_to_known_state()
def _open_connection(self):
# Opens serial connection to the device.
# Communication settings: 115200 bps 8N1
logger.info(u'Connecting to serial device "%s"',
settings.MIXER_EXT_PORT)
- self._device = Serial(port=settings.MIXER_EXT_PORT, baudrate=115200,
- timeout=self.TIMEOUT)
+ self._device = serial.Serial(port=settings.MIXER_EXT_PORT,
+ baudrate=115200, timeout=self.TIMEOUT)
self._get_device_model()
def _set_device_to_known_state(self):
@@ -147,6 +136,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()
@@ -162,7 +153,7 @@ class NadTalker(BaseThread):
self._nad_volume = 0
logger.info(u'Done calibrating NAD amplifier')
- def _set_volume(self, volume):
+ def set_volume(self, volume):
# Increase or decrease the amplifier volume until it matches the given
# target volume.
logger.debug(u'Setting volume to %d' % volume)
diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py
index 2ea04cf2..53983095 100644
--- a/mopidy/mixers/osa.py
+++ b/mopidy/mixers/osa.py
@@ -1,9 +1,11 @@
from subprocess import Popen, PIPE
import time
+from pykka.actor import ThreadingActor
+
from mopidy.mixers.base import BaseMixer
-class OsaMixer(BaseMixer):
+class OsaMixer(ThreadingActor, BaseMixer):
"""
Mixer which uses ``osascript`` on OS X to control volume.
@@ -14,7 +16,6 @@ class OsaMixer(BaseMixer):
**Settings:**
- None
-
"""
CACHE_TTL = 30
diff --git a/mopidy/models.py b/mopidy/models.py
index 8e7585f1..ef60ebbe 100644
--- a/mopidy/models.py
+++ b/mopidy/models.py
@@ -1,5 +1,3 @@
-from copy import copy
-
from mopidy.frontends.mpd import translator
class ImmutableObject(object):
@@ -23,6 +21,17 @@ class ImmutableObject(object):
return super(ImmutableObject, self).__setattr__(name, value)
raise AttributeError('Object is immutable.')
+ def __repr__(self):
+ kwarg_pairs = []
+ for (key, value) in sorted(self.__dict__.items()):
+ if isinstance(value, (frozenset, tuple)):
+ value = list(value)
+ kwarg_pairs.append('%s=%s' % (key, repr(value)))
+ return '%(classname)s(%(kwargs)s)' % {
+ 'classname': self.__class__.__name__,
+ 'kwargs': ', '.join(kwarg_pairs),
+ }
+
def __hash__(self):
hash_sum = 0
for key, value in self.__dict__.items():
@@ -65,6 +74,7 @@ class ImmutableObject(object):
% key)
return self.__class__(**data)
+
class Artist(ImmutableObject):
"""
:param uri: artist URI
@@ -105,6 +115,9 @@ class Album(ImmutableObject):
#: The album name. Read-only.
name = None
+ #: A set of album artists. Read-only.
+ artists = frozenset()
+
#: The number of tracks in the album. Read-only.
num_tracks = 0
@@ -112,14 +125,9 @@ class Album(ImmutableObject):
musicbrainz_id = None
def __init__(self, *args, **kwargs):
- self._artists = frozenset(kwargs.pop('artists', []))
+ self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
super(Album, self).__init__(*args, **kwargs)
- @property
- def artists(self):
- """List of :class:`Artist` elements. Read-only."""
- return list(self._artists)
-
class Track(ImmutableObject):
"""
@@ -149,6 +157,9 @@ class Track(ImmutableObject):
#: The track name. Read-only.
name = None
+ #: A set of track artists. Read-only.
+ artists = frozenset()
+
#: The track :class:`Album`. Read-only.
album = None
@@ -168,14 +179,9 @@ class Track(ImmutableObject):
musicbrainz_id = None
def __init__(self, *args, **kwargs):
- self._artists = frozenset(kwargs.pop('artists', []))
+ self.__dict__['artists'] = frozenset(kwargs.pop('artists', []))
super(Track, self).__init__(*args, **kwargs)
- @property
- def artists(self):
- """List of :class:`Artist`. Read-only."""
- return list(self._artists)
-
def mpd_format(self, *args, **kwargs):
return translator.track_to_mpd_format(self, *args, **kwargs)
@@ -198,24 +204,22 @@ class Playlist(ImmutableObject):
#: The playlist name. Read-only.
name = None
+ #: The playlist's tracks. Read-only.
+ tracks = tuple()
+
#: The playlist modification time. Read-only.
#:
#: :class:`datetime.datetime`, or :class:`None` if unknown.
last_modified = None
def __init__(self, *args, **kwargs):
- self._tracks = kwargs.pop('tracks', [])
+ self.__dict__['tracks'] = tuple(kwargs.pop('tracks', []))
super(Playlist, self).__init__(*args, **kwargs)
- @property
- def tracks(self):
- """List of :class:`Track` elements. Read-only."""
- return copy(self._tracks)
-
@property
def length(self):
"""The number of tracks in the playlist. Read-only."""
- return len(self._tracks)
+ return len(self.tracks)
def mpd_format(self, *args, **kwargs):
return translator.playlist_to_mpd_format(self, *args, **kwargs)
diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py
index ae1af8cf..11c2f86e 100644
--- a/mopidy/outputs/base.py
+++ b/mopidy/outputs/base.py
@@ -3,33 +3,6 @@ class BaseOutput(object):
Base class for audio outputs.
"""
- def __init__(self, core_queue):
- self.core_queue = core_queue
-
- def start(self):
- """
- Start the output.
-
- *MAY be implemented by subclasses.*
- """
- pass
-
- def destroy(self):
- """
- Destroy the output.
-
- *MAY be implemented by subclasses.*
- """
- pass
-
- def process_message(self, message):
- """
- Process messages with the output as destination.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
def play_uri(self, uri):
"""
Play URI.
diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py
index e78d269c..02f1bfca 100644
--- a/mopidy/outputs/dummy.py
+++ b/mopidy/outputs/dummy.py
@@ -1,6 +1,8 @@
+from pykka.actor import ThreadingActor
+
from mopidy.outputs.base import BaseOutput
-class DummyOutput(BaseOutput):
+class DummyOutput(ThreadingActor, BaseOutput):
"""
Audio output used for testing.
"""
@@ -8,15 +10,6 @@ class DummyOutput(BaseOutput):
# pylint: disable = R0902
# Too many instance attributes (9/7)
- #: For testing. :class:`True` if :meth:`start` has been called.
- start_called = False
-
- #: For testing. :class:`True` if :meth:`destroy` has been called.
- destroy_called = False
-
- #: For testing. Contains all messages :meth:`process_message` has received.
- messages = []
-
#: For testing. Contains the last URI passed to :meth:`play_uri`.
uri = None
@@ -40,15 +33,6 @@ class DummyOutput(BaseOutput):
#: For testing. Contains the current volume.
volume = 100
- def start(self):
- self.start_called = True
-
- def destroy(self):
- self.destroy_called = True
-
- def process_message(self, message):
- self.messages.append(message)
-
def play_uri(self, uri):
self.uri = uri
return True
diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py
index 3c2f5ea7..d7ff6e6d 100644
--- a/mopidy/outputs/gstreamer.py
+++ b/mopidy/outputs/gstreamer.py
@@ -3,113 +3,39 @@ pygst.require('0.10')
import gst
import logging
-import multiprocessing
+
+from pykka.actor import ThreadingActor
+from pykka.registry import ActorRegistry
from mopidy import settings
+from mopidy.backends.base import Backend
from mopidy.outputs.base import BaseOutput
-from mopidy.utils.process import (BaseThread, pickle_connection,
- unpickle_connection)
logger = logging.getLogger('mopidy.outputs.gstreamer')
-class GStreamerOutput(BaseOutput):
+class GStreamerOutput(ThreadingActor, BaseOutput):
"""
- Audio output through GStreamer.
-
- Starts :class:`GStreamerMessagesThread` and :class:`GStreamerPlayerThread`.
+ Audio output through `GStreamer `_.
**Settings:**
- :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
+
"""
- def __init__(self, *args, **kwargs):
- super(GStreamerOutput, self).__init__(*args, **kwargs)
- self.output_queue = multiprocessing.Queue()
- self.player_thread = GStreamerPlayerThread(self.core_queue,
- self.output_queue)
-
- def start(self):
- self.player_thread.start()
-
- def destroy(self):
- self.player_thread.destroy()
-
- def process_message(self, message):
- assert message['to'] == 'output', \
- u'Message recipient must be "output".'
- self.output_queue.put(message)
-
- def _send_recv(self, message):
- (my_end, other_end) = multiprocessing.Pipe()
- message['to'] = 'output'
- message['reply_to'] = pickle_connection(other_end)
- self.process_message(message)
- my_end.poll(None)
- return my_end.recv()
-
- def _send(self, message):
- message['to'] = 'output'
- self.process_message(message)
-
- def play_uri(self, uri):
- return self._send_recv({'command': 'play_uri', 'uri': uri})
-
- def deliver_data(self, capabilities, data):
- return self._send({
- 'command': 'deliver_data',
- 'caps': capabilities,
- 'data': data,
- })
-
- def end_of_data_stream(self):
- return self._send({'command': 'end_of_data_stream'})
-
- def get_position(self):
- return self._send_recv({'command': 'get_position'})
-
- def set_position(self, position):
- return self._send_recv({'command': 'set_position',
- 'position': position})
-
- def set_state(self, state):
- return self._send_recv({'command': 'set_state', 'state': state})
-
- def get_volume(self):
- return self._send_recv({'command': 'get_volume'})
-
- def set_volume(self, volume):
- return self._send_recv({'command': 'set_volume', 'volume': volume})
-
-
-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
- http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html.
- """
-
- def __init__(self, core_queue, output_queue):
- super(GStreamerPlayerThread, self).__init__(core_queue)
- self.name = u'GStreamerPlayerThread'
- self.output_queue = output_queue
+ def __init__(self):
self.gst_pipeline = None
- def run_inside_try(self):
- self.setup()
- while True:
- message = self.output_queue.get()
- self.process_mopidy_message(message)
+ def on_start(self):
+ self._setup_gstreamer()
+
+ def _setup_gstreamer(self):
+ """
+ **Warning:** :class:`GStreamerOutput` requires
+ :class:`mopidy.utils.process.GObjectEventThread` to be running. This is
+ not enforced by :class:`GStreamerOutput` itself.
+ """
- def setup(self):
logger.debug(u'Setting up GStreamer pipeline')
self.gst_pipeline = gst.parse_launch(' ! '.join([
@@ -122,7 +48,7 @@ class GStreamerPlayerThread(BaseThread):
if settings.BACKENDS[0] == 'mopidy.backends.local.LocalBackend':
uri_bin = gst.element_factory_make('uridecodebin', 'uri')
- uri_bin.connect('pad-added', self.process_new_pad, pad)
+ uri_bin.connect('pad-added', self._process_new_pad, pad)
self.gst_pipeline.add(uri_bin)
else:
app_src = gst.element_factory_make('appsrc', 'appsrc')
@@ -141,57 +67,29 @@ class GStreamerPlayerThread(BaseThread):
# Setup bus and message processor
gst_bus = self.gst_pipeline.get_bus()
gst_bus.add_signal_watch()
- gst_bus.connect('message', self.process_gst_message)
+ gst_bus.connect('message', self._process_gstreamer_message)
- def process_new_pad(self, source, pad, target_pad):
+ def _process_new_pad(self, source, pad, target_pad):
pad.link(target_pad)
- def process_mopidy_message(self, message):
- """Process messages from the rest of Mopidy."""
- if message['command'] == 'play_uri':
- response = self.play_uri(message['uri'])
- connection = unpickle_connection(message['reply_to'])
- connection.send(response)
- elif message['command'] == 'deliver_data':
- self.deliver_data(message['caps'], message['data'])
- elif message['command'] == 'end_of_data_stream':
- self.end_of_data_stream()
- elif message['command'] == 'set_state':
- response = self.set_state(message['state'])
- connection = unpickle_connection(message['reply_to'])
- connection.send(response)
- elif message['command'] == 'get_volume':
- volume = self.get_volume()
- connection = unpickle_connection(message['reply_to'])
- connection.send(volume)
- elif message['command'] == 'set_volume':
- response = self.set_volume(message['volume'])
- connection = unpickle_connection(message['reply_to'])
- connection.send(response)
- elif message['command'] == 'set_position':
- response = self.set_position(message['position'])
- connection = unpickle_connection(message['reply_to'])
- connection.send(response)
- elif message['command'] == 'get_position':
- response = self.get_position()
- connection = unpickle_connection(message['reply_to'])
- connection.send(response)
- else:
- logger.warning(u'Cannot handle message: %s', message)
-
- def process_gst_message(self, bus, message):
+ def _process_gstreamer_message(self, bus, message):
"""Process messages from GStreamer."""
if message.type == gst.MESSAGE_EOS:
logger.debug(u'GStreamer signalled end-of-stream. '
- 'Sending end_of_track to core_queue ...')
- self.core_queue.put({'command': 'end_of_track'})
+ 'Telling backend ...')
+ self._get_backend().playback.on_end_of_track()
elif message.type == gst.MESSAGE_ERROR:
self.set_state('NULL')
error, debug = message.parse_error()
logger.error(u'%s %s', error, debug)
- # FIXME Should we send 'stop_playback' to core here? Can we
+ # FIXME Should we send 'stop_playback' to the backend here? Can we
# differentiate on how serious the error is?
+ def _get_backend(self):
+ backend_refs = ActorRegistry.get_by_class(Backend)
+ assert len(backend_refs) == 1, 'Expected exactly one running backend.'
+ return backend_refs[0].proxy()
+
def play_uri(self, uri):
"""Play audio at URI"""
self.set_state('READY')
@@ -216,6 +114,21 @@ class GStreamerPlayerThread(BaseThread):
"""
self.gst_pipeline.get_by_name('appsrc').emit('end-of-stream')
+ def get_position(self):
+ try:
+ position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0]
+ return position // gst.MSECOND
+ except gst.QueryError, e:
+ logger.error('time_position failed: %s', e)
+ return 0
+
+ def set_position(self, position):
+ self.gst_pipeline.get_state() # block until state changes are done
+ handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME),
+ gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
+ self.gst_pipeline.get_state() # block until seek is done
+ return handeled
+
def set_state(self, state_name):
"""
Set the GStreamer state. Returns :class:`True` if successful.
diff --git a/mopidy/settings.py b/mopidy/settings.py
index 23aa7cb6..6e33ffaa 100644
--- a/mopidy/settings.py
+++ b/mopidy/settings.py
@@ -164,6 +164,11 @@ 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
diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py
index cc1c19c1..c74ff5ea 100644
--- a/mopidy/utils/log.py
+++ b/mopidy/utils/log.py
@@ -1,13 +1,15 @@
import logging
import logging.handlers
-from mopidy import settings
+from mopidy import get_version, settings
def setup_logging(verbosity_level, save_debug_log):
setup_root_logger()
setup_console_logging(verbosity_level)
if save_debug_log:
setup_debug_logging_to_file()
+ logger = logging.getLogger('mopidy.utils.log')
+ logger.info(u'-- Starting Mopidy %s --', get_version())
def setup_root_logger():
root = logging.getLogger('')
diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py
index 11dafa8a..dbc6cada 100644
--- a/mopidy/utils/process.py
+++ b/mopidy/utils/process.py
@@ -1,8 +1,5 @@
import logging
-import multiprocessing
-import multiprocessing.dummy
-from multiprocessing.reduction import reduce_connection
-import pickle
+import threading
import gobject
gobject.threads_init()
@@ -11,52 +8,10 @@ from mopidy import SettingsError
logger = logging.getLogger('mopidy.utils.process')
-def pickle_connection(connection):
- return pickle.dumps(reduce_connection(connection))
-def unpickle_connection(pickled_connection):
- # From http://stackoverflow.com/questions/1446004
- (func, args) = pickle.loads(pickled_connection)
- return func(*args)
-
-class BaseProcess(multiprocessing.Process):
- def __init__(self, core_queue):
- super(BaseProcess, self).__init__()
- self.core_queue = core_queue
-
- def run(self):
- logger.debug(u'%s: Starting process', self.name)
- try:
- self.run_inside_try()
- except KeyboardInterrupt:
- logger.info(u'Interrupted by user')
- self.exit(0, u'Interrupted by user')
- except SettingsError as e:
- logger.error(e.message)
- self.exit(1, u'Settings error')
- except ImportError as e:
- logger.error(e)
- self.exit(2, u'Import error')
- except Exception as e:
- logger.exception(e)
- self.exit(3, u'Unknown error')
-
- def run_inside_try(self):
- raise NotImplementedError
-
- def destroy(self):
- self.terminate()
-
- def exit(self, status=0, reason=None):
- self.core_queue.put({'to': 'core', 'command': 'exit',
- 'status': status, 'reason': reason})
- self.destroy()
-
-
-class BaseThread(multiprocessing.dummy.Process):
- def __init__(self, core_queue):
+class BaseThread(threading.Thread):
+ def __init__(self):
super(BaseThread, self).__init__()
- self.core_queue = core_queue
# No thread should block process from exiting
self.daemon = True
@@ -84,8 +39,6 @@ class BaseThread(multiprocessing.dummy.Process):
pass
def exit(self, status=0, reason=None):
- self.core_queue.put({'to': 'core', 'command': 'exit',
- 'status': status, 'reason': reason})
self.destroy()
@@ -98,8 +51,8 @@ class GObjectEventThread(BaseThread):
:mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc.
"""
- def __init__(self, core_queue):
- super(GObjectEventThread, self).__init__(core_queue)
+ def __init__(self):
+ super(GObjectEventThread, self).__init__()
self.name = u'GObjectEventThread'
self.loop = None
diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py
index 2ec0f716..529c6fb1 100644
--- a/mopidy/utils/settings.py
+++ b/mopidy/utils/settings.py
@@ -49,7 +49,7 @@ 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)
@@ -141,8 +141,7 @@ def list_settings_optparse_callback(*args):
lines = []
for (key, value) in sorted(settings.current.iteritems()):
default_value = settings.default.get(key)
- if key.endswith('PASSWORD') and len(value):
- value = u'********'
+ value = mask_value_if_secret(key, value)
lines.append(u'%s:' % key)
lines.append(u' Value: %s' % repr(value))
if value != default_value and default_value is not None:
@@ -151,3 +150,9 @@ def list_settings_optparse_callback(*args):
lines.append(u' Error: %s' % errors[key])
print u'Settings: %s' % indent('\n'.join(lines), places=2)
sys.exit(0)
+
+def mask_value_if_secret(key, value):
+ if key.endswith('PASSWORD') and value:
+ return u'********'
+ else:
+ return value
diff --git a/requirements/core.txt b/requirements/core.txt
new file mode 100644
index 00000000..aaae84f8
--- /dev/null
+++ b/requirements/core.txt
@@ -0,0 +1 @@
+Pykka >= 0.12
diff --git a/requirements/lastfm.txt b/requirements/lastfm.txt
index 887a0f0d..314c4223 100644
--- a/requirements/lastfm.txt
+++ b/requirements/lastfm.txt
@@ -1 +1 @@
-pylast >= 0.5
+pylast >= 0.5.7
diff --git a/requirements/tests.txt b/requirements/tests.txt
index 33f49451..f8cf2eb3 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -1,2 +1,4 @@
coverage
+mock
nose
+tox
diff --git a/setup.py b/setup.py
index d9d6af42..3d9d4fdf 100644
--- a/setup.py
+++ b/setup.py
@@ -69,7 +69,10 @@ 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']))
+if os.geteuid() == 0:
+ # Only try to install this file if we are root
+ data_files.append(
+ ('/usr/local/share/applications', ['data/mopidy.desktop']))
setup(
name='Mopidy',
diff --git a/tests/__init__.py b/tests/__init__.py
index c8618f3f..1d4d2e3d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,7 +1,9 @@
import os
try: # 2.7
+ # pylint: disable = E0611,F0401
from unittest.case import SkipTest
+ # pylint: enable = E0611,F0401
except ImportError:
try: # Nose
from nose.plugins.skip import SkipTest
@@ -14,9 +16,9 @@ from mopidy import settings
# Nuke any local settings to ensure same test env all over
settings.local.clear()
-def data_folder(name):
- folder = os.path.dirname(__file__)
- folder = os.path.join(folder, 'data')
- folder = os.path.abspath(folder)
- return os.path.join(folder, name)
+def path_to_data_dir(name):
+ path = os.path.dirname(__file__)
+ path = os.path.join(path, 'data')
+ path = os.path.abspath(path)
+ return os.path.join(path, name)
diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py
index 2b6cb84e..ee5e1111 100644
--- a/tests/backends/base/current_playlist.py
+++ b/tests/backends/base/current_playlist.py
@@ -1,11 +1,9 @@
+import mock
import multiprocessing
import random
-from mopidy import settings
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
-from mopidy.outputs.dummy import DummyOutput
-from mopidy.utils import get_class
+from mopidy.outputs.base import BaseOutput
from tests.backends.base import populate_playlist
@@ -13,19 +11,13 @@ class CurrentPlaylistControllerTest(object):
tracks = []
def setUp(self):
- self.core_queue = multiprocessing.Queue()
- self.output = DummyOutput(self.core_queue)
- self.backend = self.backend_class(
- self.core_queue, self.output, DummyMixer)
+ self.backend = self.backend_class()
+ self.backend.output = mock.Mock(spec=BaseOutput)
self.controller = self.backend.current_playlist
self.playback = self.backend.playback
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
- def tearDown(self):
- self.backend.destroy()
- self.output.destroy()
-
def test_add(self):
for track in self.tracks:
cp_track = self.controller.add(track)
diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py
index 71f62147..2a3de730 100644
--- a/tests/backends/base/library.py
+++ b/tests/backends/base/library.py
@@ -1,7 +1,6 @@
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track, Album, Artist
-from tests import SkipTest, data_folder
+from tests import SkipTest, path_to_data_dir
class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
@@ -9,18 +8,15 @@ class LibraryControllerTest(object):
Album(name='album2', artists=artists[1:2]),
Album()]
tracks = [Track(name='track1', length=4000, artists=artists[:1],
- album=albums[0], uri='file://' + data_folder('uri1')),
+ album=albums[0], uri='file://' + path_to_data_dir('uri1')),
Track(name='track2', length=4000, artists=artists[1:2],
- album=albums[1], uri='file://' + data_folder('uri2')),
+ album=albums[1], uri='file://' + path_to_data_dir('uri2')),
Track()]
def setUp(self):
- self.backend = self.backend_class(mixer_class=DummyMixer)
+ self.backend = self.backend_class()
self.library = self.backend.library
- def tearDown(self):
- self.backend.destroy()
-
def test_refresh(self):
self.library.refresh()
diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py
index 26662f96..8ea48a3a 100644
--- a/tests/backends/base/playback.py
+++ b/tests/backends/base/playback.py
@@ -1,12 +1,10 @@
+import mock
import multiprocessing
import random
import time
-from mopidy import settings
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
-from mopidy.outputs.dummy import DummyOutput
-from mopidy.utils import get_class
+from mopidy.outputs.base import BaseOutput
from tests import SkipTest
from tests.backends.base import populate_playlist
@@ -17,10 +15,8 @@ class PlaybackControllerTest(object):
tracks = []
def setUp(self):
- self.core_queue = multiprocessing.Queue()
- self.output = DummyOutput(self.core_queue)
- self.backend = self.backend_class(
- self.core_queue, self.output, DummyMixer)
+ self.backend = self.backend_class()
+ self.backend.output = mock.Mock(spec=BaseOutput)
self.playback = self.backend.playback
self.current_playlist = self.backend.current_playlist
@@ -29,10 +25,6 @@ class PlaybackControllerTest(object):
assert self.tracks[0].length >= 2000, \
'First song needs to be at least 2000 miliseconds'
- def tearDown(self):
- self.backend.destroy()
- self.output.destroy()
-
def test_initial_state_is_stopped(self):
self.assertEqual(self.playback.state, self.playback.STOPPED)
@@ -212,7 +204,7 @@ class PlaybackControllerTest(object):
def test_next_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
- for track in self.tracks:
+ for _ in self.tracks:
self.playback.next()
self.assertEqual(self.playback.current_track, None)
@@ -258,7 +250,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_next_track_at_end_of_playlist(self):
self.playback.play()
- for track in self.current_playlist.cp_tracks[1:]:
+ for _ in self.current_playlist.cp_tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.track_at_next, None)
@@ -266,7 +258,7 @@ class PlaybackControllerTest(object):
def test_next_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
- for track in self.tracks[1:]:
+ for _ in self.tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.track_at_next, self.tracks[0])
@@ -348,7 +340,7 @@ class PlaybackControllerTest(object):
def test_end_of_track_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
- for track in self.tracks:
+ for _ in self.tracks:
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, None)
@@ -394,7 +386,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_end_of_track_track_at_end_of_playlist(self):
self.playback.play()
- for track in self.current_playlist.cp_tracks[1:]:
+ for _ in self.current_playlist.cp_tracks[1:]:
self.playback.on_end_of_track()
self.assertEqual(self.playback.track_at_next, None)
@@ -402,7 +394,7 @@ class PlaybackControllerTest(object):
def test_end_of_track_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
- for track in self.tracks[1:]:
+ for _ in self.tracks[1:]:
self.playback.on_end_of_track()
self.assertEqual(self.playback.track_at_next, self.tracks[0])
@@ -466,7 +458,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_previous_track_with_consume(self):
self.playback.consume = True
- for track in self.tracks:
+ for _ in self.tracks:
self.playback.next()
self.assertEqual(self.playback.track_at_previous,
self.playback.current_track)
@@ -474,7 +466,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_previous_track_with_random(self):
self.playback.random = True
- for track in self.tracks:
+ for _ in self.tracks:
self.playback.next()
self.assertEqual(self.playback.track_at_previous,
self.playback.current_track)
@@ -547,7 +539,6 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_on_current_playlist_change_when_stopped(self):
- current_track = self.playback.current_track
self.backend.current_playlist.append([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
@@ -677,9 +668,10 @@ class PlaybackControllerTest(object):
self.playback.seek(0)
self.assertEqual(self.playback.state, self.playback.PLAYING)
+ @SkipTest
@populate_playlist
def test_seek_beyond_end_of_song(self):
- raise SkipTest # FIXME need to decide return value
+ # FIXME need to decide return value
self.playback.play()
result = self.playback.seek(self.tracks[0].length*100)
self.assert_(not result, 'Seek return value was %s' % result)
@@ -696,9 +688,10 @@ class PlaybackControllerTest(object):
self.playback.seek(self.current_playlist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, self.playback.STOPPED)
+ @SkipTest
@populate_playlist
def test_seek_beyond_start_of_song(self):
- raise SkipTest # FIXME need to decide return value
+ # FIXME need to decide return value
self.playback.play()
result = self.playback.seek(-1000)
self.assert_(not result, 'Seek return value was %s' % result)
@@ -734,10 +727,18 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.stop(), None)
def test_time_position_when_stopped(self):
+ future = mock.Mock()
+ future.get = mock.Mock(return_value=0)
+ self.backend.output.get_position = mock.Mock(return_value=future)
+
self.assertEqual(self.playback.time_position, 0)
@populate_playlist
def test_time_position_when_stopped_with_playlist(self):
+ future = mock.Mock()
+ future.get = mock.Mock(return_value=0)
+ self.backend.output.get_position = mock.Mock(return_value=future)
+
self.assertEqual(self.playback.time_position, 0)
@SkipTest # Uses sleep and does not work with LocalBackend+DummyOutput
@@ -770,7 +771,7 @@ class PlaybackControllerTest(object):
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
self.playback.consume = True
self.playback.play()
- for i in range(len(self.backend.current_playlist.tracks)):
+ for _ in range(len(self.backend.current_playlist.tracks)):
self.playback.on_end_of_track()
self.assertEqual(len(self.backend.current_playlist.tracks), 0)
@@ -824,14 +825,14 @@ class PlaybackControllerTest(object):
def test_random_until_end_of_playlist(self):
self.playback.random = True
self.playback.play()
- for track in self.tracks[1:]:
+ for _ in self.tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.track_at_next, None)
@populate_playlist
def test_random_until_end_of_playlist_and_play_from_start(self):
self.playback.repeat = True
- for track in self.tracks:
+ for _ in self.tracks:
self.playback.next()
self.assertNotEqual(self.playback.track_at_next, None)
self.assertEqual(self.playback.state, self.playback.STOPPED)
@@ -843,7 +844,7 @@ class PlaybackControllerTest(object):
self.playback.repeat = True
self.playback.random = True
self.playback.play()
- for track in self.tracks:
+ for _ in self.tracks:
self.playback.next()
self.assertNotEqual(self.playback.track_at_next, None)
@@ -852,7 +853,7 @@ class PlaybackControllerTest(object):
self.playback.random = True
self.playback.play()
played = []
- for track in self.tracks:
+ for _ in self.tracks:
self.assert_(self.playback.current_track not in played)
played.append(self.playback.current_track)
self.playback.next()
diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py
index 0ac0b167..839d5bed 100644
--- a/tests/backends/base/stored_playlists.py
+++ b/tests/backends/base/stored_playlists.py
@@ -3,23 +3,20 @@ import shutil
import tempfile
from mopidy import settings
-from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist
-from tests import SkipTest, data_folder
+from tests import SkipTest, path_to_data_dir
class StoredPlaylistsControllerTest(object):
def setUp(self):
settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
- settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
- settings.LOCAL_MUSIC_PATH = data_folder('')
+ settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
+ settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
- self.backend = self.backend_class(mixer_class=DummyMixer)
+ self.backend = self.backend_class()
self.stored = self.backend.stored_playlists
def tearDown(self):
- self.backend.destroy()
-
if os.path.exists(settings.LOCAL_PLAYLIST_PATH):
shutil.rmtree(settings.LOCAL_PLAYLIST_PATH)
diff --git a/tests/backends/local/__init__.py b/tests/backends/local/__init__.py
index 60a1bd4d..d2213297 100644
--- a/tests/backends/local/__init__.py
+++ b/tests/backends/local/__init__.py
@@ -1,6 +1,6 @@
from mopidy.utils.path import path_to_uri
-from tests import data_folder
+from tests import path_to_data_dir
-song = data_folder('song%s.wav')
+song = path_to_data_dir('song%s.wav')
generate_song = lambda i: path_to_uri(song % i)
diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py
index 0c44924a..68ab22e9 100644
--- a/tests/backends/local/library_test.py
+++ b/tests/backends/local/library_test.py
@@ -9,7 +9,7 @@ if sys.platform == 'win32':
from mopidy import settings
from mopidy.backends.local import LocalBackend
-from tests import data_folder
+from tests import path_to_data_dir
from tests.backends.base.library import LibraryControllerTest
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
@@ -17,8 +17,8 @@ class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend
def setUp(self):
- settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache')
- settings.LOCAL_MUSIC_PATH = data_folder('')
+ settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
+ settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
super(LocalLibraryControllerTest, self).setUp()
diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py
index 2007cff8..2cdeadb9 100644
--- a/tests/backends/local/playback_test.py
+++ b/tests/backends/local/playback_test.py
@@ -11,7 +11,7 @@ from mopidy.backends.local import LocalBackend
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
-from tests import data_folder
+from tests import path_to_data_dir
from tests.backends.base.playback import PlaybackControllerTest
from tests.backends.local import generate_song
@@ -32,7 +32,7 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
settings.runtime.clear()
def add_track(self, path):
- uri = path_to_uri(data_folder(path))
+ uri = path_to_uri(path_to_data_dir(path))
track = Track(uri=uri, length=4464)
self.backend.current_playlist.add(track)
diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py
index a7d9043f..b426e9ce 100644
--- a/tests/backends/local/stored_playlists_test.py
+++ b/tests/backends/local/stored_playlists_test.py
@@ -14,7 +14,7 @@ from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
from mopidy.utils.path import path_to_uri
-from tests import data_folder
+from tests import path_to_data_dir
from tests.backends.base.stored_playlists import \
StoredPlaylistsControllerTest
from tests.backends.local import generate_song
@@ -65,13 +65,12 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
self.assertEqual(uri, contents.strip())
def test_playlists_are_loaded_at_startup(self):
- track = Track(uri=path_to_uri(data_folder('uri2')))
+ track = Track(uri=path_to_uri(path_to_data_dir('uri2')))
playlist = Playlist(tracks=[track], name='test')
self.stored.save(playlist)
- self.backend.destroy()
- self.backend = self.backend_class(mixer_class=DummyMixer)
+ self.backend = self.backend_class()
self.stored = self.backend.stored_playlists
self.assert_(self.stored.playlists)
diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py
index b7fd212c..a4e9f317 100644
--- a/tests/backends/local/translator_test.py
+++ b/tests/backends/local/translator_test.py
@@ -8,26 +8,26 @@ from mopidy.utils.path import path_to_uri
from mopidy.backends.local.translator import parse_m3u, parse_mpd_tag_cache
from mopidy.models import Track, Artist, Album
-from tests import SkipTest, data_folder
+from tests import SkipTest, path_to_data_dir
-song1_path = data_folder('song1.mp3')
-song2_path = data_folder('song2.mp3')
-encoded_path = data_folder(u'æøå.mp3')
+song1_path = path_to_data_dir('song1.mp3')
+song2_path = path_to_data_dir('song2.mp3')
+encoded_path = path_to_data_dir(u'æøå.mp3')
song1_uri = path_to_uri(song1_path)
song2_uri = path_to_uri(song2_path)
encoded_uri = path_to_uri(encoded_path)
class M3UToUriTest(unittest.TestCase):
def test_empty_file(self):
- uris = parse_m3u(data_folder('empty.m3u'))
+ uris = parse_m3u(path_to_data_dir('empty.m3u'))
self.assertEqual([], uris)
def test_basic_file(self):
- uris = parse_m3u(data_folder('one.m3u'))
+ uris = parse_m3u(path_to_data_dir('one.m3u'))
self.assertEqual([song1_uri], uris)
def test_file_with_comment(self):
- uris = parse_m3u(data_folder('comment.m3u'))
+ uris = parse_m3u(path_to_data_dir('comment.m3u'))
self.assertEqual([song1_uri], uris)
def test_file_with_absolute_files(self):
@@ -64,11 +64,11 @@ class M3UToUriTest(unittest.TestCase):
os.remove(tmp.name)
def test_encoding_is_latin1(self):
- uris = parse_m3u(data_folder('encoding.m3u'))
+ uris = parse_m3u(path_to_data_dir('encoding.m3u'))
self.assertEqual([encoded_uri], uris)
def test_open_missing_file(self):
- uris = parse_m3u(data_folder('non-existant.m3u'))
+ uris = parse_m3u(path_to_data_dir('non-existant.m3u'))
self.assertEqual([], uris)
@@ -81,7 +81,7 @@ expected_albums = [Album(name='albumname', artists=expected_artists,
expected_tracks = []
def generate_track(path, ident):
- uri = path_to_uri(data_folder(path))
+ uri = path_to_uri(path_to_data_dir(path))
track = Track(name='trackname', artists=expected_artists, track_no=1,
album=expected_albums[0], length=4000, uri=uri)
expected_tracks.append(track)
@@ -98,28 +98,28 @@ generate_track('subdir1/subsubdir/song9.mp3', 1)
class MPDTagCacheToTracksTest(unittest.TestCase):
def test_emtpy_cache(self):
- tracks = parse_mpd_tag_cache(data_folder('empty_tag_cache'),
- data_folder(''))
+ tracks = parse_mpd_tag_cache(path_to_data_dir('empty_tag_cache'),
+ path_to_data_dir(''))
self.assertEqual(set(), tracks)
def test_simple_cache(self):
- tracks = parse_mpd_tag_cache(data_folder('simple_tag_cache'),
- data_folder(''))
- uri = path_to_uri(data_folder('song1.mp3'))
+ tracks = parse_mpd_tag_cache(path_to_data_dir('simple_tag_cache'),
+ path_to_data_dir(''))
+ uri = path_to_uri(path_to_data_dir('song1.mp3'))
track = Track(name='trackname', artists=expected_artists, track_no=1,
album=expected_albums[0], length=4000, uri=uri)
self.assertEqual(set([track]), tracks)
def test_advanced_cache(self):
- tracks = parse_mpd_tag_cache(data_folder('advanced_tag_cache'),
- data_folder(''))
+ tracks = parse_mpd_tag_cache(path_to_data_dir('advanced_tag_cache'),
+ path_to_data_dir(''))
self.assertEqual(set(expected_tracks), tracks)
def test_unicode_cache(self):
- tracks = parse_mpd_tag_cache(data_folder('utf8_tag_cache'),
- data_folder(''))
+ tracks = parse_mpd_tag_cache(path_to_data_dir('utf8_tag_cache'),
+ path_to_data_dir(''))
- uri = path_to_uri(data_folder('song1.mp3'))
+ uri = path_to_uri(path_to_data_dir('song1.mp3'))
artists = [Artist(name=u'æøå')]
album = Album(name=u'æøå', artists=artists)
track = Track(uri=uri, name=u'æøå', artists=artists,
@@ -132,14 +132,14 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
raise SkipTest
def test_cache_with_blank_track_info(self):
- tracks = parse_mpd_tag_cache(data_folder('blank_tag_cache'),
- data_folder(''))
- uri = path_to_uri(data_folder('song1.mp3'))
+ tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'),
+ path_to_data_dir(''))
+ uri = path_to_uri(path_to_data_dir('song1.mp3'))
self.assertEqual(set([Track(uri=uri, length=4000)]), tracks)
def test_musicbrainz_tagcache(self):
- tracks = parse_mpd_tag_cache(data_folder('musicbrainz_tag_cache'),
- data_folder(''))
+ tracks = parse_mpd_tag_cache(path_to_data_dir('musicbrainz_tag_cache'),
+ path_to_data_dir(''))
artist = list(expected_tracks[0].artists)[0].copy(
musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897')
albumartist = list(expected_tracks[0].artists)[0].copy(
@@ -153,9 +153,9 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
self.assertEqual(track, list(tracks)[0])
def test_albumartist_tag_cache(self):
- tracks = parse_mpd_tag_cache(data_folder('albumartist_tag_cache'),
- data_folder(''))
- uri = path_to_uri(data_folder('song1.mp3'))
+ tracks = parse_mpd_tag_cache(path_to_data_dir('albumartist_tag_cache'),
+ path_to_data_dir(''))
+ uri = path_to_uri(path_to_data_dir('song1.mp3'))
artist = Artist(name='albumartistname')
album = expected_albums[0].copy(artists=[artist])
track = Track(name='trackname', artists=expected_artists, track_no=1,
diff --git a/tests/data/scanner/advanced/song1.mp3 b/tests/data/scanner/advanced/song1.mp3
deleted file mode 120000
index 6896a7a2..00000000
--- a/tests/data/scanner/advanced/song1.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/song1.mp3 b/tests/data/scanner/advanced/song1.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/song1.mp3 differ
diff --git a/tests/data/scanner/advanced/song2.mp3 b/tests/data/scanner/advanced/song2.mp3
deleted file mode 120000
index 6896a7a2..00000000
--- a/tests/data/scanner/advanced/song2.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/song2.mp3 b/tests/data/scanner/advanced/song2.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/song2.mp3 differ
diff --git a/tests/data/scanner/advanced/song3.mp3 b/tests/data/scanner/advanced/song3.mp3
deleted file mode 120000
index 6896a7a2..00000000
--- a/tests/data/scanner/advanced/song3.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/song3.mp3 b/tests/data/scanner/advanced/song3.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/song3.mp3 differ
diff --git a/tests/data/scanner/advanced/subdir1/song4.mp3 b/tests/data/scanner/advanced/subdir1/song4.mp3
deleted file mode 120000
index 45812ac5..00000000
--- a/tests/data/scanner/advanced/subdir1/song4.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/subdir1/song4.mp3 b/tests/data/scanner/advanced/subdir1/song4.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/subdir1/song4.mp3 differ
diff --git a/tests/data/scanner/advanced/subdir1/song5.mp3 b/tests/data/scanner/advanced/subdir1/song5.mp3
deleted file mode 120000
index 45812ac5..00000000
--- a/tests/data/scanner/advanced/subdir1/song5.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/subdir1/song5.mp3 b/tests/data/scanner/advanced/subdir1/song5.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/subdir1/song5.mp3 differ
diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3
deleted file mode 120000
index e84bdc24..00000000
--- a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../../../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 differ
diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3
deleted file mode 120000
index e84bdc24..00000000
--- a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../../../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 differ
diff --git a/tests/data/scanner/advanced/subdir2/song6.mp3 b/tests/data/scanner/advanced/subdir2/song6.mp3
deleted file mode 120000
index 45812ac5..00000000
--- a/tests/data/scanner/advanced/subdir2/song6.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/subdir2/song6.mp3 b/tests/data/scanner/advanced/subdir2/song6.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/subdir2/song6.mp3 differ
diff --git a/tests/data/scanner/advanced/subdir2/song7.mp3 b/tests/data/scanner/advanced/subdir2/song7.mp3
deleted file mode 120000
index 45812ac5..00000000
--- a/tests/data/scanner/advanced/subdir2/song7.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/advanced/subdir2/song7.mp3 b/tests/data/scanner/advanced/subdir2/song7.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/advanced/subdir2/song7.mp3 differ
diff --git a/tests/data/scanner/simple/song1.mp3 b/tests/data/scanner/simple/song1.mp3
deleted file mode 120000
index 6896a7a2..00000000
--- a/tests/data/scanner/simple/song1.mp3
+++ /dev/null
@@ -1 +0,0 @@
-../sample.mp3
\ No newline at end of file
diff --git a/tests/data/scanner/simple/song1.mp3 b/tests/data/scanner/simple/song1.mp3
new file mode 100644
index 00000000..ad5aa37a
Binary files /dev/null and b/tests/data/scanner/simple/song1.mp3 differ
diff --git a/tests/data/song1.flac b/tests/data/song1.flac
deleted file mode 120000
index e5e7c129..00000000
--- a/tests/data/song1.flac
+++ /dev/null
@@ -1 +0,0 @@
-blank.flac
\ No newline at end of file
diff --git a/tests/data/song1.flac b/tests/data/song1.flac
new file mode 100644
index 00000000..ae18d36f
Binary files /dev/null and b/tests/data/song1.flac differ
diff --git a/tests/data/song1.mp3 b/tests/data/song1.mp3
deleted file mode 120000
index 03cdf66f..00000000
--- a/tests/data/song1.mp3
+++ /dev/null
@@ -1 +0,0 @@
-blank.mp3
\ No newline at end of file
diff --git a/tests/data/song1.mp3 b/tests/data/song1.mp3
new file mode 100644
index 00000000..ef159a70
Binary files /dev/null and b/tests/data/song1.mp3 differ
diff --git a/tests/data/song1.ogg b/tests/data/song1.ogg
deleted file mode 120000
index 33e773e1..00000000
--- a/tests/data/song1.ogg
+++ /dev/null
@@ -1 +0,0 @@
-blank.ogg
\ No newline at end of file
diff --git a/tests/data/song1.ogg b/tests/data/song1.ogg
new file mode 100644
index 00000000..e67e428b
Binary files /dev/null and b/tests/data/song1.ogg differ
diff --git a/tests/data/song1.wav b/tests/data/song1.wav
deleted file mode 120000
index 72a38fad..00000000
--- a/tests/data/song1.wav
+++ /dev/null
@@ -1 +0,0 @@
-blank.wav
\ No newline at end of file
diff --git a/tests/data/song1.wav b/tests/data/song1.wav
new file mode 100644
index 00000000..0041c7ba
Binary files /dev/null and b/tests/data/song1.wav differ
diff --git a/tests/data/song2.flac b/tests/data/song2.flac
deleted file mode 120000
index e5e7c129..00000000
--- a/tests/data/song2.flac
+++ /dev/null
@@ -1 +0,0 @@
-blank.flac
\ No newline at end of file
diff --git a/tests/data/song2.flac b/tests/data/song2.flac
new file mode 100644
index 00000000..ae18d36f
Binary files /dev/null and b/tests/data/song2.flac differ
diff --git a/tests/data/song2.mp3 b/tests/data/song2.mp3
deleted file mode 120000
index 03cdf66f..00000000
--- a/tests/data/song2.mp3
+++ /dev/null
@@ -1 +0,0 @@
-blank.mp3
\ No newline at end of file
diff --git a/tests/data/song2.mp3 b/tests/data/song2.mp3
new file mode 100644
index 00000000..ef159a70
Binary files /dev/null and b/tests/data/song2.mp3 differ
diff --git a/tests/data/song2.ogg b/tests/data/song2.ogg
deleted file mode 120000
index 33e773e1..00000000
--- a/tests/data/song2.ogg
+++ /dev/null
@@ -1 +0,0 @@
-blank.ogg
\ No newline at end of file
diff --git a/tests/data/song2.ogg b/tests/data/song2.ogg
new file mode 100644
index 00000000..e67e428b
Binary files /dev/null and b/tests/data/song2.ogg differ
diff --git a/tests/data/song2.wav b/tests/data/song2.wav
deleted file mode 120000
index 72a38fad..00000000
--- a/tests/data/song2.wav
+++ /dev/null
@@ -1 +0,0 @@
-blank.wav
\ No newline at end of file
diff --git a/tests/data/song2.wav b/tests/data/song2.wav
new file mode 100644
index 00000000..0041c7ba
Binary files /dev/null and b/tests/data/song2.wav differ
diff --git a/tests/data/song3.flac b/tests/data/song3.flac
deleted file mode 120000
index e5e7c129..00000000
--- a/tests/data/song3.flac
+++ /dev/null
@@ -1 +0,0 @@
-blank.flac
\ No newline at end of file
diff --git a/tests/data/song3.flac b/tests/data/song3.flac
new file mode 100644
index 00000000..ae18d36f
Binary files /dev/null and b/tests/data/song3.flac differ
diff --git a/tests/data/song3.mp3 b/tests/data/song3.mp3
deleted file mode 120000
index 03cdf66f..00000000
--- a/tests/data/song3.mp3
+++ /dev/null
@@ -1 +0,0 @@
-blank.mp3
\ No newline at end of file
diff --git a/tests/data/song3.mp3 b/tests/data/song3.mp3
new file mode 100644
index 00000000..ef159a70
Binary files /dev/null and b/tests/data/song3.mp3 differ
diff --git a/tests/data/song3.ogg b/tests/data/song3.ogg
deleted file mode 120000
index 33e773e1..00000000
--- a/tests/data/song3.ogg
+++ /dev/null
@@ -1 +0,0 @@
-blank.ogg
\ No newline at end of file
diff --git a/tests/data/song3.ogg b/tests/data/song3.ogg
new file mode 100644
index 00000000..e67e428b
Binary files /dev/null and b/tests/data/song3.ogg differ
diff --git a/tests/data/song3.wav b/tests/data/song3.wav
deleted file mode 120000
index 72a38fad..00000000
--- a/tests/data/song3.wav
+++ /dev/null
@@ -1 +0,0 @@
-blank.wav
\ No newline at end of file
diff --git a/tests/data/song3.wav b/tests/data/song3.wav
new file mode 100644
index 00000000..0041c7ba
Binary files /dev/null and b/tests/data/song3.wav differ
diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py
index 77ed05c4..afa99d26 100644
--- a/tests/frontends/mpd/audio_output_test.py
+++ b/tests/frontends/mpd/audio_output_test.py
@@ -2,11 +2,17 @@ import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_enableoutput(self):
result = self.h.handle_request(u'enableoutput "0"')
@@ -19,6 +25,6 @@ class AudioOutputHandlerTest(unittest.TestCase):
def test_outputs(self):
result = self.h.handle_request(u'outputs')
self.assert_(u'outputid: 0' in result)
- self.assert_(u'outputname: DummyBackend' in result)
+ self.assert_(u'outputname: None' in result)
self.assert_(u'outputenabled: 1' in result)
self.assert_(u'OK' in result)
diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py
index effc9862..7ff96bac 100644
--- a/tests/frontends/mpd/command_list_test.py
+++ b/tests/frontends/mpd/command_list_test.py
@@ -2,11 +2,17 @@ import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
class CommandListsTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_command_list_begin(self):
result = self.h.handle_request(u'command_list_begin')
diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py
index a4abbd27..cf161a5a 100644
--- a/tests/frontends/mpd/connection_test.py
+++ b/tests/frontends/mpd/connection_test.py
@@ -1,12 +1,20 @@
import unittest
+from mopidy import settings
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
class ConnectionHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
+ settings.runtime.clear()
def test_close(self):
result = self.h.handle_request(u'close')
@@ -20,9 +28,20 @@ class ConnectionHandlerTest(unittest.TestCase):
result = self.h.handle_request(u'kill')
self.assert_(u'OK' in result)
- def test_password(self):
+ def test_valid_password_is_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ result = self.h.handle_request(u'password "topsecret"')
+ self.assert_(u'OK' in result)
+
+ def test_invalid_password_is_not_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
result = self.h.handle_request(u'password "secret"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ self.assert_(u'ACK [3@0] {password} incorrect password' in result)
+
+ def test_any_password_is_not_accepted_when_password_check_turned_off(self):
+ settings.MPD_SERVER_PASSWORD = None
+ result = self.h.handle_request(u'password "secret"')
+ self.assert_(u'ACK [3@0] {password} incorrect password' in result)
def test_ping(self):
result = self.h.handle_request(u'ping')
diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py
index 06ff30ac..eb113ed7 100644
--- a/tests/frontends/mpd/current_playlist_test.py
+++ b/tests/frontends/mpd/current_playlist_test.py
@@ -2,30 +2,31 @@ import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class CurrentPlaylistHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_add(self):
needle = Track(uri='dummy://foo')
- self.b.library.provider._library = [Track(), Track(), needle, Track()]
+ self.b.library.provider.dummy_library = [
+ Track(), Track(), needle, Track()]
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'add "dummy://foo"')
- self.assertEqual(len(self.b.current_playlist.tracks), 6)
- self.assertEqual(self.b.current_playlist.tracks[5], needle)
self.assertEqual(len(result), 1)
- self.assert_(u'OK' in result)
-
- def test_add_with_uri_not_found_in_library_should_not_call_lookup(self):
- self.b.library.lookup = lambda uri: self.fail("Shouldn't run")
- result = self.h.handle_request(u'add "foo"')
- self.assertEqual(result[0],
- u'ACK [50@0] {add} directory or file not found')
+ self.assertEqual(result[0], u'OK')
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
+ self.assertEqual(self.b.current_playlist.tracks.get()[5], needle)
def test_add_with_uri_not_found_in_library_should_ack(self):
result = self.h.handle_request(u'add "dummy://foo"')
@@ -39,41 +40,43 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo')
- self.b.library.provider._library = [Track(), Track(), needle, Track()]
+ self.b.library.provider.dummy_library = [
+ Track(), Track(), needle, Track()]
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'addid "dummy://foo"')
- self.assertEqual(len(self.b.current_playlist.tracks), 6)
- self.assertEqual(self.b.current_playlist.tracks[5], needle)
- self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[5][0]
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
+ self.assertEqual(self.b.current_playlist.tracks.get()[5], needle)
+ self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[5][0]
in result)
self.assert_(u'OK' in result)
- def test_addid_with_empty_uri_does_not_lookup_and_acks(self):
- self.b.library.lookup = lambda uri: self.fail("Shouldn't run")
+ def test_addid_with_empty_uri_acks(self):
result = self.h.handle_request(u'addid ""')
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo')
- self.b.library.provider._library = [Track(), Track(), needle, Track()]
+ self.b.library.provider.dummy_library = [
+ Track(), Track(), needle, Track()]
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'addid "dummy://foo" "3"')
- self.assertEqual(len(self.b.current_playlist.tracks), 6)
- self.assertEqual(self.b.current_playlist.tracks[3], needle)
- self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[3][0]
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
+ self.assertEqual(self.b.current_playlist.tracks.get()[3], needle)
+ self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[3][0]
in result)
self.assert_(u'OK' in result)
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo')
- self.b.library.provider._library = [Track(), Track(), needle, Track()]
+ self.b.library.provider.dummy_library = [
+ Track(), Track(), needle, Track()]
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'addid "dummy://foo" "6"')
self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index')
@@ -84,65 +87,65 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_clear(self):
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'clear')
- self.assertEqual(len(self.b.current_playlist.tracks), 0)
- self.assertEqual(self.b.playback.current_track, None)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 0)
+ self.assertEqual(self.b.playback.current_track.get(), None)
self.assert_(u'OK' in result)
def test_delete_songpos(self):
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "%d"' %
- self.b.current_playlist.cp_tracks[2][0])
- self.assertEqual(len(self.b.current_playlist.tracks), 4)
+ self.b.current_playlist.cp_tracks.get()[2][0])
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 4)
self.assert_(u'OK' in result)
def test_delete_songpos_out_of_bounds(self):
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "5"')
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
def test_delete_open_range(self):
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "1:"')
- self.assertEqual(len(self.b.current_playlist.tracks), 1)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 1)
self.assert_(u'OK' in result)
def test_delete_closed_range(self):
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "1:3"')
- self.assertEqual(len(self.b.current_playlist.tracks), 3)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 3)
self.assert_(u'OK' in result)
def test_delete_range_out_of_bounds(self):
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
result = self.h.handle_request(u'delete "5:7"')
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
def test_deleteid(self):
self.b.current_playlist.append([Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 2)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
result = self.h.handle_request(u'deleteid "1"')
- self.assertEqual(len(self.b.current_playlist.tracks), 1)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 1)
self.assert_(u'OK' in result)
def test_deleteid_does_not_exist(self):
self.b.current_playlist.append([Track(), Track()])
- self.assertEqual(len(self.b.current_playlist.tracks), 2)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
result = self.h.handle_request(u'deleteid "12345"')
- self.assertEqual(len(self.b.current_playlist.tracks), 2)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
def test_move_songpos(self):
@@ -151,12 +154,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.h.handle_request(u'move "1" "0"')
- self.assertEqual(self.b.current_playlist.tracks[0].name, 'b')
- self.assertEqual(self.b.current_playlist.tracks[1].name, 'a')
- self.assertEqual(self.b.current_playlist.tracks[2].name, 'c')
- self.assertEqual(self.b.current_playlist.tracks[3].name, 'd')
- self.assertEqual(self.b.current_playlist.tracks[4].name, 'e')
- self.assertEqual(self.b.current_playlist.tracks[5].name, 'f')
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(tracks[0].name, 'b')
+ self.assertEqual(tracks[1].name, 'a')
+ self.assertEqual(tracks[2].name, 'c')
+ self.assertEqual(tracks[3].name, 'd')
+ self.assertEqual(tracks[4].name, 'e')
+ self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
def test_move_open_range(self):
@@ -165,12 +169,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.h.handle_request(u'move "2:" "0"')
- self.assertEqual(self.b.current_playlist.tracks[0].name, 'c')
- self.assertEqual(self.b.current_playlist.tracks[1].name, 'd')
- self.assertEqual(self.b.current_playlist.tracks[2].name, 'e')
- self.assertEqual(self.b.current_playlist.tracks[3].name, 'f')
- self.assertEqual(self.b.current_playlist.tracks[4].name, 'a')
- self.assertEqual(self.b.current_playlist.tracks[5].name, 'b')
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(tracks[0].name, 'c')
+ self.assertEqual(tracks[1].name, 'd')
+ self.assertEqual(tracks[2].name, 'e')
+ self.assertEqual(tracks[3].name, 'f')
+ self.assertEqual(tracks[4].name, 'a')
+ self.assertEqual(tracks[5].name, 'b')
self.assert_(u'OK' in result)
def test_move_closed_range(self):
@@ -179,12 +184,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.h.handle_request(u'move "1:3" "0"')
- self.assertEqual(self.b.current_playlist.tracks[0].name, 'b')
- self.assertEqual(self.b.current_playlist.tracks[1].name, 'c')
- self.assertEqual(self.b.current_playlist.tracks[2].name, 'a')
- self.assertEqual(self.b.current_playlist.tracks[3].name, 'd')
- self.assertEqual(self.b.current_playlist.tracks[4].name, 'e')
- self.assertEqual(self.b.current_playlist.tracks[5].name, 'f')
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(tracks[0].name, 'b')
+ self.assertEqual(tracks[1].name, 'c')
+ self.assertEqual(tracks[2].name, 'a')
+ self.assertEqual(tracks[3].name, 'd')
+ self.assertEqual(tracks[4].name, 'e')
+ self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
def test_moveid(self):
@@ -193,12 +199,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.h.handle_request(u'moveid "4" "2"')
- self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
- self.assertEqual(self.b.current_playlist.tracks[1].name, 'b')
- self.assertEqual(self.b.current_playlist.tracks[2].name, 'e')
- self.assertEqual(self.b.current_playlist.tracks[3].name, 'c')
- self.assertEqual(self.b.current_playlist.tracks[4].name, 'd')
- self.assertEqual(self.b.current_playlist.tracks[5].name, 'f')
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(tracks[0].name, 'a')
+ self.assertEqual(tracks[1].name, 'b')
+ self.assertEqual(tracks[2].name, 'e')
+ self.assertEqual(tracks[3].name, 'c')
+ self.assertEqual(tracks[4].name, 'd')
+ self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
def test_playlist_returns_same_as_playlistinfo(self):
@@ -360,14 +367,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_plchangesposid(self):
self.b.current_playlist.append([Track(), Track(), Track()])
result = self.h.handle_request(u'plchangesposid "0"')
+ cp_tracks = self.b.current_playlist.cp_tracks.get()
self.assert_(u'cpos: 0' in result)
- self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[0][0]
+ self.assert_(u'Id: %d' % cp_tracks[0][0]
in result)
self.assert_(u'cpos: 2' in result)
- self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[1][0]
+ self.assert_(u'Id: %d' % cp_tracks[1][0]
in result)
self.assert_(u'cpos: 2' in result)
- self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[2][0]
+ self.assert_(u'Id: %d' % cp_tracks[2][0]
in result)
self.assert_(u'OK' in result)
@@ -376,9 +384,9 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.b.current_playlist.version
+ version = self.b.current_playlist.version.get()
result = self.h.handle_request(u'shuffle')
- self.assert_(version < self.b.current_playlist.version)
+ self.assert_(version < self.b.current_playlist.version.get())
self.assert_(u'OK' in result)
def test_shuffle_with_open_range(self):
@@ -386,13 +394,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.b.current_playlist.version
+ version = self.b.current_playlist.version.get()
result = self.h.handle_request(u'shuffle "4:"')
- self.assert_(version < self.b.current_playlist.version)
- self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
- self.assertEqual(self.b.current_playlist.tracks[1].name, 'b')
- self.assertEqual(self.b.current_playlist.tracks[2].name, 'c')
- self.assertEqual(self.b.current_playlist.tracks[3].name, 'd')
+ self.assert_(version < self.b.current_playlist.version.get())
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(tracks[0].name, 'a')
+ self.assertEqual(tracks[1].name, 'b')
+ self.assertEqual(tracks[2].name, 'c')
+ self.assertEqual(tracks[3].name, 'd')
self.assert_(u'OK' in result)
def test_shuffle_with_closed_range(self):
@@ -400,13 +409,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.b.current_playlist.version
+ version = self.b.current_playlist.version.get()
result = self.h.handle_request(u'shuffle "1:3"')
- self.assert_(version < self.b.current_playlist.version)
- self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
- self.assertEqual(self.b.current_playlist.tracks[3].name, 'd')
- self.assertEqual(self.b.current_playlist.tracks[4].name, 'e')
- self.assertEqual(self.b.current_playlist.tracks[5].name, 'f')
+ self.assert_(version < self.b.current_playlist.version.get())
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(tracks[0].name, 'a')
+ self.assertEqual(tracks[3].name, 'd')
+ self.assertEqual(tracks[4].name, 'e')
+ self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
def test_swap(self):
@@ -415,12 +425,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.h.handle_request(u'swap "1" "4"')
- self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
- self.assertEqual(self.b.current_playlist.tracks[1].name, 'e')
- self.assertEqual(self.b.current_playlist.tracks[2].name, 'c')
- self.assertEqual(self.b.current_playlist.tracks[3].name, 'd')
- self.assertEqual(self.b.current_playlist.tracks[4].name, 'b')
- self.assertEqual(self.b.current_playlist.tracks[5].name, 'f')
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(tracks[0].name, 'a')
+ self.assertEqual(tracks[1].name, 'e')
+ self.assertEqual(tracks[2].name, 'c')
+ self.assertEqual(tracks[3].name, 'd')
+ self.assertEqual(tracks[4].name, 'b')
+ self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
def test_swapid(self):
@@ -429,10 +440,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.h.handle_request(u'swapid "1" "4"')
- self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
- self.assertEqual(self.b.current_playlist.tracks[1].name, 'e')
- self.assertEqual(self.b.current_playlist.tracks[2].name, 'c')
- self.assertEqual(self.b.current_playlist.tracks[3].name, 'd')
- self.assertEqual(self.b.current_playlist.tracks[4].name, 'b')
- self.assertEqual(self.b.current_playlist.tracks[5].name, 'f')
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(tracks[0].name, 'a')
+ self.assertEqual(tracks[1].name, 'e')
+ self.assertEqual(tracks[2].name, 'c')
+ self.assertEqual(tracks[3].name, 'd')
+ self.assertEqual(tracks[4].name, 'b')
+ self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py
index 183f01d8..77e0ddf0 100644
--- a/tests/frontends/mpd/dispatcher_test.py
+++ b/tests/frontends/mpd/dispatcher_test.py
@@ -4,11 +4,17 @@ from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
from mopidy.frontends.mpd.exceptions import MpdAckError
from mopidy.frontends.mpd.protocol import request_handlers, handle_pattern
+from mopidy.mixers.dummy import DummyMixer
class MpdDispatcherTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_register_same_pattern_twice_fails(self):
func = lambda: None
diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py
index 36d92adf..28469136 100644
--- a/tests/frontends/mpd/music_db_test.py
+++ b/tests/frontends/mpd/music_db_test.py
@@ -2,11 +2,17 @@ import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
class MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_count(self):
result = self.h.handle_request(u'count "tag" "needle"')
@@ -64,8 +70,13 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
class MusicDatabaseFindTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_find_album(self):
result = self.h.handle_request(u'find "album" "what"')
@@ -91,7 +102,20 @@ class MusicDatabaseFindTest(unittest.TestCase):
result = self.h.handle_request(u'find title "what"')
self.assert_(u'OK' in result)
+ def test_find_date(self):
+ result = self.h.handle_request(u'find "date" "2002-01-01"')
+ self.assert_(u'OK' in result)
+
+ def test_find_date_without_quotes(self):
+ result = self.h.handle_request(u'find date "2002-01-01"')
+ self.assert_(u'OK' in result)
+
+ def test_find_date_with_capital_d_and_incomplete_date(self):
+ result = self.h.handle_request(u'find Date "2005"')
+ self.assert_(u'OK' in result)
+
def test_find_else_should_fail(self):
+
result = self.h.handle_request(u'find "somethingelse" "what"')
self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments')
@@ -103,8 +127,13 @@ class MusicDatabaseFindTest(unittest.TestCase):
class MusicDatabaseListTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_list_foo_returns_ack(self):
result = self.h.handle_request(u'list "foo"')
@@ -294,8 +323,13 @@ class MusicDatabaseListTest(unittest.TestCase):
class MusicDatabaseSearchTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_search_album(self):
result = self.h.handle_request(u'search "album" "analbum"')
@@ -337,8 +371,18 @@ class MusicDatabaseSearchTest(unittest.TestCase):
result = self.h.handle_request(u'search any "anything"')
self.assert_(u'OK' in result)
+ def test_search_date(self):
+ result = self.h.handle_request(u'search "date" "2002-01-01"')
+ self.assert_(u'OK' in result)
+
+ def test_search_date_without_quotes(self):
+ result = self.h.handle_request(u'search date "2002-01-01"')
+ self.assert_(u'OK' in result)
+
+ def test_search_date_with_capital_d_and_incomplete_date(self):
+ result = self.h.handle_request(u'search Date "2005"')
+ self.assert_(u'OK' in result)
+
def test_search_else_should_fail(self):
result = self.h.handle_request(u'search "sometype" "something"')
self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')
-
-
diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py
index 70309d66..8601aa9c 100644
--- a/tests/frontends/mpd/playback_test.py
+++ b/tests/frontends/mpd/playback_test.py
@@ -1,32 +1,45 @@
import unittest
+from mopidy.backends.base import PlaybackController
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
+from tests import SkipTest
+
+PAUSED = PlaybackController.PAUSED
+PLAYING = PlaybackController.PLAYING
+STOPPED = PlaybackController.STOPPED
+
class PlaybackOptionsHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_consume_off(self):
result = self.h.handle_request(u'consume "0"')
- self.assertFalse(self.b.playback.consume)
+ self.assertFalse(self.b.playback.consume.get())
self.assert_(u'OK' in result)
def test_consume_off_without_quotes(self):
result = self.h.handle_request(u'consume 0')
- self.assertFalse(self.b.playback.consume)
+ self.assertFalse(self.b.playback.consume.get())
self.assert_(u'OK' in result)
def test_consume_on(self):
result = self.h.handle_request(u'consume "1"')
- self.assertTrue(self.b.playback.consume)
+ self.assertTrue(self.b.playback.consume.get())
self.assert_(u'OK' in result)
def test_consume_on_without_quotes(self):
result = self.h.handle_request(u'consume 1')
- self.assertTrue(self.b.playback.consume)
+ self.assertTrue(self.b.playback.consume.get())
self.assert_(u'OK' in result)
def test_crossfade(self):
@@ -35,97 +48,97 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
def test_random_off(self):
result = self.h.handle_request(u'random "0"')
- self.assertFalse(self.b.playback.random)
+ self.assertFalse(self.b.playback.random.get())
self.assert_(u'OK' in result)
def test_random_off_without_quotes(self):
result = self.h.handle_request(u'random 0')
- self.assertFalse(self.b.playback.random)
+ self.assertFalse(self.b.playback.random.get())
self.assert_(u'OK' in result)
def test_random_on(self):
result = self.h.handle_request(u'random "1"')
- self.assertTrue(self.b.playback.random)
+ self.assertTrue(self.b.playback.random.get())
self.assert_(u'OK' in result)
def test_random_on_without_quotes(self):
result = self.h.handle_request(u'random 1')
- self.assertTrue(self.b.playback.random)
+ self.assertTrue(self.b.playback.random.get())
self.assert_(u'OK' in result)
def test_repeat_off(self):
result = self.h.handle_request(u'repeat "0"')
- self.assertFalse(self.b.playback.repeat)
+ self.assertFalse(self.b.playback.repeat.get())
self.assert_(u'OK' in result)
def test_repeat_off_without_quotes(self):
result = self.h.handle_request(u'repeat 0')
- self.assertFalse(self.b.playback.repeat)
+ self.assertFalse(self.b.playback.repeat.get())
self.assert_(u'OK' in result)
def test_repeat_on(self):
result = self.h.handle_request(u'repeat "1"')
- self.assertTrue(self.b.playback.repeat)
+ self.assertTrue(self.b.playback.repeat.get())
self.assert_(u'OK' in result)
def test_repeat_on_without_quotes(self):
result = self.h.handle_request(u'repeat 1')
- self.assertTrue(self.b.playback.repeat)
+ self.assertTrue(self.b.playback.repeat.get())
self.assert_(u'OK' in result)
def test_setvol_below_min(self):
result = self.h.handle_request(u'setvol "-10"')
self.assert_(u'OK' in result)
- self.assertEqual(0, self.b.mixer.volume)
+ self.assertEqual(0, self.mixer.volume.get())
def test_setvol_min(self):
result = self.h.handle_request(u'setvol "0"')
self.assert_(u'OK' in result)
- self.assertEqual(0, self.b.mixer.volume)
+ self.assertEqual(0, self.mixer.volume.get())
def test_setvol_middle(self):
result = self.h.handle_request(u'setvol "50"')
self.assert_(u'OK' in result)
- self.assertEqual(50, self.b.mixer.volume)
+ self.assertEqual(50, self.mixer.volume.get())
def test_setvol_max(self):
result = self.h.handle_request(u'setvol "100"')
self.assert_(u'OK' in result)
- self.assertEqual(100, self.b.mixer.volume)
+ self.assertEqual(100, self.mixer.volume.get())
def test_setvol_above_max(self):
result = self.h.handle_request(u'setvol "110"')
self.assert_(u'OK' in result)
- self.assertEqual(100, self.b.mixer.volume)
+ self.assertEqual(100, self.mixer.volume.get())
def test_setvol_plus_is_ignored(self):
result = self.h.handle_request(u'setvol "+10"')
self.assert_(u'OK' in result)
- self.assertEqual(10, self.b.mixer.volume)
+ self.assertEqual(10, self.mixer.volume.get())
def test_setvol_without_quotes(self):
result = self.h.handle_request(u'setvol 50')
self.assert_(u'OK' in result)
- self.assertEqual(50, self.b.mixer.volume)
+ self.assertEqual(50, self.mixer.volume.get())
def test_single_off(self):
result = self.h.handle_request(u'single "0"')
- self.assertFalse(self.b.playback.single)
+ self.assertFalse(self.b.playback.single.get())
self.assert_(u'OK' in result)
def test_single_off_without_quotes(self):
result = self.h.handle_request(u'single 0')
- self.assertFalse(self.b.playback.single)
+ self.assertFalse(self.b.playback.single.get())
self.assert_(u'OK' in result)
def test_single_on(self):
result = self.h.handle_request(u'single "1"')
- self.assertTrue(self.b.playback.single)
+ self.assertTrue(self.b.playback.single.get())
self.assert_(u'OK' in result)
def test_single_on_without_quotes(self):
result = self.h.handle_request(u'single 1')
- self.assertTrue(self.b.playback.single)
+ self.assertTrue(self.b.playback.single.get())
self.assert_(u'OK' in result)
def test_replay_gain_mode_off(self):
@@ -146,32 +159,40 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
self.assert_(expected in result)
- #def test_replay_gain_status_off(self):
- # expected = u'off'
- # self.h._replay_gain_mode(expected)
- # result = self.h.handle_request(u'replay_gain_status')
- # self.assert_(u'OK' in result)
- # self.assert_(expected in result)
+ def test_replay_gain_status_off(self):
+ raise SkipTest
+ expected = u'off'
+ self.h._replay_gain_mode(expected)
+ result = self.h.handle_request(u'replay_gain_status')
+ self.assert_(u'OK' in result)
+ self.assert_(expected in result)
- #def test_replay_gain_status_track(self):
- # expected = u'track'
- # self.h._replay_gain_mode(expected)
- # result = self.h.handle_request(u'replay_gain_status')
- # self.assert_(u'OK' in result)
- # self.assert_(expected in result)
+ def test_replay_gain_status_track(self):
+ raise SkipTest
+ expected = u'track'
+ self.h._replay_gain_mode(expected)
+ result = self.h.handle_request(u'replay_gain_status')
+ self.assert_(u'OK' in result)
+ self.assert_(expected in result)
- #def test_replay_gain_status_album(self):
- # expected = u'album'
- # self.h._replay_gain_mode(expected)
- # result = self.h.handle_request(u'replay_gain_status')
- # self.assert_(u'OK' in result)
- # self.assert_(expected in result)
+ def test_replay_gain_status_album(self):
+ raise SkipTest
+ expected = u'album'
+ self.h._replay_gain_mode(expected)
+ result = self.h.handle_request(u'replay_gain_status')
+ self.assert_(u'OK' in result)
+ self.assert_(expected in result)
class PlaybackControlHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_next(self):
result = self.h.handle_request(u'next')
@@ -183,123 +204,155 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.h.handle_request(u'pause "1"')
result = self.h.handle_request(u'pause "0"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
def test_pause_on(self):
self.b.current_playlist.append([Track()])
self.h.handle_request(u'play "0"')
result = self.h.handle_request(u'pause "1"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PAUSED, self.b.playback.state)
+ self.assertEqual(PAUSED, self.b.playback.state.get())
def test_pause_toggle(self):
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'play "0"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
result = self.h.handle_request(u'pause')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PAUSED, self.b.playback.state)
+ self.assertEqual(PAUSED, self.b.playback.state.get())
result = self.h.handle_request(u'pause')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
def test_play_without_pos(self):
self.b.current_playlist.append([Track()])
- self.b.playback.state = self.b.playback.PAUSED
+ self.b.playback.state = PAUSED
result = self.h.handle_request(u'play')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
def test_play_with_pos(self):
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'play "0"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
def test_play_with_pos_without_quotes(self):
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'play 0')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
def test_play_with_pos_out_of_bounds(self):
self.b.current_playlist.append([])
result = self.h.handle_request(u'play "0"')
self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index')
- self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
+ self.assertEqual(STOPPED, self.b.playback.state.get())
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
- self.assertEqual(self.b.playback.current_track, None)
+ self.assertEqual(self.b.playback.current_track.get(), None)
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
result = self.h.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
- self.assertEqual(self.b.playback.current_track.uri, 'a')
+ self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(self.b.playback.current_track.get().uri, 'a')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(self.b.playback.current_track, None)
+ self.assertEqual(self.b.playback.current_track.get(), None)
self.b.playback.play()
self.b.playback.next()
self.b.playback.stop()
- self.assertNotEqual(self.b.playback.current_track, None)
+ self.assertNotEqual(self.b.playback.current_track.get(), None)
result = self.h.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
- self.assertEqual(self.b.playback.current_track.uri, 'b')
+ self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(self.b.playback.current_track.get().uri, 'b')
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
self.b.current_playlist.clear()
result = self.h.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
- self.assertEqual(self.b.playback.current_track, None)
+ self.assertEqual(STOPPED, self.b.playback.state.get())
+ self.assertEqual(self.b.playback.current_track.get(), None)
+
+ def test_play_minus_is_ignored_if_playing(self):
+ self.b.current_playlist.append([Track(length=40000)])
+ self.b.playback.seek(30000)
+ self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assertEquals(PLAYING, self.b.playback.state.get())
+ result = self.h.handle_request(u'play "-1"')
+ self.assert_(u'OK' in result)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assert_(self.b.playback.time_position.get() >= 30000)
+
+ def test_play_minus_one_resumes_if_paused(self):
+ self.b.current_playlist.append([Track(length=40000)])
+ self.b.playback.seek(30000)
+ self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assertEquals(PLAYING, self.b.playback.state.get())
+ self.b.playback.pause()
+ self.assertEquals(PAUSED, self.b.playback.state.get())
+ result = self.h.handle_request(u'play "-1"')
+ self.assert_(u'OK' in result)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assert_(self.b.playback.time_position.get() >= 30000)
def test_playid(self):
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'playid "0"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
- self.assertEqual(self.b.playback.current_track, None)
+ self.assertEqual(self.b.playback.current_track.get(), None)
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
- self.assertEqual(self.b.playback.current_track.uri, 'a')
+ self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(self.b.playback.current_track.get().uri, 'a')
- def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
+ def test_playid_minus_one_plays_current_track_if_current_track_is_set(self):
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(self.b.playback.current_track, None)
+ self.assertEqual(self.b.playback.current_track.get(), None)
self.b.playback.play()
self.b.playback.next()
self.b.playback.stop()
- self.assertNotEqual(self.b.playback.current_track, None)
+ self.assertNotEqual(self.b.playback.current_track.get(), None)
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
- self.assertEqual(self.b.playback.current_track.uri, 'b')
+ self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assertEqual(self.b.playback.current_track.get().uri, 'b')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
self.b.current_playlist.clear()
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
- self.assertEqual(self.b.playback.current_track, None)
+ self.assertEqual(STOPPED, self.b.playback.state.get())
+ self.assertEqual(self.b.playback.current_track.get(), None)
+
+ def test_playid_minus_is_ignored_if_playing(self):
+ self.b.current_playlist.append([Track(length=40000)])
+ self.b.playback.seek(30000)
+ self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assertEquals(PLAYING, self.b.playback.state.get())
+ result = self.h.handle_request(u'playid "-1"')
+ self.assert_(u'OK' in result)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assert_(self.b.playback.time_position.get() >= 30000)
def test_playid_minus_one_resumes_if_paused(self):
self.b.current_playlist.append([Track(length=40000)])
self.b.playback.seek(30000)
- self.assert_(self.b.playback.time_position >= 30000)
- self.assertEquals(self.b.playback.PLAYING, self.b.playback.state)
+ self.assert_(self.b.playback.time_position.get() >= 30000)
+ self.assertEquals(PLAYING, self.b.playback.state.get())
self.b.playback.pause()
- self.assertEquals(self.b.playback.PAUSED, self.b.playback.state)
+ self.assertEquals(PAUSED, self.b.playback.state.get())
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
- self.assert_(self.b.playback.time_position >= 30000)
+ self.assertEqual(PLAYING, self.b.playback.state.get())
+ self.assert_(self.b.playback.time_position.get() >= 30000)
def test_playid_which_does_not_exist(self):
self.b.current_playlist.append([Track()])
@@ -322,30 +375,32 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.b.current_playlist.append(
[Track(uri='1', length=40000), seek_track])
result = self.h.handle_request(u'seek "1" "30"')
- self.assertEqual(self.b.playback.current_track, seek_track)
+ self.assert_(u'OK' in result)
+ self.assertEqual(self.b.playback.current_track.get(), seek_track)
def test_seek_without_quotes(self):
self.b.current_playlist.append([Track(length=40000)])
self.h.handle_request(u'seek 0')
result = self.h.handle_request(u'seek 0 30')
self.assert_(u'OK' in result)
- self.assert_(self.b.playback.time_position >= 30000)
+ self.assert_(self.b.playback.time_position.get() >= 30000)
def test_seekid(self):
self.b.current_playlist.append([Track(length=40000)])
result = self.h.handle_request(u'seekid "0" "30"')
self.assert_(u'OK' in result)
- self.assert_(self.b.playback.time_position >= 30000)
+ self.assert_(self.b.playback.time_position.get() >= 30000)
def test_seekid_with_cpid(self):
seek_track = Track(uri='2', length=40000)
self.b.current_playlist.append(
[Track(length=40000), seek_track])
result = self.h.handle_request(u'seekid "1" "30"')
- self.assertEqual(self.b.playback.current_cpid, 1)
- self.assertEqual(self.b.playback.current_track, seek_track)
+ self.assert_(u'OK' in result)
+ self.assertEqual(self.b.playback.current_cpid.get(), 1)
+ self.assertEqual(self.b.playback.current_track.get(), seek_track)
def test_stop(self):
result = self.h.handle_request(u'stop')
self.assert_(u'OK' in result)
- self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
+ self.assertEqual(STOPPED, self.b.playback.state.get())
diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py
index 0f096930..be95c49b 100644
--- a/tests/frontends/mpd/reflection_test.py
+++ b/tests/frontends/mpd/reflection_test.py
@@ -2,11 +2,17 @@ import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
class ReflectionHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_commands_returns_list_of_all_commands(self):
result = self.h.handle_request(u'commands')
diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py
index 79ca88c9..f786cf0a 100644
--- a/tests/frontends/mpd/regression_test.py
+++ b/tests/frontends/mpd/regression_test.py
@@ -3,6 +3,7 @@ import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class IssueGH17RegressionTest(unittest.TestCase):
@@ -17,26 +18,31 @@ class IssueGH17RegressionTest(unittest.TestCase):
"""
def setUp(self):
- self.backend = DummyBackend()
+ self.backend = DummyBackend.start().proxy()
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), None,
Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
+ self.mixer = DummyMixer.start().proxy()
+ self.mpd = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.backend.stop().get()
+ self.mixer.stop().get()
def test(self):
random.seed(1) # Playlist order: abcfde
self.mpd.handle_request(u'play')
- self.assertEquals('a', self.backend.playback.current_track.uri)
+ self.assertEquals('a', self.backend.playback.current_track.get().uri)
self.mpd.handle_request(u'random "1"')
self.mpd.handle_request(u'next')
- self.assertEquals('b', self.backend.playback.current_track.uri)
+ self.assertEquals('b', self.backend.playback.current_track.get().uri)
self.mpd.handle_request(u'next')
# Should now be at track 'c', but playback fails and it skips ahead
- self.assertEquals('f', self.backend.playback.current_track.uri)
+ self.assertEquals('f', self.backend.playback.current_track.get().uri)
self.mpd.handle_request(u'next')
- self.assertEquals('d', self.backend.playback.current_track.uri)
+ self.assertEquals('d', self.backend.playback.current_track.get().uri)
self.mpd.handle_request(u'next')
- self.assertEquals('e', self.backend.playback.current_track.uri)
+ self.assertEquals('e', self.backend.playback.current_track.get().uri)
class IssueGH18RegressionTest(unittest.TestCase):
@@ -51,11 +57,16 @@ class IssueGH18RegressionTest(unittest.TestCase):
"""
def setUp(self):
- self.backend = DummyBackend()
+ self.backend = DummyBackend.start().proxy()
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
+ self.mixer = DummyMixer.start().proxy()
+ self.mpd = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.backend.stop().get()
+ self.mixer.stop().get()
def test(self):
random.seed(1)
@@ -66,11 +77,11 @@ class IssueGH18RegressionTest(unittest.TestCase):
self.mpd.handle_request(u'next')
self.mpd.handle_request(u'next')
- cp_track_1 = self.backend.playback.current_cp_track
+ cp_track_1 = self.backend.playback.current_cp_track.get()
self.mpd.handle_request(u'next')
- cp_track_2 = self.backend.playback.current_cp_track
+ cp_track_2 = self.backend.playback.current_cp_track.get()
self.mpd.handle_request(u'next')
- cp_track_3 = self.backend.playback.current_cp_track
+ cp_track_3 = self.backend.playback.current_cp_track.get()
self.assertNotEqual(cp_track_1, cp_track_2)
self.assertNotEqual(cp_track_2, cp_track_3)
@@ -90,11 +101,16 @@ class IssueGH22RegressionTest(unittest.TestCase):
"""
def setUp(self):
- self.backend = DummyBackend()
+ self.backend = DummyBackend.start().proxy()
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
- self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
+ self.mixer = DummyMixer.start().proxy()
+ self.mpd = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.backend.stop().get()
+ self.mixer.stop().get()
def test(self):
random.seed(1)
@@ -107,3 +123,36 @@ class IssueGH22RegressionTest(unittest.TestCase):
self.mpd.handle_request(u'deleteid "5"')
self.mpd.handle_request(u'deleteid "6"')
self.mpd.handle_request(u'status')
+
+
+class IssueGH69RegressionTest(unittest.TestCase):
+ """
+ The issue: https://github.com/mopidy/mopidy/issues#issue/69
+
+ How to reproduce:
+
+ Play track, stop, clear current playlist, load a new playlist, status.
+
+ The status response now contains "song: None".
+ """
+
+ def setUp(self):
+ self.backend = DummyBackend.start().proxy()
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ self.backend.stored_playlists.create('foo')
+ self.mixer = DummyMixer.start().proxy()
+ self.mpd = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.backend.stop().get()
+ self.mixer.stop().get()
+
+ def test(self):
+ self.mpd.handle_request(u'play')
+ self.mpd.handle_request(u'stop')
+ self.mpd.handle_request(u'clear')
+ self.mpd.handle_request(u'load "foo"')
+ response = self.mpd.handle_request(u'status')
+ self.assert_('song: None' not in response)
diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py
index 7e4500ea..b0c57588 100644
--- a/tests/frontends/mpd/serializer_test.py
+++ b/tests/frontends/mpd/serializer_test.py
@@ -7,8 +7,6 @@ from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol
from mopidy.models import Album, Artist, Playlist, Track
-from tests import data_folder, SkipTest
-
class TrackMpdFormatTest(unittest.TestCase):
track = Track(
uri=u'a uri',
@@ -54,7 +52,8 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Id', 2) in result)
def test_track_to_mpd_format_for_nonempty_track(self):
- result = translator.track_to_mpd_format(self.track, position=9, cpid=122)
+ result = translator.track_to_mpd_format(
+ self.track, position=9, cpid=122)
self.assert_(('file', 'a uri') in result)
self.assert_(('Time', 137) in result)
self.assert_(('Artist', 'an artist') in result)
@@ -96,6 +95,11 @@ class TrackMpdFormatTest(unittest.TestCase):
translated = translator.artists_to_mpd_format(artists)
self.assertEqual(translated, u'ABBA, Beatles')
+ def test_artists_to_mpd_format_artist_with_no_name(self):
+ artists = [Artist(name=None)]
+ translated = translator.artists_to_mpd_format(artists)
+ self.assertEqual(translated, u'')
+
class PlaylistMpdFormatTest(unittest.TestCase):
def test_mpd_format(self):
@@ -219,7 +223,6 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
def test_tag_cache_diretory_header_is_right(self):
track = Track(uri='file:///dir/subdir/folder/sub/song.mp3')
- formated = self.translate(track)
result = translator.tracks_to_tag_cache_format([track])
result = self.consume_headers(result)
diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py
index 9d006eb3..ef963347 100644
--- a/tests/frontends/mpd/server_test.py
+++ b/tests/frontends/mpd/server_test.py
@@ -1,10 +1,19 @@
import unittest
+from mopidy import settings
+from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import server
+from mopidy.mixers.dummy import DummyMixer
class MpdServerTest(unittest.TestCase):
def setUp(self):
- self.server = server.MpdServer(None)
+ self.backend = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.server = server.MpdServer()
+
+ def tearDown(self):
+ self.backend.stop().get()
+ self.mixer.stop().get()
def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self):
server.socket.has_ipv6 = True
@@ -19,10 +28,66 @@ class MpdServerTest(unittest.TestCase):
class MpdSessionTest(unittest.TestCase):
def setUp(self):
- self.session = server.MpdSession(None, None, (None, None), None)
+ self.backend = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.session = server.MpdSession(None, None, (None, None))
+
+ def tearDown(self):
+ self.backend.stop().get()
+ self.mixer.stop().get()
+ settings.runtime.clear()
def test_found_terminator_catches_decode_error(self):
# Pressing Ctrl+C in a telnet session sends a 0xff byte to the server.
self.session.input_buffer = ['\xff']
self.session.found_terminator()
self.assertEqual(len(self.session.input_buffer), 0)
+
+ def test_authentication_with_valid_password_is_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ authed, response = self.session.check_password(u'password "topsecret"')
+ self.assertTrue(authed)
+ self.assertEqual(u'OK', response)
+
+ def test_authentication_with_invalid_password_is_not_accepted(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ authed, response = self.session.check_password(u'password "secret"')
+ self.assertFalse(authed)
+ self.assertEqual(u'ACK [3@0] {password} incorrect password', response)
+
+ def test_authentication_with_anything_when_password_check_turned_off(self):
+ settings.MPD_SERVER_PASSWORD = None
+ authed, response = self.session.check_password(u'any request at all')
+ self.assertTrue(authed)
+ self.assertEqual(None, response)
+
+ def test_anything_when_not_authenticated_should_fail(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ authed, response = self.session.check_password(u'any request at all')
+ self.assertFalse(authed)
+ self.assertEqual(
+ u'ACK [4@0] {any} you don\'t have permission for "any"', response)
+
+ def test_close_is_allowed_without_authentication(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ authed, response = self.session.check_password(u'close')
+ self.assertFalse(authed)
+ self.assertEqual(None, response)
+
+ def test_commands_is_allowed_without_authentication(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ authed, response = self.session.check_password(u'commands')
+ self.assertFalse(authed)
+ self.assertEqual(None, response)
+
+ def test_notcommands_is_allowed_without_authentication(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ authed, response = self.session.check_password(u'notcommands')
+ self.assertFalse(authed)
+ self.assertEqual(None, response)
+
+ def test_ping_is_allowed_without_authentication(self):
+ settings.MPD_SERVER_PASSWORD = u'topsecret'
+ authed, response = self.session.check_password(u'ping')
+ self.assertFalse(authed)
+ self.assertEqual(None, response)
diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py
index 14fef262..791d734f 100644
--- a/tests/frontends/mpd/status_test.py
+++ b/tests/frontends/mpd/status_test.py
@@ -1,13 +1,24 @@
import unittest
+from mopidy.backends.base import PlaybackController
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
+PAUSED = PlaybackController.PAUSED
+PLAYING = PlaybackController.PLAYING
+STOPPED = PlaybackController.STOPPED
+
class StatusHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_clearerror(self):
result = self.h.handle_request(u'clearerror')
@@ -76,7 +87,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['volume']), 0)
def test_status_method_contains_volume(self):
- self.b.mixer.volume = 17
+ self.mixer.volume = 17
result = dict(dispatcher.status.status(self.h))
self.assert_('volume' in result)
self.assertEqual(int(result['volume']), 17)
@@ -135,20 +146,20 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(int(result['xfade']) >= 0)
def test_status_method_contains_state_is_play(self):
- self.b.playback.state = self.b.playback.PLAYING
+ self.b.playback.state = PLAYING
result = dict(dispatcher.status.status(self.h))
self.assert_('state' in result)
self.assertEqual(result['state'], 'play')
def test_status_method_contains_state_is_stop(self):
- self.b.playback.state = self.b.playback.STOPPED
+ self.b.playback.state = STOPPED
result = dict(dispatcher.status.status(self.h))
self.assert_('state' in result)
self.assertEqual(result['state'], 'stop')
def test_status_method_contains_state_is_pause(self):
- self.b.playback.state = self.b.playback.PLAYING
- self.b.playback.state = self.b.playback.PAUSED
+ self.b.playback.state = PLAYING
+ self.b.playback.state = PAUSED
result = dict(dispatcher.status.status(self.h))
self.assert_('state' in result)
self.assertEqual(result['state'], 'pause')
@@ -188,8 +199,8 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(position <= total)
def test_status_method_when_playing_contains_elapsed(self):
- self.b.playback.state = self.b.playback.PAUSED
- self.b.playback._play_time_accumulated = 59123
+ self.b.playback.state = PAUSED
+ self.b.playback.play_time_accumulated = 59123
result = dict(dispatcher.status.status(self.h))
self.assert_('elapsed' in result)
self.assertEqual(int(result['elapsed']), 59123)
diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py
index e5aed398..83d43792 100644
--- a/tests/frontends/mpd/stickers_test.py
+++ b/tests/frontends/mpd/stickers_test.py
@@ -2,11 +2,17 @@ import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
class StickersHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_sticker_get(self):
result = self.h.handle_request(
diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py
index f0b37b1a..e981c9ed 100644
--- a/tests/frontends/mpd/stored_playlists_test.py
+++ b/tests/frontends/mpd/stored_playlists_test.py
@@ -3,12 +3,18 @@ import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
+from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track, Playlist
class StoredPlaylistsHandlerTest(unittest.TestCase):
def setUp(self):
- self.b = DummyBackend()
- self.h = dispatcher.MpdDispatcher(backend=self.b)
+ self.b = DummyBackend.start().proxy()
+ self.mixer = DummyMixer.start().proxy()
+ self.h = dispatcher.MpdDispatcher()
+
+ def tearDown(self):
+ self.b.stop().get()
+ self.mixer.stop().get()
def test_listplaylist(self):
self.b.stored_playlists.playlists = [
@@ -48,22 +54,23 @@ class StoredPlaylistsHandlerTest(unittest.TestCase):
def test_load_known_playlist_appends_to_current_playlist(self):
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(len(self.b.current_playlist.tracks), 2)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
self.b.stored_playlists.playlists = [Playlist(name='A-list',
tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
result = self.h.handle_request(u'load "A-list"')
self.assert_(u'OK' in result)
- self.assertEqual(len(self.b.current_playlist.tracks), 5)
- self.assertEqual(self.b.current_playlist.tracks[0].uri, 'a')
- self.assertEqual(self.b.current_playlist.tracks[1].uri, 'b')
- self.assertEqual(self.b.current_playlist.tracks[2].uri, 'c')
- self.assertEqual(self.b.current_playlist.tracks[3].uri, 'd')
- self.assertEqual(self.b.current_playlist.tracks[4].uri, 'e')
+ tracks = self.b.current_playlist.tracks.get()
+ self.assertEqual(len(tracks), 5)
+ self.assertEqual(tracks[0].uri, 'a')
+ self.assertEqual(tracks[1].uri, 'b')
+ self.assertEqual(tracks[2].uri, 'c')
+ self.assertEqual(tracks[3].uri, 'd')
+ self.assertEqual(tracks[4].uri, 'e')
def test_load_unknown_playlist_acks(self):
result = self.h.handle_request(u'load "unknown playlist"')
self.assert_(u'ACK [50@0] {load} No such playlist' in result)
- self.assertEqual(len(self.b.current_playlist.tracks), 0)
+ self.assertEqual(len(self.b.current_playlist.tracks.get()), 0)
def test_playlistadd(self):
result = self.h.handle_request(
diff --git a/tests/mixers/base_test.py b/tests/mixers/base_test.py
index d6129ad5..54cd8773 100644
--- a/tests/mixers/base_test.py
+++ b/tests/mixers/base_test.py
@@ -10,7 +10,9 @@ class BaseMixerTest(object):
def setUp(self):
assert self.mixer_class is not None, \
"mixer_class must be set in subclass"
- self.mixer = self.mixer_class(None)
+ # pylint: disable = E1102
+ self.mixer = self.mixer_class()
+ # pylint: enable = E1102
def test_initial_volume(self):
self.assertEqual(self.mixer.volume, self.INITIAL)
diff --git a/tests/models_test.py b/tests/models_test.py
index 0b44f337..afbf9d50 100644
--- a/tests/models_test.py
+++ b/tests/models_test.py
@@ -39,11 +39,11 @@ class GenericCopyTets(unittest.TestCase):
self.assertEqual('bar', copy.uri)
def test_copying_track_with_private_internal_value(self):
- artists1 = [Artist(name='foo')]
- artists2 = [Artist(name='bar')]
- track = Track(artists=artists1)
- copy = track.copy(artists=artists2)
- self.assertEqual(copy.artists, artists2)
+ artist1 = Artist(name='foo')
+ artist2 = Artist(name='bar')
+ track = Track(artists=[artist1])
+ copy = track.copy(artists=[artist2])
+ self.assert_(artist2 in copy.artists)
def test_copying_track_with_invalid_key(self):
test = lambda: Track().copy(invalid_key=True)
@@ -73,6 +73,11 @@ class ArtistTest(unittest.TestCase):
test = lambda: Artist(foo='baz')
self.assertRaises(TypeError, test)
+ def test_repr(self):
+ self.assertEquals(
+ "Artist(name='name', uri='uri')",
+ repr(Artist(uri='uri', name='name')))
+
def test_eq_name(self):
artist1 = Artist(name=u'name')
artist2 = Artist(name=u'name')
@@ -142,9 +147,9 @@ class AlbumTest(unittest.TestCase):
self.assertRaises(AttributeError, setattr, album, 'name', None)
def test_artists(self):
- artists = [Artist()]
- album = Album(artists=artists)
- self.assertEqual(album.artists, artists)
+ artist = Artist()
+ album = Album(artists=[artist])
+ self.assert_(artist in album.artists)
self.assertRaises(AttributeError, setattr, album, 'artists', None)
def test_num_tracks(self):
@@ -164,6 +169,16 @@ class AlbumTest(unittest.TestCase):
test = lambda: Album(foo='baz')
self.assertRaises(TypeError, test)
+ def test_repr_without_artists(self):
+ self.assertEquals(
+ "Album(artists=[], name='name', uri='uri')",
+ repr(Album(uri='uri', name='name')))
+
+ def test_repr_with_artists(self):
+ self.assertEquals(
+ "Album(artists=[Artist(name='foo')], name='name', uri='uri')",
+ repr(Album(uri='uri', name='name', artists=[Artist(name='foo')])))
+
def test_eq_name(self):
album1 = Album(name=u'name')
album2 = Album(name=u'name')
@@ -205,8 +220,10 @@ class AlbumTest(unittest.TestCase):
def test_eq(self):
artists = [Artist()]
- album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id')
- album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id')
+ album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2,
+ musicbrainz_id='id')
+ album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2,
+ musicbrainz_id='id')
self.assertEqual(album1, album2)
self.assertEqual(hash(album1), hash(album2))
@@ -317,6 +334,16 @@ class TrackTest(unittest.TestCase):
test = lambda: Track(foo='baz')
self.assertRaises(TypeError, test)
+ def test_repr_without_artists(self):
+ self.assertEquals(
+ "Track(artists=[], name='name', uri='uri')",
+ repr(Track(uri='uri', name='name')))
+
+ def test_repr_with_artists(self):
+ self.assertEquals(
+ "Track(artists=[Artist(name='foo')], name='name', uri='uri')",
+ repr(Track(uri='uri', name='name', artists=[Artist(name='foo')])))
+
def test_eq_uri(self):
track1 = Track(uri=u'uri1')
track2 = Track(uri=u'uri1')
@@ -484,7 +511,7 @@ class PlaylistTest(unittest.TestCase):
def test_tracks(self):
tracks = [Track(), Track(), Track()]
playlist = Playlist(tracks=tracks)
- self.assertEqual(playlist.tracks, tracks)
+ self.assertEqual(list(playlist.tracks), tracks)
self.assertRaises(AttributeError, setattr, playlist, 'tracks', None)
def test_length(self):
@@ -507,7 +534,7 @@ class PlaylistTest(unittest.TestCase):
new_playlist = playlist.copy(uri=u'another uri')
self.assertEqual(new_playlist.uri, u'another uri')
self.assertEqual(new_playlist.name, u'a name')
- self.assertEqual(new_playlist.tracks, tracks)
+ self.assertEqual(list(new_playlist.tracks), tracks)
self.assertEqual(new_playlist.last_modified, last_modified)
def test_with_new_name(self):
@@ -518,7 +545,7 @@ class PlaylistTest(unittest.TestCase):
new_playlist = playlist.copy(name=u'another name')
self.assertEqual(new_playlist.uri, u'an uri')
self.assertEqual(new_playlist.name, u'another name')
- self.assertEqual(new_playlist.tracks, tracks)
+ self.assertEqual(list(new_playlist.tracks), tracks)
self.assertEqual(new_playlist.last_modified, last_modified)
def test_with_new_tracks(self):
@@ -530,7 +557,7 @@ class PlaylistTest(unittest.TestCase):
new_playlist = playlist.copy(tracks=new_tracks)
self.assertEqual(new_playlist.uri, u'an uri')
self.assertEqual(new_playlist.name, u'a name')
- self.assertEqual(new_playlist.tracks, new_tracks)
+ self.assertEqual(list(new_playlist.tracks), new_tracks)
self.assertEqual(new_playlist.last_modified, last_modified)
def test_with_new_last_modified(self):
@@ -542,13 +569,93 @@ class PlaylistTest(unittest.TestCase):
new_playlist = playlist.copy(last_modified=new_last_modified)
self.assertEqual(new_playlist.uri, u'an uri')
self.assertEqual(new_playlist.name, u'a name')
- self.assertEqual(new_playlist.tracks, tracks)
+ self.assertEqual(list(new_playlist.tracks), tracks)
self.assertEqual(new_playlist.last_modified, new_last_modified)
def test_invalid_kwarg(self):
test = lambda: Playlist(foo='baz')
self.assertRaises(TypeError, test)
+ def test_repr_without_tracks(self):
+ self.assertEquals(
+ "Playlist(name='name', tracks=[], uri='uri')",
+ repr(Playlist(uri='uri', name='name')))
+
+ def test_repr_with_tracks(self):
+ self.assertEquals(
+ "Playlist(name='name', tracks=[Track(artists=[], name='foo')], "
+ "uri='uri')",
+ repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')])))
+
+ def test_eq_name(self):
+ playlist1 = Playlist(name=u'name')
+ playlist2 = Playlist(name=u'name')
+ self.assertEqual(playlist1, playlist2)
+ self.assertEqual(hash(playlist1), hash(playlist2))
+
+ def test_eq_uri(self):
+ playlist1 = Playlist(uri=u'uri')
+ playlist2 = Playlist(uri=u'uri')
+ self.assertEqual(playlist1, playlist2)
+ self.assertEqual(hash(playlist1), hash(playlist2))
+
+ def test_eq_tracks(self):
+ tracks = [Track()]
+ playlist1 = Playlist(tracks=tracks)
+ playlist2 = Playlist(tracks=tracks)
+ self.assertEqual(playlist1, playlist2)
+ self.assertEqual(hash(playlist1), hash(playlist2))
+
+ def test_eq_uri(self):
+ playlist1 = Playlist(last_modified=1)
+ playlist2 = Playlist(last_modified=1)
+ self.assertEqual(playlist1, playlist2)
+ self.assertEqual(hash(playlist1), hash(playlist2))
+
def test_eq(self):
- # FIXME missing all equal and hash tests
- raise SkipTest
+ tracks = [Track()]
+ playlist1 = Playlist(uri=u'uri', name=u'name', tracks=tracks,
+ last_modified=1)
+ playlist2 = Playlist(uri=u'uri', name=u'name', tracks=tracks,
+ last_modified=1)
+ self.assertEqual(playlist1, playlist2)
+ self.assertEqual(hash(playlist1), hash(playlist2))
+
+ def test_eq_none(self):
+ self.assertNotEqual(Playlist(), None)
+
+ def test_eq_other(self):
+ self.assertNotEqual(Playlist(), 'other')
+
+ def test_ne_name(self):
+ playlist1 = Playlist(name=u'name1')
+ playlist2 = Playlist(name=u'name2')
+ self.assertNotEqual(playlist1, playlist2)
+ self.assertNotEqual(hash(playlist1), hash(playlist2))
+
+ def test_ne_uri(self):
+ playlist1 = Playlist(uri=u'uri1')
+ playlist2 = Playlist(uri=u'uri2')
+ self.assertNotEqual(playlist1, playlist2)
+ self.assertNotEqual(hash(playlist1), hash(playlist2))
+
+ def test_ne_tracks(self):
+ playlist1 = Playlist(tracks=[Track(uri=u'uri1')])
+ playlist2 = Playlist(tracks=[Track(uri=u'uri2')])
+ self.assertNotEqual(playlist1, playlist2)
+ self.assertNotEqual(hash(playlist1), hash(playlist2))
+
+ def test_ne_uri(self):
+ playlist1 = Playlist(last_modified=1)
+ playlist2 = Playlist(last_modified=2)
+ self.assertNotEqual(playlist1, playlist2)
+ self.assertNotEqual(hash(playlist1), hash(playlist2))
+
+ def test_ne(self):
+ playlist1 = Playlist(uri=u'uri1', name=u'name2',
+ tracks=[Track(uri=u'uri1')], last_modified=1)
+ playlist2 = Playlist(uri=u'uri2', name=u'name2',
+ tracks=[Track(uri=u'uri2')], last_modified=2)
+ self.assertNotEqual(playlist1, playlist2)
+ self.assertNotEqual(hash(playlist1), hash(playlist2))
+
diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py
index 3a578280..31a16756 100644
--- a/tests/outputs/gstreamer_test.py
+++ b/tests/outputs/gstreamer_test.py
@@ -11,20 +11,17 @@ if sys.platform == 'win32':
from mopidy import settings
from mopidy.outputs.gstreamer import GStreamerOutput
from mopidy.utils.path import path_to_uri
-from mopidy.utils.process import pickle_connection
-from tests import data_folder
+from tests import path_to_data_dir
class GStreamerOutputTest(unittest.TestCase):
def setUp(self):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
- self.song_uri = path_to_uri(data_folder('song1.wav'))
- self.core_queue = multiprocessing.Queue()
- self.output = GStreamerOutput(self.core_queue)
- self.output.start()
+ self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
+ self.output = GStreamerOutput()
+ self.output.on_start()
def tearDown(self):
- self.output.destroy()
settings.runtime.clear()
def test_play_uri_existing_file(self):
diff --git a/tests/scanner_test.py b/tests/scanner_test.py
index a1b53bcf..b98c5aa9 100644
--- a/tests/scanner_test.py
+++ b/tests/scanner_test.py
@@ -4,7 +4,7 @@ from datetime import date
from mopidy.scanner import Scanner, translator
from mopidy.models import Track, Artist, Album
-from tests import data_folder
+from tests import path_to_data_dir
class FakeGstDate(object):
def __init__(self, year, month, day):
@@ -132,12 +132,12 @@ class ScannerTest(unittest.TestCase):
self.data = {}
def scan(self, path):
- scanner = Scanner(data_folder(path),
+ scanner = Scanner(path_to_data_dir(path),
self.data_callback, self.error_callback)
scanner.start()
def check(self, name, key, value):
- name = data_folder(name)
+ name = path_to_data_dir(name)
self.assertEqual(self.data[name][key], value)
def data_callback(self, data):
@@ -159,7 +159,7 @@ class ScannerTest(unittest.TestCase):
def test_uri_is_set(self):
self.scan('scanner/simple')
self.check('scanner/simple/song1.mp3', 'uri', 'file://'
- + data_folder('scanner/simple/song1.mp3'))
+ + path_to_data_dir('scanner/simple/song1.mp3'))
def test_duration_is_set(self):
self.scan('scanner/simple')
diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py
index 4366305c..088a7049 100644
--- a/tests/utils/path_test.py
+++ b/tests/utils/path_test.py
@@ -9,7 +9,7 @@ import unittest
from mopidy.utils.path import (get_or_create_folder, mtime,
path_to_uri, uri_to_path, split_path, find_files)
-from tests import SkipTest, data_folder
+from tests import path_to_data_dir
class GetOrCreateFolderTest(unittest.TestCase):
def setUp(self):
@@ -117,7 +117,7 @@ class SplitPathTest(unittest.TestCase):
class FindFilesTest(unittest.TestCase):
def find(self, path):
- return list(find_files(data_folder(path)))
+ return list(find_files(path_to_data_dir(path)))
def test_basic_folder(self):
self.assert_(self.find(''))
@@ -128,7 +128,7 @@ class FindFilesTest(unittest.TestCase):
def test_file(self):
files = self.find('blank.mp3')
self.assertEqual(len(files), 1)
- self.assert_(files[0], data_folder('blank.mp3'))
+ self.assert_(files[0], path_to_data_dir('blank.mp3'))
def test_names_are_unicode(self):
is_unicode = lambda f: isinstance(f, unicode)
diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py
index cef0069d..11914f61 100644
--- a/tests/utils/settings_test.py
+++ b/tests/utils/settings_test.py
@@ -1,8 +1,9 @@
import os
import unittest
-from mopidy import settings as default_settings_module
+from mopidy import settings as default_settings_module, SettingsError
from mopidy.utils.settings import validate_settings, SettingsProxy
+from mopidy.utils.settings import mask_value_if_secret
class ValidateSettingsTest(unittest.TestCase):
def setUp(self):
@@ -44,7 +45,19 @@ class ValidateSettingsTest(unittest.TestCase):
def test_two_errors_are_both_reported(self):
result = validate_settings(self.defaults,
{'FOO': '', 'BAR': ''})
- self.assertEquals(len(result), 2)
+ self.assertEqual(len(result), 2)
+
+ def test_masks_value_if_secret(self):
+ secret = mask_value_if_secret('SPOTIFY_PASSWORD', 'bar')
+ self.assertEqual(u'********', secret)
+
+ def test_does_not_mask_value_if_not_secret(self):
+ not_secret = mask_value_if_secret('SPOTIFY_USERNAME', 'foo')
+ self.assertEqual('foo', not_secret)
+
+ def test_does_not_mask_value_if_none(self):
+ not_secret = mask_value_if_secret('SPOTIFY_USERNAME', None)
+ self.assertEqual(None, not_secret)
class SettingsProxyTest(unittest.TestCase):
@@ -55,6 +68,33 @@ class SettingsProxyTest(unittest.TestCase):
self.settings.TEST = 'test'
self.assertEqual(self.settings.TEST, 'test')
+ def test_getattr_raises_error_on_missing_setting(self):
+ try:
+ _ = self.settings.TEST
+ self.fail(u'Should raise exception')
+ except SettingsError as e:
+ self.assertEqual(u'Setting "TEST" is not set.', e.message)
+
+ def test_getattr_raises_error_on_empty_setting(self):
+ self.settings.TEST = u''
+ try:
+ _ = self.settings.TEST
+ self.fail(u'Should raise exception')
+ except SettingsError as e:
+ self.assertEqual(u'Setting "TEST" is empty.', e.message)
+
+ def test_getattr_does_not_raise_error_if_setting_is_false(self):
+ self.settings.TEST = False
+ self.assertEqual(False, self.settings.TEST)
+
+ def test_getattr_does_not_raise_error_if_setting_is_none(self):
+ self.settings.TEST = None
+ self.assertEqual(None, self.settings.TEST)
+
+ def test_getattr_does_not_raise_error_if_setting_is_zero(self):
+ self.settings.TEST = 0
+ self.assertEqual(0, self.settings.TEST)
+
def test_setattr_updates_runtime_settings(self):
self.settings.TEST = 'test'
self.assert_('TEST' in self.settings.runtime)
@@ -69,34 +109,34 @@ class SettingsProxyTest(unittest.TestCase):
def test_value_ending_in_path_is_expanded(self):
self.settings.TEST_PATH = '~/test'
- acctual = self.settings.TEST_PATH
+ actual = self.settings.TEST_PATH
expected = os.path.expanduser('~/test')
- self.assertEqual(acctual, expected)
+ self.assertEqual(actual, expected)
def test_value_ending_in_path_is_absolute(self):
self.settings.TEST_PATH = './test'
- acctual = self.settings.TEST_PATH
+ actual = self.settings.TEST_PATH
expected = os.path.abspath('./test')
- self.assertEqual(acctual, expected)
+ self.assertEqual(actual, expected)
def test_value_ending_in_file_is_expanded(self):
self.settings.TEST_FILE = '~/test'
- acctual = self.settings.TEST_FILE
+ actual = self.settings.TEST_FILE
expected = os.path.expanduser('~/test')
- self.assertEqual(acctual, expected)
+ self.assertEqual(actual, expected)
def test_value_ending_in_file_is_absolute(self):
self.settings.TEST_FILE = './test'
- acctual = self.settings.TEST_FILE
+ actual = self.settings.TEST_FILE
expected = os.path.abspath('./test')
- self.assertEqual(acctual, expected)
+ self.assertEqual(actual, expected)
def test_value_not_ending_in_path_or_file_is_not_expanded(self):
self.settings.TEST = '~/test'
- acctual = self.settings.TEST
- self.assertEqual(acctual, '~/test')
+ actual = self.settings.TEST
+ self.assertEqual(actual, '~/test')
def test_value_not_ending_in_path_or_file_is_not_absolute(self):
self.settings.TEST = './test'
- acctual = self.settings.TEST
- self.assertEqual(acctual, './test')
+ actual = self.settings.TEST
+ self.assertEqual(actual, './test')
diff --git a/tests/version_test.py b/tests/version_test.py
index a8bc2955..f1f86b59 100644
--- a/tests/version_test.py
+++ b/tests/version_test.py
@@ -1,11 +1,11 @@
from distutils.version import StrictVersion as SV
import unittest
-from mopidy import get_version
+from mopidy import get_plain_version
class VersionTest(unittest.TestCase):
def test_current_version_is_parsable_as_a_strict_version_number(self):
- SV(get_version())
+ SV(get_plain_version())
def test_versions_can_be_strictly_ordered(self):
self.assert_(SV('0.1.0a0') < SV('0.1.0a1'))
@@ -14,5 +14,7 @@ class VersionTest(unittest.TestCase):
self.assert_(SV('0.1.0a3') < SV('0.1.0'))
self.assert_(SV('0.1.0') < SV('0.2.0'))
self.assert_(SV('0.1.0') < SV('1.0.0'))
- self.assert_(SV('0.2.0') < SV(get_version()))
- self.assert_(SV(get_version()) < SV('0.3.1'))
+ self.assert_(SV('0.2.0') < SV('0.3.0'))
+ self.assert_(SV('0.3.0') < SV('0.3.1'))
+ self.assert_(SV('0.3.1') < SV(get_plain_version()))
+ self.assert_(SV(get_plain_version()) < SV('0.4.1'))
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..8b91c6b7
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+envlist = py26,py27,docs
+
+[testenv]
+deps = nose
+commands = nosetests []
+
+[testenv:docs]
+basepython = python
+changedir = docs
+deps = sphinx
+commands =
+ sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html