Skip to content

Commit

Permalink
Release v2.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jodal committed Feb 6, 2016
2 parents 7903253 + f7c8b96 commit 8de7786
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 47 deletions.
19 changes: 8 additions & 11 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
sudo: false
sudo: required
dist: trusty

language: python

python:
- "2.7_with_system_site_packages"

addons:
apt:
sources:
- mopidy-stable
packages:
- libffi-dev
- libspotify-dev
- mopidy
- python-all-dev

env:
- TOX_ENV=py27
- TOX_ENV=flake8

before_install:
- "wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add -"
- "sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/jessie.list"
- "sudo apt-get update -qq"
- "sudo apt-get install -y python-gst0.10 libffi-dev libspotify-dev python-all-dev"

install:
- "pip install tox"

Expand Down
32 changes: 32 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,41 @@ Project resources
- `Issue tracker <https://github.com/mopidy/mopidy-spotify/issues>`_


Credits
=======

- Original author: `Stein Magnus Jodal <https://github.com/jodal>`__
- Current maintainer: `Stein Magnus Jodal <https://github.com/jodal>`__
- `Contributors <https://github.com/mopidy/mopidy-spotify/graphs/contributors>`_


Changelog
=========

v2.3.0 (2016-02-06)
-------------------

Feature release.

- Ignore all audio data deliveries from libspotify when when a seek is in
progress. This ensures that we don't deliver audio data from before the seek
with timestamps from after the seek.

- Ignore duplicate end of track callbacks.

- Don't increase the audio buffer timestamp if the buffer is rejected by
Mopidy. This caused audio buffers delivered after one or more rejected audio
buffers to have too high timestamps.

- When changing tracks, block until Mopidy completes the appsrc URI change.
Not blocking here might break gapless playback.

- Lookup of a playlist you're not subscribed to will now properly load all of
the playlist's tracks. (Fixes: #81, PR: #82)

- Workaround teardown race outputing lots of short stack traces on Mopidy
shutdown. (See #73 for details)

v2.2.0 (2015-11-15)
-------------------

Expand Down
2 changes: 1 addition & 1 deletion mopidy_spotify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from mopidy import config, ext


__version__ = '2.2.0'
__version__ = '2.3.0'


class Extension(ext.Extension):
Expand Down
10 changes: 5 additions & 5 deletions mopidy_spotify/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

from mopidy import backend

import mopidy_spotify
from mopidy_spotify import browse, distinct, images, lookup, search, utils
# Workaround https://github.com/public/flake8-import-order/issues/49:
from mopidy_spotify import Extension
from mopidy_spotify import (
__version__, browse, distinct, images, lookup, search, utils)


logger = logging.getLogger(__name__)
Expand All @@ -19,9 +21,7 @@ def __init__(self, backend):
self._config = backend._config['spotify']
self._requests_session = utils.get_requests_session(
proxy_config=backend._config['proxy'],
user_agent='%s/%s' % (
mopidy_spotify.Extension.dist_name,
mopidy_spotify.__version__))
user_agent='%s/%s' % (Extension.dist_name, __version__))

def browse(self, uri):
return browse.browse(self._config, self._backend._session, uri)
Expand Down
1 change: 1 addition & 0 deletions mopidy_spotify/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def _lookup_playlist(config, sp_link):
sp_playlist = sp_link.as_playlist()
sp_playlist.load()
for sp_track in sp_playlist.tracks:
sp_track.load()
track = translator.to_track(
sp_track, bitrate=config['bitrate'])
if track is not None:
Expand Down
66 changes: 49 additions & 17 deletions mopidy_spotify/playback.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,69 +29,83 @@ def __init__(self, *args, **kwargs):
self._timeout = self.backend._config['spotify']['timeout']

self._buffer_timestamp = BufferTimestamp(0)
self._seeking_event = threading.Event()
self._first_seek = False
self._push_audio_data_event = threading.Event()
self._push_audio_data_event.set()
self._end_of_track_event = threading.Event()
self._events_connected = False

def _connect_events(self):
if not self._events_connected:
self._events_connected = True
self.backend._session.on(
spotify.SessionEvent.MUSIC_DELIVERY, music_delivery_callback,
self.audio, self._push_audio_data_event,
self.audio, self._seeking_event, self._push_audio_data_event,
self._buffer_timestamp)
self.backend._session.on(
spotify.SessionEvent.END_OF_TRACK, end_of_track_callback,
self.audio)
self._end_of_track_event, self.audio)

def change_track(self, track):
self._connect_events()

if track.uri is None:
return False

logger.debug(
'Audio requested change of track; '
'loading and starting Spotify player')

need_data_callback_bound = functools.partial(
need_data_callback, self._push_audio_data_event)
enough_data_callback_bound = functools.partial(
enough_data_callback, self._push_audio_data_event)

seek_data_callback_bound = functools.partial(
seek_data_callback, self.backend._actor_proxy)
seek_data_callback, self._seeking_event, self.backend._actor_proxy)

