spotify: Move to external extension

This commit is contained in:
Stein Magnus Jodal 2013-10-08 22:42:26 +02:00
parent f9a6fa525a
commit e7d6a995e8
18 changed files with 24 additions and 968 deletions

View File

@ -2,7 +2,6 @@ include *.rst
include LICENSE include LICENSE
include MANIFEST.in include MANIFEST.in
include data/mopidy.desktop include data/mopidy.desktop
include mopidy/backends/spotify/spotify_appkey.key
include pylintrc include pylintrc
recursive-include docs * recursive-include docs *

View File

@ -9,15 +9,18 @@ v0.16.0 (UNRELEASED)
**Dependencies** **Dependencies**
- The Last.fm scrobbler has been moved to its own external extension, Parts of Mopidy have been moved to their own external extensions. If you want
`Mopidy-Scrobbler <https://github.com/mopidy/mopidy-scrobbler>`_. You'll need Mopidy to continue to work like it used to, you may have to install one or more
to install it in addition to Mopidy if you want it to continue to work as it of the following extensions as well:
used to.
- The MPRIS frontend has been moved to its own external extension, - The Spotify backend has been moved to
`Mopidy-MPRIS <https://github.com/mopidy/mopidy-mpris>`_. You'll need to `Mopidy-Scrobbler <https://github.com/mopidy/mopidy-spotify>`_.
install it in addition to Mopidy if you want it to continue to work as it
used to. - The Last.fm scrobbler has been moved to
`Mopidy-Scrobbler <https://github.com/mopidy/mopidy-scrobbler>`_.
- The MPRIS frontend has been moved to
`Mopidy-MPRIS <https://github.com/mopidy/mopidy-mpris>`_.
**Audio** **Audio**

View File

@ -87,10 +87,19 @@ Mopidy-SoundCloud
https://github.com/mopidy/mopidy-soundcloud https://github.com/mopidy/mopidy-soundcloud
Provides a backend for playing music from the `SoundCloud rovides a backend for playing music from the `SoundCloud
<http://www.soundcloud.com/>`_ service. <http://www.soundcloud.com/>`_ service.
Mopidy-Spotify
--------------
https://github.com/mopidy/mopidy-spotify
Extension for playing music from the `Spotify <http://www.spotify.com/>`_ music
streaming service.
Mopidy-Subsonic Mopidy-Subsonic
--------------- ---------------

View File

@ -1,83 +0,0 @@
.. _ext-spotify:
**************
Mopidy-Spotify
**************
An extension for playing music from Spotify.
`Spotify <http://www.spotify.com/>`_ is a music streaming service. The backend
uses the official `libspotify
<http://developer.spotify.com/en/libspotify/overview/>`_ library and the
`pyspotify <http://github.com/mopidy/pyspotify/>`_ Python bindings for
libspotify. This backend handles URIs starting with ``spotify:``.
.. note::
This product uses SPOTIFY(R) CORE but is not endorsed, certified or
otherwise approved in any way by Spotify. Spotify is the registered
trade mark of the Spotify Group.
Known issues
============
https://github.com/mopidy/mopidy/issues?labels=Spotify+backend
Dependencies
============
.. literalinclude:: ../../requirements/spotify.txt
Default configuration
=====================
.. literalinclude:: ../../mopidy/backends/spotify/ext.conf
:language: ini
Configuration values
====================
.. confval:: spotify/enabled
If the Spotify extension should be enabled or not.
.. confval:: spotify/username
Your Spotify Premium username.
.. confval:: spotify/password
Your Spotify Premium password.
.. confval:: spotify/bitrate
The preferred audio bitrate. Valid values are 96, 160, 320.
.. confval:: spotify/timeout
Max number of seconds to wait for Spotify operations to complete.
.. confval:: spotify/cache_dir
Path to the Spotify data cache. Cannot be shared with other Spotify apps.
Usage
=====
If you are using the Spotify backend, which is the default, enter your Spotify
Premium account's username and password into ``~/.config/mopidy/mopidy.conf``,
like this:
.. code-block:: ini
[spotify]
username = myusername
password = mysecret
This will only work if you have the Spotify Premium subscription. Spotify
Unlimited will not work.

