Merge branch 'develop' of http://github.com/jodal/mopidy into develop
This commit is contained in:
commit
0371698706
@ -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>`_
|
||||
|
||||
@ -5,31 +5,82 @@ Changes
|
||||
This change log is used to track all major changes to Mopidy.
|
||||
|
||||
|
||||
0.2.0 (in development)
|
||||
0.3.0 (in development)
|
||||
======================
|
||||
|
||||
No description yet.
|
||||
|
||||
**Changes**
|
||||
|
||||
- None so far.
|
||||
|
||||
|
||||
0.2.0 (2010-10-24)
|
||||
==================
|
||||
|
||||
In Mopidy 0.2.0 we've added a `Last.fm <http://www.last.fm/>`_ scrobbling
|
||||
support, which means that Mopidy now can submit meta data about the tracks you
|
||||
play to your Last.fm profile. See :mod:`mopidy.frontends.lastfm` for
|
||||
details on new dependencies and settings. If you use Mopidy's Last.fm support,
|
||||
please join the `Mopidy group at Last.fm <http://www.last.fm/group/Mopidy>`_.
|
||||
|
||||
With the exception of the work on the Last.fm scrobbler, there has been a
|
||||
couple of quiet months in the Mopidy camp. About the only thing going on, has
|
||||
been stabilization work and bug fixing. All bugs reported on GitHub, plus some,
|
||||
have been fixed in 0.2.0. Thus, we hope this will be a great release!
|
||||
|
||||
We've worked a bit on OS X support, but not all issues are completely solved
|
||||
yet. :issue:`25` is the one that is currently blocking OS X support. Any help
|
||||
solving it will be greatly appreciated!
|
||||
|
||||
Finally, please :ref:`update your pyspotify installation
|
||||
<pyspotify_installation>` when upgrading to Mopidy 0.2.0. The latest pyspotify
|
||||
got a fix for the segmentation fault that occurred when playing music and
|
||||
searching at the same time, thanks to Valentin David.
|
||||
|
||||
**Important changes**
|
||||
|
||||
- Added a Last.fm scrobbler. See :mod:`mopidy.frontends.lastfm` for details.
|
||||
|
||||
**Changes**
|
||||
|
||||
- Simplify the default log format, :attr:`mopidy.settings.CONSOLE_LOG_FORMAT`.
|
||||
From a user's point of view: Less noise, more information.
|
||||
- Rename the :option:`--dump` command line option to
|
||||
:option:`--save-debug-log`.
|
||||
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to
|
||||
:attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for :option:`--verbose`
|
||||
too.
|
||||
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to
|
||||
:attr:`mopidy.settings.DEBUG_LOG_FILENAME`.
|
||||
- Switched from using subprocesses to threads. This partly fixes the OS X
|
||||
support. See :issue:`14` for details.
|
||||
- Logging and command line options:
|
||||
|
||||
- Simplify the default log format,
|
||||
:attr:`mopidy.settings.CONSOLE_LOG_FORMAT`. From a user's point of view:
|
||||
Less noise, more information.
|
||||
- Rename the :option:`--dump` command line option to
|
||||
:option:`--save-debug-log`.
|
||||
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to
|
||||
:attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for :option:`--verbose`
|
||||
too.
|
||||
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to
|
||||
:attr:`mopidy.settings.DEBUG_LOG_FILENAME`.
|
||||
|
||||
- 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`)
|
||||
- MPD command ``playid "-1"`` now correctly resumes playback if paused.
|
||||
|
||||
- Random mode:
|
||||
|
||||
- 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`)
|
||||
|
||||
- Switched from using subprocesses to threads. (Fixes: :issue:`14`)
|
||||
- :mod:`mopidy.outputs.gstreamer`: 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)
|
||||
|
||||
@ -151,20 +151,25 @@ Then, to generate docs::
|
||||
Creating releases
|
||||
=================
|
||||
|
||||
1. Update changelog and commit it.
|
||||
#. Update changelog and commit it.
|
||||
|
||||
2. Tag release::
|
||||
#. Merge the release branch (``develop`` in the example) into master::
|
||||
|
||||
git tag -a -m "Release v0.1.0a0" v0.1.0a0
|
||||
git checkout master
|
||||
git merge --no-ff -m "Release v0.2.0" develop
|
||||
|
||||
3. Push to GitHub::
|
||||
#. Tag the release::
|
||||
|
||||
git tag -a -m "Release v0.2.0" v0.2.0
|
||||
|
||||
#. Push to GitHub::
|
||||
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
4. Build package and upload to PyPI::
|
||||
#. Build package and upload to PyPI::
|
||||
|
||||
rm MANIFEST # Will be regenerated by setup.py
|
||||
python setup.py sdist upload
|
||||
|
||||
5. Spread the word.
|
||||
#. Spread the word.
|
||||
|
||||
@ -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.3.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."""
|
||||
|
||||
@ -10,6 +10,9 @@ class BasePlaybackController(object):
|
||||
:type backend: :class:`BaseBackend`
|
||||
"""
|
||||
|
||||
# pylint: disable = R0902
|
||||
# Too many instance attributes
|
||||
|
||||
#: Constant representing the paused state.
|
||||
PAUSED = u'paused'
|
||||
|
||||
@ -130,6 +133,9 @@ class BasePlaybackController(object):
|
||||
|
||||
Not necessarily the same track as :attr:`cp_track_at_next`.
|
||||
"""
|
||||
# pylint: disable = R0911
|
||||
# Too many return statements
|
||||
|
||||
cp_tracks = self.backend.current_playlist.cp_tracks
|
||||
|
||||
if not cp_tracks:
|
||||
@ -142,17 +148,16 @@ 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:
|
||||
return cp_tracks[0]
|
||||
|
||||
if self.repeat and self.single:
|
||||
return cp_tracks[
|
||||
(self.current_playlist_position) % len(cp_tracks)]
|
||||
return cp_tracks[self.current_playlist_position]
|
||||
|
||||
if self.repeat:
|
||||
if self.repeat and not self.single:
|
||||
return cp_tracks[
|
||||
(self.current_playlist_position + 1) % len(cp_tracks)]
|
||||
|
||||
@ -195,7 +200,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 +320,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 +335,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 +349,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."""
|
||||
@ -385,15 +380,21 @@ class BasePlaybackController(object):
|
||||
|
||||
if cp_track is not None:
|
||||
assert cp_track in self.backend.current_playlist.cp_tracks
|
||||
elif not self.current_cp_track:
|
||||
|
||||
if cp_track is None and self.current_cp_track is None:
|
||||
cp_track = self.cp_track_at_next
|
||||
|
||||
if self.state == self.PAUSED and cp_track is None:
|
||||
if cp_track is None and self.state == self.PAUSED:
|
||||
self.resume()
|
||||
elif cp_track is not None:
|
||||
|
||||
if cp_track is not None:
|
||||
self.state = self.STOPPED
|
||||
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 +478,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):
|
||||
"""
|
||||
@ -501,11 +510,12 @@ class BasePlaybackController(object):
|
||||
For internal use only. Should be called by the backend directly after a
|
||||
track has started playing.
|
||||
"""
|
||||
self.backend.core_queue.put({
|
||||
'to': 'frontend',
|
||||
'command': 'started_playing',
|
||||
'track': self.current_track,
|
||||
})
|
||||
if self.current_track is not None:
|
||||
self.backend.core_queue.put({
|
||||
'to': 'frontend',
|
||||
'command': 'started_playing',
|
||||
'track': self.current_track,
|
||||
})
|
||||
|
||||
def _trigger_stopped_playing_event(self):
|
||||
"""
|
||||
@ -515,9 +525,10 @@ class BasePlaybackController(object):
|
||||
is stopped playing, e.g. at the next, previous, and stop actions and at
|
||||
end-of-track.
|
||||
"""
|
||||
self.backend.core_queue.put({
|
||||
'to': 'frontend',
|
||||
'command': 'stopped_playing',
|
||||
'track': self.current_track,
|
||||
'stop_position': self.time_position,
|
||||
})
|
||||
if self.current_track is not None:
|
||||
self.backend.core_queue.put({
|
||||
'to': 'frontend',
|
||||
'command': 'stopped_playing',
|
||||
'track': self.current_track,
|
||||
'stop_position': self.time_position,
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -11,6 +11,9 @@ from mopidy.utils.process import BaseThread
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.libspotify.session_manager')
|
||||
|
||||
# pylint: disable = R0901
|
||||
# LibspotifySessionManager: Too many ancestors (9/7)
|
||||
|
||||
class LibspotifySessionManager(SpotifySessionManager, BaseThread):
|
||||
cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
|
||||
settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE)
|
||||
@ -19,12 +22,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 +68,21 @@ 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
|
||||
# pylint: disable = R0913
|
||||
# Too many arguments (8/5)
|
||||
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 +101,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)
|
||||
|
||||
@ -5,7 +5,9 @@ import os
|
||||
import shutil
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import *
|
||||
from mopidy.backends.base import (BaseBackend, BaseLibraryController,
|
||||
BaseStoredPlaylistsController, BaseCurrentPlaylistController,
|
||||
BasePlaybackController)
|
||||
from mopidy.models import Playlist, Track, Album
|
||||
from mopidy.utils.process import pickle_connection
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -5,9 +5,9 @@ import time
|
||||
|
||||
try:
|
||||
import pylast
|
||||
except ImportError as e:
|
||||
except ImportError as import_error:
|
||||
from mopidy import OptionalDependencyError
|
||||
raise OptionalDependencyError(e)
|
||||
raise OptionalDependencyError(import_error)
|
||||
|
||||
from mopidy import get_version, settings, SettingsError
|
||||
from mopidy.frontends.base import BaseFrontend
|
||||
@ -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)
|
||||
|
||||
@ -5,9 +5,11 @@ from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
|
||||
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
|
||||
# Do not remove the following import. The protocol modules must be imported to
|
||||
# get them registered as request handlers.
|
||||
# pylint: disable = W0611
|
||||
from mopidy.frontends.mpd.protocol import (audio_output, command_list,
|
||||
connection, current_playlist, empty, music_db, playback, reflection,
|
||||
status, stickers, stored_playlists)
|
||||
# pylint: enable = W0611
|
||||
from mopidy.utils import flatten
|
||||
|
||||
class MpdDispatcher(object):
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -138,6 +138,10 @@ def playid(frontend, cpid):
|
||||
at the first track.
|
||||
"""
|
||||
cpid = int(cpid)
|
||||
paused = (frontend.backend.playback.state ==
|
||||
frontend.backend.playback.PAUSED)
|
||||
if cpid == -1 and paused:
|
||||
return frontend.backend.playback.resume()
|
||||
try:
|
||||
if cpid == -1:
|
||||
cp_track = _get_cp_track_for_play_minus_one(frontend)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -40,7 +40,7 @@ class NadMixer(BaseMixer):
|
||||
super(NadMixer, self).__init__(*args, **kwargs)
|
||||
self._volume = None
|
||||
self._pipe, other_end = Pipe()
|
||||
NadTalker(pipe=other_end).start()
|
||||
NadTalker(self.backend.core_queue, pipe=other_end).start()
|
||||
|
||||
def _get_volume(self):
|
||||
return self._volume
|
||||
@ -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.
|
||||
|
||||
@ -72,8 +72,9 @@ class NadTalker(BaseProcess):
|
||||
# Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration.
|
||||
_nad_volume = None
|
||||
|
||||
def __init__(self, pipe=None):
|
||||
super(NadTalker, self).__init__(name='NadTalker')
|
||||
def __init__(self, core_queue, pipe=None):
|
||||
super(NadTalker, self).__init__(core_queue)
|
||||
self.name = u'NadTalker'
|
||||
self.pipe = pipe
|
||||
self._device = None
|
||||
|
||||
|
||||
@ -5,6 +5,9 @@ class DummyOutput(BaseOutput):
|
||||
Audio output used for testing.
|
||||
"""
|
||||
|
||||
# pylint: disable = R0902
|
||||
# Too many instance attributes (9/7)
|
||||
|
||||
#: For testing. :class:`True` if :meth:`start` has been called.
|
||||
start_called = False
|
||||
|
||||
|
||||
@ -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()
|
||||
@ -78,7 +78,8 @@ class GStreamerOutput(BaseOutput):
|
||||
return self._send_recv({'command': 'get_position'})
|
||||
|
||||
def set_position(self, position):
|
||||
return self._send_recv({'command': 'set_position', 'position': position})
|
||||
return self._send_recv({'command': 'set_position',
|
||||
'position': position})
|
||||
|
||||
def set_state(self, state):
|
||||
return self._send_recv({'command': 'set_state', 'state': state})
|
||||
@ -91,10 +92,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 +113,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 +140,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 +215,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 +229,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):
|
||||
"""
|
||||
|
||||
@ -3,7 +3,6 @@ import multiprocessing
|
||||
import multiprocessing.dummy
|
||||
from multiprocessing.reduction import reduce_connection
|
||||
import pickle
|
||||
import sys
|
||||
|
||||
from mopidy import SettingsError
|
||||
|
||||
@ -17,24 +16,27 @@ def unpickle_connection(pickled_connection):
|
||||
(func, args) = pickle.loads(pickled_connection)
|
||||
return func(*args)
|
||||
|
||||
|
||||
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 +44,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 process', self.name)
|
||||
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()
|
||||
|
||||
@ -23,7 +23,9 @@ class SettingsProxy(object):
|
||||
if not os.path.isfile(settings_file):
|
||||
return {}
|
||||
sys.path.insert(0, dotdir)
|
||||
# pylint: disable = F0401
|
||||
import settings as local_settings_module
|
||||
# pylint: enable = F0401
|
||||
return self._get_settings_dict_from_module(local_settings_module)
|
||||
|
||||
def _get_settings_dict_from_module(self, module):
|
||||
|
||||
11
pylintrc
11
pylintrc
@ -5,18 +5,19 @@
|
||||
#
|
||||
# C0103 - Invalid name "%s" (should match %s)
|
||||
# C0111 - Missing docstring
|
||||
# C0112 - Empty docstring
|
||||
# E0102 - %s already defined line %s
|
||||
# Does not understand @property getters and setters
|
||||
# E0202 - An attribute inherited from %s hide this method
|
||||
# Does not understand @property getters and setters
|
||||
# E1101 - %s %r has no %r member
|
||||
# Does not understand @property getters and setters
|
||||
# R0201 - Method could be a function
|
||||
# R0801 - Similar lines in %s files
|
||||
# R0903 - Too few public methods (%s/%s)
|
||||
# R0904 - Too many public methods (%s/%s)
|
||||
# W0141 - Used builtin function %r
|
||||
# R0921 - Abstract class not referenced
|
||||
# W0141 - Used builtin function '%s'
|
||||
# W0142 - Used * or ** magic
|
||||
# W0401 - Wildcard import %s
|
||||
# W0511 - TODO, FIXME and XXX in the code
|
||||
# W0613 - Unused argument %r
|
||||
#
|
||||
disable-msg = C0103,C0111,C0112,E0102,E0202,E1101,R0201,R0801,R0903,R0904,W0141,W0142,W0401,W0511,W0613
|
||||
disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0613
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -285,6 +285,18 @@ class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(self.b.playback.STOPPED, self.b.playback.state)
|
||||
self.assertEqual(self.b.playback.current_track, None)
|
||||
|
||||
def test_playid_minus_one_resumes_if_paused(self):
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
self.b.playback.seek(30000)
|
||||
self.assert_(self.b.playback.time_position >= 30000)
|
||||
self.assertEquals(self.b.playback.PLAYING, self.b.playback.state)
|
||||
self.b.playback.pause()
|
||||
self.assertEquals(self.b.playback.PAUSED, self.b.playback.state)
|
||||
result = self.h.handle_request(u'playid "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.PLAYING, self.b.playback.state)
|
||||
self.assert_(self.b.playback.time_position >= 30000)
|
||||
|
||||
def test_playid_which_does_not_exist(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'playid "12345"')
|
||||
@ -310,7 +322,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)
|
||||
|
||||
@ -318,8 +330,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)])
|
||||
|
||||
@ -12,7 +12,7 @@ class VersionTest(unittest.TestCase):
|
||||
self.assert_(SV('0.1.0a1') < SV('0.1.0a2'))
|
||||
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('0.2.0') < SV('1.0.0'))
|
||||
self.assert_(SV('0.1.0') < SV('0.2.0'))
|
||||
self.assert_(SV('0.1.0') < SV('1.0.0'))
|
||||
self.assert_(SV('0.2.0') < SV(get_version()))
|
||||
self.assert_(SV(get_version()) < SV('0.3.1'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user