Merge branch 'gstreamer'

This commit is contained in:
Stein Magnus Jodal 2010-08-18 20:00:04 +02:00
commit 4f0611e070
73 changed files with 2001 additions and 1303 deletions

View File

@ -1,9 +0,0 @@
Authors
=======
Contributors to Mopidy in the order of appearance:
- Stein Magnus Jodal <stein.magnus@jodal.no>
- Johannes Knutsen <johannes@knutseninfo.no>
- Thomas Adamcik <adamcik@samfundet.no>
- Kristian Klette <klette@klette.us>

View File

@ -1,4 +1,5 @@
include COPYING pylintrc *.rst *.txt
include LICENSE pylintrc *.rst *.txt
include mopidy/backends/libspotify/spotify_appkey.key
recursive-include docs *
prune docs/_build
recursive-include tests *.py

View File

@ -2,10 +2,11 @@
Mopidy
******
Mopidy is an `Music Player Daemon <http://mpd.wikia.com/>`_ (MPD) server with a
`Spotify <http://www.spotify.com/>`_ backend. Using a standard MPD client you
can search for music in Spotify's vast archive, manage Spotify playlists and
play music from Spotify.
Mopidy is a music server which can play music from `Spotify
<http://www.spotify.com/>`_ or from your local hard drive. To search for music
in Spotify's vast archive, manage playlists, and play music, you can use most
`MPD clients <http://mpd.wikia.com/>`_. MPD clients are available for most
platforms, including Windows, Mac OS X, Linux, and iPhone and Android phones.
To install Mopidy, check out
`the installation docs <http://www.mopidy.com/docs/installation/>`_.
@ -14,4 +15,3 @@ To install Mopidy, check out
* `Source code <http://github.com/jodal/mopidy>`_
* `Issue tracker <http://github.com/jodal/mopidy/issues>`_
* IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
* `Presentation of Mopidy <http://www.slideshare.net/jodal/mopidy-3380516>`_

View File

@ -82,14 +82,6 @@ Manages the music library, e.g. searching for tracks to be added to a playlist.
:undoc-members:
:mod:`mopidy.backends.despotify` -- Despotify backend
=====================================================
.. automodule:: mopidy.backends.despotify
:synopsis: Spotify backend using the Despotify library
:members:
:mod:`mopidy.backends.dummy` -- Dummy backend for testing
=========================================================

View File

@ -67,6 +67,16 @@ methods as described below.
.. inheritance-diagram:: mopidy.mixers.dummy
:mod:`mopidy.mixers.gstreamer_software` -- Software mixer for all platforms
===========================================================================
.. automodule:: mopidy.mixers.gstreamer_software
:synopsis: Software mixer for all platforms
:members:
.. inheritance-diagram:: mopidy.mixers.gstreamer_software
:mod:`mopidy.mixers.osa` -- Osa mixer for OS X
==============================================

View File

@ -13,7 +13,7 @@ there.
A complete ``~/.mopidy/settings.py`` may look like this::
MPD_SERVER_HOSTNAME = u'0.0.0.0'
MPD_SERVER_HOSTNAME = u'::'
SPOTIFY_USERNAME = u'alice'
SPOTIFY_PASSWORD = u'mysecret'

View File

@ -1 +1,22 @@
.. include:: ../AUTHORS.rst
*******
Authors
*******
Contributors to Mopidy in the order of appearance:
- Stein Magnus Jodal <stein.magnus@jodal.no>
- Johannes Knutsen <johannes@knutseninfo.no>
- Thomas Adamcik <adamcik@samfundet.no>
- Kristian Klette <klette@klette.us>
Donations
=========
If you already enjoy Mopidy, or don't enjoy it and want to help us making
Mopidy better, you can `donate money <http://pledgie.com/campaigns/12647>`_ to
Mopidy's development.
Any donated money will be used to cover service subscriptions (e.g. Spotify
and Last.fm) and hardware devices (e.g. an used iPod Touch for testing Mopidy
with MPod) needed for developing Mopidy.

View File

@ -8,45 +8,117 @@ This change log is used to track all major changes to Mopidy.
0.1.0a4 (in development)
========================
Another great release.
The greatest release ever! We present to you important improvements in search
functionality, working track position seeking, no known stability issues, and
greatly improved MPD client support.
**Changes**
**Important changes**
- License changed from GPLv2 to Apache License, version 2.0.
- GStreamer is now a required dependency.
- :mod:`mopidy.backends.libspotify` is now the default backend.
:mod:`mopidy.backends.despotify` is no longer available. This means that you
need to install the :doc:`dependencies for libspotify
<installation/libspotify>`.
- If you used :mod:`mopidy.backends.libspotify` previously, pyspotify must be
updated when updating to this release, to get working seek functionality.
- :attr:`mopidy.settings.SERVER_HOSTNAME` and
:attr:`mopidy.settings.SERVER_PORT` has been renamed to
:attr:`mopidy.settings.MPD_SERVER_HOSTNAME` and
:attr:`mopidy.settings.MPD_SERVER_PORT` to allow for multiple frontends in
the future.
**Changes**
- Exit early if not Python >= 2.6, < 3.
- Validate settings at startup and print useful error messages if the settings
has not been updated or anything is misspelled.
- Add command line option :option:`--list-settings` to print the currently
active settings.
- Include Sphinx scripts for building docs, pylintrc, tests and test data in
the packages created by ``setup.py`` for i.e. PyPI.
- Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`.
- Changed ``SERVER_HOSTNAME`` and ``SERVER_PORT`` settings to
``MPD_SERVER_HOSTNAME`` and ``MPD_SERVER_PORT``
- MPD frontend:
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
- Split gigantic protocol implementation into eleven modules.
- Search improvements, including support for multi-word search.
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty.
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty
or when a current track is set.
- Support ``plchanges "-1"`` to work better with MPDroid.
- Support ``pause`` without arguments to work better with MPDroid.
- Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and
``single`` without quotes to work better with BitMPC.
- Fixed delete current playing track from playlist, which crashed several
clients.
- Fixed deletion of the currently playing track from the current playlist,
which crashed several clients.
- Implement ``seek`` and ``seekid``.
- Fix ``playlistfind`` output so the correct song is played when playing
songs directly from search results in GMPC.
- Fix ``load`` so that one can append a playlist to the current playlist, and
make it return the correct error message if the playlist is not found.
- Support for single track repeat added. (Fixes: :issue:`4`)
- Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming
in backends.
- Backends:
- Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`.
- Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained
and the Libspotify backend is working much better. (Fixes: :issue:`9`,
:issue:`10`, :issue:`13`)
- A Spotify application key is now bundled with the source.
:attr:`mopidy.settings.SPOTIFY_LIB_APPKEY` is thus removed.
- If failing to play a track, playback will skip to the next track.
- Mixers:
- Added new :mod:`mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer`
which now is the default mixer on all platforms.
- New setting :attr:`mopidy.settings.MIXER_MAX_VOLUME` for capping the
maximum output volume.
- Backend API:
- Relocate from :mod:`mopidy.backends` to :mod:`mopidy.backends.base`.
- The ``id`` field of :class:`mopidy.models.Track` has been removed, as it is
no longer needed after the CPID refactoring.
- :meth:`mopidy.backends.base.BaseBackend()` now accepts an
``output_queue`` which it can use to send messages (i.e. audio data)
to the output process.
- :meth:`mopidy.backends.base.BaseLibraryController.find_exact()` now accepts
keyword arguments of the form ``find_exact(artist=['foo'],
album=['bar'])``.
- :meth:`mopidy.backends.base.BaseLibraryController.search()` now accepts
keyword arguments of the form ``search(artist=['foo', 'fighters'],
album=['bar', 'grooves'])``.
- :meth:`mopidy.backends.base.BaseCurrentPlaylistController.append()`
replaces
:meth:`mopidy.backends.base.BaseCurrentPlaylistController.load()`. Use
:meth:`mopidy.backends.base.BaseCurrentPlaylistController.clear()` if you
want to clear the current playlist.
- The following fields in
:class:`mopidy.backends.base.BasePlaybackController` has been renamed to
reflect their relation to methods called on the controller:
- ``next_track`` to ``track_at_next``
- ``next_cp_track`` to ``cp_track_at_next``
- ``previous_track`` to ``track_at_previous``
- ``previous_cp_track`` to ``cp_track_at_previous``
- :attr:`mopidy.backends.base.BasePlaybackController.track_at_eot` and
:attr:`mopidy.backends.base.BasePlaybackController.cp_track_at_eot` has
been added to better handle the difference between the user pressing next
and the current track ending.
- Rename
:meth:`mopidy.backends.base.BasePlaybackController.new_playlist_loaded_callback()`
to
:meth:`mopidy.backends.base.BasePlaybackController.on_current_playlist_change()`.
- Rename
:meth:`mopidy.backends.base.BasePlaybackController.end_of_track_callback()`
to :meth:`mopidy.backends.base.BasePlaybackController.on_end_of_track()`.
- Remove :meth:`mopidy.backends.base.BaseStoredPlaylistsController.search()`
since it was barely used, untested, and we got no use case for non-exact
search in stored playlists yet. Use
:meth:`mopidy.backends.base.BaseStoredPlaylistsController.get()` instead.
0.1.0a3 (2010-08-03)
@ -137,8 +209,8 @@ the established pace of at least a release per month.
- Improvements to MPD protocol handling, making Mopidy work much better with a
group of clients, including ncmpc, MPoD, and Theremin.
- New command line flag ``--dump`` for dumping debug log to ``dump.log`` in the
current directory.
- New command line flag :option:`--dump` for dumping debug log to ``dump.log``
in the current directory.
- New setting :attr:`mopidy.settings.MIXER_ALSA_CONTROL` for forcing what ALSA
control :class:`mopidy.mixers.alsa.AlsaMixer` should use.

View File

@ -11,8 +11,7 @@ Version 0.1
- Core MPD server functionality working. Gracefully handle clients' use of
non-supported functionality.
- Read-only support for Spotify through :mod:`mopidy.backends.despotify` and/or
:mod:`mopidy.backends.libspotify`.
- Read-only support for Spotify through :mod:`mopidy.backends.libspotify`.
- Initial support for local file playback through
:mod:`mopidy.backends.local`. The state of local file playback will not
block the release of 0.1.
@ -29,37 +28,53 @@ released when we reach the other goal.
possible to have both Spotify tracks and local tracks in the same playlist.
Stuff we really want to do, but just not right now
==================================================
Stuff we want to do, but not right now, and maybe never
=======================================================
- Replace libspotify with `openspotify
<http://github.com/noahwilliamsson/openspotify>`_ for
:mod:`mopidy.backends.libspotify`. *Update:* Seems like openspotify
development has stalled.
- Create `Debian packages <http://www.debian.org/doc/maint-guide/>`_ of all our
dependencies and Mopidy itself (hosted in our own Debian repo until we get
stuff into the various distros) to make Debian/Ubuntu installation a breeze.
- **[WIP]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_ recipies for
all our dependencies and Mopidy itself to make OS X installation a breeze.
- Run frontend tests against a real MPD server to ensure we are in sync.
- Start working with MPD client maintainers to get rid of weird assumptions
like only searching for first two letters and doing the rest of the filtering
locally in the client, etc.
- Packaging and distribution:
- **[PENDING]** Create `Homebrew <http://mxcl.github.com/homebrew/>`_
recipies for all our dependencies and Mopidy itself to make OS X
installation a breeze. See `Homebrew's issue #1612
<http://github.com/mxcl/homebrew/issues/issue/1612>`_.
- Create `Debian packages <http://www.debian.org/doc/maint-guide/>`_ of all
our dependencies and Mopidy itself (hosted in our own Debian repo until we
get stuff into the various distros) to make Debian/Ubuntu installation a
breeze.
Crazy stuff we had to write down somewhere
==========================================
- Compatability:
- Add an `XMMS2 <http://www.xmms2.org/>`_ frontend, so Mopidy can serve XMMS2
clients.
- Add support for serving the music as an `Icecast <http://www.icecast.org/>`_
stream instead of playing it locally.
- Integrate with `Squeezebox <http://www.logitechsqueezebox.com/>`_ in some
way.
- AirPort Express support, like in
`PulseAudio <http://git.0pointer.de/?p=pulseaudio.git;a=blob;f=src/modules/raop/raop_client.c;hb=HEAD>`_.
- DNLA and/or UPnP support. Maybe using
`Coherence <http://coherence-project.org/>`_.
- `Media Player Remote Interfacing Specification
<http://en.wikipedia.org/wiki/Media_Player_Remote_Interfacing_Specification>`_
support.
- Run frontend tests against a real MPD server to ensure we are in sync.
- Start working with MPD client maintainers to get rid of weird assumptions
like only searching for first two letters and doing the rest of the
filtering locally in the client (:issue:`1`), etc.
- Backends:
- `Last.fm <http://www.last.fm/api>`_
- `WIMP <http://twitter.com/wimp/status/8975885632>`_
- DNLA/UPnP to Mopidy can play music from other DNLA MediaServers.
- Frontends:
- Publish the server's presence to the network using `Zeroconf
<http://en.wikipedia.org/wiki/Zeroconf>`_/Avahi.
- D-Bus/`MPRIS <http://www.mpris.org/>`_
- REST/JSON web service with a jQuery client as example application. Maybe
based upon `Tornado <http://github.com/facebook/tornado>`_ and `jQuery
Mobile <http://jquerymobile.com/>`_.
- DNLA/UPnP to Mopidy can be controlled from i.e. TVs.
- `XMMS2 <http://www.xmms2.org/>`_
- LIRC frontend for controlling Mopidy with a remote.
- Mixers:
- LIRC mixer for controlling arbitrary amplifiers remotely.
- Audio streaming:
- Ogg Vorbis/MP3 audio stream over HTTP, to MPD clients, `Squeezeboxes
<http://www.logitechsqueezebox.com/>`_, etc.
- Feed audio to an `Icecast <http://www.icecast.org/>`_ server.
- Stream to AirPort Express using `RAOP
<http://en.wikipedia.org/wiki/Remote_Audio_Output_Protocol>`_.

View File

@ -1,73 +0,0 @@
**********************
Despotify installation
**********************
To use the `Despotify <http://despotify.se/>`_ backend, you first need to
install Despotify and spytify.
.. warning::
This backend requires a Spotify premium account.
Installing Despotify on Linux
=============================
Install Despotify's dependencies. At Debian/Ubuntu systems::
sudo aptitude install libssl-dev zlib1g-dev libvorbis-dev \
libtool libncursesw5-dev libao-dev python-dev
Check out revision 508 of the Despotify source code::
svn checkout https://despotify.svn.sourceforge.net/svnroot/despotify@508
Build and install Despotify::
cd despotify/src/
sudo make install
When Despotify has been installed, continue with :ref:`spytify_installation`.
Installing Despotify on OS X
============================
In OS X you need to have `XCode <http://developer.apple.com/tools/xcode/>`_ and
`Homebrew <http://mxcl.github.com/homebrew/>`_ installed. Then, to install
Despotify::
brew install despotify
When Despotify has been installed, continue with :ref:`spytify_installation`.
.. _spytify_installation:
Installing spytify
==================
spytify's source comes bundled with despotify. If you haven't already checkout
out the despotify source, do it now::
svn checkout https://despotify.svn.sourceforge.net/svnroot/despotify@508
Build and install spytify::
cd despotify/src/bindings/python/
export PKG_CONFIG_PATH=../../lib # Needed on OS X
sudo make install
Testing the installation
========================
To validate that everything is working, run the ``test.py`` script which is
distributed with spytify::
python test.py
The test script should ask for your username and password (which must be for a
Spotify Premium account), ask for a search query, list all your playlists with
tracks, play 10s from a random song from the search result, pause for two
seconds, play for five more seconds, and quit.

View File

