Release v0.6.0

This commit is contained in:
Stein Magnus Jodal 2011-10-09 00:06:19 +02:00
commit da79303e84
110 changed files with 5656 additions and 1956 deletions

View File

@ -8,3 +8,4 @@ TryExec=mopidy
Exec=mopidy
Terminal=true
Categories=AudioVideo;Audio;Player;ConsoleOnly;
StartupNotify=true

View File

@ -15,7 +15,6 @@ The backend
.. autoclass:: mopidy.backends.base.Backend
:members:
:undoc-members:
Playback controller
@ -26,7 +25,6 @@ seek.
.. autoclass:: mopidy.backends.base.PlaybackController
:members:
:undoc-members:
Mixer controller
@ -42,7 +40,6 @@ Manages everything related to the currently loaded playlist.
.. autoclass:: mopidy.backends.base.CurrentPlaylistController
:members:
:undoc-members:
Stored playlists controller
@ -52,7 +49,6 @@ Manages stored playlist.
.. autoclass:: mopidy.backends.base.StoredPlaylistsController
:members:
:undoc-members:
Library controller
@ -62,4 +58,3 @@ Manages the music library, e.g. searching for tracks to be added to a playlist.
.. autoclass:: mopidy.backends.base.LibraryController
:members:
:undoc-members:

View File

@ -14,7 +14,6 @@ Playback provider
.. autoclass:: mopidy.backends.base.BasePlaybackProvider
:members:
:undoc-members:
Stored playlists provider
@ -22,7 +21,6 @@ Stored playlists provider
.. autoclass:: mopidy.backends.base.BaseStoredPlaylistsProvider
:members:
:undoc-members:
Library provider
@ -30,7 +28,6 @@ Library provider
.. autoclass:: mopidy.backends.base.BaseLibraryProvider
:members:
:undoc-members:
Backend provider implementations

View File

@ -2,25 +2,30 @@
Frontend API
************
A frontend may do whatever it wants to, including creating threads, opening TCP
ports and exposing Mopidy for a type of clients.
Frontends got one main limitation: they are restricted to passing messages
through the ``core_queue`` for all communication with the rest of Mopidy. Thus,
the frontend API is very small and reveals little of what a frontend may do.
.. warning::
A stable frontend API is not available yet, as we've only implemented a
couple of frontend modules.
.. automodule:: mopidy.frontends.base
:synopsis: Base class for frontends
:members:
The following requirements applies to any frontend implementation:
- A frontend MAY do mostly whatever it wants to, including creating threads,
opening TCP ports and exposing Mopidy for a group of clients.
- A frontend MUST implement at least one `Pykka
<http://jodal.github.com/pykka/>`_ actor, called the "main actor" from here
on.
- It MAY use additional actors to implement whatever it does, and using actors
in frontend implementations is encouraged.
- The frontend is activated by including its main actor in the
:attr:`mopidy.settings.FRONTENDS` setting.
- The main actor MUST be able to start and stop the frontend when the main
actor is started and stopped.
- The frontend MAY require additional settings to be set for it to
work.
- Such settings MUST be documented.
- The main actor MUST stop itself if the defined settings are not adequate for
the frontend to work properly.
- Any actor which is part of the frontend MAY implement any listener interface
from :mod:`mopidy.listeners` to receive notification of the specified events.
Frontend implementations
========================
* :mod:`mopidy.frontends.lastfm`
* :mod:`mopidy.frontends.mpd`
* :mod:`mopidy.frontends.mpris`

7
docs/api/listeners.rst Normal file
View File

@ -0,0 +1,7 @@
************
Listener API
************
.. automodule:: mopidy.listeners
:synopsis: Listener API
:members:

View File

@ -30,7 +30,6 @@ methods as described below.
.. automodule:: mopidy.mixers.base
:synopsis: Mixer API
:members:
:undoc-members:
Mixer implementations

View File

@ -25,4 +25,3 @@ Data model API
.. automodule:: mopidy.models
:synopsis: Data model API
:members:
:undoc-members:

View File

@ -5,6 +5,78 @@ Changes
This change log is used to track all major changes to Mopidy.
v0.6.0 (2011-10-09)
===================
The development of Mopidy have been quite slow for the last couple of months,
but we do have some goodies to release which have been idling in the
develop branch since the warmer days of the summer. This release brings support
for the MPD ``idle`` command, which makes it possible for a client wait for
updates from the server instead of polling every second. Also, we've added
support for the MPRIS standard, so that Mopidy can be controlled over D-Bus
from e.g. the Ubuntu Sound Menu.
Please note that 0.6.0 requires some updated dependencies, as listed under
*Important changes* below.
**Important changes**
- Pykka 0.12.3 or greater is required.
- pyspotify 1.4 or greater is required.
- All config, data, and cache locations are now based on the XDG spec.
- This means that your settings file will need to be moved from
``~/.mopidy/settings.py`` to ``~/.config/mopidy/settings.py``.
- Your Spotify cache will now be stored in ``~/.cache/mopidy`` instead of
``~/.mopidy/spotify_cache``.
- The local backend's ``tag_cache`` should now be in
``~/.local/share/mopidy/tag_cache``, likewise your playlists will be in
``~/.local/share/mopidy/playlists``.
- The local client now tries to lookup where your music is via XDG, it will
fall-back to ``~/music`` or use whatever setting you set manually.
- The MPD command ``idle`` is now supported by Mopidy for the following
subsystems: player, playlist, options, and mixer. (Fixes: :issue:`32`)
- A new frontend :mod:`mopidy.frontends.mpris` have been added. It exposes
Mopidy through the `MPRIS interface <http://www.mpris.org/>`_ over D-Bus. In
practice, this makes it possible to control Mopidy through the `Ubuntu Sound
Menu <https://wiki.ubuntu.com/SoundMenu>`_.
**Changes**
- Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with
:attr:`mopidy.backends.base.Backend.uri_schemes`, which just takes the part
up to the colon of an URI, and not any prefix.
- Add Listener API, :mod:`mopidy.listeners`, to be implemented by actors
wanting to receive events from the backend. This is a formalization of the
ad hoc events the Last.fm scrobbler has already been using for some time.
- Replaced all of the MPD network code that was provided by asyncore with
custom stack. This change was made to facilitate support for the ``idle``
command, and to reduce the number of event loops being used.
- Fix metadata update in Shoutcast streaming. (Fixes: :issue:`122`)
- Unescape all incoming MPD requests. (Fixes: :issue:`113`)
- Increase the maximum number of results returned by Spotify searches from 32
to 100.
- Send Spotify search queries to pyspotify as unicode objects, as required by
pyspotify 1.4. (Fixes: :issue:`129`)
- Add setting :attr:`mopidy.settings.MPD_SERVER_MAX_CONNECTIONS`. (Fixes:
:issue:`134`)
- Remove `destroy()` methods from backend controller and provider APIs, as it
was not in use and actually not called by any code. Will reintroduce when
needed.
v0.5.0 (2011-06-15)
===================
@ -87,6 +159,18 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
- Found and worked around strange WMA metadata behaviour.
- Backend API:
- Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next`
and :meth:`mopidy.backends.base.playback.PlaybackController.previous` no
longer implies that playback should be started. The playback state--whether
playing, paused or stopped--will now be kept.
- The method
:meth:`mopidy.backends.base.playback.PlaybackController.change_track`
has been added. Like ``next()``, and ``prev()``, it changes the current
track without changing the playback state.
v0.4.1 (2011-05-06)
===================
@ -217,7 +301,7 @@ loading from Mopidy 0.3.0 is still present.
the debug log, to ease debugging of issues with attached debug logs.
v0.3.1 (2010-01-22)
v0.3.1 (2011-01-22)
===================
A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
@ -231,7 +315,7 @@ A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
installed if the installation is executed as the root user.
v0.3.0 (2010-01-22)
v0.3.0 (2011-01-22)
===================
Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large

View File

@ -20,9 +20,8 @@ A command line client. Version 0.14 had some issues with Mopidy (see
ncmpc
-----
A console client. Uses the ``idle`` command heavily, which Mopidy doesn't
support yet (see :issue:`32`). If you want a console client, use ncmpcpp
instead.
A console client. Works with Mopidy 0.6 and upwards. Uses the ``idle`` MPD
command, but in a resource inefficient way.
ncmpcpp
@ -48,15 +47,15 @@ from `Launchpad <https://launchpad.net/ubuntu/+source/ncmpcpp>`_.
Communication mode
^^^^^^^^^^^^^^^^^^
In newer versions of ncmpcpp, like 0.5.5 shipped with Ubuntu 11.04, ncmcpp
defaults to "notifications" mode for MPD communications, which Mopidy currently
does not support. To workaround this limitation in Mopidy, edit the ncmpcpp
configuration file at ``~/.ncmpcpp/config`` and add the following setting::
In newer versions of ncmpcpp, like ncmpcpp 0.5.5 shipped with Ubuntu 11.04,
ncmcpp defaults to "notifications" mode for MPD communications, which Mopidy
did not support before Mopidy 0.6. To workaround this limitation in earlier
versions of Mopidy, edit the ncmpcpp configuration file at
``~/.ncmpcpp/config`` and add the following setting::
mpd_communication_mode = "polling"
You can track the development of "notifications" mode support in Mopidy in
:issue:`32`.
If you use Mopidy 0.6 or newer, you don't need to change anything.
Graphical clients

View File

@ -25,7 +25,7 @@ Otherwise, make sure you got the required dependencies installed.
- Python >= 2.6, < 3
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12.3
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.

View File

@ -5,31 +5,8 @@
.. inheritance-diagram:: mopidy.frontends.mpd
.. automodule:: mopidy.frontends.mpd
:synopsis: MPD frontend
:synopsis: MPD server frontend
:members:
:undoc-members:
MPD server
==========
.. inheritance-diagram:: mopidy.frontends.mpd.server
.. automodule:: mopidy.frontends.mpd.server
:synopsis: MPD server
:members:
:undoc-members:
MPD session
===========
.. inheritance-diagram:: mopidy.frontends.mpd.session
.. automodule:: mopidy.frontends.mpd.session
:synopsis: MPD client session
:members:
:undoc-members:
MPD dispatcher
@ -40,7 +17,6 @@ MPD dispatcher
.. automodule:: mopidy.frontends.mpd.dispatcher
:synopsis: MPD request dispatcher
:members:
:undoc-members:
MPD protocol

View File

@ -0,0 +1,7 @@
***********************************************
:mod:`mopidy.frontends.mpris` -- MPRIS frontend
***********************************************
.. automodule:: mopidy.frontends.mpris
:synopsis: MPRIS frontend
:members:

View File

@ -92,7 +92,6 @@ To make a ``tag_cache`` of your local music available for Mopidy:
.. _use_mpd_on_a_network:
Connecting from other machines on the network
=============================================
@ -120,6 +119,33 @@ file::
LASTFM_PASSWORD = u'mysecret'
.. _install_desktop_file:
Controlling Mopidy through the Ubuntu Sound Menu
================================================
If you are running Ubuntu and installed Mopidy using the Debian package from
APT you should be able to control Mopidy through the `Ubuntu Sound Menu
<https://wiki.ubuntu.com/SoundMenu>`_ without any changes.
If you installed Mopidy in any other way and want to control Mopidy through the
Ubuntu Sound Menu, you must install the ``mopidy.desktop`` file which can be
found in the ``data/`` dir of the Mopidy source into the
``/usr/share/applications`` dir by hand::
cd /path/to/mopidy/source
sudo cp data/mopidy.desktop /usr/share/applications/
After you have installed the file, start Mopidy in any way, and Mopidy should
appear in the Ubuntu Sound Menu. When you quit Mopidy, it will still be listed
in the Ubuntu Sound Menu, and may be restarted by selecting it there.
The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend,
:mod:`mopidy.frontends.mpris`. The MPRIS frontend supports the minimum
requirements of the `MPRIS specification <http://www.mpris.org/>`_. The
``TrackList`` and the ``Playlists`` interfaces of the spec are not supported.
Streaming audio through a SHOUTcast/Icecast server
==================================================
@ -151,4 +177,3 @@ Available settings
.. automodule:: mopidy.settings
:synopsis: Available settings and their default values
:members:
:undoc-members:

View File

@ -3,9 +3,17 @@ import sys
if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
import glib
import os
from subprocess import PIPE, Popen
VERSION = (0, 5, 0)
VERSION = (0, 6, 0)
DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy')
CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy')
SETTINGS_PATH = os.path.join(glib.get_user_config_dir(), 'mopidy')
SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
def get_version():
try:

View File

@ -25,5 +25,5 @@ class Backend(object):
#: :class:`mopidy.backends.base.StoredPlaylistsController`.
stored_playlists = None
#: List of URI prefixes this backend can handle.
uri_handlers = []
#: List of URI schemes this backend can handle.
uri_schemes = []

View File

@ -2,6 +2,7 @@ from copy import copy
import logging
import random
from mopidy.listeners import BackendListener
from mopidy.models import CpTrack
logger = logging.getLogger('mopidy.backends.base')
@ -16,13 +17,10 @@ class CurrentPlaylistController(object):
def __init__(self, backend):
self.backend = backend
self.cp_id = 0
self._cp_tracks = []
self._version = 0
def destroy(self):
"""Cleanup after component."""
pass
@property
def cp_tracks(self):
"""
@ -53,8 +51,9 @@ class CurrentPlaylistController(object):
def version(self, version):
self._version = version
self.backend.playback.on_current_playlist_change()
self._trigger_playlist_changed()
def add(self, track, at_position=None):
def add(self, track, at_position=None, increase_version=True):
"""
Add the track to the end of, or at the given position in the current
playlist.
@ -68,12 +67,14 @@ class CurrentPlaylistController(object):
"""
assert at_position <= len(self._cp_tracks), \
u'at_position can not be greater than playlist length'
cp_track = CpTrack(self.version, track)
cp_track = CpTrack(self.cp_id, track)
if at_position is not None:
self._cp_tracks.insert(at_position, cp_track)
else:
self._cp_tracks.append(cp_track)
self.version += 1
if increase_version:
self.version += 1
self.cp_id += 1
return cp_track
def append(self, tracks):
@ -84,7 +85,10 @@ class CurrentPlaylistController(object):
:type tracks: list of :class:`mopidy.models.Track`
"""
for track in tracks:
self.add(track)
self.add(track, increase_version=False)
if tracks:
self.version += 1
def clear(self):
"""Clear the current playlist."""
@ -199,3 +203,7 @@ class CurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
def _trigger_playlist_changed(self):
logger.debug(u'Triggering playlist changed event')
BackendListener.send('playlist_changed')

View File

@ -16,10 +16,6 @@ class LibraryController(object):
self.backend = backend
self.provider = provider
def destroy(self):
"""Cleanup after component."""
self.provider.destroy()
def find_exact(self, **query):
"""
Search the library for tracks where ``field`` is ``values``.
@ -89,14 +85,6 @@ class BaseLibraryProvider(object):
def __init__(self, backend):
self.backend = backend
def destroy(self):
"""
Cleanup after component.
*MAY be implemented by subclasses.*
"""
pass
def find_exact(self, **query):
"""
See :meth:`mopidy.backends.base.LibraryController.find_exact`.

View File

@ -4,10 +4,21 @@ import time
from pykka.registry import ActorRegistry
from mopidy.frontends.base import BaseFrontend
from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.backends.base')
def option_wrapper(name, default):
def get_option(self):
return getattr(self, name, default)
def set_option(self, value):
if getattr(self, name, default) != value:
self._trigger_options_changed()
return setattr(self, name, value)
return property(get_option, set_option)
class PlaybackController(object):
"""
:param backend: the backend
@ -34,7 +45,7 @@ class PlaybackController(object):
#: Tracks are removed from the playlist when they have been played.
#: :class:`False`
#: Tracks are not removed from the playlist.
consume = False
consume = option_wrapper('_consume', False)
#: The currently playing or selected track.
#:
@ -46,21 +57,21 @@ class PlaybackController(object):
#: Tracks are selected at random from the playlist.
#: :class:`False`
#: Tracks are played in the order of the playlist.
random = False
random = option_wrapper('_random', False)
#: :class:`True`
#: The current playlist is played repeatedly. To repeat a single track,
#: select both :attr:`repeat` and :attr:`single`.
#: :class:`False`
#: The current playlist is played once.
repeat = False
repeat = option_wrapper('_repeat', False)
#: :class:`True`
#: Playback is stopped after current song, unless in :attr:`repeat`
#: mode.
#: :class:`False`
#: Playback continues after current song.
single = False
single = option_wrapper('_single', False)
def __init__(self, backend, provider):
self.backend = backend
@ -71,12 +82,6 @@ class PlaybackController(object):
self.play_time_accumulated = 0
self.play_time_started = None
def destroy(self):
"""
Cleanup after component.
"""
self.provider.destroy()
def _get_cpid(self, cp_track):
if cp_track is None:
return None
@ -276,6 +281,9 @@ class PlaybackController(object):
def state(self, new_state):
(old_state, self._state) = (self.state, new_state)
logger.debug(u'Changing state: %s -> %s', old_state, new_state)
self._trigger_playback_state_changed()
# FIXME play_time stuff assumes backend does not have a better way of
# handeling this stuff :/
if (old_state in (self.PLAYING, self.STOPPED)
@ -313,6 +321,26 @@ class PlaybackController(object):
def _current_wall_time(self):
return int(time.time() * 1000)
def change_track(self, cp_track, on_error_step=1):
"""
Change to the given track, keeping the current playback state.
:param cp_track: track to change to
:type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`)
or :class:`None`
:param on_error_step: direction to step at play error, 1 for next
track (default), -1 for previous track
:type on_error_step: int, -1 or 1
"""
old_state = self.state
self.stop()
self.current_cp_track = cp_track
if old_state == self.PLAYING:
self.play(on_error_step=on_error_step)
elif old_state == self.PAUSED:
self.pause()
def on_end_of_track(self):
"""
Tell the playback controller that end of track is reached.
@ -326,7 +354,7 @@ class PlaybackController(object):
original_cp_track = self.current_cp_track
if self.cp_track_at_eot:
self._trigger_stopped_playing_event()
self._trigger_track_playback_ended()
self.play(self.cp_track_at_eot)
else:
self.stop(clear_current_track=True)
@ -349,20 +377,23 @@ class PlaybackController(object):
self.stop(clear_current_track=True)
def next(self):
"""Play the next track."""
if self.state == self.STOPPED:
return
"""
Change to the next track.
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
if self.cp_track_at_next:
self._trigger_stopped_playing_event()
self.play(self.cp_track_at_next)
self._trigger_track_playback_ended()
self.change_track(self.cp_track_at_next)
else:
self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
if self.state == self.PLAYING and self.provider.pause():
if self.provider.pause():
self.state = self.PAUSED
self._trigger_track_playback_paused()
def play(self, cp_track=None, on_error_step=1):
"""
@ -379,15 +410,17 @@ class PlaybackController(object):
if cp_track is not None:
assert cp_track in self.backend.current_playlist.cp_tracks
if cp_track is None and self.current_cp_track is None:
cp_track = self.cp_track_at_next
if cp_track is None and self.state == self.PAUSED:
self.resume()
elif cp_track is None:
if self.state == self.PAUSED:
return self.resume()
elif self.current_cp_track is not None:
cp_track = self.current_cp_track
elif self.current_cp_track is None and on_error_step == 1:
cp_track = self.cp_track_at_next
elif self.current_cp_track is None and on_error_step == -1:
cp_track = self.cp_track_at_previous
if cp_track is not None:
self.state = self.STOPPED
self.current_cp_track = cp_track
self.state = self.PLAYING
if not self.provider.play(cp_track.track):
@ -402,21 +435,23 @@ class PlaybackController(object):
if self.random and self.current_cp_track in self._shuffled:
self._shuffled.remove(self.current_cp_track)
self._trigger_started_playing_event()
self._trigger_track_playback_started()
def previous(self):
"""Play the previous track."""
if self.cp_track_at_previous is None:
return
if self.state == self.STOPPED:
return
self._trigger_stopped_playing_event()
self.play(self.cp_track_at_previous, on_error_step=-1)
"""
Change to the previous track.
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
self._trigger_track_playback_ended()
self.change_track(self.cp_track_at_previous, on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""
if self.state == self.PAUSED and self.provider.resume():
self.state = self.PLAYING
self._trigger_track_playback_resumed()
def seek(self, time_position):
"""
@ -443,7 +478,10 @@ class PlaybackController(object):
self.play_time_started = self._current_wall_time
self.play_time_accumulated = time_position
return self.provider.seek(time_position)
success = self.provider.seek(time_position)
if success:
self._trigger_seeked()
return success
def stop(self, clear_current_track=False):
"""
@ -454,45 +492,54 @@ class PlaybackController(object):
:type clear_current_track: boolean
"""
if self.state != self.STOPPED:
self._trigger_stopped_playing_event()
if self.provider.stop():
self._trigger_track_playback_ended()
self.state = self.STOPPED
if clear_current_track:
self.current_cp_track = None
def _trigger_started_playing_event(self):
"""
Notifies frontends that a track has started playing.
For internal use only. Should be called by the backend directly after a
track has started playing.
"""
def _trigger_track_playback_paused(self):
logger.debug(u'Triggering track playback paused event')
if self.current_track is None:
return
frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
for frontend_ref in frontend_refs:
frontend_ref.send_one_way({
'command': 'started_playing',
'track': self.current_track,
})
BackendListener.send('track_playback_paused',
track=self.current_track,
time_position=self.time_position)
def _trigger_stopped_playing_event(self):
"""
Notifies frontends that a track has stopped playing.
For internal use only. Should be called by the backend before a track
is stopped playing, e.g. at the next, previous, and stop actions and at
end-of-track.
"""
def _trigger_track_playback_resumed(self):
logger.debug(u'Triggering track playback resumed event')
if self.current_track is None:
return
frontend_refs = ActorRegistry.get_by_class(BaseFrontend)
for frontend_ref in frontend_refs:
frontend_ref.send_one_way({
'command': 'stopped_playing',
'track': self.current_track,
'stop_position': self.time_position,
})
BackendListener.send('track_playback_resumed',
track=self.current_track,
time_position=self.time_position)
def _trigger_track_playback_started(self):
logger.debug(u'Triggering track playback started event')
if self.current_track is None:
return
BackendListener.send('track_playback_started',
track=self.current_track)
def _trigger_track_playback_ended(self):
logger.debug(u'Triggering track playback ended event')
if self.current_track is None:
return
BackendListener.send('track_playback_ended',
track=self.current_track,
time_position=self.time_position)
def _trigger_playback_state_changed(self):
logger.debug(u'Triggering playback state change event')
BackendListener.send('playback_state_changed')
def _trigger_options_changed(self):
logger.debug(u'Triggering options changed event')
BackendListener.send('options_changed')
def _trigger_seeked(self):
logger.debug(u'Triggering seeked event')
BackendListener.send('seeked')
class BasePlaybackProvider(object):
@ -506,14 +553,6 @@ class BasePlaybackProvider(object):
def __init__(self, backend):
self.backend = backend
def destroy(self):
"""
Cleanup after component.
*MAY be implemented by subclasses.*
"""
pass
def pause(self):
"""
Pause playback.

View File

@ -17,10 +17,6 @@ class StoredPlaylistsController(object):
self.backend = backend
self.provider = provider
def destroy(self):
"""Cleanup after component."""
self.provider.destroy()
@property
def playlists(self):
"""
@ -133,14 +129,6 @@ class BaseStoredPlaylistsProvider(object):
self.backend = backend
self._playlists = []
def destroy(self):
"""
Cleanup after component.
*MAY be implemented by subclass.*
"""
pass
@property
def playlists(self):
"""
@ -201,4 +189,3 @@ class BaseStoredPlaylistsProvider(object):
*MUST be implemented by subclass.*
"""
raise NotImplementedError

View File

@ -32,7 +32,7 @@ class DummyBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
self.uri_handlers = [u'dummy:']
self.uri_schemes = [u'dummy']
class DummyLibraryProvider(BaseLibraryProvider):

View File

@ -1,4 +1,5 @@
import glob
import glib
import logging
import os
import shutil
@ -6,7 +7,7 @@ import shutil
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy import settings
from mopidy import settings, DATA_PATH
from mopidy.backends.base import (Backend, CurrentPlaylistController,
LibraryController, BaseLibraryProvider, PlaybackController,
BasePlaybackProvider, StoredPlaylistsController,
@ -18,6 +19,14 @@ from .translator import parse_m3u, parse_mpd_tag_cache
logger = logging.getLogger(u'mopidy.backends.local')
DEFAULT_PLAYLIST_PATH = os.path.join(DATA_PATH, 'playlists')
DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_PATH, 'tag_cache')
DEFAULT_MUSIC_PATH = glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC)
if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'):
DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music')
class LocalBackend(ThreadingActor, Backend):
"""
A backend for playing music from a local music archive.
@ -52,13 +61,14 @@ class LocalBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
self.uri_handlers = [u'file://']
self.uri_schemes = [u'file']
self.gstreamer = None
def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
assert len(gstreamer_refs) == 1, \
'Expected exactly one running GStreamer.'
self.gstreamer = gstreamer_refs[0].proxy()
@ -96,7 +106,7 @@ class LocalPlaybackProvider(BasePlaybackProvider):
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
def __init__(self, *args, **kwargs):
super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs)
self._folder = settings.LOCAL_PLAYLIST_PATH
self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH
self.refresh()
def lookup(self, uri):
@ -173,8 +183,8 @@ class LocalLibraryProvider(BaseLibraryProvider):
self.refresh()
def refresh(self, uri=None):
tag_cache = settings.LOCAL_TAG_CACHE_FILE
music_folder = settings.LOCAL_MUSIC_PATH
tag_cache = settings.LOCAL_TAG_CACHE_FILE or DEFAULT_TAG_CACHE_FILE
music_folder = settings.LOCAL_MUSIC_PATH or DEFAULT_MUSIC_PATH
tracks = parse_mpd_tag_cache(tag_cache, music_folder)

View File

@ -67,7 +67,7 @@ class SpotifyBackend(ThreadingActor, Backend):
self.stored_playlists = StoredPlaylistsController(backend=self,
provider=stored_playlists_provider)
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
self.uri_schemes = [u'spotify']
self.gstreamer = None
self.spotify = None
@ -78,12 +78,16 @@ class SpotifyBackend(ThreadingActor, Backend):
def on_start(self):
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
assert len(gstreamer_refs) == 1, \
'Expected exactly one running GStreamer.'
self.gstreamer = gstreamer_refs[0].proxy()
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
self.spotify = self._connect()
def on_stop(self):
self.spotify.logout()
def _connect(self):
from .session_manager import SpotifySessionManager

View File

@ -55,7 +55,7 @@ class SpotifyLibraryProvider(BaseLibraryProvider):
spotify_query = u' '.join(spotify_query)
logger.debug(u'Spotify search query: %s' % spotify_query)
queue = Queue.Queue()
self.backend.spotify.search(spotify_query.encode(ENCODING), queue)
self.backend.spotify.search(spotify_query, queue)
try:
return queue.get(timeout=3) # XXX What is an reasonable timeout?
except Queue.Empty:

View File

@ -1,3 +1,4 @@
import glib
import logging
import os
import threading
@ -6,7 +7,7 @@ from spotify.manager import SpotifySessionManager as PyspotifySessionManager
from pykka.registry import ActorRegistry
from mopidy import get_version, settings
from mopidy import get_version, settings, CACHE_PATH
from mopidy.backends.base import Backend
from mopidy.backends.spotify import BITRATES
from mopidy.backends.spotify.container_manager import SpotifyContainerManager
@ -21,9 +22,10 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager')
# pylint: disable = R0901
# SpotifySessionManager: Too many ancestors (9/7)
class SpotifySessionManager(BaseThread, PyspotifySessionManager):
cache_location = settings.SPOTIFY_CACHE_PATH
settings_location = settings.SPOTIFY_CACHE_PATH
cache_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH
settings_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
user_agent = 'Mopidy %s' % get_version()
@ -118,6 +120,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
'channels': channels,
}
self.gstreamer.emit_data(capabilites, bytes(frames))
return num_frames
def play_token_lost(self, session):
"""Callback used by pyspotify"""
@ -148,9 +151,17 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
"""Search method used by Mopidy backend"""
def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
# TODO Consider launching a second search if results.total_tracks()
# is larger than len(results.tracks())
playlist = Playlist(tracks=[
SpotifyTranslator.to_mopidy_track(t)
for t in results.tracks()])
queue.put(playlist)
self.connected.wait()
self.session.search(query, callback)
self.session.search(query, callback, track_count=100,
album_count=0, artist_count=0)
def logout(self):
"""Log out from spotify"""
logger.debug(u'Logging out from spotify')
self.session.logout()

View File

@ -4,7 +4,7 @@ import logging
from spotify import Link, SpotifyError
from mopidy import settings
from mopidy.backends.spotify import ENCODING, BITRATES
from mopidy.backends.spotify import ENCODING
from mopidy.models import Artist, Album, Track, Playlist
logger = logging.getLogger('mopidy.backends.spotify.translator')
@ -44,7 +44,7 @@ class SpotifyTranslator(object):
track_no=spotify_track.index(),
date=date,
length=spotify_track.duration(),
bitrate=BITRATES[settings.SPOTIFY_BITRATE],
bitrate=settings.SPOTIFY_BITRATE,
)
@classmethod

View File

@ -1,8 +1,11 @@
import logging
import optparse
import os
import signal
import sys
import time
import gobject
gobject.threads_init()
# Extract any non-GStreamer arguments, and leave the GStreamer arguments for
# processing by GStreamer. This needs to be done before GStreamer is imported,
@ -18,30 +21,30 @@ sys.argv[1:] = gstreamer_args
from pykka.registry import ActorRegistry
from mopidy import (get_version, settings, OptionalDependencyError,
SettingsError)
SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
from mopidy.gstreamer import GStreamer
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
from mopidy.utils.process import (GObjectEventThread, exit_handler,
stop_all_actors)
from mopidy.utils.process import (exit_handler, stop_remaining_actors,
stop_actors_by_class)
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
def main():
signal.signal(signal.SIGTERM, exit_handler)
loop = gobject.MainLoop()
try:
options = parse_options()
setup_logging(options.verbosity_level, options.save_debug_log)
check_old_folders()
setup_settings(options.interactive)
setup_gobject_loop()
setup_gstreamer()
setup_mixer()
setup_backend()
setup_frontends()
while True:
time.sleep(1)
loop.run()
except SettingsError as e:
logger.error(e.message)
except KeyboardInterrupt:
@ -49,7 +52,12 @@ def main():
except Exception as e:
logger.exception(e)
finally:
stop_all_actors()
loop.quit()
stop_frontends()
stop_backend()
stop_mixer()
stop_gstreamer()
stop_remaining_actors()
def parse_options():
parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
@ -58,12 +66,12 @@ def parse_options():
help='show GStreamer help options')
parser.add_option('-i', '--interactive',
action='store_true', dest='interactive',
help='ask interactively for required settings which is missing')
help='ask interactively for required settings which are missing')
parser.add_option('-q', '--quiet',
action='store_const', const=0, dest='verbosity_level',
help='less output (warning level)')
parser.add_option('-v', '--verbose',
action='store_const', const=2, dest='verbosity_level',
action='count', default=1, dest='verbosity_level',
help='more output (debug level)')
parser.add_option('--save-debug-log',
action='store_true', dest='save_debug_log',
@ -73,30 +81,54 @@ def parse_options():
help='list current settings')
return parser.parse_args(args=mopidy_args)[0]
def check_old_folders():
old_settings_folder = os.path.expanduser(u'~/.mopidy')
if not os.path.isdir(old_settings_folder):
return
logger.warning(u'Old settings folder found at %s, settings.py should be '
'moved to %s, any cache data should be deleted. See release notes '
'for further instructions.', old_settings_folder, SETTINGS_PATH)
def setup_settings(interactive):
get_or_create_folder('~/.mopidy/')
get_or_create_file('~/.mopidy/settings.py')
get_or_create_folder(SETTINGS_PATH)
get_or_create_folder(DATA_PATH)
get_or_create_file(SETTINGS_FILE)
try:
settings.validate(interactive)
except SettingsError, e:
logger.error(e.message)
sys.exit(1)
def setup_gobject_loop():
GObjectEventThread().start()
def setup_gstreamer():
GStreamer.start()
def stop_gstreamer():
stop_actors_by_class(GStreamer)
def setup_mixer():
get_class(settings.MIXER).start()
def stop_mixer():
stop_actors_by_class(get_class(settings.MIXER))
def setup_backend():
get_class(settings.BACKENDS[0]).start()
def stop_backend():
stop_actors_by_class(get_class(settings.BACKENDS[0]))
def setup_frontends():
for frontend_class_name in settings.FRONTENDS:
try:
get_class(frontend_class_name).start()
except OptionalDependencyError as e:
logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
def stop_frontends():
for frontend_class_name in settings.FRONTENDS:
try:
stop_actors_by_class(get_class(frontend_class_name))
except OptionalDependencyError:
pass

View File

@ -1,5 +0,0 @@
class BaseFrontend(object):
"""
Base class for frontends.
"""
pass

View File

@ -10,14 +10,14 @@ except ImportError as import_error:
from pykka.actor import ThreadingActor
from mopidy import settings, SettingsError
from mopidy.frontends.base import BaseFrontend
from mopidy.listeners import BackendListener
logger = logging.getLogger('mopidy.frontends.lastfm')
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
class LastfmFrontend(ThreadingActor, BaseFrontend):
class LastfmFrontend(ThreadingActor, BackendListener):
"""
Frontend which scrobbles the music you play to your `Last.fm
<http://www.last.fm>`_ profile.
@ -57,15 +57,7 @@ class LastfmFrontend(ThreadingActor, BaseFrontend):
logger.error(u'Error during Last.fm setup: %s', e)
self.stop()
def on_receive(self, message):
if message.get('command') == 'started_playing':
self.started_playing(message['track'])
elif message.get('command') == 'stopped_playing':
self.stopped_playing(message['track'], message['stop_position'])
else:
pass # Ignore any other messages
def started_playing(self, track):
def track_playback_started(self, track):
artists = ', '.join([a.name for a in track.artists])
duration = track.length and track.length // 1000 or 0
self.last_start_time = int(time.time())
@ -82,14 +74,14 @@ class LastfmFrontend(ThreadingActor, BaseFrontend):
pylast.MalformedResponseError, pylast.WSError) as e:
logger.warning(u'Error submitting playing track to Last.fm: %s', e)
def stopped_playing(self, track, stop_position):
def track_playback_ended(self, track, time_position):
artists = ', '.join([a.name for a in track.artists])
duration = track.length and track.length // 1000 or 0
stop_position = stop_position // 1000
time_position = time_position // 1000
if duration < 30:
logger.debug(u'Track too short to scrobble. (30s)')
return
if stop_position < duration // 2 and stop_position < 240:
if time_position < duration // 2 and time_position < 240:
logger.debug(
u'Track not played long enough to scrobble. (50% or 240s)')
return