self._buffer_timestamp.set(0)
self._first_seek = True
self._end_of_track_event.clear()

try:
sp_track = self.backend._session.get_track(track.uri)
sp_track.load(self._timeout)
self.backend._session.player.load(sp_track)
self.backend._session.player.play()

self._buffer_timestamp.set(0)
self.audio.set_appsrc(
future = self.audio.set_appsrc(
LIBSPOTIFY_GST_CAPS,
need_data=need_data_callback_bound,
enough_data=enough_data_callback_bound,
seek_data=seek_data_callback_bound)
self.audio.set_metadata(track)

# Gapless playback requires that we block until URI change in
# mopidy.audio has completed before we return from change_track().
future.get()

return True
except spotify.Error as exc:
logger.info('Playback of %s failed: %s', track.uri, exc)
return False

def resume(self):
logger.debug('Audio requested resume; starting Spotify player')
self.backend._session.player.play()
return super(SpotifyPlaybackProvider, self).resume()

def stop(self):
logger.debug('Audio requested stop; pausing Spotify player')
self.backend._session.player.pause()
return super(SpotifyPlaybackProvider, self).stop()

def on_seek_data(self, time_position):
logger.debug('Audio asked us to seek to %d', time_position)
logger.debug('Audio requested seek to %d', time_position)

if time_position == 0 and self._first_seek:
self._seeking_event.clear()
self._first_seek = False
logger.debug('Skipping seek due to issue mopidy/mopidy#300')
return
Expand All @@ -105,32 +119,45 @@ def need_data_callback(push_audio_data_event, length_hint):
# This callback is called from GStreamer/the GObject event loop.
logger.log(
TRACE_LOG_LEVEL,
'Audio asked for more data (hint=%d); accepting deliveries',
'Audio requested more data (hint=%d); accepting deliveries',
length_hint)
push_audio_data_event.set()


def enough_data_callback(push_audio_data_event):
# This callback is called from GStreamer/the GObject event loop.
logger.log(
TRACE_LOG_LEVEL, 'Audio says it has enough data; rejecting deliveries')
TRACE_LOG_LEVEL, 'Audio has enough data; rejecting deliveries')
push_audio_data_event.clear()


def seek_data_callback(spotify_backend, time_position):
def seek_data_callback(seeking_event, spotify_backend, time_position):
# This callback is called from GStreamer/the GObject event loop.
# It forwards the call to the backend actor.
seeking_event.set()
spotify_backend.playback.on_seek_data(time_position)


def music_delivery_callback(
session, audio_format, frames, num_frames,
audio_actor, push_audio_data_event, buffer_timestamp):
audio_actor, seeking_event, push_audio_data_event, buffer_timestamp):
# This is called from an internal libspotify thread.
# Ideally, nothing here should block.

if seeking_event.is_set():
# A seek has happened, but libspotify hasn't confirmed yet, so
# we're dropping all audio data from libspotify.
if num_frames == 0:
# libspotify signals that it has completed the seek. We'll accept
# the next audio data delivery.
seeking_event.clear()
return num_frames

if not push_audio_data_event.is_set():
return 0
return 0 # Reject the audio data. It will be redelivered later.

if not frames:
return 0 # No audio data; return immediately.

known_format = (
audio_format.sample_type == spotify.SampleType.INT16_NATIVE_ENDIAN)
Expand All @@ -149,25 +176,30 @@ def music_delivery_callback(
'sample_rate': audio_format.sample_rate,
}

duration = audio.calculate_duration(
num_frames, audio_format.sample_rate)
duration = audio.calculate_duration(num_frames, audio_format.sample_rate)
buffer_ = audio.create_buffer(
bytes(frames), capabilites=capabilites,
timestamp=buffer_timestamp.get(), duration=duration)

buffer_timestamp.increase(duration)

# We must block here to know if the buffer was consumed successfully.
if audio_actor.emit_data(buffer_).get():
consumed = audio_actor.emit_data(buffer_).get()

if consumed:
buffer_timestamp.increase(duration)
return num_frames
else:
return 0


def end_of_track_callback(session, audio_actor):
def end_of_track_callback(session, end_of_track_event, audio_actor):
# This callback is called from the pyspotify event loop.

if end_of_track_event.is_set():
logger.debug('End of track already received; ignoring callback')
return

logger.debug('End of track reached')
end_of_track_event.set()
audio_actor.emit_data(None)


Expand Down
1 change: 1 addition & 0 deletions tests/test_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def test_lookup_of_playlist_uri(session_mock, sp_playlist_mock, provider):
session_mock.get_link.assert_called_once_with('spotify:playlist:alice:foo')
sp_playlist_mock.link.as_playlist.assert_called_once_with()
sp_playlist_mock.load.assert_called_once_with()
sp_playlist_mock.tracks[0].load.assert_called_once_with()

assert len(results) == 1
track = results[0]
Expand Down
Loading

0 comments on commit 8de7786

Please sign in to comment.