Merge branch 'develop' into feature/dump-thread-tracebacks

This commit is contained in:
Thomas Adamcik 2012-09-16 16:14:34 +02:00
commit e17e2ea96d
66 changed files with 1549 additions and 1436 deletions

4
.mailmap Normal file
View File

@ -0,0 +1,4 @@
Kristian Klette <klette@samfundet.no>
Johannes Knutsen <johannes@knutseninfo.no> <johannes@iterate.no>
Johannes Knutsen <johannes@knutseninfo.no> <johannes@barbarmaclin.(none)>
John Bäckstrand <sopues@gmail.com> <sandos@XBMCLive.(none)>

19
docs/api/audio.rst Normal file
View File

@ -0,0 +1,19 @@
.. _audio-api:
*********
Audio API
*********
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:
.. autoclass:: mopidy.audio.Audio
:members:

View File

@ -1,12 +1,12 @@
.. _backend-provider-api: .. _backend-api:
******************** ***********
Backend provider API Backend API
******************** ***********
The backend provider API is the interface that must be implemented when you The backend API is the interface that must be implemented when you create a
create a backend. If you are working on a frontend and need to access the backend. If you are working on a frontend and need to access the backend, see
backend, see the :ref:`backend-controller-api`. the :ref:`core-api`.
Playback provider Playback provider
@ -30,8 +30,8 @@ Library provider
:members: :members:
Backend provider implementations Backend implementations
================================ =======================
* :mod:`mopidy.backends.dummy` * :mod:`mopidy.backends.dummy`
* :mod:`mopidy.backends.spotify` * :mod:`mopidy.backends.spotify`

View File

@ -1,54 +0,0 @@
.. _backend-controller-api:
**********************
Backend controller API
**********************
The backend controller API is the interface that is used by frontends like
:mod:`mopidy.frontends.mpd`. If you want to implement your own backend, see the
:ref:`backend-provider-api`.
The backend
===========
.. autoclass:: mopidy.backends.base.Backend
:members:
Playback controller
===================
Manages playback, with actions like play, pause, stop, next, previous,
seek, and volume control.
.. autoclass:: mopidy.backends.base.PlaybackController
:members:
Current playlist controller
===========================
Manages everything related to the currently loaded playlist.
.. autoclass:: mopidy.backends.base.CurrentPlaylistController
:members:
Stored playlists controller
===========================
Manages stored playlist.
.. autoclass:: mopidy.backends.base.StoredPlaylistsController
:members:
Library controller
==================
Manages the music library, e.g. searching for tracks to be added to a playlist.
.. autoclass:: mopidy.backends.base.LibraryController
:members:

View File

@ -1,4 +1,4 @@
.. _backend-concepts: .. _concepts:
********************************************** **********************************************
The backend, controller, and provider concepts The backend, controller, and provider concepts
@ -12,11 +12,11 @@ Controllers:
functionality. Most, but not all, controllers delegates some work to one or functionality. Most, but not all, controllers delegates some work to one or
more providers. The controllers are responsible for choosing the right more providers. The controllers are responsible for choosing the right
provider for any given task based upon i.e. the track's URI. See provider for any given task based upon i.e. the track's URI. See
:ref:`backend-controller-api` for more details. :ref:`core-api` for more details.
Providers: Providers:
Anything specific to i.e. Spotify integration or local storage is contained 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 in the providers. To integrate with new music sources, you just add new
providers. See :ref:`backend-provider-api` for more details. providers. See :ref:`backend-api` for more details.
.. digraph:: backend_relations .. digraph:: backend_relations

50
docs/api/core.rst Normal file
View File

@ -0,0 +1,50 @@
.. _core-api:
********
Core API
********
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
backends.
Playback controller
===================
Manages playback, with actions like play, pause, stop, next, previous,
seek, and volume control.
.. autoclass:: mopidy.core.PlaybackState
:members:
.. autoclass:: mopidy.core.PlaybackController
:members:
Current playlist controller
===========================
Manages everything related to the currently loaded playlist.
.. autoclass:: mopidy.core.CurrentPlaylistController
:members:
Stored playlists controller
===========================
Manages stored playlist.
.. autoclass:: mopidy.core.StoredPlaylistsController
:members:
Library controller
==================
Manages the music library, e.g. searching for tracks to be added to a playlist.
.. autoclass:: mopidy.core.LibraryController
:members:

View File

@ -5,7 +5,10 @@ API reference
.. toctree:: .. toctree::
:glob: :glob:
backends/concepts concepts
backends/controllers models
backends/providers backends
* core
audio
frontends
listeners

View File

@ -9,6 +9,9 @@ Contributors to Mopidy in the order of appearance:
- Thomas Adamcik <adamcik@samfundet.no> - Thomas Adamcik <adamcik@samfundet.no>
- Kristian Klette <klette@klette.us> - Kristian Klette <klette@klette.us>
A complete list of persons with commits accepted into the Mopidy repo can be
found at `GitHub <https://github.com/mopidy/mopidy/graphs/contributors>`_.
Showing your appreciation Showing your appreciation
========================= =========================
@ -17,13 +20,3 @@ If you already enjoy Mopidy, or don't enjoy it and want to help us making
Mopidy better, the best way to do so is to contribute back to the community. Mopidy better, the best way to do so is to contribute back to the community.
You can contribute code, documentation, tests, bug reports, or help other You can contribute code, documentation, tests, bug reports, or help other
users, spreading the word, etc. users, spreading the word, etc.
If you want to show your appreciation in a less time consuming way, you can
`flattr us <https://flattr.com/thing/82288/Mopidy>`_, or `donate money
<http://pledgie.com/campaigns/12647>`_ to Mopidy's development.
We promise that any money donated -- to Pledgie, not Flattr, due to the size of
the amounts -- will be used to cover costs related to Mopidy development, like
service subscriptions (Spotify, Last.fm, etc.) and hardware devices like an
used iPod Touch for testing Mopidy with MPod.

View File

@ -7,24 +7,7 @@ This change log is used to track all major changes to Mopidy.
v0.8 (in development) v0.8 (in development)
===================== =====================
**Changes** **Audio output and mixer changes**
- Added tools/debug-proxy.py to tee client requests to two backends and diff
responses. Intended as a developer tool for checking for MPD protocol changes
and various client support. Requires gevent, which currently is not a
dependency of Mopidy.
- Fixed bug when the MPD command `playlistinfo` is used with a track position.
Track position and CPID was intermixed, so it would cause a crash if a CPID
matching the track position didn't exist. (Fixes: :issue:`162`)
- Added :option:`--list-deps` option to the `mopidy` command that lists
required and optional dependencies, their current versions, and some other
information useful for debugging. (Fixes: :issue:`74`)
- When unknown settings are encountered, we now check if it's similar to a
known setting, and suggests to the user what we think the setting should have
been.
- Removed multiple outputs support. Having this feature currently seems to be - Removed multiple outputs support. Having this feature currently seems to be
more trouble than what it is worth. The :attr:`mopidy.settings.OUTPUTS` more trouble than what it is worth. The :attr:`mopidy.settings.OUTPUTS`
@ -35,10 +18,10 @@ v0.8 (in development)
:issue:`159`) :issue:`159`)
- Switch to pure GStreamer based mixing. This implies that users setup a - Switch to pure GStreamer based mixing. This implies that users setup a
GStreamer bin with a mixer in it in :attr:`mopidy.setting.MIXER`. The default GStreamer bin with a mixer in it in :attr:`mopidy.settings.MIXER`. The
value is ``autoaudiomixer``, a custom mixer that attempts to find a mixer that default value is ``autoaudiomixer``, a custom mixer that attempts to find a
will work on your system. If this picks the wrong mixer you can of course mixer that will work on your system. If this picks the wrong mixer you can of
override it. Setting the mixer to :class:`None` is also supported. MPD course override it. Setting the mixer to :class:`None` is also supported. MPD
protocol support for volume has also been updated to return -1 when we have protocol support for volume has also been updated to return -1 when we have
no mixer set. no mixer set.
@ -46,7 +29,7 @@ v0.8 (in development)
- Updated the NAD hardware mixer to work in the new GStreamer based mixing - Updated the NAD hardware mixer to work in the new GStreamer based mixing
regime. Settings are now passed as GStreamer element properties. In practice regime. Settings are now passed as GStreamer element properties. In practice
that means that the following old-style config: that means that the following old-style config::
MIXER = u'mopidy.mixers.nad.NadMixer' MIXER = u'mopidy.mixers.nad.NadMixer'
MIXER_EXT_PORT = u'/dev/ttyUSB0' MIXER_EXT_PORT = u'/dev/ttyUSB0'
@ -54,7 +37,7 @@ v0.8 (in development)
MIXER_EXT_SPEAKERS_A = u'On' MIXER_EXT_SPEAKERS_A = u'On'
MIXER_EXT_SPEAKERS_B = u'Off' MIXER_EXT_SPEAKERS_B = u'Off'
Now is reduced to simply: Now is reduced to simply::
MIXER = u'nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off' MIXER = u'nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off'
@ -62,12 +45,41 @@ v0.8 (in development)
properties may be left out if you don't want the mixer to adjust the settings properties may be left out if you don't want the mixer to adjust the settings
on your NAD amplifier when Mopidy is started. on your NAD amplifier when Mopidy is started.
- Fixed :issue:`150` which caused some clients to block Mopidy completely. Bug **Changes**
was caused by some clients sending ``close`` and then shutting down the
connection right away. This trigged a situation in which the connection - When unknown settings are encountered, we now check if it's similar to a
known setting, and suggests to the user what we think the setting should have
been.
- Added :option:`--list-deps` option to the ``mopidy`` command that lists
required and optional dependencies, their current versions, and some other
information useful for debugging. (Fixes: :issue:`74`)
- Added ``tools/debug-proxy.py`` to tee client requests to two backends and
diff responses. Intended as a developer tool for checking for MPD protocol
changes and various client support. Requires gevent, which currently is not a
dependency of Mopidy.
- Support tracks with only release year, and not a full release date, like e.g.
Spotify tracks.
**Bug fixes**
- :issue:`72`: Created a Spotify track proxy that will switch to using loaded
data as soon as it becomes available.
- :issue:`162`: Fixed bug when the MPD command ``playlistinfo`` is used with a
track position. Track position and CPID was intermixed, so it would cause a
crash if a CPID matching the track position didn't exist.
- :issue:`150`: Fix bug which caused some clients to block Mopidy completely.
The bug was caused by some clients sending ``close`` and then shutting down
the connection right away. This trigged a situation in which the connection
cleanup code would wait for an response that would never come inside the cleanup code would wait for an response that would never come inside the
event loop, blocking everything else. event loop, blocking everything else.
- Fixed crash on lookup of unknown path when using local backend.
v0.7.3 (2012-08-11) v0.7.3 (2012-08-11)
=================== ===================
@ -608,9 +620,9 @@ to this problem.
:class:`mopidy.models.Album`, and :class:`mopidy.models.Track`. :class:`mopidy.models.Album`, and :class:`mopidy.models.Track`.
- Prepare for multi-backend support (see :issue:`40`) by introducing the - Prepare for multi-backend support (see :issue:`40`) by introducing the
:ref:`provider concept <backend-concepts>`. Split the backend API into a :ref:`provider concept <concepts>`. Split the backend API into a
:ref:`backend controller API <backend-controller-api>` (for frontend use) :ref:`backend controller API <core-api>` (for frontend use)
and a :ref:`backend provider API <backend-provider-api>` (for backend and a :ref:`backend provider API <backend-api>` (for backend
implementation use), which includes the following changes: implementation use), which includes the following changes:
- Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`. - Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`.
@ -852,8 +864,8 @@ In the last two months, Mopidy's MPD frontend has gotten lots of stability
fixes and error handling improvements, proper support for having the same track fixes and error handling improvements, proper support for having the same track
multiple times in a playlist, and support for IPv6. We have also fixed the multiple times in a playlist, and support for IPv6. We have also fixed the
choppy playback on the libspotify backend. For the road ahead of us, we got an choppy playback on the libspotify backend. For the road ahead of us, we got an
updated :doc:`release roadmap <development/roadmap>` with our goals for the 0.1 updated :doc:`release roadmap <development>` with our goals for the 0.1 to 0.3
to 0.3 releases. releases.
Enjoy the best alpha relase of Mopidy ever :-) Enjoy the best alpha relase of Mopidy ever :-)
@ -946,7 +958,7 @@ Since the previous release Mopidy has seen about 300 commits, more than 200 new
tests, a libspotify release, and major feature additions to Spotify. The new tests, a libspotify release, and major feature additions to Spotify. The new
releases from Spotify have lead to updates to our dependencies, and also to new releases from Spotify have lead to updates to our dependencies, and also to new
bugs in Mopidy. Thus, this is primarily a bugfix release, even though the not bugs in Mopidy. Thus, this is primarily a bugfix release, even though the not
yet finished work on a Gstreamer backend have been merged. yet finished work on a GStreamer backend have been merged.
All users are recommended to upgrade to 0.1.0a1, and should at the same time All users are recommended to upgrade to 0.1.0a1, and should at the same time
ensure that they have the latest versions of our dependencies: Despotify r508 ensure that they have the latest versions of our dependencies: Despotify r508
@ -971,7 +983,7 @@ As always, report problems at our IRC channel or our issue tracker. Thanks!
- Several new generic features, like shuffle, consume, and playlist repeat. - Several new generic features, like shuffle, consume, and playlist repeat.
(Fixes: :issue:`3`) (Fixes: :issue:`3`)
- **[Work in Progress]** A new backend for playing music from a local music - **[Work in Progress]** A new backend for playing music from a local music
archive using the Gstreamer library. archive using the GStreamer library.
- Made :class:`mopidy.mixers.alsa.AlsaMixer` work on machines without a mixer - Made :class:`mopidy.mixers.alsa.AlsaMixer` work on machines without a mixer
named "Master". named "Master".

View File