View File

@ -1,15 +1,15 @@
import asyncore
import logging
import sys
from pykka.actor import ThreadingActor
from pykka import registry, actor
from mopidy.frontends.base import BaseFrontend
from mopidy.frontends.mpd.server import MpdServer
from mopidy.utils.process import BaseThread
from mopidy import listeners, settings
from mopidy.frontends.mpd import dispatcher, protocol
from mopidy.utils import network, process, log
logger = logging.getLogger('mopidy.frontends.mpd')
class MpdFrontend(ThreadingActor, BaseFrontend):
class MpdFrontend(actor.ThreadingActor, listeners.BackendListener):
"""
The MPD frontend.
@ -25,23 +25,85 @@ class MpdFrontend(ThreadingActor, BaseFrontend):
"""
def __init__(self):
self._thread = None
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
port = settings.MPD_SERVER_PORT
try:
network.Server(hostname, port, protocol=MpdSession,
max_connections=settings.MPD_SERVER_MAX_CONNECTIONS)
except IOError, e:
logger.error(u'MPD server startup failed: %s', e)
sys.exit(1)
logger.info(u'MPD server running at [%s]:%s', hostname, port)
def on_stop(self):
process.stop_actors_by_class(MpdSession)
def send_idle(self, subsystem):
# FIXME this should be updated once pykka supports non-blocking calls
# on proxies or some similar solution
registry.ActorRegistry.broadcast({
'command': 'pykka_call',
'attr_path': ('on_idle',),
'args': [subsystem],
'kwargs': {},
}, target_class=MpdSession)
def playback_state_changed(self):
self.send_idle('player')
def playlist_changed(self):
self.send_idle('playlist')
def options_changed(self):
self.send_idle('options')
def volume_changed(self):
self.send_idle('mixer')
class MpdSession(network.LineProtocol):
"""
The MPD client session. Keeps track of a single client session. Any
requests from the client is passed on to the MPD request dispatcher.
"""
terminator = protocol.LINE_TERMINATOR
encoding = protocol.ENCODING
delimeter = r'\r?\n'
def __init__(self, connection):
super(MpdSession, self).__init__(connection)
self.dispatcher = dispatcher.MpdDispatcher(self)
def on_start(self):
self._thread = MpdThread()
self._thread.start()
logger.info(u'New MPD connection from [%s]:%s', self.host, self.port)
self.send_lines([u'OK MPD %s' % protocol.VERSION])
def on_receive(self, message):
pass # Ignore any messages
def on_line_received(self, line):
logger.debug(u'Request from [%s]:%s to %s: %s', self.host, self.port,
self.actor_urn, line)
response = self.dispatcher.handle_request(line)
if not response:
return
class MpdThread(BaseThread):
def __init__(self):
super(MpdThread, self).__init__()
self.name = u'MpdThread'
logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port,
self.actor_urn, log.indent(self.terminator.join(response)))
def run_inside_try(self):
logger.debug(u'Starting MPD server thread')
server = MpdServer()
server.start()
asyncore.loop()
self.send_lines(response)
def on_idle(self, subsystem):
self.dispatcher.handle_idle(subsystem)
def decode(self, line):
try:
return super(MpdSession, self).decode(line.decode('string_escape'))
except ValueError:
logger.warning(u'Stopping actor due to unescaping error, data '
'supplied by client was not valid.')
self.stop()
def close(self):
self.stop()

View File

@ -27,6 +27,8 @@ class MpdDispatcher(object):
back to the MPD session.
"""
_noidle = re.compile(r'^noidle$')
def __init__(self, session=None):
self.authenticated = False
self.command_list = False
@ -42,11 +44,28 @@ class MpdDispatcher(object):
self._catch_mpd_ack_errors_filter,
self._authenticate_filter,
self._command_list_filter,
self._idle_filter,
self._add_ok_filter,
self._call_handler_filter,
]
return self._call_next_filter(request, response, filter_chain)
def handle_idle(self, subsystem):
self.context.events.add(subsystem)
subsystems = self.context.subscriptions.intersection(
self.context.events)
if not subsystems:
return
response = []
for subsystem in subsystems:
response.append(u'changed: %s' % subsystem)
response.append(u'OK')
self.context.subscriptions = set()
self.context.events = set()
self.context.session.send_lines(response)
def _call_next_filter(self, request, response, filter_chain):
if filter_chain:
next_filter = filter_chain.pop(0)
@ -71,7 +90,7 @@ class MpdDispatcher(object):
def _authenticate_filter(self, request, response, filter_chain):
if self.authenticated:
return self._call_next_filter(request, response, filter_chain)
elif settings.MPD_SERVER_PASSWORD is None:
elif settings.MPD_SERVER_PASSWORD is None:
self.authenticated = True
return self._call_next_filter(request, response, filter_chain)
else:
@ -108,6 +127,29 @@ class MpdDispatcher(object):
and request != u'command_list_end')
### Filter: idle
def _idle_filter(self, request, response, filter_chain):
if self._is_currently_idle() and not self._noidle.match(request):
logger.debug(u'Client sent us %s, only %s is allowed while in '
'the idle state', repr(request), repr(u'noidle'))
self.context.session.close()
return []
if not self._is_currently_idle() and self._noidle.match(request):
return [] # noidle was called before idle
response = self._call_next_filter(request, response, filter_chain)
if self._is_currently_idle():
return []
else:
return response
def _is_currently_idle(self):
return bool(self.context.subscriptions)
### Filter: add OK
def _add_ok_filter(self, request, response, filter_chain):
@ -128,7 +170,7 @@ class MpdDispatcher(object):
return self._call_next_filter(request, response, filter_chain)
except ActorDeadError as e:
logger.warning(u'Tried to communicate with dead actor.')
raise exceptions.MpdSystemError(e.message)
raise exceptions.MpdSystemError(e)
def _call_handler(self, request):
(handler, kwargs) = self._find_handler(request)
@ -178,12 +220,20 @@ class MpdContext(object):
#: The current :class:`MpdDispatcher`.
dispatcher = None
#: The current :class:`mopidy.frontends.mpd.session.MpdSession`.
#: The current :class:`mopidy.frontends.mpd.MpdSession`.
session = None
#: The active subsystems that have pending events.
events = None
#: The subsytems that we want to be notified about in idle mode.
subscriptions = None
def __init__(self, dispatcher, session=None):
self.dispatcher = dispatcher
self.session = session
self.events = set()
self.subscriptions = set()
self._backend = None
self._mixer = None
@ -192,11 +242,10 @@ class MpdContext(object):
"""
The backend. An instance of :class:`mopidy.backends.base.Backend`.
"""
if self._backend is not None:
return self._backend
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
self._backend = backend_refs[0].proxy()
if self._backend is None:
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
self._backend = backend_refs[0].proxy()
return self._backend
@property
@ -204,9 +253,8 @@ class MpdContext(object):
"""
The mixer. An instance of :class:`mopidy.mixers.base.BaseMixer`.
"""
if self._mixer is not None:
return self._mixer
mixer_refs = ActorRegistry.get_by_class(BaseMixer)
assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
self._mixer = mixer_refs[0].proxy()
if self._mixer is None:
mixer_refs = ActorRegistry.get_by_class(BaseMixer)
assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
self._mixer = mixer_refs[0].proxy()
return self._mixer

View File

@ -19,8 +19,8 @@ def add(context, uri):
"""
if not uri:
return
for handler_prefix in context.backend.uri_handlers.get():
if uri.startswith(handler_prefix):
for uri_scheme in context.backend.uri_schemes.get():
if uri.startswith(uri_scheme):
track = context.backend.library.lookup(uri).get()
if track is not None:
context.backend.current_playlist.add(track)

View File

@ -1,6 +1,6 @@
from mopidy.frontends.mpd.protocol import handle_request
@handle_request(r'^$')
@handle_request(r'^[ ]*$')
def empty(context):
"""The original MPD server returns ``OK`` on an empty request."""
pass

View File

@ -11,28 +11,16 @@ def commands(context):
Shows which commands the current user has access to.
"""
if context.dispatcher.authenticated:
command_names = [command.name for command in mpd_commands]
command_names = set([command.name for command in mpd_commands])
else:
command_names = [command.name for command in mpd_commands
if not command.auth_required]
command_names = set([command.name for command in mpd_commands
if not command.auth_required])
# No permission to use
if 'kill' in command_names:
command_names.remove('kill')
# Not shown by MPD in its command list
if 'command_list_begin' in command_names:
command_names.remove('command_list_begin')
if 'command_list_ok_begin' in command_names:
command_names.remove('command_list_ok_begin')
if 'command_list_end' in command_names:
command_names.remove('command_list_end')
if 'idle' in command_names:
command_names.remove('idle')
if 'noidle' in command_names:
command_names.remove('noidle')
if 'sticker' in command_names:
command_names.remove('sticker')
# No one is permited to use kill, rest of commands are not listed by MPD,
# so we shouldn't either.
command_names = command_names - set(['kill', 'command_list_begin',
'command_list_ok_begin', 'command_list_ok_begin', 'command_list_end',
'idle', 'noidle', 'sticker'])
return [('command', command_name) for command_name in sorted(command_names)]
@ -95,4 +83,5 @@ def urlhandlers(context):
Gets a list of available URL handlers.
"""
return [(u'handler', uri) for uri in context.backend.uri_handlers.get()]
return [(u'handler', uri_scheme)
for uri_scheme in context.backend.uri_schemes.get()]

View File

@ -4,6 +4,10 @@ from mopidy.backends.base import PlaybackController
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
#: Subsystems that can be registered with idle command.
SUBSYSTEMS = ['database', 'mixer', 'options', 'output',
'player', 'playlist', 'stored_playlist', 'update', ]
@handle_request(r'^clearerror$')
def clearerror(context):
"""
@ -67,12 +71,36 @@ def idle(context, subsystems=None):
notifications when something changed in one of the specified
subsystems.
"""
pass # TODO
if subsystems:
subsystems = subsystems.split()
else:
subsystems = SUBSYSTEMS
for subsystem in subsystems:
context.subscriptions.add(subsystem)
active = context.subscriptions.intersection(context.events)
if not active:
context.session.prevent_timeout = True
return
response = []
context.events = set()
context.subscriptions = set()
for subsystem in active:
response.append(u'changed: %s' % subsystem)
return response
@handle_request(r'^noidle$')
def noidle(context):
"""See :meth:`_status_idle`."""
pass # TODO
if not context.subscriptions:
return
context.subscriptions = set()
context.events = set()
context.session.prevent_timeout = False
@handle_request(r'^stats$')
def stats(context):
@ -125,12 +153,17 @@ def status(context):
- ``nextsongid``: playlist songid of the next song to be played
- ``time``: total time elapsed (of current playing/paused song)
- ``elapsed``: Total time elapsed within the current song, but with
higher resolution.
higher resolution.
- ``bitrate``: instantaneous bitrate in kbps
- ``xfade``: crossfade in seconds
- ``audio``: sampleRate``:bits``:channels
- ``updatings_db``: job id
- ``error``: if there is an error, returns message here
*Clarifications based on experience implementing*
- ``volume``: can also be -1 if no output is set.
- ``elapsed``: Higher resolution means time in seconds with three
decimal places for millisecond precision.
"""
futures = {
'current_playlist.tracks': context.backend.current_playlist.tracks,
@ -214,11 +247,11 @@ def _status_state(futures):
return u'pause'
def _status_time(futures):
return u'%s:%s' % (_status_time_elapsed(futures) // 1000,
return u'%d:%d' % (futures['playback.time_position'].get() // 1000,
_status_time_total(futures) // 1000)
def _status_time_elapsed(futures):
return futures['playback.time_position'].get()
return u'%.3f' % (futures['playback.time_position'].get() / 1000.0)
def _status_time_total(futures):
current_cp_track = futures['playback.current_cp_track'].get()

View File

@ -1,38 +0,0 @@
import asyncore
import logging
import sys
from mopidy import settings
from mopidy.utils import network
from .session import MpdSession
logger = logging.getLogger('mopidy.frontends.mpd.server')
class MpdServer(asyncore.dispatcher):
"""
The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession`
for each client connection.
"""
def start(self):
"""Start MPD server."""
try:
self.set_socket(network.create_socket())
self.set_reuse_addr()
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
port = settings.MPD_SERVER_PORT
logger.debug(u'MPD server is binding to [%s]:%s', hostname, port)
self.bind((hostname, port))
self.listen(1)
logger.info(u'MPD server running at [%s]:%s', hostname, port)
except IOError, e:
logger.error(u'MPD server startup failed: %s' %
str(e).decode('utf-8'))
sys.exit(1)
def handle_accept(self):
"""Called by asyncore when a new client connects."""
(client_socket, client_socket_address) = self.accept()
logger.info(u'MPD client connection from [%s]:%s',
client_socket_address[0], client_socket_address[1])
MpdSession(self, client_socket, client_socket_address)

View File

@ -1,58 +0,0 @@
import asynchat
import logging
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
from mopidy.utils.log import indent
logger = logging.getLogger('mopidy.frontends.mpd.session')
class MpdSession(asynchat.async_chat):
"""
The MPD client session. Keeps track of a single client session. Any
requests from the client is passed on to the MPD request dispatcher.
"""
def __init__(self, server, client_socket, client_socket_address):
asynchat.async_chat.__init__(self, sock=client_socket)
self.server = server
self.client_address = client_socket_address[0]
self.client_port = client_socket_address[1]
self.input_buffer = []
self.authenticated = False
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
self.dispatcher = MpdDispatcher(session=self)
self.send_response([u'OK MPD %s' % VERSION])
def collect_incoming_data(self, data):
"""Called by asynchat when new data arrives."""
self.input_buffer.append(data)
def found_terminator(self):
"""Called by asynchat when a terminator is found in incoming data."""
data = ''.join(self.input_buffer).strip()
self.input_buffer = []
try:
self.send_response(self.handle_request(data))
except UnicodeDecodeError as e:
logger.warning(u'Received invalid data: %s', e)
def handle_request(self, request):
"""Handle the request using the MPD command handlers."""
request = request.decode(ENCODING)
logger.debug(u'Request from [%s]:%s: %s', self.client_address,
self.client_port, indent(request))
return self.dispatcher.handle_request(request)
def send_response(self, response):
"""
Format a response from the MPD command handlers and send it to the
client.
"""
if response:
response = LINE_TERMINATOR.join(response)
logger.debug(u'Response to [%s]:%s: %s', self.client_address,
self.client_port, indent(response))
response = u'%s%s' % (response, LINE_TERMINATOR)
data = response.encode(ENCODING)
self.push(data)

View File

@ -0,0 +1,130 @@
import logging
logger = logging.getLogger('mopidy.frontends.mpris')
try:
import indicate
except ImportError as import_error:
indicate = None
logger.debug(u'Startup notification will not be sent (%s)', import_error)
from pykka.actor import ThreadingActor
from mopidy import settings
from mopidy.frontends.mpris import objects
from mopidy.listeners import BackendListener
class MprisFrontend(ThreadingActor, BackendListener):
"""
Frontend which lets you control Mopidy through the Media Player Remote
Interfacing Specification (`MPRIS <http://www.mpris.org/>`_) D-Bus
interface.
An example of an MPRIS client is the `Ubuntu Sound Menu
<https://wiki.ubuntu.com/SoundMenu>`_.
**Dependencies:**
- D-Bus Python bindings. The package is named ``python-dbus`` in
Ubuntu/Debian.
- ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the
Ubuntu Sound Menu. The package is named ``python-indicate`` in
Ubuntu/Debian.
- An ``.desktop`` file for Mopidy installed at the path set in
:attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install_desktop_file` for
details.
**Testing the frontend**
To test, start Mopidy, and then run the following in a Python shell::
import dbus
bus = dbus.SessionBus()
player = bus.get_object('org.mpris.MediaPlayer2.mopidy',
'/org/mpris/MediaPlayer2')
Now you can control Mopidy through the player object. Examples:
- To get some properties from Mopidy, run::
props = player.GetAll('org.mpris.MediaPlayer2',
dbus_interface='org.freedesktop.DBus.Properties')
- To quit Mopidy through D-Bus, run::
player.Quit(dbus_interface='org.mpris.MediaPlayer2')
"""
def __init__(self):
self.indicate_server = None
self.mpris_object = None
def on_start(self):
try:
self.mpris_object = objects.MprisObject()
self._send_startup_notification()
except Exception as e:
logger.error(u'MPRIS frontend setup failed (%s)', e)
self.stop()
def on_stop(self):
logger.debug(u'Removing MPRIS object from D-Bus connection...')
if self.mpris_object:
self.mpris_object.remove_from_connection()
self.mpris_object = None
logger.debug(u'Removed MPRIS object from D-Bus connection')
def _send_startup_notification(self):
"""
Send startup notification using libindicate to make Mopidy appear in
e.g. `Ubuntu's sound menu <https://wiki.ubuntu.com/SoundMenu>`_.
A reference to the libindicate server is kept for as long as Mopidy is
running. When Mopidy exits, the server will be unreferenced and Mopidy
will automatically be unregistered from e.g. the sound menu.
"""
if not indicate:
return
logger.debug(u'Sending startup notification...')
self.indicate_server = indicate.Server()
self.indicate_server.set_type('music.mopidy')
self.indicate_server.set_desktop_file(settings.DESKTOP_FILE)
self.indicate_server.show()
logger.debug(u'Startup notification sent')
def _emit_properties_changed(self, *changed_properties):
if self.mpris_object is None:
return
props_with_new_values = [
(p, self.mpris_object.Get(objects.PLAYER_IFACE, p))
for p in changed_properties]
self.mpris_object.PropertiesChanged(objects.PLAYER_IFACE,
dict(props_with_new_values), [])
def track_playback_paused(self, track, time_position):
logger.debug(u'Received track playback paused event')
self._emit_properties_changed('PlaybackStatus')
def track_playback_resumed(self, track, time_position):
logger.debug(u'Received track playback resumed event')
self._emit_properties_changed('PlaybackStatus')
def track_playback_started(self, track):
logger.debug(u'Received track playback started event')
self._emit_properties_changed('PlaybackStatus', 'Metadata')
def track_playback_ended(self, track, time_position):
logger.debug(u'Received track playback ended event')
self._emit_properties_changed('PlaybackStatus', 'Metadata')
def volume_changed(self):
logger.debug(u'Received volume changed event')
self._emit_properties_changed('Volume')
def seeked(self):
logger.debug(u'Received seeked event')
if self.mpris_object is None:
return
self.mpris_object.Seeked(
self.mpris_object.Get(objects.PLAYER_IFACE, 'Position'))

View File

@ -0,0 +1,436 @@
import logging
import os
logger = logging.getLogger('mopidy.frontends.mpris')
try:
import dbus
import dbus.mainloop.glib
import dbus.service
import gobject
except ImportError as import_error:
from mopidy import OptionalDependencyError
raise OptionalDependencyError(import_error)
from pykka.registry import ActorRegistry
from mopidy import settings
from mopidy.backends.base import Backend
from mopidy.backends.base.playback import PlaybackController
from mopidy.mixers.base import BaseMixer
from mopidy.utils.process import exit_process
# Must be done before dbus.SessionBus() is called
gobject.threads_init()
dbus.mainloop.glib.threads_init()
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
BUS_NAME = 'org.mpris.MediaPlayer2.mopidy'
OBJECT_PATH = '/org/mpris/MediaPlayer2'
ROOT_IFACE = 'org.mpris.MediaPlayer2'
PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player'
class MprisObject(dbus.service.Object):
"""Implements http://www.mpris.org/2.1/spec/"""
properties = None
def __init__(self):
self._backend = None
self._mixer = None
self.properties = {
ROOT_IFACE: self._get_root_iface_properties(),
PLAYER_IFACE: self._get_player_iface_properties(),
}
bus_name = self._connect_to_dbus()
super(MprisObject, self).__init__(bus_name, OBJECT_PATH)
def _get_root_iface_properties(self):
return {
'CanQuit': (True, None),
'CanRaise': (False, None),
# NOTE Change if adding optional track list support
'HasTrackList': (False, None),
'Identity': ('Mopidy', None),
'DesktopEntry': (self.get_DesktopEntry, None),
'SupportedUriSchemes': (self.get_SupportedUriSchemes, None),
# NOTE Return MIME types supported by local backend if support for
# reporting supported MIME types is added
'SupportedMimeTypes': (dbus.Array([], signature='s'), None),
}
def _get_player_iface_properties(self):
return {
'PlaybackStatus': (self.get_PlaybackStatus, None),
'LoopStatus': (self.get_LoopStatus, self.set_LoopStatus),
'Rate': (1.0, self.set_Rate),
'Shuffle': (self.get_Shuffle, self.set_Shuffle),
'Metadata': (self.get_Metadata, None),
'Volume': (self.get_Volume, self.set_Volume),
'Position': (self.get_Position, None),
'MinimumRate': (1.0, None),
'MaximumRate': (1.0, None),
'CanGoNext': (self.get_CanGoNext, None),
'CanGoPrevious': (self.get_CanGoPrevious, None),
'CanPlay': (self.get_CanPlay, None),
'CanPause': (self.get_CanPause, None),
'CanSeek': (self.get_CanSeek, None),
'CanControl': (self.get_CanControl, None),
}
def _connect_to_dbus(self):
logger.debug(u'Connecting to D-Bus...')
bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus())
logger.info(u'Connected to D-Bus')
return bus_name
@property
def backend(self):
if self._backend is None:
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, \
'Expected exactly one running backend.'
self._backend = backend_refs[0].proxy()
return self._backend
@property
def mixer(self):
if self._mixer is None:
mixer_refs = ActorRegistry.get_by_class(BaseMixer)
assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
self._mixer = mixer_refs[0].proxy()
return self._mixer
def _get_track_id(self, cp_track):
return '/com/mopidy/track/%d' % cp_track.cpid
def _get_cpid(self, track_id):
assert track_id.startswith('/com/mopidy/track/')
return track_id.split('/')[-1]
### Properties interface
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
in_signature='ss', out_signature='v')
def Get(self, interface, prop):
logger.debug(u'%s.Get(%s, %s) called',
dbus.PROPERTIES_IFACE, repr(interface), repr(prop))
(getter, setter) = self.properties[interface][prop]
if callable(getter):
return getter()
else:
return getter
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
in_signature='s', out_signature='a{sv}')
def GetAll(self, interface):
logger.debug(u'%s.GetAll(%s) called',
dbus.PROPERTIES_IFACE, repr(interface))
getters = {}
for key, (getter, setter) in self.properties[interface].iteritems():
getters[key] = getter() if callable(getter) else getter
return getters
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
in_signature='ssv', out_signature='')
def Set(self, interface, prop, value):
logger.debug(u'%s.Set(%s, %s, %s) called',
dbus.PROPERTIES_IFACE, repr(interface), repr(prop), repr(value))
getter, setter = self.properties[interface][prop]
if setter is not None:
setter(value)
self.PropertiesChanged(interface,
{prop: self.Get(interface, prop)}, [])
@dbus.service.signal(dbus_interface=dbus.PROPERTIES_IFACE,
signature='sa{sv}as')
def PropertiesChanged(self, interface, changed_properties,
invalidated_properties):
logger.debug(u'%s.PropertiesChanged(%s, %s, %s) signaled',
dbus.PROPERTIES_IFACE, interface, changed_properties,
invalidated_properties)
### Root interface methods
@dbus.service.method(dbus_interface=ROOT_IFACE)
def Raise(self):
logger.debug(u'%s.Raise called', ROOT_IFACE)
# Do nothing, as we do not have a GUI
@dbus.service.method(dbus_interface=ROOT_IFACE)
def Quit(self):
logger.debug(u'%s.Quit called', ROOT_IFACE)
exit_process()
### Root interface properties
def get_DesktopEntry(self):
return os.path.splitext(os.path.basename(settings.DESKTOP_FILE))[0]
def get_SupportedUriSchemes(self):
return dbus.Array(self.backend.uri_schemes.get(), signature='s')
### Player interface methods
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Next(self):
logger.debug(u'%s.Next called', PLAYER_IFACE)
if not self.get_CanGoNext():
logger.debug(u'%s.Next not allowed', PLAYER_IFACE)
return
self.backend.playback.next().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Previous(self):
logger.debug(u'%s.Previous called', PLAYER_IFACE)
if not self.get_CanGoPrevious():
logger.debug(u'%s.Previous not allowed', PLAYER_IFACE)
return
self.backend.playback.previous().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Pause(self):
logger.debug(u'%s.Pause called', PLAYER_IFACE)
if not self.get_CanPause():
logger.debug(u'%s.Pause not allowed', PLAYER_IFACE)
return
self.backend.playback.pause().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def PlayPause(self):
logger.debug(u'%s.PlayPause called', PLAYER_IFACE)
if not self.get_CanPause():
logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE)
return
state = self.backend.playback.state.get()
if state == PlaybackController.PLAYING:
self.backend.playback.pause().get()
elif state == PlaybackController.PAUSED:
self.backend.playback.resume().get()
elif state == PlaybackController.STOPPED:
self.backend.playback.play().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Stop(self):
logger.debug(u'%s.Stop called', PLAYER_IFACE)
if not self.get_CanControl():
logger.debug(u'%s.Stop not allowed', PLAYER_IFACE)
return
self.backend.playback.stop().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Play(self):
logger.debug(u'%s.Play called', PLAYER_IFACE)
if not self.get_CanPlay():
logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
return
state = self.backend.playback.state.get()
if state == PlaybackController.PAUSED:
self.backend.playback.resume().get()
else:
self.backend.playback.play().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Seek(self, offset):
logger.debug(u'%s.Seek called', PLAYER_IFACE)
if not self.get_CanSeek():
logger.debug(u'%s.Seek not allowed', PLAYER_IFACE)
return
offset_in_milliseconds = offset // 1000
current_position = self.backend.playback.time_position.get()
new_position = current_position + offset_in_milliseconds
self.backend.playback.seek(new_position)
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def SetPosition(self, track_id, position):
logger.debug(u'%s.SetPosition called', PLAYER_IFACE)
if not self.get_CanSeek():
logger.debug(u'%s.SetPosition not allowed', PLAYER_IFACE)
return
position = position // 1000
current_cp_track = self.backend.playback.current_cp_track.get()
if current_cp_track is None:
return
if track_id != self._get_track_id(current_cp_track):
return
if position < 0:
return
if current_cp_track.track.length < position:
return
self.backend.playback.seek(position)
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def OpenUri(self, uri):
logger.debug(u'%s.OpenUri called', PLAYER_IFACE)
if not self.get_CanPlay():
# NOTE The spec does not explictly require this check, but guarding
# the other methods doesn't help much if OpenUri is open for use.
logger.debug(u'%s.Play not allowed', PLAYER_IFACE)
return
# NOTE Check if URI has MIME type known to the backend, if MIME support
# is added to the backend.
uri_schemes = self.backend.uri_schemes.get()
if not any([uri.startswith(uri_scheme) for uri_scheme in uri_schemes]):
return
track = self.backend.library.lookup(uri).get()
if track is not None:
cp_track = self.backend.current_playlist.add(track).get()
self.backend.playback.play(cp_track)
else:
logger.debug(u'Track with URI "%s" not found in library.', uri)
### Player interface signals
@dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x')
def Seeked(self, position):
logger.debug(u'%s.Seeked signaled', PLAYER_IFACE)
# Do nothing, as just calling the method is enough to emit the signal.
### Player interface properties
def get_PlaybackStatus(self):
state = self.backend.playback.state.get()
if state == PlaybackController.PLAYING:
return 'Playing'
elif state == PlaybackController.PAUSED:
return 'Paused'
elif state == PlaybackController.STOPPED:
return 'Stopped'
def get_LoopStatus(self):
repeat = self.backend.playback.repeat.get()
single = self.backend.playback.single.get()
if not repeat:
return 'None'
else:
if single:
return 'Track'
else:
return 'Playlist'
def set_LoopStatus(self, value):
if not self.get_CanControl():
logger.debug(u'Setting %s.LoopStatus not allowed', PLAYER_IFACE)
return
if value == 'None':
self.backend.playback.repeat = False
self.backend.playback.single = False
elif value == 'Track':
self.backend.playback.repeat = True
self.backend.playback.single = True
elif value == 'Playlist':
self.backend.playback.repeat = True
self.backend.playback.single = False
def set_Rate(self, value):
if not self.get_CanControl():
# NOTE The spec does not explictly require this check, but it was
# added to be consistent with all the other property setters.
logger.debug(u'Setting %s.Rate not allowed', PLAYER_IFACE)
return
if value == 0:
self.Pause()
def get_Shuffle(self):
return self.backend.playback.random.get()
def set_Shuffle(self, value):
if not self.get_CanControl():
logger.debug(u'Setting %s.Shuffle not allowed', PLAYER_IFACE)
return
if value:
self.backend.playback.random = True
else:
self.backend.playback.random = False
def get_Metadata(self):
current_cp_track = self.backend.playback.current_cp_track.get()
if current_cp_track is None:
return {'mpris:trackid': ''}
else:
(cpid, track) = current_cp_track
metadata = {'mpris:trackid': self._get_track_id(current_cp_track)}
if track.length:
metadata['mpris:length'] = track.length * 1000
if track.uri:
metadata['xesam:url'] = track.uri
if track.name:
metadata['xesam:title'] = track.name
if track.artists:
artists = list(track.artists)
artists.sort(key=lambda a: a.name)
metadata['xesam:artist'] = dbus.Array(
[a.name for a in artists if a.name], signature='s')
if track.album and track.album.name:
metadata['xesam:album'] = track.album.name
if track.album and track.album.artists:
artists = list(track.album.artists)
artists.sort(key=lambda a: a.name)
metadata['xesam:albumArtist'] = dbus.Array(
[a.name for a in artists if a.name], signature='s')
if track.track_no:
metadata['xesam:trackNumber'] = track.track_no
return dbus.Dictionary(metadata, signature='sv')
def get_Volume(self):
volume = self.mixer.volume.get()
if volume is not None:
return volume / 100.0
def set_Volume(self, value):
if not self.get_CanControl():
logger.debug(u'Setting %s.Volume not allowed', PLAYER_IFACE)
return
if value is None:
return
elif value < 0:
self.mixer.volume = 0
elif value > 1:
self.mixer.volume = 100
elif 0 <= value <= 1:
self.mixer.volume = int(value * 100)
def get_Position(self):
return self.backend.playback.time_position.get() * 1000
def get_CanGoNext(self):
if not self.get_CanControl():
return False
return (self.backend.playback.cp_track_at_next.get() !=
self.backend.playback.current_cp_track.get())
def get_CanGoPrevious(self):
if not self.get_CanControl():
return False
return (self.backend.playback.cp_track_at_previous.get() !=
self.backend.playback.current_cp_track.get())
def get_CanPlay(self):
if not self.get_CanControl():
return False
return (self.backend.playback.current_track.get() is not None
or self.backend.playback.track_at_next.get() is not None)
def get_CanPause(self):
if not self.get_CanControl():
return False
# NOTE Should be changed to vary based on capabilities of the current
# track if Mopidy starts supporting non-seekable media, like streams.
return True
def get_CanSeek(self):
if not self.get_CanControl():
return False
# NOTE Should be changed to vary based on capabilities of the current
# track if Mopidy starts supporting non-seekable media, like streams.
return True
def get_CanControl(self):
# NOTE This could be a setting for the end user to change.
return True

