Release v0.6.0
This commit is contained in:
commit
da79303e84
@ -8,3 +8,4 @@ TryExec=mopidy
|
||||
Exec=mopidy
|
||||
Terminal=true
|
||||
Categories=AudioVideo;Audio;Player;ConsoleOnly;
|
||||
StartupNotify=true
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
7
docs/api/listeners.rst
Normal file
@ -0,0 +1,7 @@
|
||||
************
|
||||
Listener API
|
||||
************
|
||||
|
||||
.. automodule:: mopidy.listeners
|
||||
:synopsis: Listener API
|
||||
:members:
|
||||
@ -30,7 +30,6 @@ methods as described below.
|
||||
.. automodule:: mopidy.mixers.base
|
||||
:synopsis: Mixer API
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Mixer implementations
|
||||
|
||||
@ -25,4 +25,3 @@ Data model API
|
||||
.. automodule:: mopidy.models
|
||||
:synopsis: Data model API
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
7
docs/modules/frontends/mpris.rst
Normal file
7
docs/modules/frontends/mpris.rst
Normal file
@ -0,0 +1,7 @@
|
||||
***********************************************
|
||||
:mod:`mopidy.frontends.mpris` -- MPRIS frontend
|
||||
***********************************************
|
||||
|
||||
.. automodule:: mopidy.frontends.mpris
|
||||
:synopsis: MPRIS frontend
|
||||
:members:
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
class BaseFrontend(object):
|
||||
"""
|
||||
Base class for frontends.
|
||||
"""
|
||||
pass
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()]
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
130
mopidy/frontends/mpris/__init__.py
Normal file
130
mopidy/frontends/mpris/__init__.py
Normal 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'))
|
||||
436
mopidy/frontends/mpris/objects.py
Normal file
436
mopidy/frontends/mpris/objects.py
Normal 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
|
||||
@ -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
116
mopidy/listeners.py
Normal 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
|
||||
@ -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')
|
||||
|
||||
@ -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.
|
||||
#:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1 +1 @@
|
||||
Pykka >= 0.12
|
||||
Pykka >= 0.12.3
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
coverage
|
||||
mock
|
||||
mock >= 0.7
|
||||
nose
|
||||
tox
|
||||
yappi
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import nose
|
||||
import yappi
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
yappi.start()
|
||||
nose.main()
|
||||
finally:
|
||||
yappi.print_stats()
|
||||
|
||||
@ -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 = []
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
53
tests/backends/events_test.py
Normal file
53
tests/backends/events_test.py
Normal 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')
|
||||
@ -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):
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
62
tests/frontends/mpd/protocol/__init__.py
Normal file
62
tests/frontends/mpd/protocol/__init__.py
Normal 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])
|
||||
18
tests/frontends/mpd/protocol/audio_output_test.py
Normal file
18
tests/frontends/mpd/protocol/audio_output_test.py
Normal 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')
|
||||
@ -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')
|
||||
54
tests/frontends/mpd/protocol/command_list_test.py
Normal file
54
tests/frontends/mpd/protocol/command_list_test.py
Normal 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.
|
||||
44
tests/frontends/mpd/protocol/connection_test.py
Normal file
44
tests/frontends/mpd/protocol/connection_test.py
Normal 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')
|
||||
@ -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')
|
||||
206
tests/frontends/mpd/protocol/idle_test.py
Normal file
206
tests/frontends/mpd/protocol/idle_test.py
Normal 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')
|
||||
344
tests/frontends/mpd/protocol/music_db_test.py
Normal file
344
tests/frontends/mpd/protocol/music_db_test.py
Normal 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')
|
||||
@ -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')
|
||||
67
tests/frontends/mpd/protocol/reflection_test.py
Normal file
67
tests/frontends/mpd/protocol/reflection_test.py
Normal 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')
|
||||
148
tests/frontends/mpd/protocol/regression_test.py
Normal file
148
tests/frontends/mpd/protocol/regression_test.py
Normal 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')
|
||||
37
tests/frontends/mpd/protocol/status_test.py
Normal file
37
tests/frontends/mpd/protocol/status_test.py
Normal 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')
|
||||
33
tests/frontends/mpd/protocol/stickers_test.py
Normal file
33
tests/frontends/mpd/protocol/stickers_test.py
Normal 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')
|
||||
94
tests/frontends/mpd/protocol/stored_playlists_test.py
Normal file
94
tests/frontends/mpd/protocol/stored_playlists_test.py
Normal 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')
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
@ -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)])
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
0
tests/frontends/mpris/__init__.py
Normal file
0
tests/frontends/mpris/__init__.py
Normal file
70
tests/frontends/mpris/events_test.py
Normal file
70
tests/frontends/mpris/events_test.py
Normal 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)
|
||||
826
tests/frontends/mpris/player_interface_test.py
Normal file
826
tests/frontends/mpris/player_interface_test.py
Normal 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')
|
||||
63
tests/frontends/mpris/root_interface_test.py
Normal file
63
tests/frontends/mpris/root_interface_test.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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
36
tests/listeners_test.py
Normal 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()
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user