@ -30,33 +30,13 @@ ncmpcpp
A console client that generally works well with Mopidy, and is regularly used A console client that generally works well with Mopidy, and is regularly used
by Mopidy developers. by Mopidy developers.
Search Search only works in two of the three search modes:
^^^^^^
Search only works for ncmpcpp versions 0.5.1 and higher, and in two of the
three search modes:
- "Match if tag contains search phrase (regexes supported)" -- Does not work. - "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. The client tries to fetch all known metadata and do the search client side.
- "Match if tag contains searched phrase (no regexes)" -- Works. - "Match if tag contains searched phrase (no regexes)" -- Works.
- "Match only if both values are the same" -- Works. - "Match only if both values are the same" -- Works.
If you run Ubuntu 10.04 or older, you can fetch an updated version of ncmpcpp
from `Launchpad <https://launchpad.net/ubuntu/+source/ncmpcpp>`_.
Communication mode
^^^^^^^^^^^^^^^^^^
In newer versions of ncmpcpp, like ncmpcpp 0.5.5 shipped with Ubuntu 11.04,
ncmcpp defaults to "notifications" mode for MPD communications, which Mopidy
did not support before Mopidy 0.6. To workaround this limitation in earlier
versions of Mopidy, edit the ncmpcpp configuration file at
``~/.ncmpcpp/config`` and add the following setting::
mpd_communication_mode = "polling"
If you use Mopidy 0.6 or newer, you don't need to change anything.
Graphical clients Graphical clients
================= =================
@ -102,8 +82,8 @@ It generally works well with Mopidy.
Android clients Android clients
=============== ===============
We've tested all six MPD clients we could find for Android with Mopidy 0.3 on a We've tested all four MPD clients we could find for Android with Mopidy 0.7.3 on
HTC Hero with Android 2.1, using the following test procedure: a Samsung Galaxy Nexus with Android 4.1.1, using the following test procedure:
#. Connect to Mopidy #. 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
@ -127,152 +107,180 @@ HTC Hero with Android 2.1, using the following test procedure:
#. Check if the app got support for single mode and consume mode #. Check if the app got support for single mode and consume mode
#. Kill Mopidy and confirm that the app handles it without crashing #. Kill Mopidy and confirm that the app handles it without crashing
In summary: We found that all four apps crashed on Android 4.1.1.
- BitMPC lacks finishing touches on its user interface but supports all Combining what we managed to find before the apps crashed with our experience
features tested. from an older version of this review, using Android 2.1, we can say that:
- Droid MPD Client works well, but got a couple of bugs one can live with and
does not expose stored playlist anywhere.
- IcyBeats is not usable yet.
- MPDroid is working well and looking good, but does not have search
functionality.
- PMix is just a lesser MPDroid, so use MPDroid instead.
- ThreeMPD is too buggy to even get connected to Mopidy.
Our recommendation: - PMix can be ignored, because it is unmaintained and its fork MPDroid is
better on all fronts.
- If you do not care about looks, use BitMPC. - Droid MPD Client was to buggy to get an impression from. Unclear if the bugs
- If you do not care about stored playlists, use Droid MPD Client. are due to the app or that it hasn't been updated for Android 4.x.
- If you do not care about searching, use MPDroid.
- BitMPC is in our experience feature complete, but ugly.
- MPDroid, now that search is in place, is probably feature complete as well,
and looks nicer than BitMPC.
In conclusion: MPD clients on Android 4.x is a sad affair. If you want to try
anyway, try BitMPC and MPDroid.
BitMPC BitMPC
------ ------
We tested version 1.0.0, which at the time had 1k-5k downloads, <100 ratings, Test date:
3.5 stars. 2012-09-12
Tested version:
1.0.0 (released 2010-04-12)
Downloads:
5,000+
Rating:
3.7 stars from about 100 ratings
The user interface lacks some finishing touches. E.g. you can't enter a
hostname for the server. Only IPv4 addresses are allowed.
All features exercised in the test procedure works. BitMPC lacks support for - The user interface lacks some finishing touches. E.g. you can't enter a
single mode and consume mode. BitMPC crashes if Mopidy is killed or crash. hostname for the server. Only IPv4 addresses are allowed.
- When we last tested the same version of BitMPC using Android 2.1:
- All features exercised in the test procedure worked.
- BitMPC lacked support for single mode and consume mode.
- 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.
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
it hasn't been maintained for 2.5 years.
Droid MPD Client Droid MPD Client
---------------- ----------------
We tested version 0.4.0, which at the time had 5k-10k downloads, >200 ratings, Test date:
4 stars. 2012-09-12
Tested version:
1.4.0 (released 2011-12-20)
Downloads:
10,000+
Rating:
4.2 stars from 400+ ratings
To find the search functionality, you have to select the menu, then "Playlist - No intutive way to ask the app to connect to the server after adding the
manager", then the search tab. I do not understand why search is hidden inside server hostname to the settings.
"Playlist manager".
The user interface have some French remnants, like "Rechercher" in the search - To find the search functionality, you have to select the menu,
field. then "Playlist manager", then the search tab. I do not understand why search
is hidden inside "Playlist manager".
When selecting the artist tab, it issues the ``list Artist`` command and - The tabs "Artists" and "Albums" did not contain anything, and did not cause
becomes stuck waiting for the results. Same thing happens for the album tab, any requests.
which issues ``list Album``, and the folder tab, which issues ``lsinfo``.
Mopidy returned zero hits immediately on all three commands. If Mopidy has
loaded your stored playlists and returns more than zero hits on these commands,
they artist and album tabs do not hang. The folder tab still freezes when
``lsinfo`` returns a list of stored playlists, though zero files. Thus, we've
discovered a couple of bugs in Droid MPD Client.
Even though ``lsinfo`` returns the stored playlists for the folder tab, they - The tab "Folders" showed a spinner and said "Updating data..." but did not
are not displayed anywhere. Thus, we had to select an album in the album tab to send any requests.
complete the test procedure.
At one point, I had problems turning off repeat mode. After I adjusted the - Searching for "foo" did nothing. No request was sent to the server.
volume and tried again, it worked.
Droid MPD client does not support single mode or consume mode. It does not - Once, I managed to get a list of stored playlists in the "Search" tab, but I
detect that the server is killed/crashed. You'll only notice it by no actions never managed to reproduce this. Opening the stored playlists doesn't work,
having any effect, e.g. you can't turn the volume knob any more. because Mopidy haven't implemented ``lsinfo "Playlist name"`` (see
:issue:`193`).
In conclusion, some bugs and caveats, but most of the test procedure was - Droid MPD client does not support single mode or consume mode.
possible to perform.
- Not able to complete the test procedure, due to the above problems.
IcyBeats In conclusion, not a client we can recommend.
--------
We tested version 0.2, which at the time had 50-100 downloads, no ratings.
The app was still in beta when we tried it.
IcyBeats successfully connected to Mopidy and I was able to adjust volume. When
I was searching for some tracks, I could not figure out how to actually start
the search, as there was no search button and pressing enter in the input field
just added a new line. I was stuck. In other words, IcyBeats 0.2 is not usable
with Mopidy.
IcyBeats does have something going for it: IcyBeats uses IPv6 to connect to
Mopidy. The future is just around the corner!
MPDroid MPDroid
------- -------
We tested version 0.6.9, which at the time had 5k-10k downloads, <200 ratings, Test date:
4.5 stars. MPDroid started out as a fork of PMix. 2012-09-12
Tested version:
0.7 (released 2011-06-19)
Downloads:
10,000+
Rating:
4.5 stars from ~500 ratings
First of all, MPDroid's user interface looks nice. - MPDroid started out as a fork of PMix.
I couldn't find any search functionality, so I added the initial track using - First of all, MPDroid's user interface looks nice.
another client. Other than the missing search functionality, everything in the
test procedure worked out flawlessly. Like all other Android clients, MPDroid
does not support single mode or consume mode. When Mopidy is killed, MPDroid
handles it gracefully and asks if you want to try to reconnect.
All in all, MPDroid is a good MPD client without search support. - 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 PMix
---- ----
We tested version 0.4.0, which at the time had 10k-50k downloads, >200 ratings, Test date:
4 stars. 2012-09-12
Tested version:
0.4.0 (released 2010-03-06)
Downloads:
10,000+
Rating:
3.8 stars from >200 ratings
Add MPDroid is a fork from PMix, it is no surprise that PMix does not support - Using Android 4.1.1, PMix, which haven't been updated for 2.5 years, crashes
search either. In addition, I could not find stored playlists. Other than that, as soon as it connects to Mopidy.
I was able to complete the test procedure. PMix crashed once during testing,
but handled the killing of Mopidy just as nicely as MPDroid. It does not - Last time we tested the same version of PMix using Android 2.1, we found
support single mode or consume mode. that:
- PMix does not support search.
- I could not find stored playlists.
- Other than that, I was able to complete the test procedure.
- PMix crashed once during testing.
- 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. All in all, PMix works but can do less than MPDroid. Use MPDroid instead.
ThreeMPD
--------
We tested version 0.3.0, which at the time had 1k-5k downloads, <25 ratings,
2.5 average. The developer request users to use MPDroid instead, due to limited
time for maintenance. Does not support password authentication.
ThreeMPD froze during startup, so we were not able to test it.
.. _ios_mpd_clients: .. _ios_mpd_clients:
iPhone/iPod Touch clients iOS clients
========================= ===========
impdclient
----------
There's an open source MPD client for iOS called `impdclient
<http://code.google.com/p/impdclient/>`_ which has not seen any updates since
August 2008. So far, we've not heard of users trying it with Mopidy. Please
notify us of your successes and/or problems if you do try it out.
MPod MPod
---- ----
The `MPoD <http://www.katoemba.net/makesnosenseatall/mpod/>`_ client can be Test date:
installed from the `iTunes Store 2011-01-19
Tested version:
1.5.1
The `MPoD <http://www.katoemba.net/makesnosenseatall/mpod/>`_ iPhone/iPod Touch
app can be installed from the `iTunes Store
<http://itunes.apple.com/us/app/mpod/id285063020>`_. <http://itunes.apple.com/us/app/mpod/id285063020>`_.
Users have reported varying success in using MPoD together with Mopidy. Thus, Users have reported varying success in using MPoD together with Mopidy. Thus,
@ -316,3 +324,10 @@ we've tested a fresh install of MPoD 1.5.1 with Mopidy as of revision e7ed28d
- **Wishlist:** MPoD supports autodetection/-configuration of MPD servers - **Wishlist:** MPoD supports autodetection/-configuration of MPD servers
through the use of Bonjour. Mopidy does not currently support this, but there through the use of Bonjour. Mopidy does not currently support this, but there
is a wishlist bug at :issue:`39`. is a wishlist bug at :issue:`39`.
MPaD
----
The `MPaD <http://www.katoemba.net/makesnosenseatall/mpad/>`_ iPad app works
with Mopidy. A complete review may appear here in the future.

View File

@ -29,6 +29,8 @@ class Mock(object):
def __getattr__(self, name): def __getattr__(self, name):
if name in ('__file__', '__path__'): if name in ('__file__', '__path__'):
return '/dev/null' return '/dev/null'
elif name[0] == name[0].upper() and not name.startswith('MIXER_TRACK'):
return type(name, (), {})
else: else:
return Mock() return Mock()
@ -51,11 +53,6 @@ MOCK_MODULES = [
for mod_name in MOCK_MODULES: for mod_name in MOCK_MODULES:
sys.modules[mod_name] = Mock() sys.modules[mod_name] = Mock()
def get_version():
init_py = open('../mopidy/__init__.py').read()
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py))
return metadata['version']
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
@ -94,6 +91,7 @@ copyright = u'2010-2012, Stein Magnus Jodal and contributors'
# built documents. # built documents.
# #
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
from mopidy import get_version
release = get_version() release = get_version()
# The short X.Y version. # The short X.Y version.
version = '.'.join(release.split('.')[:2]) version = '.'.join(release.split('.')[:2])

View File

@ -1,11 +1,42 @@
***************** ***********
How to contribute Development
***************** ***********
Development of Mopidy is coordinated through the IRC channel ``#mopidy`` at Development of Mopidy is coordinated through the IRC channel ``#mopidy`` at
``irc.freenode.net`` and through `GitHub <http://github.com/>`_. ``irc.freenode.net`` and through `GitHub <http://github.com/>`_.
Release schedule
================
We intend to have about one timeboxed feature release every month
in periods of active development. The feature releases are numbered 0.x.0. The
features added is a mix of what we feel is most important/requested of the
missing features, and features we develop just because we find them fun to
make, even though they may be useful for very few users or for a limited use
case.
Bugfix releases, numbered 0.x.y, will be released whenever we discover bugs
that are too serious to wait for the next feature release. We will only release
bugfix releases for the last feature release. E.g. when 0.3.0 is released, we
will no longer provide bugfix releases for the 0.2 series. In other words,
there will be just a single supported release at any point in time.
Feature wishlist
================
We maintain our collection of sane or less sane ideas for future Mopidy
features as `issues <https://github.com/mopidy/mopidy/issues>`_ at GitHub
labeled with `the "wishlist" label
<https://github.com/mopidy/mopidy/issues?labels=wishlist>`_. Feel free to vote
up any feature you would love to see in Mopidy, but please refrain from adding
a comment just to say "I want this too!". You are of course free to add
comments if you have suggestions for how the feature should work or be
implemented, and you may add new wishlist issues if your ideas are not already
represented.
Code style Code style
========== ==========
@ -126,6 +157,49 @@ code. So, if you're out of work, the code coverage and pylint data at the CI
server should give you a place to start. server should give you a place to start.
Protocol debugging
==================
Since the main interface provided to Mopidy is through the MPD protocol, it is
crucial that we try and stay in sync with protocol developments. In an attempt
to make it easier to debug differences Mopidy and MPD protocol handling we have
created ``tools/debug-proxy.py``.
This tool is proxy that sits in front of two MPD protocol aware servers and
sends all requests to both, returning the primary response to the client and
then printing any diff in the two responses.
Note that this tool depends on ``gevent`` unlike the rest of Mopidy at the time
of writing. See ``--help`` for available options. Sample session::
[127.0.0.1]:59714
listallinfo
--- Reference response
+++ Actual response
@@ -1,16 +1,1 @@
-file: uri1
-Time: 4
-Artist: artist1
-Title: track1
-Album: album1
-file: uri2
-Time: 4
-Artist: artist2
-Title: track2
-Album: album2
-file: uri3
-Time: 4
-Artist: artist3
-Title: track3
-Album: album3
-OK
+ACK [2@0] {listallinfo} incorrect arguments
To ensure that Mopidy and MPD have comparable state it is suggested you setup
both to use ``tests/data/library_tag_cache`` for their tag cache and
``tests/data`` for music/playlist folders.
Writing documentation Writing documentation
===================== =====================

View File

@ -1,9 +0,0 @@
***********
Development
***********
.. toctree::
:maxdepth: 3
roadmap
contributing

View File

@ -1,34 +0,0 @@
*******
Roadmap
*******
Release schedule
================
We intend to have about one timeboxed feature release every month
in periods of active development. The feature releases are numbered 0.x.0. The
features added is a mix of what we feel is most important/requested of the
missing features, and features we develop just because we find them fun to
make, even though they may be useful for very few users or for a limited use
case.
Bugfix releases, numbered 0.x.y, will be released whenever we discover bugs
that are too serious to wait for the next feature release. We will only release
bugfix releases for the last feature release. E.g. when 0.3.0 is released, we
will no longer provide bugfix releases for the 0.2 series. In other words,
there will be just a single supported release at any point in time.
Feature wishlist
================
We maintain our collection of sane or less sane ideas for future Mopidy
features as `issues <https://github.com/mopidy/mopidy/issues>`_ at GitHub
labeled with `the "wishlist" label
<https://github.com/mopidy/mopidy/issues?labels=wishlist>`_. Feel free to vote
up any feature you would love to see in Mopidy, but please refrain from adding
a comment just to say "I want this too!". You are of course free to add
comments if you have suggestions for how the feature should work or be
implemented, and you may add new wishlist issues if your ideas are not already
represented.

View File

@ -54,7 +54,7 @@ Development documentation
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3
development/index development
Indices and tables Indices and tables
================== ==================

View File

@ -2,7 +2,7 @@
GStreamer installation GStreamer installation
********************** **********************
To use the Mopidy, you first need to install GStreamer and the GStreamer Python To use Mopidy, you first need to install GStreamer and the GStreamer Python
bindings. bindings.
@ -54,15 +54,8 @@ Python bindings on OS X using Homebrew.
#. Install `Homebrew <https://github.com/mxcl/homebrew>`_. #. Install `Homebrew <https://github.com/mxcl/homebrew>`_.
#. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``, #. Download our Homebrew formula for ``gst-python``::
and ``gst-python``::
curl -o $(brew --prefix)/Library/Formula/pycairo.rb \
https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pycairo.rb
curl -o $(brew --prefix)/Library/Formula/pygobject.rb \
https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygobject.rb
curl -o $(brew --prefix)/Library/Formula/pygtk.rb \
https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygtk.rb
curl -o $(brew --prefix)/Library/Formula/gst-python.rb \ curl -o $(brew --prefix)/Library/Formula/gst-python.rb \
https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/gst-python.rb https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/gst-python.rb
@ -77,13 +70,13 @@ Python bindings on OS X using Homebrew.
You can either amend your ``PYTHONPATH`` permanently, by adding the You can either amend your ``PYTHONPATH`` permanently, by adding the
following statement to your shell's init file, e.g. ``~/.bashrc``:: following statement to your shell's init file, e.g. ``~/.bashrc``::
export PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages:$PYTHONPATH export PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages:$PYTHONPATH
Or, you can prefix the Mopidy command every time you run it:: Or, you can prefix the Mopidy command every time you run it::
PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages mopidy PYTHONPATH=$(brew --prefix)/lib/python2.7/site-packages mopidy
Note that you need to replace ``python2.6`` with ``python2.7`` if that's 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:: the Python version you are using. To find your Python version, run::
python --version python --version

View File

@ -173,7 +173,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git.
For an introduction to ``git``, please visit `git-scm.com For an introduction to ``git``, please visit `git-scm.com
<http://git-scm.com/>`_. Also, please read our :doc:`developer documentation <http://git-scm.com/>`_. Also, please read our :doc:`developer documentation
</development/index>`. </development>`.
From AUR on ArchLinux From AUR on ArchLinux

View File

@ -1,7 +0,0 @@
********************************************
:mod:`mopidy.gstreamer` -- GStreamer adapter
********************************************
.. automodule:: mopidy.gstreamer
:synopsis: GStreamer adapter
:members:

View File

