Merge branch 'develop' into feature/dump-thread-tracebacks
This commit is contained in:
commit
e17e2ea96d
4
.mailmap
Normal file
4
.mailmap
Normal 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
19
docs/api/audio.rst
Normal 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:
|
||||
@ -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
|
||||
create a backend. If you are working on a frontend and need to access the
|
||||
backend, see the :ref:`backend-controller-api`.
|
||||
The backend API is the interface that must be implemented when you create a
|
||||
backend. If you are working on a frontend and need to access the backend, see
|
||||
the :ref:`core-api`.
|
||||
|
||||
|
||||
Playback provider
|
||||
@ -30,8 +30,8 @@ Library provider
|
||||
:members:
|
||||
|
||||
|
||||
Backend provider implementations
|
||||
================================
|
||||
Backend implementations
|
||||
=======================
|
||||
|
||||
* :mod:`mopidy.backends.dummy`
|
||||
* :mod:`mopidy.backends.spotify`
|
||||
@ -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:
|
||||
@ -1,4 +1,4 @@
|
||||
.. _backend-concepts:
|
||||
.. _concepts:
|
||||
|
||||
**********************************************
|
||||
The backend, controller, and provider concepts
|
||||
@ -12,11 +12,11 @@ Controllers:
|
||||
functionality. Most, but not all, controllers delegates some work to one or
|
||||
more providers. The controllers are responsible for choosing the right
|
||||
provider for any given task based upon i.e. the track's URI. See
|
||||
:ref:`backend-controller-api` for more details.
|
||||
:ref:`core-api` for more details.
|
||||
Providers:
|
||||
Anything specific to i.e. Spotify integration or local storage is contained
|
||||
in the providers. To integrate with new music sources, you just add new
|
||||
providers. See :ref:`backend-provider-api` for more details.
|
||||
providers. See :ref:`backend-api` for more details.
|
||||
|
||||
.. digraph:: backend_relations
|
||||
|
||||
50
docs/api/core.rst
Normal file
50
docs/api/core.rst
Normal 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:
|
||||
@ -5,7 +5,10 @@ API reference
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
backends/concepts
|
||||
backends/controllers
|
||||
backends/providers
|
||||
*
|
||||
concepts
|
||||
models
|
||||
backends
|
||||
core
|
||||
audio
|
||||
frontends
|
||||
listeners
|
||||
|
||||
@ -9,6 +9,9 @@ Contributors to Mopidy in the order of appearance:
|
||||
- Thomas Adamcik <adamcik@samfundet.no>
|
||||
- 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
|
||||
=========================
|
||||
@ -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.
|
||||
You can contribute code, documentation, tests, bug reports, or help other
|
||||
users, spreading the word, etc.
|
||||
|
||||
If you want to show your appreciation in a less time consuming way, you can
|
||||
`flattr us <https://flattr.com/thing/82288/Mopidy>`_, or `donate money
|
||||
<http://pledgie.com/campaigns/12647>`_ to Mopidy's development.
|
||||
|
||||
We promise that any money donated -- to Pledgie, not Flattr, due to the size of
|
||||
the amounts -- will be used to cover costs related to Mopidy development, like
|
||||
service subscriptions (Spotify, Last.fm, etc.) and hardware devices like an
|
||||
used iPod Touch for testing Mopidy with MPod.
|
||||
|
||||
|
||||
@ -7,24 +7,7 @@ This change log is used to track all major changes to Mopidy.
|
||||
v0.8 (in development)
|
||||
=====================
|
||||
|
||||
**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.
|
||||
**Audio output and mixer changes**
|
||||
|
||||
- Removed multiple outputs support. Having this feature currently seems to be
|
||||
more trouble than what it is worth. The :attr:`mopidy.settings.OUTPUTS`
|
||||
@ -35,10 +18,10 @@ v0.8 (in development)
|
||||
:issue:`159`)
|
||||
|
||||
- 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
|
||||
value is ``autoaudiomixer``, a custom mixer that attempts to find a mixer that
|
||||
will work on your system. If this picks the wrong mixer you can of course
|
||||
override it. Setting the mixer to :class:`None` is also supported. MPD
|
||||
GStreamer bin with a mixer in it in :attr:`mopidy.settings.MIXER`. The
|
||||
default value is ``autoaudiomixer``, a custom mixer that attempts to find a
|
||||
mixer that will work on your system. If this picks the wrong mixer you can of
|
||||
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
|
||||
no mixer set.
|
||||
|
||||
@ -46,7 +29,7 @@ v0.8 (in development)
|
||||
|
||||
- Updated the NAD hardware mixer to work in the new GStreamer based mixing
|
||||
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_EXT_PORT = u'/dev/ttyUSB0'
|
||||
@ -54,7 +37,7 @@ v0.8 (in development)
|
||||
MIXER_EXT_SPEAKERS_A = u'On'
|
||||
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'
|
||||
|
||||
@ -62,12 +45,41 @@ v0.8 (in development)
|
||||
properties may be left out if you don't want the mixer to adjust the settings
|
||||
on your NAD amplifier when Mopidy is started.
|
||||
|
||||
- Fixed :issue:`150` which caused some clients to block Mopidy completely. Bug
|
||||
was caused by some clients sending ``close`` and then shutting down the
|
||||
connection right away. This trigged a situation in which the connection
|
||||
**Changes**
|
||||
|
||||
- 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
|
||||
event loop, blocking everything else.
|
||||
|
||||
- Fixed crash on lookup of unknown path when using local backend.
|
||||
|
||||
|
||||
v0.7.3 (2012-08-11)
|
||||
===================
|
||||
@ -608,9 +620,9 @@ to this problem.
|
||||
:class:`mopidy.models.Album`, and :class:`mopidy.models.Track`.
|
||||
|
||||
- Prepare for multi-backend support (see :issue:`40`) by introducing the
|
||||
:ref:`provider concept <backend-concepts>`. Split the backend API into a
|
||||
:ref:`backend controller API <backend-controller-api>` (for frontend use)
|
||||
and a :ref:`backend provider API <backend-provider-api>` (for backend
|
||||
:ref:`provider concept <concepts>`. Split the backend API into a
|
||||
:ref:`backend controller API <core-api>` (for frontend use)
|
||||
and a :ref:`backend provider API <backend-api>` (for backend
|
||||
implementation use), which includes the following changes:
|
||||
|
||||
- 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
|
||||
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
|
||||
updated :doc:`release roadmap <development/roadmap>` with our goals for the 0.1
|
||||
to 0.3 releases.
|
||||
updated :doc:`release roadmap <development>` with our goals for the 0.1 to 0.3
|
||||
releases.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
(Fixes: :issue:`3`)
|
||||
- **[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
|
||||
named "Master".
|
||||
|
||||
@ -30,33 +30,13 @@ ncmpcpp
|
||||
A console client that generally works well with Mopidy, and is regularly used
|
||||
by Mopidy developers.
|
||||
|
||||
Search
|
||||
^^^^^^
|
||||
|
||||
Search only works for ncmpcpp versions 0.5.1 and higher, and in two of the
|
||||
three search modes:
|
||||
Search only works in two of the three search modes:
|
||||
|
||||
- "Match if tag contains search phrase (regexes supported)" -- Does not work.
|
||||
The client tries to fetch all known metadata and do the search client side.
|
||||
- "Match if tag contains searched phrase (no regexes)" -- Works.
|
||||
- "Match only if both values are the same" -- Works.
|
||||
|
||||
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
|
||||
=================
|
||||
@ -102,8 +82,8 @@ It generally works well with Mopidy.
|
||||
Android clients
|
||||
===============
|
||||
|
||||
We've tested all six MPD clients we could find for Android with Mopidy 0.3 on a
|
||||
HTC Hero with Android 2.1, using the following test procedure:
|
||||
We've tested all four MPD clients we could find for Android with Mopidy 0.7.3 on
|
||||
a Samsung Galaxy Nexus with Android 4.1.1, using the following test procedure:
|
||||
|
||||
#. Connect to Mopidy
|
||||
#. 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
|
||||
#. 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
|
||||
features tested.
|
||||
- Droid MPD Client works well, but got a couple of bugs one can live with and
|
||||
does not expose stored playlist anywhere.
|
||||
- IcyBeats is not usable yet.
|
||||
- MPDroid is working well and looking good, but does not have search
|
||||
functionality.
|
||||
- PMix is just a lesser MPDroid, so use MPDroid instead.
|
||||
- ThreeMPD is too buggy to even get connected to Mopidy.
|
||||
Combining what we managed to find before the apps crashed with our experience
|
||||
from an older version of this review, using Android 2.1, we can say that:
|
||||
|
||||
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.
|
||||
- If you do not care about stored playlists, use Droid MPD Client.
|
||||
- If you do not care about searching, use MPDroid.
|
||||
- Droid MPD Client was to buggy to get an impression from. Unclear if the bugs
|
||||
are due to the app or that it hasn't been updated for Android 4.x.
|
||||
|
||||
- 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
|
||||
------
|
||||
|
||||
We tested version 1.0.0, which at the time had 1k-5k downloads, <100 ratings,
|
||||
3.5 stars.
|
||||
Test date:
|
||||
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
|
||||
single mode and consume mode. BitMPC crashes if Mopidy is killed or crash.
|
||||
- The user interface lacks some finishing touches. E.g. you can't enter a
|
||||
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
|
||||
----------------
|
||||
|
||||
We tested version 0.4.0, which at the time had 5k-10k downloads, >200 ratings,
|
||||
4 stars.
|
||||
Test date:
|
||||
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
|
||||
manager", then the search tab. I do not understand why search is hidden inside
|
||||
"Playlist manager".
|
||||
- No intutive way to ask the app to connect to the server after adding the
|
||||
server hostname to the settings.
|
||||
|
||||
The user interface have some French remnants, like "Rechercher" in the search
|
||||
field.
|
||||
- To find the search functionality, you have to select the menu,
|
||||
then "Playlist manager", then the search tab. I do not understand why search
|
||||
is hidden inside "Playlist manager".
|
||||
|
||||
When selecting the artist tab, it issues the ``list Artist`` command and
|
||||
becomes stuck waiting for the results. Same thing happens for the album tab,
|
||||
which issues ``list Album``, and the folder tab, which issues ``lsinfo``.
|
||||
Mopidy returned zero hits immediately on all three commands. If Mopidy has
|
||||
loaded your stored playlists and returns more than zero hits on these commands,
|
||||
they artist and album tabs do not hang. The folder tab still freezes when
|
||||
``lsinfo`` returns a list of stored playlists, though zero files. Thus, we've
|
||||
discovered a couple of bugs in Droid MPD Client.
|
||||
- The tabs "Artists" and "Albums" did not contain anything, and did not cause
|
||||
any requests.
|
||||
|
||||
Even though ``lsinfo`` returns the stored playlists for the folder tab, they
|
||||
are not displayed anywhere. Thus, we had to select an album in the album tab to
|
||||
complete the test procedure.
|
||||
- The tab "Folders" showed a spinner and said "Updating data..." but did not
|
||||
send any requests.
|
||||
|
||||
At one point, I had problems turning off repeat mode. After I adjusted the
|
||||
volume and tried again, it worked.
|
||||
- Searching for "foo" did nothing. No request was sent to the server.
|
||||
|
||||
Droid MPD client does not support single mode or consume mode. It does not
|
||||
detect that the server is killed/crashed. You'll only notice it by no actions
|
||||
having any effect, e.g. you can't turn the volume knob any more.
|
||||
- Once, I managed to get a list of stored playlists in the "Search" tab, but I
|
||||
never managed to reproduce this. Opening the stored playlists doesn't work,
|
||||
because Mopidy haven't implemented ``lsinfo "Playlist name"`` (see
|
||||
:issue:`193`).
|
||||
|
||||
In conclusion, some bugs and caveats, but most of the test procedure was
|
||||
possible to perform.
|
||||
- Droid MPD client does not support single mode or consume mode.
|
||||
|
||||
- Not able to complete the test procedure, due to the above problems.
|
||||
|
||||
IcyBeats
|
||||
--------
|
||||
|
||||
We tested version 0.2, which at the time had 50-100 downloads, no ratings.
|
||||
The app was still in beta when we tried it.
|
||||
|
||||
IcyBeats successfully connected to Mopidy and I was able to adjust volume. When
|
||||
I was searching for some tracks, I could not figure out how to actually start
|
||||
the search, as there was no search button and pressing enter in the input field
|
||||
just added a new line. I was stuck. In other words, IcyBeats 0.2 is not usable
|
||||
with Mopidy.
|
||||
|
||||
IcyBeats does have something going for it: IcyBeats uses IPv6 to connect to
|
||||
Mopidy. The future is just around the corner!
|
||||
In conclusion, not a client we can recommend.
|
||||
|
||||
|
||||
MPDroid
|
||||
-------
|
||||
|
||||
We tested version 0.6.9, which at the time had 5k-10k downloads, <200 ratings,
|
||||
4.5 stars. MPDroid started out as a fork of PMix.
|
||||
Test date:
|
||||
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
|
||||
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.
|
||||
- First of all, MPDroid's user interface looks nice.
|
||||
|
||||
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
|
||||
----
|
||||
|
||||
We tested version 0.4.0, which at the time had 10k-50k downloads, >200 ratings,
|
||||
4 stars.
|
||||
Test date:
|
||||
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
|
||||
search either. In addition, I could not find stored playlists. Other than that,
|
||||
I was able to complete the test procedure. PMix crashed once during testing,
|
||||
but handled the killing of Mopidy just as nicely as MPDroid. It does not
|
||||
support single mode or consume mode.
|
||||
- Using Android 4.1.1, PMix, which haven't been updated for 2.5 years, crashes
|
||||
as soon as it connects to Mopidy.
|
||||
|
||||
- Last time we tested the same version of PMix using Android 2.1, we found
|
||||
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.
|
||||
|
||||
|
||||
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:
|
||||
|
||||
iPhone/iPod Touch clients
|
||||
=========================
|
||||
|
||||
impdclient
|
||||
----------
|
||||
|
||||
There's an open source MPD client for iOS called `impdclient
|
||||
<http://code.google.com/p/impdclient/>`_ which has not seen any updates since
|
||||
August 2008. So far, we've not heard of users trying it with Mopidy. Please
|
||||
notify us of your successes and/or problems if you do try it out.
|
||||
|
||||
iOS clients
|
||||
===========
|
||||
|
||||
MPod
|
||||
----
|
||||
|
||||
The `MPoD <http://www.katoemba.net/makesnosenseatall/mpod/>`_ client can be
|
||||
installed from the `iTunes Store
|
||||
Test date:
|
||||
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>`_.
|
||||
|
||||
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
|
||||
through the use of Bonjour. Mopidy does not currently support this, but there
|
||||
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.
|
||||
|
||||
@ -29,6 +29,8 @@ class Mock(object):
|
||||
def __getattr__(self, name):
|
||||
if name in ('__file__', '__path__'):
|
||||
return '/dev/null'
|
||||
elif name[0] == name[0].upper() and not name.startswith('MIXER_TRACK'):
|
||||
return type(name, (), {})
|
||||
else:
|
||||
return Mock()
|
||||
|
||||
@ -51,11 +53,6 @@ MOCK_MODULES = [
|
||||
for mod_name in MOCK_MODULES:
|
||||
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,
|
||||
# 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.
|
||||
@ -94,6 +91,7 @@ copyright = u'2010-2012, Stein Magnus Jodal and contributors'
|
||||
# built documents.
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
from mopidy import get_version
|
||||
release = get_version()
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
|
||||
@ -1,11 +1,42 @@
|
||||
*****************
|
||||
How to contribute
|
||||
*****************
|
||||
***********
|
||||
Development
|
||||
***********
|
||||
|
||||
Development of Mopidy is coordinated through the IRC channel ``#mopidy`` at
|
||||
``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
|
||||
==========
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
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
|
||||
=====================
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
***********
|
||||
Development
|
||||
***********
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
roadmap
|
||||
contributing
|
||||
@ -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.
|
||||
@ -54,7 +54,7 @@ Development documentation
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
development/index
|
||||
development
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
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.
|
||||
|
||||
|
||||
@ -54,15 +54,8 @@ Python bindings on OS X using Homebrew.
|
||||
|
||||
#. Install `Homebrew <https://github.com/mxcl/homebrew>`_.
|
||||
|
||||
#. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``,
|
||||
and ``gst-python``::
|
||||
#. Download our Homebrew formula for ``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 \
|
||||
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
|
||||
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::
|
||||
|
||||
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::
|
||||
|
||||
python --version
|
||||
|
||||
@ -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
|
||||
<http://git-scm.com/>`_. Also, please read our :doc:`developer documentation
|
||||
</development/index>`.
|
||||
</development>`.
|
||||
|
||||
|
||||
From AUR on ArchLinux
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
********************************************
|
||||
:mod:`mopidy.gstreamer` -- GStreamer adapter
|
||||
********************************************
|
||||
|
||||
.. automodule:: mopidy.gstreamer
|
||||
:synopsis: GStreamer adapter
|
||||
:members:
|
||||
@ -166,9 +166,9 @@ server simultaneously. To use the SHOUTcast output, do the following:
|
||||
example, to set the username and password, use:
|
||||
``lame ! shout2send username="foobar" password="s3cret"``.
|
||||
|
||||
Other advanced setups are also possible for outputs. Basically anything you can
|
||||
get a ``gst-lauch`` command to output to can be plugged into
|
||||
:attr:`mopidy.settings.OUTPUT``.
|
||||
Other advanced setups are also possible for outputs. Basically, anything you
|
||||
can use with the ``gst-launch-0.10`` command can be plugged into
|
||||
:attr:`mopidy.settings.OUTPUT`.
|
||||
|
||||
|
||||
Available settings
|
||||
|
||||
@ -30,7 +30,7 @@ sys.path.insert(0,
|
||||
|
||||
from mopidy import (get_version, settings, OptionalDependencyError,
|
||||
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.deps import list_deps_optparse_callback
|
||||
from mopidy.utils.log import setup_logging
|
||||
@ -56,7 +56,7 @@ def main():
|
||||
setup_logging(options.verbosity_level, options.save_debug_log)
|
||||
check_old_folders()
|
||||
setup_settings(options.interactive)
|
||||
setup_gstreamer()
|
||||
setup_audio()
|
||||
setup_backend()
|
||||
setup_frontends()
|
||||
loop.run()
|
||||
@ -70,7 +70,7 @@ def main():
|
||||
loop.quit()
|
||||
stop_frontends()
|
||||
stop_backend()
|
||||
stop_gstreamer()
|
||||
stop_audio()
|
||||
stop_remaining_actors()
|
||||
|
||||
|
||||
@ -122,12 +122,12 @@ def setup_settings(interactive):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def setup_gstreamer():
|
||||
GStreamer.start()
|
||||
def setup_audio():
|
||||
Audio.start()
|
||||
|
||||
|
||||
def stop_gstreamer():
|
||||
stop_actors_by_class(GStreamer)
|
||||
def stop_audio():
|
||||
stop_actors_by_class(Audio)
|
||||
|
||||
def setup_backend():
|
||||
get_class(settings.BACKENDS[0]).start()
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
import gobject
|
||||
|
||||
import logging
|
||||
|
||||
@ -9,12 +10,15 @@ from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import settings, utils
|
||||
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/>`_.
|
||||
|
||||
@ -27,7 +31,8 @@ class GStreamer(ThreadingActor):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(GStreamer, self).__init__()
|
||||
super(Audio, self).__init__()
|
||||
|
||||
self._default_caps = gst.Caps("""
|
||||
audio/x-raw-int,
|
||||
endianness=(int)1234,
|
||||
@ -36,16 +41,29 @@ class GStreamer(ThreadingActor):
|
||||
depth=(int)16,
|
||||
signed=(boolean)true,
|
||||
rate=(int)44100""")
|
||||
|
||||
self._pipeline = None
|
||||
self._source = None
|
||||
self._uridecodebin = None
|
||||
self._output = None
|
||||
self._mixer = None
|
||||
|
||||
self._setup_pipeline()
|
||||
self._setup_output()
|
||||
self._setup_mixer()
|
||||
self._setup_message_processor()
|
||||
self._message_processor_set_up = False
|
||||
|
||||
def on_start(self):
|
||||
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):
|
||||
# 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._pipeline.get_by_name('queue').get_pad('sink'))
|
||||
|
||||
def _teardown_pipeline(self):
|
||||
self._pipeline.set_state(gst.STATE_NULL)
|
||||
|
||||
def _setup_output(self):
|
||||
# This will raise a gobject.GError if the description is bad.
|
||||
self._output = gst.parse_bin_from_description(
|
||||
settings.OUTPUT, ghost_unconnected_pads=True)
|
||||
try:
|
||||
self._output = gst.parse_bin_from_description(
|
||||
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)
|
||||
gst.element_link_many(self._pipeline.get_by_name('queue'),
|
||||
@ -80,8 +106,13 @@ class GStreamer(ThreadingActor):
|
||||
logger.info('Not setting up mixer.')
|
||||
return
|
||||
|
||||
# This will raise a gobject.GError if the description is bad.
|
||||
mixerbin = gst.parse_bin_from_description(settings.MIXER, False)
|
||||
try:
|
||||
mixerbin = gst.parse_bin_from_description(settings.MIXER,
|
||||
ghost_unconnected_pads=False)
|
||||
except gobject.GError as ex:
|
||||
logger.warning('Failed to create mixer "%s": %s',
|
||||
settings.MIXER, ex)
|
||||
return
|
||||
|
||||
# We assume that the bin will contain a single mixer.
|
||||
mixer = mixerbin.get_by_interface('GstMixer')
|
||||
@ -113,10 +144,21 @@ class GStreamer(ThreadingActor):
|
||||
gst.interfaces.MIXER_TRACK_OUTPUT):
|
||||
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):
|
||||
bus = self._pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect('message', self._on_message)
|
||||
self._message_processor_set_up = True
|
||||
|
||||
def _teardown_message_processor(self):
|
||||
if self._message_processor_set_up:
|
||||
bus = self._pipeline.get_bus()
|
||||
bus.remove_signal_watch()
|
||||
|
||||
def _on_new_source(self, element, pad):
|
||||
self._source = element.get_property('source')
|
||||
@ -166,6 +208,8 @@ class GStreamer(ThreadingActor):
|
||||
"""
|
||||
Call this to deliver raw audio data to be played.
|
||||
|
||||
Note that the uri must be set to ``appsrc://`` for this to work.
|
||||
|
||||
:param capabilities: a GStreamer capabilities string
|
||||
:type capabilities: string
|
||||
:param data: raw audio data to be played
|
||||
@ -289,9 +333,14 @@ class GStreamer(ThreadingActor):
|
||||
"""
|
||||
Get volume level of the installed mixer.
|
||||
|
||||
0 == muted.
|
||||
100 == max volume for given system.
|
||||
None == no mixer present, i.e. volume unknown.
|
||||
Example values:
|
||||
|
||||
0:
|
||||
Muted.
|
||||
100:
|
||||
Max volume for given system.
|
||||
:class:`None`:
|
||||
No mixer present, so the volume is unknown.
|
||||
|
||||
:rtype: int in range [0..100] or :class:`None`
|
||||
"""
|
||||
@ -339,7 +388,7 @@ class GStreamer(ThreadingActor):
|
||||
deliver raw audio data to GStreamer.
|
||||
|
||||
:param track: the current track
|
||||
:type track: :class:`mopidy.modes.Track`
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
"""
|
||||
taglist = gst.TagList()
|
||||
artists = [a for a in (track.artists or []) if a.name]
|
||||
@ -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
|
||||
# when mixers use the above code.
|
||||
from mopidy.mixers.auto import AutoAudioMixer
|
||||
from mopidy.mixers.fake import FakeMixer
|
||||
from mopidy.mixers.nad import NadMixer
|
||||
from .auto import AutoAudioMixer
|
||||
from .fake import FakeMixer
|
||||
from .nad import NadMixer
|
||||
@ -5,7 +5,7 @@ import gst
|
||||
|
||||
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?
|
||||
@ -3,7 +3,7 @@ pygst.require('0.10')
|
||||
import gobject
|
||||
import gst
|
||||
|
||||
from mopidy.mixers import create_track
|
||||
from mopidy.audio.mixers import create_track
|
||||
|
||||
|
||||
class FakeMixer(gst.Element, gst.ImplementsInterface, gst.interfaces.Mixer):
|
||||
@ -12,10 +12,10 @@ except ImportError:
|
||||
|
||||
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):
|
||||
@ -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):
|
||||
#: The current playlist controller. An instance of
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
:param backend: backend the controller is a part of
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
:param backend: the backend
|
||||
@ -560,73 +13,75 @@ class BasePlaybackProvider(object):
|
||||
"""
|
||||
Pause playback.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.backend.audio.pause_playback().get()
|
||||
|
||||
def play(self, track):
|
||||
"""
|
||||
Play given track.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param track: the track to play
|
||||
:type track: :class:`mopidy.models.Track`
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.backend.audio.start_playback().get()
|
||||
|
||||
def seek(self, 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
|
||||
:type time_position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.backend.audio.set_position(time_position).get()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop playback.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.backend.audio.stop_playback().get()
|
||||
|
||||
def get_volume(self):
|
||||
"""
|
||||
Get current volume
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:rtype: int [0..100] or :class:`None`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return self.backend.audio.get_volume().get()
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
Get current volume
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
*MAY be reimplemented by subclass.*
|
||||
|
||||
:param: volume
|
||||
:type volume: int [0..100]
|
||||
"""
|
||||
raise NotImplementedError
|
||||
self.backend.audio.set_volume(volume)
|
||||
|
||||
@ -1,120 +1,4 @@
|
||||
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):
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
from pykka.actor import ThreadingActor
|
||||
|
||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||
PlaybackController, BasePlaybackProvider, LibraryController,
|
||||
BaseLibraryProvider, StoredPlaylistsController,
|
||||
BaseStoredPlaylistsProvider)
|
||||
from mopidy import core
|
||||
from mopidy.backends import base
|
||||
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.
|
||||
Used in tests of the frontends.
|
||||
@ -18,24 +16,24 @@ class DummyBackend(ThreadingActor, Backend):
|
||||
def __init__(self, *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)
|
||||
self.library = LibraryController(backend=self,
|
||||
self.library = core.LibraryController(backend=self,
|
||||
provider=library_provider)
|
||||
|
||||
playback_provider = DummyPlaybackProvider(backend=self)
|
||||
self.playback = PlaybackController(backend=self,
|
||||
self.playback = core.PlaybackController(backend=self,
|
||||
provider=playback_provider)
|
||||
|
||||
stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self)
|
||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
||||
self.stored_playlists = core.StoredPlaylistsController(backend=self,
|
||||
provider=stored_playlists_provider)
|
||||
|
||||
self.uri_schemes = [u'dummy']
|
||||
|
||||
|
||||
class DummyLibraryProvider(BaseLibraryProvider):
|
||||
class DummyLibraryProvider(base.BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self.dummy_library = []
|
||||
@ -55,7 +53,7 @@ class DummyLibraryProvider(BaseLibraryProvider):
|
||||
return Playlist()
|
||||
|
||||
|
||||
class DummyPlaybackProvider(BasePlaybackProvider):
|
||||
class DummyPlaybackProvider(base.BasePlaybackProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DummyPlaybackProvider, self).__init__(*args, **kwargs)
|
||||
self._volume = None
|
||||
@ -83,7 +81,7 @@ class DummyPlaybackProvider(BasePlaybackProvider):
|
||||
self._volume = volume
|
||||
|
||||
|
||||
class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
||||
class DummyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
|
||||
def create(self, name):
|
||||
playlist = Playlist(name=name)
|
||||
self._playlists.append(playlist)
|
||||
|
||||
@ -7,13 +7,9 @@ import shutil
|
||||
from pykka.actor import ThreadingActor
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import settings, DATA_PATH
|
||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||
LibraryController, BaseLibraryProvider, PlaybackController,
|
||||
BasePlaybackProvider, StoredPlaylistsController,
|
||||
BaseStoredPlaylistsProvider)
|
||||
from mopidy import audio, core, settings, DATA_PATH
|
||||
from mopidy.backends import base
|
||||
from mopidy.models import Playlist, Track, Album
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
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')
|
||||
|
||||
|
||||
class LocalBackend(ThreadingActor, Backend):
|
||||
class LocalBackend(ThreadingActor, base.Backend):
|
||||
"""
|
||||
A backend for playing music from a local music archive.
|
||||
|
||||
**Issues:** https://github.com/mopidy/mopidy/issues?labels=backend-local
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
@ -47,32 +41,32 @@ class LocalBackend(ThreadingActor, Backend):
|
||||
def __init__(self, *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)
|
||||
self.library = LibraryController(backend=self,
|
||||
self.library = core.LibraryController(backend=self,
|
||||
provider=library_provider)
|
||||
|
||||
playback_provider = LocalPlaybackProvider(backend=self)
|
||||
playback_provider = base.BasePlaybackProvider(backend=self)
|
||||
self.playback = LocalPlaybackController(backend=self,
|
||||
provider=playback_provider)
|
||||
|
||||
stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self)
|
||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
||||
self.stored_playlists = core.StoredPlaylistsController(backend=self,
|
||||
provider=stored_playlists_provider)
|
||||
|
||||
self.uri_schemes = [u'file']
|
||||
|
||||
self.gstreamer = None
|
||||
self.audio = None
|
||||
|
||||
def on_start(self):
|
||||
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(gstreamer_refs) == 1, \
|
||||
'Expected exactly one running GStreamer.'
|
||||
self.gstreamer = gstreamer_refs[0].proxy()
|
||||
audio_refs = ActorRegistry.get_by_class(audio.Audio)
|
||||
assert len(audio_refs) == 1, \
|
||||
'Expected exactly one running Audio instance.'
|
||||
self.audio = audio_refs[0].proxy()
|
||||
|
||||
|
||||
class LocalPlaybackController(PlaybackController):
|
||||
class LocalPlaybackController(core.PlaybackController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalPlaybackController, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -81,35 +75,10 @@ class LocalPlaybackController(PlaybackController):
|
||||
|
||||
@property
|
||||
def time_position(self):
|
||||
return self.backend.gstreamer.get_position().get()
|
||||
return self.backend.audio.get_position().get()
|
||||
|
||||
|
||||
class LocalPlaybackProvider(BasePlaybackProvider):
|
||||
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):
|
||||
class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
|
||||
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)
|
||||
|
||||
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 = []
|
||||
for uri in parse_m3u(m3u):
|
||||
try:
|
||||
@ -182,7 +151,7 @@ class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
||||
self._playlists.append(playlist)
|
||||
|
||||
|
||||
class LocalLibraryProvider(BaseLibraryProvider):
|
||||
class LocalLibraryProvider(base.BaseLibraryProvider):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalLibraryProvider, self).__init__(*args, **kwargs)
|
||||
self._uri_mapping = {}
|
||||
@ -203,7 +172,8 @@ class LocalLibraryProvider(BaseLibraryProvider):
|
||||
try:
|
||||
return self._uri_mapping[uri]
|
||||
except KeyError:
|
||||
raise LookupError('%s not found.' % uri)
|
||||
logger.debug(u'Failed to lookup "%s"', uri)
|
||||
return None
|
||||
|
||||
def find_exact(self, **query):
|
||||
self._validate_query(query)
|
||||
|
||||
@ -3,16 +3,14 @@ import logging
|
||||
from pykka.actor import ThreadingActor
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||
LibraryController, PlaybackController, StoredPlaylistsController)
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy import audio, core, settings
|
||||
from mopidy.backends import base
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify')
|
||||
|
||||
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/>`_
|
||||
music streaming service. The backend uses the official `libspotify
|
||||
@ -51,24 +49,24 @@ class SpotifyBackend(ThreadingActor, Backend):
|
||||
|
||||
super(SpotifyBackend, self).__init__(*args, **kwargs)
|
||||
|
||||
self.current_playlist = CurrentPlaylistController(backend=self)
|
||||
self.current_playlist = core.CurrentPlaylistController(backend=self)
|
||||
|
||||
library_provider = SpotifyLibraryProvider(backend=self)
|
||||
self.library = LibraryController(backend=self,
|
||||
self.library = core.LibraryController(backend=self,
|
||||
provider=library_provider)
|
||||
|
||||
playback_provider = SpotifyPlaybackProvider(backend=self)
|
||||
self.playback = PlaybackController(backend=self,
|
||||
self.playback = core.PlaybackController(backend=self,
|
||||
provider=playback_provider)
|
||||
|
||||
stored_playlists_provider = SpotifyStoredPlaylistsProvider(
|
||||
backend=self)
|
||||
self.stored_playlists = StoredPlaylistsController(backend=self,
|
||||
self.stored_playlists = core.StoredPlaylistsController(backend=self,
|
||||
provider=stored_playlists_provider)
|
||||
|
||||
self.uri_schemes = [u'spotify']
|
||||
|
||||
self.gstreamer = None
|
||||
self.audio = None
|
||||
self.spotify = None
|
||||
|
||||
# Fail early if settings are not present
|
||||
@ -76,10 +74,10 @@ class SpotifyBackend(ThreadingActor, Backend):
|
||||
self.password = settings.SPOTIFY_PASSWORD
|
||||
|
||||
def on_start(self):
|
||||
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(gstreamer_refs) == 1, \
|
||||
'Expected exactly one running GStreamer.'
|
||||
self.gstreamer = gstreamer_refs[0].proxy()
|
||||
audio_refs = ActorRegistry.get_by_class(audio.Audio)
|
||||
assert len(audio_refs) == 1, \
|
||||
'Expected exactly one running Audio instance.'
|
||||
self.audio = audio_refs[0].proxy()
|
||||
|
||||
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
|
||||
self.spotify = self._connect()
|
||||
|
||||
@ -5,21 +5,55 @@ from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy.backends.base import BaseLibraryProvider
|
||||
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')
|
||||
|
||||
|
||||
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):
|
||||
def find_exact(self, **query):
|
||||
return self.search(**query)
|
||||
|
||||
def lookup(self, uri):
|
||||
try:
|
||||
spotify_track = Link.from_string(uri).as_track()
|
||||
# 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)
|
||||
return SpotifyTrack(uri)
|
||||
except SpotifyError as e:
|
||||
logger.debug(u'Failed to lookup "%s": %s', uri, e)
|
||||
return None
|
||||
|
||||
@ -3,15 +3,13 @@ import logging
|
||||
from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy.backends.base import BasePlaybackProvider
|
||||
from mopidy.core import PlaybackState
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.playback')
|
||||
|
||||
class SpotifyPlaybackProvider(BasePlaybackProvider):
|
||||
def pause(self):
|
||||
return self.backend.gstreamer.pause_playback()
|
||||
|
||||
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)
|
||||
if track.uri is None:
|
||||
return False
|
||||
@ -19,10 +17,10 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
|
||||
self.backend.spotify.session.load(
|
||||
Link.from_string(track.uri).as_track())
|
||||
self.backend.spotify.session.play(1)
|
||||
self.backend.gstreamer.prepare_change()
|
||||
self.backend.gstreamer.set_uri('appsrc://')
|
||||
self.backend.gstreamer.start_playback()
|
||||
self.backend.gstreamer.set_metadata(track)
|
||||
self.backend.audio.prepare_change()
|
||||
self.backend.audio.set_uri('appsrc://')
|
||||
self.backend.audio.start_playback()
|
||||
self.backend.audio.set_metadata(track)
|
||||
return True
|
||||
except SpotifyError as 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)
|
||||
|
||||
def seek(self, time_position):
|
||||
self.backend.gstreamer.prepare_change()
|
||||
self.backend.audio.prepare_change()
|
||||
self.backend.spotify.session.seek(time_position)
|
||||
self.backend.gstreamer.start_playback()
|
||||
self.backend.audio.start_playback()
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
result = self.backend.gstreamer.stop_playback()
|
||||
self.backend.spotify.session.play(0)
|
||||
return result
|
||||
|
||||
def get_volume(self):
|
||||
return self.backend.gstreamer.get_volume().get()
|
||||
|
||||
def set_volume(self, volume):
|
||||
self.backend.gstreamer.set_volume(volume)
|
||||
return super(SpotifyPlaybackProvider, self).stop()
|
||||
|
||||
@ -6,14 +6,13 @@ from spotify.manager import SpotifySessionManager as PyspotifySessionManager
|
||||
|
||||
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.spotify import BITRATES
|
||||
from mopidy.backends.spotify.container_manager import SpotifyContainerManager
|
||||
from mopidy.backends.spotify.playlist_manager import SpotifyPlaylistManager
|
||||
from mopidy.backends.spotify.translator import SpotifyTranslator
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy.utils.process import BaseThread
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.session_manager')
|
||||
@ -34,7 +33,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
BaseThread.__init__(self)
|
||||
self.name = 'SpotifyThread'
|
||||
|
||||
self.gstreamer = None
|
||||
self.audio = None
|
||||
self.backend = None
|
||||
|
||||
self.connected = threading.Event()
|
||||
@ -50,10 +49,10 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
self.connect()
|
||||
|
||||
def setup(self):
|
||||
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(gstreamer_refs) == 1, \
|
||||
'Expected exactly one running gstreamer.'
|
||||
self.gstreamer = gstreamer_refs[0].proxy()
|
||||
audio_refs = ActorRegistry.get_by_class(audio.Audio)
|
||||
assert len(audio_refs) == 1, \
|
||||
'Expected exactly one running Audio instance.'
|
||||
self.audio = audio_refs[0].proxy()
|
||||
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
@ -117,7 +116,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
'sample_rate': sample_rate,
|
||||
'channels': channels,
|
||||
}
|
||||
self.gstreamer.emit_data(capabilites, bytes(frames))
|
||||
self.audio.emit_data(capabilites, bytes(frames))
|
||||
return num_frames
|
||||
|
||||
def play_token_lost(self, session):
|
||||
@ -143,7 +142,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
def end_of_track(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
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):
|
||||
"""Refresh the stored playlists in the backend with fresh meta data
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import datetime as dt
|
||||
import logging
|
||||
|
||||
from spotify import Link, SpotifyError
|
||||
@ -31,9 +30,8 @@ class SpotifyTranslator(object):
|
||||
if not spotify_track.is_loaded():
|
||||
return Track(uri=uri, name=u'[loading...]')
|
||||
spotify_album = spotify_track.album()
|
||||
if (spotify_album is not None and spotify_album.is_loaded()
|
||||
and dt.MINYEAR <= int(spotify_album.year()) <= dt.MAXYEAR):
|
||||
date = dt.date(spotify_album.year(), 1, 1)
|
||||
if spotify_album is not None and spotify_album.is_loaded():
|
||||
date = spotify_album.year()
|
||||
else:
|
||||
date = None
|
||||
return Track(
|
||||
|
||||
4
mopidy/core/__init__.py
Normal file
4
mopidy/core/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .current_playlist import CurrentPlaylistController
|
||||
from .library import LibraryController
|
||||
from .playback import PlaybackController, PlaybackState
|
||||
from .stored_playlists import StoredPlaylistsController
|
||||
@ -5,7 +5,9 @@ import random
|
||||
from mopidy.listeners import BackendListener
|
||||
from mopidy.models import CpTrack
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
logger = logging.getLogger('mopidy.core')
|
||||
|
||||
|
||||
class CurrentPlaylistController(object):
|
||||
"""
|
||||
70
mopidy/core/library.py
Normal file
70
mopidy/core/library.py
Normal 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
557
mopidy/core/playback.py
Normal 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')
|
||||
113
mopidy/core/stored_playlists.py
Normal file
113
mopidy/core/stored_playlists.py
Normal 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)
|
||||
@ -242,7 +242,7 @@ def _list_date(context, query):
|
||||
playlist = context.backend.library.find_exact(**query).get()
|
||||
for track in playlist.tracks:
|
||||
if track.date is not None:
|
||||
dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
|
||||
dates.add((u'Date', track.date))
|
||||
return dates
|
||||
|
||||
@handle_request(r'^listall "(?P<uri>[^"]+)"')
|
||||
|
||||
@ -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.exceptions import (MpdArgError, MpdNoExistError,
|
||||
MpdNotImplemented)
|
||||
@ -104,11 +104,9 @@ def pause(context, state=None):
|
||||
- Calls ``pause`` without any arguments to toogle pause.
|
||||
"""
|
||||
if state is None:
|
||||
if (context.backend.playback.state.get() ==
|
||||
PlaybackController.PLAYING):
|
||||
if (context.backend.playback.state.get() == PlaybackState.PLAYING):
|
||||
context.backend.playback.pause()
|
||||
elif (context.backend.playback.state.get() ==
|
||||
PlaybackController.PAUSED):
|
||||
elif (context.backend.playback.state.get() == PlaybackState.PAUSED):
|
||||
context.backend.playback.resume()
|
||||
elif int(state):
|
||||
context.backend.playback.pause()
|
||||
@ -185,9 +183,9 @@ def playpos(context, songpos):
|
||||
raise MpdArgError(u'Bad song index', command=u'play')
|
||||
|
||||
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
|
||||
elif (context.backend.playback.state.get() == PlaybackController.PAUSED):
|
||||
elif (context.backend.playback.state.get() == PlaybackState.PAUSED):
|
||||
return context.backend.playback.resume().get()
|
||||
elif context.backend.playback.current_cp_track.get() is not None:
|
||||
cp_track = context.backend.playback.current_cp_track.get()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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.protocol import handle_request
|
||||
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:
|
||||
result.append(('song', _status_songpos(futures)))
|
||||
result.append(('songid', _status_songid(futures)))
|
||||
if futures['playback.state'].get() in (PlaybackController.PLAYING,
|
||||
PlaybackController.PAUSED):
|
||||
if futures['playback.state'].get() in (PlaybackState.PLAYING,
|
||||
PlaybackState.PAUSED):
|
||||
result.append(('time', _status_time(futures)))
|
||||
result.append(('elapsed', _status_time_elapsed(futures)))
|
||||
result.append(('bitrate', _status_bitrate(futures)))
|
||||
@ -239,11 +239,11 @@ def _status_songpos(futures):
|
||||
|
||||
def _status_state(futures):
|
||||
state = futures['playback.state'].get()
|
||||
if state == PlaybackController.PLAYING:
|
||||
if state == PlaybackState.PLAYING:
|
||||
return u'play'
|
||||
elif state == PlaybackController.STOPPED:
|
||||
elif state == PlaybackState.STOPPED:
|
||||
return u'stop'
|
||||
elif state == PlaybackController.PAUSED:
|
||||
elif state == PlaybackState.PAUSED:
|
||||
return u'pause'
|
||||
|
||||
def _status_time(futures):
|
||||
|
||||
@ -16,7 +16,7 @@ from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import settings
|
||||
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
|
||||
|
||||
# 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)
|
||||
return
|
||||
state = self.backend.playback.state.get()
|
||||
if state == PlaybackController.PLAYING:
|
||||
if state == PlaybackState.PLAYING:
|
||||
self.backend.playback.pause().get()
|
||||
elif state == PlaybackController.PAUSED:
|
||||
elif state == PlaybackState.PAUSED:
|
||||
self.backend.playback.resume().get()
|
||||
elif state == PlaybackController.STOPPED:
|
||||
elif state == PlaybackState.STOPPED:
|
||||
self.backend.playback.play().get()
|
||||
|
||||
@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)
|
||||
return
|
||||
state = self.backend.playback.state.get()
|
||||
if state == PlaybackController.PAUSED:
|
||||
if state == PlaybackState.PAUSED:
|
||||
self.backend.playback.resume().get()
|
||||
else:
|
||||
self.backend.playback.play().get()
|
||||
@ -287,11 +287,11 @@ class MprisObject(dbus.service.Object):
|
||||
|
||||
def get_PlaybackStatus(self):
|
||||
state = self.backend.playback.state.get()
|
||||
if state == PlaybackController.PLAYING:
|
||||
if state == PlaybackState.PLAYING:
|
||||
return 'Playing'
|
||||
elif state == PlaybackController.PAUSED:
|
||||
elif state == PlaybackState.PAUSED:
|
||||
return 'Paused'
|
||||
elif state == PlaybackController.STOPPED:
|
||||
elif state == PlaybackState.STOPPED:
|
||||
return 'Stopped'
|
||||
|
||||
def get_LoopStatus(self):
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class ImmutableObject(object):
|
||||
"""
|
||||
Superclass for immutable objects whose fields can only be modified via the
|
||||
@ -157,8 +158,8 @@ class Track(ImmutableObject):
|
||||
:type album: :class:`Album`
|
||||
:param track_no: track number in album
|
||||
:type track_no: integer
|
||||
:param date: track release date
|
||||
:type date: :class:`datetime.date`
|
||||
:param date: track release date (YYYY or YYYY-MM-DD)
|
||||
:type date: string
|
||||
:param length: track length in milliseconds
|
||||
:type length: integer
|
||||
:param bitrate: bitrate in kbit/s
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Available settings and their default values.
|
||||
All available settings and their default values.
|
||||
|
||||
.. warning::
|
||||
|
||||
@ -14,6 +14,10 @@ Available settings and their default values.
|
||||
#:
|
||||
#: BACKENDS = (u'mopidy.backends.spotify.SpotifyBackend',)
|
||||
#:
|
||||
#: Other typical values::
|
||||
#:
|
||||
#: BACKENDS = (u'mopidy.backends.local.LocalBackend',)
|
||||
#:
|
||||
#: .. note::
|
||||
#: Currently only the first backend in the list is used.
|
||||
BACKENDS = (
|
||||
@ -106,9 +110,9 @@ LOCAL_TAG_CACHE_FILE = None
|
||||
#: Sound mixer to use.
|
||||
#:
|
||||
#: 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::
|
||||
#:
|
||||
@ -118,7 +122,7 @@ MIXER = u'autoaudiomixer'
|
||||
#: Sound mixer track to use.
|
||||
#:
|
||||
#: 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``.
|
||||
#:
|
||||
#: Default::
|
||||
@ -128,7 +132,9 @@ MIXER_TRACK = None
|
||||
|
||||
#: Which address Mopidy's MPD server should bind to.
|
||||
#:
|
||||
#:Examples:
|
||||
#: Used by :mod:`mopidy.frontends.mpd`.
|
||||
#:
|
||||
#: Examples:
|
||||
#:
|
||||
#: ``127.0.0.1``
|
||||
#: 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.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.mpd`.
|
||||
#:
|
||||
#: Default: 6600
|
||||
MPD_SERVER_PORT = 6600
|
||||
|
||||
#: The password required for connecting to the MPD server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.mpd`.
|
||||
#:
|
||||
#: Default: :class:`None`, which means no password required.
|
||||
MPD_SERVER_PASSWORD = None
|
||||
|
||||
#: The maximum number of concurrent connections the MPD server will accept.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.mpd`.
|
||||
#:
|
||||
#: Default: 20
|
||||
MPD_SERVER_MAX_CONNECTIONS = 20
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import sys
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy import audio, settings
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
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',
|
||||
'Our Windows build server does not support GStreamer yet')
|
||||
class GStreamerTest(unittest.TestCase):
|
||||
class AudioTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
settings.MIXER = 'fakemixer track_max_volume=65536'
|
||||
settings.OUTPUT = 'fakesink'
|
||||
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
|
||||
self.gstreamer = GStreamer()
|
||||
self.audio = audio.Audio.start().proxy()
|
||||
|
||||
def tearDown(self):
|
||||
self.audio.stop()
|
||||
settings.runtime.clear()
|
||||
|
||||
def prepare_uri(self, uri):
|
||||
self.gstreamer.prepare_change()
|
||||
self.gstreamer.set_uri(uri)
|
||||
self.audio.prepare_change()
|
||||
self.audio.set_uri(uri)
|
||||
|
||||
def test_start_playback_existing_file(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.assertTrue(self.gstreamer.start_playback())
|
||||
self.assertTrue(self.audio.start_playback().get())
|
||||
|
||||
def test_start_playback_non_existing_file(self):
|
||||
self.prepare_uri(self.song_uri + 'bogus')
|
||||
self.assertFalse(self.gstreamer.start_playback())
|
||||
self.assertFalse(self.audio.start_playback().get())
|
||||
|
||||
def test_pause_playback_while_playing(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.gstreamer.start_playback()
|
||||
self.assertTrue(self.gstreamer.pause_playback())
|
||||
self.audio.start_playback()
|
||||
self.assertTrue(self.audio.pause_playback().get())
|
||||
|
||||
def test_stop_playback_while_playing(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.gstreamer.start_playback()
|
||||
self.assertTrue(self.gstreamer.stop_playback())
|
||||
self.audio.start_playback()
|
||||
self.assertTrue(self.audio.stop_playback().get())
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_deliver_data(self):
|
||||
@ -51,8 +51,8 @@ class GStreamerTest(unittest.TestCase):
|
||||
|
||||
def test_set_volume(self):
|
||||
for value in range(0, 101):
|
||||
self.assertTrue(self.gstreamer.set_volume(value))
|
||||
self.assertEqual(value, self.gstreamer.get_volume())
|
||||
self.assertTrue(self.audio.set_volume(value).get())
|
||||
self.assertEqual(value, self.audio.get_volume().get())
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_set_state_encapsulation(self):
|
||||
@ -65,4 +65,3 @@ class GStreamerTest(unittest.TestCase):
|
||||
@unittest.SkipTest
|
||||
def test_invalid_output_raises_error(self):
|
||||
pass # TODO
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import mock
|
||||
import random
|
||||
|
||||
from mopidy import audio
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.models import CpTrack, Playlist, Track
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
from tests.backends.base import populate_playlist
|
||||
|
||||
@ -12,7 +13,7 @@ class CurrentPlaylistControllerTest(object):
|
||||
|
||||
def setUp(self):
|
||||
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.playback = self.backend.playback
|
||||
|
||||
@ -71,9 +72,9 @@ class CurrentPlaylistControllerTest(object):
|
||||
@populate_playlist
|
||||
def test_clear_when_playing(self):
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
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):
|
||||
track = Track(uri='a')
|
||||
@ -134,13 +135,13 @@ class CurrentPlaylistControllerTest(object):
|
||||
self.playback.play()
|
||||
track = self.playback.current_track
|
||||
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)
|
||||
|
||||
@populate_playlist
|
||||
def test_append_preserves_stopped_state(self):
|
||||
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)
|
||||
|
||||
def test_index_returns_index_of_track(self):
|
||||
@ -205,7 +206,7 @@ class CurrentPlaylistControllerTest(object):
|
||||
version = self.controller.version
|
||||
self.controller.remove(uri=track1.uri)
|
||||
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])
|
||||
|
||||
@populate_playlist
|
||||
|
||||
@ -34,8 +34,8 @@ class LibraryControllerTest(object):
|
||||
self.assertEqual(track, self.tracks[0])
|
||||
|
||||
def test_lookup_unknown_track(self):
|
||||
test = lambda: self.library.lookup('fake uri')
|
||||
self.assertRaises(LookupError, test)
|
||||
track = self.library.lookup('fake uri')
|
||||
self.assertEquals(track, None)
|
||||
|
||||
def test_find_exact_no_hits(self):
|
||||
result = self.library.find_exact(track=['unknown track'])
|
||||
|
||||
@ -2,8 +2,9 @@ import mock
|
||||
import random
|
||||
import time
|
||||
|
||||
from mopidy import audio
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.models import Track
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
from tests import unittest
|
||||
from tests.backends.base import populate_playlist
|
||||
@ -16,7 +17,7 @@ class PlaybackControllerTest(object):
|
||||
|
||||
def setUp(self):
|
||||
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.current_playlist = self.backend.current_playlist
|
||||
|
||||
@ -26,21 +27,21 @@ class PlaybackControllerTest(object):
|
||||
'First song needs to be at least 2000 miliseconds'
|
||||
|
||||
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):
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
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):
|
||||
self.assertEqual(self.playback.play(), None)
|
||||
|
||||
@populate_playlist
|
||||
def test_play_state(self):
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@populate_playlist
|
||||
def test_play_return_value(self):
|
||||
@ -48,9 +49,9 @@ class PlaybackControllerTest(object):
|
||||
|
||||
@populate_playlist
|
||||
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.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@populate_playlist
|
||||
def test_play_track_return_value(self):
|
||||
@ -70,7 +71,7 @@ class PlaybackControllerTest(object):
|
||||
track = self.playback.current_track
|
||||
self.playback.pause()
|
||||
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)
|
||||
|
||||
@populate_playlist
|
||||
@ -81,7 +82,7 @@ class PlaybackControllerTest(object):
|
||||
track = self.playback.current_track
|
||||
self.playback.pause()
|
||||
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)
|
||||
|
||||
@populate_playlist
|
||||
@ -106,12 +107,12 @@ class PlaybackControllerTest(object):
|
||||
def test_current_track_after_completed_playlist(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
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.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
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)
|
||||
|
||||
@populate_playlist
|
||||
@ -141,17 +142,17 @@ class PlaybackControllerTest(object):
|
||||
self.playback.next()
|
||||
self.playback.stop()
|
||||
self.playback.previous()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_previous_at_start_of_playlist(self):
|
||||
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)
|
||||
|
||||
def test_previous_for_empty_playlist(self):
|
||||
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)
|
||||
|
||||
@populate_playlist
|
||||
@ -185,20 +186,20 @@ class PlaybackControllerTest(object):
|
||||
@populate_playlist
|
||||
def test_next_does_not_trigger_playback(self):
|
||||
self.playback.next()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_next_at_end_of_playlist(self):
|
||||
self.playback.play()
|
||||
|
||||
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_playlist_position, i)
|
||||
|
||||
self.playback.next()
|
||||
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_next_until_end_of_playlist_and_play_from_start(self):
|
||||
@ -208,15 +209,15 @@ class PlaybackControllerTest(object):
|
||||
self.playback.next()
|
||||
|
||||
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.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
self.assertEqual(self.playback.current_track, self.tracks[0])
|
||||
|
||||
def test_next_for_empty_playlist(self):
|
||||
self.playback.next()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_next_skips_to_next_track_on_failure(self):
|
||||
@ -273,7 +274,7 @@ class PlaybackControllerTest(object):
|
||||
self.playback.consume = True
|
||||
self.playback.play()
|
||||
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
|
||||
def test_next_with_single_and_repeat(self):
|
||||
@ -321,20 +322,20 @@ class PlaybackControllerTest(object):
|
||||
@populate_playlist
|
||||
def test_end_of_track_does_not_trigger_playback(self):
|
||||
self.playback.on_end_of_track()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_end_of_track_at_end_of_playlist(self):
|
||||
self.playback.play()
|
||||
|
||||
for i, track in enumerate(self.tracks):
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
self.assertEqual(self.playback.current_track, track)
|
||||
self.assertEqual(self.playback.current_playlist_position, i)
|
||||
|
||||
self.playback.on_end_of_track()
|
||||
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
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.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.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
self.assertEqual(self.playback.current_track, self.tracks[0])
|
||||
|
||||
def test_end_of_track_for_empty_playlist(self):
|
||||
self.playback.on_end_of_track()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
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.play()
|
||||
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
|
||||
def test_end_of_track_with_random(self):
|
||||
@ -534,13 +535,13 @@ class PlaybackControllerTest(object):
|
||||
self.playback.play()
|
||||
current_track = self.playback.current_track
|
||||
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)
|
||||
|
||||
@populate_playlist
|
||||
def test_on_current_playlist_change_when_stopped(self):
|
||||
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)
|
||||
|
||||
@populate_playlist
|
||||
@ -549,26 +550,26 @@ class PlaybackControllerTest(object):
|
||||
self.playback.pause()
|
||||
current_track = self.playback.current_track
|
||||
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)
|
||||
|
||||
@populate_playlist
|
||||
def test_pause_when_stopped(self):
|
||||
self.playback.pause()
|
||||
self.assertEqual(self.playback.state, self.playback.PAUSED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
|
||||
|
||||
@populate_playlist
|
||||
def test_pause_when_playing(self):
|
||||
self.playback.play()
|
||||
self.playback.pause()
|
||||
self.assertEqual(self.playback.state, self.playback.PAUSED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
|
||||
|
||||
@populate_playlist
|
||||
def test_pause_when_paused(self):
|
||||
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
|
||||
def test_pause_return_value(self):
|
||||
@ -578,20 +579,20 @@ class PlaybackControllerTest(object):
|
||||
@populate_playlist
|
||||
def test_resume_when_stopped(self):
|
||||
self.playback.resume()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_resume_when_playing(self):
|
||||
self.playback.play()
|
||||
self.playback.resume()
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@populate_playlist
|
||||
def test_resume_when_paused(self):
|
||||
self.playback.play()
|
||||
self.playback.pause()
|
||||
self.playback.resume()
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@populate_playlist
|
||||
def test_resume_return_value(self):
|
||||
@ -624,12 +625,12 @@ class PlaybackControllerTest(object):
|
||||
|
||||
def test_seek_on_empty_playlist_updates_position(self):
|
||||
self.playback.seek(0)
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_seek_when_stopped_triggers_play(self):
|
||||
self.playback.seek(0)
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@populate_playlist
|
||||
def test_seek_when_playing(self):
|
||||
@ -666,7 +667,7 @@ class PlaybackControllerTest(object):
|
||||
self.playback.play()
|
||||
self.playback.pause()
|
||||
self.playback.seek(0)
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@unittest.SkipTest
|
||||
@populate_playlist
|
||||
@ -686,7 +687,7 @@ class PlaybackControllerTest(object):
|
||||
def test_seek_beyond_end_of_song_for_last_track(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
self.playback.seek(self.current_playlist.tracks[-1].length * 100)
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@unittest.SkipTest
|
||||
@populate_playlist
|
||||
@ -702,25 +703,25 @@ class PlaybackControllerTest(object):
|
||||
self.playback.seek(-1000)
|
||||
position = self.playback.time_position
|
||||
self.assert_(position >= 0, position)
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@populate_playlist
|
||||
def test_stop_when_stopped(self):
|
||||
self.playback.stop()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_stop_when_playing(self):
|
||||
self.playback.play()
|
||||
self.playback.stop()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
@populate_playlist
|
||||
def test_stop_when_paused(self):
|
||||
self.playback.play()
|
||||
self.playback.pause()
|
||||
self.playback.stop()
|
||||
self.assertEqual(self.playback.state, self.playback.STOPPED)
|
||||
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
|
||||
|
||||
def test_stop_return_value(self):
|
||||
self.playback.play()
|
||||
@ -729,7 +730,7 @@ class PlaybackControllerTest(object):
|
||||
def test_time_position_when_stopped(self):
|
||||
future = mock.Mock()
|
||||
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)
|
||||
|
||||
@ -737,7 +738,7 @@ class PlaybackControllerTest(object):
|
||||
def test_time_position_when_stopped_with_playlist(self):
|
||||
future = mock.Mock()
|
||||
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)
|
||||
|
||||
@ -810,7 +811,7 @@ class PlaybackControllerTest(object):
|
||||
def test_end_of_playlist_stops(self):
|
||||
self.playback.play(self.current_playlist.cp_tracks[-1])
|
||||
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):
|
||||
self.assertEqual(self.playback.repeat, False)
|
||||
@ -835,9 +836,9 @@ class PlaybackControllerTest(object):
|
||||
for _ in self.tracks:
|
||||
self.playback.next()
|
||||
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.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
@populate_playlist
|
||||
def test_random_until_end_of_playlist_with_repeat(self):
|
||||
@ -854,7 +855,7 @@ class PlaybackControllerTest(object):
|
||||
self.playback.play()
|
||||
played = []
|
||||
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)
|
||||
self.playback.next()
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ class StoredPlaylistsControllerTest(object):
|
||||
def test_create_in_playlists(self):
|
||||
playlist = self.stored.create('test')
|
||||
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):
|
||||
self.assert_(not self.stored.playlists)
|
||||
@ -101,7 +101,7 @@ class StoredPlaylistsControllerTest(object):
|
||||
# FIXME should we handle playlists without names?
|
||||
playlist = Playlist(name='test')
|
||||
self.stored.save(playlist)
|
||||
self.assert_(playlist in self.stored.playlists)
|
||||
self.assertIn(playlist, self.stored.playlists)
|
||||
|
||||
@unittest.SkipTest
|
||||
def test_playlist_with_unknown_track(self):
|
||||
|
||||
@ -2,6 +2,7 @@ import sys
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.local import LocalBackend
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.models import Track
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
@ -34,19 +35,19 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
|
||||
self.backend.current_playlist.add(track)
|
||||
|
||||
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):
|
||||
self.add_track('blank.mp3')
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
def test_play_ogg(self):
|
||||
self.add_track('blank.ogg')
|
||||
self.playback.play()
|
||||
self.assertEqual(self.playback.state, self.playback.PLAYING)
|
||||
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
|
||||
|
||||
def test_play_flac(self):
|
||||
self.add_track('blank.flac')
|
||||
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
0
tests/core/__init__.py
Normal file
@ -37,7 +37,7 @@ class MpdDispatcherTest(unittest.TestCase):
|
||||
expected_handler
|
||||
(handler, kwargs) = self.dispatcher._find_handler('known_command an_arg')
|
||||
self.assertEqual(handler, expected_handler)
|
||||
self.assert_('arg1' in kwargs)
|
||||
self.assertIn('arg1', kwargs)
|
||||
self.assertEqual(kwargs['arg1'], 'an_arg')
|
||||
|
||||
def test_handling_unknown_request_yields_error(self):
|
||||
@ -48,5 +48,5 @@ class MpdDispatcherTest(unittest.TestCase):
|
||||
expected = 'magic'
|
||||
request_handlers['known request'] = lambda x: expected
|
||||
result = self.dispatcher.handle_request('known request')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(expected in result)
|
||||
self.assertIn(u'OK', result)
|
||||
self.assertIn(expected, result)
|
||||
|
||||
@ -42,7 +42,7 @@ class BaseTestCase(unittest.TestCase):
|
||||
self.assertEqual([], self.connection.response)
|
||||
|
||||
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)))
|
||||
|
||||
def assertOnceInResponse(self, value):
|
||||
@ -51,7 +51,7 @@ class BaseTestCase(unittest.TestCase):
|
||||
(repr(value), repr(self.connection.response)))
|
||||
|
||||
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)))
|
||||
|
||||
def assertEqualResponse(self, value):
|
||||
|
||||
@ -21,7 +21,7 @@ class CommandListsTest(protocol.BaseTestCase):
|
||||
self.assertEqual([], self.dispatcher.command_list)
|
||||
self.assertEqual(False, self.dispatcher.command_list_ok)
|
||||
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.assertInResponse(u'OK')
|
||||
self.assertEqual(False, self.dispatcher.command_list)
|
||||
@ -42,7 +42,7 @@ class CommandListsTest(protocol.BaseTestCase):
|
||||
self.assertEqual([], self.dispatcher.command_list)
|
||||
self.assertEqual(True, self.dispatcher.command_list_ok)
|
||||
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.assertInResponse(u'list_OK')
|
||||
self.assertInResponse(u'OK')
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
from mopidy.backends import base as backend
|
||||
from mopidy.core import PlaybackState
|
||||
from mopidy.models import Track
|
||||
|
||||
from tests import unittest
|
||||
from tests.frontends.mpd import protocol
|
||||
|
||||
PAUSED = backend.PlaybackController.PAUSED
|
||||
PLAYING = backend.PlaybackController.PLAYING
|
||||
STOPPED = backend.PlaybackController.STOPPED
|
||||
|
||||
PAUSED = PlaybackState.PAUSED
|
||||
PLAYING = PlaybackState.PLAYING
|
||||
STOPPED = PlaybackState.STOPPED
|
||||
|
||||
|
||||
class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
|
||||
|
||||
@ -31,66 +31,66 @@ class TrackMpdFormatTest(unittest.TestCase):
|
||||
|
||||
def test_track_to_mpd_format_for_empty_track(self):
|
||||
result = translator.track_to_mpd_format(Track())
|
||||
self.assert_(('file', '') in result)
|
||||
self.assert_(('Time', 0) in result)
|
||||
self.assert_(('Artist', '') in result)
|
||||
self.assert_(('Title', '') in result)
|
||||
self.assert_(('Album', '') in result)
|
||||
self.assert_(('Track', 0) in result)
|
||||
self.assert_(('Date', '') in result)
|
||||
self.assertIn(('file', ''), result)
|
||||
self.assertIn(('Time', 0), result)
|
||||
self.assertIn(('Artist', ''), result)
|
||||
self.assertIn(('Title', ''), result)
|
||||
self.assertIn(('Album', ''), result)
|
||||
self.assertIn(('Track', 0), result)
|
||||
self.assertIn(('Date', ''), result)
|
||||
self.assertEqual(len(result), 7)
|
||||
|
||||
def test_track_to_mpd_format_with_position(self):
|
||||
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):
|
||||
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):
|
||||
result = translator.track_to_mpd_format(CpTrack(2, Track()), position=1)
|
||||
self.assert_(('Pos', 1) in result)
|
||||
self.assert_(('Id', 2) in result)
|
||||
self.assertIn(('Pos', 1), result)
|
||||
self.assertIn(('Id', 2), result)
|
||||
|
||||
def test_track_to_mpd_format_for_nonempty_track(self):
|
||||
result = translator.track_to_mpd_format(
|
||||
CpTrack(122, self.track), position=9)
|
||||
self.assert_(('file', 'a uri') in result)
|
||||
self.assert_(('Time', 137) in result)
|
||||
self.assert_(('Artist', 'an artist') in result)
|
||||
self.assert_(('Title', 'a name') in result)
|
||||
self.assert_(('Album', 'an album') in result)
|
||||
self.assert_(('AlbumArtist', 'an other artist') in result)
|
||||
self.assert_(('Track', '7/13') in result)
|
||||
self.assert_(('Date', datetime.date(1977, 1, 1)) in result)
|
||||
self.assert_(('Pos', 9) in result)
|
||||
self.assert_(('Id', 122) in result)
|
||||
self.assertIn(('file', 'a uri'), result)
|
||||
self.assertIn(('Time', 137), result)
|
||||
self.assertIn(('Artist', 'an artist'), result)
|
||||
self.assertIn(('Title', 'a name'), result)
|
||||
self.assertIn(('Album', 'an album'), result)
|
||||
self.assertIn(('AlbumArtist', 'an other artist'), result)
|
||||
self.assertIn(('Track', '7/13'), result)
|
||||
self.assertIn(('Date', datetime.date(1977, 1, 1)), result)
|
||||
self.assertIn(('Pos', 9), result)
|
||||
self.assertIn(('Id', 122), result)
|
||||
self.assertEqual(len(result), 10)
|
||||
|
||||
def test_track_to_mpd_format_musicbrainz_trackid(self):
|
||||
track = self.track.copy(musicbrainz_id='foo')
|
||||
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):
|
||||
album = self.track.album.copy(musicbrainz_id='foo')
|
||||
track = self.track.copy(album=album)
|
||||
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):
|
||||
artist = list(self.track.artists)[0].copy(musicbrainz_id='foo')
|
||||
album = self.track.album.copy(artists=[artist])
|
||||
track = self.track.copy(album=album)
|
||||
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):
|
||||
artist = list(self.track.artists)[0].copy(musicbrainz_id='foo')
|
||||
track = self.track.copy(artists=[artist])
|
||||
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):
|
||||
artists = [Artist(name=u'ABBA'), Artist(name=u'Beatles')]
|
||||
|
||||
@ -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.protocol import status
|
||||
from mopidy.models import Track
|
||||
|
||||
from tests import unittest
|
||||
|
||||
PAUSED = backend.PlaybackController.PAUSED
|
||||
PLAYING = backend.PlaybackController.PLAYING
|
||||
STOPPED = backend.PlaybackController.STOPPED
|
||||
|
||||
PAUSED = PlaybackState.PAUSED
|
||||
PLAYING = PlaybackState.PLAYING
|
||||
STOPPED = PlaybackState.STOPPED
|
||||
|
||||
# FIXME migrate to using protocol.BaseTestCase instead of status.stats
|
||||
# directly?
|
||||
@ -15,7 +17,7 @@ STOPPED = backend.PlaybackController.STOPPED
|
||||
|
||||
class StatusHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.backend = backend.DummyBackend.start().proxy()
|
||||
self.backend = dummy.DummyBackend.start().proxy()
|
||||
self.dispatcher = dispatcher.MpdDispatcher()
|
||||
self.context = self.dispatcher.context
|
||||
|
||||
@ -24,123 +26,123 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
|
||||
def test_stats_method(self):
|
||||
result = status.stats(self.context)
|
||||
self.assert_('artists' in result)
|
||||
self.assertIn('artists', result)
|
||||
self.assert_(int(result['artists']) >= 0)
|
||||
self.assert_('albums' in result)
|
||||
self.assertIn('albums', result)
|
||||
self.assert_(int(result['albums']) >= 0)
|
||||
self.assert_('songs' in result)
|
||||
self.assertIn('songs', result)
|
||||
self.assert_(int(result['songs']) >= 0)
|
||||
self.assert_('uptime' in result)
|
||||
self.assertIn('uptime', result)
|
||||
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_('db_update' in result)
|
||||
self.assertIn('db_update', result)
|
||||
self.assert_(int(result['db_update']) >= 0)
|
||||
self.assert_('playtime' in result)
|
||||
self.assertIn('playtime', result)
|
||||
self.assert_(int(result['playtime']) >= 0)
|
||||
|
||||
def test_status_method_contains_volume_with_na_value(self):
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('volume' in result)
|
||||
self.assertIn('volume', result)
|
||||
self.assertEqual(int(result['volume']), -1)
|
||||
|
||||
def test_status_method_contains_volume(self):
|
||||
self.backend.playback.volume = 17
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('volume' in result)
|
||||
self.assertIn('volume', result)
|
||||
self.assertEqual(int(result['volume']), 17)
|
||||
|
||||
def test_status_method_contains_repeat_is_0(self):
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('repeat' in result)
|
||||
self.assertIn('repeat', result)
|
||||
self.assertEqual(int(result['repeat']), 0)
|
||||
|
||||
def test_status_method_contains_repeat_is_1(self):
|
||||
self.backend.playback.repeat = 1
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('repeat' in result)
|
||||
self.assertIn('repeat', result)
|
||||
self.assertEqual(int(result['repeat']), 1)
|
||||
|
||||
def test_status_method_contains_random_is_0(self):
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('random' in result)
|
||||
self.assertIn('random', result)
|
||||
self.assertEqual(int(result['random']), 0)
|
||||
|
||||
def test_status_method_contains_random_is_1(self):
|
||||
self.backend.playback.random = 1
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('random' in result)
|
||||
self.assertIn('random', result)
|
||||
self.assertEqual(int(result['random']), 1)
|
||||
|
||||
def test_status_method_contains_single(self):
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('single' in result)
|
||||
self.assert_(int(result['single']) in (0, 1))
|
||||
self.assertIn('single', result)
|
||||
self.assertIn(int(result['single']), (0, 1))
|
||||
|
||||
def test_status_method_contains_consume_is_0(self):
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('consume' in result)
|
||||
self.assertIn('consume', result)
|
||||
self.assertEqual(int(result['consume']), 0)
|
||||
|
||||
def test_status_method_contains_consume_is_1(self):
|
||||
self.backend.playback.consume = 1
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('consume' in result)
|
||||
self.assertIn('consume', result)
|
||||
self.assertEqual(int(result['consume']), 1)
|
||||
|
||||
def test_status_method_contains_playlist(self):
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('playlist' in result)
|
||||
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1))
|
||||
self.assertIn('playlist', result)
|
||||
self.assertIn(int(result['playlist']), xrange(0, 2**31 - 1))
|
||||
|
||||
def test_status_method_contains_playlistlength(self):
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('playlistlength' in result)
|
||||
self.assertIn('playlistlength', result)
|
||||
self.assert_(int(result['playlistlength']) >= 0)
|
||||
|
||||
def test_status_method_contains_xfade(self):
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('xfade' in result)
|
||||
self.assertIn('xfade', result)
|
||||
self.assert_(int(result['xfade']) >= 0)
|
||||
|
||||
def test_status_method_contains_state_is_play(self):
|
||||
self.backend.playback.state = PLAYING
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('state' in result)
|
||||
self.assertIn('state', result)
|
||||
self.assertEqual(result['state'], 'play')
|
||||
|
||||
def test_status_method_contains_state_is_stop(self):
|
||||
self.backend.playback.state = STOPPED
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('state' in result)
|
||||
self.assertIn('state', result)
|
||||
self.assertEqual(result['state'], 'stop')
|
||||
|
||||
def test_status_method_contains_state_is_pause(self):
|
||||
self.backend.playback.state = PLAYING
|
||||
self.backend.playback.state = PAUSED
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('state' in result)
|
||||
self.assertIn('state', result)
|
||||
self.assertEqual(result['state'], 'pause')
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_song(self):
|
||||
self.backend.current_playlist.append([Track()])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('song' in result)
|
||||
self.assertIn('song', result)
|
||||
self.assert_(int(result['song']) >= 0)
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
|
||||
self.backend.current_playlist.append([Track()])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('songid' in result)
|
||||
self.assertIn('songid', result)
|
||||
self.assertEqual(int(result['songid']), 0)
|
||||
|
||||
def test_status_method_when_playing_contains_time_with_no_length(self):
|
||||
self.backend.current_playlist.append([Track(length=None)])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('time' in result)
|
||||
self.assertIn('time', result)
|
||||
(position, total) = result['time'].split(':')
|
||||
position = int(position)
|
||||
total = int(total)
|
||||
@ -150,7 +152,7 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.backend.current_playlist.append([Track(length=10000)])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('time' in result)
|
||||
self.assertIn('time', result)
|
||||
(position, total) = result['time'].split(':')
|
||||
position = int(position)
|
||||
total = int(total)
|
||||
@ -160,19 +162,19 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.backend.playback.state = PAUSED
|
||||
self.backend.playback.play_time_accumulated = 59123
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('elapsed' in result)
|
||||
self.assertIn('elapsed', result)
|
||||
self.assertEqual(result['elapsed'], '59.123')
|
||||
|
||||
def test_status_method_when_starting_playing_contains_elapsed_zero(self):
|
||||
self.backend.playback.state = PAUSED
|
||||
self.backend.playback.play_time_accumulated = 123 # Less than 1000ms
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('elapsed' in result)
|
||||
self.assertIn('elapsed', result)
|
||||
self.assertEqual(result['elapsed'], '0.123')
|
||||
|
||||
def test_status_method_when_playing_contains_bitrate(self):
|
||||
self.backend.current_playlist.append([Track(bitrate=320)])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('bitrate' in result)
|
||||
self.assertIn('bitrate', result)
|
||||
self.assertEqual(int(result['bitrate']), 320)
|
||||
|
||||
@ -4,7 +4,7 @@ import mock
|
||||
|
||||
from mopidy import OptionalDependencyError
|
||||
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
|
||||
|
||||
try:
|
||||
@ -14,9 +14,9 @@ except OptionalDependencyError:
|
||||
|
||||
from tests import unittest
|
||||
|
||||
PLAYING = PlaybackController.PLAYING
|
||||
PAUSED = PlaybackController.PAUSED
|
||||
STOPPED = PlaybackController.STOPPED
|
||||
PLAYING = PlaybackState.PLAYING
|
||||
PAUSED = PlaybackState.PAUSED
|
||||
STOPPED = PlaybackState.STOPPED
|
||||
|
||||
|
||||
@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):
|
||||
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'], '')
|
||||
|
||||
def test_get_metadata_has_trackid_based_on_cpid(self):
|
||||
|
||||
@ -13,18 +13,18 @@ class HelpTest(unittest.TestCase):
|
||||
args = [sys.executable, mopidy_dir, '--help']
|
||||
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
self.assert_('--version' in output)
|
||||
self.assert_('--help' in output)
|
||||
self.assert_('--help-gst' in output)
|
||||
self.assert_('--interactive' in output)
|
||||
self.assert_('--quiet' in output)
|
||||
self.assert_('--verbose' in output)
|
||||
self.assert_('--save-debug-log' in output)
|
||||
self.assert_('--list-settings' in output)
|
||||
self.assertIn('--version', output)
|
||||
self.assertIn('--help', output)
|
||||
self.assertIn('--help-gst', output)
|
||||
self.assertIn('--interactive', output)
|
||||
self.assertIn('--quiet', output)
|
||||
self.assertIn('--verbose', output)
|
||||
self.assertIn('--save-debug-log', output)
|
||||
self.assertIn('--list-settings', output)
|
||||
|
||||
def test_help_gst_has_gstreamer_options(self):
|
||||
mopidy_dir = os.path.dirname(mopidy.__file__)
|
||||
args = [sys.executable, mopidy_dir, '--help-gst']
|
||||
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
self.assert_('--gst-version' in output)
|
||||
self.assertIn('--gst-version', output)
|
||||
|
||||
@ -43,7 +43,7 @@ class GenericCopyTets(unittest.TestCase):
|
||||
artist2 = Artist(name='bar')
|
||||
track = Track(artists=[artist1])
|
||||
copy = track.copy(artists=[artist2])
|
||||
self.assert_(artist2 in copy.artists)
|
||||
self.assertIn(artist2, copy.artists)
|
||||
|
||||
def test_copying_track_with_invalid_key(self):
|
||||
test = lambda: Track().copy(invalid_key=True)
|
||||
@ -155,7 +155,7 @@ class AlbumTest(unittest.TestCase):
|
||||
def test_artists(self):
|
||||
artist = Artist()
|
||||
album = Album(artists=[artist])
|
||||
self.assert_(artist in album.artists)
|
||||
self.assertIn(artist, album.artists)
|
||||
self.assertRaises(AttributeError, setattr, album, 'artists', None)
|
||||
|
||||
def test_num_tracks(self):
|
||||
@ -338,7 +338,7 @@ class TrackTest(unittest.TestCase):
|
||||
self.assertRaises(AttributeError, setattr, track, 'track_no', None)
|
||||
|
||||
def test_date(self):
|
||||
date = datetime.date(1977, 1, 1)
|
||||
date = '1977-01-01'
|
||||
track = Track(date=date)
|
||||
self.assertEqual(track.date, date)
|
||||
self.assertRaises(AttributeError, setattr, track, 'date', None)
|
||||
@ -434,7 +434,7 @@ class TrackTest(unittest.TestCase):
|
||||
self.assertEqual(hash(track1), hash(track2))
|
||||
|
||||
def test_eq_date(self):
|
||||
date = datetime.date.today()
|
||||
date = '1977-01-01'
|
||||
track1 = Track(date=date)
|
||||
track2 = Track(date=date)
|
||||
self.assertEqual(track1, track2)
|
||||
@ -459,7 +459,7 @@ class TrackTest(unittest.TestCase):
|
||||
self.assertEqual(hash(track1), hash(track2))
|
||||
|
||||
def test_eq(self):
|
||||
date = datetime.date.today()
|
||||
date = '1977-01-01'
|
||||
artists = [Artist()]
|
||||
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))
|
||||
|
||||
def test_ne_date(self):
|
||||
track1 = Track(date=datetime.date.today())
|
||||
track2 = Track(date=datetime.date.today()-datetime.timedelta(days=1))
|
||||
track1 = Track(date='1977-01-01')
|
||||
track2 = Track(date='1977-01-02')
|
||||
self.assertNotEqual(track1, track2)
|
||||
self.assertNotEqual(hash(track1), hash(track2))
|
||||
|
||||
@ -534,12 +534,12 @@ class TrackTest(unittest.TestCase):
|
||||
def test_ne(self):
|
||||
track1 = Track(uri=u'uri1', name=u'name1',
|
||||
artists=[Artist(name=u'name1')], album=Album(name=u'name1'),
|
||||
track_no=1, date=datetime.date.today(), length=100, bitrate=100,
|
||||
track_no=1, date='1977-01-01', length=100, bitrate=100,
|
||||
musicbrainz_id='id1')
|
||||
track2 = Track(uri=u'uri2', name=u'name2',
|
||||
artists=[Artist(name=u'name2')], album=Album(name=u'name2'),
|
||||
track_no=2, date=datetime.date.today()-datetime.timedelta(days=1),
|
||||
length=200, bitrate=200, musicbrainz_id='id2')
|
||||
track_no=2, date='1977-01-02', length=200, bitrate=200,
|
||||
musicbrainz_id='id2')
|
||||
self.assertNotEqual(track1, track2)
|
||||
self.assertNotEqual(hash(track1), hash(track2))
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ class GetClassTest(unittest.TestCase):
|
||||
try:
|
||||
utils.get_class('foo.bar.Baz')
|
||||
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):
|
||||
cls = utils.get_class('unittest.TestCase')
|
||||
|
||||
@ -107,7 +107,7 @@ class SettingsProxyTest(unittest.TestCase):
|
||||
|
||||
def test_setattr_updates_runtime_settings(self):
|
||||
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):
|
||||
self.settings.TEST = 'test'
|
||||
@ -181,34 +181,33 @@ class FormatSettingListTest(unittest.TestCase):
|
||||
def test_contains_the_setting_name(self):
|
||||
self.settings.TEST = u'test'
|
||||
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):
|
||||
self.settings.TEST = u'test'
|
||||
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):
|
||||
self.settings.TEST = 123
|
||||
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):
|
||||
self.settings.TEST = (123, u'abc')
|
||||
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):
|
||||
self.settings.TEST_PASSWORD = u'secret'
|
||||
result = setting_utils.format_settings_list(self.settings)
|
||||
self.assert_("TEST_PASSWORD: u'secret'" not in result, result)
|
||||
self.assert_("TEST_PASSWORD: u'********'" in result, result)
|
||||
self.assertNotIn("TEST_PASSWORD: u'secret'", result, result)
|
||||
self.assertIn("TEST_PASSWORD: u'********'", result, result)
|
||||
|
||||
def test_short_values_are_not_pretty_printed(self):
|
||||
self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',)
|
||||
result = setting_utils.format_settings_list(self.settings)
|
||||
self.assert_("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)" in result,
|
||||
result)
|
||||
self.assertIn("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)", result)
|
||||
|
||||
def test_long_values_are_pretty_printed(self):
|
||||
self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',
|
||||
|
||||
@ -31,10 +31,10 @@ class VersionTest(unittest.TestCase):
|
||||
self.assert_(SV(__version__) < SV('0.8.0'))
|
||||
|
||||
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):
|
||||
self.assert_(platform.python_implementation() in get_python())
|
||||
self.assertIn(platform.python_implementation(), get_python())
|
||||
|
||||
def test_get_python_contains_python_version(self):
|
||||
self.assert_(platform.python_version() in get_python())
|
||||
self.assertIn(platform.python_version(), get_python())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user