diff --git a/.travis.yml b/.travis.yml
index a57f7474..df08679b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,10 +3,19 @@ language: python
install:
- "wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -"
- "sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
- - "sudo apt-get update"
+ - "sudo apt-get update || true"
- "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy/ {print $2}')"
before_script:
- "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt"
script: nosetests
+
+notifications:
+ irc:
+ channels:
+ - "irc.freenode.org#mopidy"
+ on_success: change
+ on_failure: change
+ use_notice: true
+ skip_join: true
diff --git a/README.rst b/README.rst
index 0b0f6965..c7eea228 100644
--- a/README.rst
+++ b/README.rst
@@ -4,17 +4,22 @@ Mopidy
.. image:: https://secure.travis-ci.org/mopidy/mopidy.png?branch=develop
-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
+Mopidy is a music server which can play music both from your local hard drive
+and from Spotify. Searches returns results from both your local hard drive and
+from Spotify, and you can mix tracks from both sources in your play queue. Your
+Spotify playlists are also available for use, though we don't support modifying
+them yet.
+
+To control your music server, you can use the Ubuntu Sound Menu on the machine
+running Mopidy, any device on the same network which can control UPnP
+MediaRenderers, or any MPD client. MPD clients are available for most
platforms, including Windows, Mac OS X, Linux, Android and iOS.
-To install Mopidy, check out
-`the installation docs `_.
+To get started with Mopidy, check out `the docs `_.
- `Documentation `_
- `Source code `_
- `Issue tracker `_
+- `CI server `_
- IRC: ``#mopidy`` at `irc.freenode.net `_
- `Download development snapshot `_
diff --git a/bin/mopidy-scan b/bin/mopidy-scan
index 869aa662..00f51809 100755
--- a/bin/mopidy-scan
+++ b/bin/mopidy-scan
@@ -1,38 +1,5 @@
-#!/usr/bin/env python
+#! /usr/bin/env python
-import sys
-import logging
-
-from mopidy import settings
-from mopidy.utils.log import setup_console_logging, setup_root_logger
-from mopidy.scanner import Scanner, translator
-from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format
-
-setup_root_logger()
-setup_console_logging(2)
-
-tracks = []
-
-def store(data):
- track = translator(data)
- tracks.append(track)
- logging.debug(u'Added %s', track.uri)
-
-def debug(uri, error, debug):
- logging.error(u'Failed %s: %s - %s', uri, error, debug)
-
-logging.info(u'Scanning %s', settings.LOCAL_MUSIC_PATH)
-
-scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
-try:
- scanner.start()
-except KeyboardInterrupt:
- scanner.stop()
-
-logging.info(u'Done')
-
-for a in tracks_to_tag_cache_format(tracks):
- if len(a) == 1:
- print (u'%s' % a).encode('utf-8')
- else:
- print (u'%s: %s' % a).encode('utf-8')
+if __name__ == '__main__':
+ from mopidy.scanner import main
+ main()
diff --git a/docs/_static/mpd-client-gmpc.png b/docs/_static/mpd-client-gmpc.png
new file mode 100644
index 00000000..aa85c273
Binary files /dev/null and b/docs/_static/mpd-client-gmpc.png differ
diff --git a/docs/_static/mpd-client-mpad.jpg b/docs/_static/mpd-client-mpad.jpg
new file mode 100644
index 00000000..6a3357dc
Binary files /dev/null and b/docs/_static/mpd-client-mpad.jpg differ
diff --git a/docs/_static/mpd-client-mpdroid.jpg b/docs/_static/mpd-client-mpdroid.jpg
new file mode 100644
index 00000000..cd8bfc62
Binary files /dev/null and b/docs/_static/mpd-client-mpdroid.jpg differ
diff --git a/docs/_static/mpd-client-mpod.jpg b/docs/_static/mpd-client-mpod.jpg
new file mode 100644
index 00000000..a1704464
Binary files /dev/null and b/docs/_static/mpd-client-mpod.jpg differ
diff --git a/docs/_static/mpd-client-ncmpcpp.png b/docs/_static/mpd-client-ncmpcpp.png
new file mode 100644
index 00000000..975639c6
Binary files /dev/null and b/docs/_static/mpd-client-ncmpcpp.png differ
diff --git a/docs/_static/mpd-client-sonata.png b/docs/_static/mpd-client-sonata.png
new file mode 100644
index 00000000..5fb02049
Binary files /dev/null and b/docs/_static/mpd-client-sonata.png differ
diff --git a/docs/_static/raspberry-pi-by-jwrodgers.jpg b/docs/_static/raspberry-pi-by-jwrodgers.jpg
new file mode 100644
index 00000000..d093bb88
Binary files /dev/null and b/docs/_static/raspberry-pi-by-jwrodgers.jpg differ
diff --git a/docs/_static/ubuntu-sound-menu.png b/docs/_static/ubuntu-sound-menu.png
new file mode 100644
index 00000000..9362f6f4
Binary files /dev/null and b/docs/_static/ubuntu-sound-menu.png differ
diff --git a/docs/api/audio.rst b/docs/api/audio.rst
index d5fb5dd9..2b9f6cc5 100644
--- a/docs/api/audio.rst
+++ b/docs/api/audio.rst
@@ -4,16 +4,27 @@
Audio API
*********
+.. module:: mopidy.audio
+ :synopsis: Thin wrapper around the parts of GStreamer we use
+
+
The audio API is the interface we have built around GStreamer to support our
specific use cases. Most backends should be able to get by with simply setting
the URI of the resource they want to play, for these cases the default playback
provider should be used.
For more advanced cases such as when the raw audio data is delivered outside of
-GStreamer or the backend needs to add metadata to the currently playing resource,
-developers should sub-class the base playback provider and implement the extra
-behaviour that is needed through the following API:
+GStreamer or the backend needs to add metadata to the currently playing
+resource, developers should sub-class the base playback provider and implement
+the extra behaviour that is needed through the following API:
.. autoclass:: mopidy.audio.Audio
:members:
+
+
+Audio listener
+==============
+
+.. autoclass:: mopidy.audio.AudioListener
+ :members:
diff --git a/docs/api/backends.rst b/docs/api/backends.rst
index 781723d6..f0aadd53 100644
--- a/docs/api/backends.rst
+++ b/docs/api/backends.rst
@@ -4,6 +4,9 @@
Backend API
***********
+.. module:: mopidy.backends.base
+ :synopsis: The API implemented by backends
+
The backend API is the interface that must be implemented when you create a
backend. If you are working on a frontend and need to access the backend, see
the :ref:`core-api`.
@@ -16,10 +19,10 @@ Playback provider
:members:
-Stored playlists provider
-=========================
+Playlists provider
+==================
-.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider
+.. autoclass:: mopidy.backends.base.BasePlaylistsProvider
:members:
@@ -30,6 +33,15 @@ Library provider
:members:
+Backend listener
+================
+
+.. autoclass:: mopidy.backends.listener.BackendListener
+ :members:
+
+
+.. _backend-implementations:
+
Backend implementations
=======================
diff --git a/docs/api/concepts.rst b/docs/api/concepts.rst
index ae959237..68718935 100644
--- a/docs/api/concepts.rst
+++ b/docs/api/concepts.rst
@@ -1,29 +1,99 @@
.. _concepts:
-**********************************************
-The backend, controller, and provider concepts
-**********************************************
+*************************
+Architecture and concepts
+*************************
-Backend:
- The backend is mostly for convenience. It is a container that holds
- references to all the controllers.
-Controllers:
- Each controller has responsibility for a given part of the backend
- functionality. Most, but not all, controllers delegates some work to one or
- more providers. The controllers are responsible for choosing the right
- provider for any given task based upon i.e. the track's URI. See
- :ref:`core-api` for more details.
-Providers:
- Anything specific to i.e. Spotify integration or local storage is contained
- in the providers. To integrate with new music sources, you just add new
- providers. See :ref:`backend-api` for more details.
+The overall architecture of Mopidy is organized around multiple frontends and
+backends. The frontends use the core API. The core actor makes multiple backends
+work as one. The backends connect to various music sources. Both the core actor
+and the backends use the audio actor to play audio and control audio volume.
-.. digraph:: backend_relations
+.. digraph:: overall_architecture
- Backend -> "Current\nplaylist\ncontroller"
- Backend -> "Library\ncontroller"
- "Library\ncontroller" -> "Library\nproviders"
- Backend -> "Playback\ncontroller"
- "Playback\ncontroller" -> "Playback\nproviders"
- Backend -> "Stored\nplaylists\ncontroller"
- "Stored\nplaylists\ncontroller" -> "Stored\nplaylist\nproviders"
+ "Multiple frontends" -> Core
+ Core -> "Multiple backends"
+ Core -> Audio
+ "Multiple backends" -> Audio
+
+
+Frontends
+=========
+
+Frontends expose Mopidy to the external world. They can implement servers for
+protocols like MPD and MPRIS, and they can be used to update other services
+when something happens in Mopidy, like the Last.fm scrobbler frontend does. See
+:ref:`frontend-api` for more details.
+
+.. digraph:: frontend_architecture
+
+ "MPD\nfrontend" -> Core
+ "MPRIS\nfrontend" -> Core
+ "Last.fm\nfrontend" -> Core
+
+
+Core
+====
+
+The core is organized as a set of controllers with responsiblity for separate
+sets of functionality.
+
+The core is the single actor that the frontends send their requests to. For
+every request from a frontend it calls out to one or more backends which does
+the real work, and when the backends respond, the core actor is responsible for
+combining the responses into a single response to the requesting frontend.
+
+The core actor also keeps track of the tracklist, since it doesn't belong to a
+specific backend.
+
+See :ref:`core-api` for more details.
+
+.. digraph:: core_architecture
+
+ Core -> "Tracklist\ncontroller"
+ Core -> "Library\ncontroller"
+ Core -> "Playback\ncontroller"
+ Core -> "Playlists\ncontroller"
+
+ "Library\ncontroller" -> "Local backend"
+ "Library\ncontroller" -> "Spotify backend"
+
+ "Playback\ncontroller" -> "Local backend"
+ "Playback\ncontroller" -> "Spotify backend"
+ "Playback\ncontroller" -> Audio
+
+ "Playlists\ncontroller" -> "Local backend"
+ "Playlists\ncontroller" -> "Spotify backend"
+
+
+Backends
+========
+
+The backends are organized as a set of providers with responsiblity for
+separate sets of functionality, similar to the core actor.
+
+Anything specific to i.e. Spotify integration or local storage is contained in
+the backends. To integrate with new music sources, you just add a new backend.
+See :ref:`backend-api` for more details.
+
+.. digraph:: backend_architecture
+
+ "Local backend" -> "Local\nlibrary\nprovider" -> "Local disk"
+ "Local backend" -> "Local\nplayback\nprovider" -> "Local disk"
+ "Local backend" -> "Local\nplaylists\nprovider" -> "Local disk"
+ "Local\nplayback\nprovider" -> Audio
+
+ "Spotify backend" -> "Spotify\nlibrary\nprovider" -> "Spotify service"
+ "Spotify backend" -> "Spotify\nplayback\nprovider" -> "Spotify service"
+ "Spotify backend" -> "Spotify\nplaylists\nprovider" -> "Spotify service"
+ "Spotify\nplayback\nprovider" -> Audio
+
+
+Audio
+=====
+
+The audio actor is a thin wrapper around the parts of the GStreamer library we
+use. In addition to playback, it's responsible for volume control through both
+GStreamer's own volume mixers, and mixers we've created ourselves. If you
+implement an advanced backend, you may need to implement your own playback
+provider using the :ref:`audio-api`.
diff --git a/docs/api/core.rst b/docs/api/core.rst
index e74d9f45..de85557c 100644
--- a/docs/api/core.rst
+++ b/docs/api/core.rst
@@ -4,6 +4,9 @@
Core API
********
+.. module:: mopidy.core
+ :synopsis: Core API for use by frontends
+
The core API is the interface that is used by frontends like
:mod:`mopidy.frontends.mpd`. The core layer is inbetween the frontends and the
@@ -23,21 +26,21 @@ seek, and volume control.
:members:
-Current playlist controller
-===========================
+Tracklist controller
+====================
-Manages everything related to the currently loaded playlist.
+Manages everything related to the tracks we are currently playing.
-.. autoclass:: mopidy.core.CurrentPlaylistController
+.. autoclass:: mopidy.core.TracklistController
:members:
-Stored playlists controller
-===========================
+Playlists controller
+====================
-Manages stored playlist.
+Manages persistence of playlists.
-.. autoclass:: mopidy.core.StoredPlaylistsController
+.. autoclass:: mopidy.core.PlaylistsController
:members:
@@ -48,3 +51,10 @@ Manages the music library, e.g. searching for tracks to be added to a playlist.
.. autoclass:: mopidy.core.LibraryController
:members:
+
+
+Core listener
+=============
+
+.. autoclass:: mopidy.core.CoreListener
+ :members:
diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst
index af0cc991..2237b4e7 100644
--- a/docs/api/frontends.rst
+++ b/docs/api/frontends.rst
@@ -1,3 +1,5 @@
+.. _frontend-api:
+
************
Frontend API
************
@@ -6,22 +8,38 @@ The following requirements applies to any frontend implementation:
- A frontend MAY do mostly whatever it wants to, including creating threads,
opening TCP ports and exposing Mopidy for a group of clients.
+
- A frontend MUST implement at least one `Pykka
`_ actor, called the "main actor" from here
on.
+
+- The main actor MUST accept a constructor argument ``core``, which will be an
+ :class:`ActorProxy ` for the core actor. This object
+ gives access to the full :ref:`core-api`.
+
- It MAY use additional actors to implement whatever it does, and using actors
in frontend implementations is encouraged.
+
- The frontend is activated by including its main actor in the
:attr:`mopidy.settings.FRONTENDS` setting.
+
- The main actor MUST be able to start and stop the frontend when the main
actor is started and stopped.
+
- The frontend MAY require additional settings to be set for it to
work.
+
- Such settings MUST be documented.
+
- The main actor MUST stop itself if the defined settings are not adequate for
the frontend to work properly.
-- Any actor which is part of the frontend MAY implement any listener interface
- from :mod:`mopidy.listeners` to receive notification of the specified events.
+
+- Any actor which is part of the frontend MAY implement the
+ :class:`mopidy.core.CoreListener` interface to receive notification of the
+ specified events.
+
+
+.. _frontend-implementations:
Frontend implementations
========================
diff --git a/docs/api/index.rst b/docs/api/index.rst
index 618096ee..5a210812 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -11,4 +11,3 @@ API reference
core
audio
frontends
- listeners
diff --git a/docs/api/listeners.rst b/docs/api/listeners.rst
deleted file mode 100644
index 609dc3c7..00000000
--- a/docs/api/listeners.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-************
-Listener API
-************
-
-.. automodule:: mopidy.listeners
- :synopsis: Listener API
- :members:
diff --git a/docs/changes.rst b/docs/changes.rst
index 5ab8ebd3..64fe1ad6 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -5,6 +5,248 @@ Changes
This change log is used to track all major changes to Mopidy.
+v0.9.0 (2012-11-21)
+===================
+
+Support for using the local and Spotify backends simultaneously have for a very
+long time been our most requested feature. Finally, it's here!
+
+**Dependencies**
+
+- pyspotify >= 1.9, < 1.10 is now required for Spotify support.
+
+**Documentation**
+
+- New :ref:`installation` guides, organized by OS and distribution so that you
+ can follow one concise list of instructions instead of jumping around the
+ docs to look for instructions for each dependency.
+
+- Moved :ref:`raspberrypi-installation` howto from the wiki to the docs.
+
+- Updated :ref:`mpd-clients` overview.
+
+- Added :ref:`mpris-clients` and :ref:`upnp-clients` overview.
+
+**Multiple backends support**
+
+- Both the local backend and the Spotify backend are now turned on by default.
+ The local backend is listed first in the :attr:`mopidy.settings.BACKENDS`
+ setting, and are thus given the highest priority in e.g. search results,
+ meaning that we're listing search hits from the local backend first. If you
+ want to prioritize the backends in another way, simply set ``BACKENDS`` in
+ your own settings file and reorder the backends.
+
+ There are no other setting changes related to the local and Spotify backends.
+ As always, see :mod:`mopidy.settings` for the full list of available
+ settings.
+
+**Spotify backend**
+
+- The Spotify backend now includes release year and artist on albums.
+
+- :issue:`233`: The Spotify backend now returns the track if you search for the
+ Spotify track URI.
+
+- Added support for connecting to the Spotify service through an HTTP or SOCKS
+ proxy, which is supported by pyspotify >= 1.9.
+
+- Subscriptions to other Spotify user's "starred" playlists are ignored, as
+ they currently isn't fully supported by pyspotify.
+
+**Local backend**
+
+- :issue:`236`: The ``mopidy-scan`` command failed to include tags from ALAC
+ files (Apple lossless) because it didn't support multiple tag messages from
+ GStreamer per track it scanned.
+
+- Added support for search by filename to local backend.
+
+**MPD frontend**
+
+- :issue:`218`: The MPD commands ``listplaylist`` and ``listplaylistinfo`` now
+ accepts unquoted playlist names if they don't contain spaces.
+
+- :issue:`246`: The MPD command ``list album artist ""`` and similar
+ ``search``, ``find``, and ``list`` commands with empty filter values caused a
+ :exc:`LookupError`, but should have been ignored by the MPD server.
+
+- The MPD frontend no longer lowercases search queries. This broke e.g. search
+ by URI, where casing may be essential.
+
+- The MPD command ``plchanges`` always returned the entire playlist. It now
+ returns an empty response when the client has seen the latest version.
+
+- The MPD commands ``search`` and ``find`` now allows the key ``file``, which
+ is used by ncmpcpp instead of ``filename``.
+
+- The MPD commands ``search`` and ``find`` now allow search query values to be
+ empty strings.
+
+- The MPD command ``listplaylists`` will no longer return playlists without a
+ name. This could crash ncmpcpp.
+
+- The MPD command ``list`` will no longer return artist names, album names, or
+ dates that are blank.
+
+- The MPD command ``decoders`` will now return an empty response instead of a
+ "not implemented" error to make the ncmpcpp browse view work the first time
+ it is opened.
+
+**MPRIS frontend**
+
+- The MPRIS playlists interface is now supported by our MPRIS frontend. This
+ means that you now can select playlists to queue and play from the Ubuntu
+ Sound Menu.
+
+**Audio mixers**
+
+- Made the :mod:`NAD mixer ` responsive to interrupts
+ during amplifier calibration. It will now quit immediately, while previously
+ it completed the calibration first, and then quit, which could take more than
+ 15 seconds.
+
+**Developer support**
+
+- Added optional background thread for debugging deadlocks. When the feature is
+ enabled via the ``--debug-thread`` option or
+ :attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump
+ the traceback for all running threads.
+
+- The settings validator will now allow any setting prefixed with ``CUSTOM_``
+ to exist in the settings file.
+
+**Internal changes**
+
+Internally, Mopidy have seen a lot of changes to pave the way for multiple
+backends and the future HTTP frontend.
+
+- A new layer and actor, "core", has been added to our stack, inbetween the
+ frontends and the backends. The responsibility of the core layer and actor is
+ to take requests from the frontends, pass them on to one or more backends,
+ and combining the response from the backends into a single response to the
+ requesting frontend.
+
+ Frontends no longer know anything about the backends. They just use the
+ :ref:`core-api`.
+
+- The dependency graph between the core controllers and the backend providers
+ have been straightened out, so that we don't have any circular dependencies.
+ The frontend, core, backend, and audio layers are now strictly separate. The
+ frontend layer calls on the core layer, and the core layer calls on the
+ backend layer. Both the core layer and the backends are allowed to call on
+ the audio layer. Any data flow in the opposite direction is done by
+ broadcasting of events to listeners, through e.g.
+ :class:`mopidy.core.CoreListener` and :class:`mopidy.audio.AudioListener`.
+
+ See :ref:`concepts` for more details and illustrations of all the relations.
+
+- All dependencies are now explicitly passed to the constructors of the
+ frontends, core, and the backends. This makes testing each layer with
+ dummy/mocked lower layers easier than with the old variant, where
+ dependencies where looked up in Pykka's actor registry.
+
+- All properties in the core API now got getters, and setters if setting them
+ is allowed. They are not explictly listed in the docs as they have the same
+ behavior as the documented properties, but they are available and may be
+ used. This is useful for the future HTTP frontend.
+
+*Models:*
+
+- Added :attr:`mopidy.models.Album.date` attribute. It has the same format as
+ the existing :attr:`mopidy.models.Track.date`.
+
+- Added :class:`mopidy.models.ModelJSONEncoder` and
+ :func:`mopidy.models.model_json_decoder` for automatic JSON serialization and
+ deserialization of data structures which contains Mopidy models. This is
+ useful for the future HTTP frontend.
+
+*Library:*
+
+- :meth:`mopidy.core.LibraryController.find_exact` and
+ :meth:`mopidy.core.LibraryController.search` now returns plain lists of
+ tracks instead of playlist objects.
+
+- :meth:`mopidy.core.LibraryController.lookup` now returns a list of tracks
+ instead of a single track. This makes it possible to support lookup of
+ artist or album URIs which then can expand to a list of tracks.
+
+*Playback:*
+
+- The base playback provider has been updated with sane default behavior
+ instead of empty functions. By default, the playback provider now lets
+ GStreamer keep track of the current track's time position. The local backend
+ simply uses the base playback provider without any changes. Any future
+ backend that just feeds URIs to GStreamer to play can also use the base
+ playback provider without any changes.
+
+- Removed :attr:`mopidy.core.PlaybackController.track_at_previous`. Use
+ :attr:`mopidy.core.PlaybackController.tl_track_at_previous` instead.
+
+- Removed :attr:`mopidy.core.PlaybackController.track_at_next`. Use
+ :attr:`mopidy.core.PlaybackController.tl_track_at_next` instead.
+
+- Removed :attr:`mopidy.core.PlaybackController.track_at_eot`. Use
+ :attr:`mopidy.core.PlaybackController.tl_track_at_eot` instead.
+
+- Removed :attr:`mopidy.core.PlaybackController.current_tlid`. Use
+ :attr:`mopidy.core.PlaybackController.current_tl_track` instead.
+
+*Playlists:*
+
+The playlists part of the core API has been revised to be more focused around
+the playlist URI, and some redundant functionality has been removed:
+
+- Renamed "stored playlists" to "playlists" everywhere, including the core API
+ used by frontends.
+
+- :attr:`mopidy.core.PlaylistsController.playlists` no longer supports
+ assignment to it. The `playlists` property on the backend layer still does,
+ and all functionality is maintained by assigning to the playlists collections
+ at the backend level.
+
+- :meth:`mopidy.core.PlaylistsController.delete` now accepts an URI, and not a
+ playlist object.
+
+- :meth:`mopidy.core.PlaylistsController.save` now returns the saved playlist.
+ The returned playlist may differ from the saved playlist, and should thus be
+ used instead of the playlist passed to
+ :meth:`mopidy.core.PlaylistsController.save`.
+
+- :meth:`mopidy.core.PlaylistsController.rename` has been removed, since
+ renaming can be done with :meth:`mopidy.core.PlaylistsController.save`.
+
+- :meth:`mopidy.core.PlaylistsController.get` has been replaced by
+ :meth:`mopidy.core.PlaylistsController.filter`.
+
+- The event :meth:`mopidy.core.CoreListener.playlist_changed` has been changed
+ to include the playlist that was changed.
+
+*Tracklist:*
+
+- Renamed "current playlist" to "tracklist" everywhere, including the core API
+ used by frontends.
+
+- Removed :meth:`mopidy.core.TracklistController.append`. Use
+ :meth:`mopidy.core.TracklistController.add` instead, which is now capable of
+ adding multiple tracks.
+
+- :meth:`mopidy.core.TracklistController.get` has been replaced by
+ :meth:`mopidy.core.TracklistController.filter`.
+
+- :meth:`mopidy.core.TracklistController.remove` can now remove multiple
+ tracks, and returns the tracks it removed.
+
+- When the tracklist is changed, we now trigger the new
+ :meth:`mopidy.core.CoreListener.tracklist_changed` event. Previously we
+ triggered :meth:`mopidy.core.CoreListener.playlist_changed`, which is
+ intended for stored playlists, not the tracklist.
+
+*Towards Python 3 support:*
+
+- Make the entire code base use unicode strings by default, and only fall back
+ to bytestrings where it is required. Another step closer to Python 3.
+
+
v0.8.1 (2012-10-30)
===================
@@ -23,7 +265,8 @@ to work with Pykka 1.0.
- :issue:`216`: Volume returned by the MPD command `status` contained a
floating point ``.0`` suffix. This bug was introduced with the large audio
- outout and mixer changes in v0.8.0. It now returns an integer again.
+ output and mixer changes in v0.8.0 and broke the MPDroid Android client. It
+ now returns an integer again.
v0.8.0 (2012-09-20)
@@ -313,7 +556,7 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
- If you use the Spotify backend, you *must* upgrade to libspotify 0.0.8 and
pyspotify 1.3. 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/`.
+ instructions at :ref:`installation`.
- If you have explicitly set the :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE`
setting, you must update your settings file. The new setting is named
@@ -454,8 +697,7 @@ loading from Mopidy 0.3.0 is still present.
- 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/`.
+ installing from APT, follow the instructions at :ref:`installation`.
**Changes**
@@ -567,7 +809,7 @@ to this problem.
- 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/`.
+ :ref:`installation`.
- 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.
@@ -594,7 +836,7 @@ to this problem.
- Local backend:
- Add :command:`mopidy-scan` command to generate ``tag_cache`` files without
- any help from the original MPD server. See :ref:`generating_a_tag_cache`
+ any help from the original MPD server. See :ref:`generating-a-tag-cache`
for instructions on how to use it.
- Fix support for UTF-8 encoding in tag caches.
@@ -603,7 +845,7 @@ to this problem.
- 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:
+ :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
@@ -722,10 +964,10 @@ We've worked a bit on OS X support, but not all issues are completely solved
yet. :issue:`25` is the one that is currently blocking OS X support. Any help
solving it will be greatly appreciated!
-Finally, please :ref:`update your pyspotify installation
-` when upgrading to Mopidy 0.2.0. The latest pyspotify
-got a fix for the segmentation fault that occurred when playing music and
-searching at the same time, thanks to Valentin David.
+Finally, please :ref:`update your pyspotify installation ` when
+upgrading to Mopidy 0.2.0. The latest pyspotify got a fix for the segmentation
+fault that occurred when playing music and searching at the same time, thanks
+to Valentin David.
**Important changes**
@@ -790,12 +1032,11 @@ fixing the OS X issues for a future release. You can track the progress at
**Important changes**
- License changed from GPLv2 to Apache License, version 2.0.
-- GStreamer is now a required dependency. See our :doc:`GStreamer installation
- docs `.
+- GStreamer is now a required dependency. See our :ref:`GStreamer installation
+ docs `.
- :mod:`mopidy.backends.libspotify` is now the default backend.
:mod:`mopidy.backends.despotify` is no longer available. This means that you
- need to install the :doc:`dependencies for libspotify
- `.
+ need to install the :ref:`dependencies for libspotify `.
- If you used :mod:`mopidy.backends.libspotify` previously, pyspotify must be
updated when updating to this release, to get working seek functionality.
- :attr:`mopidy.settings.SERVER_HOSTNAME` and
@@ -1050,7 +1291,7 @@ Mopidy is working and usable. 0.1.0a0 is an alpha release, which basicly means
we will still change APIs, add features, etc. before the final 0.1.0 release.
But the software is usable as is, so we release it. Please give it a try and
give us feedback, either at our IRC channel or through the `issue tracker
-`_. Thanks!
+`_. Thanks!
**Changes**
diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst
index c7dc3799..a8cae367 100644
--- a/docs/clients/mpd.rst
+++ b/docs/clients/mpd.rst
@@ -1,92 +1,25 @@
-************************
-MPD client compatability
-************************
+.. _mpd-clients:
+
+***********
+MPD clients
+***********
This is a list of MPD clients we either know works well with Mopidy, or that we
know won't work well. For a more exhaustive list of MPD clients, see
http://mpd.wikia.com/wiki/Clients.
-
-Console clients
-===============
-
-mpc
----
-
-A command line client. Version 0.14 had some issues with Mopidy (see
-:issue:`5`), but 0.16 seems to work nicely.
+.. contents:: Contents
+ :local:
-ncmpc
------
+Test procedure
+==============
-A console client. Works with Mopidy 0.6 and upwards. Uses the ``idle`` MPD
-command, but in a resource inefficient way.
-
-
-ncmpcpp
--------
-
-A console client that generally works well with Mopidy, and is regularly used
-by Mopidy developers.
-
-Search only works in two of the three search modes:
-
-- "Match if tag contains search phrase (regexes supported)" -- Does not work.
- The client tries to fetch all known metadata and do the search client side.
-- "Match if tag contains searched phrase (no regexes)" -- Works.
-- "Match only if both values are the same" -- Works.
-
-
-Graphical clients
-=================
-
-GMPC
-----
-
-`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
-------
-
-`Sonata `_ is a graphical MPD client (GTK+).
-It generally works well with Mopidy, except for search.
-
-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 four MPD clients we could find for Android with Mopidy 0.7.3 on
-a Samsung Galaxy Nexus with Android 4.1.1, using the following test procedure:
+In some cases, we've used the following test procedure to compare the feature
+completeness of clients:
#. Connect to Mopidy
-#. Search for ``foo``, with search type "any" if it can be selected
+#. 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
@@ -107,38 +40,138 @@ a Samsung Galaxy Nexus with Android 4.1.1, using the following test procedure:
#. Check if the app got support for single mode and consume mode
#. Kill Mopidy and confirm that the app handles it without crashing
-We found that all four apps crashed on Android 4.1.1.
-Combining what we managed to find before the apps crashed with our experience
-from an older version of this review, using Android 2.1, we can say that:
-- PMix can be ignored, because it is unmaintained and its fork MPDroid is
- better on all fronts.
+Console clients
+===============
-- Droid MPD Client was to buggy to get an impression from. Unclear if the bugs
- are due to the app or that it hasn't been updated for Android 4.x.
+ncmpcpp
+-------
-- BitMPC is in our experience feature complete, but ugly.
+A console client that works well with Mopidy, and is regularly used by Mopidy
+developers.
-- MPDroid, now that search is in place, is probably feature complete as well,
- and looks nicer than BitMPC.
+.. image:: /_static/mpd-client-ncmpcpp.png
+ :width: 575
+ :height: 426
-In conclusion: MPD clients on Android 4.x is a sad affair. If you want to try
-anyway, try BitMPC and MPDroid.
+Search does not work in the "Match if tag contains search phrase (regexes
+supported)" mode because the client tries to fetch all known metadata and do
+the search on the client side. The two other search modes works nicely, so this
+is not a problem.
+
+
+ncmpc
+-----
+
+A console client. Works with Mopidy 0.6 and upwards. Uses the ``idle`` MPD
+command, but in a resource inefficient way.
+
+
+mpc
+---
+
+A command line client. Version 0.16 and upwards seems to work nicely with
+Mopidy.
+
+
+Graphical clients
+=================
+
+GMPC
+----
+
+`GMPC `_ is a graphical MPD client (GTK+) which works
+well with Mopidy.
+
+.. image:: /_static/mpd-client-gmpc.png
+ :width: 1000
+ :height: 565
+
+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
+------
+
+`Sonata `_ is a graphical MPD client (GTK+).
+It generally works well with Mopidy, except for search.
+
+.. image:: /_static/mpd-client-sonata.png
+ :width: 475
+ :height: 424
+
+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 closed `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 is unmaintained, but generally works well with Mopidy.
+
+
+.. _android_mpd_clients:
+
+Android clients
+===============
+
+We've tested all five MPD clients we could find for Android with Mopidy 0.8.1
+on a Samsung Galaxy Nexus with Android 4.1.2, using our standard test
+procedure.
+
+
+MPDroid
+-------
+
+Test date:
+ 2012-11-06
+Tested version:
+ 1.03.1 (released 2012-10-16)
+
+.. image:: /_static/mpd-client-mpdroid.jpg
+ :width: 288
+ :height: 512
+
+You can get `MPDroid from Google Play
+`_.
+
+- MPDroid started out as a fork of PMix, and is now much better.
+
+- MPDroid's user interface looks nice.
+
+- Everything in the test procedure works.
+
+- In contrast to all other Android clients, MPDroid does support single mode or
+ consume mode.
+
+- When Mopidy is killed, MPDroid handles it gracefully and asks if you want to
+ try to reconnect.
+
+MPDroid is a good MPD client, and really the only one we can recommend.
BitMPC
------
Test date:
- 2012-09-12
+ 2012-11-06
Tested version:
1.0.0 (released 2010-04-12)
-Downloads:
- 5,000+
-Rating:
- 3.7 stars from about 100 ratings
+You can get `BitMPC from Google Play
+`_.
- The user interface lacks some finishing touches. E.g. you can't enter a
hostname for the server. Only IPv4 addresses are allowed.
@@ -152,8 +185,8 @@ Rating:
- BitMPC crashed if Mopidy was killed or crashed.
- When we tried to test using Android 4.1.1, BitMPC started and connected to
- Mopidy without problems, but the app crashed as soon as fire off our search,
- and continued to crash on startup after that.
+ Mopidy without problems, but the app crashed as soon as we fired off our
+ search, and continued to crash on startup after that.
In conclusion, BitMPC is usable if you got an older Android phone and don't
care about looks. For newer Android versions, BitMPC will probably not work as
@@ -164,13 +197,12 @@ Droid MPD Client
----------------
Test date:
- 2012-09-12
+ 2012-11-06
Tested version:
1.4.0 (released 2011-12-20)
-Downloads:
- 10,000+
-Rating:
- 4.2 stars from 400+ ratings
+
+You can get `Droid MPD Client from Google Play
+`_.
- No intutive way to ask the app to connect to the server after adding the
server hostname to the settings.
@@ -187,11 +219,6 @@ Rating:
- Searching for "foo" did nothing. No request was sent to the server.
-- Once, I managed to get a list of stored playlists in the "Search" tab, but I
- never managed to reproduce this. Opening the stored playlists doesn't work,
- because Mopidy haven't implemented ``lsinfo "Playlist name"`` (see
- :issue:`193`).
-
- Droid MPD client does not support single mode or consume mode.
- Not able to complete the test procedure, due to the above problems.
@@ -199,71 +226,34 @@ Rating:
In conclusion, not a client we can recommend.
-MPDroid
--------
-
-Test date:
- 2012-09-12
-Tested version:
- 0.7 (released 2011-06-19)
-Downloads:
- 10,000+
-Rating:
- 4.5 stars from ~500 ratings
-
-- MPDroid started out as a fork of PMix.
-
-- First of all, MPDroid's user interface looks nice.
-
-- Last time we tested MPDroid (v0.6.9), we couldn't find any search
- functionality. Now we found it, and it worked.
-
-- Last time we tested MPDroid (v0.6.9) 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.
-
-- When using Android 4.1.1, MPDroid crashes here and there, e.g. when having an
- empty current playlist and pressing play.
-
-Disregarding Android 4.x problems, MPDroid is a good MPD client.
-
-
PMix
----
Test date:
- 2012-09-12
+ 2012-11-06
Tested version:
0.4.0 (released 2010-03-06)
-Downloads:
- 10,000+
-Rating:
- 3.8 stars from >200 ratings
-- Using Android 4.1.1, PMix, which haven't been updated for 2.5 years, crashes
- as soon as it connects to Mopidy.
+You can get `PMix from Google Play
+`_.
-- Last time we tested the same version of PMix using Android 2.1, we found
- that:
+PMix haven't been updated for 2.5 years, and has less working features than
+it's fork MPDroid. Ignore PMix and use MPDroid instead.
- - PMix does not support search.
- - I could not find stored playlists.
+MPD Remote
+----------
- - Other than that, I was able to complete the test procedure.
+Test date:
+ 2012-11-06
+Tested version:
+ 1.0 (released 2012-05-01)
- - PMix crashed once during testing.
+You can get `MPD Remote from Google Play
+`_.
- - PMix 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.
+This app looks terrible in the screen shots, got just 100+ downloads, and got a
+terrible rating. I honestly didn't take the time to test it.
.. _ios_mpd_clients:
@@ -271,63 +261,60 @@ All in all, PMix works but can do less than MPDroid. Use MPDroid instead.
iOS clients
===========
-MPod
+MPoD
----
Test date:
- 2011-01-19
+ 2012-11-06
Tested version:
- 1.5.1
+ 1.7.1
+
+.. image:: /_static/mpd-client-mpod.jpg
+ :width: 320
+ :height: 480
The `MPoD `_ iPhone/iPod Touch
-app can be installed from the `iTunes Store
-`_.
+app can be installed from `MPoD at 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:
+- The user interface looks nice.
-- **Works:** Playback control generally works, including stop, play, pause,
- previous, next, repeat, random, seek, and volume control.
+- All features exercised in the test procedure worked with MPaD, except seek,
+ which I didn't figure out to do.
-- **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.
+- Search only works in the "Browse" tab, and not under in the "Artist",
+ "Album", or "Song" tabs. For the tabs where search doesn't work, no queries
+ are sent to Mopidy when searching.
-- **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`.
+- Single mode and consume mode is supported.
MPaD
----
-The `MPaD `_ iPad app works
-with Mopidy. A complete review may appear here in the future.
+Test date:
+ 2012-11-06
+Tested version:
+ 1.7.1
+
+.. image:: /_static/mpd-client-mpad.jpg
+ :width: 480
+ :height: 360
+
+The `MPaD `_ iPad app can be
+purchased from `MPaD at iTunes Store
+`_
+
+- The user interface looks nice, though I would like to be able to view the
+ current playlist in the large part of the split view.
+
+- All features exercised in the test procedure worked with MPaD.
+
+- Search only works in the "Browse" tab, and not under in the "Artist",
+ "Album", or "Song" tabs. For the tabs where search doesn't work, no queries
+ are sent to Mopidy when searching.
+
+- Single mode and consume mode is supported.
+
+- The server menu can be very slow top open, and there is no visible feedback
+ when waiting for the connection to a server to succeed.
diff --git a/docs/clients/mpris.rst b/docs/clients/mpris.rst
new file mode 100644
index 00000000..c782fa26
--- /dev/null
+++ b/docs/clients/mpris.rst
@@ -0,0 +1,66 @@
+.. _mpris-clients:
+
+*************
+MPRIS clients
+*************
+
+`MPRIS `_ is short for Media Player Remote Interfacing
+Specification. It's a spec that describes a standard D-Bus interface for making
+media players available to other applications on the same system.
+
+Mopidy's :ref:`MPRIS frontend ` currently implements all
+required parts of the MPRIS spec, plus the optional playlist interface. It does
+not implement the optional tracklist interface.
+
+
+.. _ubuntu-sound-menu:
+
+Ubuntu Sound Menu
+=================
+
+The `Ubuntu Sound Menu `_ is the default
+sound menu in Ubuntu since 10.10 or 11.04. By default, it only includes the
+Rhytmbox music player, but many other players can integrate with the sound
+menu, including the official Spotify player and Mopidy.
+
+.. image:: /_static/ubuntu-sound-menu.png
+ :height: 480
+ :width: 955
+
+If you install Mopidy from apt.mopidy.com, the sound menu should work out of
+the box. If you install Mopidy in any other way, you need to make sure that the
+file located at ``data/mopidy.desktop`` in the Mopidy git repo is installed as
+``/usr/share/applications/mopidy.desktop``, and that the properties ``TryExec``
+and ``Exec`` in the file points to an existing executable file, preferably your
+Mopidy executable. If this isn't in place, the sound menu will not detect that
+Mopidy is running.
+
+Next, Mopidy's MPRIS frontend must be running for the sound menu to be able to
+control Mopidy. The frontend is activated by default, so unless you've changed
+the :attr:`mopidy.settings.FRONTENDS` setting, you should be good to go. Keep
+an eye out for warnings or errors from the MPRIS frontend when you start
+Mopidy, since it may fail because of missing dependencies or because Mopidy is
+started outside of X; the frontend won't work if ``$DISPLAY`` isn't set when
+Mopidy is started.
+
+Under normal use, if Mopidy isn't running and you open the menu and click on
+"Mopidy Music Server", a terminal window will open and automatically start
+Mopidy. If Mopidy is already running, you'll see that Mopidy is marked with an
+arrow to the left of its name, like in the screen shot above, and the player
+controls will be visible. Mopidy doesn't support the MPRIS spec's optional
+playlist interface yet, so you'll not be able to select what track to play from
+the sound menu. If you use an MPD client to queue a playlist, you can use the
+sound menu to check what you're currently playing, pause, resume, and skip to
+the next and previous track.
+
+In summary, Mopidy's sound menu integration is currently not a full featured
+client, but it's a convenient addition to an MPD client since it's always
+easily available on Unity's menu bar.
+
+
+Rygel
+=====
+
+Rygel is an application that will translate between Mopidy's MPRIS interface
+and UPnP, and thus make Mopidy controllable from devices compatible with UPnP
+and/or DLNA. To read more about this, see :ref:`upnp-clients`.
diff --git a/docs/clients/upnp.rst b/docs/clients/upnp.rst
new file mode 100644
index 00000000..567fb04f
--- /dev/null
+++ b/docs/clients/upnp.rst
@@ -0,0 +1,117 @@
+.. _upnp-clients:
+
+************
+UPnP clients
+************
+
+`UPnP `_ is a set of
+specifications for media sharing, playing, remote control, etc, across a home
+network. The specs are supported by a lot of consumer devices (like
+smartphones, TVs, Xbox, and PlayStation) that are often labeled as being `DLNA
+`_ compatible or certified.
+
+The DLNA guidelines and UPnP specifications defines several device roles, of
+which Mopidy may play two:
+
+DLNA Digital Media Server (DMS) / UPnP AV MediaServer:
+
+ A MediaServer provides a library of media and is capable of streaming that
+ media to a MediaRenderer. If Mopidy was a MediaServer, you could browse and
+ play Mopidy's music on a TV, smartphone, or tablet supporting UPnP. Mopidy
+ does not currently support this, but we may in the future. :issue:`52` is
+ the relevant wishlist issue.
+
+DLNA Digital Media Renderer (DMR) / UPnP AV MediaRenderer:
+
+ A MediaRenderer is asked by some remote controller to play some
+ given media, typically served by a MediaServer. If Mopidy was a
+ MediaRenderer, you could use e.g. your smartphone or tablet to make Mopidy
+ play media. Mopidy *does already* have experimental support for being a
+ MediaRenderer with the help of Rygel, as you can read more about below.
+
+
+.. _rygel:
+
+How to make Mopidy available as an UPnP MediaRenderer
+=====================================================
+
+With the help of `the Rygel project `_ Mopidy can
+be made available as an UPnP MediaRenderer. Rygel will interface with Mopidy's
+:ref:`MPRIS frontend `, and make Mopidy available as a
+MediaRenderer on the local network. Since this depends on the MPRIS frontend,
+which again depends on D-Bus being available, this will only work on Linux, and
+not OS X. MPRIS/D-Bus is only available to other applications on the same host,
+so Rygel must be running on the same machine as Mopidy.
+
+1. Start Mopidy and make sure the :ref:`MPRIS frontend ` is
+ working. It is activated by default, but you may miss dependencies or be
+ using OS X, in which case it will not work. Check the console output when
+ Mopidy is started for any errors related to the MPRIS frontend. If you're
+ unsure it is working, there are instructions for how to test it on the
+ :ref:`MPRIS frontend ` page.
+
+2. Install Rygel. On Debian/Ubuntu::
+
+ sudo apt-get install rygel
+
+3. Enable Rygel's MPRIS plugin. On Debian/Ubuntu, edit ``/etc/rygel.conf``,
+ find the ``[MPRIS]`` section, and change ``enabled=false`` to
+ ``enabled=true``.
+
+4. Start Rygel by running::
+
+ rygel
+
+ Example output::
+
+ $ rygel
+ Rygel-Message: New plugin 'MediaExport' available
+ Rygel-Message: New plugin 'org.mpris.MediaPlayer2.spotify' available
+ Rygel-Message: New plugin 'org.mpris.MediaPlayer2.mopidy' available
+
+ Note that in the above example, both the official Spotify client and Mopidy
+ is running and made available through Rygel.
+
+
+The UPnP-Inspector client
+=========================
+
+`UPnP-Inspector `_ is a
+graphical analyzer and debugging tool for UPnP services. It will detect any
+UPnP devices on your network, and show these in a tree structure. This is not a
+tool for your everyday music listening while relaxing on the couch, but it may
+be of use for testing that your setup works correctly.
+
+1. Install UPnP-Inspector. On Debian/Ubuntu::
+
+ sudo apt-get install upnp-inspector
+
+2. Run it::
+
+ upnp-inspector
+
+3. Assuming that Mopidy is running with a working MPRIS frontend, and that
+ Rygel is running on the same machine, Mopidy should now appear in
+ UPnP-Inspector's device list.
+
+4. If you expand the tree item saying ``Mopidy
+ (MediaRenderer:2)`` or similiar, and then the sub element named
+ ``AVTransport:2`` or similar, you'll find a list of commands you can invoke.
+ E.g. if you double-click the ``Pause`` command, you'll get a new window
+ where you can press an ``Invoke`` button, and then Mopidy should be paused.
+
+Note that if you have a firewall on the host running Mopidy and Rygel, and you
+want this to be exposed to the rest of your local network, you need to open up
+your firewall for UPnP traffic. UPnP use UDP port 1900 as well as some
+dynamically assigned ports. I've only verified that this procedure works across
+the network by temporarily disabling the firewall on the the two hosts
+involved, so I'll leave any firewall configuration as an exercise to the
+reader.
+
+
+Other clients
+=============
+
+For a long list of UPnP clients for all possible platforms, see Wikipedia's
+`List of UPnP AV media servers and clients
+`_.
diff --git a/docs/conf.py b/docs/conf.py
index 8129adec..d5debb46 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -3,7 +3,8 @@
# Mopidy documentation build configuration file, created by
# sphinx-quickstart on Fri Feb 5 22:19:08 2010.
#
-# This file is execfile()d with the current directory set to its containing dir.
+# This file is execfile()d with the current directory set to its containing
+# dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
@@ -11,10 +12,12 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
+from __future__ import unicode_literals
+
import os
-import re
import sys
+
class Mock(object):
def __init__(self, *args, **kwargs):
pass
@@ -34,6 +37,7 @@ class Mock(object):
else:
return Mock()
+
MOCK_MODULES = [
'dbus',
'dbus.mainloop',
@@ -63,12 +67,16 @@ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../'))
# the string True.
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
-# -- General configuration -----------------------------------------------------
+# -- General configuration ----------------------------------------------------
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.graphviz',
- 'sphinx.ext.extlinks', 'sphinx.ext.viewcode']
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.extlinks',
+ 'sphinx.ext.graphviz',
+ 'sphinx.ext.viewcode',
+]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -83,15 +91,15 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
-project = u'Mopidy'
-copyright = u'2010-2012, Stein Magnus Jodal and contributors'
+project = 'Mopidy'
+copyright = '2010-2012, 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
# built documents.
#
# The full version, including alpha/beta/rc tags.
-from mopidy import get_version
+from mopidy.utils.versioning import get_version
release = get_version()
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
@@ -114,7 +122,8 @@ version = '.'.join(release.split('.')[:2])
# for source files.
exclude_trees = ['_build']
-# The reST default role (used for this markup: `text`) to use for all documents.
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
@@ -135,7 +144,7 @@ pygments_style = 'sphinx'
modindex_common_prefix = ['mopidy.']
-# -- Options for HTML output ---------------------------------------------------
+# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
@@ -210,7 +219,7 @@ html_static_path = ['_static']
htmlhelp_basename = 'Mopidydoc'
-# -- Options for LaTeX output --------------------------------------------------
+# -- Options for LaTeX output -------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
@@ -218,11 +227,16 @@ htmlhelp_basename = 'Mopidydoc'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
+# Grouping the document tree into LaTeX files. List of tuples (source start
+# file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index', 'Mopidy.tex', u'Mopidy Documentation',
- u'Stein Magnus Jodal', 'manual'),
+ (
+ 'index',
+ 'Mopidy.tex',
+ 'Mopidy Documentation',
+ 'Stein Magnus Jodal',
+ 'manual'
+ ),
]
# The name of an image file (relative to this directory) to place at the top of
diff --git a/docs/development.rst b/docs/development.rst
index 49d8add5..1211cec4 100644
--- a/docs/development.rst
+++ b/docs/development.rst
@@ -3,7 +3,7 @@ Development
***********
Development of Mopidy is coordinated through the IRC channel ``#mopidy`` at
-``irc.freenode.net`` and through `GitHub `_.
+``irc.freenode.net`` and through `GitHub `_.
Release schedule
@@ -37,13 +37,74 @@ implemented, and you may add new wishlist issues if your ideas are not already
represented.
+.. _run-from-git:
+
+Run Mopidy from Git repo
+========================
+
+If you want to contribute to the development of Mopidy, you should run Mopidy
+directly from the Git repo.
+
+#. First of all, install Mopidy in the recommended way for your OS and/or
+ distribution, like described at :ref:`installation`. You can have a
+ system-wide installation of the last Mopidy release in addition to the Git
+ repo which you run from when you code on Mopidy.
+
+#. Then install Git, if haven't already. For Ubuntu/Debian::
+
+ sudo apt-get install git-core
+
+ On OS X using Homebrew::
+
+ sudo brew install git
+
+#. Clone the official Mopidy repository::
+
+ git clone git://github.com/mopidy/mopidy.git
+
+ or your own fork of it::
+
+ git clone git@github.com:mygithubuser/mopidy.git
+
+#. You can then run Mopidy directly from the Git repository::
+
+ cd mopidy/ # Move into the Git repo dir
+ python mopidy # Run python on the mopidy source code dir
+
+How you update your clone depends on whether you cloned the official Mopidy
+repository or your own fork, whether you have made any changes to the clone
+or not, and whether you are currently working on a feature branch or not. In
+other words, you'll need to learn Git.
+
+For an introduction to Git, please visit `git-scm.com `_.
+Also, please read the rest of our developer documentation before you start
+contributing.
+
+
Code style
==========
+- Always import ``unicode_literals`` and use unicode literals for everything
+ except where you're explicitly working with bytes, which are marked with the
+ ``b`` prefix.
+
+ Do this::
+
+ from __future__ import unicode_literals
+
+ foo = 'I am a unicode string, which is a sane default'
+ bar = b'I am a bytestring'
+
+ Not this::
+
+ foo = u'I am a unicode string'
+ bar = 'I am a bytestring, but was it intentional?'
+
- Follow :pep:`8` unless otherwise noted. `pep8.py
- `_ can be used to check your code against
- the guidelines, however remember that matching the style of the surrounding
- code is also important.
+ `_ or `flake8
+ `_ can be used to check your code
+ against the guidelines, however remember that matching the style of the
+ surrounding code is also important.
- Use four spaces for indentation, *never* tabs.
@@ -89,7 +150,8 @@ Code style
Commit guidelines
=================
-- We follow the development process described at http://nvie.com/git-model.
+- We follow the development process described at
+ `nvie.com `_.
- Keep commits small and on topic.
@@ -118,27 +180,35 @@ Then, to run all tests, go to the project directory and run::
For example::
$ nosetests
- ......................................................................
- ......................................................................
- ......................................................................
- .......
- ----------------------------------------------------------------------
- Ran 217 tests in 0.267s
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................................
+ .............................................................
+ -----------------------------------------------------------------------------
+ 1062 tests run in 7.4 seconds (1062 tests passed)
- OK
+To run tests with test coverage statistics, remember to specify the tests dir::
-To run tests with test coverage statistics::
-
- nosetests --with-coverage
+ nosetests --with-coverage tests/
For more documentation on testing, check out the `nose documentation
-`_.
+`_.
Continuous integration
======================
-Mopidy uses the free service `Travis CI `_
+Mopidy uses the free service `Travis CI `_
for automatically running the test suite when code is pushed to GitHub. This
works both for the main Mopidy repo, but also for any forks. This way, any
contributions to Mopidy through GitHub will automatically be tested by Travis
@@ -201,10 +271,50 @@ both to use ``tests/data/advanced_tag_cache`` for their tag cache and
playlists.
+Setting profiles during development
+===================================
+
+While developing Mopidy switching settings back and forth can become an all too
+frequent occurrence. As a quick hack to get around this you can structure your
+settings file in the following way::
+
+ import os
+ profile = os.environ.get('PROFILE', '').split(',')
+
+ if 'spotify' in profile:
+ BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',)
+ elif 'local' in profile:
+ BACKENDS = (u'mopidy.backends.local.LocalBackend',)
+ LOCAL_MUSIC_PATH = u'~/music'
+
+ if 'shoutcast' in profile:
+ OUTPUT = u'lame ! shout2send mount="/stream"'
+ elif 'silent' in profile:
+ OUTPUT = u'fakesink'
+ MIXER = None
+
+ SPOTIFY_USERNAME = u'xxxxx'
+ SPOTIFY_PASSWORD = u'xxxxx'
+
+Using this setup you can now run Mopidy with ``PROFILE=silent,spotify mopidy``
+if you for instance want to test Spotify without any actual audio output.
+
+
+Debugging deadlocks
+===================
+
+Between the numerous Pykka threads and GStreamer interactions there can
+sometimes be a potential for deadlocks. In an effort to make these slightly
+simpler to debug the setting :attr:`mopidy.settings.DEBUG_THREAD` or the option
+``--debug-thread`` can be used to turn on an extra debug thread. This thread is
+not linked to the regular program flow, and it's only task is to dump traceback
+showing the other threads state when we get a ``SIGUSR1``.
+
+
Writing documentation
=====================
-To write documentation, we use `Sphinx `_. See their
+To write documentation, we use `Sphinx `_. See their
site for lots of documentation on how to use Sphinx. To generate HTML or LaTeX
from the documentation files, you need some additional dependencies.
@@ -247,32 +357,3 @@ Creating releases
python setup.py sdist upload
#. Spread the word.
-
-
-Setting profiles during development
-===================================
-
-While developing Mopidy switching settings back and forth can become an all too
-frequent occurrence. As a quick hack to get around this you can structure your
-settings file in the following way::
-
- import os
- profile = os.environ.get('PROFILE', '').split(',')
-
- if 'spotify' in profile:
- BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',)
- elif 'local' in profile:
- BACKENDS = (u'mopidy.backends.local.LocalBackend',)
- LOCAL_MUSIC_PATH = u'~/music'
-
- if 'shoutcast' in profile:
- OUTPUT = u'lame ! shout2send mount="/stream"'
- elif 'silent' in profile:
- OUTPUT = u'fakesink'
- MIXER = None
-
- SPOTIFY_USERNAME = u'xxxxx'
- SPOTIFY_PASSWORD = u'xxxxx'
-
-Using this setup you can now run Mopidy with ``PROFILE=silent,spotify mopidy``
-if you for instance want to test Spotify without any actual audio output.
diff --git a/docs/index.rst b/docs/index.rst
index 0af510d0..bce84b5a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -2,26 +2,33 @@
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.
+Mopidy is a music server which can play music both from your :ref:`local hard
+drive ` and from :ref:`Spotify `. Searches
+returns results from both your local hard drive and from Spotify, and you can
+mix tracks from both sources in your play queue. Your Spotify playlists are
+also available for use, though we don't support modifying them yet.
-To install Mopidy, start out by reading :ref:`installation`.
+To control your music server, you can use the :ref:`Ubuntu Sound Menu
+` on the machine running Mopidy, any device on the same
+network which can control UPnP MediaRenderers (see :ref:`upnp-clients`), or any
+:ref:`MPD client `. MPD clients are available for most platforms,
+including Windows, Mac OS X, Linux, Android, and iOS.
+
+To install Mopidy, start 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 `_
-- `Source code `_
-- `Issue tracker `_
+- `Source code `_
+- `Issue tracker `_
+- `CI server `_
- IRC: ``#mopidy`` at `irc.freenode.net `_
@@ -32,6 +39,7 @@ User documentation
:maxdepth: 3
installation/index
+ installation/raspberrypi
settings
running
clients/index
@@ -39,6 +47,7 @@ User documentation
licenses
changes
+
Reference documentation
=======================
@@ -48,6 +57,7 @@ Reference documentation
api/index
modules/index
+
Development documentation
=========================
@@ -56,10 +66,10 @@ Development documentation
development
+
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
-
diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst
deleted file mode 100644
index 38dbb86c..00000000
--- a/docs/installation/gstreamer.rst
+++ /dev/null
@@ -1,98 +0,0 @@
-**********************
-GStreamer installation
-**********************
-
-To use Mopidy, you first need to install GStreamer and the GStreamer Python
-bindings.
-
-
-Installing GStreamer on Linux
-=============================
-
-GStreamer is packaged for most popular Linux distributions. Search for
-GStreamer in your package manager, and make sure to install the Python
-bindings, and the "good" and "ugly" plugin sets.
-
-
-Debian/Ubuntu
--------------
-
-If you use Debian/Ubuntu you can install GStreamer like this::
-
- sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
- gstreamer0.10-plugins-ugly
-
-If you install Mopidy from our APT archive, you don't need to install GStreamer
-yourself. The Mopidy Debian package will handle it for you.
-
-
-Arch Linux
-----------
-
-If you use Arch Linux, install the following packages from the official
-repository::
-
- sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
- gstreamer0.10-ugly-plugins
-
-
-Installing GStreamer on OS X
-============================
-
-We have been working with `Homebrew `_ for a
-to make all the GStreamer packages easily installable on OS X.
-
-#. Install `Homebrew `_.
-
-#. Install the required packages::
-
- brew install gst-python gst-plugins-good gst-plugins-ugly
-
-#. Make sure to include Homebrew's Python ``site-packages`` directory in your
- ``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer
- and crash.
-
- You can either amend your ``PYTHONPATH`` permanently, by adding the
- following statement to your shell's init file, e.g. ``~/.bashrc``::
-
- export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH
-
- Or, you can prefix the Mopidy command every time you run it::
-
- PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy
-
- Note that you need to replace ``python2.7`` with ``python2.6`` if that's
- the Python version you are using. To find your Python version, run::
-
- python --version
-
-
-Testing the installation
-========================
-
-If you now run the ``gst-inspect-0.10`` command (the version number may vary),
-you should see a long listing of installed plugins, ending in a summary line::
-
- $ gst-inspect-0.10
- ... long list of installed plugins ...
- Total count: 218 plugins (1 blacklist entry not shown), 1031 features
-
-You should be able to produce a audible tone by running::
-
- gst-launch-0.10 audiotestsrc ! autoaudiosink
-
-If you cannot hear any sound when running this command, you won't hear any
-sound from Mopidy either, as Mopidy uses GStreamer's ``autoaudiosink`` to play
-audio. Thus, make this work before you continue installing Mopidy.
-
-
-Using a custom audio sink
-=========================
-
-If you for some reason want to use some other GStreamer audio sink than
-``autoaudiosink``, you can set :attr:`mopidy.settings.OUTPUT` to a partial
-GStreamer pipeline description describing the GStreamer sink you want to use.
-
-Example of ``settings.py`` for OSS4::
-
- OUTPUT = u'oss4sink'
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index c58ba9dd..4ae04c40 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -4,60 +4,21 @@
Installation
************
-There are several ways to install Mopidy. What way is best depends upon your
-setup and whether you want to use stable releases or less stable development
-versions.
+There are several ways to install Mopidy. What way is best depends upon your OS
+and/or distribution. If you want to contribute to the development of Mopidy,
+you should first read this page, then have a look at :ref:`run-from-git`.
+
+.. contents:: Installation guides
+ :local:
-Requirements
-============
-
-.. toctree::
- :hidden:
-
- gstreamer
- libspotify
-
-If you install Mopidy from the APT archive, as described below, APT will take
-care of all the dependencies for you. Otherwise, make sure you got the required
-dependencies installed.
-
-- Hard dependencies:
-
- - Python >= 2.6, < 3
-
- - Pykka >= 1.0::
-
- sudo pip install -U pykka
-
- - GStreamer 0.10.x, with Python bindings. See :doc:`gstreamer`.
-
-- Optional dependencies:
-
- - For Spotify support, you need libspotify and pyspotify. See
- :doc:`libspotify`.
-
- - To scrobble your played tracks to Last.fm, you need pylast::
-
- sudo pip install -U pylast
-
- - To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound Menu, you
- need some additional requirements::
-
- sudo apt-get install python-dbus python-indicate
-
-
-Install latest stable release
-=============================
-
-
-From APT archive
-----------------
+Debian/Ubuntu: Install from apt.mopidy.com
+==========================================
If you run a Debian based Linux distribution, like Ubuntu, the easiest way to
-install Mopidy is from the Mopidy APT archive. When installing from the APT
-archive, you will automatically get updates to Mopidy in the same way as you
-get updates to the rest of your distribution.
+install Mopidy is from the `Mopidy APT archive `_. When
+installing from the APT archive, you will automatically get updates to Mopidy
+in the same way as you get updates to the rest of your distribution.
#. Add the archive's GPG key::
@@ -71,119 +32,48 @@ get updates to the rest of your distribution.
deb http://apt.mopidy.com/ stable main contrib non-free
deb-src http://apt.mopidy.com/ stable main contrib non-free
+ For the lazy, you can simply run the following command to create
+ ``/etc/apt/sources.list.d/mopidy.list``::
+
+ sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list
+
#. Install Mopidy and all dependencies::
sudo apt-get update
sudo apt-get install mopidy
-#. Next, you need to set a couple of :doc:`settings `, and then
+#. Finally, you need to set a couple of :doc:`settings `, and then
you're ready to :doc:`run Mopidy `.
-When a new release is out, and you can't wait for you system to figure it out
-for itself, run the following to force an upgrade::
+When a new release of Mopidy is out, and you can't wait for you system to
+figure it out for itself, run the following to upgrade right away::
sudo apt-get update
sudo apt-get dist-upgrade
-From PyPI using Pip
--------------------
+Raspberry Pi running Debian
+---------------------------
-If you are on OS X or on Linux, but can't install from the APT archive, you can
-install Mopidy from PyPI using Pip.
-
-#. When you install using Pip, you first need to ensure that all of Mopidy's
- dependencies have been installed. See the section on dependencies above.
-
-#. Then, you need to install Pip::
-
- sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
- sudo easy_install pip # On OS X
-
-#. To install the currently latest stable release of Mopidy::
-
- sudo pip install -U Mopidy
-
- To upgrade Mopidy to future releases, just rerun this command.
-
-#. Next, you need to set a couple of :doc:`settings `, and then
- you're ready to :doc:`run Mopidy `.
+Fred Hatfull has created a guide for installing a Raspberry Pi from scratch
+with Debian and Mopidy. See :ref:`raspberrypi-installation`.
-Install development version
-===========================
+Vagrant virtual machine running Ubuntu
+--------------------------------------
-If you want to follow the development of Mopidy closer, you may install a
-development version of Mopidy. These are not as stable as the releases, but
-you'll get access to new features earlier and may help us by reporting issues.
+Paul Sturgess has created a Vagrant and Chef setup that automatically creates
+and sets up a virtual machine which runs Mopidy. Check out
+https://github.com/paulsturgess/mopidy-vagrant if you're interested in trying
+it out.
-From snapshot using Pip
------------------------
+Arch Linux: Install from AUR
+============================
-If you want to follow Mopidy development closer, you may install a snapshot of
-Mopidy's ``develop`` branch.
-
-#. When you install using Pip, you first need to ensure that all of Mopidy's
- dependencies have been installed. See the section on dependencies above.
-
-#. Then, you need to install Pip::
-
- sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian
- sudo easy_install pip # On OS X
-
-#. To install the latest snapshot of Mopidy, run::
-
- sudo pip install mopidy==dev
-
- To upgrade Mopidy to future releases, just rerun this command.
-
-#. Next, you need to set a couple of :doc:`settings `, and then
- you're ready to :doc:`run Mopidy `.
-
-
-From Git
---------
-
-If you want to contribute to Mopidy, you should install Mopidy using Git.
-
-#. When you install from Git, you first need to ensure that all of Mopidy's
- dependencies have been installed. See the section on dependencies above.
-
-#. Then install Git, if haven't already::
-
- sudo apt-get install git-core # On Ubuntu/Debian
- sudo brew install git # On OS X using Homebrew
-
-#. Clone the official Mopidy repository, or your own fork of it::
-
- git clone git://github.com/mopidy/mopidy.git
-
-#. Next, you need to set a couple of :doc:`settings `.
-
-#. You can then run Mopidy directly from the Git repository::
-
- cd mopidy/ # Move into the Git repo dir
- python mopidy # Run python on the mopidy source code dir
-
-#. Later, to get the latest changes to Mopidy::
-
- cd mopidy/
- git pull
-
-For an introduction to ``git``, please visit `git-scm.com
-`_. Also, please read our :doc:`developer documentation
-`.
-
-
-From AUR on ArchLinux
----------------------
-
-If you are running ArchLinux, you can install a development snapshot of Mopidy
-using the package found at http://aur.archlinux.org/packages.php?ID=44026.
-
-#. First, you should consider installing any optional dependencies not included
- by the AUR package, like required for e.g. Last.fm scrobbling.
+If you are running Arch Linux, you can install a development snapshot of Mopidy
+using the `mopidy-git `_
+package found in AUR.
#. To install Mopidy with GStreamer, libspotify and pyspotify, you can use
``packer``, ``yaourt``, or do it by hand like this::
@@ -195,5 +85,161 @@ using the package found at http://aur.archlinux.org/packages.php?ID=44026.
To upgrade Mopidy to future releases, just rerun ``makepkg``.
-#. Next, you need to set a couple of :doc:`settings `, and then
+#. Optional: If you want to scrobble your played tracks to Last.fm, you need to
+ install `python2-pylast
+ `_ from AUR.
+
+#. Finally, you need to set a couple of :doc:`settings `, and then
+ you're ready to :doc:`run Mopidy `.
+
+
+OS X: Install from Homebrew and Pip
+===================================
+
+If you are running OS X, you can install everything needed with Homebrew and
+Pip.
+
+#. Install `Homebrew `_.
+
+ If you are already using Homebrew, make sure your installation is up to
+ date before you continue::
+
+ brew update
+ brew upgrade
+
+#. Install the required packages from Homebrew::
+
+ brew install gst-python gst-plugins-good gst-plugins-ugly libspotify
+
+#. Make sure to include Homebrew's Python ``site-packages`` directory in your
+ ``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer
+ and crash.
+
+ You can either amend your ``PYTHONPATH`` permanently, by adding the
+ following statement to your shell's init file, e.g. ``~/.bashrc``::
+
+ export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH
+
+ Or, you can prefix the Mopidy command every time you run it::
+
+ PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy
+
+ Note that you need to replace ``python2.7`` with ``python2.6`` in the above
+ ``PYTHONPATH`` examples if you are using Python 2.6. To find your Python
+ version, run::
+
+ python --version
+
+#. Next up, you need to install some Python packages. To do so, we use Pip. If
+ you don't have the ``pip`` command, you can install it now::
+
+ sudo easy_install pip
+
+#. Then get, build, and install the latest releast of pyspotify, pylast, pykka,
+ and Mopidy using Pip::
+
+ sudo pip install -U pyspotify pylast pykka mopidy
+
+#. Finally, you need to set a couple of :doc:`settings `, and then
+ you're ready to :doc:`run Mopidy `.
+
+
+Otherwise: Install from source using Pip
+========================================
+
+If you are on on Linux, but can't install from the APT archive or from AUR, you
+can install Mopidy from PyPI using Pip.
+
+#. First of all, you need Python >= 2.6, < 3. Check if you have Python and what
+ version by running::
+
+ python --version
+
+#. When you install using Pip, you need to make sure you have Pip. You'll also
+ need a C compiler and the Python development headers to build pyspotify
+ later.
+
+ This is how you install it on Debian/Ubuntu::
+
+ sudo apt-get install build-essential python-dev python-pip
+
+ And on Arch Linux from the official repository::
+
+ sudo pacman -S base-devel python2-pip
+
+#. Then you'll need to install all of Mopidy's hard dependencies:
+
+ - Pykka >= 1.0::
+
+ sudo pip install -U pykka
+
+ - GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most
+ popular Linux distributions. Search for GStreamer in your package manager,
+ and make sure to install the Python bindings, and the "good" and "ugly"
+ plugin sets.
+
+ If you use Debian/Ubuntu you can install GStreamer like this::
+
+ sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \
+ gstreamer0.10-plugins-ugly gstreamer0.10-tools
+
+ If you use Arch Linux, install the following packages from the official
+ repository::
+
+ sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
+ gstreamer0.10-ugly-plugins
+
+#. Optional: If you want Spotify support in Mopidy, you'll need to install
+ libspotify and the Python bindings, pyspotify.
+
+ #. First, check `pyspotify's changelog `_ to
+ see what's the latest version of libspotify which it supports. The
+ versions of libspotify and pyspotify are tightly coupled, so you'll need
+ to get this right.
+
+ #. Download and install the appropriate version of libspotify for your OS and
+ CPU architecture from `Spotify
+ `_.
+
+ For libspotify 12.1.51 for 64-bit Linux the process is as follows::
+
+ wget https://developer.spotify.com/download/libspotify/libspotify-12.1.51-Linux-x86_64-release.tar.gz
+ tar zxfv libspotify-12.1.51-Linux-x86_64-release.tar.gz
+ cd libspotify-12.1.51-Linux-x86_64-release/
+ sudo make install prefix=/usr/local
+ sudo ldconfig
+
+ Remember to adjust the above example for the latest libspotify version
+ supported by pyspotify, your OS, and your CPU architecture.
+
+ #. Then get, build, and install the latest release of pyspotify using Pip::
+
+ sudo pip install -U pyspotify
+
+#. Optional: If you want to scrobble your played tracks to Last.fm, you need
+ pylast::
+
+ sudo pip install -U pylast
+
+#. Optional: To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound
+ Menu or from an UPnP client via Rygel, you need some additional
+ dependencies: the Python bindings for libindicate, and the Python bindings
+ for libdbus, the reference D-Bus library.
+
+ On Debian/Ubuntu::
+
+ sudo apt-get install python-dbus python-indicate
+
+#. Then, to install the latest release of Mopidy::
+
+ sudo pip install -U mopidy
+
+ To upgrade Mopidy to future releases, just rerun this command.
+
+ Alternatively, if you want to track Mopidy development closer, you may
+ install a snapshot of Mopidy's ``develop`` Git branch using Pip::
+
+ sudo pip install mopidy==dev
+
+#. Finally, you need to set a couple of :doc:`settings `, and then
you're ready to :doc:`run Mopidy `.
diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst
deleted file mode 100644
index 223e4ed7..00000000
--- a/docs/installation/libspotify.rst
+++ /dev/null
@@ -1,112 +0,0 @@
-***********************
-libspotify installation
-***********************
-
-Mopidy uses `libspotify
-`_ for playing music from
-the Spotify music service. To use :mod:`mopidy.backends.spotify` you must
-install libspotify and `pyspotify `_.
-
-.. note::
-
- This backend requires a paid `Spotify premium account
- `_.
-
-
-Installing libspotify
-=====================
-
-
-On Linux from APT archive
--------------------------
-
-If you install from APT, jump directly to :ref:`pyspotify_installation` below.
-
-
-On Linux from source
---------------------
-
-First, check pyspotify's changelog to see what's the latest version of
-libspotify which is supported. The versions of libspotify and pyspotify are
-tightly coupled.
-
-Download and install the appropriate version of libspotify for your OS and CPU
-architecture from https://developer.spotify.com/en/libspotify/.
-
-For libspotify 0.0.8 for 64-bit Linux the process is as follows::
-
- wget http://developer.spotify.com/download/libspotify/libspotify-0.0.8-linux6-x86_64.tar.gz
- tar zxfv libspotify-0.0.8-linux6-x86_64.tar.gz
- cd libspotify-0.0.8-linux6-x86_64/
- sudo make install prefix=/usr/local
- sudo ldconfig
-
-Remember to adjust for the latest libspotify version supported by pyspotify,
-your OS and your CPU architecture.
-
-When libspotify has been installed, continue with
-:ref:`pyspotify_installation`.
-
-
-On OS X from Homebrew
----------------------
-
-In OS X you need to have `XCode `_ and
-`Homebrew `_ installed. Then, to install
-libspotify::
-
- brew install libspotify
-
-To update your existing libspotify installation using Homebrew::
-
- brew update
- brew upgrade
-
-When libspotify has been installed, continue with
-:ref:`pyspotify_installation`.
-
-
-.. _pyspotify_installation:
-
-Installing pyspotify
-====================
-
-When you've installed libspotify, it's time for making it available from Python
-by installing pyspotify.
-
-
-On Linux from APT archive
--------------------------
-
-If you run a Debian based Linux distribution, like Ubuntu, see
-http://apt.mopidy.com/ for how to use the Mopidy APT archive as a software
-source on your system. Then, simply run::
-
- sudo apt-get install python-spotify
-
-This command will install both libspotify and pyspotify for you.
-
-
-On Linux from source
--------------------------
-
-If you have have already installed libspotify, you can continue with installing
-the libspotify Python bindings, called pyspotify.
-
-On Linux, you need to get the Python development files installed. On
-Debian/Ubuntu systems run::
-
- sudo apt-get install python-dev
-
-Then get, build, and install the latest releast of pyspotify using ``pip``::
-
- sudo pip install -U pyspotify
-
-
-On OS X from source
--------------------
-
-If you have already installed libspotify, you can get, build, and install the
-latest releast of pyspotify using ``pip``::
-
- sudo pip install -U pyspotify
diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst
new file mode 100644
index 00000000..fbb07364
--- /dev/null
+++ b/docs/installation/raspberrypi.rst
@@ -0,0 +1,264 @@
+.. _raspberrypi-installation:
+
+****************************
+Installation on Raspberry Pi
+****************************
+
+As of early August, 2012, running Mopidy on a `Raspberry Pi
+`_ is possible, although there are a few
+significant drawbacks to doing so. This document is intended to help you get
+Mopidy running on your Raspberry Pi and to document the progress made and
+issues surrounding running Mopidy on the Raspberry Pi.
+
+Mopidy will not currently run with Spotify support on the foundation-provided
+`Raspbian `_ distribution. See :ref:`not-raspbian` for
+details. However, Mopidy should run with Spotify support on any ARM Debian
+image that has hardware floating-point support **disabled**.
+
+.. image:: /_static/raspberry-pi-by-jwrodgers.jpg
+ :width: 640
+ :height: 427
+
+
+.. _raspi-squeeze:
+
+How to for Debian 6 (Squeeze)
+=============================
+
+The following guide illustrates how to get Mopidy running on a minimal Debian
+squeeze distribution.
+
+1. The image used can be downloaded at
+ http://www.linuxsystems.it/2012/06/debian-wheezy-raspberry-pi-minimal-image/.
+ This image is a very minimal distribution and does not include many common
+ packages you might be used to having access to. If you find yourself trying
+ to complete instructions here and getting ``command not found``, try using
+ ``apt-get`` to install the relevant packages!
+
+2. Flash the OS image to your SD card. See
+ http://elinux.org/RPi_Easy_SD_Card_Setup for help.
+
+3. If you have an SD card that's >2 GB, resize the disk image to use some more
+ space (we'll need a bit more to install some packages and stuff). See
+ http://elinux.org/RPi_Resize_Flash_Partitions#Manually_resizing_the_SD_card_on_Raspberry_Pi
+ for help.
+
+4. To even get to the point where we can start installing software let's create
+ a new user and give it sudo access.
+
+ - Install ``sudo``::
+
+ apt-get install sudo
+
+ - Create a user account::
+
+ adduser
+
+ - Give the user sudo access by adding it to the ``sudo`` group so we don't
+ have to do everything on the ``root`` account::
+
+ adduser sudo
+
+ - While we're at it, give your user access to the sound card by adding it to
+ the audio group::
+
+ adduser audio
+
+ - Log in to your Raspberry Pi again with your new user account instead of
+ the ``root`` account.
+
+5. Enable the Raspberry Pi's sound drivers:
+
+ - To enable the Raspberry Pi's sound driver::
+
+ sudo modprobe snd_bcm2835
+
+ - To load the sound driver at boot time::
+
+ echo "snd_bcm2835" | sudo tee /etc/modules
+
+6. Let's get the Raspberry Pi up-to-date:
+
+ - Get some tools that we need to download and run the ``rpi-update``
+ script::
+
+ sudo apt-get install ca-certificates git-core binutils
+
+ - Download ``rpi-update`` from Github::
+
+ sudo wget https://raw.github.com/Hexxeh/rpi-update/master/rpi-update
+
+ - Move ``rpi-update`` to an appropriate location::
+
+ sudo mv rpi-update /usr/local/bin/rpi-update
+
+ - Make ``rpi-update`` executable::
+
+ sudo chmod +x /usr/local/bin/rpi-update
+
+ - Finally! Update your firmware::
+
+ sudo rpi-update
+
+ - After firmware updating finishes, reboot your Raspberry Pi::
+
+ sudo reboot
+
+7. To avoid a couple of potential problems with Mopidy, turn on IPv6 support:
+
+ - Load the IPv6 kernel module now::
+
+ sudo modprobe ipv6
+
+ - Add ``ipv6`` to ``/etc/modules`` to ensure the IPv6 kernel module is
+ loaded on boot::
+
+ echo ipv6 | sudo tee /etc/modules
+
+8. Installing Mopidy and its dependencies from `apt.mopidy.com
+ `_, as described in :ref:`installation`. In short::
+
+ wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
+ sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list
+ sudo apt-get update
+ sudo apt-get install mopidy
+
+9. jackd2, which should be installed at this point, seems to cause some
+ problems. Let's install jackd1, as it seems to work a little bit better::
+
+ sudo apt-get install jackd1
+
+You may encounter some issues with your audio configuration where sound does
+not play. If that happens, edit your ``/etc/asound.conf`` to read something
+like::
+
+ pcm.mmap0 {
+ type mmap_emul;
+ slave {
+ pcm "hw:0,0";
+ }
+ }
+
+ pcm.!default {
+ type plug;
+ slave {
+ pcm mmap0;
+ }
+ }
+
+
+.. _raspi-wheezy:
+
+How to for Debian 7 (Wheezy)
+============================
+
+This is a very similar system to Debian 6.0 above, but with a bit newer
+software packages, as Wheezy is going to be the next release of Debian.
+
+1. Download the latest wheezy disk image from
+ http://downloads.raspberrypi.org/images/debian/7/. I used the one dated
+ 2012-08-08.
+
+2. Flash the OS image to your SD card. See
+ http://elinux.org/RPi_Easy_SD_Card_Setup for help.
+
+3. If you have an SD card that's >2 GB, you don't have to resize the file
+ systems on another computer. Just boot up your Raspberry Pi with the
+ unaltered partions, and it will boot right into the ``raspi-config`` tool,
+ which will let you grow the root file system to fill the SD card. This tool
+ will also allow you do other useful stuff, like turning on the SSH server.
+
+4. As opposed to on Squeeze, ``sudo`` comes preinstalled. You can login to the
+ default user using username ``pi`` and password ``raspberry``. To become
+ root, just enter ``sudo -i``.
+
+ Opposed to on Squeeze, there is no need to add your user to the ``audio``
+ group, as the ``pi`` user already is a member of that group.
+
+5. As opposed to on Squeeze, the correct sound driver comes preinstalled.
+
+6. As opposed to on Squeeze, your kernel and GPU firmware is rather up to date
+ when running Wheezy.
+
+7. To avoid a couple of potential problems with Mopidy, turn on IPv6 support:
+
+ - Load the IPv6 kernel module now::
+
+ sudo modprobe ipv6
+
+ - Add ``ipv6`` to ``/etc/modules`` to ensure the IPv6 kernel module is
+ loaded on boot::
+
+ echo ipv6 | sudo tee /etc/modules
+
+8. Installing Mopidy and its dependencies from `apt.mopidy.com
+ `_, as described in :ref:`installation`. In short::
+
+ wget -q -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -
+ sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list
+ sudo apt-get update
+ sudo apt-get install mopidy
+
+9. Since I have a HDMI cable connected, but want the sound on the analog sound
+ connector, I have to run::
+
+ amixer cset numid=3 1
+
+ to force it to use analog output. ``1`` means analog, ``0`` means auto, and
+ is the default, while ``2`` means HDMI. You can test sound output
+ independent of Mopidy by running::
+
+ aplay /usr/share/sounds/alsa/Front_Center.wav
+
+ To make the change to analog output stick, you can add the ``amixer`` command
+ to e.g. ``/etc/rc.local``, which will be executed when the system is
+ booting.
+
+
+Known Issues
+============
+
+Audio Quality
+-------------
+
+The Raspberry Pi's audio quality can be sub-par through the analog output. This
+is known and unlikely to be fixed as including any higher-quality hardware
+would increase the cost of the board. If you experience crackling/hissing or
+skipping audio, you may want to try a USB sound card. Additionally, you could
+lower your default ALSA sampling rate to 22KHz, though this will lead to a
+substantial decrease in sound quality.
+
+
+.. _not-raspbian:
+
+Why Not Raspbian?
+-----------------
+
+Mopidy with Spotify support is currently unavailable on the recommended
+`Raspbian `_ Debian distribution that the Raspberry Pi
+foundation has made available. This is due to Raspbian's hardware
+floating-point support. The Raspberry Pi comes with a co-processor designed
+specifically for floating-point computations (commonly called an FPU). Taking
+advantage of the FPU can speed up many computations significantly over
+software-emulated floating point routines. Most of Mopidy's dependencies are
+open-source and have been (or can be) compiled to support the ``armhf``
+architecture. However, there is one component of Mopidy's stack which is
+closed-source and crucial to Mopidy's Spotify support: libspotify.
+
+The ARM distributions of libspotify available on `Spotify's developer website
+`_ are compiled for the ``armel`` architecture,
+which has software floating-point support. ``armel`` and ``armhf`` software
+cannot be mixed, and pyspotify links with libspotify as C extensions. Thus,
+Mopidy will not run with Spotify support on ``armhf`` distributions.
+
+If the Spotify folks ever release builds of libspotify with ``armhf`` support,
+Mopidy *should* work on Raspbian.
+
+
+Support
+=======
+
+If you had trouble with the above or got Mopidy working a different way on
+Raspberry Pi, please send us a pull request to update this page with your new
+information. As usual, the folks at ``#mopidy`` on ``irc.freenode.net`` may be
+able to help with any problems encountered.
diff --git a/docs/modules/audio/mixers/auto.rst b/docs/modules/audio/mixers/auto.rst
new file mode 100644
index 00000000..caf6e3ab
--- /dev/null
+++ b/docs/modules/audio/mixers/auto.rst
@@ -0,0 +1,6 @@
+*********************************************
+:mod:`mopidy.audio.mixers.auto` -- Auto mixer
+*********************************************
+
+.. automodule:: mopidy.audio.mixers.auto
+ :synopsis: Mixer element which automatically selects the real mixer to use
diff --git a/docs/modules/audio/mixers/fake.rst b/docs/modules/audio/mixers/fake.rst
new file mode 100644
index 00000000..dcab7767
--- /dev/null
+++ b/docs/modules/audio/mixers/fake.rst
@@ -0,0 +1,6 @@
+*********************************************
+:mod:`mopidy.audio.mixers.fake` -- Fake mixer
+*********************************************
+
+.. automodule:: mopidy.audio.mixers.fake
+ :synopsis: Fake mixer for use in tests
diff --git a/docs/modules/audio/mixers/nad.rst b/docs/modules/audio/mixers/nad.rst
new file mode 100644
index 00000000..661dc723
--- /dev/null
+++ b/docs/modules/audio/mixers/nad.rst
@@ -0,0 +1,6 @@
+*********************************************
+:mod:`mopidy.audio.mixers.nad` -- NAD mixer
+*********************************************
+
+.. automodule:: mopidy.audio.mixers.nad
+ :synopsis: Mixer element for controlling volume on NAD amplifiers
diff --git a/docs/modules/backends/local.rst b/docs/modules/backends/local.rst
index 892f5a87..9ac93bc8 100644
--- a/docs/modules/backends/local.rst
+++ b/docs/modules/backends/local.rst
@@ -1,7 +1,8 @@
+.. _local-backend:
+
*********************************************
:mod:`mopidy.backends.local` -- Local backend
*********************************************
.. automodule:: mopidy.backends.local
:synopsis: Backend for playing music files on local storage
- :members:
diff --git a/docs/modules/backends/spotify.rst b/docs/modules/backends/spotify.rst
index 938d6337..b410f272 100644
--- a/docs/modules/backends/spotify.rst
+++ b/docs/modules/backends/spotify.rst
@@ -1,7 +1,8 @@
+.. _spotify-backend:
+
*************************************************
:mod:`mopidy.backends.spotify` -- Spotify backend
*************************************************
.. automodule:: mopidy.backends.spotify
:synopsis: Backend for the Spotify music streaming service
- :members:
diff --git a/docs/modules/frontends/lastfm.rst b/docs/modules/frontends/lastfm.rst
index a726f4a2..0dba922f 100644
--- a/docs/modules/frontends/lastfm.rst
+++ b/docs/modules/frontends/lastfm.rst
@@ -4,4 +4,3 @@
.. automodule:: mopidy.frontends.lastfm
:synopsis: Last.fm scrobbler frontend
- :members:
diff --git a/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst
index 0ce138a2..090ca5cd 100644
--- a/docs/modules/frontends/mpd.rst
+++ b/docs/modules/frontends/mpd.rst
@@ -4,7 +4,6 @@
.. automodule:: mopidy.frontends.mpd
:synopsis: MPD server frontend
- :members:
MPD dispatcher
@@ -27,6 +26,7 @@ Audio output
------------
.. automodule:: mopidy.frontends.mpd.protocol.audio_output
+ :synopsis: MPD protocol: audio output
:members:
@@ -34,6 +34,7 @@ Command list
------------
.. automodule:: mopidy.frontends.mpd.protocol.command_list
+ :synopsis: MPD protocol: command list
:members:
@@ -41,6 +42,7 @@ Connection
----------
.. automodule:: mopidy.frontends.mpd.protocol.connection
+ :synopsis: MPD protocol: connection
:members:
@@ -48,12 +50,15 @@ Current playlist
----------------
.. automodule:: mopidy.frontends.mpd.protocol.current_playlist
+ :synopsis: MPD protocol: current playlist
:members:
+
Music database
--------------
.. automodule:: mopidy.frontends.mpd.protocol.music_db
+ :synopsis: MPD protocol: music database
:members:
@@ -61,6 +66,7 @@ Playback
--------
.. automodule:: mopidy.frontends.mpd.protocol.playback
+ :synopsis: MPD protocol: playback
:members:
@@ -68,6 +74,7 @@ Reflection
----------
.. automodule:: mopidy.frontends.mpd.protocol.reflection
+ :synopsis: MPD protocol: reflection
:members:
@@ -75,6 +82,7 @@ Status
------
.. automodule:: mopidy.frontends.mpd.protocol.status
+ :synopsis: MPD protocol: status
:members:
@@ -82,6 +90,7 @@ Stickers
--------
.. automodule:: mopidy.frontends.mpd.protocol.stickers
+ :synopsis: MPD protocol: stickers
:members:
@@ -89,4 +98,5 @@ Stored playlists
----------------
.. automodule:: mopidy.frontends.mpd.protocol.stored_playlists
+ :synopsis: MPD protocol: stored playlists
:members:
diff --git a/docs/modules/frontends/mpris.rst b/docs/modules/frontends/mpris.rst
index 05a6e287..e0ec63da 100644
--- a/docs/modules/frontends/mpris.rst
+++ b/docs/modules/frontends/mpris.rst
@@ -1,7 +1,8 @@
+.. _mpris-frontend:
+
***********************************************
:mod:`mopidy.frontends.mpris` -- MPRIS frontend
***********************************************
.. automodule:: mopidy.frontends.mpris
:synopsis: MPRIS frontend
- :members:
diff --git a/docs/settings.rst b/docs/settings.rst
index a79dfd78..cb47a71f 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -19,8 +19,8 @@ You can either create the settings file yourself, or run the ``mopidy``
command, and it will create an empty settings file for you.
When you have created the settings file, open it in a text editor, and add
-settings you want to change. If you want to keep the default value for setting,
-you should *not* redefine it in your own settings file.
+settings you want to change. If you want to keep the default value for a
+setting, you should *not* redefine it in your own settings file.
A complete ``~/.config/mopidy/settings.py`` may look as simple as this::
@@ -29,6 +29,8 @@ A complete ``~/.config/mopidy/settings.py`` may look as simple as this::
SPOTIFY_PASSWORD = u'mysecret'
+.. _music-from-spotify:
+
Music from Spotify
==================
@@ -39,34 +41,26 @@ Premium account's username and password into the file, like this::
SPOTIFY_PASSWORD = u'mysecret'
+.. _music-from-local-storage:
+
Music from local storage
========================
If you want use Mopidy to play music you have locally at your machine instead
-of using Spotify, you need to change the backend from the default to
-:mod:`mopidy.backends.local` by adding the following line to your settings
-file::
-
- BACKENDS = (u'mopidy.backends.local.LocalBackend',)
-
-You may also want to change some of the ``LOCAL_*`` settings. See
-:mod:`mopidy.settings`, for a full list of available settings.
-
-.. note::
-
- Currently, Mopidy supports using Spotify *or* local storage as a music
- source. We're working on using both sources simultaneously, and will
- have support for this in a future release.
+of or in addition to using Spotify, you need to review and maybe change some of
+the ``LOCAL_*`` settings. See :mod:`mopidy.settings`, for a full list of
+available settings. Then you need to generate a tag cache for your local
+music...
-.. _generating_a_tag_cache:
+.. _generating-a-tag-cache:
Generating a tag cache
----------------------
Before Mopidy 0.3 the local storage backend relied purely on ``tag_cache``
files generated by the original MPD server. To remedy this the command
-:command:`mopidy-scan` has been created. The program will scan your current
+:command:`mopidy-scan` was created. The program will scan your current
:attr:`mopidy.settings.LOCAL_MUSIC_PATH` and build a MPD compatible
``tag_cache``.
@@ -90,7 +84,7 @@ To make a ``tag_cache`` of your local music available for Mopidy:
#. Start Mopidy, find the music library in a client, and play some local music!
-.. _use_mpd_on_a_network:
+.. _use-mpd-on-a-network:
Connecting from other machines on the network
=============================================
@@ -119,7 +113,7 @@ file::
LASTFM_PASSWORD = u'mysecret'
-.. _install_desktop_file:
+.. _install-desktop-file:
Controlling Mopidy through the Ubuntu Sound Menu
================================================
@@ -146,6 +140,41 @@ requirements of the `MPRIS specification `_. The
``TrackList`` and the ``Playlists`` interfaces of the spec are not supported.
+Using a custom audio sink
+=========================
+
+If you have successfully installed GStreamer, and then run the ``gst-inspect``
+or ``gst-inspect-0.10`` command, you should see a long listing of installed
+plugins, ending in a summary line::
+
+ $ gst-inspect-0.10
+ ... long list of installed plugins ...
+ Total count: 254 plugins (1 blacklist entry not shown), 1156 features
+
+Next, you should be able to produce a audible tone by running::
+
+ gst-launch-0.10 audiotestsrc ! audioresample ! autoaudiosink
+
+If you cannot hear any sound when running this command, you won't hear any
+sound from Mopidy either, as Mopidy by default uses GStreamer's
+``autoaudiosink`` to play audio. Thus, make this work before you file a bug
+against Mopidy.
+
+If you for some reason want to use some other GStreamer audio sink than
+``autoaudiosink``, you can set the setting :attr:`mopidy.settings.OUTPUT` to a
+partial GStreamer pipeline description describing the GStreamer sink you want
+to use.
+
+Example of ``settings.py`` for using OSS4::
+
+ OUTPUT = u'oss4sink'
+
+Again, this is the equivalent of the following ``gst-inspect`` command, so make
+this work first::
+
+ gst-launch-0.10 audiotestsrc ! audioresample ! oss4sink
+
+
Streaming audio through a SHOUTcast/Icecast server
==================================================
@@ -171,6 +200,21 @@ can use with the ``gst-launch-0.10`` command can be plugged into
:attr:`mopidy.settings.OUTPUT`.
+Custom settings
+===============
+
+Mopidy's settings validator will stop you from defining any settings in your
+settings file that Mopidy doesn't know about. This may sound obnoxious, but it
+helps you detect typos in your settings, and deprecated settings that should be
+removed or updated.
+
+If you're extending Mopidy in some way, and want to use Mopidy's settings
+system, you can prefix your settings with ``CUSTOM_`` to get around the
+settings validator. We recommend that you choose names like
+``CUSTOM_MYAPP_MYSETTING`` so that multiple custom extensions to Mopidy can be
+used at the same time without any danger of naming collisions.
+
+
Available settings
==================
diff --git a/mopidy/__init__.py b/mopidy/__init__.py
index 6436522e..918e1459 100644
--- a/mopidy/__init__.py
+++ b/mopidy/__init__.py
@@ -1,75 +1,30 @@
+from __future__ import unicode_literals
+
# pylint: disable = E0611,F0401
from distutils.version import StrictVersion as SV
# pylint: enable = E0611,F0401
import sys
+import warnings
import pykka
+
if not (2, 6) <= sys.version_info < (3,):
sys.exit(
- u'Mopidy requires Python >= 2.6, < 3, but found %s' %
+ 'Mopidy requires Python >= 2.6, < 3, but found %s' %
'.'.join(map(str, sys.version_info[:3])))
if (isinstance(pykka.__version__, basestring)
and not SV('1.0') <= SV(pykka.__version__) < SV('2.0')):
sys.exit(
- u'Mopidy requires Pykka >= 1.0, < 2, but found %s' % pykka.__version__)
+ 'Mopidy requires Pykka >= 1.0, < 2, but found %s' % pykka.__version__)
-import os
-import platform
-from subprocess import PIPE, Popen
-import glib
+warnings.filterwarnings('ignore', 'could not open display')
-__version__ = '0.8.1'
-DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy')
-CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy')
-SETTINGS_PATH = os.path.join(str(glib.get_user_config_dir()), 'mopidy')
-SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
+__version__ = '0.9.0'
-def get_version():
- try:
- return get_git_version()
- except EnvironmentError:
- return __version__
-
-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_platform():
- return platform.platform()
-
-def get_python():
- implementation = platform.python_implementation()
- version = platform.python_version()
- return u' '.join([implementation, version])
-
-class MopidyException(Exception):
- def __init__(self, message, *args, **kwargs):
- super(MopidyException, self).__init__(message, *args, **kwargs)
- self._message = message
-
- @property
- def message(self):
- """Reimplement message field that was deprecated in Python 2.6"""
- return self._message
-
- @message.setter
- def message(self, message):
- self._message = message
-
-class SettingsError(MopidyException):
- pass
-
-class OptionalDependencyError(MopidyException):
- pass
from mopidy import settings as default_settings_module
from mopidy.utils.settings import SettingsProxy
diff --git a/mopidy/__main__.py b/mopidy/__main__.py
index 35518874..952f158c 100644
--- a/mopidy/__main__.py
+++ b/mopidy/__main__.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import logging
import optparse
import os
@@ -24,127 +26,162 @@ sys.argv[1:] = gstreamer_args
# Add ../ to the path so we can run Mopidy from a Git checkout without
# installing it on the system.
-sys.path.insert(0,
- os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
+sys.path.insert(
+ 0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
-from mopidy import (get_version, settings, OptionalDependencyError,
- SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
+from mopidy import exceptions, settings
from mopidy.audio import Audio
-from mopidy.utils import get_class
-from mopidy.utils.deps import list_deps_optparse_callback
-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 (exit_handler, stop_remaining_actors,
- stop_actors_by_class)
-from mopidy.utils.settings import list_settings_optparse_callback
+from mopidy.core import Core
+from mopidy.utils import (
+ deps, importing, log, path, process, settings as settings_utils,
+ versioning)
logger = logging.getLogger('mopidy.main')
def main():
- signal.signal(signal.SIGTERM, exit_handler)
+ signal.signal(signal.SIGTERM, process.exit_handler)
+
loop = gobject.MainLoop()
options = parse_options()
+
+ if options.debug_thread or settings.DEBUG_THREAD:
+ debug_thread = process.DebugThread()
+ debug_thread.start()
+ signal.signal(signal.SIGUSR1, debug_thread.handler)
+
try:
- setup_logging(options.verbosity_level, options.save_debug_log)
+ log.setup_logging(options.verbosity_level, options.save_debug_log)
check_old_folders()
setup_settings(options.interactive)
- setup_audio()
- setup_backend()
- setup_frontends()
+ audio = setup_audio()
+ backends = setup_backends(audio)
+ core = setup_core(audio, backends)
+ setup_frontends(core)
loop.run()
- except SettingsError as e:
- logger.error(e.message)
+ except exceptions.SettingsError as ex:
+ logger.error(ex.message)
except KeyboardInterrupt:
- logger.info(u'Interrupted. Exiting...')
- except Exception as e:
- logger.exception(e)
+ logger.info('Interrupted. Exiting...')
+ except Exception as ex:
+ logger.exception(ex)
finally:
loop.quit()
stop_frontends()
- stop_backend()
+ stop_core()
+ stop_backends()
stop_audio()
- stop_remaining_actors()
+ process.stop_remaining_actors()
def parse_options():
- parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
- parser.add_option('--help-gst',
+ parser = optparse.OptionParser(
+ version='Mopidy %s' % versioning.get_version())
+ parser.add_option(
+ '--help-gst',
action='store_true', dest='help_gst',
help='show GStreamer help options')
- parser.add_option('-i', '--interactive',
+ parser.add_option(
+ '-i', '--interactive',
action='store_true', dest='interactive',
help='ask interactively for required settings which are missing')
- parser.add_option('-q', '--quiet',
+ parser.add_option(
+ '-q', '--quiet',
action='store_const', const=0, dest='verbosity_level',
help='less output (warning level)')
- parser.add_option('-v', '--verbose',
+ parser.add_option(
+ '-v', '--verbose',
action='count', default=1, dest='verbosity_level',
help='more output (debug level)')
- parser.add_option('--save-debug-log',
+ 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,
+ parser.add_option(
+ '--list-settings',
+ action='callback',
+ callback=settings_utils.list_settings_optparse_callback,
help='list current settings')
- parser.add_option('--list-deps',
- action='callback', callback=list_deps_optparse_callback,
+ parser.add_option(
+ '--list-deps',
+ action='callback', callback=deps.list_deps_optparse_callback,
help='list dependencies and their versions')
+ parser.add_option(
+ '--debug-thread',
+ action='store_true', dest='debug_thread',
+ help='run background thread that dumps tracebacks on SIGUSR1')
return parser.parse_args(args=mopidy_args)[0]
def check_old_folders():
- old_settings_folder = os.path.expanduser(u'~/.mopidy')
+ old_settings_folder = os.path.expanduser('~/.mopidy')
if not os.path.isdir(old_settings_folder):
return
- logger.warning(u'Old settings folder found at %s, settings.py should be '
- 'moved to %s, any cache data should be deleted. See release notes '
- 'for further instructions.', old_settings_folder, SETTINGS_PATH)
+ logger.warning(
+ 'Old settings folder found at %s, settings.py should be moved '
+ 'to %s, any cache data should be deleted. See release notes for '
+ 'further instructions.', old_settings_folder, path.SETTINGS_PATH)
def setup_settings(interactive):
- get_or_create_folder(SETTINGS_PATH)
- get_or_create_folder(DATA_PATH)
- get_or_create_file(SETTINGS_FILE)
+ path.get_or_create_folder(path.SETTINGS_PATH)
+ path.get_or_create_folder(path.DATA_PATH)
+ path.get_or_create_file(path.SETTINGS_FILE)
try:
settings.validate(interactive)
- except SettingsError, e:
- logger.error(e.message)
+ except exceptions.SettingsError as ex:
+ logger.error(ex.message)
sys.exit(1)
def setup_audio():
- Audio.start()
+ return Audio.start().proxy()
def stop_audio():
- stop_actors_by_class(Audio)
-
-def setup_backend():
- get_class(settings.BACKENDS[0]).start()
+ process.stop_actors_by_class(Audio)
-def stop_backend():
- stop_actors_by_class(get_class(settings.BACKENDS[0]))
+def setup_backends(audio):
+ backends = []
+ for backend_class_name in settings.BACKENDS:
+ backend_class = importing.get_class(backend_class_name)
+ backend = backend_class.start(audio=audio).proxy()
+ backends.append(backend)
+ return backends
-def setup_frontends():
+def stop_backends():
+ for backend_class_name in settings.BACKENDS:
+ process.stop_actors_by_class(importing.get_class(backend_class_name))
+
+
+def setup_core(audio, backends):
+ return Core.start(audio=audio, backends=backends).proxy()
+
+
+def stop_core():
+ process.stop_actors_by_class(Core)
+
+
+def setup_frontends(core):
for frontend_class_name in settings.FRONTENDS:
try:
- get_class(frontend_class_name).start()
- except OptionalDependencyError as e:
- logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
+ importing.get_class(frontend_class_name).start(core=core)
+ except exceptions.OptionalDependencyError as ex:
+ logger.info('Disabled: %s (%s)', frontend_class_name, ex)
def stop_frontends():
for frontend_class_name in settings.FRONTENDS:
try:
- stop_actors_by_class(get_class(frontend_class_name))
- except OptionalDependencyError:
+ frontend_class = importing.get_class(frontend_class_name)
+ process.stop_actors_by_class(frontend_class)
+ except exceptions.OptionalDependencyError:
pass
diff --git a/mopidy/audio/__init__.py b/mopidy/audio/__init__.py
index d630a0f0..7cf1dcee 100644
--- a/mopidy/audio/__init__.py
+++ b/mopidy/audio/__init__.py
@@ -1,407 +1,6 @@
-import pygst
-pygst.require('0.10')
-import gst
-import gobject
+from __future__ import unicode_literals
-import logging
-
-from pykka.actor import ThreadingActor
-from pykka.registry import ActorRegistry
-
-from mopidy import settings, utils
-from mopidy.backends.base import Backend
-from mopidy.utils import process
-
-# Trigger install of gst mixer plugins
-from mopidy.audio import mixers
-
-logger = logging.getLogger('mopidy.audio')
-
-
-class Audio(ThreadingActor):
- """
- Audio output through `GStreamer `_.
-
- **Settings:**
-
- - :attr:`mopidy.settings.OUTPUT`
- - :attr:`mopidy.settings.MIXER`
- - :attr:`mopidy.settings.MIXER_TRACK`
-
- """
-
- def __init__(self):
- super(Audio, self).__init__()
-
- self._default_caps = gst.Caps("""
- audio/x-raw-int,
- endianness=(int)1234,
- channels=(int)2,
- width=(int)16,
- depth=(int)16,
- signed=(boolean)true,
- rate=(int)44100""")
-
- self._playbin = None
- self._mixer = None
- self._mixer_track = None
- self._software_mixing = False
-
- self._message_processor_set_up = False
-
- def on_start(self):
- try:
- self._setup_playbin()
- self._setup_output()
- self._setup_mixer()
- self._setup_message_processor()
- except gobject.GError as ex:
- logger.exception(ex)
- process.exit_process()
-
- def on_stop(self):
- self._teardown_message_processor()
- self._teardown_mixer()
- self._teardown_playbin()
-
- def _setup_playbin(self):
- self._playbin = gst.element_factory_make('playbin2')
-
- fakesink = gst.element_factory_make('fakesink')
- self._playbin.set_property('video-sink', fakesink)
-
- self._playbin.connect('notify::source', self._on_new_source)
-
- def _on_new_source(self, element, pad):
- uri = element.get_property('uri')
- if not uri or not uri.startswith('appsrc://'):
- return
-
- # These caps matches the audio data provided by libspotify
- default_caps = gst.Caps(
- 'audio/x-raw-int, endianness=(int)1234, channels=(int)2, '
- 'width=(int)16, depth=(int)16, signed=(boolean)true, '
- 'rate=(int)44100')
- source = element.get_property('source')
- source.set_property('caps', default_caps)
-
- def _teardown_playbin(self):
- self._playbin.set_state(gst.STATE_NULL)
-
- def _setup_output(self):
- try:
- output = gst.parse_bin_from_description(
- settings.OUTPUT, ghost_unconnected_pads=True)
- self._playbin.set_property('audio-sink', output)
- logger.info('Output set to %s', settings.OUTPUT)
- except gobject.GError as ex:
- logger.error('Failed to create output "%s": %s',
- settings.OUTPUT, ex)
- process.exit_process()
-
- def _setup_mixer(self):
- if not settings.MIXER:
- logger.info('Not setting up mixer.')
- return
-
- if settings.MIXER == 'software':
- self._software_mixing = True
- logger.info('Mixer set to software mixing.')
- return
-
- try:
- mixerbin = gst.parse_bin_from_description(settings.MIXER,
- ghost_unconnected_pads=False)
- except gobject.GError as ex:
- logger.warning('Failed to create mixer "%s": %s',
- settings.MIXER, ex)
- return
-
- # We assume that the bin will contain a single mixer.
- mixer = mixerbin.get_by_interface('GstMixer')
- if not mixer:
- logger.warning('Did not find any mixers in %r', settings.MIXER)
- return
-
- if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS:
- logger.warning('Setting mixer %r to READY failed.', settings.MIXER)
- return
-
- track = self._select_mixer_track(mixer, settings.MIXER_TRACK)
- if not track:
- logger.warning('Could not find usable mixer track.')
- return
-
- self._mixer = mixer
- self._mixer_track = track
- logger.info('Mixer set to %s using track called %s',
- mixer.get_factory().get_name(), track.label)
-
- def _select_mixer_track(self, mixer, track_label):
- # Look for track with label == MIXER_TRACK, otherwise fallback to
- # master track which is also an output.
- for track in mixer.list_tracks():
- if track_label:
- if track.label == track_label:
- return track
- elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER |
- gst.interfaces.MIXER_TRACK_OUTPUT):
- return track
-
- def _teardown_mixer(self):
- if self._mixer is not None:
- self._mixer.set_state(gst.STATE_NULL)
-
- def _setup_message_processor(self):
- bus = self._playbin.get_bus()
- bus.add_signal_watch()
- bus.connect('message', self._on_message)
- self._message_processor_set_up = True
-
- def _teardown_message_processor(self):
- if self._message_processor_set_up:
- bus = self._playbin.get_bus()
- bus.remove_signal_watch()
-
- def _on_message(self, bus, message):
- if message.type == gst.MESSAGE_EOS:
- self._notify_backend_of_eos()
- elif message.type == gst.MESSAGE_ERROR:
- error, debug = message.parse_error()
- logger.error(u'%s %s', error, debug)
- self.stop_playback()
- elif message.type == gst.MESSAGE_WARNING:
- error, debug = message.parse_warning()
- logger.warning(u'%s %s', error, debug)
-
- def _notify_backend_of_eos(self):
- backend_refs = ActorRegistry.get_by_class(Backend)
- assert len(backend_refs) <= 1, 'Expected at most one running backend.'
- if backend_refs:
- logger.debug(u'Notifying backend of end-of-stream.')
- backend_refs[0].proxy().playback.on_end_of_track()
- else:
- logger.debug(u'No backend to notify of end-of-stream found.')
-
- def set_uri(self, uri):
- """
- Set URI of audio to be played.
-
- You *MUST* call :meth:`prepare_change` before calling this method.
-
- :param uri: the URI to play
- :type uri: string
- """
- self._playbin.set_property('uri', uri)
-
- def emit_data(self, capabilities, data):
- """
- Call this to deliver raw audio data to be played.
-
- Note that the uri must be set to ``appsrc://`` for this to work.
-
- :param capabilities: a GStreamer capabilities string
- :type capabilities: string
- :param data: raw audio data to be played
- """
- caps = gst.caps_from_string(capabilities)
- buffer_ = gst.Buffer(buffer(data))
- buffer_.set_caps(caps)
-
- source = self._playbin.get_property('source')
- source.set_property('caps', caps)
- source.emit('push-buffer', buffer_)
-
- def emit_end_of_stream(self):
- """
- Put an end-of-stream token on the playbin. This is typically used in
- combination with :meth:`emit_data`.
-
- We will get a GStreamer message when the stream playback reaches the
- token, and can then do any end-of-stream related tasks.
- """
- self._playbin.get_property('source').emit('end-of-stream')
-
- def get_position(self):
- """
- Get position in milliseconds.
-
- :rtype: int
- """
- if self._playbin.get_state()[1] == gst.STATE_NULL:
- return 0
- try:
- position = self._playbin.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):
- """
- Set position in milliseconds.
-
- :param position: the position in milliseconds
- :type volume: int
- :rtype: :class:`True` if successful, else :class:`False`
- """
- self._playbin.get_state() # block until state changes are done
- handeled = self._playbin.seek_simple(gst.Format(gst.FORMAT_TIME),
- gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
- self._playbin.get_state() # block until seek is done
- return handeled
-
- def start_playback(self):
- """
- Notify GStreamer that it should start playback.
-
- :rtype: :class:`True` if successfull, else :class:`False`
- """
- return self._set_state(gst.STATE_PLAYING)
-
- def pause_playback(self):
- """
- Notify GStreamer that it should pause playback.
-
- :rtype: :class:`True` if successfull, else :class:`False`
- """
- return self._set_state(gst.STATE_PAUSED)
-
- def prepare_change(self):
- """
- Notify GStreamer that we are about to change state of playback.
-
- This function *MUST* be called before changing URIs or doing
- changes like updating data that is being pushed. The reason for this
- is that GStreamer will reset all its state when it changes to
- :attr:`gst.STATE_READY`.
- """
- return self._set_state(gst.STATE_READY)
-
- def stop_playback(self):
- """
- Notify GStreamer that is should stop playback.
-
- :rtype: :class:`True` if successfull, else :class:`False`
- """
- return self._set_state(gst.STATE_NULL)
-
- def _set_state(self, state):
- """
- Internal method for setting the raw GStreamer state.
-
- .. digraph:: gst_state_transitions
-
- graph [rankdir="LR"];
- node [fontsize=10];
-
- "NULL" -> "READY"
- "PAUSED" -> "PLAYING"
- "PAUSED" -> "READY"
- "PLAYING" -> "PAUSED"
- "READY" -> "NULL"
- "READY" -> "PAUSED"
-
- :param state: State to set playbin to. One of: `gst.STATE_NULL`,
- `gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`.
- :type state: :class:`gst.State`
- :rtype: :class:`True` if successfull, else :class:`False`
- """
- result = self._playbin.set_state(state)
- if result == gst.STATE_CHANGE_FAILURE:
- logger.warning('Setting GStreamer state to %s: failed',
- state.value_name)
- return False
- elif result == gst.STATE_CHANGE_ASYNC:
- logger.debug('Setting GStreamer state to %s: async',
- state.value_name)
- return True
- else:
- logger.debug('Setting GStreamer state to %s: OK',
- state.value_name)
- return True
-
- def get_volume(self):
- """
- Get volume level of the installed mixer.
-
- Example values:
-
- 0:
- Muted.
- 100:
- Max volume for given system.
- :class:`None`:
- No mixer present, so the volume is unknown.
-
- :rtype: int in range [0..100] or :class:`None`
- """
- if self._software_mixing:
- return round(self._playbin.get_property('volume') * 100)
-
- if self._mixer is None:
- return None
-
- volumes = self._mixer.get_volume(self._mixer_track)
- avg_volume = float(sum(volumes)) / len(volumes)
-
- new_scale = (0, 100)
- old_scale = (self._mixer_track.min_volume, self._mixer_track.max_volume)
- return utils.rescale(avg_volume, old=old_scale, new=new_scale)
-
- def set_volume(self, volume):
- """
- Set volume level of the installed mixer.
-
- :param volume: the volume in the range [0..100]
- :type volume: int
- :rtype: :class:`True` if successful, else :class:`False`
- """
- if self._software_mixing:
- self._playbin.set_property('volume', volume / 100.0)
- return True
-
- if self._mixer is None:
- return False
-
- old_scale = (0, 100)
- new_scale = (self._mixer_track.min_volume, self._mixer_track.max_volume)
-
- volume = utils.rescale(volume, old=old_scale, new=new_scale)
-
- volumes = (volume,) * self._mixer_track.num_channels
- self._mixer.set_volume(self._mixer_track, volumes)
-
- return self._mixer.get_volume(self._mixer_track) == volumes
-
- def set_metadata(self, track):
- """
- Set track metadata for currently playing song.
-
- Only needs to be called by sources such as `appsrc` which do not
- already inject tags in playbin, e.g. when using :meth:`emit_data` to
- deliver raw audio data to GStreamer.
-
- :param track: the current track
- :type track: :class:`mopidy.models.Track`
- """
- taglist = gst.TagList()
- artists = [a for a in (track.artists or []) if a.name]
-
- # Default to blank data to trick shoutcast into clearing any previous
- # values it might have.
- taglist[gst.TAG_ARTIST] = u' '
- taglist[gst.TAG_TITLE] = u' '
- taglist[gst.TAG_ALBUM] = u' '
-
- if artists:
- taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists])
-
- if track.name:
- taglist[gst.TAG_TITLE] = track.name
-
- if track.album and track.album.name:
- taglist[gst.TAG_ALBUM] = track.album.name
-
- event = gst.event_new_tag(taglist)
- self._playbin.send_event(event)
+# flake8: noqa
+from .actor import Audio
+from .listener import AudioListener
+from .constants import PlaybackState
diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py
new file mode 100644
index 00000000..7de98075
--- /dev/null
+++ b/mopidy/audio/actor.py
@@ -0,0 +1,459 @@
+from __future__ import unicode_literals
+
+import pygst
+pygst.require('0.10')
+import gst
+import gobject
+
+import logging
+
+import pykka
+
+from mopidy import settings
+from mopidy.utils import process
+
+from . import mixers
+from .constants import PlaybackState
+from .listener import AudioListener
+
+logger = logging.getLogger('mopidy.audio')
+
+mixers.register_mixers()
+
+
+class Audio(pykka.ThreadingActor):
+ """
+ Audio output through `GStreamer `_.
+
+ **Settings:**
+
+ - :attr:`mopidy.settings.OUTPUT`
+ - :attr:`mopidy.settings.MIXER`
+ - :attr:`mopidy.settings.MIXER_TRACK`
+ """
+
+ #: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState`
+ state = PlaybackState.STOPPED
+
+ def __init__(self):
+ super(Audio, self).__init__()
+
+ self._playbin = None
+ self._mixer = None
+ self._mixer_track = None
+ self._software_mixing = False
+ self._appsrc = None
+
+ self._notify_source_signal_id = None
+ self._about_to_finish_id = None
+ self._message_signal_id = None
+
+ def on_start(self):
+ try:
+ self._setup_playbin()
+ self._setup_output()
+ self._setup_mixer()
+ self._setup_message_processor()
+ except gobject.GError as ex:
+ logger.exception(ex)
+ process.exit_process()
+
+ def on_stop(self):
+ self._teardown_message_processor()
+ self._teardown_mixer()
+ self._teardown_playbin()
+
+ def _setup_playbin(self):
+ self._playbin = gst.element_factory_make('playbin2')
+
+ fakesink = gst.element_factory_make('fakesink')
+ self._playbin.set_property('video-sink', fakesink)
+
+ self._about_to_finish_id = self._playbin.connect(
+ 'about-to-finish', self._on_about_to_finish)
+ self._notify_source_signal_id = self._playbin.connect(
+ 'notify::source', self._on_new_source)
+
+ def _on_about_to_finish(self, element):
+ self._appsrc = None
+
+ def _on_new_source(self, element, pad):
+ uri = element.get_property('uri')
+ if not uri or not uri.startswith('appsrc://'):
+ return
+
+ # These caps matches the audio data provided by libspotify
+ default_caps = gst.Caps(
+ b'audio/x-raw-int, endianness=(int)1234, channels=(int)2, '
+ b'width=(int)16, depth=(int)16, signed=(boolean)true, '
+ b'rate=(int)44100')
+ source = element.get_property('source')
+ source.set_property('caps', default_caps)
+ # GStreamer does not like unicode
+ source.set_property('format', b'time')
+
+ self._appsrc = source
+
+ def _teardown_playbin(self):
+ if self._about_to_finish_id:
+ self._playbin.disconnect(self._about_to_finish_id)
+ if self._notify_source_signal_id:
+ self._playbin.disconnect(self._notify_source_signal_id)
+ self._playbin.set_state(gst.STATE_NULL)
+
+ def _setup_output(self):
+ try:
+ output = gst.parse_bin_from_description(
+ settings.OUTPUT, ghost_unconnected_pads=True)
+ self._playbin.set_property('audio-sink', output)
+ logger.info('Audio output set to "%s"', settings.OUTPUT)
+ except gobject.GError as ex:
+ logger.error(
+ 'Failed to create audio output "%s": %s', settings.OUTPUT, ex)
+ process.exit_process()
+
+ def _setup_mixer(self):
+ if not settings.MIXER:
+ logger.info('Not setting up audio mixer')
+ return
+
+ if settings.MIXER == 'software':
+ self._software_mixing = True
+ logger.info('Audio mixer is using software mixing')
+ return
+
+ try:
+ mixerbin = gst.parse_bin_from_description(
+ settings.MIXER, ghost_unconnected_pads=False)
+ except gobject.GError as ex:
+ logger.warning(
+ 'Failed to create audio mixer "%s": %s', settings.MIXER, ex)
+ return
+
+ # We assume that the bin will contain a single mixer.
+ mixer = mixerbin.get_by_interface(b'GstMixer')
+ if not mixer:
+ logger.warning(
+ 'Did not find any audio mixers in "%s"', settings.MIXER)
+ return
+
+ if mixerbin.set_state(gst.STATE_READY) != gst.STATE_CHANGE_SUCCESS:
+ logger.warning(
+ 'Setting audio mixer "%s" to READY failed', settings.MIXER)
+ return
+
+ track = self._select_mixer_track(mixer, settings.MIXER_TRACK)
+ if not track:
+ logger.warning('Could not find usable audio mixer track')
+ return
+
+ self._mixer = mixer
+ self._mixer_track = track
+ logger.info(
+ 'Audio mixer set to "%s" using track "%s"',
+ mixer.get_factory().get_name(), track.label)
+
+ def _select_mixer_track(self, mixer, track_label):
+ # Look for track with label == MIXER_TRACK, otherwise fallback to
+ # master track which is also an output.
+ for track in mixer.list_tracks():
+ if track_label:
+ if track.label == track_label:
+ return track
+ elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER |
+ gst.interfaces.MIXER_TRACK_OUTPUT):
+ return track
+
+ def _teardown_mixer(self):
+ if self._mixer is not None:
+ self._mixer.set_state(gst.STATE_NULL)
+
+ def _setup_message_processor(self):
+ bus = self._playbin.get_bus()
+ bus.add_signal_watch()
+ self._message_signal_id = bus.connect('message', self._on_message)
+
+ def _teardown_message_processor(self):
+ if self._message_signal_id:
+ bus = self._playbin.get_bus()
+ bus.disconnect(self._message_signal_id)
+ bus.remove_signal_watch()
+
+ def _on_message(self, bus, message):
+ if (message.type == gst.MESSAGE_STATE_CHANGED
+ and message.src == self._playbin):
+ old_state, new_state, pending_state = message.parse_state_changed()
+ self._on_playbin_state_changed(old_state, new_state, pending_state)
+ elif message.type == gst.MESSAGE_EOS:
+ self._on_end_of_stream()
+ elif message.type == gst.MESSAGE_ERROR:
+ error, debug = message.parse_error()
+ logger.error('%s %s', error, debug)
+ self.stop_playback()
+ elif message.type == gst.MESSAGE_WARNING:
+ error, debug = message.parse_warning()
+ logger.warning('%s %s', error, debug)
+
+ def _on_playbin_state_changed(self, old_state, new_state, pending_state):
+ if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL:
+ # XXX: We're not called on the last state change when going down to
+ # NULL, so we rewrite the second to last call to get the expected
+ # behavior.
+ new_state = gst.STATE_NULL
+ pending_state = gst.STATE_VOID_PENDING
+
+ if pending_state != gst.STATE_VOID_PENDING:
+ return # Ignore intermediate state changes
+
+ if new_state == gst.STATE_READY:
+ return # Ignore READY state as it's GStreamer specific
+
+ if new_state == gst.STATE_PLAYING:
+ new_state = PlaybackState.PLAYING
+ elif new_state == gst.STATE_PAUSED:
+ new_state = PlaybackState.PAUSED
+ elif new_state == gst.STATE_NULL:
+ new_state = PlaybackState.STOPPED
+
+ old_state, self.state = self.state, new_state
+
+ logger.debug(
+ 'Triggering event: state_changed(old_state=%s, new_state=%s)',
+ old_state, new_state)
+ AudioListener.send(
+ 'state_changed', old_state=old_state, new_state=new_state)
+
+ def _on_end_of_stream(self):
+ logger.debug('Triggering reached_end_of_stream event')
+ AudioListener.send('reached_end_of_stream')
+
+ def set_uri(self, uri):
+ """
+ Set URI of audio to be played.
+
+ You *MUST* call :meth:`prepare_change` before calling this method.
+
+ :param uri: the URI to play
+ :type uri: string
+ """
+ self._playbin.set_property('uri', uri)
+
+ def emit_data(self, buffer_):
+ """
+ Call this to deliver raw audio data to be played.
+
+ Note that the uri must be set to ``appsrc://`` for this to work.
+
+ Returns true if data was delivered.
+
+ :param buffer_: buffer to pass to appsrc
+ :type buffer_: :class:`gst.Buffer`
+ :rtype: boolean
+ """
+ if not self._appsrc:
+ return False
+ return self._appsrc.emit('push-buffer', buffer_) == gst.FLOW_OK
+
+ def emit_end_of_stream(self):
+ """
+ Put an end-of-stream token on the playbin. This is typically used in
+ combination with :meth:`emit_data`.
+
+ We will get a GStreamer message when the stream playback reaches the
+ token, and can then do any end-of-stream related tasks.
+ """
+ self._playbin.get_property('source').emit('end-of-stream')
+
+ def get_position(self):
+ """
+ Get position in milliseconds.
+
+ :rtype: int
+ """
+ if self._playbin.get_state()[1] == gst.STATE_NULL:
+ return 0
+ try:
+ position = self._playbin.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):
+ """
+ Set position in milliseconds.
+
+ :param position: the position in milliseconds
+ :type position: int
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ self._playbin.get_state() # block until state changes are done
+ handeled = self._playbin.seek_simple(
+ gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH,
+ position * gst.MSECOND)
+ self._playbin.get_state() # block until seek is done
+ return handeled
+
+ def start_playback(self):
+ """
+ Notify GStreamer that it should start playback.
+
+ :rtype: :class:`True` if successfull, else :class:`False`
+ """
+ return self._set_state(gst.STATE_PLAYING)
+
+ def pause_playback(self):
+ """
+ Notify GStreamer that it should pause playback.
+
+ :rtype: :class:`True` if successfull, else :class:`False`
+ """
+ return self._set_state(gst.STATE_PAUSED)
+
+ def prepare_change(self):
+ """
+ Notify GStreamer that we are about to change state of playback.
+
+ This function *MUST* be called before changing URIs or doing
+ changes like updating data that is being pushed. The reason for this
+ is that GStreamer will reset all its state when it changes to
+ :attr:`gst.STATE_READY`.
+ """
+ return self._set_state(gst.STATE_READY)
+
+ def stop_playback(self):
+ """
+ Notify GStreamer that is should stop playback.
+
+ :rtype: :class:`True` if successfull, else :class:`False`
+ """
+ return self._set_state(gst.STATE_NULL)
+
+ def _set_state(self, state):
+ """
+ Internal method for setting the raw GStreamer state.
+
+ .. digraph:: gst_state_transitions
+
+ graph [rankdir="LR"];
+ node [fontsize=10];
+
+ "NULL" -> "READY"
+ "PAUSED" -> "PLAYING"
+ "PAUSED" -> "READY"
+ "PLAYING" -> "PAUSED"
+ "READY" -> "NULL"
+ "READY" -> "PAUSED"
+
+ :param state: State to set playbin to. One of: `gst.STATE_NULL`,
+ `gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`.
+ :type state: :class:`gst.State`
+ :rtype: :class:`True` if successfull, else :class:`False`
+ """
+ result = self._playbin.set_state(state)
+ if result == gst.STATE_CHANGE_FAILURE:
+ logger.warning(
+ 'Setting GStreamer state to %s failed', state.value_name)
+ return False
+ elif result == gst.STATE_CHANGE_ASYNC:
+ logger.debug(
+ 'Setting GStreamer state to %s is async', state.value_name)
+ return True
+ else:
+ logger.debug(
+ 'Setting GStreamer state to %s is OK', state.value_name)
+ return True
+
+ def get_volume(self):
+ """
+ Get volume level of the installed mixer.
+
+ Example values:
+
+ 0:
+ Muted.
+ 100:
+ Max volume for given system.
+ :class:`None`:
+ No mixer present, so the volume is unknown.
+
+ :rtype: int in range [0..100] or :class:`None`
+ """
+ if self._software_mixing:
+ return int(round(self._playbin.get_property('volume') * 100))
+
+ if self._mixer is None:
+ return None
+
+ volumes = self._mixer.get_volume(self._mixer_track)
+ avg_volume = float(sum(volumes)) / len(volumes)
+
+ new_scale = (0, 100)
+ old_scale = (
+ self._mixer_track.min_volume, self._mixer_track.max_volume)
+ return self._rescale(avg_volume, old=old_scale, new=new_scale)
+
+ def set_volume(self, volume):
+ """
+ Set volume level of the installed mixer.
+
+ :param volume: the volume in the range [0..100]
+ :type volume: int
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ if self._software_mixing:
+ self._playbin.set_property('volume', volume / 100.0)
+ return True
+
+ if self._mixer is None:
+ return False
+
+ old_scale = (0, 100)
+ new_scale = (
+ self._mixer_track.min_volume, self._mixer_track.max_volume)
+
+ volume = self._rescale(volume, old=old_scale, new=new_scale)
+
+ volumes = (volume,) * self._mixer_track.num_channels
+ self._mixer.set_volume(self._mixer_track, volumes)
+
+ return self._mixer.get_volume(self._mixer_track) == volumes
+
+ def _rescale(self, value, old=None, new=None):
+ """Convert value between scales."""
+ new_min, new_max = new
+ old_min, old_max = old
+ scaling = float(new_max - new_min) / (old_max - old_min)
+ return int(round(scaling * (value - old_min) + new_min))
+
+ def set_metadata(self, track):
+ """
+ Set track metadata for currently playing song.
+
+ Only needs to be called by sources such as `appsrc` which do not
+ already inject tags in playbin, e.g. when using :meth:`emit_data` to
+ deliver raw audio data to GStreamer.
+
+ :param track: the current track
+ :type track: :class:`mopidy.models.Track`
+ """
+ taglist = gst.TagList()
+ artists = [a for a in (track.artists or []) if a.name]
+
+ # Default to blank data to trick shoutcast into clearing any previous
+ # values it might have.
+ taglist[gst.TAG_ARTIST] = ' '
+ taglist[gst.TAG_TITLE] = ' '
+ taglist[gst.TAG_ALBUM] = ' '
+
+ if artists:
+ taglist[gst.TAG_ARTIST] = ', '.join([a.name for a in artists])
+
+ if track.name:
+ taglist[gst.TAG_TITLE] = track.name
+
+ if track.album and track.album.name:
+ taglist[gst.TAG_ALBUM] = track.album.name
+
+ event = gst.event_new_tag(taglist)
+ self._playbin.send_event(event)
diff --git a/mopidy/audio/constants.py b/mopidy/audio/constants.py
new file mode 100644
index 00000000..08ad9768
--- /dev/null
+++ b/mopidy/audio/constants.py
@@ -0,0 +1,16 @@
+from __future__ import unicode_literals
+
+
+class PlaybackState(object):
+ """
+ Enum of playback states.
+ """
+
+ #: Constant representing the paused state.
+ PAUSED = 'paused'
+
+ #: Constant representing the playing state.
+ PLAYING = 'playing'
+
+ #: Constant representing the stopped state.
+ STOPPED = 'stopped'
diff --git a/mopidy/audio/listener.py b/mopidy/audio/listener.py
new file mode 100644
index 00000000..da5f7b39
--- /dev/null
+++ b/mopidy/audio/listener.py
@@ -0,0 +1,45 @@
+from __future__ import unicode_literals
+
+import pykka
+
+
+class AudioListener(object):
+ """
+ Marker interface for recipients of events sent by the audio actor.
+
+ Any Pykka actor that mixes in this class will receive calls to the methods
+ defined here when the corresponding events happen in the core actor. This
+ interface is used both for looking up what actors to notify of the events,
+ and for providing default implementations for those listeners that are not
+ interested in all events.
+ """
+
+ @staticmethod
+ def send(event, **kwargs):
+ """Helper to allow calling of audio listener events"""
+ listeners = pykka.ActorRegistry.get_by_class(AudioListener)
+ for listener in listeners:
+ getattr(listener.proxy(), event)(**kwargs)
+
+ def reached_end_of_stream(self):
+ """
+ Called whenever the end of the audio stream is reached.
+
+ *MAY* be implemented by actor.
+ """
+ pass
+
+ def state_changed(self, old_state, new_state):
+ """
+ Called after the playback state have changed.
+
+ Will be called for both immediate and async state changes in GStreamer.
+
+ *MAY* be implemented by actor.
+
+ :param old_state: the state before the change
+ :type old_state: string from :class:`mopidy.core.PlaybackState` field
+ :param new_state: the state after the change
+ :type new_state: string from :class:`mopidy.core.PlaybackState` field
+ """
+ pass
diff --git a/mopidy/audio/mixers/__init__.py b/mopidy/audio/mixers/__init__.py
index a0247519..feaccc3d 100644
--- a/mopidy/audio/mixers/__init__.py
+++ b/mopidy/audio/mixers/__init__.py
@@ -1,43 +1,22 @@
+from __future__ import unicode_literals
+
import pygst
pygst.require('0.10')
import gst
import gobject
-
-def create_track(label, initial_volume, min_volume, max_volume,
- num_channels, flags):
- class Track(gst.interfaces.MixerTrack):
- def __init__(self):
- super(Track, self).__init__()
- self.volumes = (initial_volume,) * self.num_channels
-
- @gobject.property
- def label(self):
- return label
-
- @gobject.property
- def min_volume(self):
- return min_volume
-
- @gobject.property
- def max_volume(self):
- return max_volume
-
- @gobject.property
- def num_channels(self):
- return num_channels
-
- @gobject.property
- def flags(self):
- return flags
-
- return Track()
-
-
-# Import all mixers so that they are registered with GStreamer.
-#
-# Keep these imports at the bottom of the file to avoid cyclic import problems
-# when mixers use the above code.
from .auto import AutoAudioMixer
from .fake import FakeMixer
from .nad import NadMixer
+
+
+def register_mixer(mixer_class):
+ gobject.type_register(mixer_class)
+ gst.element_register(
+ mixer_class, mixer_class.__name__.lower(), gst.RANK_MARGINAL)
+
+
+def register_mixers():
+ register_mixer(AutoAudioMixer)
+ register_mixer(FakeMixer)
+ register_mixer(NadMixer)
diff --git a/mopidy/audio/mixers/auto.py b/mopidy/audio/mixers/auto.py
index 1233afa3..bd61445e 100644
--- a/mopidy/audio/mixers/auto.py
+++ b/mopidy/audio/mixers/auto.py
@@ -1,6 +1,21 @@
+"""Mixer element that automatically selects the real mixer to use.
+
+This is Mopidy's default mixer.
+
+**Dependencies:**
+
+- None
+
+**Settings:**
+
+- If this wasn't the default, you would set :attr:`mopidy.settings.MIXER`
+ to ``autoaudiomixer`` to use this mixer.
+"""
+
+from __future__ import unicode_literals
+
import pygst
pygst.require('0.10')
-import gobject
import gst
import logging
@@ -10,16 +25,19 @@ logger = logging.getLogger('mopidy.audio.mixers.auto')
# TODO: we might want to add some ranking to the mixers we know about?
class AutoAudioMixer(gst.Bin):
- __gstdetails__ = ('AutoAudioMixer',
- 'Mixer',
- 'Element automatically selects a mixer.',
- 'Thomas Adamcik')
+ __gstdetails__ = (
+ 'AutoAudioMixer',
+ 'Mixer',
+ 'Element automatically selects a mixer.',
+ 'Mopidy')
def __init__(self):
gst.Bin.__init__(self)
mixer = self._find_mixer()
if mixer:
+ # pylint: disable=E1101
self.add(mixer)
+ # pylint: enable=E1101
logger.debug('AutoAudioMixer chose: %s', mixer.get_name())
else:
logger.debug('AutoAudioMixer did not find any usable mixers')
@@ -66,7 +84,3 @@ class AutoAudioMixer(gst.Bin):
if track.flags & flags:
return True
return False
-
-
-gobject.type_register(AutoAudioMixer)
-gst.element_register(AutoAudioMixer, 'autoaudiomixer', gst.RANK_MARGINAL)
diff --git a/mopidy/audio/mixers/fake.py b/mopidy/audio/mixers/fake.py
index c5faa03f..948ab82e 100644
--- a/mopidy/audio/mixers/fake.py
+++ b/mopidy/audio/mixers/fake.py
@@ -1,31 +1,41 @@
+"""Fake mixer for use in tests.
+
+**Dependencies:**
+
+- None
+
+**Settings:**
+
+- Set :attr:`mopidy.settings.MIXER` to ``fakemixer`` to use this mixer.
+"""
+
+from __future__ import unicode_literals
+
import pygst
pygst.require('0.10')
import gobject
import gst
-from mopidy.audio.mixers import create_track
+from . import utils
class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
- __gstdetails__ = ('FakeMixer',
- 'Mixer',
- 'Fake mixer for use in tests.',
- 'Thomas Adamcik')
+ __gstdetails__ = (
+ 'FakeMixer',
+ 'Mixer',
+ 'Fake mixer for use in tests.',
+ 'Mopidy')
track_label = gobject.property(type=str, default='Master')
track_initial_volume = gobject.property(type=int, default=0)
track_min_volume = gobject.property(type=int, default=0)
track_max_volume = gobject.property(type=int, default=100)
track_num_channels = gobject.property(type=int, default=2)
- track_flags = gobject.property(type=int,
- default=(gst.interfaces.MIXER_TRACK_MASTER |
- gst.interfaces.MIXER_TRACK_OUTPUT))
-
- def __init__(self):
- gst.Element.__init__(self)
+ track_flags = gobject.property(type=int, default=(
+ gst.interfaces.MIXER_TRACK_MASTER | gst.interfaces.MIXER_TRACK_OUTPUT))
def list_tracks(self):
- track = create_track(
+ track = utils.create_track(
self.track_label,
self.track_initial_volume,
self.track_min_volume,
@@ -42,7 +52,3 @@ class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
def set_record(self, track, record):
pass
-
-
-gobject.type_register(FakeMixer)
-gst.element_register(FakeMixer, 'fakemixer', gst.RANK_MARGINAL)
diff --git a/mopidy/audio/mixers/nad.py b/mopidy/audio/mixers/nad.py
index 667dee53..b5cb522d 100644
--- a/mopidy/audio/mixers/nad.py
+++ b/mopidy/audio/mixers/nad.py
@@ -1,3 +1,52 @@
+"""Mixer that controls volume using a NAD amplifier.
+
+**Dependencies:**
+
+- pyserial (python-serial in Debian/Ubuntu)
+
+- The NAD amplifier must be connected to the machine running Mopidy using a
+ serial cable.
+
+**Settings:**
+
+- Set :attr:`mopidy.settings.MIXER` to ``nadmixer`` to use it. You probably
+ also needs to add some properties to the ``MIXER`` setting.
+
+Supported properties includes:
+
+``port``:
+ The serial device to use, defaults to ``/dev/ttyUSB0``. This must be
+ set correctly for the mixer to work.
+
+``source``:
+ The source that should be selected on the amplifier, like ``aux``,
+ ``disc``, ``tape``, ``tuner``, etc. Leave unset if you don't want the
+ mixer to change it for you.
+
+``speakers-a``:
+ Set to ``on`` or ``off`` if you want the mixer to make sure that
+ speaker set A is turned on or off. Leave unset if you don't want the
+ mixer to change it for you.
+
+``speakers-b``:
+ See ``speakers-a``.
+
+Configuration examples::
+
+ # Minimum configuration, if the amplifier is available at /dev/ttyUSB0
+ MIXER = u'nadmixer'
+
+ # Minimum configuration, if the amplifier is available elsewhere
+ MIXER = u'nadmixer port=/dev/ttyUSB3'
+
+ # Full configuration
+ MIXER = (
+ u'nadmixer port=/dev/ttyUSB0 '
+ u'source=aux speakers-a=on speakers-b=off')
+"""
+
+from __future__ import unicode_literals
+
import logging
import pygst
@@ -8,41 +57,41 @@ import gst
try:
import serial
except ImportError:
- serial = None
+ serial = None # noqa
-from pykka.actor import ThreadingActor
+import pykka
-from mopidy.audio.mixers import create_track
+from . import utils
logger = logging.getLogger('mopidy.audio.mixers.nad')
class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
- __gstdetails__ = ('NadMixer',
- 'Mixer',
- 'Mixer to control NAD amplifiers using a serial link',
- 'Stein Magnus Jodal')
+ __gstdetails__ = (
+ 'NadMixer',
+ 'Mixer',
+ 'Mixer to control NAD amplifiers using a serial link',
+ 'Mopidy')
port = gobject.property(type=str, default='/dev/ttyUSB0')
source = gobject.property(type=str)
speakers_a = gobject.property(type=str)
speakers_b = gobject.property(type=str)
- def __init__(self):
- gst.Element.__init__(self)
- self._volume_cache = 0
- self._nad_talker = None
+ _volume_cache = 0
+ _nad_talker = None
def list_tracks(self):
- track = create_track(
+ track = utils.create_track(
label='Master',
initial_volume=0,
min_volume=0,
max_volume=100,
num_channels=1,
- flags=(gst.interfaces.MIXER_TRACK_MASTER |
- gst.interfaces.MIXER_TRACK_OUTPUT))
+ flags=(
+ gst.interfaces.MIXER_TRACK_MASTER |
+ gst.interfaces.MIXER_TRACK_OUTPUT))
return [track]
def get_volume(self, track):
@@ -60,7 +109,7 @@ class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
def do_change_state(self, transition):
if transition == gst.STATE_CHANGE_NULL_TO_READY:
if serial is None:
- logger.warning(u'nadmixer dependency python-serial not found')
+ logger.warning('nadmixer dependency python-serial not found')
return gst.STATE_CHANGE_FAILURE
self._start_nad_talker()
return gst.STATE_CHANGE_SUCCESS
@@ -74,13 +123,9 @@ class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
).proxy()
-gobject.type_register(NadMixer)
-gst.element_register(NadMixer, 'nadmixer', gst.RANK_MARGINAL)
-
-
-class NadTalker(ThreadingActor):
+class NadTalker(pykka.ThreadingActor):
"""
- Independent thread which does the communication with the NAD amplifier
+ Independent thread which does the communication with the NAD amplifier.
Since the communication is done in an independent thread, Mopidy won't
block other requests while doing rather time consuming work like
@@ -121,8 +166,7 @@ class NadTalker(ThreadingActor):
self._set_device_to_known_state()
def _open_connection(self):
- logger.info(u'NAD amplifier: Connecting through "%s"',
- self.port)
+ logger.info('NAD amplifier: Connecting through "%s"', self.port)
self._device = serial.Serial(
port=self.port,
baudrate=self.BAUDRATE,
@@ -137,11 +181,11 @@ class NadTalker(ThreadingActor):
self._select_speakers()
self._select_input_source()
self.mute(False)
- self._calibrate_volume()
+ self.calibrate_volume()
def _get_device_model(self):
model = self._ask_device('Main.Model')
- logger.info(u'NAD amplifier: Connected to model "%s"', model)
+ logger.info('NAD amplifier: Connected to model "%s"', model)
return model
def _power_device_on(self):
@@ -163,19 +207,26 @@ class NadTalker(ThreadingActor):
else:
self._check_and_set('Main.Mute', 'Off')
- def _calibrate_volume(self):
+ def calibrate_volume(self, current_nad_volume=None):
# The NAD C 355BEE amplifier has 40 different volume levels. We have no
# way of asking on which level we are. Thus, we must calibrate the
# mixer by decreasing the volume 39 times.
- logger.info(u'NAD amplifier: Calibrating by setting volume to 0')
- self._nad_volume = self.VOLUME_LEVELS
- self.set_volume(0)
- logger.info(u'NAD amplifier: Done calibrating')
+ if current_nad_volume is None:
+ current_nad_volume = self.VOLUME_LEVELS
+ if current_nad_volume == self.VOLUME_LEVELS:
+ logger.info('NAD amplifier: Calibrating by setting volume to 0')
+ self._nad_volume = current_nad_volume
+ if self._decrease_volume():
+ current_nad_volume -= 1
+ if current_nad_volume == 0:
+ logger.info('NAD amplifier: Done calibrating')
+ else:
+ self.actor_ref.proxy().calibrate_volume(current_nad_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)
+ logger.debug('Setting volume to %d' % volume)
target_nad_volume = int(round(volume * self.VOLUME_LEVELS / 100.0))
if self._nad_volume is None:
return # Calibration needed
@@ -200,11 +251,13 @@ class NadTalker(ThreadingActor):
for attempt in range(1, 4):
if self._ask_device(key) == value:
return
- logger.info(u'NAD amplifier: Setting "%s" to "%s" (attempt %d/3)',
+ logger.info(
+ 'NAD amplifier: Setting "%s" to "%s" (attempt %d/3)',
key, value, attempt)
self._command_device(key, value)
if self._ask_device(key) != value:
- logger.info(u'NAD amplifier: Gave up on setting "%s" to "%s"',
+ logger.info(
+ 'NAD amplifier: Gave up on setting "%s" to "%s"',
key, value)
def _ask_device(self, key):
diff --git a/mopidy/audio/mixers/utils.py b/mopidy/audio/mixers/utils.py
new file mode 100644
index 00000000..8d0ce280
--- /dev/null
+++ b/mopidy/audio/mixers/utils.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+import pygst
+pygst.require('0.10')
+import gst
+import gobject
+
+
+def create_track(label, initial_volume, min_volume, max_volume,
+ num_channels, flags):
+
+ class Track(gst.interfaces.MixerTrack):
+ def __init__(self):
+ super(Track, self).__init__()
+ self.volumes = (initial_volume,) * self.num_channels
+
+ @gobject.property
+ def label(self):
+ return label
+
+ @gobject.property
+ def min_volume(self):
+ return min_volume
+
+ @gobject.property
+ def max_volume(self):
+ return max_volume
+
+ @gobject.property
+ def num_channels(self):
+ return num_channels
+
+ @gobject.property
+ def flags(self):
+ return flags
+
+ return Track()
diff --git a/mopidy/backends/__init__.py b/mopidy/backends/__init__.py
index e69de29b..baffc488 100644
--- a/mopidy/backends/__init__.py
+++ b/mopidy/backends/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py
new file mode 100644
index 00000000..8250a24c
--- /dev/null
+++ b/mopidy/backends/base.py
@@ -0,0 +1,233 @@
+from __future__ import unicode_literals
+
+import copy
+
+
+class Backend(object):
+ #: Actor proxy to an instance of :class:`mopidy.audio.Audio`.
+ #:
+ #: Should be passed to the backend constructor as the kwarg ``audio``,
+ #: which will then set this field.
+ audio = None
+
+ #: The library provider. An instance of
+ #: :class:`mopidy.backends.base.BaseLibraryProvider`, or :class:`None` if
+ #: the backend doesn't provide a library.
+ library = None
+
+ #: The playback provider. An instance of
+ #: :class:`mopidy.backends.base.BasePlaybackProvider`, or :class:`None` if
+ #: the backend doesn't provide playback.
+ playback = None
+
+ #: The playlists provider. An instance of
+ #: :class:`mopidy.backends.base.BasePlaylistsProvider`, or class:`None` if
+ #: the backend doesn't provide playlists.
+ playlists = None
+
+ #: List of URI schemes this backend can handle.
+ uri_schemes = []
+
+ # Because the providers is marked as pykka_traversible, we can't get() them
+ # from another actor, and need helper methods to check if the providers are
+ # set or None.
+
+ def has_library(self):
+ return self.library is not None
+
+ def has_playback(self):
+ return self.playback is not None
+
+ def has_playlists(self):
+ return self.playlists is not None
+
+
+class BaseLibraryProvider(object):
+ """
+ :param backend: backend the controller is a part of
+ :type backend: :class:`mopidy.backends.base.Backend`
+ """
+
+ pykka_traversable = True
+
+ def __init__(self, backend):
+ self.backend = backend
+
+ def find_exact(self, **query):
+ """
+ See :meth:`mopidy.core.LibraryController.find_exact`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def lookup(self, uri):
+ """
+ See :meth:`mopidy.core.LibraryController.lookup`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def refresh(self, uri=None):
+ """
+ See :meth:`mopidy.core.LibraryController.refresh`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def search(self, **query):
+ """
+ See :meth:`mopidy.core.LibraryController.search`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+
+class BasePlaybackProvider(object):
+ """
+ :param audio: the audio actor
+ :type audio: actor proxy to an instance of :class:`mopidy.audio.Audio`
+ :param backend: the backend
+ :type backend: :class:`mopidy.backends.base.Backend`
+ """
+
+ pykka_traversable = True
+
+ def __init__(self, audio, backend):
+ self.audio = audio
+ self.backend = backend
+
+ def pause(self):
+ """
+ Pause playback.
+
+ *MAY be reimplemented by subclass.*
+
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ return self.audio.pause_playback().get()
+
+ def play(self, track):
+ """
+ Play given track.
+
+ *MAY be reimplemented by subclass.*
+
+ :param track: the track to play
+ :type track: :class:`mopidy.models.Track`
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ self.audio.prepare_change()
+ self.audio.set_uri(track.uri).get()
+ return self.audio.start_playback().get()
+
+ def resume(self):
+ """
+ Resume playback at the same time position playback was paused.
+
+ *MAY be reimplemented by subclass.*
+
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ return self.audio.start_playback().get()
+
+ def seek(self, time_position):
+ """
+ Seek to a given time position.
+
+ *MAY be reimplemented by subclass.*
+
+ :param time_position: time position in milliseconds
+ :type time_position: int
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ return self.audio.set_position(time_position).get()
+
+ def stop(self):
+ """
+ Stop playback.
+
+ *MAY be reimplemented by subclass.*
+
+ :rtype: :class:`True` if successful, else :class:`False`
+ """
+ return self.audio.stop_playback().get()
+
+ def get_time_position(self):
+ """
+ Get the current time position in milliseconds.
+
+ *MAY be reimplemented by subclass.*
+
+ :rtype: int
+ """
+ return self.audio.get_position().get()
+
+
+class BasePlaylistsProvider(object):
+ """
+ :param backend: backend the controller is a part of
+ :type backend: :class:`mopidy.backends.base.Backend`
+ """
+
+ pykka_traversable = True
+
+ def __init__(self, backend):
+ self.backend = backend
+ self._playlists = []
+
+ @property
+ def playlists(self):
+ """
+ Currently available playlists.
+
+ Read/write. List of :class:`mopidy.models.Playlist`.
+ """
+ return copy.copy(self._playlists)
+
+ @playlists.setter # noqa
+ def playlists(self, playlists):
+ self._playlists = playlists
+
+ def create(self, name):
+ """
+ See :meth:`mopidy.core.PlaylistsController.create`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def delete(self, uri):
+ """
+ See :meth:`mopidy.core.PlaylistsController.delete`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def lookup(self, uri):
+ """
+ See :meth:`mopidy.core.PlaylistsController.lookup`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def refresh(self):
+ """
+ See :meth:`mopidy.core.PlaylistsController.refresh`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
+
+ def save(self, playlist):
+ """
+ See :meth:`mopidy.core.PlaylistsController.save`.
+
+ *MUST be implemented by subclass.*
+ """
+ raise NotImplementedError
diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py
deleted file mode 100644
index e6c8b70a..00000000
--- a/mopidy/backends/base/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from .library import BaseLibraryProvider
-from .playback import BasePlaybackProvider
-from .stored_playlists import BaseStoredPlaylistsProvider
-
-
-class Backend(object):
- #: The current playlist controller. An instance of
- #: :class:`mopidy.backends.base.CurrentPlaylistController`.
- current_playlist = None
-
- #: The library controller. An instance of
- # :class:`mopidy.backends.base.LibraryController`.
- library = None
-
- #: The playback controller. An instance of
- #: :class:`mopidy.backends.base.PlaybackController`.
- playback = None
-
- #: The stored playlists controller. An instance of
- #: :class:`mopidy.backends.base.StoredPlaylistsController`.
- stored_playlists = None
-
- #: List of URI schemes this backend can handle.
- uri_schemes = []
diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py
deleted file mode 100644
index 837eef49..00000000
--- a/mopidy/backends/base/library.py
+++ /dev/null
@@ -1,42 +0,0 @@
-class BaseLibraryProvider(object):
- """
- :param backend: backend the controller is a part of
- :type backend: :class:`mopidy.backends.base.Backend`
- """
-
- pykka_traversable = True
-
- def __init__(self, backend):
- self.backend = backend
-
- def find_exact(self, **query):
- """
- See :meth:`mopidy.backends.base.LibraryController.find_exact`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def lookup(self, uri):
- """
- See :meth:`mopidy.backends.base.LibraryController.lookup`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def refresh(self, uri=None):
- """
- See :meth:`mopidy.backends.base.LibraryController.refresh`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def search(self, **query):
- """
- See :meth:`mopidy.backends.base.LibraryController.search`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py
deleted file mode 100644
index ae5a4383..00000000
--- a/mopidy/backends/base/playback.py
+++ /dev/null
@@ -1,87 +0,0 @@
-class BasePlaybackProvider(object):
- """
- :param backend: the backend
- :type backend: :class:`mopidy.backends.base.Backend`
- """
-
- pykka_traversable = True
-
- def __init__(self, backend):
- self.backend = backend
-
- def pause(self):
- """
- Pause playback.
-
- *MAY be reimplemented by subclass.*
-
- :rtype: :class:`True` if successful, else :class:`False`
- """
- return self.backend.audio.pause_playback().get()
-
- def play(self, track):
- """
- Play given track.
-
- *MAY be reimplemented by subclass.*
-
- :param track: the track to play
- :type track: :class:`mopidy.models.Track`
- :rtype: :class:`True` if successful, else :class:`False`
- """
- self.backend.audio.prepare_change()
- self.backend.audio.set_uri(track.uri).get()
- return self.backend.audio.start_playback().get()
-
- def resume(self):
- """
- Resume playback at the same time position playback was paused.
-
- *MAY be reimplemented by subclass.*
-
- :rtype: :class:`True` if successful, else :class:`False`
- """
- return self.backend.audio.start_playback().get()
-
- def seek(self, time_position):
- """
- Seek to a given time position.
-
- *MAY be reimplemented by subclass.*
-
- :param time_position: time position in milliseconds
- :type time_position: int
- :rtype: :class:`True` if successful, else :class:`False`
- """
- return self.backend.audio.set_position(time_position).get()
-
- def stop(self):
- """
- Stop playback.
-
- *MAY be reimplemented by subclass.*
-
- :rtype: :class:`True` if successful, else :class:`False`
- """
- return self.backend.audio.stop_playback().get()
-
- def get_volume(self):
- """
- Get current volume
-
- *MAY be reimplemented by subclass.*
-
- :rtype: int [0..100] or :class:`None`
- """
- return self.backend.audio.get_volume().get()
-
- def set_volume(self, volume):
- """
- Get current volume
-
- *MAY be reimplemented by subclass.*
-
- :param: volume
- :type volume: int [0..100]
- """
- self.backend.audio.set_volume(volume)
diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py
deleted file mode 100644
index d1d52c9a..00000000
--- a/mopidy/backends/base/stored_playlists.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from copy import copy
-
-
-class BaseStoredPlaylistsProvider(object):
- """
- :param backend: backend the controller is a part of
- :type backend: :class:`mopidy.backends.base.Backend`
- """
-
- pykka_traversable = True
-
- def __init__(self, backend):
- self.backend = backend
- self._playlists = []
-
- @property
- def playlists(self):
- """
- Currently stored playlists.
-
- Read/write. List of :class:`mopidy.models.Playlist`.
- """
- return copy(self._playlists)
-
- @playlists.setter
- def playlists(self, playlists):
- self._playlists = playlists
-
- def create(self, name):
- """
- See :meth:`mopidy.backends.base.StoredPlaylistsController.create`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def delete(self, playlist):
- """
- See :meth:`mopidy.backends.base.StoredPlaylistsController.delete`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def lookup(self, uri):
- """
- See :meth:`mopidy.backends.base.StoredPlaylistsController.lookup`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def refresh(self):
- """
- See :meth:`mopidy.backends.base.StoredPlaylistsController.refresh`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def rename(self, playlist, new_name):
- """
- See :meth:`mopidy.backends.base.StoredPlaylistsController.rename`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def save(self, playlist):
- """
- See :meth:`mopidy.backends.base.StoredPlaylistsController.save`.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py
new file mode 100644
index 00000000..39180bbb
--- /dev/null
+++ b/mopidy/backends/dummy.py
@@ -0,0 +1,111 @@
+"""A dummy backend for use in tests.
+
+This backend implements the backend API in the simplest way possible. It is
+used in tests of the frontends.
+
+The backend handles URIs starting with ``dummy:``.
+
+**Dependencies:**
+
+- None
+
+**Settings:**
+
+- None
+"""
+
+from __future__ import unicode_literals
+
+import pykka
+
+from mopidy.backends import base
+from mopidy.models import Playlist
+
+
+class DummyBackend(pykka.ThreadingActor, base.Backend):
+ def __init__(self, audio):
+ super(DummyBackend, self).__init__()
+
+ self.library = DummyLibraryProvider(backend=self)
+ self.playback = DummyPlaybackProvider(audio=audio, backend=self)
+ self.playlists = DummyPlaylistsProvider(backend=self)
+
+ self.uri_schemes = ['dummy']
+
+
+class DummyLibraryProvider(base.BaseLibraryProvider):
+ def __init__(self, *args, **kwargs):
+ super(DummyLibraryProvider, self).__init__(*args, **kwargs)
+ self.dummy_library = []
+ self.dummy_find_exact_result = []
+ self.dummy_search_result = []
+
+ def find_exact(self, **query):
+ return self.dummy_find_exact_result
+
+ def lookup(self, uri):
+ return filter(lambda t: uri == t.uri, self.dummy_library)
+
+ def refresh(self, uri=None):
+ pass
+
+ def search(self, **query):
+ return self.dummy_search_result
+
+
+class DummyPlaybackProvider(base.BasePlaybackProvider):
+ def __init__(self, *args, **kwargs):
+ super(DummyPlaybackProvider, self).__init__(*args, **kwargs)
+ self._time_position = 0
+
+ def pause(self):
+ return True
+
+ def play(self, track):
+ """Pass a track with URI 'dummy:error' to force failure"""
+ self._time_position = 0
+ return track.uri != 'dummy:error'
+
+ def resume(self):
+ return True
+
+ def seek(self, time_position):
+ self._time_position = time_position
+ return True
+
+ def stop(self):
+ return True
+
+ def get_time_position(self):
+ return self._time_position
+
+
+class DummyPlaylistsProvider(base.BasePlaylistsProvider):
+ def create(self, name):
+ playlist = Playlist(name=name, uri='dummy:%s' % name)
+ self._playlists.append(playlist)
+ return playlist
+
+ def delete(self, uri):
+ playlist = self.lookup(uri)
+ if playlist:
+ self._playlists.remove(playlist)
+
+ def lookup(self, uri):
+ for playlist in self._playlists:
+ if playlist.uri == uri:
+ return playlist
+
+ def refresh(self):
+ pass
+
+ def save(self, playlist):
+ old_playlist = self.lookup(playlist.uri)
+
+ if old_playlist is not None:
+ index = self._playlists.index(old_playlist)
+ self._playlists[index] = playlist
+ else:
+ self._playlists.append(playlist)
+
+ return playlist
diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py
deleted file mode 100644
index 8eb9029c..00000000
--- a/mopidy/backends/dummy/__init__.py
+++ /dev/null
@@ -1,104 +0,0 @@
-from pykka.actor import ThreadingActor
-
-from mopidy import core
-from mopidy.backends import base
-from mopidy.models import Playlist
-
-
-class DummyBackend(ThreadingActor, base.Backend):
- """
- A backend which implements the backend API in the simplest way possible.
- Used in tests of the frontends.
-
- Handles URIs starting with ``dummy:``.
- """
-
- def __init__(self, *args, **kwargs):
- super(DummyBackend, self).__init__()
-
- self.current_playlist = core.CurrentPlaylistController(backend=self)
-
- library_provider = DummyLibraryProvider(backend=self)
- self.library = core.LibraryController(backend=self,
- provider=library_provider)
-
- playback_provider = DummyPlaybackProvider(backend=self)
- self.playback = core.PlaybackController(backend=self,
- provider=playback_provider)
-
- stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self)
- self.stored_playlists = core.StoredPlaylistsController(backend=self,
- provider=stored_playlists_provider)
-
- self.uri_schemes = [u'dummy']
-
-
-class DummyLibraryProvider(base.BaseLibraryProvider):
- def __init__(self, *args, **kwargs):
- super(DummyLibraryProvider, self).__init__(*args, **kwargs)
- self.dummy_library = []
-
- def find_exact(self, **query):
- return Playlist()
-
- def lookup(self, uri):
- matches = filter(lambda t: uri == t.uri, self.dummy_library)
- if matches:
- return matches[0]
-
- def refresh(self, uri=None):
- pass
-
- def search(self, **query):
- return Playlist()
-
-
-class DummyPlaybackProvider(base.BasePlaybackProvider):
- def __init__(self, *args, **kwargs):
- super(DummyPlaybackProvider, self).__init__(*args, **kwargs)
- self._volume = None
-
- def pause(self):
- return True
-
- def play(self, track):
- """Pass None as track to force failure"""
- return track is not None
-
- def resume(self):
- return True
-
- def seek(self, time_position):
- return True
-
- def stop(self):
- return True
-
- def get_volume(self):
- return self._volume
-
- def set_volume(self, volume):
- self._volume = volume
-
-
-class DummyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
- def create(self, name):
- playlist = Playlist(name=name)
- self._playlists.append(playlist)
- return playlist
-
- def delete(self, playlist):
- self._playlists.remove(playlist)
-
- def lookup(self, uri):
- return filter(lambda p: p.uri == uri, self._playlists)
-
- def refresh(self):
- pass
-
- def rename(self, playlist, new_name):
- self._playlists[self._playlists.index(playlist)] = \
- playlist.copy(name=new_name)
-
- def save(self, playlist):
- self._playlists.append(playlist)
diff --git a/mopidy/backends/listener.py b/mopidy/backends/listener.py
new file mode 100644
index 00000000..30b3291d
--- /dev/null
+++ b/mopidy/backends/listener.py
@@ -0,0 +1,32 @@
+from __future__ import unicode_literals
+
+import pykka
+
+
+class BackendListener(object):
+ """
+ Marker interface for recipients of events sent by the backend actors.
+
+ Any Pykka actor that mixes in this class will receive calls to the methods
+ defined here when the corresponding events happen in the core actor. This
+ interface is used both for looking up what actors to notify of the events,
+ and for providing default implementations for those listeners that are not
+ interested in all events.
+
+ Normally, only the Core actor should mix in this class.
+ """
+
+ @staticmethod
+ def send(event, **kwargs):
+ """Helper to allow calling of backend listener events"""
+ listeners = pykka.ActorRegistry.get_by_class(BackendListener)
+ for listener in listeners:
+ getattr(listener.proxy(), event)(**kwargs)
+
+ def playlists_loaded(self):
+ """
+ Called when playlists are loaded or refreshed.
+
+ *MAY* be implemented by actor.
+ """
+ pass
diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py
index 6488d97c..8ee58d3b 100644
--- a/mopidy/backends/local/__init__.py
+++ b/mopidy/backends/local/__init__.py
@@ -1,243 +1,26 @@
-import glob
-import glib
-import logging
-import os
-import shutil
+"""A backend for playing music from a local music archive.
-from pykka.actor import ThreadingActor
-from pykka.registry import ActorRegistry
+This backend handles URIs starting with ``file:``.
-from mopidy import audio, core, settings
-from mopidy.backends import base
-from mopidy.models import Playlist, Track, Album
+See :ref:`music-from-local-storage` for further instructions on using this
+backend.
-from .translator import parse_m3u, parse_mpd_tag_cache
+**Issues:**
-logger = logging.getLogger(u'mopidy.backends.local')
+https://github.com/mopidy/mopidy/issues?labels=Local+backend
+**Dependencies:**
-class LocalBackend(ThreadingActor, base.Backend):
- """
- A backend for playing music from a local music archive.
+- None
- **Dependencies:**
+**Settings:**
- - None
+- :attr:`mopidy.settings.LOCAL_MUSIC_PATH`
+- :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH`
+- :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE`
+"""
- **Settings:**
+from __future__ import unicode_literals
- - :attr:`mopidy.settings.LOCAL_MUSIC_PATH`
- - :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH`
- - :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE`
- """
-
- def __init__(self, *args, **kwargs):
- super(LocalBackend, self).__init__()
-
- self.current_playlist = core.CurrentPlaylistController(backend=self)
-
- library_provider = LocalLibraryProvider(backend=self)
- self.library = core.LibraryController(backend=self,
- provider=library_provider)
-
- playback_provider = base.BasePlaybackProvider(backend=self)
- self.playback = LocalPlaybackController(backend=self,
- provider=playback_provider)
-
- stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self)
- self.stored_playlists = core.StoredPlaylistsController(backend=self,
- provider=stored_playlists_provider)
-
- self.uri_schemes = [u'file']
-
- self.audio = None
-
- def on_start(self):
- audio_refs = ActorRegistry.get_by_class(audio.Audio)
- assert len(audio_refs) == 1, \
- 'Expected exactly one running Audio instance.'
- self.audio = audio_refs[0].proxy()
-
-
-class LocalPlaybackController(core.PlaybackController):
- def __init__(self, *args, **kwargs):
- super(LocalPlaybackController, self).__init__(*args, **kwargs)
-
- # XXX Why do we call stop()? Is it to set GStreamer state to 'READY'?
- self.stop()
-
- @property
- def time_position(self):
- return self.backend.audio.get_position().get()
-
-
-class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
- def __init__(self, *args, **kwargs):
- super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
- self._folder = settings.LOCAL_PLAYLIST_PATH
- self.refresh()
-
- def lookup(self, uri):
- pass # TODO
-
- def refresh(self):
- playlists = []
-
- logger.info('Loading playlists from %s', self._folder)
-
- for m3u in glob.glob(os.path.join(self._folder, '*.m3u')):
- name = os.path.basename(m3u)[:-len('.m3u')]
- tracks = []
- for uri in parse_m3u(m3u, settings.LOCAL_MUSIC_PATH):
- try:
- tracks.append(self.backend.library.lookup(uri))
- except LookupError, e:
- logger.error('Playlist item could not be added: %s', e)
- playlist = Playlist(tracks=tracks, name=name)
-
- # FIXME playlist name needs better handling
- # FIXME tracks should come from lib. lookup
-
- playlists.append(playlist)
-
- self.playlists = playlists
-
- def create(self, name):
- playlist = Playlist(name=name)
- self.save(playlist)
- return playlist
-
- def delete(self, playlist):
- if playlist not in self._playlists:
- return
-
- self._playlists.remove(playlist)
- filename = os.path.join(self._folder, playlist.name + '.m3u')
-
- if os.path.exists(filename):
- os.remove(filename)
-
- def rename(self, playlist, name):
- if playlist not in self._playlists:
- return
-
- src = os.path.join(self._folder, playlist.name + '.m3u')
- dst = os.path.join(self._folder, name + '.m3u')
-
- renamed = playlist.copy(name=name)
- index = self._playlists.index(playlist)
- self._playlists[index] = renamed
-
- shutil.move(src, dst)
-
- def save(self, playlist):
- file_path = os.path.join(self._folder, playlist.name + '.m3u')
-
- # FIXME this should be a save_m3u function, not inside save
- with open(file_path, 'w') as file_handle:
- for track in playlist.tracks:
- if track.uri.startswith('file://'):
- file_handle.write(track.uri[len('file://'):] + '\n')
- else:
- file_handle.write(track.uri + '\n')
-
- self._playlists.append(playlist)
-
-
-class LocalLibraryProvider(base.BaseLibraryProvider):
- def __init__(self, *args, **kwargs):
- super(LocalLibraryProvider, self).__init__(*args, **kwargs)
- self._uri_mapping = {}
- self.refresh()
-
- def refresh(self, uri=None):
- tracks = parse_mpd_tag_cache(settings.LOCAL_TAG_CACHE_FILE,
- settings.LOCAL_MUSIC_PATH)
-
- logger.info('Loading tracks in %s from %s', settings.LOCAL_MUSIC_PATH,
- settings.LOCAL_TAG_CACHE_FILE)
-
- for track in tracks:
- self._uri_mapping[track.uri] = track
-
- def lookup(self, uri):
- try:
- return self._uri_mapping[uri]
- except KeyError:
- logger.debug(u'Failed to lookup "%s"', uri)
- return None
-
- def find_exact(self, **query):
- self._validate_query(query)
- result_tracks = self._uri_mapping.values()
-
- for (field, values) in query.iteritems():
- if not hasattr(values, '__iter__'):
- values = [values]
- # FIXME this is bound to be slow for large libraries
- for value in values:
- q = value.strip()
-
- track_filter = lambda t: q == t.name
- album_filter = lambda t: q == getattr(t, 'album', Album()).name
- artist_filter = lambda t: filter(
- lambda a: q == a.name, t.artists)
- uri_filter = lambda t: q == t.uri
- any_filter = lambda t: (track_filter(t) or album_filter(t) or
- artist_filter(t) or uri_filter(t))
-
- if field == 'track':
- result_tracks = filter(track_filter, result_tracks)
- elif field == 'album':
- result_tracks = filter(album_filter, result_tracks)
- elif field == 'artist':
- result_tracks = filter(artist_filter, result_tracks)
- elif field == 'uri':
- result_tracks = filter(uri_filter, result_tracks)
- elif field == 'any':
- result_tracks = filter(any_filter, result_tracks)
- else:
- raise LookupError('Invalid lookup field: %s' % field)
- return Playlist(tracks=result_tracks)
-
- def search(self, **query):
- self._validate_query(query)
- result_tracks = self._uri_mapping.values()
-
- for (field, values) in query.iteritems():
- if not hasattr(values, '__iter__'):
- values = [values]
- # FIXME this is bound to be slow for large libraries
- for value in values:
- q = value.strip().lower()
-
- track_filter = lambda t: q in t.name.lower()
- album_filter = lambda t: q in getattr(
- t, 'album', Album()).name.lower()
- artist_filter = lambda t: filter(
- lambda a: q in a.name.lower(), t.artists)
- uri_filter = lambda t: q in t.uri.lower()
- any_filter = lambda t: track_filter(t) or album_filter(t) or \
- artist_filter(t) or uri_filter(t)
-
- if field == 'track':
- result_tracks = filter(track_filter, result_tracks)
- elif field == 'album':
- result_tracks = filter(album_filter, result_tracks)
- elif field == 'artist':
- result_tracks = filter(artist_filter, result_tracks)
- elif field == 'uri':
- result_tracks = filter(uri_filter, result_tracks)
- elif field == 'any':
- result_tracks = filter(any_filter, result_tracks)
- else:
- raise LookupError('Invalid lookup field: %s' % field)
- return Playlist(tracks=result_tracks)
-
- def _validate_query(self, query):
- for (_, values) in query.iteritems():
- if not values:
- raise LookupError('Missing query')
- for value in values:
- if not value:
- raise LookupError('Missing query')
+# flake8: noqa
+from .actor import LocalBackend
diff --git a/mopidy/backends/local/actor.py b/mopidy/backends/local/actor.py
new file mode 100644
index 00000000..75baeab2
--- /dev/null
+++ b/mopidy/backends/local/actor.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+
+import logging
+
+import pykka
+
+from mopidy.backends import base
+
+from .library import LocalLibraryProvider
+from .playlists import LocalPlaylistsProvider
+
+logger = logging.getLogger('mopidy.backends.local')
+
+
+class LocalBackend(pykka.ThreadingActor, base.Backend):
+ def __init__(self, audio):
+ super(LocalBackend, self).__init__()
+
+ self.library = LocalLibraryProvider(backend=self)
+ self.playback = base.BasePlaybackProvider(audio=audio, backend=self)
+ self.playlists = LocalPlaylistsProvider(backend=self)
+
+ self.uri_schemes = ['file']
diff --git a/mopidy/backends/local/library.py b/mopidy/backends/local/library.py
new file mode 100644
index 00000000..e0e6f423
--- /dev/null
+++ b/mopidy/backends/local/library.py
@@ -0,0 +1,112 @@
+from __future__ import unicode_literals
+
+import logging
+
+from mopidy import settings
+from mopidy.backends import base
+from mopidy.models import Album
+
+from .translator import parse_mpd_tag_cache
+
+logger = logging.getLogger('mopidy.backends.local')
+
+
+class LocalLibraryProvider(base.BaseLibraryProvider):
+ def __init__(self, *args, **kwargs):
+ super(LocalLibraryProvider, self).__init__(*args, **kwargs)
+ self._uri_mapping = {}
+ self.refresh()
+
+ def refresh(self, uri=None):
+ tracks = parse_mpd_tag_cache(
+ settings.LOCAL_TAG_CACHE_FILE, settings.LOCAL_MUSIC_PATH)
+
+ logger.info(
+ 'Loading tracks from %s using %s',
+ settings.LOCAL_MUSIC_PATH, settings.LOCAL_TAG_CACHE_FILE)
+
+ for track in tracks:
+ self._uri_mapping[track.uri] = track
+
+ def lookup(self, uri):
+ try:
+ return [self._uri_mapping[uri]]
+ except KeyError:
+ logger.debug('Failed to lookup %r', uri)
+ return []
+
+ def find_exact(self, **query):
+ self._validate_query(query)
+ result_tracks = self._uri_mapping.values()
+
+ for (field, values) in query.iteritems():
+ if not hasattr(values, '__iter__'):
+ values = [values]
+ # FIXME this is bound to be slow for large libraries
+ for value in values:
+ q = value.strip()
+
+ track_filter = lambda t: q == t.name
+ album_filter = lambda t: q == getattr(t, 'album', Album()).name
+ artist_filter = lambda t: filter(
+ lambda a: q == a.name, t.artists)
+ uri_filter = lambda t: q == t.uri
+ any_filter = lambda t: (
+ track_filter(t) or album_filter(t) or
+ artist_filter(t) or uri_filter(t))
+
+ if field == 'track':
+ result_tracks = filter(track_filter, result_tracks)
+ elif field == 'album':
+ result_tracks = filter(album_filter, result_tracks)
+ elif field == 'artist':
+ result_tracks = filter(artist_filter, result_tracks)
+ elif field == 'uri':
+ result_tracks = filter(uri_filter, result_tracks)
+ elif field == 'any':
+ result_tracks = filter(any_filter, result_tracks)
+ else:
+ raise LookupError('Invalid lookup field: %s' % field)
+ return result_tracks
+
+ def search(self, **query):
+ self._validate_query(query)
+ result_tracks = self._uri_mapping.values()
+
+ for (field, values) in query.iteritems():
+ if not hasattr(values, '__iter__'):
+ values = [values]
+ # FIXME this is bound to be slow for large libraries
+ for value in values:
+ q = value.strip().lower()
+
+ track_filter = lambda t: q in t.name.lower()
+ album_filter = lambda t: q in getattr(
+ t, 'album', Album()).name.lower()
+ artist_filter = lambda t: filter(
+ lambda a: q in a.name.lower(), t.artists)
+ uri_filter = lambda t: q in t.uri.lower()
+ any_filter = lambda t: track_filter(t) or album_filter(t) or \
+ artist_filter(t) or uri_filter(t)
+
+ if field == 'track':
+ result_tracks = filter(track_filter, result_tracks)
+ elif field == 'album':
+ result_tracks = filter(album_filter, result_tracks)
+ elif field == 'artist':
+ result_tracks = filter(artist_filter, result_tracks)
+ elif field == 'uri':
+ result_tracks = filter(uri_filter, result_tracks)
+ elif field == 'any':
+ result_tracks = filter(any_filter, result_tracks)
+ else:
+ raise LookupError('Invalid lookup field: %s' % field)
+ return result_tracks
+
+ def _validate_query(self, query):
+ for (_, values) in query.iteritems():
+ if not values:
+ raise LookupError('Missing query')
+ for value in values:
+ if not value:
+ raise LookupError('Missing query')
diff --git a/mopidy/backends/local/playlists.py b/mopidy/backends/local/playlists.py
new file mode 100644
index 00000000..666532c5
--- /dev/null
+++ b/mopidy/backends/local/playlists.py
@@ -0,0 +1,119 @@
+from __future__ import unicode_literals
+
+import glob
+import logging
+import os
+import shutil
+
+from mopidy import settings
+from mopidy.backends import base, listener
+from mopidy.models import Playlist
+from mopidy.utils import formatting, path
+
+from .translator import parse_m3u
+
+
+logger = logging.getLogger('mopidy.backends.local')
+
+
+class LocalPlaylistsProvider(base.BasePlaylistsProvider):
+ def __init__(self, *args, **kwargs):
+ super(LocalPlaylistsProvider, self).__init__(*args, **kwargs)
+ self._path = settings.LOCAL_PLAYLIST_PATH
+ self.refresh()
+
+ def create(self, name):
+ name = formatting.slugify(name)
+ uri = path.path_to_uri(self._get_m3u_path(name))
+ playlist = Playlist(uri=uri, name=name)
+ return self.save(playlist)
+
+ def delete(self, uri):
+ playlist = self.lookup(uri)
+ if not playlist:
+ return
+
+ self._playlists.remove(playlist)
+ self._delete_m3u(playlist.uri)
+
+ def lookup(self, uri):
+ for playlist in self._playlists:
+ if playlist.uri == uri:
+ return playlist
+
+ def refresh(self):
+ logger.info('Loading playlists from %s', self._path)
+
+ playlists = []
+
+ for m3u in glob.glob(os.path.join(self._path, '*.m3u')):
+ uri = path.path_to_uri(m3u)
+ name = os.path.splitext(os.path.basename(m3u))[0]
+
+ tracks = []
+ for track_uri in parse_m3u(m3u, settings.LOCAL_MUSIC_PATH):
+ try:
+ # TODO We must use core.library.lookup() to support tracks
+ # from other backends
+ tracks += self.backend.library.lookup(track_uri)
+ except LookupError as ex:
+ logger.error('Playlist item could not be added: %s', ex)
+
+ playlist = Playlist(uri=uri, name=name, tracks=tracks)
+ playlists.append(playlist)
+
+ self.playlists = playlists
+ listener.BackendListener.send('playlists_loaded')
+
+ def save(self, playlist):
+ assert playlist.uri, 'Cannot save playlist without URI'
+
+ old_playlist = self.lookup(playlist.uri)
+
+ if old_playlist and playlist.name != old_playlist.name:
+ playlist = playlist.copy(name=formatting.slugify(playlist.name))
+ playlist = self._rename_m3u(playlist)
+
+ self._save_m3u(playlist)
+
+ if old_playlist is not None:
+ index = self._playlists.index(old_playlist)
+ self._playlists[index] = playlist
+ else:
+ self._playlists.append(playlist)
+
+ return playlist
+
+ def _get_m3u_path(self, name):
+ name = formatting.slugify(name)
+ file_path = os.path.join(self._path, name + '.m3u')
+ path.check_file_path_is_inside_base_dir(file_path, self._path)
+ return file_path
+
+ def _save_m3u(self, playlist):
+ file_path = path.uri_to_path(playlist.uri)
+ path.check_file_path_is_inside_base_dir(file_path, self._path)
+ with open(file_path, 'w') as file_handle:
+ for track in playlist.tracks:
+ if track.uri.startswith('file://'):
+ uri = path.uri_to_path(track.uri)
+ else:
+ uri = track.uri
+ file_handle.write(uri + '\n')
+
+ def _delete_m3u(self, uri):
+ file_path = path.uri_to_path(uri)
+ path.check_file_path_is_inside_base_dir(file_path, self._path)
+ if os.path.exists(file_path):
+ os.remove(file_path)
+
+ def _rename_m3u(self, playlist):
+ src_file_path = path.uri_to_path(playlist.uri)
+ path.check_file_path_is_inside_base_dir(src_file_path, self._path)
+
+ dst_file_path = self._get_m3u_path(playlist.name)
+ path.check_file_path_is_inside_base_dir(dst_file_path, self._path)
+
+ shutil.move(src_file_path, dst_file_path)
+
+ return playlist.copy(uri=path.path_to_uri(dst_file_path))
diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py
index 1fea555c..21e389ea 100644
--- a/mopidy/backends/local/translator.py
+++ b/mopidy/backends/local/translator.py
@@ -1,14 +1,16 @@
-import logging
-import os
+from __future__ import unicode_literals
-logger = logging.getLogger('mopidy.backends.local.translator')
+import logging
from mopidy.models import Track, Artist, Album
-from mopidy.utils import locale_decode
+from mopidy.utils.encoding import locale_decode
from mopidy.utils.path import path_to_uri
+logger = logging.getLogger('mopidy.backends.local')
+
+
def parse_m3u(file_path, music_folder):
- """
+ r"""
Convert M3U file list of uris
Example M3U data::
@@ -51,6 +53,7 @@ def parse_m3u(file_path, music_folder):
return uris
+
def parse_mpd_tag_cache(tag_cache, music_dir=''):
"""
Converts a MPD tag_cache into a lists of tracks, artists and albums.
@@ -67,19 +70,19 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
current = {}
state = None
- for line in contents.split('\n'):
- if line == 'songList begin':
+ for line in contents.split(b'\n'):
+ if line == b'songList begin':
state = 'songs'
continue
- elif line == 'songList end':
+ elif line == b'songList end':
state = None
continue
elif not state:
continue
- key, value = line.split(': ', 1)
+ key, value = line.split(b': ', 1)
- if key == 'key':
+ if key == b'key':
_convert_mpd_data(current, tracks, music_dir)
current.clear()
@@ -89,6 +92,7 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''):
return tracks
+
def _convert_mpd_data(data, tracks, music_dir):
if not data:
return
@@ -128,7 +132,8 @@ def _convert_mpd_data(data, tracks, music_dir):
artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid']
if 'musicbrainz_albumartistid' in data:
- albumartist_kwargs['musicbrainz_id'] = data['musicbrainz_albumartistid']
+ albumartist_kwargs['musicbrainz_id'] = (
+ data['musicbrainz_albumartistid'])
if data['file'][0] == '/':
path = data['file'][1:]
@@ -142,7 +147,7 @@ def _convert_mpd_data(data, tracks, music_dir):
if albumartist_kwargs:
albumartist = Artist(**albumartist_kwargs)
album_kwargs['artists'] = [albumartist]
-
+
if album_kwargs:
album = Album(**album_kwargs)
track_kwargs['album'] = album
diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py
index 3811458d..141656cc 100644
--- a/mopidy/backends/spotify/__init__.py
+++ b/mopidy/backends/spotify/__init__.py
@@ -1,94 +1,36 @@
-import logging
+"""A backend for playing music from Spotify
-from pykka.actor import ThreadingActor
-from pykka.registry import ActorRegistry
+`Spotify `_ is a music streaming service. The backend
+uses the official `libspotify
+`_ library and the
+`pyspotify `_ Python bindings for
+libspotify. This backend handles URIs starting with ``spotify:``.
-from mopidy import audio, core, settings
-from mopidy.backends import base
+See :ref:`music-from-spotify` for further instructions on using this backend.
-logger = logging.getLogger('mopidy.backends.spotify')
+.. note::
-BITRATES = {96: 2, 160: 0, 320: 1}
+ This product uses SPOTIFY(R) CORE but is not endorsed, certified or
+ otherwise approved in any way by Spotify. Spotify is the registered
+ trade mark of the Spotify Group.
-class SpotifyBackend(ThreadingActor, base.Backend):
- """
- A backend for playing music from the `Spotify `_
- music streaming service. The backend uses the official `libspotify
- `_ library and the
- `pyspotify `_ Python bindings for
- libspotify.
+**Issues:**
- .. note::
+https://github.com/mopidy/mopidy/issues?labels=Spotify+backend
- This product uses SPOTIFY(R) CORE but is not endorsed, certified or
- otherwise approved in any way by Spotify. Spotify is the registered
- trade mark of the Spotify Group.
+**Dependencies:**
- **Issues:**
- https://github.com/mopidy/mopidy/issues?labels=backend-spotify
+- libspotify >= 12, < 13 (libspotify12 package from apt.mopidy.com)
+- pyspotify >= 1.9, < 1.10 (python-spotify package from apt.mopidy.com)
- **Dependencies:**
+**Settings:**
- - libspotify >= 10, < 11 (libspotify10 package from apt.mopidy.com)
- - pyspotify >= 1.5 (python-spotify package from apt.mopidy.com)
+- :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`
+- :attr:`mopidy.settings.SPOTIFY_USERNAME`
+- :attr:`mopidy.settings.SPOTIFY_PASSWORD`
+"""
- **Settings:**
+from __future__ import unicode_literals
- - :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`
- - :attr:`mopidy.settings.SPOTIFY_USERNAME`
- - :attr:`mopidy.settings.SPOTIFY_PASSWORD`
- """
-
- # Imports inside methods are to prevent loading of __init__.py to fail on
- # missing spotify dependencies.
-
- def __init__(self, *args, **kwargs):
- from .library import SpotifyLibraryProvider
- from .playback import SpotifyPlaybackProvider
- from .stored_playlists import SpotifyStoredPlaylistsProvider
-
- super(SpotifyBackend, self).__init__()
-
- self.current_playlist = core.CurrentPlaylistController(backend=self)
-
- library_provider = SpotifyLibraryProvider(backend=self)
- self.library = core.LibraryController(backend=self,
- provider=library_provider)
-
- playback_provider = SpotifyPlaybackProvider(backend=self)
- self.playback = core.PlaybackController(backend=self,
- provider=playback_provider)
-
- stored_playlists_provider = SpotifyStoredPlaylistsProvider(
- backend=self)
- self.stored_playlists = core.StoredPlaylistsController(backend=self,
- provider=stored_playlists_provider)
-
- self.uri_schemes = [u'spotify']
-
- self.audio = None
- self.spotify = None
-
- # Fail early if settings are not present
- self.username = settings.SPOTIFY_USERNAME
- self.password = settings.SPOTIFY_PASSWORD
-
- def on_start(self):
- audio_refs = ActorRegistry.get_by_class(audio.Audio)
- assert len(audio_refs) == 1, \
- 'Expected exactly one running Audio instance.'
- self.audio = audio_refs[0].proxy()
-
- logger.info(u'Mopidy uses SPOTIFY(R) CORE')
- self.spotify = self._connect()
-
- def on_stop(self):
- self.spotify.logout()
-
- def _connect(self):
- from .session_manager import SpotifySessionManager
-
- logger.debug(u'Connecting to Spotify')
- spotify = SpotifySessionManager(self.username, self.password)
- spotify.start()
- return spotify
+# flake8: noqa
+from .actor import SpotifyBackend
diff --git a/mopidy/backends/spotify/actor.py b/mopidy/backends/spotify/actor.py
new file mode 100644
index 00000000..5e90205b
--- /dev/null
+++ b/mopidy/backends/spotify/actor.py
@@ -0,0 +1,49 @@
+from __future__ import unicode_literals
+
+import logging
+
+import pykka
+
+from mopidy import settings
+from mopidy.backends import base
+
+logger = logging.getLogger('mopidy.backends.spotify')
+
+
+class SpotifyBackend(pykka.ThreadingActor, base.Backend):
+ # Imports inside methods are to prevent loading of __init__.py to fail on
+ # missing spotify dependencies.
+
+ def __init__(self, audio):
+ super(SpotifyBackend, self).__init__()
+
+ from .library import SpotifyLibraryProvider
+ from .playback import SpotifyPlaybackProvider
+ from .session_manager import SpotifySessionManager
+ from .playlists import SpotifyPlaylistsProvider
+
+ self.library = SpotifyLibraryProvider(backend=self)
+ self.playback = SpotifyPlaybackProvider(audio=audio, backend=self)
+ self.playlists = SpotifyPlaylistsProvider(backend=self)
+
+ self.uri_schemes = ['spotify']
+
+ # Fail early if settings are not present
+ username = settings.SPOTIFY_USERNAME
+ password = settings.SPOTIFY_PASSWORD
+ proxy = settings.SPOTIFY_PROXY_HOST
+ proxy_username = settings.SPOTIFY_PROXY_USERNAME
+ proxy_password = settings.SPOTIFY_PROXY_PASSWORD
+
+ self.spotify = SpotifySessionManager(
+ username, password, audio=audio, backend_ref=self.actor_ref,
+ proxy=proxy, proxy_username=proxy_username,
+ proxy_password=proxy_password)
+
+ def on_start(self):
+ logger.info('Mopidy uses SPOTIFY(R) CORE')
+ logger.debug('Connecting to Spotify')
+ self.spotify.start()
+
+ def on_stop(self):
+ self.spotify.logout()
diff --git a/mopidy/backends/spotify/container_manager.py b/mopidy/backends/spotify/container_manager.py
index 27a4d78a..e8d1ed0b 100644
--- a/mopidy/backends/spotify/container_manager.py
+++ b/mopidy/backends/spotify/container_manager.py
@@ -1,9 +1,12 @@
+from __future__ import unicode_literals
+
import logging
from spotify.manager import SpotifyContainerManager as \
PyspotifyContainerManager
-logger = logging.getLogger('mopidy.backends.spotify.container_manager')
+logger = logging.getLogger('mopidy.backends.spotify')
+
class SpotifyContainerManager(PyspotifyContainerManager):
def __init__(self, session_manager):
@@ -12,29 +15,29 @@ class SpotifyContainerManager(PyspotifyContainerManager):
def container_loaded(self, container, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: playlist container loaded')
+ logger.debug('Callback called: playlist container loaded')
- self.session_manager.refresh_stored_playlists()
+ self.session_manager.refresh_playlists()
count = 0
for playlist in self.session_manager.session.playlist_container():
if playlist.type() == 'playlist':
self.session_manager.playlist_manager.watch(playlist)
count += 1
- logger.debug(u'Watching %d playlist(s) for changes', count)
+ logger.debug('Watching %d playlist(s) for changes', count)
def playlist_added(self, container, playlist, position, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: playlist added at position %d',
- position)
+ logger.debug(
+ 'Callback called: playlist added at position %d', position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.
def playlist_moved(self, container, playlist, old_position, new_position,
- userdata):
+ userdata):
"""Callback used by pyspotify"""
logger.debug(
- u'Callback called: playlist "%s" moved from position %d to %d',
+ 'Callback called: playlist "%s" moved from position %d to %d',
playlist.name(), old_position, new_position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.
@@ -42,7 +45,7 @@ class SpotifyContainerManager(PyspotifyContainerManager):
def playlist_removed(self, container, playlist, position, userdata):
"""Callback used by pyspotify"""
logger.debug(
- u'Callback called: playlist "%s" removed from position %d',
+ 'Callback called: playlist "%s" removed from position %d',
playlist.name(), position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.
diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py
index 18276ecd..df04058b 100644
--- a/mopidy/backends/spotify/library.py
+++ b/mopidy/backends/spotify/library.py
@@ -1,20 +1,24 @@
+from __future__ import unicode_literals
+
import logging
import Queue
from spotify import Link, SpotifyError
-from mopidy.backends.base import BaseLibraryProvider
-from mopidy.backends.spotify.translator import SpotifyTranslator
-from mopidy.models import Track, Playlist
+from mopidy.backends import base
+from mopidy.models import Track
-logger = logging.getLogger('mopidy.backends.spotify.library')
+from . import translator
+
+logger = logging.getLogger('mopidy.backends.spotify')
class SpotifyTrack(Track):
"""Proxy object for unloaded Spotify tracks."""
def __init__(self, uri):
+ super(SpotifyTrack, self).__init__()
self._spotify_track = Link.from_string(uri).as_track()
- self._unloaded_track = Track(uri=uri, name=u'[loading...]')
+ self._unloaded_track = Track(uri=uri, name='[loading...]')
self._track = None
@property
@@ -22,7 +26,7 @@ class SpotifyTrack(Track):
if self._track:
return self._track
elif self._spotify_track.is_loaded():
- self._track = SpotifyTranslator.to_mopidy_track(self._spotify_track)
+ self._track = translator.to_mopidy_track(self._spotify_track)
return self._track
else:
return self._unloaded_track
@@ -47,49 +51,56 @@ class SpotifyTrack(Track):
return self._proxy.copy(**values)
-class SpotifyLibraryProvider(BaseLibraryProvider):
+class SpotifyLibraryProvider(base.BaseLibraryProvider):
def find_exact(self, **query):
return self.search(**query)
def lookup(self, uri):
try:
- return SpotifyTrack(uri)
+ return [SpotifyTrack(uri)]
except SpotifyError as e:
- logger.debug(u'Failed to lookup "%s": %s', uri, e)
- return None
+ logger.debug('Failed to lookup "%s": %s', uri, e)
+ return []
def refresh(self, uri=None):
- pass # TODO
+ pass # TODO
def search(self, **query):
if not query:
# Since we can't search for the entire Spotify library, we return
- # all tracks in the stored playlists when the query is empty.
+ # all tracks in the playlists when the query is empty.
tracks = []
- for playlist in self.backend.stored_playlists.playlists:
+ for playlist in self.backend.playlists.playlists:
tracks += playlist.tracks
- return Playlist(tracks=tracks)
+ return tracks
spotify_query = []
for (field, values) in query.iteritems():
- if field == u'track':
- field = u'title'
- if field == u'date':
- field = u'year'
+ if field == 'uri':
+ tracks = []
+ for value in values:
+ track = self.lookup(value)
+ if track:
+ tracks.append(track)
+ return tracks
+ elif field == 'track':
+ field = 'title'
+ elif field == 'date':
+ field = 'year'
if not hasattr(values, '__iter__'):
values = [values]
for value in values:
- if field == u'any':
+ if field == 'any':
spotify_query.append(value)
- elif field == u'year':
- value = int(value.split('-')[0]) # Extract year
- spotify_query.append(u'%s:%d' % (field, value))
+ elif field == 'year':
+ value = int(value.split('-')[0]) # Extract year
+ spotify_query.append('%s:%d' % (field, value))
else:
- spotify_query.append(u'%s:"%s"' % (field, value))
- spotify_query = u' '.join(spotify_query)
- logger.debug(u'Spotify search query: %s' % spotify_query)
+ spotify_query.append('%s:"%s"' % (field, value))
+ spotify_query = ' '.join(spotify_query)
+ logger.debug('Spotify search query: %s' % spotify_query)
queue = Queue.Queue()
self.backend.spotify.search(spotify_query, queue)
try:
- return queue.get(timeout=3) # XXX What is an reasonable timeout?
+ return queue.get(timeout=3) # XXX What is an reasonable timeout?
except Queue.Empty:
- return Playlist(tracks=[])
+ return []
diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py
index 1c20da87..e4534172 100644
--- a/mopidy/backends/spotify/playback.py
+++ b/mopidy/backends/spotify/playback.py
@@ -1,40 +1,113 @@
+from __future__ import unicode_literals
+
import logging
+import time
from spotify import Link, SpotifyError
-from mopidy.backends.base import BasePlaybackProvider
+from mopidy.backends import base
from mopidy.core import PlaybackState
-logger = logging.getLogger('mopidy.backends.spotify.playback')
-class SpotifyPlaybackProvider(BasePlaybackProvider):
+logger = logging.getLogger('mopidy.backends.spotify')
+
+
+class SpotifyPlaybackProvider(base.BasePlaybackProvider):
+ def __init__(self, *args, **kwargs):
+ super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs)
+
+ self._timer = TrackPositionTimer()
+
+ def pause(self):
+ self._timer.pause()
+
+ return super(SpotifyPlaybackProvider, self).pause()
+
def play(self, track):
- if self.backend.playback.state == PlaybackState.PLAYING:
- self.backend.spotify.session.play(0)
if track.uri is None:
return False
+
try:
self.backend.spotify.session.load(
Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1)
- self.backend.audio.prepare_change()
- self.backend.audio.set_uri('appsrc://')
- self.backend.audio.start_playback()
- self.backend.audio.set_metadata(track)
+
+ self.audio.prepare_change()
+ self.audio.set_uri('appsrc://')
+ self.audio.start_playback()
+ self.audio.set_metadata(track)
+
+ self._timer.play()
+
return True
except SpotifyError as e:
logger.info('Playback of %s failed: %s', track.uri, e)
return False
def resume(self):
- return self.seek(self.backend.playback.time_position)
+ time_position = self.get_time_position()
+ self._timer.resume()
+ self.audio.prepare_change()
+ result = self.seek(time_position)
+ self.audio.start_playback()
+ return result
def seek(self, time_position):
- self.backend.audio.prepare_change()
self.backend.spotify.session.seek(time_position)
- self.backend.audio.start_playback()
+ self._timer.seek(time_position)
return True
def stop(self):
self.backend.spotify.session.play(0)
+
return super(SpotifyPlaybackProvider, self).stop()
+
+ def get_time_position(self):
+ # XXX: The default implementation of get_time_position hangs/times out
+ # when used with the Spotify backend and GStreamer appsrc. If this can
+ # be resolved, we no longer need to use a wall clock based time
+ # position for Spotify playback.
+ return self._timer.get_time_position()
+
+
+class TrackPositionTimer(object):
+ """
+ Keeps track of time position in a track using the wall clock and playback
+ events.
+
+ To not introduce a reverse dependency on the playback controller, this
+ class keeps track of playback state itself.
+ """
+
+ def __init__(self):
+ self._state = PlaybackState.STOPPED
+ self._accumulated = 0
+ self._started = 0
+
+ def play(self):
+ self._state = PlaybackState.PLAYING
+ self._accumulated = 0
+ self._started = self._wall_time()
+
+ def pause(self):
+ self._state = PlaybackState.PAUSED
+ self._accumulated += self._wall_time() - self._started
+
+ def resume(self):
+ self._state = PlaybackState.PLAYING
+
+ def seek(self, time_position):
+ self._started = self._wall_time()
+ self._accumulated = time_position
+
+ def get_time_position(self):
+ if self._state == PlaybackState.PLAYING:
+ time_since_started = self._wall_time() - self._started
+ return self._accumulated + time_since_started
+ elif self._state == PlaybackState.PAUSED:
+ return self._accumulated
+ elif self._state == PlaybackState.STOPPED:
+ return 0
+
+ def _wall_time(self):
+ return int(time.time() * 1000)
diff --git a/mopidy/backends/spotify/playlist_manager.py b/mopidy/backends/spotify/playlist_manager.py
index 05f9514d..6cd6d4ed 100644
--- a/mopidy/backends/spotify/playlist_manager.py
+++ b/mopidy/backends/spotify/playlist_manager.py
@@ -1,9 +1,12 @@
+from __future__ import unicode_literals
+
import datetime
import logging
from spotify.manager import SpotifyPlaylistManager as PyspotifyPlaylistManager
-logger = logging.getLogger('mopidy.backends.spotify.playlist_manager')
+logger = logging.getLogger('mopidy.backends.spotify')
+
class SpotifyPlaylistManager(PyspotifyPlaylistManager):
def __init__(self, session_manager):
@@ -12,83 +15,91 @@ class SpotifyPlaylistManager(PyspotifyPlaylistManager):
def tracks_added(self, playlist, tracks, position, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: '
- u'%d track(s) added to position %d in playlist "%s"',
+ logger.debug(
+ 'Callback called: '
+ '%d track(s) added to position %d in playlist "%s"',
len(tracks), position, playlist.name())
- self.session_manager.refresh_stored_playlists()
+ self.session_manager.refresh_playlists()
def tracks_moved(self, playlist, tracks, new_position, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: '
- u'%d track(s) moved to position %d in playlist "%s"',
+ logger.debug(
+ 'Callback called: '
+ '%d track(s) moved to position %d in playlist "%s"',
len(tracks), new_position, playlist.name())
- self.session_manager.refresh_stored_playlists()
+ self.session_manager.refresh_playlists()
def tracks_removed(self, playlist, tracks, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: '
- u'%d track(s) removed from playlist "%s"',
+ logger.debug(
+ 'Callback called: '
+ '%d track(s) removed from playlist "%s"',
len(tracks), playlist.name())
- self.session_manager.refresh_stored_playlists()
+ self.session_manager.refresh_playlists()
def playlist_renamed(self, playlist, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: Playlist renamed to "%s"',
- playlist.name())
- self.session_manager.refresh_stored_playlists()
+ logger.debug(
+ 'Callback called: Playlist renamed to "%s"', playlist.name())
+ self.session_manager.refresh_playlists()
def playlist_state_changed(self, playlist, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: The state of playlist "%s" changed',
+ logger.debug(
+ 'Callback called: The state of playlist "%s" changed',
playlist.name())
def playlist_update_in_progress(self, playlist, done, userdata):
"""Callback used by pyspotify"""
if done:
- logger.debug(u'Callback called: '
- u'Update of playlist "%s" done', playlist.name())
+ logger.debug(
+ 'Callback called: Update of playlist "%s" done',
+ playlist.name())
else:
- logger.debug(u'Callback called: '
- u'Update of playlist "%s" in progress', playlist.name())
+ logger.debug(
+ 'Callback called: Update of playlist "%s" in progress',
+ playlist.name())
def playlist_metadata_updated(self, playlist, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: Metadata updated for playlist "%s"',
+ logger.debug(
+ 'Callback called: Metadata updated for playlist "%s"',
playlist.name())
def track_created_changed(self, playlist, position, user, when, userdata):
"""Callback used by pyspotify"""
when = datetime.datetime.fromtimestamp(when)
logger.debug(
- u'Callback called: Created by/when for track %d in playlist '
- u'"%s" changed to user "N/A" and time "%s"',
+ 'Callback called: Created by/when for track %d in playlist '
+ '"%s" changed to user "N/A" and time "%s"',
position, playlist.name(), when)
def track_message_changed(self, playlist, position, message, userdata):
"""Callback used by pyspotify"""
logger.debug(
- u'Callback called: Message for track %d in playlist '
- u'"%s" changed to "%s"', position, playlist.name(), message)
+ 'Callback called: Message for track %d in playlist '
+ '"%s" changed to "%s"', position, playlist.name(), message)
def track_seen_changed(self, playlist, position, seen, userdata):
"""Callback used by pyspotify"""
logger.debug(
- u'Callback called: Seen attribute for track %d in playlist '
- u'"%s" changed to "%s"', position, playlist.name(), seen)
+ 'Callback called: Seen attribute for track %d in playlist '
+ '"%s" changed to "%s"', position, playlist.name(), seen)
def description_changed(self, playlist, description, userdata):
"""Callback used by pyspotify"""
logger.debug(
- u'Callback called: Description changed for playlist "%s" to "%s"',
+ 'Callback called: Description changed for playlist "%s" to "%s"',
playlist.name(), description)
def subscribers_changed(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
- u'Callback called: Subscribers changed for playlist "%s"',
+ 'Callback called: Subscribers changed for playlist "%s"',
playlist.name())
def image_changed(self, playlist, image, userdata):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: Image changed for playlist "%s"',
+ logger.debug(
+ 'Callback called: Image changed for playlist "%s"',
playlist.name())
diff --git a/mopidy/backends/spotify/playlists.py b/mopidy/backends/spotify/playlists.py
new file mode 100644
index 00000000..bd201179
--- /dev/null
+++ b/mopidy/backends/spotify/playlists.py
@@ -0,0 +1,22 @@
+from __future__ import unicode_literals
+
+from mopidy.backends import base
+
+
+class SpotifyPlaylistsProvider(base.BasePlaylistsProvider):
+ def create(self, name):
+ pass # TODO
+
+ def delete(self, uri):
+ pass # TODO
+
+ def lookup(self, uri):
+ for playlist in self._playlists:
+ if playlist.uri == uri:
+ return playlist
+
+ def refresh(self):
+ pass # TODO
+
+ def save(self, playlist):
+ pass # TODO
diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py
index ce1226d8..cfe4e433 100644
--- a/mopidy/backends/spotify/session_manager.py
+++ b/mopidy/backends/spotify/session_manager.py
@@ -1,39 +1,49 @@
+from __future__ import unicode_literals
+
+import pygst
+pygst.require('0.10')
+import gst
+
import logging
import os
import threading
from spotify.manager import SpotifySessionManager as PyspotifySessionManager
-from pykka.registry import ActorRegistry
+from mopidy import audio, settings
+from mopidy.backends.listener import BackendListener
+from mopidy.utils import process, versioning
-from mopidy import audio, get_version, settings
-from mopidy.backends.base import Backend
-from mopidy.backends.spotify import BITRATES
-from mopidy.backends.spotify.container_manager import SpotifyContainerManager
-from mopidy.backends.spotify.playlist_manager import SpotifyPlaylistManager
-from mopidy.backends.spotify.translator import SpotifyTranslator
-from mopidy.models import Playlist
-from mopidy.utils.process import BaseThread
+from . import translator
+from .container_manager import SpotifyContainerManager
+from .playlist_manager import SpotifyPlaylistManager
-logger = logging.getLogger('mopidy.backends.spotify.session_manager')
+logger = logging.getLogger('mopidy.backends.spotify')
+
+BITRATES = {96: 2, 160: 0, 320: 1}
# pylint: disable = R0901
# SpotifySessionManager: Too many ancestors (9/7)
-class SpotifySessionManager(BaseThread, PyspotifySessionManager):
+class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
cache_location = settings.SPOTIFY_CACHE_PATH
settings_location = cache_location
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
- user_agent = 'Mopidy %s' % get_version()
+ user_agent = 'Mopidy %s' % versioning.get_version()
- def __init__(self, username, password):
- PyspotifySessionManager.__init__(self, username, password)
- BaseThread.__init__(self)
+ def __init__(self, username, password, audio, backend_ref, proxy=None,
+ proxy_username=None, proxy_password=None):
+ PyspotifySessionManager.__init__(
+ self, username, password, proxy=proxy,
+ proxy_username=proxy_username,
+ proxy_password=proxy_password)
+ process.BaseThread.__init__(self)
self.name = 'SpotifyThread'
- self.audio = None
+ self.audio = audio
self.backend = None
+ self.backend_ref = backend_ref
self.connected = threading.Event()
self.session = None
@@ -44,29 +54,20 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
self._initial_data_receive_completed = False
def run_inside_try(self):
- self.setup()
+ self.backend = self.backend_ref.proxy()
self.connect()
- def setup(self):
- audio_refs = ActorRegistry.get_by_class(audio.Audio)
- assert len(audio_refs) == 1, \
- 'Expected exactly one running Audio instance.'
- self.audio = audio_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)
+ logger.error('Spotify login error: %s', error)
return
- logger.info(u'Connected to Spotify')
+ logger.info('Connected to Spotify')
self.session = session
- logger.debug(u'Preferred Spotify bitrate is %s kbps',
+ logger.debug(
+ 'Preferred Spotify bitrate is %s kbps',
settings.SPOTIFY_BITRATE)
self.session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE])
@@ -79,30 +80,31 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def logged_out(self, session):
"""Callback used by pyspotify"""
- logger.info(u'Disconnected from Spotify')
+ logger.info('Disconnected from Spotify')
def metadata_updated(self, session):
"""Callback used by pyspotify"""
- logger.debug(u'Callback called: Metadata updated')
+ logger.debug('Callback called: Metadata updated')
def connection_error(self, session, error):
"""Callback used by pyspotify"""
if error is None:
- logger.info(u'Spotify connection OK')
+ logger.info('Spotify connection OK')
else:
- logger.error(u'Spotify connection error: %s', error)
- self.backend.playback.pause()
+ logger.error('Spotify connection error: %s', error)
+ if self.audio.state.get() == audio.PlaybackState.PLAYING:
+ self.backend.playback.pause()
def message_to_user(self, session, message):
"""Callback used by pyspotify"""
- logger.debug(u'User message: %s', message.strip())
+ logger.debug('User message: %s', message.strip())
def music_delivery(self, session, frames, frame_size, num_frames,
- sample_type, sample_rate, channels):
+ sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
# pylint: disable = R0913
# Too many arguments (8/5)
- assert sample_type == 0, u'Expects 16-bit signed integer samples'
+ assert sample_type == 0, 'Expects 16-bit signed integer samples'
capabilites = """
audio/x-raw-int,
endianness=(int)1234,
@@ -115,45 +117,50 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
'sample_rate': sample_rate,
'channels': channels,
}
- self.audio.emit_data(capabilites, bytes(frames))
- return num_frames
+ buffer_ = gst.Buffer(bytes(frames))
+ buffer_.set_caps(gst.caps_from_string(capabilites))
+
+ if self.audio.emit_data(buffer_).get():
+ return num_frames
+ else:
+ return 0
def play_token_lost(self, session):
"""Callback used by pyspotify"""
- logger.debug(u'Play token lost')
+ logger.debug('Play token lost')
self.backend.playback.pause()
def log_message(self, session, data):
"""Callback used by pyspotify"""
- logger.debug(u'System message: %s' % data.strip())
+ logger.debug('System message: %s' % data.strip())
if 'offline-mgr' in data and 'files unlocked' in data:
# XXX This is a very very fragile and ugly hack, but we get no
# proper event when libspotify is done with initial data loading.
- # We delay the expensive refresh of Mopidy's stored playlists until
- # this message arrives. This way, we avoid doing the refresh once
- # for every playlist or other change. This reduces the time from
+ # We delay the expensive refresh of Mopidy's playlists until this
+ # message arrives. This way, we avoid doing the refresh once for
+ # every playlist or other change. This reduces the time from
# startup until the Spotify backend is ready from 35s to 12s in one
# test with clean Spotify cache. In cases with an outdated cache
- # the time improvements should be a lot better.
+ # the time improvements should be a lot greater.
self._initial_data_receive_completed = True
- self.refresh_stored_playlists()
+ self.refresh_playlists()
def end_of_track(self, session):
"""Callback used by pyspotify"""
- logger.debug(u'End of data stream reached')
+ logger.debug('End of data stream reached')
self.audio.emit_end_of_stream()
- def refresh_stored_playlists(self):
- """Refresh the stored playlists in the backend with fresh meta data
- from Spotify"""
+ def refresh_playlists(self):
+ """Refresh the playlists in the backend with data from Spotify"""
if not self._initial_data_receive_completed:
- logger.debug(u'Still getting data; skipped refresh of playlists')
+ logger.debug('Still getting data; skipped refresh of playlists')
return
- playlists = map(SpotifyTranslator.to_mopidy_playlist,
- self.session.playlist_container())
+ playlists = map(
+ translator.to_mopidy_playlist, self.session.playlist_container())
playlists = filter(None, playlists)
- self.backend.stored_playlists.playlists = playlists
- logger.info(u'Loaded %d Spotify playlist(s)', len(playlists))
+ self.backend.playlists.playlists = playlists
+ logger.info('Loaded %d Spotify playlist(s)', len(playlists))
+ BackendListener.send('playlists_loaded')
def search(self, query, queue):
"""Search method used by Mopidy backend"""
@@ -161,16 +168,15 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
# TODO Include results from results.albums(), etc. too
# TODO Consider launching a second search if results.total_tracks()
# is larger than len(results.tracks())
- playlist = Playlist(tracks=[
- SpotifyTranslator.to_mopidy_track(t)
- for t in results.tracks()])
- queue.put(playlist)
+ tracks = [
+ translator.to_mopidy_track(t) for t in results.tracks()]
+ queue.put(tracks)
self.connected.wait()
- self.session.search(query, callback, track_count=100,
- album_count=0, artist_count=0)
+ self.session.search(
+ query, callback, track_count=100, album_count=0, artist_count=0)
def logout(self):
"""Log out from spotify"""
- logger.debug(u'Logging out from Spotify')
+ logger.debug('Logging out from Spotify')
if self.session:
self.session.logout()
diff --git a/mopidy/backends/spotify/stored_playlists.py b/mopidy/backends/spotify/stored_playlists.py
deleted file mode 100644
index 054e2bd1..00000000
--- a/mopidy/backends/spotify/stored_playlists.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from mopidy.backends.base import BaseStoredPlaylistsProvider
-
-class SpotifyStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
- def create(self, name):
- pass # TODO
-
- def delete(self, playlist):
- pass # TODO
-
- def lookup(self, uri):
- pass # TODO
-
- def refresh(self):
- pass # TODO
-
- def rename(self, playlist, new_name):
- pass # TODO
-
- def save(self, playlist):
- pass # TODO
diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py
index 1a8f048d..92b4514e 100644
--- a/mopidy/backends/spotify/translator.py
+++ b/mopidy/backends/spotify/translator.py
@@ -1,63 +1,69 @@
-import logging
+from __future__ import unicode_literals
-from spotify import Link, SpotifyError
+from spotify import Link
from mopidy import settings
from mopidy.models import Artist, Album, Track, Playlist
-logger = logging.getLogger('mopidy.backends.spotify.translator')
-class SpotifyTranslator(object):
- @classmethod
- def to_mopidy_artist(cls, spotify_artist):
- if not spotify_artist.is_loaded():
- return Artist(name=u'[loading...]')
- return Artist(
- uri=str(Link.from_artist(spotify_artist)),
- name=spotify_artist.name()
- )
+def to_mopidy_artist(spotify_artist):
+ if spotify_artist is None:
+ return
+ uri = str(Link.from_artist(spotify_artist))
+ if not spotify_artist.is_loaded():
+ return Artist(uri=uri, name='[loading...]')
+ return Artist(uri=uri, name=spotify_artist.name())
- @classmethod
- def to_mopidy_album(cls, spotify_album):
- if spotify_album is None or not spotify_album.is_loaded():
- return Album(name=u'[loading...]')
- # TODO pyspotify got much more data on albums than this
- return Album(name=spotify_album.name())
- @classmethod
- def to_mopidy_track(cls, spotify_track):
- uri = str(Link.from_track(spotify_track, 0))
- if not spotify_track.is_loaded():
- return Track(uri=uri, name=u'[loading...]')
- spotify_album = spotify_track.album()
- if spotify_album is not None and spotify_album.is_loaded():
- date = spotify_album.year()
- else:
- date = None
- return Track(
- uri=uri,
- name=spotify_track.name(),
- artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()],
- album=cls.to_mopidy_album(spotify_track.album()),
- track_no=spotify_track.index(),
- date=date,
- length=spotify_track.duration(),
- bitrate=settings.SPOTIFY_BITRATE,
- )
+def to_mopidy_album(spotify_album):
+ if spotify_album is None:
+ return
+ uri = str(Link.from_album(spotify_album))
+ if not spotify_album.is_loaded():
+ return Album(uri=uri, name='[loading...]')
+ return Album(
+ uri=uri,
+ name=spotify_album.name(),
+ artists=[to_mopidy_artist(spotify_album.artist())],
+ date=spotify_album.year())
- @classmethod
- def to_mopidy_playlist(cls, spotify_playlist):
- if not spotify_playlist.is_loaded():
- return Playlist(name=u'[loading...]')
- if spotify_playlist.type() != 'playlist':
- return
- try:
- return Playlist(
- uri=str(Link.from_playlist(spotify_playlist)),
- name=spotify_playlist.name(),
- # FIXME if check on link is a hackish workaround for is_local
- tracks=[cls.to_mopidy_track(t) for t in spotify_playlist
- if str(Link.from_track(t, 0))],
- )
- except SpotifyError, e:
- logger.warning(u'Failed translating Spotify playlist: %s', e)
+
+def to_mopidy_track(spotify_track):
+ if spotify_track is None:
+ return
+ uri = str(Link.from_track(spotify_track, 0))
+ if not spotify_track.is_loaded():
+ return Track(uri=uri, name='[loading...]')
+ spotify_album = spotify_track.album()
+ if spotify_album is not None and spotify_album.is_loaded():
+ date = spotify_album.year()
+ else:
+ date = None
+ return Track(
+ uri=uri,
+ name=spotify_track.name(),
+ artists=[to_mopidy_artist(a) for a in spotify_track.artists()],
+ album=to_mopidy_album(spotify_track.album()),
+ track_no=spotify_track.index(),
+ date=date,
+ length=spotify_track.duration(),
+ bitrate=settings.SPOTIFY_BITRATE)
+
+
+def to_mopidy_playlist(spotify_playlist):
+ if spotify_playlist is None or spotify_playlist.type() != 'playlist':
+ return
+ uri = str(Link.from_playlist(spotify_playlist))
+ if not spotify_playlist.is_loaded():
+ return Playlist(uri=uri, name='[loading...]')
+ if not spotify_playlist.name():
+ # Other user's "starred" playlists isn't handled properly by pyspotify
+ # See https://github.com/mopidy/pyspotify/issues/81
+ return
+ return Playlist(
+ uri=uri,
+ name=spotify_playlist.name(),
+ tracks=[
+ to_mopidy_track(spotify_track)
+ for spotify_track in spotify_playlist
+ if not spotify_track.is_local()])
diff --git a/mopidy/core/__init__.py b/mopidy/core/__init__.py
index 87df96c9..f49bbbe7 100644
--- a/mopidy/core/__init__.py
+++ b/mopidy/core/__init__.py
@@ -1,4 +1,9 @@
-from .current_playlist import CurrentPlaylistController
+from __future__ import unicode_literals
+
+# flake8: noqa
+from .actor import Core
from .library import LibraryController
+from .listener import CoreListener
from .playback import PlaybackController, PlaybackState
-from .stored_playlists import StoredPlaylistsController
+from .playlists import PlaylistsController
+from .tracklist import TracklistController
diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py
new file mode 100644
index 00000000..cd4ba180
--- /dev/null
+++ b/mopidy/core/actor.py
@@ -0,0 +1,112 @@
+from __future__ import unicode_literals
+
+import itertools
+
+import pykka
+
+from mopidy.audio import AudioListener, PlaybackState
+from mopidy.backends.listener import BackendListener
+
+from .library import LibraryController
+from .listener import CoreListener
+from .playback import PlaybackController
+from .playlists import PlaylistsController
+from .tracklist import TracklistController
+
+
+class Core(pykka.ThreadingActor, AudioListener, BackendListener):
+ #: The library controller. An instance of
+ # :class:`mopidy.core.LibraryController`.
+ library = None
+
+ #: The playback controller. An instance of
+ #: :class:`mopidy.core.PlaybackController`.
+ playback = None
+
+ #: The playlists controller. An instance of
+ #: :class:`mopidy.core.PlaylistsController`.
+ playlists = None
+
+ #: The tracklist controller. An instance of
+ #: :class:`mopidy.core.TracklistController`.
+ tracklist = None
+
+ def __init__(self, audio=None, backends=None):
+ super(Core, self).__init__()
+
+ self.backends = Backends(backends)
+
+ self.library = LibraryController(backends=self.backends, core=self)
+
+ self.playback = PlaybackController(
+ audio=audio, backends=self.backends, core=self)
+
+ self.playlists = PlaylistsController(
+ backends=self.backends, core=self)
+
+ self.tracklist = TracklistController(core=self)
+
+ def get_uri_schemes(self):
+ futures = [b.uri_schemes for b in self.backends]
+ results = pykka.get_all(futures)
+ uri_schemes = itertools.chain(*results)
+ return sorted(uri_schemes)
+
+ uri_schemes = property(get_uri_schemes)
+ """List of URI schemes we can handle"""
+
+ def reached_end_of_stream(self):
+ self.playback.on_end_of_track()
+
+ def state_changed(self, old_state, new_state):
+ # XXX: This is a temporary fix for issue #232 while we wait for a more
+ # permanent solution with the implementation of issue #234. When the
+ # Spotify play token is lost, the Spotify backend pauses audio
+ # playback, but mopidy.core doesn't know this, so we need to update
+ # mopidy.core's state to match the actual state in mopidy.audio. If we
+ # don't do this, clients will think that we're still playing.
+ if (new_state == PlaybackState.PAUSED
+ and self.playback.state != PlaybackState.PAUSED):
+ self.playback.state = new_state
+ self.playback._trigger_track_playback_paused()
+
+ def playlists_loaded(self):
+ # Forward event from backend to frontends
+ CoreListener.send('playlists_loaded')
+
+
+class Backends(list):
+ def __init__(self, backends):
+ super(Backends, self).__init__(backends)
+
+ # These lists keeps the backends in the original order, but only
+ # includes those which implements the required backend provider. Since
+ # it is important to keep the order, we can't simply use .values() on
+ # the X_by_uri_scheme dicts below.
+ self.with_library = [b for b in backends if b.has_library().get()]
+ self.with_playback = [b for b in backends if b.has_playback().get()]
+ self.with_playlists = [
+ b for b in backends if b.has_playlists().get()]
+
+ self.by_uri_scheme = {}
+ for backend in backends:
+ for uri_scheme in backend.uri_schemes.get():
+ assert uri_scheme not in self.by_uri_scheme, (
+ 'Cannot add URI scheme %s for %s, '
+ 'it is already handled by %s'
+ ) % (
+ uri_scheme, backend.__class__.__name__,
+ self.by_uri_scheme[uri_scheme].__class__.__name__)
+ self.by_uri_scheme[uri_scheme] = backend
+
+ self.with_library_by_uri_scheme = {}
+ self.with_playback_by_uri_scheme = {}
+ self.with_playlists_by_uri_scheme = {}
+
+ for uri_scheme, backend in self.by_uri_scheme.items():
+ if backend.has_library().get():
+ self.with_library_by_uri_scheme[uri_scheme] = backend
+ if backend.has_playback().get():
+ self.with_playback_by_uri_scheme[uri_scheme] = backend
+ if backend.has_playlists().get():
+ self.with_playlists_by_uri_scheme[uri_scheme] = backend
diff --git a/mopidy/core/current_playlist.py b/mopidy/core/current_playlist.py
deleted file mode 100644
index af06e05e..00000000
--- a/mopidy/core/current_playlist.py
+++ /dev/null
@@ -1,243 +0,0 @@
-from copy import copy
-import logging
-import random
-
-from mopidy.listeners import BackendListener
-from mopidy.models import CpTrack
-
-
-logger = logging.getLogger('mopidy.core')
-
-
-class CurrentPlaylistController(object):
- """
- :param backend: backend the controller is a part of
- :type backend: :class:`mopidy.backends.base.Backend`
- """
-
- pykka_traversable = True
-
- def __init__(self, backend):
- self.backend = backend
- self.cp_id = 0
- self._cp_tracks = []
- self._version = 0
-
- @property
- def cp_tracks(self):
- """
- List of two-tuples of (CPID integer, :class:`mopidy.models.Track`).
-
- Read-only.
- """
- return [copy(cp_track) for cp_track in self._cp_tracks]
-
- @property
- def tracks(self):
- """
- List of :class:`mopidy.models.Track` in the current playlist.
-
- Read-only.
- """
- return [cp_track.track for cp_track in self._cp_tracks]
-
- @property
- def length(self):
- """
- Length of the current playlist.
- """
- return len(self._cp_tracks)
-
- @property
- def version(self):
- """
- The current playlist version. Integer which is increased every time the
- current playlist is changed. Is not reset before Mopidy is restarted.
- """
- return self._version
-
- @version.setter
- def version(self, version):
- self._version = version
- self.backend.playback.on_current_playlist_change()
- self._trigger_playlist_changed()
-
- def add(self, track, at_position=None, increase_version=True):
- """
- Add the track to the end of, or at the given position in the current
- playlist.
-
- :param track: track to add
- :type track: :class:`mopidy.models.Track`
- :param at_position: position in current playlist to add track
- :type at_position: int or :class:`None`
- :rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`) that
- was added to the current playlist playlist
- """
- assert at_position <= len(self._cp_tracks), \
- u'at_position can not be greater than playlist length'
- cp_track = CpTrack(self.cp_id, track)
- if at_position is not None:
- self._cp_tracks.insert(at_position, cp_track)
- else:
- self._cp_tracks.append(cp_track)
- if increase_version:
- self.version += 1
- self.cp_id += 1
- return cp_track
-
- def append(self, tracks):
- """
- Append the given tracks to the current playlist.
-
- :param tracks: tracks to append
- :type tracks: list of :class:`mopidy.models.Track`
- """
- for track in tracks:
- self.add(track, increase_version=False)
-
- if tracks:
- self.version += 1
-
- def clear(self):
- """Clear the current playlist."""
- self._cp_tracks = []
- self.version += 1
-
- def get(self, **criteria):
- """
- Get track by given criterias from current playlist.
-
- Raises :exc:`LookupError` if a unique match is not found.
-
- Examples::
-
- get(cpid=7) # Returns track with CPID 7
- # (current playlist ID)
- get(id=1) # Returns track with ID 1
- get(uri='xyz') # Returns track with URI 'xyz'
- get(id=1, uri='xyz') # Returns track with ID 1 and URI 'xyz'
-
- :param criteria: on or more criteria to match by
- :type criteria: dict
- :rtype: two-tuple (CPID integer, :class:`mopidy.models.Track`)
- """
- matches = self._cp_tracks
- for (key, value) in criteria.iteritems():
- if key == 'cpid':
- matches = filter(lambda ct: ct.cpid == value, matches)
- else:
- matches = filter(lambda ct: getattr(ct.track, key) == value,
- matches)
- if len(matches) == 1:
- return matches[0]
- criteria_string = ', '.join(
- ['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
- if len(matches) == 0:
- raise LookupError(u'"%s" match no tracks' % criteria_string)
- else:
- raise LookupError(u'"%s" match multiple tracks' % criteria_string)
-
- def index(self, cp_track):
- """
- Get index of the given (CPID integer, :class:`mopidy.models.Track`)
- two-tuple in the current playlist.
-
- Raises :exc:`ValueError` if not found.
-
- :param cp_track: track to find the index of
- :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
- :rtype: int
- """
- return self._cp_tracks.index(cp_track)
-
- def move(self, start, end, to_position):
- """
- Move the tracks in the slice ``[start:end]`` to ``to_position``.
-
- :param start: position of first track to move
- :type start: int
- :param end: position after last track to move
- :type end: int
- :param to_position: new position for the tracks
- :type to_position: int
- """
- if start == end:
- end += 1
-
- cp_tracks = self._cp_tracks
-
- assert start < end, 'start must be smaller than end'
- assert start >= 0, 'start must be at least zero'
- assert end <= len(cp_tracks), \
- 'end can not be larger than playlist length'
- assert to_position >= 0, 'to_position must be at least zero'
- assert to_position <= len(cp_tracks), \
- 'to_position can not be larger than playlist length'
-
- new_cp_tracks = cp_tracks[:start] + cp_tracks[end:]
- for cp_track in cp_tracks[start:end]:
- new_cp_tracks.insert(to_position, cp_track)
- to_position += 1
- self._cp_tracks = new_cp_tracks
- self.version += 1
-
- def remove(self, **criteria):
- """
- Remove the track from the current playlist.
-
- Uses :meth:`get()` to lookup the track to remove.
-
- :param criteria: on or more criteria to match by
- :type criteria: dict
- """
- cp_track = self.get(**criteria)
- position = self._cp_tracks.index(cp_track)
- del self._cp_tracks[position]
- self.version += 1
-
- def shuffle(self, start=None, end=None):
- """
- Shuffles the entire playlist. If ``start`` and ``end`` is given only
- shuffles the slice ``[start:end]``.
-
- :param start: position of first track to shuffle
- :type start: int or :class:`None`
- :param end: position after last track to shuffle
- :type end: int or :class:`None`
- """
- cp_tracks = self._cp_tracks
-
- if start is not None and end is not None:
- assert start < end, 'start must be smaller than end'
-
- if start is not None:
- assert start >= 0, 'start must be at least zero'
-
- if end is not None:
- assert end <= len(cp_tracks), 'end can not be larger than ' + \
- 'playlist length'
-
- before = cp_tracks[:start or 0]
- shuffled = cp_tracks[start:end]
- after = cp_tracks[end or len(cp_tracks):]
- random.shuffle(shuffled)
- self._cp_tracks = before + shuffled + after
- self.version += 1
-
- def slice(self, start, end):
- """
- Returns a slice of the current playlist, limited by the given
- start and end positions.
-
- :param start: position of first track to include in slice
- :type start: int
- :param end: position after last track to include in slice
- :type end: int
- :rtype: two-tuple of (CPID integer, :class:`mopidy.models.Track`)
- """
- return [copy(cp_track) for cp_track in self._cp_tracks[start:end]]
-
- def _trigger_playlist_changed(self):
- logger.debug(u'Triggering playlist changed event')
- BackendListener.send('playlist_changed')
diff --git a/mopidy/core/library.py b/mopidy/core/library.py
index fc55aaeb..c1a89222 100644
--- a/mopidy/core/library.py
+++ b/mopidy/core/library.py
@@ -1,16 +1,21 @@
-class LibraryController(object):
- """
- :param backend: backend the controller is a part of
- :type backend: :class:`mopidy.backends.base.Backend`
- :param provider: provider the controller should use
- :type provider: instance of :class:`BaseLibraryProvider`
- """
+from __future__ import unicode_literals
+import itertools
+import urlparse
+
+import pykka
+
+
+class LibraryController(object):
pykka_traversable = True
- def __init__(self, backend, provider):
- self.backend = backend
- self.provider = provider
+ def __init__(self, backends, core):
+ self.backends = backends
+ self.core = core
+
+ def _get_backend(self, uri):
+ uri_scheme = urlparse.urlparse(uri).scheme
+ return self.backends.with_library_by_uri_scheme.get(uri_scheme, None)
def find_exact(self, **query):
"""
@@ -27,19 +32,29 @@ class LibraryController(object):
:param query: one or more queries to search for
:type query: dict
- :rtype: :class:`mopidy.models.Playlist`
+ :rtype: list of :class:`mopidy.models.Track`
"""
- return self.provider.find_exact(**query)
+ futures = [
+ b.library.find_exact(**query) for b in self.backends.with_library]
+ results = pykka.get_all(futures)
+ return list(itertools.chain(*results))
def lookup(self, uri):
"""
- Lookup track with given URI. Returns :class:`None` if not found.
+ Lookup the given URI.
+
+ If the URI expands to multiple tracks, the returned list will contain
+ them all.
:param uri: track URI
:type uri: string
- :rtype: :class:`mopidy.models.Track` or :class:`None`
+ :rtype: list of :class:`mopidy.models.Track`
"""
- return self.provider.lookup(uri)
+ backend = self._get_backend(uri)
+ if backend:
+ return backend.library.lookup(uri).get()
+ else:
+ return []
def refresh(self, uri=None):
"""
@@ -48,7 +63,14 @@ class LibraryController(object):
:param uri: directory or track URI
:type uri: string
"""
- return self.provider.refresh(uri)
+ if uri is not None:
+ backend = self._get_backend(uri)
+ if backend:
+ backend.library.refresh(uri).get()
+ else:
+ futures = [
+ b.library.refresh(uri) for b in self.backends.with_library]
+ pykka.get_all(futures)
def search(self, **query):
"""
@@ -65,6 +87,9 @@ class LibraryController(object):
:param query: one or more queries to search for
:type query: dict
- :rtype: :class:`mopidy.models.Playlist`
+ :rtype: list of :class:`mopidy.models.Track`
"""
- return self.provider.search(**query)
+ futures = [
+ b.library.search(**query) for b in self.backends.with_library]
+ results = pykka.get_all(futures)
+ return list(itertools.chain(*results))
diff --git a/mopidy/listeners.py b/mopidy/core/listener.py
similarity index 65%
rename from mopidy/listeners.py
rename to mopidy/core/listener.py
index ee360bf3..dc8bf1d7 100644
--- a/mopidy/listeners.py
+++ b/mopidy/core/listener.py
@@ -1,11 +1,14 @@
-from pykka import registry
+from __future__ import unicode_literals
-class BackendListener(object):
+import pykka
+
+
+class CoreListener(object):
"""
- Marker interface for recipients of events sent by the backend.
+ Marker interface for recipients of events sent by the core actor.
Any Pykka actor that mixes in this class will receive calls to the methods
- defined here when the corresponding events happen in the backend. This
+ defined here when the corresponding events happen in the core actor. This
interface is used both for looking up what actors to notify of the events,
and for providing default implementations for those listeners that are not
interested in all events.
@@ -13,15 +16,10 @@ class BackendListener(object):
@staticmethod
def send(event, **kwargs):
- """Helper to allow calling of backend listener events"""
- # FIXME this should be updated once Pykka supports non-blocking calls
- # on proxies or some similar solution.
- registry.ActorRegistry.broadcast({
- 'command': 'pykka_call',
- 'attr_path': (event,),
- 'args': [],
- 'kwargs': kwargs,
- }, target_class=BackendListener)
+ """Helper to allow calling of core listener events"""
+ listeners = pykka.ActorRegistry.get_by_class(CoreListener)
+ for listener in listeners:
+ getattr(listener.proxy(), event)(**kwargs)
def track_playback_paused(self, track, time_position):
"""
@@ -49,7 +47,6 @@ class BackendListener(object):
"""
pass
-
def track_playback_started(self, track):
"""
Called whenever a new track starts playing.
@@ -74,19 +71,43 @@ class BackendListener(object):
"""
pass
- def playback_state_changed(self):
+ def playback_state_changed(self, old_state, new_state):
"""
Called whenever playback state is changed.
+ *MAY* be implemented by actor.
+
+ :param old_state: the state before the change
+ :type old_state: string from :class:`mopidy.core.PlaybackState` field
+ :param new_state: the state after the change
+ :type new_state: string from :class:`mopidy.core.PlaybackState` field
+ """
+ pass
+
+ def tracklist_changed(self):
+ """
+ Called whenever the tracklist is changed.
+
*MAY* be implemented by actor.
"""
pass
- def playlist_changed(self):
+ def playlists_loaded(self):
+ """
+ Called when playlists are loaded or refreshed.
+
+ *MAY* be implemented by actor.
+ """
+ pass
+
+ def playlist_changed(self, playlist):
"""
Called whenever a playlist is changed.
*MAY* be implemented by actor.
+
+ :param playlist: the changed playlist
+ :type playlist: :class:`mopidy.models.Playlist`
"""
pass
@@ -106,11 +127,14 @@ class BackendListener(object):
"""
pass
- def seeked(self):
+ def seeked(self, time_position):
"""
Called whenever the time position changes by an unexpected amount, e.g.
at seek to a new time position.
*MAY* be implemented by actor.
+
+ :param time_position: the position that was seeked to in milliseconds
+ :type time_position: int
"""
pass
diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py
index 31a1acc5..e50de2e7 100644
--- a/mopidy/core/playback.py
+++ b/mopidy/core/playback.py
@@ -1,359 +1,309 @@
+from __future__ import unicode_literals
+
import logging
import random
-import time
+import urlparse
-from mopidy.listeners import BackendListener
+from mopidy.audio import PlaybackState
+
+from . import listener
-logger = logging.getLogger('mopidy.backends.base')
-
-
-def option_wrapper(name, default):
- def get_option(self):
- return getattr(self, name, default)
-
- def set_option(self, value):
- if getattr(self, name, default) != value:
- self._trigger_options_changed()
- return setattr(self, name, value)
-
- return property(get_option, set_option)
-
-
-
-class PlaybackState(object):
- """
- Enum of playback states.
- """
-
- #: Constant representing the paused state.
- PAUSED = u'paused'
-
- #: Constant representing the playing state.
- PLAYING = u'playing'
-
- #: Constant representing the stopped state.
- STOPPED = u'stopped'
+logger = logging.getLogger('mopidy.core')
class PlaybackController(object):
- """
- :param backend: the backend
- :type backend: :class:`mopidy.backends.base.Backend`
- :param provider: provider the controller should use
- :type provider: instance of :class:`BasePlaybackProvider`
- """
-
# pylint: disable = R0902
# Too many instance attributes
pykka_traversable = True
- #: :class:`True`
- #: Tracks are removed from the playlist when they have been played.
- #: :class:`False`
- #: Tracks are not removed from the playlist.
- consume = option_wrapper('_consume', False)
+ def __init__(self, audio, backends, core):
+ self.audio = audio
+ self.backends = backends
+ self.core = core
- #: The currently playing or selected track.
- #:
- #: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
- #: :class:`None`.
- current_cp_track = None
-
- #: :class:`True`
- #: Tracks are selected at random from the playlist.
- #: :class:`False`
- #: Tracks are played in the order of the playlist.
- random = option_wrapper('_random', False)
-
- #: :class:`True`
- #: The current playlist is played repeatedly. To repeat a single track,
- #: select both :attr:`repeat` and :attr:`single`.
- #: :class:`False`
- #: The current playlist is played once.
- repeat = option_wrapper('_repeat', False)
-
- #: :class:`True`
- #: Playback is stopped after current song, unless in :attr:`repeat`
- #: mode.
- #: :class:`False`
- #: Playback continues after current song.
- single = option_wrapper('_single', False)
-
- def __init__(self, backend, provider):
- self.backend = backend
- self.provider = provider
self._state = PlaybackState.STOPPED
self._shuffled = []
self._first_shuffle = True
- self.play_time_accumulated = 0
- self.play_time_started = 0
+ self._volume = None
- def _get_cpid(self, cp_track):
- if cp_track is None:
+ def _get_backend(self):
+ if self.current_tl_track is None:
return None
- return cp_track.cpid
+ uri = self.current_tl_track.track.uri
+ uri_scheme = urlparse.urlparse(uri).scheme
+ return self.backends.with_playback_by_uri_scheme.get(uri_scheme, None)
- def _get_track(self, cp_track):
- if cp_track is None:
- return None
- return cp_track.track
+ ### Properties
- @property
- def current_cpid(self):
- """
- The CPID (current playlist ID) of the currently playing or selected
- track.
+ def get_consume(self):
+ return getattr(self, '_consume', False)
- Read-only. Extracted from :attr:`current_cp_track` for convenience.
- """
- return self._get_cpid(self.current_cp_track)
+ def set_consume(self, value):
+ if self.get_consume() != value:
+ self._trigger_options_changed()
+ return setattr(self, '_consume', value)
- @property
- def current_track(self):
- """
- The currently playing or selected :class:`mopidy.models.Track`.
+ consume = property(get_consume, set_consume)
+ """
+ :class:`True`
+ Tracks are removed from the playlist when they have been played.
+ :class:`False`
+ Tracks are not removed from the playlist.
+ """
- Read-only. Extracted from :attr:`current_cp_track` for convenience.
- """
- return self._get_track(self.current_cp_track)
+ current_tl_track = None
+ """
+ The currently playing or selected :class:`mopidy.models.TlTrack`, or
+ :class:`None`.
+ """
- @property
- def current_playlist_position(self):
- """
- The position of the current track in the current playlist.
+ def get_current_track(self):
+ return self.current_tl_track and self.current_tl_track.track
- Read-only.
- """
- if self.current_cp_track is None:
+ current_track = property(get_current_track)
+ """
+ The currently playing or selected :class:`mopidy.models.Track`.
+
+ Read-only. Extracted from :attr:`current_tl_track` for convenience.
+ """
+
+ def get_random(self):
+ return getattr(self, '_random', False)
+
+ def set_random(self, value):
+ if self.get_random() != value:
+ self._trigger_options_changed()
+ return setattr(self, '_random', value)
+
+ random = property(get_random, set_random)
+ """
+ :class:`True`
+ Tracks are selected at random from the playlist.
+ :class:`False`
+ Tracks are played in the order of the playlist.
+ """
+
+ def get_repeat(self):
+ return getattr(self, '_repeat', False)
+
+ def set_repeat(self, value):
+ if self.get_repeat() != value:
+ self._trigger_options_changed()
+ return setattr(self, '_repeat', value)
+
+ repeat = property(get_repeat, set_repeat)
+ """
+ :class:`True`
+ The current playlist is played repeatedly. To repeat a single track,
+ select both :attr:`repeat` and :attr:`single`.
+ :class:`False`
+ The current playlist is played once.
+ """
+
+ def get_single(self):
+ return getattr(self, '_single', False)
+
+ def set_single(self, value):
+ if self.get_single() != value:
+ self._trigger_options_changed()
+ return setattr(self, '_single', value)
+
+ single = property(get_single, set_single)
+ """
+ :class:`True`
+ Playback is stopped after current song, unless in :attr:`repeat`
+ mode.
+ :class:`False`
+ Playback continues after current song.
+ """
+
+ def get_state(self):
+ return self._state
+
+ def set_state(self, new_state):
+ (old_state, self._state) = (self.state, new_state)
+ logger.debug('Changing state: %s -> %s', old_state, new_state)
+
+ self._trigger_playback_state_changed(old_state, new_state)
+
+ state = property(get_state, set_state)
+ """
+ The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
+ :attr:`STOPPED`.
+
+ Possible states and transitions:
+
+ .. digraph:: state_transitions
+
+ "STOPPED" -> "PLAYING" [ label="play" ]
+ "STOPPED" -> "PAUSED" [ label="pause" ]
+ "PLAYING" -> "STOPPED" [ label="stop" ]
+ "PLAYING" -> "PAUSED" [ label="pause" ]
+ "PLAYING" -> "PLAYING" [ label="play" ]
+ "PAUSED" -> "PLAYING" [ label="resume" ]
+ "PAUSED" -> "STOPPED" [ label="stop" ]
+ """
+
+ def get_time_position(self):
+ backend = self._get_backend()
+ if backend:
+ return backend.playback.get_time_position().get()
+ else:
+ return 0
+
+ time_position = property(get_time_position)
+ """Time position in milliseconds."""
+
+ def get_tracklist_position(self):
+ if self.current_tl_track is None:
return None
try:
- return self.backend.current_playlist.cp_tracks.index(
- self.current_cp_track)
+ return self.core.tracklist.tl_tracks.index(self.current_tl_track)
except ValueError:
return None
- @property
- def track_at_eot(self):
- """
- The track that will be played at the end of the current track.
+ tracklist_position = property(get_tracklist_position)
+ """
+ The position of the current track in the tracklist.
- Read-only. A :class:`mopidy.models.Track` extracted from
- :attr:`cp_track_at_eot` for convenience.
- """
- return self._get_track(self.cp_track_at_eot)
+ Read-only.
+ """
- @property
- def cp_track_at_eot(self):
- """
- The track that will be played at the end of the current track.
-
- Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
-
- Not necessarily the same track as :attr:`cp_track_at_next`.
- """
+ def get_tl_track_at_eot(self):
# pylint: disable = R0911
# Too many return statements
- cp_tracks = self.backend.current_playlist.cp_tracks
+ tl_tracks = self.core.tracklist.tl_tracks
- if not cp_tracks:
+ if not tl_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
- self._shuffled = cp_tracks
+ self._shuffled = tl_tracks
random.shuffle(self._shuffled)
self._first_shuffle = False
if self.random and self._shuffled:
return self._shuffled[0]
- if self.current_cp_track is None:
- return cp_tracks[0]
+ if self.current_tl_track is None:
+ return tl_tracks[0]
if self.repeat and self.single:
- return cp_tracks[self.current_playlist_position]
+ return tl_tracks[self.tracklist_position]
if self.repeat and not self.single:
- return cp_tracks[
- (self.current_playlist_position + 1) % len(cp_tracks)]
+ return tl_tracks[(self.tracklist_position + 1) % len(tl_tracks)]
try:
- return cp_tracks[self.current_playlist_position + 1]
+ return tl_tracks[self.tracklist_position + 1]
except IndexError:
return None
- @property
- def track_at_next(self):
- """
- The track that will be played if calling :meth:`next()`.
+ tl_track_at_eot = property(get_tl_track_at_eot)
+ """
+ The track that will be played at the end of the current track.
- Read-only. A :class:`mopidy.models.Track` extracted from
- :attr:`cp_track_at_next` for convenience.
- """
- return self._get_track(self.cp_track_at_next)
+ Read-only. A :class:`mopidy.models.TlTrack`.
- @property
- def cp_track_at_next(self):
- """
- The track that will be played if calling :meth:`next()`.
+ Not necessarily the same track as :attr:`tl_track_at_next`.
+ """
- Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
+ def get_tl_track_at_next(self):
+ tl_tracks = self.core.tracklist.tl_tracks
- For normal playback this is the next track in the playlist. If repeat
- is enabled the next track can loop around the playlist. When random is
- enabled this should be a random track, all tracks should be played once
- before the list repeats.
- """
- cp_tracks = self.backend.current_playlist.cp_tracks
-
- if not cp_tracks:
+ if not tl_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
- self._shuffled = cp_tracks
+ self._shuffled = tl_tracks
random.shuffle(self._shuffled)
self._first_shuffle = False
if self.random and self._shuffled:
return self._shuffled[0]
- if self.current_cp_track is None:
- return cp_tracks[0]
+ if self.current_tl_track is None:
+ return tl_tracks[0]
if self.repeat:
- return cp_tracks[
- (self.current_playlist_position + 1) % len(cp_tracks)]
+ return tl_tracks[(self.tracklist_position + 1) % len(tl_tracks)]
try:
- return cp_tracks[self.current_playlist_position + 1]
+ return tl_tracks[self.tracklist_position + 1]
except IndexError:
return None
- @property
- def track_at_previous(self):
- """
- The track that will be played if calling :meth:`previous()`.
+ tl_track_at_next = property(get_tl_track_at_next)
+ """
+ The track that will be played if calling :meth:`next()`.
- Read-only. A :class:`mopidy.models.Track` extracted from
- :attr:`cp_track_at_previous` for convenience.
- """
- return self._get_track(self.cp_track_at_previous)
+ Read-only. A :class:`mopidy.models.TlTrack`.
- @property
- def cp_track_at_previous(self):
- """
- The track that will be played if calling :meth:`previous()`.
+ For normal playback this is the next track in the playlist. If repeat
+ is enabled the next track can loop around the playlist. When random is
+ enabled this should be a random track, all tracks should be played once
+ before the list repeats.
+ """
- A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
-
- For normal playback this is the previous track in the playlist. If
- random and/or consume is enabled it should return the current track
- instead.
- """
+ def get_tl_track_at_previous(self):
if self.repeat or self.consume or self.random:
- return self.current_cp_track
+ return self.current_tl_track
- if self.current_playlist_position in (None, 0):
+ if self.tracklist_position in (None, 0):
return None
- return self.backend.current_playlist.cp_tracks[
- self.current_playlist_position - 1]
+ return self.core.tracklist.tl_tracks[self.tracklist_position - 1]
- @property
- def state(self):
- """
- The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
- :attr:`STOPPED`.
+ tl_track_at_previous = property(get_tl_track_at_previous)
+ """
+ The track that will be played if calling :meth:`previous()`.
- Possible states and transitions:
+ A :class:`mopidy.models.TlTrack`.
- .. digraph:: state_transitions
+ For normal playback this is the previous track in the playlist. If
+ random and/or consume is enabled it should return the current track
+ instead.
+ """
- "STOPPED" -> "PLAYING" [ label="play" ]
- "STOPPED" -> "PAUSED" [ label="pause" ]
- "PLAYING" -> "STOPPED" [ label="stop" ]
- "PLAYING" -> "PAUSED" [ label="pause" ]
- "PLAYING" -> "PLAYING" [ label="play" ]
- "PAUSED" -> "PLAYING" [ label="resume" ]
- "PAUSED" -> "STOPPED" [ label="stop" ]
- """
- return self._state
+ def get_volume(self):
+ if self.audio:
+ return self.audio.get_volume().get()
+ else:
+ # For testing
+ return self._volume
- @state.setter
- def state(self, new_state):
- (old_state, self._state) = (self.state, new_state)
- logger.debug(u'Changing state: %s -> %s', old_state, new_state)
+ def set_volume(self, volume):
+ if self.audio:
+ self.audio.set_volume(volume)
+ else:
+ # For testing
+ self._volume = volume
- self._trigger_playback_state_changed()
+ volume = property(get_volume, set_volume)
+ """Volume as int in range [0..100] or :class:`None`"""
- # FIXME play_time stuff assumes backend does not have a better way of
- # handeling this stuff :/
- if (old_state in (PlaybackState.PLAYING, PlaybackState.STOPPED)
- and new_state == PlaybackState.PLAYING):
- self._play_time_start()
- elif (old_state == PlaybackState.PLAYING
- and new_state == PlaybackState.PAUSED):
- self._play_time_pause()
- elif (old_state == PlaybackState.PAUSED
- and new_state == PlaybackState.PLAYING):
- self._play_time_resume()
+ ### Methods
- @property
- def time_position(self):
- """Time position in milliseconds."""
- if self.state == PlaybackState.PLAYING:
- time_since_started = (self._current_wall_time -
- self.play_time_started)
- return self.play_time_accumulated + time_since_started
- elif self.state == PlaybackState.PAUSED:
- return self.play_time_accumulated
- elif self.state == PlaybackState.STOPPED:
- return 0
-
- def _play_time_start(self):
- 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
-
- def _play_time_resume(self):
- self.play_time_started = self._current_wall_time
-
- @property
- def _current_wall_time(self):
- return int(time.time() * 1000)
-
- @property
- def volume(self):
- return self.provider.get_volume()
-
- @volume.setter
- def volume(self, volume):
- self.provider.set_volume(volume)
-
- def change_track(self, cp_track, on_error_step=1):
+ def change_track(self, tl_track, on_error_step=1):
"""
Change to the given track, keeping the current playback state.
- :param cp_track: track to change to
- :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
- or :class:`None`
+ :param tl_track: track to change to
+ :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track
:type on_error_step: int, -1 or 1
-
"""
old_state = self.state
self.stop()
- self.current_cp_track = cp_track
+ self.current_tl_track = tl_track
if old_state == PlaybackState.PLAYING:
self.play(on_error_step=on_error_step)
elif old_state == PlaybackState.PAUSED:
@@ -362,33 +312,35 @@ class PlaybackController(object):
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
+
+ Used by event handler in :class:`mopidy.core.Core`.
"""
if self.state == PlaybackState.STOPPED:
return
- original_cp_track = self.current_cp_track
+ original_tl_track = self.current_tl_track
- if self.cp_track_at_eot:
+ if self.tl_track_at_eot:
self._trigger_track_playback_ended()
- self.play(self.cp_track_at_eot)
+ self.play(self.tl_track_at_eot)
else:
self.stop(clear_current_track=True)
if self.consume:
- self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
+ self.core.tracklist.remove(tlid=original_tl_track.tlid)
- def on_current_playlist_change(self):
+ def on_tracklist_change(self):
"""
Tell the playback controller that the current playlist has changed.
- Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
+ Used by :class:`mopidy.core.TracklistController`.
"""
self._first_shuffle = True
self._shuffled = []
- if (not self.backend.current_playlist.cp_tracks or
- self.current_cp_track not in
- self.backend.current_playlist.cp_tracks):
+ if (not self.core.tracklist.tl_tracks or
+ self.current_tl_track not in
+ self.core.tracklist.tl_tracks):
self.stop(clear_current_track=True)
def next(self):
@@ -398,57 +350,58 @@ class PlaybackController(object):
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
- if self.cp_track_at_next:
+ if self.tl_track_at_next:
self._trigger_track_playback_ended()
- self.change_track(self.cp_track_at_next)
+ self.change_track(self.tl_track_at_next)
else:
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
- if self.provider.pause():
+ backend = self._get_backend()
+ if not backend or backend.playback.pause().get():
self.state = PlaybackState.PAUSED
self._trigger_track_playback_paused()
- def play(self, cp_track=None, on_error_step=1):
+ def play(self, tl_track=None, on_error_step=1):
"""
Play the given track, or if the given track is :class:`None`, play the
currently active track.
- :param cp_track: track to play
- :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
- or :class:`None`
+ :param tl_track: track to play
+ :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track
:type on_error_step: int, -1 or 1
"""
- if cp_track is not None:
- assert cp_track in self.backend.current_playlist.cp_tracks
- elif cp_track is None:
+ if tl_track is not None:
+ assert tl_track in self.core.tracklist.tl_tracks
+ elif tl_track is None:
if self.state == PlaybackState.PAUSED:
return self.resume()
- elif self.current_cp_track is not None:
- cp_track = self.current_cp_track
- elif self.current_cp_track is None and on_error_step == 1:
- cp_track = self.cp_track_at_next
- elif self.current_cp_track is None and on_error_step == -1:
- cp_track = self.cp_track_at_previous
+ elif self.current_tl_track is not None:
+ tl_track = self.current_tl_track
+ elif self.current_tl_track is None and on_error_step == 1:
+ tl_track = self.tl_track_at_next
+ elif self.current_tl_track is None and on_error_step == -1:
+ tl_track = self.tl_track_at_previous
- if cp_track is not None:
- self.current_cp_track = cp_track
+ if tl_track is not None:
+ self.current_tl_track = tl_track
self.state = PlaybackState.PLAYING
- if not self.provider.play(cp_track.track):
+ backend = self._get_backend()
+ if not backend or not backend.playback.play(tl_track.track).get():
# Track is not playable
if self.random and self._shuffled:
- self._shuffled.remove(cp_track)
+ self._shuffled.remove(tl_track)
if on_error_step == 1:
self.next()
elif on_error_step == -1:
self.previous()
- if self.random and self.current_cp_track in self._shuffled:
- self._shuffled.remove(self.current_cp_track)
+ if self.random and self.current_tl_track in self._shuffled:
+ self._shuffled.remove(self.current_tl_track)
self._trigger_track_playback_started()
@@ -460,11 +413,14 @@ class PlaybackController(object):
will continue. If it was paused, it will still be paused, etc.
"""
self._trigger_track_playback_ended()
- self.change_track(self.cp_track_at_previous, on_error_step=-1)
+ self.change_track(self.tl_track_at_previous, on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""
- if self.state == PlaybackState.PAUSED and self.provider.resume():
+ if self.state != PlaybackState.PAUSED:
+ return
+ backend = self._get_backend()
+ if backend and backend.playback.resume().get():
self.state = PlaybackState.PLAYING
self._trigger_track_playback_resumed()
@@ -476,7 +432,7 @@ class PlaybackController(object):
:type time_position: int
:rtype: :class:`True` if successful, else :class:`False`
"""
- if not self.backend.current_playlist.tracks:
+ if not self.core.tracklist.tracks:
return False
if self.state == PlaybackState.STOPPED:
@@ -490,12 +446,13 @@ class PlaybackController(object):
self.next()
return True
- self.play_time_started = self._current_wall_time
- self.play_time_accumulated = time_position
+ backend = self._get_backend()
+ if not backend:
+ return False
- success = self.provider.seek(time_position)
+ success = backend.playback.seek(time_position).get()
if success:
- self._trigger_seeked()
+ self._trigger_seeked(time_position)
return success
def stop(self, clear_current_track=False):
@@ -507,51 +464,54 @@ class PlaybackController(object):
:type clear_current_track: boolean
"""
if self.state != PlaybackState.STOPPED:
- if self.provider.stop():
+ backend = self._get_backend()
+ if not backend or backend.playback.stop().get():
self._trigger_track_playback_ended()
self.state = PlaybackState.STOPPED
if clear_current_track:
- self.current_cp_track = None
+ self.current_tl_track = None
def _trigger_track_playback_paused(self):
- logger.debug(u'Triggering track playback paused event')
+ logger.debug('Triggering track playback paused event')
if self.current_track is None:
return
- BackendListener.send('track_playback_paused',
- track=self.current_track,
- time_position=self.time_position)
+ listener.CoreListener.send(
+ 'track_playback_paused',
+ track=self.current_track, time_position=self.time_position)
def _trigger_track_playback_resumed(self):
- logger.debug(u'Triggering track playback resumed event')
+ logger.debug('Triggering track playback resumed event')
if self.current_track is None:
return
- BackendListener.send('track_playback_resumed',
- track=self.current_track,
- time_position=self.time_position)
+ listener.CoreListener.send(
+ 'track_playback_resumed',
+ track=self.current_track, time_position=self.time_position)
def _trigger_track_playback_started(self):
- logger.debug(u'Triggering track playback started event')
+ logger.debug('Triggering track playback started event')
if self.current_track is None:
return
- BackendListener.send('track_playback_started',
- track=self.current_track)
+ listener.CoreListener.send(
+ 'track_playback_started', track=self.current_track)
def _trigger_track_playback_ended(self):
- logger.debug(u'Triggering track playback ended event')
+ logger.debug('Triggering track playback ended event')
if self.current_track is None:
return
- BackendListener.send('track_playback_ended',
- track=self.current_track,
- time_position=self.time_position)
+ listener.CoreListener.send(
+ 'track_playback_ended',
+ track=self.current_track, time_position=self.time_position)
- def _trigger_playback_state_changed(self):
- logger.debug(u'Triggering playback state change event')
- BackendListener.send('playback_state_changed')
+ def _trigger_playback_state_changed(self, old_state, new_state):
+ logger.debug('Triggering playback state change event')
+ listener.CoreListener.send(
+ 'playback_state_changed',
+ old_state=old_state, new_state=new_state)
def _trigger_options_changed(self):
- logger.debug(u'Triggering options changed event')
- BackendListener.send('options_changed')
+ logger.debug('Triggering options changed event')
+ listener.CoreListener.send('options_changed')
- def _trigger_seeked(self):
- logger.debug(u'Triggering seeked event')
- BackendListener.send('seeked')
+ def _trigger_seeked(self, time_position):
+ logger.debug('Triggering seeked event')
+ listener.CoreListener.send('seeked', time_position=time_position)
diff --git a/mopidy/core/playlists.py b/mopidy/core/playlists.py
new file mode 100644
index 00000000..6a368ac6
--- /dev/null
+++ b/mopidy/core/playlists.py
@@ -0,0 +1,164 @@
+from __future__ import unicode_literals
+
+import itertools
+import urlparse
+
+import pykka
+
+from . import listener
+
+
+class PlaylistsController(object):
+ pykka_traversable = True
+
+ def __init__(self, backends, core):
+ self.backends = backends
+ self.core = core
+
+ def get_playlists(self):
+ futures = [
+ b.playlists.playlists for b in self.backends.with_playlists]
+ results = pykka.get_all(futures)
+ return list(itertools.chain(*results))
+
+ playlists = property(get_playlists)
+ """
+ The available playlists.
+
+ Read-only. List of :class:`mopidy.models.Playlist`.
+ """
+
+ def create(self, name, uri_scheme=None):
+ """
+ Create a new playlist.
+
+ If ``uri_scheme`` matches an URI scheme handled by a current backend,
+ that backend is asked to create the playlist. If ``uri_scheme`` is
+ :class:`None` or doesn't match a current backend, the first backend is
+ asked to create the playlist.
+
+ All new playlists should be created by calling this method, and **not**
+ by creating new instances of :class:`mopidy.models.Playlist`.
+
+ :param name: name of the new playlist
+ :type name: string
+ :param uri_scheme: use the backend matching the URI scheme
+ :type uri_scheme: string
+ :rtype: :class:`mopidy.models.Playlist`
+ """
+ if uri_scheme in self.backends.with_playlists_by_uri_scheme:
+ backend = self.backends.by_uri_scheme[uri_scheme]
+ else:
+ backend = self.backends.with_playlists[0]
+ playlist = backend.playlists.create(name).get()
+ listener.CoreListener.send('playlist_changed', playlist=playlist)
+ return playlist
+
+ def delete(self, uri):
+ """
+ Delete playlist identified by the URI.
+
+ If the URI doesn't match the URI schemes handled by the current
+ backends, nothing happens.
+
+ :param uri: URI of the playlist to delete
+ :type uri: string
+ """
+ uri_scheme = urlparse.urlparse(uri).scheme
+ backend = self.backends.with_playlists_by_uri_scheme.get(
+ uri_scheme, None)
+ if backend:
+ backend.playlists.delete(uri).get()
+
+ def filter(self, **criteria):
+ """
+ Filter playlists by the given criterias.
+
+ Examples::
+
+ filter(name='a') # Returns track with name 'a'
+ filter(uri='xyz') # Returns track with URI 'xyz'
+ filter(name='a', uri='xyz') # Returns track with name 'a' and URI
+ # 'xyz'
+
+ :param criteria: one or more criteria to match by
+ :type criteria: dict
+ :rtype: list of :class:`mopidy.models.Playlist`
+ """
+ matches = self.playlists
+ for (key, value) in criteria.iteritems():
+ matches = filter(lambda p: getattr(p, key) == value, matches)
+ return matches
+
+ def lookup(self, uri):
+ """
+ Lookup playlist with given URI in both the set of playlists and in any
+ other playlist sources. Returns :class:`None` if not found.
+
+ :param uri: playlist URI
+ :type uri: string
+ :rtype: :class:`mopidy.models.Playlist` or :class:`None`
+ """
+ uri_scheme = urlparse.urlparse(uri).scheme
+ backend = self.backends.with_playlists_by_uri_scheme.get(
+ uri_scheme, None)
+ if backend:
+ return backend.playlists.lookup(uri).get()
+ else:
+ return None
+
+ def refresh(self, uri_scheme=None):
+ """
+ Refresh the playlists in :attr:`playlists`.
+
+ If ``uri_scheme`` is :class:`None`, all backends are asked to refresh.
+ If ``uri_scheme`` is an URI scheme handled by a backend, only that
+ backend is asked to refresh. If ``uri_scheme`` doesn't match any
+ current backend, nothing happens.
+
+ :param uri_scheme: limit to the backend matching the URI scheme
+ :type uri_scheme: string
+ """
+ if uri_scheme is None:
+ futures = [
+ b.playlists.refresh() for b in self.backends.with_playlists]
+ pykka.get_all(futures)
+ listener.CoreListener.send('playlists_loaded')
+ else:
+ backend = self.backends.with_playlists_by_uri_scheme.get(
+ uri_scheme, None)
+ if backend:
+ backend.playlists.refresh().get()
+ listener.CoreListener.send('playlists_loaded')
+
+ def save(self, playlist):
+ """
+ Save the playlist.
+
+ For a playlist to be saveable, it must have the ``uri`` attribute set.
+ You should not set the ``uri`` atribute yourself, but use playlist
+ objects returned by :meth:`create` or retrieved from :attr:`playlists`,
+ which will always give you saveable playlists.
+
+ The method returns the saved playlist. The return playlist may differ
+ from the saved playlist. E.g. if the playlist name was changed, the
+ returned playlist may have a different URI. The caller of this method
+ should throw away the playlist sent to this method, and use the
+ returned playlist instead.
+
+ If the playlist's URI isn't set or doesn't match the URI scheme of a
+ current backend, nothing is done and :class:`None` is returned.
+
+ :param playlist: the playlist
+ :type playlist: :class:`mopidy.models.Playlist`
+ :rtype: :class:`mopidy.models.Playlist` or :class:`None`
+ """
+ if playlist.uri is None:
+ return
+ uri_scheme = urlparse.urlparse(playlist.uri).scheme
+ backend = self.backends.with_playlists_by_uri_scheme.get(
+ uri_scheme, None)
+ if backend:
+ playlist = backend.playlists.save(playlist).get()
+ listener.CoreListener.send('playlist_changed', playlist=playlist)
+ return playlist
diff --git a/mopidy/core/stored_playlists.py b/mopidy/core/stored_playlists.py
deleted file mode 100644
index a29e34fc..00000000
--- a/mopidy/core/stored_playlists.py
+++ /dev/null
@@ -1,113 +0,0 @@
-class StoredPlaylistsController(object):
- """
- :param backend: backend the controller is a part of
- :type backend: :class:`mopidy.backends.base.Backend`
- :param provider: provider the controller should use
- :type provider: instance of :class:`BaseStoredPlaylistsProvider`
- """
-
- pykka_traversable = True
-
- def __init__(self, backend, provider):
- self.backend = backend
- self.provider = provider
-
- @property
- def playlists(self):
- """
- Currently stored playlists.
-
- Read/write. List of :class:`mopidy.models.Playlist`.
- """
- return self.provider.playlists
-
- @playlists.setter
- def playlists(self, playlists):
- self.provider.playlists = playlists
-
- def create(self, name):
- """
- Create a new playlist.
-
- :param name: name of the new playlist
- :type name: string
- :rtype: :class:`mopidy.models.Playlist`
- """
- return self.provider.create(name)
-
- def delete(self, playlist):
- """
- Delete playlist.
-
- :param playlist: the playlist to delete
- :type playlist: :class:`mopidy.models.Playlist`
- """
- return self.provider.delete(playlist)
-
- def get(self, **criteria):
- """
- Get playlist by given criterias from the set of stored playlists.
-
- Raises :exc:`LookupError` if a unique match is not found.
-
- Examples::
-
- get(name='a') # Returns track with name 'a'
- get(uri='xyz') # Returns track with URI 'xyz'
- get(name='a', uri='xyz') # Returns track with name 'a' and URI
- # 'xyz'
-
- :param criteria: one or more criteria to match by
- :type criteria: dict
- :rtype: :class:`mopidy.models.Playlist`
- """
- matches = self.playlists
- for (key, value) in criteria.iteritems():
- matches = filter(lambda p: getattr(p, key) == value, matches)
- if len(matches) == 1:
- return matches[0]
- criteria_string = ', '.join(
- ['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
- if len(matches) == 0:
- raise LookupError('"%s" match no playlists' % criteria_string)
- else:
- raise LookupError('"%s" match multiple playlists'
- % criteria_string)
-
- def lookup(self, uri):
- """
- Lookup playlist with given URI in both the set of stored playlists and
- in any other playlist sources.
-
- :param uri: playlist URI
- :type uri: string
- :rtype: :class:`mopidy.models.Playlist`
- """
- return self.provider.lookup(uri)
-
- def refresh(self):
- """
- Refresh the stored playlists in
- :attr:`mopidy.backends.base.StoredPlaylistsController.playlists`.
- """
- return self.provider.refresh()
-
- def rename(self, playlist, new_name):
- """
- Rename playlist.
-
- :param playlist: the playlist
- :type playlist: :class:`mopidy.models.Playlist`
- :param new_name: the new name
- :type new_name: string
- """
- return self.provider.rename(playlist, new_name)
-
- def save(self, playlist):
- """
- Save the playlist to the set of stored playlists.
-
- :param playlist: the playlist
- :type playlist: :class:`mopidy.models.Playlist`
- """
- return self.provider.save(playlist)
diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py
new file mode 100644
index 00000000..656e15b1
--- /dev/null
+++ b/mopidy/core/tracklist.py
@@ -0,0 +1,240 @@
+from __future__ import unicode_literals
+
+import logging
+import random
+
+from mopidy.models import TlTrack
+
+from . import listener
+
+
+logger = logging.getLogger('mopidy.core')
+
+
+class TracklistController(object):
+ pykka_traversable = True
+
+ def __init__(self, core):
+ self._core = core
+ self._next_tlid = 0
+ self._tl_tracks = []
+ self._version = 0
+
+ def get_tl_tracks(self):
+ return self._tl_tracks[:]
+
+ tl_tracks = property(get_tl_tracks)
+ """
+ List of :class:`mopidy.models.TlTrack`.
+
+ Read-only.
+ """
+
+ def get_tracks(self):
+ return [tl_track.track for tl_track in self._tl_tracks]
+
+ tracks = property(get_tracks)
+ """
+ List of :class:`mopidy.models.Track` in the tracklist.
+
+ Read-only.
+ """
+
+ def get_length(self):
+ return len(self._tl_tracks)
+
+ length = property(get_length)
+ """Length of the tracklist."""
+
+ def get_version(self):
+ return self._version
+
+ def _increase_version(self):
+ self._version += 1
+ self._core.playback.on_tracklist_change()
+ self._trigger_tracklist_changed()
+
+ version = property(get_version)
+ """
+ The tracklist version.
+
+ Read-only. Integer which is increased every time the tracklist is changed.
+ Is not reset before Mopidy is restarted.
+ """
+
+ def add(self, tracks, at_position=None):
+ """
+ Add the track or list of tracks to the tracklist.
+
+ If ``at_position`` is given, the tracks placed at the given position in
+ the tracklist. If ``at_position`` is not given, the tracks are appended
+ to the end of the tracklist.
+
+ Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
+
+ :param tracks: tracks to add
+ :type tracks: list of :class:`mopidy.models.Track`
+ :param at_position: position in tracklist to add track
+ :type at_position: int or :class:`None`
+ :rtype: list of :class:`mopidy.models.TlTrack`
+ """
+ tl_tracks = []
+ for track in tracks:
+ tl_track = TlTrack(self._next_tlid, track)
+ self._next_tlid += 1
+ if at_position is not None:
+ self._tl_tracks.insert(at_position, tl_track)
+ at_position += 1
+ else:
+ self._tl_tracks.append(tl_track)
+ tl_tracks.append(tl_track)
+
+ if tl_tracks:
+ self._increase_version()
+
+ return tl_tracks
+
+ def clear(self):
+ """
+ Clear the tracklist.
+
+ Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
+ """
+ self._tl_tracks = []
+ self._increase_version()
+
+ def filter(self, **criteria):
+ """
+ Filter the tracklist by the given criterias.
+
+ Examples::
+
+ filter(tlid=7) # Returns track with TLID 7 (tracklist ID)
+ filter(id=1) # Returns track with ID 1
+ filter(uri='xyz') # Returns track with URI 'xyz'
+ filter(id=1, uri='xyz') # Returns track with ID 1 and URI 'xyz'
+
+ :param criteria: on or more criteria to match by
+ :type criteria: dict
+ :rtype: list of :class:`mopidy.models.TlTrack`
+ """
+ matches = self._tl_tracks
+ for (key, value) in criteria.iteritems():
+ if key == 'tlid':
+ matches = filter(lambda ct: ct.tlid == value, matches)
+ else:
+ matches = filter(
+ lambda ct: getattr(ct.track, key) == value, matches)
+ return matches
+
+ def index(self, tl_track):
+ """
+ Get index of the given :class:`mopidy.models.TlTrack` in the tracklist.
+
+ Raises :exc:`ValueError` if not found.
+
+ :param tl_track: track to find the index of
+ :type tl_track: :class:`mopidy.models.TlTrack`
+ :rtype: int
+ """
+ return self._tl_tracks.index(tl_track)
+
+ def move(self, start, end, to_position):
+ """
+ Move the tracks in the slice ``[start:end]`` to ``to_position``.
+
+ Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
+
+ :param start: position of first track to move
+ :type start: int
+ :param end: position after last track to move
+ :type end: int
+ :param to_position: new position for the tracks
+ :type to_position: int
+ """
+ if start == end:
+ end += 1
+
+ tl_tracks = self._tl_tracks
+
+ assert start < end, 'start must be smaller than end'
+ assert start >= 0, 'start must be at least zero'
+ assert end <= len(tl_tracks), \
+ 'end can not be larger than tracklist length'
+ assert to_position >= 0, 'to_position must be at least zero'
+ assert to_position <= len(tl_tracks), \
+ 'to_position can not be larger than tracklist length'
+
+ new_tl_tracks = tl_tracks[:start] + tl_tracks[end:]
+ for tl_track in tl_tracks[start:end]:
+ new_tl_tracks.insert(to_position, tl_track)
+ to_position += 1
+ self._tl_tracks = new_tl_tracks
+ self._increase_version()
+
+ def remove(self, **criteria):
+ """
+ Remove the matching tracks from the tracklist.
+
+ Uses :meth:`filter()` to lookup the tracks to remove.
+
+ Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
+
+ :param criteria: on or more criteria to match by
+ :type criteria: dict
+ :rtype: list of :class:`mopidy.models.TlTrack` that was removed
+ """
+ tl_tracks = self.filter(**criteria)
+ for tl_track in tl_tracks:
+ position = self._tl_tracks.index(tl_track)
+ del self._tl_tracks[position]
+ self._increase_version()
+ return tl_tracks
+
+ def shuffle(self, start=None, end=None):
+ """
+ Shuffles the entire tracklist. If ``start`` and ``end`` is given only
+ shuffles the slice ``[start:end]``.
+
+ Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event.
+
+ :param start: position of first track to shuffle
+ :type start: int or :class:`None`
+ :param end: position after last track to shuffle
+ :type end: int or :class:`None`
+ """
+ tl_tracks = self._tl_tracks
+
+ if start is not None and end is not None:
+ assert start < end, 'start must be smaller than end'
+
+ if start is not None:
+ assert start >= 0, 'start must be at least zero'
+
+ if end is not None:
+ assert end <= len(tl_tracks), 'end can not be larger than ' + \
+ 'tracklist length'
+
+ before = tl_tracks[:start or 0]
+ shuffled = tl_tracks[start:end]
+ after = tl_tracks[end or len(tl_tracks):]
+ random.shuffle(shuffled)
+ self._tl_tracks = before + shuffled + after
+ self._increase_version()
+
+ def slice(self, start, end):
+ """
+ Returns a slice of the tracklist, limited by the given start and end
+ positions.
+
+ :param start: position of first track to include in slice
+ :type start: int
+ :param end: position after last track to include in slice
+ :type end: int
+ :rtype: :class:`mopidy.models.TlTrack`
+ """
+ return self._tl_tracks[start:end]
+
+ def _trigger_tracklist_changed(self):
+ logger.debug('Triggering event: tracklist_changed()')
+ listener.CoreListener.send('tracklist_changed')
diff --git a/mopidy/exceptions.py b/mopidy/exceptions.py
new file mode 100644
index 00000000..b8d183fb
--- /dev/null
+++ b/mopidy/exceptions.py
@@ -0,0 +1,24 @@
+from __future__ import unicode_literals
+
+
+class MopidyException(Exception):
+ def __init__(self, message, *args, **kwargs):
+ super(MopidyException, self).__init__(message, *args, **kwargs)
+ self._message = message
+
+ @property
+ def message(self):
+ """Reimplement message field that was deprecated in Python 2.6"""
+ return self._message
+
+ @message.setter # noqa
+ def message(self, message):
+ self._message = message
+
+
+class SettingsError(MopidyException):
+ pass
+
+
+class OptionalDependencyError(MopidyException):
+ pass
diff --git a/mopidy/frontends/__init__.py b/mopidy/frontends/__init__.py
index e69de29b..baffc488 100644
--- a/mopidy/frontends/__init__.py
+++ b/mopidy/frontends/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py
index 0e79024b..7f367262 100644
--- a/mopidy/frontends/lastfm.py
+++ b/mopidy/frontends/lastfm.py
@@ -1,42 +1,50 @@
+"""
+Frontend which scrobbles the music you play to your `Last.fm
+`_ profile.
+
+.. note::
+
+ This frontend requires a free user account at Last.fm.
+
+**Dependencies:**
+
+- `pylast `_ >= 0.5.7
+
+**Settings:**
+
+- :attr:`mopidy.settings.LASTFM_USERNAME`
+- :attr:`mopidy.settings.LASTFM_PASSWORD`
+
+**Usage:**
+
+Make sure :attr:`mopidy.settings.FRONTENDS` includes
+``mopidy.frontends.lastfm.LastfmFrontend``. By default, the setting includes
+the Last.fm frontend.
+"""
+
+from __future__ import unicode_literals
+
import logging
import time
+import pykka
+
+from mopidy import exceptions, settings
+from mopidy.core import CoreListener
+
try:
import pylast
except ImportError as import_error:
- from mopidy import OptionalDependencyError
- raise OptionalDependencyError(import_error)
-
-from pykka.actor import ThreadingActor
-
-from mopidy import settings, SettingsError
-from mopidy.listeners import BackendListener
+ raise exceptions.OptionalDependencyError(import_error)
logger = logging.getLogger('mopidy.frontends.lastfm')
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
-class LastfmFrontend(ThreadingActor, BackendListener):
- """
- Frontend which scrobbles the music you play to your `Last.fm
- `_ profile.
- .. note::
-
- This frontend requires a free user account at Last.fm.
-
- **Dependencies:**
-
- - `pylast `_ >= 0.5.7
-
- **Settings:**
-
- - :attr:`mopidy.settings.LASTFM_USERNAME`
- - :attr:`mopidy.settings.LASTFM_PASSWORD`
- """
-
- def __init__(self):
+class LastfmFrontend(pykka.ThreadingActor, CoreListener):
+ def __init__(self, core):
super(LastfmFrontend, self).__init__()
self.lastfm = None
self.last_start_time = None
@@ -48,21 +56,21 @@ class LastfmFrontend(ThreadingActor, BackendListener):
self.lastfm = pylast.LastFMNetwork(
api_key=API_KEY, api_secret=API_SECRET,
username=username, password_hash=password_hash)
- logger.info(u'Connected to Last.fm')
- except SettingsError as e:
- logger.info(u'Last.fm scrobbler not started')
- logger.debug(u'Last.fm settings error: %s', e)
+ logger.info('Connected to Last.fm')
+ except exceptions.SettingsError as e:
+ logger.info('Last.fm scrobbler not started')
+ logger.debug('Last.fm settings error: %s', e)
self.stop()
except (pylast.NetworkError, pylast.MalformedResponseError,
pylast.WSError) as e:
- logger.error(u'Error during Last.fm setup: %s', e)
+ logger.error('Error during Last.fm setup: %s', e)
self.stop()
def track_playback_started(self, track):
artists = ', '.join([a.name for a in track.artists])
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)
+ logger.debug('Now playing track: %s - %s', artists, track.name)
try:
self.lastfm.update_now_playing(
artists,
@@ -73,22 +81,22 @@ class LastfmFrontend(ThreadingActor, BackendListener):
mbid=(track.musicbrainz_id or ''))
except (pylast.ScrobblingError, pylast.NetworkError,
pylast.MalformedResponseError, pylast.WSError) as e:
- logger.warning(u'Error submitting playing track to Last.fm: %s', e)
+ logger.warning('Error submitting playing track to Last.fm: %s', e)
def track_playback_ended(self, track, time_position):
artists = ', '.join([a.name for a in track.artists])
duration = track.length and track.length // 1000 or 0
time_position = time_position // 1000
if duration < 30:
- logger.debug(u'Track too short to scrobble. (30s)')
+ logger.debug('Track too short to scrobble. (30s)')
return
if time_position < duration // 2 and time_position < 240:
logger.debug(
- u'Track not played long enough to scrobble. (50% or 240s)')
+ 'Track not played long enough to scrobble. (50% or 240s)')
return
if self.last_start_time is None:
self.last_start_time = int(time.time()) - duration
- logger.debug(u'Scrobbling track: %s - %s', artists, track.name)
+ logger.debug('Scrobbling track: %s - %s', artists, track.name)
try:
self.lastfm.scrobble(
artists,
@@ -100,4 +108,4 @@ class LastfmFrontend(ThreadingActor, BackendListener):
mbid=(track.musicbrainz_id or ''))
except (pylast.ScrobblingError, pylast.NetworkError,
pylast.MalformedResponseError, pylast.WSError) as e:
- logger.warning(u'Error submitting played track to Last.fm: %s', e)
+ logger.warning('Error submitting played track to Last.fm: %s', e)
diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py
index e8b2aabe..572192ef 100644
--- a/mopidy/frontends/mpd/__init__.py
+++ b/mopidy/frontends/mpd/__init__.py
@@ -1,110 +1,27 @@
-import logging
-import sys
+"""The MPD server frontend.
-from pykka import registry, actor
+MPD stands for Music Player Daemon. MPD is an independent project and server.
+Mopidy implements the MPD protocol, and is thus compatible with clients for the
+original MPD server.
-from mopidy import listeners, settings
-from mopidy.frontends.mpd import dispatcher, protocol
-from mopidy.utils import locale_decode, log, network, process
+**Dependencies:**
-logger = logging.getLogger('mopidy.frontends.mpd')
+- None
-class MpdFrontend(actor.ThreadingActor, listeners.BackendListener):
- """
- The MPD frontend.
+**Settings:**
- **Dependencies:**
+- :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`
+- :attr:`mopidy.settings.MPD_SERVER_PORT`
+- :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
- - None
+**Usage:**
- **Settings:**
+Make sure :attr:`mopidy.settings.FRONTENDS` includes
+``mopidy.frontends.mpd.MpdFrontend``. By default, the setting includes the MPD
+frontend.
+"""
- - :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`
- - :attr:`mopidy.settings.MPD_SERVER_PORT`
- - :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
- """
+from __future__ import unicode_literals
- def __init__(self):
- super(MpdFrontend, self).__init__()
- hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
- port = settings.MPD_SERVER_PORT
-
- try:
- network.Server(hostname, port, protocol=MpdSession,
- max_connections=settings.MPD_SERVER_MAX_CONNECTIONS)
- except IOError as error:
- logger.error(u'MPD server startup failed: %s', locale_decode(error))
- sys.exit(1)
-
- logger.info(u'MPD server running at [%s]:%s', hostname, port)
-
- def on_stop(self):
- process.stop_actors_by_class(MpdSession)
-
- def send_idle(self, subsystem):
- # FIXME this should be updated once pykka supports non-blocking calls
- # on proxies or some similar solution
- registry.ActorRegistry.broadcast({
- 'command': 'pykka_call',
- 'attr_path': ('on_idle',),
- 'args': [subsystem],
- 'kwargs': {},
- }, target_class=MpdSession)
-
- def playback_state_changed(self):
- self.send_idle('player')
-
- def playlist_changed(self):
- self.send_idle('playlist')
-
- def options_changed(self):
- self.send_idle('options')
-
- def volume_changed(self):
- self.send_idle('mixer')
-
-
-class MpdSession(network.LineProtocol):
- """
- The MPD client session. Keeps track of a single client session. Any
- requests from the client is passed on to the MPD request dispatcher.
- """
-
- terminator = protocol.LINE_TERMINATOR
- encoding = protocol.ENCODING
- delimeter = r'\r?\n'
-
- def __init__(self, connection):
- super(MpdSession, self).__init__(connection)
- self.dispatcher = dispatcher.MpdDispatcher(self)
-
- def on_start(self):
- logger.info(u'New MPD connection from [%s]:%s', self.host, self.port)
- self.send_lines([u'OK MPD %s' % protocol.VERSION])
-
- def on_line_received(self, line):
- logger.debug(u'Request from [%s]:%s to %s: %s', self.host, self.port,
- self.actor_urn, line)
-
- response = self.dispatcher.handle_request(line)
- if not response:
- return
-
- logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port,
- self.actor_urn, log.indent(self.terminator.join(response)))
-
- self.send_lines(response)
-
- def on_idle(self, subsystem):
- self.dispatcher.handle_idle(subsystem)
-
- def decode(self, line):
- try:
- return super(MpdSession, self).decode(line.decode('string_escape'))
- except ValueError:
- logger.warning(u'Stopping actor due to unescaping error, data '
- 'supplied by client was not valid.')
- self.stop()
-
- def close(self):
- self.stop()
+# flake8: noqa
+from .actor import MpdFrontend
diff --git a/mopidy/frontends/mpd/actor.py b/mopidy/frontends/mpd/actor.py
new file mode 100644
index 00000000..925b15b7
--- /dev/null
+++ b/mopidy/frontends/mpd/actor.py
@@ -0,0 +1,53 @@
+from __future__ import unicode_literals
+
+import logging
+import sys
+
+import pykka
+
+from mopidy import settings
+from mopidy.core import CoreListener
+from mopidy.frontends.mpd import session
+from mopidy.utils import encoding, network, process
+
+logger = logging.getLogger('mopidy.frontends.mpd')
+
+
+class MpdFrontend(pykka.ThreadingActor, CoreListener):
+ def __init__(self, core):
+ super(MpdFrontend, self).__init__()
+ hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
+ port = settings.MPD_SERVER_PORT
+
+ try:
+ network.Server(
+ hostname, port,
+ protocol=session.MpdSession, protocol_kwargs={'core': core},
+ max_connections=settings.MPD_SERVER_MAX_CONNECTIONS)
+ except IOError as error:
+ logger.error(
+ 'MPD server startup failed: %s',
+ encoding.locale_decode(error))
+ sys.exit(1)
+
+ logger.info('MPD server running at [%s]:%s', hostname, port)
+
+ def on_stop(self):
+ process.stop_actors_by_class(session.MpdSession)
+
+ def send_idle(self, subsystem):
+ listeners = pykka.ActorRegistry.get_by_class(session.MpdSession)
+ for listener in listeners:
+ getattr(listener.proxy(), 'on_idle')(subsystem)
+
+ def playback_state_changed(self, old_state, new_state):
+ self.send_idle('player')
+
+ def tracklist_changed(self):
+ self.send_idle('playlist')
+
+ def options_changed(self):
+ self.send_idle('options')
+
+ def volume_changed(self):
+ self.send_idle('mixer')
diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py
index 94ac6bf9..4f0001ac 100644
--- a/mopidy/frontends/mpd/dispatcher.py
+++ b/mopidy/frontends/mpd/dispatcher.py
@@ -1,24 +1,18 @@
+from __future__ import unicode_literals
+
import logging
import re
-from pykka import ActorDeadError
-from pykka.registry import ActorRegistry
+import pykka
from mopidy import settings
-from mopidy.backends.base import Backend
-from mopidy.frontends.mpd import exceptions
-from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
-# Do not remove the following import. The protocol modules must be imported to
-# get them registered as request handlers.
-# pylint: disable = W0611
-from mopidy.frontends.mpd.protocol import (audio_output, command_list,
- connection, current_playlist, empty, music_db, playback, reflection,
- status, stickers, stored_playlists)
-# pylint: enable = W0611
-from mopidy.utils import flatten
+from mopidy.frontends.mpd import exceptions, protocol
logger = logging.getLogger('mopidy.frontends.mpd.dispatcher')
+protocol.load_protocol_modules()
+
+
class MpdDispatcher(object):
"""
The MPD session feeds the MPD dispatcher with requests. The dispatcher
@@ -28,12 +22,13 @@ class MpdDispatcher(object):
_noidle = re.compile(r'^noidle$')
- def __init__(self, session=None):
+ def __init__(self, session=None, core=None):
self.authenticated = False
- self.command_list = False
+ self.command_list_receiving = False
self.command_list_ok = False
+ self.command_list = []
self.command_list_index = None
- self.context = MpdContext(self, session=session)
+ self.context = MpdContext(self, session=session, core=core)
def handle_request(self, request, current_command_list_index=None):
"""Dispatch incoming requests to the correct handler."""
@@ -59,8 +54,8 @@ class MpdDispatcher(object):
response = []
for subsystem in subsystems:
- response.append(u'changed: %s' % subsystem)
- response.append(u'OK')
+ response.append('changed: %s' % subsystem)
+ response.append('OK')
self.context.subscriptions = set()
self.context.events = set()
self.context.session.send_lines(response)
@@ -72,7 +67,6 @@ class MpdDispatcher(object):
else:
return response
-
### Filter: catch MPD ACK errors
def _catch_mpd_ack_errors_filter(self, request, response, filter_chain):
@@ -83,7 +77,6 @@ class MpdDispatcher(object):
mpd_ack_error.index = self.command_list_index
return [mpd_ack_error.get_mpd_ack()]
-
### Filter: authenticate
def _authenticate_filter(self, request, response, filter_chain):
@@ -95,14 +88,13 @@ class MpdDispatcher(object):
else:
command_name = request.split(' ')[0]
command_names_not_requiring_auth = [
- command.name for command in mpd_commands
+ command.name for command in protocol.mpd_commands
if not command.auth_required]
if command_name in command_names_not_requiring_auth:
return self._call_next_filter(request, response, filter_chain)
else:
raise exceptions.MpdPermissionError(command=command_name)
-
### Filter: command list
def _command_list_filter(self, request, response, filter_chain):
@@ -113,30 +105,31 @@ class MpdDispatcher(object):
response = self._call_next_filter(request, response, filter_chain)
if (self._is_receiving_command_list(request) or
self._is_processing_command_list(request)):
- if response and response[-1] == u'OK':
+ if response and response[-1] == 'OK':
response = response[:-1]
return response
def _is_receiving_command_list(self, request):
- return (self.command_list is not False
- and request != u'command_list_end')
+ return (
+ self.command_list_receiving and request != 'command_list_end')
def _is_processing_command_list(self, request):
- return (self.command_list_index is not None
- and request != u'command_list_end')
-
+ return (
+ self.command_list_index is not None and
+ request != 'command_list_end')
### Filter: idle
def _idle_filter(self, request, response, filter_chain):
if self._is_currently_idle() and not self._noidle.match(request):
- logger.debug(u'Client sent us %s, only %s is allowed while in '
- 'the idle state', repr(request), repr(u'noidle'))
+ logger.debug(
+ 'Client sent us %s, only %s is allowed while in '
+ 'the idle state', repr(request), repr('noidle'))
self.context.session.close()
return []
if not self._is_currently_idle() and self._noidle.match(request):
- return [] # noidle was called before idle
+ return [] # noidle was called before idle
response = self._call_next_filter(request, response, filter_chain)
@@ -148,18 +141,16 @@ class MpdDispatcher(object):
def _is_currently_idle(self):
return bool(self.context.subscriptions)
-
### Filter: add OK
def _add_ok_filter(self, request, response, filter_chain):
response = self._call_next_filter(request, response, filter_chain)
if not self._has_error(response):
- response.append(u'OK')
+ response.append('OK')
return response
def _has_error(self, response):
- return response and response[-1].startswith(u'ACK')
-
+ return response and response[-1].startswith('ACK')
### Filter: call handler
@@ -167,8 +158,8 @@ class MpdDispatcher(object):
try:
response = self._format_response(self._call_handler(request))
return self._call_next_filter(request, response, filter_chain)
- except ActorDeadError as e:
- logger.warning(u'Tried to communicate with dead actor.')
+ except pykka.ActorDeadError as e:
+ logger.warning('Tried to communicate with dead actor.')
raise exceptions.MpdSystemError(e)
def _call_handler(self, request):
@@ -176,14 +167,15 @@ class MpdDispatcher(object):
return handler(self.context, **kwargs)
def _find_handler(self, request):
- for pattern in request_handlers:
+ for pattern in protocol.request_handlers:
matches = re.match(pattern, request)
if matches is not None:
- return (request_handlers[pattern], matches.groupdict())
+ return (
+ protocol.request_handlers[pattern], matches.groupdict())
command_name = request.split(' ')[0]
- if command_name in [command.name for command in mpd_commands]:
- raise exceptions.MpdArgError(u'incorrect arguments',
- command=command_name)
+ if command_name in [command.name for command in protocol.mpd_commands]:
+ raise exceptions.MpdArgError(
+ 'incorrect arguments', command=command_name)
raise exceptions.MpdUnknownCommand(command=command_name)
def _format_response(self, response):
@@ -196,17 +188,26 @@ class MpdDispatcher(object):
if result is None:
return []
if isinstance(result, set):
- return flatten(list(result))
+ return self._flatten(list(result))
if not isinstance(result, list):
return [result]
- return flatten(result)
+ return self._flatten(result)
+
+ def _flatten(self, the_list):
+ result = []
+ for element in the_list:
+ if isinstance(element, list):
+ result.extend(self._flatten(element))
+ else:
+ result.append(element)
+ return result
def _format_lines(self, line):
if isinstance(line, dict):
- return [u'%s: %s' % (key, value) for (key, value) in line.items()]
+ return ['%s: %s' % (key, value) for (key, value) in line.items()]
if isinstance(line, tuple):
(key, value) = line
- return [u'%s: %s' % (key, value)]
+ return ['%s: %s' % (key, value)]
return [line]
@@ -222,27 +223,18 @@ class MpdContext(object):
#: The current :class:`mopidy.frontends.mpd.MpdSession`.
session = None
+ #: The Mopidy core API. An instance of :class:`mopidy.core.Core`.
+ core = None
+
#: The active subsystems that have pending events.
events = None
#: The subsytems that we want to be notified about in idle mode.
subscriptions = None
- def __init__(self, dispatcher, session=None):
+ def __init__(self, dispatcher, session=None, core=None):
self.dispatcher = dispatcher
self.session = session
+ self.core = core
self.events = set()
self.subscriptions = set()
- self._backend = None
-
- @property
- def backend(self):
- """
- The backend. An instance of :class:`mopidy.backends.base.Backend`.
- """
- if self._backend is None:
- backend_refs = ActorRegistry.get_by_class(Backend)
- assert len(backend_refs) == 1, \
- 'Expected exactly one running backend.'
- self._backend = backend_refs[0].proxy()
- return self._backend
diff --git a/mopidy/frontends/mpd/exceptions.py b/mopidy/frontends/mpd/exceptions.py
index 661d6905..db3212d8 100644
--- a/mopidy/frontends/mpd/exceptions.py
+++ b/mopidy/frontends/mpd/exceptions.py
@@ -1,4 +1,7 @@
-from mopidy import MopidyException
+from __future__ import unicode_literals
+
+from mopidy.exceptions import MopidyException
+
class MpdAckError(MopidyException):
"""See fields on this class for available MPD error codes"""
@@ -18,7 +21,7 @@ class MpdAckError(MopidyException):
error_code = 0
- def __init__(self, message=u'', index=0, command=u''):
+ def __init__(self, message='', index=0, command=''):
super(MpdAckError, self).__init__(message, index, command)
self.message = message
self.index = index
@@ -30,39 +33,46 @@ class MpdAckError(MopidyException):
ACK [%(error_code)i@%(index)i] {%(command)s} description
"""
- return u'ACK [%i@%i] {%s} %s' % (
+ return 'ACK [%i@%i] {%s} %s' % (
self.__class__.error_code, self.index, self.command, self.message)
+
class MpdArgError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_ARG
+
class MpdPasswordError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_PASSWORD
+
class MpdPermissionError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_PERMISSION
def __init__(self, *args, **kwargs):
super(MpdPermissionError, self).__init__(*args, **kwargs)
- self.message = u'you don\'t have permission for "%s"' % self.command
+ self.message = 'you don\'t have permission for "%s"' % self.command
+
class MpdUnknownCommand(MpdAckError):
error_code = MpdAckError.ACK_ERROR_UNKNOWN
def __init__(self, *args, **kwargs):
super(MpdUnknownCommand, self).__init__(*args, **kwargs)
- self.message = u'unknown command "%s"' % self.command
- self.command = u''
+ self.message = 'unknown command "%s"' % self.command
+ self.command = ''
+
class MpdNoExistError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_NO_EXIST
+
class MpdSystemError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_SYSTEM
+
class MpdNotImplemented(MpdAckError):
error_code = 0
def __init__(self, *args, **kwargs):
super(MpdNotImplemented, self).__init__(*args, **kwargs)
- self.message = u'Not implemented'
+ self.message = 'Not implemented'
diff --git a/mopidy/frontends/mpd/protocol/__init__.py b/mopidy/frontends/mpd/protocol/__init__.py
index f0b56a57..a8bdc2c7 100644
--- a/mopidy/frontends/mpd/protocol/__init__.py
+++ b/mopidy/frontends/mpd/protocol/__init__.py
@@ -10,25 +10,29 @@ implement our own MPD server which is compatible with the numerous existing
`MPD clients `_.
"""
+from __future__ import unicode_literals
+
from collections import namedtuple
import re
#: The MPD protocol uses UTF-8 for encoding all data.
-ENCODING = u'UTF-8'
+ENCODING = 'UTF-8'
#: The MPD protocol uses ``\n`` as line terminator.
-LINE_TERMINATOR = u'\n'
+LINE_TERMINATOR = '\n'
#: The MPD protocol version is 0.16.0.
-VERSION = u'0.16.0'
+VERSION = '0.16.0'
MpdCommand = namedtuple('MpdCommand', ['name', 'auth_required'])
-#: List of all available commands, represented as :class:`MpdCommand` objects.
+#: Set of all available commands, represented as :class:`MpdCommand` objects.
mpd_commands = set()
+#: Map between request matchers and request handler functions.
request_handlers = {}
+
def handle_request(pattern, auth_required=True):
"""
Decorator for connecting command handlers to command requests.
@@ -52,11 +56,24 @@ def handle_request(pattern, auth_required=True):
if match is not None:
mpd_commands.add(
MpdCommand(name=match.group(), auth_required=auth_required))
- if pattern in request_handlers:
- raise ValueError(u'Tried to redefine handler for %s with %s' % (
+ compiled_pattern = re.compile(pattern, flags=re.UNICODE)
+ if compiled_pattern in request_handlers:
+ raise ValueError('Tried to redefine handler for %s with %s' % (
pattern, func))
- request_handlers[pattern] = func
+ request_handlers[compiled_pattern] = func
func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % (
pattern, func.__doc__ or '')
return func
return decorator
+
+
+def load_protocol_modules():
+ """
+ The protocol modules must be imported to get them registered in
+ :attr:`request_handlers` and :attr:`mpd_commands`.
+ """
+ # pylint: disable = W0612
+ from . import ( # noqa
+ audio_output, command_list, connection, current_playlist, empty,
+ music_db, playback, reflection, status, stickers, stored_playlists)
+ # pylint: enable = W0612
diff --git a/mopidy/frontends/mpd/protocol/audio_output.py b/mopidy/frontends/mpd/protocol/audio_output.py
index 7147963a..b4d491e5 100644
--- a/mopidy/frontends/mpd/protocol/audio_output.py
+++ b/mopidy/frontends/mpd/protocol/audio_output.py
@@ -1,6 +1,9 @@
+from __future__ import unicode_literals
+
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
+
@handle_request(r'^disableoutput "(?P\d+)"$')
def disableoutput(context, outputid):
"""
@@ -10,7 +13,8 @@ def disableoutput(context, outputid):
Turns an output off.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^enableoutput "(?P\d+)"$')
def enableoutput(context, outputid):
@@ -21,7 +25,8 @@ def enableoutput(context, outputid):
Turns an output on.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^outputs$')
def outputs(context):
diff --git a/mopidy/frontends/mpd/protocol/command_list.py b/mopidy/frontends/mpd/protocol/command_list.py
index 37e5c93d..8d5769ef 100644
--- a/mopidy/frontends/mpd/protocol/command_list.py
+++ b/mopidy/frontends/mpd/protocol/command_list.py
@@ -1,6 +1,9 @@
+from __future__ import unicode_literals
+
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdUnknownCommand
+
@handle_request(r'^command_list_begin$')
def command_list_begin(context):
"""
@@ -18,17 +21,19 @@ def command_list_begin(context):
returned. If ``command_list_ok_begin`` is used, ``list_OK`` is
returned for each successful command executed in the command list.
"""
- context.dispatcher.command_list = []
+ context.dispatcher.command_list_receiving = True
context.dispatcher.command_list_ok = False
+ context.dispatcher.command_list = []
+
@handle_request(r'^command_list_end$')
def command_list_end(context):
"""See :meth:`command_list_begin()`."""
- if context.dispatcher.command_list is False:
- # Test for False exactly, and not e.g. empty list
+ if not context.dispatcher.command_list_receiving:
raise MpdUnknownCommand(command='command_list_end')
+ context.dispatcher.command_list_receiving = False
(command_list, context.dispatcher.command_list) = (
- context.dispatcher.command_list, False)
+ context.dispatcher.command_list, [])
(command_list_ok, context.dispatcher.command_list_ok) = (
context.dispatcher.command_list_ok, False)
command_list_response = []
@@ -37,14 +42,16 @@ def command_list_end(context):
command, current_command_list_index=index)
command_list_response.extend(response)
if (command_list_response and
- command_list_response[-1].startswith(u'ACK')):
+ command_list_response[-1].startswith('ACK')):
return command_list_response
if command_list_ok:
- command_list_response.append(u'list_OK')
+ command_list_response.append('list_OK')
return command_list_response
+
@handle_request(r'^command_list_ok_begin$')
def command_list_ok_begin(context):
"""See :meth:`command_list_begin()`."""
- context.dispatcher.command_list = []
+ context.dispatcher.command_list_receiving = True
context.dispatcher.command_list_ok = True
+ context.dispatcher.command_list = []
diff --git a/mopidy/frontends/mpd/protocol/connection.py b/mopidy/frontends/mpd/protocol/connection.py
index ff230173..f7898d21 100644
--- a/mopidy/frontends/mpd/protocol/connection.py
+++ b/mopidy/frontends/mpd/protocol/connection.py
@@ -1,7 +1,10 @@
+from __future__ import unicode_literals
+
from mopidy import settings
from mopidy.frontends.mpd.protocol import handle_request
-from mopidy.frontends.mpd.exceptions import (MpdPasswordError,
- MpdPermissionError)
+from mopidy.frontends.mpd.exceptions import (
+ MpdPasswordError, MpdPermissionError)
+
@handle_request(r'^close$', auth_required=False)
def close(context):
@@ -14,6 +17,7 @@ def close(context):
"""
context.session.close()
+
@handle_request(r'^kill$')
def kill(context):
"""
@@ -23,7 +27,8 @@ def kill(context):
Kills MPD.
"""
- raise MpdPermissionError(command=u'kill')
+ raise MpdPermissionError(command='kill')
+
@handle_request(r'^password "(?P[^"]+)"$', auth_required=False)
def password_(context, password):
@@ -38,7 +43,8 @@ def password_(context, password):
if password == settings.MPD_SERVER_PASSWORD:
context.dispatcher.authenticated = True
else:
- raise MpdPasswordError(u'incorrect password', command=u'password')
+ raise MpdPasswordError('incorrect password', command='password')
+
@handle_request(r'^ping$', auth_required=False)
def ping(context):
diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py
index c60cbc4a..d1b0e59a 100644
--- a/mopidy/frontends/mpd/protocol/current_playlist.py
+++ b/mopidy/frontends/mpd/protocol/current_playlist.py
@@ -1,8 +1,10 @@
-from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
- MpdNotImplemented)
+from __future__ import unicode_literals
+
+from mopidy.frontends.mpd import translator
+from mopidy.frontends.mpd.exceptions import (
+ MpdArgError, MpdNoExistError, MpdNotImplemented)
from mopidy.frontends.mpd.protocol import handle_request
-from mopidy.frontends.mpd.translator import (track_to_mpd_format,
- tracks_to_mpd_format)
+
@handle_request(r'^add "(?P[^"]*)"$')
def add(context, uri):
@@ -20,14 +22,12 @@ def add(context, uri):
"""
if not uri:
return
- for uri_scheme in context.backend.uri_schemes.get():
- if uri.startswith(uri_scheme):
- track = context.backend.library.lookup(uri).get()
- if track is not None:
- context.backend.current_playlist.add(track)
- return
- raise MpdNoExistError(
- u'directory or file not found', command=u'add')
+ tracks = context.core.library.lookup(uri).get()
+ if tracks:
+ context.core.tracklist.add(tracks)
+ return
+ raise MpdNoExistError('directory or file not found', command='add')
+
@handle_request(r'^addid "(?P[^"]*)"( "(?P\d+)")*$')
def addid(context, uri, songpos=None):
@@ -49,17 +49,17 @@ def addid(context, uri, songpos=None):
- ``addid ""`` should return an error.
"""
if not uri:
- raise MpdNoExistError(u'No such song', command=u'addid')
+ raise MpdNoExistError('No such song', command='addid')
if songpos is not None:
songpos = int(songpos)
- track = context.backend.library.lookup(uri).get()
- if track is None:
- raise MpdNoExistError(u'No such song', command=u'addid')
- if songpos and songpos > context.backend.current_playlist.length.get():
- raise MpdArgError(u'Bad song index', command=u'addid')
- cp_track = context.backend.current_playlist.add(track,
- at_position=songpos).get()
- return ('Id', cp_track.cpid)
+ tracks = context.core.library.lookup(uri).get()
+ if not tracks:
+ raise MpdNoExistError('No such song', command='addid')
+ if songpos and songpos > context.core.tracklist.length.get():
+ raise MpdArgError('Bad song index', command='addid')
+ tl_tracks = context.core.tracklist.add(tracks, at_position=songpos).get()
+ return ('Id', tl_tracks[0].tlid)
+
@handle_request(r'^delete "(?P\d+):(?P\d+)*"$')
def delete_range(context, start, end=None):
@@ -74,26 +74,28 @@ def delete_range(context, start, end=None):
if end is not None:
end = int(end)
else:
- end = context.backend.current_playlist.length.get()
- cp_tracks = context.backend.current_playlist.slice(start, end).get()
- if not cp_tracks:
- raise MpdArgError(u'Bad song index', command=u'delete')
- for (cpid, _) in cp_tracks:
- context.backend.current_playlist.remove(cpid=cpid)
+ end = context.core.tracklist.length.get()
+ tl_tracks = context.core.tracklist.slice(start, end).get()
+ if not tl_tracks:
+ raise MpdArgError('Bad song index', command='delete')
+ for (tlid, _) in tl_tracks:
+ context.core.tracklist.remove(tlid=tlid)
+
@handle_request(r'^delete "(?P\d+)"$')
def delete_songpos(context, songpos):
"""See :meth:`delete_range`"""
try:
songpos = int(songpos)
- (cpid, _) = context.backend.current_playlist.slice(
+ (tlid, _) = context.core.tracklist.slice(
songpos, songpos + 1).get()[0]
- context.backend.current_playlist.remove(cpid=cpid)
+ context.core.tracklist.remove(tlid=tlid)
except IndexError:
- raise MpdArgError(u'Bad song index', command=u'delete')
+ raise MpdArgError('Bad song index', command='delete')
-@handle_request(r'^deleteid "(?P\d+)"$')
-def deleteid(context, cpid):
+
+@handle_request(r'^deleteid "(?P\d+)"$')
+def deleteid(context, tlid):
"""
*musicpd.org, current playlist section:*
@@ -101,13 +103,14 @@ def deleteid(context, cpid):
Deletes the song ``SONGID`` from the playlist
"""
- try:
- cpid = int(cpid)
- if context.backend.playback.current_cpid.get() == cpid:
- context.backend.playback.next()
- return context.backend.current_playlist.remove(cpid=cpid).get()
- except LookupError:
- raise MpdNoExistError(u'No such song', command=u'deleteid')
+ tlid = int(tlid)
+ tl_track = context.core.playback.current_tl_track.get()
+ if tl_track and tl_track.tlid == tlid:
+ context.core.playback.next()
+ tl_tracks = context.core.tracklist.remove(tlid=tlid).get()
+ if not tl_tracks:
+ raise MpdNoExistError('No such song', command='deleteid')
+
@handle_request(r'^clear$')
def clear(context):
@@ -118,7 +121,8 @@ def clear(context):
Clears the current playlist.
"""
- context.backend.current_playlist.clear()
+ context.core.tracklist.clear()
+
@handle_request(r'^move "(?P\d+):(?P\d+)*" "(?P\d+)"$')
def move_range(context, start, to, end=None):
@@ -131,21 +135,23 @@ def move_range(context, start, to, end=None):
``TO`` in the playlist.
"""
if end is None:
- end = context.backend.current_playlist.length.get()
+ end = context.core.tracklist.length.get()
start = int(start)
end = int(end)
to = int(to)
- context.backend.current_playlist.move(start, end, to)
+ context.core.tracklist.move(start, end, to)
+
@handle_request(r'^move "(?P\d+)" "(?P\d+)"$')
def move_songpos(context, songpos, to):
"""See :meth:`move_range`."""
songpos = int(songpos)
to = int(to)
- context.backend.current_playlist.move(songpos, songpos + 1, to)
+ context.core.tracklist.move(songpos, songpos + 1, to)
-@handle_request(r'^moveid "(?P\d+)" "(?P\d+)"$')
-def moveid(context, cpid, to):
+
+@handle_request(r'^moveid "(?P\d+)" "(?P\d+)"$')
+def moveid(context, tlid, to):
"""
*musicpd.org, current playlist section:*
@@ -155,11 +161,14 @@ def moveid(context, cpid, to):
the playlist. If ``TO`` is negative, it is relative to the current
song in the playlist (if there is one).
"""
- cpid = int(cpid)
+ tlid = int(tlid)
to = int(to)
- cp_track = context.backend.current_playlist.get(cpid=cpid).get()
- position = context.backend.current_playlist.index(cp_track).get()
- context.backend.current_playlist.move(position, position + 1, to)
+ tl_tracks = context.core.tracklist.filter(tlid=tlid).get()
+ if not tl_tracks:
+ raise MpdNoExistError('No such song', command='moveid')
+ position = context.core.tracklist.index(tl_tracks[0]).get()
+ context.core.tracklist.move(position, position + 1, to)
+
@handle_request(r'^playlist$')
def playlist(context):
@@ -176,6 +185,7 @@ def playlist(context):
"""
return playlistinfo(context)
+
@handle_request(r'^playlistfind (?P[^"]+) "(?P[^"]+)"$')
@handle_request(r'^playlistfind "(?P[^"]+)" "(?P[^"]+)"$')
def playlistfind(context, tag, needle):
@@ -191,16 +201,16 @@ def playlistfind(context, tag, needle):
- does not add quotes around the tag.
"""
if tag == 'filename':
- try:
- cp_track = context.backend.current_playlist.get(uri=needle).get()
- position = context.backend.current_playlist.index(cp_track).get()
- return track_to_mpd_format(cp_track, position=position)
- except LookupError:
+ tl_tracks = context.core.tracklist.filter(uri=needle).get()
+ if not tl_tracks:
return None
- raise MpdNotImplemented # TODO
+ position = context.core.tracklist.index(tl_tracks[0]).get()
+ return translator.track_to_mpd_format(tl_tracks[0], position=position)
+ raise MpdNotImplemented # TODO
-@handle_request(r'^playlistid( "(?P\d+)")*$')
-def playlistid(context, cpid=None):
+
+@handle_request(r'^playlistid( "(?P\d+)")*$')
+def playlistid(context, tlid=None):
"""
*musicpd.org, current playlist section:*
@@ -209,24 +219,22 @@ def playlistid(context, cpid=None):
Displays a list of songs in the playlist. ``SONGID`` is optional
and specifies a single song to display info for.
"""
- if cpid is not None:
- try:
- cpid = int(cpid)
- cp_track = context.backend.current_playlist.get(cpid=cpid).get()
- position = context.backend.current_playlist.index(cp_track).get()
- return track_to_mpd_format(cp_track, position=position)
- except LookupError:
- raise MpdNoExistError(u'No such song', command=u'playlistid')
+ if tlid is not None:
+ tlid = int(tlid)
+ tl_tracks = context.core.tracklist.filter(tlid=tlid).get()
+ if not tl_tracks:
+ raise MpdNoExistError('No such song', command='playlistid')
+ position = context.core.tracklist.index(tl_tracks[0]).get()
+ return translator.track_to_mpd_format(tl_tracks[0], position=position)
else:
- return tracks_to_mpd_format(
- context.backend.current_playlist.cp_tracks.get())
+ return translator.tracks_to_mpd_format(
+ context.core.tracklist.tl_tracks.get())
+
@handle_request(r'^playlistinfo$')
-@handle_request(r'^playlistinfo "-1"$')
@handle_request(r'^playlistinfo "(?P-?\d+)"$')
@handle_request(r'^playlistinfo "(?P\d+):(?P\d+)*"$')
-def playlistinfo(context, songpos=None,
- start=None, end=None):
+def playlistinfo(context, songpos=None, start=None, end=None):
"""
*musicpd.org, current playlist section:*
@@ -241,25 +249,28 @@ def playlistinfo(context, songpos=None,
- uses negative indexes, like ``playlistinfo "-1"``, to request
the entire playlist
"""
+ if songpos == '-1':
+ songpos = None
if songpos is not None:
songpos = int(songpos)
- cp_track = context.backend.current_playlist.cp_tracks.get()[songpos]
- return track_to_mpd_format(cp_track, position=songpos)
+ tl_track = context.core.tracklist.tl_tracks.get()[songpos]
+ return translator.track_to_mpd_format(tl_track, position=songpos)
else:
if start is None:
start = 0
start = int(start)
- if not (0 <= start <= context.backend.current_playlist.length.get()):
- raise MpdArgError(u'Bad song index', command=u'playlistinfo')
+ if not (0 <= start <= context.core.tracklist.length.get()):
+ raise MpdArgError('Bad song index', command='playlistinfo')
if end is not None:
end = int(end)
- if end > context.backend.current_playlist.length.get():
+ if end > context.core.tracklist.length.get():
end = None
- cp_tracks = context.backend.current_playlist.cp_tracks.get()
- return tracks_to_mpd_format(cp_tracks, start, end)
+ tl_tracks = context.core.tracklist.tl_tracks.get()
+ return translator.tracks_to_mpd_format(tl_tracks, start, end)
+
@handle_request(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$')
-@handle_request(r'^playlistsearch (?P\S+) "(?P[^"]+)"$')
+@handle_request(r'^playlistsearch (?P\w+) "(?P[^"]+)"$')
def playlistsearch(context, tag, needle):
"""
*musicpd.org, current playlist section:*
@@ -274,7 +285,8 @@ def playlistsearch(context, tag, needle):
- does not add quotes around the tag
- uses ``filename`` and ``any`` as tags
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^plchanges (?P-?\d+)$')
@handle_request(r'^plchanges "(?P-?\d+)"$')
@@ -294,9 +306,10 @@ def plchanges(context, version):
- Calls ``plchanges "-1"`` two times per second to get the entire playlist.
"""
# XXX Naive implementation that returns all tracks as changed
- if int(version) < context.backend.current_playlist.version:
- return tracks_to_mpd_format(
- context.backend.current_playlist.cp_tracks.get())
+ if int(version) < context.core.tracklist.version.get():
+ return translator.tracks_to_mpd_format(
+ context.core.tracklist.tl_tracks.get())
+
@handle_request(r'^plchangesposid "(?P\d+)"$')
def plchangesposid(context, version):
@@ -313,14 +326,15 @@ def plchangesposid(context, version):
``playlistlength`` returned by status command.
"""
# XXX Naive implementation that returns all tracks as changed
- if int(version) != context.backend.current_playlist.version.get():
+ if int(version) != context.core.tracklist.version.get():
result = []
- for (position, (cpid, _)) in enumerate(
- context.backend.current_playlist.cp_tracks.get()):
- result.append((u'cpos', position))
- result.append((u'Id', cpid))
+ for (position, (tlid, _)) in enumerate(
+ context.core.tracklist.tl_tracks.get()):
+ result.append(('cpos', position))
+ result.append(('Id', tlid))
return result
+
@handle_request(r'^shuffle$')
@handle_request(r'^shuffle "(?P\d+):(?P\d+)*"$')
def shuffle(context, start=None, end=None):
@@ -336,7 +350,8 @@ def shuffle(context, start=None, end=None):
start = int(start)
if end is not None:
end = int(end)
- context.backend.current_playlist.shuffle(start, end)
+ context.core.tracklist.shuffle(start, end)
+
@handle_request(r'^swap "(?P\d+)" "(?P\d+)"$')
def swap(context, songpos1, songpos2):
@@ -349,18 +364,19 @@ def swap(context, songpos1, songpos2):
"""
songpos1 = int(songpos1)
songpos2 = int(songpos2)
- tracks = context.backend.current_playlist.tracks.get()
+ tracks = context.core.tracklist.tracks.get()
song1 = tracks[songpos1]
song2 = tracks[songpos2]
del tracks[songpos1]
tracks.insert(songpos1, song2)
del tracks[songpos2]
tracks.insert(songpos2, song1)
- context.backend.current_playlist.clear()
- context.backend.current_playlist.append(tracks)
+ context.core.tracklist.clear()
+ context.core.tracklist.add(tracks)
-@handle_request(r'^swapid "(?P\d+)" "(?P\d+)"$')
-def swapid(context, cpid1, cpid2):
+
+@handle_request(r'^swapid "(?P\d+)" "(?P\d+)"$')
+def swapid(context, tlid1, tlid2):
"""
*musicpd.org, current playlist section:*
@@ -368,10 +384,12 @@ def swapid(context, cpid1, cpid2):
Swaps the positions of ``SONG1`` and ``SONG2`` (both song ids).
"""
- cpid1 = int(cpid1)
- cpid2 = int(cpid2)
- cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get()
- cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get()
- position1 = context.backend.current_playlist.index(cp_track1).get()
- position2 = context.backend.current_playlist.index(cp_track2).get()
+ tlid1 = int(tlid1)
+ tlid2 = int(tlid2)
+ tl_tracks1 = context.core.tracklist.filter(tlid=tlid1).get()
+ tl_tracks2 = context.core.tracklist.filter(tlid=tlid2).get()
+ if not tl_tracks1 or not tl_tracks2:
+ raise MpdNoExistError('No such song', command='swapid')
+ position1 = context.core.tracklist.index(tl_tracks1[0]).get()
+ position2 = context.core.tracklist.index(tl_tracks2[0]).get()
swap(context, position1, position2)
diff --git a/mopidy/frontends/mpd/protocol/empty.py b/mopidy/frontends/mpd/protocol/empty.py
index 4cdafd87..dfd610a9 100644
--- a/mopidy/frontends/mpd/protocol/empty.py
+++ b/mopidy/frontends/mpd/protocol/empty.py
@@ -1,5 +1,8 @@
+from __future__ import unicode_literals
+
from mopidy.frontends.mpd.protocol import handle_request
+
@handle_request(r'^[ ]*$')
def empty(context):
"""The original MPD server returns ``OK`` on an empty request."""
diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py
index d0128a1e..00b9ec00 100644
--- a/mopidy/frontends/mpd/protocol/music_db.py
+++ b/mopidy/frontends/mpd/protocol/music_db.py
@@ -1,34 +1,42 @@
+from __future__ import unicode_literals
+
import re
import shlex
from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
-from mopidy.frontends.mpd.translator import playlist_to_mpd_format
+from mopidy.frontends.mpd.translator import tracks_to_mpd_format
+
def _build_query(mpd_query):
"""
Parses a MPD query string and converts it to the Mopidy query format.
"""
query_pattern = (
- r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? "[^"]+"')
+ r'"?(?:[Aa]lbum|[Aa]rtist|[Ff]ile[name]*|[Tt]itle|[Aa]ny)"? "[^"]+"')
query_parts = re.findall(query_pattern, mpd_query)
query_part_pattern = (
- r'"?(?P([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny))"? '
+ r'"?(?P([Aa]lbum|[Aa]rtist|[Ff]ile[name]*|[Tt]itle|[Aa]ny))"? '
r'"(?P[^"]+)"')
query = {}
for query_part in query_parts:
m = re.match(query_part_pattern, query_part)
field = m.groupdict()['field'].lower()
- if field == u'title':
- field = u'track'
- field = str(field) # Needed for kwargs keys on OS X and Windows
- what = m.groupdict()['what'].lower()
+ if field == 'title':
+ field = 'track'
+ elif field in ('file', 'filename'):
+ field = 'uri'
+ field = str(field) # Needed for kwargs keys on OS X and Windows
+ what = m.groupdict()['what']
+ if not what:
+ raise ValueError
if field in query:
query[field].append(what)
else:
query[field] = [what]
return query
+
@handle_request(r'^count "(?P[^"]+)" "(?P[^"]*)"$')
def count(context, tag, needle):
"""
@@ -39,11 +47,12 @@ def count(context, tag, needle):
Counts the number of songs and their total playtime in the db
matching ``TAG`` exactly.
"""
- return [('songs', 0), ('playtime', 0)] # TODO
+ return [('songs', 0), ('playtime', 0)] # TODO
-@handle_request(r'^find '
- r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
- r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
+
+@handle_request(
+ r'^find (?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|'
+ r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$')
def find(context, mpd_query):
"""
*musicpd.org, music database section:*
@@ -67,14 +76,20 @@ def find(context, mpd_query):
*ncmpcpp:*
- also uses the search type "date".
+ - uses "file" instead of "filename".
"""
- query = _build_query(mpd_query)
- return playlist_to_mpd_format(
- context.backend.library.find_exact(**query).get())
+ try:
+ query = _build_query(mpd_query)
+ except ValueError:
+ return
+ return tracks_to_mpd_format(
+ context.core.library.find_exact(**query).get())
-@handle_request(r'^findadd '
- r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
- '"[^"]+"\s?)+)$')
+
+@handle_request(
+ r'^findadd '
+ r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
+ r'"[^"]+"\s?)+)$')
def findadd(context, query):
"""
*musicpd.org, music database section:*
@@ -88,8 +103,10 @@ def findadd(context, query):
# TODO Add result to current playlist
#result = context.find(query)
-@handle_request(r'^list "?(?P([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
- '( (?P.*))?$')
+
+@handle_request(
+ r'^list "?(?P([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
+ r'( (?P.*))?$')
def list_(context, field, mpd_query=None):
"""
*musicpd.org, music database section:*
@@ -100,9 +117,7 @@ def list_(context, field, mpd_query=None):
``artist``, ``date``, or ``genre``.
``ARTIST`` is an optional parameter when type is ``album``,
- ``date``, or ``genre``.
-
- This filters the result list by an artist.
+ ``date``, or ``genre``. This filters the result list by an artist.
*Clarifications:*
@@ -175,15 +190,19 @@ def list_(context, field, mpd_query=None):
- capitalizes the field argument.
"""
field = field.lower()
- query = _list_build_query(field, mpd_query)
- if field == u'artist':
+ try:
+ query = _list_build_query(field, mpd_query)
+ except ValueError:
+ return
+ if field == 'artist':
return _list_artist(context, query)
- elif field == u'album':
+ elif field == 'album':
return _list_album(context, query)
- elif field == u'date':
+ elif field == 'date':
return _list_date(context, query)
- elif field == u'genre':
- pass # TODO We don't have genre in our internal data structures yet
+ elif field == 'genre':
+ pass # TODO We don't have genre in our internal data structures yet
+
def _list_build_query(field, mpd_query):
"""Converts a ``list`` query to a Mopidy query."""
@@ -194,57 +213,66 @@ def _list_build_query(field, mpd_query):
tokens = shlex.split(mpd_query.encode('utf-8'))
except ValueError as error:
if str(error) == 'No closing quotation':
- raise MpdArgError(u'Invalid unquoted character', command=u'list')
+ raise MpdArgError('Invalid unquoted character', command='list')
else:
raise
tokens = [t.decode('utf-8') for t in tokens]
if len(tokens) == 1:
- if field == u'album':
+ if field == 'album':
+ if not tokens[0]:
+ raise ValueError
return {'artist': [tokens[0]]}
else:
raise MpdArgError(
- u'should be "Album" for 3 arguments', command=u'list')
+ 'should be "Album" for 3 arguments', command='list')
elif len(tokens) % 2 == 0:
query = {}
while tokens:
key = tokens[0].lower()
- key = str(key) # Needed for kwargs keys on OS X and Windows
+ key = str(key) # Needed for kwargs keys on OS X and Windows
value = tokens[1]
tokens = tokens[2:]
- if key not in (u'artist', u'album', u'date', u'genre'):
- raise MpdArgError(u'not able to parse args', command=u'list')
+ if key not in ('artist', 'album', 'date', 'genre'):
+ raise MpdArgError('not able to parse args', command='list')
+ if not value:
+ raise ValueError
if key in query:
query[key].append(value)
else:
query[key] = [value]
return query
else:
- raise MpdArgError(u'not able to parse args', command=u'list')
+ raise MpdArgError('not able to parse args', command='list')
+
def _list_artist(context, query):
artists = set()
- playlist = context.backend.library.find_exact(**query).get()
- for track in playlist.tracks:
+ tracks = context.core.library.find_exact(**query).get()
+ for track in tracks:
for artist in track.artists:
- artists.add((u'Artist', artist.name))
+ if artist.name:
+ artists.add(('Artist', artist.name))
return artists
+
def _list_album(context, query):
albums = set()
- playlist = context.backend.library.find_exact(**query).get()
- for track in playlist.tracks:
- if track.album is not None:
- albums.add((u'Album', track.album.name))
+ tracks = context.core.library.find_exact(**query).get()
+ for track in tracks:
+ if track.album and track.album.name:
+ albums.add(('Album', track.album.name))
return albums
+
def _list_date(context, query):
dates = set()
- playlist = context.backend.library.find_exact(**query).get()
- for track in playlist.tracks:
- if track.date is not None:
- dates.add((u'Date', track.date))
+ tracks = context.core.library.find_exact(**query).get()
+ for track in tracks:
+ if track.date:
+ dates.add(('Date', track.date))
return dates
+
@handle_request(r'^listall "(?P[^"]+)"')
def listall(context, uri):
"""
@@ -254,7 +282,8 @@ def listall(context, uri):
Lists all songs and directories in ``URI``.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^listallinfo "(?P[^"]+)"')
def listallinfo(context, uri):
@@ -266,7 +295,8 @@ def listallinfo(context, uri):
Same as ``listall``, except it also returns metadata info in the
same format as ``lsinfo``.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^lsinfo$')
@handle_request(r'^lsinfo "(?P[^"]*)"$')
@@ -286,9 +316,10 @@ def lsinfo(context, uri=None):
directories located at the root level, for both ``lsinfo``, ``lsinfo
""``, and ``lsinfo "/"``.
"""
- if uri is None or uri == u'/' or uri == u'':
+ if uri is None or uri == '/' or uri == '':
return stored_playlists.listplaylists(context)
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^rescan( "(?P[^"]+)")*$')
def rescan(context, uri=None):
@@ -301,9 +332,10 @@ def rescan(context, uri=None):
"""
return update(context, uri, rescan_unmodified_files=True)
-@handle_request(r'^search '
- r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
- r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
+
+@handle_request(
+ r'^search (?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile[name]*|'
+ r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$')
def search(context, mpd_query):
"""
*musicpd.org, music database section:*
@@ -330,10 +362,15 @@ def search(context, mpd_query):
*ncmpcpp:*
- also uses the search type "date".
+ - uses "file" instead of "filename".
"""
- query = _build_query(mpd_query)
- return playlist_to_mpd_format(
- context.backend.library.search(**query).get())
+ try:
+ query = _build_query(mpd_query)
+ except ValueError:
+ return
+ return tracks_to_mpd_format(
+ context.core.library.search(**query).get())
+
@handle_request(r'^update( "(?P[^"]+)")*$')
def update(context, uri=None, rescan_unmodified_files=False):
@@ -352,4 +389,4 @@ def update(context, uri=None, rescan_unmodified_files=False):
identifying the update job. You can read the current job id in the
``status`` response.
"""
- return {'updating_db': 0} # TODO
+ return {'updating_db': 0} # TODO
diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py
index 4152f11e..5a4569e1 100644
--- a/mopidy/frontends/mpd/protocol/playback.py
+++ b/mopidy/frontends/mpd/protocol/playback.py
@@ -1,7 +1,10 @@
+from __future__ import unicode_literals
+
from mopidy.core import PlaybackState
from mopidy.frontends.mpd.protocol import handle_request
-from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
- MpdNotImplemented)
+from mopidy.frontends.mpd.exceptions import (
+ MpdArgError, MpdNoExistError, MpdNotImplemented)
+
@handle_request(r'^consume (?P[01])$')
@handle_request(r'^consume "(?P[01])"$')
@@ -16,9 +19,10 @@ def consume(context, state):
playlist.
"""
if int(state):
- context.backend.playback.consume = True
+ context.core.playback.consume = True
else:
- context.backend.playback.consume = False
+ context.core.playback.consume = False
+
@handle_request(r'^crossfade "(?P\d+)"$')
def crossfade(context, seconds):
@@ -30,7 +34,8 @@ def crossfade(context, seconds):
Sets crossfading between songs.
"""
seconds = int(seconds)
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^next$')
def next_(context):
@@ -87,7 +92,8 @@ def next_(context):
order as the first time.
"""
- return context.backend.playback.next().get()
+ return context.core.playback.next().get()
+
@handle_request(r'^pause$')
@handle_request(r'^pause "(?P[01])"$')
@@ -104,14 +110,15 @@ def pause(context, state=None):
- Calls ``pause`` without any arguments to toogle pause.
"""
if state is None:
- if (context.backend.playback.state.get() == PlaybackState.PLAYING):
- context.backend.playback.pause()
- elif (context.backend.playback.state.get() == PlaybackState.PAUSED):
- context.backend.playback.resume()
+ if (context.core.playback.state.get() == PlaybackState.PLAYING):
+ context.core.playback.pause()
+ elif (context.core.playback.state.get() == PlaybackState.PAUSED):
+ context.core.playback.resume()
elif int(state):
- context.backend.playback.pause()
+ context.core.playback.pause()
else:
- context.backend.playback.resume()
+ context.core.playback.resume()
+
@handle_request(r'^play$')
def play(context):
@@ -119,11 +126,12 @@ def play(context):
The original MPD server resumes from the paused state on ``play``
without arguments.
"""
- return context.backend.playback.play().get()
+ return context.core.playback.play().get()
-@handle_request(r'^playid (?P-?\d+)$')
-@handle_request(r'^playid "(?P-?\d+)"$')
-def playid(context, cpid):
+
+@handle_request(r'^playid (?P-?\d+)$')
+@handle_request(r'^playid "(?P-?\d+)"$')
+def playid(context, tlid):
"""
*musicpd.org, playback section:*
@@ -140,14 +148,14 @@ def playid(context, cpid):
- ``playid "-1"`` when stopped without a current track, e.g. after playlist
replacement, starts playback at the first track.
"""
- cpid = int(cpid)
- if cpid == -1:
+ tlid = int(tlid)
+ if tlid == -1:
return _play_minus_one(context)
- try:
- cp_track = context.backend.current_playlist.get(cpid=cpid).get()
- return context.backend.playback.play(cp_track).get()
- except LookupError:
- raise MpdNoExistError(u'No such song', command=u'playid')
+ tl_tracks = context.core.tracklist.filter(tlid=tlid).get()
+ if not tl_tracks:
+ raise MpdNoExistError('No such song', command='playid')
+ return context.core.playback.play(tl_tracks[0]).get()
+
@handle_request(r'^play (?P-?\d+)$')
@handle_request(r'^play "(?P-?\d+)"$')
@@ -176,25 +184,26 @@ def playpos(context, songpos):
if songpos == -1:
return _play_minus_one(context)
try:
- cp_track = context.backend.current_playlist.slice(
- songpos, songpos + 1).get()[0]
- return context.backend.playback.play(cp_track).get()
+ tl_track = context.core.tracklist.slice(songpos, songpos + 1).get()[0]
+ return context.core.playback.play(tl_track).get()
except IndexError:
- raise MpdArgError(u'Bad song index', command=u'play')
+ raise MpdArgError('Bad song index', command='play')
+
def _play_minus_one(context):
- if (context.backend.playback.state.get() == PlaybackState.PLAYING):
- return # Nothing to do
- elif (context.backend.playback.state.get() == PlaybackState.PAUSED):
- return context.backend.playback.resume().get()
- elif context.backend.playback.current_cp_track.get() is not None:
- cp_track = context.backend.playback.current_cp_track.get()
- return context.backend.playback.play(cp_track).get()
- elif context.backend.current_playlist.slice(0, 1).get():
- cp_track = context.backend.current_playlist.slice(0, 1).get()[0]
- return context.backend.playback.play(cp_track).get()
+ if (context.core.playback.state.get() == PlaybackState.PLAYING):
+ return # Nothing to do
+ elif (context.core.playback.state.get() == PlaybackState.PAUSED):
+ return context.core.playback.resume().get()
+ elif context.core.playback.current_tl_track.get() is not None:
+ tl_track = context.core.playback.current_tl_track.get()
+ return context.core.playback.play(tl_track).get()
+ elif context.core.tracklist.slice(0, 1).get():
+ tl_track = context.core.tracklist.slice(0, 1).get()[0]
+ return context.core.playback.play(tl_track).get()
else:
- return # Fail silently
+ return # Fail silently
+
@handle_request(r'^previous$')
def previous(context):
@@ -240,7 +249,8 @@ def previous(context):
``previous`` should do a seek to time position 0.
"""
- return context.backend.playback.previous().get()
+ return context.core.playback.previous().get()
+
@handle_request(r'^random (?P[01])$')
@handle_request(r'^random "(?P[01])"$')
@@ -253,9 +263,10 @@ def random(context, state):
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
"""
if int(state):
- context.backend.playback.random = True
+ context.core.playback.random = True
else:
- context.backend.playback.random = False
+ context.core.playback.random = False
+
@handle_request(r'^repeat (?P[01])$')
@handle_request(r'^repeat "(?P[01])"$')
@@ -268,9 +279,10 @@ def repeat(context, state):
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
"""
if int(state):
- context.backend.playback.repeat = True
+ context.core.playback.repeat = True
else:
- context.backend.playback.repeat = False
+ context.core.playback.repeat = False
+
@handle_request(r'^replay_gain_mode "(?P(off|track|album))"$')
def replay_gain_mode(context, mode):
@@ -286,7 +298,8 @@ def replay_gain_mode(context, mode):
This command triggers the options idle event.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^replay_gain_status$')
def replay_gain_status(context):
@@ -298,7 +311,8 @@ def replay_gain_status(context):
Prints replay gain options. Currently, only the variable
``replay_gain_mode`` is returned.
"""
- return u'off' # TODO
+ return 'off' # TODO
+
@handle_request(r'^seek (?P\d+) (?P\d+)$')
@handle_request(r'^seek "(?P\d+)" "(?P\d+)"$')
@@ -315,12 +329,13 @@ def seek(context, songpos, seconds):
- issues ``seek 1 120`` without quotes around the arguments.
"""
- if context.backend.playback.current_playlist_position != songpos:
+ if context.core.playback.tracklist_position.get() != songpos:
playpos(context, songpos)
- context.backend.playback.seek(int(seconds) * 1000)
+ context.core.playback.seek(int(seconds) * 1000).get()
-@handle_request(r'^seekid "(?P\d+)" "(?P\d+)"$')
-def seekid(context, cpid, seconds):
+
+@handle_request(r'^seekid "(?P\d+)" "(?P\d+)"$')
+def seekid(context, tlid, seconds):
"""
*musicpd.org, playback section:*
@@ -328,9 +343,11 @@ def seekid(context, cpid, seconds):
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
"""
- if context.backend.playback.current_cpid != cpid:
- playid(context, cpid)
- context.backend.playback.seek(int(seconds) * 1000)
+ tl_track = context.core.playback.current_tl_track.get()
+ if not tl_track or tl_track.tlid != tlid:
+ playid(context, tlid)
+ context.core.playback.seek(int(seconds) * 1000).get()
+
@handle_request(r'^setvol (?P[-+]*\d+)$')
@handle_request(r'^setvol "(?P[-+]*\d+)"$')
@@ -351,7 +368,8 @@ def setvol(context, volume):
volume = 0
if volume > 100:
volume = 100
- context.backend.playback.volume = volume
+ context.core.playback.volume = volume
+
@handle_request(r'^single (?P[01])$')
@handle_request(r'^single "(?P[01])"$')
@@ -366,9 +384,10 @@ def single(context, state):
song is repeated if the ``repeat`` mode is enabled.
"""
if int(state):
- context.backend.playback.single = True
+ context.core.playback.single = True
else:
- context.backend.playback.single = False
+ context.core.playback.single = False
+
@handle_request(r'^stop$')
def stop(context):
@@ -379,4 +398,4 @@ def stop(context):
Stops playing.
"""
- context.backend.playback.stop()
+ context.core.playback.stop()
diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py
index df13b4b4..d9c35743 100644
--- a/mopidy/frontends/mpd/protocol/reflection.py
+++ b/mopidy/frontends/mpd/protocol/reflection.py
@@ -1,5 +1,7 @@
+from __future__ import unicode_literals
+
from mopidy.frontends.mpd.protocol import handle_request, mpd_commands
-from mopidy.frontends.mpd.exceptions import MpdNotImplemented
+
@handle_request(r'^commands$', auth_required=False)
def commands(context):
@@ -13,16 +15,20 @@ def commands(context):
if context.dispatcher.authenticated:
command_names = set([command.name for command in mpd_commands])
else:
- command_names = set([command.name for command in mpd_commands
+ command_names = set([
+ command.name for command in mpd_commands
if not command.auth_required])
# No one is permited to use kill, rest of commands are not listed by MPD,
# so we shouldn't either.
- command_names = command_names - set(['kill', 'command_list_begin',
- 'command_list_ok_begin', 'command_list_ok_begin', 'command_list_end',
- 'idle', 'noidle', 'sticker'])
+ command_names = command_names - set([
+ 'kill', 'command_list_begin', 'command_list_ok_begin',
+ 'command_list_ok_begin', 'command_list_end', 'idle', 'noidle',
+ 'sticker'])
+
+ return [
+ ('command', command_name) for command_name in sorted(command_names)]
- return [('command', command_name) for command_name in sorted(command_names)]
@handle_request(r'^decoders$')
def decoders(context):
@@ -40,8 +46,16 @@ def decoders(context):
mime_type: audio/mpeg
plugin: mpcdec
suffix: mpc
+
+ *Clarifications:*
+
+ - ncmpcpp asks for decoders the first time you open the browse view. By
+ returning nothing and OK instead of an not implemented error, we avoid
+ "Not implemented" showing up in the ncmpcpp interface, and we get the
+ list of playlists without having to enter the browse interface twice.
"""
- raise MpdNotImplemented # TODO
+ return # TODO
+
@handle_request(r'^notcommands$', auth_required=False)
def notcommands(context):
@@ -55,13 +69,15 @@ def notcommands(context):
if context.dispatcher.authenticated:
command_names = []
else:
- command_names = [command.name for command in mpd_commands
- if command.auth_required]
+ command_names = [
+ command.name for command in mpd_commands if command.auth_required]
# No permission to use
command_names.append('kill')
- return [('command', command_name) for command_name in sorted(command_names)]
+ return [
+ ('command', command_name) for command_name in sorted(command_names)]
+
@handle_request(r'^tagtypes$')
def tagtypes(context):
@@ -72,7 +88,8 @@ def tagtypes(context):
Shows a list of available song metadata.
"""
- pass # TODO
+ pass # TODO
+
@handle_request(r'^urlhandlers$')
def urlhandlers(context):
@@ -83,5 +100,6 @@ def urlhandlers(context):
Gets a list of available URL handlers.
"""
- return [(u'handler', uri_scheme)
- for uri_scheme in context.backend.uri_schemes.get()]
+ return [
+ ('handler', uri_scheme)
+ for uri_scheme in context.core.uri_schemes.get()]
diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py
index fc24e1e1..34e2fa64 100644
--- a/mopidy/frontends/mpd/protocol/status.py
+++ b/mopidy/frontends/mpd/protocol/status.py
@@ -1,4 +1,6 @@
-import pykka.future
+from __future__ import unicode_literals
+
+import pykka
from mopidy.core import PlaybackState
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@@ -6,8 +8,10 @@ from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.translator import track_to_mpd_format
#: Subsystems that can be registered with idle command.
-SUBSYSTEMS = ['database', 'mixer', 'options', 'output',
- 'player', 'playlist', 'stored_playlist', 'update', ]
+SUBSYSTEMS = [
+ 'database', 'mixer', 'options', 'output', 'player', 'playlist',
+ 'stored_playlist', 'update']
+
@handle_request(r'^clearerror$')
def clearerror(context):
@@ -19,7 +23,8 @@ def clearerror(context):
Clears the current error message in status (this is also
accomplished by any command that starts playback).
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^currentsong$')
def currentsong(context):
@@ -31,10 +36,11 @@ def currentsong(context):
Displays the song info of the current song (same song that is
identified in status).
"""
- current_cp_track = context.backend.playback.current_cp_track.get()
- if current_cp_track is not None:
- position = context.backend.playback.current_playlist_position.get()
- return track_to_mpd_format(current_cp_track, position=position)
+ current_tl_track = context.core.playback.current_tl_track.get()
+ if current_tl_track is not None:
+ position = context.core.playback.tracklist_position.get()
+ return track_to_mpd_format(current_tl_track, position=position)
+
@handle_request(r'^idle$')
@handle_request(r'^idle (?P.+)$')
@@ -90,9 +96,10 @@ def idle(context, subsystems=None):
context.subscriptions = set()
for subsystem in active:
- response.append(u'changed: %s' % subsystem)
+ response.append('changed: %s' % subsystem)
return response
+
@handle_request(r'^noidle$')
def noidle(context):
"""See :meth:`_status_idle`."""
@@ -102,6 +109,7 @@ def noidle(context):
context.events = set()
context.session.prevent_timeout = False
+
@handle_request(r'^stats$')
def stats(context):
"""
@@ -119,15 +127,16 @@ def stats(context):
- ``playtime``: time length of music played
"""
return {
- 'artists': 0, # TODO
- 'albums': 0, # TODO
- 'songs': 0, # TODO
- 'uptime': 0, # TODO
- 'db_playtime': 0, # TODO
- 'db_update': 0, # TODO
- 'playtime': 0, # TODO
+ 'artists': 0, # TODO
+ 'albums': 0, # TODO
+ 'songs': 0, # TODO
+ 'uptime': 0, # TODO
+ 'db_playtime': 0, # TODO
+ 'db_update': 0, # TODO
+ 'playtime': 0, # TODO
}
+
@handle_request(r'^status$')
def status(context):
"""
@@ -153,7 +162,7 @@ def status(context):
- ``nextsongid``: playlist songid of the next song to be played
- ``time``: total time elapsed (of current playing/paused song)
- ``elapsed``: Total time elapsed within the current song, but with
- higher resolution.
+ higher resolution.
- ``bitrate``: instantaneous bitrate in kbps
- ``xfade``: crossfade in seconds
- ``audio``: sampleRate``:bits``:channels
@@ -166,20 +175,20 @@ def status(context):
decimal places for millisecond precision.
"""
futures = {
- 'current_playlist.length': context.backend.current_playlist.length,
- 'current_playlist.version': context.backend.current_playlist.version,
- 'playback.volume': context.backend.playback.volume,
- 'playback.consume': context.backend.playback.consume,
- 'playback.random': context.backend.playback.random,
- 'playback.repeat': context.backend.playback.repeat,
- 'playback.single': context.backend.playback.single,
- 'playback.state': context.backend.playback.state,
- 'playback.current_cp_track': context.backend.playback.current_cp_track,
- 'playback.current_playlist_position':
- context.backend.playback.current_playlist_position,
- 'playback.time_position': context.backend.playback.time_position,
+ 'tracklist.length': context.core.tracklist.length,
+ 'tracklist.version': context.core.tracklist.version,
+ 'playback.volume': context.core.playback.volume,
+ 'playback.consume': context.core.playback.consume,
+ 'playback.random': context.core.playback.random,
+ 'playback.repeat': context.core.playback.repeat,
+ 'playback.single': context.core.playback.single,
+ 'playback.state': context.core.playback.state,
+ 'playback.current_tl_track': context.core.playback.current_tl_track,
+ 'playback.tracklist_position': (
+ context.core.playback.tracklist_position),
+ 'playback.time_position': context.core.playback.time_position,
}
- pykka.future.get_all(futures.values())
+ pykka.get_all(futures.values())
result = [
('volume', _status_volume(futures)),
('repeat', _status_repeat(futures)),
@@ -191,20 +200,22 @@ def status(context):
('xfade', _status_xfade(futures)),
('state', _status_state(futures)),
]
- if futures['playback.current_cp_track'].get() is not None:
+ if futures['playback.current_tl_track'].get() is not None:
result.append(('song', _status_songpos(futures)))
result.append(('songid', _status_songid(futures)))
- if futures['playback.state'].get() in (PlaybackState.PLAYING,
- PlaybackState.PAUSED):
+ if futures['playback.state'].get() in (
+ PlaybackState.PLAYING, PlaybackState.PAUSED):
result.append(('time', _status_time(futures)))
result.append(('elapsed', _status_time_elapsed(futures)))
result.append(('bitrate', _status_bitrate(futures)))
return result
+
def _status_bitrate(futures):
- current_cp_track = futures['playback.current_cp_track'].get()
- if current_cp_track is not None:
- return current_cp_track.track.bitrate
+ current_tl_track = futures['playback.current_tl_track'].get()
+ if current_tl_track is not None:
+ return current_tl_track.track.bitrate
+
def _status_consume(futures):
if futures['playback.consume'].get():
@@ -212,55 +223,68 @@ def _status_consume(futures):
else:
return 0
+
def _status_playlist_length(futures):
- return futures['current_playlist.length'].get()
+ return futures['tracklist.length'].get()
+
def _status_playlist_version(futures):
- return futures['current_playlist.version'].get()
+ return futures['tracklist.version'].get()
+
def _status_random(futures):
return int(futures['playback.random'].get())
+
def _status_repeat(futures):
return int(futures['playback.repeat'].get())
+
def _status_single(futures):
return int(futures['playback.single'].get())
+
def _status_songid(futures):
- current_cp_track = futures['playback.current_cp_track'].get()
- if current_cp_track is not None:
- return current_cp_track.cpid
+ current_tl_track = futures['playback.current_tl_track'].get()
+ if current_tl_track is not None:
+ return current_tl_track.tlid
else:
return _status_songpos(futures)
+
def _status_songpos(futures):
- return futures['playback.current_playlist_position'].get()
+ return futures['playback.tracklist_position'].get()
+
def _status_state(futures):
state = futures['playback.state'].get()
if state == PlaybackState.PLAYING:
- return u'play'
+ return 'play'
elif state == PlaybackState.STOPPED:
- return u'stop'
+ return 'stop'
elif state == PlaybackState.PAUSED:
- return u'pause'
+ return 'pause'
+
def _status_time(futures):
- return u'%d:%d' % (futures['playback.time_position'].get() // 1000,
+ return '%d:%d' % (
+ futures['playback.time_position'].get() // 1000,
_status_time_total(futures) // 1000)
+
def _status_time_elapsed(futures):
- return u'%.3f' % (futures['playback.time_position'].get() / 1000.0)
+ return '%.3f' % (futures['playback.time_position'].get() / 1000.0)
+
def _status_time_total(futures):
- current_cp_track = futures['playback.current_cp_track'].get()
- if current_cp_track is None:
+ current_tl_track = futures['playback.current_tl_track'].get()
+ if current_tl_track is None:
return 0
- elif current_cp_track.track.length is None:
+ elif current_tl_track.track.length is None:
return 0
else:
- return current_cp_track.track.length
+ return current_tl_track.track.length
+
def _status_volume(futures):
volume = futures['playback.volume'].get()
@@ -269,5 +293,6 @@ def _status_volume(futures):
else:
return -1
+
def _status_xfade(futures):
- return 0 # Not supported
+ return 0 # Not supported
diff --git a/mopidy/frontends/mpd/protocol/stickers.py b/mopidy/frontends/mpd/protocol/stickers.py
index c3663ff1..439d8d5b 100644
--- a/mopidy/frontends/mpd/protocol/stickers.py
+++ b/mopidy/frontends/mpd/protocol/stickers.py
@@ -1,7 +1,11 @@
+from __future__ import unicode_literals
+
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
-@handle_request(r'^sticker delete "(?P[^"]+)" '
+
+@handle_request(
+ r'^sticker delete "(?P[^"]+)" '
r'"(?P[^"]+)"( "(?P[^"]+)")*$')
def sticker_delete(context, field, uri, name=None):
"""
@@ -12,9 +16,11 @@ def sticker_delete(context, field, uri, name=None):
Deletes a sticker value from the specified object. If you do not
specify a sticker name, all sticker values are deleted.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
-@handle_request(r'^sticker find "(?P[^"]+)" "(?P[^"]+)" '
+
+@handle_request(
+ r'^sticker find "(?P[^"]+)" "(?P[^"]+)" '
r'"(?P[^"]+)"$')
def sticker_find(context, field, uri, name):
"""
@@ -26,9 +32,11 @@ def sticker_find(context, field, uri, name):
below the specified directory (``URI``). For each matching song, it
prints the ``URI`` and that one sticker's value.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
-@handle_request(r'^sticker get "(?P[^"]+)" "(?P[^"]+)" '
+
+@handle_request(
+ r'^sticker get "(?P[^"]+)" "(?P[^"]+)" '
r'"(?P[^"]+)"$')
def sticker_get(context, field, uri, name):
"""
@@ -38,7 +46,8 @@ def sticker_get(context, field, uri, name):
Reads a sticker value for the specified object.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^sticker list "(?P[^"]+)" "(?P[^"]+)"$')
def sticker_list(context, field, uri):
@@ -49,9 +58,11 @@ def sticker_list(context, field, uri):
Lists the stickers for the specified object.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
-@handle_request(r'^sticker set "(?P[^"]+)" "(?P[^"]+)" '
+
+@handle_request(
+ r'^sticker set "(?P[^"]+)" "(?P[^"]+)" '
r'"(?P[^"]+)" "(?P[^"]+)"$')
def sticker_set(context, field, uri, name, value):
"""
@@ -62,4 +73,4 @@ def sticker_set(context, field, uri, name, value):
Adds a sticker value to the specified object. If a sticker item
with that name already exists, it is replaced.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py
index bb39d328..eef1f3d1 100644
--- a/mopidy/frontends/mpd/protocol/stored_playlists.py
+++ b/mopidy/frontends/mpd/protocol/stored_playlists.py
@@ -1,9 +1,13 @@
+from __future__ import unicode_literals
+
import datetime as dt
from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.translator import playlist_to_mpd_format
+
+@handle_request(r'^listplaylist (?P\w+)$')
@handle_request(r'^listplaylist "(?P[^"]+)"$')
def listplaylist(context, name):
"""
@@ -19,12 +23,13 @@ def listplaylist(context, name):
file: relative/path/to/file2.ogg
file: relative/path/to/file3.mp3
"""
- try:
- playlist = context.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')
+ playlists = context.core.playlists.filter(name=name).get()
+ if not playlists:
+ raise MpdNoExistError('No such playlist', command='listplaylist')
+ return ['file: %s' % t.uri for t in playlists[0].tracks]
+
+@handle_request(r'^listplaylistinfo (?P\w+)$')
@handle_request(r'^listplaylistinfo "(?P[^"]+)"$')
def listplaylistinfo(context, name):
"""
@@ -39,12 +44,11 @@ def listplaylistinfo(context, name):
Standard track listing, with fields: file, Time, Title, Date,
Album, Artist, Track
"""
- try:
- playlist = context.backend.stored_playlists.get(name=name).get()
- return playlist_to_mpd_format(playlist)
- except LookupError:
- raise MpdNoExistError(
- u'No such playlist', command=u'listplaylistinfo')
+ playlists = context.core.playlists.filter(name=name).get()
+ if not playlists:
+ raise MpdNoExistError('No such playlist', command='listplaylistinfo')
+ return playlist_to_mpd_format(playlists[0])
+
@handle_request(r'^listplaylists$')
def listplaylists(context):
@@ -66,20 +70,28 @@ def listplaylists(context):
Last-Modified: 2010-02-06T02:10:25Z
playlist: b
Last-Modified: 2010-02-06T02:11:08Z
+
+ *Clarifications:*
+
+ - ncmpcpp 0.5.10 segfaults if we return 'playlist: ' on a line, so we must
+ ignore playlists without names, which isn't very useful anyway.
"""
result = []
- for playlist in context.backend.stored_playlists.playlists.get():
- result.append((u'playlist', playlist.name))
- last_modified = (playlist.last_modified or
- dt.datetime.now()).isoformat()
+ for playlist in context.core.playlists.playlists.get():
+ if not playlist.name:
+ continue
+ result.append(('playlist', playlist.name))
+ last_modified = (
+ playlist.last_modified or dt.datetime.now()).isoformat()
# Remove microseconds
last_modified = last_modified.split('.')[0]
# Add time zone information
# TODO Convert to UTC before adding Z
last_modified = last_modified + 'Z'
- result.append((u'Last-Modified', last_modified))
+ result.append(('Last-Modified', last_modified))
return result
+
@handle_request(r'^load "(?P[^"]+)"$')
def load(context, name):
"""
@@ -93,11 +105,11 @@ def load(context, name):
- ``load`` appends the given playlist to the current playlist.
"""
- try:
- playlist = context.backend.stored_playlists.get(name=name).get()
- context.backend.current_playlist.append(playlist.tracks)
- except LookupError:
- raise MpdNoExistError(u'No such playlist', command=u'load')
+ playlists = context.core.playlists.filter(name=name).get()
+ if not playlists:
+ raise MpdNoExistError('No such playlist', command='load')
+ context.core.tracklist.add(playlists[0].tracks)
+
@handle_request(r'^playlistadd "(?P[^"]+)" "(?P[^"]+)"$')
def playlistadd(context, name, uri):
@@ -110,7 +122,8 @@ def playlistadd(context, name, uri):
``NAME.m3u`` will be created if it does not exist.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^playlistclear "(?P[^"]+)"$')
def playlistclear(context, name):
@@ -121,7 +134,8 @@ def playlistclear(context, name):
Clears the playlist ``NAME.m3u``.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^playlistdelete "(?P[^"]+)" "(?P\d+)"$')
def playlistdelete(context, name, songpos):
@@ -132,9 +146,11 @@ def playlistdelete(context, name, songpos):
Deletes ``SONGPOS`` from the playlist ``NAME.m3u``.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
-@handle_request(r'^playlistmove "(?P[^"]+)" '
+
+@handle_request(
+ r'^playlistmove "(?P[^"]+)" '
r'"(?P\d+)" "(?P\d+)"$')
def playlistmove(context, name, from_pos, to_pos):
"""
@@ -151,7 +167,8 @@ def playlistmove(context, name, from_pos, to_pos):
documentation, but just the ``SONGPOS`` to move *from*, i.e.
``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^rename "(?P[^"]+)" "(?P[^"]+)"$')
def rename(context, old_name, new_name):
@@ -162,7 +179,8 @@ def rename(context, old_name, new_name):
Renames the playlist ``NAME.m3u`` to ``NEW_NAME.m3u``.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^rm "(?P[^"]+)"$')
def rm(context, name):
@@ -173,7 +191,8 @@ def rm(context, name):
Removes the playlist ``NAME.m3u`` from the playlist directory.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
+
@handle_request(r'^save "(?P[^"]+)"$')
def save(context, name):
@@ -185,4 +204,4 @@ def save(context, name):
Saves the current playlist to ``NAME.m3u`` in the playlist
directory.
"""
- raise MpdNotImplemented # TODO
+ raise MpdNotImplemented # TODO
diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py
new file mode 100644
index 00000000..8a5deecd
--- /dev/null
+++ b/mopidy/frontends/mpd/session.py
@@ -0,0 +1,55 @@
+from __future__ import unicode_literals
+
+import logging
+
+from mopidy.frontends.mpd import dispatcher, protocol
+from mopidy.utils import formatting, network
+
+logger = logging.getLogger('mopidy.frontends.mpd')
+
+
+class MpdSession(network.LineProtocol):
+ """
+ The MPD client session. Keeps track of a single client session. Any
+ requests from the client is passed on to the MPD request dispatcher.
+ """
+
+ terminator = protocol.LINE_TERMINATOR
+ encoding = protocol.ENCODING
+ delimiter = r'\r?\n'
+
+ def __init__(self, connection, core=None):
+ super(MpdSession, self).__init__(connection)
+ self.dispatcher = dispatcher.MpdDispatcher(session=self, core=core)
+
+ def on_start(self):
+ logger.info('New MPD connection from [%s]:%s', self.host, self.port)
+ self.send_lines(['OK MPD %s' % protocol.VERSION])
+
+ def on_line_received(self, line):
+ logger.debug('Request from [%s]:%s: %s', self.host, self.port, line)
+
+ response = self.dispatcher.handle_request(line)
+ if not response:
+ return
+
+ logger.debug(
+ 'Response to [%s]:%s: %s', self.host, self.port,
+ formatting.indent(self.terminator.join(response)))
+
+ self.send_lines(response)
+
+ def on_idle(self, subsystem):
+ self.dispatcher.handle_idle(subsystem)
+
+ def decode(self, line):
+ try:
+ return super(MpdSession, self).decode(line.decode('string_escape'))
+ except ValueError:
+ logger.warning(
+ 'Stopping actor due to unescaping error, data '
+ 'supplied by client was not valid.')
+ self.stop()
+
+ def close(self):
+ self.stop()
diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py
index 6ae32c9e..36b00772 100644
--- a/mopidy/frontends/mpd/translator.py
+++ b/mopidy/frontends/mpd/translator.py
@@ -1,17 +1,20 @@
+from __future__ import unicode_literals
+
import os
import re
from mopidy import settings
from mopidy.frontends.mpd import protocol
-from mopidy.models import CpTrack
+from mopidy.models import TlTrack
from mopidy.utils.path import mtime as get_mtime, uri_to_path, split_path
+
def track_to_mpd_format(track, position=None):
"""
Format track for output to MPD client.
:param track: the track
- :type track: :class:`mopidy.models.Track` or :class:`mopidy.models.CpTrack`
+ :type track: :class:`mopidy.models.Track` or :class:`mopidy.models.TlTrack`
:param position: track's position in playlist
:type position: integer
:param key: if we should set key
@@ -20,10 +23,10 @@ def track_to_mpd_format(track, position=None):
:type mtime: boolean
:rtype: list of two-tuples
"""
- if isinstance(track, CpTrack):
- (cpid, track) = track
+ if isinstance(track, TlTrack):
+ (tlid, track) = track
else:
- (cpid, track) = (None, track)
+ (tlid, track) = (None, track)
result = [
('file', track.uri or ''),
('Time', track.length and (track.length // 1000) or 0),
@@ -40,16 +43,16 @@ def track_to_mpd_format(track, position=None):
if track.album is not None and track.album.artists:
artists = artists_to_mpd_format(track.album.artists)
result.append(('AlbumArtist', artists))
- if position is not None and cpid is not None:
+ if position is not None and tlid is not None:
result.append(('Pos', position))
- result.append(('Id', cpid))
+ result.append(('Id', tlid))
if track.album is not None and track.album.musicbrainz_id is not None:
result.append(('MUSICBRAINZ_ALBUMID', track.album.musicbrainz_id))
# FIXME don't use first and best artist?
# FIXME don't duplicate following code?
if track.album is not None and track.album.artists:
- artists = filter(lambda a: a.musicbrainz_id is not None,
- track.album.artists)
+ artists = filter(
+ lambda a: a.musicbrainz_id is not None, track.album.artists)
if artists:
result.append(
('MUSICBRAINZ_ALBUMARTISTID', artists[0].musicbrainz_id))
@@ -61,16 +64,19 @@ def track_to_mpd_format(track, position=None):
result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id))
return result
+
MPD_KEY_ORDER = '''
key file Time Artist AlbumArtist Title Album Track Date MUSICBRAINZ_ALBUMID
MUSICBRAINZ_ALBUMARTISTID MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime
'''.split()
+
def order_mpd_track_info(result):
"""
- Order results from :func:`mopidy.frontends.mpd.translator.track_to_mpd_format`
- so that it matches MPD's ordering. Simply a cosmetic fix for easier
- diffing of tag_caches.
+ Order results from
+ :func:`mopidy.frontends.mpd.translator.track_to_mpd_format` so that it
+ matches MPD's ordering. Simply a cosmetic fix for easier diffing of
+ tag_caches.
:param result: the track info
:type result: list of tuples
@@ -78,6 +84,7 @@ def order_mpd_track_info(result):
"""
return sorted(result, key=lambda i: MPD_KEY_ORDER.index(i[0]))
+
def artists_to_mpd_format(artists):
"""
Format track artists for output to MPD client.
@@ -88,7 +95,8 @@ def artists_to_mpd_format(artists):
"""
artists = list(artists)
artists.sort(key=lambda a: a.name)
- return u', '.join([a.name for a in artists if a.name])
+ return ', '.join([a.name for a in artists if a.name])
+
def tracks_to_mpd_format(tracks, start=0, end=None):
"""
@@ -98,7 +106,7 @@ def tracks_to_mpd_format(tracks, start=0, end=None):
:param tracks: the tracks
:type tracks: list of :class:`mopidy.models.Track` or
- :class:`mopidy.models.CpTrack`
+ :class:`mopidy.models.TlTrack`
:param start: position of first track to include in output
:type start: int (positive or negative)
:param end: position after last track to include in output
@@ -115,6 +123,7 @@ def tracks_to_mpd_format(tracks, start=0, end=None):
result.append(track_to_mpd_format(track, position))
return result
+
def playlist_to_mpd_format(playlist, *args, **kwargs):
"""
Format playlist for output to MPD client.
@@ -123,6 +132,7 @@ def playlist_to_mpd_format(playlist, *args, **kwargs):
"""
return tracks_to_mpd_format(playlist.tracks, *args, **kwargs)
+
def tracks_to_tag_cache_format(tracks):
"""
Format list of tracks for output to MPD tag cache
@@ -141,6 +151,7 @@ def tracks_to_tag_cache_format(tracks):
_add_to_tag_cache(result, *tracks_to_directory_tree(tracks))
return result
+
def _add_to_tag_cache(result, folders, files):
music_folder = settings.LOCAL_MUSIC_PATH
regexp = '^' + re.escape(music_folder).rstrip('/') + '/?'
@@ -165,10 +176,11 @@ def _add_to_tag_cache(result, folders, files):
result.extend(track_result)
result.append(('songList end',))
+
def tracks_to_directory_tree(tracks):
directories = ({}, [])
for track in tracks:
- path = u''
+ path = ''
current = directories
local_folder = settings.LOCAL_MUSIC_PATH
diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py
index 0f5d35c5..2be6efea 100644
--- a/mopidy/frontends/mpris/__init__.py
+++ b/mopidy/frontends/mpris/__init__.py
@@ -1,131 +1,56 @@
-import logging
+"""
+Frontend which lets you control Mopidy through the Media Player Remote
+Interfacing Specification (`MPRIS `_) D-Bus
+interface.
-logger = logging.getLogger('mopidy.frontends.mpris')
+An example of an MPRIS client is the `Ubuntu Sound Menu
+`_.
-try:
- import indicate
-except ImportError as import_error:
- indicate = None
- logger.debug(u'Startup notification will not be sent (%s)', import_error)
+**Dependencies:**
-from pykka.actor import ThreadingActor
+- D-Bus Python bindings. The package is named ``python-dbus`` in
+ Ubuntu/Debian.
-from mopidy import settings
-from mopidy.frontends.mpris import objects
-from mopidy.listeners import BackendListener
+- ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the
+ Ubuntu Sound Menu. The package is named ``python-indicate`` in
+ Ubuntu/Debian.
+- An ``.desktop`` file for Mopidy installed at the path set in
+ :attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install-desktop-file` for
+ details.
-class MprisFrontend(ThreadingActor, BackendListener):
- """
- Frontend which lets you control Mopidy through the Media Player Remote
- Interfacing Specification (`MPRIS `_) D-Bus
- interface.
+**Settings:**
- An example of an MPRIS client is the `Ubuntu Sound Menu
- `_.
+- :attr:`mopidy.settings.DESKTOP_FILE`
- **Dependencies:**
+**Usage:**
- - D-Bus Python bindings. The package is named ``python-dbus`` in
- Ubuntu/Debian.
- - ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the
- Ubuntu Sound Menu. The package is named ``python-indicate`` in
- Ubuntu/Debian.
- - An ``.desktop`` file for Mopidy installed at the path set in
- :attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install_desktop_file` for
- details.
+Make sure :attr:`mopidy.settings.FRONTENDS` includes
+``mopidy.frontends.mpris.MprisFrontend``. By default, the setting includes the
+MPRIS frontend.
- **Testing the frontend**
+**Testing the frontend**
- To test, start Mopidy, and then run the following in a Python shell::
+To test, start Mopidy, and then run the following in a Python shell::
- import dbus
- bus = dbus.SessionBus()
- player = bus.get_object('org.mpris.MediaPlayer2.mopidy',
- '/org/mpris/MediaPlayer2')
+ import dbus
+ bus = dbus.SessionBus()
+ player = bus.get_object('org.mpris.MediaPlayer2.mopidy',
+ '/org/mpris/MediaPlayer2')
- Now you can control Mopidy through the player object. Examples:
+Now you can control Mopidy through the player object. Examples:
- - To get some properties from Mopidy, run::
+- To get some properties from Mopidy, run::
- props = player.GetAll('org.mpris.MediaPlayer2',
- dbus_interface='org.freedesktop.DBus.Properties')
+ props = player.GetAll('org.mpris.MediaPlayer2',
+ dbus_interface='org.freedesktop.DBus.Properties')
- - To quit Mopidy through D-Bus, run::
+- To quit Mopidy through D-Bus, run::
- player.Quit(dbus_interface='org.mpris.MediaPlayer2')
- """
+ player.Quit(dbus_interface='org.mpris.MediaPlayer2')
+"""
- def __init__(self):
- super(MprisFrontend, self).__init__()
- self.indicate_server = None
- self.mpris_object = None
+from __future__ import unicode_literals
- def on_start(self):
- try:
- self.mpris_object = objects.MprisObject()
- self._send_startup_notification()
- except Exception as e:
- logger.error(u'MPRIS frontend setup failed (%s)', e)
- self.stop()
-
- def on_stop(self):
- logger.debug(u'Removing MPRIS object from D-Bus connection...')
- if self.mpris_object:
- self.mpris_object.remove_from_connection()
- self.mpris_object = None
- logger.debug(u'Removed MPRIS object from D-Bus connection')
-
- def _send_startup_notification(self):
- """
- Send startup notification using libindicate to make Mopidy appear in
- e.g. `Ubuntu's sound menu `_.
-
- A reference to the libindicate server is kept for as long as Mopidy is
- running. When Mopidy exits, the server will be unreferenced and Mopidy
- will automatically be unregistered from e.g. the sound menu.
- """
- if not indicate:
- return
- logger.debug(u'Sending startup notification...')
- self.indicate_server = indicate.Server()
- self.indicate_server.set_type('music.mopidy')
- self.indicate_server.set_desktop_file(settings.DESKTOP_FILE)
- self.indicate_server.show()
- logger.debug(u'Startup notification sent')
-
- def _emit_properties_changed(self, *changed_properties):
- if self.mpris_object is None:
- return
- props_with_new_values = [
- (p, self.mpris_object.Get(objects.PLAYER_IFACE, p))
- for p in changed_properties]
- self.mpris_object.PropertiesChanged(objects.PLAYER_IFACE,
- dict(props_with_new_values), [])
-
- def track_playback_paused(self, track, time_position):
- logger.debug(u'Received track playback paused event')
- self._emit_properties_changed('PlaybackStatus')
-
- def track_playback_resumed(self, track, time_position):
- logger.debug(u'Received track playback resumed event')
- self._emit_properties_changed('PlaybackStatus')
-
- def track_playback_started(self, track):
- logger.debug(u'Received track playback started event')
- self._emit_properties_changed('PlaybackStatus', 'Metadata')
-
- def track_playback_ended(self, track, time_position):
- logger.debug(u'Received track playback ended event')
- self._emit_properties_changed('PlaybackStatus', 'Metadata')
-
- def volume_changed(self):
- logger.debug(u'Received volume changed event')
- self._emit_properties_changed('Volume')
-
- def seeked(self):
- logger.debug(u'Received seeked event')
- if self.mpris_object is None:
- return
- self.mpris_object.Seeked(
- self.mpris_object.Get(objects.PLAYER_IFACE, 'Position'))
+# flake8: noqa
+from .actor import MprisFrontend
diff --git a/mopidy/frontends/mpris/actor.py b/mopidy/frontends/mpris/actor.py
new file mode 100644
index 00000000..795b2694
--- /dev/null
+++ b/mopidy/frontends/mpris/actor.py
@@ -0,0 +1,104 @@
+from __future__ import unicode_literals
+
+import logging
+
+import pykka
+
+from mopidy import settings
+from mopidy.core import CoreListener
+from mopidy.frontends.mpris import objects
+
+logger = logging.getLogger('mopidy.frontends.mpris')
+
+try:
+ import indicate
+except ImportError as import_error:
+ indicate = None # noqa
+ logger.debug('Startup notification will not be sent (%s)', import_error)
+
+
+class MprisFrontend(pykka.ThreadingActor, CoreListener):
+ def __init__(self, core):
+ super(MprisFrontend, self).__init__()
+ self.core = core
+ self.indicate_server = None
+ self.mpris_object = None
+
+ def on_start(self):
+ try:
+ self.mpris_object = objects.MprisObject(self.core)
+ self._send_startup_notification()
+ except Exception as e:
+ logger.error('MPRIS frontend setup failed (%s)', e)
+ self.stop()
+
+ def on_stop(self):
+ logger.debug('Removing MPRIS object from D-Bus connection...')
+ if self.mpris_object:
+ self.mpris_object.remove_from_connection()
+ self.mpris_object = None
+ logger.debug('Removed MPRIS object from D-Bus connection')
+
+ def _send_startup_notification(self):
+ """
+ Send startup notification using libindicate to make Mopidy appear in
+ e.g. `Ubunt's sound menu `_.
+
+ A reference to the libindicate server is kept for as long as Mopidy is
+ running. When Mopidy exits, the server will be unreferenced and Mopidy
+ will automatically be unregistered from e.g. the sound menu.
+ """
+ if not indicate:
+ return
+ logger.debug('Sending startup notification...')
+ self.indicate_server = indicate.Server()
+ self.indicate_server.set_type('music.mopidy')
+ self.indicate_server.set_desktop_file(settings.DESKTOP_FILE)
+ self.indicate_server.show()
+ logger.debug('Startup notification sent')
+
+ def _emit_properties_changed(self, interface, changed_properties):
+ if self.mpris_object is None:
+ return
+ props_with_new_values = [
+ (p, self.mpris_object.Get(interface, p))
+ for p in changed_properties]
+ self.mpris_object.PropertiesChanged(
+ interface, dict(props_with_new_values), [])
+
+ def track_playback_paused(self, track, time_position):
+ logger.debug('Received track_playback_paused event')
+ self._emit_properties_changed(objects.PLAYER_IFACE, ['PlaybackStatus'])
+
+ def track_playback_resumed(self, track, time_position):
+ logger.debug('Received track_playback_resumed event')
+ self._emit_properties_changed(objects.PLAYER_IFACE, ['PlaybackStatus'])
+
+ def track_playback_started(self, track):
+ logger.debug('Received track_playback_started event')
+ self._emit_properties_changed(
+ objects.PLAYER_IFACE, ['PlaybackStatus', 'Metadata'])
+
+ def track_playback_ended(self, track, time_position):
+ logger.debug('Received track_playback_ended event')
+ self._emit_properties_changed(
+ objects.PLAYER_IFACE, ['PlaybackStatus', 'Metadata'])
+
+ def volume_changed(self):
+ logger.debug('Received volume_changed event')
+ self._emit_properties_changed(objects.PLAYER_IFACE, ['Volume'])
+
+ def seeked(self, time_position_in_ms):
+ logger.debug('Received seeked event')
+ self.mpris_object.Seeked(time_position_in_ms * 1000)
+
+ def playlists_loaded(self):
+ logger.debug('Received playlists_loaded event')
+ self._emit_properties_changed(
+ objects.PLAYLISTS_IFACE, ['PlaylistCount'])
+
+ def playlist_changed(self, playlist):
+ logger.debug('Received playlist_changed event')
+ playlist_id = self.mpris_object.get_playlist_id(playlist.uri)
+ playlist = (playlist_id, playlist.name, '')
+ self.mpris_object.PlaylistChanged(playlist)
diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py
index 93669977..15ef9383 100644
--- a/mopidy/frontends/mpris/objects.py
+++ b/mopidy/frontends/mpris/objects.py
@@ -1,24 +1,25 @@
+from __future__ import unicode_literals
+
+import base64
import logging
import os
-logger = logging.getLogger('mopidy.frontends.mpris')
-
try:
import dbus
import dbus.mainloop.glib
import dbus.service
import gobject
except ImportError as import_error:
- from mopidy import OptionalDependencyError
+ from mopidy.exceptions import OptionalDependencyError
raise OptionalDependencyError(import_error)
-from pykka.registry import ActorRegistry
-
from mopidy import settings
-from mopidy.backends.base import Backend
from mopidy.core import PlaybackState
from mopidy.utils.process import exit_process
+
+logger = logging.getLogger('mopidy.frontends.mpris')
+
# Must be done before dbus.SessionBus() is called
gobject.threads_init()
dbus.mainloop.glib.threads_init()
@@ -27,25 +28,29 @@ BUS_NAME = 'org.mpris.MediaPlayer2.mopidy'
OBJECT_PATH = '/org/mpris/MediaPlayer2'
ROOT_IFACE = 'org.mpris.MediaPlayer2'
PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player'
+PLAYLISTS_IFACE = 'org.mpris.MediaPlayer2.Playlists'
class MprisObject(dbus.service.Object):
- """Implements http://www.mpris.org/2.1/spec/"""
+ """Implements http://www.mpris.org/2.2/spec/"""
properties = None
- def __init__(self):
- self._backend = None
+ def __init__(self, core):
+ self.core = core
self.properties = {
ROOT_IFACE: self._get_root_iface_properties(),
PLAYER_IFACE: self._get_player_iface_properties(),
+ PLAYLISTS_IFACE: self._get_playlists_iface_properties(),
}
bus_name = self._connect_to_dbus()
- super(MprisObject, self).__init__(bus_name, OBJECT_PATH)
+ dbus.service.Object.__init__(self, bus_name, OBJECT_PATH)
def _get_root_iface_properties(self):
return {
'CanQuit': (True, None),
+ 'Fullscreen': (False, None),
+ 'CanSetFullscreen': (False, None),
'CanRaise': (False, None),
# NOTE Change if adding optional track list support
'HasTrackList': (False, None),
@@ -76,217 +81,222 @@ class MprisObject(dbus.service.Object):
'CanControl': (self.get_CanControl, None),
}
+ def _get_playlists_iface_properties(self):
+ return {
+ 'PlaylistCount': (self.get_PlaylistCount, None),
+ 'Orderings': (self.get_Orderings, None),
+ 'ActivePlaylist': (self.get_ActivePlaylist, None),
+ }
+
def _connect_to_dbus(self):
- logger.debug(u'Connecting to D-Bus...')
+ logger.debug('Connecting to D-Bus...')
mainloop = dbus.mainloop.glib.DBusGMainLoop()
- bus_name = dbus.service.BusName(BUS_NAME,
- dbus.SessionBus(mainloop=mainloop))
- logger.info(u'Connected to D-Bus')
+ bus_name = dbus.service.BusName(
+ BUS_NAME, dbus.SessionBus(mainloop=mainloop))
+ logger.info('Connected to D-Bus')
return bus_name
- @property
- def backend(self):
- if self._backend is None:
- backend_refs = ActorRegistry.get_by_class(Backend)
- assert len(backend_refs) == 1, \
- 'Expected exactly one running backend.'
- self._backend = backend_refs[0].proxy()
- return self._backend
+ def get_playlist_id(self, playlist_uri):
+ # Only A-Za-z0-9_ is allowed, which is 63 chars, so we can't use
+ # base64. Luckily, D-Bus does not limit the length of object paths.
+ # Since base32 pads trailing bytes with "=" chars, we need to replace
+ # them with an allowed character such as "_".
+ encoded_uri = base64.b32encode(playlist_uri).replace('=', '_')
+ return '/com/mopidy/playlist/%s' % encoded_uri
- def _get_track_id(self, cp_track):
- return '/com/mopidy/track/%d' % cp_track.cpid
+ def get_playlist_uri(self, playlist_id):
+ encoded_uri = playlist_id.split('/')[-1].replace('_', '=')
+ return base64.b32decode(encoded_uri)
- def _get_cpid(self, track_id):
+ def get_track_id(self, tl_track):
+ return '/com/mopidy/track/%d' % tl_track.tlid
+
+ def get_track_tlid(self, track_id):
assert track_id.startswith('/com/mopidy/track/')
return track_id.split('/')[-1]
### Properties interface
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
- in_signature='ss', out_signature='v')
+ in_signature='ss', out_signature='v')
def Get(self, interface, prop):
- logger.debug(u'%s.Get(%s, %s) called',
+ logger.debug(
+ '%s.Get(%s, %s) called',
dbus.PROPERTIES_IFACE, repr(interface), repr(prop))
- (getter, setter) = self.properties[interface][prop]
+ (getter, _) = self.properties[interface][prop]
if callable(getter):
return getter()
else:
return getter
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
- in_signature='s', out_signature='a{sv}')
+ in_signature='s', out_signature='a{sv}')
def GetAll(self, interface):
- logger.debug(u'%s.GetAll(%s) called',
- dbus.PROPERTIES_IFACE, repr(interface))
+ logger.debug(
+ '%s.GetAll(%s) called', dbus.PROPERTIES_IFACE, repr(interface))
getters = {}
- for key, (getter, setter) in self.properties[interface].iteritems():
+ for key, (getter, _) in self.properties[interface].iteritems():
getters[key] = getter() if callable(getter) else getter
return getters
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
- in_signature='ssv', out_signature='')
+ in_signature='ssv', out_signature='')
def Set(self, interface, prop, value):
- logger.debug(u'%s.Set(%s, %s, %s) called',
+ logger.debug(
+ '%s.Set(%s, %s, %s) called',
dbus.PROPERTIES_IFACE, repr(interface), repr(prop), repr(value))
- getter, setter = self.properties[interface][prop]
+ _, setter = self.properties[interface][prop]
if setter is not None:
setter(value)
- self.PropertiesChanged(interface,
- {prop: self.Get(interface, prop)}, [])
+ self.PropertiesChanged(
+ interface, {prop: self.Get(interface, prop)}, [])
@dbus.service.signal(dbus_interface=dbus.PROPERTIES_IFACE,
- signature='sa{sv}as')
+ signature='sa{sv}as')
def PropertiesChanged(self, interface, changed_properties,
- invalidated_properties):
- logger.debug(u'%s.PropertiesChanged(%s, %s, %s) signaled',
+ invalidated_properties):
+ logger.debug(
+ '%s.PropertiesChanged(%s, %s, %s) signaled',
dbus.PROPERTIES_IFACE, interface, changed_properties,
invalidated_properties)
-
### Root interface methods
@dbus.service.method(dbus_interface=ROOT_IFACE)
def Raise(self):
- logger.debug(u'%s.Raise called', ROOT_IFACE)
+ logger.debug('%s.Raise called', ROOT_IFACE)
# Do nothing, as we do not have a GUI
@dbus.service.method(dbus_interface=ROOT_IFACE)
def Quit(self):
- logger.debug(u'%s.Quit called', ROOT_IFACE)
+ logger.debug('%s.Quit called', ROOT_IFACE)
exit_process()
-
### Root interface properties
def get_DesktopEntry(self):
return os.path.splitext(os.path.basename(settings.DESKTOP_FILE))[0]
def get_SupportedUriSchemes(self):
- return dbus.Array(self.backend.uri_schemes.get(), signature='s')
-
+ return dbus.Array(self.core.uri_schemes.get(), signature='s')
### Player interface methods
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Next(self):
- logger.debug(u'%s.Next called', PLAYER_IFACE)
+ logger.debug('%s.Next called', PLAYER_IFACE)
if not self.get_CanGoNext():
- logger.debug(u'%s.Next not allowed', PLAYER_IFACE)
+ logger.debug('%s.Next not allowed', PLAYER_IFACE)
return
- self.backend.playback.next().get()
+ self.core.playback.next().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Previous(self):
- logger.debug(u'%s.Previous called', PLAYER_IFACE)
+ logger.debug('%s.Previous called', PLAYER_IFACE)
if not self.get_CanGoPrevious():
- logger.debug(u'%s.Previous not allowed', PLAYER_IFACE)
+ logger.debug('%s.Previous not allowed', PLAYER_IFACE)
return
- self.backend.playback.previous().get()
+ self.core.playback.previous().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Pause(self):
- logger.debug(u'%s.Pause called', PLAYER_IFACE)
+ logger.debug('%s.Pause called', PLAYER_IFACE)
if not self.get_CanPause():
- logger.debug(u'%s.Pause not allowed', PLAYER_IFACE)
+ logger.debug('%s.Pause not allowed', PLAYER_IFACE)
return
- self.backend.playback.pause().get()
+ self.core.playback.pause().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def PlayPause(self):
- logger.debug(u'%s.PlayPause called', PLAYER_IFACE)
+ logger.debug('%s.PlayPause called', PLAYER_IFACE)
if not self.get_CanPause():
- logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE)
+ logger.debug('%s.PlayPause not allowed', PLAYER_IFACE)
return
- state = self.backend.playback.state.get()
+ state = self.core.playback.state.get()
if state == PlaybackState.PLAYING:
- self.backend.playback.pause().get()
+ self.core.playback.pause().get()
elif state == PlaybackState.PAUSED:
- self.backend.playback.resume().get()
+ self.core.playback.resume().get()
elif state == PlaybackState.STOPPED:
- self.backend.playback.play().get()
+ self.core.playback.play().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Stop(self):
- logger.debug(u'%s.Stop called', PLAYER_IFACE)
+ logger.debug('%s.Stop called', PLAYER_IFACE)
if not self.get_CanControl():
- logger.debug(u'%s.Stop not allowed', PLAYER_IFACE)
+ logger.debug('%s.Stop not allowed', PLAYER_IFACE)
return
- self.backend.playback.stop().get()
+ self.core.playback.stop().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Play(self):
- logger.debug(u'%s.Play called', PLAYER_IFACE)
+ logger.debug('%s.Play called', PLAYER_IFACE)
if not self.get_CanPlay():
- logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
+ logger.debug('%s.Play not allowed', PLAYER_IFACE)
return
- state = self.backend.playback.state.get()
+ state = self.core.playback.state.get()
if state == PlaybackState.PAUSED:
- self.backend.playback.resume().get()
+ self.core.playback.resume().get()
else:
- self.backend.playback.play().get()
+ self.core.playback.play().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Seek(self, offset):
- logger.debug(u'%s.Seek called', PLAYER_IFACE)
+ logger.debug('%s.Seek called', PLAYER_IFACE)
if not self.get_CanSeek():
- logger.debug(u'%s.Seek not allowed', PLAYER_IFACE)
+ logger.debug('%s.Seek not allowed', PLAYER_IFACE)
return
offset_in_milliseconds = offset // 1000
- current_position = self.backend.playback.time_position.get()
+ current_position = self.core.playback.time_position.get()
new_position = current_position + offset_in_milliseconds
- self.backend.playback.seek(new_position)
+ self.core.playback.seek(new_position)
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def SetPosition(self, track_id, position):
- logger.debug(u'%s.SetPosition called', PLAYER_IFACE)
+ logger.debug('%s.SetPosition called', PLAYER_IFACE)
if not self.get_CanSeek():
- logger.debug(u'%s.SetPosition not allowed', PLAYER_IFACE)
+ logger.debug('%s.SetPosition not allowed', PLAYER_IFACE)
return
position = position // 1000
- current_cp_track = self.backend.playback.current_cp_track.get()
- if current_cp_track is None:
+ current_tl_track = self.core.playback.current_tl_track.get()
+ if current_tl_track is None:
return
- if track_id != self._get_track_id(current_cp_track):
+ if track_id != self.get_track_id(current_tl_track):
return
if position < 0:
return
- if current_cp_track.track.length < position:
+ if current_tl_track.track.length < position:
return
- self.backend.playback.seek(position)
+ self.core.playback.seek(position)
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def OpenUri(self, uri):
- logger.debug(u'%s.OpenUri called', PLAYER_IFACE)
+ logger.debug('%s.OpenUri called', PLAYER_IFACE)
if not self.get_CanPlay():
# NOTE The spec does not explictly require this check, but guarding
# the other methods doesn't help much if OpenUri is open for use.
- logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
+ logger.debug('%s.Play not allowed', PLAYER_IFACE)
return
# NOTE Check if URI has MIME type known to the backend, if MIME support
# is added to the backend.
- uri_schemes = self.backend.uri_schemes.get()
- if not any([uri.startswith(uri_scheme) for uri_scheme in uri_schemes]):
- return
- track = self.backend.library.lookup(uri).get()
- if track is not None:
- cp_track = self.backend.current_playlist.add(track).get()
- self.backend.playback.play(cp_track)
+ tracks = self.core.library.lookup(uri).get()
+ if tracks:
+ tl_tracks = self.core.tracklist.add(tracks).get()
+ self.core.playback.play(tl_tracks[0])
else:
- logger.debug(u'Track with URI "%s" not found in library.', uri)
-
+ logger.debug('Track with URI "%s" not found in library.', uri)
### Player interface signals
@dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x')
def Seeked(self, position):
- logger.debug(u'%s.Seeked signaled', PLAYER_IFACE)
+ logger.debug('%s.Seeked signaled', PLAYER_IFACE)
# Do nothing, as just calling the method is enough to emit the signal.
-
### Player interface properties
def get_PlaybackStatus(self):
- state = self.backend.playback.state.get()
+ state = self.core.playback.state.get()
if state == PlaybackState.PLAYING:
return 'Playing'
elif state == PlaybackState.PAUSED:
@@ -295,8 +305,8 @@ class MprisObject(dbus.service.Object):
return 'Stopped'
def get_LoopStatus(self):
- repeat = self.backend.playback.repeat.get()
- single = self.backend.playback.single.get()
+ repeat = self.core.playback.repeat.get()
+ single = self.core.playback.single.get()
if not repeat:
return 'None'
else:
@@ -307,46 +317,46 @@ class MprisObject(dbus.service.Object):
def set_LoopStatus(self, value):
if not self.get_CanControl():
- logger.debug(u'Setting %s.LoopStatus not allowed', PLAYER_IFACE)
+ logger.debug('Setting %s.LoopStatus not allowed', PLAYER_IFACE)
return
if value == 'None':
- self.backend.playback.repeat = False
- self.backend.playback.single = False
+ self.core.playback.repeat = False
+ self.core.playback.single = False
elif value == 'Track':
- self.backend.playback.repeat = True
- self.backend.playback.single = True
+ self.core.playback.repeat = True
+ self.core.playback.single = True
elif value == 'Playlist':
- self.backend.playback.repeat = True
- self.backend.playback.single = False
+ self.core.playback.repeat = True
+ self.core.playback.single = False
def set_Rate(self, value):
if not self.get_CanControl():
# NOTE The spec does not explictly require this check, but it was
# added to be consistent with all the other property setters.
- logger.debug(u'Setting %s.Rate not allowed', PLAYER_IFACE)
+ logger.debug('Setting %s.Rate not allowed', PLAYER_IFACE)
return
if value == 0:
self.Pause()
def get_Shuffle(self):
- return self.backend.playback.random.get()
+ return self.core.playback.random.get()
def set_Shuffle(self, value):
if not self.get_CanControl():
- logger.debug(u'Setting %s.Shuffle not allowed', PLAYER_IFACE)
+ logger.debug('Setting %s.Shuffle not allowed', PLAYER_IFACE)
return
if value:
- self.backend.playback.random = True
+ self.core.playback.random = True
else:
- self.backend.playback.random = False
+ self.core.playback.random = False
def get_Metadata(self):
- current_cp_track = self.backend.playback.current_cp_track.get()
- if current_cp_track is None:
+ current_tl_track = self.core.playback.current_tl_track.get()
+ if current_tl_track is None:
return {'mpris:trackid': ''}
else:
- (cpid, track) = current_cp_track
- metadata = {'mpris:trackid': self._get_track_id(current_cp_track)}
+ (_, track) = current_tl_track
+ metadata = {'mpris:trackid': self.get_track_id(current_tl_track)}
if track.length:
metadata['mpris:length'] = track.length * 1000
if track.uri:
@@ -370,44 +380,47 @@ class MprisObject(dbus.service.Object):
return dbus.Dictionary(metadata, signature='sv')
def get_Volume(self):
- volume = self.backend.playback.volume.get()
+ volume = self.core.playback.volume.get()
if volume is None:
return 0
return volume / 100.0
def set_Volume(self, value):
if not self.get_CanControl():
- logger.debug(u'Setting %s.Volume not allowed', PLAYER_IFACE)
+ logger.debug('Setting %s.Volume not allowed', PLAYER_IFACE)
return
if value is None:
return
elif value < 0:
- self.backend.playback.volume = 0
+ self.core.playback.volume = 0
elif value > 1:
- self.backend.playback.volume = 100
+ self.core.playback.volume = 100
elif 0 <= value <= 1:
- self.backend.playback.volume = int(value * 100)
+ self.core.playback.volume = int(value * 100)
def get_Position(self):
- return self.backend.playback.time_position.get() * 1000
+ return self.core.playback.time_position.get() * 1000
def get_CanGoNext(self):
if not self.get_CanControl():
return False
- return (self.backend.playback.cp_track_at_next.get() !=
- self.backend.playback.current_cp_track.get())
+ return (
+ self.core.playback.tl_track_at_next.get() !=
+ self.core.playback.current_tl_track.get())
def get_CanGoPrevious(self):
if not self.get_CanControl():
return False
- return (self.backend.playback.cp_track_at_previous.get() !=
- self.backend.playback.current_cp_track.get())
+ return (
+ self.core.playback.tl_track_at_previous.get() !=
+ self.core.playback.current_tl_track.get())
def get_CanPlay(self):
if not self.get_CanControl():
return False
- return (self.backend.playback.current_track.get() is not None
- or self.backend.playback.track_at_next.get() is not None)
+ return (
+ self.core.playback.current_tl_track.get() is not None or
+ self.core.playback.tl_track_at_next.get() is not None)
def get_CanPause(self):
if not self.get_CanControl():
@@ -426,3 +439,58 @@ class MprisObject(dbus.service.Object):
def get_CanControl(self):
# NOTE This could be a setting for the end user to change.
return True
+
+ ### Playlists interface methods
+
+ @dbus.service.method(dbus_interface=PLAYLISTS_IFACE)
+ def ActivatePlaylist(self, playlist_id):
+ logger.debug(
+ '%s.ActivatePlaylist(%r) called', PLAYLISTS_IFACE, playlist_id)
+ playlist_uri = self.get_playlist_uri(playlist_id)
+ playlist = self.core.playlists.lookup(playlist_uri).get()
+ if playlist and playlist.tracks:
+ tl_tracks = self.core.tracklist.add(playlist.tracks).get()
+ self.core.playback.play(tl_tracks[0])
+
+ @dbus.service.method(dbus_interface=PLAYLISTS_IFACE)
+ def GetPlaylists(self, index, max_count, order, reverse):
+ logger.debug(
+ '%s.GetPlaylists(%r, %r, %r, %r) called',
+ PLAYLISTS_IFACE, index, max_count, order, reverse)
+ playlists = self.core.playlists.playlists.get()
+ if order == 'Alphabetical':
+ playlists.sort(key=lambda p: p.name, reverse=reverse)
+ elif order == 'Modified':
+ playlists.sort(key=lambda p: p.last_modified, reverse=reverse)
+ elif order == 'User' and reverse:
+ playlists.reverse()
+ slice_end = index + max_count
+ playlists = playlists[index:slice_end]
+ results = [
+ (self.get_playlist_id(p.uri), p.name, '')
+ for p in playlists]
+ return dbus.Array(results, signature='(oss)')
+
+ ### Playlists interface signals
+
+ @dbus.service.signal(dbus_interface=PLAYLISTS_IFACE, signature='(oss)')
+ def PlaylistChanged(self, playlist):
+ logger.debug('%s.PlaylistChanged signaled', PLAYLISTS_IFACE)
+ # Do nothing, as just calling the method is enough to emit the signal.
+
+ ### Playlists interface properties
+
+ def get_PlaylistCount(self):
+ return len(self.core.playlists.playlists.get())
+
+ def get_Orderings(self):
+ return [
+ 'Alphabetical', # Order by playlist.name
+ 'Modified', # Order by playlist.last_modified
+ 'User', # Don't change order
+ ]
+
+ def get_ActivePlaylist(self):
+ playlist_is_valid = False
+ playlist = ('/', 'None', '')
+ return (playlist_is_valid, playlist)
diff --git a/mopidy/models.py b/mopidy/models.py
index 507ca088..a4ed1b4f 100644
--- a/mopidy/models.py
+++ b/mopidy/models.py
@@ -1,4 +1,6 @@
-from collections import namedtuple
+from __future__ import unicode_literals
+
+import json
class ImmutableObject(object):
@@ -12,9 +14,10 @@ class ImmutableObject(object):
def __init__(self, *args, **kwargs):
for key, value in kwargs.items():
- if not hasattr(self, key):
- raise TypeError('__init__() got an unexpected keyword ' + \
- 'argument \'%s\'' % key)
+ if not hasattr(self, key) or callable(getattr(self, key)):
+ raise TypeError(
+ '__init__() got an unexpected keyword argument "%s"' %
+ key)
self.__dict__[key] = value
def __setattr__(self, name, value):
@@ -71,12 +74,13 @@ class ImmutableObject(object):
if hasattr(self, key):
data[key] = values.pop(key)
if values:
- raise TypeError("copy() got an unexpected keyword argument '%s'"
- % key)
+ raise TypeError(
+ 'copy() got an unexpected keyword argument "%s"' % key)
return self.__class__(**data)
def serialize(self):
data = {}
+ data['__model__'] = self.__class__.__name__
for key in self.__dict__.keys():
public_key = key.lstrip('_')
value = self.__dict__[key]
@@ -89,6 +93,44 @@ class ImmutableObject(object):
return data
+class ModelJSONEncoder(json.JSONEncoder):
+ """
+ Automatically serialize Mopidy models to JSON.
+
+ Usage::
+
+ >>> import json
+ >>> json.dumps({'a_track': Track(name='name')}, cls=ModelJSONEncoder)
+ '{"a_track": {"__model__": "Track", "name": "name"}}'
+
+ """
+ def default(self, obj):
+ if isinstance(obj, ImmutableObject):
+ return obj.serialize()
+ return json.JSONEncoder.default(self, obj)
+
+
+def model_json_decoder(dct):
+ """
+ Automatically deserialize Mopidy models from JSON.
+
+ Usage::
+
+ >>> import json
+ >>> json.loads(
+ ... '{"a_track": {"__model__": "Track", "name": "name"}}',
+ ... object_hook=model_json_decoder)
+ {u'a_track': Track(artists=[], name=u'name')}
+
+ """
+ if '__model__' in dct:
+ model_name = dct.pop('__model__')
+ cls = globals().get(model_name, None)
+ if issubclass(cls, ImmutableObject):
+ return cls(**dct)
+ return dct
+
+
class Artist(ImmutableObject):
"""
:param uri: artist URI
@@ -119,6 +161,8 @@ class Album(ImmutableObject):
:type artists: list of :class:`Artist`
:param num_tracks: number of tracks in album
:type num_tracks: integer
+ :param date: album release date (YYYY or YYYY-MM-DD)
+ :type date: string
:param musicbrainz_id: MusicBrainz ID
:type musicbrainz_id: string
"""
@@ -135,6 +179,9 @@ class Album(ImmutableObject):
#: The number of tracks in the album. Read-only.
num_tracks = 0
+ #: The album release date. Read-only.
+ date = None
+
#: The MusicBrainz ID of the album. Read-only.
musicbrainz_id = None
@@ -143,9 +190,6 @@ class Album(ImmutableObject):
super(Album, self).__init__(*args, **kwargs)
-CpTrack = namedtuple('CpTrack', ['cpid', 'track'])
-
-
class Track(ImmutableObject):
"""
:param uri: track URI
@@ -200,16 +244,54 @@ class Track(ImmutableObject):
super(Track, self).__init__(*args, **kwargs)
+class TlTrack(ImmutableObject):
+ """
+ A tracklist track. Wraps a regular track and it's tracklist ID.
+
+ The use of :class:`TlTrack` allows the same track to appear multiple times
+ in the tracklist.
+
+ This class also accepts it's parameters as positional arguments. Both
+ arguments must be provided, and they must appear in the order they are
+ listed here.
+
+ This class also supports iteration, so your extract its values like this::
+
+ (tlid, track) = tl_track
+
+ :param tlid: tracklist ID
+ :type tlid: int
+ :param track: the track
+ :type track: :class:`Track`
+ """
+
+ #: The tracklist ID. Read-only.
+ tlid = None
+
+ #: The track. Read-only.
+ track = None
+
+ def __init__(self, *args, **kwargs):
+ if len(args) == 2 and len(kwargs) == 0:
+ kwargs['tlid'] = args[0]
+ kwargs['track'] = args[1]
+ args = []
+ super(TlTrack, self).__init__(*args, **kwargs)
+
+ def __iter__(self):
+ return iter([self.tlid, self.track])
+
+
class Playlist(ImmutableObject):
"""
- :param uri: playlist URI
- :type uri: string
- :param name: playlist name
- :type name: string
- :param tracks: playlist's tracks
- :type tracks: list of :class:`Track` elements
- :param last_modified: playlist's modification time
- :type last_modified: :class:`datetime.datetime`
+ :param uri: playlist URI
+ :type uri: string
+ :param name: playlist name
+ :type name: string
+ :param tracks: playlist's tracks
+ :type tracks: list of :class:`Track` elements
+ :param last_modified: playlist's modification time
+ :type last_modified: :class:`datetime.datetime`
"""
#: The playlist URI. Read-only.
diff --git a/mopidy/scanner.py b/mopidy/scanner.py
index 29511c80..d84c262c 100644
--- a/mopidy/scanner.py
+++ b/mopidy/scanner.py
@@ -1,3 +1,8 @@
+from __future__ import unicode_literals
+
+import logging
+import datetime
+
import gobject
gobject.threads_init()
@@ -5,10 +10,41 @@ import pygst
pygst.require('0.10')
import gst
-import datetime
-
-from mopidy.utils.path import path_to_uri, find_files
+from mopidy import settings
+from mopidy.frontends.mpd import translator as mpd_translator
from mopidy.models import Track, Artist, Album
+from mopidy.utils import log, path
+
+
+def main():
+ log.setup_root_logger()
+ log.setup_console_logging(2)
+
+ tracks = []
+
+ def store(data):
+ track = translator(data)
+ tracks.append(track)
+ logging.debug('Added %s', track.uri)
+
+ def debug(uri, error, debug):
+ logging.error('Failed %s: %s - %s', uri, error, debug)
+
+ logging.info('Scanning %s', settings.LOCAL_MUSIC_PATH)
+ scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
+ try:
+ scanner.start()
+ except KeyboardInterrupt:
+ scanner.stop()
+
+ logging.info('Done')
+
+ for row in mpd_translator.tracks_to_tag_cache_format(tracks):
+ if len(row) == 1:
+ print ('%s' % row).encode('utf-8')
+ else:
+ print ('%s: %s' % row).encode('utf-8')
+
def translator(data):
albumartist_kwargs = {}
@@ -37,7 +73,8 @@ def translator(data):
_retrieve('musicbrainz-trackid', 'musicbrainz_id', track_kwargs)
_retrieve('musicbrainz-artistid', 'musicbrainz_id', artist_kwargs)
_retrieve('musicbrainz-albumid', 'musicbrainz_id', album_kwargs)
- _retrieve('musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs)
+ _retrieve(
+ 'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs)
if albumartist_kwargs:
album_kwargs['artists'] = [Artist(**albumartist_kwargs)]
@@ -52,51 +89,70 @@ def translator(data):
class Scanner(object):
def __init__(self, folder, data_callback, error_callback=None):
- self.files = find_files(folder)
+ self.data = {}
+ self.files = path.find_files(folder)
self.data_callback = data_callback
self.error_callback = error_callback
self.loop = gobject.MainLoop()
- fakesink = gst.element_factory_make('fakesink')
+ self.fakesink = gst.element_factory_make('fakesink')
+ self.fakesink.set_property('signal-handoffs', True)
+ self.fakesink.connect('handoff', self.process_handoff)
self.uribin = gst.element_factory_make('uridecodebin')
- self.uribin.set_property('caps', gst.Caps('audio/x-raw-int'))
- self.uribin.connect('pad-added', self.process_new_pad,
- fakesink.get_pad('sink'))
+ self.uribin.set_property('caps', gst.Caps(b'audio/x-raw-int'))
+ self.uribin.connect('pad-added', self.process_new_pad)
self.pipe = gst.element_factory_make('pipeline')
self.pipe.add(self.uribin)
- self.pipe.add(fakesink)
+ self.pipe.add(self.fakesink)
bus = self.pipe.get_bus()
bus.add_signal_watch()
+ bus.connect('message::application', self.process_application)
bus.connect('message::tag', self.process_tags)
bus.connect('message::error', self.process_error)
- def process_new_pad(self, source, pad, target_pad):
- pad.link(target_pad)
+ def process_handoff(self, fakesink, buffer_, pad):
+ # When this function is called the first buffer has reached the end of
+ # the pipeline, and we can continue with the next track. Since we're
+ # in another thread, we send a message back to the main thread using
+ # the bus.
+ structure = gst.Structure('handoff')
+ message = gst.message_new_application(fakesink, structure)
+ bus = self.pipe.get_bus()
+ bus.post(message)
+
+ def process_new_pad(self, source, pad):
+ pad.link(self.fakesink.get_pad('sink'))
+
+ def process_application(self, bus, message):
+ if message.src != self.fakesink:
+ return
+
+ if message.structure.get_name() != 'handoff':
+ return
+
+ self.data['uri'] = unicode(self.uribin.get_property('uri'))
+ self.data[gst.TAG_DURATION] = self.get_duration()
+
+ try:
+ self.data_callback(self.data)
+ self.next_uri()
+ except KeyboardInterrupt:
+ self.stop()
def process_tags(self, bus, message):
taglist = message.parse_tag()
- data = {
- 'uri': unicode(self.uribin.get_property('uri')),
- gst.TAG_DURATION: self.get_duration(),
- }
for key in taglist.keys():
# XXX: For some crazy reason some wma files spit out lists here,
# not sure if this is due to better data in headers or wma being
# stupid. So ugly hack for now :/
if type(taglist[key]) is list:
- data[key] = taglist[key][0]
+ self.data[key] = taglist[key][0]
else:
- data[key] = taglist[key]
-
- try:
- self.data_callback(data)
- self.next_uri()
- except KeyboardInterrupt:
- self.stop()
+ self.data[key] = taglist[key]
def process_error(self, bus, message):
if self.error_callback:
@@ -106,7 +162,7 @@ class Scanner(object):
self.next_uri()
def get_duration(self):
- self.pipe.get_state() # Block until state change is done.
+ self.pipe.get_state() # Block until state change is done.
try:
return self.pipe.query_duration(
gst.FORMAT_TIME, None)[0] // gst.MSECOND
@@ -114,14 +170,15 @@ class Scanner(object):
return None
def next_uri(self):
+ self.data = {}
try:
- uri = path_to_uri(self.files.next())
+ uri = path.path_to_uri(self.files.next())
except StopIteration:
self.stop()
return False
self.pipe.set_state(gst.STATE_NULL)
self.uribin.set_property('uri', uri)
- self.pipe.set_state(gst.STATE_PAUSED)
+ self.pipe.set_state(gst.STATE_PLAYING)
return True
def start(self):
diff --git a/mopidy/settings.py b/mopidy/settings.py
index 98f7e05e..2e022bc2 100644
--- a/mopidy/settings.py
+++ b/mopidy/settings.py
@@ -7,34 +7,36 @@ All available settings and their default values.
file called ``~/.config/mopidy/settings.py`` and redefine settings there.
"""
-#: List of playback backends to use. See :mod:`mopidy.backends` for all
+from __future__ import unicode_literals
+
+#: List of playback backends to use. See :ref:`backend-implementations` for all
#: available backends.
#:
+#: When results from multiple backends are combined, they are combined in the
+#: order the backends are listed here.
+#:
#: Default::
#:
-#: BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',)
-#:
-#: Other typical values::
-#:
-#: BACKENDS = (u'mopidy.backends.local.LocalBackend',)
-#:
-#: .. note::
-#: Currently only the first backend in the list is used.
+#: BACKENDS = (
+#: u'mopidy.backends.local.LocalBackend',
+#: u'mopidy.backends.spotify.SpotifyBackend',
+#: )
BACKENDS = (
- u'mopidy.backends.spotify.SpotifyBackend',
+ 'mopidy.backends.local.LocalBackend',
+ 'mopidy.backends.spotify.SpotifyBackend',
)
#: The log format used for informational logging.
#:
-#: See http://docs.python.org/library/logging.html#formatter-objects for
+#: See http://docs.python.org/2/library/logging.html#formatter-objects for
#: details on the format.
-CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
+CONSOLE_LOG_FORMAT = '%(levelname)-8s %(message)s'
#: The log format used for debug logging.
#:
#: See http://docs.python.org/library/logging.html#formatter-objects for
#: details on the format.
-DEBUG_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
+DEBUG_LOG_FORMAT = '%(levelname)-8s %(asctime)s' + \
' [%(process)d:%(threadName)s] %(name)s\n %(message)s'
#: The file to dump debug log data to when Mopidy is run with the
@@ -43,7 +45,15 @@ DEBUG_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
#: Default::
#:
#: DEBUG_LOG_FILENAME = u'mopidy.log'
-DEBUG_LOG_FILENAME = u'mopidy.log'
+DEBUG_LOG_FILENAME = 'mopidy.log'
+
+#: If we should start a background thread that dumps thread's traceback when we
+#: get a SIGUSR1. Mainly a debug tool for figuring out deadlocks.
+#:
+#: Default::
+#:
+#: DEBUG_THREAD = False
+DEBUG_THREAD = False
#: Location of the Mopidy .desktop file.
#:
@@ -52,9 +62,10 @@ DEBUG_LOG_FILENAME = u'mopidy.log'
#: Default::
#:
#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
-DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
+DESKTOP_FILE = '/usr/share/applications/mopidy.desktop'
-#: List of server frontends to use.
+#: List of server frontends to use. See :ref:`frontend-implementations` for
+#: available frontends.
#:
#: Default::
#:
@@ -64,20 +75,20 @@ DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
#: u'mopidy.frontends.mpris.MprisFrontend',
#: )
FRONTENDS = (
- u'mopidy.frontends.mpd.MpdFrontend',
- u'mopidy.frontends.lastfm.LastfmFrontend',
- u'mopidy.frontends.mpris.MprisFrontend',
+ 'mopidy.frontends.mpd.MpdFrontend',
+ 'mopidy.frontends.lastfm.LastfmFrontend',
+ 'mopidy.frontends.mpris.MprisFrontend',
)
#: Your `Last.fm `_ username.
#:
#: Used by :mod:`mopidy.frontends.lastfm`.
-LASTFM_USERNAME = u''
+LASTFM_USERNAME = ''
#: Your `Last.fm `_ password.
#:
#: Used by :mod:`mopidy.frontends.lastfm`.
-LASTFM_PASSWORD = u''
+LASTFM_PASSWORD = ''
#: Path to folder with local music.
#:
@@ -86,7 +97,7 @@ LASTFM_PASSWORD = u''
#: Default::
#:
#: LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR'
-LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR'
+LOCAL_MUSIC_PATH = '$XDG_MUSIC_DIR'
#: Path to playlist folder with m3u files for local music.
#:
@@ -95,7 +106,7 @@ LOCAL_MUSIC_PATH = u'$XDG_MUSIC_DIR'
#: Default::
#:
#: LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists'
-LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists'
+LOCAL_PLAYLIST_PATH = '$XDG_DATA_DIR/mopidy/playlists'
#: Path to tag cache for local music.
#:
@@ -104,9 +115,9 @@ LOCAL_PLAYLIST_PATH = u'$XDG_DATA_DIR/mopidy/playlists'
#: Default::
#:
#: LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache'
-LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache'
+LOCAL_TAG_CACHE_FILE = '$XDG_DATA_DIR/mopidy/tag_cache'
-#: Sound mixer to use.
+#: Audio mixer to use.
#:
#: Expects a GStreamer mixer to use, typical values are:
#: ``alsamixer``, ``pulsemixer``, ``ossmixer``, and ``oss4mixer``.
@@ -117,9 +128,9 @@ LOCAL_TAG_CACHE_FILE = u'$XDG_DATA_DIR/mopidy/tag_cache'
#: Default::
#:
#: MIXER = u'autoaudiomixer'
-MIXER = u'autoaudiomixer'
+MIXER = 'autoaudiomixer'
-#: Sound mixer track to use.
+#: Audio mixer track to use.
#:
#: Name of the mixer track to use. If this is not set we will try to find the
#: master output track. As an example, using ``alsamixer`` you would
@@ -144,7 +155,7 @@ MIXER_TRACK = None
#: Listens on all IPv4 interfaces.
#: ``::``
#: Listens on all interfaces, both IPv4 and IPv6.
-MPD_SERVER_HOSTNAME = u'127.0.0.1'
+MPD_SERVER_HOSTNAME = '127.0.0.1'
#: Which TCP port Mopidy's MPD server should listen to.
#:
@@ -167,12 +178,16 @@ MPD_SERVER_PASSWORD = None
#: Default: 20
MPD_SERVER_MAX_CONNECTIONS = 20
-#: Output to use. See :mod:`mopidy.outputs` for all available backends
+#: Audio output to use.
+#:
+#: Expects a GStreamer sink. Typical values are ``autoaudiosink``,
+#: ``alsasink``, ``osssink``, ``oss4sink``, ``pulsesink``, and ``shout2send``,
+#: and additional arguments specific to each sink.
#:
#: Default::
#:
#: OUTPUT = u'autoaudiosink'
-OUTPUT = u'autoaudiosink'
+OUTPUT = 'autoaudiosink'
#: Path to the Spotify cache.
#:
@@ -181,17 +196,17 @@ OUTPUT = u'autoaudiosink'
#: Default::
#:
#: SPOTIFY_CACHE_PATH = u'$XDG_CACHE_DIR/mopidy/spotify'
-SPOTIFY_CACHE_PATH = u'$XDG_CACHE_DIR/mopidy/spotify'
+SPOTIFY_CACHE_PATH = '$XDG_CACHE_DIR/mopidy/spotify'
#: Your Spotify Premium username.
#:
#: Used by :mod:`mopidy.backends.spotify`.
-SPOTIFY_USERNAME = u''
+SPOTIFY_USERNAME = ''
#: Your Spotify Premium password.
#:
#: Used by :mod:`mopidy.backends.spotify`.
-SPOTIFY_PASSWORD = u''
+SPOTIFY_PASSWORD = ''
#: Spotify preferred bitrate.
#:
@@ -203,3 +218,34 @@ SPOTIFY_PASSWORD = u''
#:
#: SPOTIFY_BITRATE = 160
SPOTIFY_BITRATE = 160
+
+#: Spotify proxy host.
+#:
+#: Used by :mod:`mopidy.backends.spotify`.
+#:
+#: Example::
+#:
+#: SPOTIFY_PROXY_HOST = u'protocol://host:port'
+#:
+#: Default::
+#:
+#: SPOTIFY_PROXY_HOST = None
+SPOTIFY_PROXY_HOST = None
+
+#: Spotify proxy username.
+#:
+#: Used by :mod:`mopidy.backends.spotify`.
+#:
+#: Default::
+#:
+#: SPOTIFY_PROXY_USERNAME = None
+SPOTIFY_PROXY_USERNAME = None
+
+#: Spotify proxy password.
+#:
+#: Used by :mod:`mopidy.backends.spotify`
+#:
+#: Default::
+#:
+#: SPOTIFY_PROXY_PASSWORD = None
+SPOTIFY_PROXY_PASSWORD = None
diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py
index 7d1a6dd6..baffc488 100644
--- a/mopidy/utils/__init__.py
+++ b/mopidy/utils/__init__.py
@@ -1,53 +1 @@
-from __future__ import division
-
-import locale
-import logging
-import os
-import sys
-
-logger = logging.getLogger('mopidy.utils')
-
-
-# TODO: use itertools.chain.from_iterable(the_list)?
-def flatten(the_list):
- result = []
- for element in the_list:
- if isinstance(element, list):
- result.extend(flatten(element))
- else:
- result.append(element)
- return result
-
-
-def rescale(v, old=None, new=None):
- """Convert value between scales."""
- new_min, new_max = new
- old_min, old_max = old
- scaling = float(new_max - new_min) / (old_max - old_min)
- return int(round(scaling * (v - old_min) + new_min))
-
-
-def import_module(name):
- __import__(name)
- return sys.modules[name]
-
-
-def get_class(name):
- logger.debug('Loading: %s', name)
- if '.' not in name:
- raise ImportError("Couldn't load: %s" % name)
- module_name = name[:name.rindex('.')]
- cls_name = name[name.rindex('.') + 1:]
- try:
- module = import_module(module_name)
- cls = getattr(module, cls_name)
- except (ImportError, AttributeError):
- raise ImportError("Couldn't load: %s" % name)
- return cls
-
-
-def locale_decode(bytestr):
- try:
- return unicode(bytestr)
- except UnicodeError:
- return str(bytestr).decode(locale.getpreferredencoding())
+from __future__ import unicode_literals
diff --git a/mopidy/utils/deps.py b/mopidy/utils/deps.py
index 2c68e429..3c177036 100644
--- a/mopidy/utils/deps.py
+++ b/mopidy/utils/deps.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import os
import platform
import sys
@@ -8,7 +10,7 @@ import gst
import pykka
-from mopidy.utils.log import indent
+from . import formatting
def list_deps_optparse_callback(*args):
@@ -47,7 +49,7 @@ def format_dependency_list(adapters=None):
os.path.dirname(dep_info['path'])))
if 'other' in dep_info:
lines.append(' Other: %s' % (
- indent(dep_info['other'])),)
+ formatting.indent(dep_info['other'])),)
return '\n'.join(lines)
@@ -61,8 +63,8 @@ def platform_info():
def python_info():
return {
'name': 'Python',
- 'version': '%s %s' % (platform.python_implementation(),
- platform.python_version()),
+ 'version': '%s %s' % (
+ platform.python_implementation(), platform.python_version()),
'path': platform.__file__,
}
@@ -125,21 +127,17 @@ def _gstreamer_check_elements():
# Shoutcast output
'shout2send',
]
- known_elements = [factory.get_name() for factory in
+ known_elements = [
+ factory.get_name() for factory in
gst.registry_get_default().get_feature_list(gst.TYPE_ELEMENT_FACTORY)]
- return [(element, element in known_elements) for element in elements_to_check]
+ return [
+ (element, element in known_elements) for element in elements_to_check]
def pykka_info():
- if hasattr(pykka, '__version__'):
- # Pykka >= 0.14
- version = pykka.__version__
- else:
- # Pykka < 0.14
- version = pykka.get_version()
return {
'name': 'Pykka',
- 'version': version,
+ 'version': pykka.__version__,
'path': pykka.__file__,
}
diff --git a/mopidy/utils/encoding.py b/mopidy/utils/encoding.py
new file mode 100644
index 00000000..a21b3384
--- /dev/null
+++ b/mopidy/utils/encoding.py
@@ -0,0 +1,10 @@
+from __future__ import unicode_literals
+
+import locale
+
+
+def locale_decode(bytestr):
+ try:
+ return unicode(bytestr)
+ except UnicodeError:
+ return str(bytestr).decode(locale.getpreferredencoding())
diff --git a/mopidy/utils/formatting.py b/mopidy/utils/formatting.py
new file mode 100644
index 00000000..ba311fb5
--- /dev/null
+++ b/mopidy/utils/formatting.py
@@ -0,0 +1,28 @@
+from __future__ import unicode_literals
+
+import re
+import unicodedata
+
+
+def indent(string, places=4, linebreak='\n'):
+ lines = string.split(linebreak)
+ if len(lines) == 1:
+ return string
+ result = ''
+ for line in lines:
+ result += linebreak + ' ' * places + line
+ return result
+
+
+def slugify(value):
+ """
+ Converts to lowercase, removes non-word characters (alphanumerics and
+ underscores) and converts spaces to hyphens. Also strips leading and
+ trailing whitespace.
+
+ This function is based on Django's slugify implementation.
+ """
+ value = unicodedata.normalize('NFKD', value)
+ value = value.encode('ascii', 'ignore').decode('ascii')
+ value = re.sub(r'[^\w\s-]', '', value).strip().lower()
+ return re.sub(r'[-\s]+', '-', value)
diff --git a/mopidy/utils/importing.py b/mopidy/utils/importing.py
new file mode 100644
index 00000000..591071a1
--- /dev/null
+++ b/mopidy/utils/importing.py
@@ -0,0 +1,26 @@
+from __future__ import unicode_literals
+
+import logging
+import sys
+
+
+logger = logging.getLogger('mopidy.utils')
+
+
+def import_module(name):
+ __import__(name)
+ return sys.modules[name]
+
+
+def get_class(name):
+ logger.debug('Loading: %s', name)
+ if '.' not in name:
+ raise ImportError("Couldn't load: %s" % name)
+ module_name = name[:name.rindex('.')]
+ cls_name = name[name.rindex('.') + 1:]
+ try:
+ module = import_module(module_name)
+ cls = getattr(module, cls_name)
+ except (ImportError, AttributeError):
+ raise ImportError("Couldn't load: %s" % name)
+ return cls
diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py
index 191efa2f..e503ff9f 100644
--- a/mopidy/utils/log.py
+++ b/mopidy/utils/log.py
@@ -1,22 +1,31 @@
+from __future__ import unicode_literals
+
import logging
import logging.handlers
-from mopidy import get_version, get_platform, get_python, settings
+from mopidy import settings
+from . import deps, versioning
+
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()
+ if hasattr(logging, 'captureWarnings'):
+ # New in Python 2.7
+ logging.captureWarnings(True)
logger = logging.getLogger('mopidy.utils.log')
- logger.info(u'Starting Mopidy %s', get_version())
- logger.info(u'Platform: %s', get_platform())
- logger.info(u'Python: %s', get_python())
+ logger.info('Starting Mopidy %s', versioning.get_version())
+ logger.info('%(name)s: %(version)s', deps.platform_info())
+ logger.info('%(name)s: %(version)s', deps.python_info())
+
def setup_root_logger():
root = logging.getLogger('')
root.setLevel(logging.DEBUG)
+
def setup_console_logging(verbosity_level):
if verbosity_level == 0:
log_level = logging.WARNING
@@ -37,6 +46,7 @@ def setup_console_logging(verbosity_level):
if verbosity_level < 3:
logging.getLogger('pykka').setLevel(logging.INFO)
+
def setup_debug_logging_to_file():
formatter = logging.Formatter(settings.DEBUG_LOG_FORMAT)
handler = logging.handlers.RotatingFileHandler(
@@ -45,12 +55,3 @@ def setup_debug_logging_to_file():
handler.setLevel(logging.DEBUG)
root = logging.getLogger('')
root.addHandler(handler)
-
-def indent(string, places=4, linebreak='\n'):
- lines = string.split(linebreak)
- if len(lines) == 1:
- return string
- result = u''
- for line in lines:
- result += linebreak + ' ' * places + line
- return result
diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py
index ed81684e..604350d1 100644
--- a/mopidy/utils/network.py
+++ b/mopidy/utils/network.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import errno
import gobject
import logging
@@ -5,17 +7,18 @@ import re
import socket
import threading
-from pykka import ActorDeadError
-from pykka.actor import ThreadingActor
-from pykka.registry import ActorRegistry
+import pykka
+
+from mopidy.utils import encoding
-from mopidy.utils import locale_decode
logger = logging.getLogger('mopidy.utils.server')
+
class ShouldRetrySocketCall(Exception):
"""Indicate that attempted socket call should be retried"""
+
def try_ipv6_socket():
"""Determine if system really supports IPv6"""
if not socket.has_ipv6:
@@ -24,13 +27,17 @@ def try_ipv6_socket():
socket.socket(socket.AF_INET6).close()
return True
except IOError as error:
- logger.debug(u'Platform supports IPv6, but socket '
- 'creation failed, disabling: %s', locale_decode(error))
+ logger.debug(
+ 'Platform supports IPv6, but socket creation failed, '
+ 'disabling: %s',
+ encoding.locale_decode(error))
return False
+
#: Boolean value that indicates if creating an IPv6 socket will succeed.
has_ipv6 = try_ipv6_socket()
+
def create_socket():
"""Create a TCP socket with or without IPv6 depending on system support"""
if has_ipv6:
@@ -42,17 +49,21 @@ def create_socket():
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return sock
+
def format_hostname(hostname):
"""Format hostname for display."""
- if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
+ if (has_ipv6 and re.match(r'\d+.\d+.\d+.\d+', hostname) is not None):
hostname = '::ffff:%s' % hostname
return hostname
+
class Server(object):
"""Setup listener and register it with gobject's event loop."""
- def __init__(self, host, port, protocol, max_connections=5, timeout=30):
+ def __init__(self, host, port, protocol, protocol_kwargs=None,
+ max_connections=5, timeout=30):
self.protocol = protocol
+ self.protocol_kwargs = protocol_kwargs or {}
self.max_connections = max_connections
self.timeout = timeout
self.server_socket = self.create_server_socket(host, port)
@@ -94,18 +105,19 @@ class Server(object):
self.number_of_connections() >= self.max_connections)
def number_of_connections(self):
- return len(ActorRegistry.get_by_class(self.protocol))
+ return len(pykka.ActorRegistry.get_by_class(self.protocol))
def reject_connection(self, sock, addr):
# FIXME provide more context in logging?
- logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1])
+ logger.warning('Rejected connection from [%s]:%s', addr[0], addr[1])
try:
sock.close()
except socket.error:
pass
def init_connection(self, sock, addr):
- Connection(self.protocol, sock, addr, self.timeout)
+ Connection(
+ self.protocol, self.protocol_kwargs, sock, addr, self.timeout)
class Connection(object):
@@ -117,17 +129,18 @@ class Connection(object):
# false return value would only tell us that what we thought was registered
# is already gone, there is really nothing more we can do.
- def __init__(self, protocol, sock, addr, timeout):
+ def __init__(self, protocol, protocol_kwargs, sock, addr, timeout):
sock.setblocking(False)
- self.host, self.port = addr[:2] # IPv6 has larger addr
+ self.host, self.port = addr[:2] # IPv6 has larger addr
self.sock = sock
self.protocol = protocol
+ self.protocol_kwargs = protocol_kwargs
self.timeout = timeout
self.send_lock = threading.Lock()
- self.send_buffer = ''
+ self.send_buffer = b''
self.stopping = False
@@ -135,7 +148,7 @@ class Connection(object):
self.send_id = None
self.timeout_id = None
- self.actor_ref = self.protocol.start(self)
+ self.actor_ref = self.protocol.start(self, **self.protocol_kwargs)
self.enable_recv()
self.enable_timeout()
@@ -151,7 +164,7 @@ class Connection(object):
try:
self.actor_ref.stop(block=False)
- except ActorDeadError:
+ except pykka.ActorDeadError:
pass
self.disable_timeout()
@@ -179,8 +192,8 @@ class Connection(object):
except socket.error as e:
if e.errno in (errno.EWOULDBLOCK, errno.EINTR):
return data
- self.stop(u'Unexpected client error: %s' % e)
- return ''
+ self.stop('Unexpected client error: %s' % e)
+ return b''
def enable_timeout(self):
"""Reactivate timeout mechanism."""
@@ -203,11 +216,12 @@ class Connection(object):
return
try:
- self.recv_id = gobject.io_add_watch(self.sock.fileno(),
+ self.recv_id = gobject.io_add_watch(
+ self.sock.fileno(),
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
self.recv_callback)
except socket.error as e:
- self.stop(u'Problem with connection: %s' % e)
+ self.stop('Problem with connection: %s' % e)
def disable_recv(self):
if self.recv_id is None:
@@ -220,11 +234,12 @@ class Connection(object):
return
try:
- self.send_id = gobject.io_add_watch(self.sock.fileno(),
+ self.send_id = gobject.io_add_watch(
+ self.sock.fileno(),
gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP,
self.send_callback)
except socket.error as e:
- self.stop(u'Problem with connection: %s' % e)
+ self.stop('Problem with connection: %s' % e)
def disable_send(self):
if self.send_id is None:
@@ -235,30 +250,30 @@ class Connection(object):
def recv_callback(self, fd, flags):
if flags & (gobject.IO_ERR | gobject.IO_HUP):
- self.stop(u'Bad client flags: %s' % flags)
+ self.stop('Bad client flags: %s' % flags)
return True
try:
data = self.sock.recv(4096)
except socket.error as e:
if e.errno not in (errno.EWOULDBLOCK, errno.EINTR):
- self.stop(u'Unexpected client error: %s' % e)
+ self.stop('Unexpected client error: %s' % e)
return True
if not data:
- self.stop(u'Client most likely disconnected.')
+ self.stop('Client most likely disconnected.')
return True
try:
self.actor_ref.tell({'received': data})
- except ActorDeadError:
- self.stop(u'Actor is dead.')
+ except pykka.ActorDeadError:
+ self.stop('Actor is dead.')
return True
def send_callback(self, fd, flags):
if flags & (gobject.IO_ERR | gobject.IO_HUP):
- self.stop(u'Bad client flags: %s' % flags)
+ self.stop('Bad client flags: %s' % flags)
return True
# If with can't get the lock, simply try again next time socket is
@@ -276,11 +291,11 @@ class Connection(object):
return True
def timeout_callback(self):
- self.stop(u'Client timeout out after %s seconds' % self.timeout)
+ self.stop('Client timeout out after %s seconds' % self.timeout)
return False
-class LineProtocol(ThreadingActor):
+class LineProtocol(pykka.ThreadingActor):
"""
Base class for handling line based protocols.
@@ -293,7 +308,7 @@ class LineProtocol(ThreadingActor):
#: Regex to use for spliting lines, will be set compiled version of its
#: own value, or to ``terminator``s value if it is not set itself.
- delimeter = None
+ delimiter = None
#: What encoding to expect incomming data to be in, can be :class:`None`.
encoding = 'utf-8'
@@ -302,12 +317,12 @@ class LineProtocol(ThreadingActor):
super(LineProtocol, self).__init__()
self.connection = connection
self.prevent_timeout = False
- self.recv_buffer = ''
+ self.recv_buffer = b''
- if self.delimeter:
- self.delimeter = re.compile(self.delimeter)
+ if self.delimiter:
+ self.delimiter = re.compile(self.delimiter)
else:
- self.delimeter = re.compile(self.terminator)
+ self.delimiter = re.compile(self.terminator)
@property
def host(self):
@@ -343,12 +358,12 @@ class LineProtocol(ThreadingActor):
def on_stop(self):
"""Ensure that cleanup when actor stops."""
- self.connection.stop(u'Actor is shutting down.')
+ self.connection.stop('Actor is shutting down.')
def parse_lines(self):
"""Consume new data and yield any lines found."""
while re.search(self.terminator, self.recv_buffer):
- line, self.recv_buffer = self.delimeter.split(
+ line, self.recv_buffer = self.delimiter.split(
self.recv_buffer, 1)
yield line
@@ -361,8 +376,10 @@ class LineProtocol(ThreadingActor):
try:
return line.encode(self.encoding)
except UnicodeError:
- logger.warning(u'Stopping actor due to encode problem, data '
- 'supplied by client was not valid %s', self.encoding)
+ logger.warning(
+ 'Stopping actor due to encode problem, data '
+ 'supplied by client was not valid %s',
+ self.encoding)
self.stop()
def decode(self, line):
@@ -374,13 +391,15 @@ class LineProtocol(ThreadingActor):
try:
return line.decode(self.encoding)
except UnicodeError:
- logger.warning(u'Stopping actor due to decode problem, data '
- 'supplied by client was not valid %s', self.encoding)
+ logger.warning(
+ 'Stopping actor due to decode problem, data '
+ 'supplied by client was not valid %s',
+ self.encoding)
self.stop()
def join_lines(self, lines):
if not lines:
- return u''
+ return ''
return self.terminator.join(lines) + self.terminator
def send_lines(self, lines):
diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py
index 7f1b9233..0c06eedd 100644
--- a/mopidy/utils/path.py
+++ b/mopidy/utils/path.py
@@ -1,13 +1,21 @@
-import glib
+from __future__ import unicode_literals
+
import logging
import os
import re
+# pylint: disable = W0402
import string
+# pylint: enable = W0402
import sys
import urllib
+import glib
+
logger = logging.getLogger('mopidy.utils.path')
+DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy')
+SETTINGS_PATH = os.path.join(str(glib.get_user_config_dir()), 'mopidy')
+SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
XDG_DIRS = {
'XDG_CACHE_DIR': glib.get_user_cache_dir(),
'XDG_DATA_DIR': glib.get_user_data_dir(),
@@ -18,10 +26,11 @@ XDG_DIRS = {
def get_or_create_folder(folder):
folder = os.path.expanduser(folder)
if os.path.isfile(folder):
- raise OSError('A file with the same name as the desired ' \
- 'dir, "%s", already exists.' % folder)
+ raise OSError(
+ 'A file with the same name as the desired dir, '
+ '"%s", already exists.' % folder)
elif not os.path.isdir(folder):
- logger.info(u'Creating dir %s', folder)
+ logger.info('Creating dir %s', folder)
os.makedirs(folder, 0755)
return folder
@@ -29,7 +38,7 @@ def get_or_create_folder(folder):
def get_or_create_file(filename):
filename = os.path.expanduser(filename)
if not os.path.isfile(filename):
- logger.info(u'Creating file %s', filename)
+ logger.info('Creating file %s', filename)
open(filename, 'w')
return filename
@@ -47,7 +56,7 @@ def uri_to_path(uri):
path = urllib.url2pathname(re.sub('^file:', '', uri))
else:
path = urllib.url2pathname(re.sub('^file://', '', uri))
- return path.encode('latin1').decode('utf-8') # Undo double encoding
+ return path.encode('latin1').decode('utf-8') # Undo double encoding
def split_path(path):
@@ -95,6 +104,25 @@ def find_files(path):
yield filename
+def check_file_path_is_inside_base_dir(file_path, base_path):
+ assert not file_path.endswith(os.sep), (
+ 'File path %s cannot end with a path separator' % file_path)
+
+ # Expand symlinks
+ real_base_path = os.path.realpath(base_path)
+ real_file_path = os.path.realpath(file_path)
+
+ # Use dir of file for prefix comparision, so we don't accept
+ # /tmp/foo.m3u as being inside /tmp/foo, simply because they have a
+ # common prefix, /tmp/foo, which matches the base path, /tmp/foo.
+ real_dir_path = os.path.dirname(real_file_path)
+
+ # Check if dir of file is the base path or a subdir
+ common_prefix = os.path.commonprefix([real_base_path, real_dir_path])
+ assert common_prefix == real_base_path, (
+ 'File path %s must be in %s' % (real_file_path, real_base_path))
+
+
# FIXME replace with mock usage in tests.
class Mtime(object):
def __init__(self):
diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py
index 80d850fe..5edf287e 100644
--- a/mopidy/utils/process.py
+++ b/mopidy/utils/process.py
@@ -1,45 +1,59 @@
+from __future__ import unicode_literals
+
import logging
import signal
+import sys
import thread
import threading
+import traceback
from pykka import ActorDeadError
from pykka.registry import ActorRegistry
-from mopidy import SettingsError
+from mopidy import exceptions
+
logger = logging.getLogger('mopidy.utils.process')
+
+SIGNALS = dict(
+ (k, v) for v, k in signal.__dict__.iteritems()
+ if v.startswith('SIG') and not v.startswith('SIG_'))
+
+
def exit_process():
- logger.debug(u'Interrupting main...')
+ logger.debug('Interrupting main...')
thread.interrupt_main()
- logger.debug(u'Interrupted main')
+ logger.debug('Interrupted main')
+
def exit_handler(signum, frame):
"""A :mod:`signal` handler which will exit the program on signal."""
- signals = dict((k, v) for v, k in signal.__dict__.iteritems()
- if v.startswith('SIG') and not v.startswith('SIG_'))
- logger.info(u'Got %s signal', signals[signum])
+ logger.info('Got %s signal', SIGNALS[signum])
exit_process()
+
def stop_actors_by_class(klass):
actors = ActorRegistry.get_by_class(klass)
- logger.debug(u'Stopping %d instance(s) of %s', len(actors), klass.__name__)
+ logger.debug('Stopping %d instance(s) of %s', len(actors), klass.__name__)
for actor in actors:
actor.stop()
+
def stop_remaining_actors():
num_actors = len(ActorRegistry.get_all())
while num_actors:
logger.error(
- u'There are actor threads still running, this is probably a bug')
- logger.debug(u'Seeing %d actor and %d non-actor thread(s): %s',
+ 'There are actor threads still running, this is probably a bug')
+ logger.debug(
+ 'Seeing %d actor and %d non-actor thread(s): %s',
num_actors, threading.active_count() - num_actors,
', '.join([t.name for t in threading.enumerate()]))
- logger.debug(u'Stopping %d actor(s)...', num_actors)
+ logger.debug('Stopping %d actor(s)...', num_actors)
ActorRegistry.stop_all()
num_actors = len(ActorRegistry.get_all())
- logger.debug(u'All actors stopped.')
+ logger.debug('All actors stopped.')
+
class BaseThread(threading.Thread):
def __init__(self):
@@ -48,12 +62,12 @@ class BaseThread(threading.Thread):
self.daemon = True
def run(self):
- logger.debug(u'%s: Starting thread', self.name)
+ logger.debug('%s: Starting thread', self.name)
try:
self.run_inside_try()
except KeyboardInterrupt:
- logger.info(u'Interrupted by user')
- except SettingsError as e:
+ logger.info('Interrupted by user')
+ except exceptions.SettingsError as e:
logger.error(e.message)
except ImportError as e:
logger.error(e)
@@ -61,7 +75,33 @@ class BaseThread(threading.Thread):
logger.warning(e)
except Exception as e:
logger.exception(e)
- logger.debug(u'%s: Exiting thread', self.name)
+ logger.debug('%s: Exiting thread', self.name)
def run_inside_try(self):
raise NotImplementedError
+
+
+class DebugThread(threading.Thread):
+ daemon = True
+ name = 'DebugThread'
+
+ event = threading.Event()
+
+ def handler(self, signum, frame):
+ logger.info('Got %s signal', SIGNALS[signum])
+ self.event.set()
+
+ def run(self):
+ while True:
+ self.event.wait()
+ threads = dict((t.ident, t.name) for t in threading.enumerate())
+
+ for ident, frame in sys._current_frames().items():
+ if self.ident != ident:
+ stack = ''.join(traceback.format_stack(frame))
+ logger.debug(
+ 'Current state of %s (%s):\n%s',
+ threads[ident], ident, stack)
+ del frame
+
+ self.event.clear()
diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py
index 5468b9bf..fee5252d 100644
--- a/mopidy/utils/settings.py
+++ b/mopidy/utils/settings.py
@@ -1,5 +1,6 @@
-# Absolute import needed to import ~/.mopidy/settings.py and not ourselves
-from __future__ import absolute_import
+# Absolute import needed to import ~/.config/mopidy/settings.py and not
+# ourselves
+from __future__ import absolute_import, unicode_literals
import copy
import getpass
@@ -8,9 +9,8 @@ import os
import pprint
import sys
-from mopidy import SettingsError, SETTINGS_PATH, SETTINGS_FILE
-from mopidy.utils import log
-from mopidy.utils import path
+from mopidy import exceptions
+from mopidy.utils import formatting, path
logger = logging.getLogger('mopidy.utils.settings')
@@ -23,16 +23,17 @@ class SettingsProxy(object):
self.runtime = {}
def _get_local_settings(self):
- if not os.path.isfile(SETTINGS_FILE):
+ if not os.path.isfile(path.SETTINGS_FILE):
return {}
- sys.path.insert(0, SETTINGS_PATH)
+ sys.path.insert(0, path.SETTINGS_PATH)
# pylint: disable = F0401
import settings as local_settings_module
# pylint: enable = F0401
return self._get_settings_dict_from_module(local_settings_module)
def _get_settings_dict_from_module(self, module):
- settings = filter(lambda (key, value): self._is_setting(key),
+ settings = filter(
+ lambda (key, value): self._is_setting(key),
module.__dict__.iteritems())
return dict(settings)
@@ -50,13 +51,13 @@ class SettingsProxy(object):
if not self._is_setting(attr):
return
- current = self.current # bind locally to avoid copying+updates
+ current = self.current # bind locally to avoid copying+updates
if attr not in current:
- raise SettingsError(u'Setting "%s" is not set.' % attr)
+ raise exceptions.SettingsError('Setting "%s" is not set.' % attr)
value = current[attr]
if isinstance(value, basestring) and len(value) == 0:
- raise SettingsError(u'Setting "%s" is empty.' % attr)
+ raise exceptions.SettingsError('Setting "%s" is empty.' % attr)
if not value:
return value
if attr.endswith('_PATH') or attr.endswith('_FILE'):
@@ -73,22 +74,25 @@ class SettingsProxy(object):
if interactive:
self._read_missing_settings_from_stdin(self.current, self.runtime)
if self.get_errors():
- logger.error(u'Settings validation errors: %s',
- log.indent(self.get_errors_as_string()))
- raise SettingsError(u'Settings validation failed.')
+ logger.error(
+ 'Settings validation errors: %s',
+ formatting.indent(self.get_errors_as_string()))
+ raise exceptions.SettingsError('Settings validation failed.')
def _read_missing_settings_from_stdin(self, current, runtime):
for setting, value in sorted(current.iteritems()):
if isinstance(value, basestring) and len(value) == 0:
- runtime[setting] = self._read_from_stdin(setting + u': ')
+ runtime[setting] = self._read_from_stdin(setting + ': ')
def _read_from_stdin(self, prompt):
- if u'_PASSWORD' in prompt:
- return (getpass.getpass(prompt)
+ if '_PASSWORD' in prompt:
+ return (
+ getpass.getpass(prompt)
.decode(sys.stdin.encoding, 'ignore'))
else:
sys.stdout.write(prompt)
- return (sys.stdin.readline().strip()
+ return (
+ sys.stdin.readline().strip()
.decode(sys.stdin.encoding, 'ignore'))
def get_errors(self):
@@ -97,7 +101,7 @@ class SettingsProxy(object):
def get_errors_as_string(self):
lines = []
for (setting, error) in self.get_errors().iteritems():
- lines.append(u'%s: %s' % (setting, error))
+ lines.append('%s: %s' % (setting, error))
return '\n'.join(lines)
@@ -117,7 +121,6 @@ def validate_settings(defaults, settings):
errors = {}
changed = {
- 'CUSTOM_OUTPUT': 'OUTPUT',
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
'FRONTEND': 'FRONTENDS',
@@ -139,43 +142,45 @@ def validate_settings(defaults, settings):
'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH',
}
+ list_of_one_or_more = [
+ 'BACKENDS',
+ 'FRONTENDS',
+ ]
+
for setting, value in settings.iteritems():
if setting in changed:
if changed[setting] is None:
- errors[setting] = u'Deprecated setting. It may be removed.'
+ errors[setting] = 'Deprecated setting. It may be removed.'
else:
- errors[setting] = u'Deprecated setting. Use %s.' % (
+ errors[setting] = 'Deprecated setting. Use %s.' % (
changed[setting],)
- elif setting == 'BACKENDS':
- if 'mopidy.backends.despotify.DespotifyBackend' in value:
- errors[setting] = (
- u'Deprecated setting value. '
- u'"mopidy.backends.despotify.DespotifyBackend" is no '
- u'longer available.')
-
elif setting == 'OUTPUTS':
errors[setting] = (
- u'Deprecated setting, please change to OUTPUT. OUTPUT expects '
- u'a GStreamer bin description string for your desired output.')
+ 'Deprecated setting, please change to OUTPUT. OUTPUT expects '
+ 'a GStreamer bin description string for your desired output.')
elif setting == 'SPOTIFY_BITRATE':
if value not in (96, 160, 320):
errors[setting] = (
- u'Unavailable Spotify bitrate. Available bitrates are 96, '
- u'160, and 320.')
+ 'Unavailable Spotify bitrate. Available bitrates are 96, '
+ '160, and 320.')
elif setting.startswith('SHOUTCAST_OUTPUT_'):
errors[setting] = (
- u'Deprecated setting, please set the value via the GStreamer '
- u'bin in OUTPUT.')
+ 'Deprecated setting, please set the value via the GStreamer '
+ 'bin in OUTPUT.')
- elif setting not in defaults:
- errors[setting] = u'Unknown setting.'
+ elif setting in list_of_one_or_more:
+ if not value:
+ errors[setting] = 'Must contain at least one value.'
+
+ elif setting not in defaults and not setting.startswith('CUSTOM_'):
+ errors[setting] = 'Unknown setting.'
suggestion = did_you_mean(setting, defaults)
if suggestion:
- errors[setting] += u' Did you mean %s?' % suggestion
+ errors[setting] += ' Did you mean %s?' % suggestion
return errors
@@ -198,19 +203,20 @@ def format_settings_list(settings):
for (key, value) in sorted(settings.current.iteritems()):
default_value = settings.default.get(key)
masked_value = mask_value_if_secret(key, value)
- lines.append(u'%s: %s' % (
- key, log.indent(pprint.pformat(masked_value), places=2)))
+ lines.append('%s: %s' % (
+ key, formatting.indent(pprint.pformat(masked_value), places=2)))
if value != default_value and default_value is not None:
- lines.append(u' Default: %s' %
- log.indent(pprint.pformat(default_value), places=4))
+ lines.append(
+ ' Default: %s' %
+ formatting.indent(pprint.pformat(default_value), places=4))
if errors.get(key) is not None:
- lines.append(u' Error: %s' % errors[key])
+ lines.append(' Error: %s' % errors[key])
return '\n'.join(lines)
def mask_value_if_secret(key, value):
if key.endswith('PASSWORD') and value:
- return u'********'
+ return '********'
else:
return value
@@ -229,19 +235,19 @@ def did_you_mean(setting, defaults):
return None
-def levenshtein(a, b, max=3):
+def levenshtein(a, b):
"""Calculates the Levenshtein distance between a and b."""
n, m = len(a), len(b)
if n > m:
return levenshtein(b, a)
- current = xrange(n+1)
- for i in xrange(1, m+1):
+ current = xrange(n + 1)
+ for i in xrange(1, m + 1):
previous, current = current, [i] + [0] * n
- for j in xrange(1, n+1):
- add, delete = previous[j] + 1, current[j-1] + 1
- change = previous[j-1]
- if a[j-1] != b[i-1]:
+ for j in xrange(1, n + 1):
+ add, delete = previous[j] + 1, current[j - 1] + 1
+ change = previous[j - 1]
+ if a[j - 1] != b[i - 1]:
change += 1
current[j] = min(add, delete, change)
return current[n]
diff --git a/mopidy/utils/versioning.py b/mopidy/utils/versioning.py
new file mode 100644
index 00000000..3ad72458
--- /dev/null
+++ b/mopidy/utils/versioning.py
@@ -0,0 +1,24 @@
+from __future__ import unicode_literals
+
+from subprocess import PIPE, Popen
+
+from mopidy import __version__
+
+
+def get_version():
+ try:
+ return get_git_version()
+ except EnvironmentError:
+ return __version__
+
+
+def get_git_version():
+ process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE)
+ # pylint: disable = E1101
+ if process.wait() != 0:
+ raise EnvironmentError('Execution of "git describe" failed')
+ version = process.stdout.read().strip()
+ # pylint: enable = E1101
+ if version.startswith('v'):
+ version = version[1:]
+ return version
diff --git a/pylintrc b/pylintrc
index 98e10416..41e1ab5d 100644
--- a/pylintrc
+++ b/pylintrc
@@ -5,20 +5,17 @@
#
# C0103 - Invalid name "%s" (should match %s)
# C0111 - Missing docstring
-# E0102 - %s already defined line %s
-# Does not understand @property getters and setters
-# E0202 - An attribute inherited from %s hide this method
-# Does not understand @property getters and setters
-# E1101 - %s %r has no %r member
-# Does not understand @property getters and setters
# R0201 - Method could be a function
# R0801 - Similar lines in %s files
+# R0902 - Too many instance attributes (%s/%s)
# R0903 - Too few public methods (%s/%s)
# R0904 - Too many public methods (%s/%s)
+# R0912 - Too many branches (%s/%s)
+# R0913 - Too many arguments (%s/%s)
# R0921 - Abstract class not referenced
# W0141 - Used builtin function '%s'
# W0142 - Used * or ** magic
# W0511 - TODO, FIXME and XXX in the code
# W0613 - Unused argument %r
#
-disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613
+disable = C0103,C0111,R0201,R0801,R0902,R0903,R0904,R0912,R0913,R0921,W0141,W0142,W0511,W0613
diff --git a/requirements/spotify.txt b/requirements/spotify.txt
new file mode 100644
index 00000000..c37d4674
--- /dev/null
+++ b/requirements/spotify.txt
@@ -0,0 +1 @@
+pyspotify >= 1.9, < 1.10
diff --git a/requirements/tests.txt b/requirements/tests.txt
index e24edd3c..20aff929 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -1,6 +1,8 @@
coverage
+flake8
mock >= 0.7
nose
+pylint
tox
unittest2
yappi
diff --git a/setup.cfg b/setup.cfg
index e09a7b15..bce0a6e2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,8 +1,6 @@
[nosetests]
verbosity = 1
-#with-doctest = 1
#with-coverage = 1
cover-package = mopidy
cover-inclusive = 1
cover-html = 1
-with-xunit = 1
diff --git a/setup.py b/setup.py
index ae6cc699..6135df31 100644
--- a/setup.py
+++ b/setup.py
@@ -2,6 +2,8 @@
Most of this file is taken from the Django project, which is BSD licensed.
"""
+from __future__ import unicode_literals
+
from distutils.core import setup
from distutils.command.install_data import install_data
from distutils.command.install import INSTALL_SCHEMES
@@ -9,11 +11,13 @@ import os
import re
import sys
+
def get_version():
init_py = open('mopidy/__init__.py').read()
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py))
return metadata['version']
+
class osx_install_data(install_data):
# On MacOS, the platform-specific lib dir is
# /System/Library/Framework/Python/.../ which is wrong. Python 2.5 supplied
@@ -28,11 +32,13 @@ class osx_install_data(install_data):
self.set_undefined_options('install', ('install_lib', 'install_dir'))
install_data.finalize_options(self)
+
if sys.platform == "darwin":
cmdclasses = {'install_data': osx_install_data}
else:
cmdclasses = {'install_data': install_data}
+
def fullsplit(path, result=None):
"""
Split a pathname into components (the opposite of os.path.join) in a
@@ -47,6 +53,7 @@ def fullsplit(path, result=None):
return result
return fullsplit(head, [tail] + result)
+
# Tell distutils to put the data_files in platform-specific installation
# locations. See here for an explanation:
# http://groups.google.com/group/comp.lang.python/browse_thread/
@@ -54,24 +61,26 @@ def fullsplit(path, result=None):
for scheme in INSTALL_SCHEMES.values():
scheme['data'] = scheme['purelib']
+
# Compile the list of packages available, because distutils doesn't have
# an easy way to do this.
packages, data_files = [], []
root_dir = os.path.dirname(__file__)
-if root_dir != '':
+if root_dir != b'':
os.chdir(root_dir)
-project_dir = 'mopidy'
+project_dir = b'mopidy'
for dirpath, dirnames, filenames in os.walk(project_dir):
# Ignore dirnames that start with '.'
for i, dirname in enumerate(dirnames):
- if dirname.startswith('.'):
+ if dirname.startswith(b'.'):
del dirnames[i]
- if '__init__.py' in filenames:
- packages.append('.'.join(fullsplit(dirpath)))
+ if b'__init__.py' in filenames:
+ packages.append(b'.'.join(fullsplit(dirpath)))
elif filenames:
- data_files.append([dirpath,
- [os.path.join(dirpath, f) for f in filenames]])
+ data_files.append([
+ dirpath, [os.path.join(dirpath, f) for f in filenames]])
+
setup(
name='Mopidy',
@@ -79,7 +88,7 @@ setup(
author='Stein Magnus Jodal',
author_email='stein.magnus@jodal.no',
packages=packages,
- package_data={'mopidy': ['backends/spotify/spotify_appkey.key']},
+ package_data={b'mopidy': ['backends/spotify/spotify_appkey.key']},
cmdclass=cmdclasses,
data_files=data_files,
scripts=['bin/mopidy', 'bin/mopidy-scan'],
diff --git a/tests/__init__.py b/tests/__init__.py
index 833ff239..7f7a9c36 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,10 +1,12 @@
+from __future__ import unicode_literals
+
import os
import sys
if sys.version_info < (2, 7):
import unittest2 as unittest
else:
- import unittest
+ import unittest # noqa
from mopidy import settings
diff --git a/tests/__main__.py b/tests/__main__.py
index 69113580..11757cbb 100644
--- a/tests/__main__.py
+++ b/tests/__main__.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import nose
import yappi
diff --git a/tests/audio/__init__.py b/tests/audio/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/audio/actor_test.py b/tests/audio/actor_test.py
new file mode 100644
index 00000000..64666d9d
--- /dev/null
+++ b/tests/audio/actor_test.py
@@ -0,0 +1,114 @@
+from __future__ import unicode_literals
+
+import pygst
+pygst.require('0.10')
+import gst
+
+from mopidy import audio, settings
+from mopidy.utils.path import path_to_uri
+
+from tests import unittest, path_to_data_dir
+
+
+class AudioTest(unittest.TestCase):
+ def setUp(self):
+ settings.MIXER = 'fakemixer track_max_volume=65536'
+ settings.OUTPUT = 'fakesink'
+ self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
+ self.audio = audio.Audio.start().proxy()
+
+ def tearDown(self):
+ self.audio.stop()
+ settings.runtime.clear()
+
+ def prepare_uri(self, uri):
+ self.audio.prepare_change()
+ self.audio.set_uri(uri)
+
+ def test_start_playback_existing_file(self):
+ self.prepare_uri(self.song_uri)
+ self.assertTrue(self.audio.start_playback().get())
+
+ def test_start_playback_non_existing_file(self):
+ self.prepare_uri(self.song_uri + 'bogus')
+ self.assertFalse(self.audio.start_playback().get())
+
+ def test_pause_playback_while_playing(self):
+ self.prepare_uri(self.song_uri)
+ self.audio.start_playback()
+ self.assertTrue(self.audio.pause_playback().get())
+
+ def test_stop_playback_while_playing(self):
+ self.prepare_uri(self.song_uri)
+ self.audio.start_playback()
+ self.assertTrue(self.audio.stop_playback().get())
+
+ @unittest.SkipTest
+ def test_deliver_data(self):
+ pass # TODO
+
+ @unittest.SkipTest
+ def test_end_of_data_stream(self):
+ pass # TODO
+
+ def test_set_volume(self):
+ for value in range(0, 101):
+ self.assertTrue(self.audio.set_volume(value).get())
+ self.assertEqual(value, self.audio.get_volume().get())
+
+ @unittest.SkipTest
+ def test_set_state_encapsulation(self):
+ pass # TODO
+
+ @unittest.SkipTest
+ def test_set_position(self):
+ pass # TODO
+
+ @unittest.SkipTest
+ def test_invalid_output_raises_error(self):
+ pass # TODO
+
+
+class AudioStateTest(unittest.TestCase):
+ def setUp(self):
+ self.audio = audio.Audio()
+
+ def test_state_starts_as_stopped(self):
+ self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
+
+ def test_state_does_not_change_when_in_gst_ready_state(self):
+ self.audio._on_playbin_state_changed(
+ gst.STATE_NULL, gst.STATE_READY, gst.STATE_VOID_PENDING)
+
+ self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
+
+ def test_state_changes_from_stopped_to_playing_on_play(self):
+ self.audio._on_playbin_state_changed(
+ gst.STATE_NULL, gst.STATE_READY, gst.STATE_PLAYING)
+ self.audio._on_playbin_state_changed(
+ gst.STATE_READY, gst.STATE_PAUSED, gst.STATE_PLAYING)
+ self.audio._on_playbin_state_changed(
+ gst.STATE_PAUSED, gst.STATE_PLAYING, gst.STATE_VOID_PENDING)
+
+ self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state)
+
+ def test_state_changes_from_playing_to_paused_on_pause(self):
+ self.audio.state = audio.PlaybackState.PLAYING
+
+ self.audio._on_playbin_state_changed(
+ gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_VOID_PENDING)
+
+ self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state)
+
+ def test_state_changes_from_playing_to_stopped_on_stop(self):
+ self.audio.state = audio.PlaybackState.PLAYING
+
+ self.audio._on_playbin_state_changed(
+ gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_NULL)
+ self.audio._on_playbin_state_changed(
+ gst.STATE_PAUSED, gst.STATE_READY, gst.STATE_NULL)
+ # We never get the following call, so the logic must work without it
+ #self.audio._on_playbin_state_changed(
+ # gst.STATE_READY, gst.STATE_NULL, gst.STATE_VOID_PENDING)
+
+ self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
diff --git a/tests/audio/listener_test.py b/tests/audio/listener_test.py
new file mode 100644
index 00000000..b3274721
--- /dev/null
+++ b/tests/audio/listener_test.py
@@ -0,0 +1,16 @@
+from __future__ import unicode_literals
+
+from mopidy import audio
+
+from tests import unittest
+
+
+class AudioListenerTest(unittest.TestCase):
+ def setUp(self):
+ self.listener = audio.AudioListener()
+
+ def test_listener_has_default_impl_for_reached_end_of_stream(self):
+ self.listener.reached_end_of_stream()
+
+ def test_listener_has_default_impl_for_state_changed(self):
+ self.listener.state_changed(None, None)
diff --git a/tests/audio_test.py b/tests/audio_test.py
deleted file mode 100644
index fcafa75f..00000000
--- a/tests/audio_test.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import sys
-
-from mopidy import audio, settings
-from mopidy.utils.path import path_to_uri
-
-from tests import unittest, path_to_data_dir
-
-
-@unittest.skipIf(sys.platform == 'win32',
- 'Our Windows build server does not support GStreamer yet')
-class AudioTest(unittest.TestCase):
- def setUp(self):
- settings.MIXER = 'fakemixer track_max_volume=65536'
- settings.OUTPUT = 'fakesink'
- self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
- self.audio = audio.Audio.start().proxy()
-
- def tearDown(self):
- self.audio.stop()
- settings.runtime.clear()
-
- def prepare_uri(self, uri):
- self.audio.prepare_change()
- self.audio.set_uri(uri)
-
- def test_start_playback_existing_file(self):
- self.prepare_uri(self.song_uri)
- self.assertTrue(self.audio.start_playback().get())
-
- def test_start_playback_non_existing_file(self):
- self.prepare_uri(self.song_uri + 'bogus')
- self.assertFalse(self.audio.start_playback().get())
-
- def test_pause_playback_while_playing(self):
- self.prepare_uri(self.song_uri)
- self.audio.start_playback()
- self.assertTrue(self.audio.pause_playback().get())
-
- def test_stop_playback_while_playing(self):
- self.prepare_uri(self.song_uri)
- self.audio.start_playback()
- self.assertTrue(self.audio.stop_playback().get())
-
- @unittest.SkipTest
- def test_deliver_data(self):
- pass # TODO
-
- @unittest.SkipTest
- def test_end_of_data_stream(self):
- pass # TODO
-
- def test_set_volume(self):
- for value in range(0, 101):
- self.assertTrue(self.audio.set_volume(value).get())
- self.assertEqual(value, self.audio.get_volume().get())
-
- @unittest.SkipTest
- def test_set_state_encapsulation(self):
- pass # TODO
-
- @unittest.SkipTest
- def test_set_position(self):
- pass # TODO
-
- @unittest.SkipTest
- def test_invalid_output_raises_error(self):
- pass # TODO
diff --git a/tests/backends/__init__.py b/tests/backends/__init__.py
index e69de29b..baffc488 100644
--- a/tests/backends/__init__.py
+++ b/tests/backends/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/tests/backends/base/__init__.py b/tests/backends/base/__init__.py
index 29f010e1..7dc4bcf6 100644
--- a/tests/backends/base/__init__.py
+++ b/tests/backends/base/__init__.py
@@ -1,7 +1,9 @@
-def populate_playlist(func):
+from __future__ import unicode_literals
+
+
+def populate_tracklist(func):
def wrapper(self):
- for track in self.tracks:
- self.backend.current_playlist.add(track)
+ self.tl_tracks = self.core.tracklist.add(self.tracks)
return func(self)
wrapper.__name__ = func.__name__
diff --git a/tests/backends/base/events.py b/tests/backends/base/events.py
new file mode 100644
index 00000000..0a2e6722
--- /dev/null
+++ b/tests/backends/base/events.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+
+import mock
+import pykka
+
+from mopidy import core, audio
+from mopidy.backends import listener
+
+
+@mock.patch.object(listener.BackendListener, 'send')
+class BackendEventsTest(object):
+ def setUp(self):
+ self.audio = mock.Mock(spec=audio.Audio)
+ self.backend = self.backend_class.start(audio=audio).proxy()
+ self.core = core.Core.start(backends=[self.backend]).proxy()
+
+ def tearDown(self):
+ pykka.ActorRegistry.stop_all()
+
+ def test_playlists_refresh_sends_playlists_loaded_event(self, send):
+ send.reset_mock()
+ self.core.playlists.refresh().get()
+ self.assertEqual(send.call_args[0][0], 'playlists_loaded')
diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py
index f76d9d75..4e9232e5 100644
--- a/tests/backends/base/library.py
+++ b/tests/backends/base/library.py
@@ -1,22 +1,35 @@
-from mopidy.models import Playlist, Track, Album, Artist
+from __future__ import unicode_literals
+
+import pykka
+
+from mopidy import core
+from mopidy.models import Track, Album, Artist
from tests import unittest, path_to_data_dir
class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
- albums = [Album(name='album1', artists=artists[:1]),
+ albums = [
+ Album(name='album1', artists=artists[:1]),
Album(name='album2', artists=artists[1:2]),
Album()]
- tracks = [Track(name='track1', length=4000, artists=artists[:1],
+ tracks = [
+ Track(
+ name='track1', length=4000, artists=artists[:1],
album=albums[0], uri='file://' + path_to_data_dir('uri1')),
- Track(name='track2', length=4000, artists=artists[1:2],
+ Track(
+ name='track2', length=4000, artists=artists[1:2],
album=albums[1], uri='file://' + path_to_data_dir('uri2')),
Track()]
def setUp(self):
- self.backend = self.backend_class()
- self.library = self.backend.library
+ self.backend = self.backend_class.start(audio=None).proxy()
+ self.core = core.Core(backends=[self.backend])
+ self.library = self.core.library
+
+ def tearDown(self):
+ pykka.ActorRegistry.stop_all()
def test_refresh(self):
self.library.refresh()
@@ -30,43 +43,52 @@ class LibraryControllerTest(object):
pass
def test_lookup(self):
- track = self.library.lookup(self.tracks[0].uri)
- self.assertEqual(track, self.tracks[0])
+ tracks = self.library.lookup(self.tracks[0].uri)
+ self.assertEqual(tracks, self.tracks[0:1])
def test_lookup_unknown_track(self):
- track = self.library.lookup('fake uri')
- self.assertEquals(track, None)
+ tracks = self.library.lookup('fake uri')
+ self.assertEqual(tracks, [])
def test_find_exact_no_hits(self):
result = self.library.find_exact(track=['unknown track'])
- self.assertEqual(result, Playlist())
+ self.assertEqual(result, [])
result = self.library.find_exact(artist=['unknown artist'])
- self.assertEqual(result, Playlist())
+ self.assertEqual(result, [])
result = self.library.find_exact(album=['unknown artist'])
- self.assertEqual(result, Playlist())
+ self.assertEqual(result, [])
def test_find_exact_artist(self):
result = self.library.find_exact(artist=['artist1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.find_exact(artist=['artist2'])
- self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
+ self.assertEqual(result, self.tracks[1:2])
def test_find_exact_track(self):
result = self.library.find_exact(track=['track1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.find_exact(track=['track2'])
- self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
+ self.assertEqual(result, self.tracks[1:2])
def test_find_exact_album(self):
result = self.library.find_exact(album=['album1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.find_exact(album=['album2'])
- self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
+ self.assertEqual(result, self.tracks[1:2])
+
+ def test_find_exact_uri(self):
+ track_1_uri = 'file://' + path_to_data_dir('uri1')
+ result = self.library.find_exact(uri=track_1_uri)
+ self.assertEqual(result, self.tracks[:1])
+
+ track_2_uri = 'file://' + path_to_data_dir('uri2')
+ result = self.library.find_exact(uri=track_2_uri)
+ self.assertEqual(result, self.tracks[1:2])
def test_find_exact_wrong_type(self):
test = lambda: self.library.find_exact(wrong=['test'])
@@ -84,57 +106,57 @@ class LibraryControllerTest(object):
def test_search_no_hits(self):
result = self.library.search(track=['unknown track'])
- self.assertEqual(result, Playlist())
+ self.assertEqual(result, [])
result = self.library.search(artist=['unknown artist'])
- self.assertEqual(result, Playlist())
+ self.assertEqual(result, [])
result = self.library.search(album=['unknown artist'])
- self.assertEqual(result, Playlist())
+ self.assertEqual(result, [])
result = self.library.search(uri=['unknown'])
- self.assertEqual(result, Playlist())
+ self.assertEqual(result, [])
result = self.library.search(any=['unknown'])
- self.assertEqual(result, Playlist())
+ self.assertEqual(result, [])
def test_search_artist(self):
result = self.library.search(artist=['Tist1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.search(artist=['Tist2'])
- self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
+ self.assertEqual(result, self.tracks[1:2])
def test_search_track(self):
result = self.library.search(track=['Rack1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.search(track=['Rack2'])
- self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
+ self.assertEqual(result, self.tracks[1:2])
def test_search_album(self):
result = self.library.search(album=['Bum1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.search(album=['Bum2'])
- self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
+ self.assertEqual(result, self.tracks[1:2])
def test_search_uri(self):
result = self.library.search(uri=['RI1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.search(uri=['RI2'])
- self.assertEqual(result, Playlist(tracks=self.tracks[1:2]))
+ self.assertEqual(result, self.tracks[1:2])
def test_search_any(self):
result = self.library.search(any=['Tist1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.search(any=['Rack1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.search(any=['Bum1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
result = self.library.search(any=['RI1'])
- self.assertEqual(result, Playlist(tracks=self.tracks[:1]))
+ self.assertEqual(result, self.tracks[:1])
def test_search_wrong_type(self):
test = lambda: self.library.search(wrong=['test'])
diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py
index 1e434e35..09dffbab 100644
--- a/tests/backends/base/playback.py
+++ b/tests/backends/base/playback.py
@@ -1,13 +1,15 @@
+from __future__ import unicode_literals
+
import mock
import random
import time
-from mopidy import audio
+from mopidy import audio, core
from mopidy.core import PlaybackState
from mopidy.models import Track
from tests import unittest
-from tests.backends.base import populate_playlist
+from tests.backends.base import populate_tracklist
# TODO Test 'playlist repeat', e.g. repeat=1,single=0
@@ -16,10 +18,11 @@ class PlaybackControllerTest(object):
tracks = []
def setUp(self):
- self.backend = self.backend_class()
- self.backend.audio = mock.Mock(spec=audio.Audio)
- self.playback = self.backend.playback
- self.current_playlist = self.backend.current_playlist
+ self.audio = mock.Mock(spec=audio.Audio)
+ self.backend = self.backend_class.start(audio=self.audio).proxy()
+ self.core = core.Core(backends=[self.backend])
+ self.playback = self.core.playback
+ self.tracklist = self.core.tracklist
assert len(self.tracks) >= 3, \
'Need at least three tracks to run tests.'
@@ -37,35 +40,35 @@ class PlaybackControllerTest(object):
def test_play_with_empty_playlist_return_value(self):
self.assertEqual(self.playback.play(), None)
- @populate_playlist
+ @populate_tracklist
def test_play_state(self):
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
- @populate_playlist
+ @populate_tracklist
def test_play_return_value(self):
self.assertEqual(self.playback.play(), None)
- @populate_playlist
+ @populate_tracklist
def test_play_track_state(self):
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- self.playback.play(self.current_playlist.cp_tracks[-1])
+ self.playback.play(self.tracklist.tl_tracks[-1])
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
- @populate_playlist
+ @populate_tracklist
def test_play_track_return_value(self):
self.assertEqual(self.playback.play(
- self.current_playlist.cp_tracks[-1]), None)
+ self.tracklist.tl_tracks[-1]), None)
- @populate_playlist
+ @populate_tracklist
def test_play_when_playing(self):
self.playback.play()
track = self.playback.current_track
self.playback.play()
self.assertEqual(track, self.playback.current_track)
- @populate_playlist
+ @populate_tracklist
def test_play_when_paused(self):
self.playback.play()
track = self.playback.current_track
@@ -74,7 +77,7 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(track, self.playback.current_track)
- @populate_playlist
+ @populate_tracklist
def test_play_when_pause_after_next(self):
self.playback.play()
self.playback.next()
@@ -85,58 +88,58 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(track, self.playback.current_track)
- @populate_playlist
+ @populate_tracklist
def test_play_sets_current_track(self):
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_play_track_sets_current_track(self):
- self.playback.play(self.current_playlist.cp_tracks[-1])
+ self.playback.play(self.tracklist.tl_tracks[-1])
self.assertEqual(self.playback.current_track, self.tracks[-1])
- @populate_playlist
+ @populate_tracklist
def test_play_skips_to_next_track_on_failure(self):
- # If provider.play() returns False, it is a failure.
- self.playback.provider.play = lambda track: track != self.tracks[0]
+ # If backend's play() returns False, it is a failure.
+ self.backend.playback.play = lambda track: track != self.tracks[0]
self.playback.play()
self.assertNotEqual(self.playback.current_track, self.tracks[0])
self.assertEqual(self.playback.current_track, self.tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_current_track_after_completed_playlist(self):
- self.playback.play(self.current_playlist.cp_tracks[-1])
+ self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
- self.playback.play(self.current_playlist.cp_tracks[-1])
+ self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.next()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
- @populate_playlist
+ @populate_tracklist
def test_previous(self):
self.playback.play()
self.playback.next()
self.playback.previous()
self.assertEqual(self.playback.current_track, self.tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_previous_more(self):
- self.playback.play() # At track 0
- self.playback.next() # At track 1
- self.playback.next() # At track 2
- self.playback.previous() # At track 1
+ self.playback.play() # At track 0
+ self.playback.next() # At track 1
+ self.playback.next() # At track 2
+ self.playback.previous() # At track 1
self.assertEqual(self.playback.current_track, self.tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_previous_return_value(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.previous(), None)
- @populate_playlist
+ @populate_tracklist
def test_previous_does_not_trigger_playback(self):
self.playback.play()
self.playback.next()
@@ -144,7 +147,7 @@ class PlaybackControllerTest(object):
self.playback.previous()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_previous_at_start_of_playlist(self):
self.playback.previous()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@@ -155,53 +158,53 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
- @populate_playlist
+ @populate_tracklist
def test_previous_skips_to_previous_track_on_failure(self):
- # If provider.play() returns False, it is a failure.
- self.playback.provider.play = lambda track: track != self.tracks[1]
- self.playback.play(self.current_playlist.cp_tracks[2])
+ # If backend's play() returns False, it is a failure.
+ self.backend.playback.play = lambda track: track != self.tracks[1]
+ self.playback.play(self.tracklist.tl_tracks[2])
self.assertEqual(self.playback.current_track, self.tracks[2])
self.playback.previous()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_next(self):
self.playback.play()
- old_position = self.playback.current_playlist_position
+ old_position = self.playback.tracklist_position
old_uri = self.playback.current_track.uri
self.playback.next()
- self.assertEqual(self.playback.current_playlist_position,
- old_position+1)
+ self.assertEqual(
+ self.playback.tracklist_position, old_position + 1)
self.assertNotEqual(self.playback.current_track.uri, old_uri)
- @populate_playlist
+ @populate_tracklist
def test_next_return_value(self):
self.playback.play()
self.assertEqual(self.playback.next(), None)
- @populate_playlist
+ @populate_tracklist
def test_next_does_not_trigger_playback(self):
self.playback.next()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_next_at_end_of_playlist(self):
self.playback.play()
for i, track in enumerate(self.tracks):
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track)
- self.assertEqual(self.playback.current_playlist_position, i)
+ self.assertEqual(self.playback.tracklist_position, i)
self.playback.next()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_next_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
@@ -219,64 +222,64 @@ class PlaybackControllerTest(object):
self.playback.next()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_next_skips_to_next_track_on_failure(self):
- # If provider.play() returns False, it is a failure.
- self.playback.provider.play = lambda track: track != self.tracks[1]
+ # If backend's play() returns False, it is a failure.
+ self.backend.playback.play = lambda track: track != self.tracks[1]
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
self.playback.next()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[2])
- @populate_playlist
+ @populate_tracklist
def test_next_track_before_play(self):
- self.assertEqual(self.playback.track_at_next, self.tracks[0])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_next_track_during_play(self):
self.playback.play()
- self.assertEqual(self.playback.track_at_next, self.tracks[1])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_next_track_after_previous(self):
self.playback.play()
self.playback.next()
self.playback.previous()
- self.assertEqual(self.playback.track_at_next, self.tracks[1])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
def test_next_track_empty_playlist(self):
- self.assertEqual(self.playback.track_at_next, None)
+ self.assertEqual(self.playback.tl_track_at_next, None)
- @populate_playlist
+ @populate_tracklist
def test_next_track_at_end_of_playlist(self):
self.playback.play()
- for _ in self.current_playlist.cp_tracks[1:]:
+ for _ in self.tracklist.tl_tracks[1:]:
self.playback.next()
- self.assertEqual(self.playback.track_at_next, None)
+ self.assertEqual(self.playback.tl_track_at_next, None)
- @populate_playlist
+ @populate_tracklist
def test_next_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
for _ in self.tracks[1:]:
self.playback.next()
- self.assertEqual(self.playback.track_at_next, self.tracks[0])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_next_track_with_random(self):
random.seed(1)
self.playback.random = True
- self.assertEqual(self.playback.track_at_next, self.tracks[2])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2])
- @populate_playlist
+ @populate_tracklist
def test_next_with_consume(self):
self.playback.consume = True
self.playback.play()
self.playback.next()
- self.assertIn(self.tracks[0], self.backend.current_playlist.tracks)
+ self.assertIn(self.tracks[0], self.tracklist.tracks)
- @populate_playlist
+ @populate_tracklist
def test_next_with_single_and_repeat(self):
self.playback.single = True
self.playback.repeat = True
@@ -284,7 +287,7 @@ class PlaybackControllerTest(object):
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_next_with_random(self):
# FIXME feels very fragile
random.seed(1)
@@ -293,51 +296,51 @@ class PlaybackControllerTest(object):
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_next_track_with_random_after_append_playlist(self):
random.seed(1)
self.playback.random = True
- self.assertEqual(self.playback.track_at_next, self.tracks[2])
- self.backend.current_playlist.append(self.tracks[:1])
- self.assertEqual(self.playback.track_at_next, self.tracks[1])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2])
+ self.tracklist.add(self.tracks[:1])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_end_of_track(self):
self.playback.play()
- old_position = self.playback.current_playlist_position
+ old_position = self.playback.tracklist_position
old_uri = self.playback.current_track.uri
self.playback.on_end_of_track()
- self.assertEqual(self.playback.current_playlist_position,
- old_position+1)
+ self.assertEqual(
+ self.playback.tracklist_position, old_position + 1)
self.assertNotEqual(self.playback.current_track.uri, old_uri)
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_return_value(self):
self.playback.play()
self.assertEqual(self.playback.on_end_of_track(), None)
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_does_not_trigger_playback(self):
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_at_end_of_playlist(self):
self.playback.play()
for i, track in enumerate(self.tracks):
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track)
- self.assertEqual(self.playback.current_playlist_position, i)
+ self.assertEqual(self.playback.tracklist_position, i)
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
@@ -355,65 +358,64 @@ class PlaybackControllerTest(object):
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_skips_to_next_track_on_failure(self):
- # If provider.play() returns False, it is a failure.
- self.playback.provider.play = lambda track: track != self.tracks[1]
+ # If backend's play() returns False, it is a failure.
+ self.backend.playback.play = lambda track: track != self.tracks[1]
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
self.playback.on_end_of_track()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[2])
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_track_before_play(self):
- self.assertEqual(self.playback.track_at_next, self.tracks[0])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_track_during_play(self):
self.playback.play()
- self.assertEqual(self.playback.track_at_next, self.tracks[1])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_track_after_previous(self):
self.playback.play()
self.playback.on_end_of_track()
self.playback.previous()
- self.assertEqual(self.playback.track_at_next, self.tracks[1])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
def test_end_of_track_track_empty_playlist(self):
- self.assertEqual(self.playback.track_at_next, None)
+ self.assertEqual(self.playback.tl_track_at_next, None)
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_track_at_end_of_playlist(self):
self.playback.play()
- for _ in self.current_playlist.cp_tracks[1:]:
+ for _ in self.tracklist.tl_tracks[1:]:
self.playback.on_end_of_track()
- self.assertEqual(self.playback.track_at_next, None)
+ self.assertEqual(self.playback.tl_track_at_next, None)
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
for _ in self.tracks[1:]:
self.playback.on_end_of_track()
- self.assertEqual(self.playback.track_at_next, self.tracks[0])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_track_with_random(self):
random.seed(1)
self.playback.random = True
- self.assertEqual(self.playback.track_at_next, self.tracks[2])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2])
-
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_with_consume(self):
self.playback.consume = True
self.playback.play()
self.playback.on_end_of_track()
- self.assertNotIn(self.tracks[0], self.backend.current_playlist.tracks)
+ self.assertNotIn(self.tracks[0], self.tracklist.tracks)
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_with_random(self):
# FIXME feels very fragile
random.seed(1)
@@ -422,107 +424,109 @@ class PlaybackControllerTest(object):
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_end_of_track_track_with_random_after_append_playlist(self):
random.seed(1)
self.playback.random = True
- self.assertEqual(self.playback.track_at_next, self.tracks[2])
- self.backend.current_playlist.append(self.tracks[:1])
- self.assertEqual(self.playback.track_at_next, self.tracks[1])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2])
+ self.tracklist.add(self.tracks[:1])
+ self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_previous_track_before_play(self):
- self.assertEqual(self.playback.track_at_previous, None)
+ self.assertEqual(self.playback.tl_track_at_previous, None)
- @populate_playlist
+ @populate_tracklist
def test_previous_track_after_play(self):
self.playback.play()
- self.assertEqual(self.playback.track_at_previous, None)
+ self.assertEqual(self.playback.tl_track_at_previous, None)
- @populate_playlist
+ @populate_tracklist
def test_previous_track_after_next(self):
self.playback.play()
self.playback.next()
- self.assertEqual(self.playback.track_at_previous, self.tracks[0])
+ self.assertEqual(self.playback.tl_track_at_previous, self.tl_tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_previous_track_after_previous(self):
- self.playback.play() # At track 0
- self.playback.next() # At track 1
- self.playback.next() # At track 2
- self.playback.previous() # At track 1
- self.assertEqual(self.playback.track_at_previous, self.tracks[0])
+ self.playback.play() # At track 0
+ self.playback.next() # At track 1
+ self.playback.next() # At track 2
+ self.playback.previous() # At track 1
+ self.assertEqual(self.playback.tl_track_at_previous, self.tl_tracks[0])
def test_previous_track_empty_playlist(self):
- self.assertEqual(self.playback.track_at_previous, None)
+ self.assertEqual(self.playback.tl_track_at_previous, None)
- @populate_playlist
+ @populate_tracklist
def test_previous_track_with_consume(self):
self.playback.consume = True
for _ in self.tracks:
self.playback.next()
- self.assertEqual(self.playback.track_at_previous,
- self.playback.current_track)
+ self.assertEqual(
+ self.playback.tl_track_at_previous,
+ self.playback.current_tl_track)
- @populate_playlist
+ @populate_tracklist
def test_previous_track_with_random(self):
self.playback.random = True
for _ in self.tracks:
self.playback.next()
- self.assertEqual(self.playback.track_at_previous,
- self.playback.current_track)
+ self.assertEqual(
+ self.playback.tl_track_at_previous,
+ self.playback.current_tl_track)
- @populate_playlist
+ @populate_tracklist
def test_initial_current_track(self):
self.assertEqual(self.playback.current_track, None)
- @populate_playlist
+ @populate_tracklist
def test_current_track_during_play(self):
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_current_track_after_next(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
- @populate_playlist
- def test_initial_current_playlist_position(self):
- self.assertEqual(self.playback.current_playlist_position, None)
+ @populate_tracklist
+ def test_initial_tracklist_position(self):
+ self.assertEqual(self.playback.tracklist_position, None)
- @populate_playlist
- def test_current_playlist_position_during_play(self):
+ @populate_tracklist
+ def test_tracklist_position_during_play(self):
self.playback.play()
- self.assertEqual(self.playback.current_playlist_position, 0)
+ self.assertEqual(self.playback.tracklist_position, 0)
- @populate_playlist
- def test_current_playlist_position_after_next(self):
+ @populate_tracklist
+ def test_tracklist_position_after_next(self):
self.playback.play()
self.playback.next()
- self.assertEqual(self.playback.current_playlist_position, 1)
+ self.assertEqual(self.playback.tracklist_position, 1)
- @populate_playlist
- def test_current_playlist_position_at_end_of_playlist(self):
- self.playback.play(self.current_playlist.cp_tracks[-1])
+ @populate_tracklist
+ def test_tracklist_position_at_end_of_playlist(self):
+ self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.on_end_of_track()
- self.assertEqual(self.playback.current_playlist_position, None)
+ self.assertEqual(self.playback.tracklist_position, None)
- def test_on_current_playlist_change_gets_called(self):
- callback = self.playback.on_current_playlist_change
+ def test_on_tracklist_change_gets_called(self):
+ callback = self.playback.on_tracklist_change
def wrapper():
wrapper.called = True
return callback()
wrapper.called = False
- self.playback.on_current_playlist_change = wrapper
- self.backend.current_playlist.append([Track()])
+ self.playback.on_tracklist_change = wrapper
+ self.tracklist.add([Track()])
self.assert_(wrapper.called)
- @unittest.SkipTest # Blocks for 10ms
- @populate_playlist
+ @unittest.SkipTest # Blocks for 10ms
+ @populate_tracklist
def test_end_of_track_callback_gets_called(self):
self.playback.play()
result = self.playback.seek(self.tracks[0].length - 10)
@@ -530,78 +534,78 @@ class PlaybackControllerTest(object):
message = self.core_queue.get(True, 1)
self.assertEqual('end_of_track', message['command'])
- @populate_playlist
- def test_on_current_playlist_change_when_playing(self):
+ @populate_tracklist
+ def test_on_tracklist_change_when_playing(self):
self.playback.play()
current_track = self.playback.current_track
- self.backend.current_playlist.append([self.tracks[2]])
+ self.tracklist.add([self.tracks[2]])
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, current_track)
- @populate_playlist
- def test_on_current_playlist_change_when_stopped(self):
- self.backend.current_playlist.append([self.tracks[2]])
+ @populate_tracklist
+ def test_on_tracklist_change_when_stopped(self):
+ self.tracklist.add([self.tracks[2]])
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
- @populate_playlist
- def test_on_current_playlist_change_when_paused(self):
+ @populate_tracklist
+ def test_on_tracklist_change_when_paused(self):
self.playback.play()
self.playback.pause()
current_track = self.playback.current_track
- self.backend.current_playlist.append([self.tracks[2]])
+ self.tracklist.add([self.tracks[2]])
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
self.assertEqual(self.playback.current_track, current_track)
- @populate_playlist
+ @populate_tracklist
def test_pause_when_stopped(self):
self.playback.pause()
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
- @populate_playlist
+ @populate_tracklist
def test_pause_when_playing(self):
self.playback.play()
self.playback.pause()
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
- @populate_playlist
+ @populate_tracklist
def test_pause_when_paused(self):
self.playback.play()
self.playback.pause()
self.playback.pause()
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
- @populate_playlist
+ @populate_tracklist
def test_pause_return_value(self):
self.playback.play()
self.assertEqual(self.playback.pause(), None)
- @populate_playlist
+ @populate_tracklist
def test_resume_when_stopped(self):
self.playback.resume()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_resume_when_playing(self):
self.playback.play()
self.playback.resume()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
- @populate_playlist
+ @populate_tracklist
def test_resume_when_paused(self):
self.playback.play()
self.playback.pause()
self.playback.resume()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
- @populate_playlist
+ @populate_tracklist
def test_resume_return_value(self):
self.playback.play()
self.playback.pause()
self.assertEqual(self.playback.resume(), None)
- @unittest.SkipTest # Uses sleep and might not work with LocalBackend
- @populate_playlist
+ @unittest.SkipTest # Uses sleep and might not work with LocalBackend
+ @populate_tracklist
def test_resume_continues_from_right_position(self):
self.playback.play()
time.sleep(0.2)
@@ -609,16 +613,16 @@ class PlaybackControllerTest(object):
self.playback.resume()
self.assertNotEqual(self.playback.time_position, 0)
- @populate_playlist
+ @populate_tracklist
def test_seek_when_stopped(self):
result = self.playback.seek(1000)
self.assert_(result, 'Seek return value was %s' % result)
- @populate_playlist
+ @populate_tracklist
def test_seek_when_stopped_updates_position(self):
self.playback.seek(1000)
position = self.playback.time_position
- self.assert_(position >= 990, position)
+ self.assertGreaterEqual(position, 990)
def test_seek_on_empty_playlist(self):
self.assertFalse(self.playback.seek(0))
@@ -627,42 +631,42 @@ class PlaybackControllerTest(object):
self.playback.seek(0)
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_seek_when_stopped_triggers_play(self):
self.playback.seek(0)
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
- @populate_playlist
+ @populate_tracklist
def test_seek_when_playing(self):
self.playback.play()
result = self.playback.seek(self.tracks[0].length - 1000)
self.assert_(result, 'Seek return value was %s' % result)
- @populate_playlist
+ @populate_tracklist
def test_seek_when_playing_updates_position(self):
- length = self.backend.current_playlist.tracks[0].length
+ length = self.tracklist.tracks[0].length
self.playback.play()
self.playback.seek(length - 1000)
position = self.playback.time_position
- self.assert_(position >= length - 1010, position)
+ self.assertGreaterEqual(position, length - 1010)
- @populate_playlist
+ @populate_tracklist
def test_seek_when_paused(self):
self.playback.play()
self.playback.pause()
result = self.playback.seek(self.tracks[0].length - 1000)
self.assert_(result, 'Seek return value was %s' % result)
- @populate_playlist
+ @populate_tracklist
def test_seek_when_paused_updates_position(self):
- length = self.backend.current_playlist.tracks[0].length
+ length = self.tracklist.tracks[0].length
self.playback.play()
self.playback.pause()
self.playback.seek(length - 1000)
position = self.playback.time_position
- self.assert_(position >= length - 1010, position)
+ self.assertGreaterEqual(position, length - 1010)
- @populate_playlist
+ @populate_tracklist
def test_seek_when_paused_triggers_play(self):
self.playback.play()
self.playback.pause()
@@ -670,53 +674,53 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@unittest.SkipTest
- @populate_playlist
+ @populate_tracklist
def test_seek_beyond_end_of_song(self):
# FIXME need to decide return value
self.playback.play()
- result = self.playback.seek(self.tracks[0].length*100)
+ result = self.playback.seek(self.tracks[0].length * 100)
self.assert_(not result, 'Seek return value was %s' % result)
- @populate_playlist
+ @populate_tracklist
def test_seek_beyond_end_of_song_jumps_to_next_song(self):
self.playback.play()
- self.playback.seek(self.tracks[0].length*100)
+ self.playback.seek(self.tracks[0].length * 100)
self.assertEqual(self.playback.current_track, self.tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_seek_beyond_end_of_song_for_last_track(self):
- self.playback.play(self.current_playlist.cp_tracks[-1])
- self.playback.seek(self.current_playlist.tracks[-1].length * 100)
+ self.playback.play(self.tracklist.tl_tracks[-1])
+ self.playback.seek(self.tracklist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@unittest.SkipTest
- @populate_playlist
+ @populate_tracklist
def test_seek_beyond_start_of_song(self):
# FIXME need to decide return value
self.playback.play()
result = self.playback.seek(-1000)
self.assert_(not result, 'Seek return value was %s' % result)
- @populate_playlist
+ @populate_tracklist
def test_seek_beyond_start_of_song_update_postion(self):
self.playback.play()
self.playback.seek(-1000)
position = self.playback.time_position
- self.assert_(position >= 0, position)
+ self.assertGreaterEqual(position, 0)
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
- @populate_playlist
+ @populate_tracklist
def test_stop_when_stopped(self):
self.playback.stop()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_stop_when_playing(self):
self.playback.play()
self.playback.stop()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- @populate_playlist
+ @populate_tracklist
def test_stop_when_paused(self):
self.playback.play()
self.playback.pause()
@@ -730,29 +734,29 @@ class PlaybackControllerTest(object):
def test_time_position_when_stopped(self):
future = mock.Mock()
future.get = mock.Mock(return_value=0)
- self.backend.audio.get_position = mock.Mock(return_value=future)
+ self.audio.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0)
- @populate_playlist
+ @populate_tracklist
def test_time_position_when_stopped_with_playlist(self):
future = mock.Mock()
future.get = mock.Mock(return_value=0)
- self.backend.audio.get_position = mock.Mock(return_value=future)
+ self.audio.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0)
- @unittest.SkipTest # Uses sleep and does might not work with LocalBackend
- @populate_playlist
+ @unittest.SkipTest # Uses sleep and does might not work with LocalBackend
+ @populate_tracklist
def test_time_position_when_playing(self):
self.playback.play()
first = self.playback.time_position
time.sleep(1)
second = self.playback.time_position
- self.assert_(second > first, '%s - %s' % (first, second))
+ self.assertGreater(second, first)
- @unittest.SkipTest # Uses sleep
- @populate_playlist
+ @unittest.SkipTest # Uses sleep
+ @populate_tracklist
def test_time_position_when_paused(self):
self.playback.play()
time.sleep(0.2)
@@ -762,28 +766,28 @@ class PlaybackControllerTest(object):
second = self.playback.time_position
self.assertEqual(first, second)
- @populate_playlist
+ @populate_tracklist
def test_play_with_consume(self):
self.playback.consume = True
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
self.playback.consume = True
self.playback.play()
- for _ in range(len(self.backend.current_playlist.tracks)):
+ for _ in range(len(self.tracklist.tracks)):
self.playback.on_end_of_track()
- self.assertEqual(len(self.backend.current_playlist.tracks), 0)
+ self.assertEqual(len(self.tracklist.tracks), 0)
- @populate_playlist
+ @populate_tracklist
def test_play_with_random(self):
random.seed(1)
self.playback.random = True
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[2])
- @populate_playlist
+ @populate_tracklist
def test_previous_with_random(self):
random.seed(1)
self.playback.random = True
@@ -793,13 +797,13 @@ class PlaybackControllerTest(object):
self.playback.previous()
self.assertEqual(self.playback.current_track, current_track)
- @populate_playlist
+ @populate_tracklist
def test_end_of_song_starts_next_track(self):
self.playback.play()
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_end_of_song_with_single_and_repeat_starts_same(self):
self.playback.single = True
self.playback.repeat = True
@@ -807,9 +811,9 @@ class PlaybackControllerTest(object):
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_end_of_playlist_stops(self):
- self.playback.play(self.current_playlist.cp_tracks[-1])
+ self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@@ -822,34 +826,34 @@ class PlaybackControllerTest(object):
def test_consume_off_by_default(self):
self.assertEqual(self.playback.consume, False)
- @populate_playlist
+ @populate_tracklist
def test_random_until_end_of_playlist(self):
self.playback.random = True
self.playback.play()
for _ in self.tracks[1:]:
self.playback.next()
- self.assertEqual(self.playback.track_at_next, None)
+ self.assertEqual(self.playback.tl_track_at_next, None)
- @populate_playlist
+ @populate_tracklist
def test_random_until_end_of_playlist_and_play_from_start(self):
self.playback.repeat = True
for _ in self.tracks:
self.playback.next()
- self.assertNotEqual(self.playback.track_at_next, None)
+ self.assertNotEqual(self.playback.tl_track_at_next, None)
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
- @populate_playlist
+ @populate_tracklist
def test_random_until_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.random = True
self.playback.play()
for _ in self.tracks:
self.playback.next()
- self.assertNotEqual(self.playback.track_at_next, None)
+ self.assertNotEqual(self.playback.tl_track_at_next, None)
- @populate_playlist
+ @populate_tracklist
def test_played_track_during_random_not_played_again(self):
self.playback.random = True
self.playback.play()
@@ -859,7 +863,7 @@ class PlaybackControllerTest(object):
played.append(self.playback.current_track)
self.playback.next()
- @populate_playlist
+ @populate_tracklist
def test_playing_track_that_isnt_in_playlist(self):
test = lambda: self.playback.play((17, Track()))
self.assertRaises(AssertionError, test)
diff --git a/tests/backends/base/playlists.py b/tests/backends/base/playlists.py
new file mode 100644
index 00000000..c162e500
--- /dev/null
+++ b/tests/backends/base/playlists.py
@@ -0,0 +1,113 @@
+from __future__ import unicode_literals
+
+import os
+import shutil
+import tempfile
+
+import mock
+import pykka
+
+from mopidy import audio, core, settings
+from mopidy.models import Playlist
+
+from tests import unittest, path_to_data_dir
+
+
+class PlaylistsControllerTest(object):
+ def setUp(self):
+ settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
+ settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache')
+ settings.LOCAL_MUSIC_PATH = path_to_data_dir('')
+
+ self.audio = mock.Mock(spec=audio.Audio)
+ self.backend = self.backend_class.start(audio=self.audio).proxy()
+ self.core = core.Core(backends=[self.backend])
+
+ def tearDown(self):
+ pykka.ActorRegistry.stop_all()
+
+ if os.path.exists(settings.LOCAL_PLAYLIST_PATH):
+ shutil.rmtree(settings.LOCAL_PLAYLIST_PATH)
+
+ settings.runtime.clear()
+
+ def test_create_returns_playlist_with_name_set(self):
+ playlist = self.core.playlists.create('test')
+ self.assertEqual(playlist.name, 'test')
+
+ def test_create_returns_playlist_with_uri_set(self):
+ playlist = self.core.playlists.create('test')
+ self.assert_(playlist.uri)
+
+ def test_create_adds_playlist_to_playlists_collection(self):
+ playlist = self.core.playlists.create('test')
+ self.assert_(self.core.playlists.playlists)
+ self.assertIn(playlist, self.core.playlists.playlists)
+
+ def test_playlists_empty_to_start_with(self):
+ self.assert_(not self.core.playlists.playlists)
+
+ def test_delete_non_existant_playlist(self):
+ self.core.playlists.delete('file:///unknown/playlist')
+
+ def test_delete_playlist_removes_it_from_the_collection(self):
+ playlist = self.core.playlists.create('test')
+ self.assertIn(playlist, self.core.playlists.playlists)
+
+ self.core.playlists.delete(playlist.uri)
+
+ self.assertNotIn(playlist, self.core.playlists.playlists)
+
+ def test_filter_without_criteria(self):
+ self.assertEqual(
+ self.core.playlists.playlists, self.core.playlists.filter())
+
+ def test_filter_with_wrong_criteria(self):
+ self.assertEqual([], self.core.playlists.filter(name='foo'))
+
+ def test_filter_with_right_criteria(self):
+ playlist = self.core.playlists.create('test')
+ playlists = self.core.playlists.filter(name='test')
+ self.assertEqual([playlist], playlists)
+
+ def test_filter_by_name_returns_single_match(self):
+ playlist = Playlist(name='b')
+ self.backend.playlists.playlists = [Playlist(name='a'), playlist]
+ self.assertEqual([playlist], self.core.playlists.filter(name='b'))
+
+ def test_filter_by_name_returns_multiple_matches(self):
+ playlist = Playlist(name='b')
+ self.backend.playlists.playlists = [
+ playlist, Playlist(name='a'), Playlist(name='b')]
+ playlists = self.core.playlists.filter(name='b')
+ self.assertIn(playlist, playlists)
+ self.assertEqual(2, len(playlists))
+
+ def test_filter_by_name_returns_no_matches(self):
+ self.backend.playlists.playlists = [
+ Playlist(name='a'), Playlist(name='b')]
+ self.assertEqual([], self.core.playlists.filter(name='c'))
+
+ def test_lookup_finds_playlist_by_uri(self):
+ original_playlist = self.core.playlists.create('test')
+
+ looked_up_playlist = self.core.playlists.lookup(original_playlist.uri)
+
+ self.assertEqual(original_playlist, looked_up_playlist)
+
+ @unittest.SkipTest
+ def test_refresh(self):
+ pass
+
+ def test_save_replaces_existing_playlist_with_updated_playlist(self):
+ playlist1 = self.core.playlists.create('test1')
+ self.assertIn(playlist1, self.core.playlists.playlists)
+
+ playlist2 = playlist1.copy(name='test2')
+ playlist2 = self.core.playlists.save(playlist2)
+ self.assertNotIn(playlist1, self.core.playlists.playlists)
+ self.assertIn(playlist2, self.core.playlists.playlists)
+
+ @unittest.SkipTest
+ def test_playlist_with_unknown_track(self):
+ pass
diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py
deleted file mode 100644
index 1e575b9e..00000000
--- a/tests/backends/base/stored_playlists.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import os
-import shutil
-import tempfile
-
-from mopidy import settings
-from mopidy.models import Playlist
-
-from tests import unittest, path_to_data_dir
-
-
-class StoredPlaylistsControllerTest(object):
- def setUp(self):
- settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp()
- 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()
- self.stored = self.backend.stored_playlists
-
- def tearDown(self):
- if os.path.exists(settings.LOCAL_PLAYLIST_PATH):
- shutil.rmtree(settings.LOCAL_PLAYLIST_PATH)
-
- settings.runtime.clear()
-
- def test_create(self):
- playlist = self.stored.create('test')
- self.assertEqual(playlist.name, 'test')
-
- def test_create_in_playlists(self):
- playlist = self.stored.create('test')
- self.assert_(self.stored.playlists)
- self.assertIn(playlist, self.stored.playlists)
-
- def test_playlists_empty_to_start_with(self):
- self.assert_(not self.stored.playlists)
-
- def test_delete_non_existant_playlist(self):
- self.stored.delete(Playlist())
-
- def test_delete_playlist(self):
- playlist = self.stored.create('test')
- self.stored.delete(playlist)
- self.assert_(not self.stored.playlists)
-
- def test_get_without_criteria(self):
- test = self.stored.get
- self.assertRaises(LookupError, test)
-
- def test_get_with_wrong_cirteria(self):
- test = lambda: self.stored.get(name='foo')
- self.assertRaises(LookupError, test)
-
- def test_get_with_right_criteria(self):
- playlist1 = self.stored.create('test')
- playlist2 = self.stored.get(name='test')
- self.assertEqual(playlist1, playlist2)
-
- def test_get_by_name_returns_unique_match(self):
- playlist = Playlist(name='b')
- self.stored.playlists = [Playlist(name='a'), playlist]
- self.assertEqual(playlist, self.stored.get(name='b'))
-
- def test_get_by_name_returns_first_of_multiple_matches(self):
- playlist = Playlist(name='b')
- self.stored.playlists = [
- playlist, Playlist(name='a'), Playlist(name='b')]
- try:
- self.stored.get(name='b')
- self.fail(u'Should raise LookupError if multiple matches')
- except LookupError as e:
- self.assertEqual(u'"name=b" match multiple playlists', e[0])
-
- def test_get_by_name_raises_keyerror_if_no_match(self):
- self.stored.playlists = [Playlist(name='a'), Playlist(name='b')]
- try:
- self.stored.get(name='c')
- self.fail(u'Should raise LookupError if no match')
- except LookupError as e:
- self.assertEqual(u'"name=c" match no playlists', e[0])
-
- @unittest.SkipTest
- def test_lookup(self):
- pass
-
- @unittest.SkipTest
- def test_refresh(self):
- pass
-
- def test_rename(self):
- playlist = self.stored.create('test')
- self.stored.rename(playlist, 'test2')
- self.stored.get(name='test2')
-
- def test_rename_unknown_playlist(self):
- self.stored.rename(Playlist(), 'test2')
- test = lambda: self.stored.get(name='test2')
- self.assertRaises(LookupError, test)
-
- def test_save(self):
- # FIXME should we handle playlists without names?
- playlist = Playlist(name='test')
- self.stored.save(playlist)
- self.assertIn(playlist, self.stored.playlists)
-
- @unittest.SkipTest
- def test_playlist_with_unknown_track(self):
- pass
diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/tracklist.py
similarity index 51%
rename from tests/backends/base/current_playlist.py
rename to tests/backends/base/tracklist.py
index 430e4c40..71f44018 100644
--- a/tests/backends/base/current_playlist.py
+++ b/tests/backends/base/tracklist.py
@@ -1,66 +1,114 @@
+from __future__ import unicode_literals
+
import mock
import random
-from mopidy import audio
+import pykka
+
+from mopidy import audio, core
from mopidy.core import PlaybackState
-from mopidy.models import CpTrack, Playlist, Track
+from mopidy.models import TlTrack, Playlist, Track
-from tests.backends.base import populate_playlist
+from tests.backends.base import populate_tracklist
-class CurrentPlaylistControllerTest(object):
+class TracklistControllerTest(object):
tracks = []
def setUp(self):
- self.backend = self.backend_class()
- self.backend.audio = mock.Mock(spec=audio.Audio)
- self.controller = self.backend.current_playlist
- self.playback = self.backend.playback
+ self.audio = mock.Mock(spec=audio.Audio)
+ self.backend = self.backend_class.start(audio=self.audio).proxy()
+ self.core = core.Core(audio=audio, backends=[self.backend])
+ self.controller = self.core.tracklist
+ self.playback = self.core.playback
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
+ def tearDown(self):
+ pykka.ActorRegistry.stop_all()
+
def test_length(self):
- self.assertEqual(0, len(self.controller.cp_tracks))
+ self.assertEqual(0, len(self.controller.tl_tracks))
self.assertEqual(0, self.controller.length)
- self.controller.append(self.tracks)
- self.assertEqual(3, len(self.controller.cp_tracks))
+ self.controller.add(self.tracks)
+ self.assertEqual(3, len(self.controller.tl_tracks))
self.assertEqual(3, self.controller.length)
def test_add(self):
for track in self.tracks:
- cp_track = self.controller.add(track)
+ tl_tracks = self.controller.add([track])
self.assertEqual(track, self.controller.tracks[-1])
- self.assertEqual(cp_track, self.controller.cp_tracks[-1])
- self.assertEqual(track, cp_track.track)
+ self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1])
+ self.assertEqual(track, tl_tracks[0].track)
def test_add_at_position(self):
for track in self.tracks[:-1]:
- cp_track = self.controller.add(track, 0)
+ tl_tracks = self.controller.add([track], 0)
self.assertEqual(track, self.controller.tracks[0])
- self.assertEqual(cp_track, self.controller.cp_tracks[0])
- self.assertEqual(track, cp_track.track)
+ self.assertEqual(tl_tracks[0], self.controller.tl_tracks[0])
+ self.assertEqual(track, tl_tracks[0].track)
- @populate_playlist
+ @populate_tracklist
def test_add_at_position_outside_of_playlist(self):
- test = lambda: self.controller.add(self.tracks[0], len(self.tracks)+2)
- self.assertRaises(AssertionError, test)
+ for track in self.tracks:
+ tl_tracks = self.controller.add([track], len(self.tracks) + 2)
+ self.assertEqual(track, self.controller.tracks[-1])
+ self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1])
+ self.assertEqual(track, tl_tracks[0].track)
- @populate_playlist
- def test_get_by_cpid(self):
- cp_track = self.controller.cp_tracks[1]
- self.assertEqual(cp_track, self.controller.get(cpid=cp_track.cpid))
+ @populate_tracklist
+ def test_filter_by_tlid(self):
+ tl_track = self.controller.tl_tracks[1]
+ self.assertEqual(
+ [tl_track], self.controller.filter(tlid=tl_track.tlid))
- @populate_playlist
- def test_get_by_uri(self):
- cp_track = self.controller.cp_tracks[1]
- self.assertEqual(cp_track, self.controller.get(uri=cp_track.track.uri))
+ @populate_tracklist
+ def test_filter_by_uri(self):
+ tl_track = self.controller.tl_tracks[1]
+ self.assertEqual(
+ [tl_track], self.controller.filter(uri=tl_track.track.uri))
- @populate_playlist
- def test_get_by_uri_raises_error_for_invalid_uri(self):
- test = lambda: self.controller.get(uri='foobar')
- self.assertRaises(LookupError, test)
+ @populate_tracklist
+ def test_filter_by_uri_returns_nothing_for_invalid_uri(self):
+ self.assertEqual([], self.controller.filter(uri='foobar'))
- @populate_playlist
+ def test_filter_by_uri_returns_single_match(self):
+ track = Track(uri='a')
+ self.controller.add([Track(uri='z'), track, Track(uri='y')])
+ self.assertEqual(track, self.controller.filter(uri='a')[0].track)
+
+ def test_filter_by_uri_returns_multiple_matches(self):
+ track = Track(uri='a')
+ self.controller.add([Track(uri='z'), track, track])
+ tl_tracks = self.controller.filter(uri='a')
+ self.assertEqual(track, tl_tracks[0].track)
+ self.assertEqual(track, tl_tracks[1].track)
+
+ def test_filter_by_uri_returns_nothing_if_no_match(self):
+ self.controller.playlist = Playlist(
+ tracks=[Track(uri='z'), Track(uri='y')])
+ self.assertEqual([], self.controller.filter(uri='a'))
+
+ def test_filter_by_multiple_criteria_returns_elements_matching_all(self):
+ track1 = Track(uri='a', name='x')
+ track2 = Track(uri='b', name='x')
+ track3 = Track(uri='b', name='y')
+ self.controller.add([track1, track2, track3])
+ self.assertEqual(
+ track1, self.controller.filter(uri='a', name='x')[0].track)
+ self.assertEqual(
+ track2, self.controller.filter(uri='b', name='x')[0].track)
+ self.assertEqual(
+ track3, self.controller.filter(uri='b', name='y')[0].track)
+
+ def test_filter_by_criteria_that_is_not_present_in_all_elements(self):
+ track1 = Track()
+ track2 = Track(uri='b')
+ track3 = Track()
+ self.controller.add([track1, track2, track3])
+ self.assertEqual(track2, self.controller.filter(uri='b')[0].track)
+
+ @populate_tracklist
def test_clear(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
@@ -69,101 +117,65 @@ class CurrentPlaylistControllerTest(object):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
- @populate_playlist
+ @populate_tracklist
def test_clear_when_playing(self):
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.controller.clear()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
- def test_get_by_uri_returns_unique_match(self):
- track = Track(uri='a')
- self.controller.append([Track(uri='z'), track, Track(uri='y')])
- self.assertEqual(track, self.controller.get(uri='a')[1])
-
- def test_get_by_uri_raises_error_if_multiple_matches(self):
- track = Track(uri='a')
- self.controller.append([Track(uri='z'), track, track])
- try:
- self.controller.get(uri='a')
- self.fail(u'Should raise LookupError if multiple matches')
- except LookupError as e:
- self.assertEqual(u'"uri=a" match multiple tracks', e[0])
-
- def test_get_by_uri_raises_error_if_no_match(self):
- self.controller.playlist = Playlist(
- tracks=[Track(uri='z'), Track(uri='y')])
- try:
- self.controller.get(uri='a')
- self.fail(u'Should raise LookupError if no match')
- except LookupError as e:
- self.assertEqual(u'"uri=a" match no tracks', e[0])
-
- def test_get_by_multiple_criteria_returns_elements_matching_all(self):
- track1 = Track(uri='a', name='x')
- track2 = Track(uri='b', name='x')
- track3 = Track(uri='b', name='y')
- self.controller.append([track1, track2, track3])
- self.assertEqual(track1, self.controller.get(uri='a', name='x')[1])
- self.assertEqual(track2, self.controller.get(uri='b', name='x')[1])
- self.assertEqual(track3, self.controller.get(uri='b', name='y')[1])
-
- def test_get_by_criteria_that_is_not_present_in_all_elements(self):
- track1 = Track()
- track2 = Track(uri='b')
- track3 = Track()
- self.controller.append([track1, track2, track3])
- self.assertEqual(track2, self.controller.get(uri='b')[1])
-
- def test_append_appends_to_the_current_playlist(self):
- self.controller.append([Track(uri='a'), Track(uri='b')])
+ def test_add_appends_to_the_tracklist(self):
+ self.controller.add([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.controller.tracks), 2)
- self.controller.append([Track(uri='c'), Track(uri='d')])
+ self.controller.add([Track(uri='c'), Track(uri='d')])
self.assertEqual(len(self.controller.tracks), 4)
self.assertEqual(self.controller.tracks[0].uri, 'a')
self.assertEqual(self.controller.tracks[1].uri, 'b')
self.assertEqual(self.controller.tracks[2].uri, 'c')
self.assertEqual(self.controller.tracks[3].uri, 'd')
- def test_append_does_not_reset_version(self):
+ def test_add_does_not_reset_version(self):
version = self.controller.version
- self.controller.append([])
+ self.controller.add([])
self.assertEqual(self.controller.version, version)
- @populate_playlist
- def test_append_preserves_playing_state(self):
+ @populate_tracklist
+ def test_add_preserves_playing_state(self):
self.playback.play()
track = self.playback.current_track
- self.controller.append(self.controller.tracks[1:2])
+ self.controller.add(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track)
- @populate_playlist
- def test_append_preserves_stopped_state(self):
- self.controller.append(self.controller.tracks[1:2])
+ @populate_tracklist
+ def test_add_preserves_stopped_state(self):
+ self.controller.add(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
+ @populate_tracklist
+ def test_add_returns_the_tl_tracks_that_was_added(self):
+ tl_tracks = self.controller.add(self.controller.tracks[1:2])
+ self.assertEqual(tl_tracks[0].track, self.controller.tracks[1])
+
def test_index_returns_index_of_track(self):
- cp_tracks = []
- for track in self.tracks:
- cp_tracks.append(self.controller.add(track))
- self.assertEquals(0, self.controller.index(cp_tracks[0]))
- self.assertEquals(1, self.controller.index(cp_tracks[1]))
- self.assertEquals(2, self.controller.index(cp_tracks[2]))
+ tl_tracks = self.controller.add(self.tracks)
+ self.assertEquals(0, self.controller.index(tl_tracks[0]))
+ self.assertEquals(1, self.controller.index(tl_tracks[1]))
+ self.assertEquals(2, self.controller.index(tl_tracks[2]))
def test_index_raises_value_error_if_item_not_found(self):
- test = lambda: self.controller.index(CpTrack(0, Track()))
+ test = lambda: self.controller.index(TlTrack(0, Track()))
self.assertRaises(ValueError, test)
- @populate_playlist
+ @populate_tracklist
def test_move_single(self):
self.controller.move(0, 0, 2)
tracks = self.controller.tracks
self.assertEqual(tracks[2], self.tracks[0])
- @populate_playlist
+ @populate_tracklist
def test_move_group(self):
self.controller.move(0, 2, 1)
@@ -171,25 +183,25 @@ class CurrentPlaylistControllerTest(object):
self.assertEqual(tracks[1], self.tracks[0])
self.assertEqual(tracks[2], self.tracks[1])
- @populate_playlist
+ @populate_tracklist
def test_moving_track_outside_of_playlist(self):
tracks = len(self.controller.tracks)
- test = lambda: self.controller.move(0, 0, tracks+5)
+ test = lambda: self.controller.move(0, 0, tracks + 5)
self.assertRaises(AssertionError, test)
- @populate_playlist
+ @populate_tracklist
def test_move_group_outside_of_playlist(self):
tracks = len(self.controller.tracks)
- test = lambda: self.controller.move(0, 2, tracks+5)
+ test = lambda: self.controller.move(0, 2, tracks + 5)
self.assertRaises(AssertionError, test)
- @populate_playlist
+ @populate_tracklist
def test_move_group_out_of_range(self):
tracks = len(self.controller.tracks)
- test = lambda: self.controller.move(tracks+2, tracks+3, 0)
+ test = lambda: self.controller.move(tracks + 2, tracks + 3, 0)
self.assertRaises(AssertionError, test)
- @populate_playlist
+ @populate_tracklist
def test_move_group_invalid_group(self):
test = lambda: self.controller.move(2, 1, 0)
self.assertRaises(AssertionError, test)
@@ -199,26 +211,24 @@ class CurrentPlaylistControllerTest(object):
tracks2 = self.controller.tracks
self.assertNotEqual(id(tracks1), id(tracks2))
- @populate_playlist
+ @populate_tracklist
def test_remove(self):
track1 = self.controller.tracks[1]
track2 = self.controller.tracks[2]
version = self.controller.version
self.controller.remove(uri=track1.uri)
- self.assert_(version < self.controller.version)
+ self.assertLess(version, self.controller.version)
self.assertNotIn(track1, self.controller.tracks)
self.assertEqual(track2, self.controller.tracks[1])
- @populate_playlist
- def test_removing_track_that_does_not_exist(self):
- test = lambda: self.controller.remove(uri='/nonexistant')
- self.assertRaises(LookupError, test)
+ @populate_tracklist
+ def test_removing_track_that_does_not_exist_does_nothing(self):
+ self.controller.remove(uri='/nonexistant')
- def test_removing_from_empty_playlist(self):
- test = lambda: self.controller.remove(uri='/nonexistant')
- self.assertRaises(LookupError, test)
+ def test_removing_from_empty_playlist_does_nothing(self):
+ self.controller.remove(uri='/nonexistant')
- @populate_playlist
+ @populate_tracklist
def test_shuffle(self):
random.seed(1)
self.controller.shuffle()
@@ -228,7 +238,7 @@ class CurrentPlaylistControllerTest(object):
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(set(self.tracks), set(shuffled_tracks))
- @populate_playlist
+ @populate_tracklist
def test_shuffle_subset(self):
random.seed(1)
self.controller.shuffle(1, 3)
@@ -239,18 +249,18 @@ class CurrentPlaylistControllerTest(object):
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
- @populate_playlist
+ @populate_tracklist
def test_shuffle_invalid_subset(self):
test = lambda: self.controller.shuffle(3, 1)
self.assertRaises(AssertionError, test)
- @populate_playlist
+ @populate_tracklist
def test_shuffle_superset(self):
tracks = len(self.controller.tracks)
- test = lambda: self.controller.shuffle(1, tracks+5)
+ test = lambda: self.controller.shuffle(1, tracks + 5)
self.assertRaises(AssertionError, test)
- @populate_playlist
+ @populate_tracklist
def test_shuffle_open_subset(self):
random.seed(1)
self.controller.shuffle(1)
@@ -261,24 +271,24 @@ class CurrentPlaylistControllerTest(object):
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
- @populate_playlist
+ @populate_tracklist
def test_slice_returns_a_subset_of_tracks(self):
track_slice = self.controller.slice(1, 3)
self.assertEqual(2, len(track_slice))
self.assertEqual(self.tracks[1], track_slice[0].track)
self.assertEqual(self.tracks[2], track_slice[1].track)
- @populate_playlist
+ @populate_tracklist
def test_slice_returns_empty_list_if_indexes_outside_tracks_list(self):
self.assertEqual(0, len(self.controller.slice(7, 8)))
self.assertEqual(0, len(self.controller.slice(-1, 1)))
- def test_version_does_not_change_when_appending_nothing(self):
+ def test_version_does_not_change_when_adding_nothing(self):
version = self.controller.version
- self.controller.append([])
+ self.controller.add([])
self.assertEquals(version, self.controller.version)
- def test_version_increases_when_appending_something(self):
+ def test_version_increases_when_adding_something(self):
version = self.controller.version
- self.controller.append([Track()])
- self.assert_(version < self.controller.version)
+ self.controller.add([Track()])
+ self.assertLess(version, self.controller.version)
diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py
deleted file mode 100644
index d761676d..00000000
--- a/tests/backends/events_test.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import mock
-
-from pykka.registry import ActorRegistry
-
-from mopidy.backends.dummy import DummyBackend
-from mopidy.listeners import BackendListener
-from mopidy.models import Track
-
-from tests import unittest
-
-
-@mock.patch.object(BackendListener, 'send')
-class BackendEventsTest(unittest.TestCase):
- def setUp(self):
- self.backend = DummyBackend.start().proxy()
-
- def tearDown(self):
- ActorRegistry.stop_all()
-
- def test_pause_sends_track_playback_paused_event(self, send):
- self.backend.current_playlist.add(Track(uri='a'))
- self.backend.playback.play().get()
- send.reset_mock()
- self.backend.playback.pause().get()
- self.assertEqual(send.call_args[0][0], 'track_playback_paused')
-
- def test_resume_sends_track_playback_resumed(self, send):
- self.backend.current_playlist.add(Track(uri='a'))
- self.backend.playback.play()
- self.backend.playback.pause().get()
- send.reset_mock()
- self.backend.playback.resume().get()
- self.assertEqual(send.call_args[0][0], 'track_playback_resumed')
-
- def test_play_sends_track_playback_started_event(self, send):
- self.backend.current_playlist.add(Track(uri='a'))
- send.reset_mock()
- self.backend.playback.play().get()
- self.assertEqual(send.call_args[0][0], 'track_playback_started')
-
- def test_stop_sends_track_playback_ended_event(self, send):
- self.backend.current_playlist.add(Track(uri='a'))
- self.backend.playback.play().get()
- send.reset_mock()
- self.backend.playback.stop().get()
- self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended')
-
- def test_seek_sends_seeked_event(self, send):
- self.backend.current_playlist.add(Track(uri='a', length=40000))
- self.backend.playback.play().get()
- send.reset_mock()
- self.backend.playback.seek(1000).get()
- self.assertEqual(send.call_args[0][0], 'seeked')
diff --git a/tests/backends/listener_test.py b/tests/backends/listener_test.py
new file mode 100644
index 00000000..a4df513c
--- /dev/null
+++ b/tests/backends/listener_test.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+
+from mopidy.backends.listener import BackendListener
+
+from tests import unittest
+
+
+class CoreListenerTest(unittest.TestCase):
+ def setUp(self):
+ self.listener = BackendListener()
+
+ def test_listener_has_default_impl_for_playlists_loaded(self):
+ self.listener.playlists_loaded()
diff --git a/tests/backends/local/__init__.py b/tests/backends/local/__init__.py
index d2213297..684e12d8 100644
--- a/tests/backends/local/__init__.py
+++ b/tests/backends/local/__init__.py
@@ -1,6 +1,9 @@
+from __future__ import unicode_literals
+
from mopidy.utils.path import path_to_uri
from tests import path_to_data_dir
+
song = path_to_data_dir('song%s.wav')
generate_song = lambda i: path_to_uri(song % i)
diff --git a/tests/backends/local/current_playlist_test.py b/tests/backends/local/current_playlist_test.py
deleted file mode 100644
index a475a6fd..00000000
--- a/tests/backends/local/current_playlist_test.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import sys
-
-from mopidy import settings
-from mopidy.backends.local import LocalBackend
-from mopidy.models import Track
-
-from tests import unittest
-from tests.backends.base.current_playlist import CurrentPlaylistControllerTest
-from tests.backends.local import generate_song
-
-
-@unittest.skipIf(sys.platform == 'win32',
- 'Our Windows build server does not support GStreamer yet')
-class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest,
- unittest.TestCase):
-
- backend_class = LocalBackend
- tracks = [Track(uri=generate_song(i), length=4464)
- for i in range(1, 4)]
-
- def setUp(self):
- settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
- super(LocalCurrentPlaylistControllerTest, self).setUp()
-
- def tearDown(self):
- super(LocalCurrentPlaylistControllerTest, self).tearDown()
- settings.runtime.clear()
diff --git a/tests/backends/local/events_test.py b/tests/backends/local/events_test.py
new file mode 100644
index 00000000..ba61f97a
--- /dev/null
+++ b/tests/backends/local/events_test.py
@@ -0,0 +1,8 @@
+from mopidy.backends.local import LocalBackend
+
+from tests import unittest
+from tests.backends.base import events
+
+
+class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase):
+ backend_class = LocalBackend
diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py
index 046e747a..7324d85f 100644
--- a/tests/backends/local/library_test.py
+++ b/tests/backends/local/library_test.py
@@ -1,4 +1,4 @@
-import sys
+from __future__ import unicode_literals
from mopidy import settings
from mopidy.backends.local import LocalBackend
@@ -7,8 +7,6 @@ from tests import unittest, path_to_data_dir
from tests.backends.base.library import LibraryControllerTest
-@unittest.skipIf(sys.platform == 'win32',
- 'Our Windows build server does not support GStreamer yet')
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend
diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py
index c167fbcc..9731f70d 100644
--- a/tests/backends/local/playback_test.py
+++ b/tests/backends/local/playback_test.py
@@ -1,4 +1,4 @@
-import sys
+from __future__ import unicode_literals
from mopidy import settings
from mopidy.backends.local import LocalBackend
@@ -11,19 +11,14 @@ from tests.backends.base.playback import PlaybackControllerTest
from tests.backends.local import generate_song
-@unittest.skipIf(sys.platform == 'win32',
- 'Our Windows build server does not support GStreamer yet')
class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
backend_class = LocalBackend
- tracks = [Track(uri=generate_song(i), length=4464)
- for i in range(1, 4)]
+ tracks = [
+ Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
def setUp(self):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
-
super(LocalPlaybackControllerTest, self).setUp()
- # Two tests does not work at all when using the fake sink
- #self.backend.playback.use_fake_sink()
def tearDown(self):
super(LocalPlaybackControllerTest, self).tearDown()
@@ -32,10 +27,10 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
def add_track(self, path):
uri = path_to_uri(path_to_data_dir(path))
track = Track(uri=uri, length=4464)
- self.backend.current_playlist.add(track)
+ self.tracklist.add([track])
def test_uri_scheme(self):
- self.assertIn('file', self.backend.uri_schemes)
+ self.assertIn('file', self.core.uri_schemes)
def test_play_mp3(self):
self.add_track('blank.mp3')
diff --git a/tests/backends/local/playlists_test.py b/tests/backends/local/playlists_test.py
new file mode 100644
index 00000000..fcc39132
--- /dev/null
+++ b/tests/backends/local/playlists_test.py
@@ -0,0 +1,108 @@
+from __future__ import unicode_literals
+
+import os
+
+from mopidy import settings
+from mopidy.backends.local import LocalBackend
+from mopidy.models import Track
+from mopidy.utils.path import path_to_uri
+
+from tests import unittest, path_to_data_dir
+from tests.backends.base.playlists import (
+ PlaylistsControllerTest)
+from tests.backends.local import generate_song
+
+
+class LocalPlaylistsControllerTest(
+ PlaylistsControllerTest, unittest.TestCase):
+
+ backend_class = LocalBackend
+
+ def test_created_playlist_is_persisted(self):
+ path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
+ self.assertFalse(os.path.exists(path))
+
+ self.core.playlists.create('test')
+ self.assertTrue(os.path.exists(path))
+
+ def test_create_slugifies_playlist_name(self):
+ path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u')
+ self.assertFalse(os.path.exists(path))
+
+ playlist = self.core.playlists.create('test FOO baR')
+ self.assertEqual('test-foo-bar', playlist.name)
+ self.assertTrue(os.path.exists(path))
+
+ def test_create_slugifies_names_which_tries_to_change_directory(self):
+ path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test-foo-bar.m3u')
+ self.assertFalse(os.path.exists(path))
+
+ playlist = self.core.playlists.create('../../test FOO baR')
+ self.assertEqual('test-foo-bar', playlist.name)
+ self.assertTrue(os.path.exists(path))
+
+ def test_saved_playlist_is_persisted(self):
+ path1 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test1.m3u')
+ path2 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2-foo-bar.m3u')
+
+ playlist = self.core.playlists.create('test1')
+
+ self.assertTrue(os.path.exists(path1))
+ self.assertFalse(os.path.exists(path2))
+
+ playlist = playlist.copy(name='test2 FOO baR')
+ playlist = self.core.playlists.save(playlist)
+
+ self.assertEqual('test2-foo-bar', playlist.name)
+ self.assertFalse(os.path.exists(path1))
+ self.assertTrue(os.path.exists(path2))
+
+ def test_deleted_playlist_is_removed(self):
+ path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
+ self.assertFalse(os.path.exists(path))
+
+ playlist = self.core.playlists.create('test')
+ self.assertTrue(os.path.exists(path))
+
+ self.core.playlists.delete(playlist.uri)
+ self.assertFalse(os.path.exists(path))
+
+ def test_playlist_contents_is_written_to_disk(self):
+ track = Track(uri=generate_song(1))
+ track_path = track.uri[len('file://'):]
+ playlist = self.core.playlists.create('test')
+ playlist_path = playlist.uri[len('file://'):]
+ playlist = playlist.copy(tracks=[track])
+ playlist = self.core.playlists.save(playlist)
+
+ with open(playlist_path) as playlist_file:
+ contents = playlist_file.read()
+
+ self.assertEqual(track_path, contents.strip())
+
+ def test_playlists_are_loaded_at_startup(self):
+ playlist_path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
+
+ track = Track(uri=path_to_uri(path_to_data_dir('uri2')))
+ playlist = self.core.playlists.create('test')
+ playlist = playlist.copy(tracks=[track])
+ playlist = self.core.playlists.save(playlist)
+
+ backend = self.backend_class(audio=self.audio)
+
+ self.assert_(backend.playlists.playlists)
+ self.assertEqual(
+ path_to_uri(playlist_path),
+ backend.playlists.playlists[0].uri)
+ self.assertEqual(
+ playlist.name, backend.playlists.playlists[0].name)
+ self.assertEqual(
+ track.uri, backend.playlists.playlists[0].tracks[0].uri)
+
+ @unittest.SkipTest
+ def test_santitising_of_playlist_filenames(self):
+ pass
+
+ @unittest.SkipTest
+ def test_playlist_folder_is_createad(self):
+ pass
diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py
deleted file mode 100644
index 56be92c4..00000000
--- a/tests/backends/local/stored_playlists_test.py
+++ /dev/null
@@ -1,89 +0,0 @@
-import os
-import sys
-
-from mopidy import settings
-from mopidy.backends.local import LocalBackend
-from mopidy.models import Playlist, Track
-from mopidy.utils.path import path_to_uri
-
-from tests import unittest, path_to_data_dir
-from tests.backends.base.stored_playlists import (
- StoredPlaylistsControllerTest)
-from tests.backends.local import generate_song
-
-
-@unittest.skipIf(sys.platform == 'win32',
- 'Our Windows build server does not support GStreamer yet')
-class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
- unittest.TestCase):
-
- backend_class = LocalBackend
-
- def test_created_playlist_is_persisted(self):
- path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
- self.assert_(not os.path.exists(path))
- self.stored.create('test')
- self.assert_(os.path.exists(path))
-
- def test_saved_playlist_is_persisted(self):
- path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2.m3u')
- self.assert_(not os.path.exists(path))
- self.stored.save(Playlist(name='test2'))
- self.assert_(os.path.exists(path))
-
- def test_deleted_playlist_get_removed(self):
- playlist = self.stored.create('test')
- self.stored.delete(playlist)
- path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
- self.assert_(not os.path.exists(path))
-
- def test_renamed_playlist_gets_moved(self):
- playlist = self.stored.create('test')
- file1 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
- file2 = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test2.m3u')
- self.assert_(not os.path.exists(file2))
- self.stored.rename(playlist, 'test2')
- self.assert_(not os.path.exists(file1))
- self.assert_(os.path.exists(file2))
-
- def test_playlist_contents_get_written_to_disk(self):
- track = Track(uri=generate_song(1))
- uri = track.uri[len('file://'):]
- playlist = Playlist(tracks=[track], name='test')
- path = os.path.join(settings.LOCAL_PLAYLIST_PATH, 'test.m3u')
-
- self.stored.save(playlist)
-
- with open(path) as playlist_file:
- contents = playlist_file.read()
-
- self.assertEqual(uri, contents.strip())
-
- def test_playlists_are_loaded_at_startup(self):
- track = Track(uri=path_to_uri(path_to_data_dir('uri2')))
- playlist = Playlist(tracks=[track], name='test')
-
- self.stored.save(playlist)
-
- self.backend = self.backend_class()
- self.stored = self.backend.stored_playlists
-
- self.assert_(self.stored.playlists)
- self.assertEqual('test', self.stored.playlists[0].name)
- self.assertEqual(track.uri, self.stored.playlists[0].tracks[0].uri)
-
- @unittest.SkipTest
- def test_santitising_of_playlist_filenames(self):
- pass
-
- @unittest.SkipTest
- def test_playlist_folder_is_createad(self):
- pass
-
- @unittest.SkipTest
- def test_create_sets_playlist_uri(self):
- pass
-
- @unittest.SkipTest
- def test_save_sets_playlist_uri(self):
- pass
diff --git a/tests/backends/local/tracklist_test.py b/tests/backends/local/tracklist_test.py
new file mode 100644
index 00000000..f5330f52
--- /dev/null
+++ b/tests/backends/local/tracklist_test.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+
+from mopidy import settings
+from mopidy.backends.local import LocalBackend
+from mopidy.models import Track
+
+from tests import unittest
+from tests.backends.base.tracklist import TracklistControllerTest
+from tests.backends.local import generate_song
+
+
+class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase):
+ backend_class = LocalBackend
+ tracks = [
+ Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
+
+ def setUp(self):
+ settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
+ super(LocalTracklistControllerTest, self).setUp()
+
+ def tearDown(self):
+ super(LocalTracklistControllerTest, self).tearDown()
+ settings.runtime.clear()
diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py
index 08f29c1b..e18b13fe 100644
--- a/tests/backends/local/translator_test.py
+++ b/tests/backends/local/translator_test.py
@@ -1,5 +1,7 @@
# encoding: utf-8
+from __future__ import unicode_literals
+
import os
import tempfile
@@ -12,7 +14,7 @@ from tests import unittest, path_to_data_dir
data_dir = path_to_data_dir('')
song1_path = path_to_data_dir('song1.mp3')
song2_path = path_to_data_dir('song2.mp3')
-encoded_path = path_to_data_dir(u'æøå.mp3')
+encoded_path = path_to_data_dir('æøå.mp3')
song1_uri = path_to_uri(song1_path)
song2_uri = path_to_uri(song2_path)
encoded_uri = path_to_uri(encoded_path)
@@ -55,7 +57,7 @@ class M3UToUriTest(unittest.TestCase):
def test_file_with_multiple_absolute_files(self):
with tempfile.NamedTemporaryFile(delete=False) as tmp:
- tmp.write(song1_path+'\n')
+ tmp.write(song1_path + '\n')
tmp.write('# comment \n')
tmp.write(song2_path)
try:
@@ -87,17 +89,21 @@ class M3UToUriTest(unittest.TestCase):
class URItoM3UTest(unittest.TestCase):
pass
+
expected_artists = [Artist(name='name')]
-expected_albums = [Album(name='albumname', artists=expected_artists,
- num_tracks=2)]
+expected_albums = [
+ Album(name='albumname', artists=expected_artists, num_tracks=2)]
expected_tracks = []
+
def generate_track(path, ident):
uri = path_to_uri(path_to_data_dir(path))
- track = Track(name='trackname', artists=expected_artists, track_no=1,
+ track = Track(
+ name='trackname', artists=expected_artists, track_no=1,
album=expected_albums[0], length=4000, uri=uri)
expected_tracks.append(track)
+
generate_track('song1.mp3', 6)
generate_track('song2.mp3', 7)
generate_track('song3.mp3', 8)
@@ -108,34 +114,36 @@ generate_track('subdir2/song7.mp3', 5)
generate_track('subdir1/subsubdir/song8.mp3', 0)
generate_track('subdir1/subsubdir/song9.mp3', 1)
+
class MPDTagCacheToTracksTest(unittest.TestCase):
def test_emtpy_cache(self):
- tracks = parse_mpd_tag_cache(path_to_data_dir('empty_tag_cache'),
- path_to_data_dir(''))
+ 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(path_to_data_dir('simple_tag_cache'),
- path_to_data_dir(''))
+ 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,
+ 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(path_to_data_dir('advanced_tag_cache'),
- path_to_data_dir(''))
+ 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(path_to_data_dir('utf8_tag_cache'),
- path_to_data_dir(''))
+ tracks = parse_mpd_tag_cache(
+ path_to_data_dir('utf8_tag_cache'), path_to_data_dir(''))
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,
- album=album, length=4000)
+ artists = [Artist(name='æøå')]
+ album = Album(name='æøå', artists=artists)
+ track = Track(
+ uri=uri, name='æøå', artists=artists, album=album, length=4000)
self.assertEqual(track, list(tracks)[0])
@@ -145,32 +153,35 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
pass
def test_cache_with_blank_track_info(self):
- tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'),
- path_to_data_dir(''))
+ 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(path_to_data_dir('musicbrainz_tag_cache'),
- path_to_data_dir(''))
+ 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(
name='albumartistname',
musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897')
- album = expected_tracks[0].album.copy(artists=[albumartist],
+ album = expected_tracks[0].album.copy(
+ artists=[albumartist],
musicbrainz_id='cb5f1603-d314-4c9c-91e5-e295cfb125d2')
- track = expected_tracks[0].copy(artists=[artist], album=album,
+ track = expected_tracks[0].copy(
+ artists=[artist], album=album,
musicbrainz_id='90488461-8c1f-4a4e-826b-4c6dc70801f0')
self.assertEqual(track, list(tracks)[0])
def test_albumartist_tag_cache(self):
- tracks = parse_mpd_tag_cache(path_to_data_dir('albumartist_tag_cache'),
- path_to_data_dir(''))
+ 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,
+ track = Track(
+ name='trackname', artists=expected_artists, track_no=1,
album=album, length=4000, uri=uri)
self.assertEqual(track, list(tracks)[0])
diff --git a/tests/core/__init__.py b/tests/core/__init__.py
index e69de29b..baffc488 100644
--- a/tests/core/__init__.py
+++ b/tests/core/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/tests/core/actor_test.py b/tests/core/actor_test.py
new file mode 100644
index 00000000..d86b8702
--- /dev/null
+++ b/tests/core/actor_test.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+import mock
+import pykka
+
+from mopidy.core import Core
+
+from tests import unittest
+
+
+class CoreActorTest(unittest.TestCase):
+ def setUp(self):
+ self.backend1 = mock.Mock()
+ self.backend1.uri_schemes.get.return_value = ['dummy1']
+
+ self.backend2 = mock.Mock()
+ self.backend2.uri_schemes.get.return_value = ['dummy2']
+
+ self.core = Core(audio=None, backends=[self.backend1, self.backend2])
+
+ def tearDown(self):
+ pykka.ActorRegistry.stop_all()
+
+ def test_uri_schemes_has_uris_from_all_backends(self):
+ result = self.core.uri_schemes
+
+ self.assertIn('dummy1', result)
+ self.assertIn('dummy2', result)
+
+ def test_backends_with_colliding_uri_schemes_fails(self):
+ self.backend1.__class__.__name__ = b'B1'
+ self.backend2.__class__.__name__ = b'B2'
+ self.backend2.uri_schemes.get.return_value = ['dummy1', 'dummy2']
+ self.assertRaisesRegexp(
+ AssertionError,
+ 'Cannot add URI scheme dummy1 for B2, it is already handled by B1',
+ Core, audio=None, backends=[self.backend1, self.backend2])
diff --git a/tests/core/events_test.py b/tests/core/events_test.py
new file mode 100644
index 00000000..88f07de6
--- /dev/null
+++ b/tests/core/events_test.py
@@ -0,0 +1,119 @@
+from __future__ import unicode_literals
+
+import mock
+import pykka
+
+from mopidy import audio, core
+from mopidy.backends import dummy
+from mopidy.models import Track
+
+from tests import unittest
+
+
+@mock.patch.object(core.CoreListener, 'send')
+class BackendEventsTest(unittest.TestCase):
+ def setUp(self):
+ self.audio = mock.Mock(spec=audio.Audio)
+ self.backend = dummy.DummyBackend.start(audio=audio).proxy()
+ self.core = core.Core.start(backends=[self.backend]).proxy()
+
+ def tearDown(self):
+ pykka.ActorRegistry.stop_all()
+
+ def test_backends_playlists_loaded_forwards_event_to_frontends(self, send):
+ send.reset_mock()
+ self.core.playlists_loaded().get()
+ self.assertEqual(send.call_args[0][0], 'playlists_loaded')
+
+ def test_pause_sends_track_playback_paused_event(self, send):
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.play().get()
+ send.reset_mock()
+ self.core.playback.pause().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_paused')
+
+ def test_resume_sends_track_playback_resumed(self, send):
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.play()
+ self.core.playback.pause().get()
+ send.reset_mock()
+ self.core.playback.resume().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_resumed')
+
+ def test_play_sends_track_playback_started_event(self, send):
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ send.reset_mock()
+ self.core.playback.play().get()
+ self.assertEqual(send.call_args[0][0], 'track_playback_started')
+
+ def test_stop_sends_track_playback_ended_event(self, send):
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.play().get()
+ send.reset_mock()
+ self.core.playback.stop().get()
+ self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended')
+
+ def test_seek_sends_seeked_event(self, send):
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play().get()
+ send.reset_mock()
+ self.core.playback.seek(1000).get()
+ self.assertEqual(send.call_args[0][0], 'seeked')
+
+ def test_tracklist_add_sends_tracklist_changed_event(self, send):
+ send.reset_mock()
+ self.core.tracklist.add([Track(uri='dummy:a')]).get()
+ self.assertEqual(send.call_args[0][0], 'tracklist_changed')
+
+ def test_tracklist_clear_sends_tracklist_changed_event(self, send):
+ self.core.tracklist.add([Track(uri='dummy:a')]).get()
+ send.reset_mock()
+ self.core.tracklist.clear().get()
+ self.assertEqual(send.call_args[0][0], 'tracklist_changed')
+
+ def test_tracklist_move_sends_tracklist_changed_event(self, send):
+ self.core.tracklist.add(
+ [Track(uri='dummy:a'), Track(uri='dummy:b')]).get()
+ send.reset_mock()
+ self.core.tracklist.move(0, 1, 1).get()
+ self.assertEqual(send.call_args[0][0], 'tracklist_changed')
+
+ def test_tracklist_remove_sends_tracklist_changed_event(self, send):
+ self.core.tracklist.add([Track(uri='dummy:a')]).get()
+ send.reset_mock()
+ self.core.tracklist.remove(uri='dummy:a').get()
+ self.assertEqual(send.call_args[0][0], 'tracklist_changed')
+
+ def test_tracklist_shuffle_sends_tracklist_changed_event(self, send):
+ self.core.tracklist.add(
+ [Track(uri='dummy:a'), Track(uri='dummy:b')]).get()
+ send.reset_mock()
+ self.core.tracklist.shuffle().get()
+ self.assertEqual(send.call_args[0][0], 'tracklist_changed')
+
+ def test_playlists_refresh_sends_playlists_loaded_event(self, send):
+ send.reset_mock()
+ self.core.playlists.refresh().get()
+ self.assertEqual(send.call_args[0][0], 'playlists_loaded')
+
+ def test_playlists_refresh_uri_sends_playlists_loaded_event(self, send):
+ send.reset_mock()
+ self.core.playlists.refresh(uri_scheme='dummy').get()
+ self.assertEqual(send.call_args[0][0], 'playlists_loaded')
+
+ def test_playlists_create_sends_playlist_changed_event(self, send):
+ send.reset_mock()
+ self.core.playlists.create('foo').get()
+ self.assertEqual(send.call_args[0][0], 'playlist_changed')
+
+ @unittest.SkipTest
+ def test_playlists_delete_sends_playlist_deleted_event(self, send):
+ # TODO We should probably add a playlist_deleted event
+ pass
+
+ def test_playlists_save_sends_playlist_changed_event(self, send):
+ playlist = self.core.playlists.create('foo').get()
+ send.reset_mock()
+ playlist = playlist.copy(name='bar')
+ self.core.playlists.save(playlist).get()
+ self.assertEqual(send.call_args[0][0], 'playlist_changed')
diff --git a/tests/core/library_test.py b/tests/core/library_test.py
new file mode 100644
index 00000000..1bd481de
--- /dev/null
+++ b/tests/core/library_test.py
@@ -0,0 +1,103 @@
+from __future__ import unicode_literals
+
+import mock
+
+from mopidy.backends import base
+from mopidy.core import Core
+from mopidy.models import Track
+
+from tests import unittest
+
+
+class CoreLibraryTest(unittest.TestCase):
+ def setUp(self):
+ self.backend1 = mock.Mock()
+ self.backend1.uri_schemes.get.return_value = ['dummy1']
+ self.library1 = mock.Mock(spec=base.BaseLibraryProvider)
+ self.backend1.library = self.library1
+
+ self.backend2 = mock.Mock()
+ self.backend2.uri_schemes.get.return_value = ['dummy2']
+ self.library2 = mock.Mock(spec=base.BaseLibraryProvider)
+ self.backend2.library = self.library2
+
+ # A backend without the optional library provider
+ self.backend3 = mock.Mock()
+ self.backend3.uri_schemes.get.return_value = ['dummy3']
+ self.backend3.has_library().get.return_value = False
+
+ self.core = Core(audio=None, backends=[
+ self.backend1, self.backend2, self.backend3])
+
+ def test_lookup_selects_dummy1_backend(self):
+ self.core.library.lookup('dummy1:a')
+
+ self.library1.lookup.assert_called_once_with('dummy1:a')
+ self.assertFalse(self.library2.lookup.called)
+
+ def test_lookup_selects_dummy2_backend(self):
+ self.core.library.lookup('dummy2:a')
+
+ self.assertFalse(self.library1.lookup.called)
+ self.library2.lookup.assert_called_once_with('dummy2:a')
+
+ def test_lookup_returns_nothing_for_dummy3_track(self):
+ result = self.core.library.lookup('dummy3:a')
+
+ self.assertEqual(result, [])
+ self.assertFalse(self.library1.lookup.called)
+ self.assertFalse(self.library2.lookup.called)
+
+ def test_refresh_with_uri_selects_dummy1_backend(self):
+ self.core.library.refresh('dummy1:a')
+
+ self.library1.refresh.assert_called_once_with('dummy1:a')
+ self.assertFalse(self.library2.refresh.called)
+
+ def test_refresh_with_uri_selects_dummy2_backend(self):
+ self.core.library.refresh('dummy2:a')
+
+ self.assertFalse(self.library1.refresh.called)
+ self.library2.refresh.assert_called_once_with('dummy2:a')
+
+ def test_refresh_with_uri_fails_silently_for_dummy3_uri(self):
+ self.core.library.refresh('dummy3:a')
+
+ self.assertFalse(self.library1.refresh.called)
+ self.assertFalse(self.library2.refresh.called)
+
+ def test_refresh_without_uri_calls_all_backends(self):
+ self.core.library.refresh()
+
+ self.library1.refresh.assert_called_once_with(None)
+ self.library2.refresh.assert_called_once_with(None)
+
+ def test_find_exact_combines_results_from_all_backends(self):
+ track1 = Track(uri='dummy1:a')
+ track2 = Track(uri='dummy2:a')
+ self.library1.find_exact().get.return_value = [track1]
+ self.library1.find_exact.reset_mock()
+ self.library2.find_exact().get.return_value = [track2]
+ self.library2.find_exact.reset_mock()
+
+ result = self.core.library.find_exact(any=['a'])
+
+ self.assertIn(track1, result)
+ self.assertIn(track2, result)
+ self.library1.find_exact.assert_called_once_with(any=['a'])
+ self.library2.find_exact.assert_called_once_with(any=['a'])
+
+ def test_search_combines_results_from_all_backends(self):
+ track1 = Track(uri='dummy1:a')
+ track2 = Track(uri='dummy2:a')
+ self.library1.search().get.return_value = [track1]
+ self.library1.search.reset_mock()
+ self.library2.search().get.return_value = [track2]
+ self.library2.search.reset_mock()
+
+ result = self.core.library.search(any=['a'])
+
+ self.assertIn(track1, result)
+ self.assertIn(track2, result)
+ self.library1.search.assert_called_once_with(any=['a'])
+ self.library2.search.assert_called_once_with(any=['a'])
diff --git a/tests/listeners_test.py b/tests/core/listener_test.py
similarity index 61%
rename from tests/listeners_test.py
rename to tests/core/listener_test.py
index 486dcf9c..2e121796 100644
--- a/tests/listeners_test.py
+++ b/tests/core/listener_test.py
@@ -1,12 +1,14 @@
-from mopidy.listeners import BackendListener
-from mopidy.models import Track
+from __future__ import unicode_literals
+
+from mopidy.core import CoreListener, PlaybackState
+from mopidy.models import Playlist, Track
from tests import unittest
-class BackendListenerTest(unittest.TestCase):
+class CoreListenerTest(unittest.TestCase):
def setUp(self):
- self.listener = BackendListener()
+ self.listener = CoreListener()
def test_listener_has_default_impl_for_track_playback_paused(self):
self.listener.track_playback_paused(Track(), 0)
@@ -21,10 +23,17 @@ class BackendListenerTest(unittest.TestCase):
self.listener.track_playback_ended(Track(), 0)
def test_listener_has_default_impl_for_playback_state_changed(self):
- self.listener.playback_state_changed()
+ self.listener.playback_state_changed(
+ PlaybackState.STOPPED, PlaybackState.PLAYING)
+
+ def test_listener_has_default_impl_for_tracklist_changed(self):
+ self.listener.tracklist_changed()
+
+ def test_listener_has_default_impl_for_playlists_loaded(self):
+ self.listener.playlists_loaded()
def test_listener_has_default_impl_for_playlist_changed(self):
- self.listener.playlist_changed()
+ self.listener.playlist_changed(Playlist())
def test_listener_has_default_impl_for_options_changed(self):
self.listener.options_changed()
@@ -33,4 +42,4 @@ class BackendListenerTest(unittest.TestCase):
self.listener.volume_changed()
def test_listener_has_default_impl_for_seeked(self):
- self.listener.seeked()
+ self.listener.seeked(0)
diff --git a/tests/core/playback_test.py b/tests/core/playback_test.py
new file mode 100644
index 00000000..ffbca506
--- /dev/null
+++ b/tests/core/playback_test.py
@@ -0,0 +1,180 @@
+from __future__ import unicode_literals
+
+import mock
+
+from mopidy.backends import base
+from mopidy.core import Core, PlaybackState
+from mopidy.models import Track
+
+from tests import unittest
+
+
+class CorePlaybackTest(unittest.TestCase):
+ def setUp(self):
+ self.backend1 = mock.Mock()
+ self.backend1.uri_schemes.get.return_value = ['dummy1']
+ self.playback1 = mock.Mock(spec=base.BasePlaybackProvider)
+ self.backend1.playback = self.playback1
+
+ self.backend2 = mock.Mock()
+ self.backend2.uri_schemes.get.return_value = ['dummy2']
+ self.playback2 = mock.Mock(spec=base.BasePlaybackProvider)
+ self.backend2.playback = self.playback2
+
+ # A backend without the optional playback provider
+ self.backend3 = mock.Mock()
+ self.backend3.uri_schemes.get.return_value = ['dummy3']
+ self.backend3.has_playback().get.return_value = False
+
+ self.tracks = [
+ Track(uri='dummy1:a', length=40000),
+ Track(uri='dummy2:a', length=40000),
+ Track(uri='dummy3:a', length=40000), # Unplayable
+ Track(uri='dummy1:b', length=40000),
+ ]
+
+ self.core = Core(audio=None, backends=[
+ self.backend1, self.backend2, self.backend3])
+ self.core.tracklist.add(self.tracks)
+
+ self.tl_tracks = self.core.tracklist.tl_tracks
+ self.unplayable_tl_track = self.tl_tracks[2]
+
+ def test_play_selects_dummy1_backend(self):
+ self.core.playback.play(self.tl_tracks[0])
+
+ self.playback1.play.assert_called_once_with(self.tracks[0])
+ self.assertFalse(self.playback2.play.called)
+
+ def test_play_selects_dummy2_backend(self):
+ self.core.playback.play(self.tl_tracks[1])
+
+ self.assertFalse(self.playback1.play.called)
+ self.playback2.play.assert_called_once_with(self.tracks[1])
+
+ def test_play_skips_to_next_on_unplayable_track(self):
+ self.core.playback.play(self.unplayable_tl_track)
+
+ self.playback1.play.assert_called_once_with(self.tracks[3])
+ self.assertFalse(self.playback2.play.called)
+
+ self.assertEqual(
+ self.core.playback.current_tl_track, self.tl_tracks[3])
+
+ def test_pause_selects_dummy1_backend(self):
+ self.core.playback.play(self.tl_tracks[0])
+ self.core.playback.pause()
+
+ self.playback1.pause.assert_called_once_with()
+ self.assertFalse(self.playback2.pause.called)
+
+ def test_pause_selects_dummy2_backend(self):
+ self.core.playback.play(self.tl_tracks[1])
+ self.core.playback.pause()
+
+ self.assertFalse(self.playback1.pause.called)
+ self.playback2.pause.assert_called_once_with()
+
+ def test_pause_changes_state_even_if_track_is_unplayable(self):
+ self.core.playback.current_tl_track = self.unplayable_tl_track
+ self.core.playback.pause()
+
+ self.assertEqual(self.core.playback.state, PlaybackState.PAUSED)
+ self.assertFalse(self.playback1.pause.called)
+ self.assertFalse(self.playback2.pause.called)
+
+ def test_resume_selects_dummy1_backend(self):
+ self.core.playback.play(self.tl_tracks[0])
+ self.core.playback.pause()
+ self.core.playback.resume()
+
+ self.playback1.resume.assert_called_once_with()
+ self.assertFalse(self.playback2.resume.called)
+
+ def test_resume_selects_dummy2_backend(self):
+ self.core.playback.play(self.tl_tracks[1])
+ self.core.playback.pause()
+ self.core.playback.resume()
+
+ self.assertFalse(self.playback1.resume.called)
+ self.playback2.resume.assert_called_once_with()
+
+ def test_resume_does_nothing_if_track_is_unplayable(self):
+ self.core.playback.current_tl_track = self.unplayable_tl_track
+ self.core.playback.state = PlaybackState.PAUSED
+ self.core.playback.resume()
+
+ self.assertEqual(self.core.playback.state, PlaybackState.PAUSED)
+ self.assertFalse(self.playback1.resume.called)
+ self.assertFalse(self.playback2.resume.called)
+
+ def test_stop_selects_dummy1_backend(self):
+ self.core.playback.play(self.tl_tracks[0])
+ self.core.playback.stop()
+
+ self.playback1.stop.assert_called_once_with()
+ self.assertFalse(self.playback2.stop.called)
+
+ def test_stop_selects_dummy2_backend(self):
+ self.core.playback.play(self.tl_tracks[1])
+ self.core.playback.stop()
+
+ self.assertFalse(self.playback1.stop.called)
+ self.playback2.stop.assert_called_once_with()
+
+ def test_stop_changes_state_even_if_track_is_unplayable(self):
+ self.core.playback.current_tl_track = self.unplayable_tl_track
+ self.core.playback.state = PlaybackState.PAUSED
+ self.core.playback.stop()
+
+ self.assertEqual(self.core.playback.state, PlaybackState.STOPPED)
+ self.assertFalse(self.playback1.stop.called)
+ self.assertFalse(self.playback2.stop.called)
+
+ def test_seek_selects_dummy1_backend(self):
+ self.core.playback.play(self.tl_tracks[0])
+ self.core.playback.seek(10000)
+
+ self.playback1.seek.assert_called_once_with(10000)
+ self.assertFalse(self.playback2.seek.called)
+
+ def test_seek_selects_dummy2_backend(self):
+ self.core.playback.play(self.tl_tracks[1])
+ self.core.playback.seek(10000)
+
+ self.assertFalse(self.playback1.seek.called)
+ self.playback2.seek.assert_called_once_with(10000)
+
+ def test_seek_fails_for_unplayable_track(self):
+ self.core.playback.current_tl_track = self.unplayable_tl_track
+ self.core.playback.state = PlaybackState.PLAYING
+ success = self.core.playback.seek(1000)
+
+ self.assertFalse(success)
+ self.assertFalse(self.playback1.seek.called)
+ self.assertFalse(self.playback2.seek.called)
+
+ def test_time_position_selects_dummy1_backend(self):
+ self.core.playback.play(self.tl_tracks[0])
+ self.core.playback.seek(10000)
+ self.core.playback.time_position
+
+ self.playback1.get_time_position.assert_called_once_with()
+ self.assertFalse(self.playback2.get_time_position.called)
+
+ def test_time_position_selects_dummy2_backend(self):
+ self.core.playback.play(self.tl_tracks[1])
+ self.core.playback.seek(10000)
+ self.core.playback.time_position
+
+ self.assertFalse(self.playback1.get_time_position.called)
+ self.playback2.get_time_position.assert_called_once_with()
+
+ def test_time_position_returns_0_if_track_is_unplayable(self):
+ self.core.playback.current_tl_track = self.unplayable_tl_track
+
+ result = self.core.playback.time_position
+
+ self.assertEqual(result, 0)
+ self.assertFalse(self.playback1.get_time_position.called)
+ self.assertFalse(self.playback2.get_time_position.called)
diff --git a/tests/core/playlists.py b/tests/core/playlists.py
new file mode 100644
index 00000000..949625fe
--- /dev/null
+++ b/tests/core/playlists.py
@@ -0,0 +1,190 @@
+from __future__ import unicode_literals
+
+import mock
+
+from mopidy.backends import base
+from mopidy.core import Core
+from mopidy.models import Playlist, Track
+
+from tests import unittest
+
+
+class PlaylistsTest(unittest.TestCase):
+ def setUp(self):
+ self.backend1 = mock.Mock()
+ self.backend1.uri_schemes.get.return_value = ['dummy1']
+ self.sp1 = mock.Mock(spec=base.BasePlaylistsProvider)
+ self.backend1.playlists = self.sp1
+
+ self.backend2 = mock.Mock()
+ self.backend2.uri_schemes.get.return_value = ['dummy2']
+ self.sp2 = mock.Mock(spec=base.BasePlaylistsProvider)
+ self.backend2.playlists = self.sp2
+
+ # A backend without the optional playlists provider
+ self.backend3 = mock.Mock()
+ self.backend3.uri_schemes.get.return_value = ['dummy3']
+ self.backend3.has_playlists().get.return_value = False
+ self.backend3.playlists = None
+
+ self.pl1a = Playlist(tracks=[Track(uri='dummy1:a')])
+ self.pl1b = Playlist(tracks=[Track(uri='dummy1:b')])
+ self.sp1.playlists.get.return_value = [self.pl1a, self.pl1b]
+
+ self.pl2a = Playlist(tracks=[Track(uri='dummy2:a')])
+ self.pl2b = Playlist(tracks=[Track(uri='dummy2:b')])
+ self.sp2.playlists.get.return_value = [self.pl2a, self.pl2b]
+
+ self.core = Core(audio=None, backends=[
+ self.backend3, self.backend1, self.backend2])
+
+ def test_get_playlists_combines_result_from_backends(self):
+ result = self.core.playlists.playlists
+
+ self.assertIn(self.pl1a, result)
+ self.assertIn(self.pl1b, result)
+ self.assertIn(self.pl2a, result)
+ self.assertIn(self.pl2b, result)
+
+ def test_create_without_uri_scheme_uses_first_backend(self):
+ playlist = Playlist()
+ self.sp1.create().get.return_value = playlist
+ self.sp1.reset_mock()
+
+ result = self.core.playlists.create('foo')
+
+ self.assertEqual(playlist, result)
+ self.sp1.create.assert_called_once_with('foo')
+ self.assertFalse(self.sp2.create.called)
+
+ def test_create_with_uri_scheme_selects_the_matching_backend(self):
+ playlist = Playlist()
+ self.sp2.create().get.return_value = playlist
+ self.sp2.reset_mock()
+
+ result = self.core.playlists.create('foo', uri_scheme='dummy2')
+
+ self.assertEqual(playlist, result)
+ self.assertFalse(self.sp1.create.called)
+ self.sp2.create.assert_called_once_with('foo')
+
+ def test_create_with_unsupported_uri_scheme_uses_first_backend(self):
+ playlist = Playlist()
+ self.sp1.create().get.return_value = playlist
+ self.sp1.reset_mock()
+
+ result = self.core.playlists.create('foo', uri_scheme='dummy3')
+
+ self.assertEqual(playlist, result)
+ self.sp1.create.assert_called_once_with('foo')
+ self.assertFalse(self.sp2.create.called)
+
+ def test_delete_selects_the_dummy1_backend(self):
+ self.core.playlists.delete('dummy1:a')
+
+ self.sp1.delete.assert_called_once_with('dummy1:a')
+ self.assertFalse(self.sp2.delete.called)
+
+ def test_delete_selects_the_dummy2_backend(self):
+ self.core.playlists.delete('dummy2:a')
+
+ self.assertFalse(self.sp1.delete.called)
+ self.sp2.delete.assert_called_once_with('dummy2:a')
+
+ def test_delete_with_unknown_uri_scheme_does_nothing(self):
+ self.core.playlists.delete('unknown:a')
+
+ self.assertFalse(self.sp1.delete.called)
+ self.assertFalse(self.sp2.delete.called)
+
+ def test_delete_ignores_backend_without_playlist_support(self):
+ self.core.playlists.delete('dummy3:a')
+
+ self.assertFalse(self.sp1.delete.called)
+ self.assertFalse(self.sp2.delete.called)
+
+ def test_lookup_selects_the_dummy1_backend(self):
+ self.core.playlists.lookup('dummy1:a')
+
+ self.sp1.lookup.assert_called_once_with('dummy1:a')
+ self.assertFalse(self.sp2.lookup.called)
+
+ def test_lookup_selects_the_dummy2_backend(self):
+ self.core.playlists.lookup('dummy2:a')
+
+ self.assertFalse(self.sp1.lookup.called)
+ self.sp2.lookup.assert_called_once_with('dummy2:a')
+
+ def test_lookup_track_in_backend_without_playlists_fails(self):
+ result = self.core.playlists.lookup('dummy3:a')
+
+ self.assertIsNone(result)
+ self.assertFalse(self.sp1.lookup.called)
+ self.assertFalse(self.sp2.lookup.called)
+
+ def test_refresh_without_uri_scheme_refreshes_all_backends(self):
+ self.core.playlists.refresh()
+
+ self.sp1.refresh.assert_called_once_with()
+ self.sp2.refresh.assert_called_once_with()
+
+ def test_refresh_with_uri_scheme_refreshes_matching_backend(self):
+ self.core.playlists.refresh(uri_scheme='dummy2')
+
+ self.assertFalse(self.sp1.refresh.called)
+ self.sp2.refresh.assert_called_once_with()
+
+ def test_refresh_with_unknown_uri_scheme_refreshes_nothing(self):
+ self.core.playlists.refresh(uri_scheme='foobar')
+
+ self.assertFalse(self.sp1.refresh.called)
+ self.assertFalse(self.sp2.refresh.called)
+
+ def test_refresh_ignores_backend_without_playlist_support(self):
+ self.core.playlists.refresh(uri_scheme='dummy3')
+
+ self.assertFalse(self.sp1.refresh.called)
+ self.assertFalse(self.sp2.refresh.called)
+
+ def test_save_selects_the_dummy1_backend(self):
+ playlist = Playlist(uri='dummy1:a')
+ self.sp1.save().get.return_value = playlist
+ self.sp1.reset_mock()
+
+ result = self.core.playlists.save(playlist)
+
+ self.assertEqual(playlist, result)
+ self.sp1.save.assert_called_once_with(playlist)
+ self.assertFalse(self.sp2.save.called)
+
+ def test_save_selects_the_dummy2_backend(self):
+ playlist = Playlist(uri='dummy2:a')
+ self.sp2.save().get.return_value = playlist
+ self.sp2.reset_mock()
+
+ result = self.core.playlists.save(playlist)
+
+ self.assertEqual(playlist, result)
+ self.assertFalse(self.sp1.save.called)
+ self.sp2.save.assert_called_once_with(playlist)
+
+ def test_save_does_nothing_if_playlist_uri_is_unset(self):
+ result = self.core.playlists.save(Playlist())
+
+ self.assertIsNone(result)
+ self.assertFalse(self.sp1.save.called)
+ self.assertFalse(self.sp2.save.called)
+
+ def test_save_does_nothing_if_playlist_uri_has_unknown_scheme(self):
+ result = self.core.playlists.save(Playlist(uri='foobar:a'))
+
+ self.assertIsNone(result)
+ self.assertFalse(self.sp1.save.called)
+ self.assertFalse(self.sp2.save.called)
+
+ def test_save_ignores_backend_without_playlist_support(self):
+ result = self.core.playlists.save(Playlist(uri='dummy3:a'))
+
+ self.assertIsNone(result)
+ self.assertFalse(self.sp1.save.called)
+ self.assertFalse(self.sp2.save.called)
diff --git a/tests/frontends/__init__.py b/tests/frontends/__init__.py
index e69de29b..baffc488 100644
--- a/tests/frontends/__init__.py
+++ b/tests/frontends/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/tests/frontends/mpd/__init__.py b/tests/frontends/mpd/__init__.py
index e69de29b..baffc488 100644
--- a/tests/frontends/mpd/__init__.py
+++ b/tests/frontends/mpd/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py
index 9f05d7dd..3404db95 100644
--- a/tests/frontends/mpd/dispatcher_test.py
+++ b/tests/frontends/mpd/dispatcher_test.py
@@ -1,4 +1,9 @@
-from mopidy.backends.dummy import DummyBackend
+from __future__ import unicode_literals
+
+import pykka
+
+from mopidy import core
+from mopidy.backends import dummy
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.exceptions import MpdAckError
from mopidy.frontends.mpd.protocol import request_handlers, handle_request
@@ -8,11 +13,12 @@ from tests import unittest
class MpdDispatcherTest(unittest.TestCase):
def setUp(self):
- self.backend = DummyBackend.start().proxy()
+ self.backend = dummy.DummyBackend.start(audio=None).proxy()
+ self.core = core.Core.start(backends=[self.backend]).proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
- self.backend.stop().get()
+ pykka.ActorRegistry.stop_all()
def test_register_same_pattern_twice_fails(self):
func = lambda: None
@@ -28,25 +34,27 @@ class MpdDispatcherTest(unittest.TestCase):
self.dispatcher._find_handler('an_unknown_command with args')
self.fail('Should raise exception')
except MpdAckError as e:
- self.assertEqual(e.get_mpd_ack(),
- u'ACK [5@0] {} unknown command "an_unknown_command"')
+ self.assertEqual(
+ e.get_mpd_ack(),
+ 'ACK [5@0] {} unknown command "an_unknown_command"')
- def test_finding_handler_for_known_command_returns_handler_and_kwargs(self):
+ def test_find_handler_for_known_command_returns_handler_and_kwargs(self):
expected_handler = lambda x: None
request_handlers['known_command (?P.+)'] = \
expected_handler
- (handler, kwargs) = self.dispatcher._find_handler('known_command an_arg')
+ (handler, kwargs) = self.dispatcher._find_handler(
+ 'known_command an_arg')
self.assertEqual(handler, expected_handler)
self.assertIn('arg1', kwargs)
self.assertEqual(kwargs['arg1'], 'an_arg')
def test_handling_unknown_request_yields_error(self):
result = self.dispatcher.handle_request('an unhandled request')
- self.assertEqual(result[0], u'ACK [5@0] {} unknown command "an"')
+ self.assertEqual(result[0], 'ACK [5@0] {} unknown command "an"')
def test_handling_known_request(self):
expected = 'magic'
request_handlers['known request'] = lambda x: expected
result = self.dispatcher.handle_request('known request')
- self.assertIn(u'OK', result)
+ self.assertIn('OK', result)
self.assertIn(expected, result)
diff --git a/tests/frontends/mpd/exception_test.py b/tests/frontends/mpd/exception_test.py
index 2ea3fe62..fe834673 100644
--- a/tests/frontends/mpd/exception_test.py
+++ b/tests/frontends/mpd/exception_test.py
@@ -1,5 +1,8 @@
-from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdPermissionError,
- MpdUnknownCommand, MpdSystemError, MpdNotImplemented)
+from __future__ import unicode_literals
+
+from mopidy.frontends.mpd.exceptions import (
+ MpdAckError, MpdPermissionError, MpdUnknownCommand, MpdSystemError,
+ MpdNotImplemented)
from tests import unittest
@@ -8,45 +11,48 @@ class MpdExceptionsTest(unittest.TestCase):
def test_key_error_wrapped_in_mpd_ack_error(self):
try:
try:
- raise KeyError(u'Track X not found')
+ raise KeyError('Track X not found')
except KeyError as e:
raise MpdAckError(e[0])
except MpdAckError as e:
- self.assertEqual(e.message, u'Track X not found')
+ self.assertEqual(e.message, 'Track X not found')
def test_mpd_not_implemented_is_a_mpd_ack_error(self):
try:
raise MpdNotImplemented
except MpdAckError as e:
- self.assertEqual(e.message, u'Not implemented')
+ self.assertEqual(e.message, 'Not implemented')
def test_get_mpd_ack_with_default_values(self):
e = MpdAckError('A description')
- self.assertEqual(e.get_mpd_ack(), u'ACK [0@0] {} A description')
+ self.assertEqual(e.get_mpd_ack(), 'ACK [0@0] {} A description')
def test_get_mpd_ack_with_values(self):
try:
raise MpdAckError('A description', index=7, command='foo')
except MpdAckError as e:
- self.assertEqual(e.get_mpd_ack(), u'ACK [0@7] {foo} A description')
+ self.assertEqual(e.get_mpd_ack(), 'ACK [0@7] {foo} A description')
def test_mpd_unknown_command(self):
try:
- raise MpdUnknownCommand(command=u'play')
+ raise MpdUnknownCommand(command='play')
except MpdAckError as e:
- self.assertEqual(e.get_mpd_ack(),
- u'ACK [5@0] {} unknown command "play"')
+ self.assertEqual(
+ e.get_mpd_ack(),
+ 'ACK [5@0] {} unknown command "play"')
def test_mpd_system_error(self):
try:
raise MpdSystemError('foo')
except MpdSystemError as e:
- self.assertEqual(e.get_mpd_ack(),
- u'ACK [52@0] {} foo')
+ self.assertEqual(
+ e.get_mpd_ack(),
+ 'ACK [52@0] {} foo')
def test_mpd_permission_error(self):
try:
raise MpdPermissionError(command='foo')
except MpdPermissionError as e:
- self.assertEqual(e.get_mpd_ack(),
- u'ACK [4@0] {foo} you don\'t have permission for "foo"')
+ self.assertEqual(
+ e.get_mpd_ack(),
+ 'ACK [4@0] {foo} you don\'t have permission for "foo"')
diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py
index 3b8fbe33..00594206 100644
--- a/tests/frontends/mpd/protocol/__init__.py
+++ b/tests/frontends/mpd/protocol/__init__.py
@@ -1,8 +1,11 @@
-import mock
+from __future__ import unicode_literals
-from mopidy import settings
-from mopidy.backends import dummy as backend
-from mopidy.frontends import mpd
+import mock
+import pykka
+
+from mopidy import core, settings
+from mopidy.backends import dummy
+from mopidy.frontends.mpd import session
from tests import unittest
@@ -21,15 +24,16 @@ class MockConnection(mock.Mock):
class BaseTestCase(unittest.TestCase):
def setUp(self):
- self.backend = backend.DummyBackend.start().proxy()
+ self.backend = dummy.DummyBackend.start(audio=None).proxy()
+ self.core = core.Core.start(backends=[self.backend]).proxy()
self.connection = MockConnection()
- self.session = mpd.MpdSession(self.connection)
+ self.session = session.MpdSession(self.connection, core=self.core)
self.dispatcher = self.session.dispatcher
self.context = self.dispatcher.context
def tearDown(self):
- self.backend.stop().get()
+ pykka.ActorRegistry.stop_all()
settings.runtime.clear()
def sendRequest(self, request):
@@ -42,17 +46,23 @@ class BaseTestCase(unittest.TestCase):
self.assertEqual([], self.connection.response)
def assertInResponse(self, value):
- self.assertIn(value, self.connection.response, u'Did not find %s '
- 'in %s' % (repr(value), repr(self.connection.response)))
+ self.assertIn(
+ value, self.connection.response,
+ 'Did not find %s in %s' % (
+ repr(value), repr(self.connection.response)))
def assertOnceInResponse(self, value):
matched = len([r for r in self.connection.response if r == value])
- self.assertEqual(1, matched, 'Expected to find %s once in %s' %
- (repr(value), repr(self.connection.response)))
+ self.assertEqual(
+ 1, matched,
+ 'Expected to find %s once in %s' % (
+ repr(value), repr(self.connection.response)))
def assertNotInResponse(self, value):
- self.assertNotIn(value, self.connection.response, u'Found %s in %s' %
- (repr(value), repr(self.connection.response)))
+ self.assertNotIn(
+ value, self.connection.response,
+ 'Found %s in %s' % (
+ repr(value), repr(self.connection.response)))
def assertEqualResponse(self, value):
self.assertEqual(1, len(self.connection.response))
diff --git a/tests/frontends/mpd/protocol/audio_output_test.py b/tests/frontends/mpd/protocol/audio_output_test.py
index 3bb8dce8..11cd249e 100644
--- a/tests/frontends/mpd/protocol/audio_output_test.py
+++ b/tests/frontends/mpd/protocol/audio_output_test.py
@@ -1,18 +1,20 @@
+from __future__ import unicode_literals
+
from tests.frontends.mpd import protocol
class AudioOutputHandlerTest(protocol.BaseTestCase):
def test_enableoutput(self):
- self.sendRequest(u'enableoutput "0"')
- self.assertInResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('enableoutput "0"')
+ self.assertInResponse('ACK [0@0] {} Not implemented')
def test_disableoutput(self):
- self.sendRequest(u'disableoutput "0"')
- self.assertInResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('disableoutput "0"')
+ self.assertInResponse('ACK [0@0] {} Not implemented')
def test_outputs(self):
- self.sendRequest(u'outputs')
- self.assertInResponse(u'outputid: 0')
- self.assertInResponse(u'outputname: None')
- self.assertInResponse(u'outputenabled: 1')
- self.assertInResponse(u'OK')
+ self.sendRequest('outputs')
+ self.assertInResponse('outputid: 0')
+ self.assertInResponse('outputname: None')
+ self.assertInResponse('outputenabled: 1')
+ self.assertInResponse('OK')
diff --git a/tests/frontends/mpd/protocol/authentication_test.py b/tests/frontends/mpd/protocol/authentication_test.py
index 0f0d9c86..26b03f45 100644
--- a/tests/frontends/mpd/protocol/authentication_test.py
+++ b/tests/frontends/mpd/protocol/authentication_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from mopidy import settings
from tests.frontends.mpd import protocol
@@ -7,28 +9,28 @@ class AuthenticationTest(protocol.BaseTestCase):
def test_authentication_with_valid_password_is_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'password "topsecret"')
+ self.sendRequest('password "topsecret"')
self.assertTrue(self.dispatcher.authenticated)
- self.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_authentication_with_invalid_password_is_not_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'password "secret"')
+ self.sendRequest('password "secret"')
self.assertFalse(self.dispatcher.authenticated)
- self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
+ self.assertEqualResponse('ACK [3@0] {password} incorrect password')
def test_authentication_with_anything_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None
- self.sendRequest(u'any request at all')
+ self.sendRequest('any request at all')
self.assertTrue(self.dispatcher.authenticated)
self.assertEqualResponse('ACK [5@0] {} unknown command "any"')
def test_anything_when_not_authenticated_should_fail(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'any request at all')
+ self.sendRequest('any request at all')
self.assertFalse(self.dispatcher.authenticated)
self.assertEqualResponse(
u'ACK [4@0] {any} you don\'t have permission for "any"')
@@ -36,26 +38,26 @@ class AuthenticationTest(protocol.BaseTestCase):
def test_close_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'close')
+ self.sendRequest('close')
self.assertFalse(self.dispatcher.authenticated)
def test_commands_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'commands')
+ self.sendRequest('commands')
self.assertFalse(self.dispatcher.authenticated)
- self.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_notcommands_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'notcommands')
+ self.sendRequest('notcommands')
self.assertFalse(self.dispatcher.authenticated)
- self.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_ping_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'ping')
+ self.sendRequest('ping')
self.assertFalse(self.dispatcher.authenticated)
- self.assertInResponse(u'OK')
+ self.assertInResponse('OK')
diff --git a/tests/frontends/mpd/protocol/command_list_test.py b/tests/frontends/mpd/protocol/command_list_test.py
index 65b051d3..222dcb61 100644
--- a/tests/frontends/mpd/protocol/command_list_test.py
+++ b/tests/frontends/mpd/protocol/command_list_test.py
@@ -1,53 +1,64 @@
+from __future__ import unicode_literals
+
from tests.frontends.mpd import protocol
class CommandListsTest(protocol.BaseTestCase):
def test_command_list_begin(self):
- response = self.sendRequest(u'command_list_begin')
+ response = self.sendRequest('command_list_begin')
self.assertEquals([], response)
def test_command_list_end(self):
- self.sendRequest(u'command_list_begin')
- self.sendRequest(u'command_list_end')
- self.assertInResponse(u'OK')
+ self.sendRequest('command_list_begin')
+ self.sendRequest('command_list_end')
+ self.assertInResponse('OK')
def test_command_list_end_without_start_first_is_an_unknown_command(self):
- self.sendRequest(u'command_list_end')
+ self.sendRequest('command_list_end')
self.assertEqualResponse(
- u'ACK [5@0] {} unknown command "command_list_end"')
+ 'ACK [5@0] {} unknown command "command_list_end"')
def test_command_list_with_ping(self):
- self.sendRequest(u'command_list_begin')
+ self.sendRequest('command_list_begin')
+ self.assertTrue(self.dispatcher.command_list_receiving)
+ self.assertFalse(self.dispatcher.command_list_ok)
+ self.assertEqual([], self.dispatcher.command_list)
+
+ self.sendRequest('ping')
+ self.assertIn('ping', self.dispatcher.command_list)
+
+ self.sendRequest('command_list_end')
+ self.assertInResponse('OK')
+ self.assertFalse(self.dispatcher.command_list_receiving)
+ self.assertFalse(self.dispatcher.command_list_ok)
self.assertEqual([], self.dispatcher.command_list)
- self.assertEqual(False, self.dispatcher.command_list_ok)
- self.sendRequest(u'ping')
- self.assertIn(u'ping', self.dispatcher.command_list)
- self.sendRequest(u'command_list_end')
- self.assertInResponse(u'OK')
- self.assertEqual(False, self.dispatcher.command_list)
def test_command_list_with_error_returns_ack_with_correct_index(self):
- self.sendRequest(u'command_list_begin')
- self.sendRequest(u'play') # Known command
- self.sendRequest(u'paly') # Unknown command
- self.sendRequest(u'command_list_end')
- self.assertEqualResponse(u'ACK [5@1] {} unknown command "paly"')
+ self.sendRequest('command_list_begin')
+ self.sendRequest('play') # Known command
+ self.sendRequest('paly') # Unknown command
+ self.sendRequest('command_list_end')
+ self.assertEqualResponse('ACK [5@1] {} unknown command "paly"')
def test_command_list_ok_begin(self):
- response = self.sendRequest(u'command_list_ok_begin')
+ response = self.sendRequest('command_list_ok_begin')
self.assertEquals([], response)
def test_command_list_ok_with_ping(self):
- self.sendRequest(u'command_list_ok_begin')
+ self.sendRequest('command_list_ok_begin')
+ self.assertTrue(self.dispatcher.command_list_receiving)
+ self.assertTrue(self.dispatcher.command_list_ok)
+ self.assertEqual([], self.dispatcher.command_list)
+
+ self.sendRequest('ping')
+ self.assertIn('ping', self.dispatcher.command_list)
+
+ self.sendRequest('command_list_end')
+ self.assertInResponse('list_OK')
+ self.assertInResponse('OK')
+ self.assertFalse(self.dispatcher.command_list_receiving)
+ self.assertFalse(self.dispatcher.command_list_ok)
self.assertEqual([], self.dispatcher.command_list)
- self.assertEqual(True, self.dispatcher.command_list_ok)
- self.sendRequest(u'ping')
- self.assertIn(u'ping', self.dispatcher.command_list)
- self.sendRequest(u'command_list_end')
- self.assertInResponse(u'list_OK')
- self.assertInResponse(u'OK')
- self.assertEqual(False, self.dispatcher.command_list)
- self.assertEqual(False, self.dispatcher.command_list_ok)
# FIXME this should also include the special handling of idle within a
# command list. That is that once a idle/noidle command is found inside a
diff --git a/tests/frontends/mpd/protocol/connection_test.py b/tests/frontends/mpd/protocol/connection_test.py
index cd08313f..840ce48f 100644
--- a/tests/frontends/mpd/protocol/connection_test.py
+++ b/tests/frontends/mpd/protocol/connection_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from mock import patch
from mopidy import settings
@@ -8,37 +10,37 @@ from tests.frontends.mpd import protocol
class ConnectionHandlerTest(protocol.BaseTestCase):
def test_close_closes_the_client_connection(self):
with patch.object(self.session, 'close') as close_mock:
- response = self.sendRequest(u'close')
+ self.sendRequest('close')
close_mock.assertEqualResponsecalled_once_with()
- self.assertEqualResponse(u'OK')
+ self.assertEqualResponse('OK')
def test_empty_request(self):
- self.sendRequest(u'')
- self.assertEqualResponse(u'OK')
+ self.sendRequest('')
+ self.assertEqualResponse('OK')
- self.sendRequest(u' ')
- self.assertEqualResponse(u'OK')
+ self.sendRequest(' ')
+ self.assertEqualResponse('OK')
def test_kill(self):
- self.sendRequest(u'kill')
+ self.sendRequest('kill')
self.assertEqualResponse(
- u'ACK [4@0] {kill} you don\'t have permission for "kill"')
+ 'ACK [4@0] {kill} you don\'t have permission for "kill"')
def test_valid_password_is_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'password "topsecret"')
- self.assertEqualResponse(u'OK')
+ settings.MPD_SERVER_PASSWORD = 'topsecret'
+ self.sendRequest('password "topsecret"')
+ self.assertEqualResponse('OK')
def test_invalid_password_is_not_accepted(self):
- settings.MPD_SERVER_PASSWORD = u'topsecret'
- self.sendRequest(u'password "secret"')
- self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
+ settings.MPD_SERVER_PASSWORD = 'topsecret'
+ self.sendRequest('password "secret"')
+ self.assertEqualResponse('ACK [3@0] {password} incorrect password')
def test_any_password_is_not_accepted_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None
- self.sendRequest(u'password "secret"')
- self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
+ self.sendRequest('password "secret"')
+ self.assertEqualResponse('ACK [3@0] {password} incorrect password')
def test_ping(self):
- self.sendRequest(u'ping')
- self.assertEqualResponse(u'OK')
+ self.sendRequest('ping')
+ self.assertEqualResponse('OK')
diff --git a/tests/frontends/mpd/protocol/current_playlist_test.py b/tests/frontends/mpd/protocol/current_playlist_test.py
index 21889e82..fc4640b1 100644
--- a/tests/frontends/mpd/protocol/current_playlist_test.py
+++ b/tests/frontends/mpd/protocol/current_playlist_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from mopidy.models import Track
from tests.frontends.mpd import protocol
@@ -6,478 +8,516 @@ from tests.frontends.mpd import protocol
class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
def test_add(self):
needle = Track(uri='dummy://foo')
- self.backend.library.provider.dummy_library = [
+ self.backend.library.dummy_library = [
Track(), Track(), needle, Track()]
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'add "dummy://foo"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
- self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
- self.assertEqualResponse(u'OK')
+ self.sendRequest('add "dummy://foo"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 6)
+ self.assertEqual(self.core.tracklist.tracks.get()[5], needle)
+ self.assertEqualResponse('OK')
def test_add_with_uri_not_found_in_library_should_ack(self):
- self.sendRequest(u'add "dummy://foo"')
+ self.sendRequest('add "dummy://foo"')
self.assertEqualResponse(
- u'ACK [50@0] {add} directory or file not found')
+ 'ACK [50@0] {add} directory or file not found')
def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self):
- self.sendRequest(u'add ""')
+ self.sendRequest('add ""')
# TODO check that we add all tracks (we currently don't)
- self.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo')
- self.backend.library.provider.dummy_library = [
+ self.backend.library.dummy_library = [
Track(), Track(), needle, Track()]
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'addid "dummy://foo"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
- self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
- self.assertInResponse(u'Id: %d' %
- self.backend.current_playlist.cp_tracks.get()[5][0])
- self.assertInResponse(u'OK')
+ self.sendRequest('addid "dummy://foo"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 6)
+ self.assertEqual(self.core.tracklist.tracks.get()[5], needle)
+ self.assertInResponse(
+ 'Id: %d' % self.core.tracklist.tl_tracks.get()[5].tlid)
+ self.assertInResponse('OK')
def test_addid_with_empty_uri_acks(self):
- self.sendRequest(u'addid ""')
- self.assertEqualResponse(u'ACK [50@0] {addid} No such song')
+ self.sendRequest('addid ""')
+ self.assertEqualResponse('ACK [50@0] {addid} No such song')
def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo')
- self.backend.library.provider.dummy_library = [
+ self.backend.library.dummy_library = [
Track(), Track(), needle, Track()]
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'addid "dummy://foo" "3"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
- self.assertEqual(self.backend.current_playlist.tracks.get()[3], needle)
- self.assertInResponse(u'Id: %d' %
- self.backend.current_playlist.cp_tracks.get()[3][0])
- self.assertInResponse(u'OK')
+ self.sendRequest('addid "dummy://foo" "3"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 6)
+ self.assertEqual(self.core.tracklist.tracks.get()[3], needle)
+ self.assertInResponse(
+ 'Id: %d' % self.core.tracklist.tl_tracks.get()[3].tlid)
+ self.assertInResponse('OK')
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo')
- self.backend.library.provider.dummy_library = [
+ self.backend.library.dummy_library = [
Track(), Track(), needle, Track()]
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'addid "dummy://foo" "6"')
- self.assertEqualResponse(u'ACK [2@0] {addid} Bad song index')
+ self.sendRequest('addid "dummy://foo" "6"')
+ self.assertEqualResponse('ACK [2@0] {addid} Bad song index')
def test_addid_with_uri_not_found_in_library_should_ack(self):
- self.sendRequest(u'addid "dummy://foo"')
- self.assertEqualResponse(u'ACK [50@0] {addid} No such song')
+ self.sendRequest('addid "dummy://foo"')
+ self.assertEqualResponse('ACK [50@0] {addid} No such song')
def test_clear(self):
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'clear')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
- self.assertEqual(self.backend.playback.current_track.get(), None)
- self.assertInResponse(u'OK')
+ self.sendRequest('clear')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 0)
+ self.assertEqual(self.core.playback.current_track.get(), None)
+ self.assertInResponse('OK')
def test_delete_songpos(self):
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'delete "%d"' %
- self.backend.current_playlist.cp_tracks.get()[2][0])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 4)
- self.assertInResponse(u'OK')
+ self.sendRequest(
+ 'delete "%d"' % self.core.tracklist.tl_tracks.get()[2].tlid)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 4)
+ self.assertInResponse('OK')
def test_delete_songpos_out_of_bounds(self):
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'delete "5"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index')
+ self.sendRequest('delete "5"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
+ self.assertEqualResponse('ACK [2@0] {delete} Bad song index')
def test_delete_open_range(self):
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'delete "1:"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
- self.assertInResponse(u'OK')
+ self.sendRequest('delete "1:"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 1)
+ self.assertInResponse('OK')
def test_delete_closed_range(self):
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'delete "1:3"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 3)
- self.assertInResponse(u'OK')
+ self.sendRequest('delete "1:3"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 3)
+ self.assertInResponse('OK')
def test_delete_range_out_of_bounds(self):
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(), Track(), Track(), Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
- self.sendRequest(u'delete "5:7"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
- self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index')
+ self.sendRequest('delete "5:7"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 5)
+ self.assertEqualResponse('ACK [2@0] {delete} Bad song index')
def test_deleteid(self):
- self.backend.current_playlist.append([Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
+ self.core.tracklist.add([Track(), Track()])
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 2)
- self.sendRequest(u'deleteid "1"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
- self.assertInResponse(u'OK')
+ self.sendRequest('deleteid "1"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 1)
+ self.assertInResponse('OK')
def test_deleteid_does_not_exist(self):
- self.backend.current_playlist.append([Track(), Track()])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
+ self.core.tracklist.add([Track(), Track()])
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 2)
- self.sendRequest(u'deleteid "12345"')
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- self.assertEqualResponse(u'ACK [50@0] {deleteid} No such song')
+ self.sendRequest('deleteid "12345"')
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 2)
+ self.assertEqualResponse('ACK [50@0] {deleteid} No such song')
def test_move_songpos(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'move "1" "0"')
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('move "1" "0"')
+ tracks = self.core.tracklist.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.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_move_open_range(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'move "2:" "0"')
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('move "2:" "0"')
+ tracks = self.core.tracklist.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.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_move_closed_range(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'move "1:3" "0"')
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('move "1:3" "0"')
+ tracks = self.core.tracklist.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.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_moveid(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'moveid "4" "2"')
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('moveid "4" "2"')
+ tracks = self.core.tracklist.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.assertInResponse(u'OK')
+ self.assertInResponse('OK')
+
+ def test_moveid_with_tlid_not_found_in_tracklist_should_ack(self):
+ self.sendRequest('moveid "9" "0"')
+ self.assertEqualResponse(
+ 'ACK [50@0] {moveid} No such song')
def test_playlist_returns_same_as_playlistinfo(self):
- playlist_response = self.sendRequest(u'playlist')
- playlistinfo_response = self.sendRequest(u'playlistinfo')
+ playlist_response = self.sendRequest('playlist')
+ playlistinfo_response = self.sendRequest('playlistinfo')
self.assertEqual(playlist_response, playlistinfo_response)
def test_playlistfind(self):
- self.sendRequest(u'playlistfind "tag" "needle"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('playlistfind "tag" "needle"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
- def test_playlistfind_by_filename_not_in_current_playlist(self):
- self.sendRequest(u'playlistfind "filename" "file:///dev/null"')
- self.assertEqualResponse(u'OK')
+ def test_playlistfind_by_filename_not_in_tracklist(self):
+ self.sendRequest('playlistfind "filename" "file:///dev/null"')
+ self.assertEqualResponse('OK')
def test_playlistfind_by_filename_without_quotes(self):
- self.sendRequest(u'playlistfind filename "file:///dev/null"')
- self.assertEqualResponse(u'OK')
+ self.sendRequest('playlistfind filename "file:///dev/null"')
+ self.assertEqualResponse('OK')
- def test_playlistfind_by_filename_in_current_playlist(self):
- self.backend.current_playlist.append([
- Track(uri='file:///exists')])
+ def test_playlistfind_by_filename_in_tracklist(self):
+ self.core.tracklist.add([Track(uri='file:///exists')])
- self.sendRequest( u'playlistfind filename "file:///exists"')
- self.assertInResponse(u'file: file:///exists')
- self.assertInResponse(u'Id: 0')
- self.assertInResponse(u'Pos: 0')
- self.assertInResponse(u'OK')
+ self.sendRequest('playlistfind filename "file:///exists"')
+ self.assertInResponse('file: file:///exists')
+ self.assertInResponse('Id: 0')
+ self.assertInResponse('Pos: 0')
+ self.assertInResponse('OK')
def test_playlistid_without_songid(self):
- self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
+ self.core.tracklist.add([Track(name='a'), Track(name='b')])
- self.sendRequest(u'playlistid')
- self.assertInResponse(u'Title: a')
- self.assertInResponse(u'Title: b')
- self.assertInResponse(u'OK')
+ self.sendRequest('playlistid')
+ self.assertInResponse('Title: a')
+ self.assertInResponse('Title: b')
+ self.assertInResponse('OK')
def test_playlistid_with_songid(self):
- self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
+ self.core.tracklist.add([Track(name='a'), Track(name='b')])
- self.sendRequest(u'playlistid "1"')
- self.assertNotInResponse(u'Title: a')
- self.assertNotInResponse(u'Id: 0')
- self.assertInResponse(u'Title: b')
- self.assertInResponse(u'Id: 1')
- self.assertInResponse(u'OK')
+ self.sendRequest('playlistid "1"')
+ self.assertNotInResponse('Title: a')
+ self.assertNotInResponse('Id: 0')
+ self.assertInResponse('Title: b')
+ self.assertInResponse('Id: 1')
+ self.assertInResponse('OK')
def test_playlistid_with_not_existing_songid_fails(self):
- self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
+ self.core.tracklist.add([Track(name='a'), Track(name='b')])
- self.sendRequest(u'playlistid "25"')
- self.assertEqualResponse(u'ACK [50@0] {playlistid} No such song')
+ self.sendRequest('playlistid "25"')
+ self.assertEqualResponse('ACK [50@0] {playlistid} No such song')
def test_playlistinfo_without_songpos_or_range(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'playlistinfo')
- self.assertInResponse(u'Title: a')
- self.assertInResponse(u'Pos: 0')
- self.assertInResponse(u'Title: b')
- self.assertInResponse(u'Pos: 1')
- self.assertInResponse(u'Title: c')
- self.assertInResponse(u'Pos: 2')
- self.assertInResponse(u'Title: d')
- self.assertInResponse(u'Pos: 3')
- self.assertInResponse(u'Title: e')
- self.assertInResponse(u'Pos: 4')
- self.assertInResponse(u'Title: f')
- self.assertInResponse(u'Pos: 5')
- self.assertInResponse(u'OK')
+ self.sendRequest('playlistinfo')
+ self.assertInResponse('Title: a')
+ self.assertInResponse('Pos: 0')
+ self.assertInResponse('Title: b')
+ self.assertInResponse('Pos: 1')
+ self.assertInResponse('Title: c')
+ self.assertInResponse('Pos: 2')
+ self.assertInResponse('Title: d')
+ self.assertInResponse('Pos: 3')
+ self.assertInResponse('Title: e')
+ self.assertInResponse('Pos: 4')
+ self.assertInResponse('Title: f')
+ self.assertInResponse('Pos: 5')
+ self.assertInResponse('OK')
def test_playlistinfo_with_songpos(self):
# Make the track's CPID not match the playlist position
- self.backend.current_playlist.cp_id = 17
- self.backend.current_playlist.append([
+ self.core.tracklist.tlid = 17
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'playlistinfo "4"')
- self.assertNotInResponse(u'Title: a')
- self.assertNotInResponse(u'Pos: 0')
- self.assertNotInResponse(u'Title: b')
- self.assertNotInResponse(u'Pos: 1')
- self.assertNotInResponse(u'Title: c')
- self.assertNotInResponse(u'Pos: 2')
- self.assertNotInResponse(u'Title: d')
- self.assertNotInResponse(u'Pos: 3')
- self.assertInResponse(u'Title: e')
- self.assertInResponse(u'Pos: 4')
- self.assertNotInResponse(u'Title: f')
- self.assertNotInResponse(u'Pos: 5')
- self.assertInResponse(u'OK')
+ self.sendRequest('playlistinfo "4"')
+ self.assertNotInResponse('Title: a')
+ self.assertNotInResponse('Pos: 0')
+ self.assertNotInResponse('Title: b')
+ self.assertNotInResponse('Pos: 1')
+ self.assertNotInResponse('Title: c')
+ self.assertNotInResponse('Pos: 2')
+ self.assertNotInResponse('Title: d')
+ self.assertNotInResponse('Pos: 3')
+ self.assertInResponse('Title: e')
+ self.assertInResponse('Pos: 4')
+ self.assertNotInResponse('Title: f')
+ self.assertNotInResponse('Pos: 5')
+ self.assertInResponse('OK')
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
- response1 = self.sendRequest(u'playlistinfo "-1"')
- response2 = self.sendRequest(u'playlistinfo')
+ response1 = self.sendRequest('playlistinfo "-1"')
+ response2 = self.sendRequest('playlistinfo')
self.assertEqual(response1, response2)
def test_playlistinfo_with_open_range(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'playlistinfo "2:"')
- self.assertNotInResponse(u'Title: a')
- self.assertNotInResponse(u'Pos: 0')
- self.assertNotInResponse(u'Title: b')
- self.assertNotInResponse(u'Pos: 1')
- self.assertInResponse(u'Title: c')
- self.assertInResponse(u'Pos: 2')
- self.assertInResponse(u'Title: d')
- self.assertInResponse(u'Pos: 3')
- self.assertInResponse(u'Title: e')
- self.assertInResponse(u'Pos: 4')
- self.assertInResponse(u'Title: f')
- self.assertInResponse(u'Pos: 5')
- self.assertInResponse(u'OK')
+ self.sendRequest('playlistinfo "2:"')
+ self.assertNotInResponse('Title: a')
+ self.assertNotInResponse('Pos: 0')
+ self.assertNotInResponse('Title: b')
+ self.assertNotInResponse('Pos: 1')
+ self.assertInResponse('Title: c')
+ self.assertInResponse('Pos: 2')
+ self.assertInResponse('Title: d')
+ self.assertInResponse('Pos: 3')
+ self.assertInResponse('Title: e')
+ self.assertInResponse('Pos: 4')
+ self.assertInResponse('Title: f')
+ self.assertInResponse('Pos: 5')
+ self.assertInResponse('OK')
def test_playlistinfo_with_closed_range(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'playlistinfo "2:4"')
- self.assertNotInResponse(u'Title: a')
- self.assertNotInResponse(u'Title: b')
- self.assertInResponse(u'Title: c')
- self.assertInResponse(u'Title: d')
- self.assertNotInResponse(u'Title: e')
- self.assertNotInResponse(u'Title: f')
- self.assertInResponse(u'OK')
+ self.sendRequest('playlistinfo "2:4"')
+ self.assertNotInResponse('Title: a')
+ self.assertNotInResponse('Title: b')
+ self.assertInResponse('Title: c')
+ self.assertInResponse('Title: d')
+ self.assertNotInResponse('Title: e')
+ self.assertNotInResponse('Title: f')
+ self.assertInResponse('OK')
def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self):
- self.sendRequest(u'playlistinfo "10:20"')
- self.assertEqualResponse(u'ACK [2@0] {playlistinfo} Bad song index')
+ self.sendRequest('playlistinfo "10:20"')
+ self.assertEqualResponse('ACK [2@0] {playlistinfo} Bad song index')
def test_playlistinfo_with_too_high_end_of_range_returns_ok(self):
- self.sendRequest(u'playlistinfo "0:20"')
- self.assertInResponse(u'OK')
+ self.sendRequest('playlistinfo "0:20"')
+ self.assertInResponse('OK')
def test_playlistsearch(self):
- self.sendRequest( u'playlistsearch "any" "needle"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('playlistsearch "any" "needle"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_playlistsearch_without_quotes(self):
- self.sendRequest(u'playlistsearch any "needle"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('playlistsearch any "needle"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
- def test_plchanges(self):
- self.backend.current_playlist.append(
+ def test_plchanges_with_lower_version_returns_changes(self):
+ self.core.tracklist.add(
[Track(name='a'), Track(name='b'), Track(name='c')])
- self.sendRequest(u'plchanges "0"')
- self.assertInResponse(u'Title: a')
- self.assertInResponse(u'Title: b')
- self.assertInResponse(u'Title: c')
- self.assertInResponse(u'OK')
+ self.sendRequest('plchanges "0"')
+ self.assertInResponse('Title: a')
+ self.assertInResponse('Title: b')
+ self.assertInResponse('Title: c')
+ self.assertInResponse('OK')
+
+ def test_plchanges_with_equal_version_returns_nothing(self):
+ self.core.tracklist.add(
+ [Track(name='a'), Track(name='b'), Track(name='c')])
+
+ self.assertEqual(self.core.tracklist.version.get(), 1)
+ self.sendRequest('plchanges "1"')
+ self.assertNotInResponse('Title: a')
+ self.assertNotInResponse('Title: b')
+ self.assertNotInResponse('Title: c')
+ self.assertInResponse('OK')
+
+ def test_plchanges_with_greater_version_returns_nothing(self):
+ self.core.tracklist.add(
+ [Track(name='a'), Track(name='b'), Track(name='c')])
+
+ self.assertEqual(self.core.tracklist.version.get(), 1)
+ self.sendRequest('plchanges "2"')
+ self.assertNotInResponse('Title: a')
+ self.assertNotInResponse('Title: b')
+ self.assertNotInResponse('Title: c')
+ self.assertInResponse('OK')
def test_plchanges_with_minus_one_returns_entire_playlist(self):
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(name='a'), Track(name='b'), Track(name='c')])
- self.sendRequest(u'plchanges "-1"')
- self.assertInResponse(u'Title: a')
- self.assertInResponse(u'Title: b')
- self.assertInResponse(u'Title: c')
- self.assertInResponse(u'OK')
+ self.sendRequest('plchanges "-1"')
+ self.assertInResponse('Title: a')
+ self.assertInResponse('Title: b')
+ self.assertInResponse('Title: c')
+ self.assertInResponse('OK')
def test_plchanges_without_quotes_works(self):
- self.backend.current_playlist.append(
+ self.core.tracklist.add(
[Track(name='a'), Track(name='b'), Track(name='c')])
- self.sendRequest(u'plchanges 0')
- self.assertInResponse(u'Title: a')
- self.assertInResponse(u'Title: b')
- self.assertInResponse(u'Title: c')
- self.assertInResponse(u'OK')
+ self.sendRequest('plchanges 0')
+ self.assertInResponse('Title: a')
+ self.assertInResponse('Title: b')
+ self.assertInResponse('Title: c')
+ self.assertInResponse('OK')
def test_plchangesposid(self):
- self.backend.current_playlist.append([Track(), Track(), Track()])
+ self.core.tracklist.add([Track(), Track(), Track()])
- self.sendRequest(u'plchangesposid "0"')
- cp_tracks = self.backend.current_playlist.cp_tracks.get()
- self.assertInResponse(u'cpos: 0')
- self.assertInResponse(u'Id: %d' % cp_tracks[0][0])
- self.assertInResponse(u'cpos: 2')
- self.assertInResponse(u'Id: %d' % cp_tracks[1][0])
- self.assertInResponse(u'cpos: 2')
- self.assertInResponse(u'Id: %d' % cp_tracks[2][0])
- self.assertInResponse(u'OK')
+ self.sendRequest('plchangesposid "0"')
+ tl_tracks = self.core.tracklist.tl_tracks.get()
+ self.assertInResponse('cpos: 0')
+ self.assertInResponse('Id: %d' % tl_tracks[0].tlid)
+ self.assertInResponse('cpos: 2')
+ self.assertInResponse('Id: %d' % tl_tracks[1].tlid)
+ self.assertInResponse('cpos: 2')
+ self.assertInResponse('Id: %d' % tl_tracks[2].tlid)
+ self.assertInResponse('OK')
def test_shuffle_without_range(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.backend.current_playlist.version.get()
+ version = self.core.tracklist.version.get()
- self.sendRequest(u'shuffle')
- self.assert_(version < self.backend.current_playlist.version.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('shuffle')
+ self.assertLess(version, self.core.tracklist.version.get())
+ self.assertInResponse('OK')
def test_shuffle_with_open_range(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.backend.current_playlist.version.get()
+ version = self.core.tracklist.version.get()
- self.sendRequest(u'shuffle "4:"')
- self.assert_(version < self.backend.current_playlist.version.get())
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('shuffle "4:"')
+ self.assertLess(version, self.core.tracklist.version.get())
+ tracks = self.core.tracklist.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.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_shuffle_with_closed_range(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- version = self.backend.current_playlist.version.get()
+ version = self.core.tracklist.version.get()
- self.sendRequest(u'shuffle "1:3"')
- self.assert_(version < self.backend.current_playlist.version.get())
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('shuffle "1:3"')
+ self.assertLess(version, self.core.tracklist.version.get())
+ tracks = self.core.tracklist.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.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_swap(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'swap "1" "4"')
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('swap "1" "4"')
+ tracks = self.core.tracklist.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.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_swapid(self):
- self.backend.current_playlist.append([
+ self.core.tracklist.add([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- self.sendRequest(u'swapid "1" "4"')
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('swapid "1" "4"')
+ tracks = self.core.tracklist.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.assertInResponse(u'OK')
+ self.assertInResponse('OK')
+
+ def test_swapid_with_first_id_unknown_should_ack(self):
+ self.core.tracklist.add([Track()])
+ self.sendRequest('swapid "0" "4"')
+ self.assertEqualResponse(
+ 'ACK [50@0] {swapid} No such song')
+
+ def test_swapid_with_second_id_unknown_should_ack(self):
+ self.core.tracklist.add([Track()])
+ self.sendRequest('swapid "4" "0"')
+ self.assertEqualResponse(
+ 'ACK [50@0] {swapid} No such song')
diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py
index ae23c88e..e6910988 100644
--- a/tests/frontends/mpd/protocol/idle_test.py
+++ b/tests/frontends/mpd/protocol/idle_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from mock import patch
from mopidy.frontends.mpd.protocol.status import SUBSYSTEMS
@@ -27,180 +29,180 @@ class IdleHandlerTest(protocol.BaseTestCase):
self.assertNoResponse()
def test_idle(self):
- self.sendRequest(u'idle')
+ self.sendRequest('idle')
self.assertEqualSubscriptions(SUBSYSTEMS)
self.assertNoEvents()
self.assertNoResponse()
def test_idle_disables_timeout(self):
- self.sendRequest(u'idle')
+ self.sendRequest('idle')
self.connection.disable_timeout.assert_called_once_with()
def test_noidle(self):
- self.sendRequest(u'noidle')
+ self.sendRequest('noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player(self):
- self.sendRequest(u'idle player')
+ self.sendRequest('idle player')
self.assertEqualSubscriptions(['player'])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player_playlist(self):
- self.sendRequest(u'idle player playlist')
+ self.sendRequest('idle player playlist')
self.assertEqualSubscriptions(['player', 'playlist'])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_then_noidle(self):
- self.sendRequest(u'idle')
- self.sendRequest(u'noidle')
+ self.sendRequest('idle')
+ self.sendRequest('noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('OK')
def test_idle_then_noidle_enables_timeout(self):
- self.sendRequest(u'idle')
- self.sendRequest(u'noidle')
+ self.sendRequest('idle')
+ self.sendRequest('noidle')
self.connection.enable_timeout.assert_called_once_with()
def test_idle_then_play(self):
with patch.object(self.session, 'stop') as stop_mock:
- self.sendRequest(u'idle')
- self.sendRequest(u'play')
+ self.sendRequest('idle')
+ self.sendRequest('play')
stop_mock.assert_called_once_with()
def test_idle_then_idle(self):
with patch.object(self.session, 'stop') as stop_mock:
- self.sendRequest(u'idle')
- self.sendRequest(u'idle')
+ self.sendRequest('idle')
+ self.sendRequest('idle')
stop_mock.assert_called_once_with()
def test_idle_player_then_play(self):
with patch.object(self.session, 'stop') as stop_mock:
- self.sendRequest(u'idle player')
- self.sendRequest(u'play')
+ self.sendRequest('idle player')
+ self.sendRequest('play')
stop_mock.assert_called_once_with()
def test_idle_then_player(self):
- self.sendRequest(u'idle')
- self.idleEvent(u'player')
+ self.sendRequest('idle')
+ self.idleEvent('player')
self.assertNoSubscriptions()
self.assertNoEvents()
- self.assertOnceInResponse(u'changed: player')
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('changed: player')
+ self.assertOnceInResponse('OK')
def test_idle_player_then_event_player(self):
- self.sendRequest(u'idle player')
- self.idleEvent(u'player')
+ self.sendRequest('idle player')
+ self.idleEvent('player')
self.assertNoSubscriptions()
self.assertNoEvents()
- self.assertOnceInResponse(u'changed: player')
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('changed: player')
+ self.assertOnceInResponse('OK')
def test_idle_player_then_noidle(self):
- self.sendRequest(u'idle player')
- self.sendRequest(u'noidle')
+ self.sendRequest('idle player')
+ self.sendRequest('noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('OK')
def test_idle_player_playlist_then_noidle(self):
- self.sendRequest(u'idle player playlist')
- self.sendRequest(u'noidle')
+ self.sendRequest('idle player playlist')
+ self.sendRequest('noidle')
self.assertNoEvents()
self.assertNoSubscriptions()
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('OK')
def test_idle_player_playlist_then_player(self):
- self.sendRequest(u'idle player playlist')
- self.idleEvent(u'player')
+ self.sendRequest('idle player playlist')
+ self.idleEvent('player')
self.assertNoEvents()
self.assertNoSubscriptions()
- self.assertOnceInResponse(u'changed: player')
- self.assertNotInResponse(u'changed: playlist')
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('changed: player')
+ self.assertNotInResponse('changed: playlist')
+ self.assertOnceInResponse('OK')
def test_idle_playlist_then_player(self):
- self.sendRequest(u'idle playlist')
- self.idleEvent(u'player')
+ self.sendRequest('idle playlist')
+ self.idleEvent('player')
self.assertEqualEvents(['player'])
self.assertEqualSubscriptions(['playlist'])
self.assertNoResponse()
def test_idle_playlist_then_player_then_playlist(self):
- self.sendRequest(u'idle playlist')
- self.idleEvent(u'player')
- self.idleEvent(u'playlist')
+ self.sendRequest('idle playlist')
+ self.idleEvent('player')
+ self.idleEvent('playlist')
self.assertNoEvents()
self.assertNoSubscriptions()
- self.assertNotInResponse(u'changed: player')
- self.assertOnceInResponse(u'changed: playlist')
- self.assertOnceInResponse(u'OK')
+ self.assertNotInResponse('changed: player')
+ self.assertOnceInResponse('changed: playlist')
+ self.assertOnceInResponse('OK')
def test_player(self):
- self.idleEvent(u'player')
+ self.idleEvent('player')
self.assertEqualEvents(['player'])
self.assertNoSubscriptions()
self.assertNoResponse()
def test_player_then_idle_player(self):
- self.idleEvent(u'player')
- self.sendRequest(u'idle player')
+ self.idleEvent('player')
+ self.sendRequest('idle player')
self.assertNoEvents()
self.assertNoSubscriptions()
- self.assertOnceInResponse(u'changed: player')
- self.assertNotInResponse(u'changed: playlist')
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('changed: player')
+ self.assertNotInResponse('changed: playlist')
+ self.assertOnceInResponse('OK')
def test_player_then_playlist(self):
- self.idleEvent(u'player')
- self.idleEvent(u'playlist')
+ self.idleEvent('player')
+ self.idleEvent('playlist')
self.assertEqualEvents(['player', 'playlist'])
self.assertNoSubscriptions()
self.assertNoResponse()
def test_player_then_idle(self):
- self.idleEvent(u'player')
- self.sendRequest(u'idle')
+ self.idleEvent('player')
+ self.sendRequest('idle')
self.assertNoEvents()
self.assertNoSubscriptions()
- self.assertOnceInResponse(u'changed: player')
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('changed: player')
+ self.assertOnceInResponse('OK')
def test_player_then_playlist_then_idle(self):
- self.idleEvent(u'player')
- self.idleEvent(u'playlist')
- self.sendRequest(u'idle')
+ self.idleEvent('player')
+ self.idleEvent('playlist')
+ self.sendRequest('idle')
self.assertNoEvents()
self.assertNoSubscriptions()
- self.assertOnceInResponse(u'changed: player')
- self.assertOnceInResponse(u'changed: playlist')
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('changed: player')
+ self.assertOnceInResponse('changed: playlist')
+ self.assertOnceInResponse('OK')
def test_player_then_idle_playlist(self):
- self.idleEvent(u'player')
- self.sendRequest(u'idle playlist')
+ self.idleEvent('player')
+ self.sendRequest('idle playlist')
self.assertEqualEvents(['player'])
self.assertEqualSubscriptions(['playlist'])
self.assertNoResponse()
def test_player_then_idle_playlist_then_noidle(self):
- self.idleEvent(u'player')
- self.sendRequest(u'idle playlist')
- self.sendRequest(u'noidle')
+ self.idleEvent('player')
+ self.sendRequest('idle playlist')
+ self.sendRequest('noidle')
self.assertNoEvents()
self.assertNoSubscriptions()
- self.assertOnceInResponse(u'OK')
+ self.assertOnceInResponse('OK')
def test_player_then_playlist_then_idle_playlist(self):
- self.idleEvent(u'player')
- self.idleEvent(u'playlist')
- self.sendRequest(u'idle playlist')
+ self.idleEvent('player')
+ self.idleEvent('playlist')
+ self.sendRequest('idle playlist')
self.assertNoEvents()
self.assertNoSubscriptions()
- self.assertNotInResponse(u'changed: player')
- self.assertOnceInResponse(u'changed: playlist')
- self.assertOnceInResponse(u'OK')
+ self.assertNotInResponse('changed: player')
+ self.assertOnceInResponse('changed: playlist')
+ self.assertOnceInResponse('OK')
diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py
index 088502c4..4539eb4c 100644
--- a/tests/frontends/mpd/protocol/music_db_test.py
+++ b/tests/frontends/mpd/protocol/music_db_test.py
@@ -1,344 +1,447 @@
+from __future__ import unicode_literals
+
+from mopidy.models import Album, Artist, Track
+
from tests.frontends.mpd import protocol
class MusicDatabaseHandlerTest(protocol.BaseTestCase):
def test_count(self):
- self.sendRequest(u'count "tag" "needle"')
- self.assertInResponse(u'songs: 0')
- self.assertInResponse(u'playtime: 0')
- self.assertInResponse(u'OK')
+ self.sendRequest('count "tag" "needle"')
+ self.assertInResponse('songs: 0')
+ self.assertInResponse('playtime: 0')
+ self.assertInResponse('OK')
def test_findadd(self):
- self.sendRequest(u'findadd "album" "what"')
- self.assertInResponse(u'OK')
+ self.sendRequest('findadd "album" "what"')
+ self.assertInResponse('OK')
def test_listall(self):
- self.sendRequest(u'listall "file:///dev/urandom"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('listall "file:///dev/urandom"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_listallinfo(self):
- self.sendRequest(u'listallinfo "file:///dev/urandom"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('listallinfo "file:///dev/urandom"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_lsinfo_without_path_returns_same_as_listplaylists(self):
- lsinfo_response = self.sendRequest(u'lsinfo')
- listplaylists_response = self.sendRequest(u'listplaylists')
+ lsinfo_response = self.sendRequest('lsinfo')
+ listplaylists_response = self.sendRequest('listplaylists')
self.assertEqual(lsinfo_response, listplaylists_response)
def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
- lsinfo_response = self.sendRequest(u'lsinfo ""')
- listplaylists_response = self.sendRequest(u'listplaylists')
+ lsinfo_response = self.sendRequest('lsinfo ""')
+ listplaylists_response = self.sendRequest('listplaylists')
self.assertEqual(lsinfo_response, listplaylists_response)
def test_lsinfo_for_root_returns_same_as_listplaylists(self):
- lsinfo_response = self.sendRequest(u'lsinfo "/"')
- listplaylists_response = self.sendRequest(u'listplaylists')
+ lsinfo_response = self.sendRequest('lsinfo "/"')
+ listplaylists_response = self.sendRequest('listplaylists')
self.assertEqual(lsinfo_response, listplaylists_response)
def test_update_without_uri(self):
- self.sendRequest(u'update')
- self.assertInResponse(u'updating_db: 0')
- self.assertInResponse(u'OK')
+ self.sendRequest('update')
+ self.assertInResponse('updating_db: 0')
+ self.assertInResponse('OK')
def test_update_with_uri(self):
- self.sendRequest(u'update "file:///dev/urandom"')
- self.assertInResponse(u'updating_db: 0')
- self.assertInResponse(u'OK')
+ self.sendRequest('update "file:///dev/urandom"')
+ self.assertInResponse('updating_db: 0')
+ self.assertInResponse('OK')
def test_rescan_without_uri(self):
- self.sendRequest(u'rescan')
- self.assertInResponse(u'updating_db: 0')
- self.assertInResponse(u'OK')
+ self.sendRequest('rescan')
+ self.assertInResponse('updating_db: 0')
+ self.assertInResponse('OK')
def test_rescan_with_uri(self):
- self.sendRequest(u'rescan "file:///dev/urandom"')
- self.assertInResponse(u'updating_db: 0')
- self.assertInResponse(u'OK')
+ self.sendRequest('rescan "file:///dev/urandom"')
+ self.assertInResponse('updating_db: 0')
+ self.assertInResponse('OK')
class MusicDatabaseFindTest(protocol.BaseTestCase):
def test_find_album(self):
- self.sendRequest(u'find "album" "what"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find "album" "what"')
+ self.assertInResponse('OK')
def test_find_album_without_quotes(self):
- self.sendRequest(u'find album "what"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find album "what"')
+ self.assertInResponse('OK')
def test_find_artist(self):
- self.sendRequest(u'find "artist" "what"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find "artist" "what"')
+ self.assertInResponse('OK')
def test_find_artist_without_quotes(self):
- self.sendRequest(u'find artist "what"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find artist "what"')
+ self.assertInResponse('OK')
+
+ def test_find_filename(self):
+ self.sendRequest('find "filename" "afilename"')
+ self.assertInResponse('OK')
+
+ def test_find_filename_without_quotes(self):
+ self.sendRequest('find filename "afilename"')
+ self.assertInResponse('OK')
+
+ def test_find_file(self):
+ self.sendRequest('find "file" "afilename"')
+ self.assertInResponse('OK')
+
+ def test_find_file_without_quotes(self):
+ self.sendRequest('find file "afilename"')
+ self.assertInResponse('OK')
def test_find_title(self):
- self.sendRequest(u'find "title" "what"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find "title" "what"')
+ self.assertInResponse('OK')
def test_find_title_without_quotes(self):
- self.sendRequest(u'find title "what"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find title "what"')
+ self.assertInResponse('OK')
def test_find_date(self):
- self.sendRequest(u'find "date" "2002-01-01"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find "date" "2002-01-01"')
+ self.assertInResponse('OK')
def test_find_date_without_quotes(self):
- self.sendRequest(u'find date "2002-01-01"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find date "2002-01-01"')
+ self.assertInResponse('OK')
def test_find_date_with_capital_d_and_incomplete_date(self):
- self.sendRequest(u'find Date "2005"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find Date "2005"')
+ self.assertInResponse('OK')
def test_find_else_should_fail(self):
- self.sendRequest(u'find "somethingelse" "what"')
- self.assertEqualResponse(u'ACK [2@0] {find} incorrect arguments')
+ self.sendRequest('find "somethingelse" "what"')
+ self.assertEqualResponse('ACK [2@0] {find} incorrect arguments')
def test_find_album_and_artist(self):
- self.sendRequest(u'find album "album_what" artist "artist_what"')
- self.assertInResponse(u'OK')
+ self.sendRequest('find album "album_what" artist "artist_what"')
+ self.assertInResponse('OK')
+
+ def test_find_without_filter_value(self):
+ self.sendRequest('find "album" ""')
+ self.assertInResponse('OK')
class MusicDatabaseListTest(protocol.BaseTestCase):
def test_list_foo_returns_ack(self):
- self.sendRequest(u'list "foo"')
- self.assertEqualResponse(u'ACK [2@0] {list} incorrect arguments')
+ self.sendRequest('list "foo"')
+ self.assertEqualResponse('ACK [2@0] {list} incorrect arguments')
### Artist
def test_list_artist_with_quotes(self):
- self.sendRequest(u'list "artist"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "artist"')
+ self.assertInResponse('OK')
def test_list_artist_without_quotes(self):
- self.sendRequest(u'list artist')
- self.assertInResponse(u'OK')
+ self.sendRequest('list artist')
+ self.assertInResponse('OK')
def test_list_artist_without_quotes_and_capitalized(self):
- self.sendRequest(u'list Artist')
- self.assertInResponse(u'OK')
+ self.sendRequest('list Artist')
+ self.assertInResponse('OK')
def test_list_artist_with_query_of_one_token(self):
- self.sendRequest(u'list "artist" "anartist"')
+ self.sendRequest('list "artist" "anartist"')
self.assertEqualResponse(
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
+ 'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_artist_with_unknown_field_in_query_returns_ack(self):
- self.sendRequest(u'list "artist" "foo" "bar"')
- self.assertEqualResponse(u'ACK [2@0] {list} not able to parse args')
+ self.sendRequest('list "artist" "foo" "bar"')
+ self.assertEqualResponse('ACK [2@0] {list} not able to parse args')
def test_list_artist_by_artist(self):
- self.sendRequest(u'list "artist" "artist" "anartist"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "artist" "artist" "anartist"')
+ self.assertInResponse('OK')
def test_list_artist_by_album(self):
- self.sendRequest(u'list "artist" "album" "analbum"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "artist" "album" "analbum"')
+ self.assertInResponse('OK')
def test_list_artist_by_full_date(self):
- self.sendRequest(u'list "artist" "date" "2001-01-01"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "artist" "date" "2001-01-01"')
+ self.assertInResponse('OK')
def test_list_artist_by_year(self):
- self.sendRequest(u'list "artist" "date" "2001"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "artist" "date" "2001"')
+ self.assertInResponse('OK')
def test_list_artist_by_genre(self):
- self.sendRequest(u'list "artist" "genre" "agenre"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "artist" "genre" "agenre"')
+ self.assertInResponse('OK')
def test_list_artist_by_artist_and_album(self):
self.sendRequest(
- u'list "artist" "artist" "anartist" "album" "analbum"')
- self.assertInResponse(u'OK')
+ 'list "artist" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse('OK')
+
+ def test_list_artist_without_filter_value(self):
+ self.sendRequest('list "artist" "artist" ""')
+ self.assertInResponse('OK')
+
+ def test_list_artist_should_not_return_artists_without_names(self):
+ self.backend.library.dummy_find_exact_result = [
+ Track(artists=[Artist(name='')])]
+
+ self.sendRequest('list "artist"')
+ self.assertNotInResponse('Artist: ')
+ self.assertInResponse('OK')
### Album
def test_list_album_with_quotes(self):
- self.sendRequest(u'list "album"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "album"')
+ self.assertInResponse('OK')
def test_list_album_without_quotes(self):
- self.sendRequest(u'list album')
- self.assertInResponse(u'OK')
+ self.sendRequest('list album')
+ self.assertInResponse('OK')
def test_list_album_without_quotes_and_capitalized(self):
- self.sendRequest(u'list Album')
- self.assertInResponse(u'OK')
+ self.sendRequest('list Album')
+ self.assertInResponse('OK')
def test_list_album_with_artist_name(self):
- self.sendRequest(u'list "album" "anartist"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "album" "anartist"')
+ self.assertInResponse('OK')
+
+ def test_list_album_with_artist_name_without_filter_value(self):
+ self.sendRequest('list "album" ""')
+ self.assertInResponse('OK')
def test_list_album_by_artist(self):
- self.sendRequest(u'list "album" "artist" "anartist"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "album" "artist" "anartist"')
+ self.assertInResponse('OK')
def test_list_album_by_album(self):
- self.sendRequest(u'list "album" "album" "analbum"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "album" "album" "analbum"')
+ self.assertInResponse('OK')
def test_list_album_by_full_date(self):
- self.sendRequest(u'list "album" "date" "2001-01-01"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "album" "date" "2001-01-01"')
+ self.assertInResponse('OK')
def test_list_album_by_year(self):
- self.sendRequest(u'list "album" "date" "2001"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "album" "date" "2001"')
+ self.assertInResponse('OK')
def test_list_album_by_genre(self):
- self.sendRequest(u'list "album" "genre" "agenre"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "album" "genre" "agenre"')
+ self.assertInResponse('OK')
def test_list_album_by_artist_and_album(self):
self.sendRequest(
- u'list "album" "artist" "anartist" "album" "analbum"')
- self.assertInResponse(u'OK')
+ 'list "album" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse('OK')
+
+ def test_list_album_without_filter_value(self):
+ self.sendRequest('list "album" "artist" ""')
+ self.assertInResponse('OK')
+
+ def test_list_album_should_not_return_albums_without_names(self):
+ self.backend.library.dummy_find_exact_result = [
+ Track(album=Album(name=''))]
+
+ self.sendRequest('list "album"')
+ self.assertNotInResponse('Album: ')
+ self.assertInResponse('OK')
### Date
def test_list_date_with_quotes(self):
- self.sendRequest(u'list "date"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "date"')
+ self.assertInResponse('OK')
def test_list_date_without_quotes(self):
- self.sendRequest(u'list date')
- self.assertInResponse(u'OK')
+ self.sendRequest('list date')
+ self.assertInResponse('OK')
def test_list_date_without_quotes_and_capitalized(self):
- self.sendRequest(u'list Date')
- self.assertInResponse(u'OK')
+ self.sendRequest('list Date')
+ self.assertInResponse('OK')
def test_list_date_with_query_of_one_token(self):
- self.sendRequest(u'list "date" "anartist"')
+ self.sendRequest('list "date" "anartist"')
self.assertEqualResponse(
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
+ 'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_date_by_artist(self):
- self.sendRequest(u'list "date" "artist" "anartist"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "date" "artist" "anartist"')
+ self.assertInResponse('OK')
def test_list_date_by_album(self):
- self.sendRequest(u'list "date" "album" "analbum"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "date" "album" "analbum"')
+ self.assertInResponse('OK')
def test_list_date_by_full_date(self):
- self.sendRequest(u'list "date" "date" "2001-01-01"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "date" "date" "2001-01-01"')
+ self.assertInResponse('OK')
def test_list_date_by_year(self):
- self.sendRequest(u'list "date" "date" "2001"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "date" "date" "2001"')
+ self.assertInResponse('OK')
def test_list_date_by_genre(self):
- self.sendRequest(u'list "date" "genre" "agenre"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "date" "genre" "agenre"')
+ self.assertInResponse('OK')
def test_list_date_by_artist_and_album(self):
- self.sendRequest(u'list "date" "artist" "anartist" "album" "analbum"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "date" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse('OK')
+
+ def test_list_date_without_filter_value(self):
+ self.sendRequest('list "date" "artist" ""')
+ self.assertInResponse('OK')
+
+ def test_list_date_should_not_return_blank_dates(self):
+ self.backend.library.dummy_find_exact_result = [Track(date='')]
+
+ self.sendRequest('list "date"')
+ self.assertNotInResponse('Date: ')
+ self.assertInResponse('OK')
### Genre
def test_list_genre_with_quotes(self):
- self.sendRequest(u'list "genre"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "genre"')
+ self.assertInResponse('OK')
def test_list_genre_without_quotes(self):
- self.sendRequest(u'list genre')
- self.assertInResponse(u'OK')
+ self.sendRequest('list genre')
+ self.assertInResponse('OK')
def test_list_genre_without_quotes_and_capitalized(self):
- self.sendRequest(u'list Genre')
- self.assertInResponse(u'OK')
+ self.sendRequest('list Genre')
+ self.assertInResponse('OK')
def test_list_genre_with_query_of_one_token(self):
- self.sendRequest(u'list "genre" "anartist"')
+ self.sendRequest('list "genre" "anartist"')
self.assertEqualResponse(
- u'ACK [2@0] {list} should be "Album" for 3 arguments')
+ 'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_genre_by_artist(self):
- self.sendRequest(u'list "genre" "artist" "anartist"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "genre" "artist" "anartist"')
+ self.assertInResponse('OK')
def test_list_genre_by_album(self):
- self.sendRequest(u'list "genre" "album" "analbum"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "genre" "album" "analbum"')
+ self.assertInResponse('OK')
def test_list_genre_by_full_date(self):
- self.sendRequest(u'list "genre" "date" "2001-01-01"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "genre" "date" "2001-01-01"')
+ self.assertInResponse('OK')
def test_list_genre_by_year(self):
- self.sendRequest(u'list "genre" "date" "2001"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "genre" "date" "2001"')
+ self.assertInResponse('OK')
def test_list_genre_by_genre(self):
- self.sendRequest(u'list "genre" "genre" "agenre"')
- self.assertInResponse(u'OK')
+ self.sendRequest('list "genre" "genre" "agenre"')
+ self.assertInResponse('OK')
def test_list_genre_by_artist_and_album(self):
self.sendRequest(
- u'list "genre" "artist" "anartist" "album" "analbum"')
- self.assertInResponse(u'OK')
+ 'list "genre" "artist" "anartist" "album" "analbum"')
+ self.assertInResponse('OK')
+
+ def test_list_genre_without_filter_value(self):
+ self.sendRequest('list "genre" "artist" ""')
+ self.assertInResponse('OK')
class MusicDatabaseSearchTest(protocol.BaseTestCase):
def test_search_album(self):
- self.sendRequest(u'search "album" "analbum"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search "album" "analbum"')
+ self.assertInResponse('OK')
def test_search_album_without_quotes(self):
- self.sendRequest(u'search album "analbum"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search album "analbum"')
+ self.assertInResponse('OK')
+
+ def test_search_album_without_filter_value(self):
+ self.sendRequest('search "album" ""')
+ self.assertInResponse('OK')
def test_search_artist(self):
- self.sendRequest(u'search "artist" "anartist"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search "artist" "anartist"')
+ self.assertInResponse('OK')
def test_search_artist_without_quotes(self):
- self.sendRequest(u'search artist "anartist"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search artist "anartist"')
+ self.assertInResponse('OK')
+
+ def test_search_artist_without_filter_value(self):
+ self.sendRequest('search "artist" ""')
+ self.assertInResponse('OK')
def test_search_filename(self):
- self.sendRequest(u'search "filename" "afilename"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search "filename" "afilename"')
+ self.assertInResponse('OK')
def test_search_filename_without_quotes(self):
- self.sendRequest(u'search filename "afilename"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search filename "afilename"')
+ self.assertInResponse('OK')
+
+ def test_search_filename_without_filter_value(self):
+ self.sendRequest('search "filename" ""')
+ self.assertInResponse('OK')
+
+ def test_search_file(self):
+ self.sendRequest('search "file" "afilename"')
+ self.assertInResponse('OK')
+
+ def test_search_file_without_quotes(self):
+ self.sendRequest('search file "afilename"')
+ self.assertInResponse('OK')
+
+ def test_search_file_without_filter_value(self):
+ self.sendRequest('search "file" ""')
+ self.assertInResponse('OK')
def test_search_title(self):
- self.sendRequest(u'search "title" "atitle"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search "title" "atitle"')
+ self.assertInResponse('OK')
def test_search_title_without_quotes(self):
- self.sendRequest(u'search title "atitle"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search title "atitle"')
+ self.assertInResponse('OK')
+
+ def test_search_title_without_filter_value(self):
+ self.sendRequest('search "title" ""')
+ self.assertInResponse('OK')
def test_search_any(self):
- self.sendRequest(u'search "any" "anything"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search "any" "anything"')
+ self.assertInResponse('OK')
def test_search_any_without_quotes(self):
- self.sendRequest(u'search any "anything"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search any "anything"')
+ self.assertInResponse('OK')
+
+ def test_search_any_without_filter_value(self):
+ self.sendRequest('search "any" ""')
+ self.assertInResponse('OK')
def test_search_date(self):
- self.sendRequest(u'search "date" "2002-01-01"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search "date" "2002-01-01"')
+ self.assertInResponse('OK')
def test_search_date_without_quotes(self):
- self.sendRequest(u'search date "2002-01-01"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search date "2002-01-01"')
+ self.assertInResponse('OK')
def test_search_date_with_capital_d_and_incomplete_date(self):
- self.sendRequest(u'search Date "2005"')
- self.assertInResponse(u'OK')
+ self.sendRequest('search Date "2005"')
+ self.assertInResponse('OK')
+
+ def test_search_date_without_filter_value(self):
+ self.sendRequest('search "date" ""')
+ self.assertInResponse('OK')
def test_search_else_should_fail(self):
- self.sendRequest(u'search "sometype" "something"')
- self.assertEqualResponse(u'ACK [2@0] {search} incorrect arguments')
+ self.sendRequest('search "sometype" "something"')
+ self.assertEqualResponse('ACK [2@0] {search} incorrect arguments')
diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py
index 4f8f7430..14168a35 100644
--- a/tests/frontends/mpd/protocol/playback_test.py
+++ b/tests/frontends/mpd/protocol/playback_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from mopidy.core import PlaybackState
from mopidy.models import Track
@@ -12,140 +14,140 @@ STOPPED = PlaybackState.STOPPED
class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_consume_off(self):
- self.sendRequest(u'consume "0"')
- self.assertFalse(self.backend.playback.consume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('consume "0"')
+ self.assertFalse(self.core.playback.consume.get())
+ self.assertInResponse('OK')
def test_consume_off_without_quotes(self):
- self.sendRequest(u'consume 0')
- self.assertFalse(self.backend.playback.consume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('consume 0')
+ self.assertFalse(self.core.playback.consume.get())
+ self.assertInResponse('OK')
def test_consume_on(self):
- self.sendRequest(u'consume "1"')
- self.assertTrue(self.backend.playback.consume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('consume "1"')
+ self.assertTrue(self.core.playback.consume.get())
+ self.assertInResponse('OK')
def test_consume_on_without_quotes(self):
- self.sendRequest(u'consume 1')
- self.assertTrue(self.backend.playback.consume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('consume 1')
+ self.assertTrue(self.core.playback.consume.get())
+ self.assertInResponse('OK')
def test_crossfade(self):
- self.sendRequest(u'crossfade "10"')
- self.assertInResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('crossfade "10"')
+ self.assertInResponse('ACK [0@0] {} Not implemented')
def test_random_off(self):
- self.sendRequest(u'random "0"')
- self.assertFalse(self.backend.playback.random.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('random "0"')
+ self.assertFalse(self.core.playback.random.get())
+ self.assertInResponse('OK')
def test_random_off_without_quotes(self):
- self.sendRequest(u'random 0')
- self.assertFalse(self.backend.playback.random.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('random 0')
+ self.assertFalse(self.core.playback.random.get())
+ self.assertInResponse('OK')
def test_random_on(self):
- self.sendRequest(u'random "1"')
- self.assertTrue(self.backend.playback.random.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('random "1"')
+ self.assertTrue(self.core.playback.random.get())
+ self.assertInResponse('OK')
def test_random_on_without_quotes(self):
- self.sendRequest(u'random 1')
- self.assertTrue(self.backend.playback.random.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('random 1')
+ self.assertTrue(self.core.playback.random.get())
+ self.assertInResponse('OK')
def test_repeat_off(self):
- self.sendRequest(u'repeat "0"')
- self.assertFalse(self.backend.playback.repeat.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('repeat "0"')
+ self.assertFalse(self.core.playback.repeat.get())
+ self.assertInResponse('OK')
def test_repeat_off_without_quotes(self):
- self.sendRequest(u'repeat 0')
- self.assertFalse(self.backend.playback.repeat.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('repeat 0')
+ self.assertFalse(self.core.playback.repeat.get())
+ self.assertInResponse('OK')
def test_repeat_on(self):
- self.sendRequest(u'repeat "1"')
- self.assertTrue(self.backend.playback.repeat.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('repeat "1"')
+ self.assertTrue(self.core.playback.repeat.get())
+ self.assertInResponse('OK')
def test_repeat_on_without_quotes(self):
- self.sendRequest(u'repeat 1')
- self.assertTrue(self.backend.playback.repeat.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('repeat 1')
+ self.assertTrue(self.core.playback.repeat.get())
+ self.assertInResponse('OK')
def test_setvol_below_min(self):
- self.sendRequest(u'setvol "-10"')
- self.assertEqual(0, self.backend.playback.volume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('setvol "-10"')
+ self.assertEqual(0, self.core.playback.volume.get())
+ self.assertInResponse('OK')
def test_setvol_min(self):
- self.sendRequest(u'setvol "0"')
- self.assertEqual(0, self.backend.playback.volume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('setvol "0"')
+ self.assertEqual(0, self.core.playback.volume.get())
+ self.assertInResponse('OK')
def test_setvol_middle(self):
- self.sendRequest(u'setvol "50"')
- self.assertEqual(50, self.backend.playback.volume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('setvol "50"')
+ self.assertEqual(50, self.core.playback.volume.get())
+ self.assertInResponse('OK')
def test_setvol_max(self):
- self.sendRequest(u'setvol "100"')
- self.assertEqual(100, self.backend.playback.volume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('setvol "100"')
+ self.assertEqual(100, self.core.playback.volume.get())
+ self.assertInResponse('OK')
def test_setvol_above_max(self):
- self.sendRequest(u'setvol "110"')
- self.assertEqual(100, self.backend.playback.volume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('setvol "110"')
+ self.assertEqual(100, self.core.playback.volume.get())
+ self.assertInResponse('OK')
def test_setvol_plus_is_ignored(self):
- self.sendRequest(u'setvol "+10"')
- self.assertEqual(10, self.backend.playback.volume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('setvol "+10"')
+ self.assertEqual(10, self.core.playback.volume.get())
+ self.assertInResponse('OK')
def test_setvol_without_quotes(self):
- self.sendRequest(u'setvol 50')
- self.assertEqual(50, self.backend.playback.volume.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('setvol 50')
+ self.assertEqual(50, self.core.playback.volume.get())
+ self.assertInResponse('OK')
def test_single_off(self):
- self.sendRequest(u'single "0"')
- self.assertFalse(self.backend.playback.single.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('single "0"')
+ self.assertFalse(self.core.playback.single.get())
+ self.assertInResponse('OK')
def test_single_off_without_quotes(self):
- self.sendRequest(u'single 0')
- self.assertFalse(self.backend.playback.single.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('single 0')
+ self.assertFalse(self.core.playback.single.get())
+ self.assertInResponse('OK')
def test_single_on(self):
- self.sendRequest(u'single "1"')
- self.assertTrue(self.backend.playback.single.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('single "1"')
+ self.assertTrue(self.core.playback.single.get())
+ self.assertInResponse('OK')
def test_single_on_without_quotes(self):
- self.sendRequest(u'single 1')
- self.assertTrue(self.backend.playback.single.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('single 1')
+ self.assertTrue(self.core.playback.single.get())
+ self.assertInResponse('OK')
def test_replay_gain_mode_off(self):
- self.sendRequest(u'replay_gain_mode "off"')
- self.assertInResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('replay_gain_mode "off"')
+ self.assertInResponse('ACK [0@0] {} Not implemented')
def test_replay_gain_mode_track(self):
- self.sendRequest(u'replay_gain_mode "track"')
- self.assertInResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('replay_gain_mode "track"')
+ self.assertInResponse('ACK [0@0] {} Not implemented')
def test_replay_gain_mode_album(self):
- self.sendRequest(u'replay_gain_mode "album"')
- self.assertInResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('replay_gain_mode "album"')
+ self.assertInResponse('ACK [0@0] {} Not implemented')
def test_replay_gain_status_default(self):
- self.sendRequest(u'replay_gain_status')
- self.assertInResponse(u'OK')
- self.assertInResponse(u'off')
+ self.sendRequest('replay_gain_status')
+ self.assertInResponse('OK')
+ self.assertInResponse('off')
@unittest.SkipTest
def test_replay_gain_status_off(self):
@@ -162,244 +164,257 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
class PlaybackControlHandlerTest(protocol.BaseTestCase):
def test_next(self):
- self.sendRequest(u'next')
- self.assertInResponse(u'OK')
+ self.sendRequest('next')
+ self.assertInResponse('OK')
def test_pause_off(self):
- self.backend.current_playlist.append([Track()])
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'play "0"')
- self.sendRequest(u'pause "1"')
- self.sendRequest(u'pause "0"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('play "0"')
+ self.sendRequest('pause "1"')
+ self.sendRequest('pause "0"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertInResponse('OK')
def test_pause_on(self):
- self.backend.current_playlist.append([Track()])
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'play "0"')
- self.sendRequest(u'pause "1"')
- self.assertEqual(PAUSED, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('play "0"')
+ self.sendRequest('pause "1"')
+ self.assertEqual(PAUSED, self.core.playback.state.get())
+ self.assertInResponse('OK')
def test_pause_toggle(self):
- self.backend.current_playlist.append([Track()])
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'play "0"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('play "0"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertInResponse('OK')
- self.sendRequest(u'pause')
- self.assertEqual(PAUSED, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('pause')
+ self.assertEqual(PAUSED, self.core.playback.state.get())
+ self.assertInResponse('OK')
- self.sendRequest(u'pause')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('pause')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertInResponse('OK')
def test_play_without_pos(self):
- self.backend.current_playlist.append([Track()])
- self.backend.playback.state = PAUSED
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'play')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('play')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertInResponse('OK')
def test_play_with_pos(self):
- self.backend.current_playlist.append([Track()])
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'play "0"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('play "0"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertInResponse('OK')
def test_play_with_pos_without_quotes(self):
- self.backend.current_playlist.append([Track()])
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'play 0')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('play 0')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertInResponse('OK')
def test_play_with_pos_out_of_bounds(self):
- self.backend.current_playlist.append([])
+ self.core.tracklist.add([])
- self.sendRequest(u'play "0"')
- self.assertEqual(STOPPED, self.backend.playback.state.get())
- self.assertInResponse(u'ACK [2@0] {play} Bad song index')
+ self.sendRequest('play "0"')
+ self.assertEqual(STOPPED, self.core.playback.state.get())
+ self.assertInResponse('ACK [2@0] {play} Bad song index')
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
- self.assertEqual(self.backend.playback.current_track.get(), None)
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ self.assertEqual(self.core.playback.current_track.get(), None)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
- self.sendRequest(u'play "-1"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual('a', self.backend.playback.current_track.get().uri)
- self.assertInResponse(u'OK')
+ self.sendRequest('play "-1"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertEqual(
+ 'dummy:a', self.core.playback.current_track.get().uri)
+ self.assertInResponse('OK')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(self.backend.playback.current_track.get(), None)
- self.backend.playback.play()
- self.backend.playback.next()
- self.backend.playback.stop()
- self.assertNotEqual(self.backend.playback.current_track.get(), None)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.assertEqual(self.core.playback.current_track.get(), None)
+ self.core.playback.play()
+ self.core.playback.next()
+ self.core.playback.stop()
+ self.assertNotEqual(self.core.playback.current_track.get(), None)
- self.sendRequest(u'play "-1"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual('b', self.backend.playback.current_track.get().uri)
- self.assertInResponse(u'OK')
+ self.sendRequest('play "-1"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertEqual(
+ 'dummy:b', self.core.playback.current_track.get().uri)
+ self.assertInResponse('OK')
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
- self.backend.current_playlist.clear()
+ self.core.tracklist.clear()
- self.sendRequest(u'play "-1"')
- self.assertEqual(STOPPED, self.backend.playback.state.get())
- self.assertEqual(None, self.backend.playback.current_track.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('play "-1"')
+ self.assertEqual(STOPPED, self.core.playback.state.get())
+ self.assertEqual(None, self.core.playback.current_track.get())
+ self.assertInResponse('OK')
def test_play_minus_is_ignored_if_playing(self):
- self.backend.current_playlist.append([Track(length=40000)])
- self.backend.playback.seek(30000)
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertEquals(PLAYING, self.backend.playback.state.get())
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.seek(30000)
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertEquals(PLAYING, self.core.playback.state.get())
- self.sendRequest(u'play "-1"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertInResponse(u'OK')
+ self.sendRequest('play "-1"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertInResponse('OK')
def test_play_minus_one_resumes_if_paused(self):
- self.backend.current_playlist.append([Track(length=40000)])
- self.backend.playback.seek(30000)
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertEquals(PLAYING, self.backend.playback.state.get())
- self.backend.playback.pause()
- self.assertEquals(PAUSED, self.backend.playback.state.get())
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.seek(30000)
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertEquals(PLAYING, self.core.playback.state.get())
+ self.core.playback.pause()
+ self.assertEquals(PAUSED, self.core.playback.state.get())
- self.sendRequest(u'play "-1"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertInResponse(u'OK')
+ self.sendRequest('play "-1"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertInResponse('OK')
def test_playid(self):
- self.backend.current_playlist.append([Track()])
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'playid "0"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('playid "0"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertInResponse('OK')
def test_playid_without_quotes(self):
- self.backend.current_playlist.append([Track()])
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'playid 0')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('playid 0')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertInResponse('OK')
- def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
- self.assertEqual(self.backend.playback.current_track.get(), None)
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
+ def test_playid_minus_1_plays_first_in_playlist_if_no_current_track(self):
+ self.assertEqual(self.core.playback.current_track.get(), None)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
- self.sendRequest(u'playid "-1"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual('a', self.backend.playback.current_track.get().uri)
- self.assertInResponse(u'OK')
+ self.sendRequest('playid "-1"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertEqual(
+ 'dummy:a', self.core.playback.current_track.get().uri)
+ self.assertInResponse('OK')
- def test_playid_minus_one_plays_current_track_if_current_track_is_set(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(self.backend.playback.current_track.get(), None)
- self.backend.playback.play()
- self.backend.playback.next()
- self.backend.playback.stop()
- self.assertNotEqual(None, self.backend.playback.current_track.get())
+ def test_playid_minus_1_plays_current_track_if_current_track_is_set(self):
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.assertEqual(self.core.playback.current_track.get(), None)
+ self.core.playback.play()
+ self.core.playback.next()
+ self.core.playback.stop()
+ self.assertNotEqual(None, self.core.playback.current_track.get())
- self.sendRequest(u'playid "-1"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assertEqual('b', self.backend.playback.current_track.get().uri)
- self.assertInResponse(u'OK')
+ self.sendRequest('playid "-1"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertEqual(
+ 'dummy:b', self.core.playback.current_track.get().uri)
+ self.assertInResponse('OK')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
- self.backend.current_playlist.clear()
+ self.core.tracklist.clear()
- self.sendRequest(u'playid "-1"')
- self.assertEqual(STOPPED, self.backend.playback.state.get())
- self.assertEqual(None, self.backend.playback.current_track.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('playid "-1"')
+ self.assertEqual(STOPPED, self.core.playback.state.get())
+ self.assertEqual(None, self.core.playback.current_track.get())
+ self.assertInResponse('OK')
def test_playid_minus_is_ignored_if_playing(self):
- self.backend.current_playlist.append([Track(length=40000)])
- self.backend.playback.seek(30000)
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertEquals(PLAYING, self.backend.playback.state.get())
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.seek(30000)
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertEquals(PLAYING, self.core.playback.state.get())
- self.sendRequest(u'playid "-1"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertInResponse(u'OK')
+ self.sendRequest('playid "-1"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertInResponse('OK')
def test_playid_minus_one_resumes_if_paused(self):
- self.backend.current_playlist.append([Track(length=40000)])
- self.backend.playback.seek(30000)
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertEquals(PLAYING, self.backend.playback.state.get())
- self.backend.playback.pause()
- self.assertEquals(PAUSED, self.backend.playback.state.get())
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.seek(30000)
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertEquals(PLAYING, self.core.playback.state.get())
+ self.core.playback.pause()
+ self.assertEquals(PAUSED, self.core.playback.state.get())
- self.sendRequest(u'playid "-1"')
- self.assertEqual(PLAYING, self.backend.playback.state.get())
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertInResponse(u'OK')
+ self.sendRequest('playid "-1"')
+ self.assertEqual(PLAYING, self.core.playback.state.get())
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertInResponse('OK')
def test_playid_which_does_not_exist(self):
- self.backend.current_playlist.append([Track()])
+ self.core.tracklist.add([Track(uri='dummy:a')])
- self.sendRequest(u'playid "12345"')
- self.assertInResponse(u'ACK [50@0] {playid} No such song')
+ self.sendRequest('playid "12345"')
+ self.assertInResponse('ACK [50@0] {playid} No such song')
def test_previous(self):
- self.sendRequest(u'previous')
- self.assertInResponse(u'OK')
+ self.sendRequest('previous')
+ self.assertInResponse('OK')
def test_seek(self):
- self.backend.current_playlist.append([Track(length=40000)])
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
- self.sendRequest(u'seek "0"')
- self.sendRequest(u'seek "0" "30"')
- self.assert_(self.backend.playback.time_position >= 30000)
- self.assertInResponse(u'OK')
+ self.sendRequest('seek "0"')
+ self.sendRequest('seek "0" "30"')
+ self.assertGreaterEqual(self.core.playback.time_position, 30000)
+ self.assertInResponse('OK')
def test_seek_with_songpos(self):
- seek_track = Track(uri='2', length=40000)
- self.backend.current_playlist.append(
- [Track(uri='1', length=40000), seek_track])
+ seek_track = Track(uri='dummy:b', length=40000)
+ self.core.tracklist.add(
+ [Track(uri='dummy:a', length=40000), seek_track])
- self.sendRequest(u'seek "1" "30"')
- self.assertEqual(self.backend.playback.current_track.get(), seek_track)
- self.assertInResponse(u'OK')
+ self.sendRequest('seek "1" "30"')
+ self.assertEqual(self.core.playback.current_track.get(), seek_track)
+ self.assertInResponse('OK')
def test_seek_without_quotes(self):
- self.backend.current_playlist.append([Track(length=40000)])
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
- self.sendRequest(u'seek 0')
- self.sendRequest(u'seek 0 30')
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertInResponse(u'OK')
+ self.sendRequest('seek 0')
+ self.sendRequest('seek 0 30')
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertInResponse('OK')
def test_seekid(self):
- self.backend.current_playlist.append([Track(length=40000)])
- self.sendRequest(u'seekid "0" "30"')
- self.assert_(self.backend.playback.time_position.get() >= 30000)
- self.assertInResponse(u'OK')
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.sendRequest('seekid "0" "30"')
+ self.assertGreaterEqual(
+ self.core.playback.time_position.get(), 30000)
+ self.assertInResponse('OK')
- def test_seekid_with_cpid(self):
- seek_track = Track(uri='2', length=40000)
- self.backend.current_playlist.append(
- [Track(length=40000), seek_track])
+ def test_seekid_with_tlid(self):
+ seek_track = Track(uri='dummy:b', length=40000)
+ self.core.tracklist.add(
+ [Track(uri='dummy:a', length=40000), seek_track])
- self.sendRequest(u'seekid "1" "30"')
- self.assertEqual(1, self.backend.playback.current_cpid.get())
- self.assertEqual(seek_track, self.backend.playback.current_track.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('seekid "1" "30"')
+ self.assertEqual(1, self.core.playback.current_tl_track.get().tlid)
+ self.assertEqual(seek_track, self.core.playback.current_track.get())
+ self.assertInResponse('OK')
def test_stop(self):
- self.sendRequest(u'stop')
- self.assertEqual(STOPPED, self.backend.playback.state.get())
- self.assertInResponse(u'OK')
+ self.sendRequest('stop')
+ self.assertEqual(STOPPED, self.core.playback.state.get())
+ self.assertInResponse('OK')
diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py
index 8bd9b7e0..9c07f104 100644
--- a/tests/frontends/mpd/protocol/reflection_test.py
+++ b/tests/frontends/mpd/protocol/reflection_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from mopidy import settings
from tests.frontends.mpd import protocol
@@ -5,63 +7,63 @@ from tests.frontends.mpd import protocol
class ReflectionHandlerTest(protocol.BaseTestCase):
def test_commands_returns_list_of_all_commands(self):
- self.sendRequest(u'commands')
+ self.sendRequest('commands')
# Check if some random commands are included
- self.assertInResponse(u'command: commands')
- self.assertInResponse(u'command: play')
- self.assertInResponse(u'command: status')
+ self.assertInResponse('command: commands')
+ self.assertInResponse('command: play')
+ self.assertInResponse('command: status')
# Check if commands you do not have access to are not present
- self.assertNotInResponse(u'command: kill')
+ self.assertNotInResponse('command: kill')
# Check if the blacklisted commands are not present
- self.assertNotInResponse(u'command: command_list_begin')
- self.assertNotInResponse(u'command: command_list_ok_begin')
- self.assertNotInResponse(u'command: command_list_end')
- self.assertNotInResponse(u'command: idle')
- self.assertNotInResponse(u'command: noidle')
- self.assertNotInResponse(u'command: sticker')
- self.assertInResponse(u'OK')
+ self.assertNotInResponse('command: command_list_begin')
+ self.assertNotInResponse('command: command_list_ok_begin')
+ self.assertNotInResponse('command: command_list_end')
+ self.assertNotInResponse('command: idle')
+ self.assertNotInResponse('command: noidle')
+ self.assertNotInResponse('command: sticker')
+ self.assertInResponse('OK')
def test_commands_show_less_if_auth_required_and_not_authed(self):
settings.MPD_SERVER_PASSWORD = u'secret'
- self.sendRequest(u'commands')
+ self.sendRequest('commands')
# Not requiring auth
- self.assertInResponse(u'command: close')
- self.assertInResponse(u'command: commands')
- self.assertInResponse(u'command: notcommands')
- self.assertInResponse(u'command: password')
- self.assertInResponse(u'command: ping')
+ self.assertInResponse('command: close')
+ self.assertInResponse('command: commands')
+ self.assertInResponse('command: notcommands')
+ self.assertInResponse('command: password')
+ self.assertInResponse('command: ping')
# Requiring auth
- self.assertNotInResponse(u'command: play')
- self.assertNotInResponse(u'command: status')
+ self.assertNotInResponse('command: play')
+ self.assertNotInResponse('command: status')
def test_decoders(self):
- self.sendRequest(u'decoders')
- self.assertInResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('decoders')
+ self.assertInResponse('OK')
def test_notcommands_returns_only_kill_and_ok(self):
- response = self.sendRequest(u'notcommands')
+ response = self.sendRequest('notcommands')
self.assertEqual(2, len(response))
- self.assertInResponse(u'command: kill')
- self.assertInResponse(u'OK')
+ self.assertInResponse('command: kill')
+ self.assertInResponse('OK')
def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
settings.MPD_SERVER_PASSWORD = u'secret'
- self.sendRequest(u'notcommands')
+ self.sendRequest('notcommands')
# Not requiring auth
- self.assertNotInResponse(u'command: close')
- self.assertNotInResponse(u'command: commands')
- self.assertNotInResponse(u'command: notcommands')
- self.assertNotInResponse(u'command: password')
- self.assertNotInResponse(u'command: ping')
+ self.assertNotInResponse('command: close')
+ self.assertNotInResponse('command: commands')
+ self.assertNotInResponse('command: notcommands')
+ self.assertNotInResponse('command: password')
+ self.assertNotInResponse('command: ping')
# Requiring auth
- self.assertInResponse(u'command: play')
- self.assertInResponse(u'command: status')
+ self.assertInResponse('command: play')
+ self.assertInResponse('command: status')
def test_tagtypes(self):
- self.sendRequest(u'tagtypes')
- self.assertInResponse(u'OK')
+ self.sendRequest('tagtypes')
+ self.assertInResponse('OK')
def test_urlhandlers(self):
- self.sendRequest(u'urlhandlers')
- self.assertInResponse(u'OK')
- self.assertInResponse(u'handler: dummy')
+ self.sendRequest('urlhandlers')
+ self.assertInResponse('OK')
+ self.assertInResponse('handler: dummy')
diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py
index 7f214efa..0bc488fd 100644
--- a/tests/frontends/mpd/protocol/regression_test.py
+++ b/tests/frontends/mpd/protocol/regression_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import random
from mopidy.models import Track
@@ -16,23 +18,33 @@ class IssueGH17RegressionTest(protocol.BaseTestCase):
- Press next until you get to the unplayable track
"""
def test(self):
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), None,
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
- random.seed(1) # Playlist order: abcfde
+ self.core.tracklist.add([
+ Track(uri='dummy:a'),
+ Track(uri='dummy:b'),
+ Track(uri='dummy:error'),
+ Track(uri='dummy:d'),
+ Track(uri='dummy:e'),
+ Track(uri='dummy:f'),
+ ])
+ random.seed(1) # Playlist order: abcfde
- self.sendRequest(u'play')
- self.assertEquals('a', self.backend.playback.current_track.get().uri)
- self.sendRequest(u'random "1"')
- self.sendRequest(u'next')
- self.assertEquals('b', self.backend.playback.current_track.get().uri)
- self.sendRequest(u'next')
+ self.sendRequest('play')
+ self.assertEquals(
+ 'dummy:a', self.core.playback.current_track.get().uri)
+ self.sendRequest('random "1"')
+ self.sendRequest('next')
+ self.assertEquals(
+ 'dummy:b', self.core.playback.current_track.get().uri)
+ self.sendRequest('next')
# Should now be at track 'c', but playback fails and it skips ahead
- self.assertEquals('f', self.backend.playback.current_track.get().uri)
- self.sendRequest(u'next')
- self.assertEquals('d', self.backend.playback.current_track.get().uri)
- self.sendRequest(u'next')
- self.assertEquals('e', self.backend.playback.current_track.get().uri)
+ self.assertEquals(
+ 'dummy:f', self.core.playback.current_track.get().uri)
+ self.sendRequest('next')
+ self.assertEquals(
+ 'dummy:d', self.core.playback.current_track.get().uri)
+ self.sendRequest('next')
+ self.assertEquals(
+ 'dummy:e', self.core.playback.current_track.get().uri)
class IssueGH18RegressionTest(protocol.BaseTestCase):
@@ -47,26 +59,26 @@ class IssueGH18RegressionTest(protocol.BaseTestCase):
"""
def test(self):
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ self.core.tracklist.add([
+ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'),
+ Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')])
random.seed(1)
- self.sendRequest(u'play')
- self.sendRequest(u'random "1"')
- self.sendRequest(u'next')
- self.sendRequest(u'random "0"')
- self.sendRequest(u'next')
+ self.sendRequest('play')
+ self.sendRequest('random "1"')
+ self.sendRequest('next')
+ self.sendRequest('random "0"')
+ self.sendRequest('next')
- self.sendRequest(u'next')
- cp_track_1 = self.backend.playback.current_cp_track.get()
- self.sendRequest(u'next')
- cp_track_2 = self.backend.playback.current_cp_track.get()
- self.sendRequest(u'next')
- cp_track_3 = self.backend.playback.current_cp_track.get()
+ self.sendRequest('next')
+ tl_track_1 = self.core.playback.current_tl_track.get()
+ self.sendRequest('next')
+ tl_track_2 = self.core.playback.current_tl_track.get()
+ self.sendRequest('next')
+ tl_track_3 = self.core.playback.current_tl_track.get()
- self.assertNotEqual(cp_track_1, cp_track_2)
- self.assertNotEqual(cp_track_2, cp_track_3)
+ self.assertNotEqual(tl_track_1, tl_track_2)
+ self.assertNotEqual(tl_track_2, tl_track_3)
class IssueGH22RegressionTest(protocol.BaseTestCase):
@@ -83,20 +95,20 @@ class IssueGH22RegressionTest(protocol.BaseTestCase):
"""
def test(self):
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ self.core.tracklist.add([
+ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'),
+ Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')])
random.seed(1)
- self.sendRequest(u'play')
- self.sendRequest(u'random "1"')
- self.sendRequest(u'deleteid "1"')
- self.sendRequest(u'deleteid "2"')
- self.sendRequest(u'deleteid "3"')
- self.sendRequest(u'deleteid "4"')
- self.sendRequest(u'deleteid "5"')
- self.sendRequest(u'deleteid "6"')
- self.sendRequest(u'status')
+ self.sendRequest('play')
+ self.sendRequest('random "1"')
+ self.sendRequest('deleteid "1"')
+ self.sendRequest('deleteid "2"')
+ self.sendRequest('deleteid "3"')
+ self.sendRequest('deleteid "4"')
+ self.sendRequest('deleteid "5"')
+ self.sendRequest('deleteid "6"')
+ self.sendRequest('status')
class IssueGH69RegressionTest(protocol.BaseTestCase):
@@ -111,15 +123,15 @@ class IssueGH69RegressionTest(protocol.BaseTestCase):
"""
def test(self):
- self.backend.stored_playlists.create('foo')
- self.backend.current_playlist.append([
- Track(uri='a'), Track(uri='b'), Track(uri='c'),
- Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ self.core.playlists.create('foo')
+ self.core.tracklist.add([
+ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'),
+ Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')])
- self.sendRequest(u'play')
- self.sendRequest(u'stop')
- self.sendRequest(u'clear')
- self.sendRequest(u'load "foo"')
+ self.sendRequest('play')
+ self.sendRequest('stop')
+ self.sendRequest('clear')
+ self.sendRequest('load "foo"')
self.assertNotInResponse('song: None')
@@ -136,10 +148,10 @@ class IssueGH113RegressionTest(protocol.BaseTestCase):
"""
def test(self):
- self.backend.stored_playlists.create(
+ self.core.playlists.create(
u'all lart spotify:track:\w\{22\} pastes')
- self.sendRequest(u'lsinfo "/"')
+ self.sendRequest('lsinfo "/"')
self.assertInResponse(
u'playlist: all lart spotify:track:\w\{22\} pastes')
@@ -158,7 +170,8 @@ class IssueGH137RegressionTest(protocol.BaseTestCase):
"""
def test(self):
- self.sendRequest(u'list Date Artist "Anita Ward" '
+ self.sendRequest(
+ u'list Date Artist "Anita Ward" '
u'Album "This Is Remixed Hits - Mashups & Rare 12" Mixes"')
self.assertInResponse('ACK [2@0] {list} Invalid unquoted character')
diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py
index e6572eab..24f24ab2 100644
--- a/tests/frontends/mpd/protocol/status_test.py
+++ b/tests/frontends/mpd/protocol/status_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from mopidy.models import Track
from tests.frontends.mpd import protocol
@@ -5,33 +7,33 @@ from tests.frontends.mpd import protocol
class StatusHandlerTest(protocol.BaseTestCase):
def test_clearerror(self):
- self.sendRequest(u'clearerror')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('clearerror')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_currentsong(self):
track = Track()
- self.backend.current_playlist.append([track])
- self.backend.playback.play()
- self.sendRequest(u'currentsong')
- self.assertInResponse(u'file: ')
- self.assertInResponse(u'Time: 0')
- self.assertInResponse(u'Artist: ')
- self.assertInResponse(u'Title: ')
- self.assertInResponse(u'Album: ')
- self.assertInResponse(u'Track: 0')
- self.assertInResponse(u'Date: ')
- self.assertInResponse(u'Pos: 0')
- self.assertInResponse(u'Id: 0')
- self.assertInResponse(u'OK')
+ self.core.tracklist.add([track])
+ self.core.playback.play()
+ self.sendRequest('currentsong')
+ self.assertInResponse('file: ')
+ self.assertInResponse('Time: 0')
+ self.assertInResponse('Artist: ')
+ self.assertInResponse('Title: ')
+ self.assertInResponse('Album: ')
+ self.assertInResponse('Track: 0')
+ self.assertInResponse('Date: ')
+ self.assertInResponse('Pos: 0')
+ self.assertInResponse('Id: 0')
+ self.assertInResponse('OK')
def test_currentsong_without_song(self):
- self.sendRequest(u'currentsong')
- self.assertInResponse(u'OK')
+ self.sendRequest('currentsong')
+ self.assertInResponse('OK')
def test_stats_command(self):
- self.sendRequest(u'stats')
- self.assertInResponse(u'OK')
+ self.sendRequest('stats')
+ self.assertInResponse('OK')
def test_status_command(self):
- self.sendRequest(u'status')
- self.assertInResponse(u'OK')
+ self.sendRequest('status')
+ self.assertInResponse('OK')
diff --git a/tests/frontends/mpd/protocol/stickers_test.py b/tests/frontends/mpd/protocol/stickers_test.py
index 3e8b687f..de610521 100644
--- a/tests/frontends/mpd/protocol/stickers_test.py
+++ b/tests/frontends/mpd/protocol/stickers_test.py
@@ -1,33 +1,35 @@
+from __future__ import unicode_literals
+
from tests.frontends.mpd import protocol
class StickersHandlerTest(protocol.BaseTestCase):
def test_sticker_get(self):
self.sendRequest(
- u'sticker get "song" "file:///dev/urandom" "a_name"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ 'sticker get "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_sticker_set(self):
self.sendRequest(
- u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ 'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_sticker_delete_with_name(self):
self.sendRequest(
- u'sticker delete "song" "file:///dev/urandom" "a_name"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ 'sticker delete "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_sticker_delete_without_name(self):
self.sendRequest(
- u'sticker delete "song" "file:///dev/urandom"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ 'sticker delete "song" "file:///dev/urandom"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_sticker_list(self):
self.sendRequest(
- u'sticker list "song" "file:///dev/urandom"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ 'sticker list "song" "file:///dev/urandom"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_sticker_find(self):
self.sendRequest(
- u'sticker find "song" "file:///dev/urandom" "a_name"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ 'sticker find "song" "file:///dev/urandom" "a_name"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py
index 45d6a09a..be2afd4c 100644
--- a/tests/frontends/mpd/protocol/stored_playlists_test.py
+++ b/tests/frontends/mpd/protocol/stored_playlists_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import datetime
from mopidy.models import Track, Playlist
@@ -5,90 +7,118 @@ from mopidy.models import Track, Playlist
from tests.frontends.mpd import protocol
-class StoredPlaylistsHandlerTest(protocol.BaseTestCase):
+class PlaylistsHandlerTest(protocol.BaseTestCase):
def test_listplaylist(self):
- self.backend.stored_playlists.playlists = [
- Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
+ self.backend.playlists.playlists = [
+ Playlist(name='name', tracks=[Track(uri='dummy:a')])]
- self.sendRequest(u'listplaylist "name"')
- self.assertInResponse(u'file: file:///dev/urandom')
- self.assertInResponse(u'OK')
+ self.sendRequest('listplaylist "name"')
+ self.assertInResponse('file: dummy:a')
+ self.assertInResponse('OK')
+
+ def test_listplaylist_without_quotes(self):
+ self.backend.playlists.playlists = [
+ Playlist(name='name', tracks=[Track(uri='dummy:a')])]
+
+ self.sendRequest('listplaylist name')
+ self.assertInResponse('file: dummy:a')
+ self.assertInResponse('OK')
def test_listplaylist_fails_if_no_playlist_is_found(self):
- self.sendRequest(u'listplaylist "name"')
- self.assertEqualResponse(u'ACK [50@0] {listplaylist} No such playlist')
+ self.sendRequest('listplaylist "name"')
+ self.assertEqualResponse('ACK [50@0] {listplaylist} No such playlist')
def test_listplaylistinfo(self):
- self.backend.stored_playlists.playlists = [
- Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
+ self.backend.playlists.playlists = [
+ Playlist(name='name', tracks=[Track(uri='dummy:a')])]
- self.sendRequest(u'listplaylistinfo "name"')
- self.assertInResponse(u'file: file:///dev/urandom')
- self.assertInResponse(u'Track: 0')
- self.assertNotInResponse(u'Pos: 0')
- self.assertInResponse(u'OK')
+ self.sendRequest('listplaylistinfo "name"')
+ self.assertInResponse('file: dummy:a')
+ self.assertInResponse('Track: 0')
+ self.assertNotInResponse('Pos: 0')
+ self.assertInResponse('OK')
+
+ def test_listplaylistinfo_without_quotes(self):
+ self.backend.playlists.playlists = [
+ Playlist(name='name', tracks=[Track(uri='dummy:a')])]
+
+ self.sendRequest('listplaylistinfo name')
+ self.assertInResponse('file: dummy:a')
+ self.assertInResponse('Track: 0')
+ self.assertNotInResponse('Pos: 0')
+ self.assertInResponse('OK')
def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
- self.sendRequest(u'listplaylistinfo "name"')
+ self.sendRequest('listplaylistinfo "name"')
self.assertEqualResponse(
- u'ACK [50@0] {listplaylistinfo} No such playlist')
+ 'ACK [50@0] {listplaylistinfo} No such playlist')
def test_listplaylists(self):
last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345)
- self.backend.stored_playlists.playlists = [Playlist(name='a',
- last_modified=last_modified)]
+ self.backend.playlists.playlists = [
+ Playlist(name='a', last_modified=last_modified)]
- self.sendRequest(u'listplaylists')
- self.assertInResponse(u'playlist: a')
+ self.sendRequest('listplaylists')
+ self.assertInResponse('playlist: a')
# Date without microseconds and with time zone information
- self.assertInResponse(u'Last-Modified: 2001-03-17T13:41:17Z')
- self.assertInResponse(u'OK')
+ self.assertInResponse('Last-Modified: 2001-03-17T13:41:17Z')
+ self.assertInResponse('OK')
- def test_load_known_playlist_appends_to_current_playlist(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
- self.backend.stored_playlists.playlists = [Playlist(name='A-list',
- tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
+ def test_listplaylists_ignores_playlists_without_name(self):
+ last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345)
+ self.backend.playlists.playlists = [
+ Playlist(name='', last_modified=last_modified)]
- self.sendRequest(u'load "A-list"')
- tracks = self.backend.current_playlist.tracks.get()
+ self.sendRequest('listplaylists')
+ self.assertNotInResponse('playlist: ')
+ self.assertInResponse('OK')
+
+ def test_load_known_playlist_appends_to_tracklist(self):
+ self.core.tracklist.add([Track(uri='a'), Track(uri='b')])
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 2)
+ self.backend.playlists.playlists = [
+ Playlist(name='A-list', tracks=[
+ Track(uri='c'), Track(uri='d'), Track(uri='e')])]
+
+ self.sendRequest('load "A-list"')
+ tracks = self.core.tracklist.tracks.get()
self.assertEqual(5, len(tracks))
self.assertEqual('a', tracks[0].uri)
self.assertEqual('b', tracks[1].uri)
self.assertEqual('c', tracks[2].uri)
self.assertEqual('d', tracks[3].uri)
self.assertEqual('e', tracks[4].uri)
- self.assertInResponse(u'OK')
+ self.assertInResponse('OK')
def test_load_unknown_playlist_acks(self):
- self.sendRequest(u'load "unknown playlist"')
- self.assertEqual(0, len(self.backend.current_playlist.tracks.get()))
- self.assertEqualResponse(u'ACK [50@0] {load} No such playlist')
+ self.sendRequest('load "unknown playlist"')
+ self.assertEqual(0, len(self.core.tracklist.tracks.get()))
+ self.assertEqualResponse('ACK [50@0] {load} No such playlist')
def test_playlistadd(self):
- self.sendRequest(u'playlistadd "name" "file:///dev/urandom"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('playlistadd "name" "dummy:a"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_playlistclear(self):
- self.sendRequest(u'playlistclear "name"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('playlistclear "name"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_playlistdelete(self):
- self.sendRequest(u'playlistdelete "name" "5"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('playlistdelete "name" "5"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_playlistmove(self):
- self.sendRequest(u'playlistmove "name" "5" "10"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('playlistmove "name" "5" "10"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_rename(self):
- self.sendRequest(u'rename "old_name" "new_name"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('rename "old_name" "new_name"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_rm(self):
- self.sendRequest(u'rm "name"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('rm "name"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
def test_save(self):
- self.sendRequest(u'save "name"')
- self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
+ self.sendRequest('save "name"')
+ self.assertEqualResponse('ACK [0@0] {} Not implemented')
diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py
index e6cd80e2..711a069e 100644
--- a/tests/frontends/mpd/serializer_test.py
+++ b/tests/frontends/mpd/serializer_test.py
@@ -1,21 +1,23 @@
+from __future__ import unicode_literals
+
import datetime
import os
from mopidy import settings
from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol
-from mopidy.models import Album, Artist, CpTrack, Playlist, Track
+from mopidy.models import Album, Artist, TlTrack, Playlist, Track
from tests import unittest
class TrackMpdFormatTest(unittest.TestCase):
track = Track(
- uri=u'a uri',
- artists=[Artist(name=u'an artist')],
- name=u'a name',
- album=Album(name=u'an album', num_tracks=13,
- artists=[Artist(name=u'an other artist')]),
+ uri='a uri',
+ artists=[Artist(name='an artist')],
+ name='a name',
+ album=Album(name='an album', num_tracks=13,
+ artists=[Artist(name='an other artist')]),
track_no=7,
date=datetime.date(1977, 1, 1),
length=137000,
@@ -44,18 +46,19 @@ class TrackMpdFormatTest(unittest.TestCase):
result = translator.track_to_mpd_format(Track(), position=1)
self.assertNotIn(('Pos', 1), result)
- def test_track_to_mpd_format_with_cpid(self):
- result = translator.track_to_mpd_format(CpTrack(1, Track()))
+ def test_track_to_mpd_format_with_tlid(self):
+ result = translator.track_to_mpd_format(TlTrack(1, Track()))
self.assertNotIn(('Id', 1), result)
- def test_track_to_mpd_format_with_position_and_cpid(self):
- result = translator.track_to_mpd_format(CpTrack(2, Track()), position=1)
+ def test_track_to_mpd_format_with_position_and_tlid(self):
+ result = translator.track_to_mpd_format(
+ TlTrack(2, Track()), position=1)
self.assertIn(('Pos', 1), result)
self.assertIn(('Id', 2), result)
def test_track_to_mpd_format_for_nonempty_track(self):
result = translator.track_to_mpd_format(
- CpTrack(122, self.track), position=9)
+ TlTrack(122, self.track), position=9)
self.assertIn(('file', 'a uri'), result)
self.assertIn(('Time', 137), result)
self.assertIn(('Artist', 'an artist'), result)
@@ -79,7 +82,7 @@ class TrackMpdFormatTest(unittest.TestCase):
result = translator.track_to_mpd_format(track)
self.assertIn(('MUSICBRAINZ_ALBUMID', 'foo'), result)
- def test_track_to_mpd_format_musicbrainz_albumid(self):
+ def test_track_to_mpd_format_musicbrainz_albumartistid(self):
artist = list(self.track.artists)[0].copy(musicbrainz_id='foo')
album = self.track.album.copy(artists=[artist])
track = self.track.copy(album=album)
@@ -93,14 +96,14 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assertIn(('MUSICBRAINZ_ARTISTID', 'foo'), result)
def test_artists_to_mpd_format(self):
- artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')]
+ artists = [Artist(name='ABBA'), Artist(name='Beatles')]
translated = translator.artists_to_mpd_format(artists)
- self.assertEqual(translated, u'ABBA, Beatles')
+ self.assertEqual(translated, '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'')
+ self.assertEqual(translated, '')
class PlaylistMpdFormatTest(unittest.TestCase):
@@ -131,7 +134,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
folder = settings.LOCAL_MUSIC_PATH
result = dict(translator.track_to_mpd_format(track))
result['file'] = uri_to_path(result['file'])
- result['file'] = result['file'][len(folder)+1:]
+ result['file'] = result['file'][len(folder) + 1:]
result['key'] = os.path.basename(result['file'])
result['mtime'] = mtime('')
return translator.order_mpd_track_info(result.items())
@@ -147,7 +150,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
self.assertEqual(('songList begin',), result[0])
for i, row in enumerate(result):
if row == ('songList end',):
- return result[1:i], result[i+1:]
+ return result[1:i], result[i + 1:]
self.fail("Couldn't find songList end in result")
def consume_directory(self, result):
@@ -157,7 +160,7 @@ class TracksToTagCacheFormatTest(unittest.TestCase):
directory = result[2][1]
for i, row in enumerate(result):
if row == ('end', directory):
- return result[3:i], result[i+1:]
+ return result[3:i], result[i + 1:]
self.fail("Couldn't find end %s in result" % directory)
def test_empty_tag_cache_has_header(self):
diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py
index 2bc3488b..d508cbf0 100644
--- a/tests/frontends/mpd/status_test.py
+++ b/tests/frontends/mpd/status_test.py
@@ -1,3 +1,8 @@
+from __future__ import unicode_literals
+
+import pykka
+
+from mopidy import core
from mopidy.backends import dummy
from mopidy.core import PlaybackState
from mopidy.frontends.mpd import dispatcher
@@ -17,29 +22,30 @@ STOPPED = PlaybackState.STOPPED
class StatusHandlerTest(unittest.TestCase):
def setUp(self):
- self.backend = dummy.DummyBackend.start().proxy()
- self.dispatcher = dispatcher.MpdDispatcher()
+ self.backend = dummy.DummyBackend.start(audio=None).proxy()
+ self.core = core.Core.start(backends=[self.backend]).proxy()
+ self.dispatcher = dispatcher.MpdDispatcher(core=self.core)
self.context = self.dispatcher.context
def tearDown(self):
- self.backend.stop().get()
+ pykka.ActorRegistry.stop_all()
def test_stats_method(self):
result = status.stats(self.context)
self.assertIn('artists', result)
- self.assert_(int(result['artists']) >= 0)
+ self.assertGreaterEqual(int(result['artists']), 0)
self.assertIn('albums', result)
- self.assert_(int(result['albums']) >= 0)
+ self.assertGreaterEqual(int(result['albums']), 0)
self.assertIn('songs', result)
- self.assert_(int(result['songs']) >= 0)
+ self.assertGreaterEqual(int(result['songs']), 0)
self.assertIn('uptime', result)
- self.assert_(int(result['uptime']) >= 0)
+ self.assertGreaterEqual(int(result['uptime']), 0)
self.assertIn('db_playtime', result)
- self.assert_(int(result['db_playtime']) >= 0)
+ self.assertGreaterEqual(int(result['db_playtime']), 0)
self.assertIn('db_update', result)
- self.assert_(int(result['db_update']) >= 0)
+ self.assertGreaterEqual(int(result['db_update']), 0)
self.assertIn('playtime', result)
- self.assert_(int(result['playtime']) >= 0)
+ self.assertGreaterEqual(int(result['playtime']), 0)
def test_status_method_contains_volume_with_na_value(self):
result = dict(status.status(self.context))
@@ -47,7 +53,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['volume']), -1)
def test_status_method_contains_volume(self):
- self.backend.playback.volume = 17
+ self.core.playback.volume = 17
result = dict(status.status(self.context))
self.assertIn('volume', result)
self.assertEqual(int(result['volume']), 17)
@@ -58,7 +64,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['repeat']), 0)
def test_status_method_contains_repeat_is_1(self):
- self.backend.playback.repeat = 1
+ self.core.playback.repeat = 1
result = dict(status.status(self.context))
self.assertIn('repeat', result)
self.assertEqual(int(result['repeat']), 1)
@@ -69,7 +75,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['random']), 0)
def test_status_method_contains_random_is_1(self):
- self.backend.playback.random = 1
+ self.core.playback.random = 1
result = dict(status.status(self.context))
self.assertIn('random', result)
self.assertEqual(int(result['random']), 1)
@@ -85,7 +91,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['consume']), 0)
def test_status_method_contains_consume_is_1(self):
- self.backend.playback.consume = 1
+ self.core.playback.consume = 1
result = dict(status.status(self.context))
self.assertIn('consume', result)
self.assertEqual(int(result['consume']), 1)
@@ -93,88 +99,91 @@ class StatusHandlerTest(unittest.TestCase):
def test_status_method_contains_playlist(self):
result = dict(status.status(self.context))
self.assertIn('playlist', result)
- self.assertIn(int(result['playlist']), xrange(0, 2**31 - 1))
+ self.assertIn(int(result['playlist']), xrange(0, 2 ** 31 - 1))
def test_status_method_contains_playlistlength(self):
result = dict(status.status(self.context))
self.assertIn('playlistlength', result)
- self.assert_(int(result['playlistlength']) >= 0)
+ self.assertGreaterEqual(int(result['playlistlength']), 0)
def test_status_method_contains_xfade(self):
result = dict(status.status(self.context))
self.assertIn('xfade', result)
- self.assert_(int(result['xfade']) >= 0)
+ self.assertGreaterEqual(int(result['xfade']), 0)
def test_status_method_contains_state_is_play(self):
- self.backend.playback.state = PLAYING
+ self.core.playback.state = PLAYING
result = dict(status.status(self.context))
self.assertIn('state', result)
self.assertEqual(result['state'], 'play')
def test_status_method_contains_state_is_stop(self):
- self.backend.playback.state = STOPPED
+ self.core.playback.state = STOPPED
result = dict(status.status(self.context))
self.assertIn('state', result)
self.assertEqual(result['state'], 'stop')
def test_status_method_contains_state_is_pause(self):
- self.backend.playback.state = PLAYING
- self.backend.playback.state = PAUSED
+ self.core.playback.state = PLAYING
+ self.core.playback.state = PAUSED
result = dict(status.status(self.context))
self.assertIn('state', result)
self.assertEqual(result['state'], 'pause')
def test_status_method_when_playlist_loaded_contains_song(self):
- self.backend.current_playlist.append([Track()])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.play()
result = dict(status.status(self.context))
self.assertIn('song', result)
- self.assert_(int(result['song']) >= 0)
+ self.assertGreaterEqual(int(result['song']), 0)
- def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
- self.backend.current_playlist.append([Track()])
- self.backend.playback.play()
+ def test_status_method_when_playlist_loaded_contains_tlid_as_songid(self):
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.play()
result = dict(status.status(self.context))
self.assertIn('songid', result)
self.assertEqual(int(result['songid']), 0)
def test_status_method_when_playing_contains_time_with_no_length(self):
- self.backend.current_playlist.append([Track(length=None)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', length=None)])
+ self.core.playback.play()
result = dict(status.status(self.context))
self.assertIn('time', result)
(position, total) = result['time'].split(':')
position = int(position)
total = int(total)
- self.assert_(position <= total)
+ self.assertLessEqual(position, total)
def test_status_method_when_playing_contains_time_with_length(self):
- self.backend.current_playlist.append([Track(length=10000)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', length=10000)])
+ self.core.playback.play()
result = dict(status.status(self.context))
self.assertIn('time', result)
(position, total) = result['time'].split(':')
position = int(position)
total = int(total)
- self.assert_(position <= total)
+ self.assertLessEqual(position, total)
def test_status_method_when_playing_contains_elapsed(self):
- self.backend.playback.state = PAUSED
- self.backend.playback.play_time_accumulated = 59123
+ self.core.tracklist.add([Track(uri='dummy:a', length=60000)])
+ self.core.playback.play()
+ self.core.playback.pause()
+ self.core.playback.seek(59123)
result = dict(status.status(self.context))
self.assertIn('elapsed', result)
self.assertEqual(result['elapsed'], '59.123')
def test_status_method_when_starting_playing_contains_elapsed_zero(self):
- self.backend.playback.state = PAUSED
- self.backend.playback.play_time_accumulated = 123 # Less than 1000ms
+ self.core.tracklist.add([Track(uri='dummy:a', length=10000)])
+ self.core.playback.play()
+ self.core.playback.pause()
result = dict(status.status(self.context))
self.assertIn('elapsed', result)
- self.assertEqual(result['elapsed'], '0.123')
+ self.assertEqual(result['elapsed'], '0.000')
def test_status_method_when_playing_contains_bitrate(self):
- self.backend.current_playlist.append([Track(bitrate=320)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', bitrate=320)])
+ self.core.playback.play()
result = dict(status.status(self.context))
self.assertIn('bitrate', result)
self.assertEqual(int(result['bitrate']), 320)
diff --git a/tests/frontends/mpris/__init__.py b/tests/frontends/mpris/__init__.py
index e69de29b..baffc488 100644
--- a/tests/frontends/mpris/__init__.py
+++ b/tests/frontends/mpris/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py
index 49e56226..18a9de6f 100644
--- a/tests/frontends/mpris/events_test.py
+++ b/tests/frontends/mpris/events_test.py
@@ -1,9 +1,11 @@
+from __future__ import unicode_literals
+
import sys
import mock
-from mopidy import OptionalDependencyError
-from mopidy.models import Track
+from mopidy.exceptions import OptionalDependencyError
+from mopidy.models import Playlist, Track
try:
from mopidy.frontends.mpris import MprisFrontend, objects
@@ -16,7 +18,8 @@ from tests import unittest
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
class BackendEventsTest(unittest.TestCase):
def setUp(self):
- self.mpris_frontend = MprisFrontend() # As a plain class, not an actor
+ # As a plain class, not an actor:
+ self.mpris_frontend = MprisFrontend(core=None)
self.mpris_object = mock.Mock(spec=objects.MprisObject)
self.mpris_frontend.mpris_object = self.mpris_object
@@ -38,7 +41,7 @@ class BackendEventsTest(unittest.TestCase):
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, [])
- def test_track_playback_started_event_changes_playback_status_and_metadata(self):
+ def test_track_playback_started_changes_playback_status_and_metadata(self):
self.mpris_object.Get.return_value = '...'
self.mpris_frontend.track_playback_started(Track())
self.assertListEqual(self.mpris_object.Get.call_args_list, [
@@ -49,7 +52,7 @@ class BackendEventsTest(unittest.TestCase):
objects.PLAYER_IFACE,
{'Metadata': '...', 'PlaybackStatus': '...'}, [])
- def test_track_playback_ended_event_changes_playback_status_and_metadata(self):
+ def test_track_playback_ended_changes_playback_status_and_metadata(self):
self.mpris_object.Get.return_value = '...'
self.mpris_frontend.track_playback_ended(Track(), 0)
self.assertListEqual(self.mpris_object.Get.call_args_list, [
@@ -70,9 +73,21 @@ class BackendEventsTest(unittest.TestCase):
objects.PLAYER_IFACE, {'Volume': 1.0}, [])
def test_seeked_event_causes_mpris_seeked_event(self):
- self.mpris_object.Get.return_value = 31000000
- self.mpris_frontend.seeked()
- self.assertListEqual(self.mpris_object.Get.call_args_list, [
- ((objects.PLAYER_IFACE, 'Position'), {}),
- ])
+ self.mpris_frontend.seeked(31000)
self.mpris_object.Seeked.assert_called_with(31000000)
+
+ def test_playlists_loaded_event_changes_playlist_count(self):
+ self.mpris_object.Get.return_value = 17
+ self.mpris_frontend.playlists_loaded()
+ self.assertListEqual(self.mpris_object.Get.call_args_list, [
+ ((objects.PLAYLISTS_IFACE, 'PlaylistCount'), {}),
+ ])
+ self.mpris_object.PropertiesChanged.assert_called_with(
+ objects.PLAYLISTS_IFACE, {'PlaylistCount': 17}, [])
+
+ def test_playlist_changed_event_causes_mpris_playlist_changed_event(self):
+ self.mpris_object.get_playlist_id.return_value = 'id-for-dummy:foo'
+ playlist = Playlist(uri='dummy:foo', name='foo')
+ self.mpris_frontend.playlist_changed(playlist)
+ self.mpris_object.PlaylistChanged.assert_called_with(
+ ('id-for-dummy:foo', 'foo', ''))
diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py
index db7f9265..c48ffa98 100644
--- a/tests/frontends/mpris/player_interface_test.py
+++ b/tests/frontends/mpris/player_interface_test.py
@@ -1,15 +1,18 @@
+from __future__ import unicode_literals
+
import sys
import mock
+import pykka
-from mopidy import OptionalDependencyError
-from mopidy.backends.dummy import DummyBackend
+from mopidy import core, exceptions
+from mopidy.backends import dummy
from mopidy.core import PlaybackState
from mopidy.models import Album, Artist, Track
try:
from mopidy.frontends.mpris import objects
-except OptionalDependencyError:
+except exceptions.OptionalDependencyError:
pass
from tests import unittest
@@ -23,301 +26,303 @@ STOPPED = PlaybackState.STOPPED
class PlayerInterfaceTest(unittest.TestCase):
def setUp(self):
objects.MprisObject._connect_to_dbus = mock.Mock()
- self.backend = DummyBackend.start().proxy()
- self.mpris = objects.MprisObject()
- self.mpris._backend = self.backend
+ self.backend = dummy.DummyBackend.start(audio=None).proxy()
+ self.core = core.Core.start(backends=[self.backend]).proxy()
+ self.mpris = objects.MprisObject(core=self.core)
def tearDown(self):
- self.backend.stop()
+ pykka.ActorRegistry.stop_all()
def test_get_playback_status_is_playing_when_playing(self):
- self.backend.playback.state = PLAYING
+ self.core.playback.state = PLAYING
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Playing', result)
def test_get_playback_status_is_paused_when_paused(self):
- self.backend.playback.state = PAUSED
+ self.core.playback.state = PAUSED
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Paused', result)
def test_get_playback_status_is_stopped_when_stopped(self):
- self.backend.playback.state = STOPPED
+ self.core.playback.state = STOPPED
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Stopped', result)
def test_get_loop_status_is_none_when_not_looping(self):
- self.backend.playback.repeat = False
- self.backend.playback.single = False
+ self.core.playback.repeat = False
+ self.core.playback.single = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('None', result)
def test_get_loop_status_is_track_when_looping_a_single_track(self):
- self.backend.playback.repeat = True
- self.backend.playback.single = True
+ self.core.playback.repeat = True
+ self.core.playback.single = True
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('Track', result)
- def test_get_loop_status_is_playlist_when_looping_the_current_playlist(self):
- self.backend.playback.repeat = True
- self.backend.playback.single = False
+ def test_get_loop_status_is_playlist_when_looping_tracklist(self):
+ self.core.playback.repeat = True
+ self.core.playback.single = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('Playlist', result)
def test_set_loop_status_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
- self.backend.playback.repeat = True
- self.backend.playback.single = True
+ self.core.playback.repeat = True
+ self.core.playback.single = True
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
- self.assertEquals(self.backend.playback.repeat.get(), True)
- self.assertEquals(self.backend.playback.single.get(), True)
+ self.assertEqual(self.core.playback.repeat.get(), True)
+ self.assertEqual(self.core.playback.single.get(), True)
def test_set_loop_status_to_none_unsets_repeat_and_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
- self.assertEquals(self.backend.playback.repeat.get(), False)
- self.assertEquals(self.backend.playback.single.get(), False)
+ self.assertEqual(self.core.playback.repeat.get(), False)
+ self.assertEqual(self.core.playback.single.get(), False)
def test_set_loop_status_to_track_sets_repeat_and_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Track')
- self.assertEquals(self.backend.playback.repeat.get(), True)
- self.assertEquals(self.backend.playback.single.get(), True)
+ self.assertEqual(self.core.playback.repeat.get(), True)
+ self.assertEqual(self.core.playback.single.get(), True)
def test_set_loop_status_to_playlists_sets_repeat_and_not_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Playlist')
- self.assertEquals(self.backend.playback.repeat.get(), True)
- self.assertEquals(self.backend.playback.single.get(), False)
+ self.assertEqual(self.core.playback.repeat.get(), True)
+ self.assertEqual(self.core.playback.single.get(), False)
def test_get_rate_is_greater_or_equal_than_minimum_rate(self):
rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate')
minimum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
- self.assert_(rate >= minimum_rate)
+ self.assertGreaterEqual(rate, minimum_rate)
def test_get_rate_is_less_or_equal_than_maximum_rate(self):
rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate')
maximum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
- self.assert_(rate >= maximum_rate)
+ self.assertGreaterEqual(rate, maximum_rate)
def test_set_rate_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_set_rate_to_zero_pauses_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_get_shuffle_returns_true_if_random_is_active(self):
- self.backend.playback.random = True
+ self.core.playback.random = True
result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle')
self.assertTrue(result)
def test_get_shuffle_returns_false_if_random_is_inactive(self):
- self.backend.playback.random = False
+ self.core.playback.random = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle')
self.assertFalse(result)
def test_set_shuffle_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
- self.backend.playback.random = False
- result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
- self.assertFalse(self.backend.playback.random.get())
+ self.core.playback.random = False
+ self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
+ self.assertFalse(self.core.playback.random.get())
def test_set_shuffle_to_true_activates_random_mode(self):
- self.backend.playback.random = False
- self.assertFalse(self.backend.playback.random.get())
- result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
- self.assertTrue(self.backend.playback.random.get())
+ self.core.playback.random = False
+ self.assertFalse(self.core.playback.random.get())
+ self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
+ self.assertTrue(self.core.playback.random.get())
def test_set_shuffle_to_false_deactivates_random_mode(self):
- self.backend.playback.random = True
- self.assertTrue(self.backend.playback.random.get())
- result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', False)
- self.assertFalse(self.backend.playback.random.get())
+ self.core.playback.random = True
+ self.assertTrue(self.core.playback.random.get())
+ self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', False)
+ self.assertFalse(self.core.playback.random.get())
def test_get_metadata_has_trackid_even_when_no_current_track(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:trackid', result.keys())
- self.assertEquals(result['mpris:trackid'], '')
+ self.assertEqual(result['mpris:trackid'], '')
- def test_get_metadata_has_trackid_based_on_cpid(self):
- self.backend.current_playlist.append([Track(uri='a')])
- self.backend.playback.play()
- (cpid, track) = self.backend.playback.current_cp_track.get()
+ def test_get_metadata_has_trackid_based_on_tlid(self):
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.play()
+ (tlid, track) = self.core.playback.current_tl_track.get()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:trackid', result.keys())
- self.assertEquals(result['mpris:trackid'],
- '/com/mopidy/track/%d' % cpid)
+ self.assertEqual(
+ result['mpris:trackid'], '/com/mopidy/track/%d' % tlid)
def test_get_metadata_has_track_length(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:length', result.keys())
- self.assertEquals(result['mpris:length'], 40000000)
+ self.assertEqual(result['mpris:length'], 40000000)
def test_get_metadata_has_track_uri(self):
- self.backend.current_playlist.append([Track(uri='a')])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:url', result.keys())
- self.assertEquals(result['xesam:url'], 'a')
+ self.assertEqual(result['xesam:url'], 'dummy:a')
def test_get_metadata_has_track_title(self):
- self.backend.current_playlist.append([Track(name='a')])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(name='a')])
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:title', result.keys())
- self.assertEquals(result['xesam:title'], 'a')
+ self.assertEqual(result['xesam:title'], 'a')
def test_get_metadata_has_track_artists(self):
- self.backend.current_playlist.append([Track(artists=[
+ self.core.tracklist.add([Track(artists=[
Artist(name='a'), Artist(name='b'), Artist(name=None)])])
- self.backend.playback.play()
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:artist', result.keys())
- self.assertEquals(result['xesam:artist'], ['a', 'b'])
+ self.assertEqual(result['xesam:artist'], ['a', 'b'])
def test_get_metadata_has_track_album(self):
- self.backend.current_playlist.append([Track(album=Album(name='a'))])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(album=Album(name='a'))])
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:album', result.keys())
- self.assertEquals(result['xesam:album'], 'a')
+ self.assertEqual(result['xesam:album'], 'a')
def test_get_metadata_has_track_album_artists(self):
- self.backend.current_playlist.append([Track(album=Album(artists=[
+ self.core.tracklist.add([Track(album=Album(artists=[
Artist(name='a'), Artist(name='b'), Artist(name=None)]))])
- self.backend.playback.play()
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:albumArtist', result.keys())
- self.assertEquals(result['xesam:albumArtist'], ['a', 'b'])
+ self.assertEqual(result['xesam:albumArtist'], ['a', 'b'])
def test_get_metadata_has_track_number_in_album(self):
- self.backend.current_playlist.append([Track(track_no=7)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(track_no=7)])
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:trackNumber', result.keys())
- self.assertEquals(result['xesam:trackNumber'], 7)
+ self.assertEqual(result['xesam:trackNumber'], 7)
def test_get_volume_should_return_volume_between_zero_and_one(self):
- self.backend.playback.volume = None
+ self.core.playback.volume = None
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
- self.assertEquals(result, 0)
+ self.assertEqual(result, 0)
- self.backend.playback.volume = 0
+ self.core.playback.volume = 0
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
- self.assertEquals(result, 0)
+ self.assertEqual(result, 0)
- self.backend.playback.volume = 50
+ self.core.playback.volume = 50
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
- self.assertEquals(result, 0.5)
+ self.assertEqual(result, 0.5)
- self.backend.playback.volume = 100
+ self.core.playback.volume = 100
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
- self.assertEquals(result, 1)
+ self.assertEqual(result, 1)
def test_set_volume_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
- self.backend.playback.volume = 0
+ self.core.playback.volume = 0
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
- self.assertEquals(self.backend.playback.volume.get(), 0)
+ self.assertEqual(self.core.playback.volume.get(), 0)
def test_set_volume_to_one_should_set_mixer_volume_to_100(self):
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
- self.assertEquals(self.backend.playback.volume.get(), 100)
+ self.assertEqual(self.core.playback.volume.get(), 100)
- def test_set_volume_to_anything_above_one_should_set_mixer_volume_to_100(self):
+ def test_set_volume_to_anything_above_one_sets_mixer_volume_to_100(self):
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 2.0)
- self.assertEquals(self.backend.playback.volume.get(), 100)
+ self.assertEqual(self.core.playback.volume.get(), 100)
def test_set_volume_to_anything_not_a_number_does_not_change_volume(self):
- self.backend.playback.volume = 10
+ self.core.playback.volume = 10
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', None)
- self.assertEquals(self.backend.playback.volume.get(), 10)
+ self.assertEqual(self.core.playback.volume.get(), 10)
def test_get_position_returns_time_position_in_microseconds(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
- self.backend.playback.seek(10000)
- result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position')
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
+ self.core.playback.seek(10000)
+ result_in_microseconds = self.mpris.Get(
+ objects.PLAYER_IFACE, 'Position')
result_in_milliseconds = result_in_microseconds // 1000
- self.assert_(result_in_milliseconds >= 10000)
+ self.assertGreaterEqual(result_in_milliseconds, 10000)
def test_get_position_when_no_current_track_should_be_zero(self):
- result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position')
+ result_in_microseconds = self.mpris.Get(
+ objects.PLAYER_IFACE, 'Position')
result_in_milliseconds = result_in_microseconds // 1000
- self.assertEquals(result_in_milliseconds, 0)
+ self.assertEqual(result_in_milliseconds, 0)
def test_get_minimum_rate_is_one_or_less(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
- self.assert_(result <= 1.0)
+ self.assertLessEqual(result, 1.0)
def test_get_maximum_rate_is_one_or_more(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
- self.assert_(result >= 1.0)
+ self.assertGreaterEqual(result, 1.0)
def test_can_go_next_is_true_if_can_control_and_other_next_track(self):
self.mpris.get_CanControl = lambda *_: True
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertTrue(result)
def test_can_go_next_is_false_if_next_track_is_the_same(self):
self.mpris.get_CanControl = lambda *_: True
- self.backend.current_playlist.append([Track(uri='a')])
- self.backend.playback.repeat = True
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.repeat = True
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertFalse(result)
def test_can_go_next_is_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertFalse(result)
- def test_can_go_previous_is_true_if_can_control_and_other_previous_track(self):
+ def test_can_go_previous_is_true_if_can_control_and_previous_track(self):
self.mpris.get_CanControl = lambda *_: True
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.next()
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.next()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertTrue(result)
def test_can_go_previous_is_false_if_previous_track_is_the_same(self):
self.mpris.get_CanControl = lambda *_: True
- self.backend.current_playlist.append([Track(uri='a')])
- self.backend.playback.repeat = True
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.repeat = True
+ self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertFalse(result)
def test_can_go_previous_is_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.next()
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.next()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertFalse(result)
def test_can_play_is_true_if_can_control_and_current_track(self):
self.mpris.get_CanControl = lambda *_: True
- self.backend.current_playlist.append([Track(uri='a')])
- self.backend.playback.play()
- self.assertTrue(self.backend.playback.current_track.get())
+ self.core.tracklist.add([Track(uri='dummy:a')])
+ self.core.playback.play()
+ self.assertTrue(self.core.playback.current_track.get())
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
self.assertTrue(result)
def test_can_play_is_false_if_no_current_track(self):
self.mpris.get_CanControl = lambda *_: True
- self.assertFalse(self.backend.playback.current_track.get())
+ self.assertFalse(self.core.playback.current_track.get())
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
self.assertFalse(result)
@@ -352,484 +357,481 @@ class PlayerInterfaceTest(unittest.TestCase):
def test_next_is_ignored_if_can_go_next_is_false(self):
self.mpris.get_CanGoNext = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.mpris.Next()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
- def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ def test_next_when_playing_skips_to_next_track_and_keep_playing(self):
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Next()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_next_when_at_end_of_list_should_stop_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.next()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.next()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Next()
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_next_when_paused_should_skip_to_next_track_and_stay_paused(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.pause()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.pause()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
self.mpris.Next()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
- def test_next_when_stopped_should_skip_to_next_track_and_stay_stopped(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.stop()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ def test_next_when_stopped_skips_to_next_track_and_stay_stopped(self):
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.stop()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Next()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_previous_is_ignored_if_can_go_previous_is_false(self):
self.mpris.get_CanGoPrevious = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.next()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.next()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.mpris.Previous()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
- def test_previous_when_playing_should_skip_to_prev_track_and_keep_playing(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.next()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ def test_previous_when_playing_skips_to_prev_track_and_keep_playing(self):
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.next()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Previous()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_previous_when_at_start_of_list_should_stop_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Previous()
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
- def test_previous_when_paused_should_skip_to_previous_track_and_stay_paused(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.next()
- self.backend.playback.pause()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ def test_previous_when_paused_skips_to_previous_track_and_pause(self):
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.next()
+ self.core.playback.pause()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
self.mpris.Previous()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
- def test_previous_when_stopped_should_skip_to_previous_track_and_stay_stopped(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.next()
- self.backend.playback.stop()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ def test_previous_when_stopped_skips_to_previous_track_and_stops(self):
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.next()
+ self.core.playback.stop()
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Previous()
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_pause_is_ignored_if_can_pause_is_false(self):
self.mpris.get_CanPause = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Pause()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_pause_when_playing_should_pause_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Pause()
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_pause_when_paused_has_no_effect(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.pause()
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.pause()
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
self.mpris.Pause()
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_playpause_is_ignored_if_can_pause_is_false(self):
self.mpris.get_CanPause = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.PlayPause()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_playpause_when_playing_should_pause_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.PlayPause()
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_playpause_when_paused_should_resume_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.pause()
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.pause()
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
- at_pause = self.backend.playback.time_position.get()
- self.assert_(at_pause >= 0)
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
+ at_pause = self.core.playback.time_position.get()
+ self.assertGreaterEqual(at_pause, 0)
self.mpris.PlayPause()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- after_pause = self.backend.playback.time_position.get()
- self.assert_(after_pause >= at_pause)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ after_pause = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_pause, at_pause)
def test_playpause_when_stopped_should_start_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.PlayPause()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_stop_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Stop()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_stop_when_playing_should_stop_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Stop()
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_stop_when_paused_should_stop_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.pause()
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.pause()
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
self.mpris.Stop()
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_play_is_ignored_if_can_play_is_false(self):
self.mpris.get_CanPlay = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Play()
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_play_when_stopped_starts_playback(self):
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_play_after_pause_resumes_from_same_position(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
- before_pause = self.backend.playback.time_position.get()
- self.assert_(before_pause >= 0)
+ before_pause = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_pause, 0)
self.mpris.Pause()
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
- at_pause = self.backend.playback.time_position.get()
- self.assert_(at_pause >= before_pause)
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
+ at_pause = self.core.playback.time_position.get()
+ self.assertGreaterEqual(at_pause, before_pause)
self.mpris.Play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- after_pause = self.backend.playback.time_position.get()
- self.assert_(after_pause >= at_pause)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ after_pause = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_pause, at_pause)
def test_play_when_there_is_no_track_has_no_effect(self):
- self.backend.current_playlist.clear()
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.core.tracklist.clear()
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Play()
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_seek_is_ignored_if_can_seek_is_false(self):
self.mpris.get_CanSeek = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
- before_seek = self.backend.playback.time_position.get()
- self.assert_(before_seek >= 0)
+ before_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_seek, 0)
milliseconds_to_seek = 10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
- after_seek = self.backend.playback.time_position.get()
- self.assert_(before_seek <= after_seek < (
- before_seek + milliseconds_to_seek))
+ after_seek = self.core.playback.time_position.get()
+ self.assertLessEqual(before_seek, after_seek)
+ self.assertLess(after_seek, before_seek + milliseconds_to_seek)
def test_seek_seeks_given_microseconds_forward_in_the_current_track(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
- before_seek = self.backend.playback.time_position.get()
- self.assert_(before_seek >= 0)
+ before_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_seek, 0)
milliseconds_to_seek = 10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
- after_seek = self.backend.playback.time_position.get()
- self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
+ after_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek)
def test_seek_seeks_given_microseconds_backward_if_negative(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
- self.backend.playback.seek(20000)
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
+ self.core.playback.seek(20000)
- before_seek = self.backend.playback.time_position.get()
- self.assert_(before_seek >= 20000)
+ before_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_seek, 20000)
milliseconds_to_seek = -10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
- after_seek = self.backend.playback.time_position.get()
- self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
- self.assert_(after_seek < before_seek)
+ after_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek)
+ self.assertLess(after_seek, before_seek)
def test_seek_seeks_to_start_of_track_if_new_position_is_negative(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
- self.backend.playback.seek(20000)
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
+ self.core.playback.seek(20000)
- before_seek = self.backend.playback.time_position.get()
- self.assert_(before_seek >= 20000)
+ before_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_seek, 20000)
milliseconds_to_seek = -30000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
- after_seek = self.backend.playback.time_position.get()
- self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
- self.assert_(after_seek < before_seek)
- self.assert_(after_seek >= 0)
+ after_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek)
+ self.assertLess(after_seek, before_seek)
+ self.assertGreaterEqual(after_seek, 0)
- def test_seek_skips_to_next_track_if_new_position_larger_than_track_length(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000),
- Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.seek(20000)
+ def test_seek_skips_to_next_track_if_new_position_gt_track_length(self):
+ self.core.tracklist.add([
+ Track(uri='dummy:a', length=40000),
+ Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.seek(20000)
- before_seek = self.backend.playback.time_position.get()
- self.assert_(before_seek >= 20000)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ before_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_seek, 20000)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
milliseconds_to_seek = 50000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
- after_seek = self.backend.playback.time_position.get()
- self.assert_(after_seek >= 0)
- self.assert_(after_seek < before_seek)
+ after_seek = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_seek, 0)
+ self.assertLess(after_seek, before_seek)
def test_set_position_is_ignored_if_can_seek_is_false(self):
self.mpris.get_CanSeek = lambda *_: False
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
- before_set_position = self.backend.playback.time_position.get()
- self.assert_(before_set_position <= 5000)
+ before_set_position = self.core.playback.time_position.get()
+ self.assertLessEqual(before_set_position, 5000)
track_id = 'a'
- position_to_set_in_milliseconds = 20000
- position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+ position_to_set_in_millisec = 20000
+ position_to_set_in_microsec = position_to_set_in_millisec * 1000
- self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+ self.mpris.SetPosition(track_id, position_to_set_in_microsec)
- after_set_position = self.backend.playback.time_position.get()
- self.assert_(before_set_position <= after_set_position <
- position_to_set_in_milliseconds)
+ after_set_position = self.core.playback.time_position.get()
+ self.assertLessEqual(before_set_position, after_set_position)
+ self.assertLess(after_set_position, position_to_set_in_millisec)
def test_set_position_sets_the_current_track_position_in_microsecs(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
- before_set_position = self.backend.playback.time_position.get()
- self.assert_(before_set_position <= 5000)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ before_set_position = self.core.playback.time_position.get()
+ self.assertLessEqual(before_set_position, 5000)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
track_id = '/com/mopidy/track/0'
- position_to_set_in_milliseconds = 20000
- position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+ position_to_set_in_millisec = 20000
+ position_to_set_in_microsec = position_to_set_in_millisec * 1000
- self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+ self.mpris.SetPosition(track_id, position_to_set_in_microsec)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
- after_set_position = self.backend.playback.time_position.get()
- self.assert_(after_set_position >= position_to_set_in_milliseconds)
+ after_set_position = self.core.playback.time_position.get()
+ self.assertGreaterEqual(
+ after_set_position, position_to_set_in_millisec)
def test_set_position_does_nothing_if_the_position_is_negative(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
- self.backend.playback.seek(20000)
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
+ self.core.playback.seek(20000)
- before_set_position = self.backend.playback.time_position.get()
- self.assert_(before_set_position >= 20000)
- self.assert_(before_set_position <= 25000)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ before_set_position = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_set_position, 20000)
+ self.assertLessEqual(before_set_position, 25000)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
track_id = '/com/mopidy/track/0'
- position_to_set_in_milliseconds = -1000
- position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+ position_to_set_in_millisec = -1000
+ position_to_set_in_microsec = position_to_set_in_millisec * 1000
- self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+ self.mpris.SetPosition(track_id, position_to_set_in_microsec)
- after_set_position = self.backend.playback.time_position.get()
- self.assert_(after_set_position >= before_set_position)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ after_set_position = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_set_position, before_set_position)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
- def test_set_position_does_nothing_if_position_is_larger_than_track_length(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
- self.backend.playback.seek(20000)
+ def test_set_position_does_nothing_if_position_is_gt_track_length(self):
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
+ self.core.playback.seek(20000)
- before_set_position = self.backend.playback.time_position.get()
- self.assert_(before_set_position >= 20000)
- self.assert_(before_set_position <= 25000)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ before_set_position = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_set_position, 20000)
+ self.assertLessEqual(before_set_position, 25000)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
track_id = 'a'
- position_to_set_in_milliseconds = 50000
- position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+ position_to_set_in_millisec = 50000
+ position_to_set_in_microsec = position_to_set_in_millisec * 1000
- self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+ self.mpris.SetPosition(track_id, position_to_set_in_microsec)
- after_set_position = self.backend.playback.time_position.get()
- self.assert_(after_set_position >= before_set_position)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ after_set_position = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_set_position, before_set_position)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
- def test_set_position_does_nothing_if_track_id_does_not_match_current_track(self):
- self.backend.current_playlist.append([Track(uri='a', length=40000)])
- self.backend.playback.play()
- self.backend.playback.seek(20000)
+ def test_set_position_is_noop_if_track_id_isnt_current_track(self):
+ self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
+ self.core.playback.play()
+ self.core.playback.seek(20000)
- before_set_position = self.backend.playback.time_position.get()
- self.assert_(before_set_position >= 20000)
- self.assert_(before_set_position <= 25000)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ before_set_position = self.core.playback.time_position.get()
+ self.assertGreaterEqual(before_set_position, 20000)
+ self.assertLessEqual(before_set_position, 25000)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
track_id = 'b'
- position_to_set_in_milliseconds = 0
- position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
+ position_to_set_in_millisec = 0
+ position_to_set_in_microsec = position_to_set_in_millisec * 1000
- self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
+ self.mpris.SetPosition(track_id, position_to_set_in_microsec)
- after_set_position = self.backend.playback.time_position.get()
- self.assert_(after_set_position >= before_set_position)
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ after_set_position = self.core.playback.time_position.get()
+ self.assertGreaterEqual(after_set_position, before_set_position)
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
def test_open_uri_is_ignored_if_can_play_is_false(self):
self.mpris.get_CanPlay = lambda *_: False
- self.backend.library.provider.dummy_library = [
+ self.backend.library.dummy_library = [
Track(uri='dummy:/test/uri')]
self.mpris.OpenUri('dummy:/test/uri')
- self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 0)
def test_open_uri_ignores_uris_with_unknown_uri_scheme(self):
- self.assertListEqual(self.backend.uri_schemes.get(), ['dummy'])
+ self.assertListEqual(self.core.uri_schemes.get(), ['dummy'])
self.mpris.get_CanPlay = lambda *_: True
- self.backend.library.provider.dummy_library = [
- Track(uri='notdummy:/test/uri')]
+ self.backend.library.dummy_library = [Track(uri='notdummy:/test/uri')]
self.mpris.OpenUri('notdummy:/test/uri')
- self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0)
+ self.assertEqual(len(self.core.tracklist.tracks.get()), 0)
- def test_open_uri_adds_uri_to_current_playlist(self):
+ def test_open_uri_adds_uri_to_tracklist(self):
self.mpris.get_CanPlay = lambda *_: True
- self.backend.library.provider.dummy_library = [
- Track(uri='dummy:/test/uri')]
+ self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')]
self.mpris.OpenUri('dummy:/test/uri')
- self.assertEquals(self.backend.current_playlist.tracks.get()[0].uri,
- 'dummy:/test/uri')
+ self.assertEqual(
+ self.core.tracklist.tracks.get()[0].uri, 'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_stopped(self):
self.mpris.get_CanPlay = lambda *_: True
- self.backend.library.provider.dummy_library = [
- Track(uri='dummy:/test/uri')]
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.assertEquals(self.backend.playback.state.get(), STOPPED)
+ self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')]
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.OpenUri('dummy:/test/uri')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri,
- 'dummy:/test/uri')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(
+ self.core.playback.current_track.get().uri, 'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_paused(self):
self.mpris.get_CanPlay = lambda *_: True
- self.backend.library.provider.dummy_library = [
- Track(uri='dummy:/test/uri')]
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.backend.playback.pause()
- self.assertEquals(self.backend.playback.state.get(), PAUSED)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')]
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.core.playback.pause()
+ self.assertEqual(self.core.playback.state.get(), PAUSED)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.mpris.OpenUri('dummy:/test/uri')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri,
- 'dummy:/test/uri')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(
+ self.core.playback.current_track.get().uri, 'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_playing(self):
self.mpris.get_CanPlay = lambda *_: True
- self.backend.library.provider.dummy_library = [
- Track(uri='dummy:/test/uri')]
- self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
- self.backend.playback.play()
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
+ self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')]
+ self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
+ self.core.playback.play()
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.mpris.OpenUri('dummy:/test/uri')
- self.assertEquals(self.backend.playback.state.get(), PLAYING)
- self.assertEquals(self.backend.playback.current_track.get().uri,
- 'dummy:/test/uri')
+ self.assertEqual(self.core.playback.state.get(), PLAYING)
+ self.assertEqual(
+ self.core.playback.current_track.get().uri, 'dummy:/test/uri')
diff --git a/tests/frontends/mpris/playlists_interface_test.py b/tests/frontends/mpris/playlists_interface_test.py
new file mode 100644
index 00000000..2adffaf3
--- /dev/null
+++ b/tests/frontends/mpris/playlists_interface_test.py
@@ -0,0 +1,171 @@
+from __future__ import unicode_literals
+
+import datetime
+import sys
+
+import mock
+import pykka
+
+from mopidy import core, exceptions
+from mopidy.audio import PlaybackState
+from mopidy.backends import dummy
+from mopidy.models import Track
+
+try:
+ from mopidy.frontends.mpris import objects
+except exceptions.OptionalDependencyError:
+ pass
+
+from tests import unittest
+
+
+@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
+class PlayerInterfaceTest(unittest.TestCase):
+ def setUp(self):
+ objects.MprisObject._connect_to_dbus = mock.Mock()
+ self.backend = dummy.DummyBackend.start(audio=None).proxy()
+ self.core = core.Core.start(backends=[self.backend]).proxy()
+ self.mpris = objects.MprisObject(core=self.core)
+
+ foo = self.core.playlists.create('foo').get()
+ foo = foo.copy(last_modified=datetime.datetime(2012, 3, 1, 6, 0, 0))
+ foo = self.core.playlists.save(foo).get()
+
+ bar = self.core.playlists.create('bar').get()
+ bar = bar.copy(last_modified=datetime.datetime(2012, 2, 1, 6, 0, 0))
+ bar = self.core.playlists.save(bar).get()
+
+ baz = self.core.playlists.create('baz').get()
+ baz = baz.copy(last_modified=datetime.datetime(2012, 1, 1, 6, 0, 0))
+ baz = self.core.playlists.save(baz).get()
+ self.playlist = baz
+
+ def tearDown(self):
+ pykka.ActorRegistry.stop_all()
+
+ def test_activate_playlist_appends_tracks_to_tracklist(self):
+ self.core.tracklist.add([
+ Track(uri='dummy:old-a'),
+ Track(uri='dummy:old-b'),
+ ])
+ self.playlist = self.playlist.copy(tracks=[
+ Track(uri='dummy:baz-a'),
+ Track(uri='dummy:baz-b'),
+ Track(uri='dummy:baz-c'),
+ ])
+ self.playlist = self.core.playlists.save(self.playlist).get()
+
+ self.assertEqual(2, self.core.tracklist.length.get())
+
+ playlists = self.mpris.GetPlaylists(0, 100, 'User', False)
+ playlist_id = playlists[2][0]
+ self.mpris.ActivatePlaylist(playlist_id)
+
+ self.assertEqual(5, self.core.tracklist.length.get())
+ self.assertEqual(
+ PlaybackState.PLAYING, self.core.playback.state.get())
+ self.assertEqual(
+ self.playlist.tracks[0], self.core.playback.current_track.get())
+
+ def test_activate_empty_playlist_is_harmless(self):
+ self.assertEqual(0, self.core.tracklist.length.get())
+
+ playlists = self.mpris.GetPlaylists(0, 100, 'User', False)
+ playlist_id = playlists[2][0]
+ self.mpris.ActivatePlaylist(playlist_id)
+
+ self.assertEqual(0, self.core.tracklist.length.get())
+ self.assertEqual(
+ PlaybackState.STOPPED, self.core.playback.state.get())
+ self.assertIsNone(self.core.playback.current_track.get())
+
+ def test_get_playlists_in_alphabetical_order(self):
+ result = self.mpris.GetPlaylists(0, 100, 'Alphabetical', False)
+
+ self.assertEqual(3, len(result))
+
+ self.assertEqual('/com/mopidy/playlist/MR2W23LZHJRGC4Q_', result[0][0])
+ self.assertEqual('bar', result[0][1])
+
+ self.assertEqual('/com/mopidy/playlist/MR2W23LZHJRGC6Q_', result[1][0])
+ self.assertEqual('baz', result[1][1])
+
+ self.assertEqual('/com/mopidy/playlist/MR2W23LZHJTG63Y_', result[2][0])
+ self.assertEqual('foo', result[2][1])
+
+ def test_get_playlists_in_reverse_alphabetical_order(self):
+ result = self.mpris.GetPlaylists(0, 100, 'Alphabetical', True)
+
+ self.assertEqual(3, len(result))
+ self.assertEqual('foo', result[0][1])
+ self.assertEqual('baz', result[1][1])
+ self.assertEqual('bar', result[2][1])
+
+ def test_get_playlists_in_modified_order(self):
+ result = self.mpris.GetPlaylists(0, 100, 'Modified', False)
+
+ self.assertEqual(3, len(result))
+ self.assertEqual('baz', result[0][1])
+ self.assertEqual('bar', result[1][1])
+ self.assertEqual('foo', result[2][1])
+
+ def test_get_playlists_in_reverse_modified_order(self):
+ result = self.mpris.GetPlaylists(0, 100, 'Modified', True)
+
+ self.assertEqual(3, len(result))
+ self.assertEqual('foo', result[0][1])
+ self.assertEqual('bar', result[1][1])
+ self.assertEqual('baz', result[2][1])
+
+ def test_get_playlists_in_user_order(self):
+ result = self.mpris.GetPlaylists(0, 100, 'User', False)
+
+ self.assertEqual(3, len(result))
+ self.assertEqual('foo', result[0][1])
+ self.assertEqual('bar', result[1][1])
+ self.assertEqual('baz', result[2][1])
+
+ def test_get_playlists_in_reverse_user_order(self):
+ result = self.mpris.GetPlaylists(0, 100, 'User', True)
+
+ self.assertEqual(3, len(result))
+ self.assertEqual('baz', result[0][1])
+ self.assertEqual('bar', result[1][1])
+ self.assertEqual('foo', result[2][1])
+
+ def test_get_playlists_slice_on_start_of_list(self):
+ result = self.mpris.GetPlaylists(0, 2, 'User', False)
+
+ self.assertEqual(2, len(result))
+ self.assertEqual('foo', result[0][1])
+ self.assertEqual('bar', result[1][1])
+
+ def test_get_playlists_slice_later_in_list(self):
+ result = self.mpris.GetPlaylists(2, 2, 'User', False)
+
+ self.assertEqual(1, len(result))
+ self.assertEqual('baz', result[0][1])
+
+ def test_get_playlist_count_returns_number_of_playlists(self):
+ result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'PlaylistCount')
+
+ self.assertEqual(3, result)
+
+ def test_get_orderings_includes_alpha_modified_and_user(self):
+ result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'Orderings')
+
+ self.assertIn('Alphabetical', result)
+ self.assertNotIn('Created', result)
+ self.assertIn('Modified', result)
+ self.assertNotIn('Played', result)
+ self.assertIn('User', result)
+
+ def test_get_active_playlist_does_not_return_a_playlist(self):
+ result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'ActivePlaylist')
+ valid, playlist = result
+ playlist_id, playlist_name, playlist_icon_uri = playlist
+
+ self.assertEqual(False, valid)
+ self.assertEqual('/', playlist_id)
+ self.assertEqual('None', playlist_name)
+ self.assertEqual('', playlist_icon_uri)
diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py
index 1e54fc15..722fd2cd 100644
--- a/tests/frontends/mpris/root_interface_test.py
+++ b/tests/frontends/mpris/root_interface_test.py
@@ -1,13 +1,16 @@
+from __future__ import unicode_literals
+
import sys
import mock
+import pykka
-from mopidy import OptionalDependencyError, settings
-from mopidy.backends.dummy import DummyBackend
+from mopidy import core, exceptions, settings
+from mopidy.backends import dummy
try:
from mopidy.frontends.mpris import objects
-except OptionalDependencyError:
+except exceptions.OptionalDependencyError:
pass
from tests import unittest
@@ -18,15 +21,28 @@ class RootInterfaceTest(unittest.TestCase):
def setUp(self):
objects.exit_process = mock.Mock()
objects.MprisObject._connect_to_dbus = mock.Mock()
- self.backend = DummyBackend.start().proxy()
- self.mpris = objects.MprisObject()
+ self.backend = dummy.DummyBackend.start(audio=None).proxy()
+ self.core = core.Core.start(backends=[self.backend]).proxy()
+ self.mpris = objects.MprisObject(core=self.core)
def tearDown(self):
- self.backend.stop()
+ pykka.ActorRegistry.stop_all()
def test_constructor_connects_to_dbus(self):
self.assert_(self.mpris._connect_to_dbus.called)
+ def test_fullscreen_returns_false(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'Fullscreen')
+ self.assertFalse(result)
+
+ def test_setting_fullscreen_fails_and_returns_none(self):
+ result = self.mpris.Set(objects.ROOT_IFACE, 'Fullscreen', 'True')
+ self.assertIsNone(result)
+
+ def test_can_set_fullscreen_returns_false(self):
+ result = self.mpris.Get(objects.ROOT_IFACE, 'CanSetFullscreen')
+ self.assertFalse(result)
+
def test_can_raise_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'CanRaise')
self.assertFalse(result)
@@ -60,7 +76,7 @@ class RootInterfaceTest(unittest.TestCase):
self.assertEquals(result, 'foo')
settings.runtime.clear()
- def test_supported_uri_schemes_is_empty(self):
+ def test_supported_uri_schemes_includes_backend_uri_schemes(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes')
self.assertEquals(len(result), 1)
self.assertEquals(result[0], 'dummy')
diff --git a/tests/help_test.py b/tests/help_test.py
index a2803b72..fdef0f52 100644
--- a/tests/help_test.py
+++ b/tests/help_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import os
import subprocess
import sys
diff --git a/tests/models_test.py b/tests/models_test.py
index 779d1a4b..9a3062fc 100644
--- a/tests/models_test.py
+++ b/tests/models_test.py
@@ -1,11 +1,16 @@
-import datetime
+from __future__ import unicode_literals
-from mopidy.models import Artist, Album, CpTrack, Track, Playlist
+import datetime
+import json
+
+from mopidy.models import (
+ Artist, Album, TlTrack, Track, Playlist,
+ ModelJSONEncoder, model_json_decoder)
from tests import unittest
-class GenericCopyTets(unittest.TestCase):
+class GenericCopyTest(unittest.TestCase):
def compare(self, orig, other):
self.assertEqual(orig, other)
self.assertNotEqual(id(orig), id(other))
@@ -52,59 +57,93 @@ class GenericCopyTets(unittest.TestCase):
class ArtistTest(unittest.TestCase):
def test_uri(self):
- uri = u'an_uri'
+ uri = 'an_uri'
artist = Artist(uri=uri)
self.assertEqual(artist.uri, uri)
self.assertRaises(AttributeError, setattr, artist, 'uri', None)
def test_name(self):
- name = u'a name'
+ name = 'a name'
artist = Artist(name=name)
self.assertEqual(artist.name, name)
self.assertRaises(AttributeError, setattr, artist, 'name', None)
def test_musicbrainz_id(self):
- mb_id = u'mb-id'
+ mb_id = 'mb-id'
artist = Artist(musicbrainz_id=mb_id)
self.assertEqual(artist.musicbrainz_id, mb_id)
- self.assertRaises(AttributeError, setattr, artist,
- 'musicbrainz_id', None)
+ self.assertRaises(
+ AttributeError, setattr, artist, 'musicbrainz_id', None)
def test_invalid_kwarg(self):
test = lambda: Artist(foo='baz')
self.assertRaises(TypeError, test)
+ def test_invalid_kwarg_with_name_matching_method(self):
+ test = lambda: Artist(copy='baz')
+ self.assertRaises(TypeError, test)
+
+ test = lambda: Artist(serialize='baz')
+ self.assertRaises(TypeError, test)
+
def test_repr(self):
self.assertEquals(
- "Artist(name='name', uri='uri')",
+ "Artist(name=u'name', uri=u'uri')",
repr(Artist(uri='uri', name='name')))
def test_serialize(self):
self.assertDictEqual(
- {'uri': 'uri', 'name': 'name'},
+ {'__model__': 'Artist', 'uri': 'uri', 'name': 'name'},
Artist(uri='uri', name='name').serialize())
+ def test_to_json_and_back(self):
+ artist1 = Artist(uri='uri', name='name')
+ serialized = json.dumps(artist1, cls=ModelJSONEncoder)
+ artist2 = json.loads(serialized, object_hook=model_json_decoder)
+ self.assertEqual(artist1, artist2)
+
+ def test_to_json_and_back_with_unknown_field(self):
+ artist = Artist(uri='uri', name='name').serialize()
+ artist['foo'] = 'foo'
+ serialized = json.dumps(artist)
+ test = lambda: json.loads(serialized, object_hook=model_json_decoder)
+ self.assertRaises(TypeError, test)
+
+ def test_to_json_and_back_with_field_matching_method(self):
+ artist = Artist(uri='uri', name='name').serialize()
+ artist['copy'] = 'foo'
+ serialized = json.dumps(artist)
+ test = lambda: json.loads(serialized, object_hook=model_json_decoder)
+ self.assertRaises(TypeError, test)
+
+ def test_to_json_and_back_with_field_matching_internal_field(self):
+ artist = Artist(uri='uri', name='name').serialize()
+ artist['__mro__'] = 'foo'
+ serialized = json.dumps(artist)
+ test = lambda: json.loads(serialized, object_hook=model_json_decoder)
+ self.assertRaises(TypeError, test)
+
def test_eq_name(self):
- artist1 = Artist(name=u'name')
- artist2 = Artist(name=u'name')
+ artist1 = Artist(name='name')
+ artist2 = Artist(name='name')
self.assertEqual(artist1, artist2)
self.assertEqual(hash(artist1), hash(artist2))
def test_eq_uri(self):
- artist1 = Artist(uri=u'uri')
- artist2 = Artist(uri=u'uri')
+ artist1 = Artist(uri='uri')
+ artist2 = Artist(uri='uri')
self.assertEqual(artist1, artist2)
self.assertEqual(hash(artist1), hash(artist2))
def test_eq_musibrainz_id(self):
- artist1 = Artist(musicbrainz_id=u'id')
- artist2 = Artist(musicbrainz_id=u'id')
+ artist1 = Artist(musicbrainz_id='id')
+ artist2 = Artist(musicbrainz_id='id')
self.assertEqual(artist1, artist2)
self.assertEqual(hash(artist1), hash(artist2))
def test_eq(self):
- artist1 = Artist(uri=u'uri', name=u'name', musicbrainz_id='id')
- artist2 = Artist(uri=u'uri', name=u'name', musicbrainz_id='id')
+ artist1 = Artist(uri='uri', name='name', musicbrainz_id='id')
+ artist2 = Artist(uri='uri', name='name', musicbrainz_id='id')
self.assertEqual(artist1, artist2)
self.assertEqual(hash(artist1), hash(artist2))
@@ -115,39 +154,39 @@ class ArtistTest(unittest.TestCase):
self.assertNotEqual(Artist(), 'other')
def test_ne_name(self):
- artist1 = Artist(name=u'name1')
- artist2 = Artist(name=u'name2')
+ artist1 = Artist(name='name1')
+ artist2 = Artist(name='name2')
self.assertNotEqual(artist1, artist2)
self.assertNotEqual(hash(artist1), hash(artist2))
def test_ne_uri(self):
- artist1 = Artist(uri=u'uri1')
- artist2 = Artist(uri=u'uri2')
+ artist1 = Artist(uri='uri1')
+ artist2 = Artist(uri='uri2')
self.assertNotEqual(artist1, artist2)
self.assertNotEqual(hash(artist1), hash(artist2))
def test_ne_musicbrainz_id(self):
- artist1 = Artist(musicbrainz_id=u'id1')
- artist2 = Artist(musicbrainz_id=u'id2')
+ artist1 = Artist(musicbrainz_id='id1')
+ artist2 = Artist(musicbrainz_id='id2')
self.assertNotEqual(artist1, artist2)
self.assertNotEqual(hash(artist1), hash(artist2))
def test_ne(self):
- artist1 = Artist(uri=u'uri1', name=u'name1', musicbrainz_id='id1')
- artist2 = Artist(uri=u'uri2', name=u'name2', musicbrainz_id='id2')
+ artist1 = Artist(uri='uri1', name='name1', musicbrainz_id='id1')
+ artist2 = Artist(uri='uri2', name='name2', musicbrainz_id='id2')
self.assertNotEqual(artist1, artist2)
self.assertNotEqual(hash(artist1), hash(artist2))
class AlbumTest(unittest.TestCase):
def test_uri(self):
- uri = u'an_uri'
+ uri = 'an_uri'
album = Album(uri=uri)
self.assertEqual(album.uri, uri)
self.assertRaises(AttributeError, setattr, album, 'uri', None)
def test_name(self):
- name = u'a name'
+ name = 'a name'
album = Album(name=name)
self.assertEqual(album.name, name)
self.assertRaises(AttributeError, setattr, album, 'name', None)
@@ -164,12 +203,18 @@ class AlbumTest(unittest.TestCase):
self.assertEqual(album.num_tracks, num_tracks)
self.assertRaises(AttributeError, setattr, album, 'num_tracks', None)
+ def test_date(self):
+ date = '1977-01-01'
+ album = Album(date=date)
+ self.assertEqual(album.date, date)
+ self.assertRaises(AttributeError, setattr, album, 'date', None)
+
def test_musicbrainz_id(self):
- mb_id = u'mb-id'
+ mb_id = 'mb-id'
album = Album(musicbrainz_id=mb_id)
self.assertEqual(album.musicbrainz_id, mb_id)
- self.assertRaises(AttributeError, setattr, album,
- 'musicbrainz_id', None)
+ self.assertRaises(
+ AttributeError, setattr, album, 'musicbrainz_id', None)
def test_invalid_kwarg(self):
test = lambda: Album(foo='baz')
@@ -177,34 +222,41 @@ class AlbumTest(unittest.TestCase):
def test_repr_without_artists(self):
self.assertEquals(
- "Album(artists=[], name='name', uri='uri')",
+ "Album(artists=[], name=u'name', uri=u'uri')",
repr(Album(uri='uri', name='name')))
def test_repr_with_artists(self):
self.assertEquals(
- "Album(artists=[Artist(name='foo')], name='name', uri='uri')",
+ "Album(artists=[Artist(name=u'foo')], name=u'name', uri=u'uri')",
repr(Album(uri='uri', name='name', artists=[Artist(name='foo')])))
def test_serialize_without_artists(self):
self.assertDictEqual(
- {'uri': 'uri', 'name': 'name'},
+ {'__model__': 'Album', 'uri': 'uri', 'name': 'name'},
Album(uri='uri', name='name').serialize())
def test_serialize_with_artists(self):
artist = Artist(name='foo')
self.assertDictEqual(
- {'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]},
+ {'__model__': 'Album', 'uri': 'uri', 'name': 'name',
+ 'artists': [artist.serialize()]},
Album(uri='uri', name='name', artists=[artist]).serialize())
+ def test_to_json_and_back(self):
+ album1 = Album(uri='uri', name='name', artists=[Artist(name='foo')])
+ serialized = json.dumps(album1, cls=ModelJSONEncoder)
+ album2 = json.loads(serialized, object_hook=model_json_decoder)
+ self.assertEqual(album1, album2)
+
def test_eq_name(self):
- album1 = Album(name=u'name')
- album2 = Album(name=u'name')
+ album1 = Album(name='name')
+ album2 = Album(name='name')
self.assertEqual(album1, album2)
self.assertEqual(hash(album1), hash(album2))
def test_eq_uri(self):
- album1 = Album(uri=u'uri')
- album2 = Album(uri=u'uri')
+ album1 = Album(uri='uri')
+ album2 = Album(uri='uri')
self.assertEqual(album1, album2)
self.assertEqual(hash(album1), hash(album2))
@@ -216,8 +268,8 @@ class AlbumTest(unittest.TestCase):
self.assertEqual(hash(album1), hash(album2))
def test_eq_artists_order(self):
- artist1 = Artist(name=u'name1')
- artist2 = Artist(name=u'name2')
+ artist1 = Artist(name='name1')
+ artist2 = Artist(name='name2')
album1 = Album(artists=[artist1, artist2])
album2 = Album(artists=[artist2, artist1])
self.assertEqual(album1, album2)
@@ -229,17 +281,26 @@ class AlbumTest(unittest.TestCase):
self.assertEqual(album1, album2)
self.assertEqual(hash(album1), hash(album2))
+ def test_eq_date(self):
+ date = '1977-01-01'
+ album1 = Album(date=date)
+ album2 = Album(date=date)
+ self.assertEqual(album1, album2)
+ self.assertEqual(hash(album1), hash(album2))
+
def test_eq_musibrainz_id(self):
- album1 = Album(musicbrainz_id=u'id')
- album2 = Album(musicbrainz_id=u'id')
+ album1 = Album(musicbrainz_id='id')
+ album2 = Album(musicbrainz_id='id')
self.assertEqual(album1, album2)
self.assertEqual(hash(album1), hash(album2))
def test_eq(self):
artists = [Artist()]
- album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2,
+ album1 = Album(
+ name='name', uri='uri', artists=artists, num_tracks=2,
musicbrainz_id='id')
- album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2,
+ album2 = Album(
+ name='name', uri='uri', artists=artists, num_tracks=2,
musicbrainz_id='id')
self.assertEqual(album1, album2)
self.assertEqual(hash(album1), hash(album2))
@@ -251,20 +312,20 @@ class AlbumTest(unittest.TestCase):
self.assertNotEqual(Album(), 'other')
def test_ne_name(self):
- album1 = Album(name=u'name1')
- album2 = Album(name=u'name2')
+ album1 = Album(name='name1')
+ album2 = Album(name='name2')
self.assertNotEqual(album1, album2)
self.assertNotEqual(hash(album1), hash(album2))
def test_ne_uri(self):
- album1 = Album(uri=u'uri1')
- album2 = Album(uri=u'uri2')
+ album1 = Album(uri='uri1')
+ album2 = Album(uri='uri2')
self.assertNotEqual(album1, album2)
self.assertNotEqual(hash(album1), hash(album2))
def test_ne_artists(self):
- album1 = Album(artists=[Artist(name=u'name1')])
- album2 = Album(artists=[Artist(name=u'name2')])
+ album1 = Album(artists=[Artist(name='name1')])
+ album2 = Album(artists=[Artist(name='name2')])
self.assertNotEqual(album1, album2)
self.assertNotEqual(hash(album1), hash(album2))
@@ -274,53 +335,44 @@ class AlbumTest(unittest.TestCase):
self.assertNotEqual(album1, album2)
self.assertNotEqual(hash(album1), hash(album2))
+ def test_ne_date(self):
+ album1 = Album(date='1977-01-01')
+ album2 = Album(date='1977-01-02')
+ self.assertNotEqual(album1, album2)
+ self.assertNotEqual(hash(album1), hash(album2))
+
def test_ne_musicbrainz_id(self):
- album1 = Album(musicbrainz_id=u'id1')
- album2 = Album(musicbrainz_id=u'id2')
+ album1 = Album(musicbrainz_id='id1')
+ album2 = Album(musicbrainz_id='id2')
self.assertNotEqual(album1, album2)
self.assertNotEqual(hash(album1), hash(album2))
def test_ne(self):
- album1 = Album(name=u'name1', uri=u'uri1',
- artists=[Artist(name=u'name1')], num_tracks=1,
- musicbrainz_id='id1')
- album2 = Album(name=u'name2', uri=u'uri2',
- artists=[Artist(name=u'name2')], num_tracks=2,
- musicbrainz_id='id2')
+ album1 = Album(
+ name='name1', uri='uri1', artists=[Artist(name='name1')],
+ num_tracks=1, musicbrainz_id='id1')
+ album2 = Album(
+ name='name2', uri='uri2', artists=[Artist(name='name2')],
+ num_tracks=2, musicbrainz_id='id2')
self.assertNotEqual(album1, album2)
self.assertNotEqual(hash(album1), hash(album2))
-class CpTrackTest(unittest.TestCase):
- def setUp(self):
- self.cpid = 123
- self.track = Track()
- self.cp_track = CpTrack(self.cpid, self.track)
-
- def test_cp_track_can_be_accessed_as_a_tuple(self):
- self.assertEqual(self.cpid, self.cp_track[0])
- self.assertEqual(self.track, self.cp_track[1])
-
- def test_cp_track_can_be_accessed_by_attribute_names(self):
- self.assertEqual(self.cpid, self.cp_track.cpid)
- self.assertEqual(self.track, self.cp_track.track)
-
-
class TrackTest(unittest.TestCase):
def test_uri(self):
- uri = u'an_uri'
+ uri = 'an_uri'
track = Track(uri=uri)
self.assertEqual(track.uri, uri)
self.assertRaises(AttributeError, setattr, track, 'uri', None)
def test_name(self):
- name = u'a name'
+ name = 'a name'
track = Track(name=name)
self.assertEqual(track.name, name)
self.assertRaises(AttributeError, setattr, track, 'name', None)
def test_artists(self):
- artists = [Artist(name=u'name1'), Artist(name=u'name2')]
+ artists = [Artist(name='name1'), Artist(name='name2')]
track = Track(artists=artists)
self.assertEqual(set(track.artists), set(artists))
self.assertRaises(AttributeError, setattr, track, 'artists', None)
@@ -356,11 +408,11 @@ class TrackTest(unittest.TestCase):
self.assertRaises(AttributeError, setattr, track, 'bitrate', None)
def test_musicbrainz_id(self):
- mb_id = u'mb-id'
+ mb_id = 'mb-id'
track = Track(musicbrainz_id=mb_id)
self.assertEqual(track.musicbrainz_id, mb_id)
- self.assertRaises(AttributeError, setattr, track,
- 'musicbrainz_id', None)
+ self.assertRaises(
+ AttributeError, setattr, track, 'musicbrainz_id', None)
def test_invalid_kwarg(self):
test = lambda: Track(foo='baz')
@@ -368,40 +420,50 @@ class TrackTest(unittest.TestCase):
def test_repr_without_artists(self):
self.assertEquals(
- "Track(artists=[], name='name', uri='uri')",
+ "Track(artists=[], name=u'name', uri=u'uri')",
repr(Track(uri='uri', name='name')))
def test_repr_with_artists(self):
self.assertEquals(
- "Track(artists=[Artist(name='foo')], name='name', uri='uri')",
+ "Track(artists=[Artist(name=u'foo')], name=u'name', uri=u'uri')",
repr(Track(uri='uri', name='name', artists=[Artist(name='foo')])))
def test_serialize_without_artists(self):
self.assertDictEqual(
- {'uri': 'uri', 'name': 'name'},
+ {'__model__': 'Track', 'uri': 'uri', 'name': 'name'},
Track(uri='uri', name='name').serialize())
def test_serialize_with_artists(self):
artist = Artist(name='foo')
self.assertDictEqual(
- {'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]},
+ {'__model__': 'Track', 'uri': 'uri', 'name': 'name',
+ 'artists': [artist.serialize()]},
Track(uri='uri', name='name', artists=[artist]).serialize())
def test_serialize_with_album(self):
album = Album(name='foo')
self.assertDictEqual(
- {'uri': 'uri', 'name': 'name', 'album': album.serialize()},
+ {'__model__': 'Track', 'uri': 'uri', 'name': 'name',
+ 'album': album.serialize()},
Track(uri='uri', name='name', album=album).serialize())
+ def test_to_json_and_back(self):
+ track1 = Track(
+ uri='uri', name='name', album=Album(name='foo'),
+ artists=[Artist(name='foo')])
+ serialized = json.dumps(track1, cls=ModelJSONEncoder)
+ track2 = json.loads(serialized, object_hook=model_json_decoder)
+ self.assertEqual(track1, track2)
+
def test_eq_uri(self):
- track1 = Track(uri=u'uri1')
- track2 = Track(uri=u'uri1')
+ track1 = Track(uri='uri1')
+ track2 = Track(uri='uri1')
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
def test_eq_name(self):
- track1 = Track(name=u'name1')
- track2 = Track(name=u'name1')
+ track1 = Track(name='name1')
+ track2 = Track(name='name1')
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
@@ -413,8 +475,8 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2))
def test_eq_artists_order(self):
- artist1 = Artist(name=u'name1')
- artist2 = Artist(name=u'name2')
+ artist1 = Artist(name='name1')
+ artist2 = Artist(name='name2')
track1 = Track(artists=[artist1, artist2])
track2 = Track(artists=[artist2, artist1])
self.assertEqual(track1, track2)
@@ -453,8 +515,8 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2))
def test_eq_musibrainz_id(self):
- track1 = Track(musicbrainz_id=u'id')
- track2 = Track(musicbrainz_id=u'id')
+ track1 = Track(musicbrainz_id='id')
+ track2 = Track(musicbrainz_id='id')
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
@@ -462,12 +524,12 @@ class TrackTest(unittest.TestCase):
date = '1977-01-01'
artists = [Artist()]
album = Album()
- track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
- track_no=1, date=date, length=100, bitrate=100,
- musicbrainz_id='id')
- track2 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
- track_no=1, date=date, length=100, bitrate=100,
- musicbrainz_id='id')
+ track1 = Track(
+ uri='uri', name='name', artists=artists, album=album, track_no=1,
+ date=date, length=100, bitrate=100, musicbrainz_id='id')
+ track2 = Track(
+ uri='uri', name='name', artists=artists, album=album, track_no=1,
+ date=date, length=100, bitrate=100, musicbrainz_id='id')
self.assertEqual(track1, track2)
self.assertEqual(hash(track1), hash(track2))
@@ -478,26 +540,26 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(Track(), 'other')
def test_ne_uri(self):
- track1 = Track(uri=u'uri1')
- track2 = Track(uri=u'uri2')
+ track1 = Track(uri='uri1')
+ track2 = Track(uri='uri2')
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
def test_ne_name(self):
- track1 = Track(name=u'name1')
- track2 = Track(name=u'name2')
+ track1 = Track(name='name1')
+ track2 = Track(name='name2')
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
def test_ne_artists(self):
- track1 = Track(artists=[Artist(name=u'name1')])
- track2 = Track(artists=[Artist(name=u'name2')])
+ track1 = Track(artists=[Artist(name='name1')])
+ track2 = Track(artists=[Artist(name='name2')])
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
def test_ne_album(self):
- track1 = Track(album=Album(name=u'name1'))
- track2 = Track(album=Album(name=u'name2'))
+ track1 = Track(album=Album(name='name1'))
+ track2 = Track(album=Album(name='name2'))
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
@@ -526,33 +588,109 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(hash(track1), hash(track2))
def test_ne_musicbrainz_id(self):
- track1 = Track(musicbrainz_id=u'id1')
- track2 = Track(musicbrainz_id=u'id2')
+ track1 = Track(musicbrainz_id='id1')
+ track2 = Track(musicbrainz_id='id2')
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
def test_ne(self):
- track1 = Track(uri=u'uri1', name=u'name1',
- artists=[Artist(name=u'name1')], album=Album(name=u'name1'),
- track_no=1, date='1977-01-01', length=100, bitrate=100,
- musicbrainz_id='id1')
- track2 = Track(uri=u'uri2', name=u'name2',
- artists=[Artist(name=u'name2')], album=Album(name=u'name2'),
- track_no=2, date='1977-01-02', length=200, bitrate=200,
- musicbrainz_id='id2')
+ track1 = Track(
+ uri='uri1', name='name1', artists=[Artist(name='name1')],
+ album=Album(name='name1'), track_no=1, date='1977-01-01',
+ length=100, bitrate=100, musicbrainz_id='id1')
+ track2 = Track(
+ uri='uri2', name='name2', artists=[Artist(name='name2')],
+ album=Album(name='name2'), track_no=2, date='1977-01-02',
+ length=200, bitrate=200, musicbrainz_id='id2')
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
+class TlTrackTest(unittest.TestCase):
+ def test_tlid(self):
+ tlid = 123
+ tl_track = TlTrack(tlid=tlid)
+ self.assertEqual(tl_track.tlid, tlid)
+ self.assertRaises(AttributeError, setattr, tl_track, 'tlid', None)
+
+ def test_track(self):
+ track = Track()
+ tl_track = TlTrack(track=track)
+ self.assertEqual(tl_track.track, track)
+ self.assertRaises(AttributeError, setattr, tl_track, 'track', None)
+
+ def test_invalid_kwarg(self):
+ test = lambda: TlTrack(foo='baz')
+ self.assertRaises(TypeError, test)
+
+ def test_positional_args(self):
+ tlid = 123
+ track = Track()
+ tl_track = TlTrack(tlid, track)
+ self.assertEqual(tl_track.tlid, tlid)
+ self.assertEqual(tl_track.track, track)
+
+ def test_iteration(self):
+ tlid = 123
+ track = Track()
+ tl_track = TlTrack(tlid, track)
+ (tlid2, track2) = tl_track
+ self.assertEqual(tlid2, tlid)
+ self.assertEqual(track2, track)
+
+ def test_repr(self):
+ self.assertEquals(
+ "TlTrack(tlid=123, track=Track(artists=[], uri=u'uri'))",
+ repr(TlTrack(tlid=123, track=Track(uri='uri'))))
+
+ def test_serialize(self):
+ track = Track(uri='uri', name='name')
+ self.assertDictEqual(
+ {'__model__': 'TlTrack', 'tlid': 123, 'track': track.serialize()},
+ TlTrack(tlid=123, track=track).serialize())
+
+ def test_to_json_and_back(self):
+ tl_track1 = TlTrack(tlid=123, track=Track(uri='uri', name='name'))
+ serialized = json.dumps(tl_track1, cls=ModelJSONEncoder)
+ tl_track2 = json.loads(serialized, object_hook=model_json_decoder)
+ self.assertEqual(tl_track1, tl_track2)
+
+ def test_eq(self):
+ tlid = 123
+ track = Track()
+ tl_track1 = TlTrack(tlid=tlid, track=track)
+ tl_track2 = TlTrack(tlid=tlid, track=track)
+ self.assertEqual(tl_track1, tl_track2)
+ self.assertEqual(hash(tl_track1), hash(tl_track2))
+
+ def test_eq_none(self):
+ self.assertNotEqual(TlTrack(), None)
+
+ def test_eq_other(self):
+ self.assertNotEqual(TlTrack(), 'other')
+
+ def test_ne_tlid(self):
+ tl_track1 = TlTrack(tlid=123)
+ tl_track2 = TlTrack(tlid=321)
+ self.assertNotEqual(tl_track1, tl_track2)
+ self.assertNotEqual(hash(tl_track1), hash(tl_track2))
+
+ def test_ne_track(self):
+ tl_track1 = TlTrack(track=Track(uri='a'))
+ tl_track2 = TlTrack(track=Track(uri='b'))
+ self.assertNotEqual(tl_track1, tl_track2)
+ self.assertNotEqual(hash(tl_track1), hash(tl_track2))
+
+
class PlaylistTest(unittest.TestCase):
def test_uri(self):
- uri = u'an_uri'
+ uri = 'an_uri'
playlist = Playlist(uri=uri)
self.assertEqual(playlist.uri, uri)
self.assertRaises(AttributeError, setattr, playlist, 'uri', None)
def test_name(self):
- name = u'a name'
+ name = 'a name'
playlist = Playlist(name=name)
self.assertEqual(playlist.name, name)
self.assertRaises(AttributeError, setattr, playlist, 'name', None)
@@ -572,40 +710,43 @@ class PlaylistTest(unittest.TestCase):
last_modified = datetime.datetime.now()
playlist = Playlist(last_modified=last_modified)
self.assertEqual(playlist.last_modified, last_modified)
- self.assertRaises(AttributeError, setattr, playlist, 'last_modified',
- None)
+ self.assertRaises(
+ AttributeError, setattr, playlist, 'last_modified', None)
def test_with_new_uri(self):
tracks = [Track()]
last_modified = datetime.datetime.now()
- playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
+ playlist = Playlist(
+ uri='an uri', name='a name', tracks=tracks,
last_modified=last_modified)
- new_playlist = playlist.copy(uri=u'another uri')
- self.assertEqual(new_playlist.uri, u'another uri')
- self.assertEqual(new_playlist.name, u'a name')
+ new_playlist = playlist.copy(uri='another uri')
+ self.assertEqual(new_playlist.uri, 'another uri')
+ self.assertEqual(new_playlist.name, 'a name')
self.assertEqual(list(new_playlist.tracks), tracks)
self.assertEqual(new_playlist.last_modified, last_modified)
def test_with_new_name(self):
tracks = [Track()]
last_modified = datetime.datetime.now()
- playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
+ playlist = Playlist(
+ uri='an uri', name='a name', tracks=tracks,
last_modified=last_modified)
- new_playlist = playlist.copy(name=u'another name')
- self.assertEqual(new_playlist.uri, u'an uri')
- self.assertEqual(new_playlist.name, u'another name')
+ new_playlist = playlist.copy(name='another name')
+ self.assertEqual(new_playlist.uri, 'an uri')
+ self.assertEqual(new_playlist.name, 'another name')
self.assertEqual(list(new_playlist.tracks), tracks)
self.assertEqual(new_playlist.last_modified, last_modified)
def test_with_new_tracks(self):
tracks = [Track()]
last_modified = datetime.datetime.now()
- playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
+ playlist = Playlist(
+ uri='an uri', name='a name', tracks=tracks,
last_modified=last_modified)
new_tracks = [Track(), Track()]
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.uri, 'an uri')
+ self.assertEqual(new_playlist.name, 'a name')
self.assertEqual(list(new_playlist.tracks), new_tracks)
self.assertEqual(new_playlist.last_modified, last_modified)
@@ -613,11 +754,12 @@ class PlaylistTest(unittest.TestCase):
tracks = [Track()]
last_modified = datetime.datetime.now()
new_last_modified = last_modified + datetime.timedelta(1)
- playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
+ playlist = Playlist(
+ uri='an uri', name='a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(last_modified=new_last_modified)
- self.assertEqual(new_playlist.uri, u'an uri')
- self.assertEqual(new_playlist.name, u'a name')
+ self.assertEqual(new_playlist.uri, 'an uri')
+ self.assertEqual(new_playlist.name, 'a name')
self.assertEqual(list(new_playlist.tracks), tracks)
self.assertEqual(new_playlist.last_modified, new_last_modified)
@@ -627,35 +769,42 @@ class PlaylistTest(unittest.TestCase):
def test_repr_without_tracks(self):
self.assertEquals(
- "Playlist(name='name', tracks=[], uri='uri')",
+ "Playlist(name=u'name', tracks=[], uri=u'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')",
+ "Playlist(name=u'name', tracks=[Track(artists=[], name=u'foo')], "
+ "uri=u'uri')",
repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')])))
def test_serialize_without_tracks(self):
self.assertDictEqual(
- {'uri': 'uri', 'name': 'name'},
+ {'__model__': 'Playlist', 'uri': 'uri', 'name': 'name'},
Playlist(uri='uri', name='name').serialize())
def test_serialize_with_tracks(self):
track = Track(name='foo')
self.assertDictEqual(
- {'uri': 'uri', 'name': 'name', 'tracks': [track.serialize()]},
+ {'__model__': 'Playlist', 'uri': 'uri', 'name': 'name',
+ 'tracks': [track.serialize()]},
Playlist(uri='uri', name='name', tracks=[track]).serialize())
+ def test_to_json_and_back(self):
+ playlist1 = Playlist(uri='uri', name='name')
+ serialized = json.dumps(playlist1, cls=ModelJSONEncoder)
+ playlist2 = json.loads(serialized, object_hook=model_json_decoder)
+ self.assertEqual(playlist1, playlist2)
+
def test_eq_name(self):
- playlist1 = Playlist(name=u'name')
- playlist2 = Playlist(name=u'name')
+ playlist1 = Playlist(name='name')
+ playlist2 = Playlist(name='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')
+ playlist1 = Playlist(uri='uri')
+ playlist2 = Playlist(uri='uri')
self.assertEqual(playlist1, playlist2)
self.assertEqual(hash(playlist1), hash(playlist2))
@@ -666,7 +815,7 @@ class PlaylistTest(unittest.TestCase):
self.assertEqual(playlist1, playlist2)
self.assertEqual(hash(playlist1), hash(playlist2))
- def test_eq_uri(self):
+ def test_eq_last_modified(self):
playlist1 = Playlist(last_modified=1)
playlist2 = Playlist(last_modified=1)
self.assertEqual(playlist1, playlist2)
@@ -674,10 +823,10 @@ class PlaylistTest(unittest.TestCase):
def test_eq(self):
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)
+ playlist1 = Playlist(
+ uri='uri', name='name', tracks=tracks, last_modified=1)
+ playlist2 = Playlist(
+ uri='uri', name='name', tracks=tracks, last_modified=1)
self.assertEqual(playlist1, playlist2)
self.assertEqual(hash(playlist1), hash(playlist2))
@@ -688,34 +837,35 @@ class PlaylistTest(unittest.TestCase):
self.assertNotEqual(Playlist(), 'other')
def test_ne_name(self):
- playlist1 = Playlist(name=u'name1')
- playlist2 = Playlist(name=u'name2')
+ playlist1 = Playlist(name='name1')
+ playlist2 = Playlist(name='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')
+ playlist1 = Playlist(uri='uri1')
+ playlist2 = Playlist(uri='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')])
+ playlist1 = Playlist(tracks=[Track(uri='uri1')])
+ playlist2 = Playlist(tracks=[Track(uri='uri2')])
self.assertNotEqual(playlist1, playlist2)
self.assertNotEqual(hash(playlist1), hash(playlist2))
- def test_ne_uri(self):
+ def test_ne_last_modified(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)
+ playlist1 = Playlist(
+ uri='uri1', name='name2', tracks=[Track(uri='uri1')],
+ last_modified=1)
+ playlist2 = Playlist(
+ uri='uri2', name='name2', tracks=[Track(uri='uri2')],
+ last_modified=2)
self.assertNotEqual(playlist1, playlist2)
self.assertNotEqual(hash(playlist1), hash(playlist2))
-
diff --git a/tests/outputs/__init__.py b/tests/outputs/__init__.py
index e69de29b..baffc488 100644
--- a/tests/outputs/__init__.py
+++ b/tests/outputs/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/tests/scanner_test.py b/tests/scanner_test.py
index 91e67e11..08784458 100644
--- a/tests/scanner_test.py
+++ b/tests/scanner_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
from datetime import date
from mopidy.scanner import Scanner, translator
@@ -17,14 +19,14 @@ class TranslatorTest(unittest.TestCase):
def setUp(self):
self.data = {
'uri': 'uri',
- 'album': u'albumname',
+ 'album': 'albumname',
'track-number': 1,
- 'artist': u'name',
+ 'artist': 'name',
'album-artist': 'albumartistname',
- 'title': u'trackname',
+ 'title': 'trackname',
'track-count': 2,
'date': FakeGstDate(2006, 1, 1,),
- 'container-format': u'ID3 tag',
+ 'container-format': 'ID3 tag',
'duration': 4531,
'musicbrainz-trackid': 'mbtrackid',
'musicbrainz-albumid': 'mbalbumid',
@@ -134,8 +136,8 @@ class ScannerTest(unittest.TestCase):
self.data = {}
def scan(self, path):
- scanner = Scanner(path_to_data_dir(path),
- self.data_callback, self.error_callback)
+ scanner = Scanner(
+ path_to_data_dir(path), self.data_callback, self.error_callback)
scanner.start()
def check(self, name, key, value):
@@ -160,8 +162,9 @@ class ScannerTest(unittest.TestCase):
def test_uri_is_set(self):
self.scan('scanner/simple')
- self.check('scanner/simple/song1.mp3', 'uri', 'file://'
- + path_to_data_dir('scanner/simple/song1.mp3'))
+ self.check(
+ 'scanner/simple/song1.mp3', 'uri',
+ 'file://%s' % path_to_data_dir('scanner/simple/song1.mp3'))
def test_duration_is_set(self):
self.scan('scanner/simple')
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
index e69de29b..baffc488 100644
--- a/tests/utils/__init__.py
+++ b/tests/utils/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/tests/utils/deps_test.py b/tests/utils/deps_test.py
index f5aa0b1e..168f98e5 100644
--- a/tests/utils/deps_test.py
+++ b/tests/utils/deps_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import platform
import pygst
@@ -65,10 +67,12 @@ class DepsTest(unittest.TestCase):
result = deps.gstreamer_info()
self.assertEquals('GStreamer', result['name'])
- self.assertEquals('.'.join(map(str, gst.get_gst_version())), result['version'])
+ self.assertEquals(
+ '.'.join(map(str, gst.get_gst_version())), result['version'])
self.assertIn('gst', result['path'])
self.assertIn('Python wrapper: gst-python', result['other'])
- self.assertIn('.'.join(map(str, gst.get_pygst_version())), result['other'])
+ self.assertIn(
+ '.'.join(map(str, gst.get_pygst_version())), result['other'])
self.assertIn('Relevant elements:', result['other'])
def test_pykka_info(self):
diff --git a/tests/utils/decode_test.py b/tests/utils/encoding_test.py
similarity index 59%
rename from tests/utils/decode_test.py
rename to tests/utils/encoding_test.py
index edbfe651..1a4e56c5 100644
--- a/tests/utils/decode_test.py
+++ b/tests/utils/encoding_test.py
@@ -1,32 +1,34 @@
+from __future__ import unicode_literals
+
import mock
-from mopidy.utils import locale_decode
+from mopidy.utils.encoding import locale_decode
from tests import unittest
-@mock.patch('mopidy.utils.locale.getpreferredencoding')
+@mock.patch('mopidy.utils.encoding.locale.getpreferredencoding')
class LocaleDecodeTest(unittest.TestCase):
def test_can_decode_utf8_strings_with_french_content(self, mock):
mock.return_value = 'UTF-8'
result = locale_decode(
- '[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
+ b'[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
- self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
+ self.assertEquals('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
def test_can_decode_an_ioerror_with_french_content(self, mock):
mock.return_value = 'UTF-8'
- error = IOError(98, 'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
+ error = IOError(98, b'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e')
result = locale_decode(error)
- self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
+ self.assertEquals('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result)
def test_does_not_use_locale_to_decode_unicode_strings(self, mock):
mock.return_value = 'UTF-8'
- locale_decode(u'abc')
+ locale_decode('abc')
self.assertFalse(mock.called)
diff --git a/tests/utils/init_test.py b/tests/utils/importing_test.py
similarity index 65%
rename from tests/utils/init_test.py
rename to tests/utils/importing_test.py
index bdd0adc5..5be4078b 100644
--- a/tests/utils/init_test.py
+++ b/tests/utils/importing_test.py
@@ -1,4 +1,6 @@
-from mopidy import utils
+from __future__ import unicode_literals
+
+from mopidy.utils import importing
from tests import unittest
@@ -6,22 +8,22 @@ from tests import unittest
class GetClassTest(unittest.TestCase):
def test_loading_module_that_does_not_exist(self):
with self.assertRaises(ImportError):
- utils.get_class('foo.bar.Baz')
+ importing.get_class('foo.bar.Baz')
def test_loading_class_that_does_not_exist(self):
with self.assertRaises(ImportError):
- utils.get_class('unittest.FooBarBaz')
+ importing.get_class('unittest.FooBarBaz')
def test_loading_incorrect_class_path(self):
with self.assertRaises(ImportError):
- utils.get_class('foobarbaz')
+ importing.get_class('foobarbaz')
def test_import_error_message_contains_complete_class_path(self):
try:
- utils.get_class('foo.bar.Baz')
+ importing.get_class('foo.bar.Baz')
except ImportError as e:
self.assertIn('foo.bar.Baz', str(e))
def test_loading_existing_class(self):
- cls = utils.get_class('unittest.TestCase')
+ cls = importing.get_class('unittest.TestCase')
self.assertEqual(cls.__name__, 'TestCase')
diff --git a/tests/utils/network/__init__.py b/tests/utils/network/__init__.py
index e69de29b..baffc488 100644
--- a/tests/utils/network/__init__.py
+++ b/tests/utils/network/__init__.py
@@ -0,0 +1 @@
+from __future__ import unicode_literals
diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py
index 0ca86a7f..3e63cdfc 100644
--- a/tests/utils/network/connection_test.py
+++ b/tests/utils/network/connection_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import errno
import gobject
import logging
@@ -17,44 +19,50 @@ class ConnectionTest(unittest.TestCase):
def test_init_ensure_nonblocking_io(self):
sock = Mock(spec=socket.SocketType)
- network.Connection.__init__(self.mock, Mock(), sock,
- (sentinel.host, sentinel.port), sentinel.timeout)
+ network.Connection.__init__(
+ self.mock, Mock(), {}, sock, (sentinel.host, sentinel.port),
+ sentinel.timeout)
sock.setblocking.assert_called_once_with(False)
def test_init_starts_actor(self):
protocol = Mock(spec=network.LineProtocol)
- network.Connection.__init__(self.mock, protocol, Mock(),
- (sentinel.host, sentinel.port), sentinel.timeout)
+ network.Connection.__init__(
+ self.mock, protocol, {}, Mock(), (sentinel.host, sentinel.port),
+ sentinel.timeout)
protocol.start.assert_called_once_with(self.mock)
def test_init_enables_recv_and_timeout(self):
- network.Connection.__init__(self.mock, Mock(), Mock(),
- (sentinel.host, sentinel.port), sentinel.timeout)
+ network.Connection.__init__(
+ self.mock, Mock(), {}, Mock(), (sentinel.host, sentinel.port),
+ sentinel.timeout)
self.mock.enable_recv.assert_called_once_with()
self.mock.enable_timeout.assert_called_once_with()
def test_init_stores_values_in_attributes(self):
addr = (sentinel.host, sentinel.port)
protocol = Mock(spec=network.LineProtocol)
+ protocol_kwargs = {}
sock = Mock(spec=socket.SocketType)
network.Connection.__init__(
- self.mock, protocol, sock, addr, sentinel.timeout)
+ self.mock, protocol, protocol_kwargs, sock, addr, sentinel.timeout)
self.assertEqual(sock, self.mock.sock)
self.assertEqual(protocol, self.mock.protocol)
+ self.assertEqual(protocol_kwargs, self.mock.protocol_kwargs)
self.assertEqual(sentinel.timeout, self.mock.timeout)
self.assertEqual(sentinel.host, self.mock.host)
self.assertEqual(sentinel.port, self.mock.port)
def test_init_handles_ipv6_addr(self):
- addr = (sentinel.host, sentinel.port,
- sentinel.flowinfo, sentinel.scopeid)
+ addr = (
+ sentinel.host, sentinel.port, sentinel.flowinfo, sentinel.scopeid)
protocol = Mock(spec=network.LineProtocol)
+ protocol_kwargs = {}
sock = Mock(spec=socket.SocketType)
network.Connection.__init__(
- self.mock, protocol, sock, addr, sentinel.timeout)
+ self.mock, protocol, protocol_kwargs, sock, addr, sentinel.timeout)
self.assertEqual(sentinel.host, self.mock.host)
self.assertEqual(sentinel.port, self.mock.port)
@@ -135,8 +143,8 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock()
self.mock.sock = Mock(spec=socket.SocketType)
- network.Connection.stop(self.mock, sentinel.reason,
- level=sentinel.level)
+ network.Connection.stop(
+ self.mock, sentinel.reason, level=sentinel.level)
network.logger.log.assert_called_once_with(
sentinel.level, sentinel.reason)
@@ -157,7 +165,8 @@ class ConnectionTest(unittest.TestCase):
gobject.io_add_watch.return_value = sentinel.tag
network.Connection.enable_recv(self.mock)
- gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
+ gobject.io_add_watch.assert_called_once_with(
+ sentinel.fileno,
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
self.mock.recv_callback)
self.assertEqual(sentinel.tag, self.mock.recv_id)
@@ -210,7 +219,8 @@ class ConnectionTest(unittest.TestCase):
gobject.io_add_watch.return_value = sentinel.tag
network.Connection.enable_send(self.mock)
- gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
+ gobject.io_add_watch.assert_called_once_with(
+ sentinel.fileno,
gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP,
self.mock.send_callback)
self.assertEqual(sentinel.tag, self.mock.send_id)
@@ -267,8 +277,8 @@ class ConnectionTest(unittest.TestCase):
gobject.timeout_add_seconds.return_value = sentinel.tag
network.Connection.enable_timeout(self.mock)
- gobject.timeout_add_seconds.assert_called_once_with(10,
- self.mock.timeout_callback)
+ gobject.timeout_add_seconds.assert_called_once_with(
+ 10, self.mock.timeout_callback)
self.assertEqual(sentinel.tag, self.mock.timeout_id)
@patch.object(gobject, 'timeout_add_seconds', new=Mock())
@@ -356,24 +366,25 @@ class ConnectionTest(unittest.TestCase):
self.mock.sock = Mock(spec=socket.SocketType)
self.mock.actor_ref = Mock()
- self.assertTrue(network.Connection.recv_callback(self.mock,
- sentinel.fd, gobject.IO_IN | gobject.IO_ERR))
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR))
self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_respects_io_hup(self):
self.mock.sock = Mock(spec=socket.SocketType)
self.mock.actor_ref = Mock()
- self.assertTrue(network.Connection.recv_callback(self.mock,
- sentinel.fd, gobject.IO_IN | gobject.IO_HUP))
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP))
self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_respects_io_hup_and_io_err(self):
self.mock.sock = Mock(spec=socket.SocketType)
self.mock.actor_ref = Mock()
- self.assertTrue(network.Connection.recv_callback(self.mock,
- sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR))
+ self.assertTrue(network.Connection.recv_callback(
+ self.mock, sentinel.fd,
+ gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR))
self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_sends_data_to_actor(self):
@@ -429,8 +440,8 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock()
self.mock.send_buffer = ''
- self.assertTrue(network.Connection.send_callback(self.mock,
- sentinel.fd, gobject.IO_IN | gobject.IO_ERR))
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR))
self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_hup(self):
@@ -440,8 +451,8 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock()
self.mock.send_buffer = ''
- self.assertTrue(network.Connection.send_callback(self.mock,
- sentinel.fd, gobject.IO_IN | gobject.IO_HUP))
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP))
self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_hup_and_io_err(self):
@@ -451,8 +462,9 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock()
self.mock.send_buffer = ''
- self.assertTrue(network.Connection.send_callback(self.mock,
- sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR))
+ self.assertTrue(network.Connection.send_callback(
+ self.mock, sentinel.fd,
+ gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR))
self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_acquires_and_releases_lock(self):
diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py
index b323de09..530c708c 100644
--- a/tests/utils/network/lineprotocol_test.py
+++ b/tests/utils/network/lineprotocol_test.py
@@ -1,4 +1,6 @@
-#encoding: utf-8
+# encoding: utf-8
+
+from __future__ import unicode_literals
import re
from mock import sentinel, Mock
@@ -14,23 +16,23 @@ class LineProtocolTest(unittest.TestCase):
self.mock.terminator = network.LineProtocol.terminator
self.mock.encoding = network.LineProtocol.encoding
- self.mock.delimeter = network.LineProtocol.delimeter
+ self.mock.delimiter = network.LineProtocol.delimiter
self.mock.prevent_timeout = False
def test_init_stores_values_in_attributes(self):
- delimeter = re.compile(network.LineProtocol.terminator)
+ delimiter = re.compile(network.LineProtocol.terminator)
network.LineProtocol.__init__(self.mock, sentinel.connection)
self.assertEqual(sentinel.connection, self.mock.connection)
self.assertEqual('', self.mock.recv_buffer)
- self.assertEqual(delimeter, self.mock.delimeter)
+ self.assertEqual(delimiter, self.mock.delimiter)
self.assertFalse(self.mock.prevent_timeout)
- def test_init_compiles_delimeter(self):
- self.mock.delimeter = '\r?\n'
- delimeter = re.compile('\r?\n')
+ def test_init_compiles_delimiter(self):
+ self.mock.delimiter = '\r?\n'
+ delimiter = re.compile('\r?\n')
network.LineProtocol.__init__(self.mock, sentinel.connection)
- self.assertEqual(delimeter, self.mock.delimeter)
+ self.assertEqual(delimiter, self.mock.delimiter)
def test_on_receive_no_new_lines_adds_to_recv_buffer(self):
self.mock.connection = Mock(spec=network.Connection)
@@ -103,26 +105,26 @@ class LineProtocolTest(unittest.TestCase):
self.mock.parse_lines.return_value = ['line1', 'line2']
self.mock.decode.return_value = sentinel.decoded
- network.LineProtocol.on_receive(self.mock,
- {'received': 'line1\nline2\n'})
+ network.LineProtocol.on_receive(
+ self.mock, {'received': 'line1\nline2\n'})
self.assertEqual(2, self.mock.on_line_received.call_count)
def test_parse_lines_emtpy_buffer(self):
- self.mock.delimeter = re.compile(r'\n')
+ self.mock.delimiter = re.compile(r'\n')
self.mock.recv_buffer = ''
lines = network.LineProtocol.parse_lines(self.mock)
self.assertRaises(StopIteration, lines.next)
def test_parse_lines_no_terminator(self):
- self.mock.delimeter = re.compile(r'\n')
+ self.mock.delimiter = re.compile(r'\n')
self.mock.recv_buffer = 'data'
lines = network.LineProtocol.parse_lines(self.mock)
self.assertRaises(StopIteration, lines.next)
def test_parse_lines_termintor(self):
- self.mock.delimeter = re.compile(r'\n')
+ self.mock.delimiter = re.compile(r'\n')
self.mock.recv_buffer = 'data\n'
lines = network.LineProtocol.parse_lines(self.mock)
@@ -131,7 +133,7 @@ class LineProtocolTest(unittest.TestCase):
self.assertEqual('', self.mock.recv_buffer)
def test_parse_lines_termintor_with_carriage_return(self):
- self.mock.delimeter = re.compile(r'\r?\n')
+ self.mock.delimiter = re.compile(r'\r?\n')
self.mock.recv_buffer = 'data\r\n'
lines = network.LineProtocol.parse_lines(self.mock)
@@ -140,7 +142,7 @@ class LineProtocolTest(unittest.TestCase):
self.assertEqual('', self.mock.recv_buffer)
def test_parse_lines_no_data_before_terminator(self):
- self.mock.delimeter = re.compile(r'\n')
+ self.mock.delimiter = re.compile(r'\n')
self.mock.recv_buffer = '\n'
lines = network.LineProtocol.parse_lines(self.mock)
@@ -149,7 +151,7 @@ class LineProtocolTest(unittest.TestCase):
self.assertEqual('', self.mock.recv_buffer)
def test_parse_lines_extra_data_after_terminator(self):
- self.mock.delimeter = re.compile(r'\n')
+ self.mock.delimiter = re.compile(r'\n')
self.mock.recv_buffer = 'data1\ndata2'
lines = network.LineProtocol.parse_lines(self.mock)
@@ -158,16 +160,16 @@ class LineProtocolTest(unittest.TestCase):
self.assertEqual('data2', self.mock.recv_buffer)
def test_parse_lines_unicode(self):
- self.mock.delimeter = re.compile(r'\n')
- self.mock.recv_buffer = u'æøå\n'.encode('utf-8')
+ self.mock.delimiter = re.compile(r'\n')
+ self.mock.recv_buffer = 'æøå\n'.encode('utf-8')
lines = network.LineProtocol.parse_lines(self.mock)
- self.assertEqual(u'æøå'.encode('utf-8'), lines.next())
+ self.assertEqual('æøå'.encode('utf-8'), lines.next())
self.assertRaises(StopIteration, lines.next)
self.assertEqual('', self.mock.recv_buffer)
def test_parse_lines_multiple_lines(self):
- self.mock.delimeter = re.compile(r'\n')
+ self.mock.delimiter = re.compile(r'\n')
self.mock.recv_buffer = 'abc\ndef\nghi\njkl'
lines = network.LineProtocol.parse_lines(self.mock)
@@ -178,7 +180,7 @@ class LineProtocolTest(unittest.TestCase):
self.assertEqual('jkl', self.mock.recv_buffer)
def test_parse_lines_multiple_calls(self):
- self.mock.delimeter = re.compile(r'\n')
+ self.mock.delimiter = re.compile(r'\n')
self.mock.recv_buffer = 'data1'
lines = network.LineProtocol.parse_lines(self.mock)
@@ -208,10 +210,10 @@ class LineProtocolTest(unittest.TestCase):
def test_send_line_encodes_joined_lines_with_final_terminator(self):
self.mock.connection = Mock(spec=network.Connection)
- self.mock.join_lines.return_value = u'lines\n'
+ self.mock.join_lines.return_value = 'lines\n'
network.LineProtocol.send_lines(self.mock, sentinel.lines)
- self.mock.encode.assert_called_once_with(u'lines\n')
+ self.mock.encode.assert_called_once_with('lines\n')
def test_send_lines_sends_encoded_string(self):
self.mock.connection = Mock(spec=network.Connection)
@@ -222,11 +224,11 @@ class LineProtocolTest(unittest.TestCase):
self.mock.connection.queue_send.assert_called_once_with(sentinel.data)
def test_join_lines_returns_empty_string_for_no_lines(self):
- self.assertEqual(u'', network.LineProtocol.join_lines(self.mock, []))
+ self.assertEqual('', network.LineProtocol.join_lines(self.mock, []))
def test_join_lines_returns_joined_lines(self):
- self.assertEqual(u'1\n2\n', network.LineProtocol.join_lines(
- self.mock, [u'1', u'2']))
+ self.assertEqual('1\n2\n', network.LineProtocol.join_lines(
+ self.mock, ['1', '2']))
def test_decode_calls_decode_on_string(self):
string = Mock()
@@ -236,13 +238,13 @@ class LineProtocolTest(unittest.TestCase):
def test_decode_plain_ascii(self):
result = network.LineProtocol.decode(self.mock, 'abc')
- self.assertEqual(u'abc', result)
+ self.assertEqual('abc', result)
self.assertEqual(unicode, type(result))
def test_decode_utf8(self):
result = network.LineProtocol.decode(
- self.mock, u'æøå'.encode('utf-8'))
- self.assertEqual(u'æøå', result)
+ self.mock, 'æøå'.encode('utf-8'))
+ self.assertEqual('æøå', result)
self.assertEqual(unicode, type(result))
def test_decode_invalid_data(self):
@@ -259,13 +261,13 @@ class LineProtocolTest(unittest.TestCase):
string.encode.assert_called_once_with(self.mock.encoding)
def test_encode_plain_ascii(self):
- result = network.LineProtocol.encode(self.mock, u'abc')
+ result = network.LineProtocol.encode(self.mock, 'abc')
self.assertEqual('abc', result)
self.assertEqual(str, type(result))
def test_encode_utf8(self):
- result = network.LineProtocol.encode(self.mock, u'æøå')
- self.assertEqual(u'æøå'.encode('utf-8'), result)
+ result = network.LineProtocol.encode(self.mock, 'æøå')
+ self.assertEqual('æøå'.encode('utf-8'), result)
self.assertEqual(str, type(result))
def test_encode_invalid_data(self):
diff --git a/tests/utils/network/server_test.py b/tests/utils/network/server_test.py
index e0399525..3f7da337 100644
--- a/tests/utils/network/server_test.py
+++ b/tests/utils/network/server_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import errno
import gobject
import socket
@@ -13,8 +15,8 @@ class ServerTest(unittest.TestCase):
self.mock = Mock(spec=network.Server)
def test_init_calls_create_server_socket(self):
- network.Server.__init__(self.mock, sentinel.host,
- sentinel.port, sentinel.protocol)
+ network.Server.__init__(
+ self.mock, sentinel.host, sentinel.port, sentinel.protocol)
self.mock.create_server_socket.assert_called_once_with(
sentinel.host, sentinel.port)
@@ -23,8 +25,8 @@ class ServerTest(unittest.TestCase):
sock.fileno.return_value = sentinel.fileno
self.mock.create_server_socket.return_value = sock
- network.Server.__init__(self.mock, sentinel.host,
- sentinel.port, sentinel.protocol)
+ network.Server.__init__(
+ self.mock, sentinel.host, sentinel.port, sentinel.protocol)
self.mock.register_server_socket.assert_called_once_with(
sentinel.fileno)
@@ -33,17 +35,18 @@ class ServerTest(unittest.TestCase):
sock.fileno.side_effect = socket.error
self.mock.create_server_socket.return_value = sock
- self.assertRaises(socket.error, network.Server.__init__,
- self.mock, sentinel.host, sentinel.port, sentinel.protocol)
+ self.assertRaises(
+ socket.error, network.Server.__init__, self.mock, sentinel.host,
+ sentinel.port, sentinel.protocol)
def test_init_stores_values_in_attributes(self):
# This need to be a mock and no a sentinel as fileno() is called on it
sock = Mock(spec=socket.SocketType)
self.mock.create_server_socket.return_value = sock
- network.Server.__init__(self.mock, sentinel.host, sentinel.port,
- sentinel.protocol, max_connections=sentinel.max_connections,
- timeout=sentinel.timeout)
+ network.Server.__init__(
+ self.mock, sentinel.host, sentinel.port, sentinel.protocol,
+ max_connections=sentinel.max_connections, timeout=sentinel.timeout)
self.assertEqual(sentinel.protocol, self.mock.protocol)
self.assertEqual(sentinel.max_connections, self.mock.max_connections)
self.assertEqual(sentinel.timeout, self.mock.timeout)
@@ -53,8 +56,8 @@ class ServerTest(unittest.TestCase):
def test_create_server_socket_sets_up_listener(self, create_socket):
sock = create_socket.return_value
- network.Server.create_server_socket(self.mock,
- sentinel.host, sentinel.port)
+ network.Server.create_server_socket(
+ self.mock, sentinel.host, sentinel.port)
sock.setblocking.assert_called_once_with(False)
sock.bind.assert_called_once_with((sentinel.host, sentinel.port))
sock.listen.assert_called_once_with(any_int)
@@ -62,30 +65,33 @@ class ServerTest(unittest.TestCase):
@patch.object(network, 'create_socket', new=Mock())
def test_create_server_socket_fails(self):
network.create_socket.side_effect = socket.error
- self.assertRaises(socket.error, network.Server.create_server_socket,
- self.mock, sentinel.host, sentinel.port)
+ self.assertRaises(
+ socket.error, network.Server.create_server_socket, self.mock,
+ sentinel.host, sentinel.port)
@patch.object(network, 'create_socket', new=Mock())
def test_create_server_bind_fails(self):
sock = network.create_socket.return_value
sock.bind.side_effect = socket.error
- self.assertRaises(socket.error, network.Server.create_server_socket,
- self.mock, sentinel.host, sentinel.port)
+ self.assertRaises(
+ socket.error, network.Server.create_server_socket, self.mock,
+ sentinel.host, sentinel.port)
@patch.object(network, 'create_socket', new=Mock())
def test_create_server_listen_fails(self):
sock = network.create_socket.return_value
sock.listen.side_effect = socket.error
- self.assertRaises(socket.error, network.Server.create_server_socket,
- self.mock, sentinel.host, sentinel.port)
+ self.assertRaises(
+ socket.error, network.Server.create_server_socket, self.mock,
+ sentinel.host, sentinel.port)
@patch.object(gobject, 'io_add_watch', new=Mock())
def test_register_server_socket_sets_up_io_watch(self):
network.Server.register_server_socket(self.mock, sentinel.fileno)
- gobject.io_add_watch.assert_called_once_with(sentinel.fileno,
- gobject.IO_IN, self.mock.handle_connection)
+ gobject.io_add_watch.assert_called_once_with(
+ sentinel.fileno, gobject.IO_IN, self.mock.handle_connection)
def test_handle_connection(self):
self.mock.accept_connection.return_value = (
@@ -128,7 +134,8 @@ class ServerTest(unittest.TestCase):
for error in (errno.EAGAIN, errno.EINTR):
sock.accept.side_effect = socket.error(error, '')
- self.assertRaises(network.ShouldRetrySocketCall,
+ self.assertRaises(
+ network.ShouldRetrySocketCall,
network.Server.accept_connection, self.mock)
# FIXME decide if this should be allowed to propegate
@@ -136,8 +143,8 @@ class ServerTest(unittest.TestCase):
sock = Mock(spec=socket.SocketType)
self.mock.server_socket = sock
sock.accept.side_effect = socket.error
- self.assertRaises(socket.error,
- network.Server.accept_connection, self.mock)
+ self.assertRaises(
+ socket.error, network.Server.accept_connection, self.mock)
def test_maximum_connections_exceeded(self):
self.mock.max_connections = 10
@@ -149,7 +156,8 @@ class ServerTest(unittest.TestCase):
self.assertTrue(network.Server.maximum_connections_exceeded(self.mock))
self.mock.number_of_connections.return_value = 9
- self.assertFalse(network.Server.maximum_connections_exceeded(self.mock))
+ self.assertFalse(
+ network.Server.maximum_connections_exceeded(self.mock))
@patch('pykka.registry.ActorRegistry.get_by_class')
def test_number_of_connections(self, get_by_class):
@@ -164,23 +172,25 @@ class ServerTest(unittest.TestCase):
@patch.object(network, 'Connection', new=Mock())
def test_init_connection(self):
self.mock.protocol = sentinel.protocol
+ self.mock.protocol_kwargs = {}
self.mock.timeout = sentinel.timeout
network.Server.init_connection(self.mock, sentinel.sock, sentinel.addr)
- network.Connection.assert_called_once_with(sentinel.protocol,
- sentinel.sock, sentinel.addr, sentinel.timeout)
+ network.Connection.assert_called_once_with(
+ sentinel.protocol, {}, sentinel.sock, sentinel.addr,
+ sentinel.timeout)
def test_reject_connection(self):
sock = Mock(spec=socket.SocketType)
- network.Server.reject_connection(self.mock, sock,
- (sentinel.host, sentinel.port))
+ network.Server.reject_connection(
+ self.mock, sock, (sentinel.host, sentinel.port))
sock.close.assert_called_once_with()
def test_reject_connection_error(self):
sock = Mock(spec=socket.SocketType)
sock.close.side_effect = socket.error
- network.Server.reject_connection(self.mock, sock,
- (sentinel.host, sentinel.port))
+ network.Server.reject_connection(
+ self.mock, sock, (sentinel.host, sentinel.port))
sock.close.assert_called_once_with()
diff --git a/tests/utils/network/utils_test.py b/tests/utils/network/utils_test.py
index 1e11673e..ff8af9bd 100644
--- a/tests/utils/network/utils_test.py
+++ b/tests/utils/network/utils_test.py
@@ -1,3 +1,5 @@
+from __future__ import unicode_literals
+
import socket
from mock import patch, Mock
@@ -42,15 +44,15 @@ class CreateSocketTest(unittest.TestCase):
@patch('socket.socket')
def test_ipv4_socket(self, socket_mock):
network.create_socket()
- self.assertEqual(socket_mock.call_args[0],
- (socket.AF_INET, socket.SOCK_STREAM))
+ self.assertEqual(
+ socket_mock.call_args[0], (socket.AF_INET, socket.SOCK_STREAM))
@patch('mopidy.utils.network.has_ipv6', True)
@patch('socket.socket')
def test_ipv6_socket(self, socket_mock):
network.create_socket()
- self.assertEqual(socket_mock.call_args[0],
- (socket.AF_INET6, socket.SOCK_STREAM))
+ self.assertEqual(
+ socket_mock.call_args[0], (socket.AF_INET6, socket.SOCK_STREAM))
@unittest.SkipTest
def test_ipv6_only_is_set(self):
diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py
index d782aa15..512a3ba1 100644
--- a/tests/utils/path_test.py
+++ b/tests/utils/path_test.py
@@ -1,5 +1,7 @@
# encoding: utf-8
+from __future__ import unicode_literals
+
import glib
import os
import shutil
@@ -58,61 +60,61 @@ class GetOrCreateFolderTest(unittest.TestCase):
class PathToFileURITest(unittest.TestCase):
def test_simple_path(self):
if sys.platform == 'win32':
- result = path.path_to_uri(u'C:/WINDOWS/clock.avi')
+ result = path.path_to_uri('C:/WINDOWS/clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
else:
- result = path.path_to_uri(u'/etc/fstab')
+ result = path.path_to_uri('/etc/fstab')
self.assertEqual(result, 'file:///etc/fstab')
def test_folder_and_path(self):
if sys.platform == 'win32':
- result = path.path_to_uri(u'C:/WINDOWS/', u'clock.avi')
+ result = path.path_to_uri('C:/WINDOWS/', 'clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
else:
- result = path.path_to_uri(u'/etc', u'fstab')
- self.assertEqual(result, u'file:///etc/fstab')
+ result = path.path_to_uri('/etc', 'fstab')
+ self.assertEqual(result, 'file:///etc/fstab')
def test_space_in_path(self):
if sys.platform == 'win32':
- result = path.path_to_uri(u'C:/test this')
+ result = path.path_to_uri('C:/test this')
self.assertEqual(result, 'file:///C://test%20this')
else:
- result = path.path_to_uri(u'/tmp/test this')
- self.assertEqual(result, u'file:///tmp/test%20this')
+ result = path.path_to_uri('/tmp/test this')
+ self.assertEqual(result, 'file:///tmp/test%20this')
def test_unicode_in_path(self):
if sys.platform == 'win32':
- result = path.path_to_uri(u'C:/æøå')
+ result = path.path_to_uri('C:/æøå')
self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5')
else:
- result = path.path_to_uri(u'/tmp/æøå')
- self.assertEqual(result, u'file:///tmp/%C3%A6%C3%B8%C3%A5')
+ result = path.path_to_uri('/tmp/æøå')
+ self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5')
class UriToPathTest(unittest.TestCase):
def test_simple_uri(self):
if sys.platform == 'win32':
result = path.uri_to_path('file:///C://WINDOWS/clock.avi')
- self.assertEqual(result, u'C:/WINDOWS/clock.avi')
+ self.assertEqual(result, 'C:/WINDOWS/clock.avi')
else:
result = path.uri_to_path('file:///etc/fstab')
- self.assertEqual(result, u'/etc/fstab')
+ self.assertEqual(result, '/etc/fstab')
def test_space_in_uri(self):
if sys.platform == 'win32':
result = path.uri_to_path('file:///C://test%20this')
- self.assertEqual(result, u'C:/test this')
+ self.assertEqual(result, 'C:/test this')
else:
- result = path.uri_to_path(u'file:///tmp/test%20this')
- self.assertEqual(result, u'/tmp/test this')
+ result = path.uri_to_path('file:///tmp/test%20this')
+ self.assertEqual(result, '/tmp/test this')
def test_unicode_in_uri(self):
if sys.platform == 'win32':
- result = path.uri_to_path( 'file:///C://%C3%A6%C3%B8%C3%A5')
- self.assertEqual(result, u'C:/æøå')
+ result = path.uri_to_path('file:///C://%C3%A6%C3%B8%C3%A5')
+ self.assertEqual(result, 'C:/æøå')
else:
- result = path.uri_to_path(u'file:///tmp/%C3%A6%C3%B8%C3%A5')
- self.assertEqual(result, u'/tmp/æøå')
+ result = path.uri_to_path('file:///tmp/%C3%A6%C3%B8%C3%A5')
+ self.assertEqual(result, '/tmp/æøå')
class SplitPathTest(unittest.TestCase):
@@ -125,11 +127,9 @@ class SplitPathTest(unittest.TestCase):
def test_folders(self):
self.assertEqual(['foo', 'bar', 'baz'], path.split_path('foo/bar/baz'))
- def test_folders(self):
- self.assertEqual(['foo', 'bar', 'baz'], path.split_path('foo/bar/baz'))
-
def test_initial_slash_is_ignored(self):
- self.assertEqual(['foo', 'bar', 'baz'], path.split_path('/foo/bar/baz'))
+ self.assertEqual(
+ ['foo', 'bar', 'baz'], path.split_path('/foo/bar/baz'))
def test_only_slash(self):
self.assertEqual([], path.split_path('/'))
@@ -145,17 +145,20 @@ class ExpandPathTest(unittest.TestCase):
self.assertEqual('/tmp/foo', path.expand_path('/tmp/foo'))
def test_home_dir_expansion(self):
- self.assertEqual(os.path.expanduser('~/foo'), path.expand_path('~/foo'))
+ self.assertEqual(
+ os.path.expanduser('~/foo'), path.expand_path('~/foo'))
def test_abspath(self):
self.assertEqual(os.path.abspath('./foo'), path.expand_path('./foo'))
def test_xdg_subsititution(self):
- self.assertEqual(glib.get_user_data_dir() + '/foo',
+ self.assertEqual(
+ glib.get_user_data_dir() + '/foo',
path.expand_path('$XDG_DATA_DIR/foo'))
def test_xdg_subsititution_unknown(self):
- self.assertEqual('/tmp/$XDG_INVALID_DIR/foo',
+ self.assertEqual(
+ '/tmp/$XDG_INVALID_DIR/foo',
path.expand_path('/tmp/$XDG_INVALID_DIR/foo'))
@@ -177,8 +180,8 @@ class FindFilesTest(unittest.TestCase):
def test_names_are_unicode(self):
is_unicode = lambda f: isinstance(f, unicode)
for name in self.find(''):
- self.assert_(is_unicode(name),
- '%s is not unicode object' % repr(name))
+ self.assert_(
+ is_unicode(name), '%s is not unicode object' % repr(name))
def test_ignores_hidden_folders(self):
self.assertEqual(self.find('.hidden'), [])
diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py
index cf476c24..0ecbb90f 100644
--- a/tests/utils/settings_test.py
+++ b/tests/utils/settings_test.py
@@ -1,6 +1,8 @@
+from __future__ import unicode_literals
+
import os
-import mopidy
+from mopidy import exceptions, settings
from mopidy.utils import settings as setting_utils
from tests import unittest
@@ -9,6 +11,8 @@ from tests import unittest
class ValidateSettingsTest(unittest.TestCase):
def setUp(self):
self.defaults = {
+ 'BACKENDS': ['a'],
+ 'FRONTENDS': ['a'],
'MPD_SERVER_HOSTNAME': '::',
'MPD_SERVER_PORT': 6600,
'SPOTIFY_BITRATE': 160,
@@ -19,59 +23,74 @@ class ValidateSettingsTest(unittest.TestCase):
self.assertEqual(result, {})
def test_unknown_setting_returns_error(self):
- result = setting_utils.validate_settings(self.defaults,
- {'MPD_SERVER_HOSTNMAE': '127.0.0.1'})
- self.assertEqual(result['MPD_SERVER_HOSTNMAE'],
- u'Unknown setting. Did you mean MPD_SERVER_HOSTNAME?')
+ result = setting_utils.validate_settings(
+ self.defaults, {'MPD_SERVER_HOSTNMAE': '127.0.0.1'})
+ self.assertEqual(
+ result['MPD_SERVER_HOSTNMAE'],
+ 'Unknown setting. Did you mean MPD_SERVER_HOSTNAME?')
+
+ def test_custom_settings_does_not_return_errors(self):
+ result = setting_utils.validate_settings(
+ self.defaults, {'CUSTOM_MYAPP_SETTING': 'foobar'})
+ self.assertNotIn('CUSTOM_MYAPP_SETTING', result)
def test_not_renamed_setting_returns_error(self):
- result = setting_utils.validate_settings(self.defaults,
- {'SERVER_HOSTNAME': '127.0.0.1'})
- self.assertEqual(result['SERVER_HOSTNAME'],
- u'Deprecated setting. Use MPD_SERVER_HOSTNAME.')
+ result = setting_utils.validate_settings(
+ self.defaults, {'SERVER_HOSTNAME': '127.0.0.1'})
+ self.assertEqual(
+ result['SERVER_HOSTNAME'],
+ 'Deprecated setting. Use MPD_SERVER_HOSTNAME.')
def test_unneeded_settings_returns_error(self):
- result = setting_utils.validate_settings(self.defaults,
- {'SPOTIFY_LIB_APPKEY': '/tmp/foo'})
- self.assertEqual(result['SPOTIFY_LIB_APPKEY'],
- u'Deprecated setting. It may be removed.')
-
- def test_deprecated_setting_value_returns_error(self):
- result = setting_utils.validate_settings(self.defaults,
- {'BACKENDS': ('mopidy.backends.despotify.DespotifyBackend',)})
- self.assertEqual(result['BACKENDS'],
- u'Deprecated setting value. ' +
- '"mopidy.backends.despotify.DespotifyBackend" is no longer ' +
- 'available.')
+ result = setting_utils.validate_settings(
+ self.defaults, {'SPOTIFY_LIB_APPKEY': '/tmp/foo'})
+ self.assertEqual(
+ result['SPOTIFY_LIB_APPKEY'],
+ 'Deprecated setting. It may be removed.')
def test_unavailable_bitrate_setting_returns_error(self):
- result = setting_utils.validate_settings(self.defaults,
- {'SPOTIFY_BITRATE': 50})
- self.assertEqual(result['SPOTIFY_BITRATE'],
- u'Unavailable Spotify bitrate. ' +
- u'Available bitrates are 96, 160, and 320.')
+ result = setting_utils.validate_settings(
+ self.defaults, {'SPOTIFY_BITRATE': 50})
+ self.assertEqual(
+ result['SPOTIFY_BITRATE'],
+ 'Unavailable Spotify bitrate. '
+ 'Available bitrates are 96, 160, and 320.')
def test_two_errors_are_both_reported(self):
- result = setting_utils.validate_settings(self.defaults,
- {'FOO': '', 'BAR': ''})
+ result = setting_utils.validate_settings(
+ self.defaults, {'FOO': '', 'BAR': ''})
self.assertEqual(len(result), 2)
def test_masks_value_if_secret(self):
secret = setting_utils.mask_value_if_secret('SPOTIFY_PASSWORD', 'bar')
- self.assertEqual(u'********', secret)
+ self.assertEqual('********', secret)
def test_does_not_mask_value_if_not_secret(self):
- not_secret = setting_utils.mask_value_if_secret('SPOTIFY_USERNAME', 'foo')
+ not_secret = setting_utils.mask_value_if_secret(
+ 'SPOTIFY_USERNAME', 'foo')
self.assertEqual('foo', not_secret)
def test_does_not_mask_value_if_none(self):
- not_secret = setting_utils.mask_value_if_secret('SPOTIFY_USERNAME', None)
+ not_secret = setting_utils.mask_value_if_secret(
+ 'SPOTIFY_USERNAME', None)
self.assertEqual(None, not_secret)
+ def test_empty_frontends_list_returns_error(self):
+ result = setting_utils.validate_settings(
+ self.defaults, {'FRONTENDS': []})
+ self.assertEqual(
+ result['FRONTENDS'], 'Must contain at least one value.')
+
+ def test_empty_backends_list_returns_error(self):
+ result = setting_utils.validate_settings(
+ self.defaults, {'BACKENDS': []})
+ self.assertEqual(
+ result['BACKENDS'], 'Must contain at least one value.')
+
class SettingsProxyTest(unittest.TestCase):
def setUp(self):
- self.settings = setting_utils.SettingsProxy(mopidy.settings)
+ self.settings = setting_utils.SettingsProxy(settings)
self.settings.local.clear()
def test_set_and_get_attr(self):
@@ -80,18 +99,18 @@ class SettingsProxyTest(unittest.TestCase):
def test_getattr_raises_error_on_missing_setting(self):
try:
- _ = self.settings.TEST
- self.fail(u'Should raise exception')
- except mopidy.SettingsError as e:
- self.assertEqual(u'Setting "TEST" is not set.', e.message)
+ self.settings.TEST
+ self.fail('Should raise exception')
+ except exceptions.SettingsError as e:
+ self.assertEqual('Setting "TEST" is not set.', e.message)
def test_getattr_raises_error_on_empty_setting(self):
- self.settings.TEST = u''
+ self.settings.TEST = ''
try:
- _ = self.settings.TEST
- self.fail(u'Should raise exception')
- except mopidy.SettingsError as e:
- self.assertEqual(u'Setting "TEST" is empty.', e.message)
+ self.settings.TEST
+ self.fail('Should raise exception')
+ except exceptions.SettingsError as e:
+ self.assertEqual('Setting "TEST" is empty.', e.message)
def test_getattr_does_not_raise_error_if_setting_is_false(self):
self.settings.TEST = False
@@ -176,15 +195,15 @@ class SettingsProxyTest(unittest.TestCase):
class FormatSettingListTest(unittest.TestCase):
def setUp(self):
- self.settings = setting_utils.SettingsProxy(mopidy.settings)
+ self.settings = setting_utils.SettingsProxy(settings)
def test_contains_the_setting_name(self):
- self.settings.TEST = u'test'
+ self.settings.TEST = 'test'
result = setting_utils.format_settings_list(self.settings)
self.assertIn('TEST:', result, result)
def test_repr_of_a_string_value(self):
- self.settings.TEST = u'test'
+ self.settings.TEST = 'test'
result = setting_utils.format_settings_list(self.settings)
self.assertIn("TEST: u'test'", result, result)
@@ -194,28 +213,32 @@ class FormatSettingListTest(unittest.TestCase):
self.assertIn("TEST: 123", result, result)
def test_repr_of_a_tuple_value(self):
- self.settings.TEST = (123, u'abc')
+ self.settings.TEST = (123, 'abc')
result = setting_utils.format_settings_list(self.settings)
self.assertIn("TEST: (123, u'abc')", result, result)
def test_passwords_are_masked(self):
- self.settings.TEST_PASSWORD = u'secret'
+ self.settings.TEST_PASSWORD = 'secret'
result = setting_utils.format_settings_list(self.settings)
self.assertNotIn("TEST_PASSWORD: u'secret'", result, result)
self.assertIn("TEST_PASSWORD: u'********'", result, result)
def test_short_values_are_not_pretty_printed(self):
- self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',)
+ self.settings.FRONTEND = ('mopidy.frontends.mpd.MpdFrontend',)
result = setting_utils.format_settings_list(self.settings)
- self.assertIn("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)", result)
+ self.assertIn(
+ "FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)", result)
def test_long_values_are_pretty_printed(self):
- self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',
+ self.settings.FRONTEND = (
+ u'mopidy.frontends.mpd.MpdFrontend',
u'mopidy.frontends.lastfm.LastfmFrontend')
result = setting_utils.format_settings_list(self.settings)
- self.assert_("""FRONTEND:
- (u'mopidy.frontends.mpd.MpdFrontend',
- u'mopidy.frontends.lastfm.LastfmFrontend')""" in result, result)
+ self.assertIn(
+ "FRONTEND: \n"
+ " (u'mopidy.frontends.mpd.MpdFrontend',\n"
+ " u'mopidy.frontends.lastfm.LastfmFrontend')",
+ result)
class DidYouMeanTest(unittest.TestCase):
diff --git a/tests/version_test.py b/tests/version_test.py
index 0608a143..966b8b94 100644
--- a/tests/version_test.py
+++ b/tests/version_test.py
@@ -1,7 +1,8 @@
-from distutils.version import StrictVersion as SV
-import platform
+from __future__ import unicode_literals
-from mopidy import __version__, get_platform, get_python
+from distutils.version import StrictVersion as SV
+
+from mopidy import __version__
from tests import unittest
@@ -11,32 +12,24 @@ class VersionTest(unittest.TestCase):
SV(__version__)
def test_versions_can_be_strictly_ordered(self):
- self.assert_(SV('0.1.0a0') < SV('0.1.0a1'))
- self.assert_(SV('0.1.0a1') < SV('0.1.0a2'))
- self.assert_(SV('0.1.0a2') < SV('0.1.0a3'))
- 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('0.3.0'))
- self.assert_(SV('0.3.0') < SV('0.3.1'))
- self.assert_(SV('0.3.1') < SV('0.4.0'))
- self.assert_(SV('0.4.0') < SV('0.4.1'))
- self.assert_(SV('0.4.1') < SV('0.5.0'))
- self.assert_(SV('0.5.0') < SV('0.6.0'))
- self.assert_(SV('0.6.0') < SV('0.6.1'))
- self.assert_(SV('0.6.1') < SV('0.7.0'))
- self.assert_(SV('0.7.0') < SV('0.7.1'))
- self.assert_(SV('0.7.1') < SV('0.7.2'))
- self.assert_(SV('0.7.2') < SV('0.7.3'))
- self.assert_(SV('0.7.3') < SV('0.8.0'))
- self.assert_(SV('0.8.0') < SV(__version__))
- self.assert_(SV(__version__) < SV('0.8.2'))
-
- def test_get_platform_contains_platform(self):
- self.assertIn(platform.platform(), get_platform())
-
- def test_get_python_contains_python_implementation(self):
- self.assertIn(platform.python_implementation(), get_python())
-
- def test_get_python_contains_python_version(self):
- self.assertIn(platform.python_version(), get_python())
+ self.assertLess(SV('0.1.0a0'), SV('0.1.0a1'))
+ self.assertLess(SV('0.1.0a1'), SV('0.1.0a2'))
+ self.assertLess(SV('0.1.0a2'), SV('0.1.0a3'))
+ self.assertLess(SV('0.1.0a3'), SV('0.1.0'))
+ self.assertLess(SV('0.1.0'), SV('0.2.0'))
+ self.assertLess(SV('0.1.0'), SV('1.0.0'))
+ self.assertLess(SV('0.2.0'), SV('0.3.0'))
+ self.assertLess(SV('0.3.0'), SV('0.3.1'))
+ self.assertLess(SV('0.3.1'), SV('0.4.0'))
+ self.assertLess(SV('0.4.0'), SV('0.4.1'))
+ self.assertLess(SV('0.4.1'), SV('0.5.0'))
+ self.assertLess(SV('0.5.0'), SV('0.6.0'))
+ self.assertLess(SV('0.6.0'), SV('0.6.1'))
+ self.assertLess(SV('0.6.1'), SV('0.7.0'))
+ self.assertLess(SV('0.7.0'), SV('0.7.1'))
+ self.assertLess(SV('0.7.1'), SV('0.7.2'))
+ self.assertLess(SV('0.7.2'), SV('0.7.3'))
+ self.assertLess(SV('0.7.3'), SV('0.8.0'))
+ self.assertLess(SV('0.8.0'), SV('0.8.1'))
+ self.assertLess(SV('0.8.1'), SV(__version__))
+ self.assertLess(SV(__version__), SV('0.9.1'))
diff --git a/tools/debug-proxy.py b/tools/debug-proxy.py
index 2f54ea36..938afa57 100755
--- a/tools/debug-proxy.py
+++ b/tools/debug-proxy.py
@@ -1,12 +1,14 @@
#! /usr/bin/env python
+from __future__ import unicode_literals
+
import argparse
import difflib
import sys
from gevent import select, server, socket
-COLORS = ['\033[1;%dm' % (30+i) for i in range(8)]
+COLORS = ['\033[1;%dm' % (30 + i) for i in range(8)]
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = COLORS
RESET = "\033[0m"
BOLD = "\033[1m"
@@ -53,7 +55,8 @@ def loop(client, address, reference, actual):
# Consume banners from backends
responses = dict()
- disconnected = read([reference, actual], responses, find_response_end_token)
+ disconnected = read(
+ [reference, actual], responses, find_response_end_token)
diff(address, '', responses[reference], responses[actual])
# We lost a backend, might as well give up.
@@ -78,13 +81,15 @@ def loop(client, address, reference, actual):
actual.sendall(responses[client])
# Get the entire resonse from both backends.
- disconnected = read([reference, actual], responses, find_response_end_token)
+ disconnected = read(
+ [reference, actual], responses, find_response_end_token)
# Send the client the complete reference response
client.sendall(responses[reference])
# Compare our responses
- diff(address, responses[client], responses[reference], responses[actual])
+ diff(address,
+ responses[client], responses[reference], responses[actual])
# Give up if we lost a backend.
if disconnected:
diff --git a/tools/idle.py b/tools/idle.py
index aa56dce2..122e998d 100644
--- a/tools/idle.py
+++ b/tools/idle.py
@@ -3,6 +3,8 @@
# This script is helper to systematicly test the behaviour of MPD's idle
# command. It is simply provided as a quick hack, expect nothing more.
+from __future__ import unicode_literals
+
import logging
import pprint
import socket
@@ -17,98 +19,98 @@ data = {'id': None, 'id2': None, 'url': url, 'artist': artist}
# Commands to run before test requests to coerce MPD into right state
setup_requests = [
- 'clear',
- 'add "%(url)s"',
- 'add "%(url)s"',
- 'add "%(url)s"',
- 'play',
-# 'pause', # Uncomment to test paused idle behaviour
-# 'stop', # Uncomment to test stopped idle behaviour
+ 'clear',
+ 'add "%(url)s"',
+ 'add "%(url)s"',
+ 'add "%(url)s"',
+ 'play',
+ #'pause', # Uncomment to test paused idle behaviour
+ #'stop', # Uncomment to test stopped idle behaviour
]
# List of commands to test for idle behaviour. Ordering of list is important in
# order to keep MPD state as intended. Commands that are obviously
# informational only or "harmfull" have been excluded.
test_requests = [
- 'add "%(url)s"',
- 'addid "%(url)s" "1"',
- 'clear',
-# 'clearerror',
-# 'close',
-# 'commands',
- 'consume "1"',
- 'consume "0"',
-# 'count',
- 'crossfade "1"',
- 'crossfade "0"',
-# 'currentsong',
-# 'delete "1:2"',
- 'delete "0"',
- 'deleteid "%(id)s"',
- 'disableoutput "0"',
- 'enableoutput "0"',
-# 'find',
-# 'findadd "artist" "%(artist)s"',
-# 'idle',
-# 'kill',
-# 'list',
-# 'listall',
-# 'listallinfo',
-# 'listplaylist',
-# 'listplaylistinfo',
-# 'listplaylists',
-# 'lsinfo',
- 'move "0:1" "2"',
- 'move "0" "1"',
- 'moveid "%(id)s" "1"',
- 'next',
-# 'notcommands',
-# 'outputs',
-# 'password',
- 'pause',
-# 'ping',
- 'play',
- 'playid "%(id)s"',
-# 'playlist',
- 'playlistadd "foo" "%(url)s"',
- 'playlistclear "foo"',
- 'playlistadd "foo" "%(url)s"',
- 'playlistdelete "foo" "0"',
-# 'playlistfind',
-# 'playlistid',
-# 'playlistinfo',
- 'playlistadd "foo" "%(url)s"',
- 'playlistadd "foo" "%(url)s"',
- 'playlistmove "foo" "0" "1"',
-# 'playlistsearch',
-# 'plchanges',
-# 'plchangesposid',
- 'previous',
- 'random "1"',
- 'random "0"',
- 'rm "bar"',
- 'rename "foo" "bar"',
- 'repeat "0"',
- 'rm "bar"',
- 'save "bar"',
- 'load "bar"',
-# 'search',
- 'seek "1" "10"',
- 'seekid "%(id)s" "10"',
-# 'setvol "10"',
- 'shuffle',
- 'shuffle "0:1"',
- 'single "1"',
- 'single "0"',
-# 'stats',
-# 'status',
- 'stop',
- 'swap "1" "2"',
- 'swapid "%(id)s" "%(id2)s"',
-# 'tagtypes',
-# 'update',
-# 'urlhandlers',
-# 'volume',
+ 'add "%(url)s"',
+ 'addid "%(url)s" "1"',
+ 'clear',
+ #'clearerror',
+ #'close',
+ #'commands',
+ 'consume "1"',
+ 'consume "0"',
+ # 'count',
+ 'crossfade "1"',
+ 'crossfade "0"',
+ #'currentsong',
+ #'delete "1:2"',
+ 'delete "0"',
+ 'deleteid "%(id)s"',
+ 'disableoutput "0"',
+ 'enableoutput "0"',
+ #'find',
+ #'findadd "artist" "%(artist)s"',
+ #'idle',
+ #'kill',
+ #'list',
+ #'listall',
+ #'listallinfo',
+ #'listplaylist',
+ #'listplaylistinfo',
+ #'listplaylists',
+ #'lsinfo',
+ 'move "0:1" "2"',
+ 'move "0" "1"',
+ 'moveid "%(id)s" "1"',
+ 'next',
+ #'notcommands',
+ #'outputs',
+ #'password',
+ 'pause',
+ #'ping',
+ 'play',
+ 'playid "%(id)s"',
+ #'playlist',
+ 'playlistadd "foo" "%(url)s"',
+ 'playlistclear "foo"',
+ 'playlistadd "foo" "%(url)s"',
+ 'playlistdelete "foo" "0"',
+ #'playlistfind',
+ #'playlistid',
+ #'playlistinfo',
+ 'playlistadd "foo" "%(url)s"',
+ 'playlistadd "foo" "%(url)s"',
+ 'playlistmove "foo" "0" "1"',
+ #'playlistsearch',
+ #'plchanges',
+ #'plchangesposid',
+ 'previous',
+ 'random "1"',
+ 'random "0"',
+ 'rm "bar"',
+ 'rename "foo" "bar"',
+ 'repeat "0"',
+ 'rm "bar"',
+ 'save "bar"',
+ 'load "bar"',
+ #'search',
+ 'seek "1" "10"',
+ 'seekid "%(id)s" "10"',
+ #'setvol "10"',
+ 'shuffle',
+ 'shuffle "0:1"',
+ 'single "1"',
+ 'single "0"',
+ #'stats',
+ #'status',
+ 'stop',
+ 'swap "1" "2"',
+ 'swapid "%(id)s" "%(id2)s"',
+ #'tagtypes',
+ #'update',
+ #'urlhandlers',
+ #'volume',
]
@@ -116,8 +118,8 @@ def create_socketfile():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.settimeout(0.5)
- fd = sock.makefile('rw', 1) # 1 = line buffered
- fd.readline() # Read banner
+ fd = sock.makefile('rw', 1) # 1 = line buffered
+ fd.readline() # Read banner
return fd