@ -2,12 +2,10 @@
Installation
************
Mopidy itself is a breeze to install, as it just requires a standard Python
installation and the GStreamer library. The libraries we depend on to connect
to the Spotify service is far more tricky to get working for the time being.
Until installation of these libraries are either well documented by their
developers, or the libraries are packaged for various Linux distributions, we
will supply our own installation guides, as linked to below.
To get a basic version of Mopidy running, you need Python and the GStreamer
library. To use Spotify with Mopidy, you also need :doc:`libspotify and
pyspotify <libspotify>`. Mopidy itself can either be installed from the Python
package index, PyPI, or from git.
Install dependencies
@ -18,12 +16,11 @@ Install dependencies
gstreamer
libspotify
despotify
Make sure you got the required dependencies installed.
- Python >= 2.6, < 3
- :doc:`GStreamer <gstreamer>` (>= 0.10 ?) with Python bindings
- :doc:`GStreamer <gstreamer>` >= 0.10, with Python bindings
- Dependencies for at least one Mopidy mixer:
- :mod:`mopidy.mixers.alsa` (Linux only)
@ -44,10 +41,6 @@ Make sure you got the required dependencies installed.
- Dependencies for at least one Mopidy backend:
- :mod:`mopidy.backends.despotify` (Linux and OS X)
- :doc:`Despotify and spytify <despotify>`
- :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows)
- :doc:`libspotify and pyspotify <libspotify>`
@ -106,16 +99,8 @@ username and password into the file, like this::
SPOTIFY_USERNAME = u'myusername'
SPOTIFY_PASSWORD = u'mysecret'
Currently :mod:`mopidy.backends.despotify` is the default
backend.
If you want to use :mod:`mopidy.backends.libspotify`, copy the Spotify
application key to ``~/.mopidy/spotify_appkey.key``, and add the following
setting::
BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',)
If you want to use :mod:`mopidy.backends.local`, add the following setting::
Currently :mod:`mopidy.backends.libspotify` is the default backend. If you want
to use :mod:`mopidy.backends.local`, add the following setting::
BACKENDS = (u'mopidy.backends.local.LocalBackend',)

View File

@ -2,15 +2,21 @@
libspotify installation
***********************
As an alternative to the despotify backend, we are working on a
`libspotify <http://developer.spotify.com/en/libspotify/overview/>`_ backend.
To use the libspotify backend you must install libspotify and
`pyspotify <http://github.com/winjer/pyspotify>`_.
Mopidy uses `libspotify
<http://developer.spotify.com/en/libspotify/overview/>`_ for playing music from
the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must
install libspotify and `pyspotify <http://github.com/winjer/pyspotify>`_.
.. warning::
This backend requires a Spotify premium account, and it requires you to get
an application key from Spotify before use.
This backend requires a `Spotify premium account
<http://www.spotify.com/no/get-spotify/premium/>`_.
.. note::
This product uses SPOTIFY CORE but is not endorsed, certified or otherwise
approved in any way by Spotify. Spotify is the registered trade mark of the
Spotify Group.
Installing libspotify on Linux
@ -57,22 +63,20 @@ Installing pyspotify
Install pyspotify's dependencies. At Debian/Ubuntu systems::
sudo aptitude install python-dev python-alsaaudio
sudo aptitude install python-dev
In OS X no additional dependencies are needed.
Check out the pyspotify code, and install it::
git clone git://github.com/jodal/pyspotify.git
cd pyspotify/pyspotify/
sudo rm -rf build/ # If you are upgrading pyspotify
sudo python setup.py install
.. note::
Testing the installation
========================
Apply for an application key at
https://developer.spotify.com/en/libspotify/application-key, download the
binary version, and place the file at ``pyspotify/spotify_appkey.key``.
Test your libspotify setup::
examples/example1.py -u USERNAME -p PASSWORD
The ``sudo rm -rf build/`` step is needed if you are upgrading pyspotify.
Simply running ``python setup.py clean`` will *not* clean out the C parts
of the ``build/`` directory, and you will thus not get any changes to the C
code included in your installation.

View File

@ -2,7 +2,7 @@
Licenses
********
For a list of contributors, see :ref:`authors`. For details on who have
For a list of contributors, see :doc:`authors`. For details on who have
contributed what, please refer to our git repository.
Source code license

View File

@ -2,8 +2,6 @@ import sys
if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
from mopidy import settings as raw_settings
def get_version():
return u'0.1.0a4'
@ -27,13 +25,6 @@ class MopidyException(Exception):
class SettingsError(MopidyException):
pass
class Settings(object):
def __getattr__(self, attr):
if attr.isupper() and not hasattr(raw_settings, attr):
raise SettingsError(u'Setting "%s" is not set.' % attr)
value = getattr(raw_settings, attr)
if type(value) != bool and not value:
raise SettingsError(u'Setting "%s" is empty.' % attr)
return value
settings = Settings()
from mopidy import settings as default_settings_module
from mopidy.utils.settings import SettingsProxy
settings = SettingsProxy(default_settings_module)

View File

