Merge branch 'develop' into feature/end-of-track
Conflicts: mopidy/audio/actor.py mopidy/backends/spotify/playback.py
This commit is contained in:
commit
6d1f84ad3c
@ -18,6 +18,8 @@ v0.12.0 (in development)
|
||||
- ``foo(**data)`` fails if the keys in ``data`` is unicode strings on Python
|
||||
< 2.6.5rc1.
|
||||
|
||||
- Improve selection of mixer tracks for volume control. (Fixes: :issuse:`307`)
|
||||
|
||||
**Spotify backend**
|
||||
|
||||
- Let GStreamer handle time position tracking and seeks. (Fixes: :issue:`191`)
|
||||
@ -44,6 +46,13 @@ Current limitations:
|
||||
yourself. See :issue:`303` for progress on this.
|
||||
|
||||
|
||||
v0.11.1 (2012-12-24)
|
||||
====================
|
||||
|
||||
Spotify search was broken in 0.11.0 for users of Python 2.6. This release fixes
|
||||
it. If you're using Python 2.7, v0.11.0 and v0.11.1 should be equivalent.
|
||||
|
||||
|
||||
v0.11.0 (2012-12-24)
|
||||
====================
|
||||
|
||||
|
||||
@ -36,40 +36,27 @@ Building from source
|
||||
sudo apt-get update
|
||||
sudo apt-get install nodejs npm
|
||||
|
||||
2. Assuming you install from PPA, setup your ``NODE_PATH`` environment variable
|
||||
to include ``/usr/lib/node_modules``. Add the following to your
|
||||
``~/.bashrc`` or equivalent::
|
||||
|
||||
export NODE_PATH=/usr/lib/node_modules:$NODE_PATH
|
||||
|
||||
3. Install `Buster.js <http://busterjs.org/>`_ and `Grunt
|
||||
<http://gruntjs.com/>`_ globally (or locally, and make sure you get their
|
||||
binaries on your ``PATH``)::
|
||||
|
||||
sudo npm -g install buster grunt
|
||||
|
||||
4. Install the grunt-buster Grunt plugin locally, when in the ``js/`` dir::
|
||||
2. Enter the ``js/`` dir and install development dependencies::
|
||||
|
||||
cd js/
|
||||
npm install grunt-buster
|
||||
npm install
|
||||
|
||||
5. Install `PhantomJS <http://phantomjs.org/>`_ so that we can run the tests
|
||||
without a browser::
|
||||
That's it.
|
||||
|
||||
sudo apt-get install phantomjs
|
||||
You can now run the tests::
|
||||
|
||||
It is packaged in Ubuntu since 12.04, but I haven't tested with versions
|
||||
older than 1.6 which is the one packaged in Ubuntu 12.10.
|
||||
npm test
|
||||
|
||||
6. Run Grunt to lint, test, concatenate, and minify the source::
|
||||
To run tests automatically when you save a file::
|
||||
|
||||
grunt
|
||||
npm run-script watch
|
||||
|
||||
The files in ``../mopidy/frontends/http/data/`` should now be up to date.
|
||||
To run tests, concatenate, minify the source, and update the JavaScript files
|
||||
in ``mopidy/frontends/http/data/``::
|
||||
|
||||
npm run-script build
|
||||
|
||||
Development tips
|
||||
================
|
||||
To run other `grunt <http://gruntjs.com/>`_ targets which isn't predefined in
|
||||
``package.json`` and thus isn't available through ``npm run-script``::
|
||||
|
||||
If you're coding on the JavaScript library, you should know about ``grunt
|
||||
watch``. It lints and tests the code every time you save a file.
|
||||
PATH=./node_modules/.bin:$PATH grunt foo
|
||||
|
||||
@ -64,7 +64,9 @@ module.exports = function (grunt) {
|
||||
uglify: {}
|
||||
});
|
||||
|
||||
grunt.registerTask("default", "lint buster concat min");
|
||||
grunt.registerTask("test", "lint buster");
|
||||
grunt.registerTask("build", "lint buster concat min");
|
||||
grunt.registerTask("default", "build");
|
||||
|
||||
grunt.loadNpmTasks("grunt-buster");
|
||||
};
|
||||
|
||||
15
js/package.json
Normal file
15
js/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "mopidy",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"buster": "*",
|
||||
"grunt": "*",
|
||||
"grunt-buster": "*",
|
||||
"phantomjs": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test",
|
||||
"build": "grunt build",
|
||||
"watch": "grunt watch"
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,7 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.11.0'
|
||||
__version__ = '0.11.1'
|
||||
|
||||
|
||||
from mopidy import settings as default_settings_module
|
||||
|
||||
@ -12,7 +12,7 @@ import pykka
|
||||
from mopidy import settings
|
||||
from mopidy.utils import process
|
||||
|
||||
from . import mixers
|
||||
from . import mixers, utils
|
||||
from .constants import PlaybackState
|
||||
from .listener import AudioListener
|
||||
|
||||
@ -20,6 +20,8 @@ logger = logging.getLogger('mopidy.audio')
|
||||
|
||||
mixers.register_mixers()
|
||||
|
||||
MB = 1 << 20
|
||||
|
||||
|
||||
class Audio(pykka.ThreadingActor):
|
||||
"""
|
||||
@ -39,6 +41,7 @@ class Audio(pykka.ThreadingActor):
|
||||
super(Audio, self).__init__()
|
||||
|
||||
self._playbin = None
|
||||
self._signal_ids = {} # {(element, event): signal_id}
|
||||
|
||||
self._mixer = None
|
||||
self._mixer_track = None
|
||||
@ -48,12 +51,9 @@ class Audio(pykka.ThreadingActor):
|
||||
|
||||
self._appsrc = None
|
||||
self._appsrc_caps = None
|
||||
self._appsrc_need_data_callback = None
|
||||
self._appsrc_enough_data_callback = None
|
||||
self._appsrc_seek_data_callback = None
|
||||
self._appsrc_seek_data_id = None
|
||||
|
||||
self._notify_source_signal_id = None
|
||||
self._about_to_finish_id = None
|
||||
self._message_signal_id = None
|
||||
|
||||
def on_start(self):
|
||||
try:
|
||||
@ -70,26 +70,36 @@ class Audio(pykka.ThreadingActor):
|
||||
self._teardown_mixer()
|
||||
self._teardown_playbin()
|
||||
|
||||
def _connect(self, element, event, *args):
|
||||
"""Helper to keep track of signal ids based on element+event"""
|
||||
self._signal_ids[(element, event)] = element.connect(event, *args)
|
||||
|
||||
def _disconnect(self, element, event):
|
||||
"""Helper to disconnect signals created with _connect helper."""
|
||||
signal_id = self._signal_ids.pop((element, event), None)
|
||||
if signal_id is not None:
|
||||
element.disconnect(signal_id)
|
||||
|
||||
def _setup_playbin(self):
|
||||
self._playbin = gst.element_factory_make('playbin2')
|
||||
playbin = gst.element_factory_make('playbin2')
|
||||
|
||||
fakesink = gst.element_factory_make('fakesink')
|
||||
self._playbin.set_property('video-sink', fakesink)
|
||||
playbin.set_property('video-sink', fakesink)
|
||||
|
||||
self._about_to_finish_id = self._playbin.connect(
|
||||
'about-to-finish', self._on_about_to_finish)
|
||||
self._notify_source_signal_id = self._playbin.connect(
|
||||
'notify::source', self._on_new_source)
|
||||
self._connect(playbin, 'about-to-finish', self._on_about_to_finish)
|
||||
self._connect(playbin, 'notify::source', self._on_new_source)
|
||||
|
||||
self._playbin = playbin
|
||||
|
||||
def _on_about_to_finish(self, element):
|
||||
# Cleanup appsrc related stuff.
|
||||
old_appsrc, self._appsrc = self._appsrc, None
|
||||
|
||||
if self._appsrc_seek_data_id is not None and old_appsrc:
|
||||
old_appsrc.disconnect(self._appsrc_seek_data_id)
|
||||
self._disconnect(old_appsrc, 'need-data')
|
||||
self._disconnect(old_appsrc, 'enough-data')
|
||||
self._disconnect(old_appsrc, 'seek-data')
|
||||
|
||||
self._appsrc_caps = None
|
||||
self._appsrc_seek_data_id = None
|
||||
|
||||
# TODO: this is just a horrible hack to get us started. the
|
||||
# comunication is correct, but this way of hooking it up is not.
|
||||
@ -112,23 +122,35 @@ class Audio(pykka.ThreadingActor):
|
||||
source.set_property('caps', self._appsrc_caps)
|
||||
source.set_property('format', b'time')
|
||||
source.set_property('stream-type', b'seekable')
|
||||
source.set_property('max-bytes', 1 * MB)
|
||||
source.set_property('min-percent', 50)
|
||||
|
||||
self._appsrc_seek_data_id = source.connect(
|
||||
'seek-data', self._appsrc_on_seek_data)
|
||||
self._connect(source, 'need-data', self._appsrc_on_need_data)
|
||||
self._connect(source, 'enough-data', self._appsrc_on_enough_data)
|
||||
self._connect(source, 'seek-data', self._appsrc_on_seek_data)
|
||||
|
||||
self._appsrc = source
|
||||
|
||||
def _appsrc_on_seek_data(self, appsrc, time_in_ns):
|
||||
time_in_ms = time_in_ns // gst.MSECOND
|
||||
def _appsrc_on_need_data(self, appsrc, gst_length_hint):
|
||||
length_hint = utils.clocktime_to_millisecond(gst_length_hint)
|
||||
if self._appsrc_need_data_callback is not None:
|
||||
self._appsrc_need_data_callback(length_hint)
|
||||
return True
|
||||
|
||||
def _appsrc_on_enough_data(self, appsrc):
|
||||
if self._appsrc_enough_data_callback is not None:
|
||||
self._appsrc_enough_data_callback()
|
||||
return True
|
||||
|
||||
def _appsrc_on_seek_data(self, appsrc, gst_position):
|
||||
position = utils.clocktime_to_millisecond(gst_position)
|
||||
if self._appsrc_seek_data_callback is not None:
|
||||
self._appsrc_seek_data_callback(time_in_ms)
|
||||
self._appsrc_seek_data_callback(position)
|
||||
return True
|
||||
|
||||
def _teardown_playbin(self):
|
||||
if self._about_to_finish_id:
|
||||
self._playbin.disconnect(self._about_to_finish_id)
|
||||
if self._notify_source_signal_id:
|
||||
self._playbin.disconnect(self._notify_source_signal_id)
|
||||
self._disconnect(self._playbin, 'about-to-finish')
|
||||
self._disconnect(self._playbin, 'notify::source')
|
||||
self._playbin.set_state(gst.STATE_NULL)
|
||||
|
||||
def _setup_output(self):
|
||||
@ -186,15 +208,23 @@ class Audio(pykka.ThreadingActor):
|
||||
mixer.get_factory().get_name(), track.label)
|
||||
|
||||
def _select_mixer_track(self, mixer, track_label):
|
||||
# Look for track with label == MIXER_TRACK, otherwise fallback to
|
||||
# master track which is also an output.
|
||||
# Ignore tracks without volumes, then look for track with
|
||||
# label == settings.MIXER_TRACK, otherwise fallback to first usable
|
||||
# track hoping the mixer gave them to us in a sensible order.
|
||||
|
||||
usable_tracks = []
|
||||
for track in mixer.list_tracks():
|
||||
if track_label:
|
||||
if track.label == track_label:
|
||||
return track
|
||||
if not mixer.get_volume(track):
|
||||
continue
|
||||
|
||||
if track_label and track.label == track_label:
|
||||
return track
|
||||
elif track.flags & (gst.interfaces.MIXER_TRACK_MASTER |
|
||||
gst.interfaces.MIXER_TRACK_OUTPUT):
|
||||
return track
|
||||
usable_tracks.append(track)
|
||||
|
||||
if usable_tracks:
|
||||
return usable_tracks[0]
|
||||
|
||||
def _teardown_mixer(self):
|
||||
if self._mixer is not None:
|
||||
@ -203,19 +233,21 @@ class Audio(pykka.ThreadingActor):
|
||||
def _setup_message_processor(self):
|
||||
bus = self._playbin.get_bus()
|
||||
bus.add_signal_watch()
|
||||
self._message_signal_id = bus.connect('message', self._on_message)
|
||||
self._connect(bus, 'message', self._on_message)
|
||||
|
||||
def _teardown_message_processor(self):
|
||||
if self._message_signal_id:
|
||||
bus = self._playbin.get_bus()
|
||||
bus.disconnect(self._message_signal_id)
|
||||
bus.remove_signal_watch()
|
||||
bus = self._playbin.get_bus()
|
||||
self._disconnect(bus, 'message')
|
||||
bus.remove_signal_watch()
|
||||
|
||||
def _on_message(self, bus, message):
|
||||
if (message.type == gst.MESSAGE_STATE_CHANGED
|
||||
and message.src == self._playbin):
|
||||
old_state, new_state, pending_state = message.parse_state_changed()
|
||||
self._on_playbin_state_changed(old_state, new_state, pending_state)
|
||||
elif message.type == gst.MESSAGE_BUFFERING:
|
||||
percent = message.parse_buffering()
|
||||
logger.debug('Buffer %d%% full', percent)
|
||||
elif message.type == gst.MESSAGE_EOS:
|
||||
self._on_end_of_stream()
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
@ -270,7 +302,8 @@ class Audio(pykka.ThreadingActor):
|
||||
"""
|
||||
self._playbin.set_property('uri', uri)
|
||||
|
||||
def set_appsrc(self, caps, seek_data=None):
|
||||
def set_appsrc(
|
||||
self, caps, need_data=None, enough_data=None, seek_data=None):
|
||||
"""
|
||||
Switch to using appsrc for getting audio to be played.
|
||||
|
||||
@ -279,6 +312,10 @@ class Audio(pykka.ThreadingActor):
|
||||
:param caps: GStreamer caps string describing the audio format to
|
||||
expect
|
||||
:type caps: string
|
||||
:param need_data: callback for when appsrc needs data
|
||||
:type need_data: callable which takes data length hint in ms
|
||||
:param enough_data: callback for when appsrc has enough data
|
||||
:type enough_data: callable
|
||||
:param seek_data: callback for when data from a new position is needed
|
||||
to continue playback
|
||||
:type seek_data: callable which takes time position in ms
|
||||
@ -286,6 +323,8 @@ class Audio(pykka.ThreadingActor):
|
||||
if isinstance(caps, unicode):
|
||||
caps = caps.encode('utf-8')
|
||||
self._appsrc_caps = gst.Caps(caps)
|
||||
self._appsrc_need_data_callback = need_data
|
||||
self._appsrc_enough_data_callback = enough_data
|
||||
self._appsrc_seek_data_callback = seek_data
|
||||
self._playbin.set_property('uri', 'appsrc://')
|
||||
|
||||
@ -322,8 +361,8 @@ class Audio(pykka.ThreadingActor):
|
||||
:rtype: int
|
||||
"""
|
||||
try:
|
||||
position = self._playbin.query_position(gst.FORMAT_TIME)[0]
|
||||
return position // gst.MSECOND
|
||||
gst_position = self._playbin.query_position(gst.FORMAT_TIME)[0]
|
||||
return utils.clocktime_to_millisecond(gst_position)
|
||||
except gst.QueryError:
|
||||
logger.debug('Position query failed')
|
||||
return 0
|
||||
@ -336,9 +375,9 @@ class Audio(pykka.ThreadingActor):
|
||||
:type position: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
gst_position = utils.millisecond_to_clocktime(position)
|
||||
return self._playbin.seek_simple(
|
||||
gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH,
|
||||
position * gst.MSECOND)
|
||||
gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, gst_position)
|
||||
|
||||
def start_playback(self):
|
||||
"""
|
||||
|
||||
@ -6,7 +6,8 @@ import gst
|
||||
|
||||
|
||||
def calculate_duration(num_samples, sample_rate):
|
||||
"""Determine duration of samples using GStreamer helper for precise math."""
|
||||
"""Determine duration of samples using GStreamer helper for precise
|
||||
math."""
|
||||
return gst.util_uint64_scale(num_samples, gst.SECOND, sample_rate)
|
||||
|
||||
|
||||
@ -28,10 +29,15 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None):
|
||||
|
||||
|
||||
def millisecond_to_clocktime(value):
|
||||
"""Convert a millisecond time to internal gstreamer time."""
|
||||
"""Convert a millisecond time to internal GStreamer time."""
|
||||
return value * gst.MSECOND
|
||||
|
||||
|
||||
def clocktime_to_millisecond(value):
|
||||
"""Convert an internal GStreamer time to millisecond time."""
|
||||
return value // gst.MSECOND
|
||||
|
||||
|
||||
def supported_uri_schemes(uri_schemes):
|
||||
"""Determine which URIs we can actually support from provided whitelist.
|
||||
|
||||
|
||||
@ -12,8 +12,15 @@ from mopidy.backends import base
|
||||
logger = logging.getLogger('mopidy.backends.spotify')
|
||||
|
||||
|
||||
def need_data_callback(spotify_backend, length_hint):
|
||||
spotify_backend.playback.on_need_data(length_hint)
|
||||
|
||||
|
||||
def enough_data_callback(spotify_backend):
|
||||
spotify_backend.playback.on_enough_data()
|
||||
|
||||
|
||||
def seek_data_callback(spotify_backend, time_position):
|
||||
logger.debug('seek_data_callback(%d) called', time_position)
|
||||
spotify_backend.playback.on_seek_data(time_position)
|
||||
|
||||
|
||||
@ -29,12 +36,22 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider):
|
||||
self._first_seek = False
|
||||
|
||||
def change_track(self, track):
|
||||
spotify_backend = self.backend.actor_ref.proxy()
|
||||
|
||||
need_data_callback_bound = functools.partial(
|
||||
need_data_callback, spotify_backend)
|
||||
enough_data_callback_bound = functools.partial(
|
||||
enough_data_callback, spotify_backend)
|
||||
seek_data_callback_bound = functools.partial(
|
||||
seek_data_callback, self.backend.actor_ref.proxy())
|
||||
seek_data_callback, spotify_backend)
|
||||
|
||||
self._first_seek = True
|
||||
|
||||
self.audio.set_appsrc(self._caps, seek_data=seek_data_callback_bound)
|
||||
self.audio.set_appsrc(
|
||||
self._caps,
|
||||
need_data=need_data_callback_bound,
|
||||
enough_data=enough_data_callback_bound,
|
||||
seek_data=seek_data_callback_bound)
|
||||
self.audio.set_metadata(track)
|
||||
|
||||
try:
|
||||
@ -42,7 +59,6 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider):
|
||||
Link.from_string(track.uri).as_track())
|
||||
self.backend.spotify.buffer_timestamp = 0
|
||||
self.backend.spotify.session.play(1)
|
||||
|
||||
return True
|
||||
except SpotifyError as e:
|
||||
logger.info('Playback of %s failed: %s', track.uri, e)
|
||||
@ -52,6 +68,14 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider):
|
||||
self.backend.spotify.session.play(0)
|
||||
return super(SpotifyPlaybackProvider, self).stop()
|
||||
|
||||
def on_need_data(self, length_hint):
|
||||
logger.debug('playback.on_need_data(%d) called', length_hint)
|
||||
self.backend.spotify.push_audio_data = True
|
||||
|
||||
def on_enough_data(self):
|
||||
logger.debug('playback.on_enough_data() called')
|
||||
self.backend.spotify.push_audio_data = False
|
||||
|
||||
def on_seek_data(self, time_position):
|
||||
logger.debug('playback.on_seek_data(%d) called', time_position)
|
||||
|
||||
|
||||
@ -42,6 +42,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
self.backend_ref = backend_ref
|
||||
|
||||
self.connected = threading.Event()
|
||||
self.push_audio_data = True
|
||||
self.buffer_timestamp = 0
|
||||
|
||||
self.container_manager = None
|
||||
@ -104,6 +105,10 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
|
||||
"""Callback used by pyspotify"""
|
||||
# pylint: disable = R0913
|
||||
# Too many arguments (8/5)
|
||||
|
||||
if not self.push_audio_data:
|
||||
return 0
|
||||
|
||||
assert sample_type == 0, 'Expects 16-bit signed integer samples'
|
||||
capabilites = """
|
||||
audio/x-raw-int,
|
||||
|
||||
@ -198,7 +198,7 @@ them:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.on(console.log);
|
||||
mopidy.on(console.log.bind(console));
|
||||
|
||||
Several types of events are emitted:
|
||||
|
||||
@ -289,7 +289,8 @@ Instead, typical usage will look like this:
|
||||
}
|
||||
};
|
||||
|
||||
mopidy.playback.getCurrentTrack().then(printCurrentTrack, console.error);
|
||||
mopidy.playback.getCurrentTrack().then(
|
||||
printCurrentTrack, console.error.bind(console));
|
||||
|
||||
The first function passed to ``then()``, ``printCurrentTrack``, is the callback
|
||||
that will be called if the method call succeeds. The second function,
|
||||
@ -303,7 +304,7 @@ callback:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
mopidy.playback.next().then(null, console.error);
|
||||
mopidy.playback.next().then(null, console.error.bind(console));
|
||||
|
||||
The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A
|
||||
<http://wiki.commonjs.org/wiki/Promises/A>`_ standard. We use the
|
||||
@ -368,6 +369,8 @@ Example to get started with
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var consoleError = console.error.bind(error);
|
||||
|
||||
var trackDesc = function (track) {
|
||||
return track.name + " by " + track.artists[0].name +
|
||||
" from " + track.album.name;
|
||||
@ -381,14 +384,14 @@ Example to get started with
|
||||
mopidy.playback.play(tlTracks[0]).then(function () {
|
||||
mopidy.playback.getCurrentTrack().then(function (track) {
|
||||
console.log("Now playing:", trackDesc(track));
|
||||
}, console.error);
|
||||
}, console.error);
|
||||
}, console.error);
|
||||
}, console.error);
|
||||
}, consoleError);
|
||||
}, consoleError);
|
||||
}, consoleError);
|
||||
}, consoleError);
|
||||
};
|
||||
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log); // Log all events
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log.bind(console)); // Log all events
|
||||
mopidy.on("state:online", queueAndPlayFirstPlaylist);
|
||||
|
||||
Approximately the same behavior in a more functional style, using chaining
|
||||
@ -396,6 +399,8 @@ Example to get started with
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
var consoleError = console.error.bind(error);
|
||||
|
||||
var getFirst = function (list) {
|
||||
return list[0];
|
||||
};
|
||||
@ -429,23 +434,23 @@ Example to get started with
|
||||
var queueAndPlayFirstPlaylist = function () {
|
||||
mopidy.playlists.getPlaylists()
|
||||
// => list of Playlists
|
||||
.then(getFirst, console.error)
|
||||
.then(getFirst, consoleError)
|
||||
// => Playlist
|
||||
.then(printTypeAndName, console.error)
|
||||
.then(printTypeAndName, consoleError)
|
||||
// => Playlist
|
||||
.then(extractTracks, console.error)
|
||||
.then(extractTracks, consoleError)
|
||||
// => list of Tracks
|
||||
.then(mopidy.tracklist.add, console.error)
|
||||
.then(mopidy.tracklist.add, consoleError)
|
||||
// => list of TlTracks
|
||||
.then(getFirst, console.error)
|
||||
.then(getFirst, consoleError)
|
||||
// => TlTrack
|
||||
.then(mopidy.playback.play, console.error)
|
||||
.then(mopidy.playback.play, consoleError)
|
||||
// => null
|
||||
.then(printNowPlaying, console.error);
|
||||
.then(printNowPlaying, consoleError);
|
||||
};
|
||||
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log); // Log all events
|
||||
var mopidy = new Mopidy(); // Connect to server
|
||||
mopidy.on(console.log.bind(console)); // Log all events
|
||||
mopidy.on("state:online", queueAndPlayFirstPlaylist);
|
||||
|
||||
9. The web page should now queue and play your first playlist every time your
|
||||
|
||||
@ -19,10 +19,12 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
|
||||
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||
port = settings.MPD_SERVER_PORT
|
||||
|
||||
# NOTE: dict key must be bytestring to work on Python < 2.6.5
|
||||
# See https://github.com/mopidy/mopidy/issues/302 for details
|
||||
try:
|
||||
network.Server(
|
||||
hostname, port,
|
||||
protocol=session.MpdSession, protocol_kwargs={'core': core},
|
||||
protocol=session.MpdSession, protocol_kwargs={b'core': core},
|
||||
max_connections=settings.MPD_SERVER_MAX_CONNECTIONS,
|
||||
timeout=settings.MPD_SERVER_CONNECTION_TIMEOUT)
|
||||
except IOError as error:
|
||||
|
||||
@ -56,7 +56,12 @@ def handle_request(pattern, auth_required=True):
|
||||
if match is not None:
|
||||
mpd_commands.add(
|
||||
MpdCommand(name=match.group(), auth_required=auth_required))
|
||||
compiled_pattern = re.compile(pattern, flags=re.UNICODE)
|
||||
# NOTE: Make pattern a bytestring to get bytestring keys in the dict
|
||||
# returned from matches.groupdict(), which is again used as a **kwargs
|
||||
# dict. This is needed to work on Python < 2.6.5. See
|
||||
# https://github.com/mopidy/mopidy/issues/302 for details.
|
||||
bytestring_pattern = pattern.encode('utf-8')
|
||||
compiled_pattern = re.compile(bytestring_pattern, flags=re.UNICODE)
|
||||
if compiled_pattern in request_handlers:
|
||||
raise ValueError('Tried to redefine handler for %s with %s' % (
|
||||
pattern, func))
|
||||
|
||||
@ -33,5 +33,6 @@ class VersionTest(unittest.TestCase):
|
||||
self.assertLess(SV('0.8.0'), SV('0.8.1'))
|
||||
self.assertLess(SV('0.8.1'), SV('0.9.0'))
|
||||
self.assertLess(SV('0.9.0'), SV('0.10.0'))
|
||||
self.assertLess(SV('0.10.0'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.11.1'))
|
||||
self.assertLess(SV('0.10.0'), SV('0.11.0'))
|
||||
self.assertLess(SV('0.11.0'), SV(__version__))
|
||||
self.assertLess(SV(__version__), SV('0.11.2'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user