Merge branch 'develop' into feature/playid-minus-one-should-resume
This commit is contained in:
commit
24015cafa8
@ -11,8 +11,9 @@ platforms, including Windows, Mac OS X, Linux, and iPhone and Android phones.
|
||||
To install Mopidy, check out
|
||||
`the installation docs <http://www.mopidy.com/docs/master/installation/>`_.
|
||||
|
||||
* `Documentation <http://www.mopidy.com/docs/master/>`_
|
||||
* `Documentation (latest release) <http://www.mopidy.com/docs/master/>`_
|
||||
* `Documentation (development version) <http://www.mopidy.com/docs/develop/>`_
|
||||
* `Source code <http://github.com/jodal/mopidy>`_
|
||||
* `Issue tracker <http://github.com/jodal/mopidy/issues>`_
|
||||
* IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
|
||||
* `Download development snapshot <http://github.com/jodal/mopidy/tarball/develop#egg=mopidy-dev>`_
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<field>[Aa]rtist)$')
|
||||
@handle_pattern(r'^list "(?P<field>[Aa]rtist)"$')
|
||||
@handle_pattern(r'^list (?P<field>album( artist)?)'
|
||||
'( "(?P<artist>[^"]+)")*$')
|
||||
@handle_pattern(r'^list "(?P<field>album(" "artist)?)"'
|
||||
'( "(?P<artist>[^"]+)")*$')
|
||||
def list_(frontend, field, artist=None):
|
||||
@handle_pattern(r'^list "?(?P<field>([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
|
||||
'( (?P<mpd_query>.*))?$')
|
||||
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<uri>[^"]+)"')
|
||||
def listall(frontend, uri):
|
||||
"""
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
110
tests/frontends/mpd/regression_test.py
Normal file
110
tests/frontends/mpd/regression_test.py
Normal file
@ -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')
|
||||
@ -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)])
|
||||
|
||||
@ -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'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user