From e7d6a995e8c395759e8f72254adb776b387871a5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Oct 2013 22:42:26 +0200 Subject: [PATCH] spotify: Move to external extension --- MANIFEST.in | 1 - docs/changelog.rst | 19 +- docs/ext/index.rst | 11 +- docs/ext/spotify.rst | 83 -------- docs/index.rst | 6 +- mopidy/backends/spotify/__init__.py | 36 ---- mopidy/backends/spotify/actor.py | 37 ---- mopidy/backends/spotify/container_manager.py | 51 ----- mopidy/backends/spotify/ext.conf | 7 - mopidy/backends/spotify/library.py | 211 ------------------- mopidy/backends/spotify/playback.py | 94 --------- mopidy/backends/spotify/playlist_manager.py | 105 --------- mopidy/backends/spotify/playlists.py | 22 -- mopidy/backends/spotify/session_manager.py | 201 ------------------ mopidy/backends/spotify/spotify_appkey.key | Bin 321 -> 0 bytes mopidy/backends/spotify/translator.py | 97 --------- requirements/spotify.txt | 8 - setup.py | 3 - 18 files changed, 24 insertions(+), 968 deletions(-) delete mode 100644 docs/ext/spotify.rst delete mode 100644 mopidy/backends/spotify/__init__.py delete mode 100644 mopidy/backends/spotify/actor.py delete mode 100644 mopidy/backends/spotify/container_manager.py delete mode 100644 mopidy/backends/spotify/ext.conf delete mode 100644 mopidy/backends/spotify/library.py delete mode 100644 mopidy/backends/spotify/playback.py delete mode 100644 mopidy/backends/spotify/playlist_manager.py delete mode 100644 mopidy/backends/spotify/playlists.py delete mode 100644 mopidy/backends/spotify/session_manager.py delete mode 100644 mopidy/backends/spotify/spotify_appkey.key delete mode 100644 mopidy/backends/spotify/translator.py delete mode 100644 requirements/spotify.txt diff --git a/MANIFEST.in b/MANIFEST.in index 6385e4ff..84122dcc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,6 @@ include *.rst include LICENSE include MANIFEST.in include data/mopidy.desktop -include mopidy/backends/spotify/spotify_appkey.key include pylintrc recursive-include docs * diff --git a/docs/changelog.rst b/docs/changelog.rst index 85f35a91..e3fa167d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,15 +9,18 @@ v0.16.0 (UNRELEASED) **Dependencies** -- The Last.fm scrobbler has been moved to its own external extension, - `Mopidy-Scrobbler `_. You'll need - to install it in addition to Mopidy if you want it to continue to work as it - used to. +Parts of Mopidy have been moved to their own external extensions. If you want +Mopidy to continue to work like it used to, you may have to install one or more +of the following extensions as well: -- The MPRIS frontend has been moved to its own external extension, - `Mopidy-MPRIS `_. You'll need to - install it in addition to Mopidy if you want it to continue to work as it - used to. +- The Spotify backend has been moved to + `Mopidy-Scrobbler `_. + +- The Last.fm scrobbler has been moved to + `Mopidy-Scrobbler `_. + +- The MPRIS frontend has been moved to + `Mopidy-MPRIS `_. **Audio** diff --git a/docs/ext/index.rst b/docs/ext/index.rst index bdc1efe8..a909883d 100644 --- a/docs/ext/index.rst +++ b/docs/ext/index.rst @@ -87,10 +87,19 @@ 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 `_ service. +Mopidy-Spotify +-------------- + +https://github.com/mopidy/mopidy-spotify + +Extension for playing music from the `Spotify `_ music +streaming service. + + Mopidy-Subsonic --------------- diff --git a/docs/ext/spotify.rst b/docs/ext/spotify.rst deleted file mode 100644 index 4bb5b7a3..00000000 --- a/docs/ext/spotify.rst +++ /dev/null @@ -1,83 +0,0 @@ -.. _ext-spotify: - -************** -Mopidy-Spotify -************** - -An extension for playing music from Spotify. - -`Spotify `_ is a music streaming service. The backend -uses the official `libspotify -`_ library and the -`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. diff --git a/docs/index.rst b/docs/index.rst index ca40c96c..c5183471 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,9 +4,9 @@ Mopidy Mopidy is a music server which can play music both from multiple sources, like your :ref:`local hard drive `, :ref:`radio streams `, -and from :ref:`Spotify ` and SoundCloud. Searches combines results -from all music sources, and you can mix tracks from all sources in your play -queue. Your playlists from Spotify or SoundCloud are also available for use. +and from Spotify and SoundCloud. Searches combines results from all music +sources, and you can mix tracks from all sources in your play queue. Your +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 clients `, the :ref:`Ubuntu Sound Menu `, any diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py deleted file mode 100644 index 3cee609a..00000000 --- a/mopidy/backends/spotify/__init__.py +++ /dev/null @@ -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] diff --git a/mopidy/backends/spotify/actor.py b/mopidy/backends/spotify/actor.py deleted file mode 100644 index 1f90ba51..00000000 --- a/mopidy/backends/spotify/actor.py +++ /dev/null @@ -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() diff --git a/mopidy/backends/spotify/container_manager.py b/mopidy/backends/spotify/container_manager.py deleted file mode 100644 index e8d1ed0b..00000000 --- a/mopidy/backends/spotify/container_manager.py +++ /dev/null @@ -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. diff --git a/mopidy/backends/spotify/ext.conf b/mopidy/backends/spotify/ext.conf deleted file mode 100644 index 83bf191a..00000000 --- a/mopidy/backends/spotify/ext.conf +++ /dev/null @@ -1,7 +0,0 @@ -[spotify] -enabled = true -username = -password = -bitrate = 160 -timeout = 10 -cache_dir = $XDG_CACHE_DIR/mopidy/spotify diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py deleted file mode 100644 index 49caa709..00000000 --- a/mopidy/backends/spotify/library.py +++ /dev/null @@ -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 diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py deleted file mode 100644 index bda17634..00000000 --- a/mopidy/backends/spotify/playback.py +++ /dev/null @@ -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) diff --git a/mopidy/backends/spotify/playlist_manager.py b/mopidy/backends/spotify/playlist_manager.py deleted file mode 100644 index 6cd6d4ed..00000000 --- a/mopidy/backends/spotify/playlist_manager.py +++ /dev/null @@ -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()) diff --git a/mopidy/backends/spotify/playlists.py b/mopidy/backends/spotify/playlists.py deleted file mode 100644 index bd201179..00000000 --- a/mopidy/backends/spotify/playlists.py +++ /dev/null @@ -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 diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py deleted file mode 100644 index 3ab4498b..00000000 --- a/mopidy/backends/spotify/session_manager.py +++ /dev/null @@ -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() diff --git a/mopidy/backends/spotify/spotify_appkey.key b/mopidy/backends/spotify/spotify_appkey.key deleted file mode 100644 index 1f840b962d9245820e73803ae5995650b4f84f62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 321 zcmV-H0lxkL&xsG-pVlEz7LL?2e{+JtQpZk(M<9(;xguUY#VZNv&txxTh0nuFe(N{} zC?#&u)&58KeoT-KpSTN{8Wb)hzuj?jZNaN?^McImAMP|w&4GR8DyOK-#=V!cSw`&V5lyby`QwVzk}bWQ#Ui#m2fN)=wRSqK33~=D8OATMF|fdmT#G0B?yVov-+)u7w0gkTjyb{I{VGW`-;#R z$iCRsr@I8@9i#w7y@Y$>dnR3OOhWI%a!F~QeP*7Os+7-($V~m!LFZ(l=H!@+PtT&9 diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py deleted file mode 100644 index f35cad2e..00000000 --- a/mopidy/backends/spotify/translator.py +++ /dev/null @@ -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) diff --git a/requirements/spotify.txt b/requirements/spotify.txt deleted file mode 100644 index d11a5c04..00000000 --- a/requirements/spotify.txt +++ /dev/null @@ -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 diff --git a/setup.py b/setup.py index ff6d49de..a448a029 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,6 @@ setup( 'Pykka >= 1.1', ], extras_require={ - 'spotify': ['pyspotify >= 1.9, < 2'], - 'scrobbler': ['Mopidy-Scrobbler'], 'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'], }, test_suite='nose.collector', @@ -47,7 +45,6 @@ setup( 'http = mopidy.frontends.http:Extension [http]', 'local = mopidy.backends.local:Extension', 'mpd = mopidy.frontends.mpd:Extension', - 'spotify = mopidy.backends.spotify:Extension [spotify]', 'stream = mopidy.backends.stream:Extension', ], },