@ -11,18 +11,24 @@ sys.path.insert(0,
from mopidy import get_version, settings, SettingsError
from mopidy.process import CoreProcess
from mopidy.utils import get_class, get_or_create_folder
from mopidy.utils import get_class
from mopidy.utils.path import get_or_create_folder
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.main')
def main():
options = _parse_options()
_setup_logging(options.verbosity_level, options.dump)
settings.validate()
logger.info('-- Starting Mopidy --')
get_or_create_folder('~/.mopidy/')
core_queue = multiprocessing.Queue()
get_class(settings.SERVER)(core_queue).start()
core = CoreProcess(core_queue)
output_class = get_class(settings.OUTPUT)
backend_class = get_class(settings.BACKENDS[0])
frontend_class = get_class(settings.FRONTEND)
core = CoreProcess(core_queue, output_class, backend_class, frontend_class)
core.start()
asyncore.loop()
@ -37,6 +43,9 @@ def _parse_options():
parser.add_option('--dump',
action='store_true', dest='dump',
help='dump debug log to file')
parser.add_option('--list-settings',
action='callback', callback=list_settings_optparse_callback,
help='list current settings')
return parser.parse_args()[0]
def _setup_logging(verbosity_level, dump):

View File

@ -23,17 +23,20 @@ class BaseBackend(object):
:param core_queue: a queue for sending messages to
:class:`mopidy.process.CoreProcess`
:type core_queue: :class:`multiprocessing.Queue`
:param mixer: either a mixer instance, or :class:`None` to use the mixer
:param output_queue: a queue for sending messages to the output process
:type output_queue: :class:`multiprocessing.Queue`
:param mixer_class: either a mixer class, or :class:`None` to use the mixer
defined in settings
:type mixer: :class:`mopidy.mixers.BaseMixer` or :class:`None`
:type mixer_class: a subclass of :class:`mopidy.mixers.BaseMixer` or
:class:`None`
"""
def __init__(self, core_queue=None, mixer=None):
def __init__(self, core_queue=None, output_queue=None, mixer_class=None):
self.core_queue = core_queue
if mixer is not None:
self.mixer = mixer
else:
self.mixer = get_class(settings.MIXER)()
self.output_queue = output_queue
if mixer_class is None:
mixer_class = get_class(settings.MIXER)
self.mixer = mixer_class(self)
#: A :class:`multiprocessing.Queue` which can be used by e.g. library
#: callbacks executing in other threads to send messages to the core

View File

@ -64,12 +64,23 @@ class BaseCurrentPlaylistController(object):
self.version += 1
return cp_track
def append(self, tracks):
"""
Append the given tracks to the current playlist.
:param tracks: tracks to append
:type tracks: list of :class:`mopidy.models.Track`
"""
self.version += 1
for track in tracks:
self.add(track)
self.backend.playback.on_current_playlist_change()
def clear(self):
"""Clear the current playlist."""
self.backend.playback.stop()
self.backend.playback.current_cp_track = None
self._cp_tracks = []
self.version += 1
self.backend.playback.on_current_playlist_change()
def get(self, **criteria):
"""
@ -105,19 +116,6 @@ class BaseCurrentPlaylistController(object):
else:
raise LookupError(u'"%s" match multiple tracks' % criteria_string)
def load(self, tracks):
"""
Replace the tracks in the current playlist with the given tracks.
:param tracks: tracks to load
:type tracks: list of :class:`mopidy.models.Track`
"""
self._cp_tracks = []
self.version += 1
for track in tracks:
self.add(track)
self.backend.playback.new_playlist_loaded_callback()
def move(self, start, end, to_position):
"""
Move the tracks in the slice ``[start:end]`` to ``to_position``.
@ -148,6 +146,7 @@ class BaseCurrentPlaylistController(object):
to_position += 1
self._cp_tracks = new_cp_tracks
self.version += 1
self.backend.playback.on_current_playlist_change()
def remove(self, **criteria):
"""
@ -192,6 +191,7 @@ class BaseCurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
self.backend.playback.on_current_playlist_change()
def mpd_format(self, *args, **kwargs):
"""Not a part of the generic backend API."""

View File

@ -25,7 +25,7 @@ class BasePlaybackController(object):
#: Tracks are not removed from the playlist.
consume = False
#: The currently playing or selected track
#: The currently playing or selected track.
#:
#: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or
#: :class:`None`.
@ -45,7 +45,8 @@ class BasePlaybackController(object):
repeat = False
#: :class:`True`
#: Playback is stopped after current song, unless in repeat mode.
#: Playback is stopped after current song, unless in :attr:`repeat`
#: mode.
#: :class:`False`
#: Playback continues after current song.
single = False
@ -59,19 +60,32 @@ class BasePlaybackController(object):
self._play_time_started = None
def destroy(self):
"""Cleanup after component."""
"""
Cleanup after component.
May be overridden by subclasses.
"""
pass
def _get_cpid(self, cp_track):
if cp_track is None:
return None
return cp_track[0]
def _get_track(self, cp_track):
if cp_track is None:
return None
return cp_track[1]
@property
def current_cpid(self):
"""
The CPID (current playlist ID) of :attr:`current_track`.
The CPID (current playlist ID) of the currently playing or selected
track.
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
if self.current_cp_track is None:
return None
return self.current_cp_track[0]
return self._get_cpid(self.current_cp_track)
@property
def current_track(self):
@ -80,13 +94,15 @@ class BasePlaybackController(object):
Read-only. Extracted from :attr:`current_cp_track` for convenience.
"""
if self.current_cp_track is None:
return None
return self.current_cp_track[1]
return self._get_track(self.current_cp_track)
@property
def current_playlist_position(self):
"""The position of the current track in the current playlist."""
"""
The position of the current track in the current playlist.
Read-only.
"""
if self.current_cp_track is None:
return None
try:
@ -96,24 +112,71 @@ class BasePlaybackController(object):
return None
@property
def next_track(self):
def track_at_eot(self):
"""
The next track in the playlist.
The track that will be played at the end of the current track.
A :class:`mopidy.models.Track` extracted from :attr:`next_cp_track` for
convenience.
Read-only. A :class:`mopidy.models.Track` extracted from
:attr:`cp_track_at_eot` for convenience.
"""
next_cp_track = self.next_cp_track
if next_cp_track is None:
return None
return next_cp_track[1]
return self._get_track(self.cp_track_at_eot)
@property
def next_cp_track(self):
def cp_track_at_eot(self):
"""
The next track in the playlist.
The track that will be played at the end of the current track.
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
Not necessarily the same track as :attr:`cp_track_at_next`.
"""
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._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) % len(cp_tracks)]
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_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
@ -148,22 +211,19 @@ class BasePlaybackController(object):
return None
@property
def previous_track(self):
def track_at_previous(self):
"""
The previous track in the playlist.
The track that will be played if calling :meth:`previous()`.
A :class:`mopidy.models.Track` extracted from :attr:`previous_cp_track`
for convenience.
Read-only. A :class:`mopidy.models.Track` extracted from
:attr:`cp_track_at_previous` for convenience.
"""
previous_cp_track = self.previous_cp_track
if previous_cp_track is None:
return None
return previous_cp_track[1]
return self._get_track(self.cp_track_at_previous)
@property
def previous_cp_track(self):
def cp_track_at_previous(self):
"""
The previous track in the playlist.
The track that will be played if calling :meth:`previous()`.
A two-tuple of (CPID integer, :class:`mopidy.models.Track`).
@ -240,109 +300,125 @@ class BasePlaybackController(object):
def _current_wall_time(self):
return int(time.time() * 1000)
def end_of_track_callback(self):
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
Typically called by :class:`mopidy.process.CoreProcess` after a message
from a library thread is received.
"""
if self.next_cp_track is not None:
self.next()
if self.state == self.STOPPED:
return
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
self.play(self.cp_track_at_eot)
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
else:
self.stop()
self.current_cp_track = None
def new_playlist_loaded_callback(self):
"""
Tell the playback controller that a new playlist has been loaded.
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track[0])
Typically called by :class:`mopidy.process.CoreProcess` after a message
from a library thread is received.
def on_current_playlist_change(self):
"""
Tell the playback controller that the current playlist has changed.
Used by :class:`mopidy.backends.base.BaseCurrentPlaylistController`.
"""
self.current_cp_track = None
self._first_shuffle = True
self._shuffled = []
if self.state == self.PLAYING:
if len(self.backend.current_playlist.tracks) > 0:
self.play()
else:
self.stop()
elif self.state == self.PAUSED:
if not self.backend.current_playlist.cp_tracks:
self.stop()
self.current_cp_track = None
elif (self.current_cp_track not in
self.backend.current_playlist.cp_tracks):
self.current_cp_track = None
self.stop()
def next(self):
"""Play the next track."""
original_cp_track = self.current_cp_track
if self.state == self.STOPPED:
return
elif self.next_cp_track is not None and self._next(self.next_track):
self.current_cp_track = self.next_cp_track
self.state = self.PLAYING
elif self.next_cp_track is None:
if self.cp_track_at_next:
self.play(self.cp_track_at_next)
else:
self.stop()
self.current_cp_track = None
# FIXME handle in play aswell?
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track[0])
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
def _next(self, track):
return self._play(track)
def pause(self):
"""Pause playback."""
if self.state == self.PLAYING and self._pause():
self.state = self.PAUSED
def _pause(self):
"""
To be overridden by subclass. Implement your backend's pause
functionality here.
:rtype: :class:`True` if successful, else :class:`False`
"""
raise NotImplementedError
def play(self, cp_track=None):
def play(self, cp_track=None, on_error_step=1):
"""
Play the given track or the currently active track.
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 not self.current_cp_track:
cp_track = self.next_cp_track
cp_track = self.cp_track_at_next
if self.state == self.PAUSED and cp_track is None:
self.resume()
elif cp_track is not None and self._play(cp_track[1]):
elif cp_track is not None:
self.current_cp_track = cp_track
self.state = self.PLAYING
# TODO Do something sensible when _play() returns False, like calling
# next(). Adding this todo instead of just implementing it as I want a
# test case first.
if not self._play(cp_track[1]):
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)
def _play(self, track):
"""
To be overridden by subclass. Implement your backend's play
functionality here.
:param track: the track to play
:type track: :class:`mopidy.models.Track`
:rtype: :class:`True` if successful, else :class:`False`
"""
raise NotImplementedError
def previous(self):
"""Play the previous track."""
if (self.previous_cp_track is not None
and self.state != self.STOPPED
and self._previous(self.previous_track)):
self.current_cp_track = self.previous_cp_track
self.state = self.PLAYING
def _previous(self, track):
return self._play(track)
if self.cp_track_at_previous is None:
return
if self.state == self.STOPPED:
return
self.play(self.cp_track_at_previous, on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""
@ -350,6 +426,12 @@ class BasePlaybackController(object):
self.state = self.PLAYING
def _resume(self):
"""
To be overridden by subclass. Implement your backend's resume
functionality here.
:rtype: :class:`True` if successful, else :class:`False`
"""
raise NotImplementedError
def seek(self, time_position):
@ -370,9 +452,20 @@ class BasePlaybackController(object):
self.next()
return
self._play_time_started = self._current_wall_time
self._play_time_accumulated = time_position
self._seek(time_position)
def _seek(self, time_position):
"""
To be overridden by subclass. Implement your backend's seek
functionality here.
:param time_position: time position in milliseconds
:type time_position: int
:rtype: :class:`True` if successful, else :class:`False`
"""
raise NotImplementedError
def stop(self):
@ -381,4 +474,10 @@ class BasePlaybackController(object):
self.state = self.STOPPED
def _stop(self):
"""
To be overridden by subclass. Implement your backend's stop
functionality here.
:rtype: :class:`True` if successful, else :class:`False`
"""
raise NotImplementedError

View File

@ -107,13 +107,3 @@ class BaseStoredPlaylistsController(object):
:type playlist: :class:`mopidy.models.Playlist`
"""
raise NotImplementedError
def search(self, query):
"""
Search for playlists whose name contains ``query``.
:param query: query to search for
:type query: string
:rtype: list of :class:`mopidy.models.Playlist`
"""
return filter(lambda p: query in p.name, self._playlists)

View File

@ -1,209 +0,0 @@
import datetime as dt
import logging
import sys
import spytify
from mopidy import settings
from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
BaseLibraryController, BasePlaybackController,
BaseStoredPlaylistsController)
from mopidy.models import Artist, Album, Track, Playlist
logger = logging.getLogger('mopidy.backends.despotify')
ENCODING = 'utf-8'
class DespotifyBackend(BaseBackend):
"""
A Spotify backend which uses the open source `despotify library
<http://despotify.se/>`_.
`spytify <http://despotify.svn.sourceforge.net/viewvc/despotify/src/bindings/python/>`_
is the Python bindings for the despotify library. It got litle
documentation, but a couple of examples are available.
**Issues:** http://github.com/jodal/mopidy/issues/labels/backend-despotify
"""
def __init__(self, *args, **kwargs):
super(DespotifyBackend, self).__init__(*args, **kwargs)
self.current_playlist = DespotifyCurrentPlaylistController(backend=self)
self.library = DespotifyLibraryController(backend=self)
self.playback = DespotifyPlaybackController(backend=self)
self.stored_playlists = DespotifyStoredPlaylistsController(backend=self)
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
self.spotify = self._connect()
self.stored_playlists.refresh()
def _connect(self):
logger.info(u'Connecting to Spotify')
try:
return DespotifySessionManager(
settings.SPOTIFY_USERNAME.encode(ENCODING),
settings.SPOTIFY_PASSWORD.encode(ENCODING),
core_queue=self.core_queue)
except spytify.SpytifyError as e:
logger.exception(e)
sys.exit(1)
class DespotifyCurrentPlaylistController(BaseCurrentPlaylistController):
pass
class DespotifyLibraryController(BaseLibraryController):
def find_exact(self, **query):
return self.search(**query)
def lookup(self, uri):
track = self.backend.spotify.lookup(uri.encode(ENCODING))
return DespotifyTranslator.to_mopidy_track(track)
def refresh(self, uri=None):
pass # TODO
def search(self, **query):
spotify_query = []
for (field, values) in query.iteritems():
if not hasattr(values, '__iter__'):
values = [values]
for value in values:
if field == u'track':
field = u'title'
if field == u'any':
spotify_query.append(value)
else:
spotify_query.append(u'%s:"%s"' % (field, value))
spotify_query = u' '.join(spotify_query)
logger.debug(u'Spotify search query: %s', spotify_query)
result = self.backend.spotify.search(spotify_query.encode(ENCODING))
if (result is None or result.playlist.tracks[0].get_uri() ==
'spotify:track:0000000000000000000000'):
return Playlist()
return DespotifyTranslator.to_mopidy_playlist(result.playlist)
class DespotifyPlaybackController(BasePlaybackController):
def _pause(self):
try:
self.backend.spotify.pause()
return True
except spytify.SpytifyError as e:
logger.error(e)
return False
def _play(self, track):
try:
self.backend.spotify.play(self.backend.spotify.lookup(track.uri))
return True
except spytify.SpytifyError as e:
logger.error(e)
return False
def _resume(self):
try:
self.backend.spotify.resume()
return True
except spytify.SpytifyError as e:
logger.error(e)
return False
def _seek(self, time_position):
pass # TODO
def _stop(self):
try:
self.backend.spotify.stop()
return True
except spytify.SpytifyError as e:
logger.error(e)
return False
class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController):
def create(self, name):
pass # TODO
def delete(self, playlist):
pass # TODO
def lookup(self, uri):
pass # TODO
def refresh(self):
logger.info(u'Caching stored playlists')
playlists = []
for spotify_playlist in self.backend.spotify.stored_playlists:
playlists.append(
DespotifyTranslator.to_mopidy_playlist(spotify_playlist))
self._playlists = playlists
logger.debug(u'Available playlists: %s',
u', '.join([u'<%s>' % p.name for p in self.playlists]))
logger.info(u'Done caching stored playlists')
def rename(self, playlist, new_name):
pass # TODO
def save(self, playlist):
pass # TODO
class DespotifyTranslator(object):
@classmethod
def to_mopidy_artist(cls, spotify_artist):
return Artist(
uri=spotify_artist.get_uri(),
name=spotify_artist.name.decode(ENCODING)
)
@classmethod
def to_mopidy_album(cls, spotify_album_name):
return Album(name=spotify_album_name.decode(ENCODING))
@classmethod
def to_mopidy_track(cls, spotify_track):
if spotify_track is None or not spotify_track.has_meta_data():
return None
if dt.MINYEAR <= int(spotify_track.year) <= dt.MAXYEAR:
date = dt.date(spotify_track.year, 1, 1)
else:
date = None
return Track(
uri=spotify_track.get_uri(),
name=spotify_track.title.decode(ENCODING),
artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists],
album=cls.to_mopidy_album(spotify_track.album),
track_no=spotify_track.tracknumber,
date=date,
length=spotify_track.length,
bitrate=320,
)
@classmethod
def to_mopidy_playlist(cls, spotify_playlist):
return Playlist(
uri=spotify_playlist.get_uri(),
name=spotify_playlist.name.decode(ENCODING),
tracks=filter(None,
[cls.to_mopidy_track(t) for t in spotify_playlist.tracks]),
)
class DespotifySessionManager(spytify.Spytify):
DESPOTIFY_NEW_TRACK = 1
DESPOTIFY_TIME_TELL = 2
DESPOTIFY_END_OF_PLAYLIST = 3
DESPOTIFY_TRACK_PLAY_ERROR = 4
def __init__(self, *args, **kwargs):
kwargs['callback'] = self.callback
self.core_queue = kwargs.pop('core_queue')
super(DespotifySessionManager, self).__init__(*args, **kwargs)
def callback(self, signal, data):
if signal == self.DESPOTIFY_END_OF_PLAYLIST:
logger.debug('Despotify signalled end of playlist')
self.core_queue.put({'command': 'end_of_track'})
elif signal == self.DESPOTIFY_TRACK_PLAY_ERROR:
logger.error('Despotify signalled track play error')

View File

@ -1,20 +1,7 @@
import datetime as dt
import logging
import os
import multiprocessing
import threading
from spotify import Link, SpotifyError
from spotify.manager import SpotifySessionManager
from spotify.alsahelper import AlsaController
from mopidy import get_version, settings
from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController,
BaseLibraryController, BasePlaybackController,
BaseStoredPlaylistsController)
from mopidy.models import Artist, Album, Track, Playlist
import alsaaudio
from mopidy import settings
from mopidy.backends.base import BaseBackend, BaseCurrentPlaylistController
logger = logging.getLogger('mopidy.backends.libspotify')
@ -22,269 +9,46 @@ ENCODING = 'utf-8'
class LibspotifyBackend(BaseBackend):
"""
A Spotify backend which uses the official `libspotify library
<http://developer.spotify.com/en/libspotify/overview/>`_.
`pyspotify <http://github.com/winjer/pyspotify/>`_ is the Python bindings
for libspotify. It got no documentation, but multiple examples are
available. Like libspotify, pyspotify's calls are mostly asynchronous.
This backend should also work with `openspotify
<http://github.com/noahwilliamsson/openspotify>`_, but we haven't tested
that yet.
A `Spotify <http://www.spotify.com/>`_ backend which uses the official
`libspotify <http://developer.spotify.com/en/libspotify/overview/>`_
library and the `pyspotify <http://github.com/winjer/pyspotify/>`_ Python
bindings for libspotify.
**Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify
.. note::
This product uses SPOTIFY(R) CORE but is not endorsed, certified or
otherwise approved in any way by Spotify. Spotify is the registered
trade mark of the Spotify Group.
"""
# Imports inside methods are to prevent loading of __init__.py to fail on
# missing spotify dependencies.
def __init__(self, *args, **kwargs):
from .library import LibspotifyLibraryController
from .playback import LibspotifyPlaybackController
from .stored_playlists import LibspotifyStoredPlaylistsController
super(LibspotifyBackend, self).__init__(*args, **kwargs)
self.current_playlist = BaseCurrentPlaylistController(backend=self)
self.library = LibspotifyLibraryController(backend=self)
self.playback = LibspotifyPlaybackController(backend=self)
self.stored_playlists = LibspotifyStoredPlaylistsController(
backend=self)
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
self.audio_controller_class = kwargs.get(
'audio_controller_class', AlsaController)
self.spotify = self._connect()
def _connect(self):
from .session_manager import LibspotifySessionManager
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
logger.info(u'Connecting to Spotify')
spotify = LibspotifySessionManager(
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD,
core_queue=self.core_queue,
audio_controller_class=self.audio_controller_class)
output_queue=self.output_queue)
spotify.start()
return spotify
class LibspotifyLibraryController(BaseLibraryController):
def find_exact(self, **query):
return self.search(**query)
def lookup(self, uri):
spotify_track = Link.from_string(uri).as_track()
return LibspotifyTranslator.to_mopidy_track(spotify_track)
def refresh(self, uri=None):
pass # TODO
def search(self, **query):
spotify_query = []
for (field, values) in query.iteritems():
if not hasattr(values, '__iter__'):
values = [values]
for value in values:
if field == u'track':
field = u'title'
if field == u'any':
spotify_query.append(value)
else:
spotify_query.append(u'%s:"%s"' % (field, value))
spotify_query = u' '.join(spotify_query)
logger.debug(u'In search method, search for: %s' % spotify_query)
my_end, other_end = multiprocessing.Pipe()
self.backend.spotify.search(spotify_query.encode(ENCODING), other_end)
logger.debug(u'In Library.search(), waiting for search results')
my_end.poll(None)
logger.debug(u'In Library.search(), receiving search results')
playlist = my_end.recv()
logger.debug(u'In Library.search(), done receiving search results')
logger.debug(['%s' % t.name for t in playlist.tracks])
return playlist
class LibspotifyPlaybackController(BasePlaybackController):
def _pause(self):
# TODO
return False
def _play(self, track):
if self.state == self.PLAYING:
self.stop()
if track.uri is None:
return False
try:
self.backend.spotify.session.load(
Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1)
return True
except SpotifyError as e:
logger.warning('Play %s failed: %s', track.uri, e)
return False
def _resume(self):
# TODO
return False
def _seek(self, time_position):
pass # TODO
def _stop(self):
self.backend.spotify.session.play(0)
return True
class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController):
def create(self, name):
pass # TODO
def delete(self, playlist):
pass # TODO
def lookup(self, uri):
pass # TODO
def refresh(self):
pass # TODO
def rename(self, playlist, new_name):
pass # TODO
def save(self, playlist):
pass # TODO
class LibspotifyTranslator(object):
@classmethod
def to_mopidy_artist(cls, spotify_artist):
if not spotify_artist.is_loaded():
return Artist(name=u'[loading...]')
return Artist(
uri=str(Link.from_artist(spotify_artist)),
name=spotify_artist.name().decode(ENCODING),
)
@classmethod
def to_mopidy_album(cls, spotify_album):
if not spotify_album.is_loaded():
return Album(name=u'[loading...]')
# TODO pyspotify got much more data on albums than this
return Album(name=spotify_album.name().decode(ENCODING))
@classmethod
def to_mopidy_track(cls, spotify_track):
if not spotify_track.is_loaded():
return Track(name=u'[loading...]')
uri = str(Link.from_track(spotify_track, 0))
if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
date = dt.date(spotify_track.album().year(), 1, 1)
else:
date = None
return Track(
uri=uri,
name=spotify_track.name().decode(ENCODING),
artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()],
album=cls.to_mopidy_album(spotify_track.album()),
track_no=spotify_track.index(),
date=date,
length=spotify_track.duration(),
bitrate=320,
)
@classmethod
def to_mopidy_playlist(cls, spotify_playlist):
if not spotify_playlist.is_loaded():
return Playlist(name=u'[loading...]')
return Playlist(
uri=str(Link.from_playlist(spotify_playlist)),
name=spotify_playlist.name().decode(ENCODING),
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist],
)
class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
appkey_file = os.path.expanduser(settings.SPOTIFY_LIB_APPKEY)
user_agent = 'Mopidy %s' % get_version()
def __init__(self, username, password, core_queue, audio_controller_class):
SpotifySessionManager.__init__(self, username, password)
threading.Thread.__init__(self)
self.core_queue = core_queue
self.connected = threading.Event()
self.audio = audio_controller_class(alsaaudio.PCM_NORMAL)
self.session = None
def run(self):
self.connect()
def logged_in(self, session, error):
"""Callback used by pyspotify"""
logger.info('Logged in')
self.session = session
self.connected.set()
def logged_out(self, session):
"""Callback used by pyspotify"""
logger.info('Logged out')
def metadata_updated(self, session):
"""Callback used by pyspotify"""
logger.debug('Metadata updated, refreshing stored playlists')
playlists = []
for spotify_playlist in session.playlist_container():
playlists.append(
LibspotifyTranslator.to_mopidy_playlist(spotify_playlist))
self.core_queue.put({
'command': 'set_stored_playlists',
'playlists': playlists,
})
def connection_error(self, session, error):
"""Callback used by pyspotify"""
logger.error('Connection error: %s', error)
def message_to_user(self, session, message):
"""Callback used by pyspotify"""
logger.info(message)
def notify_main_thread(self, session):
"""Callback used by pyspotify"""
logger.debug('Notify main thread')
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
self.audio.music_delivery(session, frames, frame_size, num_frames,
sample_type, sample_rate, channels)
def play_token_lost(self, session):
"""Callback used by pyspotify"""
logger.debug('Play token lost')
self.core_queue.put({'command': 'stop_playback'})
def log_message(self, session, data):
"""Callback used by pyspotify"""
logger.debug(data)
def end_of_track(self, session):
"""Callback used by pyspotify"""
logger.debug('End of track')
self.core_queue.put({'command': 'end_of_track'})
def search(self, query, connection):
"""Search method used by Mopidy backend"""
def callback(results, userdata):
logger.debug(u'In SessionManager.search().callback(), '
'translating search results')
logger.debug(results.tracks())
# TODO Include results from results.albums(), etc. too
playlist = Playlist(tracks=[
LibspotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
logger.debug(u'In SessionManager.search().callback(), '
'sending search results')
logger.debug(['%s' % t.name for t in playlist.tracks])
connection.send(playlist)
logger.debug(u'In SessionManager.search().callback(), '
'done sending search results')
logger.debug(u'In SessionManager.search(), '
'waiting for Spotify connection')
self.connected.wait()
logger.debug(u'In SessionManager.search(), '
'sending search query')
self.session.search(query, callback)
logger.debug(u'In SessionManager.search(), '
'done sending search query')

View File

@ -0,0 +1,41 @@
import logging
import multiprocessing
from spotify import Link
from mopidy.backends.base import BaseLibraryController
from mopidy.backends.libspotify import ENCODING
from mopidy.backends.libspotify.translator import LibspotifyTranslator
logger = logging.getLogger('mopidy.backends.libspotify.library')
class LibspotifyLibraryController(BaseLibraryController):
def find_exact(self, **query):
return self.search(**query)
def lookup(self, uri):
spotify_track = Link.from_string(uri).as_track()
return LibspotifyTranslator.to_mopidy_track(spotify_track)
def refresh(self, uri=None):
pass # TODO
def search(self, **query):
spotify_query = []
for (field, values) in query.iteritems():
if not hasattr(values, '__iter__'):
values = [values]
for value in values:
if field == u'track':
field = u'title'
if field == u'any':
spotify_query.append(value)
else:
spotify_query.append(u'%s:"%s"' % (field, value))
spotify_query = u' '.join(spotify_query)
logger.debug(u'Spotify search query: %s' % spotify_query)
my_end, other_end = multiprocessing.Pipe()
self.backend.spotify.search(spotify_query.encode(ENCODING), other_end)
my_end.poll(None)
playlist = my_end.recv()
return playlist

View File

@ -0,0 +1,54 @@
import logging
import multiprocessing
from spotify import Link, SpotifyError
from mopidy.backends.base import BasePlaybackController
from mopidy.process import pickle_connection
logger = logging.getLogger('mopidy.backends.libspotify.playback')
class LibspotifyPlaybackController(BasePlaybackController):
def _set_output_state(self, state_name):
logger.debug(u'Setting output state to %s ...', state_name)
(my_end, other_end) = multiprocessing.Pipe()
self.backend.output_queue.put({
'command': 'set_state',
'state': state_name,
'reply_to': pickle_connection(other_end),
})
my_end.poll(None)
return my_end.recv()
def _pause(self):
return self._set_output_state('PAUSED')
def _play(self, track):
self._set_output_state('READY')
if self.state == self.PLAYING:
self.backend.spotify.session.play(0)
if track.uri is None:
return False
try:
self.backend.spotify.session.load(
Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1)
self._set_output_state('PLAYING')
return True
except SpotifyError as e:
logger.warning('Play %s failed: %s', track.uri, e)
return False
def _resume(self):
return self._seek(self.time_position)
def _seek(self, time_position):
self._set_output_state('READY')
self.backend.spotify.session.seek(time_position)
self._set_output_state('PLAYING')
return True
def _stop(self):
result = self._set_output_state('READY')
self.backend.spotify.session.play(0)
return result

View File

@ -0,0 +1,106 @@
import logging
import os
import threading
from spotify.manager import SpotifySessionManager
from mopidy import get_version, settings
from mopidy.models import Playlist
from mopidy.backends.libspotify.translator import LibspotifyTranslator
logger = logging.getLogger('mopidy.backends.libspotify.session_manager')
class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
user_agent = 'Mopidy %s' % get_version()
def __init__(self, username, password, core_queue, output_queue):
SpotifySessionManager.__init__(self, username, password)
threading.Thread.__init__(self)
self.core_queue = core_queue
self.output_queue = output_queue
self.connected = threading.Event()
self.session = None
def run(self):
self.connect()
def logged_in(self, session, error):
"""Callback used by pyspotify"""
logger.info('Logged in')
self.session = session
self.connected.set()
def logged_out(self, session):
"""Callback used by pyspotify"""
logger.info('Logged out')
def metadata_updated(self, session):
"""Callback used by pyspotify"""
logger.debug('Metadata updated, refreshing stored playlists')
playlists = []
for spotify_playlist in session.playlist_container():
playlists.append(
LibspotifyTranslator.to_mopidy_playlist(spotify_playlist))
self.core_queue.put({
'command': 'set_stored_playlists',
'playlists': playlists,
})
def connection_error(self, session, error):
"""Callback used by pyspotify"""
logger.error('Connection error: %s', error)
def message_to_user(self, session, message):
"""Callback used by pyspotify"""
logger.info(message.strip())
def notify_main_thread(self, session):
"""Callback used by pyspotify"""
logger.debug('Notify main thread')
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
# TODO Base caps_string on arguments
caps_string = """
audio/x-raw-int,
endianness=(int)1234,
channels=(int)2,
width=(int)16,
depth=(int)16,
signed=True,
rate=(int)44100
"""
self.output_queue.put({
'command': 'deliver_data',
'caps': caps_string,
'data': bytes(frames),
})
def play_token_lost(self, session):
"""Callback used by pyspotify"""
logger.debug('Play token lost')
self.core_queue.put({'command': 'stop_playback'})
def log_message(self, session, data):
"""Callback used by pyspotify"""
logger.debug(data.strip())
def end_of_track(self, session):
"""Callback used by pyspotify"""
logger.debug('End of data stream.')
self.output_queue.put({'command': 'end_of_data_stream'})
def search(self, query, connection):
"""Search method used by Mopidy backend"""
def callback(results, userdata):
# TODO Include results from results.albums(), etc. too
playlist = Playlist(tracks=[
LibspotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
connection.send(playlist)
self.connected.wait()
self.session.search(query, callback)

Binary file not shown.

View File

@ -0,0 +1,20 @@
from mopidy.backends.base import BaseStoredPlaylistsController
class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController):
def create(self, name):
pass # TODO
def delete(self, playlist):
pass # TODO
def lookup(self, uri):
pass # TODO
def refresh(self):
pass # TODO
def rename(self, playlist, new_name):
pass # TODO
def save(self, playlist):
pass # TODO

View File

@ -0,0 +1,53 @@
import datetime as dt
from spotify import Link
from mopidy.models import Artist, Album, Track, Playlist
from mopidy.backends.libspotify import ENCODING
class LibspotifyTranslator(object):
@classmethod
def to_mopidy_artist(cls, spotify_artist):
if not spotify_artist.is_loaded():
return Artist(name=u'[loading...]')
return Artist(
uri=str(Link.from_artist(spotify_artist)),
name=spotify_artist.name().decode(ENCODING),
)
@classmethod
def to_mopidy_album(cls, spotify_album):
if not spotify_album.is_loaded():
return Album(name=u'[loading...]')
# TODO pyspotify got much more data on albums than this
return Album(name=spotify_album.name().decode(ENCODING))
@classmethod
def to_mopidy_track(cls, spotify_track):
if not spotify_track.is_loaded():
return Track(name=u'[loading...]')
uri = str(Link.from_track(spotify_track, 0))
if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
date = dt.date(spotify_track.album().year(), 1, 1)
else:
date = None
return Track(
uri=uri,
name=spotify_track.name().decode(ENCODING),
artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()],
album=cls.to_mopidy_album(spotify_track.album()),
track_no=spotify_track.index(),
date=date,
length=spotify_track.duration(),
bitrate=160,
)
@classmethod
def to_mopidy_playlist(cls, spotify_playlist):
if not spotify_playlist.is_loaded():
return Playlist(name=u'[loading...]')
return Playlist(
uri=str(Link.from_playlist(spotify_playlist)),
name=spotify_playlist.name().decode(ENCODING),
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist],
)

View File

@ -14,10 +14,10 @@ import glob
import shutil
import threading
from mopidy import settings
from mopidy.backends.base import *
from mopidy.models import Playlist, Track, Album
from mopidy import settings
from mopidy.utils import parse_m3u, parse_mpd_tag_cache
from .translator import parse_m3u, parse_mpd_tag_cache
logger = logging.getLogger(u'mopidy.backends.local')
@ -69,7 +69,7 @@ class LocalPlaybackController(BasePlaybackController):
def _message(self, bus, message):
if message.type == gst.MESSAGE_EOS:
self.end_of_track_callback()
self.on_end_of_track()
elif message.type == gst.MESSAGE_ERROR:
self._bin.set_state(gst.STATE_NULL)
error, debug = message.parse_error()

View File

@ -1,66 +1,10 @@
import logging
from multiprocessing.reduction import reduce_connection
import os
import pickle
import sys
import urllib
logger = logging.getLogger('mopidy.utils')
logger = logging.getLogger('mopidy.backends.local.translator')
from mopidy.models import Track, Artist, Album
def flatten(the_list):
result = []
for element in the_list:
if isinstance(element, list):
result.extend(flatten(element))
else:
result.append(element)
return result
def import_module(name):
__import__(name)
return sys.modules[name]
def get_class(name):
module_name = name[:name.rindex('.')]
class_name = name[name.rindex('.') + 1:]
logger.debug('Loading: %s', name)
module = import_module(module_name)
class_object = getattr(module, class_name)
return class_object
def get_or_create_folder(folder):
folder = os.path.expanduser(folder)
if not os.path.isdir(folder):
logger.info(u'Creating %s', folder)
os.mkdir(folder, 0755)
return folder
def path_to_uri(*paths):
path = os.path.join(*paths)
#path = os.path.expanduser(path) # FIXME
path = path.encode('utf-8')
if sys.platform == 'win32':
return 'file:' + urllib.pathname2url(path)
return 'file://' + urllib.pathname2url(path)
def indent(string, places=4, linebreak='\n'):
lines = string.split(linebreak)
if len(lines) == 1:
return string
result = u''
for line in lines:
result += linebreak + ' ' * places + line
return result
def pickle_connection(connection):
return pickle.dumps(reduce_connection(connection))
def unpickle_connection(pickled_connection):
# From http://stackoverflow.com/questions/1446004
(func, args) = pickle.loads(pickled_connection)
return func(*args)
from mopidy.utils.path import path_to_uri
def parse_m3u(file_path):
"""

View File

@ -11,11 +11,15 @@ def add(frontend, uri):
Adds the file ``URI`` to the playlist (directories add recursively).
``URI`` can also be a single file.
"""
track = frontend.backend.library.lookup(uri)
if track is None:
raise MpdNoExistError(
u'directory or file not found', command=u'add')
frontend.backend.current_playlist.add(track)
for handler_prefix in frontend.backend.uri_handlers:
if uri.startswith(handler_prefix):
track = frontend.backend.library.lookup(uri)
if track is not None:
frontend.backend.current_playlist.add(track)
return
raise MpdNoExistError(
u'directory or file not found', command=u'add')
@handle_pattern(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
def addid(frontend, uri, songpos=None):
@ -173,7 +177,10 @@ def playlistfind(frontend, tag, needle):
if tag == 'filename':
try:
cp_track = frontend.backend.current_playlist.get(uri=needle)
return cp_track[1].mpd_format()
(cpid, track) = cp_track
position = frontend.backend.current_playlist.cp_tracks.index(
cp_track)
return track.mpd_format(cpid=cpid, position=position)
except LookupError:
return None
raise MpdNotImplemented # TODO
@ -338,7 +345,8 @@ def swap(frontend, songpos1, songpos2):
tracks.insert(songpos1, song2)
del tracks[songpos2]
tracks.insert(songpos2, song1)
frontend.backend.current_playlist.load(tracks)
frontend.backend.current_playlist.clear()
frontend.backend.current_playlist.append(tracks)
@handle_pattern(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
def swapid(frontend, cpid1, cpid2):

View File

@ -139,9 +139,7 @@ def playid(frontend, cpid):
cpid = int(cpid)
try:
if cpid == -1:
if not frontend.backend.current_playlist.cp_tracks:
return # Fail silently
cp_track = frontend.backend.current_playlist.cp_tracks[0]
cp_track = _get_cp_track_for_play_minus_one(frontend)
else:
cp_track = frontend.backend.current_playlist.get(cpid=cpid)
return frontend.backend.playback.play(cp_track)
@ -158,10 +156,11 @@ def playpos(frontend, songpos):
Begins playing the playlist at song number ``SONGPOS``.
*MPoD:*
*Many clients:*
- issues ``play "-1"`` after playlist replacement to start playback at
the first track.
- issue ``play "-1"`` after playlist replacement to start the current
track. If the current track is not set, start playback at the first
track.
*BitMPC:*
@ -170,15 +169,21 @@ def playpos(frontend, songpos):
songpos = int(songpos)
try:
if songpos == -1:
if not frontend.backend.current_playlist.cp_tracks:
return # Fail silently
cp_track = frontend.backend.current_playlist.cp_tracks[0]
cp_track = _get_cp_track_for_play_minus_one(frontend)
else:
cp_track = frontend.backend.current_playlist.cp_tracks[songpos]
return frontend.backend.playback.play(cp_track)
except IndexError:
raise MpdArgError(u'Bad song index', command=u'play')
def _get_cp_track_for_play_minus_one(frontend):
if not frontend.backend.current_playlist.cp_tracks:
return # Fail silently
cp_track = frontend.backend.playback.current_cp_track
if cp_track is None:
cp_track = frontend.backend.current_playlist.cp_tracks[0]
return cp_track
@handle_pattern(r'^previous$')
def previous(frontend):
"""
@ -293,7 +298,9 @@ def seek(frontend, songpos, seconds):
Seeks to the position ``TIME`` (in seconds) of entry ``SONGPOS`` in
the playlist.
"""
raise MpdNotImplemented # TODO
if frontend.backend.playback.current_playlist_position != songpos:
playpos(frontend, songpos)
return frontend.backend.playback.seek(int(seconds) * 1000)
@handle_pattern(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$')
def seekid(frontend, cpid, seconds):
@ -304,7 +311,9 @@ def seekid(frontend, cpid, seconds):
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
"""
raise MpdNotImplemented # TODO
if frontend.backend.playback.current_cpid != cpid:
playid(frontend, cpid)
return frontend.backend.playback.seek(int(seconds) * 1000)
@handle_pattern(r'^setvol "(?P<volume>[-+]*\d+)"$')
def setvol(frontend, volume):

View File

@ -86,10 +86,16 @@ def load(frontend, name):
``load {NAME}``
Loads the playlist ``NAME.m3u`` from the playlist directory.
*Clarifications:*
- ``load`` appends the given playlist to the current playlist.
"""
matches = frontend.backend.stored_playlists.search(name)
if matches:
frontend.backend.current_playlist.load(matches[0].tracks)
try:
playlist = frontend.backend.stored_playlists.get(name=name)
frontend.backend.current_playlist.append(playlist.tracks)
except LookupError:
raise MpdNoExistError(u'No such playlist', command=u'load')
@handle_pattern(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
def playlistadd(frontend, name, uri):
@ -139,9 +145,9 @@ def playlistmove(frontend, name, from_pos, to_pos):
*Clarifications:*
- The second argument is not a ``SONGID`` as used elsewhere in the
protocol documentation, but just the ``SONGPOS`` to move *from*,
i.e. ``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``.
- The second argument is not a ``SONGID`` as used elsewhere in the protocol
documentation, but just the ``SONGPOS`` to move *from*, i.e.
``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``.
"""
raise MpdNotImplemented # TODO

View File

@ -8,7 +8,8 @@ import sys
from mopidy import get_mpd_protocol_version, settings
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR
from mopidy.utils import indent, pickle_connection
from mopidy.process import pickle_connection
from mopidy.utils import indent
logger = logging.getLogger('mopidy.frontends.mpd.server')

View File

@ -1,6 +1,14 @@
from mopidy import settings
class BaseMixer(object):
def __init__(self, *args, **kwargs):
pass
"""
:param backend: a backend instance
:type mixer: :class:`mopidy.backends.base.BaseBackend`
"""
def __init__(self, backend, *args, **kwargs):
self.backend = backend
self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0
@property
def volume(self):
@ -10,11 +18,13 @@ class BaseMixer(object):
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
equal to 0. Values above 100 is equal to 100.
"""
return self._get_volume()
if self._get_volume() is None:
return None
return int(self._get_volume() / self.amplification_factor)
@volume.setter
def volume(self, volume):
volume = int(volume)
volume = int(int(volume) * self.amplification_factor)
if volume < 0:
volume = 0
elif volume > 100:

View File

@ -0,0 +1,25 @@
import multiprocessing
from mopidy.mixers import BaseMixer
from mopidy.process import pickle_connection
class GStreamerSoftwareMixer(BaseMixer):
"""Mixer which uses GStreamer to control volume in software."""
def __init__(self, *args, **kwargs):
super(GStreamerSoftwareMixer, self).__init__(*args, **kwargs)
def _get_volume(self):
my_end, other_end = multiprocessing.Pipe()
self.backend.output_queue.put({
'command': 'get_volume',
'reply_to': pickle_connection(other_end),
})
my_end.poll(None)
return my_end.recv()
def _set_volume(self, volume):
self.backend.output_queue.put({
'command': 'set_volume',
'volume': volume,
})

View File

@ -22,10 +22,7 @@ class NadMixer(BaseMixer):
currently used by this mixer.
Sadly, this means that if you use the remote control to change the volume
on the amplifier, Mopidy will no longer report the correct volume. To
recalibrate the mixer, set the volume to 0 through Mopidy. This will reset
the amplifier to a known state, including powering on the device, selecting
the configured speakers and input sources.
on the amplifier, Mopidy will no longer report the correct volume.
**Dependencies**
@ -51,8 +48,6 @@ class NadMixer(BaseMixer):
def _set_volume(self, volume):
self._volume = volume
if volume == 0:
self._pipe.send({'command': 'reset_device'})
self._pipe.send({'command': 'set_volume', 'volume': volume})
@ -83,7 +78,7 @@ class NadTalker(BaseProcess):
self.pipe = pipe
self._device = None
def _run(self):
def run_inside_try(self):
self._open_connection()
self._set_device_to_known_state()
while self.pipe.poll(None):

View File

180
mopidy/outputs/gstreamer.py Normal file
View File

@ -0,0 +1,180 @@
import gobject
gobject.threads_init()
import pygst
pygst.require('0.10')
import gst
import logging
import threading
from mopidy.process import BaseProcess, unpickle_connection
logger = logging.getLogger('mopidy.outputs.gstreamer')
class GStreamerOutput(object):
"""
Audio output through GStreamer.
Starts the :class:`GStreamerProcess`.
"""
def __init__(self, core_queue, output_queue):
self.process = GStreamerProcess(core_queue, output_queue)
self.process.start()
def destroy(self):
self.process.terminate()
class GStreamerMessagesThread(threading.Thread):
def run(self):
gobject.MainLoop().run()
class GStreamerProcess(BaseProcess):
"""
A process for all work related to GStreamer.
The main loop processes events from both Mopidy and GStreamer.
Make sure this subprocess is started by the MainThread in the top-most
parent process, and not some other thread. If not, we can get into the
problems described at
http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html.
"""
pipeline_description = ' ! '.join([
'appsrc name=src',
'volume name=volume',
'autoaudiosink name=sink',
])
def __init__(self, core_queue, output_queue):
super(GStreamerProcess, self).__init__()
self.core_queue = core_queue
self.output_queue = output_queue
self.gst_pipeline = None
self.gst_bus = None
self.gst_bus_id = None
self.gst_uri_bin = None
self.gst_data_src = None
self.gst_volume = None
def run_inside_try(self):
self.setup()
while True:
message = self.output_queue.get()
self.process_mopidy_message(message)
def setup(self):
logger.debug(u'Setting up GStreamer pipeline')
# Start a helper thread that can run the gobject.MainLoop
messages_thread = GStreamerMessagesThread()
messages_thread.daemon = True
messages_thread.start()
self.gst_pipeline = gst.parse_launch(self.pipeline_description)
self.gst_data_src = self.gst_pipeline.get_by_name('src')
#self.gst_uri_bin = self.gst_pipeline.get_by_name('uri')
self.gst_volume = self.gst_pipeline.get_by_name('volume')
# Setup bus and message processor
self.gst_bus = self.gst_pipeline.get_bus()
self.gst_bus.add_signal_watch()
self.gst_bus_id = self.gst_bus.connect('message',
self.process_gst_message)
def process_mopidy_message(self, message):
"""Process messages from the rest of Mopidy."""
if message['command'] == 'play_uri':
response = self.play_uri(message['uri'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
elif message['command'] == 'deliver_data':
self.deliver_data(message['caps'], message['data'])
elif message['command'] == 'end_of_data_stream':
self.end_of_data_stream()
elif message['command'] == 'set_state':
response = self.set_state(message['state'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
elif message['command'] == 'get_volume':
volume = self.get_volume()
connection = unpickle_connection(message['reply_to'])
connection.send(volume)
elif message['command'] == 'set_volume':
self.set_volume(message['volume'])
else:
logger.warning(u'Cannot handle message: %s', message)
def process_gst_message(self, bus, message):
"""Process messages from GStreamer."""
if message.type == gst.MESSAGE_EOS:
logger.debug(u'GStreamer signalled end-of-stream. '
'Sending end_of_track to core_queue ...')
self.core_queue.put({'command': 'end_of_track'})
elif message.type == gst.MESSAGE_ERROR:
self.set_state('NULL')
error, debug = message.parse_error()
logger.error(u'%s %s', error, debug)
# FIXME Should we send 'stop_playback' to core here? Can we
# differentiate on how serious the error is?
def play_uri(self, uri):
"""Play audio at URI"""
self.set_state('READY')
self.gst_uri_bin.set_property('uri', uri)
return self.set_state('PLAYING')
def deliver_data(self, caps_string, data):
"""Deliver audio data to be played"""
caps = gst.caps_from_string(caps_string)
buffer_ = gst.Buffer(buffer(data))
buffer_.set_caps(caps)
self.gst_data_src.set_property('caps', caps)
self.gst_data_src.emit('push-buffer', buffer_)
def end_of_data_stream(self):
"""
Add end-of-stream token to source.
We will get a GStreamer message when the stream playback reaches the
token, and can then do any end-of-stream related tasks.
"""
self.gst_data_src.emit('end-of-stream')
def set_state(self, state_name):
"""
Set the GStreamer state. Returns :class:`True` if successful.
.. digraph:: gst_state_transitions
"NULL" -> "READY"
"PAUSED" -> "PLAYING"
"PAUSED" -> "READY"
"PLAYING" -> "PAUSED"
"READY" -> "NULL"
"READY" -> "PAUSED"
:param state_name: NULL, READY, PAUSED, or PLAYING
:type state_name: string
:rtype: :class:`True` or :class:`False`
"""
result = self.gst_pipeline.set_state(
getattr(gst, 'STATE_' + state_name))
if result == gst.STATE_CHANGE_FAILURE:
logger.warning('Setting GStreamer state to %s: failed', state_name)
return False
else:
logger.debug('Setting GStreamer state to %s: OK', state_name)
return True
def get_volume(self):
"""Get volume in range [0..100]"""
gst_volume = self.gst_volume.get_property('volume')
return int(gst_volume * 100)
def set_volume(self, volume):
"""Set volume in range [0..100]"""
gst_volume = volume / 100.0
self.gst_volume.set_property('volume', gst_volume)

View File

@ -1,55 +1,77 @@
import logging
import multiprocessing
from multiprocessing.reduction import reduce_connection
import pickle
import sys
from mopidy import settings, SettingsError
from mopidy.utils import get_class, unpickle_connection
from mopidy import SettingsError
logger = logging.getLogger('mopidy.process')
def pickle_connection(connection):
return pickle.dumps(reduce_connection(connection))
def unpickle_connection(pickled_connection):
# From http://stackoverflow.com/questions/1446004
(func, args) = pickle.loads(pickled_connection)
return func(*args)
class BaseProcess(multiprocessing.Process):
def run(self):
try:
self._run()
self.run_inside_try()
except KeyboardInterrupt:
logger.info(u'Interrupted by user')
sys.exit(0)
except SettingsError as e:
logger.error(e.message)
sys.exit(1)
except ImportError as e:
logger.error(e)
sys.exit(1)
def _run(self):
def run_inside_try(self):
raise NotImplementedError
class CoreProcess(BaseProcess):
def __init__(self, core_queue):
def __init__(self, core_queue, output_class, backend_class,
frontend_class):
super(CoreProcess, self).__init__()
self.core_queue = core_queue
self._backend = None
self._frontend = None
self.output_queue = None
self.output_class = output_class
self.backend_class = backend_class
self.frontend_class = frontend_class
self.output = None
self.backend = None
self.frontend = None
def _run(self):
self._setup()
def run_inside_try(self):
self.setup()
while True:
message = self.core_queue.get()
self._process_message(message)
self.process_message(message)
def _setup(self):
self._backend = get_class(settings.BACKENDS[0])(
core_queue=self.core_queue)
self._frontend = get_class(settings.FRONTEND)(backend=self._backend)
def setup(self):
self.output_queue = multiprocessing.Queue()
self.output = self.output_class(self.core_queue, self.output_queue)
self.backend = self.backend_class(self.core_queue, self.output_queue)
self.frontend = self.frontend_class(self.backend)
def _process_message(self, message):
if message['command'] == 'mpd_request':
response = self._frontend.handle_request(message['request'])
def process_message(self, message):
if message.get('to') == 'output':
self.output_queue.put(message)
elif message['command'] == 'mpd_request':
response = self.frontend.handle_request(message['request'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
elif message['command'] == 'end_of_track':
self._backend.playback.end_of_track_callback()
self.backend.playback.on_end_of_track()
elif message['command'] == 'stop_playback':
self._backend.playback.stop()
self.backend.playback.stop()
elif message['command'] == 'set_stored_playlists':
self._backend.stored_playlists.playlists = message['playlists']
self.backend.stored_playlists.playlists = message['playlists']
else:
logger.warning(u'Cannot handle message: %s', message)

View File

@ -3,24 +3,21 @@ Available settings and their default values.
.. warning::
Do *not* change settings in ``mopidy/settings.py``. Instead, add a file
called ``~/.mopidy/settings.py`` and redefine settings there.
Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a
file called ``~/.mopidy/settings.py`` and redefine settings there.
"""
from __future__ import absolute_import
import os
import sys
#: List of playback backends to use. See :mod:`mopidy.backends` for all
#: available backends. Default::
#: available backends.
#:
#: BACKENDS = (u'mopidy.backends.despotify.DespotifyBackend',)
#: Default::
#:
#: BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',)
#:
#: .. note::
#: Currently only the first backend in the list is used.
BACKENDS = (
u'mopidy.backends.despotify.DespotifyBackend',
#u'mopidy.backends.libspotify.LibspotifyBackend',
u'mopidy.backends.libspotify.LibspotifyBackend',
)
#: The log format used on the console. See
@ -29,54 +26,61 @@ BACKENDS = (
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
' [%(process)d:%(threadName)s] %(name)s\n %(message)s'
#: The log format used for dump logs. Default::
#: The log format used for dump logs.
#:
#: Default::
#:
#: DUMP_LOG_FILENAME = CONSOLE_LOG_FORMAT
DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT
#: The file to dump debug log data to. Default::
#: The file to dump debug log data to when Mopidy is run with the
#: :option:`--dump` option.
#:
#: Default::
#:
#: DUMP_LOG_FILENAME = u'dump.log'
DUMP_LOG_FILENAME = u'dump.log'
#: Protocol frontend to use. Default::
#: Protocol frontend to use.
#:
#: Default::
#:
#: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
#: Path to folder with local music. Default::
#: Path to folder with local music.
#:
#: Used by :mod:`mopidy.backends.local`.
#:
#: Default::
#:
#: LOCAL_MUSIC_FOLDER = u'~/music'
LOCAL_MUSIC_FOLDER = u'~/music'
#: Path to playlist folder with m3u files for local music. Default::
#: Path to playlist folder with m3u files for local music.
#:
#: Used by :mod:`mopidy.backends.local`.
#:
#: Default::
#:
#: LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists'
#: Path to tag cache for local music. Default::
#: Path to tag cache for local music.
#:
#: Used by :mod:`mopidy.backends.local`.
#:
#: Default::
#:
#: LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache'
#: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers.
#:
#: Default on Linux::
#: Default::
#:
#: MIXER = u'mopidy.mixers.alsa.AlsaMixer'
#:
#: Default on OS X::
#:
#: MIXER = u'mopidy.mixers.osa.OsaMixer'
#:
#: Default on other operating systems::
#:
#: MIXER = u'mopidy.mixers.dummy.DummyMixer'
MIXER = u'mopidy.mixers.dummy.DummyMixer'
if sys.platform == 'linux2':
MIXER = u'mopidy.mixers.alsa.AlsaMixer'
elif sys.platform == 'darwin':
MIXER = u'mopidy.mixers.osa.OsaMixer'
#: MIXER = u'mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer'
MIXER = u'mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer'
#: ALSA mixer only. What mixer control to use. If set to :class:`False`, first
#: ``Master`` and then ``PCM`` will be tried.
@ -87,6 +91,7 @@ MIXER_ALSA_CONTROL = False
#: External mixers only. Which port the mixer is connected to.
#:
#: This must point to the device port like ``/dev/ttyUSB0``.
#:
#: Default: :class:`None`
MIXER_EXT_PORT = None
@ -105,12 +110,33 @@ MIXER_EXT_SPEAKERS_A = None
#: Default: :class:`None`.
MIXER_EXT_SPEAKERS_B = None
#: Server to use. Default::
#: The maximum volume. Integer in the range 0 to 100.
#:
#: If this settings is set to 80, the mixer will set the actual volume to 80
#: when asked to set it to 100.
#:
#: Default::
#:
#: MIXER_MAX_VOLUME = 100
MIXER_MAX_VOLUME = 100
#: Audio output handler to use.
#:
#: Default::
#:
#: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
#: Server to use.
#:
#: Default::
#:
#: SERVER = u'mopidy.frontends.mpd.server.MpdServer'
SERVER = u'mopidy.frontends.mpd.server.MpdServer'
#: Which address Mopidy should bind to. Examples:
#: Which address Mopidy's MPD server should bind to.
#:
#:Examples:
#:
#: ``127.0.0.1``
#: Listens only on the IPv4 loopback interface. Default.
@ -122,24 +148,22 @@ SERVER = u'mopidy.frontends.mpd.server.MpdServer'
#: Listens on all interfaces, both IPv4 and IPv6.
MPD_SERVER_HOSTNAME = u'127.0.0.1'
#: Which TCP port Mopidy should listen to. Default: 6600
#: Which TCP port Mopidy's MPD server should listen to.
#:
#: Default: 6600
MPD_SERVER_PORT = 6600
#: Your Spotify Premium username. Used by all Spotify backends.
SPOTIFY_USERNAME = u''
#: Your Spotify Premium password. Used by all Spotify backends.
SPOTIFY_PASSWORD = u''
#: Path to your libspotify application key. Used by LibspotifyBackend.
SPOTIFY_LIB_APPKEY = u'~/.mopidy/spotify_appkey.key'
#: Path to the libspotify cache. Used by LibspotifyBackend.
#: Path to the libspotify cache.
#:
#: Used by :mod:`mopidy.backends.libspotify`.
SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache'
# Import user specific settings
dotdir = os.path.expanduser(u'~/.mopidy/')
settings_file = os.path.join(dotdir, u'settings.py')
if os.path.isfile(settings_file):
sys.path.insert(0, dotdir)
from settings import *
#: Your Spotify Premium username.
#:
#: Used by :mod:`mopidy.backends.libspotify`.
SPOTIFY_USERNAME = u''
#: Your Spotify Premium password.
#:
#: Used by :mod:`mopidy.backends.libspotify`.
SPOTIFY_PASSWORD = u''

38
mopidy/utils/__init__.py Normal file
View File

@ -0,0 +1,38 @@
import logging
import os
import sys
logger = logging.getLogger('mopidy.utils')
def flatten(the_list):
result = []
for element in the_list:
if isinstance(element, list):
result.extend(flatten(element))
else:
result.append(element)
return result
def import_module(name):
__import__(name)
return sys.modules[name]
def get_class(name):
module_name = name[:name.rindex('.')]
class_name = name[name.rindex('.') + 1:]
logger.debug('Loading: %s', name)
try:
module = import_module(module_name)
class_object = getattr(module, class_name)
except (ImportError, AttributeError):
raise ImportError("Couldn't load: %s" % name)
return class_object
def indent(string, places=4, linebreak='\n'):
lines = string.split(linebreak)
if len(lines) == 1:
return string
result = u''
for line in lines:
result += linebreak + ' ' * places + line
return result

21
mopidy/utils/path.py Normal file
View File

@ -0,0 +1,21 @@
import logging
import os
import sys
import urllib
logger = logging.getLogger('mopidy.utils.path')
def get_or_create_folder(folder):
folder = os.path.expanduser(folder)
if not os.path.isdir(folder):
logger.info(u'Creating %s', folder)
os.mkdir(folder, 0755)
return folder
def path_to_uri(*paths):
path = os.path.join(*paths)
#path = os.path.expanduser(path) # FIXME Waiting for test case?
path = path.encode('utf-8')
if sys.platform == 'win32':
return 'file:' + urllib.pathname2url(path)
return 'file://' + urllib.pathname2url(path)

132
mopidy/utils/settings.py Normal file
View File

@ -0,0 +1,132 @@
# Absolute import needed to import ~/.mopidy/settings.py and not ourselves
from __future__ import absolute_import
from copy import copy
import logging
import os
import sys
from mopidy import SettingsError
from mopidy.utils import indent
logger = logging.getLogger('mopidy.utils.settings')
class SettingsProxy(object):
def __init__(self, default_settings_module):
self.default = self._get_settings_dict_from_module(
default_settings_module)
self.local = self._get_local_settings()
def _get_local_settings(self):
dotdir = os.path.expanduser(u'~/.mopidy/')
settings_file = os.path.join(dotdir, u'settings.py')
if not os.path.isfile(settings_file):
return {}
sys.path.insert(0, dotdir)
import settings as local_settings_module
return self._get_settings_dict_from_module(local_settings_module)
def _get_settings_dict_from_module(self, module):
settings = filter(lambda (key, value): self._is_setting(key),
module.__dict__.iteritems())
return dict(settings)
def _is_setting(self, name):
return name.isupper()
@property
def current(self):
current = copy(self.default)
current.update(self.local)
return current
def __getattr__(self, attr):
if not self._is_setting(attr):
return
if attr not in self.current:
raise SettingsError(u'Setting "%s" is not set.' % attr)
value = self.current[attr]
if type(value) != bool and not value:
raise SettingsError(u'Setting "%s" is empty.' % attr)
return value
def validate(self):
if self.get_errors():
logger.error(u'Settings validation errors: %s',
indent(self.get_errors_as_string()))
raise SettingsError(u'Settings validation failed.')
def get_errors(self):
return validate_settings(self.default, self.local)
def get_errors_as_string(self):
lines = []
for (setting, error) in self.get_errors().iteritems():
lines.append(u'%s: %s' % (setting, error))
return '\n'.join(lines)
def validate_settings(defaults, settings):
"""
Checks the settings for both errors like misspellings and against a set of
rules for renamed settings, etc.
Returns of setting names with associated errors.
:param defaults: Mopidy's default settings
:type defaults: dict
:param settings: the user's local settings
:type settings: dict
:rtype: dict
"""
errors = {}
changed = {
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
'SERVER_PORT': 'MPD_SERVER_PORT',
'SPOTIFY_LIB_APPKEY': None,
}
for setting, value in settings.iteritems():
if setting in changed:
if changed[setting] is None:
errors[setting] = u'Deprecated setting. It may be removed.'
else:
errors[setting] = u'Deprecated setting. Use %s.' % (
changed[setting],)
continue
if setting == 'BACKENDS':
if 'mopidy.backends.despotify.DespotifyBackend' in value:
errors[setting] = (u'Deprecated setting value. ' +
'"mopidy.backends.despotify.DespotifyBackend" is no ' +
'longer available.')
continue
if setting not in defaults:
errors[setting] = u'Unknown setting. Is it misspelled?'
continue
return errors
def list_settings_optparse_callback(*args):
"""
Prints a list of all settings.
Called by optparse when Mopidy is run with the :option:`--list-settings`
option.
"""
from mopidy import settings
errors = settings.get_errors()
lines = []
for (key, value) in sorted(settings.current.iteritems()):
default_value = settings.default.get(key)
if key.endswith('PASSWORD'):
value = u'********'
lines.append(u'%s:' % key)
lines.append(u' Value: %s' % repr(value))
if value != default_value and default_value is not None:
lines.append(u' Default: %s' % repr(default_value))
if errors.get(key) is not None:
lines.append(u' Error: %s' % errors[key])
print u'Settings: %s' % indent('\n'.join(lines), places=2)
sys.exit(0)

View File

@ -1,9 +1,34 @@
"""
Most of this file is taken from the Django project, which is BSD licensed.
"""
from distutils.core import setup
from distutils.command.install_data import install_data
from distutils.command.install import INSTALL_SCHEMES
import os
import sys
from mopidy import get_version
class osx_install_data(install_data):
# On MacOS, the platform-specific lib dir is
# /System/Library/Framework/Python/.../ which is wrong. Python 2.5 supplied
# with MacOS 10.5 has an Apple-specific fix for this in
# distutils.command.install_data#306. It fixes install_lib but not
# install_data, which is why we roll our own install_data class.
def finalize_options(self):
# By the time finalize_options is called, install.install_lib is set to
# the fixed directory, so we set the installdir to install_lib. The
# install_data class uses ('install_data', 'install_dir') instead.
self.set_undefined_options('install', ('install_lib', 'install_dir'))
install_data.finalize_options(self)
if sys.platform == "darwin":
cmdclasses = {'install_data': osx_install_data}
else:
cmdclasses = {'install_data': install_data}
def fullsplit(path, result=None):
"""
Split a pathname into components (the opposite of os.path.join) in a
@ -20,7 +45,8 @@ def fullsplit(path, result=None):
# Tell distutils to put the data_files in platform-specific installation
# locations. See here for an explanation:
# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb
# http://groups.google.com/group/comp.lang.python/browse_thread/
# thread/35ec7b2fed36eaec/2105ee4d9e8042cb
for scheme in INSTALL_SCHEMES.values():
scheme['data'] = scheme['purelib']
@ -49,17 +75,19 @@ setup(
author='Stein Magnus Jodal',
author_email='stein.magnus@jodal.no',
packages=packages,
package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']},
cmdclass=cmdclasses,
data_files=data_files,
scripts=['bin/mopidy'],
url='http://www.mopidy.com/',
license='GPLv2',
license='Apache License, Version 2.0',
description='MPD server with Spotify support',
long_description=open('README.rst').read(),
classifiers=[
'Development Status :: 3 - Alpha',
'Development Status :: 4 - Beta',
'Environment :: No Input/Output (Daemon)',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License (GPL)',
'License :: OSI Approved :: Apache Software License',
'Operating System :: MacOS :: MacOS X',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 2.6',

View File

@ -32,7 +32,7 @@ class BaseCurrentPlaylistControllerTest(object):
backend_class = None
def setUp(self):
self.backend = self.backend_class(mixer=DummyMixer())
self.backend = self.backend_class(mixer_class=DummyMixer)
self.controller = self.backend.current_playlist
self.playback = self.backend.playback
@ -91,20 +91,14 @@ class BaseCurrentPlaylistControllerTest(object):
self.controller.clear()
self.assertEqual(self.playback.state, self.playback.STOPPED)
def test_load(self):
tracks = []
self.assertNotEqual(id(tracks), id(self.controller.tracks))
self.controller.load(tracks)
self.assertEqual(tracks, self.controller.tracks)
def test_get_by_uri_returns_unique_match(self):
track = Track(uri='a')
self.controller.load([Track(uri='z'), track, Track(uri='y')])
self.controller.append([Track(uri='z'), track, Track(uri='y')])
self.assertEqual(track, self.controller.get(uri='a')[1])
def test_get_by_uri_raises_error_if_multiple_matches(self):
track = Track(uri='a')
self.controller.load([Track(uri='z'), track, track])
self.controller.append([Track(uri='z'), track, track])
try:
self.controller.get(uri='a')
self.fail(u'Should raise LookupError if multiple matches')
@ -124,7 +118,7 @@ class BaseCurrentPlaylistControllerTest(object):
track1 = Track(uri='a', name='x')
track2 = Track(uri='b', name='x')
track3 = Track(uri='b', name='y')
self.controller.load([track1, track2, track3])
self.controller.append([track1, track2, track3])
self.assertEqual(track1, self.controller.get(uri='a', name='x')[1])
self.assertEqual(track2, self.controller.get(uri='b', name='x')[1])
self.assertEqual(track3, self.controller.get(uri='b', name='y')[1])
@ -133,37 +127,37 @@ class BaseCurrentPlaylistControllerTest(object):
track1 = Track()
track2 = Track(uri='b')
track3 = Track()
self.controller.load([track1, track2, track3])
self.controller.append([track1, track2, track3])
self.assertEqual(track2, self.controller.get(uri='b')[1])
@populate_playlist
def test_load_replaces_playlist(self):
self.backend.current_playlist.load([])
self.assertEqual(len(self.backend.current_playlist.tracks), 0)
def test_append_appends_to_the_current_playlist(self):
self.controller.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.controller.tracks), 2)
self.controller.append([Track(uri='c'), Track(uri='d')])
self.assertEqual(len(self.controller.tracks), 4)
self.assertEqual(self.controller.tracks[0].uri, 'a')
self.assertEqual(self.controller.tracks[1].uri, 'b')
self.assertEqual(self.controller.tracks[2].uri, 'c')
self.assertEqual(self.controller.tracks[3].uri, 'd')
def test_load_does_not_reset_version(self):
def test_append_does_not_reset_version(self):
version = self.controller.version
self.controller.load([])
self.controller.append([])
self.assertEqual(self.controller.version, version + 1)
@populate_playlist
def test_load_preserves_playing_state(self):
tracks = self.controller.tracks
playback = self.playback
def test_append_preserves_playing_state(self):
self.playback.play()
self.controller.load([tracks[1]])
self.assertEqual(playback.state, playback.PLAYING)
self.assertEqual(tracks[1], self.playback.current_track)
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.current_track, track)
@populate_playlist
def test_load_preserves_stopped_state(self):
tracks = self.controller.tracks
playback = self.playback
self.controller.load([tracks[2]])
self.assertEqual(playback.state, playback.STOPPED)
self.assertEqual(None, self.playback.current_track)
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.current_track, None)
@populate_playlist
def test_move_single(self):
@ -272,7 +266,7 @@ class BaseCurrentPlaylistControllerTest(object):
def test_version(self):
version = self.controller.version
self.controller.load([])
self.controller.append([])
self.assert_(version < self.controller.version)
@ -281,7 +275,7 @@ class BasePlaybackControllerTest(object):
backend_class = None
def setUp(self):
self.backend = self.backend_class(mixer=DummyMixer())
self.backend = self.backend_class(mixer_class=DummyMixer)
self.playback = self.backend.playback
self.current_playlist = self.backend.current_playlist
@ -351,10 +345,18 @@ class BasePlaybackControllerTest(object):
self.playback.play(self.current_playlist.cp_tracks[-1])
self.assertEqual(self.playback.current_track, self.tracks[-1])
@populate_playlist
def test_play_skips_to_next_track_on_failure(self):
# If _play() returns False, it is a failure.
self.playback._play = lambda track: track != self.tracks[0]
self.playback.play()
self.assertNotEqual(self.playback.current_track, self.tracks[0])
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_current_track_after_completed_playlist(self):
self.playback.play(self.current_playlist.cp_tracks[-1])
self.playback.end_of_track_callback()
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
@ -363,6 +365,56 @@ class BasePlaybackControllerTest(object):
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_playlist
def test_previous(self):
self.playback.play()
self.playback.next()
self.playback.previous()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_playlist
def test_previous_more(self):
self.playback.play() # At track 0
self.playback.next() # At track 1
self.playback.next() # At track 2
self.playback.previous() # At track 1
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_previous_return_value(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.previous(), None)
@populate_playlist
def test_previous_does_not_trigger_playback(self):
self.playback.play()
self.playback.next()
self.playback.stop()
self.playback.previous()
self.assertEqual(self.playback.state, self.playback.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.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.current_track, None)
@populate_playlist
def test_previous_skips_to_previous_track_on_failure(self):
# If _play() returns False, it is a failure.
self.playback._play = lambda track: track != self.tracks[1]
self.playback.play(self.current_playlist.cp_tracks[2])
self.assertEqual(self.playback.current_track, self.tracks[2])
self.playback.previous()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_playlist
def test_next(self):
self.playback.play()
@ -418,70 +470,40 @@ class BasePlaybackControllerTest(object):
self.assertEqual(self.playback.state, self.playback.STOPPED)
@populate_playlist
def test_previous(self):
def test_next_skips_to_next_track_on_failure(self):
# If _play() returns False, it is a failure.
self.playback._play = lambda track: track != self.tracks[1]
self.playback.play()
self.playback.next()
self.playback.previous()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_playlist
def test_previous_more(self):
self.playback.play() # At track 0
self.playback.next() # At track 1
self.playback.next() # At track 2
self.playback.previous() # At track 1
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_previous_return_value(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.previous(), None)
@populate_playlist
def test_previous_does_not_trigger_playback(self):
self.playback.play()
self.playback.next()
self.playback.stop()
self.playback.previous()
self.assertEqual(self.playback.state, self.playback.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.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.current_track, None)
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[2])
@populate_playlist
def test_next_track_before_play(self):
self.assertEqual(self.playback.next_track, self.tracks[0])
self.assertEqual(self.playback.track_at_next, self.tracks[0])
@populate_playlist
def test_next_track_during_play(self):
self.playback.play()
self.assertEqual(self.playback.next_track, self.tracks[1])
self.assertEqual(self.playback.track_at_next, self.tracks[1])
@populate_playlist
def test_next_track_after_previous(self):
self.playback.play()
self.playback.next()
self.playback.previous()
self.assertEqual(self.playback.next_track, self.tracks[1])
self.assertEqual(self.playback.track_at_next, self.tracks[1])
def test_next_track_empty_playlist(self):
self.assertEqual(self.playback.next_track, None)
self.assertEqual(self.playback.track_at_next, None)
@populate_playlist
def test_next_track_at_end_of_playlist(self):
self.playback.play()
for track in self.current_playlist.cp_tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.next_track, None)
self.assertEqual(self.playback.track_at_next, None)
@populate_playlist
def test_next_track_at_end_of_playlist_with_repeat(self):
@ -489,28 +511,189 @@ class BasePlaybackControllerTest(object):
self.playback.play()
for track in self.tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.next_track, self.tracks[0])
self.assertEqual(self.playback.track_at_next, self.tracks[0])
@populate_playlist
def test_next_track_with_random(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.next_track, self.tracks[2])
self.assertEqual(self.playback.track_at_next, self.tracks[2])
@populate_playlist
def test_next_with_consume(self):
self.playback.consume = True
self.playback.play()
self.playback.next()
self.assert_(self.tracks[0] in self.backend.current_playlist.tracks)
@populate_playlist
def test_next_with_single_and_repeat(self):
self.playback.single = True
self.playback.repeat = True
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_next_with_random(self):
# FIXME feels very fragile
random.seed(1)
self.playback.random = True
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_next_track_with_random_after_append_playlist(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.track_at_next, self.tracks[2])
self.backend.current_playlist.append(self.tracks[:1])
self.assertEqual(self.playback.track_at_next, self.tracks[1])
@populate_playlist
def test_end_of_track(self):
self.playback.play()
old_position = self.playback.current_playlist_position
old_uri = self.playback.current_track.uri
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_playlist_position,
old_position+1)
self.assertNotEqual(self.playback.current_track.uri, old_uri)
@populate_playlist
def test_end_of_track_return_value(self):
self.playback.play()
self.assertEqual(self.playback.on_end_of_track(), None)
@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)
@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.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)
@populate_playlist
def test_end_of_track_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
for track in self.tracks:
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, self.playback.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)
@populate_playlist
def test_end_of_track_skips_to_next_track_on_failure(self):
# If _play() returns False, it is a failure.
self.playback._play = lambda track: track != self.tracks[1]
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
self.playback.on_end_of_track()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[2])
@populate_playlist
def test_end_of_track_track_before_play(self):
self.assertEqual(self.playback.track_at_next, self.tracks[0])
@populate_playlist
def test_end_of_track_track_during_play(self):
self.playback.play()
self.assertEqual(self.playback.track_at_next, self.tracks[1])
@populate_playlist
def test_end_of_track_track_after_previous(self):
self.playback.play()
self.playback.on_end_of_track()
self.playback.previous()
self.assertEqual(self.playback.track_at_next, self.tracks[1])
def test_end_of_track_track_empty_playlist(self):
self.assertEqual(self.playback.track_at_next, None)
@populate_playlist
def test_end_of_track_track_at_end_of_playlist(self):
self.playback.play()
for track in self.current_playlist.cp_tracks[1:]:
self.playback.on_end_of_track()
self.assertEqual(self.playback.track_at_next, None)
@populate_playlist
def test_end_of_track_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
for track in self.tracks[1:]:
self.playback.on_end_of_track()
self.assertEqual(self.playback.track_at_next, self.tracks[0])
@populate_playlist
def test_end_of_track_track_with_random(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.track_at_next, self.tracks[2])
@populate_playlist
def test_end_of_track_with_consume(self):
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)
@populate_playlist
def test_end_of_track_with_random(self):
# FIXME feels very fragile
random.seed(1)
self.playback.random = True
self.playback.play()
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_end_of_track_track_with_random_after_append_playlist(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.track_at_next, self.tracks[2])
self.backend.current_playlist.append(self.tracks[:1])
self.assertEqual(self.playback.track_at_next, self.tracks[1])
@populate_playlist
def test_previous_track_before_play(self):
self.assertEqual(self.playback.previous_track, None)
self.assertEqual(self.playback.track_at_previous, None)
@populate_playlist
def test_previous_track_after_play(self):
self.playback.play()
self.assertEqual(self.playback.previous_track, None)
self.assertEqual(self.playback.track_at_previous, None)
@populate_playlist
def test_previous_track_after_next(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.previous_track, self.tracks[0])
self.assertEqual(self.playback.track_at_previous, self.tracks[0])
@populate_playlist
def test_previous_track_after_previous(self):
@ -518,17 +701,17 @@ class BasePlaybackControllerTest(object):
self.playback.next() # At track 1
self.playback.next() # At track 2
self.playback.previous() # At track 1
self.assertEqual(self.playback.previous_track, self.tracks[0])
self.assertEqual(self.playback.track_at_previous, self.tracks[0])
def test_previous_track_empty_playlist(self):
self.assertEqual(self.playback.previous_track, None)
self.assertEqual(self.playback.track_at_previous, None)
@populate_playlist
def test_previous_track_with_consume(self):
self.playback.consume = True
for track in self.tracks:
self.playback.next()
self.assertEqual(self.playback.previous_track,
self.assertEqual(self.playback.track_at_previous,
self.playback.current_track)
@populate_playlist
@ -536,7 +719,7 @@ class BasePlaybackControllerTest(object):
self.playback.random = True
for track in self.tracks:
self.playback.next()
self.assertEqual(self.playback.previous_track,
self.assertEqual(self.playback.track_at_previous,
self.playback.current_track)
@populate_playlist
@ -572,33 +755,33 @@ class BasePlaybackControllerTest(object):
@populate_playlist
def test_current_playlist_position_at_end_of_playlist(self):
self.playback.play(self.current_playlist.cp_tracks[-1])
self.playback.end_of_track_callback()
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_playlist_position, None)
def test_new_playlist_loaded_callback_gets_called(self):
callback = self.playback.new_playlist_loaded_callback
def test_on_current_playlist_change_gets_called(self):
callback = self.playback.on_current_playlist_change
def wrapper():
wrapper.called = True
return callback()
wrapper.called = False
self.playback.new_playlist_loaded_callback = wrapper
self.backend.current_playlist.load([])
self.playback.on_current_playlist_change = wrapper
self.backend.current_playlist.append([])
self.assert_(wrapper.called)
@populate_playlist
def test_end_of_track_callback_gets_called(self):
end_of_track_callback = self.playback.end_of_track_callback
def test_on_end_of_track_gets_called(self):
on_end_of_track = self.playback.on_end_of_track
event = threading.Event()
def wrapper():
result = end_of_track_callback()
result = on_end_of_track()
event.set()
return result
self.playback.end_of_track_callback = wrapper
self.playback.on_end_of_track = wrapper
self.playback.play()
self.playback.seek(self.tracks[0].length - 10)
@ -608,27 +791,28 @@ class BasePlaybackControllerTest(object):
self.assert_(event.is_set())
@populate_playlist
def test_new_playlist_loaded_callback_when_playing(self):
def test_on_current_playlist_change_when_playing(self):
self.playback.play()
self.backend.current_playlist.load([self.tracks[2]])
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.current_track, self.tracks[2])
self.assertEqual(self.playback.current_track, current_track)
@populate_playlist
def test_new_playlist_loaded_callback_when_stopped(self):
self.backend.current_playlist.load([self.tracks[2]])
def test_on_current_playlist_change_when_stopped(self):
current_track = self.playback.current_track
self.backend.current_playlist.append([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.next_track, self.tracks[2])
@populate_playlist
def test_new_playlist_loaded_callback_when_paused(self):
def test_on_current_playlist_change_when_paused(self):
self.playback.play()
self.playback.pause()
self.backend.current_playlist.load([self.tracks[2]])
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.next_track, self.tracks[2])
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.current_track, current_track)
@populate_playlist
def test_pause_when_stopped(self):
@ -804,19 +988,12 @@ class BasePlaybackControllerTest(object):
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_playlist
def test_next_with_consume(self):
self.playback.consume = True
self.playback.play()
self.playback.next()
self.assert_(self.tracks[0] not in self.backend.current_playlist.tracks)
@populate_playlist
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
self.playback.consume = True
self.playback.play()
for i in range(len(self.backend.current_playlist.tracks)):
self.playback.next()
self.playback.on_end_of_track()
self.assertEqual(len(self.backend.current_playlist.tracks), 0)
@populate_playlist
@ -826,15 +1003,6 @@ class BasePlaybackControllerTest(object):
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[2])
@populate_playlist
def test_next_with_random(self):
# FIXME feels very fragile
random.seed(1)
self.playback.random = True
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_previous_with_random(self):
random.seed(1)
@ -848,13 +1016,21 @@ class BasePlaybackControllerTest(object):
@populate_playlist
def test_end_of_song_starts_next_track(self):
self.playback.play()
self.playback.end_of_track_callback()
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_playlist
def test_end_of_song_with_single_and_repeat_starts_same(self):
self.playback.single = True
self.playback.repeat = True
self.playback.play()
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_playlist
def test_end_of_playlist_stops(self):
self.playback.play(self.current_playlist.cp_tracks[-1])
self.playback.end_of_track_callback()
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, self.playback.STOPPED)
def test_repeat_off_by_default(self):
@ -872,14 +1048,14 @@ class BasePlaybackControllerTest(object):
self.playback.play()
for track in self.tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.next_track, None)
self.assertEqual(self.playback.track_at_next, None)
@populate_playlist
def test_random_until_end_of_playlist_and_play_from_start(self):
self.playback.repeat = True
for track in self.tracks:
self.playback.next()
self.assertNotEqual(self.playback.next_track, None)
self.assertNotEqual(self.playback.track_at_next, None)
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, self.playback.PLAYING)
@ -891,15 +1067,7 @@ class BasePlaybackControllerTest(object):
self.playback.play()
for track in self.tracks:
self.playback.next()
self.assertNotEqual(self.playback.next_track, None)
@populate_playlist
def test_next_track_with_random_after_load_playlist(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.next_track, self.tracks[2])
self.backend.current_playlist.load(self.tracks[:1])
self.assertEqual(self.playback.next_track, self.tracks[0])
self.assertNotEqual(self.playback.track_at_next, None)
@populate_playlist
def test_played_track_during_random_not_played_again(self):
@ -911,13 +1079,9 @@ class BasePlaybackControllerTest(object):
played.append(self.playback.current_track)
self.playback.next()
def test_playing_track_with_invalid_uri(self):
self.backend.current_playlist.load([Track(uri='foobar')])
self.playback.play()
self.assertEqual(self.playback.state, self.playback.STOPPED)
@populate_playlist
def test_playing_track_that_isnt_in_playlist(self):
test = lambda: self.playback.play(self.tracks[0])
test = lambda: self.playback.play((17, Track()))
self.assertRaises(AssertionError, test)
@ -933,7 +1097,7 @@ class BaseStoredPlaylistsControllerTest(object):
settings.LOCAL_TAG_CACHE = data_folder('library_tag_cache')
settings.LOCAL_MUSIC_FOLDER = data_folder('')
self.backend = self.backend_class(mixer=DummyMixer())
self.backend = self.backend_class(mixer_class=DummyMixer)
self.stored = self.backend.stored_playlists
def tearDown(self):
@ -1002,21 +1166,6 @@ class BaseStoredPlaylistsControllerTest(object):
except LookupError as e:
self.assertEqual(u'"name=c" match no playlists', e[0])
def test_search_returns_empty_list(self):
self.assertEqual([], self.stored.search('test'))
def test_search_returns_playlist(self):
playlist = self.stored.create('test')
playlists = self.stored.search('test')
self.assert_(playlist in playlists)
def test_search_returns_mulitple_playlists(self):
playlist1 = self.stored.create('test')
playlist2 = self.stored.create('test2')
playlists = self.stored.search('test')
self.assert_(playlist1 in playlists)
self.assert_(playlist2 in playlists)
def test_lookup(self):
raise SkipTest
@ -1055,7 +1204,7 @@ class BaseLibraryControllerTest(object):
Track()]
def setUp(self):
self.backend = self.backend_class(mixer=DummyMixer())
self.backend = self.backend_class(mixer_class=DummyMixer)
self.library = self.backend.library
def tearDown(self):

View File

@ -1,35 +0,0 @@
# TODO This integration test is work in progress.
import unittest
from mopidy.backends.despotify import DespotifyBackend
from mopidy.models import Track
from tests.backends.base import *
uris = [
'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt',
'spotify:track:111sulhaZqgsnypz3MkiaW',
'spotify:track:7t8oznvbeiAPMDRuK0R5ZT',
]
class DespotifyCurrentPlaylistControllerTest(
BaseCurrentPlaylistControllerTest, unittest.TestCase):
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
backend_class = DespotifyBackend
class DespotifyPlaybackControllerTest(
BasePlaybackControllerTest, unittest.TestCase):
tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)]
backend_class = DespotifyBackend
class DespotifyStoredPlaylistsControllerTest(
BaseStoredPlaylistsControllerTest, unittest.TestCase):
backend_class = DespotifyBackend
class DespotifyLibraryControllerTest(
BaseLibraryControllerTest, unittest.TestCase):
backend_class = DespotifyBackend

View File

View File

@ -11,7 +11,7 @@ from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Playlist, Track
from mopidy.utils import path_to_uri
from mopidy.utils.path import path_to_uri
from tests.backends.base import *
from tests import SkipTest, data_folder
@ -19,7 +19,6 @@ from tests import SkipTest, data_folder
song = data_folder('song%s.wav')
generate_song = lambda i: path_to_uri(song % i)
# FIXME can be switched to generic test
class LocalCurrentPlaylistControllerTest(BaseCurrentPlaylistControllerTest,
unittest.TestCase):
@ -116,7 +115,7 @@ class LocalStoredPlaylistsControllerTest(BaseStoredPlaylistsControllerTest,
self.stored.save(playlist)
self.backend.destroy()
self.backend = self.backend_class(mixer=DummyMixer())
self.backend = self.backend_class(mixer_class=DummyMixer)
self.stored = self.backend.stored_playlists
self.assert_(self.stored.playlists)

View File

@ -1,77 +1,15 @@
#encoding: utf-8
# encoding: utf-8
import os
import sys
import shutil
import tempfile
import unittest
from mopidy.utils import *
from mopidy.utils.path import path_to_uri
from mopidy.backends.local.translator import parse_m3u, parse_mpd_tag_cache
from mopidy.models import Track, Artist, Album
from tests import SkipTest, data_folder
class GetOrCreateFolderTest(unittest.TestCase):
def setUp(self):
self.parent = tempfile.mkdtemp()
def tearDown(self):
if os.path.isdir(self.parent):
shutil.rmtree(self.parent)
def test_creating_folder(self):
folder = os.path.join(self.parent, 'test')
self.assert_(not os.path.exists(folder))
self.assert_(not os.path.isdir(folder))
created = get_or_create_folder(folder)
self.assert_(os.path.exists(folder))
self.assert_(os.path.isdir(folder))
self.assertEqual(created, folder)
def test_creating_existing_folder(self):
created = get_or_create_folder(self.parent)
self.assert_(os.path.exists(self.parent))
self.assert_(os.path.isdir(self.parent))
self.assertEqual(created, self.parent)
def test_that_userfolder_is_expanded(self):
raise SkipTest # Not sure how to safely test this
class PathToFileURITest(unittest.TestCase):
def test_simple_path(self):
if sys.platform == 'win32':
result = path_to_uri(u'C:/WINDOWS/clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
else:
result = path_to_uri(u'/etc/fstab')
self.assertEqual(result, 'file:///etc/fstab')
def test_folder_and_path(self):
if sys.platform == 'win32':
result = path_to_uri(u'C:/WINDOWS/', u'clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
else:
result = path_to_uri(u'/etc', u'fstab')
self.assertEqual(result, u'file:///etc/fstab')
def test_space_in_path(self):
if sys.platform == 'win32':
result = path_to_uri(u'C:/test this')
self.assertEqual(result, 'file:///C://test%20this')
else:
result = path_to_uri(u'/tmp/test this')
self.assertEqual(result, u'file:///tmp/test%20this')
def test_unicode_in_path(self):
if sys.platform == 'win32':
result = path_to_uri(u'C:/æøå')
self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5')
else:
result = path_to_uri(u'/tmp/æøå')
self.assertEqual(result, u'file:///tmp/%C3%A6%C3%B8%C3%A5')
song1_path = data_folder('song1.mp3')
song2_path = data_folder('song2.mp3')
encoded_path = data_folder(u'æøå.mp3')
@ -79,7 +17,6 @@ song1_uri = path_to_uri(song1_path)
song2_uri = path_to_uri(song2_path)
encoded_uri = path_to_uri(encoded_path)
class M3UToUriTest(unittest.TestCase):
def test_empty_file(self):
uris = parse_m3u(data_folder('empty.m3u'))

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,8 +6,7 @@ from mopidy.mixers.dummy import DummyMixer
class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_enableoutput(self):

View File

@ -6,8 +6,7 @@ from mopidy.mixers.dummy import DummyMixer
class CommandListsTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_command_list_begin(self):

View File

@ -6,8 +6,7 @@ from mopidy.mixers.dummy import DummyMixer
class ConnectionHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_close(self):

View File

@ -7,14 +7,13 @@ from mopidy.models import Track
class CurrentPlaylistHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_add(self):
needle = Track(uri='dummy://foo')
self.b.library._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'add "dummy://foo"')
@ -23,6 +22,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(len(result), 1)
self.assert_(u'OK' in result)
def test_add_with_uri_not_found_in_library_should_not_call_lookup(self):
self.b.library.lookup = lambda uri: self.fail("Shouldn't run")
result = self.h.handle_request(u'add "foo"')
self.assertEqual(result[0],
u'ACK [50@0] {add} directory or file not found')
def test_add_with_uri_not_found_in_library_should_ack(self):
result = self.h.handle_request(u'add "dummy://foo"')
self.assertEqual(result[0],
@ -31,7 +36,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo')
self.b.library._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'addid "dummy://foo"')
@ -44,7 +49,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo')
self.b.library._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'addid "dummy://foo" "3"')
@ -57,7 +62,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo')
self.b.library._library = [Track(), Track(), needle, Track()]
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'addid "dummy://foo" "6"')
@ -68,7 +73,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
def test_clear(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'clear')
@ -77,7 +82,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_delete_songpos(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'delete "%d"' %
@ -86,7 +91,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_delete_songpos_out_of_bounds(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'delete "5"')
@ -94,7 +99,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
def test_delete_open_range(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'delete "1:"')
@ -102,7 +107,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_delete_closed_range(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'delete "1:3"')
@ -110,7 +115,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_delete_range_out_of_bounds(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 5)
result = self.h.handle_request(u'delete "5:7"')
@ -118,21 +123,21 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
def test_deleteid(self):
self.b.current_playlist.load([Track(), Track()])
self.b.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 2)
result = self.h.handle_request(u'deleteid "2"')
self.assertEqual(len(self.b.current_playlist.tracks), 1)
self.assert_(u'OK' in result)
def test_deleteid_does_not_exist(self):
self.b.current_playlist.load([Track(), Track()])
self.b.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 2)
result = self.h.handle_request(u'deleteid "12345"')
self.assertEqual(len(self.b.current_playlist.tracks), 2)
self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
def test_move_songpos(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -146,7 +151,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_move_open_range(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -160,7 +165,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_move_closed_range(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -174,7 +179,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_moveid(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -196,33 +201,37 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
result = self.h.handle_request(u'playlistfind "tag" "needle"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistfind_by_filename(self):
def test_playlistfind_by_filename_not_in_current_playlist(self):
result = self.h.handle_request(
u'playlistfind "filename" "file:///dev/null"')
self.assertEqual(len(result), 1)
self.assert_(u'OK' in result)
def test_playlistfind_by_filename_without_quotes(self):
result = self.h.handle_request(
u'playlistfind filename "file:///dev/null"')
self.assertEqual(len(result), 1)
self.assert_(u'OK' in result)
def test_playlistfind_by_filename_in_current_playlist(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(uri='file:///exists')])
result = self.h.handle_request(
u'playlistfind filename "file:///exists"')
self.assert_(u'file: file:///exists' in result)
self.assert_(u'Id: 1' in result)
self.assert_(u'Pos: 0' in result)
self.assert_(u'OK' in result)
def test_playlistid_without_songid(self):
self.b.current_playlist.load([Track(name='a'), Track(name='b')])
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.h.handle_request(u'playlistid')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'OK' in result)
def test_playlistid_with_songid(self):
self.b.current_playlist.load([Track(name='a'), Track(name='b')])
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.h.handle_request(u'playlistid "2"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Id: 1' not in result)
@ -231,12 +240,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistid_with_not_existing_songid_fails(self):
self.b.current_playlist.load([Track(name='a'), Track(name='b')])
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.h.handle_request(u'playlistid "25"')
self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song')
def test_playlistinfo_without_songpos_or_range(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -250,7 +259,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistinfo_with_songpos(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -269,7 +278,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(result1, result2)
def test_playlistinfo_with_open_range(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -283,7 +292,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_playlistinfo_with_closed_range(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -313,7 +322,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_plchanges(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
result = self.h.handle_request(u'plchanges "0"')
self.assert_(u'Title: a' in result)
@ -322,7 +331,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_plchanges_with_minus_one_returns_entire_playlist(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
result = self.h.handle_request(u'plchanges "-1"')
self.assert_(u'Title: a' in result)
@ -331,7 +340,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_plchanges_without_quotes_works(self):
self.b.current_playlist.load(
self.b.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
result = self.h.handle_request(u'plchanges 0')
self.assert_(u'Title: a' in result)
@ -340,7 +349,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_plchangesposid(self):
self.b.current_playlist.load([Track(), Track(), Track()])
self.b.current_playlist.append([Track(), Track(), Track()])
result = self.h.handle_request(u'plchangesposid "0"')
self.assert_(u'cpos: 0' in result)
self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[0][0]
@ -354,7 +363,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_shuffle_without_range(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -364,7 +373,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_shuffle_with_open_range(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -378,7 +387,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_shuffle_with_closed_range(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -392,7 +401,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_swap(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
@ -406,7 +415,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_swapid(self):
self.b.current_playlist.load([
self.b.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])

View File

@ -6,8 +6,7 @@ from mopidy.mixers.dummy import DummyMixer
class MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_count(self):

View File

@ -7,8 +7,7 @@ from mopidy.models import Track
class PlaybackOptionsHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_consume_off(self):
@ -167,8 +166,7 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
class PlaybackControlHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_next(self):
@ -176,7 +174,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_pause_off(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
self.h.handle_request(u'play "0"')
self.h.handle_request(u'pause "1"')
result = self.h.handle_request(u'pause "0"')
@ -184,14 +182,14 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
def test_pause_on(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
self.h.handle_request(u'play "0"')
result = self.h.handle_request(u'pause "1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PAUSED, self.b.playback.state)
def test_pause_toggle(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'play "0"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
@ -203,37 +201,49 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
def test_play_without_pos(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
self.b.playback.state = self.b.playback.PAUSED
result = self.h.handle_request(u'play')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
def test_play_with_pos(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'play "0"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
def test_play_with_pos_without_quotes(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'play 0')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
def test_play_with_pos_out_of_bounds(self):
self.b.current_playlist.load([])
self.b.current_playlist.append([])
result = self.h.handle_request(u'play "0"')
self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index')
self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
def test_play_minus_one_plays_first_in_playlist(self):
track = Track()
self.b.current_playlist.load([track])
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.b.playback.current_track, None)
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
result = self.h.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assertEqual(self.b.playback.current_track, track)
self.assertEqual(self.b.playback.current_track.uri, 'a')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(self.b.playback.current_track, None)
self.b.playback.play()
self.b.playback.next()
self.b.playback.stop()
self.assertNotEqual(self.b.playback.current_track, None)
result = self.h.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assertEqual(self.b.playback.current_track.uri, 'b')
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
self.b.current_playlist.clear()
@ -243,18 +253,30 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEqual(self.b.playback.current_track, None)
def test_playid(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'playid "1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
def test_playid_minus_one_plays_first_in_playlist(self):
track = Track()
self.b.current_playlist.load([track])
def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.b.playback.current_track, None)
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assertEqual(self.b.playback.current_track, track)
self.assertEqual(self.b.playback.current_track.uri, 'a')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(self.b.playback.current_track, None)
self.b.playback.play()
self.b.playback.next()
self.b.playback.stop()
self.assertNotEqual(self.b.playback.current_track, None)
result = self.h.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
self.assertEqual(self.b.playback.current_track.uri, 'b')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
self.b.current_playlist.clear()
@ -264,7 +286,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEqual(self.b.playback.current_track, None)
def test_playid_which_does_not_exist(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
result = self.h.handle_request(u'playid "12345"')
self.assertEqual(result[0], u'ACK [50@0] {playid} No such song')
@ -273,12 +295,32 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_seek(self):
self.b.current_playlist.append([Track(length=40000)])
self.h.handle_request(u'seek "0"')
result = self.h.handle_request(u'seek "0" "30"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.assert_(u'OK' in result)
self.assert_(self.b.playback.time_position >= 30000)
def test_seek_with_songpos(self):
seek_track = Track(uri='2', length=40000)
self.b.current_playlist.append(
[Track(uri='1', length=40000), seek_track])
result = self.h.handle_request(u'seek "1" "30"')
self.assertEqual(self.b.playback.current_track, seek_track)
def test_seekid(self):
result = self.h.handle_request(u'seekid "0" "30"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.b.current_playlist.append([Track(length=40000)])
result = self.h.handle_request(u'seekid "1" "30"')
self.assert_(u'OK' in result)
self.assert_(self.b.playback.time_position >= 30000)
def test_seekid_with_cpid(self):
seek_track = Track(uri='2', length=40000)
self.b.current_playlist.append(
[Track(length=40000), seek_track])
result = self.h.handle_request(u'seekid "2" "30"')
self.assertEqual(self.b.playback.current_cpid, 2)
self.assertEqual(self.b.playback.current_track, seek_track)
def test_stop(self):
result = self.h.handle_request(u'stop')

View File

@ -6,8 +6,7 @@ from mopidy.mixers.dummy import DummyMixer
class ReflectionHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_commands_returns_list_of_all_commands(self):

View File

@ -6,8 +6,7 @@ from mopidy.mixers.dummy import DummyMixer
class RequestHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_register_same_pattern_twice_fails(self):

View File

@ -7,8 +7,7 @@ from mopidy.models import Track
class StatusHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_clearerror(self):
@ -17,7 +16,7 @@ class StatusHandlerTest(unittest.TestCase):
def test_currentsong(self):
track = Track()
self.b.current_playlist.load([track])
self.b.current_playlist.append([track])
self.b.playback.play()
result = self.h.handle_request(u'currentsong')
self.assert_(u'file: ' in result)
@ -156,21 +155,21 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(result['state'], 'pause')
def test_status_method_when_playlist_loaded_contains_song(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
self.assert_('song' in result)
self.assert_(int(result['song']) >= 0)
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
self.b.current_playlist.load([Track()])
self.b.current_playlist.append([Track()])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
self.assert_('songid' in result)
self.assertEqual(int(result['songid']), 1)
def test_status_method_when_playing_contains_time_with_no_length(self):
self.b.current_playlist.load([Track(length=None)])
self.b.current_playlist.append([Track(length=None)])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
self.assert_('time' in result)
@ -180,7 +179,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(position <= total)
def test_status_method_when_playing_contains_time_with_length(self):
self.b.current_playlist.load([Track(length=10000)])
self.b.current_playlist.append([Track(length=10000)])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
self.assert_('time' in result)
@ -197,7 +196,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['elapsed']), 59123)
def test_status_method_when_playing_contains_bitrate(self):
self.b.current_playlist.load([Track(bitrate=320)])
self.b.current_playlist.append([Track(bitrate=320)])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
self.assert_('bitrate' in result)

View File

@ -6,8 +6,7 @@ from mopidy.mixers.dummy import DummyMixer
class StickersHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_sticker_get(self):

View File

@ -6,12 +6,9 @@ from mopidy.frontends.mpd import frontend
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track, Playlist
from tests import SkipTest
class StoredPlaylistsHandlerTest(unittest.TestCase):
def setUp(self):
self.m = DummyMixer()
self.b = DummyBackend(mixer=self.m)
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
def test_listplaylist(self):
@ -50,12 +47,24 @@ class StoredPlaylistsHandlerTest(unittest.TestCase):
self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result)
self.assert_(u'OK' in result)
def test_load(self):
result = self.h.handle_request(u'load "name"')
def test_load_known_playlist_appends_to_current_playlist(self):
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.b.current_playlist.tracks), 2)
self.b.stored_playlists.playlists = [Playlist(name='A-list',
tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
result = self.h.handle_request(u'load "A-list"')
self.assert_(u'OK' in result)
self.assertEqual(len(self.b.current_playlist.tracks), 5)
self.assertEqual(self.b.current_playlist.tracks[0].uri, 'a')
self.assertEqual(self.b.current_playlist.tracks[1].uri, 'b')
self.assertEqual(self.b.current_playlist.tracks[2].uri, 'c')
self.assertEqual(self.b.current_playlist.tracks[3].uri, 'd')
self.assertEqual(self.b.current_playlist.tracks[4].uri, 'e')
def test_load_appends(self):
raise SkipTest
def test_load_unknown_playlist_acks(self):
result = self.h.handle_request(u'load "unknown playlist"')
self.assert_(u'ACK [50@0] {load} No such playlist' in result)
self.assertEqual(len(self.b.current_playlist.tracks), 0)
def test_playlistadd(self):
result = self.h.handle_request(

View File

@ -1,18 +1,16 @@
import unittest
from mopidy.mixers.dummy import DummyMixer
class BaseMixerTest(unittest.TestCase):
class BaseMixerTest(object):
MIN = 0
MAX = 100
ACTUAL_MIN = MIN
ACTUAL_MAX = MAX
INITIAL = None
mixer_class = None
def setUp(self):
self.mixer = DummyMixer()
assert self.mixer_class is not None, \
"mixer_class must be set in subclass"
self.mixer = self.mixer_class(None)
def test_initial_volume(self):
self.assertEqual(self.mixer.volume, self.INITIAL)

View File

@ -1,3 +1,5 @@
import unittest
from mopidy.mixers.denon import DenonMixer
from tests.mixers.base_test import BaseMixerTest
@ -22,14 +24,15 @@ class DenonMixerDeviceMock(object):
def open(self):
self._open = True
class DenonMixerTest(BaseMixerTest):
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
ACTUAL_MAX = 99
INITIAL = 1
mixer_class = DenonMixer
def setUp(self):
self.device = DenonMixerDeviceMock()
self.mixer = DenonMixer(device=self.device)
self.mixer = DenonMixer(None, device=self.device)
def test_reopen_device(self):
self.device._open = False

View File

@ -0,0 +1,17 @@
import unittest
from mopidy.mixers.dummy import DummyMixer
from tests.mixers.base_test import BaseMixerTest
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
mixer_class = DummyMixer
def test_set_volume_is_capped(self):
self.mixer.amplification_factor = 0.5
self.mixer.volume = 100
self.assertEquals(self.mixer._volume, 50)
def test_get_volume_does_not_show_that_the_volume_is_capped(self):
self.mixer.amplification_factor = 0.5
self.mixer._volume = 50
self.assertEquals(self.mixer.volume, 100)

View File

View File

@ -0,0 +1,58 @@
import multiprocessing
import unittest
from mopidy.outputs.gstreamer import GStreamerOutput
from mopidy.process import pickle_connection
from mopidy.utils.path import path_to_uri
from tests import data_folder, SkipTest
class GStreamerOutputTest(unittest.TestCase):
def setUp(self):
self.song_uri = path_to_uri(data_folder('song1.wav'))
self.output_queue = multiprocessing.Queue()
self.core_queue = multiprocessing.Queue()
self.output = GStreamerOutput(self.core_queue, self.output_queue)
def tearDown(self):
self.output.destroy()
def send_recv(self, message):
(my_end, other_end) = multiprocessing.Pipe()
message.update({'reply_to': pickle_connection(other_end)})
self.output_queue.put(message)
my_end.poll(None)
return my_end.recv()
def send(self, message):
self.output_queue.put(message)
@SkipTest
def test_play_uri_existing_file(self):
message = {'command': 'play_uri', 'uri': self.song_uri}
self.assertEqual(True, self.send_recv(message))
@SkipTest
def test_play_uri_non_existing_file(self):
message = {'command': 'play_uri', 'uri': self.song_uri + 'bogus'}
self.assertEqual(False, self.send_recv(message))
def test_default_get_volume_result(self):
message = {'command': 'get_volume'}
self.assertEqual(100, self.send_recv(message))
def test_set_volume(self):
self.send({'command': 'set_volume', 'volume': 50})
self.assertEqual(50, self.send_recv({'command': 'get_volume'}))
def test_set_volume_to_zero(self):
self.send({'command': 'set_volume', 'volume': 0})
self.assertEqual(0, self.send_recv({'command': 'get_volume'}))
def test_set_volume_to_one_hundred(self):
self.send({'command': 'set_volume', 'volume': 100})
self.assertEqual(100, self.send_recv({'command': 'get_volume'}))
@SkipTest
def test_set_state(self):
raise NotImplementedError

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

22
tests/utils/init_test.py Normal file
View File

@ -0,0 +1,22 @@
import unittest
from mopidy.utils import get_class
class GetClassTest(unittest.TestCase):
def test_loading_module_that_does_not_exist(self):
test = lambda: get_class('foo.bar.Baz')
self.assertRaises(ImportError, test)
def test_loading_class_that_does_not_exist(self):
test = lambda: get_class('unittest.FooBarBaz')
self.assertRaises(ImportError, test)
def test_import_error_message_contains_complete_class_path(self):
try:
get_class('foo.bar.Baz')
except ImportError as e:
self.assert_('foo.bar.Baz' in str(e))
def test_loading_existing_class(self):
cls = get_class('unittest.TestCase')
self.assertEqual(cls.__name__, 'TestCase')

71
tests/utils/path_test.py Normal file
View File

@ -0,0 +1,71 @@
# encoding: utf-8
import os
import shutil
import sys
import tempfile
import unittest
from mopidy.utils.path import get_or_create_folder, path_to_uri
from tests import SkipTest
class GetOrCreateFolderTest(unittest.TestCase):
def setUp(self):
self.parent = tempfile.mkdtemp()
def tearDown(self):
if os.path.isdir(self.parent):
shutil.rmtree(self.parent)
def test_creating_folder(self):
folder = os.path.join(self.parent, 'test')
self.assert_(not os.path.exists(folder))
self.assert_(not os.path.isdir(folder))
created = get_or_create_folder(folder)
self.assert_(os.path.exists(folder))
self.assert_(os.path.isdir(folder))
self.assertEqual(created, folder)
def test_creating_existing_folder(self):
created = get_or_create_folder(self.parent)
self.assert_(os.path.exists(self.parent))
self.assert_(os.path.isdir(self.parent))
self.assertEqual(created, self.parent)
def test_that_userfolder_is_expanded(self):
raise SkipTest # Not sure how to safely test this
class PathToFileURITest(unittest.TestCase):
def test_simple_path(self):
if sys.platform == 'win32':
result = path_to_uri(u'C:/WINDOWS/clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
else:
result = path_to_uri(u'/etc/fstab')
self.assertEqual(result, 'file:///etc/fstab')
def test_folder_and_path(self):
if sys.platform == 'win32':
result = path_to_uri(u'C:/WINDOWS/', u'clock.avi')
self.assertEqual(result, 'file:///C://WINDOWS/clock.avi')
else:
result = path_to_uri(u'/etc', u'fstab')
self.assertEqual(result, u'file:///etc/fstab')
def test_space_in_path(self):
if sys.platform == 'win32':
result = path_to_uri(u'C:/test this')
self.assertEqual(result, 'file:///C://test%20this')
else:
result = path_to_uri(u'/tmp/test this')
self.assertEqual(result, u'file:///tmp/test%20this')
def test_unicode_in_path(self):
if sys.platform == 'win32':
result = path_to_uri(u'C:/æøå')
self.assertEqual(result, 'file:///C://%C3%A6%C3%B8%C3%A5')
else:
result = path_to_uri(u'/tmp/æøå')
self.assertEqual(result, u'file:///tmp/%C3%A6%C3%B8%C3%A5')

View File

@ -0,0 +1,45 @@
import unittest
from mopidy.utils.settings import validate_settings
class ValidateSettingsTest(unittest.TestCase):
def setUp(self):
self.defaults = {
'MPD_SERVER_HOSTNAME': '::',
'MPD_SERVER_PORT': 6600,
}
def test_no_errors_yields_empty_dict(self):
result = validate_settings(self.defaults, {})
self.assertEqual(result, {})
def test_unknown_setting_returns_error(self):
result = validate_settings(self.defaults,
{'MPD_SERVER_HOSTNMAE': '127.0.0.1'})
self.assertEqual(result['MPD_SERVER_HOSTNMAE'],
u'Unknown setting. Is it misspelled?')
def test_not_renamed_setting_returns_error(self):
result = validate_settings(self.defaults,
{'SERVER_HOSTNAME': '127.0.0.1'})
self.assertEqual(result['SERVER_HOSTNAME'],
u'Deprecated setting. Use MPD_SERVER_HOSTNAME.')
def test_unneeded_settings_returns_error(self):
result = validate_settings(self.defaults,
{'SPOTIFY_LIB_APPKEY': '/tmp/foo'})
self.assertEqual(result['SPOTIFY_LIB_APPKEY'],
u'Deprecated setting. It may be removed.')
def test_deprecated_setting_value_returns_error(self):
result = validate_settings(self.defaults,
{'BACKENDS': ('mopidy.backends.despotify.DespotifyBackend',)})
self.assertEqual(result['BACKENDS'],
u'Deprecated setting value. ' +
'"mopidy.backends.despotify.DespotifyBackend" is no longer ' +
'available.')
def test_two_errors_are_both_reported(self):
result = validate_settings(self.defaults,
{'FOO': '', 'BAR': ''})
self.assertEquals(len(result), 2)