View File

@ -43,9 +43,6 @@ class GStreamer(ThreadingActor):
self._handlers = {}
def on_start(self):
# **Warning:** :class:`GStreamer` requires
# :class:`mopidy.utils.process.GObjectEventThread` to be running. This
# is not enforced by :class:`GStreamer` itself.
self._setup_pipeline()
self._setup_outputs()
self._setup_message_processor()
@ -277,10 +274,18 @@ class GStreamer(ThreadingActor):
taglist = gst.TagList()
artists = [a for a in (track.artists or []) if a.name]
# Default to blank data to trick shoutcast into clearing any previous
# values it might have.
taglist[gst.TAG_ARTIST] = u' '
taglist[gst.TAG_TITLE] = u' '
taglist[gst.TAG_ALBUM] = u' '
if artists:
taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists])
if track.name:
taglist[gst.TAG_TITLE] = track.name
if track.album and track.album.name:
taglist[gst.TAG_ALBUM] = track.album.name

116
mopidy/listeners.py Normal file
View File

@ -0,0 +1,116 @@
from pykka import registry
class BackendListener(object):
"""
Marker interface for recipients of events sent by the backend.
Any Pykka actor that mixes in this class will receive calls to the methods
defined here when the corresponding events happen in the backend. This
interface is used both for looking up what actors to notify of the events,
and for providing default implementations for those listeners that are not
interested in all events.
"""
@staticmethod
def send(event, **kwargs):
"""Helper to allow calling of backend listener events"""
# FIXME this should be updated once Pykka supports non-blocking calls
# on proxies or some similar solution.
registry.ActorRegistry.broadcast({
'command': 'pykka_call',
'attr_path': (event,),
'args': [],
'kwargs': kwargs,
}, target_class=BackendListener)
def track_playback_paused(self, track, time_position):
"""
Called whenever track playback is paused.
*MAY* be implemented by actor.
:param track: the track that was playing when playback paused
:type track: :class:`mopidy.models.Track`
:param time_position: the time position in milliseconds
:type time_position: int
"""
pass
def track_playback_resumed(self, track, time_position):
"""
Called whenever track playback is resumed.
*MAY* be implemented by actor.
:param track: the track that was playing when playback resumed
:type track: :class:`mopidy.models.Track`
:param time_position: the time position in milliseconds
:type time_position: int
"""
pass
def track_playback_started(self, track):
"""
Called whenever a new track starts playing.
*MAY* be implemented by actor.
:param track: the track that just started playing
:type track: :class:`mopidy.models.Track`
"""
pass
def track_playback_ended(self, track, time_position):
"""
Called whenever playback of a track ends.
*MAY* be implemented by actor.
:param track: the track that was played before playback stopped
:type track: :class:`mopidy.models.Track`
:param time_position: the time position in milliseconds
:type time_position: int
"""
pass
def playback_state_changed(self):
"""
Called whenever playback state is changed.
*MAY* be implemented by actor.
"""
pass
def playlist_changed(self):
"""
Called whenever a playlist is changed.
*MAY* be implemented by actor.
"""
pass
def options_changed(self):
"""
Called whenever an option is changed.
*MAY* be implemented by actor.
"""
pass
def volume_changed(self):
"""
Called whenever the volume is changed.
*MAY* be implemented by actor.
"""
pass
def seeked(self):
"""
Called whenever the time position changes by an unexpected amount, e.g.
at seek to a new time position.
*MAY* be implemented by actor.
"""
pass

View File

@ -1,4 +1,8 @@
from mopidy import settings
import logging
from mopidy import listeners, settings
logger = logging.getLogger('mopdy.mixers')
class BaseMixer(object):
"""
@ -30,6 +34,7 @@ class BaseMixer(object):
elif volume > 100:
volume = 100
self.set_volume(volume)
self._trigger_volume_changed()
def get_volume(self):
"""
@ -46,3 +51,7 @@ class BaseMixer(object):
*MUST be implemented by subclass.*
"""
raise NotImplementedError
def _trigger_volume_changed(self):
logger.debug(u'Triggering volume changed event')
listeners.BackendListener.send('volume_changed')

View File

@ -4,7 +4,7 @@ Available settings and their default values.
.. warning::
Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a
file called ``~/.mopidy/settings.py`` and redefine settings there.
file called ``~/.config/mopidy/settings.py`` and redefine settings there.
"""
#: List of playback backends to use. See :mod:`mopidy.backends` for all
@ -26,7 +26,8 @@ BACKENDS = (
#: details on the format.
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
#: Which GStreamer bin description to use in :class:`mopidy.outputs.CustomOutput`.
#: Which GStreamer bin description to use in
#: :class:`mopidy.outputs.custom.CustomOutput`.
#:
#: Default::
#:
@ -48,6 +49,15 @@ DEBUG_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \
#: DEBUG_LOG_FILENAME = u'mopidy.log'
DEBUG_LOG_FILENAME = u'mopidy.log'
#: Location of the Mopidy .desktop file.
#:
#: Used by :mod:`mopidy.frontends.mpris`.
#:
#: Default::
#:
#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop'
#: List of server frontends to use.
#:
#: Default::
@ -55,10 +65,12 @@ DEBUG_LOG_FILENAME = u'mopidy.log'
#: FRONTENDS = (
#: u'mopidy.frontends.mpd.MpdFrontend',
#: u'mopidy.frontends.lastfm.LastfmFrontend',
#: u'mopidy.frontends.mpris.MprisFrontend',
#: )
FRONTENDS = (
u'mopidy.frontends.mpd.MpdFrontend',
u'mopidy.frontends.lastfm.LastfmFrontend',
u'mopidy.frontends.mpris.MprisFrontend',
)
#: Your `Last.fm <http://www.last.fm/>`_ username.
@ -77,8 +89,9 @@ LASTFM_PASSWORD = u''
#:
#: Default::
#:
#: LOCAL_MUSIC_PATH = u'~/music'
LOCAL_MUSIC_PATH = u'~/music'
#: # Defaults to asking glib where music is stored, fallback is ~/music
#: LOCAL_MUSIC_PATH = None
LOCAL_MUSIC_PATH = None
#: Path to playlist folder with m3u files for local music.
#:
@ -86,8 +99,8 @@ LOCAL_MUSIC_PATH = u'~/music'
#:
#: Default::
#:
#: LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
#: LOCAL_PLAYLIST_PATH = None # Implies $XDG_DATA_DIR/mopidy/playlists
LOCAL_PLAYLIST_PATH = None
#: Path to tag cache for local music.
#:
@ -95,8 +108,8 @@ LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists'
#:
#: Default::
#:
#: LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache'
#: LOCAL_TAG_CACHE_FILE = None # Implies $XDG_DATA_DIR/mopidy/tag_cache
LOCAL_TAG_CACHE_FILE = None
#: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers.
#:
@ -167,6 +180,11 @@ MPD_SERVER_PORT = 6600
#: Default: :class:`None`, which means no password required.
MPD_SERVER_PASSWORD = None
#: The maximum number of concurrent connections the MPD server will accept.
#:
#: Default: 20
MPD_SERVER_MAX_CONNECTIONS = 20
#: List of outputs to use. See :mod:`mopidy.outputs` for all available
#: backends
#:
@ -236,7 +254,7 @@ SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
#: Path to the Spotify cache.
#:
#: Used by :mod:`mopidy.backends.spotify`.
SPOTIFY_CACHE_PATH = u'~/.mopidy/spotify_cache'
SPOTIFY_CACHE_PATH = None
#: Your Spotify Premium username.
#:

View File

@ -18,9 +18,11 @@ def import_module(name):
return sys.modules[name]
def get_class(name):
logger.debug('Loading: %s', name)
if '.' not in name:
raise ImportError("Couldn't load: %s" % name)
module_name = name[:name.rindex('.')]
class_name = name[name.rindex('.') + 1:]
logger.debug('Loading: %s', name)
try:
module = import_module(module_name)
class_object = getattr(module, class_name)

View File

@ -20,7 +20,7 @@ def setup_console_logging(verbosity_level):
if verbosity_level == 0:
log_level = logging.WARNING
log_format = settings.CONSOLE_LOG_FORMAT
elif verbosity_level == 2:
elif verbosity_level >= 2:
log_level = logging.DEBUG
log_format = settings.DEBUG_LOG_FORMAT
else:
@ -33,6 +33,9 @@ def setup_console_logging(verbosity_level):
root = logging.getLogger('')
root.addHandler(handler)
if verbosity_level < 3:
logging.getLogger('pykka').setLevel(logging.INFO)
def setup_debug_logging_to_file():
formatter = logging.Formatter(settings.DEBUG_LOG_FORMAT)
handler = logging.handlers.RotatingFileHandler(

View File

@ -1,10 +1,20 @@
import errno
import gobject
import logging
import re
import socket
import threading
from pykka import ActorDeadError
from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
logger = logging.getLogger('mopidy.utils.server')
def _try_ipv6_socket():
class ShouldRetrySocketCall(Exception):
"""Indicate that attempted socket call should be retried"""
def try_ipv6_socket():
"""Determine if system really supports IPv6"""
if not socket.has_ipv6:
return False
@ -17,7 +27,7 @@ def _try_ipv6_socket():
return False
#: Boolean value that indicates if creating an IPv6 socket will succeed.
has_ipv6 = _try_ipv6_socket()
has_ipv6 = try_ipv6_socket()
def create_socket():
"""Create a TCP socket with or without IPv6 depending on system support"""
@ -27,6 +37,7 @@ def create_socket():
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return sock
def format_hostname(hostname):
@ -34,3 +45,350 @@ def format_hostname(hostname):
if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
hostname = '::ffff:%s' % hostname
return hostname
class Server(object):
"""Setup listener and register it with gobject's event loop."""
def __init__(self, host, port, protocol, max_connections=5, timeout=30):
self.protocol = protocol
self.max_connections = max_connections
self.timeout = timeout
self.server_socket = self.create_server_socket(host, port)
self.register_server_socket(self.server_socket.fileno())
def create_server_socket(self, host, port):
sock = create_socket()
sock.setblocking(False)
sock.bind((host, port))
sock.listen(1)
return sock
def register_server_socket(self, fileno):
gobject.io_add_watch(fileno, gobject.IO_IN, self.handle_connection)
def handle_connection(self, fd, flags):
try:
sock, addr = self.accept_connection()
except ShouldRetrySocketCall:
return True
if self.maximum_connections_exceeded():
self.reject_connection(sock, addr)
else:
self.init_connection(sock, addr)
return True
def accept_connection(self):
try:
return self.server_socket.accept()
except socket.error as e:
if e.errno in (errno.EAGAIN, errno.EINTR):
raise ShouldRetrySocketCall
raise
def maximum_connections_exceeded(self):
return (self.max_connections is not None and
self.number_of_connections() >= self.max_connections)
def number_of_connections(self):
return len(ActorRegistry.get_by_class(self.protocol))
def reject_connection(self, sock, addr):
# FIXME provide more context in logging?
logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1])
try:
sock.close()
except socket.error:
pass
def init_connection(self, sock, addr):
Connection(self.protocol, sock, addr, self.timeout)
class Connection(object):
# NOTE: the callback code is _not_ run in the actor's thread, but in the
# same one as the event loop. If code in the callbacks blocks, the rest of
# gobject code will likely be blocked as well...
#
# Also note that source_remove() return values are ignored on purpose, a
# false return value would only tell us that what we thought was registered
# is already gone, there is really nothing more we can do.
def __init__(self, protocol, sock, addr, timeout):
sock.setblocking(False)
self.host, self.port = addr[:2] # IPv6 has larger addr
self.sock = sock
self.protocol = protocol
self.timeout = timeout
self.send_lock = threading.Lock()
self.send_buffer = ''
self.stopping = False
self.recv_id = None
self.send_id = None
self.timeout_id = None
self.actor_ref = self.protocol.start(self)
self.enable_recv()
self.enable_timeout()
def stop(self, reason, level=logging.DEBUG):
if self.stopping:
logger.log(level, 'Already stopping: %s' % reason)
return
else:
self.stopping = True
logger.log(level, reason)
try:
self.actor_ref.stop()
except ActorDeadError:
pass
self.disable_timeout()
self.disable_recv()
self.disable_send()
try:
self.sock.close()
except socket.error:
pass
def queue_send(self, data):
"""Try to send data to client exactly as is and queue rest."""
self.send_lock.acquire(True)
self.send_buffer = self.send(self.send_buffer + data)
self.send_lock.release()
if self.send_buffer:
self.enable_send()
def send(self, data):
"""Send data to client, return any unsent data."""
try:
sent = self.sock.send(data)
return data[sent:]
except socket.error as e:
if e.errno in (errno.EWOULDBLOCK, errno.EINTR):
return data
self.stop(u'Unexpected client error: %s' % e)
return ''
def enable_timeout(self):
"""Reactivate timeout mechanism."""
if self.timeout <= 0:
return
self.disable_timeout()
self.timeout_id = gobject.timeout_add_seconds(
self.timeout, self.timeout_callback)
def disable_timeout(self):
"""Deactivate timeout mechanism."""
if self.timeout_id is None:
return
gobject.source_remove(self.timeout_id)
self.timeout_id = None
def enable_recv(self):
if self.recv_id is not None:
return
try:
self.recv_id = gobject.io_add_watch(self.sock.fileno(),
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
self.recv_callback)
except socket.error as e:
self.stop(u'Problem with connection: %s' % e)
def disable_recv(self):
if self.recv_id is None:
return
gobject.source_remove(self.recv_id)
self.recv_id = None
def enable_send(self):
if self.send_id is not None:
return
try:
self.send_id = gobject.io_add_watch(self.sock.fileno(),
gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP,
self.send_callback)
except socket.error as e:
self.stop(u'Problem with connection: %s' % e)
def disable_send(self):
if self.send_id is None:
return
gobject.source_remove(self.send_id)
self.send_id = None
def recv_callback(self, fd, flags):
if flags & (gobject.IO_ERR | gobject.IO_HUP):
self.stop(u'Bad client flags: %s' % flags)
return True
try:
data = self.sock.recv(4096)
except socket.error as e:
if e.errno not in (errno.EWOULDBLOCK, errno.EINTR):
self.stop(u'Unexpected client error: %s' % e)
return True
if not data:
self.stop(u'Client most likely disconnected.')
return True
try:
self.actor_ref.send_one_way({'received': data})
except ActorDeadError:
self.stop(u'Actor is dead.')
return True
def send_callback(self, fd, flags):
if flags & (gobject.IO_ERR | gobject.IO_HUP):
self.stop(u'Bad client flags: %s' % flags)
return True
# If with can't get the lock, simply try again next time socket is
# ready for sending.
if not self.send_lock.acquire(False):
return True
try:
self.send_buffer = self.send(self.send_buffer)
if not self.send_buffer:
self.disable_send()
finally:
self.send_lock.release()
return True
def timeout_callback(self):
self.stop(u'Client timeout out after %s seconds' % self.timeout)
return False
class LineProtocol(ThreadingActor):
"""
Base class for handling line based protocols.
Takes care of receiving new data from server's client code, decoding and
then splitting data along line boundaries.
"""
#: Line terminator to use for outputed lines.
terminator = '\n'
#: Regex to use for spliting lines, will be set compiled version of its
#: own value, or to ``terminator``s value if it is not set itself.
delimeter = None
#: What encoding to expect incomming data to be in, can be :class:`None`.
encoding = 'utf-8'
def __init__(self, connection):
self.connection = connection
self.prevent_timeout = False
self.recv_buffer = ''
if self.delimeter:
self.delimeter = re.compile(self.delimeter)
else:
self.delimeter = re.compile(self.terminator)
@property
def host(self):
return self.connection.host
@property
def port(self):
return self.connection.port
def on_line_received(self, line):
"""
Called whenever a new line is found.
Should be implemented by subclasses.
"""
raise NotImplementedError
def on_receive(self, message):
"""Handle messages with new data from server."""
if 'received' not in message:
return
self.connection.disable_timeout()
self.recv_buffer += message['received']
for line in self.parse_lines():
line = self.decode(line)
if line is not None:
self.on_line_received(line)
if not self.prevent_timeout:
self.connection.enable_timeout()
def on_stop(self):
"""Ensure that cleanup when actor stops."""
self.connection.stop(u'Actor is shutting down.')
def parse_lines(self):
"""Consume new data and yield any lines found."""
while re.search(self.terminator, self.recv_buffer):
line, self.recv_buffer = self.delimeter.split(
self.recv_buffer, 1)
yield line
def encode(self, line):
"""
Handle encoding of line.
Can be overridden by subclasses to change encoding behaviour.
"""
try:
return line.encode(self.encoding)
except UnicodeError:
logger.warning(u'Stopping actor due to encode problem, data '
'supplied by client was not valid %s', self.encoding)
self.stop()
def decode(self, line):
"""
Handle decoding of line.
Can be overridden by subclasses to change decoding behaviour.
"""
try:
return line.decode(self.encoding)
except UnicodeError:
logger.warning(u'Stopping actor due to decode problem, data '
'supplied by client was not valid %s', self.encoding)
self.stop()
def join_lines(self, lines):
if not lines:
return u''
return self.terminator.join(lines) + self.terminator
def send_lines(self, lines):
"""
Send array of lines to client via connection.
Join lines using the terminator that is set for this class, encode it
and send it to the client.
"""
if not lines:
return
data = self.join_lines(lines)
self.connection.queue_send(self.encode(data))

View File

@ -60,6 +60,7 @@ def find_files(path):
yield filename
# pylint: enable = W0612
# FIXME replace with mock usage in tests.
class Mtime(object):
def __init__(self):
self.fake = None

View File