@ -166,9 +166,9 @@ server simultaneously. To use the SHOUTcast output, do the following:
example, to set the username and password, use: example, to set the username and password, use:
``lame ! shout2send username="foobar" password="s3cret"``. ``lame ! shout2send username="foobar" password="s3cret"``.
Other advanced setups are also possible for outputs. Basically anything you can Other advanced setups are also possible for outputs. Basically, anything you
get a ``gst-lauch`` command to output to can be plugged into can use with the ``gst-launch-0.10`` command can be plugged into
:attr:`mopidy.settings.OUTPUT``. :attr:`mopidy.settings.OUTPUT`.
Available settings Available settings

View File

@ -30,7 +30,7 @@ sys.path.insert(0,
from mopidy import (get_version, settings, OptionalDependencyError, from mopidy import (get_version, settings, OptionalDependencyError,
SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE) SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
from mopidy.gstreamer import GStreamer from mopidy.audio import Audio
from mopidy.utils import get_class from mopidy.utils import get_class
from mopidy.utils.deps import list_deps_optparse_callback from mopidy.utils.deps import list_deps_optparse_callback
from mopidy.utils.log import setup_logging from mopidy.utils.log import setup_logging
@ -56,7 +56,7 @@ def main():
setup_logging(options.verbosity_level, options.save_debug_log) setup_logging(options.verbosity_level, options.save_debug_log)
check_old_folders() check_old_folders()
setup_settings(options.interactive) setup_settings(options.interactive)
setup_gstreamer() setup_audio()
setup_backend() setup_backend()
setup_frontends() setup_frontends()
loop.run() loop.run()
@ -70,7 +70,7 @@ def main():
loop.quit() loop.quit()
stop_frontends() stop_frontends()
stop_backend() stop_backend()
stop_gstreamer() stop_audio()
stop_remaining_actors() stop_remaining_actors()
@ -122,12 +122,12 @@ def setup_settings(interactive):
sys.exit(1) sys.exit(1)
def setup_gstreamer(): def setup_audio():
GStreamer.start() Audio.start()
def stop_gstreamer(): def stop_audio():
stop_actors_by_class(GStreamer) stop_actors_by_class(Audio)
def setup_backend(): def setup_backend():
get_class(settings.BACKENDS[0]).start() get_class(settings.BACKENDS[0]).start()

View File

@ -1,6 +1,7 @@
import pygst import pygst
pygst.require('0.10') pygst.require('0.10')
import gst import gst
import gobject
import logging import logging
@ -9,12 +10,15 @@ from pykka.registry import ActorRegistry
from mopidy import settings, utils from mopidy import settings, utils
from mopidy.backends.base import Backend from mopidy.backends.base import Backend
from mopidy import mixers # Trigger install of gst mixer plugins. from mopidy.utils import process
logger = logging.getLogger('mopidy.gstreamer') # Trigger install of gst mixer plugins
from mopidy.audio import mixers
logger = logging.getLogger('mopidy.audio')
class GStreamer(ThreadingActor): class Audio(ThreadingActor):
""" """
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_. Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
@ -27,7 +31,8 @@ class GStreamer(ThreadingActor):
""" """
def __init__(self): def __init__(self):
super(GStreamer, self).__init__() super(Audio, self).__init__()
self._default_caps = gst.Caps(""" self._default_caps = gst.Caps("""
audio/x-raw-int, audio/x-raw-int,
endianness=(int)1234, endianness=(int)1234,
@ -36,16 +41,29 @@ class GStreamer(ThreadingActor):
depth=(int)16, depth=(int)16,
signed=(boolean)true, signed=(boolean)true,
rate=(int)44100""") rate=(int)44100""")
self._pipeline = None self._pipeline = None
self._source = None self._source = None
self._uridecodebin = None self._uridecodebin = None
self._output = None self._output = None
self._mixer = None self._mixer = None
self._setup_pipeline() self._message_processor_set_up = False
self._setup_output()
self._setup_mixer() def on_start(self):
self._setup_message_processor() try:
self._setup_pipeline()
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_pipeline()
def _setup_pipeline(self): def _setup_pipeline(self):
# TODO: replace with and input bin so we simply have an input bin we # TODO: replace with and input bin so we simply have an input bin we
@ -65,10 +83,18 @@ class GStreamer(ThreadingActor):
self._uridecodebin.connect('pad-added', self._on_new_pad, self._uridecodebin.connect('pad-added', self._on_new_pad,
self._pipeline.get_by_name('queue').get_pad('sink')) self._pipeline.get_by_name('queue').get_pad('sink'))
def _teardown_pipeline(self):
self._pipeline.set_state(gst.STATE_NULL)
def _setup_output(self): def _setup_output(self):
# This will raise a gobject.GError if the description is bad. try:
self._output = gst.parse_bin_from_description( self._output = gst.parse_bin_from_description(
settings.OUTPUT, ghost_unconnected_pads=True) settings.OUTPUT, ghost_unconnected_pads=True)
except gobject.GError as ex:
logger.error('Failed to create output "%s": %s',
settings.OUTPUT, ex)
process.exit_process()
return
self._pipeline.add(self._output) self._pipeline.add(self._output)
gst.element_link_many(self._pipeline.get_by_name('queue'), gst.element_link_many(self._pipeline.get_by_name('queue'),
@ -80,8 +106,13 @@ class GStreamer(ThreadingActor):
logger.info('Not setting up mixer.') logger.info('Not setting up mixer.')
return return
# This will raise a gobject.GError if the description is bad. try:
mixerbin = gst.parse_bin_from_description(settings.MIXER, False) 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. # We assume that the bin will contain a single mixer.
mixer = mixerbin.get_by_interface('GstMixer') mixer = mixerbin.get_by_interface('GstMixer')
@ -113,10 +144,21 @@ class GStreamer(ThreadingActor):
gst.interfaces.MIXER_TRACK_OUTPUT): gst.interfaces.MIXER_TRACK_OUTPUT):
return track return track
def _teardown_mixer(self):
if self._mixer is not None:
(mixer, track) = self._mixer
mixer.set_state(gst.STATE_NULL)
def _setup_message_processor(self): def _setup_message_processor(self):
bus = self._pipeline.get_bus() bus = self._pipeline.get_bus()
bus.add_signal_watch() bus.add_signal_watch()
bus.connect('message', self._on_message) 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._pipeline.get_bus()
bus.remove_signal_watch()
def _on_new_source(self, element, pad): def _on_new_source(self, element, pad):
self._source = element.get_property('source') self._source = element.get_property('source')
@ -166,6 +208,8 @@ class GStreamer(ThreadingActor):
""" """
Call this to deliver raw audio data to be played. 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 :param capabilities: a GStreamer capabilities string
:type capabilities: string :type capabilities: string
:param data: raw audio data to be played :param data: raw audio data to be played
@ -289,9 +333,14 @@ class GStreamer(ThreadingActor):
""" """
Get volume level of the installed mixer. Get volume level of the installed mixer.
0 == muted. Example values:
100 == max volume for given system.
None == no mixer present, i.e. volume unknown. 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` :rtype: int in range [0..100] or :class:`None`
""" """
@ -339,7 +388,7 @@ class GStreamer(ThreadingActor):
deliver raw audio data to GStreamer. deliver raw audio data to GStreamer.
:param track: the current track :param track: the current track
:type track: :class:`mopidy.modes.Track` :type track: :class:`mopidy.models.Track`
""" """
taglist = gst.TagList() taglist = gst.TagList()
artists = [a for a in (track.artists or []) if a.name] artists = [a for a in (track.artists or []) if a.name]

View File

@ -38,6 +38,6 @@ def create_track(label, initial_volume, min_volume, max_volume,
# #
# Keep these imports at the bottom of the file to avoid cyclic import problems # Keep these imports at the bottom of the file to avoid cyclic import problems
# when mixers use the above code. # when mixers use the above code.
from mopidy.mixers.auto import AutoAudioMixer from .auto import AutoAudioMixer
from mopidy.mixers.fake import FakeMixer from .fake import FakeMixer
from mopidy.mixers.nad import NadMixer from .nad import NadMixer

View File

@ -5,7 +5,7 @@ import gst
import logging import logging
logger = logging.getLogger('mopidy.mixers.auto') logger = logging.getLogger('mopidy.audio.mixers.auto')
# TODO: we might want to add some ranking to the mixers we know about? # TODO: we might want to add some ranking to the mixers we know about?

View File

@ -3,7 +3,7 @@ pygst.require('0.10')
import gobject import gobject
import gst import gst
from mopidy.mixers import create_track from mopidy.audio.mixers import create_track
class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):

View File

@ -12,10 +12,10 @@ except ImportError:
from pykka.actor import ThreadingActor from pykka.actor import ThreadingActor
from mopidy.mixers import create_track from mopidy.audio.mixers import create_track
logger = logging.getLogger('mopidy.mixers.nad') logger = logging.getLogger('mopidy.audio.mixers.nad')
class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer): class NadMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):

View File

@ -1,12 +1,7 @@
import logging from .library import BaseLibraryProvider
from .playback import BasePlaybackProvider
from .stored_playlists import BaseStoredPlaylistsProvider
from .current_playlist import CurrentPlaylistController
from .library import LibraryController, BaseLibraryProvider
from .playback import PlaybackController, BasePlaybackProvider
from .stored_playlists import (StoredPlaylistsController,
BaseStoredPlaylistsProvider)
logger = logging.getLogger('mopidy.backends.base')
class Backend(object): class Backend(object):
#: The current playlist controller. An instance of #: The current playlist controller. An instance of

View File

