Merge branch 'develop' into feature/playid-minus-one-should-resume

This commit is contained in:
Stein Magnus Jodal 2010-10-24 20:15:34 +02:00
commit 24015cafa8
24 changed files with 635 additions and 186 deletions

View File

@ -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>`_

View File

@ -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)

View File

@ -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):

View File

@ -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."""

View File

@ -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):
"""

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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):
"""

View File

@ -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()

View File

@ -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')

View File

@ -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.

View File

@ -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):
"""

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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):

View 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')

View File

@ -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)])

View File

@ -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'))