@ -3,9 +3,6 @@ import signal
import thread
import threading
import gobject
gobject.threads_init()
from pykka import ActorDeadError
from pykka.registry import ActorRegistry
@ -25,9 +22,17 @@ def exit_handler(signum, frame):
logger.info(u'Got %s signal', signals[signum])
exit_process()
def stop_all_actors():
def stop_actors_by_class(klass):
actors = ActorRegistry.get_by_class(klass)
logger.debug(u'Stopping %d instance(s) of %s', len(actors), klass.__name__)
for actor in actors:
actor.stop()
def stop_remaining_actors():
num_actors = len(ActorRegistry.get_all())
while num_actors:
logger.error(
u'There are actor threads still running, this is probably a bug')
logger.debug(u'Seeing %d actor and %d non-actor thread(s): %s',
num_actors, threading.active_count() - num_actors,
', '.join([t.name for t in threading.enumerate()]))
@ -60,25 +65,3 @@ class BaseThread(threading.Thread):
def run_inside_try(self):
raise NotImplementedError
class GObjectEventThread(BaseThread):
"""
A GObject event loop which is shared by all Mopidy components that uses
libraries that need a GObject event loop, like GStreamer and D-Bus.
Should be started by Mopidy's core and used by
:mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc.
"""
def __init__(self):
super(GObjectEventThread, self).__init__()
self.name = u'GObjectEventThread'
self.loop = None
def run_inside_try(self):
self.loop = gobject.MainLoop().run()
def destroy(self):
self.loop.quit()
super(GObjectEventThread, self).destroy()

View File

@ -2,12 +2,13 @@
from __future__ import absolute_import
from copy import copy
import getpass
import glib
import logging
import os
from pprint import pformat
import sys
from mopidy import SettingsError
from mopidy import SettingsError, SETTINGS_PATH, SETTINGS_FILE
from mopidy.utils.log import indent
logger = logging.getLogger('mopidy.utils.settings')
@ -20,11 +21,9 @@ class SettingsProxy(object):
self.runtime = {}
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):
if not os.path.isfile(SETTINGS_FILE):
return {}
sys.path.insert(0, dotdir)
sys.path.insert(0, SETTINGS_PATH)
# pylint: disable = F0401
import settings as local_settings_module
# pylint: enable = F0401
@ -53,6 +52,8 @@ class SettingsProxy(object):
value = self.current[attr]
if isinstance(value, basestring) and len(value) == 0:
raise SettingsError(u'Setting "%s" is empty.' % attr)
if not value:
return value
if attr.endswith('_PATH') or attr.endswith('_FILE'):
value = os.path.expanduser(value)
value = os.path.abspath(value)

View File

@ -1 +1 @@
Pykka >= 0.12
Pykka >= 0.12.3

View File

@ -1,4 +1,5 @@
coverage
mock
mock >= 0.7
nose
tox
yappi

View File

@ -1,24 +1,41 @@
import os
import sys
try: # 2.7
# pylint: disable = E0611,F0401
from unittest.case import SkipTest
# pylint: enable = E0611,F0401
except ImportError:
try: # Nose
from nose.plugins.skip import SkipTest
except ImportError: # Failsafe
class SkipTest(Exception):
pass
if sys.version_info < (2, 7):
import unittest2 as unittest
else:
import unittest
from mopidy import settings
# Nuke any local settings to ensure same test env all over
settings.local.clear()
def path_to_data_dir(name):
path = os.path.dirname(__file__)
path = os.path.join(path, 'data')
path = os.path.abspath(path)
return os.path.join(path, name)
class IsA(object):
def __init__(self, klass):
self.klass = klass
def __eq__(self, rhs):
try:
return isinstance(rhs, self.klass)
except TypeError:
return type(rhs) == type(self.klass)
def __ne__(self, rhs):
return not self.__eq__(rhs)
def __repr__(self):
return str(self.klass)
any_int = IsA(int)
any_str = IsA(str)
any_unicode = IsA(unicode)

View File

@ -1,4 +1,8 @@
import nose
import yappi
if __name__ == '__main__':
try:
yappi.start()
nose.main()
finally:
yappi.print_stats()

View File

@ -1,5 +1,4 @@
import mock
import multiprocessing
import random
from mopidy.models import Playlist, Track
@ -7,6 +6,7 @@ from mopidy.gstreamer import GStreamer
from tests.backends.base import populate_playlist
class CurrentPlaylistControllerTest(object):
tracks = []

View File

@ -1,6 +1,7 @@
from mopidy.models import Playlist, Track, Album, Artist
from tests import SkipTest, path_to_data_dir
from tests import unittest, path_to_data_dir
class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
@ -20,11 +21,13 @@ class LibraryControllerTest(object):
def test_refresh(self):
self.library.refresh()
@unittest.SkipTest
def test_refresh_uri(self):
raise SkipTest
pass
@unittest.SkipTest
def test_refresh_missing_uri(self):
raise SkipTest
pass
def test_lookup(self):
track = self.library.lookup(self.tracks[0].uri)

View File

@ -1,16 +1,16 @@
import mock
import multiprocessing
import random
import time
from mopidy.models import Track
from mopidy.gstreamer import GStreamer
from tests import SkipTest
from tests import unittest
from tests.backends.base import populate_playlist
# TODO Test 'playlist repeat', e.g. repeat=1,single=0
class PlaybackControllerTest(object):
tracks = []
@ -520,7 +520,7 @@ class PlaybackControllerTest(object):
self.assert_(wrapper.called)
@SkipTest # Blocks for 10ms
@unittest.SkipTest # Blocks for 10ms
@populate_playlist
def test_end_of_track_callback_gets_called(self):
self.playback.play()
@ -555,7 +555,7 @@ class PlaybackControllerTest(object):
@populate_playlist
def test_pause_when_stopped(self):
self.playback.pause()
self.assertEqual(self.playback.state, self.playback.STOPPED)
self.assertEqual(self.playback.state, self.playback.PAUSED)
@populate_playlist
def test_pause_when_playing(self):
@ -599,7 +599,7 @@ class PlaybackControllerTest(object):
self.playback.pause()
self.assertEqual(self.playback.resume(), None)
@SkipTest # Uses sleep and might not work with LocalBackend
@unittest.SkipTest # Uses sleep and might not work with LocalBackend
@populate_playlist
def test_resume_continues_from_right_position(self):
self.playback.play()
@ -668,7 +668,7 @@ class PlaybackControllerTest(object):
self.playback.seek(0)
self.assertEqual(self.playback.state, self.playback.PLAYING)
@SkipTest
@unittest.SkipTest
@populate_playlist
def test_seek_beyond_end_of_song(self):
# FIXME need to decide return value
@ -688,7 +688,7 @@ class PlaybackControllerTest(object):
self.playback.seek(self.current_playlist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, self.playback.STOPPED)
@SkipTest
@unittest.SkipTest
@populate_playlist
def test_seek_beyond_start_of_song(self):
# FIXME need to decide return value
@ -741,7 +741,7 @@ class PlaybackControllerTest(object):
self.assertEqual(self.playback.time_position, 0)
@SkipTest # Uses sleep and does might not work with LocalBackend
@unittest.SkipTest # Uses sleep and does might not work with LocalBackend
@populate_playlist
def test_time_position_when_playing(self):
self.playback.play()
@ -750,7 +750,7 @@ class PlaybackControllerTest(object):
second = self.playback.time_position
self.assert_(second > first, '%s - %s' % (first, second))
@SkipTest # Uses sleep
@unittest.SkipTest # Uses sleep
@populate_playlist
def test_time_position_when_paused(self):
self.playback.play()

View File

@ -5,7 +5,8 @@ import tempfile
from mopidy import settings
from mopidy.models import Playlist
from tests import SkipTest, path_to_data_dir
from tests import unittest, path_to_data_dir
class StoredPlaylistsControllerTest(object):
def setUp(self):
@ -78,11 +79,13 @@ class StoredPlaylistsControllerTest(object):
except LookupError as e:
self.assertEqual(u'"name=c" match no playlists', e[0])
@unittest.SkipTest
def test_lookup(self):
raise SkipTest
pass
@unittest.SkipTest
def test_refresh(self):
raise SkipTest
pass
def test_rename(self):
playlist = self.stored.create('test')
@ -100,5 +103,6 @@ class StoredPlaylistsControllerTest(object):
self.stored.save(playlist)
self.assert_(playlist in self.stored.playlists)
@unittest.SkipTest
def test_playlist_with_unknown_track(self):
raise SkipTest
pass

View File

@ -0,0 +1,53 @@
import mock
from pykka.registry import ActorRegistry
from mopidy.backends.dummy import DummyBackend
from mopidy.listeners import BackendListener
from mopidy.models import Track
from tests import unittest
@mock.patch.object(BackendListener, 'send')
class BackendEventsTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
def tearDown(self):
ActorRegistry.stop_all()
def test_pause_sends_track_playback_paused_event(self, send):
self.backend.current_playlist.add(Track(uri='a'))
self.backend.playback.play().get()
send.reset_mock()
self.backend.playback.pause().get()
self.assertEqual(send.call_args[0][0], 'track_playback_paused')
def test_resume_sends_track_playback_resumed(self, send):
self.backend.current_playlist.add(Track(uri='a'))
self.backend.playback.play()
self.backend.playback.pause().get()
send.reset_mock()
self.backend.playback.resume().get()
self.assertEqual(send.call_args[0][0], 'track_playback_resumed')
def test_play_sends_track_playback_started_event(self, send):
self.backend.current_playlist.add(Track(uri='a'))
send.reset_mock()
self.backend.playback.play().get()
self.assertEqual(send.call_args[0][0], 'track_playback_started')
def test_stop_sends_track_playback_ended_event(self, send):
self.backend.current_playlist.add(Track(uri='a'))
self.backend.playback.play().get()
send.reset_mock()
self.backend.playback.stop().get()
self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended')
def test_seek_sends_seeked_event(self, send):
self.backend.current_playlist.add(Track(uri='a', length=40000))
self.backend.playback.play().get()
send.reset_mock()
self.backend.playback.seek(1000).get()
self.assertEqual(send.call_args[0][0], 'seeked')

View File

@ -1,18 +1,16 @@
import unittest
# FIXME Our Windows build server does not support GStreamer yet
import sys
if sys.platform == 'win32':
from tests import SkipTest
raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.models import Track
from tests import unittest
from tests.backends.base.current_playlist import CurrentPlaylistControllerTest
from tests.backends.local import generate_song
@unittest.skipIf(sys.platform == 'win32',
'Our Windows build server does not support GStreamer yet')
class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest,
unittest.TestCase):

View File

@ -1,17 +1,14 @@
import unittest
# FIXME Our Windows build server does not support GStreamer yet
import sys
if sys.platform == 'win32':
from tests import SkipTest
raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
from tests import path_to_data_dir
from tests import unittest, path_to_data_dir
from tests.backends.base.library import LibraryControllerTest
@unittest.skipIf(sys.platform == 'win32',
'Our Windows build server does not support GStreamer yet')
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = LocalBackend

View File

@ -1,20 +1,17 @@
import unittest
# FIXME Our Windows build server does not support GStreamer yet
import sys
if sys.platform == 'win32':
from tests import SkipTest
raise SkipTest
from mopidy import settings
from mopidy.backends.local import LocalBackend
from mopidy.models import Track
from mopidy.utils.path import path_to_uri
from tests import path_to_data_dir
from tests import unittest, path_to_data_dir
from tests.backends.base.playback import PlaybackControllerTest
from tests.backends.local import generate_song
@unittest.skipIf(sys.platform == 'win32',
'Our Windows build server does not support GStreamer yet')
class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
backend_class = LocalBackend
tracks = [Track(uri=generate_song(i), length=4464)
@ -36,8 +33,8 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase):
track = Track(uri=uri, length=4464)
self.backend.current_playlist.add(track)
def test_uri_handler(self):
self.assert_('file://' in self.backend.uri_handlers)
def test_uri_scheme(self):
self.assert_('file' in self.backend.uri_schemes)
def test_play_mp3(self):
self.add_track('blank.mp3')

View File

@ -1,24 +1,19 @@
import unittest
import os
from tests import SkipTest
# FIXME Our Windows build server does not support GStreamer yet
import sys
if sys.platform == 'win32':
raise SkipTest
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.path import path_to_uri
from tests import path_to_data_dir
from tests.backends.base.stored_playlists import \
StoredPlaylistsControllerTest
from tests import unittest, path_to_data_dir
from tests.backends.base.stored_playlists import (
StoredPlaylistsControllerTest)
from tests.backends.local import generate_song
@unittest.skipIf(sys.platform == 'win32',
'Our Windows build server does not support GStreamer yet')
class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
unittest.TestCase):
@ -77,14 +72,18 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest,
self.assertEqual('test', self.stored.playlists[0].name)
self.assertEqual(track.uri, self.stored.playlists[0].tracks[0].uri)
@unittest.SkipTest
def test_santitising_of_playlist_filenames(self):
raise SkipTest
pass
@unittest.SkipTest
def test_playlist_folder_is_createad(self):
raise SkipTest
pass
@unittest.SkipTest
def test_create_sets_playlist_uri(self):
raise SkipTest
pass
@unittest.SkipTest
def test_save_sets_playlist_uri(self):
raise SkipTest
pass

View File

@ -2,13 +2,12 @@
import os
import tempfile
import unittest
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, path_to_data_dir
from tests import unittest, path_to_data_dir
song1_path = path_to_data_dir('song1.mp3')
song2_path = path_to_data_dir('song2.mp3')
@ -17,6 +16,9 @@ song1_uri = path_to_uri(song1_path)
song2_uri = path_to_uri(song2_path)
encoded_uri = path_to_uri(encoded_path)
# FIXME use mock instead of tempfile.NamedTemporaryFile
class M3UToUriTest(unittest.TestCase):
def test_empty_file(self):
uris = parse_m3u(path_to_data_dir('empty.m3u'))
@ -127,9 +129,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase):
self.assertEqual(track, list(tracks)[0])
@unittest.SkipTest
def test_misencoded_cache(self):
# FIXME not sure if this can happen
raise SkipTest
pass
def test_cache_with_blank_track_info(self):
tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'),

View File

@ -1,30 +0,0 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test_enableoutput(self):
result = self.dispatcher.handle_request(u'enableoutput "0"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_disableoutput(self):
result = self.dispatcher.handle_request(u'disableoutput "0"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_outputs(self):
result = self.dispatcher.handle_request(u'outputs')
self.assert_(u'outputid: 0' in result)
self.assert_(u'outputname: None' in result)
self.assert_(u'outputenabled: 1' in result)
self.assert_(u'OK' in result)

View File

@ -1,63 +0,0 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
class CommandListsTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = dispatcher.MpdDispatcher()
def tearDown(self):
self.b.stop().get()
self.mixer.stop().get()
def test_command_list_begin(self):
result = self.dispatcher.handle_request(u'command_list_begin')
self.assertEquals(result, [])
def test_command_list_end(self):
self.dispatcher.handle_request(u'command_list_begin')
result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'OK' in result)
def test_command_list_end_without_start_first_is_an_unknown_command(self):
result = self.dispatcher.handle_request(u'command_list_end')
self.assertEquals(result[0],
u'ACK [5@0] {} unknown command "command_list_end"')
def test_command_list_with_ping(self):
self.dispatcher.handle_request(u'command_list_begin')
self.assertEqual([], self.dispatcher.command_list)
self.assertEqual(False, self.dispatcher.command_list_ok)
self.dispatcher.handle_request(u'ping')
self.assert_(u'ping' in self.dispatcher.command_list)
result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'OK' in result)
self.assertEqual(False, self.dispatcher.command_list)
def test_command_list_with_error_returns_ack_with_correct_index(self):
self.dispatcher.handle_request(u'command_list_begin')
self.dispatcher.handle_request(u'play') # Known command
self.dispatcher.handle_request(u'paly') # Unknown command
result = self.dispatcher.handle_request(u'command_list_end')
self.assertEqual(len(result), 1, result)
self.assertEqual(result[0], u'ACK [5@1] {} unknown command "paly"')
def test_command_list_ok_begin(self):
result = self.dispatcher.handle_request(u'command_list_ok_begin')
self.assertEquals(result, [])
def test_command_list_ok_with_ping(self):
self.dispatcher.handle_request(u'command_list_ok_begin')
self.assertEqual([], self.dispatcher.command_list)
self.assertEqual(True, self.dispatcher.command_list_ok)
self.dispatcher.handle_request(u'ping')
self.assert_(u'ping' in self.dispatcher.command_list)
result = self.dispatcher.handle_request(u'command_list_end')
self.assert_(u'list_OK' in result)
self.assert_(u'OK' in result)
self.assertEqual(False, self.dispatcher.command_list)
self.assertEqual(False, self.dispatcher.command_list_ok)

View File

@ -1,53 +0,0 @@
import mock
import unittest
from mopidy import settings
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.session import MpdSession
from mopidy.mixers.dummy import DummyMixer
class ConnectionHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.session = mock.Mock(spec=MpdSession)
self.dispatcher = MpdDispatcher(session=self.session)
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
settings.runtime.clear()
def test_close_closes_the_client_connection(self):
result = self.dispatcher.handle_request(u'close')
self.assert_(self.session.close.called,
u'Should call close() on MpdSession')
self.assert_(u'OK' in result)
def test_empty_request(self):
result = self.dispatcher.handle_request(u'')
self.assert_(u'OK' in result)
def test_kill(self):
result = self.dispatcher.handle_request(u'kill')
self.assert_(u'ACK [4@0] {kill} you don\'t have permission for "kill"' in result)
def test_valid_password_is_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
result = self.dispatcher.handle_request(u'password "topsecret"')
self.assert_(u'OK' in result)
def test_invalid_password_is_not_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
result = self.dispatcher.handle_request(u'password "secret"')
self.assert_(u'ACK [3@0] {password} incorrect password' in result)
def test_any_password_is_not_accepted_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None
result = self.dispatcher.handle_request(u'password "secret"')
self.assert_(u'ACK [3@0] {password} incorrect password' in result)
def test_ping(self):
result = self.dispatcher.handle_request(u'ping')
self.assert_(u'OK' in result)

View File

@ -1,11 +1,12 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.exceptions import MpdAckError
from mopidy.frontends.mpd.protocol import request_handlers, handle_request
from mopidy.mixers.dummy import DummyMixer
from tests import unittest
class MpdDispatcherTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()

View File

@ -1,8 +1,9 @@
import unittest
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdPermissionError,
MpdUnknownCommand, MpdSystemError, MpdNotImplemented)
from tests import unittest
class MpdExceptionsTest(unittest.TestCase):
def test_key_error_wrapped_in_mpd_ack_error(self):
try:

View File