@ -1,79 +1,3 @@
import logging
logger = logging.getLogger('mopidy.backends.base')
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`
"""
pykka_traversable = True
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
def find_exact(self, **query):
"""
Search the library for tracks where ``field`` is ``values``.
Examples::
# Returns results matching 'a'
find_exact(any=['a'])
# Returns results matching artist 'xyz'
find_exact(artist=['xyz'])
# Returns results matching 'a' and 'b' and artist 'xyz'
find_exact(any=['a', 'b'], artist=['xyz'])
:param query: one or more queries to search for
:type query: dict
:rtype: :class:`mopidy.models.Playlist`
"""
return self.provider.find_exact(**query)
def lookup(self, uri):
"""
Lookup track with given URI. Returns :class:`None` if not found.
:param uri: track URI
:type uri: string
:rtype: :class:`mopidy.models.Track` or :class:`None`
"""
return self.provider.lookup(uri)
def refresh(self, uri=None):
"""
Refresh library. Limit to URI and below if an URI is given.
:param uri: directory or track URI
:type uri: string
"""
return self.provider.refresh(uri)
def search(self, **query):
"""
Search the library for tracks where ``field`` contains ``values``.
Examples::
# Returns results matching 'a'
search(any=['a'])
# Returns results matching artist 'xyz'
search(artist=['xyz'])
# Returns results matching 'a' and 'b' and artist 'xyz'
search(any=['a', 'b'], artist=['xyz'])
:param query: one or more queries to search for
:type query: dict
:rtype: :class:`mopidy.models.Playlist`
"""
return self.provider.search(**query)
class BaseLibraryProvider(object): class BaseLibraryProvider(object):
""" """
:param backend: backend the controller is a part of :param backend: backend the controller is a part of

View File

@ -1,550 +1,3 @@
import logging
import random
import time
from mopidy.listeners import BackendListener
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 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
#: Constant representing the paused state.
PAUSED = u'paused'
#: Constant representing the playing state.
PLAYING = u'playing'
#: Constant representing the stopped state.
STOPPED = u'stopped'
#: :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)
#: 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 = self.STOPPED
self._shuffled = []
self._first_shuffle = True
self.play_time_accumulated = 0
self.play_time_started = None
def _get_cpid(self, cp_track):
if cp_track is None:
return None
return cp_track.cpid
def _get_track(self, cp_track):
if cp_track is None:
return None
return cp_track.track
@property
def current_cpid(self):
"""
The CPID (current playlist ID) of the currently playing or selected
track.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
return self._get_cpid(self.current_cp_track)
@property
def current_track(self):
"""
The currently playing or selected :class:`mopidy.models.Track`.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
return self._get_track(self.current_cp_track)
@property
def current_playlist_position(self):
"""
The position of the current track in the current playlist.
Read-only.
"""
if self.current_cp_track is None:
return None
try:
return self.backend.current_playlist.cp_tracks.index(
self.current_cp_track)
except ValueError:
return None
@property
def track_at_eot(self):
"""
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_eot` for convenience.
"""
return self._get_track(self.cp_track_at_eot)
@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`.
"""
# pylint: disable = R0911
# Too many return statements
cp_tracks = self.backend.current_playlist.cp_tracks
if not cp_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
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.repeat and self.single:
return cp_tracks[self.current_playlist_position]
if self.repeat and not self.single:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
try:
return cp_tracks[self.current_playlist_position + 1]
except IndexError:
return None
@property
def track_at_next(self):
"""
The track that will be played if calling :meth:`next()`.
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)
@property
def cp_track_at_next(self):
"""
The track that will be played if calling :meth:`next()`.
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
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:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
self._shuffled = cp_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.repeat:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
try:
return cp_tracks[self.current_playlist_position + 1]
except IndexError:
return None
@property
def track_at_previous(self):
"""
The track that will be played if calling :meth:`previous()`.
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)
@property
def cp_track_at_previous(self):
"""
The track that will be played if calling :meth:`previous()`.
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.
"""
if self.repeat or self.consume or self.random:
return self.current_cp_track
if self.current_playlist_position in (None, 0):
return None
return self.backend.current_playlist.cp_tracks[
self.current_playlist_position - 1]
@property
def state(self):
"""
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" ]
"""
return self._state
@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)
self._trigger_playback_state_changed()
# FIXME play_time stuff assumes backend does not have a better way of
# handeling this stuff :/
if (old_state in (self.PLAYING, self.STOPPED)
and new_state == self.PLAYING):
self._play_time_start()
elif old_state == self.PLAYING and new_state == self.PAUSED:
self._play_time_pause()
elif old_state == self.PAUSED and new_state == self.PLAYING:
self._play_time_resume()
@property
def time_position(self):
"""Time position in milliseconds."""
if self.state == self.PLAYING:
time_since_started = (self._current_wall_time -
self.play_time_started)
return self.play_time_accumulated + time_since_started
elif self.state == self.PAUSED:
return self.play_time_accumulated
elif self.state == self.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):
"""
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 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
if old_state == self.PLAYING:
self.play(on_error_step=on_error_step)
elif old_state == self.PAUSED:
self.pause()
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
"""
if self.state == self.STOPPED:
return
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
self._trigger_track_playback_ended()
self.play(self.cp_track_at_eot)
else:
self.stop(clear_current_track=True)
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
def on_current_playlist_change(self):
"""
Tell the playback controller that the current playlist has changed.
Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
"""
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):
self.stop(clear_current_track=True)
def next(self):
"""
Change to the next track.
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:
self._trigger_track_playback_ended()
self.change_track(self.cp_track_at_next)
else:
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
if self.provider.pause():
self.state = self.PAUSED
self._trigger_track_playback_paused()
def play(self, cp_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 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 self.state == self.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
if cp_track is not None:
self.current_cp_track = cp_track
self.state = self.PLAYING
if not self.provider.play(cp_track.track):
# Track is not playable
if self.random and self._shuffled:
self._shuffled.remove(cp_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)
self._trigger_track_playback_started()
def previous(self):
"""
Change to the previous track.
The current playback state will be kept. If it was playing, playing
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)
def resume(self):
"""If paused, resume playing the current track."""
if self.state == self.PAUSED and self.provider.resume():
self.state = self.PLAYING
self._trigger_track_playback_resumed()
def seek(self, time_position):
"""
Seeks to time position given in milliseconds.
:param time_position: time position in milliseconds
:type time_position: int
:rtype: :class:`True` if successful, else :class:`False`
"""
if not self.backend.current_playlist.tracks:
return False
if self.state == self.STOPPED:
self.play()
elif self.state == self.PAUSED:
self.resume()
if time_position < 0:
time_position = 0
elif time_position > self.current_track.length:
self.next()
return True
self.play_time_started = self._current_wall_time
self.play_time_accumulated = time_position
success = self.provider.seek(time_position)
if success:
self._trigger_seeked()
return success
def stop(self, clear_current_track=False):
"""
Stop playing.
:param clear_current_track: whether to clear the current track _after_
stopping
:type clear_current_track: boolean
"""
if self.state != self.STOPPED:
if self.provider.stop():
self._trigger_track_playback_ended()
self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
def _trigger_track_playback_paused(self):
logger.debug(u'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)
def _trigger_track_playback_resumed(self):
logger.debug(u'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)
def _trigger_track_playback_started(self):
logger.debug(u'Triggering track playback started event')
if self.current_track is None:
return
BackendListener.send('track_playback_started',
track=self.current_track)
def _trigger_track_playback_ended(self):
logger.debug(u'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)
def _trigger_playback_state_changed(self):
logger.debug(u'Triggering playback state change event')
BackendListener.send('playback_state_changed')
def _trigger_options_changed(self):
logger.debug(u'Triggering options changed event')
BackendListener.send('options_changed')
def _trigger_seeked(self):
logger.debug(u'Triggering seeked event')
BackendListener.send('seeked')
class BasePlaybackProvider(object): class BasePlaybackProvider(object):
""" """
:param backend: the backend :param backend: the backend
@ -560,73 +13,75 @@ class BasePlaybackProvider(object):
""" """
Pause playback. Pause playback.
*MUST be implemented by subclass.* *MAY be reimplemented by subclass.*
:rtype: :class:`True` if successful, else :class:`False` :rtype: :class:`True` if successful, else :class:`False`
""" """
raise NotImplementedError return self.backend.audio.pause_playback().get()
def play(self, track): def play(self, track):
""" """
Play given track. Play given track.
*MUST be implemented by subclass.* *MAY be reimplemented by subclass.*
:param track: the track to play :param track: the track to play
:type track: :class:`mopidy.models.Track` :type track: :class:`mopidy.models.Track`
:rtype: :class:`True` if successful, else :class:`False` :rtype: :class:`True` if successful, else :class:`False`
""" """
raise NotImplementedError self.backend.audio.prepare_change()
self.backend.audio.set_uri(track.uri).get()
return self.backend.audio.start_playback().get()
def resume(self): def resume(self):
""" """
Resume playback at the same time position playback was paused. Resume playback at the same time position playback was paused.
*MUST be implemented by subclass.* *MAY be reimplemented by subclass.*
:rtype: :class:`True` if successful, else :class:`False` :rtype: :class:`True` if successful, else :class:`False`
""" """
raise NotImplementedError return self.backend.audio.start_playback().get()
def seek(self, time_position): def seek(self, time_position):
""" """
Seek to a given time position. Seek to a given time position.
*MUST be implemented by subclass.* *MAY be reimplemented by subclass.*
:param time_position: time position in milliseconds :param time_position: time position in milliseconds
:type time_position: int :type time_position: int
:rtype: :class:`True` if successful, else :class:`False` :rtype: :class:`True` if successful, else :class:`False`
""" """
raise NotImplementedError return self.backend.audio.set_position(time_position).get()
def stop(self): def stop(self):
""" """
Stop playback. Stop playback.
*MUST be implemented by subclass.* *MAY be reimplemented by subclass.*
:rtype: :class:`True` if successful, else :class:`False` :rtype: :class:`True` if successful, else :class:`False`
""" """
raise NotImplementedError return self.backend.audio.stop_playback().get()
def get_volume(self): def get_volume(self):
""" """
Get current volume Get current volume
*MUST be implemented by subclass.* *MAY be reimplemented by subclass.*
:rtype: int [0..100] or :class:`None` :rtype: int [0..100] or :class:`None`
""" """
raise NotImplementedError return self.backend.audio.get_volume().get()
def set_volume(self, volume): def set_volume(self, volume):
""" """
Get current volume Get current volume
*MUST be implemented by subclass.* *MAY be reimplemented by subclass.*
:param: volume :param: volume
:type volume: int [0..100] :type volume: int [0..100]
""" """
raise NotImplementedError self.backend.audio.set_volume(volume)

View File

@ -1,120 +1,4 @@
from copy import copy from copy import copy
import logging
logger = logging.getLogger('mopidy.backends.base')
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)
class BaseStoredPlaylistsProvider(object): class BaseStoredPlaylistsProvider(object):

View File

@ -1,13 +1,11 @@
from pykka.actor import ThreadingActor from pykka.actor import ThreadingActor
from mopidy.backends.base import (Backend, CurrentPlaylistController, from mopidy import core
PlaybackController, BasePlaybackProvider, LibraryController, from mopidy.backends import base
BaseLibraryProvider, StoredPlaylistsController,
BaseStoredPlaylistsProvider)
from mopidy.models import Playlist from mopidy.models import Playlist
class DummyBackend(ThreadingActor, Backend): class DummyBackend(ThreadingActor, base.Backend):
""" """
A backend which implements the backend API in the simplest way possible. A backend which implements the backend API in the simplest way possible.
Used in tests of the frontends. Used in tests of the frontends.
@ -18,24 +16,24 @@ class DummyBackend(ThreadingActor, Backend):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DummyBackend, self).__init__(*args, **kwargs) super(DummyBackend, self).__init__(*args, **kwargs)
self.current_playlist = CurrentPlaylistController(backend=self) self.current_playlist = core.CurrentPlaylistController(backend=self)
library_provider = DummyLibraryProvider(backend=self) library_provider = DummyLibraryProvider(backend=self)
self.library = LibraryController(backend=self, self.library = core.LibraryController(backend=self,
provider=library_provider) provider=library_provider)
playback_provider = DummyPlaybackProvider(backend=self) playback_provider = DummyPlaybackProvider(backend=self)
self.playback = PlaybackController(backend=self, self.playback = core.PlaybackController(backend=self,
provider=playback_provider) provider=playback_provider)
stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self) stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self)
self.stored_playlists = StoredPlaylistsController(backend=self, self.stored_playlists = core.StoredPlaylistsController(backend=self,
provider=stored_playlists_provider) provider=stored_playlists_provider)
self.uri_schemes = [u'dummy'] self.uri_schemes = [u'dummy']
class DummyLibraryProvider(BaseLibraryProvider): class DummyLibraryProvider(base.BaseLibraryProvider):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DummyLibraryProvider, self).__init__(*args, **kwargs) super(DummyLibraryProvider, self).__init__(*args, **kwargs)
self.dummy_library = [] self.dummy_library = []
@ -55,7 +53,7 @@ class DummyLibraryProvider(BaseLibraryProvider):
return Playlist() return Playlist()
class DummyPlaybackProvider(BasePlaybackProvider): class DummyPlaybackProvider(base.BasePlaybackProvider):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DummyPlaybackProvider, self).__init__(*args, **kwargs) super(DummyPlaybackProvider, self).__init__(*args, **kwargs)
self._volume = None self._volume = None
@ -83,7 +81,7 @@ class DummyPlaybackProvider(BasePlaybackProvider):
self._volume = volume self._volume = volume
class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider): class DummyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
def create(self, name): def create(self, name):
playlist = Playlist(name=name) playlist = Playlist(name=name)
self._playlists.append(playlist) self._playlists.append(playlist)

View File

@ -7,13 +7,9 @@ import shutil
from pykka.actor import ThreadingActor from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry from pykka.registry import ActorRegistry
from mopidy import settings, DATA_PATH from mopidy import audio, core, settings, DATA_PATH
from mopidy.backends.base import (Backend, CurrentPlaylistController, from mopidy.backends import base
LibraryController, BaseLibraryProvider, PlaybackController,
BasePlaybackProvider, StoredPlaylistsController,
BaseStoredPlaylistsProvider)
from mopidy.models import Playlist, Track, Album from mopidy.models import Playlist, Track, Album
from mopidy.gstreamer import GStreamer
from .translator import parse_m3u, parse_mpd_tag_cache from .translator import parse_m3u, parse_mpd_tag_cache
@ -27,12 +23,10 @@ if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'):
DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music') DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music')
class LocalBackend(ThreadingActor, Backend): class LocalBackend(ThreadingActor, base.Backend):
""" """
A backend for playing music from a local music archive. A backend for playing music from a local music archive.
**Issues:** https://github.com/mopidy/mopidy/issues?labels=backend-local
**Dependencies:** **Dependencies:**
- None - None
@ -47,32 +41,32 @@ class LocalBackend(ThreadingActor, Backend):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LocalBackend, self).__init__(*args, **kwargs) super(LocalBackend, self).__init__(*args, **kwargs)
self.current_playlist = CurrentPlaylistController(backend=self) self.current_playlist = core.CurrentPlaylistController(backend=self)
library_provider = LocalLibraryProvider(backend=self) library_provider = LocalLibraryProvider(backend=self)
self.library = LibraryController(backend=self, self.library = core.LibraryController(backend=self,
provider=library_provider) provider=library_provider)
playback_provider = LocalPlaybackProvider(backend=self) playback_provider = base.BasePlaybackProvider(backend=self)
self.playback = LocalPlaybackController(backend=self, self.playback = LocalPlaybackController(backend=self,
provider=playback_provider) provider=playback_provider)
stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self)
self.stored_playlists = StoredPlaylistsController(backend=self, self.stored_playlists = core.StoredPlaylistsController(backend=self,
provider=stored_playlists_provider) provider=stored_playlists_provider)
self.uri_schemes = [u'file'] self.uri_schemes = [u'file']
self.gstreamer = None self.audio = None
def on_start(self): def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer) audio_refs = ActorRegistry.get_by_class(audio.Audio)
assert len(gstreamer_refs) == 1, \ assert len(audio_refs) == 1, \
'Expected exactly one running GStreamer.' 'Expected exactly one running Audio instance.'
self.gstreamer = gstreamer_refs[0].proxy() self.audio = audio_refs[0].proxy()
class LocalPlaybackController(PlaybackController): class LocalPlaybackController(core.PlaybackController):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LocalPlaybackController, self).__init__(*args, **kwargs) super(LocalPlaybackController, self).__init__(*args, **kwargs)
@ -81,35 +75,10 @@ class LocalPlaybackController(PlaybackController):
@property @property
def time_position(self): def time_position(self):
return self.backend.gstreamer.get_position().get() return self.backend.audio.get_position().get()
class LocalPlaybackProvider(BasePlaybackProvider): class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
def pause(self):
return self.backend.gstreamer.pause_playback().get()
def play(self, track):
self.backend.gstreamer.prepare_change()
self.backend.gstreamer.set_uri(track.uri).get()
return self.backend.gstreamer.start_playback().get()
def resume(self):
return self.backend.gstreamer.start_playback().get()
def seek(self, time_position):
return self.backend.gstreamer.set_position(time_position).get()
def stop(self):
return self.backend.gstreamer.stop_playback().get()
def get_volume(self):
return self.backend.gstreamer.get_volume().get()
def set_volume(self, volume):
self.backend.gstreamer.set_volume(volume).get()
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs) super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH
@ -124,7 +93,7 @@ class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
logger.info('Loading playlists from %s', self._folder) logger.info('Loading playlists from %s', self._folder)
for m3u in glob.glob(os.path.join(self._folder, '*.m3u')): for m3u in glob.glob(os.path.join(self._folder, '*.m3u')):
name = os.path.basename(m3u)[:len('.m3u')] name = os.path.basename(m3u)[:-len('.m3u')]
tracks = [] tracks = []
for uri in parse_m3u(m3u): for uri in parse_m3u(m3u):
try: try:
@ -182,7 +151,7 @@ class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
self._playlists.append(playlist) self._playlists.append(playlist)
class LocalLibraryProvider(BaseLibraryProvider): class LocalLibraryProvider(base.BaseLibraryProvider):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LocalLibraryProvider, self).__init__(*args, **kwargs) super(LocalLibraryProvider, self).__init__(*args, **kwargs)
self._uri_mapping = {} self._uri_mapping = {}
@ -203,7 +172,8 @@ class LocalLibraryProvider(BaseLibraryProvider):
try: try:
return self._uri_mapping[uri] return self._uri_mapping[uri]
except KeyError: except KeyError:
raise LookupError('%s not found.' % uri) logger.debug(u'Failed to lookup "%s"', uri)
return None
def find_exact(self, **query): def find_exact(self, **query):
self._validate_query(query) self._validate_query(query)

View File

@ -3,16 +3,14 @@ import logging
from pykka.actor import ThreadingActor from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry from pykka.registry import ActorRegistry
from mopidy import settings from mopidy import audio, core, settings
from mopidy.backends.base import (Backend, CurrentPlaylistController, from mopidy.backends import base
LibraryController, PlaybackController, StoredPlaylistsController)
from mopidy.gstreamer import GStreamer
logger = logging.getLogger('mopidy.backends.spotify') logger = logging.getLogger('mopidy.backends.spotify')
BITRATES = {96: 2, 160: 0, 320: 1} BITRATES = {96: 2, 160: 0, 320: 1}
class SpotifyBackend(ThreadingActor, Backend): class SpotifyBackend(ThreadingActor, base.Backend):
""" """
A backend for playing music from the `Spotify <http://www.spotify.com/>`_ A backend for playing music from the `Spotify <http://www.spotify.com/>`_
music streaming service. The backend uses the official `libspotify music streaming service. The backend uses the official `libspotify
@ -51,24 +49,24 @@ class SpotifyBackend(ThreadingActor, Backend):
super(SpotifyBackend, self).__init__(*args, **kwargs) super(SpotifyBackend, self).__init__(*args, **kwargs)
self.current_playlist = CurrentPlaylistController(backend=self) self.current_playlist = core.CurrentPlaylistController(backend=self)
library_provider = SpotifyLibraryProvider(backend=self) library_provider = SpotifyLibraryProvider(backend=self)
self.library = LibraryController(backend=self, self.library = core.LibraryController(backend=self,
provider=library_provider) provider=library_provider)
playback_provider = SpotifyPlaybackProvider(backend=self) playback_provider = SpotifyPlaybackProvider(backend=self)
self.playback = PlaybackController(backend=self, self.playback = core.PlaybackController(backend=self,
provider=playback_provider) provider=playback_provider)
stored_playlists_provider = SpotifyStoredPlaylistsProvider( stored_playlists_provider = SpotifyStoredPlaylistsProvider(
backend=self) backend=self)
self.stored_playlists = StoredPlaylistsController(backend=self, self.stored_playlists = core.StoredPlaylistsController(backend=self,
provider=stored_playlists_provider) provider=stored_playlists_provider)
self.uri_schemes = [u'spotify'] self.uri_schemes = [u'spotify']
self.gstreamer = None self.audio = None
self.spotify = None self.spotify = None
# Fail early if settings are not present # Fail early if settings are not present
@ -76,10 +74,10 @@ class SpotifyBackend(ThreadingActor, Backend):
self.password = settings.SPOTIFY_PASSWORD self.password = settings.SPOTIFY_PASSWORD
def on_start(self): def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer) audio_refs = ActorRegistry.get_by_class(audio.Audio)
assert len(gstreamer_refs) == 1, \ assert len(audio_refs) == 1, \
'Expected exactly one running GStreamer.' 'Expected exactly one running Audio instance.'
self.gstreamer = gstreamer_refs[0].proxy() self.audio = audio_refs[0].proxy()
logger.info(u'Mopidy uses SPOTIFY(R) CORE') logger.info(u'Mopidy uses SPOTIFY(R) CORE')
self.spotify = self._connect() self.spotify = self._connect()

View File

@ -5,21 +5,55 @@ from spotify import Link, SpotifyError
from mopidy.backends.base import BaseLibraryProvider from mopidy.backends.base import BaseLibraryProvider
from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist from mopidy.models import Track, Playlist
logger = logging.getLogger('mopidy.backends.spotify.library') logger = logging.getLogger('mopidy.backends.spotify.library')
class SpotifyTrack(Track):
"""Proxy object for unloaded Spotify tracks."""
def __init__(self, uri):
self._spotify_track = Link.from_string(uri).as_track()
self._unloaded_track = Track(uri=uri, name=u'[loading...]')
self._track = None
@property
def _proxy(self):
if self._track:
return self._track
elif self._spotify_track.is_loaded():
self._track = SpotifyTranslator.to_mopidy_track(self._spotify_track)
return self._track
else:
return self._unloaded_track
def __getattribute__(self, name):
if name.startswith('_'):
return super(SpotifyTrack, self).__getattribute__(name)
return self._proxy.__getattribute__(name)
def __repr__(self):
return self._proxy.__repr__()
def __hash__(self):
return hash(self._proxy.uri)
def __eq__(self, other):
if not isinstance(other, Track):
return False
return self._proxy.uri == other.uri
def copy(self, **values):
return self._proxy.copy(**values)
class SpotifyLibraryProvider(BaseLibraryProvider): class SpotifyLibraryProvider(BaseLibraryProvider):
def find_exact(self, **query): def find_exact(self, **query):
return self.search(**query) return self.search(**query)
def lookup(self, uri): def lookup(self, uri):
try: try:
spotify_track = Link.from_string(uri).as_track() return SpotifyTrack(uri)
# TODO Block until metadata_updated callback is called. Before that
# the track will be unloaded, unless it's already in the stored
# playlists.
return SpotifyTranslator.to_mopidy_track(spotify_track)
except SpotifyError as e: except SpotifyError as e:
logger.debug(u'Failed to lookup "%s": %s', uri, e) logger.debug(u'Failed to lookup "%s": %s', uri, e)
return None return None

View File

@ -3,15 +3,13 @@ import logging
from spotify import Link, SpotifyError from spotify import Link, SpotifyError
from mopidy.backends.base import BasePlaybackProvider from mopidy.backends.base import BasePlaybackProvider
from mopidy.core import PlaybackState
logger = logging.getLogger('mopidy.backends.spotify.playback') logger = logging.getLogger('mopidy.backends.spotify.playback')
class SpotifyPlaybackProvider(BasePlaybackProvider): class SpotifyPlaybackProvider(BasePlaybackProvider):
def pause(self):
return self.backend.gstreamer.pause_playback()
def play(self, track): def play(self, track):
if self.backend.playback.state == self.backend.playback.PLAYING: if self.backend.playback.state == PlaybackState.PLAYING:
self.backend.spotify.session.play(0) self.backend.spotify.session.play(0)
if track.uri is None: if track.uri is None:
return False return False
@ -19,10 +17,10 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
self.backend.spotify.session.load( self.backend.spotify.session.load(
Link.from_string(track.uri).as_track()) Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1) self.backend.spotify.session.play(1)
self.backend.gstreamer.prepare_change() self.backend.audio.prepare_change()
self.backend.gstreamer.set_uri('appsrc://') self.backend.audio.set_uri('appsrc://')
self.backend.gstreamer.start_playback() self.backend.audio.start_playback()
self.backend.gstreamer.set_metadata(track) self.backend.audio.set_metadata(track)
return True return True
except SpotifyError as e: except SpotifyError as e:
logger.info('Playback of %s failed: %s', track.uri, e) logger.info('Playback of %s failed: %s', track.uri, e)
@ -32,18 +30,11 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
return self.seek(self.backend.playback.time_position) return self.seek(self.backend.playback.time_position)
def seek(self, time_position): def seek(self, time_position):
self.backend.gstreamer.prepare_change() self.backend.audio.prepare_change()
self.backend.spotify.session.seek(time_position) self.backend.spotify.session.seek(time_position)
self.backend.gstreamer.start_playback() self.backend.audio.start_playback()
return True return True
def stop(self): def stop(self):
result = self.backend.gstreamer.stop_playback()
self.backend.spotify.session.play(0) self.backend.spotify.session.play(0)
return result return super(SpotifyPlaybackProvider, self).stop()
def get_volume(self):
return self.backend.gstreamer.get_volume().get()
def set_volume(self, volume):
self.backend.gstreamer.set_volume(volume)

View File

@ -6,14 +6,13 @@ from spotify.manager import SpotifySessionManager as PyspotifySessionManager
from pykka.registry import ActorRegistry from pykka.registry import ActorRegistry
from mopidy import get_version, settings, CACHE_PATH from mopidy import audio, get_version, settings, CACHE_PATH
from mopidy.backends.base import Backend from mopidy.backends.base import Backend
from mopidy.backends.spotify import BITRATES from mopidy.backends.spotify import BITRATES
from mopidy.backends.spotify.container_manager import SpotifyContainerManager from mopidy.backends.spotify.container_manager import SpotifyContainerManager
from mopidy.backends.spotify.playlist_manager import SpotifyPlaylistManager from mopidy.backends.spotify.playlist_manager import SpotifyPlaylistManager
from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist from mopidy.models import Playlist
from mopidy.gstreamer import GStreamer
from mopidy.utils.process import BaseThread from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.backends.spotify.session_manager') logger = logging.getLogger('mopidy.backends.spotify.session_manager')
@ -34,7 +33,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
BaseThread.__init__(self) BaseThread.__init__(self)
self.name = 'SpotifyThread' self.name = 'SpotifyThread'
self.gstreamer = None self.audio = None
self.backend = None self.backend = None
self.connected = threading.Event() self.connected = threading.Event()
@ -50,10 +49,10 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
self.connect() self.connect()
def setup(self): def setup(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer) audio_refs = ActorRegistry.get_by_class(audio.Audio)
assert len(gstreamer_refs) == 1, \ assert len(audio_refs) == 1, \
'Expected exactly one running gstreamer.' 'Expected exactly one running Audio instance.'
self.gstreamer = gstreamer_refs[0].proxy() self.audio = audio_refs[0].proxy()
backend_refs = ActorRegistry.get_by_class(Backend) backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.' assert len(backend_refs) == 1, 'Expected exactly one running backend.'
@ -117,7 +116,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
'sample_rate': sample_rate, 'sample_rate': sample_rate,
'channels': channels, 'channels': channels,
} }
self.gstreamer.emit_data(capabilites, bytes(frames)) self.audio.emit_data(capabilites, bytes(frames))
return num_frames return num_frames
def play_token_lost(self, session): def play_token_lost(self, session):
@ -143,7 +142,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def end_of_track(self, session): def end_of_track(self, session):
"""Callback used by pyspotify""" """Callback used by pyspotify"""
logger.debug(u'End of data stream reached') logger.debug(u'End of data stream reached')
self.gstreamer.emit_end_of_stream() self.audio.emit_end_of_stream()
def refresh_stored_playlists(self): def refresh_stored_playlists(self):
"""Refresh the stored playlists in the backend with fresh meta data """Refresh the stored playlists in the backend with fresh meta data

View File

@ -1,4 +1,3 @@
import datetime as dt
import logging import logging
from spotify import Link, SpotifyError from spotify import Link, SpotifyError
@ -31,9 +30,8 @@ class SpotifyTranslator(object):
if not spotify_track.is_loaded(): if not spotify_track.is_loaded():
return Track(uri=uri, name=u'[loading...]') return Track(uri=uri, name=u'[loading...]')
spotify_album = spotify_track.album() spotify_album = spotify_track.album()
if (spotify_album is not None and spotify_album.is_loaded() if spotify_album is not None and spotify_album.is_loaded():
and dt.MINYEAR <= int(spotify_album.year()) <= dt.MAXYEAR): date = spotify_album.year()
date = dt.date(spotify_album.year(), 1, 1)
else: else:
date = None date = None
return Track( return Track(

4
mopidy/core/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from .current_playlist import CurrentPlaylistController
from .library import LibraryController
from .playback import PlaybackController, PlaybackState
from .stored_playlists import StoredPlaylistsController

View File

@ -5,7 +5,9 @@ import random
from mopidy.listeners import BackendListener from mopidy.listeners import BackendListener
from mopidy.models import CpTrack from mopidy.models import CpTrack
logger = logging.getLogger('mopidy.backends.base')
logger = logging.getLogger('mopidy.core')
class CurrentPlaylistController(object): class CurrentPlaylistController(object):
""" """

70
mopidy/core/library.py Normal file
View File

@ -0,0 +1,70 @@
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`
"""
pykka_traversable = True
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
def find_exact(self, **query):
"""
Search the library for tracks where ``field`` is ``values``.
Examples::
# Returns results matching 'a'
find_exact(any=['a'])
# Returns results matching artist 'xyz'
find_exact(artist=['xyz'])
# Returns results matching 'a' and 'b' and artist 'xyz'
find_exact(any=['a', 'b'], artist=['xyz'])
:param query: one or more queries to search for
:type query: dict
:rtype: :class:`mopidy.models.Playlist`
"""
return self.provider.find_exact(**query)
def lookup(self, uri):
"""
Lookup track with given URI. Returns :class:`None` if not found.
:param uri: track URI
:type uri: string
:rtype: :class:`mopidy.models.Track` or :class:`None`
"""
return self.provider.lookup(uri)
def refresh(self, uri=None):
"""
Refresh library. Limit to URI and below if an URI is given.
:param uri: directory or track URI
:type uri: string
"""
return self.provider.refresh(uri)
def search(self, **query):
"""
Search the library for tracks where ``field`` contains ``values``.
Examples::
# Returns results matching 'a'
search(any=['a'])
# Returns results matching artist 'xyz'
search(artist=['xyz'])
# Returns results matching 'a' and 'b' and artist 'xyz'
search(any=['a', 'b'], artist=['xyz'])
:param query: one or more queries to search for
:type query: dict
:rtype: :class:`mopidy.models.Playlist`
"""
return self.provider.search(**query)

557
mopidy/core/playback.py Normal file
View File

@ -0,0 +1,557 @@
import logging
import random
import time
from mopidy.listeners import BackendListener
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'
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)
#: 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 = None
def _get_cpid(self, cp_track):
if cp_track is None:
return None
return cp_track.cpid
def _get_track(self, cp_track):
if cp_track is None:
return None
return cp_track.track
@property
def current_cpid(self):
"""
The CPID (current playlist ID) of the currently playing or selected
track.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
return self._get_cpid(self.current_cp_track)
@property
def current_track(self):
"""
The currently playing or selected :class:`mopidy.models.Track`.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
return self._get_track(self.current_cp_track)
@property
def current_playlist_position(self):
"""
The position of the current track in the current playlist.
Read-only.
"""
if self.current_cp_track is None:
return None
try:
return self.backend.current_playlist.cp_tracks.index(
self.current_cp_track)
except ValueError:
return None
@property
def track_at_eot(self):
"""
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_eot` for convenience.
"""
return self._get_track(self.cp_track_at_eot)
@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`.
"""
# pylint: disable = R0911
# Too many return statements
cp_tracks = self.backend.current_playlist.cp_tracks
if not cp_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
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.repeat and self.single:
return cp_tracks[self.current_playlist_position]
if self.repeat and not self.single:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
try:
return cp_tracks[self.current_playlist_position + 1]
except IndexError:
return None
@property
def track_at_next(self):
"""
The track that will be played if calling :meth:`next()`.
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)
@property
def cp_track_at_next(self):
"""
The track that will be played if calling :meth:`next()`.
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
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:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
self._shuffled = cp_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.repeat:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
try:
return cp_tracks[self.current_playlist_position + 1]
except IndexError:
return None
@property
def track_at_previous(self):
"""
The track that will be played if calling :meth:`previous()`.
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)
@property
def cp_track_at_previous(self):
"""
The track that will be played if calling :meth:`previous()`.
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.
"""
if self.repeat or self.consume or self.random:
return self.current_cp_track
if self.current_playlist_position in (None, 0):
return None
return self.backend.current_playlist.cp_tracks[
self.current_playlist_position - 1]
@property
def state(self):
"""
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" ]
"""
return self._state
@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)
self._trigger_playback_state_changed()
# 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()
@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):
"""
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 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
if old_state == PlaybackState.PLAYING:
self.play(on_error_step=on_error_step)
elif old_state == PlaybackState.PAUSED:
self.pause()
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
"""
if self.state == PlaybackState.STOPPED:
return
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
self._trigger_track_playback_ended()
self.play(self.cp_track_at_eot)
else:
self.stop(clear_current_track=True)
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
def on_current_playlist_change(self):
"""
Tell the playback controller that the current playlist has changed.
Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
"""
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):
self.stop(clear_current_track=True)
def next(self):
"""
Change to the next track.
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:
self._trigger_track_playback_ended()
self.change_track(self.cp_track_at_next)
else:
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
if self.provider.pause():
self.state = PlaybackState.PAUSED
self._trigger_track_playback_paused()
def play(self, cp_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 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 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
if cp_track is not None:
self.current_cp_track = cp_track
self.state = PlaybackState.PLAYING
if not self.provider.play(cp_track.track):
# Track is not playable
if self.random and self._shuffled:
self._shuffled.remove(cp_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)
self._trigger_track_playback_started()
def previous(self):
"""
Change to the previous track.
The current playback state will be kept. If it was playing, playing
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)
def resume(self):
"""If paused, resume playing the current track."""
if self.state == PlaybackState.PAUSED and self.provider.resume():
self.state = PlaybackState.PLAYING
self._trigger_track_playback_resumed()
def seek(self, time_position):
"""
Seeks to time position given in milliseconds.
:param time_position: time position in milliseconds
:type time_position: int
:rtype: :class:`True` if successful, else :class:`False`
"""
if not self.backend.current_playlist.tracks:
return False
if self.state == PlaybackState.STOPPED:
self.play()
elif self.state == PlaybackState.PAUSED:
self.resume()
if time_position < 0:
time_position = 0
elif time_position > self.current_track.length:
self.next()
return True
self.play_time_started = self._current_wall_time
self.play_time_accumulated = time_position
success = self.provider.seek(time_position)
if success:
self._trigger_seeked()
return success
def stop(self, clear_current_track=False):
"""
Stop playing.
:param clear_current_track: whether to clear the current track _after_
stopping
:type clear_current_track: boolean
"""
if self.state != PlaybackState.STOPPED:
if self.provider.stop():
self._trigger_track_playback_ended()
self.state = PlaybackState.STOPPED
if clear_current_track:
self.current_cp_track = None
def _trigger_track_playback_paused(self):
logger.debug(u'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)
def _trigger_track_playback_resumed(self):
logger.debug(u'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)
def _trigger_track_playback_started(self):
logger.debug(u'Triggering track playback started event')
if self.current_track is None:
return
BackendListener.send('track_playback_started',
track=self.current_track)
def _trigger_track_playback_ended(self):
logger.debug(u'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)
def _trigger_playback_state_changed(self):
logger.debug(u'Triggering playback state change event')
BackendListener.send('playback_state_changed')
def _trigger_options_changed(self):
logger.debug(u'Triggering options changed event')
BackendListener.send('options_changed')
def _trigger_seeked(self):
logger.debug(u'Triggering seeked event')
BackendListener.send('seeked')

View File

@ -0,0 +1,113 @@
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)

View File

@ -242,7 +242,7 @@ def _list_date(context, query):
playlist = context.backend.library.find_exact(**query).get() playlist = context.backend.library.find_exact(**query).get()
for track in playlist.tracks: for track in playlist.tracks:
if track.date is not None: if track.date is not None:
dates.add((u'Date', track.date.strftime('%Y-%m-%d'))) dates.add((u'Date', track.date))
return dates return dates
@handle_request(r'^listall "(?P<uri>[^"]+)"') @handle_request(r'^listall "(?P<uri>[^"]+)"')

View File

@ -1,4 +1,4 @@
from mopidy.backends.base import PlaybackController from mopidy.core import PlaybackState
from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented) MpdNotImplemented)
@ -104,11 +104,9 @@ def pause(context, state=None):
- Calls ``pause`` without any arguments to toogle pause. - Calls ``pause`` without any arguments to toogle pause.
""" """
if state is None: if state is None:
if (context.backend.playback.state.get() == if (context.backend.playback.state.get() == PlaybackState.PLAYING):
PlaybackController.PLAYING):
context.backend.playback.pause() context.backend.playback.pause()
elif (context.backend.playback.state.get() == elif (context.backend.playback.state.get() == PlaybackState.PAUSED):
PlaybackController.PAUSED):
context.backend.playback.resume() context.backend.playback.resume()
elif int(state): elif int(state):
context.backend.playback.pause() context.backend.playback.pause()
@ -185,9 +183,9 @@ def playpos(context, songpos):
raise MpdArgError(u'Bad song index', command=u'play') raise MpdArgError(u'Bad song index', command=u'play')
def _play_minus_one(context): def _play_minus_one(context):
if (context.backend.playback.state.get() == PlaybackController.PLAYING): if (context.backend.playback.state.get() == PlaybackState.PLAYING):
return # Nothing to do return # Nothing to do
elif (context.backend.playback.state.get() == PlaybackController.PAUSED): elif (context.backend.playback.state.get() == PlaybackState.PAUSED):
return context.backend.playback.resume().get() return context.backend.playback.resume().get()
elif context.backend.playback.current_cp_track.get() is not None: elif context.backend.playback.current_cp_track.get() is not None:
cp_track = context.backend.playback.current_cp_track.get() cp_track = context.backend.playback.current_cp_track.get()

View File

@ -1,6 +1,6 @@
import pykka.future import pykka.future
from mopidy.backends.base import PlaybackController from mopidy.core import PlaybackState
from mopidy.frontends.mpd.exceptions import MpdNotImplemented from mopidy.frontends.mpd.exceptions import MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.translator import track_to_mpd_format from mopidy.frontends.mpd.translator import track_to_mpd_format
@ -194,8 +194,8 @@ def status(context):
if futures['playback.current_cp_track'].get() is not None: if futures['playback.current_cp_track'].get() is not None:
result.append(('song', _status_songpos(futures))) result.append(('song', _status_songpos(futures)))
result.append(('songid', _status_songid(futures))) result.append(('songid', _status_songid(futures)))
if futures['playback.state'].get() in (PlaybackController.PLAYING, if futures['playback.state'].get() in (PlaybackState.PLAYING,
PlaybackController.PAUSED): PlaybackState.PAUSED):
result.append(('time', _status_time(futures))) result.append(('time', _status_time(futures)))
result.append(('elapsed', _status_time_elapsed(futures))) result.append(('elapsed', _status_time_elapsed(futures)))
result.append(('bitrate', _status_bitrate(futures))) result.append(('bitrate', _status_bitrate(futures)))
@ -239,11 +239,11 @@ def _status_songpos(futures):
def _status_state(futures): def _status_state(futures):
state = futures['playback.state'].get() state = futures['playback.state'].get()
if state == PlaybackController.PLAYING: if state == PlaybackState.PLAYING:
return u'play' return u'play'
elif state == PlaybackController.STOPPED: elif state == PlaybackState.STOPPED:
return u'stop' return u'stop'
elif state == PlaybackController.PAUSED: elif state == PlaybackState.PAUSED:
return u'pause' return u'pause'
def _status_time(futures): def _status_time(futures):

View File

@ -16,7 +16,7 @@ from pykka.registry import ActorRegistry
from mopidy import settings from mopidy import settings
from mopidy.backends.base import Backend from mopidy.backends.base import Backend
from mopidy.backends.base.playback import PlaybackController from mopidy.core import PlaybackState
from mopidy.utils.process import exit_process from mopidy.utils.process import exit_process
# Must be done before dbus.SessionBus() is called # Must be done before dbus.SessionBus() is called
@ -198,11 +198,11 @@ class MprisObject(dbus.service.Object):
logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE) logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE)
return return
state = self.backend.playback.state.get() state = self.backend.playback.state.get()
if state == PlaybackController.PLAYING: if state == PlaybackState.PLAYING:
self.backend.playback.pause().get() self.backend.playback.pause().get()
elif state == PlaybackController.PAUSED: elif state == PlaybackState.PAUSED:
self.backend.playback.resume().get() self.backend.playback.resume().get()
elif state == PlaybackController.STOPPED: elif state == PlaybackState.STOPPED:
self.backend.playback.play().get() self.backend.playback.play().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE) @dbus.service.method(dbus_interface=PLAYER_IFACE)
@ -220,7 +220,7 @@ class MprisObject(dbus.service.Object):
logger.debug(u'%s.Play not allowed', PLAYER_IFACE) logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
return return
state = self.backend.playback.state.get() state = self.backend.playback.state.get()
if state == PlaybackController.PAUSED: if state == PlaybackState.PAUSED:
self.backend.playback.resume().get() self.backend.playback.resume().get()
else: else:
self.backend.playback.play().get() self.backend.playback.play().get()
@ -287,11 +287,11 @@ class MprisObject(dbus.service.Object):
def get_PlaybackStatus(self): def get_PlaybackStatus(self):
state = self.backend.playback.state.get() state = self.backend.playback.state.get()
if state == PlaybackController.PLAYING: if state == PlaybackState.PLAYING:
return 'Playing' return 'Playing'
elif state == PlaybackController.PAUSED: elif state == PlaybackState.PAUSED:
return 'Paused' return 'Paused'
elif state == PlaybackController.STOPPED: elif state == PlaybackState.STOPPED:
return 'Stopped' return 'Stopped'
def get_LoopStatus(self): def get_LoopStatus(self):

View File

@ -1,5 +1,6 @@
from collections import namedtuple from collections import namedtuple
class ImmutableObject(object): class ImmutableObject(object):
""" """
Superclass for immutable objects whose fields can only be modified via the Superclass for immutable objects whose fields can only be modified via the
@ -157,8 +158,8 @@ class Track(ImmutableObject):
:type album: :class:`Album` :type album: :class:`Album`
:param track_no: track number in album :param track_no: track number in album
:type track_no: integer :type track_no: integer
:param date: track release date :param date: track release date (YYYY or YYYY-MM-DD)
:type date: :class:`datetime.date` :type date: string
:param length: track length in milliseconds :param length: track length in milliseconds
:type length: integer :type length: integer
:param bitrate: bitrate in kbit/s :param bitrate: bitrate in kbit/s

View File

@ -1,5 +1,5 @@
""" """
Available settings and their default values. All available settings and their default values.
.. warning:: .. warning::
@ -14,6 +14,10 @@ Available settings and their default values.
#: #:
#: BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',) #: BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',)
#: #:
#: Other typical values::
#:
#: BACKENDS = (u'mopidy.backends.local.LocalBackend',)
#:
#: .. note:: #: .. note::
#: Currently only the first backend in the list is used. #: Currently only the first backend in the list is used.
BACKENDS = ( BACKENDS = (
@ -106,9 +110,9 @@ LOCAL_TAG_CACHE_FILE = None
#: Sound mixer to use. #: Sound mixer to use.
#: #:
#: Expects a GStreamer mixer to use, typical values are: #: Expects a GStreamer mixer to use, typical values are:
#: alsamixer, pulsemixer, oss4mixer, ossmixer. #: ``alsamixer``, ``pulsemixer``, ``ossmixer``, and ``oss4mixer``.
#: #:
#: Setting this to ``None`` means no volume control. #: Setting this to :class:`None` turns off volume control.
#: #:
#: Default:: #: Default::
#: #:
@ -118,7 +122,7 @@ MIXER = u'autoaudiomixer'
#: Sound mixer track to use. #: Sound mixer track to use.
#: #:
#: Name of the mixer track to use. If this is not set we will try to find the #: Name of the mixer track to use. If this is not set we will try to find the
#: output track with master set. As an example, using ``alsamixer`` you would #: master output track. As an example, using ``alsamixer`` you would
#: typically set this to ``Master`` or ``PCM``. #: typically set this to ``Master`` or ``PCM``.
#: #:
#: Default:: #: Default::
@ -128,7 +132,9 @@ MIXER_TRACK = None
#: Which address Mopidy's MPD server should bind to. #: Which address Mopidy's MPD server should bind to.
#: #:
#:Examples: #: Used by :mod:`mopidy.frontends.mpd`.
#:
#: Examples:
#: #:
#: ``127.0.0.1`` #: ``127.0.0.1``
#: Listens only on the IPv4 loopback interface. Default. #: Listens only on the IPv4 loopback interface. Default.
@ -142,16 +148,22 @@ MPD_SERVER_HOSTNAME = u'127.0.0.1'
#: Which TCP port Mopidy's MPD server should listen to. #: Which TCP port Mopidy's MPD server should listen to.
#: #:
#: Used by :mod:`mopidy.frontends.mpd`.
#:
#: Default: 6600 #: Default: 6600
MPD_SERVER_PORT = 6600 MPD_SERVER_PORT = 6600
#: The password required for connecting to the MPD server. #: The password required for connecting to the MPD server.
#: #:
#: Used by :mod:`mopidy.frontends.mpd`.
#:
#: Default: :class:`None`, which means no password required. #: Default: :class:`None`, which means no password required.
MPD_SERVER_PASSWORD = None MPD_SERVER_PASSWORD = None
#: The maximum number of concurrent connections the MPD server will accept. #: The maximum number of concurrent connections the MPD server will accept.
#: #:
#: Used by :mod:`mopidy.frontends.mpd`.
#:
#: Default: 20 #: Default: 20
MPD_SERVER_MAX_CONNECTIONS = 20 MPD_SERVER_MAX_CONNECTIONS = 20

View File

@ -1,7 +1,6 @@
import sys import sys
from mopidy import settings from mopidy import audio, settings
from mopidy.gstreamer import GStreamer
from mopidy.utils.path import path_to_uri from mopidy.utils.path import path_to_uri
from tests import unittest, path_to_data_dir from tests import unittest, path_to_data_dir
@ -9,37 +8,38 @@ from tests import unittest, path_to_data_dir
@unittest.skipIf(sys.platform == 'win32', @unittest.skipIf(sys.platform == 'win32',
'Our Windows build server does not support GStreamer yet') 'Our Windows build server does not support GStreamer yet')
class GStreamerTest(unittest.TestCase): class AudioTest(unittest.TestCase):
def setUp(self): def setUp(self):
settings.MIXER = 'fakemixer track_max_volume=65536' settings.MIXER = 'fakemixer track_max_volume=65536'
settings.OUTPUT = 'fakesink' settings.OUTPUT = 'fakesink'
self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
self.gstreamer = GStreamer() self.audio = audio.Audio.start().proxy()
def tearDown(self): def tearDown(self):
self.audio.stop()
settings.runtime.clear() settings.runtime.clear()
def prepare_uri(self, uri): def prepare_uri(self, uri):
self.gstreamer.prepare_change() self.audio.prepare_change()
self.gstreamer.set_uri(uri) self.audio.set_uri(uri)
def test_start_playback_existing_file(self): def test_start_playback_existing_file(self):
self.prepare_uri(self.song_uri) self.prepare_uri(self.song_uri)
self.assertTrue(self.gstreamer.start_playback()) self.assertTrue(self.audio.start_playback().get())
def test_start_playback_non_existing_file(self): def test_start_playback_non_existing_file(self):
self.prepare_uri(self.song_uri + 'bogus') self.prepare_uri(self.song_uri + 'bogus')
self.assertFalse(self.gstreamer.start_playback()) self.assertFalse(self.audio.start_playback().get())
def test_pause_playback_while_playing(self): def test_pause_playback_while_playing(self):
self.prepare_uri(self.song_uri) self.prepare_uri(self.song_uri)
self.gstreamer.start_playback() self.audio.start_playback()
self.assertTrue(self.gstreamer.pause_playback()) self.assertTrue(self.audio.pause_playback().get())
def test_stop_playback_while_playing(self): def test_stop_playback_while_playing(self):
self.prepare_uri(self.song_uri) self.prepare_uri(self.song_uri)
self.gstreamer.start_playback() self.audio.start_playback()
self.assertTrue(self.gstreamer.stop_playback()) self.assertTrue(self.audio.stop_playback().get())
@unittest.SkipTest @unittest.SkipTest
def test_deliver_data(self): def test_deliver_data(self):
@ -51,8 +51,8 @@ class GStreamerTest(unittest.TestCase):
def test_set_volume(self): def test_set_volume(self):
for value in range(0, 101): for value in range(0, 101):
self.assertTrue(self.gstreamer.set_volume(value)) self.assertTrue(self.audio.set_volume(value).get())
self.assertEqual(value, self.gstreamer.get_volume()) self.assertEqual(value, self.audio.get_volume().get())
@unittest.SkipTest @unittest.SkipTest
def test_set_state_encapsulation(self): def test_set_state_encapsulation(self):
@ -65,4 +65,3 @@ class GStreamerTest(unittest.TestCase):
@unittest.SkipTest @unittest.SkipTest
def test_invalid_output_raises_error(self): def test_invalid_output_raises_error(self):
pass # TODO pass # TODO

View File

@ -1,8 +1,9 @@
import mock import mock
import random import random
from mopidy import audio
from mopidy.core import PlaybackState
from mopidy.models import CpTrack, Playlist, Track from mopidy.models import CpTrack, Playlist, Track
from mopidy.gstreamer import GStreamer
from tests.backends.base import populate_playlist from tests.backends.base import populate_playlist
@ -12,7 +13,7 @@ class CurrentPlaylistControllerTest(object):
def setUp(self): def setUp(self):
self.backend = self.backend_class() self.backend = self.backend_class()
self.backend.gstreamer = mock.Mock(spec=GStreamer) self.backend.audio = mock.Mock(spec=audio.Audio)
self.controller = self.backend.current_playlist self.controller = self.backend.current_playlist
self.playback = self.backend.playback self.playback = self.backend.playback
@ -71,9 +72,9 @@ class CurrentPlaylistControllerTest(object):
@populate_playlist @populate_playlist
def test_clear_when_playing(self): def test_clear_when_playing(self):
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.controller.clear() self.controller.clear()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_get_by_uri_returns_unique_match(self): def test_get_by_uri_returns_unique_match(self):
track = Track(uri='a') track = Track(uri='a')
@ -134,13 +135,13 @@ class CurrentPlaylistControllerTest(object):
self.playback.play() self.playback.play()
track = self.playback.current_track track = self.playback.current_track
self.controller.append(self.controller.tracks[1:2]) self.controller.append(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track) self.assertEqual(self.playback.current_track, track)
@populate_playlist @populate_playlist
def test_append_preserves_stopped_state(self): def test_append_preserves_stopped_state(self):
self.controller.append(self.controller.tracks[1:2]) self.controller.append(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
def test_index_returns_index_of_track(self): def test_index_returns_index_of_track(self):
@ -205,7 +206,7 @@ class CurrentPlaylistControllerTest(object):
version = self.controller.version version = self.controller.version
self.controller.remove(uri=track1.uri) self.controller.remove(uri=track1.uri)
self.assert_(version < self.controller.version) self.assert_(version < self.controller.version)
self.assert_(track1 not in self.controller.tracks) self.assertNotIn(track1, self.controller.tracks)
self.assertEqual(track2, self.controller.tracks[1]) self.assertEqual(track2, self.controller.tracks[1])
@populate_playlist @populate_playlist

View File

@ -34,8 +34,8 @@ class LibraryControllerTest(object):
self.assertEqual(track, self.tracks[0]) self.assertEqual(track, self.tracks[0])
def test_lookup_unknown_track(self): def test_lookup_unknown_track(self):
test = lambda: self.library.lookup('fake uri') track = self.library.lookup('fake uri')
self.assertRaises(LookupError, test) self.assertEquals(track, None)
def test_find_exact_no_hits(self): def test_find_exact_no_hits(self):
result = self.library.find_exact(track=['unknown track']) result = self.library.find_exact(track=['unknown track'])

View File

@ -2,8 +2,9 @@ import mock
import random import random
import time import time
from mopidy import audio
from mopidy.core import PlaybackState
from mopidy.models import Track from mopidy.models import Track
from mopidy.gstreamer import GStreamer
from tests import unittest from tests import unittest
from tests.backends.base import populate_playlist from tests.backends.base import populate_playlist
@ -16,7 +17,7 @@ class PlaybackControllerTest(object):
def setUp(self): def setUp(self):
self.backend = self.backend_class() self.backend = self.backend_class()
self.backend.gstreamer = mock.Mock(spec=GStreamer) self.backend.audio = mock.Mock(spec=audio.Audio)
self.playback = self.backend.playback self.playback = self.backend.playback
self.current_playlist = self.backend.current_playlist self.current_playlist = self.backend.current_playlist
@ -26,21 +27,21 @@ class PlaybackControllerTest(object):
'First song needs to be at least 2000 miliseconds' 'First song needs to be at least 2000 miliseconds'
def test_initial_state_is_stopped(self): def test_initial_state_is_stopped(self):
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_play_with_empty_playlist(self): def test_play_with_empty_playlist(self):
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_play_with_empty_playlist_return_value(self): def test_play_with_empty_playlist_return_value(self):
self.assertEqual(self.playback.play(), None) self.assertEqual(self.playback.play(), None)
@populate_playlist @populate_playlist
def test_play_state(self): def test_play_state(self):
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_playlist @populate_playlist
def test_play_return_value(self): def test_play_return_value(self):
@ -48,9 +49,9 @@ class PlaybackControllerTest(object):
@populate_playlist @populate_playlist
def test_play_track_state(self): def test_play_track_state(self):
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.play(self.current_playlist.cp_tracks[-1])
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_playlist @populate_playlist
def test_play_track_return_value(self): def test_play_track_return_value(self):
@ -70,7 +71,7 @@ class PlaybackControllerTest(object):
track = self.playback.current_track track = self.playback.current_track
self.playback.pause() self.playback.pause()
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(track, self.playback.current_track) self.assertEqual(track, self.playback.current_track)
@populate_playlist @populate_playlist
@ -81,7 +82,7 @@ class PlaybackControllerTest(object):
track = self.playback.current_track track = self.playback.current_track
self.playback.pause() self.playback.pause()
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(track, self.playback.current_track) self.assertEqual(track, self.playback.current_track)
@populate_playlist @populate_playlist
@ -106,12 +107,12 @@ class PlaybackControllerTest(object):
def test_current_track_after_completed_playlist(self): def test_current_track_after_completed_playlist(self):
self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.play(self.current_playlist.cp_tracks[-1])
self.playback.on_end_of_track() self.playback.on_end_of_track()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.play(self.current_playlist.cp_tracks[-1])
self.playback.next() self.playback.next()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
@populate_playlist @populate_playlist
@ -141,17 +142,17 @@ class PlaybackControllerTest(object):
self.playback.next() self.playback.next()
self.playback.stop() self.playback.stop()
self.playback.previous() self.playback.previous()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_previous_at_start_of_playlist(self): def test_previous_at_start_of_playlist(self):
self.playback.previous() self.playback.previous()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
def test_previous_for_empty_playlist(self): def test_previous_for_empty_playlist(self):
self.playback.previous() self.playback.previous()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
@populate_playlist @populate_playlist
@ -185,20 +186,20 @@ class PlaybackControllerTest(object):
@populate_playlist @populate_playlist
def test_next_does_not_trigger_playback(self): def test_next_does_not_trigger_playback(self):
self.playback.next() self.playback.next()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_next_at_end_of_playlist(self): def test_next_at_end_of_playlist(self):
self.playback.play() self.playback.play()
for i, track in enumerate(self.tracks): for i, track in enumerate(self.tracks):
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track) self.assertEqual(self.playback.current_track, track)
self.assertEqual(self.playback.current_playlist_position, i) self.assertEqual(self.playback.current_playlist_position, i)
self.playback.next() self.playback.next()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_next_until_end_of_playlist_and_play_from_start(self): def test_next_until_end_of_playlist_and_play_from_start(self):
@ -208,15 +209,15 @@ class PlaybackControllerTest(object):
self.playback.next() self.playback.next()
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, self.tracks[0]) self.assertEqual(self.playback.current_track, self.tracks[0])
def test_next_for_empty_playlist(self): def test_next_for_empty_playlist(self):
self.playback.next() self.playback.next()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_next_skips_to_next_track_on_failure(self): def test_next_skips_to_next_track_on_failure(self):
@ -273,7 +274,7 @@ class PlaybackControllerTest(object):
self.playback.consume = True self.playback.consume = True
self.playback.play() self.playback.play()
self.playback.next() self.playback.next()
self.assert_(self.tracks[0] in self.backend.current_playlist.tracks) self.assertIn(self.tracks[0], self.backend.current_playlist.tracks)
@populate_playlist @populate_playlist
def test_next_with_single_and_repeat(self): def test_next_with_single_and_repeat(self):
@ -321,20 +322,20 @@ class PlaybackControllerTest(object):
@populate_playlist @populate_playlist
def test_end_of_track_does_not_trigger_playback(self): def test_end_of_track_does_not_trigger_playback(self):
self.playback.on_end_of_track() self.playback.on_end_of_track()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_end_of_track_at_end_of_playlist(self): def test_end_of_track_at_end_of_playlist(self):
self.playback.play() self.playback.play()
for i, track in enumerate(self.tracks): for i, track in enumerate(self.tracks):
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track) self.assertEqual(self.playback.current_track, track)
self.assertEqual(self.playback.current_playlist_position, i) self.assertEqual(self.playback.current_playlist_position, i)
self.playback.on_end_of_track() self.playback.on_end_of_track()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_end_of_track_until_end_of_playlist_and_play_from_start(self): def test_end_of_track_until_end_of_playlist_and_play_from_start(self):
@ -344,15 +345,15 @@ class PlaybackControllerTest(object):
self.playback.on_end_of_track() self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, self.tracks[0]) self.assertEqual(self.playback.current_track, self.tracks[0])
def test_end_of_track_for_empty_playlist(self): def test_end_of_track_for_empty_playlist(self):
self.playback.on_end_of_track() self.playback.on_end_of_track()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_end_of_track_skips_to_next_track_on_failure(self): def test_end_of_track_skips_to_next_track_on_failure(self):
@ -410,7 +411,7 @@ class PlaybackControllerTest(object):
self.playback.consume = True self.playback.consume = True
self.playback.play() self.playback.play()
self.playback.on_end_of_track() self.playback.on_end_of_track()
self.assert_(self.tracks[0] not in self.backend.current_playlist.tracks) self.assertNotIn(self.tracks[0], self.backend.current_playlist.tracks)
@populate_playlist @populate_playlist
def test_end_of_track_with_random(self): def test_end_of_track_with_random(self):
@ -534,13 +535,13 @@ class PlaybackControllerTest(object):
self.playback.play() self.playback.play()
current_track = self.playback.current_track current_track = self.playback.current_track
self.backend.current_playlist.append([self.tracks[2]]) self.backend.current_playlist.append([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, current_track) self.assertEqual(self.playback.current_track, current_track)
@populate_playlist @populate_playlist
def test_on_current_playlist_change_when_stopped(self): def test_on_current_playlist_change_when_stopped(self):
self.backend.current_playlist.append([self.tracks[2]]) self.backend.current_playlist.append([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.current_track, None)
@populate_playlist @populate_playlist
@ -549,26 +550,26 @@ class PlaybackControllerTest(object):
self.playback.pause() self.playback.pause()
current_track = self.playback.current_track current_track = self.playback.current_track
self.backend.current_playlist.append([self.tracks[2]]) self.backend.current_playlist.append([self.tracks[2]])
self.assertEqual(self.playback.state, self.backend.playback.PAUSED) self.assertEqual(self.playback.state, PlaybackState.PAUSED)
self.assertEqual(self.playback.current_track, current_track) self.assertEqual(self.playback.current_track, current_track)
@populate_playlist @populate_playlist
def test_pause_when_stopped(self): def test_pause_when_stopped(self):
self.playback.pause() self.playback.pause()
self.assertEqual(self.playback.state, self.playback.PAUSED) self.assertEqual(self.playback.state, PlaybackState.PAUSED)
@populate_playlist @populate_playlist
def test_pause_when_playing(self): def test_pause_when_playing(self):
self.playback.play() self.playback.play()
self.playback.pause() self.playback.pause()
self.assertEqual(self.playback.state, self.playback.PAUSED) self.assertEqual(self.playback.state, PlaybackState.PAUSED)
@populate_playlist @populate_playlist
def test_pause_when_paused(self): def test_pause_when_paused(self):
self.playback.play() self.playback.play()
self.playback.pause() self.playback.pause()
self.playback.pause() self.playback.pause()
self.assertEqual(self.playback.state, self.playback.PAUSED) self.assertEqual(self.playback.state, PlaybackState.PAUSED)
@populate_playlist @populate_playlist
def test_pause_return_value(self): def test_pause_return_value(self):
@ -578,20 +579,20 @@ class PlaybackControllerTest(object):
@populate_playlist @populate_playlist
def test_resume_when_stopped(self): def test_resume_when_stopped(self):
self.playback.resume() self.playback.resume()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_resume_when_playing(self): def test_resume_when_playing(self):
self.playback.play() self.playback.play()
self.playback.resume() self.playback.resume()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_playlist @populate_playlist
def test_resume_when_paused(self): def test_resume_when_paused(self):
self.playback.play() self.playback.play()
self.playback.pause() self.playback.pause()
self.playback.resume() self.playback.resume()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_playlist @populate_playlist
def test_resume_return_value(self): def test_resume_return_value(self):
@ -624,12 +625,12 @@ class PlaybackControllerTest(object):
def test_seek_on_empty_playlist_updates_position(self): def test_seek_on_empty_playlist_updates_position(self):
self.playback.seek(0) self.playback.seek(0)
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_seek_when_stopped_triggers_play(self): def test_seek_when_stopped_triggers_play(self):
self.playback.seek(0) self.playback.seek(0)
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_playlist @populate_playlist
def test_seek_when_playing(self): def test_seek_when_playing(self):
@ -666,7 +667,7 @@ class PlaybackControllerTest(object):
self.playback.play() self.playback.play()
self.playback.pause() self.playback.pause()
self.playback.seek(0) self.playback.seek(0)
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@unittest.SkipTest @unittest.SkipTest
@populate_playlist @populate_playlist
@ -686,7 +687,7 @@ class PlaybackControllerTest(object):
def test_seek_beyond_end_of_song_for_last_track(self): def test_seek_beyond_end_of_song_for_last_track(self):
self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.play(self.current_playlist.cp_tracks[-1])
self.playback.seek(self.current_playlist.tracks[-1].length * 100) self.playback.seek(self.current_playlist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@unittest.SkipTest @unittest.SkipTest
@populate_playlist @populate_playlist
@ -702,25 +703,25 @@ class PlaybackControllerTest(object):
self.playback.seek(-1000) self.playback.seek(-1000)
position = self.playback.time_position position = self.playback.time_position
self.assert_(position >= 0, position) self.assert_(position >= 0, position)
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_playlist @populate_playlist
def test_stop_when_stopped(self): def test_stop_when_stopped(self):
self.playback.stop() self.playback.stop()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_stop_when_playing(self): def test_stop_when_playing(self):
self.playback.play() self.playback.play()
self.playback.stop() self.playback.stop()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_playlist @populate_playlist
def test_stop_when_paused(self): def test_stop_when_paused(self):
self.playback.play() self.playback.play()
self.playback.pause() self.playback.pause()
self.playback.stop() self.playback.stop()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_stop_return_value(self): def test_stop_return_value(self):
self.playback.play() self.playback.play()
@ -729,7 +730,7 @@ class PlaybackControllerTest(object):
def test_time_position_when_stopped(self): def test_time_position_when_stopped(self):
future = mock.Mock() future = mock.Mock()
future.get = mock.Mock(return_value=0) future.get = mock.Mock(return_value=0)
self.backend.gstreamer.get_position = mock.Mock(return_value=future) self.backend.audio.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0) self.assertEqual(self.playback.time_position, 0)
@ -737,7 +738,7 @@ class PlaybackControllerTest(object):
def test_time_position_when_stopped_with_playlist(self): def test_time_position_when_stopped_with_playlist(self):
future = mock.Mock() future = mock.Mock()
future.get = mock.Mock(return_value=0) future.get = mock.Mock(return_value=0)
self.backend.gstreamer.get_position = mock.Mock(return_value=future) self.backend.audio.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0) self.assertEqual(self.playback.time_position, 0)
@ -810,7 +811,7 @@ class PlaybackControllerTest(object):
def test_end_of_playlist_stops(self): def test_end_of_playlist_stops(self):
self.playback.play(self.current_playlist.cp_tracks[-1]) self.playback.play(self.current_playlist.cp_tracks[-1])
self.playback.on_end_of_track() self.playback.on_end_of_track()
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_repeat_off_by_default(self): def test_repeat_off_by_default(self):
self.assertEqual(self.playback.repeat, False) self.assertEqual(self.playback.repeat, False)
@ -835,9 +836,9 @@ class PlaybackControllerTest(object):
for _ in self.tracks: for _ in self.tracks:
self.playback.next() self.playback.next()
self.assertNotEqual(self.playback.track_at_next, None) self.assertNotEqual(self.playback.track_at_next, None)
self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_playlist @populate_playlist
def test_random_until_end_of_playlist_with_repeat(self): def test_random_until_end_of_playlist_with_repeat(self):
@ -854,7 +855,7 @@ class PlaybackControllerTest(object):
self.playback.play() self.playback.play()
played = [] played = []
for _ in self.tracks: for _ in self.tracks:
self.assert_(self.playback.current_track not in played) self.assertNotIn(self.playback.current_track, played)
played.append(self.playback.current_track) played.append(self.playback.current_track)
self.playback.next() self.playback.next()

View File

@ -30,7 +30,7 @@ class StoredPlaylistsControllerTest(object):
def test_create_in_playlists(self): def test_create_in_playlists(self):
playlist = self.stored.create('test') playlist = self.stored.create('test')
self.assert_(self.stored.playlists) self.assert_(self.stored.playlists)
self.assert_(playlist in self.stored.playlists) self.assertIn(playlist, self.stored.playlists)
def test_playlists_empty_to_start_with(self): def test_playlists_empty_to_start_with(self):
self.assert_(not self.stored.playlists) self.assert_(not self.stored.playlists)
@ -101,7 +101,7 @@ class StoredPlaylistsControllerTest(object):
# FIXME should we handle playlists without names? # FIXME should we handle playlists without names?
playlist = Playlist(name='test') playlist = Playlist(name='test')
self.stored.save(playlist) self.stored.save(playlist)
self.assert_(playlist in self.stored.playlists) self.assertIn(playlist, self.stored.playlists)
@unittest.SkipTest @unittest.SkipTest
def test_playlist_with_unknown_track(self): def test_playlist_with_unknown_track(self):

View File

@ -2,6 +2,7 @@ import sys
from mopidy import settings from mopidy import settings
from mopidy.backends.local import LocalBackend from mopidy.backends.local import LocalBackend
from mopidy.core import PlaybackState
from mopidy.models import Track from mopidy.models import Track
from mopidy.utils.path import path_to_uri from mopidy.utils.path import path_to_uri
@ -34,19 +35,19 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
self.backend.current_playlist.add(track) self.backend.current_playlist.add(track)
def test_uri_scheme(self): def test_uri_scheme(self):
self.assert_('file' in self.backend.uri_schemes) self.assertIn('file', self.backend.uri_schemes)
def test_play_mp3(self): def test_play_mp3(self):
self.add_track('blank.mp3') self.add_track('blank.mp3')
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
def test_play_ogg(self): def test_play_ogg(self):
self.add_track('blank.ogg') self.add_track('blank.ogg')
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)
def test_play_flac(self): def test_play_flac(self):
self.add_track('blank.flac') self.add_track('blank.flac')
self.playback.play() self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING) self.assertEqual(self.playback.state, PlaybackState.PLAYING)

0
tests/core/__init__.py Normal file
View File

View File

@ -37,7 +37,7 @@ class MpdDispatcherTest(unittest.TestCase):
expected_handler 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.assertEqual(handler, expected_handler)
self.assert_('arg1' in kwargs) self.assertIn('arg1', kwargs)
self.assertEqual(kwargs['arg1'], 'an_arg') self.assertEqual(kwargs['arg1'], 'an_arg')
def test_handling_unknown_request_yields_error(self): def test_handling_unknown_request_yields_error(self):
@ -48,5 +48,5 @@ class MpdDispatcherTest(unittest.TestCase):
expected = 'magic' expected = 'magic'
request_handlers['known request'] = lambda x: expected request_handlers['known request'] = lambda x: expected
result = self.dispatcher.handle_request('known request') result = self.dispatcher.handle_request('known request')
self.assert_(u'OK' in result) self.assertIn(u'OK', result)
self.assert_(expected in result) self.assertIn(expected, result)

View File

@ -42,7 +42,7 @@ class BaseTestCase(unittest.TestCase):
self.assertEqual([], self.connection.response) self.assertEqual([], self.connection.response)
def assertInResponse(self, value): def assertInResponse(self, value):
self.assert_(value in self.connection.response, u'Did not find %s ' self.assertIn(value, self.connection.response, u'Did not find %s '
'in %s' % (repr(value), repr(self.connection.response))) 'in %s' % (repr(value), repr(self.connection.response)))
def assertOnceInResponse(self, value): def assertOnceInResponse(self, value):
@ -51,7 +51,7 @@ class BaseTestCase(unittest.TestCase):
(repr(value), repr(self.connection.response))) (repr(value), repr(self.connection.response)))
def assertNotInResponse(self, value): def assertNotInResponse(self, value):
self.assert_(value not in self.connection.response, u'Found %s in %s' % self.assertNotIn(value, self.connection.response, u'Found %s in %s' %
(repr(value), repr(self.connection.response))) (repr(value), repr(self.connection.response)))
def assertEqualResponse(self, value): def assertEqualResponse(self, value):

View File

@ -21,7 +21,7 @@ class CommandListsTest(protocol.BaseTestCase):
self.assertEqual([], self.dispatcher.command_list) self.assertEqual([], self.dispatcher.command_list)
self.assertEqual(False, self.dispatcher.command_list_ok) self.assertEqual(False, self.dispatcher.command_list_ok)
self.sendRequest(u'ping') self.sendRequest(u'ping')
self.assert_(u'ping' in self.dispatcher.command_list) self.assertIn(u'ping', self.dispatcher.command_list)
self.sendRequest(u'command_list_end') self.sendRequest(u'command_list_end')
self.assertInResponse(u'OK') self.assertInResponse(u'OK')
self.assertEqual(False, self.dispatcher.command_list) self.assertEqual(False, self.dispatcher.command_list)
@ -42,7 +42,7 @@ class CommandListsTest(protocol.BaseTestCase):
self.assertEqual([], self.dispatcher.command_list) self.assertEqual([], self.dispatcher.command_list)
self.assertEqual(True, self.dispatcher.command_list_ok) self.assertEqual(True, self.dispatcher.command_list_ok)
self.sendRequest(u'ping') self.sendRequest(u'ping')
self.assert_(u'ping' in self.dispatcher.command_list) self.assertIn(u'ping', self.dispatcher.command_list)
self.sendRequest(u'command_list_end') self.sendRequest(u'command_list_end')
self.assertInResponse(u'list_OK') self.assertInResponse(u'list_OK')
self.assertInResponse(u'OK') self.assertInResponse(u'OK')

View File

@ -1,12 +1,13 @@
from mopidy.backends import base as backend from mopidy.core import PlaybackState
from mopidy.models import Track from mopidy.models import Track
from tests import unittest from tests import unittest
from tests.frontends.mpd import protocol from tests.frontends.mpd import protocol
PAUSED = backend.PlaybackController.PAUSED
PLAYING = backend.PlaybackController.PLAYING PAUSED = PlaybackState.PAUSED
STOPPED = backend.PlaybackController.STOPPED PLAYING = PlaybackState.PLAYING
STOPPED = PlaybackState.STOPPED
class PlaybackOptionsHandlerTest(protocol.BaseTestCase): class PlaybackOptionsHandlerTest(protocol.BaseTestCase):

View File

@ -31,66 +31,66 @@ class TrackMpdFormatTest(unittest.TestCase):
def test_track_to_mpd_format_for_empty_track(self): def test_track_to_mpd_format_for_empty_track(self):
result = translator.track_to_mpd_format(Track()) result = translator.track_to_mpd_format(Track())
self.assert_(('file', '') in result) self.assertIn(('file', ''), result)
self.assert_(('Time', 0) in result) self.assertIn(('Time', 0), result)
self.assert_(('Artist', '') in result) self.assertIn(('Artist', ''), result)
self.assert_(('Title', '') in result) self.assertIn(('Title', ''), result)
self.assert_(('Album', '') in result) self.assertIn(('Album', ''), result)
self.assert_(('Track', 0) in result) self.assertIn(('Track', 0), result)
self.assert_(('Date', '') in result) self.assertIn(('Date', ''), result)
self.assertEqual(len(result), 7) self.assertEqual(len(result), 7)
def test_track_to_mpd_format_with_position(self): def test_track_to_mpd_format_with_position(self):
result = translator.track_to_mpd_format(Track(), position=1) result = translator.track_to_mpd_format(Track(), position=1)
self.assert_(('Pos', 1) not in result) self.assertNotIn(('Pos', 1), result)
def test_track_to_mpd_format_with_cpid(self): def test_track_to_mpd_format_with_cpid(self):
result = translator.track_to_mpd_format(CpTrack(1, Track())) result = translator.track_to_mpd_format(CpTrack(1, Track()))
self.assert_(('Id', 1) not in result) self.assertNotIn(('Id', 1), result)
def test_track_to_mpd_format_with_position_and_cpid(self): def test_track_to_mpd_format_with_position_and_cpid(self):
result = translator.track_to_mpd_format(CpTrack(2, Track()), position=1) result = translator.track_to_mpd_format(CpTrack(2, Track()), position=1)
self.assert_(('Pos', 1) in result) self.assertIn(('Pos', 1), result)
self.assert_(('Id', 2) in result) self.assertIn(('Id', 2), result)
def test_track_to_mpd_format_for_nonempty_track(self): def test_track_to_mpd_format_for_nonempty_track(self):
result = translator.track_to_mpd_format( result = translator.track_to_mpd_format(
CpTrack(122, self.track), position=9) CpTrack(122, self.track), position=9)
self.assert_(('file', 'a uri') in result) self.assertIn(('file', 'a uri'), result)
self.assert_(('Time', 137) in result) self.assertIn(('Time', 137), result)
self.assert_(('Artist', 'an artist') in result) self.assertIn(('Artist', 'an artist'), result)
self.assert_(('Title', 'a name') in result) self.assertIn(('Title', 'a name'), result)
self.assert_(('Album', 'an album') in result) self.assertIn(('Album', 'an album'), result)
self.assert_(('AlbumArtist', 'an other artist') in result) self.assertIn(('AlbumArtist', 'an other artist'), result)
self.assert_(('Track', '7/13') in result) self.assertIn(('Track', '7/13'), result)
self.assert_(('Date', datetime.date(1977, 1, 1)) in result) self.assertIn(('Date', datetime.date(1977, 1, 1)), result)
self.assert_(('Pos', 9) in result) self.assertIn(('Pos', 9), result)
self.assert_(('Id', 122) in result) self.assertIn(('Id', 122), result)
self.assertEqual(len(result), 10) self.assertEqual(len(result), 10)
def test_track_to_mpd_format_musicbrainz_trackid(self): def test_track_to_mpd_format_musicbrainz_trackid(self):
track = self.track.copy(musicbrainz_id='foo') track = self.track.copy(musicbrainz_id='foo')
result = translator.track_to_mpd_format(track) result = translator.track_to_mpd_format(track)
self.assert_(('MUSICBRAINZ_TRACKID', 'foo') in result) self.assertIn(('MUSICBRAINZ_TRACKID', 'foo'), result)
def test_track_to_mpd_format_musicbrainz_albumid(self): def test_track_to_mpd_format_musicbrainz_albumid(self):
album = self.track.album.copy(musicbrainz_id='foo') album = self.track.album.copy(musicbrainz_id='foo')
track = self.track.copy(album=album) track = self.track.copy(album=album)
result = translator.track_to_mpd_format(track) result = translator.track_to_mpd_format(track)
self.assert_(('MUSICBRAINZ_ALBUMID', 'foo') in result) self.assertIn(('MUSICBRAINZ_ALBUMID', 'foo'), result)
def test_track_to_mpd_format_musicbrainz_albumid(self): def test_track_to_mpd_format_musicbrainz_albumid(self):
artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') artist = list(self.track.artists)[0].copy(musicbrainz_id='foo')
album = self.track.album.copy(artists=[artist]) album = self.track.album.copy(artists=[artist])
track = self.track.copy(album=album) track = self.track.copy(album=album)
result = translator.track_to_mpd_format(track) result = translator.track_to_mpd_format(track)
self.assert_(('MUSICBRAINZ_ALBUMARTISTID', 'foo') in result) self.assertIn(('MUSICBRAINZ_ALBUMARTISTID', 'foo'), result)
def test_track_to_mpd_format_musicbrainz_artistid(self): def test_track_to_mpd_format_musicbrainz_artistid(self):
artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') artist = list(self.track.artists)[0].copy(musicbrainz_id='foo')
track = self.track.copy(artists=[artist]) track = self.track.copy(artists=[artist])
result = translator.track_to_mpd_format(track) result = translator.track_to_mpd_format(track)
self.assert_(('MUSICBRAINZ_ARTISTID', 'foo') in result) self.assertIn(('MUSICBRAINZ_ARTISTID', 'foo'), result)
def test_artists_to_mpd_format(self): def test_artists_to_mpd_format(self):
artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')] artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')]

View File

@ -1,13 +1,15 @@
from mopidy.backends import dummy as backend from mopidy.backends import dummy
from mopidy.core import PlaybackState
from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd import dispatcher
from mopidy.frontends.mpd.protocol import status from mopidy.frontends.mpd.protocol import status
from mopidy.models import Track from mopidy.models import Track
from tests import unittest from tests import unittest
PAUSED = backend.PlaybackController.PAUSED
PLAYING = backend.PlaybackController.PLAYING PAUSED = PlaybackState.PAUSED
STOPPED = backend.PlaybackController.STOPPED PLAYING = PlaybackState.PLAYING
STOPPED = PlaybackState.STOPPED
# FIXME migrate to using protocol.BaseTestCase instead of status.stats # FIXME migrate to using protocol.BaseTestCase instead of status.stats
# directly? # directly?
@ -15,7 +17,7 @@ STOPPED = backend.PlaybackController.STOPPED
class StatusHandlerTest(unittest.TestCase): class StatusHandlerTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.backend = backend.DummyBackend.start().proxy() self.backend = dummy.DummyBackend.start().proxy()
self.dispatcher = dispatcher.MpdDispatcher() self.dispatcher = dispatcher.MpdDispatcher()
self.context = self.dispatcher.context self.context = self.dispatcher.context
@ -24,123 +26,123 @@ class StatusHandlerTest(unittest.TestCase):
def test_stats_method(self): def test_stats_method(self):
result = status.stats(self.context) result = status.stats(self.context)
self.assert_('artists' in result) self.assertIn('artists', result)
self.assert_(int(result['artists']) >= 0) self.assert_(int(result['artists']) >= 0)
self.assert_('albums' in result) self.assertIn('albums', result)
self.assert_(int(result['albums']) >= 0) self.assert_(int(result['albums']) >= 0)
self.assert_('songs' in result) self.assertIn('songs', result)
self.assert_(int(result['songs']) >= 0) self.assert_(int(result['songs']) >= 0)
self.assert_('uptime' in result) self.assertIn('uptime', result)
self.assert_(int(result['uptime']) >= 0) self.assert_(int(result['uptime']) >= 0)
self.assert_('db_playtime' in result) self.assertIn('db_playtime', result)
self.assert_(int(result['db_playtime']) >= 0) self.assert_(int(result['db_playtime']) >= 0)
self.assert_('db_update' in result) self.assertIn('db_update', result)
self.assert_(int(result['db_update']) >= 0) self.assert_(int(result['db_update']) >= 0)
self.assert_('playtime' in result) self.assertIn('playtime', result)
self.assert_(int(result['playtime']) >= 0) self.assert_(int(result['playtime']) >= 0)
def test_status_method_contains_volume_with_na_value(self): def test_status_method_contains_volume_with_na_value(self):
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('volume' in result) self.assertIn('volume', result)
self.assertEqual(int(result['volume']), -1) self.assertEqual(int(result['volume']), -1)
def test_status_method_contains_volume(self): def test_status_method_contains_volume(self):
self.backend.playback.volume = 17 self.backend.playback.volume = 17
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('volume' in result) self.assertIn('volume', result)
self.assertEqual(int(result['volume']), 17) self.assertEqual(int(result['volume']), 17)
def test_status_method_contains_repeat_is_0(self): def test_status_method_contains_repeat_is_0(self):
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('repeat' in result) self.assertIn('repeat', result)
self.assertEqual(int(result['repeat']), 0) self.assertEqual(int(result['repeat']), 0)
def test_status_method_contains_repeat_is_1(self): def test_status_method_contains_repeat_is_1(self):
self.backend.playback.repeat = 1 self.backend.playback.repeat = 1
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('repeat' in result) self.assertIn('repeat', result)
self.assertEqual(int(result['repeat']), 1) self.assertEqual(int(result['repeat']), 1)
def test_status_method_contains_random_is_0(self): def test_status_method_contains_random_is_0(self):
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('random' in result) self.assertIn('random', result)
self.assertEqual(int(result['random']), 0) self.assertEqual(int(result['random']), 0)
def test_status_method_contains_random_is_1(self): def test_status_method_contains_random_is_1(self):
self.backend.playback.random = 1 self.backend.playback.random = 1
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('random' in result) self.assertIn('random', result)
self.assertEqual(int(result['random']), 1) self.assertEqual(int(result['random']), 1)
def test_status_method_contains_single(self): def test_status_method_contains_single(self):
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('single' in result) self.assertIn('single', result)
self.assert_(int(result['single']) in (0, 1)) self.assertIn(int(result['single']), (0, 1))
def test_status_method_contains_consume_is_0(self): def test_status_method_contains_consume_is_0(self):
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('consume' in result) self.assertIn('consume', result)
self.assertEqual(int(result['consume']), 0) self.assertEqual(int(result['consume']), 0)
def test_status_method_contains_consume_is_1(self): def test_status_method_contains_consume_is_1(self):
self.backend.playback.consume = 1 self.backend.playback.consume = 1
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('consume' in result) self.assertIn('consume', result)
self.assertEqual(int(result['consume']), 1) self.assertEqual(int(result['consume']), 1)
def test_status_method_contains_playlist(self): def test_status_method_contains_playlist(self):
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('playlist' in result) self.assertIn('playlist', result)
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1)) self.assertIn(int(result['playlist']), xrange(0, 2**31 - 1))
def test_status_method_contains_playlistlength(self): def test_status_method_contains_playlistlength(self):
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('playlistlength' in result) self.assertIn('playlistlength', result)
self.assert_(int(result['playlistlength']) >= 0) self.assert_(int(result['playlistlength']) >= 0)
def test_status_method_contains_xfade(self): def test_status_method_contains_xfade(self):
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('xfade' in result) self.assertIn('xfade', result)
self.assert_(int(result['xfade']) >= 0) self.assert_(int(result['xfade']) >= 0)
def test_status_method_contains_state_is_play(self): def test_status_method_contains_state_is_play(self):
self.backend.playback.state = PLAYING self.backend.playback.state = PLAYING
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('state' in result) self.assertIn('state', result)
self.assertEqual(result['state'], 'play') self.assertEqual(result['state'], 'play')
def test_status_method_contains_state_is_stop(self): def test_status_method_contains_state_is_stop(self):
self.backend.playback.state = STOPPED self.backend.playback.state = STOPPED
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('state' in result) self.assertIn('state', result)
self.assertEqual(result['state'], 'stop') self.assertEqual(result['state'], 'stop')
def test_status_method_contains_state_is_pause(self): def test_status_method_contains_state_is_pause(self):
self.backend.playback.state = PLAYING self.backend.playback.state = PLAYING
self.backend.playback.state = PAUSED self.backend.playback.state = PAUSED
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('state' in result) self.assertIn('state', result)
self.assertEqual(result['state'], 'pause') self.assertEqual(result['state'], 'pause')
def test_status_method_when_playlist_loaded_contains_song(self): def test_status_method_when_playlist_loaded_contains_song(self):
self.backend.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
self.backend.playback.play() self.backend.playback.play()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('song' in result) self.assertIn('song', result)
self.assert_(int(result['song']) >= 0) self.assert_(int(result['song']) >= 0)
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self): def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
self.backend.current_playlist.append([Track()]) self.backend.current_playlist.append([Track()])
self.backend.playback.play() self.backend.playback.play()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('songid' in result) self.assertIn('songid', result)
self.assertEqual(int(result['songid']), 0) self.assertEqual(int(result['songid']), 0)
def test_status_method_when_playing_contains_time_with_no_length(self): def test_status_method_when_playing_contains_time_with_no_length(self):
self.backend.current_playlist.append([Track(length=None)]) self.backend.current_playlist.append([Track(length=None)])
self.backend.playback.play() self.backend.playback.play()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('time' in result) self.assertIn('time', result)
(position, total) = result['time'].split(':') (position, total) = result['time'].split(':')
position = int(position) position = int(position)
total = int(total) total = int(total)
@ -150,7 +152,7 @@ class StatusHandlerTest(unittest.TestCase):
self.backend.current_playlist.append([Track(length=10000)]) self.backend.current_playlist.append([Track(length=10000)])
self.backend.playback.play() self.backend.playback.play()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('time' in result) self.assertIn('time', result)
(position, total) = result['time'].split(':') (position, total) = result['time'].split(':')
position = int(position) position = int(position)
total = int(total) total = int(total)
@ -160,19 +162,19 @@ class StatusHandlerTest(unittest.TestCase):
self.backend.playback.state = PAUSED self.backend.playback.state = PAUSED
self.backend.playback.play_time_accumulated = 59123 self.backend.playback.play_time_accumulated = 59123
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('elapsed' in result) self.assertIn('elapsed', result)
self.assertEqual(result['elapsed'], '59.123') self.assertEqual(result['elapsed'], '59.123')
def test_status_method_when_starting_playing_contains_elapsed_zero(self): def test_status_method_when_starting_playing_contains_elapsed_zero(self):
self.backend.playback.state = PAUSED self.backend.playback.state = PAUSED
self.backend.playback.play_time_accumulated = 123 # Less than 1000ms self.backend.playback.play_time_accumulated = 123 # Less than 1000ms
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('elapsed' in result) self.assertIn('elapsed', result)
self.assertEqual(result['elapsed'], '0.123') self.assertEqual(result['elapsed'], '0.123')
def test_status_method_when_playing_contains_bitrate(self): def test_status_method_when_playing_contains_bitrate(self):
self.backend.current_playlist.append([Track(bitrate=320)]) self.backend.current_playlist.append([Track(bitrate=320)])
self.backend.playback.play() self.backend.playback.play()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assert_('bitrate' in result) self.assertIn('bitrate', result)
self.assertEqual(int(result['bitrate']), 320) self.assertEqual(int(result['bitrate']), 320)

View File

@ -4,7 +4,7 @@ import mock
from mopidy import OptionalDependencyError from mopidy import OptionalDependencyError
from mopidy.backends.dummy import DummyBackend from mopidy.backends.dummy import DummyBackend
from mopidy.backends.base.playback import PlaybackController from mopidy.core import PlaybackState
from mopidy.models import Album, Artist, Track from mopidy.models import Album, Artist, Track
try: try:
@ -14,9 +14,9 @@ except OptionalDependencyError:
from tests import unittest from tests import unittest
PLAYING = PlaybackController.PLAYING PLAYING = PlaybackState.PLAYING
PAUSED = PlaybackController.PAUSED PAUSED = PlaybackState.PAUSED
STOPPED = PlaybackController.STOPPED STOPPED = PlaybackState.STOPPED
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') @unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')
@ -141,7 +141,7 @@ class PlayerInterfaceTest(unittest.TestCase):
def test_get_metadata_has_trackid_even_when_no_current_track(self): def test_get_metadata_has_trackid_even_when_no_current_track(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assert_('mpris:trackid' in result.keys()) self.assertIn('mpris:trackid', result.keys())
self.assertEquals(result['mpris:trackid'], '') self.assertEquals(result['mpris:trackid'], '')
def test_get_metadata_has_trackid_based_on_cpid(self): def test_get_metadata_has_trackid_based_on_cpid(self):

View File

@ -13,18 +13,18 @@ class HelpTest(unittest.TestCase):
args = [sys.executable, mopidy_dir, '--help'] args = [sys.executable, mopidy_dir, '--help']
process = subprocess.Popen(args, stdout=subprocess.PIPE) process = subprocess.Popen(args, stdout=subprocess.PIPE)
output = process.communicate()[0] output = process.communicate()[0]
self.assert_('--version' in output) self.assertIn('--version', output)
self.assert_('--help' in output) self.assertIn('--help', output)
self.assert_('--help-gst' in output) self.assertIn('--help-gst', output)
self.assert_('--interactive' in output) self.assertIn('--interactive', output)
self.assert_('--quiet' in output) self.assertIn('--quiet', output)
self.assert_('--verbose' in output) self.assertIn('--verbose', output)
self.assert_('--save-debug-log' in output) self.assertIn('--save-debug-log', output)
self.assert_('--list-settings' in output) self.assertIn('--list-settings', output)
def test_help_gst_has_gstreamer_options(self): def test_help_gst_has_gstreamer_options(self):
mopidy_dir = os.path.dirname(mopidy.__file__) mopidy_dir = os.path.dirname(mopidy.__file__)
args = [sys.executable, mopidy_dir, '--help-gst'] args = [sys.executable, mopidy_dir, '--help-gst']
process = subprocess.Popen(args, stdout=subprocess.PIPE) process = subprocess.Popen(args, stdout=subprocess.PIPE)
output = process.communicate()[0] output = process.communicate()[0]
self.assert_('--gst-version' in output) self.assertIn('--gst-version', output)

View File

@ -43,7 +43,7 @@ class GenericCopyTets(unittest.TestCase):
artist2 = Artist(name='bar') artist2 = Artist(name='bar')
track = Track(artists=[artist1]) track = Track(artists=[artist1])
copy = track.copy(artists=[artist2]) copy = track.copy(artists=[artist2])
self.assert_(artist2 in copy.artists) self.assertIn(artist2, copy.artists)
def test_copying_track_with_invalid_key(self): def test_copying_track_with_invalid_key(self):
test = lambda: Track().copy(invalid_key=True) test = lambda: Track().copy(invalid_key=True)
@ -155,7 +155,7 @@ class AlbumTest(unittest.TestCase):
def test_artists(self): def test_artists(self):
artist = Artist() artist = Artist()
album = Album(artists=[artist]) album = Album(artists=[artist])
self.assert_(artist in album.artists) self.assertIn(artist, album.artists)
self.assertRaises(AttributeError, setattr, album, 'artists', None) self.assertRaises(AttributeError, setattr, album, 'artists', None)
def test_num_tracks(self): def test_num_tracks(self):
@ -338,7 +338,7 @@ class TrackTest(unittest.TestCase):
self.assertRaises(AttributeError, setattr, track, 'track_no', None) self.assertRaises(AttributeError, setattr, track, 'track_no', None)
def test_date(self): def test_date(self):
date = datetime.date(1977, 1, 1) date = '1977-01-01'
track = Track(date=date) track = Track(date=date)
self.assertEqual(track.date, date) self.assertEqual(track.date, date)
self.assertRaises(AttributeError, setattr, track, 'date', None) self.assertRaises(AttributeError, setattr, track, 'date', None)
@ -434,7 +434,7 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2)) self.assertEqual(hash(track1), hash(track2))
def test_eq_date(self): def test_eq_date(self):
date = datetime.date.today() date = '1977-01-01'
track1 = Track(date=date) track1 = Track(date=date)
track2 = Track(date=date) track2 = Track(date=date)
self.assertEqual(track1, track2) self.assertEqual(track1, track2)
@ -459,7 +459,7 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2)) self.assertEqual(hash(track1), hash(track2))
def test_eq(self): def test_eq(self):
date = datetime.date.today() date = '1977-01-01'
artists = [Artist()] artists = [Artist()]
album = Album() album = Album()
track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album, track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
@ -508,8 +508,8 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(hash(track1), hash(track2)) self.assertNotEqual(hash(track1), hash(track2))
def test_ne_date(self): def test_ne_date(self):
track1 = Track(date=datetime.date.today()) track1 = Track(date='1977-01-01')
track2 = Track(date=datetime.date.today()-datetime.timedelta(days=1)) track2 = Track(date='1977-01-02')
self.assertNotEqual(track1, track2) self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2)) self.assertNotEqual(hash(track1), hash(track2))
@ -534,12 +534,12 @@ class TrackTest(unittest.TestCase):
def test_ne(self): def test_ne(self):
track1 = Track(uri=u'uri1', name=u'name1', track1 = Track(uri=u'uri1', name=u'name1',
artists=[Artist(name=u'name1')], album=Album(name=u'name1'), artists=[Artist(name=u'name1')], album=Album(name=u'name1'),
track_no=1, date=datetime.date.today(), length=100, bitrate=100, track_no=1, date='1977-01-01', length=100, bitrate=100,
musicbrainz_id='id1') musicbrainz_id='id1')
track2 = Track(uri=u'uri2', name=u'name2', track2 = Track(uri=u'uri2', name=u'name2',
artists=[Artist(name=u'name2')], album=Album(name=u'name2'), artists=[Artist(name=u'name2')], album=Album(name=u'name2'),
track_no=2, date=datetime.date.today()-datetime.timedelta(days=1), track_no=2, date='1977-01-02', length=200, bitrate=200,
length=200, bitrate=200, musicbrainz_id='id2') musicbrainz_id='id2')
self.assertNotEqual(track1, track2) self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2)) self.assertNotEqual(hash(track1), hash(track2))

View File

@ -20,7 +20,7 @@ class GetClassTest(unittest.TestCase):
try: try:
utils.get_class('foo.bar.Baz') utils.get_class('foo.bar.Baz')
except ImportError as e: except ImportError as e:
self.assert_('foo.bar.Baz' in str(e)) self.assertIn('foo.bar.Baz', str(e))
def test_loading_existing_class(self): def test_loading_existing_class(self):
cls = utils.get_class('unittest.TestCase') cls = utils.get_class('unittest.TestCase')

View File

@ -107,7 +107,7 @@ class SettingsProxyTest(unittest.TestCase):
def test_setattr_updates_runtime_settings(self): def test_setattr_updates_runtime_settings(self):
self.settings.TEST = 'test' self.settings.TEST = 'test'
self.assert_('TEST' in self.settings.runtime) self.assertIn('TEST', self.settings.runtime)
def test_setattr_updates_runtime_with_value(self): def test_setattr_updates_runtime_with_value(self):
self.settings.TEST = 'test' self.settings.TEST = 'test'
@ -181,34 +181,33 @@ class FormatSettingListTest(unittest.TestCase):
def test_contains_the_setting_name(self): def test_contains_the_setting_name(self):
self.settings.TEST = u'test' self.settings.TEST = u'test'
result = setting_utils.format_settings_list(self.settings) result = setting_utils.format_settings_list(self.settings)
self.assert_('TEST:' in result, result) self.assertIn('TEST:', result, result)
def test_repr_of_a_string_value(self): def test_repr_of_a_string_value(self):
self.settings.TEST = u'test' self.settings.TEST = u'test'
result = setting_utils.format_settings_list(self.settings) result = setting_utils.format_settings_list(self.settings)
self.assert_("TEST: u'test'" in result, result) self.assertIn("TEST: u'test'", result, result)
def test_repr_of_an_int_value(self): def test_repr_of_an_int_value(self):
self.settings.TEST = 123 self.settings.TEST = 123
result = setting_utils.format_settings_list(self.settings) result = setting_utils.format_settings_list(self.settings)
self.assert_("TEST: 123" in result, result) self.assertIn("TEST: 123", result, result)
def test_repr_of_a_tuple_value(self): def test_repr_of_a_tuple_value(self):
self.settings.TEST = (123, u'abc') self.settings.TEST = (123, u'abc')
result = setting_utils.format_settings_list(self.settings) result = setting_utils.format_settings_list(self.settings)
self.assert_("TEST: (123, u'abc')" in result, result) self.assertIn("TEST: (123, u'abc')", result, result)
def test_passwords_are_masked(self): def test_passwords_are_masked(self):
self.settings.TEST_PASSWORD = u'secret' self.settings.TEST_PASSWORD = u'secret'
result = setting_utils.format_settings_list(self.settings) result = setting_utils.format_settings_list(self.settings)
self.assert_("TEST_PASSWORD: u'secret'" not in result, result) self.assertNotIn("TEST_PASSWORD: u'secret'", result, result)
self.assert_("TEST_PASSWORD: u'********'" in result, result) self.assertIn("TEST_PASSWORD: u'********'", result, result)
def test_short_values_are_not_pretty_printed(self): def test_short_values_are_not_pretty_printed(self):
self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',) self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',)
result = setting_utils.format_settings_list(self.settings) result = setting_utils.format_settings_list(self.settings)
self.assert_("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)" in result, self.assertIn("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)", result)
result)
def test_long_values_are_pretty_printed(self): def test_long_values_are_pretty_printed(self):
self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend', self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',

View File

@ -31,10 +31,10 @@ class VersionTest(unittest.TestCase):
self.assert_(SV(__version__) < SV('0.8.0')) self.assert_(SV(__version__) < SV('0.8.0'))
def test_get_platform_contains_platform(self): def test_get_platform_contains_platform(self):
self.assert_(platform.platform() in get_platform()) self.assertIn(platform.platform(), get_platform())
def test_get_python_contains_python_implementation(self): def test_get_python_contains_python_implementation(self):
self.assert_(platform.python_implementation() in get_python()) self.assertIn(platform.python_implementation(), get_python())
def test_get_python_contains_python_version(self): def test_get_python_contains_python_version(self):
self.assert_(platform.python_version() in get_python()) self.assertIn(platform.python_version(), get_python())