diff --git a/README.rst b/README.rst
index 6855135e..c6187119 100644
--- a/README.rst
+++ b/README.rst
@@ -11,8 +11,9 @@ platforms, including Windows, Mac OS X, Linux, and iPhone and Android phones.
To install Mopidy, check out
`the installation docs `_.
-* `Documentation `_
+* `Documentation (latest release) `_
* `Documentation (development version) `_
* `Source code `_
* `Issue tracker `_
* IRC: ``#mopidy`` at `irc.freenode.net `_
+* `Download development snapshot `_
diff --git a/docs/changes.rst b/docs/changes.rst
index dadbb6b9..ed05050c 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -27,9 +27,22 @@ No description yet.
:attr:`mopidy.settings.DEBUG_LOG_FILENAME`.
- Switched from using subprocesses to threads. This partly fixes the OS X
support. See :issue:`14` for details.
-- MPD frontend:
-
- - ``add ""`` and ``addid ""`` now behaves as expected.
+- MPD command ``list`` now supports queries by artist, album name, and date, as
+ used by e.g. the Ario client. (Fixes: :issue:`20`)
+- MPD command ``add ""`` and ``addid ""`` now behaves as expected. (Fixes
+ :issue:`16`)
+- Fix wrong behavior on end of track and next after random mode has been used.
+ (Fixes: :issue:`18`)
+- Fix infinite recursion loop crash on playback of non-playable tracks when in
+ random mode. (Fixes :issue:`17`)
+- Fix assertion error that happened if one removed tracks from the current
+ playlist, while in random mode. (Fixes :issue:`22`)
+- GStreamerOutput: Set ``caps`` on the ``appsrc`` bin before use. This makes
+ sound output work with GStreamer >= 0.10.29, which includes the versions used
+ in Ubuntu 10.10 and on OS X if using Homebrew. (Fixes: :issue:`21`,
+ :issue:`24`, contributes to :issue:`14`)
+- Improved handling of uncaught exceptions in threads. The entire process
+ should now exit immediately.
0.1.0 (2010-08-23)
diff --git a/mopidy/__init__.py b/mopidy/__init__.py
index 7d3052c4..5e1b26de 100644
--- a/mopidy/__init__.py
+++ b/mopidy/__init__.py
@@ -3,7 +3,7 @@ if not (2, 6) <= sys.version_info < (3,):
sys.exit(u'Mopidy requires Python >= 2.6, < 3')
def get_version():
- return u'0.2.0a1'
+ return u'0.2.0'
class MopidyException(Exception):
def __init__(self, message, *args, **kwargs):
diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py
index c8c83a62..34a16369 100644
--- a/mopidy/backends/base/current_playlist.py
+++ b/mopidy/backends/base/current_playlist.py
@@ -12,13 +12,10 @@ class BaseCurrentPlaylistController(object):
:type backend: :class:`BaseBackend`
"""
- #: The current playlist version. Integer which is increased every time the
- #: current playlist is changed. Is not reset before Mopidy is restarted.
- version = 0
-
def __init__(self, backend):
self.backend = backend
self._cp_tracks = []
+ self._version = 0
def destroy(self):
"""Cleanup after component."""
@@ -42,6 +39,19 @@ class BaseCurrentPlaylistController(object):
"""
return [ct[1] for ct in self._cp_tracks]
+ @property
+ def version(self):
+ """
+ The current playlist version. Integer which is increased every time the
+ current playlist is changed. Is not reset before Mopidy is restarted.
+ """
+ return self._version
+
+ @version.setter
+ def version(self, version):
+ self._version = version
+ self.backend.playback.on_current_playlist_change()
+
def add(self, track, at_position=None):
"""
Add the track to the end of, or at the given position in the current
@@ -71,16 +81,13 @@ class BaseCurrentPlaylistController(object):
:param tracks: tracks to append
:type tracks: list of :class:`mopidy.models.Track`
"""
- self.version += 1
for track in tracks:
self.add(track)
- self.backend.playback.on_current_playlist_change()
def clear(self):
"""Clear the current playlist."""
self._cp_tracks = []
self.version += 1
- self.backend.playback.on_current_playlist_change()
def get(self, **criteria):
"""
@@ -146,7 +153,6 @@ class BaseCurrentPlaylistController(object):
to_position += 1
self._cp_tracks = new_cp_tracks
self.version += 1
- self.backend.playback.on_current_playlist_change()
def remove(self, **criteria):
"""
@@ -191,7 +197,6 @@ class BaseCurrentPlaylistController(object):
random.shuffle(shuffled)
self._cp_tracks = before + shuffled + after
self.version += 1
- self.backend.playback.on_current_playlist_change()
def mpd_format(self, *args, **kwargs):
"""Not a part of the generic backend API."""
diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py
index 00676f09..779903a7 100644
--- a/mopidy/backends/base/playback.py
+++ b/mopidy/backends/base/playback.py
@@ -142,7 +142,7 @@ class BasePlaybackController(object):
random.shuffle(self._shuffled)
self._first_shuffle = False
- if self._shuffled:
+ if self.random and self._shuffled:
return self._shuffled[0]
if self.current_cp_track is None:
@@ -195,7 +195,7 @@ class BasePlaybackController(object):
random.shuffle(self._shuffled)
self._first_shuffle = False
- if self._shuffled:
+ if self.random and self._shuffled:
return self._shuffled[0]
if self.current_cp_track is None:
@@ -315,11 +315,8 @@ class BasePlaybackController(object):
if self.cp_track_at_eot:
self._trigger_stopped_playing_event()
self.play(self.cp_track_at_eot)
- if self.random and self.current_cp_track in self._shuffled:
- self._shuffled.remove(self.current_cp_track)
else:
- self.stop()
- self.current_cp_track = None
+ self.stop(clear_current_track=True)
if self.consume:
self.backend.current_playlist.remove(cpid=original_cp_track[0])
@@ -333,13 +330,10 @@ class BasePlaybackController(object):
self._first_shuffle = True
self._shuffled = []
- if not self.backend.current_playlist.cp_tracks:
- self.stop()
- self.current_cp_track = None
- elif (self.current_cp_track not in
+ if (not self.backend.current_playlist.cp_tracks or
+ self.current_cp_track not in
self.backend.current_playlist.cp_tracks):
- self.current_cp_track = None
- self.stop()
+ self.stop(clear_current_track=True)
def next(self):
"""Play the next track."""
@@ -350,11 +344,7 @@ class BasePlaybackController(object):
self._trigger_stopped_playing_event()
self.play(self.cp_track_at_next)
else:
- self.stop()
- self.current_cp_track = None
-
- if self.random and self.current_cp_track in self._shuffled:
- self._shuffled.remove(self.current_cp_track)
+ self.stop(clear_current_track=True)
def pause(self):
"""Pause playback."""
@@ -394,6 +384,9 @@ class BasePlaybackController(object):
self.current_cp_track = cp_track
self.state = self.PLAYING
if not self._play(cp_track[1]):
+ # Track is not playable
+ if self.random and self._shuffled:
+ self._shuffled.remove(cp_track)
if on_error_step == 1:
self.next()
elif on_error_step == -1:
@@ -477,13 +470,21 @@ class BasePlaybackController(object):
"""
raise NotImplementedError
- def stop(self):
- """Stop playing."""
+ def stop(self, clear_current_track=False):
+ """
+ Stop playing.
+
+ :param clear_current_track: whether to clear the current track _after_
+ stopping
+ :type clear_current_track: boolean
+ """
if self.state == self.STOPPED:
return
self._trigger_stopped_playing_event()
if self._stop():
self.state = self.STOPPED
+ if clear_current_track:
+ self.current_cp_track = None
def _stop(self):
"""
diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py
index 6d4f3156..62cbd7e2 100644
--- a/mopidy/backends/dummy/__init__.py
+++ b/mopidy/backends/dummy/__init__.py
@@ -44,16 +44,19 @@ class DummyLibraryController(BaseLibraryController):
class DummyPlaybackController(BasePlaybackController):
def _next(self, track):
- return True
+ """Pass None as track to force failure"""
+ return track is not None
def _pause(self):
return True
def _play(self, track):
- return True
+ """Pass None as track to force failure"""
+ return track is not None
def _previous(self, track):
- return True
+ """Pass None as track to force failure"""
+ return track is not None
def _resume(self):
return True
diff --git a/mopidy/backends/libspotify/library.py b/mopidy/backends/libspotify/library.py
index eb1c24d9..972eaf03 100644
--- a/mopidy/backends/libspotify/library.py
+++ b/mopidy/backends/libspotify/library.py
@@ -6,6 +6,7 @@ from spotify import Link, SpotifyError
from mopidy.backends.base import BaseLibraryController
from mopidy.backends.libspotify import ENCODING
from mopidy.backends.libspotify.translator import LibspotifyTranslator
+from mopidy.models import Playlist
logger = logging.getLogger('mopidy.backends.libspotify.library')
@@ -28,15 +29,27 @@ class LibspotifyLibraryController(BaseLibraryController):
pass # TODO
def search(self, **query):
+ if not query:
+ # Since we can't search for the entire Spotify library, we return
+ # all tracks in the stored playlists when the query is empty.
+ tracks = []
+ for playlist in self.backend.stored_playlists.playlists:
+ tracks += playlist.tracks
+ return Playlist(tracks=tracks)
spotify_query = []
for (field, values) in query.iteritems():
+ if field == u'track':
+ field = u'title'
+ if field == u'date':
+ field = u'year'
if not hasattr(values, '__iter__'):
values = [values]
for value in values:
- if field == u'track':
- field = u'title'
if field == u'any':
spotify_query.append(value)
+ elif field == u'year':
+ value = int(value.split('-')[0]) # Extract year
+ spotify_query.append(u'%s:%d' % (field, value))
else:
spotify_query.append(u'%s:"%s"' % (field, value))
spotify_query = u' '.join(spotify_query)
diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py
index 9554fa3f..7f541236 100644
--- a/mopidy/backends/libspotify/session_manager.py
+++ b/mopidy/backends/libspotify/session_manager.py
@@ -19,12 +19,8 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
def __init__(self, username, password, core_queue, output):
SpotifySessionManager.__init__(self, username, password)
- BaseThread.__init__(self)
+ BaseThread.__init__(self, core_queue)
self.name = 'LibspotifySMThread'
- # Run as a daemon thread, so Mopidy won't wait for this thread to exit
- # before Mopidy exits.
- self.daemon = True
- self.core_queue = core_queue
self.output = output
self.connected = threading.Event()
self.session = None
@@ -69,16 +65,19 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
- # TODO Base caps_string on arguments
+ assert sample_type == 0, u'Expects 16-bit signed integer samples'
capabilites = """
audio/x-raw-int,
endianness=(int)1234,
- channels=(int)2,
+ channels=(int)%(channels)d,
width=(int)16,
depth=(int)16,
- signed=True,
- rate=(int)44100
- """
+ signed=(boolean)true,
+ rate=(int)%(sample_rate)d
+ """ % {
+ 'sample_rate': sample_rate,
+ 'channels': channels,
+ }
self.output.deliver_data(capabilites, bytes(frames))
def play_token_lost(self, session):
@@ -97,7 +96,7 @@ class LibspotifySessionManager(SpotifySessionManager, BaseThread):
def search(self, query, connection):
"""Search method used by Mopidy backend"""
- def callback(results, userdata):
+ def callback(results, userdata=None):
# TODO Include results from results.albums(), etc. too
playlist = Playlist(tracks=[
LibspotifyTranslator.to_mopidy_track(t)
diff --git a/mopidy/core.py b/mopidy/core.py
index 5351e2a5..69760094 100644
--- a/mopidy/core.py
+++ b/mopidy/core.py
@@ -1,20 +1,22 @@
import logging
import multiprocessing
import optparse
+import sys
from mopidy import get_version, settings, OptionalDependencyError
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 BaseProcess
+from mopidy.utils.process import BaseThread
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
-class CoreProcess(BaseProcess):
+class CoreProcess(BaseThread):
def __init__(self):
- super(CoreProcess, self).__init__(name='CoreProcess')
self.core_queue = multiprocessing.Queue()
+ super(CoreProcess, self).__init__(self.core_queue)
+ self.name = 'CoreProcess'
self.options = self.parse_options()
self.output = None
self.backend = None
@@ -79,7 +81,9 @@ class CoreProcess(BaseProcess):
return frontends
def process_message(self, message):
- if message.get('to') == 'output':
+ if message.get('to') == 'core':
+ self.process_message_to_core(message)
+ elif message.get('to') == 'output':
self.output.process_message(message)
elif message.get('to') == 'frontend':
for frontend in self.frontends:
@@ -92,3 +96,12 @@ class CoreProcess(BaseProcess):
self.backend.stored_playlists.playlists = message['playlists']
else:
logger.warning(u'Cannot handle message: %s', message)
+
+ def process_message_to_core(self, message):
+ assert message['to'] == 'core', u'Message recipient must be "core".'
+ if message['command'] == 'exit':
+ if message['reason'] is not None:
+ logger.info(u'Exiting (%s)', message['reason'])
+ sys.exit(message['status'])
+ else:
+ logger.warning(u'Cannot handle message: %s', message)
diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py
index bba69a5b..e91dd272 100644
--- a/mopidy/frontends/lastfm.py
+++ b/mopidy/frontends/lastfm.py
@@ -45,7 +45,7 @@ class LastfmFrontend(BaseFrontend):
def __init__(self, *args, **kwargs):
super(LastfmFrontend, self).__init__(*args, **kwargs)
(self.connection, other_end) = multiprocessing.Pipe()
- self.thread = LastfmFrontendThread(other_end)
+ self.thread = LastfmFrontendThread(self.core_queue, other_end)
def start(self):
self.thread.start()
@@ -58,10 +58,9 @@ class LastfmFrontend(BaseFrontend):
class LastfmFrontendThread(BaseThread):
- def __init__(self, connection):
- super(LastfmFrontendThread, self).__init__()
+ def __init__(self, core_queue, connection):
+ super(LastfmFrontendThread, self).__init__(core_queue)
self.name = u'LastfmFrontendThread'
- self.daemon = True
self.connection = connection
self.lastfm = None
self.scrobbler = None
@@ -84,7 +83,7 @@ class LastfmFrontendThread(BaseThread):
CLIENT_ID, CLIENT_VERSION)
logger.info(u'Connected to Last.fm')
except SettingsError as e:
- logger.info(u'Last.fm scrobbler did not start.')
+ logger.info(u'Last.fm scrobbler not started')
logger.debug(u'Last.fm settings error: %s', e)
except (pylast.WSError, socket.error) as e:
logger.error(u'Last.fm connection error: %s', e)
diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py
index d4dcf50d..fb3a3a09 100644
--- a/mopidy/frontends/mpd/protocol/music_db.py
+++ b/mopidy/frontends/mpd/protocol/music_db.py
@@ -1,7 +1,8 @@
import re
+import shlex
from mopidy.frontends.mpd.protocol import handle_pattern, stored_playlists
-from mopidy.frontends.mpd.exceptions import MpdNotImplemented
+from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
def _build_query(mpd_query):
"""
@@ -81,13 +82,9 @@ def findadd(frontend, query):
# TODO Add result to current playlist
#result = frontend.find(query)
-@handle_pattern(r'^list (?P[Aa]rtist)$')
-@handle_pattern(r'^list "(?P[Aa]rtist)"$')
-@handle_pattern(r'^list (?Palbum( artist)?)'
- '( "(?P[^"]+)")*$')
-@handle_pattern(r'^list "(?Palbum(" "artist)?)"'
- '( "(?P[^"]+)")*$')
-def list_(frontend, field, artist=None):
+@handle_pattern(r'^list "?(?P([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
+ '( (?P.*))?$')
+def list_(frontend, field, mpd_query=None):
"""
*musicpd.org, music database section:*
@@ -101,22 +98,70 @@ def list_(frontend, field, artist=None):
This filters the result list by an artist.
+ *Clarifications:*
+
+ The musicpd.org documentation for ``list`` is far from complete. The
+ command also supports the following variant:
+
+ ``list {TYPE} {QUERY}``
+
+ Where ``QUERY`` applies to all ``TYPE``. ``QUERY`` is one or more pairs
+ of a field name and a value. If the ``QUERY`` consists of more than one
+ pair, the pairs are AND-ed together to find the result. Examples of
+ valid queries and what they should return:
+
+ ``list "artist" "artist" "ABBA"``
+ List artists where the artist name is "ABBA". Response::
+
+ Artist: ABBA
+ OK
+
+ ``list "album" "artist" "ABBA"``
+ Lists albums where the artist name is "ABBA". Response::
+
+ Album: More ABBA Gold: More ABBA Hits
+ Album: Absolute More Christmas
+ Album: Gold: Greatest Hits
+ OK
+
+ ``list "artist" "album" "Gold: Greatest Hits"``
+ Lists artists where the album name is "Gold: Greatest Hits".
+ Response::
+
+ Artist: ABBA
+ OK
+
+ ``list "artist" "artist" "ABBA" "artist" "TLC"``
+ Lists artists where the artist name is "ABBA" *and* "TLC". Should
+ never match anything. Response::
+
+ OK
+
+ ``list "date" "artist" "ABBA"``
+ Lists dates where artist name is "ABBA". Response::
+
+ Date:
+ Date: 1992
+ Date: 1993
+ OK
+
+ ``list "date" "artist" "ABBA" "album" "Gold: Greatest Hits"``
+ Lists dates where artist name is "ABBA" and album name is "Gold:
+ Greatest Hits". Response::
+
+ Date: 1992
+ OK
+
+ ``list "genre" "artist" "The Rolling Stones"``
+ Lists genres where artist name is "The Rolling Stones". Response::
+
+ Genre:
+ Genre: Rock
+ OK
+
*GMPC:*
- does not add quotes around the field argument.
- - asks for "list artist" to get available artists and will not query
- for artist/album information if this is not retrived
- - asks for multiple fields, i.e.::
-
- list album artist "an artist name"
-
- returns the albums available for the asked artist::
-
- list album artist "Tiesto"
- Album: Radio Trance Vol 4-Promo-CD
- Album: Ur A Tear in the Open CDR
- Album: Simple Trance 2004 Step One
- Album: In Concert 05-10-2003
*ncmpc:*
@@ -124,31 +169,70 @@ def list_(frontend, field, artist=None):
- capitalizes the field argument.
"""
field = field.lower()
+ query = _list_build_query(field, mpd_query)
if field == u'artist':
- return _list_artist(frontend)
- elif field == u'album artist':
- return _list_album_artist(frontend, artist)
- # TODO More to implement
+ return _list_artist(frontend, query)
+ elif field == u'album':
+ return _list_album(frontend, query)
+ elif field == u'date':
+ return _list_date(frontend, query)
+ elif field == u'genre':
+ pass # TODO We don't have genre in our internal data structures yet
-def _list_artist(frontend):
- """
- Since we don't know exactly all available artists, we respond with
- the artists we know for sure, which is all artists in our stored playlists.
- """
+def _list_build_query(field, mpd_query):
+ """Converts a ``list`` query to a Mopidy query."""
+ if mpd_query is None:
+ return {}
+ # shlex does not seem to be friends with unicode objects
+ tokens = shlex.split(mpd_query.encode('utf-8'))
+ tokens = [t.decode('utf-8') for t in tokens]
+ if len(tokens) == 1:
+ if field == u'album':
+ return {'artist': [tokens[0]]}
+ else:
+ raise MpdArgError(
+ u'should be "Album" for 3 arguments', command=u'list')
+ elif len(tokens) % 2 == 0:
+ query = {}
+ while tokens:
+ key = tokens[0].lower()
+ key = str(key) # Needed for kwargs keys on OS X and Windows
+ value = tokens[1]
+ tokens = tokens[2:]
+ if key not in (u'artist', u'album', u'date', u'genre'):
+ raise MpdArgError(u'not able to parse args', command=u'list')
+ if key in query:
+ query[key].append(value)
+ else:
+ query[key] = [value]
+ return query
+ else:
+ raise MpdArgError(u'not able to parse args', command=u'list')
+
+def _list_artist(frontend, query):
artists = set()
- for playlist in frontend.backend.stored_playlists.playlists:
- for track in playlist.tracks:
- for artist in track.artists:
- artists.add((u'Artist', artist.name))
+ playlist = frontend.backend.library.find_exact(**query)
+ for track in playlist.tracks:
+ for artist in track.artists:
+ artists.add((u'Artist', artist.name))
return artists
-def _list_album_artist(frontend, artist):
- playlist = frontend.backend.library.find_exact(artist=[artist])
+def _list_album(frontend, query):
albums = set()
+ playlist = frontend.backend.library.find_exact(**query)
for track in playlist.tracks:
- albums.add((u'Album', track.album.name))
+ if track.album is not None:
+ albums.add((u'Album', track.album.name))
return albums
+def _list_date(frontend, query):
+ dates = set()
+ playlist = frontend.backend.library.find_exact(**query)
+ for track in playlist.tracks:
+ if track.date is not None:
+ dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
+ return dates
+
@handle_pattern(r'^listall "(?P[^"]+)"')
def listall(frontend, uri):
"""
diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py
index 4381fe0a..7caf21f9 100644
--- a/mopidy/frontends/mpd/server.py
+++ b/mopidy/frontends/mpd/server.py
@@ -24,6 +24,9 @@ class MpdServer(asyncore.dispatcher):
try:
if socket.has_ipv6:
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
+ # Explicitly configure socket to work for both IPv4 and IPv6
+ self.socket.setsockopt(
+ socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
else:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
diff --git a/mopidy/frontends/mpd/thread.py b/mopidy/frontends/mpd/thread.py
index 0fb048ec..0ad5ee68 100644
--- a/mopidy/frontends/mpd/thread.py
+++ b/mopidy/frontends/mpd/thread.py
@@ -8,10 +8,8 @@ logger = logging.getLogger('mopidy.frontends.mpd.thread')
class MpdThread(BaseThread):
def __init__(self, core_queue):
- super(MpdThread, self).__init__()
+ super(MpdThread, self).__init__(core_queue)
self.name = u'MpdThread'
- self.daemon = True
- self.core_queue = core_queue
def run_inside_try(self):
logger.debug(u'Starting MPD server thread')
diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py
index 929d2e1d..7a8f006e 100644
--- a/mopidy/mixers/nad.py
+++ b/mopidy/mixers/nad.py
@@ -4,7 +4,7 @@ from multiprocessing import Pipe
from mopidy import settings
from mopidy.mixers import BaseMixer
-from mopidy.utils.process import BaseProcess
+from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.mixers.nad')
@@ -50,7 +50,7 @@ class NadMixer(BaseMixer):
self._pipe.send({'command': 'set_volume', 'volume': volume})
-class NadTalker(BaseProcess):
+class NadTalker(BaseThread):
"""
Independent process which does the communication with the NAD device.
diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py
index 346f6254..3714fed6 100644
--- a/mopidy/outputs/gstreamer.py
+++ b/mopidy/outputs/gstreamer.py
@@ -29,7 +29,7 @@ class GStreamerOutput(BaseOutput):
def __init__(self, *args, **kwargs):
super(GStreamerOutput, self).__init__(*args, **kwargs)
# Start a helper thread that can run the gobject.MainLoop
- self.messages_thread = GStreamerMessagesThread()
+ self.messages_thread = GStreamerMessagesThread(self.core_queue)
# Start a helper thread that can process the output_queue
self.output_queue = multiprocessing.Queue()
@@ -91,10 +91,9 @@ class GStreamerOutput(BaseOutput):
class GStreamerMessagesThread(BaseThread):
- def __init__(self):
- super(GStreamerMessagesThread, self).__init__()
+ def __init__(self, core_queue):
+ super(GStreamerMessagesThread, self).__init__(core_queue)
self.name = u'GStreamerMessagesThread'
- self.daemon = True
def run_inside_try(self):
gobject.MainLoop().run()
@@ -113,10 +112,8 @@ class GStreamerPlayerThread(BaseThread):
"""
def __init__(self, core_queue, output_queue):
- super(GStreamerPlayerThread, self).__init__()
+ super(GStreamerPlayerThread, self).__init__(core_queue)
self.name = u'GStreamerPlayerThread'
- self.daemon = True
- self.core_queue = core_queue
self.output_queue = output_queue
self.gst_pipeline = None
@@ -142,7 +139,16 @@ class GStreamerPlayerThread(BaseThread):
uri_bin.connect('pad-added', self.process_new_pad, pad)
self.gst_pipeline.add(uri_bin)
else:
- app_src = gst.element_factory_make('appsrc', 'src')
+ app_src = gst.element_factory_make('appsrc', 'appsrc')
+ app_src_caps = gst.Caps("""
+ audio/x-raw-int,
+ endianness=(int)1234,
+ channels=(int)2,
+ width=(int)16,
+ depth=(int)16,
+ signed=(boolean)true,
+ rate=(int)44100""")
+ app_src.set_property('caps', app_src_caps)
self.gst_pipeline.add(app_src)
app_src.get_pad('src').link(pad)
@@ -208,12 +214,12 @@ class GStreamerPlayerThread(BaseThread):
def deliver_data(self, caps_string, data):
"""Deliver audio data to be played"""
- data_src = self.gst_pipeline.get_by_name('src')
+ app_src = self.gst_pipeline.get_by_name('appsrc')
caps = gst.caps_from_string(caps_string)
buffer_ = gst.Buffer(buffer(data))
buffer_.set_caps(caps)
- data_src.set_property('caps', caps)
- data_src.emit('push-buffer', buffer_)
+ app_src.set_property('caps', caps)
+ app_src.emit('push-buffer', buffer_)
def end_of_data_stream(self):
"""
@@ -222,7 +228,7 @@ class GStreamerPlayerThread(BaseThread):
We will get a GStreamer message when the stream playback reaches the
token, and can then do any end-of-stream related tasks.
"""
- self.gst_pipeline.get_by_name('src').emit('end-of-stream')
+ self.gst_pipeline.get_by_name('appsrc').emit('end-of-stream')
def set_state(self, state_name):
"""
diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py
index 0acccb4d..7855d69c 100644
--- a/mopidy/utils/process.py
+++ b/mopidy/utils/process.py
@@ -19,22 +19,26 @@ def unpickle_connection(pickled_connection):
class BaseProcess(multiprocessing.Process):
+ def __init__(self, core_queue):
+ super(BaseProcess, self).__init__()
+ self.core_queue = core_queue
+
def run(self):
logger.debug(u'%s: Starting process', self.name)
try:
self.run_inside_try()
except KeyboardInterrupt:
- logger.info(u'%s: Interrupted by user', self.name)
- sys.exit(0)
+ logger.info(u'Interrupted by user')
+ self.exit(0, u'Interrupted by user')
except SettingsError as e:
logger.error(e.message)
- sys.exit(1)
+ self.exit(1, u'Settings error')
except ImportError as e:
logger.error(e)
- sys.exit(1)
+ self.exit(2, u'Import error')
except Exception as e:
logger.exception(e)
- raise e
+ self.exit(3, u'Unknown error')
def run_inside_try(self):
raise NotImplementedError
@@ -42,27 +46,43 @@ class BaseProcess(multiprocessing.Process):
def destroy(self):
self.terminate()
+ def exit(self, status=0, reason=None):
+ self.core_queue.put({'to': 'core', 'command': 'exit',
+ 'status': status, 'reason': reason})
+ self.destroy()
+
class BaseThread(multiprocessing.dummy.Process):
+ def __init__(self, core_queue):
+ super(BaseThread, self).__init__()
+ self.core_queue = core_queue
+ # No thread should block process from exiting
+ self.daemon = True
+
def run(self):
logger.debug(u'%s: Starting thread', self.name)
try:
self.run_inside_try()
except KeyboardInterrupt:
- logger.info(u'%s: Interrupted by user', self.name)
- sys.exit(0)
+ logger.info(u'Interrupted by user')
+ self.exit(0, u'Interrupted by user')
except SettingsError as e:
logger.error(e.message)
- sys.exit(1)
+ self.exit(1, u'Settings error')
except ImportError as e:
logger.error(e)
- sys.exit(1)
+ self.exit(2, u'Import error')
except Exception as e:
logger.exception(e)
- raise e
+ self.exit(3, u'Unknown error')
def run_inside_try(self):
raise NotImplementedError
def destroy(self):
pass
+
+ def exit(self, status=0, reason=None):
+ self.core_queue.put({'to': 'core', 'command': 'exit',
+ 'status': status, 'reason': reason})
+ self.destroy()
diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py
index 59c7b39f..05f08e18 100644
--- a/tests/backends/base/current_playlist.py
+++ b/tests/backends/base/current_playlist.py
@@ -128,7 +128,7 @@ class BaseCurrentPlaylistControllerTest(object):
def test_append_does_not_reset_version(self):
version = self.controller.version
self.controller.append([])
- self.assertEqual(self.controller.version, version + 1)
+ self.assertEqual(self.controller.version, version)
@populate_playlist
def test_append_preserves_playing_state(self):
@@ -249,7 +249,12 @@ class BaseCurrentPlaylistControllerTest(object):
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
- def test_version(self):
+ def test_version_does_not_change_when_appending_nothing(self):
version = self.controller.version
self.controller.append([])
+ self.assertEquals(version, self.controller.version)
+
+ def test_version_increases_when_appending_something(self):
+ version = self.controller.version
+ self.controller.append([Track()])
self.assert_(version < self.controller.version)
diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py
index ca4d9941..4caaf44b 100644
--- a/tests/backends/base/playback.py
+++ b/tests/backends/base/playback.py
@@ -524,7 +524,7 @@ class BasePlaybackControllerTest(object):
wrapper.called = False
self.playback.on_current_playlist_change = wrapper
- self.backend.current_playlist.append([])
+ self.backend.current_playlist.append([Track()])
self.assert_(wrapper.called)
diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py
index 8e4b62f9..8a4b9ab5 100644
--- a/tests/frontends/mpd/current_playlist_test.py
+++ b/tests/frontends/mpd/current_playlist_test.py
@@ -135,7 +135,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_deleteid(self):
self.b.current_playlist.append([Track(), Track()])
self.assertEqual(len(self.b.current_playlist.tracks), 2)
- result = self.h.handle_request(u'deleteid "2"')
+ result = self.h.handle_request(u'deleteid "1"')
self.assertEqual(len(self.b.current_playlist.tracks), 1)
self.assert_(u'OK' in result)
@@ -193,7 +193,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'moveid "5" "2"')
+ result = self.h.handle_request(u'moveid "4" "2"')
self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
self.assertEqual(self.b.current_playlist.tracks[1].name, 'b')
self.assertEqual(self.b.current_playlist.tracks[2].name, 'e')
@@ -229,7 +229,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
result = self.h.handle_request(
u'playlistfind filename "file:///exists"')
self.assert_(u'file: file:///exists' in result)
- self.assert_(u'Id: 1' in result)
+ self.assert_(u'Id: 0' in result)
self.assert_(u'Pos: 0' in result)
self.assert_(u'OK' in result)
@@ -242,11 +242,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
def test_playlistid_with_songid(self):
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
- result = self.h.handle_request(u'playlistid "2"')
+ result = self.h.handle_request(u'playlistid "1"')
self.assert_(u'Title: a' not in result)
- self.assert_(u'Id: 1' not in result)
+ self.assert_(u'Id: 0' not in result)
self.assert_(u'Title: b' in result)
- self.assert_(u'Id: 2' in result)
+ self.assert_(u'Id: 1' in result)
self.assert_(u'OK' in result)
def test_playlistid_with_not_existing_songid_fails(self):
@@ -429,7 +429,7 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
Track(name='a'), Track(name='b'), Track(name='c'),
Track(name='d'), Track(name='e'), Track(name='f'),
])
- result = self.h.handle_request(u'swapid "2" "5"')
+ result = self.h.handle_request(u'swapid "1" "4"')
self.assertEqual(self.b.current_playlist.tracks[0].name, 'a')
self.assertEqual(self.b.current_playlist.tracks[1].name, 'e')
self.assertEqual(self.b.current_playlist.tracks[2].name, 'c')
diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py
index 5fcc393c..05b8ebd0 100644
--- a/tests/frontends/mpd/music_db_test.py
+++ b/tests/frontends/mpd/music_db_test.py
@@ -15,6 +15,59 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
self.assert_(u'playtime: 0' in result)
self.assert_(u'OK' in result)
+ def test_findadd(self):
+ result = self.h.handle_request(u'findadd "album" "what"')
+ self.assert_(u'OK' in result)
+
+ def test_listall(self):
+ result = self.h.handle_request(u'listall "file:///dev/urandom"')
+ self.assert_(u'ACK [0@0] {} Not implemented' in result)
+
+ def test_listallinfo(self):
+ result = self.h.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.h.handle_request(u'lsinfo')
+ listplaylists_result = self.h.handle_request(u'listplaylists')
+ self.assertEqual(lsinfo_result, listplaylists_result)
+
+ def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
+ lsinfo_result = self.h.handle_request(u'lsinfo ""')
+ listplaylists_result = self.h.handle_request(u'listplaylists')
+ self.assertEqual(lsinfo_result, listplaylists_result)
+
+ def test_lsinfo_for_root_returns_same_as_listplaylists(self):
+ lsinfo_result = self.h.handle_request(u'lsinfo "/"')
+ listplaylists_result = self.h.handle_request(u'listplaylists')
+ self.assertEqual(lsinfo_result, listplaylists_result)
+
+ def test_update_without_uri(self):
+ result = self.h.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.h.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.h.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.h.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.b = DummyBackend(mixer_class=DummyMixer)
+ self.h = dispatcher.MpdDispatcher(backend=self.b)
+
def test_find_album(self):
result = self.h.handle_request(u'find "album" "what"')
self.assert_(u'OK' in result)
@@ -48,11 +101,20 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
u'find album "album_what" artist "artist_what"')
self.assert_(u'OK' in result)
- def test_findadd(self):
- result = self.h.handle_request(u'findadd "album" "what"')
- self.assert_(u'OK' in result)
- def test_list_artist(self):
+class MusicDatabaseListTest(unittest.TestCase):
+ def setUp(self):
+ self.b = DummyBackend(mixer_class=DummyMixer)
+ self.h = dispatcher.MpdDispatcher(backend=self.b)
+
+ def test_list_foo_returns_ack(self):
+ result = self.h.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.h.handle_request(u'list "artist"')
self.assert_(u'OK' in result)
@@ -64,44 +126,177 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
result = self.h.handle_request(u'list Artist')
self.assert_(u'OK' in result)
- def test_list_artist_with_artist_should_fail(self):
+ def test_list_artist_with_query_of_one_token(self):
result = self.h.handle_request(u'list "artist" "anartist"')
- self.assertEqual(result[0], u'ACK [2@0] {list} incorrect arguments')
+ self.assertEqual(result[0],
+ u'ACK [2@0] {list} should be "Album" for 3 arguments')
- def test_list_album_without_artist(self):
+ def test_list_artist_with_unknown_field_in_query_returns_ack(self):
+ result = self.h.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.h.handle_request(u'list "artist" "artist" "anartist"')
+ self.assert_(u'OK' in result)
+
+ def test_list_artist_by_album(self):
+ result = self.h.handle_request(u'list "artist" "album" "analbum"')
+ self.assert_(u'OK' in result)
+
+ def test_list_artist_by_full_date(self):
+ result = self.h.handle_request(u'list "artist" "date" "2001-01-01"')
+ self.assert_(u'OK' in result)
+
+ def test_list_artist_by_year(self):
+ result = self.h.handle_request(u'list "artist" "date" "2001"')
+ self.assert_(u'OK' in result)
+
+ def test_list_artist_by_genre(self):
+ result = self.h.handle_request(u'list "artist" "genre" "agenre"')
+ self.assert_(u'OK' in result)
+
+ def test_list_artist_by_artist_and_album(self):
+ result = self.h.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.h.handle_request(u'list "album"')
self.assert_(u'OK' in result)
- def test_list_album_with_artist(self):
+ def test_list_album_without_quotes(self):
+ result = self.h.handle_request(u'list album')
+ self.assert_(u'OK' in result)
+
+ def test_list_album_without_quotes_and_capitalized(self):
+ result = self.h.handle_request(u'list Album')
+ self.assert_(u'OK' in result)
+
+ def test_list_album_with_artist_name(self):
result = self.h.handle_request(u'list "album" "anartist"')
self.assert_(u'OK' in result)
- def test_list_album_artist_with_artist_without_quotes(self):
- result = self.h.handle_request(u'list album artist "anartist"')
+ def test_list_album_by_artist(self):
+ result = self.h.handle_request(u'list "album" "artist" "anartist"')
self.assert_(u'OK' in result)
- def test_listall(self):
- result = self.h.handle_request(u'listall "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ def test_list_album_by_album(self):
+ result = self.h.handle_request(u'list "album" "album" "analbum"')
+ self.assert_(u'OK' in result)
- def test_listallinfo(self):
- result = self.h.handle_request(u'listallinfo "file:///dev/urandom"')
- self.assert_(u'ACK [0@0] {} Not implemented' in result)
+ def test_list_album_by_full_date(self):
+ result = self.h.handle_request(u'list "album" "date" "2001-01-01"')
+ self.assert_(u'OK' in result)
- def test_lsinfo_without_path_returns_same_as_listplaylists(self):
- lsinfo_result = self.h.handle_request(u'lsinfo')
- listplaylists_result = self.h.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
+ def test_list_album_by_year(self):
+ result = self.h.handle_request(u'list "album" "date" "2001"')
+ self.assert_(u'OK' in result)
- def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
- lsinfo_result = self.h.handle_request(u'lsinfo ""')
- listplaylists_result = self.h.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
+ def test_list_album_by_genre(self):
+ result = self.h.handle_request(u'list "album" "genre" "agenre"')
+ self.assert_(u'OK' in result)
- def test_lsinfo_for_root_returns_same_as_listplaylists(self):
- lsinfo_result = self.h.handle_request(u'lsinfo "/"')
- listplaylists_result = self.h.handle_request(u'listplaylists')
- self.assertEqual(lsinfo_result, listplaylists_result)
+ def test_list_album_by_artist_and_album(self):
+ result = self.h.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.h.handle_request(u'list "date"')
+ self.assert_(u'OK' in result)
+
+ def test_list_date_without_quotes(self):
+ result = self.h.handle_request(u'list date')
+ self.assert_(u'OK' in result)
+
+ def test_list_date_without_quotes_and_capitalized(self):
+ result = self.h.handle_request(u'list Date')
+ self.assert_(u'OK' in result)
+
+ def test_list_date_with_query_of_one_token(self):
+ result = self.h.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.h.handle_request(u'list "date" "artist" "anartist"')
+ self.assert_(u'OK' in result)
+
+ def test_list_date_by_album(self):
+ result = self.h.handle_request(u'list "date" "album" "analbum"')
+ self.assert_(u'OK' in result)
+
+ def test_list_date_by_full_date(self):
+ result = self.h.handle_request(u'list "date" "date" "2001-01-01"')
+ self.assert_(u'OK' in result)
+
+ def test_list_date_by_year(self):
+ result = self.h.handle_request(u'list "date" "date" "2001"')
+ self.assert_(u'OK' in result)
+
+ def test_list_date_by_genre(self):
+ result = self.h.handle_request(u'list "date" "genre" "agenre"')
+ self.assert_(u'OK' in result)
+
+ def test_list_date_by_artist_and_album(self):
+ result = self.h.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.h.handle_request(u'list "genre"')
+ self.assert_(u'OK' in result)
+
+ def test_list_genre_without_quotes(self):
+ result = self.h.handle_request(u'list genre')
+ self.assert_(u'OK' in result)
+
+ def test_list_genre_without_quotes_and_capitalized(self):
+ result = self.h.handle_request(u'list Genre')
+ self.assert_(u'OK' in result)
+
+ def test_list_genre_with_query_of_one_token(self):
+ result = self.h.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.h.handle_request(u'list "genre" "artist" "anartist"')
+ self.assert_(u'OK' in result)
+
+ def test_list_genre_by_album(self):
+ result = self.h.handle_request(u'list "genre" "album" "analbum"')
+ self.assert_(u'OK' in result)
+
+ def test_list_genre_by_full_date(self):
+ result = self.h.handle_request(u'list "genre" "date" "2001-01-01"')
+ self.assert_(u'OK' in result)
+
+ def test_list_genre_by_year(self):
+ result = self.h.handle_request(u'list "genre" "date" "2001"')
+ self.assert_(u'OK' in result)
+
+ def test_list_genre_by_genre(self):
+ result = self.h.handle_request(u'list "genre" "genre" "agenre"')
+ self.assert_(u'OK' in result)
+
+ def test_list_genre_by_artist_and_album(self):
+ result = self.h.handle_request(
+ u'list "genre" "artist" "anartist" "album" "analbum"')
+ self.assert_(u'OK' in result)
+
+
+class MusicDatabaseSearchTest(unittest.TestCase):
+ def setUp(self):
+ self.b = DummyBackend(mixer_class=DummyMixer)
+ self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_search_album(self):
result = self.h.handle_request(u'search "album" "analbum"')
@@ -147,22 +342,4 @@ class MusicDatabaseHandlerTest(unittest.TestCase):
result = self.h.handle_request(u'search "sometype" "something"')
self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')
- def test_update_without_uri(self):
- result = self.h.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.h.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.h.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.h.handle_request(u'rescan "file:///dev/urandom"')
- self.assert_(u'OK' in result)
- self.assert_(u'updating_db: 0' in result)
diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py
index 64cdf5a6..b6e0751e 100644
--- a/tests/frontends/mpd/playback_test.py
+++ b/tests/frontends/mpd/playback_test.py
@@ -254,7 +254,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
def test_playid(self):
self.b.current_playlist.append([Track()])
- result = self.h.handle_request(u'playid "1"')
+ result = self.h.handle_request(u'playid "0"')
self.assert_(u'OK' in result)
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
@@ -321,7 +321,7 @@ class PlaybackControlHandlerTest(unittest.TestCase):
def test_seekid(self):
self.b.current_playlist.append([Track(length=40000)])
- result = self.h.handle_request(u'seekid "1" "30"')
+ result = self.h.handle_request(u'seekid "0" "30"')
self.assert_(u'OK' in result)
self.assert_(self.b.playback.time_position >= 30000)
@@ -329,8 +329,8 @@ class PlaybackControlHandlerTest(unittest.TestCase):
seek_track = Track(uri='2', length=40000)
self.b.current_playlist.append(
[Track(length=40000), seek_track])
- result = self.h.handle_request(u'seekid "2" "30"')
- self.assertEqual(self.b.playback.current_cpid, 2)
+ result = self.h.handle_request(u'seekid "1" "30"')
+ self.assertEqual(self.b.playback.current_cpid, 1)
self.assertEqual(self.b.playback.current_track, seek_track)
def test_stop(self):
diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py
new file mode 100644
index 00000000..3cfdb855
--- /dev/null
+++ b/tests/frontends/mpd/regression_test.py
@@ -0,0 +1,110 @@
+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/jodal/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(mixer_class=DummyMixer)
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), None,
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
+
+ def test(self):
+ random.seed(1) # Playlist order: abcfde
+ self.mpd.handle_request(u'play')
+ self.assertEquals('a', self.backend.playback.current_track.uri)
+ self.mpd.handle_request(u'random "1"')
+ self.mpd.handle_request(u'next')
+ self.assertEquals('b', self.backend.playback.current_track.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.uri)
+ self.mpd.handle_request(u'next')
+ self.assertEquals('d', self.backend.playback.current_track.uri)
+ self.mpd.handle_request(u'next')
+ self.assertEquals('e', self.backend.playback.current_track.uri)
+
+
+class IssueGH18RegressionTest(unittest.TestCase):
+ """
+ The issue: http://github.com/jodal/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(mixer_class=DummyMixer)
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
+
+ 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
+ self.mpd.handle_request(u'next')
+ cp_track_2 = self.backend.playback.current_cp_track
+ self.mpd.handle_request(u'next')
+ cp_track_3 = self.backend.playback.current_cp_track
+
+ 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/jodal/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(mixer_class=DummyMixer)
+ self.backend.current_playlist.append([
+ Track(uri='a'), Track(uri='b'), Track(uri='c'),
+ Track(uri='d'), Track(uri='e'), Track(uri='f')])
+ self.mpd = dispatcher.MpdDispatcher(backend=self.backend)
+
+ 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')
diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py
index fbd0ff9e..1afe6ccd 100644
--- a/tests/frontends/mpd/status_test.py
+++ b/tests/frontends/mpd/status_test.py
@@ -27,7 +27,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'Track: 0' in result)
self.assert_(u'Date: ' in result)
self.assert_(u'Pos: 0' in result)
- self.assert_(u'Id: 1' in result)
+ self.assert_(u'Id: 0' in result)
self.assert_(u'OK' in result)
def test_currentsong_without_song(self):
@@ -166,7 +166,7 @@ class StatusHandlerTest(unittest.TestCase):
self.b.playback.play()
result = dict(dispatcher.status.status(self.h))
self.assert_('songid' in result)
- self.assertEqual(int(result['songid']), 1)
+ self.assertEqual(int(result['songid']), 0)
def test_status_method_when_playing_contains_time_with_no_length(self):
self.b.current_playlist.append([Track(length=None)])
diff --git a/tests/version_test.py b/tests/version_test.py
index b2ef1fce..fcc95c4c 100644
--- a/tests/version_test.py
+++ b/tests/version_test.py
@@ -13,6 +13,5 @@ class VersionTest(unittest.TestCase):
self.assert_(SV('0.1.0a2') < SV('0.1.0a3'))
self.assert_(SV('0.1.0a3') < SV('0.1.0'))
self.assert_(SV('0.1.0') < SV(get_version()))
- self.assert_(SV(get_version()) < SV('0.2.0'))
- self.assert_(SV('0.1.1') < SV('0.2.0'))
+ self.assert_(SV(get_version()) < SV('0.2.1'))
self.assert_(SV('0.2.0') < SV('1.0.0'))