@ -1,412 +0,0 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
class MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test_count(self):
result = self.dispatcher.handle_request(u'count "tag" "needle"')
self.assert_(u'songs: 0' in result)
self.assert_(u'playtime: 0' in result)
self.assert_(u'OK' in result)
def test_findadd(self):
result = self.dispatcher.handle_request(u'findadd "album" "what"')
self.assert_(u'OK' in result)
def test_listall(self):
result = self.dispatcher.handle_request(
u'listall "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_listallinfo(self):
result = self.dispatcher.handle_request(
u'listallinfo "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_lsinfo_without_path_returns_same_as_listplaylists(self):
lsinfo_result = self.dispatcher.handle_request(u'lsinfo')
listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
lsinfo_result = self.dispatcher.handle_request(u'lsinfo ""')
listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_lsinfo_for_root_returns_same_as_listplaylists(self):
lsinfo_result = self.dispatcher.handle_request(u'lsinfo "/"')
listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
self.assertEqual(lsinfo_result, listplaylists_result)
def test_update_without_uri(self):
result = self.dispatcher.handle_request(u'update')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_update_with_uri(self):
result = self.dispatcher.handle_request(u'update "file:///dev/urandom"')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_rescan_without_uri(self):
result = self.dispatcher.handle_request(u'rescan')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
def test_rescan_with_uri(self):
result = self.dispatcher.handle_request(u'rescan "file:///dev/urandom"')
self.assert_(u'OK' in result)
self.assert_(u'updating_db: 0' in result)
class MusicDatabaseFindTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test_find_album(self):
result = self.dispatcher.handle_request(u'find "album" "what"')
self.assert_(u'OK' in result)
def test_find_album_without_quotes(self):
result = self.dispatcher.handle_request(u'find album "what"')
self.assert_(u'OK' in result)
def test_find_artist(self):
result = self.dispatcher.handle_request(u'find "artist" "what"')
self.assert_(u'OK' in result)
def test_find_artist_without_quotes(self):
result = self.dispatcher.handle_request(u'find artist "what"')
self.assert_(u'OK' in result)
def test_find_title(self):
result = self.dispatcher.handle_request(u'find "title" "what"')
self.assert_(u'OK' in result)
def test_find_title_without_quotes(self):
result = self.dispatcher.handle_request(u'find title "what"')
self.assert_(u'OK' in result)
def test_find_date(self):
result = self.dispatcher.handle_request(u'find "date" "2002-01-01"')
self.assert_(u'OK' in result)
def test_find_date_without_quotes(self):
result = self.dispatcher.handle_request(u'find date "2002-01-01"')
self.assert_(u'OK' in result)
def test_find_date_with_capital_d_and_incomplete_date(self):
result = self.dispatcher.handle_request(u'find Date "2005"')
self.assert_(u'OK' in result)
def test_find_else_should_fail(self):
result = self.dispatcher.handle_request(u'find "somethingelse" "what"')
self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments')
def test_find_album_and_artist(self):
result = self.dispatcher.handle_request(
u'find album "album_what" artist "artist_what"')
self.assert_(u'OK' in result)
class MusicDatabaseListTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test_list_foo_returns_ack(self):
result = self.dispatcher.handle_request(u'list "foo"')
self.assertEqual(result[0],
u'ACK [2@0] {list} incorrect arguments')
### Artist
def test_list_artist_with_quotes(self):
result = self.dispatcher.handle_request(u'list "artist"')
self.assert_(u'OK' in result)
def test_list_artist_without_quotes(self):
result = self.dispatcher.handle_request(u'list artist')
self.assert_(u'OK' in result)
def test_list_artist_without_quotes_and_capitalized(self):
result = self.dispatcher.handle_request(u'list Artist')
self.assert_(u'OK' in result)
def test_list_artist_with_query_of_one_token(self):
result = self.dispatcher.handle_request(u'list "artist" "anartist"')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_artist_with_unknown_field_in_query_returns_ack(self):
result = self.dispatcher.handle_request(u'list "artist" "foo" "bar"')
self.assertEqual(result[0],
u'ACK [2@0] {list} not able to parse args')
def test_list_artist_by_artist(self):
result = self.dispatcher.handle_request(
u'list "artist" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_artist_by_album(self):
result = self.dispatcher.handle_request(
u'list "artist" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_artist_by_full_date(self):
result = self.dispatcher.handle_request(
u'list "artist" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_artist_by_year(self):
result = self.dispatcher.handle_request(
u'list "artist" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_artist_by_genre(self):
result = self.dispatcher.handle_request(
u'list "artist" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_artist_by_artist_and_album(self):
result = self.dispatcher.handle_request(
u'list "artist" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Album
def test_list_album_with_quotes(self):
result = self.dispatcher.handle_request(u'list "album"')
self.assert_(u'OK' in result)
def test_list_album_without_quotes(self):
result = self.dispatcher.handle_request(u'list album')
self.assert_(u'OK' in result)
def test_list_album_without_quotes_and_capitalized(self):
result = self.dispatcher.handle_request(u'list Album')
self.assert_(u'OK' in result)
def test_list_album_with_artist_name(self):
result = self.dispatcher.handle_request(u'list "album" "anartist"')
self.assert_(u'OK' in result)
def test_list_album_by_artist(self):
result = self.dispatcher.handle_request(
u'list "album" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_album_by_album(self):
result = self.dispatcher.handle_request(
u'list "album" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_album_by_full_date(self):
result = self.dispatcher.handle_request(
u'list "album" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_album_by_year(self):
result = self.dispatcher.handle_request(
u'list "album" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_album_by_genre(self):
result = self.dispatcher.handle_request(
u'list "album" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_album_by_artist_and_album(self):
result = self.dispatcher.handle_request(
u'list "album" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Date
def test_list_date_with_quotes(self):
result = self.dispatcher.handle_request(u'list "date"')
self.assert_(u'OK' in result)
def test_list_date_without_quotes(self):
result = self.dispatcher.handle_request(u'list date')
self.assert_(u'OK' in result)
def test_list_date_without_quotes_and_capitalized(self):
result = self.dispatcher.handle_request(u'list Date')
self.assert_(u'OK' in result)
def test_list_date_with_query_of_one_token(self):
result = self.dispatcher.handle_request(u'list "date" "anartist"')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_date_by_artist(self):
result = self.dispatcher.handle_request(
u'list "date" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_date_by_album(self):
result = self.dispatcher.handle_request(
u'list "date" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_date_by_full_date(self):
result = self.dispatcher.handle_request(
u'list "date" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_date_by_year(self):
result = self.dispatcher.handle_request(u'list "date" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_date_by_genre(self):
result = self.dispatcher.handle_request(u'list "date" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_date_by_artist_and_album(self):
result = self.dispatcher.handle_request(
u'list "date" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
### Genre
def test_list_genre_with_quotes(self):
result = self.dispatcher.handle_request(u'list "genre"')
self.assert_(u'OK' in result)
def test_list_genre_without_quotes(self):
result = self.dispatcher.handle_request(u'list genre')
self.assert_(u'OK' in result)
def test_list_genre_without_quotes_and_capitalized(self):
result = self.dispatcher.handle_request(u'list Genre')
self.assert_(u'OK' in result)
def test_list_genre_with_query_of_one_token(self):
result = self.dispatcher.handle_request(u'list "genre" "anartist"')
self.assertEqual(result[0],
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_genre_by_artist(self):
result = self.dispatcher.handle_request(
u'list "genre" "artist" "anartist"')
self.assert_(u'OK' in result)
def test_list_genre_by_album(self):
result = self.dispatcher.handle_request(
u'list "genre" "album" "analbum"')
self.assert_(u'OK' in result)
def test_list_genre_by_full_date(self):
result = self.dispatcher.handle_request(
u'list "genre" "date" "2001-01-01"')
self.assert_(u'OK' in result)
def test_list_genre_by_year(self):
result = self.dispatcher.handle_request(
u'list "genre" "date" "2001"')
self.assert_(u'OK' in result)
def test_list_genre_by_genre(self):
result = self.dispatcher.handle_request(
u'list "genre" "genre" "agenre"')
self.assert_(u'OK' in result)
def test_list_genre_by_artist_and_album(self):
result = self.dispatcher.handle_request(
u'list "genre" "artist" "anartist" "album" "analbum"')
self.assert_(u'OK' in result)
class MusicDatabaseSearchTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test_search_album(self):
result = self.dispatcher.handle_request(u'search "album" "analbum"')
self.assert_(u'OK' in result)
def test_search_album_without_quotes(self):
result = self.dispatcher.handle_request(u'search album "analbum"')
self.assert_(u'OK' in result)
def test_search_artist(self):
result = self.dispatcher.handle_request(u'search "artist" "anartist"')
self.assert_(u'OK' in result)
def test_search_artist_without_quotes(self):
result = self.dispatcher.handle_request(u'search artist "anartist"')
self.assert_(u'OK' in result)
def test_search_filename(self):
result = self.dispatcher.handle_request(
u'search "filename" "afilename"')
self.assert_(u'OK' in result)
def test_search_filename_without_quotes(self):
result = self.dispatcher.handle_request(u'search filename "afilename"')
self.assert_(u'OK' in result)
def test_search_title(self):
result = self.dispatcher.handle_request(u'search "title" "atitle"')
self.assert_(u'OK' in result)
def test_search_title_without_quotes(self):
result = self.dispatcher.handle_request(u'search title "atitle"')
self.assert_(u'OK' in result)
def test_search_any(self):
result = self.dispatcher.handle_request(u'search "any" "anything"')
self.assert_(u'OK' in result)
def test_search_any_without_quotes(self):
result = self.dispatcher.handle_request(u'search any "anything"')
self.assert_(u'OK' in result)
def test_search_date(self):
result = self.dispatcher.handle_request(u'search "date" "2002-01-01"')
self.assert_(u'OK' in result)
def test_search_date_without_quotes(self):
result = self.dispatcher.handle_request(u'search date "2002-01-01"')
self.assert_(u'OK' in result)
def test_search_date_with_capital_d_and_incomplete_date(self):
result = self.dispatcher.handle_request(u'search Date "2005"')
self.assert_(u'OK' in result)
def test_search_else_should_fail(self):
result = self.dispatcher.handle_request(
u'search "sometype" "something"')
self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')

View File

@ -0,0 +1,62 @@
import mock
from mopidy import settings
from mopidy.backends import dummy as backend
from mopidy.frontends import mpd
from mopidy.mixers import dummy as mixer
from tests import unittest
class MockConnection(mock.Mock):
def __init__(self, *args, **kwargs):
super(MockConnection, self).__init__(*args, **kwargs)
self.host = mock.sentinel.host
self.port = mock.sentinel.port
self.response = []
def queue_send(self, data):
lines = (line for line in data.split('\n') if line)
self.response.extend(lines)
class BaseTestCase(unittest.TestCase):
def setUp(self):
self.backend = backend.DummyBackend.start().proxy()
self.mixer = mixer.DummyMixer.start().proxy()
self.connection = MockConnection()
self.session = mpd.MpdSession(self.connection)
self.dispatcher = self.session.dispatcher
self.context = self.dispatcher.context
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
settings.runtime.clear()
def sendRequest(self, request):
self.connection.response = []
request = '%s\n' % request.encode('utf-8')
self.session.on_receive({'received': request})
return self.connection.response
def assertNoResponse(self):
self.assertEqual([], self.connection.response)
def assertInResponse(self, value):
self.assert_(value in self.connection.response, u'Did not find %s '
'in %s' % (repr(value), repr(self.connection.response)))
def assertOnceInResponse(self, value):
matched = len([r for r in self.connection.response if r == value])
self.assertEqual(1, matched, 'Expected to find %s once in %s' %
(repr(value), repr(self.connection.response)))
def assertNotInResponse(self, value):
self.assert_(value not in self.connection.response, u'Found %s in %s' %
(repr(value), repr(self.connection.response)))
def assertEqualResponse(self, value):
self.assertEqual(1, len(self.connection.response))
self.assertEqual(value, self.connection.response[0])

View File

@ -0,0 +1,18 @@
from tests.frontends.mpd import protocol
class AudioOutputHandlerTest(protocol.BaseTestCase):
def test_enableoutput(self):
self.sendRequest(u'enableoutput "0"')
self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_disableoutput(self):
self.sendRequest(u'disableoutput "0"')
self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_outputs(self):
self.sendRequest(u'outputs')
self.assertInResponse(u'outputid: 0')
self.assertInResponse(u'outputname: None')
self.assertInResponse(u'outputenabled: 1')
self.assertInResponse(u'OK')

View File

@ -1,63 +1,62 @@
import mock
import unittest
from mopidy import settings
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.session import MpdSession
class AuthenticationTest(unittest.TestCase):
def setUp(self):
self.session = mock.Mock(spec=MpdSession)
self.dispatcher = MpdDispatcher(session=self.session)
from tests.frontends.mpd import protocol
def tearDown(self):
settings.runtime.clear()
class AuthenticationTest(protocol.BaseTestCase):
def test_authentication_with_valid_password_is_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
response = self.dispatcher.handle_request(u'password "topsecret"')
self.sendRequest(u'password "topsecret"')
self.assertTrue(self.dispatcher.authenticated)
self.assert_(u'OK' in response)
self.assertInResponse(u'OK')
def test_authentication_with_invalid_password_is_not_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
response = self.dispatcher.handle_request(u'password "secret"')
self.sendRequest(u'password "secret"')
self.assertFalse(self.dispatcher.authenticated)
self.assert_(u'ACK [3@0] {password} incorrect password' in response)
self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
def test_authentication_with_anything_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None
response = self.dispatcher.handle_request(u'any request at all')
self.sendRequest(u'any request at all')
self.assertTrue(self.dispatcher.authenticated)
self.assert_('ACK [5@0] {} unknown command "any"' in response)
self.assertEqualResponse('ACK [5@0] {} unknown command "any"')
def test_anything_when_not_authenticated_should_fail(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
response = self.dispatcher.handle_request(u'any request at all')
self.sendRequest(u'any request at all')
self.assertFalse(self.dispatcher.authenticated)
self.assert_(
u'ACK [4@0] {any} you don\'t have permission for "any"' in response)
self.assertEqualResponse(
u'ACK [4@0] {any} you don\'t have permission for "any"')
def test_close_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
response = self.dispatcher.handle_request(u'close')
self.sendRequest(u'close')
self.assertFalse(self.dispatcher.authenticated)
self.assert_(u'OK' in response)
self.assertInResponse(u'OK')
def test_commands_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
response = self.dispatcher.handle_request(u'commands')
self.sendRequest(u'commands')
self.assertFalse(self.dispatcher.authenticated)
self.assert_(u'OK' in response)
self.assertInResponse(u'OK')
def test_notcommands_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
response = self.dispatcher.handle_request(u'notcommands')
self.sendRequest(u'notcommands')
self.assertFalse(self.dispatcher.authenticated)
self.assert_(u'OK' in response)
self.assertInResponse(u'OK')
def test_ping_is_allowed_without_authentication(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
response = self.dispatcher.handle_request(u'ping')
self.sendRequest(u'ping')
self.assertFalse(self.dispatcher.authenticated)
self.assert_(u'OK' in response)
self.assertInResponse(u'OK')

View File

@ -0,0 +1,54 @@
from tests.frontends.mpd import protocol
class CommandListsTest(protocol.BaseTestCase):
def test_command_list_begin(self):
response = self.sendRequest(u'command_list_begin')
self.assertEquals([], response)
def test_command_list_end(self):
self.sendRequest(u'command_list_begin')
self.sendRequest(u'command_list_end')
self.assertInResponse(u'OK')
def test_command_list_end_without_start_first_is_an_unknown_command(self):
self.sendRequest(u'command_list_end')
self.assertEqualResponse(
u'ACK [5@0] {} unknown command "command_list_end"')
def test_command_list_with_ping(self):
self.sendRequest(u'command_list_begin')
self.assertEqual([], self.dispatcher.command_list)
self.assertEqual(False, self.dispatcher.command_list_ok)
self.sendRequest(u'ping')
self.assert_(u'ping' in self.dispatcher.command_list)
self.sendRequest(u'command_list_end')
self.assertInResponse(u'OK')
self.assertEqual(False, self.dispatcher.command_list)
def test_command_list_with_error_returns_ack_with_correct_index(self):
self.sendRequest(u'command_list_begin')
self.sendRequest(u'play') # Known command
self.sendRequest(u'paly') # Unknown command
self.sendRequest(u'command_list_end')
self.assertEqualResponse(u'ACK [5@1] {} unknown command "paly"')
def test_command_list_ok_begin(self):
response = self.sendRequest(u'command_list_ok_begin')
self.assertEquals([], response)
def test_command_list_ok_with_ping(self):
self.sendRequest(u'command_list_ok_begin')
self.assertEqual([], self.dispatcher.command_list)
self.assertEqual(True, self.dispatcher.command_list_ok)
self.sendRequest(u'ping')
self.assert_(u'ping' in self.dispatcher.command_list)
self.sendRequest(u'command_list_end')
self.assertInResponse(u'list_OK')
self.assertInResponse(u'OK')
self.assertEqual(False, self.dispatcher.command_list)
self.assertEqual(False, self.dispatcher.command_list_ok)
# FIXME this should also include the special handling of idle within a
# command list. That is that once a idle/noidle command is found inside a
# commad list, the rest of the list seems to be ignored.

View File

@ -0,0 +1,44 @@
from mock import patch
from mopidy import settings
from tests.frontends.mpd import protocol
class ConnectionHandlerTest(protocol.BaseTestCase):
def test_close_closes_the_client_connection(self):
with patch.object(self.session, 'close') as close_mock:
response = self.sendRequest(u'close')
close_mock.assertEqualResponsecalled_once_with()
self.assertEqualResponse(u'OK')
def test_empty_request(self):
self.sendRequest(u'')
self.assertEqualResponse(u'OK')
self.sendRequest(u' ')
self.assertEqualResponse(u'OK')
def test_kill(self):
self.sendRequest(u'kill')
self.assertEqualResponse(
u'ACK [4@0] {kill} you don\'t have permission for "kill"')
def test_valid_password_is_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
self.sendRequest(u'password "topsecret"')
self.assertEqualResponse(u'OK')
def test_invalid_password_is_not_accepted(self):
settings.MPD_SERVER_PASSWORD = u'topsecret'
self.sendRequest(u'password "secret"')
self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
def test_any_password_is_not_accepted_when_password_check_turned_off(self):
settings.MPD_SERVER_PASSWORD = None
self.sendRequest(u'password "secret"')
self.assertEqualResponse(u'ACK [3@0] {password} incorrect password')
def test_ping(self):
self.sendRequest(u'ping')
self.assertEqualResponse(u'OK')

View File

@ -1,20 +1,9 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class CurrentPlaylistHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
from tests.frontends.mpd import protocol
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
def test_add(self):
needle = Track(uri='dummy://foo')
self.backend.library.provider.dummy_library = [
@ -22,21 +11,21 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'add "dummy://foo"')
self.assertEqual(len(result), 1)
self.assertEqual(result[0], u'OK')
self.sendRequest(u'add "dummy://foo"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
self.assertEqualResponse(u'OK')
def test_add_with_uri_not_found_in_library_should_ack(self):
result = self.dispatcher.handle_request(u'add "dummy://foo"')
self.assertEqual(result[0],
self.sendRequest(u'add "dummy://foo"')
self.assertEqualResponse(
u'ACK [50@0] {add} directory or file not found')
def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self):
result = self.dispatcher.handle_request(u'add ""')
self.sendRequest(u'add ""')
# TODO check that we add all tracks (we currently don't)
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_addid_without_songpos(self):
needle = Track(uri='dummy://foo')
@ -45,16 +34,17 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'addid "dummy://foo"')
self.sendRequest(u'addid "dummy://foo"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
self.assert_(u'Id: %d' %
self.backend.current_playlist.cp_tracks.get()[5][0] in result)
self.assert_(u'OK' in result)
self.assertInResponse(u'Id: %d' %
self.backend.current_playlist.cp_tracks.get()[5][0])
self.assertInResponse(u'OK')
def test_addid_with_empty_uri_acks(self):
result = self.dispatcher.handle_request(u'addid ""')
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
self.sendRequest(u'addid ""')
self.assertEqualResponse(u'ACK [50@0] {addid} No such song')
def test_addid_with_songpos(self):
needle = Track(uri='dummy://foo')
@ -63,12 +53,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'addid "dummy://foo" "3"')
self.sendRequest(u'addid "dummy://foo" "3"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
self.assertEqual(self.backend.current_playlist.tracks.get()[3], needle)
self.assert_(u'Id: %d' %
self.backend.current_playlist.cp_tracks.get()[3][0] in result)
self.assert_(u'OK' in result)
self.assertInResponse(u'Id: %d' %
self.backend.current_playlist.cp_tracks.get()[3][0])
self.assertInResponse(u'OK')
def test_addid_with_songpos_out_of_bounds_should_ack(self):
needle = Track(uri='dummy://foo')
@ -77,83 +68,93 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'addid "dummy://foo" "6"')
self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index')
self.sendRequest(u'addid "dummy://foo" "6"')
self.assertEqualResponse(u'ACK [2@0] {addid} Bad song index')
def test_addid_with_uri_not_found_in_library_should_ack(self):
result = self.dispatcher.handle_request(u'addid "dummy://foo"')
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
self.sendRequest(u'addid "dummy://foo"')
self.assertEqualResponse(u'ACK [50@0] {addid} No such song')
def test_clear(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'clear')
self.sendRequest(u'clear')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
self.assertEqual(self.backend.playback.current_track.get(), None)
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_delete_songpos(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'delete "%d"' %
self.sendRequest(u'delete "%d"' %
self.backend.current_playlist.cp_tracks.get()[2][0])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 4)
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_delete_songpos_out_of_bounds(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'delete "5"')
self.sendRequest(u'delete "5"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index')
def test_delete_open_range(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'delete "1:"')
self.sendRequest(u'delete "1:"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_delete_closed_range(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'delete "1:3"')
self.sendRequest(u'delete "1:3"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 3)
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_delete_range_out_of_bounds(self):
self.backend.current_playlist.append(
[Track(), Track(), Track(), Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
result = self.dispatcher.handle_request(u'delete "5:7"')
self.sendRequest(u'delete "5:7"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index')
def test_deleteid(self):
self.backend.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
result = self.dispatcher.handle_request(u'deleteid "1"')
self.sendRequest(u'deleteid "1"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_deleteid_does_not_exist(self):
self.backend.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
result = self.dispatcher.handle_request(u'deleteid "12345"')
self.sendRequest(u'deleteid "12345"')
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
self.assertEqualResponse(u'ACK [50@0] {deleteid} No such song')
def test_move_songpos(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'move "1" "0"')
self.sendRequest(u'move "1" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'a')
@ -161,14 +162,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_move_open_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'move "2:" "0"')
self.sendRequest(u'move "2:" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'c')
self.assertEqual(tracks[1].name, 'd')
@ -176,14 +178,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'f')
self.assertEqual(tracks[4].name, 'a')
self.assertEqual(tracks[5].name, 'b')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_move_closed_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'move "1:3" "0"')
self.sendRequest(u'move "1:3" "0"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'b')
self.assertEqual(tracks[1].name, 'c')
@ -191,14 +194,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_moveid(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'moveid "4" "2"')
self.sendRequest(u'moveid "4" "2"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b')
@ -206,179 +210,182 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'c')
self.assertEqual(tracks[4].name, 'd')
self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_playlist_returns_same_as_playlistinfo(self):
playlist_result = self.dispatcher.handle_request(u'playlist')
playlistinfo_result = self.dispatcher.handle_request(u'playlistinfo')
self.assertEqual(playlist_result, playlistinfo_result)
playlist_response = self.sendRequest(u'playlist')
playlistinfo_response = self.sendRequest(u'playlistinfo')
self.assertEqual(playlist_response, playlistinfo_response)
def test_playlistfind(self):
result = self.dispatcher.handle_request(u'playlistfind "tag" "needle"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.sendRequest(u'playlistfind "tag" "needle"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistfind_by_filename_not_in_current_playlist(self):
result = self.dispatcher.handle_request(
u'playlistfind "filename" "file:///dev/null"')
self.assertEqual(len(result), 1)
self.assert_(u'OK' in result)
self.sendRequest(u'playlistfind "filename" "file:///dev/null"')
self.assertEqualResponse(u'OK')
def test_playlistfind_by_filename_without_quotes(self):
result = self.dispatcher.handle_request(
u'playlistfind filename "file:///dev/null"')
self.assertEqual(len(result), 1)
self.assert_(u'OK' in result)
self.sendRequest(u'playlistfind filename "file:///dev/null"')
self.assertEqualResponse(u'OK')
def test_playlistfind_by_filename_in_current_playlist(self):
self.backend.current_playlist.append([
Track(uri='file:///exists')])
result = self.dispatcher.handle_request(
u'playlistfind filename "file:///exists"')
self.assert_(u'file: file:///exists' in result)
self.assert_(u'Id: 0' in result)
self.assert_(u'Pos: 0' in result)
self.assert_(u'OK' in result)
self.sendRequest( u'playlistfind filename "file:///exists"')
self.assertInResponse(u'file: file:///exists')
self.assertInResponse(u'Id: 0')
self.assertInResponse(u'Pos: 0')
self.assertInResponse(u'OK')
def test_playlistid_without_songid(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.dispatcher.handle_request(u'playlistid')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'OK' in result)
self.sendRequest(u'playlistid')
self.assertInResponse(u'Title: a')
self.assertInResponse(u'Title: b')
self.assertInResponse(u'OK')
def test_playlistid_with_songid(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.dispatcher.handle_request(u'playlistid "1"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Id: 0' not in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Id: 1' in result)
self.assert_(u'OK' in result)
self.sendRequest(u'playlistid "1"')
self.assertNotInResponse(u'Title: a')
self.assertNotInResponse(u'Id: 0')
self.assertInResponse(u'Title: b')
self.assertInResponse(u'Id: 1')
self.assertInResponse(u'OK')
def test_playlistid_with_not_existing_songid_fails(self):
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
result = self.dispatcher.handle_request(u'playlistid "25"')
self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song')
self.sendRequest(u'playlistid "25"')
self.assertEqualResponse(u'ACK [50@0] {playlistid} No such song')
def test_playlistinfo_without_songpos_or_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'playlistinfo')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result)
self.assert_(u'Title: d' in result)
self.assert_(u'Title: e' in result)
self.assert_(u'Title: f' in result)
self.assert_(u'OK' in result)
self.sendRequest(u'playlistinfo')
self.assertInResponse(u'Title: a')
self.assertInResponse(u'Title: b')
self.assertInResponse(u'Title: c')
self.assertInResponse(u'Title: d')
self.assertInResponse(u'Title: e')
self.assertInResponse(u'Title: f')
self.assertInResponse(u'OK')
def test_playlistinfo_with_songpos(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'playlistinfo "4"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' not in result)
self.assert_(u'Title: d' not in result)
self.assert_(u'Title: e' in result)
self.assert_(u'Title: f' not in result)
self.assert_(u'OK' in result)
self.sendRequest(u'playlistinfo "4"')
self.assertNotInResponse(u'Title: a')
self.assertNotInResponse(u'Title: b')
self.assertNotInResponse(u'Title: c')
self.assertNotInResponse(u'Title: d')
self.assertInResponse(u'Title: e')
self.assertNotInResponse(u'Title: f')
self.assertInResponse(u'OK')
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
result1 = self.dispatcher.handle_request(u'playlistinfo "-1"')
result2 = self.dispatcher.handle_request(u'playlistinfo')
self.assertEqual(result1, result2)
response1 = self.sendRequest(u'playlistinfo "-1"')
response2 = self.sendRequest(u'playlistinfo')
self.assertEqual(response1, response2)
def test_playlistinfo_with_open_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'playlistinfo "2:"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' in result)
self.assert_(u'Title: d' in result)
self.assert_(u'Title: e' in result)
self.assert_(u'Title: f' in result)
self.assert_(u'OK' in result)
self.sendRequest(u'playlistinfo "2:"')
self.assertNotInResponse(u'Title: a')
self.assertNotInResponse(u'Title: b')
self.assertInResponse(u'Title: c')
self.assertInResponse(u'Title: d')
self.assertInResponse(u'Title: e')
self.assertInResponse(u'Title: f')
self.assertInResponse(u'OK')
def test_playlistinfo_with_closed_range(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'playlistinfo "2:4"')
self.assert_(u'Title: a' not in result)
self.assert_(u'Title: b' not in result)
self.assert_(u'Title: c' in result)
self.assert_(u'Title: d' in result)
self.assert_(u'Title: e' not in result)
self.assert_(u'Title: f' not in result)
self.assert_(u'OK' in result)
self.sendRequest(u'playlistinfo "2:4"')
self.assertNotInResponse(u'Title: a')
self.assertNotInResponse(u'Title: b')
self.assertInResponse(u'Title: c')
self.assertInResponse(u'Title: d')
self.assertNotInResponse(u'Title: e')
self.assertNotInResponse(u'Title: f')
self.assertInResponse(u'OK')
def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self):
result = self.dispatcher.handle_request(u'playlistinfo "10:20"')
self.assert_(u'ACK [2@0] {playlistinfo} Bad song index' in result)
self.sendRequest(u'playlistinfo "10:20"')
self.assertEqualResponse(u'ACK [2@0] {playlistinfo} Bad song index')
def test_playlistinfo_with_too_high_end_of_range_returns_ok(self):
result = self.dispatcher.handle_request(u'playlistinfo "0:20"')
self.assert_(u'OK' in result)
self.sendRequest(u'playlistinfo "0:20"')
self.assertInResponse(u'OK')
def test_playlistsearch(self):
result = self.dispatcher.handle_request(
u'playlistsearch "any" "needle"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.sendRequest( u'playlistsearch "any" "needle"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistsearch_without_quotes(self):
result = self.dispatcher.handle_request(u'playlistsearch any "needle"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.sendRequest(u'playlistsearch any "needle"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_plchanges(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
result = self.dispatcher.handle_request(u'plchanges "0"')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result)
self.sendRequest(u'plchanges "0"')
self.assertInResponse(u'Title: a')
self.assertInResponse(u'Title: b')
self.assertInResponse(u'Title: c')
self.assertInResponse(u'OK')
def test_plchanges_with_minus_one_returns_entire_playlist(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
result = self.dispatcher.handle_request(u'plchanges "-1"')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result)
self.sendRequest(u'plchanges "-1"')
self.assertInResponse(u'Title: a')
self.assertInResponse(u'Title: b')
self.assertInResponse(u'Title: c')
self.assertInResponse(u'OK')
def test_plchanges_without_quotes_works(self):
self.backend.current_playlist.append(
[Track(name='a'), Track(name='b'), Track(name='c')])
result = self.dispatcher.handle_request(u'plchanges 0')
self.assert_(u'Title: a' in result)
self.assert_(u'Title: b' in result)
self.assert_(u'Title: c' in result)
self.assert_(u'OK' in result)
self.sendRequest(u'plchanges 0')
self.assertInResponse(u'Title: a')
self.assertInResponse(u'Title: b')
self.assertInResponse(u'Title: c')
self.assertInResponse(u'OK')
def test_plchangesposid(self):
self.backend.current_playlist.append([Track(), Track(), Track()])
result = self.dispatcher.handle_request(u'plchangesposid "0"')
self.sendRequest(u'plchangesposid "0"')
cp_tracks = self.backend.current_playlist.cp_tracks.get()
self.assert_(u'cpos: 0' in result)
self.assert_(u'Id: %d' % cp_tracks[0][0]
in result)
self.assert_(u'cpos: 2' in result)
self.assert_(u'Id: %d' % cp_tracks[1][0]
in result)
self.assert_(u'cpos: 2' in result)
self.assert_(u'Id: %d' % cp_tracks[2][0]
in result)
self.assert_(u'OK' in result)
self.assertInResponse(u'cpos: 0')
self.assertInResponse(u'Id: %d' % cp_tracks[0][0])
self.assertInResponse(u'cpos: 2')
self.assertInResponse(u'Id: %d' % cp_tracks[1][0])
self.assertInResponse(u'cpos: 2')
self.assertInResponse(u'Id: %d' % cp_tracks[2][0])
self.assertInResponse(u'OK')
def test_shuffle_without_range(self):
self.backend.current_playlist.append([
@ -386,9 +393,10 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
result = self.dispatcher.handle_request(u'shuffle')
self.sendRequest(u'shuffle')
self.assert_(version < self.backend.current_playlist.version.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_shuffle_with_open_range(self):
self.backend.current_playlist.append([
@ -396,14 +404,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
result = self.dispatcher.handle_request(u'shuffle "4:"')
self.sendRequest(u'shuffle "4:"')
self.assert_(version < self.backend.current_playlist.version.get())
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'b')
self.assertEqual(tracks[2].name, 'c')
self.assertEqual(tracks[3].name, 'd')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_shuffle_with_closed_range(self):
self.backend.current_playlist.append([
@ -411,21 +420,23 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='d'), Track(name='e'), Track(name='f'),
])
version = self.backend.current_playlist.version.get()
result = self.dispatcher.handle_request(u'shuffle "1:3"')
self.sendRequest(u'shuffle "1:3"')
self.assert_(version < self.backend.current_playlist.version.get())
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'e')
self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_swap(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'swap "1" "4"')
self.sendRequest(u'swap "1" "4"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e')
@ -433,14 +444,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'b')
self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_swapid(self):
self.backend.current_playlist.append([
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
result = self.dispatcher.handle_request(u'swapid "1" "4"')
self.sendRequest(u'swapid "1" "4"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(tracks[0].name, 'a')
self.assertEqual(tracks[1].name, 'e')
@ -448,4 +460,4 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
self.assertEqual(tracks[3].name, 'd')
self.assertEqual(tracks[4].name, 'b')
self.assertEqual(tracks[5].name, 'f')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')

View File

@ -0,0 +1,206 @@
from mock import patch
from mopidy.frontends.mpd.protocol.status import SUBSYSTEMS
from tests.frontends.mpd import protocol
class IdleHandlerTest(protocol.BaseTestCase):
def idleEvent(self, subsystem):
self.session.on_idle(subsystem)
def assertEqualEvents(self, events):
self.assertEqual(set(events), self.context.events)
def assertEqualSubscriptions(self, events):
self.assertEqual(set(events), self.context.subscriptions)
def assertNoEvents(self):
self.assertEqualEvents([])
def assertNoSubscriptions(self):
self.assertEqualSubscriptions([])
def test_base_state(self):
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertNoResponse()
def test_idle(self):
self.sendRequest(u'idle')
self.assertEqualSubscriptions(SUBSYSTEMS)
self.assertNoEvents()
self.assertNoResponse()
def test_idle_disables_timeout(self):
self.sendRequest(u'idle')
self.connection.disable_timeout.assert_called_once_with()
def test_noidle(self):
self.sendRequest(u'noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player(self):
self.sendRequest(u'idle player')
self.assertEqualSubscriptions(['player'])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player_playlist(self):
self.sendRequest(u'idle player playlist')
self.assertEqualSubscriptions(['player', 'playlist'])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_then_noidle(self):
self.sendRequest(u'idle')
self.sendRequest(u'noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse(u'OK')
def test_idle_then_noidle_enables_timeout(self):
self.sendRequest(u'idle')
self.sendRequest(u'noidle')
self.connection.enable_timeout.assert_called_once_with()
def test_idle_then_play(self):
with patch.object(self.session, 'stop') as stop_mock:
self.sendRequest(u'idle')
self.sendRequest(u'play')
stop_mock.assert_called_once_with()
def test_idle_then_idle(self):
with patch.object(self.session, 'stop') as stop_mock:
self.sendRequest(u'idle')
self.sendRequest(u'idle')
stop_mock.assert_called_once_with()
def test_idle_player_then_play(self):
with patch.object(self.session, 'stop') as stop_mock:
self.sendRequest(u'idle player')
self.sendRequest(u'play')
stop_mock.assert_called_once_with()
def test_idle_then_player(self):
self.sendRequest(u'idle')
self.idleEvent(u'player')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse(u'changed: player')
self.assertOnceInResponse(u'OK')
def test_idle_player_then_event_player(self):
self.sendRequest(u'idle player')
self.idleEvent(u'player')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse(u'changed: player')
self.assertOnceInResponse(u'OK')
def test_idle_player_then_noidle(self):
self.sendRequest(u'idle player')
self.sendRequest(u'noidle')
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse(u'OK')
def test_idle_player_playlist_then_noidle(self):
self.sendRequest(u'idle player playlist')
self.sendRequest(u'noidle')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'OK')
def test_idle_player_playlist_then_player(self):
self.sendRequest(u'idle player playlist')
self.idleEvent(u'player')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'changed: player')
self.assertNotInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')
def test_idle_playlist_then_player(self):
self.sendRequest(u'idle playlist')
self.idleEvent(u'player')
self.assertEqualEvents(['player'])
self.assertEqualSubscriptions(['playlist'])
self.assertNoResponse()
def test_idle_playlist_then_player_then_playlist(self):
self.sendRequest(u'idle playlist')
self.idleEvent(u'player')
self.idleEvent(u'playlist')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertNotInResponse(u'changed: player')
self.assertOnceInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')
def test_player(self):
self.idleEvent(u'player')
self.assertEqualEvents(['player'])
self.assertNoSubscriptions()
self.assertNoResponse()
def test_player_then_idle_player(self):
self.idleEvent(u'player')
self.sendRequest(u'idle player')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'changed: player')
self.assertNotInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')
def test_player_then_playlist(self):
self.idleEvent(u'player')
self.idleEvent(u'playlist')
self.assertEqualEvents(['player', 'playlist'])
self.assertNoSubscriptions()
self.assertNoResponse()
def test_player_then_idle(self):
self.idleEvent(u'player')
self.sendRequest(u'idle')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'changed: player')
self.assertOnceInResponse(u'OK')
def test_player_then_playlist_then_idle(self):
self.idleEvent(u'player')
self.idleEvent(u'playlist')
self.sendRequest(u'idle')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'changed: player')
self.assertOnceInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')
def test_player_then_idle_playlist(self):
self.idleEvent(u'player')
self.sendRequest(u'idle playlist')
self.assertEqualEvents(['player'])
self.assertEqualSubscriptions(['playlist'])
self.assertNoResponse()
def test_player_then_idle_playlist_then_noidle(self):
self.idleEvent(u'player')
self.sendRequest(u'idle playlist')
self.sendRequest(u'noidle')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse(u'OK')
def test_player_then_playlist_then_idle_playlist(self):
self.idleEvent(u'player')
self.idleEvent(u'playlist')
self.sendRequest(u'idle playlist')
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertNotInResponse(u'changed: player')
self.assertOnceInResponse(u'changed: playlist')
self.assertOnceInResponse(u'OK')

View File

@ -0,0 +1,344 @@
from tests.frontends.mpd import protocol
class MusicDatabaseHandlerTest(protocol.BaseTestCase):
def test_count(self):
self.sendRequest(u'count "tag" "needle"')
self.assertInResponse(u'songs: 0')
self.assertInResponse(u'playtime: 0')
self.assertInResponse(u'OK')
def test_findadd(self):
self.sendRequest(u'findadd "album" "what"')
self.assertInResponse(u'OK')
def test_listall(self):
self.sendRequest(u'listall "file:///dev/urandom"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_listallinfo(self):
self.sendRequest(u'listallinfo "file:///dev/urandom"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_lsinfo_without_path_returns_same_as_listplaylists(self):
lsinfo_response = self.sendRequest(u'lsinfo')
listplaylists_response = self.sendRequest(u'listplaylists')
self.assertEqual(lsinfo_response, listplaylists_response)
def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
lsinfo_response = self.sendRequest(u'lsinfo ""')
listplaylists_response = self.sendRequest(u'listplaylists')
self.assertEqual(lsinfo_response, listplaylists_response)
def test_lsinfo_for_root_returns_same_as_listplaylists(self):
lsinfo_response = self.sendRequest(u'lsinfo "/"')
listplaylists_response = self.sendRequest(u'listplaylists')
self.assertEqual(lsinfo_response, listplaylists_response)
def test_update_without_uri(self):
self.sendRequest(u'update')
self.assertInResponse(u'updating_db: 0')
self.assertInResponse(u'OK')
def test_update_with_uri(self):
self.sendRequest(u'update "file:///dev/urandom"')
self.assertInResponse(u'updating_db: 0')
self.assertInResponse(u'OK')
def test_rescan_without_uri(self):
self.sendRequest(u'rescan')
self.assertInResponse(u'updating_db: 0')
self.assertInResponse(u'OK')
def test_rescan_with_uri(self):
self.sendRequest(u'rescan "file:///dev/urandom"')
self.assertInResponse(u'updating_db: 0')
self.assertInResponse(u'OK')
class MusicDatabaseFindTest(protocol.BaseTestCase):
def test_find_album(self):
self.sendRequest(u'find "album" "what"')
self.assertInResponse(u'OK')
def test_find_album_without_quotes(self):
self.sendRequest(u'find album "what"')
self.assertInResponse(u'OK')
def test_find_artist(self):
self.sendRequest(u'find "artist" "what"')
self.assertInResponse(u'OK')
def test_find_artist_without_quotes(self):
self.sendRequest(u'find artist "what"')
self.assertInResponse(u'OK')
def test_find_title(self):
self.sendRequest(u'find "title" "what"')
self.assertInResponse(u'OK')
def test_find_title_without_quotes(self):
self.sendRequest(u'find title "what"')
self.assertInResponse(u'OK')
def test_find_date(self):
self.sendRequest(u'find "date" "2002-01-01"')
self.assertInResponse(u'OK')
def test_find_date_without_quotes(self):
self.sendRequest(u'find date "2002-01-01"')
self.assertInResponse(u'OK')
def test_find_date_with_capital_d_and_incomplete_date(self):
self.sendRequest(u'find Date "2005"')
self.assertInResponse(u'OK')
def test_find_else_should_fail(self):
self.sendRequest(u'find "somethingelse" "what"')
self.assertEqualResponse(u'ACK [2@0] {find} incorrect arguments')
def test_find_album_and_artist(self):
self.sendRequest(u'find album "album_what" artist "artist_what"')
self.assertInResponse(u'OK')
class MusicDatabaseListTest(protocol.BaseTestCase):
def test_list_foo_returns_ack(self):
self.sendRequest(u'list "foo"')
self.assertEqualResponse(u'ACK [2@0] {list} incorrect arguments')
### Artist
def test_list_artist_with_quotes(self):
self.sendRequest(u'list "artist"')
self.assertInResponse(u'OK')
def test_list_artist_without_quotes(self):
self.sendRequest(u'list artist')
self.assertInResponse(u'OK')
def test_list_artist_without_quotes_and_capitalized(self):
self.sendRequest(u'list Artist')
self.assertInResponse(u'OK')
def test_list_artist_with_query_of_one_token(self):
self.sendRequest(u'list "artist" "anartist"')
self.assertEqualResponse(
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_artist_with_unknown_field_in_query_returns_ack(self):
self.sendRequest(u'list "artist" "foo" "bar"')
self.assertEqualResponse(u'ACK [2@0] {list} not able to parse args')
def test_list_artist_by_artist(self):
self.sendRequest(u'list "artist" "artist" "anartist"')
self.assertInResponse(u'OK')
def test_list_artist_by_album(self):
self.sendRequest(u'list "artist" "album" "analbum"')
self.assertInResponse(u'OK')
def test_list_artist_by_full_date(self):
self.sendRequest(u'list "artist" "date" "2001-01-01"')
self.assertInResponse(u'OK')
def test_list_artist_by_year(self):
self.sendRequest(u'list "artist" "date" "2001"')
self.assertInResponse(u'OK')
def test_list_artist_by_genre(self):
self.sendRequest(u'list "artist" "genre" "agenre"')
self.assertInResponse(u'OK')
def test_list_artist_by_artist_and_album(self):
self.sendRequest(
u'list "artist" "artist" "anartist" "album" "analbum"')
self.assertInResponse(u'OK')
### Album
def test_list_album_with_quotes(self):
self.sendRequest(u'list "album"')
self.assertInResponse(u'OK')
def test_list_album_without_quotes(self):
self.sendRequest(u'list album')
self.assertInResponse(u'OK')
def test_list_album_without_quotes_and_capitalized(self):
self.sendRequest(u'list Album')
self.assertInResponse(u'OK')
def test_list_album_with_artist_name(self):
self.sendRequest(u'list "album" "anartist"')
self.assertInResponse(u'OK')
def test_list_album_by_artist(self):
self.sendRequest(u'list "album" "artist" "anartist"')
self.assertInResponse(u'OK')
def test_list_album_by_album(self):
self.sendRequest(u'list "album" "album" "analbum"')
self.assertInResponse(u'OK')
def test_list_album_by_full_date(self):
self.sendRequest(u'list "album" "date" "2001-01-01"')
self.assertInResponse(u'OK')
def test_list_album_by_year(self):
self.sendRequest(u'list "album" "date" "2001"')
self.assertInResponse(u'OK')
def test_list_album_by_genre(self):
self.sendRequest(u'list "album" "genre" "agenre"')
self.assertInResponse(u'OK')
def test_list_album_by_artist_and_album(self):
self.sendRequest(
u'list "album" "artist" "anartist" "album" "analbum"')
self.assertInResponse(u'OK')
### Date
def test_list_date_with_quotes(self):
self.sendRequest(u'list "date"')
self.assertInResponse(u'OK')
def test_list_date_without_quotes(self):
self.sendRequest(u'list date')
self.assertInResponse(u'OK')
def test_list_date_without_quotes_and_capitalized(self):
self.sendRequest(u'list Date')
self.assertInResponse(u'OK')
def test_list_date_with_query_of_one_token(self):
self.sendRequest(u'list "date" "anartist"')
self.assertEqualResponse(
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_date_by_artist(self):
self.sendRequest(u'list "date" "artist" "anartist"')
self.assertInResponse(u'OK')
def test_list_date_by_album(self):
self.sendRequest(u'list "date" "album" "analbum"')
self.assertInResponse(u'OK')
def test_list_date_by_full_date(self):
self.sendRequest(u'list "date" "date" "2001-01-01"')
self.assertInResponse(u'OK')
def test_list_date_by_year(self):
self.sendRequest(u'list "date" "date" "2001"')
self.assertInResponse(u'OK')
def test_list_date_by_genre(self):
self.sendRequest(u'list "date" "genre" "agenre"')
self.assertInResponse(u'OK')
def test_list_date_by_artist_and_album(self):
self.sendRequest(u'list "date" "artist" "anartist" "album" "analbum"')
self.assertInResponse(u'OK')
### Genre
def test_list_genre_with_quotes(self):
self.sendRequest(u'list "genre"')
self.assertInResponse(u'OK')
def test_list_genre_without_quotes(self):
self.sendRequest(u'list genre')
self.assertInResponse(u'OK')
def test_list_genre_without_quotes_and_capitalized(self):
self.sendRequest(u'list Genre')
self.assertInResponse(u'OK')
def test_list_genre_with_query_of_one_token(self):
self.sendRequest(u'list "genre" "anartist"')
self.assertEqualResponse(
u'ACK [2@0] {list} should be "Album" for 3 arguments')
def test_list_genre_by_artist(self):
self.sendRequest(u'list "genre" "artist" "anartist"')
self.assertInResponse(u'OK')
def test_list_genre_by_album(self):
self.sendRequest(u'list "genre" "album" "analbum"')
self.assertInResponse(u'OK')
def test_list_genre_by_full_date(self):
self.sendRequest(u'list "genre" "date" "2001-01-01"')
self.assertInResponse(u'OK')
def test_list_genre_by_year(self):
self.sendRequest(u'list "genre" "date" "2001"')
self.assertInResponse(u'OK')
def test_list_genre_by_genre(self):
self.sendRequest(u'list "genre" "genre" "agenre"')
self.assertInResponse(u'OK')
def test_list_genre_by_artist_and_album(self):
self.sendRequest(
u'list "genre" "artist" "anartist" "album" "analbum"')
self.assertInResponse(u'OK')
class MusicDatabaseSearchTest(protocol.BaseTestCase):
def test_search_album(self):
self.sendRequest(u'search "album" "analbum"')
self.assertInResponse(u'OK')
def test_search_album_without_quotes(self):
self.sendRequest(u'search album "analbum"')
self.assertInResponse(u'OK')
def test_search_artist(self):
self.sendRequest(u'search "artist" "anartist"')
self.assertInResponse(u'OK')
def test_search_artist_without_quotes(self):
self.sendRequest(u'search artist "anartist"')
self.assertInResponse(u'OK')
def test_search_filename(self):
self.sendRequest(u'search "filename" "afilename"')
self.assertInResponse(u'OK')
def test_search_filename_without_quotes(self):
self.sendRequest(u'search filename "afilename"')
self.assertInResponse(u'OK')
def test_search_title(self):
self.sendRequest(u'search "title" "atitle"')
self.assertInResponse(u'OK')
def test_search_title_without_quotes(self):
self.sendRequest(u'search title "atitle"')
self.assertInResponse(u'OK')
def test_search_any(self):
self.sendRequest(u'search "any" "anything"')
self.assertInResponse(u'OK')
def test_search_any_without_quotes(self):
self.sendRequest(u'search any "anything"')
self.assertInResponse(u'OK')
def test_search_date(self):
self.sendRequest(u'search "date" "2002-01-01"')
self.assertInResponse(u'OK')
def test_search_date_without_quotes(self):
self.sendRequest(u'search date "2002-01-01"')
self.assertInResponse(u'OK')
def test_search_date_with_capital_d_and_incomplete_date(self):
self.sendRequest(u'search Date "2005"')
self.assertInResponse(u'OK')
def test_search_else_should_fail(self):
self.sendRequest(u'search "sometype" "something"')
self.assertEqualResponse(u'ACK [2@0] {search} incorrect arguments')

View File

@ -1,247 +1,238 @@
import unittest
from mopidy.backends.base import PlaybackController
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.backends import base as backend
from mopidy.models import Track
from tests import SkipTest
from tests import unittest
from tests.frontends.mpd import protocol
PAUSED = PlaybackController.PAUSED
PLAYING = PlaybackController.PLAYING
STOPPED = PlaybackController.STOPPED
PAUSED = backend.PlaybackController.PAUSED
PLAYING = backend.PlaybackController.PLAYING
STOPPED = backend.PlaybackController.STOPPED
class PlaybackOptionsHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_consume_off(self):
result = self.dispatcher.handle_request(u'consume "0"')
self.sendRequest(u'consume "0"')
self.assertFalse(self.backend.playback.consume.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_consume_off_without_quotes(self):
result = self.dispatcher.handle_request(u'consume 0')
self.sendRequest(u'consume 0')
self.assertFalse(self.backend.playback.consume.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_consume_on(self):
result = self.dispatcher.handle_request(u'consume "1"')
self.sendRequest(u'consume "1"')
self.assertTrue(self.backend.playback.consume.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_consume_on_without_quotes(self):
result = self.dispatcher.handle_request(u'consume 1')
self.sendRequest(u'consume 1')
self.assertTrue(self.backend.playback.consume.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_crossfade(self):
result = self.dispatcher.handle_request(u'crossfade "10"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.sendRequest(u'crossfade "10"')
self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_random_off(self):
result = self.dispatcher.handle_request(u'random "0"')
self.sendRequest(u'random "0"')
self.assertFalse(self.backend.playback.random.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_random_off_without_quotes(self):
result = self.dispatcher.handle_request(u'random 0')
self.sendRequest(u'random 0')
self.assertFalse(self.backend.playback.random.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_random_on(self):
result = self.dispatcher.handle_request(u'random "1"')
self.sendRequest(u'random "1"')
self.assertTrue(self.backend.playback.random.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_random_on_without_quotes(self):
result = self.dispatcher.handle_request(u'random 1')
self.sendRequest(u'random 1')
self.assertTrue(self.backend.playback.random.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_repeat_off(self):
result = self.dispatcher.handle_request(u'repeat "0"')
self.sendRequest(u'repeat "0"')
self.assertFalse(self.backend.playback.repeat.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_repeat_off_without_quotes(self):
result = self.dispatcher.handle_request(u'repeat 0')
self.sendRequest(u'repeat 0')
self.assertFalse(self.backend.playback.repeat.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_repeat_on(self):
result = self.dispatcher.handle_request(u'repeat "1"')
self.sendRequest(u'repeat "1"')
self.assertTrue(self.backend.playback.repeat.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_repeat_on_without_quotes(self):
result = self.dispatcher.handle_request(u'repeat 1')
self.sendRequest(u'repeat 1')
self.assertTrue(self.backend.playback.repeat.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_setvol_below_min(self):
result = self.dispatcher.handle_request(u'setvol "-10"')
self.assert_(u'OK' in result)
self.sendRequest(u'setvol "-10"')
self.assertEqual(0, self.mixer.volume.get())
self.assertInResponse(u'OK')
def test_setvol_min(self):
result = self.dispatcher.handle_request(u'setvol "0"')
self.assert_(u'OK' in result)
self.sendRequest(u'setvol "0"')
self.assertEqual(0, self.mixer.volume.get())
self.assertInResponse(u'OK')
def test_setvol_middle(self):
result = self.dispatcher.handle_request(u'setvol "50"')
self.assert_(u'OK' in result)
self.sendRequest(u'setvol "50"')
self.assertEqual(50, self.mixer.volume.get())
self.assertInResponse(u'OK')
def test_setvol_max(self):
result = self.dispatcher.handle_request(u'setvol "100"')
self.assert_(u'OK' in result)
self.sendRequest(u'setvol "100"')
self.assertEqual(100, self.mixer.volume.get())
self.assertInResponse(u'OK')
def test_setvol_above_max(self):
result = self.dispatcher.handle_request(u'setvol "110"')
self.assert_(u'OK' in result)
self.sendRequest(u'setvol "110"')
self.assertEqual(100, self.mixer.volume.get())
self.assertInResponse(u'OK')
def test_setvol_plus_is_ignored(self):
result = self.dispatcher.handle_request(u'setvol "+10"')
self.assert_(u'OK' in result)
self.sendRequest(u'setvol "+10"')
self.assertEqual(10, self.mixer.volume.get())
self.assertInResponse(u'OK')
def test_setvol_without_quotes(self):
result = self.dispatcher.handle_request(u'setvol 50')
self.assert_(u'OK' in result)
self.sendRequest(u'setvol 50')
self.assertEqual(50, self.mixer.volume.get())
self.assertInResponse(u'OK')
def test_single_off(self):
result = self.dispatcher.handle_request(u'single "0"')
self.sendRequest(u'single "0"')
self.assertFalse(self.backend.playback.single.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_single_off_without_quotes(self):
result = self.dispatcher.handle_request(u'single 0')
self.sendRequest(u'single 0')
self.assertFalse(self.backend.playback.single.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_single_on(self):
result = self.dispatcher.handle_request(u'single "1"')
self.sendRequest(u'single "1"')
self.assertTrue(self.backend.playback.single.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_single_on_without_quotes(self):
result = self.dispatcher.handle_request(u'single 1')
self.sendRequest(u'single 1')
self.assertTrue(self.backend.playback.single.get())
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
def test_replay_gain_mode_off(self):
result = self.dispatcher.handle_request(u'replay_gain_mode "off"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.sendRequest(u'replay_gain_mode "off"')
self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_mode_track(self):
result = self.dispatcher.handle_request(u'replay_gain_mode "track"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.sendRequest(u'replay_gain_mode "track"')
self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_mode_album(self):
result = self.dispatcher.handle_request(u'replay_gain_mode "album"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
self.sendRequest(u'replay_gain_mode "album"')
self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_replay_gain_status_default(self):
expected = u'off'
result = self.dispatcher.handle_request(u'replay_gain_status')
self.assert_(u'OK' in result)
self.assert_(expected in result)
self.sendRequest(u'replay_gain_status')
self.assertInResponse(u'OK')
self.assertInResponse(u'off')
@unittest.SkipTest
def test_replay_gain_status_off(self):
raise SkipTest # TODO
pass
@unittest.SkipTest
def test_replay_gain_status_track(self):
raise SkipTest # TODO
pass
@unittest.SkipTest
def test_replay_gain_status_album(self):
raise SkipTest # TODO
pass
class PlaybackControlHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
class PlaybackControlHandlerTest(protocol.BaseTestCase):
def test_next(self):
result = self.dispatcher.handle_request(u'next')
self.assert_(u'OK' in result)
self.sendRequest(u'next')
self.assertInResponse(u'OK')
def test_pause_off(self):
self.backend.current_playlist.append([Track()])
self.dispatcher.handle_request(u'play "0"')
self.dispatcher.handle_request(u'pause "1"')
result = self.dispatcher.handle_request(u'pause "0"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "0"')
self.sendRequest(u'pause "1"')
self.sendRequest(u'pause "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertInResponse(u'OK')
def test_pause_on(self):
self.backend.current_playlist.append([Track()])
self.dispatcher.handle_request(u'play "0"')
result = self.dispatcher.handle_request(u'pause "1"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "0"')
self.sendRequest(u'pause "1"')
self.assertEqual(PAUSED, self.backend.playback.state.get())
self.assertInResponse(u'OK')
def test_pause_toggle(self):
self.backend.current_playlist.append([Track()])
result = self.dispatcher.handle_request(u'play "0"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
result = self.dispatcher.handle_request(u'pause')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
self.sendRequest(u'pause')
self.assertEqual(PAUSED, self.backend.playback.state.get())
result = self.dispatcher.handle_request(u'pause')
self.assert_(u'OK' in result)
self.assertInResponse(u'OK')
self.sendRequest(u'pause')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertInResponse(u'OK')
def test_play_without_pos(self):
self.backend.current_playlist.append([Track()])
self.backend.playback.state = PAUSED
result = self.dispatcher.handle_request(u'play')
self.assert_(u'OK' in result)
self.sendRequest(u'play')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertInResponse(u'OK')
def test_play_with_pos(self):
self.backend.current_playlist.append([Track()])
result = self.dispatcher.handle_request(u'play "0"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertInResponse(u'OK')
def test_play_with_pos_without_quotes(self):
self.backend.current_playlist.append([Track()])
result = self.dispatcher.handle_request(u'play 0')
self.assert_(u'OK' in result)
self.sendRequest(u'play 0')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertInResponse(u'OK')
def test_play_with_pos_out_of_bounds(self):
self.backend.current_playlist.append([])
result = self.dispatcher.handle_request(u'play "0"')
self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index')
self.sendRequest(u'play "0"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
self.assertInResponse(u'ACK [2@0] {play} Bad song index')
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.backend.playback.current_track.get(), None)
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
self.assertEqual('a', self.backend.playback.current_track.get().uri)
self.assertInResponse(u'OK')
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
@ -250,27 +241,30 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.backend.playback.next()
self.backend.playback.stop()
self.assertNotEqual(self.backend.playback.current_track.get(), None)
result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
self.assertEqual('b', self.backend.playback.current_track.get().uri)
self.assertInResponse(u'OK')
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
self.backend.current_playlist.clear()
result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "-1"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
self.assertEqual(self.backend.playback.current_track.get(), None)
self.assertEqual(None, self.backend.playback.current_track.get())
self.assertInResponse(u'OK')
def test_play_minus_is_ignored_if_playing(self):
self.backend.current_playlist.append([Track(length=40000)])
self.backend.playback.seek(30000)
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.backend.playback.state.get())
result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertInResponse(u'OK')
def test_play_minus_one_resumes_if_paused(self):
self.backend.current_playlist.append([Track(length=40000)])
@ -279,24 +273,27 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEquals(PLAYING, self.backend.playback.state.get())
self.backend.playback.pause()
self.assertEquals(PAUSED, self.backend.playback.state.get())
result = self.dispatcher.handle_request(u'play "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'play "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertInResponse(u'OK')
def test_playid(self):
self.backend.current_playlist.append([Track()])
result = self.dispatcher.handle_request(u'playid "0"')
self.assert_(u'OK' in result)
self.sendRequest(u'playid "0"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertInResponse(u'OK')
def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
self.assertEqual(self.backend.playback.current_track.get(), None)
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
self.assertEqual('a', self.backend.playback.current_track.get().uri)
self.assertInResponse(u'OK')
def test_playid_minus_one_plays_current_track_if_current_track_is_set(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
@ -304,28 +301,31 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.backend.playback.play()
self.backend.playback.next()
self.backend.playback.stop()
self.assertNotEqual(self.backend.playback.current_track.get(), None)
result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.assertNotEqual(None, self.backend.playback.current_track.get())
self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
self.assertEqual('b', self.backend.playback.current_track.get().uri)
self.assertInResponse(u'OK')
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
self.backend.current_playlist.clear()
result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'playid "-1"')
self.assertEqual(STOPPED, self.backend.playback.state.get())
self.assertEqual(self.backend.playback.current_track.get(), None)
self.assertEqual(None, self.backend.playback.current_track.get())
self.assertInResponse(u'OK')
def test_playid_minus_is_ignored_if_playing(self):
self.backend.current_playlist.append([Track(length=40000)])
self.backend.playback.seek(30000)
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertEquals(PLAYING, self.backend.playback.state.get())
result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertInResponse(u'OK')
def test_playid_minus_one_resumes_if_paused(self):
self.backend.current_playlist.append([Track(length=40000)])
@ -334,58 +334,64 @@ class PlaybackControlHandlerTest(unittest.TestCase):
self.assertEquals(PLAYING, self.backend.playback.state.get())
self.backend.playback.pause()
self.assertEquals(PAUSED, self.backend.playback.state.get())
result = self.dispatcher.handle_request(u'playid "-1"')
self.assert_(u'OK' in result)
self.sendRequest(u'playid "-1"')
self.assertEqual(PLAYING, self.backend.playback.state.get())
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertInResponse(u'OK')
def test_playid_which_does_not_exist(self):
self.backend.current_playlist.append([Track()])
result = self.dispatcher.handle_request(u'playid "12345"')
self.assertEqual(result[0], u'ACK [50@0] {playid} No such song')
self.sendRequest(u'playid "12345"')
self.assertInResponse(u'ACK [50@0] {playid} No such song')
def test_previous(self):
result = self.dispatcher.handle_request(u'previous')
self.assert_(u'OK' in result)
self.sendRequest(u'previous')
self.assertInResponse(u'OK')
def test_seek(self):
self.backend.current_playlist.append([Track(length=40000)])
self.dispatcher.handle_request(u'seek "0"')
result = self.dispatcher.handle_request(u'seek "0" "30"')
self.assert_(u'OK' in result)
self.sendRequest(u'seek "0"')
self.sendRequest(u'seek "0" "30"')
self.assert_(self.backend.playback.time_position >= 30000)
self.assertInResponse(u'OK')
def test_seek_with_songpos(self):
seek_track = Track(uri='2', length=40000)
self.backend.current_playlist.append(
[Track(uri='1', length=40000), seek_track])
result = self.dispatcher.handle_request(u'seek "1" "30"')
self.assert_(u'OK' in result)
self.sendRequest(u'seek "1" "30"')
self.assertEqual(self.backend.playback.current_track.get(), seek_track)
self.assertInResponse(u'OK')
def test_seek_without_quotes(self):
self.backend.current_playlist.append([Track(length=40000)])
self.dispatcher.handle_request(u'seek 0')
result = self.dispatcher.handle_request(u'seek 0 30')
self.assert_(u'OK' in result)
self.sendRequest(u'seek 0')
self.sendRequest(u'seek 0 30')
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertInResponse(u'OK')
def test_seekid(self):
self.backend.current_playlist.append([Track(length=40000)])
result = self.dispatcher.handle_request(u'seekid "0" "30"')
self.assert_(u'OK' in result)
self.sendRequest(u'seekid "0" "30"')
self.assert_(self.backend.playback.time_position.get() >= 30000)
self.assertInResponse(u'OK')
def test_seekid_with_cpid(self):
seek_track = Track(uri='2', length=40000)
self.backend.current_playlist.append(
[Track(length=40000), seek_track])
result = self.dispatcher.handle_request(u'seekid "1" "30"')
self.assert_(u'OK' in result)
self.assertEqual(self.backend.playback.current_cpid.get(), 1)
self.assertEqual(self.backend.playback.current_track.get(), seek_track)
self.sendRequest(u'seekid "1" "30"')
self.assertEqual(1, self.backend.playback.current_cpid.get())
self.assertEqual(seek_track, self.backend.playback.current_track.get())
self.assertInResponse(u'OK')
def test_stop(self):
result = self.dispatcher.handle_request(u'stop')
self.assert_(u'OK' in result)
self.sendRequest(u'stop')
self.assertEqual(STOPPED, self.backend.playback.state.get())
self.assertInResponse(u'OK')

View File

@ -0,0 +1,67 @@
from mopidy import settings
from tests.frontends.mpd import protocol
class ReflectionHandlerTest(protocol.BaseTestCase):
def test_commands_returns_list_of_all_commands(self):
self.sendRequest(u'commands')
# Check if some random commands are included
self.assertInResponse(u'command: commands')
self.assertInResponse(u'command: play')
self.assertInResponse(u'command: status')
# Check if commands you do not have access to are not present
self.assertNotInResponse(u'command: kill')
# Check if the blacklisted commands are not present
self.assertNotInResponse(u'command: command_list_begin')
self.assertNotInResponse(u'command: command_list_ok_begin')
self.assertNotInResponse(u'command: command_list_end')
self.assertNotInResponse(u'command: idle')
self.assertNotInResponse(u'command: noidle')
self.assertNotInResponse(u'command: sticker')
self.assertInResponse(u'OK')
def test_commands_show_less_if_auth_required_and_not_authed(self):
settings.MPD_SERVER_PASSWORD = u'secret'
self.sendRequest(u'commands')
# Not requiring auth
self.assertInResponse(u'command: close')
self.assertInResponse(u'command: commands')
self.assertInResponse(u'command: notcommands')
self.assertInResponse(u'command: password')
self.assertInResponse(u'command: ping')
# Requiring auth
self.assertNotInResponse(u'command: play')
self.assertNotInResponse(u'command: status')
def test_decoders(self):
self.sendRequest(u'decoders')
self.assertInResponse(u'ACK [0@0] {} Not implemented')
def test_notcommands_returns_only_kill_and_ok(self):
response = self.sendRequest(u'notcommands')
self.assertEqual(2, len(response))
self.assertInResponse(u'command: kill')
self.assertInResponse(u'OK')
def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
settings.MPD_SERVER_PASSWORD = u'secret'
self.sendRequest(u'notcommands')
# Not requiring auth
self.assertNotInResponse(u'command: close')
self.assertNotInResponse(u'command: commands')
self.assertNotInResponse(u'command: notcommands')
self.assertNotInResponse(u'command: password')
self.assertNotInResponse(u'command: ping')
# Requiring auth
self.assertInResponse(u'command: play')
self.assertInResponse(u'command: status')
def test_tagtypes(self):
self.sendRequest(u'tagtypes')
self.assertInResponse(u'OK')
def test_urlhandlers(self):
self.sendRequest(u'urlhandlers')
self.assertInResponse(u'OK')
self.assertInResponse(u'handler: dummy')

View File

@ -0,0 +1,148 @@
import random
from mopidy.models import Track
from tests.frontends.mpd import protocol
class IssueGH17RegressionTest(protocol.BaseTestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues/17
How to reproduce:
- Play a playlist where one track cannot be played
- Turn on random mode
- Press next until you get to the unplayable track
"""
def test(self):
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), None,
Track(uri='d'), Track(uri='e'), Track(uri='f')])
random.seed(1) # Playlist order: abcfde
self.sendRequest(u'play')
self.assertEquals('a', self.backend.playback.current_track.get().uri)
self.sendRequest(u'random "1"')
self.sendRequest(u'next')
self.assertEquals('b', self.backend.playback.current_track.get().uri)
self.sendRequest(u'next')
# Should now be at track 'c', but playback fails and it skips ahead
self.assertEquals('f', self.backend.playback.current_track.get().uri)
self.sendRequest(u'next')
self.assertEquals('d', self.backend.playback.current_track.get().uri)
self.sendRequest(u'next')
self.assertEquals('e', self.backend.playback.current_track.get().uri)
class IssueGH18RegressionTest(protocol.BaseTestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues/18
How to reproduce:
Play, random on, next, random off, next, next.
At this point it gives the same song over and over.
"""
def test(self):
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
random.seed(1)
self.sendRequest(u'play')
self.sendRequest(u'random "1"')
self.sendRequest(u'next')
self.sendRequest(u'random "0"')
self.sendRequest(u'next')
self.sendRequest(u'next')
cp_track_1 = self.backend.playback.current_cp_track.get()
self.sendRequest(u'next')
cp_track_2 = self.backend.playback.current_cp_track.get()
self.sendRequest(u'next')
cp_track_3 = self.backend.playback.current_cp_track.get()
self.assertNotEqual(cp_track_1, cp_track_2)
self.assertNotEqual(cp_track_2, cp_track_3)
class IssueGH22RegressionTest(protocol.BaseTestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues/22
How to reproduce:
Play, random on, remove all tracks from the current playlist (as in
"delete" each one, not "clear").
Alternatively: Play, random on, remove a random track from the current
playlist, press next until it crashes.
"""
def test(self):
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
random.seed(1)
self.sendRequest(u'play')
self.sendRequest(u'random "1"')
self.sendRequest(u'deleteid "1"')
self.sendRequest(u'deleteid "2"')
self.sendRequest(u'deleteid "3"')
self.sendRequest(u'deleteid "4"')
self.sendRequest(u'deleteid "5"')
self.sendRequest(u'deleteid "6"')
self.sendRequest(u'status')
class IssueGH69RegressionTest(protocol.BaseTestCase):
"""
The issue: https://github.com/mopidy/mopidy/issues/69
How to reproduce:
Play track, stop, clear current playlist, load a new playlist, status.
The status response now contains "song: None".
"""
def test(self):
self.backend.stored_playlists.create('foo')
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
self.sendRequest(u'play')
self.sendRequest(u'stop')
self.sendRequest(u'clear')
self.sendRequest(u'load "foo"')
self.assertNotInResponse('song: None')
class IssueGH113RegressionTest(protocol.BaseTestCase):
"""
The issue: https://github.com/mopidy/mopidy/issues/113
How to reproduce:
- Have a playlist with a name contining backslashes, like
"all lart spotify:track:\w\{22\} pastes".
- Try to load the playlist with the backslashes in the playlist name
escaped.
"""
def test(self):
self.backend.stored_playlists.create(
u'all lart spotify:track:\w\{22\} pastes')
self.sendRequest(u'lsinfo "/"')
self.assertInResponse(
u'playlist: all lart spotify:track:\w\{22\} pastes')
self.sendRequest(
r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"')
self.assertInResponse('OK')

View File

@ -0,0 +1,37 @@
from mopidy.models import Track
from tests.frontends.mpd import protocol
class StatusHandlerTest(protocol.BaseTestCase):
def test_clearerror(self):
self.sendRequest(u'clearerror')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_currentsong(self):
track = Track()
self.backend.current_playlist.append([track])
self.backend.playback.play()
self.sendRequest(u'currentsong')
self.assertInResponse(u'file: ')
self.assertInResponse(u'Time: 0')
self.assertInResponse(u'Artist: ')
self.assertInResponse(u'Title: ')
self.assertInResponse(u'Album: ')
self.assertInResponse(u'Track: 0')
self.assertInResponse(u'Date: ')
self.assertInResponse(u'Pos: 0')
self.assertInResponse(u'Id: 0')
self.assertInResponse(u'OK')
def test_currentsong_without_song(self):
self.sendRequest(u'currentsong')
self.assertInResponse(u'OK')
def test_stats_command(self):
self.sendRequest(u'stats')
self.assertInResponse(u'OK')
def test_status_command(self):
self.sendRequest(u'status')
self.assertInResponse(u'OK')

View File

@ -0,0 +1,33 @@
from tests.frontends.mpd import protocol
class StickersHandlerTest(protocol.BaseTestCase):
def test_sticker_get(self):
self.sendRequest(
u'sticker get "song" "file:///dev/urandom" "a_name"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_sticker_set(self):
self.sendRequest(
u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_sticker_delete_with_name(self):
self.sendRequest(
u'sticker delete "song" "file:///dev/urandom" "a_name"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_sticker_delete_without_name(self):
self.sendRequest(
u'sticker delete "song" "file:///dev/urandom"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_sticker_list(self):
self.sendRequest(
u'sticker list "song" "file:///dev/urandom"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_sticker_find(self):
self.sendRequest(
u'sticker find "song" "file:///dev/urandom" "a_name"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')

View File

@ -0,0 +1,94 @@
import datetime
from mopidy.models import Track, Playlist
from tests.frontends.mpd import protocol
class StoredPlaylistsHandlerTest(protocol.BaseTestCase):
def test_listplaylist(self):
self.backend.stored_playlists.playlists = [
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
self.sendRequest(u'listplaylist "name"')
self.assertInResponse(u'file: file:///dev/urandom')
self.assertInResponse(u'OK')
def test_listplaylist_fails_if_no_playlist_is_found(self):
self.sendRequest(u'listplaylist "name"')
self.assertEqualResponse(u'ACK [50@0] {listplaylist} No such playlist')
def test_listplaylistinfo(self):
self.backend.stored_playlists.playlists = [
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
self.sendRequest(u'listplaylistinfo "name"')
self.assertInResponse(u'file: file:///dev/urandom')
self.assertInResponse(u'Track: 0')
self.assertNotInResponse(u'Pos: 0')
self.assertInResponse(u'OK')
def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
self.sendRequest(u'listplaylistinfo "name"')
self.assertEqualResponse(
u'ACK [50@0] {listplaylistinfo} No such playlist')
def test_listplaylists(self):
last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345)
self.backend.stored_playlists.playlists = [Playlist(name='a',
last_modified=last_modified)]
self.sendRequest(u'listplaylists')
self.assertInResponse(u'playlist: a')
# Date without microseconds and with time zone information
self.assertInResponse(u'Last-Modified: 2001-03-17T13:41:17Z')
self.assertInResponse(u'OK')
def test_load_known_playlist_appends_to_current_playlist(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
self.backend.stored_playlists.playlists = [Playlist(name='A-list',
tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
self.sendRequest(u'load "A-list"')
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(5, len(tracks))
self.assertEqual('a', tracks[0].uri)
self.assertEqual('b', tracks[1].uri)
self.assertEqual('c', tracks[2].uri)
self.assertEqual('d', tracks[3].uri)
self.assertEqual('e', tracks[4].uri)
self.assertInResponse(u'OK')
def test_load_unknown_playlist_acks(self):
self.sendRequest(u'load "unknown playlist"')
self.assertEqual(0, len(self.backend.current_playlist.tracks.get()))
self.assertEqualResponse(u'ACK [50@0] {load} No such playlist')
def test_playlistadd(self):
self.sendRequest(u'playlistadd "name" "file:///dev/urandom"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistclear(self):
self.sendRequest(u'playlistclear "name"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistdelete(self):
self.sendRequest(u'playlistdelete "name" "5"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_playlistmove(self):
self.sendRequest(u'playlistmove "name" "5" "10"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_rename(self):
self.sendRequest(u'rename "old_name" "new_name"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_rm(self):
self.sendRequest(u'rm "name"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')
def test_save(self):
self.sendRequest(u'save "name"')
self.assertEqualResponse(u'ACK [0@0] {} Not implemented')

View File

@ -1,79 +0,0 @@
import unittest
from mopidy import settings
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
class ReflectionHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
settings.runtime.clear()
self.backend.stop().get()
self.mixer.stop().get()
def test_commands_returns_list_of_all_commands(self):
result = self.dispatcher.handle_request(u'commands')
# Check if some random commands are included
self.assert_(u'command: commands' in result)
self.assert_(u'command: play' in result)
self.assert_(u'command: status' in result)
# Check if commands you do not have access to are not present
self.assert_(u'command: kill' not in result)
# Check if the blacklisted commands are not present
self.assert_(u'command: command_list_begin' not in result)
self.assert_(u'command: command_list_ok_begin' not in result)
self.assert_(u'command: command_list_end' not in result)
self.assert_(u'command: idle' not in result)
self.assert_(u'command: noidle' not in result)
self.assert_(u'command: sticker' not in result)
self.assert_(u'OK' in result)
def test_commands_show_less_if_auth_required_and_not_authed(self):
settings.MPD_SERVER_PASSWORD = u'secret'
result = self.dispatcher.handle_request(u'commands')
# Not requiring auth
self.assert_(u'command: close' in result, result)
self.assert_(u'command: commands' in result, result)
self.assert_(u'command: notcommands' in result, result)
self.assert_(u'command: password' in result, result)
self.assert_(u'command: ping' in result, result)
# Requiring auth
self.assert_(u'command: play' not in result, result)
self.assert_(u'command: status' not in result, result)
def test_decoders(self):
result = self.dispatcher.handle_request(u'decoders')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_notcommands_returns_only_kill_and_ok(self):
result = self.dispatcher.handle_request(u'notcommands')
self.assertEqual(2, len(result))
self.assert_(u'command: kill' in result)
self.assert_(u'OK' in result)
def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
settings.MPD_SERVER_PASSWORD = u'secret'
result = self.dispatcher.handle_request(u'notcommands')
# Not requiring auth
self.assert_(u'command: close' not in result, result)
self.assert_(u'command: commands' not in result, result)
self.assert_(u'command: notcommands' not in result, result)
self.assert_(u'command: password' not in result, result)
self.assert_(u'command: ping' not in result, result)
# Requiring auth
self.assert_(u'command: play' in result, result)
self.assert_(u'command: status' in result, result)
def test_tagtypes(self):
result = self.dispatcher.handle_request(u'tagtypes')
self.assert_(u'OK' in result)
def test_urlhandlers(self):
result = self.dispatcher.handle_request(u'urlhandlers')
self.assert_(u'OK' in result)
self.assert_(u'handler: dummy:' in result)

View File

@ -1,158 +0,0 @@
import random
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class IssueGH17RegressionTest(unittest.TestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues#issue/17
How to reproduce:
- Play a playlist where one track cannot be played
- Turn on random mode
- Press next until you get to the unplayable track
"""
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), None,
Track(uri='d'), Track(uri='e'), Track(uri='f')])
self.mixer = DummyMixer.start().proxy()
self.mpd = dispatcher.MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test(self):
random.seed(1) # Playlist order: abcfde
self.mpd.handle_request(u'play')
self.assertEquals('a', self.backend.playback.current_track.get().uri)
self.mpd.handle_request(u'random "1"')
self.mpd.handle_request(u'next')
self.assertEquals('b', self.backend.playback.current_track.get().uri)
self.mpd.handle_request(u'next')
# Should now be at track 'c', but playback fails and it skips ahead
self.assertEquals('f', self.backend.playback.current_track.get().uri)
self.mpd.handle_request(u'next')
self.assertEquals('d', self.backend.playback.current_track.get().uri)
self.mpd.handle_request(u'next')
self.assertEquals('e', self.backend.playback.current_track.get().uri)
class IssueGH18RegressionTest(unittest.TestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues#issue/18
How to reproduce:
Play, random on, next, random off, next, next.
At this point it gives the same song over and over.
"""
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
self.mixer = DummyMixer.start().proxy()
self.mpd = dispatcher.MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test(self):
random.seed(1)
self.mpd.handle_request(u'play')
self.mpd.handle_request(u'random "1"')
self.mpd.handle_request(u'next')
self.mpd.handle_request(u'random "0"')
self.mpd.handle_request(u'next')
self.mpd.handle_request(u'next')
cp_track_1 = self.backend.playback.current_cp_track.get()
self.mpd.handle_request(u'next')
cp_track_2 = self.backend.playback.current_cp_track.get()
self.mpd.handle_request(u'next')
cp_track_3 = self.backend.playback.current_cp_track.get()
self.assertNotEqual(cp_track_1, cp_track_2)
self.assertNotEqual(cp_track_2, cp_track_3)
class IssueGH22RegressionTest(unittest.TestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues/#issue/22
How to reproduce:
Play, random on, remove all tracks from the current playlist (as in
"delete" each one, not "clear").
Alternatively: Play, random on, remove a random track from the current
playlist, press next until it crashes.
"""
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
self.mixer = DummyMixer.start().proxy()
self.mpd = dispatcher.MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test(self):
random.seed(1)
self.mpd.handle_request(u'play')
self.mpd.handle_request(u'random "1"')
self.mpd.handle_request(u'deleteid "1"')
self.mpd.handle_request(u'deleteid "2"')
self.mpd.handle_request(u'deleteid "3"')
self.mpd.handle_request(u'deleteid "4"')
self.mpd.handle_request(u'deleteid "5"')
self.mpd.handle_request(u'deleteid "6"')
self.mpd.handle_request(u'status')
class IssueGH69RegressionTest(unittest.TestCase):
"""
The issue: https://github.com/mopidy/mopidy/issues#issue/69
How to reproduce:
Play track, stop, clear current playlist, load a new playlist, status.
The status response now contains "song: None".
"""
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.backend.current_playlist.append([
Track(uri='a'), Track(uri='b'), Track(uri='c'),
Track(uri='d'), Track(uri='e'), Track(uri='f')])
self.backend.stored_playlists.create('foo')
self.mixer = DummyMixer.start().proxy()
self.mpd = dispatcher.MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test(self):
self.mpd.handle_request(u'play')
self.mpd.handle_request(u'stop')
self.mpd.handle_request(u'clear')
self.mpd.handle_request(u'load "foo"')
response = self.mpd.handle_request(u'status')
self.assert_('song: None' not in response)

View File

@ -1,12 +1,14 @@
import datetime as dt
import datetime
import os
import unittest
from mopidy import settings
from mopidy.utils.path import mtime, uri_to_path
from mopidy.frontends.mpd import translator, protocol
from mopidy.models import Album, Artist, Playlist, Track
from tests import unittest
class TrackMpdFormatTest(unittest.TestCase):
track = Track(
uri=u'a uri',
@ -15,7 +17,7 @@ class TrackMpdFormatTest(unittest.TestCase):
album=Album(name=u'an album', num_tracks=13,
artists=[Artist(name=u'an other artist')]),
track_no=7,
date=dt.date(1977, 1, 1),
date=datetime.date(1977, 1, 1),
length=137000,
)
@ -61,7 +63,7 @@ class TrackMpdFormatTest(unittest.TestCase):
self.assert_(('Album', 'an album') in result)
self.assert_(('AlbumArtist', 'an other artist') in result)
self.assert_(('Track', '7/13') in result)
self.assert_(('Date', dt.date(1977, 1, 1)) in result)
self.assert_(('Date', datetime.date(1977, 1, 1)) in result)
self.assert_(('Pos', 9) in result)
self.assert_(('Id', 122) in result)
self.assertEqual(len(result), 10)

View File

@ -1,23 +0,0 @@
import unittest
from mopidy import settings
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import server
from mopidy.mixers.dummy import DummyMixer
class MpdSessionTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.session = server.MpdSession(None, None, (None, None))
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
settings.runtime.clear()
def test_found_terminator_catches_decode_error(self):
# Pressing Ctrl+C in a telnet session sends a 0xff byte to the server.
self.session.input_buffer = ['\xff']
self.session.found_terminator()
self.assertEqual(len(self.session.input_buffer), 0)

View File

@ -1,67 +1,30 @@
import unittest
from mopidy.backends.base import PlaybackController
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.backends import dummy as backend
from mopidy.frontends.mpd import dispatcher
from mopidy.frontends.mpd.protocol import status
from mopidy.mixers.dummy import DummyMixer
from mopidy.mixers import dummy as mixer
from mopidy.models import Track
PAUSED = PlaybackController.PAUSED
PLAYING = PlaybackController.PLAYING
STOPPED = PlaybackController.STOPPED
from tests import unittest
PAUSED = backend.PlaybackController.PAUSED
PLAYING = backend.PlaybackController.PLAYING
STOPPED = backend.PlaybackController.STOPPED
# FIXME migrate to using protocol.BaseTestCase instead of status.stats
# directly?
class StatusHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
self.backend = backend.DummyBackend.start().proxy()
self.mixer = mixer.DummyMixer.start().proxy()
self.dispatcher = dispatcher.MpdDispatcher()
self.context = self.dispatcher.context
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test_clearerror(self):
result = self.dispatcher.handle_request(u'clearerror')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_currentsong(self):
track = Track()
self.backend.current_playlist.append([track])
self.backend.playback.play()
result = self.dispatcher.handle_request(u'currentsong')
self.assert_(u'file: ' in result)
self.assert_(u'Time: 0' in result)
self.assert_(u'Artist: ' in result)
self.assert_(u'Title: ' in result)
self.assert_(u'Album: ' in result)
self.assert_(u'Track: 0' in result)
self.assert_(u'Date: ' in result)
self.assert_(u'Pos: 0' in result)
self.assert_(u'Id: 0' in result)
self.assert_(u'OK' in result)
def test_currentsong_without_song(self):
result = self.dispatcher.handle_request(u'currentsong')
self.assert_(u'OK' in result)
def test_idle_without_subsystems(self):
result = self.dispatcher.handle_request(u'idle')
self.assert_(u'OK' in result)
def test_idle_with_subsystems(self):
result = self.dispatcher.handle_request(u'idle database playlist')
self.assert_(u'OK' in result)
def test_noidle(self):
result = self.dispatcher.handle_request(u'noidle')
self.assert_(u'OK' in result)
def test_stats_command(self):
result = self.dispatcher.handle_request(u'stats')
self.assert_(u'OK' in result)
def test_stats_method(self):
result = status.stats(self.context)
self.assert_('artists' in result)
@ -79,10 +42,6 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_('playtime' in result)
self.assert_(int(result['playtime']) >= 0)
def test_status_command(self):
result = self.dispatcher.handle_request(u'status')
self.assert_(u'OK' in result)
def test_status_method_contains_volume_which_defaults_to_0(self):
result = dict(status.status(self.context))
self.assert_('volume' in result)
@ -205,7 +164,14 @@ class StatusHandlerTest(unittest.TestCase):
self.backend.playback.play_time_accumulated = 59123
result = dict(status.status(self.context))
self.assert_('elapsed' in result)
self.assertEqual(int(result['elapsed']), 59123)
self.assertEqual(result['elapsed'], '59.123')
def test_status_method_when_starting_playing_contains_elapsed_zero(self):
self.backend.playback.state = PAUSED
self.backend.playback.play_time_accumulated = 123 # Less than 1000ms
result = dict(status.status(self.context))
self.assert_('elapsed' in result)
self.assertEqual(result['elapsed'], '0.123')
def test_status_method_when_playing_contains_bitrate(self):
self.backend.current_playlist.append([Track(bitrate=320)])

View File

@ -1,45 +0,0 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
class StickersHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test_sticker_get(self):
result = self.dispatcher.handle_request(
u'sticker get "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_set(self):
result = self.dispatcher.handle_request(
u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_delete_with_name(self):
result = self.dispatcher.handle_request(
u'sticker delete "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_delete_without_name(self):
result = self.dispatcher.handle_request(
u'sticker delete "song" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_list(self):
result = self.dispatcher.handle_request(
u'sticker list "song" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_sticker_find(self):
result = self.dispatcher.handle_request(
u'sticker find "song" "file:///dev/urandom" "a_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)

View File

@ -1,102 +0,0 @@
import datetime as dt
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track, Playlist
class StoredPlaylistsHandlerTest(unittest.TestCase):
def setUp(self):
self.backend = DummyBackend.start().proxy()
self.mixer = DummyMixer.start().proxy()
self.dispatcher = MpdDispatcher()
def tearDown(self):
self.backend.stop().get()
self.mixer.stop().get()
def test_listplaylist(self):
self.backend.stored_playlists.playlists = [
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
result = self.dispatcher.handle_request(u'listplaylist "name"')
self.assert_(u'file: file:///dev/urandom' in result)
self.assert_(u'OK' in result)
def test_listplaylist_fails_if_no_playlist_is_found(self):
result = self.dispatcher.handle_request(u'listplaylist "name"')
self.assertEqual(result[0],
u'ACK [50@0] {listplaylist} No such playlist')
def test_listplaylistinfo(self):
self.backend.stored_playlists.playlists = [
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
self.assert_(u'file: file:///dev/urandom' in result)
self.assert_(u'Track: 0' in result)
self.assert_(u'Pos: 0' not in result)
self.assert_(u'OK' in result)
def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
self.assertEqual(result[0],
u'ACK [50@0] {listplaylistinfo} No such playlist')
def test_listplaylists(self):
last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345)
self.backend.stored_playlists.playlists = [Playlist(name='a',
last_modified=last_modified)]
result = self.dispatcher.handle_request(u'listplaylists')
self.assert_(u'playlist: a' in result)
# Date without microseconds and with time zone information
self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result)
self.assert_(u'OK' in result)
def test_load_known_playlist_appends_to_current_playlist(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
self.backend.stored_playlists.playlists = [Playlist(name='A-list',
tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
result = self.dispatcher.handle_request(u'load "A-list"')
self.assert_(u'OK' in result)
tracks = self.backend.current_playlist.tracks.get()
self.assertEqual(len(tracks), 5)
self.assertEqual(tracks[0].uri, 'a')
self.assertEqual(tracks[1].uri, 'b')
self.assertEqual(tracks[2].uri, 'c')
self.assertEqual(tracks[3].uri, 'd')
self.assertEqual(tracks[4].uri, 'e')
def test_load_unknown_playlist_acks(self):
result = self.dispatcher.handle_request(u'load "unknown playlist"')
self.assert_(u'ACK [50@0] {load} No such playlist' in result)
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
def test_playlistadd(self):
result = self.dispatcher.handle_request(
u'playlistadd "name" "file:///dev/urandom"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistclear(self):
result = self.dispatcher.handle_request(u'playlistclear "name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistdelete(self):
result = self.dispatcher.handle_request(u'playlistdelete "name" "5"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_playlistmove(self):
result = self.dispatcher.handle_request(u'playlistmove "name" "5" "10"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_rename(self):
result = self.dispatcher.handle_request(u'rename "old_name" "new_name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_rm(self):
result = self.dispatcher.handle_request(u'rm "name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)
def test_save(self):
result = self.dispatcher.handle_request(u'save "name"')
self.assert_(u'ACK [0@0] {} Not implemented' in result)

View File

View File

@ -0,0 +1,70 @@
import mock
from mopidy.frontends.mpris import MprisFrontend, objects
from mopidy.models import Track
from tests import unittest
class BackendEventsTest(unittest.TestCase):
def setUp(self):
self.mpris_frontend = MprisFrontend() # As a plain class, not an actor
self.mpris_object = mock.Mock(spec=objects.MprisObject)
self.mpris_frontend.mpris_object = self.mpris_object
def test_track_playback_paused_event_changes_playback_status(self):
self.mpris_object.Get.return_value = 'Paused'
self.mpris_frontend.track_playback_paused(Track(), 0)
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE, {'PlaybackStatus': 'Paused'}, [])
def test_track_playback_resumed_event_changes_playback_status(self):
self.mpris_object.Get.return_value = 'Playing'
self.mpris_frontend.track_playback_resumed(Track(), 0)
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, [])
def test_track_playback_started_event_changes_playback_status_and_metadata(self):
self.mpris_object.Get.return_value = '...'
self.mpris_frontend.track_playback_started(Track())
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
((objects.PLAYER_IFACE, 'Metadata'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE,
{'Metadata': '...', 'PlaybackStatus': '...'}, [])
def test_track_playback_ended_event_changes_playback_status_and_metadata(self):
self.mpris_object.Get.return_value = '...'
self.mpris_frontend.track_playback_ended(Track(), 0)
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
((objects.PLAYER_IFACE, 'Metadata'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE,
{'Metadata': '...', 'PlaybackStatus': '...'}, [])
def test_volume_changed_event_changes_volume(self):
self.mpris_object.Get.return_value = 1.0
self.mpris_frontend.volume_changed()
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'Volume'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE, {'Volume': 1.0}, [])
def test_seeked_event_causes_mpris_seeked_event(self):
self.mpris_object.Get.return_value = 31000000
self.mpris_frontend.seeked()
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'Position'), {}),
])
self.mpris_object.Seeked.assert_called_with(31000000)

View File

@ -0,0 +1,826 @@
import mock
from mopidy.backends.dummy import DummyBackend
from mopidy.backends.base.playback import PlaybackController
from mopidy.frontends.mpris import objects
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Album, Artist, Track
from tests import unittest
PLAYING = PlaybackController.PLAYING
PAUSED = PlaybackController.PAUSED
STOPPED = PlaybackController.STOPPED
class PlayerInterfaceTest(unittest.TestCase):
def setUp(self):
objects.MprisObject._connect_to_dbus = mock.Mock()
self.mixer = DummyMixer.start().proxy()
self.backend = DummyBackend.start().proxy()
self.mpris = objects.MprisObject()
self.mpris._backend = self.backend
def tearDown(self):
self.backend.stop()
self.mixer.stop()
def test_get_playback_status_is_playing_when_playing(self):
self.backend.playback.state = PLAYING
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Playing', result)
def test_get_playback_status_is_paused_when_paused(self):
self.backend.playback.state = PAUSED
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Paused', result)
def test_get_playback_status_is_stopped_when_stopped(self):
self.backend.playback.state = STOPPED
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Stopped', result)
def test_get_loop_status_is_none_when_not_looping(self):
self.backend.playback.repeat = False
self.backend.playback.single = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('None', result)
def test_get_loop_status_is_track_when_looping_a_single_track(self):
self.backend.playback.repeat = True
self.backend.playback.single = True
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('Track', result)
def test_get_loop_status_is_playlist_when_looping_the_current_playlist(self):
self.backend.playback.repeat = True
self.backend.playback.single = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('Playlist', result)
def test_set_loop_status_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.backend.playback.repeat = True
self.backend.playback.single = True
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
self.assertEquals(self.backend.playback.repeat.get(), True)
self.assertEquals(self.backend.playback.single.get(), True)
def test_set_loop_status_to_none_unsets_repeat_and_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
self.assertEquals(self.backend.playback.repeat.get(), False)
self.assertEquals(self.backend.playback.single.get(), False)
def test_set_loop_status_to_track_sets_repeat_and_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Track')
self.assertEquals(self.backend.playback.repeat.get(), True)
self.assertEquals(self.backend.playback.single.get(), True)
def test_set_loop_status_to_playlists_sets_repeat_and_not_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Playlist')
self.assertEquals(self.backend.playback.repeat.get(), True)
self.assertEquals(self.backend.playback.single.get(), False)
def test_get_rate_is_greater_or_equal_than_minimum_rate(self):
rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate')
minimum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
self.assert_(rate >= minimum_rate)
def test_get_rate_is_less_or_equal_than_maximum_rate(self):
rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate')
maximum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
self.assert_(rate >= maximum_rate)
def test_set_rate_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
def test_set_rate_to_zero_pauses_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
self.assertEquals(self.backend.playback.state.get(), PAUSED)
def test_get_shuffle_returns_true_if_random_is_active(self):
self.backend.playback.random = True
result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle')
self.assertTrue(result)
def test_get_shuffle_returns_false_if_random_is_inactive(self):
self.backend.playback.random = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle')
self.assertFalse(result)
def test_set_shuffle_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.backend.playback.random = False
result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
self.assertFalse(self.backend.playback.random.get())
def test_set_shuffle_to_true_activates_random_mode(self):
self.backend.playback.random = False
self.assertFalse(self.backend.playback.random.get())
result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
self.assertTrue(self.backend.playback.random.get())
def test_set_shuffle_to_false_deactivates_random_mode(self):
self.backend.playback.random = True
self.assertTrue(self.backend.playback.random.get())
result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', False)
self.assertFalse(self.backend.playback.random.get())
def test_get_metadata_has_trackid_even_when_no_current_track(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assert_('mpris:trackid' in result.keys())
self.assertEquals(result['mpris:trackid'], '')
def test_get_metadata_has_trackid_based_on_cpid(self):
self.backend.current_playlist.append([Track(uri='a')])
self.backend.playback.play()
(cpid, track) = self.backend.playback.current_cp_track.get()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:trackid', result.keys())
self.assertEquals(result['mpris:trackid'],
'/com/mopidy/track/%d' % cpid)
def test_get_metadata_has_track_length(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:length', result.keys())
self.assertEquals(result['mpris:length'], 40000000)
def test_get_metadata_has_track_uri(self):
self.backend.current_playlist.append([Track(uri='a')])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:url', result.keys())
self.assertEquals(result['xesam:url'], 'a')
def test_get_metadata_has_track_title(self):
self.backend.current_playlist.append([Track(name='a')])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:title', result.keys())
self.assertEquals(result['xesam:title'], 'a')
def test_get_metadata_has_track_artists(self):
self.backend.current_playlist.append([Track(artists=[
Artist(name='a'), Artist(name='b'), Artist(name=None)])])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:artist', result.keys())
self.assertEquals(result['xesam:artist'], ['a', 'b'])
def test_get_metadata_has_track_album(self):
self.backend.current_playlist.append([Track(album=Album(name='a'))])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:album', result.keys())
self.assertEquals(result['xesam:album'], 'a')
def test_get_metadata_has_track_album_artists(self):
self.backend.current_playlist.append([Track(album=Album(artists=[
Artist(name='a'), Artist(name='b'), Artist(name=None)]))])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:albumArtist', result.keys())
self.assertEquals(result['xesam:albumArtist'], ['a', 'b'])
def test_get_metadata_has_track_number_in_album(self):
self.backend.current_playlist.append([Track(track_no=7)])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:trackNumber', result.keys())
self.assertEquals(result['xesam:trackNumber'], 7)
def test_get_volume_should_return_volume_between_zero_and_one(self):
self.mixer.volume = 0
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
self.assertEquals(result, 0)
self.mixer.volume = 50
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
self.assertEquals(result, 0.5)
self.mixer.volume = 100
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
self.assertEquals(result, 1)
def test_set_volume_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.mixer.volume = 0
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
self.assertEquals(self.mixer.volume.get(), 0)
def test_set_volume_to_one_should_set_mixer_volume_to_100(self):
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
self.assertEquals(self.mixer.volume.get(), 100)
def test_set_volume_to_anything_above_one_should_set_mixer_volume_to_100(self):
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 2.0)
self.assertEquals(self.mixer.volume.get(), 100)
def test_set_volume_to_anything_not_a_number_does_not_change_volume(self):
self.mixer.volume = 10
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', None)
self.assertEquals(self.mixer.volume.get(), 10)
def test_get_position_returns_time_position_in_microseconds(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
self.backend.playback.seek(10000)
result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position')
result_in_milliseconds = result_in_microseconds // 1000
self.assert_(result_in_milliseconds >= 10000)
def test_get_position_when_no_current_track_should_be_zero(self):
result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position')
result_in_milliseconds = result_in_microseconds // 1000
self.assertEquals(result_in_milliseconds, 0)
def test_get_minimum_rate_is_one_or_less(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
self.assert_(result <= 1.0)
def test_get_maximum_rate_is_one_or_more(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
self.assert_(result >= 1.0)
def test_can_go_next_is_true_if_can_control_and_other_next_track(self):
self.mpris.get_CanControl = lambda *_: True
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertTrue(result)
def test_can_go_next_is_false_if_next_track_is_the_same(self):
self.mpris.get_CanControl = lambda *_: True
self.backend.current_playlist.append([Track(uri='a')])
self.backend.playback.repeat = True
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertFalse(result)
def test_can_go_next_is_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertFalse(result)
def test_can_go_previous_is_true_if_can_control_and_other_previous_track(self):
self.mpris.get_CanControl = lambda *_: True
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.next()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertTrue(result)
def test_can_go_previous_is_false_if_previous_track_is_the_same(self):
self.mpris.get_CanControl = lambda *_: True
self.backend.current_playlist.append([Track(uri='a')])
self.backend.playback.repeat = True
self.backend.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertFalse(result)
def test_can_go_previous_is_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.next()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertFalse(result)
def test_can_play_is_true_if_can_control_and_current_track(self):
self.mpris.get_CanControl = lambda *_: True
self.backend.current_playlist.append([Track(uri='a')])
self.backend.playback.play()
self.assertTrue(self.backend.playback.current_track.get())
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
self.assertTrue(result)
def test_can_play_is_false_if_no_current_track(self):
self.mpris.get_CanControl = lambda *_: True
self.assertFalse(self.backend.playback.current_track.get())
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
self.assertFalse(result)
def test_can_play_if_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
self.assertFalse(result)
def test_can_pause_is_true_if_can_control_and_track_can_be_paused(self):
self.mpris.get_CanControl = lambda *_: True
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPause')
self.assertTrue(result)
def test_can_pause_if_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPause')
self.assertFalse(result)
def test_can_seek_is_true_if_can_control_is_true(self):
self.mpris.get_CanControl = lambda *_: True
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanSeek')
self.assertTrue(result)
def test_can_seek_is_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanSeek')
self.assertFalse(result)
def test_can_control_is_true(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanControl')
self.assertTrue(result)
def test_next_is_ignored_if_can_go_next_is_false(self):
self.mpris.get_CanGoNext = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.mpris.Next()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Next()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
def test_next_when_at_end_of_list_should_stop_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.next()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Next()
self.assertEquals(self.backend.playback.state.get(), STOPPED)
def test_next_when_paused_should_skip_to_next_track_and_stay_paused(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.pause()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.assertEquals(self.backend.playback.state.get(), PAUSED)
self.mpris.Next()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
self.assertEquals(self.backend.playback.state.get(), PAUSED)
def test_next_when_stopped_should_skip_to_next_track_and_stay_stopped(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.stop()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.assertEquals(self.backend.playback.state.get(), STOPPED)
self.mpris.Next()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
self.assertEquals(self.backend.playback.state.get(), STOPPED)
def test_previous_is_ignored_if_can_go_previous_is_false(self):
self.mpris.get_CanGoPrevious = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.next()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
self.mpris.Previous()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
def test_previous_when_playing_should_skip_to_prev_track_and_keep_playing(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.next()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Previous()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
def test_previous_when_at_start_of_list_should_stop_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Previous()
self.assertEquals(self.backend.playback.state.get(), STOPPED)
def test_previous_when_paused_should_skip_to_previous_track_and_stay_paused(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.next()
self.backend.playback.pause()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
self.assertEquals(self.backend.playback.state.get(), PAUSED)
self.mpris.Previous()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.assertEquals(self.backend.playback.state.get(), PAUSED)
def test_previous_when_stopped_should_skip_to_previous_track_and_stay_stopped(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.next()
self.backend.playback.stop()
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
self.assertEquals(self.backend.playback.state.get(), STOPPED)
self.mpris.Previous()
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.assertEquals(self.backend.playback.state.get(), STOPPED)
def test_pause_is_ignored_if_can_pause_is_false(self):
self.mpris.get_CanPause = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Pause()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
def test_pause_when_playing_should_pause_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Pause()
self.assertEquals(self.backend.playback.state.get(), PAUSED)
def test_pause_when_paused_has_no_effect(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.pause()
self.assertEquals(self.backend.playback.state.get(), PAUSED)
self.mpris.Pause()
self.assertEquals(self.backend.playback.state.get(), PAUSED)
def test_playpause_is_ignored_if_can_pause_is_false(self):
self.mpris.get_CanPause = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.PlayPause()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
def test_playpause_when_playing_should_pause_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.PlayPause()
self.assertEquals(self.backend.playback.state.get(), PAUSED)
def test_playpause_when_paused_should_resume_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.pause()
self.assertEquals(self.backend.playback.state.get(), PAUSED)
at_pause = self.backend.playback.time_position.get()
self.assert_(at_pause >= 0)
self.mpris.PlayPause()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
after_pause = self.backend.playback.time_position.get()
self.assert_(after_pause >= at_pause)
def test_playpause_when_stopped_should_start_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEquals(self.backend.playback.state.get(), STOPPED)
self.mpris.PlayPause()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
def test_stop_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Stop()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
def test_stop_when_playing_should_stop_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.mpris.Stop()
self.assertEquals(self.backend.playback.state.get(), STOPPED)
def test_stop_when_paused_should_stop_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.pause()
self.assertEquals(self.backend.playback.state.get(), PAUSED)
self.mpris.Stop()
self.assertEquals(self.backend.playback.state.get(), STOPPED)
def test_play_is_ignored_if_can_play_is_false(self):
self.mpris.get_CanPlay = lambda *_: False
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEquals(self.backend.playback.state.get(), STOPPED)
self.mpris.Play()
self.assertEquals(self.backend.playback.state.get(), STOPPED)
def test_play_when_stopped_starts_playback(self):
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEquals(self.backend.playback.state.get(), STOPPED)
self.mpris.Play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
def test_play_after_pause_resumes_from_same_position(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
before_pause = self.backend.playback.time_position.get()
self.assert_(before_pause >= 0)
self.mpris.Pause()
self.assertEquals(self.backend.playback.state.get(), PAUSED)
at_pause = self.backend.playback.time_position.get()
self.assert_(at_pause >= before_pause)
self.mpris.Play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
after_pause = self.backend.playback.time_position.get()
self.assert_(after_pause >= at_pause)
def test_play_when_there_is_no_track_has_no_effect(self):
self.backend.current_playlist.clear()
self.assertEquals(self.backend.playback.state.get(), STOPPED)
self.mpris.Play()
self.assertEquals(self.backend.playback.state.get(), STOPPED)
def test_seek_is_ignored_if_can_seek_is_false(self):
self.mpris.get_CanSeek = lambda *_: False
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
before_seek = self.backend.playback.time_position.get()
self.assert_(before_seek >= 0)
milliseconds_to_seek = 10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
after_seek = self.backend.playback.time_position.get()
self.assert_(before_seek <= after_seek < (
before_seek + milliseconds_to_seek))
def test_seek_seeks_given_microseconds_forward_in_the_current_track(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
before_seek = self.backend.playback.time_position.get()
self.assert_(before_seek >= 0)
milliseconds_to_seek = 10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
after_seek = self.backend.playback.time_position.get()
self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
def test_seek_seeks_given_microseconds_backward_if_negative(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
self.backend.playback.seek(20000)
before_seek = self.backend.playback.time_position.get()
self.assert_(before_seek >= 20000)
milliseconds_to_seek = -10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
after_seek = self.backend.playback.time_position.get()
self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
self.assert_(after_seek < before_seek)
def test_seek_seeks_to_start_of_track_if_new_position_is_negative(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
self.backend.playback.seek(20000)
before_seek = self.backend.playback.time_position.get()
self.assert_(before_seek >= 20000)
milliseconds_to_seek = -30000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
after_seek = self.backend.playback.time_position.get()
self.assert_(after_seek >= (before_seek + milliseconds_to_seek))
self.assert_(after_seek < before_seek)
self.assert_(after_seek >= 0)
def test_seek_skips_to_next_track_if_new_position_larger_than_track_length(self):
self.backend.current_playlist.append([Track(uri='a', length=40000),
Track(uri='b')])
self.backend.playback.play()
self.backend.playback.seek(20000)
before_seek = self.backend.playback.time_position.get()
self.assert_(before_seek >= 20000)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
milliseconds_to_seek = 50000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'b')
after_seek = self.backend.playback.time_position.get()
self.assert_(after_seek >= 0)
self.assert_(after_seek < before_seek)
def test_set_position_is_ignored_if_can_seek_is_false(self):
self.mpris.get_CanSeek = lambda *_: False
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
before_set_position = self.backend.playback.time_position.get()
self.assert_(before_set_position <= 5000)
track_id = 'a'
position_to_set_in_milliseconds = 20000
position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
after_set_position = self.backend.playback.time_position.get()
self.assert_(before_set_position <= after_set_position <
position_to_set_in_milliseconds)
def test_set_position_sets_the_current_track_position_in_microsecs(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
before_set_position = self.backend.playback.time_position.get()
self.assert_(before_set_position <= 5000)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
track_id = '/com/mopidy/track/0'
position_to_set_in_milliseconds = 20000
position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
after_set_position = self.backend.playback.time_position.get()
self.assert_(after_set_position >= position_to_set_in_milliseconds)
def test_set_position_does_nothing_if_the_position_is_negative(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
self.backend.playback.seek(20000)
before_set_position = self.backend.playback.time_position.get()
self.assert_(before_set_position >= 20000)
self.assert_(before_set_position <= 25000)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
track_id = '/com/mopidy/track/0'
position_to_set_in_milliseconds = -1000
position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
after_set_position = self.backend.playback.time_position.get()
self.assert_(after_set_position >= before_set_position)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
def test_set_position_does_nothing_if_position_is_larger_than_track_length(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
self.backend.playback.seek(20000)
before_set_position = self.backend.playback.time_position.get()
self.assert_(before_set_position >= 20000)
self.assert_(before_set_position <= 25000)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
track_id = 'a'
position_to_set_in_milliseconds = 50000
position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
after_set_position = self.backend.playback.time_position.get()
self.assert_(after_set_position >= before_set_position)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
def test_set_position_does_nothing_if_track_id_does_not_match_current_track(self):
self.backend.current_playlist.append([Track(uri='a', length=40000)])
self.backend.playback.play()
self.backend.playback.seek(20000)
before_set_position = self.backend.playback.time_position.get()
self.assert_(before_set_position >= 20000)
self.assert_(before_set_position <= 25000)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
track_id = 'b'
position_to_set_in_milliseconds = 0
position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microseconds)
after_set_position = self.backend.playback.time_position.get()
self.assert_(after_set_position >= before_set_position)
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
def test_open_uri_is_ignored_if_can_play_is_false(self):
self.mpris.get_CanPlay = lambda *_: False
self.backend.library.provider.dummy_library = [
Track(uri='dummy:/test/uri')]
self.mpris.OpenUri('dummy:/test/uri')
self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0)
def test_open_uri_ignores_uris_with_unknown_uri_scheme(self):
self.assertListEqual(self.backend.uri_schemes.get(), ['dummy'])
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.provider.dummy_library = [
Track(uri='notdummy:/test/uri')]
self.mpris.OpenUri('notdummy:/test/uri')
self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0)
def test_open_uri_adds_uri_to_current_playlist(self):
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.provider.dummy_library = [
Track(uri='dummy:/test/uri')]
self.mpris.OpenUri('dummy:/test/uri')
self.assertEquals(self.backend.current_playlist.tracks.get()[0].uri,
'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_stopped(self):
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.provider.dummy_library = [
Track(uri='dummy:/test/uri')]
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.assertEquals(self.backend.playback.state.get(), STOPPED)
self.mpris.OpenUri('dummy:/test/uri')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri,
'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_paused(self):
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.provider.dummy_library = [
Track(uri='dummy:/test/uri')]
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.backend.playback.pause()
self.assertEquals(self.backend.playback.state.get(), PAUSED)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.mpris.OpenUri('dummy:/test/uri')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri,
'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_playing(self):
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.provider.dummy_library = [
Track(uri='dummy:/test/uri')]
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
self.backend.playback.play()
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri, 'a')
self.mpris.OpenUri('dummy:/test/uri')
self.assertEquals(self.backend.playback.state.get(), PLAYING)
self.assertEquals(self.backend.playback.current_track.get().uri,
'dummy:/test/uri')

View File

@ -0,0 +1,63 @@
import mock
from mopidy import settings
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpris import objects
from tests import unittest
class RootInterfaceTest(unittest.TestCase):
def setUp(self):
objects.exit_process = mock.Mock()
objects.MprisObject._connect_to_dbus = mock.Mock()
self.backend = DummyBackend.start().proxy()
self.mpris = objects.MprisObject()
def tearDown(self):
self.backend.stop()
def test_constructor_connects_to_dbus(self):
self.assert_(self.mpris._connect_to_dbus.called)
def test_can_raise_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'CanRaise')
self.assertFalse(result)
def test_raise_does_nothing(self):
self.mpris.Raise()
def test_can_quit_returns_true(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'CanQuit')
self.assertTrue(result)
def test_quit_should_stop_all_actors(self):
self.mpris.Quit()
self.assert_(objects.exit_process.called)
def test_has_track_list_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'HasTrackList')
self.assertFalse(result)
def test_identify_is_mopidy(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'Identity')
self.assertEquals(result, 'Mopidy')
def test_desktop_entry_is_mopidy(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
self.assertEquals(result, 'mopidy')
def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self):
settings.runtime['DESKTOP_FILE'] = '/tmp/foo.desktop'
result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
self.assertEquals(result, 'foo')
settings.runtime.clear()
def test_supported_uri_schemes_is_empty(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes')
self.assertEquals(len(result), 1)
self.assertEquals(result[0], 'dummy')
def test_supported_mime_types_is_empty(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedMimeTypes')
self.assertEquals(len(result), 0)

View File

@ -1,21 +1,16 @@
import multiprocessing
import unittest
from tests import SkipTest
# FIXME Our Windows build server does not support GStreamer yet
import sys
if sys.platform == 'win32':
raise SkipTest
from mopidy import settings
from mopidy.gstreamer import GStreamer
from mopidy.utils.path import path_to_uri
from tests import path_to_data_dir
from tests import unittest, path_to_data_dir
# TODO BaseOutputTest?
@unittest.skipIf(sys.platform == 'win32',
'Our Windows build server does not support GStreamer yet')
class GStreamerTest(unittest.TestCase):
def setUp(self):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
@ -48,11 +43,11 @@ class GStreamerTest(unittest.TestCase):
self.gstreamer.start_playback()
self.assertTrue(self.gstreamer.stop_playback())
@SkipTest
@unittest.SkipTest
def test_deliver_data(self):
pass # TODO
@SkipTest
@unittest.SkipTest
def test_end_of_data_stream(self):
pass # TODO
@ -71,10 +66,10 @@ class GStreamerTest(unittest.TestCase):
self.assertTrue(self.gstreamer.set_volume(100))
self.assertEqual(100, self.gstreamer.get_volume())
@SkipTest
@unittest.SkipTest
def test_set_state_encapsulation(self):
pass # TODO
@SkipTest
@unittest.SkipTest
def test_set_position(self):
pass # TODO

View File

@ -1,10 +1,12 @@
import os
import subprocess
import sys
import unittest
import mopidy
from tests import unittest
class HelpTest(unittest.TestCase):
def test_help_has_mopidy_options(self):
mopidy_dir = os.path.dirname(mopidy.__file__)

36
tests/listeners_test.py Normal file
View File

@ -0,0 +1,36 @@
from mopidy.listeners import BackendListener
from mopidy.models import Track
from tests import unittest
class BackendListenerTest(unittest.TestCase):
def setUp(self):
self.listener = BackendListener()
def test_listener_has_default_impl_for_track_playback_paused(self):
self.listener.track_playback_paused(Track(), 0)
def test_listener_has_default_impl_for_track_playback_resumed(self):
self.listener.track_playback_resumed(Track(), 0)
def test_listener_has_default_impl_for_track_playback_started(self):
self.listener.track_playback_started(Track())
def test_listener_has_default_impl_for_track_playback_ended(self):
self.listener.track_playback_ended(Track(), 0)
def test_listener_has_default_impl_for_playback_state_changed(self):
self.listener.playback_state_changed()
def test_listener_has_default_impl_for_playlist_changed(self):
self.listener.playlist_changed()
def test_listener_has_default_impl_for_options_changed(self):
self.listener.options_changed()
def test_listener_has_default_impl_for_volume_changed(self):
self.listener.volume_changed()
def test_listener_has_default_impl_for_seeked(self):
self.listener.seeked()

View File

@ -1,8 +1,9 @@
import unittest
from mopidy.mixers.denon import DenonMixer
from tests.mixers.base_test import BaseMixerTest
from tests import unittest
class DenonMixerDeviceMock(object):
def __init__(self):
self._open = True
@ -24,6 +25,7 @@ class DenonMixerDeviceMock(object):
def open(self):
self._open = True
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
ACTUAL_MAX = 99
INITIAL = 1

View File

@ -1,8 +1,9 @@
import unittest
from mopidy.mixers.dummy import DummyMixer
from tests import unittest
from tests.mixers.base_test import BaseMixerTest
class DenonMixerTest(BaseMixerTest, unittest.TestCase):
mixer_class = DummyMixer

View File

@ -1,9 +1,9 @@
import datetime as dt
import unittest
import datetime
from mopidy.models import Artist, Album, CpTrack, Track, Playlist
from tests import SkipTest
from tests import unittest
class GenericCopyTets(unittest.TestCase):
def compare(self, orig, other):
@ -49,6 +49,7 @@ class GenericCopyTets(unittest.TestCase):
test = lambda: Track().copy(invalid_key=True)
self.assertRaises(TypeError, test)
class ArtistTest(unittest.TestCase):
def test_uri(self):
uri = u'an_uri'
@ -321,7 +322,7 @@ class TrackTest(unittest.TestCase):
self.assertRaises(AttributeError, setattr, track, 'track_no', None)
def test_date(self):
date = dt.date(1977, 1, 1)
date = datetime.date(1977, 1, 1)
track = Track(date=date)
self.assertEqual(track.date, date)
self.assertRaises(AttributeError, setattr, track, 'date', None)
@ -400,7 +401,7 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2))
def test_eq_date(self):
date = dt.date.today()
date = datetime.date.today()
track1 = Track(date=date)
track2 = Track(date=date)
self.assertEqual(track1, track2)
@ -425,7 +426,7 @@ class TrackTest(unittest.TestCase):
self.assertEqual(hash(track1), hash(track2))
def test_eq(self):
date = dt.date.today()
date = datetime.date.today()
artists = [Artist()]
album = Album()
track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album,
@ -474,8 +475,8 @@ class TrackTest(unittest.TestCase):
self.assertNotEqual(hash(track1), hash(track2))
def test_ne_date(self):
track1 = Track(date=dt.date.today())
track2 = Track(date=dt.date.today()-dt.timedelta(days=1))
track1 = Track(date=datetime.date.today())
track2 = Track(date=datetime.date.today()-datetime.timedelta(days=1))
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
@ -500,11 +501,11 @@ class TrackTest(unittest.TestCase):
def test_ne(self):
track1 = Track(uri=u'uri1', name=u'name1',
artists=[Artist(name=u'name1')], album=Album(name=u'name1'),
track_no=1, date=dt.date.today(), length=100, bitrate=100,
track_no=1, date=datetime.date.today(), length=100, bitrate=100,
musicbrainz_id='id1')
track2 = Track(uri=u'uri2', name=u'name2',
artists=[Artist(name=u'name2')], album=Album(name=u'name2'),
track_no=2, date=dt.date.today()-dt.timedelta(days=1),
track_no=2, date=datetime.date.today()-datetime.timedelta(days=1),
length=200, bitrate=200, musicbrainz_id='id2')
self.assertNotEqual(track1, track2)
self.assertNotEqual(hash(track1), hash(track2))
@ -535,7 +536,7 @@ class PlaylistTest(unittest.TestCase):
self.assertEqual(playlist.length, 3)
def test_last_modified(self):
last_modified = dt.datetime.now()
last_modified = datetime.datetime.now()
playlist = Playlist(last_modified=last_modified)
self.assertEqual(playlist.last_modified, last_modified)
self.assertRaises(AttributeError, setattr, playlist, 'last_modified',
@ -543,7 +544,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_uri(self):
tracks = [Track()]
last_modified = dt.datetime.now()
last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(uri=u'another uri')
@ -554,7 +555,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_name(self):
tracks = [Track()]
last_modified = dt.datetime.now()
last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(name=u'another name')
@ -565,7 +566,7 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_tracks(self):
tracks = [Track()]
last_modified = dt.datetime.now()
last_modified = datetime.datetime.now()
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_tracks = [Track(), Track()]
@ -577,8 +578,8 @@ class PlaylistTest(unittest.TestCase):
def test_with_new_last_modified(self):
tracks = [Track()]
last_modified = dt.datetime.now()
new_last_modified = last_modified + dt.timedelta(1)
last_modified = datetime.datetime.now()
new_last_modified = last_modified + datetime.timedelta(1)
playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks,
last_modified=last_modified)
new_playlist = playlist.copy(last_modified=new_last_modified)

View File

@ -1,10 +1,10 @@
import unittest
from datetime import date
from mopidy.scanner import Scanner, translator
from mopidy.models import Track, Artist, Album
from tests import path_to_data_dir, SkipTest
from tests import unittest, path_to_data_dir
class FakeGstDate(object):
def __init__(self, year, month, day):
@ -12,6 +12,7 @@ class FakeGstDate(object):
self.month = month
self.day = day
class TranslatorTest(unittest.TestCase):
def setUp(self):
self.data = {
@ -126,6 +127,7 @@ class TranslatorTest(unittest.TestCase):
del self.track['date']
self.check()
class ScannerTest(unittest.TestCase):
def setUp(self):
self.errors = {}
@ -185,6 +187,6 @@ class ScannerTest(unittest.TestCase):
self.scan('scanner/image')
self.assert_(self.errors)
@SkipTest
@unittest.SkipTest
def test_song_without_time_is_handeled(self):
pass

Some files were not shown because too many files have changed in this diff Show More