View File

@ -4,9 +4,9 @@ Mopidy
Mopidy is a music server which can play music both from multiple sources, like Mopidy is a music server which can play music both from multiple sources, like
your :ref:`local hard drive <ext-local>`, :ref:`radio streams <ext-stream>`, your :ref:`local hard drive <ext-local>`, :ref:`radio streams <ext-stream>`,
and from :ref:`Spotify <ext-spotify>` and SoundCloud. Searches combines results and from Spotify and SoundCloud. Searches combines results from all music
from all music sources, and you can mix tracks from all sources in your play sources, and you can mix tracks from all sources in your play queue. Your
queue. Your playlists from Spotify or SoundCloud are also available for use. playlists from Spotify or SoundCloud are also available for use.
To control your Mopidy music server, you can use one of Mopidy's :ref:`web To control your Mopidy music server, you can use one of Mopidy's :ref:`web
clients <http-clients>`, the :ref:`Ubuntu Sound Menu <ubuntu-sound-menu>`, any clients <http-clients>`, the :ref:`Ubuntu Sound Menu <ubuntu-sound-menu>`, any

View File

@ -1,36 +0,0 @@
from __future__ import unicode_literals
import os
import mopidy
from mopidy import config, exceptions, ext
class Extension(ext.Extension):
dist_name = 'Mopidy-Spotify'
ext_name = 'spotify'
version = mopidy.__version__
def get_default_config(self):
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
return config.read(conf_file)
def get_config_schema(self):
schema = super(Extension, self).get_config_schema()
schema['username'] = config.String()
schema['password'] = config.Secret()
schema['bitrate'] = config.Integer(choices=(96, 160, 320))
schema['timeout'] = config.Integer(minimum=0)
schema['cache_dir'] = config.Path()
return schema
def validate_environment(self):
try:
import spotify # noqa
except ImportError as e:
raise exceptions.ExtensionError('pyspotify library not found', e)
def get_backend_classes(self):
from .actor import SpotifyBackend
return [SpotifyBackend]

View File

@ -1,37 +0,0 @@
from __future__ import unicode_literals
import logging
import pykka
from mopidy.backends import base
from mopidy.backends.spotify.library import SpotifyLibraryProvider
from mopidy.backends.spotify.playback import SpotifyPlaybackProvider
from mopidy.backends.spotify.session_manager import SpotifySessionManager
from mopidy.backends.spotify.playlists import SpotifyPlaylistsProvider
logger = logging.getLogger('mopidy.backends.spotify')
class SpotifyBackend(pykka.ThreadingActor, base.Backend):
def __init__(self, config, audio):
super(SpotifyBackend, self).__init__()
self.config = config
self.library = SpotifyLibraryProvider(backend=self)
self.playback = SpotifyPlaybackProvider(audio=audio, backend=self)
self.playlists = SpotifyPlaylistsProvider(backend=self)
self.uri_schemes = ['spotify']
self.spotify = SpotifySessionManager(
config, audio=audio, backend_ref=self.actor_ref)
def on_start(self):
logger.info('Mopidy uses SPOTIFY(R) CORE')
logger.debug('Connecting to Spotify')
self.spotify.start()
def on_stop(self):
self.spotify.logout()

View File

@ -1,51 +0,0 @@
from __future__ import unicode_literals
import logging
from spotify.manager import SpotifyContainerManager as \
PyspotifyContainerManager
logger = logging.getLogger('mopidy.backends.spotify')
class SpotifyContainerManager(PyspotifyContainerManager):
def __init__(self, session_manager):
PyspotifyContainerManager.__init__(self)
self.session_manager = session_manager
def container_loaded(self, container, userdata):
"""Callback used by pyspotify"""
logger.debug('Callback called: playlist container loaded')
self.session_manager.refresh_playlists()
count = 0
for playlist in self.session_manager.session.playlist_container():
if playlist.type() == 'playlist':
self.session_manager.playlist_manager.watch(playlist)
count += 1
logger.debug('Watching %d playlist(s) for changes', count)
def playlist_added(self, container, playlist, position, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: playlist added at position %d', position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.
def playlist_moved(self, container, playlist, old_position, new_position,
userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: playlist "%s" moved from position %d to %d',
playlist.name(), old_position, new_position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.
def playlist_removed(self, container, playlist, position, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: playlist "%s" removed from position %d',
playlist.name(), position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.

View File

@ -1,7 +0,0 @@
[spotify]
enabled = true
username =
password =
bitrate = 160
timeout = 10
cache_dir = $XDG_CACHE_DIR/mopidy/spotify

View File

@ -1,211 +0,0 @@
from __future__ import unicode_literals
import logging
import time
import urllib
import pykka
from spotify import Link, SpotifyError
from mopidy.backends import base
from mopidy.models import Track, SearchResult
from . import translator
logger = logging.getLogger('mopidy.backends.spotify')
TRACK_AVAILABLE = 1
class SpotifyTrack(Track):
"""Proxy object for unloaded Spotify tracks."""
def __init__(self, uri=None, track=None):
super(SpotifyTrack, self).__init__()
if (uri and track) or (not uri and not track):
raise AttributeError('uri or track must be provided')
elif uri:
self._spotify_track = Link.from_string(uri).as_track()
elif track:
self._spotify_track = track
self._unloaded_track = Track(uri=uri, name='[loading...]')
self._track = None
@property
def _proxy(self):
if self._track:
return self._track
elif self._spotify_track.is_loaded():
self._track = translator.to_mopidy_track(self._spotify_track)
return self._track
else:
return self._unloaded_track
def __getattribute__(self, name):
if name.startswith('_'):
return super(SpotifyTrack, self).__getattribute__(name)
return self._proxy.__getattribute__(name)
def __repr__(self):
return self._proxy.__repr__()
def __hash__(self):
return hash(self._proxy.uri)
def __eq__(self, other):
if not isinstance(other, Track):
return False
return self._proxy.uri == other.uri
def copy(self, **values):
return self._proxy.copy(**values)
class SpotifyLibraryProvider(base.BaseLibraryProvider):
def __init__(self, *args, **kwargs):
super(SpotifyLibraryProvider, self).__init__(*args, **kwargs)
self._timeout = self.backend.config['spotify']['timeout']
def find_exact(self, query=None, uris=None):
return self.search(query=query, uris=uris)
def lookup(self, uri):
try:
link = Link.from_string(uri)
if link.type() == Link.LINK_TRACK:
return self._lookup_track(uri)
if link.type() == Link.LINK_ALBUM:
return self._lookup_album(uri)
elif link.type() == Link.LINK_ARTIST:
return self._lookup_artist(uri)
elif link.type() == Link.LINK_PLAYLIST:
return self._lookup_playlist(uri)
else:
return []
except SpotifyError as error:
logger.debug(u'Failed to lookup "%s": %s', uri, error)
return []
def _lookup_track(self, uri):
track = Link.from_string(uri).as_track()
self._wait_for_object_to_load(track)
if track.is_loaded():
if track.availability() == TRACK_AVAILABLE:
return [SpotifyTrack(track=track)]
else:
return []
else:
return [SpotifyTrack(uri=uri)]
def _lookup_album(self, uri):
album = Link.from_string(uri).as_album()
album_browser = self.backend.spotify.session.browse_album(album)
self._wait_for_object_to_load(album_browser)
return [
SpotifyTrack(track=t)
for t in album_browser if t.availability() == TRACK_AVAILABLE]
def _lookup_artist(self, uri):
artist = Link.from_string(uri).as_artist()
artist_browser = self.backend.spotify.session.browse_artist(artist)
self._wait_for_object_to_load(artist_browser)
return [
SpotifyTrack(track=t)
for t in artist_browser if t.availability() == TRACK_AVAILABLE]
def _lookup_playlist(self, uri):
playlist = Link.from_string(uri).as_playlist()
self._wait_for_object_to_load(playlist)
return [
SpotifyTrack(track=t)
for t in playlist if t.availability() == TRACK_AVAILABLE]
def _wait_for_object_to_load(self, spotify_obj, timeout=None):
# XXX Sleeping to wait for the Spotify object to load is an ugly hack,
# but it works. We should look into other solutions for this.
if timeout is None:
timeout = self._timeout
wait_until = time.time() + timeout
while not spotify_obj.is_loaded():
time.sleep(0.1)
if time.time() > wait_until:
logger.debug(
'Timeout: Spotify object did not load in %ds', timeout)
return
def refresh(self, uri=None):
pass # TODO
def search(self, query=None, uris=None):
# TODO Only return results within URI roots given by ``uris``
if not query:
return self._get_all_tracks()
uris = query.get('uri', [])
if uris:
tracks = []
for uri in uris:
tracks += self.lookup(uri)
if len(uris) == 1:
uri = uris[0]
else:
uri = 'spotify:search'
return SearchResult(uri=uri, tracks=tracks)
spotify_query = self._translate_search_query(query)
logger.debug('Spotify search query: %s' % spotify_query)
future = pykka.ThreadingFuture()
def callback(results, userdata=None):
search_result = SearchResult(
uri='spotify:search:%s' % (
urllib.quote(results.query().encode('utf-8'))),
albums=[
translator.to_mopidy_album(a) for a in results.albums()],
artists=[
translator.to_mopidy_artist(a) for a in results.artists()],
tracks=[
translator.to_mopidy_track(t) for t in results.tracks()])
future.set(search_result)
if not self.backend.spotify.connected.is_set():
logger.debug('Not connected: Spotify search cancelled')
return SearchResult(uri='spotify:search')
self.backend.spotify.session.search(
spotify_query, callback,
album_count=200, artist_count=200, track_count=200)
try:
return future.get(timeout=self._timeout)
except pykka.Timeout:
logger.debug(
'Timeout: Spotify search did not return in %ds', self._timeout)
return SearchResult(uri='spotify:search')
def _get_all_tracks(self):
# Since we can't search for the entire Spotify library, we return
# all tracks in the playlists when the query is empty.
tracks = []
for playlist in self.backend.playlists.playlists:
tracks += playlist.tracks
return SearchResult(uri='spotify:search', tracks=tracks)
def _translate_search_query(self, mopidy_query):
spotify_query = []
for (field, values) in mopidy_query.iteritems():
if field == 'date':
field = 'year'
if not hasattr(values, '__iter__'):
values = [values]
for value in values:
if field == 'any':
spotify_query.append(value)
elif field == 'year':
value = int(value.split('-')[0]) # Extract year
spotify_query.append('%s:%d' % (field, value))
else:
spotify_query.append('%s:"%s"' % (field, value))
spotify_query = ' '.join(spotify_query)
return spotify_query

View File

@ -1,94 +0,0 @@
from __future__ import unicode_literals
import logging
import functools
from spotify import Link, SpotifyError
from mopidy import audio
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):
spotify_backend.playback.on_seek_data(time_position)
class SpotifyPlaybackProvider(base.BasePlaybackProvider):
# These GStreamer caps matches the audio data provided by libspotify
_caps = (
'audio/x-raw-int, endianness=(int)1234, channels=(int)2, '
'width=(int)16, depth=(int)16, signed=(boolean)true, '
'rate=(int)44100')
def __init__(self, *args, **kwargs):
super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs)
self._first_seek = False
def play(self, track):
if track.uri is None:
return False
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, spotify_backend)
self._first_seek = True
try:
self.backend.spotify.session.load(
Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1)
self.backend.spotify.buffer_timestamp = 0
self.audio.prepare_change()
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.start_playback()
self.audio.set_metadata(track)
return True
except SpotifyError as e:
logger.info('Playback of %s failed: %s', track.uri, e)
return False
def stop(self):
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)
if time_position == 0 and self._first_seek:
self._first_seek = False
logger.debug('Skipping seek due to issue #300')
return
self.backend.spotify.buffer_timestamp = audio.millisecond_to_clocktime(
time_position)
self.backend.spotify.session.seek(time_position)

View File

@ -1,105 +0,0 @@
from __future__ import unicode_literals
import datetime
import logging
from spotify.manager import SpotifyPlaylistManager as PyspotifyPlaylistManager
logger = logging.getLogger('mopidy.backends.spotify')
class SpotifyPlaylistManager(PyspotifyPlaylistManager):
def __init__(self, session_manager):
PyspotifyPlaylistManager.__init__(self)
self.session_manager = session_manager
def tracks_added(self, playlist, tracks, position, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: '
'%d track(s) added to position %d in playlist "%s"',
len(tracks), position, playlist.name())
self.session_manager.refresh_playlists()
def tracks_moved(self, playlist, tracks, new_position, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: '
'%d track(s) moved to position %d in playlist "%s"',
len(tracks), new_position, playlist.name())
self.session_manager.refresh_playlists()
def tracks_removed(self, playlist, tracks, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: '
'%d track(s) removed from playlist "%s"',
len(tracks), playlist.name())
self.session_manager.refresh_playlists()
def playlist_renamed(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Playlist renamed to "%s"', playlist.name())
self.session_manager.refresh_playlists()
def playlist_state_changed(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: The state of playlist "%s" changed',
playlist.name())
def playlist_update_in_progress(self, playlist, done, userdata):
"""Callback used by pyspotify"""
if done:
logger.debug(
'Callback called: Update of playlist "%s" done',
playlist.name())
else:
logger.debug(
'Callback called: Update of playlist "%s" in progress',
playlist.name())
def playlist_metadata_updated(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Metadata updated for playlist "%s"',
playlist.name())
def track_created_changed(self, playlist, position, user, when, userdata):
"""Callback used by pyspotify"""
when = datetime.datetime.fromtimestamp(when)
logger.debug(
'Callback called: Created by/when for track %d in playlist '
'"%s" changed to user "N/A" and time "%s"',
position, playlist.name(), when)
def track_message_changed(self, playlist, position, message, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Message for track %d in playlist '
'"%s" changed to "%s"', position, playlist.name(), message)
def track_seen_changed(self, playlist, position, seen, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Seen attribute for track %d in playlist '
'"%s" changed to "%s"', position, playlist.name(), seen)
def description_changed(self, playlist, description, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Description changed for playlist "%s" to "%s"',
playlist.name(), description)
def subscribers_changed(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Subscribers changed for playlist "%s"',
playlist.name())
def image_changed(self, playlist, image, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Image changed for playlist "%s"',
playlist.name())

View File

@ -1,22 +0,0 @@
from __future__ import unicode_literals
from mopidy.backends import base
class SpotifyPlaylistsProvider(base.BasePlaylistsProvider):
def create(self, name):
pass # TODO
def delete(self, uri):
pass # TODO
def lookup(self, uri):
for playlist in self._playlists:
if playlist.uri == uri:
return playlist
def refresh(self):
pass # TODO
def save(self, playlist):
pass # TODO

View File

@ -1,201 +0,0 @@
from __future__ import unicode_literals
import logging
import os
import threading
from spotify.manager import SpotifySessionManager as PyspotifySessionManager
from mopidy import audio
from mopidy.backends.listener import BackendListener
from mopidy.utils import process, versioning
from . import translator
from .container_manager import SpotifyContainerManager
from .playlist_manager import SpotifyPlaylistManager
logger = logging.getLogger('mopidy.backends.spotify')
BITRATES = {96: 2, 160: 0, 320: 1}
class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
cache_location = None
settings_location = None
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
user_agent = 'Mopidy %s' % versioning.get_version()
def __init__(self, config, audio, backend_ref):
self.cache_location = config['spotify']['cache_dir']
self.settings_location = config['spotify']['cache_dir']
full_proxy = ''
if config['proxy']['hostname']:
full_proxy = config['proxy']['hostname']
if config['proxy']['port']:
full_proxy += ':' + str(config['proxy']['port'])
if config['proxy']['scheme']:
full_proxy = config['proxy']['scheme'] + "://" + full_proxy
PyspotifySessionManager.__init__(
self, config['spotify']['username'], config['spotify']['password'],
proxy=full_proxy,
proxy_username=config['proxy']['username'],
proxy_password=config['proxy']['password'])
process.BaseThread.__init__(self)
self.name = 'SpotifyThread'
self.audio = audio
self.backend = None
self.backend_ref = backend_ref
self.bitrate = config['spotify']['bitrate']
self.connected = threading.Event()
self.push_audio_data = True
self.buffer_timestamp = 0
self.container_manager = None
self.playlist_manager = None
self._initial_data_receive_completed = False
def run_inside_try(self):
self.backend = self.backend_ref.proxy()
self.connect()
def logged_in(self, session, error):
"""Callback used by pyspotify"""
if error:
logger.error('Spotify login error: %s', error)
return
logger.info('Connected to Spotify')
# To work with both pyspotify 1.9 and 1.10
if not hasattr(self, 'session'):
self.session = session
logger.debug('Preferred Spotify bitrate is %d kbps', self.bitrate)
session.set_preferred_bitrate(BITRATES[self.bitrate])
self.container_manager = SpotifyContainerManager(self)
self.playlist_manager = SpotifyPlaylistManager(self)
self.container_manager.watch(session.playlist_container())
self.connected.set()
def logged_out(self, session):
"""Callback used by pyspotify"""
logger.info('Disconnected from Spotify')
self.connected.clear()
def metadata_updated(self, session):
"""Callback used by pyspotify"""
logger.debug('Callback called: Metadata updated')
def connection_error(self, session, error):
"""Callback used by pyspotify"""
if error is None:
logger.info('Spotify connection OK')
else:
logger.error('Spotify connection error: %s', error)
if self.audio.state.get() == audio.PlaybackState.PLAYING:
self.backend.playback.pause()
def message_to_user(self, session, message):
"""Callback used by pyspotify"""
logger.debug('User message: %s', message.strip())
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
if not self.push_audio_data:
return 0
assert sample_type == 0, 'Expects 16-bit signed integer samples'
capabilites = """
audio/x-raw-int,
endianness=(int)1234,
channels=(int)%(channels)d,
width=(int)16,
depth=(int)16,
signed=(boolean)true,
rate=(int)%(sample_rate)d
""" % {
'sample_rate': sample_rate,
'channels': channels,
}
duration = audio.calculate_duration(num_frames, sample_rate)
buffer_ = audio.create_buffer(bytes(frames),
capabilites=capabilites,
timestamp=self.buffer_timestamp,
duration=duration)
self.buffer_timestamp += duration
if self.audio.emit_data(buffer_).get():
return num_frames
else:
return 0
def play_token_lost(self, session):
"""Callback used by pyspotify"""
logger.debug('Play token lost')
self.backend.playback.pause()
def log_message(self, session, data):
"""Callback used by pyspotify"""
logger.debug('System message: %s' % data.strip())
if 'offline-mgr' in data and 'files unlocked' in data:
# XXX This is a very very fragile and ugly hack, but we get no
# proper event when libspotify is done with initial data loading.
# We delay the expensive refresh of Mopidy's playlists until this
# message arrives. This way, we avoid doing the refresh once for
# every playlist or other change. This reduces the time from
# startup until the Spotify backend is ready from 35s to 12s in one
# test with clean Spotify cache. In cases with an outdated cache
# the time improvements should be a lot greater.
if not self._initial_data_receive_completed:
self._initial_data_receive_completed = True
self.refresh_playlists()
def end_of_track(self, session):
"""Callback used by pyspotify"""
logger.debug('End of data stream reached')
self.audio.emit_end_of_stream()
def refresh_playlists(self):
"""Refresh the playlists in the backend with data from Spotify"""
if not self._initial_data_receive_completed:
logger.debug('Still getting data; skipped refresh of playlists')
return
playlists = []
folders = []
for spotify_playlist in self.session.playlist_container():
if spotify_playlist.type() == 'folder_start':
folders.append(spotify_playlist)
if spotify_playlist.type() == 'folder_end':
folders.pop()
playlists.append(translator.to_mopidy_playlist(
spotify_playlist, folders=folders,
bitrate=self.bitrate, username=self.username))
playlists.append(translator.to_mopidy_playlist(
self.session.starred(),
bitrate=self.bitrate, username=self.username))
playlists = filter(None, playlists)
self.backend.playlists.playlists = playlists
logger.info('Loaded %d Spotify playlists', len(playlists))
BackendListener.send('playlists_loaded')
def logout(self):
"""Log out from spotify"""
logger.debug('Logging out from Spotify')
# To work with both pyspotify 1.9 and 1.10
if getattr(self, 'session', None):
self.session.logout()

View File

@ -1,97 +0,0 @@
from __future__ import unicode_literals
import logging
import spotify
from mopidy.models import Artist, Album, Track, Playlist
logger = logging.getLogger('mopidy.backends.spotify')
artist_cache = {}
album_cache = {}
track_cache = {}
def to_mopidy_artist(spotify_artist):
if spotify_artist is None:
return
uri = str(spotify.Link.from_artist(spotify_artist))
if uri in artist_cache:
return artist_cache[uri]
if not spotify_artist.is_loaded():
return Artist(uri=uri, name='[loading...]')
artist_cache[uri] = Artist(uri=uri, name=spotify_artist.name())
return artist_cache[uri]
def to_mopidy_album(spotify_album):
if spotify_album is None:
return
uri = str(spotify.Link.from_album(spotify_album))
if uri in album_cache:
return album_cache[uri]
if not spotify_album.is_loaded():
return Album(uri=uri, name='[loading...]')
album_cache[uri] = Album(
uri=uri,
name=spotify_album.name(),
artists=[to_mopidy_artist(spotify_album.artist())],
date=spotify_album.year())
return album_cache[uri]
def to_mopidy_track(spotify_track, bitrate=None):
if spotify_track is None:
return
uri = str(spotify.Link.from_track(spotify_track, 0))
if uri in track_cache:
return track_cache[uri]
if not spotify_track.is_loaded():
return Track(uri=uri, name='[loading...]')
spotify_album = spotify_track.album()
if spotify_album is not None and spotify_album.is_loaded():
date = spotify_album.year()
else:
date = None
track_cache[uri] = Track(
uri=uri,
name=spotify_track.name(),
artists=[to_mopidy_artist(a) for a in spotify_track.artists()],
album=to_mopidy_album(spotify_track.album()),
track_no=spotify_track.index(),
date=date,
length=spotify_track.duration(),
bitrate=bitrate)
return track_cache[uri]
def to_mopidy_playlist(
spotify_playlist, folders=None, bitrate=None, username=None):
if spotify_playlist is None or spotify_playlist.type() != 'playlist':
return
try:
uri = str(spotify.Link.from_playlist(spotify_playlist))
except spotify.SpotifyError as e:
logger.debug('Spotify playlist translation error: %s', e)
return
if not spotify_playlist.is_loaded():
return Playlist(uri=uri, name='[loading...]')
name = spotify_playlist.name()
if folders:
folder_names = '/'.join(folder.name() for folder in folders)
name = folder_names + '/' + name
tracks = [
to_mopidy_track(spotify_track, bitrate=bitrate)
for spotify_track in spotify_playlist
if not spotify_track.is_local()
]
if not name:
name = 'Starred'
# Tracks in the Starred playlist are in reverse order from the official
# client.
tracks.reverse()
if spotify_playlist.owner().canonical_name() != username:
name += ' by ' + spotify_playlist.owner().canonical_name()
return Playlist(uri=uri, name=name, tracks=tracks)

View File

@ -1,8 +0,0 @@
pyspotify >= 1.9, < 2
# The libspotify Python wrapper
# Available as the python-spotify package from apt.mopidy.com
# libspotify >= 12, < 13
# The libspotify C library from
# https://developer.spotify.com/technologies/libspotify/
# Available as the libspotify12 package from apt.mopidy.com

View File

@ -28,8 +28,6 @@ setup(
'Pykka >= 1.1', 'Pykka >= 1.1',
], ],
extras_require={ extras_require={
'spotify': ['pyspotify >= 1.9, < 2'],
'scrobbler': ['Mopidy-Scrobbler'],
'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'], 'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'],
}, },
test_suite='nose.collector', test_suite='nose.collector',
@ -47,7 +45,6 @@ setup(
'http = mopidy.frontends.http:Extension [http]', 'http = mopidy.frontends.http:Extension [http]',
'local = mopidy.backends.local:Extension', 'local = mopidy.backends.local:Extension',
'mpd = mopidy.frontends.mpd:Extension', 'mpd = mopidy.frontends.mpd:Extension',
'spotify = mopidy.backends.spotify:Extension [spotify]',
'stream = mopidy.backends.stream:Extension', 'stream = mopidy.backends.stream:Extension',
], ],
}, },