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

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

4
.mailmap Normal file
View File

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

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

@ -0,0 +1,19 @@
.. _audio-api:
*********
Audio API
*********
The audio API is the interface we have built around GStreamer to support our
specific use cases. Most backends should be able to get by with simply setting
the URI of the resource they want to play, for these cases the default playback
provider should be used.
For more advanced cases such as when the raw audio data is delivered outside of
GStreamer or the backend needs to add metadata to the currently playing resource,
developers should sub-class the base playback provider and implement the extra
behaviour that is needed through the following API:
.. autoclass:: mopidy.audio.Audio
:members:

View File

@ -1,12 +1,12 @@
.. _backend-provider-api:
.. _backend-api:
********************
Backend provider API
********************
***********
Backend API
***********
The backend provider API is the interface that must be implemented when you
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`

View File

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

View File

@ -1,4 +1,4 @@
.. _backend-concepts:
.. _concepts:
**********************************************
The backend, controller, and provider concepts
@ -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
View File

@ -0,0 +1,50 @@
.. _core-api:
********
Core API
********
The core API is the interface that is used by frontends like
:mod:`mopidy.frontends.mpd`. The core layer is inbetween the frontends and the
backends.
Playback controller
===================
Manages playback, with actions like play, pause, stop, next, previous,
seek, and volume control.
.. autoclass:: mopidy.core.PlaybackState
:members:
.. autoclass:: mopidy.core.PlaybackController
:members:
Current playlist controller
===========================
Manages everything related to the currently loaded playlist.
.. autoclass:: mopidy.core.CurrentPlaylistController
:members:
Stored playlists controller
===========================
Manages stored playlist.
.. autoclass:: mopidy.core.StoredPlaylistsController
:members:
Library controller
==================
Manages the music library, e.g. searching for tracks to be added to a playlist.
.. autoclass:: mopidy.core.LibraryController
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -166,9 +166,9 @@ server simultaneously. To use the SHOUTcast output, do the following:
example, to set the username and password, use:
``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

View File

@ -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()

View File

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

View File

@ -38,6 +38,6 @@ def create_track(label, initial_volume, min_volume, max_volume,
#
# Keep these imports at the bottom of the file to avoid cyclic import problems
# 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

View File

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

View File

@ -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):

View File

@ -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):

View File

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

View File

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

View File

@ -1,550 +1,3 @@
import logging
import random
import time
from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.backends.base')
def option_wrapper(name, default):
def get_option(self):
return getattr(self, name, default)
def set_option(self, value):
if getattr(self, name, default) != value:
self._trigger_options_changed()
return setattr(self, name, value)
return property(get_option, set_option)
class PlaybackController(object):
"""
:param backend: the backend
:type backend: :class:`mopidy.backends.base.Backend`
:param provider: provider the controller should use
:type provider: instance of :class:`BasePlaybackProvider`
"""
# pylint: disable = R0902
# Too many instance attributes
pykka_traversable = True
#: Constant representing the paused state.
PAUSED = u'paused'
#: Constant representing the playing state.
PLAYING = u'playing'
#: Constant representing the stopped state.
STOPPED = u'stopped'
#: :class:`True`
#: Tracks are removed from the playlist when they have been played.
#: :class:`False`
#: Tracks are not removed from the playlist.
consume = option_wrapper('_consume', False)
#: The currently playing or selected track.
#:
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
#: :class:`None`.
current_cp_track = None
#: :class:`True`
#: Tracks are selected at random from the playlist.
#: :class:`False`
#: Tracks are played in the order of the playlist.
random = option_wrapper('_random', False)
#: :class:`True`
#: The current playlist is played repeatedly. To repeat a single track,
#: select both :attr:`repeat` and :attr:`single`.
#: :class:`False`
#: The current playlist is played once.
repeat = option_wrapper('_repeat', False)
#: :class:`True`
#: Playback is stopped after current song, unless in :attr:`repeat`
#: mode.
#: :class:`False`
#: Playback continues after current song.
single = option_wrapper('_single', False)
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
self._state = self.STOPPED
self._shuffled = []
self._first_shuffle = True
self.play_time_accumulated = 0
self.play_time_started = None
def _get_cpid(self, cp_track):
if cp_track is None:
return None
return cp_track.cpid
def _get_track(self, cp_track):
if cp_track is None:
return None
return cp_track.track
@property
def current_cpid(self):
"""
The CPID (current playlist ID) of the currently playing or selected
track.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
return self._get_cpid(self.current_cp_track)
@property
def current_track(self):
"""
The currently playing or selected :class:`mopidy.models.Track`.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
return self._get_track(self.current_cp_track)
@property
def current_playlist_position(self):
"""
The position of the current track in the current playlist.
Read-only.
"""
if self.current_cp_track is None:
return None
try:
return self.backend.current_playlist.cp_tracks.index(
self.current_cp_track)
except ValueError:
return None
@property
def track_at_eot(self):
"""
The track that will be played at the end of the current track.
Read-only. A :class:`mopidy.models.Track` extracted from
:attr:`cp_track_at_eot` for convenience.
"""
return self._get_track(self.cp_track_at_eot)
@property
def cp_track_at_eot(self):
"""
The track that will be played at the end of the current track.
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
Not necessarily the same track as :attr:`cp_track_at_next`.
"""
# pylint: disable = R0911
# Too many return statements
cp_tracks = self.backend.current_playlist.cp_tracks
if not cp_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
self._shuffled = cp_tracks
random.shuffle(self._shuffled)
self._first_shuffle = False
if self.random and self._shuffled:
return self._shuffled[0]
if self.current_cp_track is None:
return cp_tracks[0]
if self.repeat and self.single:
return cp_tracks[self.current_playlist_position]
if self.repeat and not self.single:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
try:
return cp_tracks[self.current_playlist_position + 1]
except IndexError:
return None
@property
def track_at_next(self):
"""
The track that will be played if calling :meth:`next()`.
Read-only. A :class:`mopidy.models.Track` extracted from
:attr:`cp_track_at_next` for convenience.
"""
return self._get_track(self.cp_track_at_next)
@property
def cp_track_at_next(self):
"""
The track that will be played if calling :meth:`next()`.
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
For normal playback this is the next track in the playlist. If repeat
is enabled the next track can loop around the playlist. When random is
enabled this should be a random track, all tracks should be played once
before the list repeats.
"""
cp_tracks = self.backend.current_playlist.cp_tracks
if not cp_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
self._shuffled = cp_tracks
random.shuffle(self._shuffled)
self._first_shuffle = False
if self.random and self._shuffled:
return self._shuffled[0]
if self.current_cp_track is None:
return cp_tracks[0]
if self.repeat:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
try:
return cp_tracks[self.current_playlist_position + 1]
except IndexError:
return None
@property
def track_at_previous(self):
"""
The track that will be played if calling :meth:`previous()`.
Read-only. A :class:`mopidy.models.Track` extracted from
:attr:`cp_track_at_previous` for convenience.
"""
return self._get_track(self.cp_track_at_previous)
@property
def cp_track_at_previous(self):
"""
The track that will be played if calling :meth:`previous()`.
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
For normal playback this is the previous track in the playlist. If
random and/or consume is enabled it should return the current track
instead.
"""
if self.repeat or self.consume or self.random:
return self.current_cp_track
if self.current_playlist_position in (None, 0):
return None
return self.backend.current_playlist.cp_tracks[
self.current_playlist_position - 1]
@property
def state(self):
"""
The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
:attr:`STOPPED`.
Possible states and transitions:
.. digraph:: state_transitions
"STOPPED" -> "PLAYING" [ label="play" ]
"STOPPED" -> "PAUSED" [ label="pause" ]
"PLAYING" -> "STOPPED" [ label="stop" ]
"PLAYING" -> "PAUSED" [ label="pause" ]
"PLAYING" -> "PLAYING" [ label="play" ]
"PAUSED" -> "PLAYING" [ label="resume" ]
"PAUSED" -> "STOPPED" [ label="stop" ]
"""
return self._state
@state.setter
def state(self, new_state):
(old_state, self._state) = (self.state, new_state)
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
self._trigger_playback_state_changed()
# FIXME play_time stuff assumes backend does not have a better way of
# handeling this stuff :/
if (old_state in (self.PLAYING, self.STOPPED)
and new_state == self.PLAYING):
self._play_time_start()
elif old_state == self.PLAYING and new_state == self.PAUSED:
self._play_time_pause()
elif old_state == self.PAUSED and new_state == self.PLAYING:
self._play_time_resume()
@property
def time_position(self):
"""Time position in milliseconds."""
if self.state == self.PLAYING:
time_since_started = (self._current_wall_time -
self.play_time_started)
return self.play_time_accumulated + time_since_started
elif self.state == self.PAUSED:
return self.play_time_accumulated
elif self.state == self.STOPPED:
return 0
def _play_time_start(self):
self.play_time_accumulated = 0
self.play_time_started = self._current_wall_time
def _play_time_pause(self):
time_since_started = self._current_wall_time - self.play_time_started
self.play_time_accumulated += time_since_started
def _play_time_resume(self):
self.play_time_started = self._current_wall_time
@property
def _current_wall_time(self):
return int(time.time() * 1000)
@property
def volume(self):
return self.provider.get_volume()
@volume.setter
def volume(self, volume):
self.provider.set_volume(volume)
def change_track(self, cp_track, on_error_step=1):
"""
Change to the given track, keeping the current playback state.
:param cp_track: track to change to
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track
:type on_error_step: int, -1 or 1
"""
old_state = self.state
self.stop()
self.current_cp_track = cp_track
if old_state == self.PLAYING:
self.play(on_error_step=on_error_step)
elif old_state == self.PAUSED:
self.pause()
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
"""
if self.state == self.STOPPED:
return
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
self._trigger_track_playback_ended()
self.play(self.cp_track_at_eot)
else:
self.stop(clear_current_track=True)
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
def on_current_playlist_change(self):
"""
Tell the playback controller that the current playlist has changed.
Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
"""
self._first_shuffle = True
self._shuffled = []
if (not self.backend.current_playlist.cp_tracks or
self.current_cp_track not in
self.backend.current_playlist.cp_tracks):
self.stop(clear_current_track=True)
def next(self):
"""
Change to the next track.
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
if self.cp_track_at_next:
self._trigger_track_playback_ended()
self.change_track(self.cp_track_at_next)
else:
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
if self.provider.pause():
self.state = self.PAUSED
self._trigger_track_playback_paused()
def play(self, cp_track=None, on_error_step=1):
"""
Play the given track, or if the given track is :class:`None`, play the
currently active track.
:param cp_track: track to play
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track
:type on_error_step: int, -1 or 1
"""
if cp_track is not None:
assert cp_track in self.backend.current_playlist.cp_tracks
elif cp_track is None:
if self.state == self.PAUSED:
return self.resume()
elif self.current_cp_track is not None:
cp_track = self.current_cp_track
elif self.current_cp_track is None and on_error_step == 1:
cp_track = self.cp_track_at_next
elif self.current_cp_track is None and on_error_step == -1:
cp_track = self.cp_track_at_previous
if cp_track is not None:
self.current_cp_track = cp_track
self.state = self.PLAYING
if not self.provider.play(cp_track.track):
# Track is not playable
if self.random and self._shuffled:
self._shuffled.remove(cp_track)
if on_error_step == 1:
self.next()
elif on_error_step == -1:
self.previous()
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
self._trigger_track_playback_started()
def previous(self):
"""
Change to the previous track.
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
self._trigger_track_playback_ended()
self.change_track(self.cp_track_at_previous, on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""
if self.state == self.PAUSED and self.provider.resume():
self.state = self.PLAYING
self._trigger_track_playback_resumed()
def seek(self, time_position):
"""
Seeks to time position given in milliseconds.
:param time_position: time position in milliseconds
:type time_position: int
:rtype: :class:`True` if successful, else :class:`False`
"""
if not self.backend.current_playlist.tracks:
return False
if self.state == self.STOPPED:
self.play()
elif self.state == self.PAUSED:
self.resume()
if time_position < 0:
time_position = 0
elif time_position > self.current_track.length:
self.next()
return True
self.play_time_started = self._current_wall_time
self.play_time_accumulated = time_position
success = self.provider.seek(time_position)
if success:
self._trigger_seeked()
return success
def stop(self, clear_current_track=False):
"""
Stop playing.
:param clear_current_track: whether to clear the current track _after_
stopping
:type clear_current_track: boolean
"""
if self.state != self.STOPPED:
if self.provider.stop():
self._trigger_track_playback_ended()
self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
def _trigger_track_playback_paused(self):
logger.debug(u'Triggering track playback paused event')
if self.current_track is None:
return
BackendListener.send('track_playback_paused',
track=self.current_track,
time_position=self.time_position)
def _trigger_track_playback_resumed(self):
logger.debug(u'Triggering track playback resumed event')
if self.current_track is None:
return
BackendListener.send('track_playback_resumed',
track=self.current_track,
time_position=self.time_position)
def _trigger_track_playback_started(self):
logger.debug(u'Triggering track playback started event')
if self.current_track is None:
return
BackendListener.send('track_playback_started',
track=self.current_track)
def _trigger_track_playback_ended(self):
logger.debug(u'Triggering track playback ended event')
if self.current_track is None:
return
BackendListener.send('track_playback_ended',
track=self.current_track,
time_position=self.time_position)
def _trigger_playback_state_changed(self):
logger.debug(u'Triggering playback state change event')
BackendListener.send('playback_state_changed')
def _trigger_options_changed(self):
logger.debug(u'Triggering options changed event')
BackendListener.send('options_changed')
def _trigger_seeked(self):
logger.debug(u'Triggering seeked event')
BackendListener.send('seeked')
class BasePlaybackProvider(object):
"""
: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)

View File

@ -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):

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -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()

View File

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

View File

@ -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
View File

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

View File

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

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

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

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

@ -0,0 +1,557 @@
import logging
import random
import time
from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.backends.base')
def option_wrapper(name, default):
def get_option(self):
return getattr(self, name, default)
def set_option(self, value):
if getattr(self, name, default) != value:
self._trigger_options_changed()
return setattr(self, name, value)
return property(get_option, set_option)
class PlaybackState(object):
"""
Enum of playback states.
"""
#: Constant representing the paused state.
PAUSED = u'paused'
#: Constant representing the playing state.
PLAYING = u'playing'
#: Constant representing the stopped state.
STOPPED = u'stopped'
class PlaybackController(object):
"""
:param backend: the backend
:type backend: :class:`mopidy.backends.base.Backend`
:param provider: provider the controller should use
:type provider: instance of :class:`BasePlaybackProvider`
"""
# pylint: disable = R0902
# Too many instance attributes
pykka_traversable = True
#: :class:`True`
#: Tracks are removed from the playlist when they have been played.
#: :class:`False`
#: Tracks are not removed from the playlist.
consume = option_wrapper('_consume', False)
#: The currently playing or selected track.
#:
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
#: :class:`None`.
current_cp_track = None
#: :class:`True`
#: Tracks are selected at random from the playlist.
#: :class:`False`
#: Tracks are played in the order of the playlist.
random = option_wrapper('_random', False)
#: :class:`True`
#: The current playlist is played repeatedly. To repeat a single track,
#: select both :attr:`repeat` and :attr:`single`.
#: :class:`False`
#: The current playlist is played once.
repeat = option_wrapper('_repeat', False)
#: :class:`True`
#: Playback is stopped after current song, unless in :attr:`repeat`
#: mode.
#: :class:`False`
#: Playback continues after current song.
single = option_wrapper('_single', False)
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
self._state = PlaybackState.STOPPED
self._shuffled = []
self._first_shuffle = True
self.play_time_accumulated = 0
self.play_time_started = None
def _get_cpid(self, cp_track):
if cp_track is None:
return None
return cp_track.cpid
def _get_track(self, cp_track):
if cp_track is None:
return None
return cp_track.track
@property
def current_cpid(self):
"""
The CPID (current playlist ID) of the currently playing or selected
track.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
return self._get_cpid(self.current_cp_track)
@property
def current_track(self):
"""
The currently playing or selected :class:`mopidy.models.Track`.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
return self._get_track(self.current_cp_track)
@property
def current_playlist_position(self):
"""
The position of the current track in the current playlist.
Read-only.
"""
if self.current_cp_track is None:
return None
try:
return self.backend.current_playlist.cp_tracks.index(
self.current_cp_track)
except ValueError:
return None
@property
def track_at_eot(self):
"""
The track that will be played at the end of the current track.
Read-only. A :class:`mopidy.models.Track` extracted from
:attr:`cp_track_at_eot` for convenience.
"""
return self._get_track(self.cp_track_at_eot)
@property
def cp_track_at_eot(self):
"""
The track that will be played at the end of the current track.
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
Not necessarily the same track as :attr:`cp_track_at_next`.
"""
# pylint: disable = R0911
# Too many return statements
cp_tracks = self.backend.current_playlist.cp_tracks
if not cp_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
self._shuffled = cp_tracks
random.shuffle(self._shuffled)
self._first_shuffle = False
if self.random and self._shuffled:
return self._shuffled[0]
if self.current_cp_track is None:
return cp_tracks[0]
if self.repeat and self.single:
return cp_tracks[self.current_playlist_position]
if self.repeat and not self.single:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
try:
return cp_tracks[self.current_playlist_position + 1]
except IndexError:
return None
@property
def track_at_next(self):
"""
The track that will be played if calling :meth:`next()`.
Read-only. A :class:`mopidy.models.Track` extracted from
:attr:`cp_track_at_next` for convenience.
"""
return self._get_track(self.cp_track_at_next)
@property
def cp_track_at_next(self):
"""
The track that will be played if calling :meth:`next()`.
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
For normal playback this is the next track in the playlist. If repeat
is enabled the next track can loop around the playlist. When random is
enabled this should be a random track, all tracks should be played once
before the list repeats.
"""
cp_tracks = self.backend.current_playlist.cp_tracks
if not cp_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
self._shuffled = cp_tracks
random.shuffle(self._shuffled)
self._first_shuffle = False
if self.random and self._shuffled:
return self._shuffled[0]
if self.current_cp_track is None:
return cp_tracks[0]
if self.repeat:
return cp_tracks[
(self.current_playlist_position + 1) % len(cp_tracks)]
try:
return cp_tracks[self.current_playlist_position + 1]
except IndexError:
return None
@property
def track_at_previous(self):
"""
The track that will be played if calling :meth:`previous()`.
Read-only. A :class:`mopidy.models.Track` extracted from
:attr:`cp_track_at_previous` for convenience.
"""
return self._get_track(self.cp_track_at_previous)
@property
def cp_track_at_previous(self):
"""
The track that will be played if calling :meth:`previous()`.
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
For normal playback this is the previous track in the playlist. If
random and/or consume is enabled it should return the current track
instead.
"""
if self.repeat or self.consume or self.random:
return self.current_cp_track
if self.current_playlist_position in (None, 0):
return None
return self.backend.current_playlist.cp_tracks[
self.current_playlist_position - 1]
@property
def state(self):
"""
The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or
:attr:`STOPPED`.
Possible states and transitions:
.. digraph:: state_transitions
"STOPPED" -> "PLAYING" [ label="play" ]
"STOPPED" -> "PAUSED" [ label="pause" ]
"PLAYING" -> "STOPPED" [ label="stop" ]
"PLAYING" -> "PAUSED" [ label="pause" ]
"PLAYING" -> "PLAYING" [ label="play" ]
"PAUSED" -> "PLAYING" [ label="resume" ]
"PAUSED" -> "STOPPED" [ label="stop" ]
"""
return self._state
@state.setter
def state(self, new_state):
(old_state, self._state) = (self.state, new_state)
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
self._trigger_playback_state_changed()
# FIXME play_time stuff assumes backend does not have a better way of
# handeling this stuff :/
if (old_state in (PlaybackState.PLAYING, PlaybackState.STOPPED)
and new_state == PlaybackState.PLAYING):
self._play_time_start()
elif (old_state == PlaybackState.PLAYING
and new_state == PlaybackState.PAUSED):
self._play_time_pause()
elif (old_state == PlaybackState.PAUSED
and new_state == PlaybackState.PLAYING):
self._play_time_resume()
@property
def time_position(self):
"""Time position in milliseconds."""
if self.state == PlaybackState.PLAYING:
time_since_started = (self._current_wall_time -
self.play_time_started)
return self.play_time_accumulated + time_since_started
elif self.state == PlaybackState.PAUSED:
return self.play_time_accumulated
elif self.state == PlaybackState.STOPPED:
return 0
def _play_time_start(self):
self.play_time_accumulated = 0
self.play_time_started = self._current_wall_time
def _play_time_pause(self):
time_since_started = self._current_wall_time - self.play_time_started
self.play_time_accumulated += time_since_started
def _play_time_resume(self):
self.play_time_started = self._current_wall_time
@property
def _current_wall_time(self):
return int(time.time() * 1000)
@property
def volume(self):
return self.provider.get_volume()
@volume.setter
def volume(self, volume):
self.provider.set_volume(volume)
def change_track(self, cp_track, on_error_step=1):
"""
Change to the given track, keeping the current playback state.
:param cp_track: track to change to
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track
:type on_error_step: int, -1 or 1
"""
old_state = self.state
self.stop()
self.current_cp_track = cp_track
if old_state == PlaybackState.PLAYING:
self.play(on_error_step=on_error_step)
elif old_state == PlaybackState.PAUSED:
self.pause()
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
"""
if self.state == PlaybackState.STOPPED:
return
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
self._trigger_track_playback_ended()
self.play(self.cp_track_at_eot)
else:
self.stop(clear_current_track=True)
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
def on_current_playlist_change(self):
"""
Tell the playback controller that the current playlist has changed.
Used by :class:`mopidy.backends.base.CurrentPlaylistController`.
"""
self._first_shuffle = True
self._shuffled = []
if (not self.backend.current_playlist.cp_tracks or
self.current_cp_track not in
self.backend.current_playlist.cp_tracks):
self.stop(clear_current_track=True)
def next(self):
"""
Change to the next track.
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
if self.cp_track_at_next:
self._trigger_track_playback_ended()
self.change_track(self.cp_track_at_next)
else:
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
if self.provider.pause():
self.state = PlaybackState.PAUSED
self._trigger_track_playback_paused()
def play(self, cp_track=None, on_error_step=1):
"""
Play the given track, or if the given track is :class:`None`, play the
currently active track.
:param cp_track: track to play
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track
:type on_error_step: int, -1 or 1
"""
if cp_track is not None:
assert cp_track in self.backend.current_playlist.cp_tracks
elif cp_track is None:
if self.state == PlaybackState.PAUSED:
return self.resume()
elif self.current_cp_track is not None:
cp_track = self.current_cp_track
elif self.current_cp_track is None and on_error_step == 1:
cp_track = self.cp_track_at_next
elif self.current_cp_track is None and on_error_step == -1:
cp_track = self.cp_track_at_previous
if cp_track is not None:
self.current_cp_track = cp_track
self.state = PlaybackState.PLAYING
if not self.provider.play(cp_track.track):
# Track is not playable
if self.random and self._shuffled:
self._shuffled.remove(cp_track)
if on_error_step == 1:
self.next()
elif on_error_step == -1:
self.previous()
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
self._trigger_track_playback_started()
def previous(self):
"""
Change to the previous track.
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
self._trigger_track_playback_ended()
self.change_track(self.cp_track_at_previous, on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""
if self.state == PlaybackState.PAUSED and self.provider.resume():
self.state = PlaybackState.PLAYING
self._trigger_track_playback_resumed()
def seek(self, time_position):
"""
Seeks to time position given in milliseconds.
:param time_position: time position in milliseconds
:type time_position: int
:rtype: :class:`True` if successful, else :class:`False`
"""
if not self.backend.current_playlist.tracks:
return False
if self.state == PlaybackState.STOPPED:
self.play()
elif self.state == PlaybackState.PAUSED:
self.resume()
if time_position < 0:
time_position = 0
elif time_position > self.current_track.length:
self.next()
return True
self.play_time_started = self._current_wall_time
self.play_time_accumulated = time_position
success = self.provider.seek(time_position)
if success:
self._trigger_seeked()
return success
def stop(self, clear_current_track=False):
"""
Stop playing.
:param clear_current_track: whether to clear the current track _after_
stopping
:type clear_current_track: boolean
"""
if self.state != PlaybackState.STOPPED:
if self.provider.stop():
self._trigger_track_playback_ended()
self.state = PlaybackState.STOPPED
if clear_current_track:
self.current_cp_track = None
def _trigger_track_playback_paused(self):
logger.debug(u'Triggering track playback paused event')
if self.current_track is None:
return
BackendListener.send('track_playback_paused',
track=self.current_track,
time_position=self.time_position)
def _trigger_track_playback_resumed(self):
logger.debug(u'Triggering track playback resumed event')
if self.current_track is None:
return
BackendListener.send('track_playback_resumed',
track=self.current_track,
time_position=self.time_position)
def _trigger_track_playback_started(self):
logger.debug(u'Triggering track playback started event')
if self.current_track is None:
return
BackendListener.send('track_playback_started',
track=self.current_track)
def _trigger_track_playback_ended(self):
logger.debug(u'Triggering track playback ended event')
if self.current_track is None:
return
BackendListener.send('track_playback_ended',
track=self.current_track,
time_position=self.time_position)
def _trigger_playback_state_changed(self):
logger.debug(u'Triggering playback state change event')
BackendListener.send('playback_state_changed')
def _trigger_options_changed(self):
logger.debug(u'Triggering options changed event')
BackendListener.send('options_changed')
def _trigger_seeked(self):
logger.debug(u'Triggering seeked event')
BackendListener.send('seeked')

View File

@ -0,0 +1,113 @@
class StoredPlaylistsController(object):
"""
:param backend: backend the controller is a part of
:type backend: :class:`mopidy.backends.base.Backend`
:param provider: provider the controller should use
:type provider: instance of :class:`BaseStoredPlaylistsProvider`
"""
pykka_traversable = True
def __init__(self, backend, provider):
self.backend = backend
self.provider = provider
@property
def playlists(self):
"""
Currently stored playlists.
Read/write. List of :class:`mopidy.models.Playlist`.
"""
return self.provider.playlists
@playlists.setter
def playlists(self, playlists):
self.provider.playlists = playlists
def create(self, name):
"""
Create a new playlist.
:param name: name of the new playlist
:type name: string
:rtype: :class:`mopidy.models.Playlist`
"""
return self.provider.create(name)
def delete(self, playlist):
"""
Delete playlist.
:param playlist: the playlist to delete
:type playlist: :class:`mopidy.models.Playlist`
"""
return self.provider.delete(playlist)
def get(self, **criteria):
"""
Get playlist by given criterias from the set of stored playlists.
Raises :exc:`LookupError` if a unique match is not found.
Examples::
get(name='a') # Returns track with name 'a'
get(uri='xyz') # Returns track with URI 'xyz'
get(name='a', uri='xyz') # Returns track with name 'a' and URI
# 'xyz'
:param criteria: one or more criteria to match by
:type criteria: dict
:rtype: :class:`mopidy.models.Playlist`
"""
matches = self.playlists
for (key, value) in criteria.iteritems():
matches = filter(lambda p: getattr(p, key) == value, matches)
if len(matches) == 1:
return matches[0]
criteria_string = ', '.join(
['%s=%s' % (k, v) for (k, v) in criteria.iteritems()])
if len(matches) == 0:
raise LookupError('"%s" match no playlists' % criteria_string)
else:
raise LookupError('"%s" match multiple playlists'
% criteria_string)
def lookup(self, uri):
"""
Lookup playlist with given URI in both the set of stored playlists and
in any other playlist sources.
:param uri: playlist URI
:type uri: string
:rtype: :class:`mopidy.models.Playlist`
"""
return self.provider.lookup(uri)
def refresh(self):
"""
Refresh the stored playlists in
:attr:`mopidy.backends.base.StoredPlaylistsController.playlists`.
"""
return self.provider.refresh()
def rename(self, playlist, new_name):
"""
Rename playlist.
:param playlist: the playlist
:type playlist: :class:`mopidy.models.Playlist`
:param new_name: the new name
:type new_name: string
"""
return self.provider.rename(playlist, new_name)
def save(self, playlist):
"""
Save the playlist to the set of stored playlists.
:param playlist: the playlist
:type playlist: :class:`mopidy.models.Playlist`
"""
return self.provider.save(playlist)

View File

@ -242,7 +242,7 @@ def _list_date(context, query):
playlist = context.backend.library.find_exact(**query).get()
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>[^"]+)"')

View File

@ -1,4 +1,4 @@
from mopidy.backends.base import PlaybackController
from mopidy.core import PlaybackState
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.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()

View File

@ -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):

View File

@ -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):

View File

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

View File

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

View File

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

View File

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

View File

@ -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'])

View File

@ -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()

View File

@ -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):

View File

@ -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
View File

View 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)

View File

@ -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):

View File

@ -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')

View File

@ -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):

View File

@ -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')]

View File

@ -1,13 +1,15 @@
from mopidy.backends import dummy as backend
from mopidy.backends import dummy
from mopidy.core import PlaybackState
from mopidy.frontends.mpd import dispatcher
from mopidy.frontends.mpd.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)

View File

@ -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):

View File

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

View File

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

View File

@ -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')

View File

@ -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',

View File

@ -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())