PK USGr mopidy_spotify/library.pyfrom __future__ import unicode_literals
import logging
from mopidy import backend
from mopidy_spotify import browse, distinct, images, lookup, search
logger = logging.getLogger(__name__)
class SpotifyLibraryProvider(backend.LibraryProvider):
root_directory = browse.ROOT_DIR
def __init__(self, backend):
self._backend = backend
self._config = self._backend._config['spotify']
def browse(self, uri):
return browse.browse(self._config, self._backend._session, uri)
def get_distinct(self, field, query=None):
return distinct.get_distinct(
self._config, self._backend._session, field, query)
def get_images(self, uris):
return images.get_images(uris)
def lookup(self, uri):
return lookup.lookup(self._config, self._backend._session, uri)
def search(self, query=None, uris=None, exact=False):
return search.search(
self._config, self._backend._session,
query, uris, exact)
PK USG\n mopidy_spotify/search.pyfrom __future__ import unicode_literals
import logging
import urllib
from mopidy import models
import spotify
from mopidy_spotify import lookup, translator
logger = logging.getLogger(__name__)
def search(config, session, query=None, uris=None, exact=False):
# TODO Respect `uris` argument
# TODO Support `exact` search
if query is None:
logger.debug('Ignored search without query')
return models.SearchResult(uri='spotify:search')
if 'uri' in query:
return _search_by_uri(config, session, query)
sp_query = translator.sp_search_query(query)
if not sp_query:
logger.debug('Ignored search with empty query')
return models.SearchResult(uri='spotify:search')
uri = 'spotify:search:%s' % urllib.quote(sp_query.encode('utf-8'))
logger.info('Searching Spotify for: %s', sp_query)
if session.connection.state is not spotify.ConnectionState.LOGGED_IN:
logger.info('Spotify search aborted: Spotify is offline')
return models.SearchResult(uri=uri)
sp_search = session.search(
sp_query,
album_count=config['search_album_count'],
artist_count=config['search_artist_count'],
track_count=config['search_track_count'])
sp_search.load()
albums = [
translator.to_album(sp_album) for sp_album in sp_search.albums]
artists = [
translator.to_artist(sp_artist) for sp_artist in sp_search.artists]
tracks = [
translator.to_track(sp_track) for sp_track in sp_search.tracks]
return models.SearchResult(
uri=uri, albums=albums, artists=artists, tracks=tracks)
def _search_by_uri(config, session, query):
tracks = []
for uri in query['uri']:
tracks += lookup.lookup(config, session, uri)
uri = 'spotify:search'
if len(query['uri']) == 1:
uri = query['uri'][0]
return models.SearchResult(uri=uri, tracks=tracks)
PK ZSG' mopidy_spotify/distinct.pyfrom __future__ import unicode_literals
import logging
import spotify
from mopidy_spotify import translator
logger = logging.getLogger(__name__)
def get_distinct(config, session, field, query=None):
# To make the returned data as interesting as possible, we limit
# ourselves to data extracted from the user's playlists when no search
# query is included.
sp_query = translator.sp_search_query(query) if query else None
if field == 'artist':
result = _get_distinct_artists(config, session, sp_query)
elif field == 'albumartist':
result = _get_distinct_albumartists(config, session, sp_query)
elif field == 'album':
result = _get_distinct_albums(config, session, sp_query)
elif field == 'date':
result = _get_distinct_dates(config, session, sp_query)
else:
result = set()
return result - {None}
def _get_distinct_artists(config, session, sp_query):
logger.debug('Getting distinct artists: %s', sp_query)
if sp_query:
sp_search = _get_sp_search(config, session, sp_query, artist=True)
if sp_search is None:
return set()
return {artist.name for artist in sp_search.artists}
else:
return {
artist.name
for track in _get_playlist_tracks(config, session)
for artist in track.artists}
def _get_distinct_albumartists(config, session, sp_query):
logger.debug(
'Getting distinct albumartists: %s', sp_query)
if sp_query:
sp_search = _get_sp_search(config, session, sp_query, album=True)
if sp_search is None:
return set()
return {
album.artist.name
for album in sp_search.albums
if album.artist}
else:
return {
track.album.artist.name
for track in _get_playlist_tracks(config, session)
if track.album and track.album.artist}
def _get_distinct_albums(config, session, sp_query):
logger.debug('Getting distinct albums: %s', sp_query)
if sp_query:
sp_search = _get_sp_search(config, session, sp_query, album=True)
if sp_search is None:
return set()
return {album.name for album in sp_search.albums}
else:
return {
track.album.name
for track in _get_playlist_tracks(config, session)
if track.album}
def _get_distinct_dates(config, session, sp_query):
logger.debug('Getting distinct album years: %s', sp_query)
if sp_query:
sp_search = _get_sp_search(config, session, sp_query, album=True)
if sp_search is None:
return set()
return {
'%s' % album.year
for album in sp_search.albums
if album.year not in (None, 0)}
else:
return {
'%s' % track.album.year
for track in _get_playlist_tracks(config, session)
if track.album and track.album.year not in (None, 0)}
def _get_sp_search(
config, session, sp_query, album=False, artist=False, track=False):
if session.connection.state is not spotify.ConnectionState.LOGGED_IN:
logger.info('Spotify search aborted: Spotify is offline')
return None
sp_search = session.search(
sp_query,
album_count=config['search_album_count'] if album else 0,
artist_count=config['search_artist_count'] if artist else 0,
track_count=config['search_track_count'] if track else 0)
sp_search.load()
return sp_search
def _get_playlist_tracks(config, session):
if not config['allow_playlists']:
return
for playlist in session.playlist_container:
if not isinstance(playlist, spotify.Playlist):
continue
playlist.load()
for track in playlist.tracks:
try:
track.load()
yield track
except spotify.Error: # TODO Why did we get "General error"?
continue
PK USG"jʇ mopidy_spotify/browse.pyfrom __future__ import unicode_literals
import logging
from mopidy import models
import spotify
from mopidy_spotify import countries, translator
logger = logging.getLogger(__name__)
ROOT_DIR = models.Ref.directory(
uri='spotify:directory', name='Spotify')
_ROOT_DIR_CONTENTS = [
models.Ref.directory(
uri='spotify:top:tracks', name='Top tracks'),
models.Ref.directory(
uri='spotify:top:albums', name='Top albums'),
models.Ref.directory(
uri='spotify:top:artists', name='Top artists'),
]
_TOPLIST_TYPES = {
'albums': spotify.ToplistType.ALBUMS,
'artists': spotify.ToplistType.ARTISTS,
'tracks': spotify.ToplistType.TRACKS,
}
_TOPLIST_REGIONS = {
'user': lambda session: spotify.ToplistRegion.USER,
'country': lambda session: session.user_country,
'everywhere': lambda session: spotify.ToplistRegion.EVERYWHERE,
}
def browse(config, session, uri):
if uri == ROOT_DIR.uri:
return _ROOT_DIR_CONTENTS
elif uri.startswith('spotify:user:'):
return _browse_playlist(session, uri)
elif uri.startswith('spotify:album:'):
return _browse_album(session, uri)
elif uri.startswith('spotify:artist:'):
return _browse_artist(session, uri)
elif uri.startswith('spotify:top:'):
parts = uri.replace('spotify:top:', '').split(':')
if len(parts) == 1:
return _browse_toplist_regions(variant=parts[0])
elif len(parts) == 2:
return _browse_toplist(
config, session, variant=parts[0], region=parts[1])
else:
logger.info(
'Failed to browse "%s": Toplist URI parsing failed', uri)
return []
else:
logger.info('Failed to browse "%s": Unknown URI type', uri)
return []
def _browse_playlist(session, uri):
sp_playlist = session.get_playlist(uri)
sp_playlist.load()
return list(translator.to_track_refs(sp_playlist.tracks))
def _browse_album(session, uri):
sp_album_browser = session.get_album(uri).browse()
sp_album_browser.load()
return list(translator.to_track_refs(sp_album_browser.tracks))
def _browse_artist(session, uri):
sp_artist_browser = session.get_artist(uri).browse(
type=spotify.ArtistBrowserType.NO_TRACKS)
sp_artist_browser.load()
top_tracks = list(translator.to_track_refs(
sp_artist_browser.tophit_tracks))
albums = list(translator.to_album_refs(sp_artist_browser.albums))
return top_tracks + albums
def _browse_toplist_regions(variant):
return [
models.Ref.directory(
uri='spotify:top:%s:user' % variant, name='Personal'),
models.Ref.directory(
uri='spotify:top:%s:country' % variant, name='Country'),
models.Ref.directory(
uri='spotify:top:%s:countries' % variant,
name='Other countries'),
models.Ref.directory(
uri='spotify:top:%s:everywhere' % variant, name='Global'),
]
def _browse_toplist(config, session, variant, region):
if region == 'countries':
codes = config['toplist_countries']
if not codes:
codes = countries.COUNTRIES.keys()
return [
models.Ref.directory(
uri='spotify:top:%s:%s' % (variant, code.lower()),
name=countries.COUNTRIES.get(code.upper(), code.upper()))
for code in codes]
if region in ('user', 'country', 'everywhere'):
sp_toplist = session.get_toplist(
type=_TOPLIST_TYPES[variant],
region=_TOPLIST_REGIONS[region](session))
elif len(region) == 2:
sp_toplist = session.get_toplist(
type=_TOPLIST_TYPES[variant], region=region.upper())
else:
return []
if session.connection.state is spotify.ConnectionState.LOGGED_IN:
sp_toplist.load()
if not sp_toplist.is_loaded:
return []
if variant == 'tracks':
return list(translator.to_track_refs(sp_toplist.tracks))
elif variant == 'albums':
return list(translator.to_album_refs(sp_toplist.albums))
elif variant == 'artists':
return list(translator.to_artist_refs(sp_toplist.artists))
else:
return []
PK USGB< mopidy_spotify/countries.pyfrom __future__ import unicode_literals
"""
List of countries that Spotify is available in.
The list is based on:
https://support.spotify.com/us/learn-more/faq/#!/article/Availability-in-overseas-territories
Last updated: 2015-01-22
"""
COUNTRIES = {
'AD': 'Andorra',
'AR': 'Argentina',
'AU': 'Australia',
'AT': 'Austria',
'BE': 'Belgium',
'BO': 'Bolivia',
'BR': 'Brazil',
'BG': 'Bulgaria',
'CA': 'Canada',
'CL': 'Chile',
'CO': 'Colombia',
'CR': 'Costa Rica',
'CY': 'Cyprus',
'CZ': 'Czech Republic',
'DK': 'Denmark',
'DO': 'Dominican Republic',
'EC': 'Ecuador',
'SV': 'El Salvador',
'EE': 'Estonia',
'FI': 'Finland',
'FR': 'France',
'DE': 'Germany',
'GR': 'Greece',
'GT': 'Guatemala',
'HN': 'Honduras',
'HK': 'Hong Kong',
'HU': 'Hungary',
'IS': 'Iceland',
'IE': 'Ireland',
'IT': 'Italy',
'LV': 'Latvia',
'LI': 'Liechtenstein',
'LT': 'Lithuania',
'LU': 'Luxembourg',
'MY': 'Malaysia',
'MX': 'Mexico',
'MC': 'Monaco',
'NL': 'Netherlands',
'NZ': 'New Zealand',
'NI': 'Nicaragua',
'NO': 'Norway',
'PA': 'Panama',
'PY': 'Paraguay',
'PE': 'Peru',
'PH': 'Philipines',
'PL': 'Poland',
'PT': 'Portugal',
'SG': 'Singapore',
'SK': 'Slovakia',
'ES': 'Spain',
'SE': 'Sweden',
'CH': 'Switzerland',
'TW': 'Taiwan',
'TR': 'Turkey',
'GB': 'United Kingdom',
'US': 'United States',
'UY': 'Uruguay',
}
PK USGܚ mopidy_spotify/playback.pyfrom __future__ import unicode_literals
import functools
import logging
import threading
from mopidy import audio, backend
import spotify
logger = logging.getLogger(__name__)
# These GStreamer caps matches the audio data provided by libspotify
LIBSPOTIFY_GST_CAPS = (
'audio/x-raw-int, endianness=(int)1234, channels=(int)2, '
'width=(int)16, depth=(int)16, signed=(boolean)true, '
'rate=(int)44100')
# Extra log level with lower importance than DEBUG=10 for noisy debug logging
TRACE_LOG_LEVEL = 5
class SpotifyPlaybackProvider(backend.PlaybackProvider):
def __init__(self, *args, **kwargs):
super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs)
self._timeout = self.backend._config['spotify']['timeout']
self._buffer_timestamp = BufferTimestamp(0)
self._first_seek = False
self._push_audio_data_event = threading.Event()
self._push_audio_data_event.set()
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._buffer_timestamp)
self.backend._session.on(
spotify.SessionEvent.END_OF_TRACK, end_of_track_callback,
self.audio)
def change_track(self, track):
self._connect_events()
if track.uri is None:
return False
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)
self._first_seek = True
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(
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)
return True
except spotify.Error as exc:
logger.info('Playback of %s failed: %s', track.uri, exc)
return False
def resume(self):
self.backend._session.player.play()
return super(SpotifyPlaybackProvider, self).resume()
def stop(self):
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)
if time_position == 0 and self._first_seek:
self._first_seek = False
logger.debug('Skipping seek due to issue mopidy/mopidy#300')
return
self._buffer_timestamp.set(
audio.millisecond_to_clocktime(time_position))
self.backend._session.player.seek(time_position)
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',
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')
push_audio_data_event.clear()
def seek_data_callback(spotify_backend, time_position):
# This callback is called from GStreamer/the GObject event loop.
# It forwards the call to the backend actor.
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):
# This is called from an internal libspotify thread.
# Ideally, nothing here should block.
if not push_audio_data_event.is_set():
return 0
known_format = (
audio_format.sample_type == spotify.SampleType.INT16_NATIVE_ENDIAN)
assert known_format, '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
""" % {
'channels': audio_format.channels,
'sample_rate': 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():
return num_frames
else:
return 0
def end_of_track_callback(session, audio_actor):
# This callback is called from the pyspotify event loop.
logger.debug('End of track reached')
audio_actor.emit_data(None)
class BufferTimestamp(object):
"""Wrapper around an int to serialize access by multiple threads.
The value is used both from the backend actor and callbacks called by
internal libspotify threads.
"""
def __init__(self, value):
self._value = value
self._lock = threading.RLock()
def get(self):
with self._lock:
return self._value
def set(self, value):
with self._lock:
self._value = value
def increase(self, value):
with self._lock:
self._value += value
PK USGSݭ mopidy_spotify/utils.pyfrom __future__ import unicode_literals
import contextlib
import locale
import logging
import time
logger = logging.getLogger(__name__)
TRACE = logging.getLevelName('TRACE')
def locale_decode(bytestr):
try:
return unicode(bytestr)
except UnicodeError:
return bytes(bytestr).decode(locale.getpreferredencoding())
@contextlib.contextmanager
def time_logger(name, level=TRACE):
start = time.time()
yield
logger.log(level, '%s took %dms', name, (time.time() - start) * 1000)
PK USG' = mopidy_spotify/ext.conf[spotify]
enabled = true
username =
password =
bitrate = 160
volume_normalization = true
private_session = false
timeout = 10
allow_cache = true
allow_network = true
allow_playlists = true
search_album_count = 20
search_artist_count = 10
search_track_count = 50
toplist_countries =
PK USG{a a mopidy_spotify/images.pyfrom __future__ import unicode_literals
import itertools
import json
import logging
import operator
import urllib2
import urlparse
from mopidy import models
from mopidy_spotify import utils
# NOTE: This module is independent of libspotify and built using the Spotify
# Web APIs. As such it does not tie in with any of the regular code used
# elsewhere in the mopidy-spotify extensions. It is also intended to be used
# across both the 1.x and 2.x versions.
_API_MAX_IDS_PER_REQUEST = 50
_API_BASE_URI = 'https://api.spotify.com/v1/%ss/?ids=%s'
_cache = {} # (type, id) -> [Image(), ...]
logger = logging.getLogger(__name__)
def get_images(uris):
result = {}
uri_type_getter = operator.itemgetter('type')
uris = sorted((_parse_uri(u) for u in uris), key=uri_type_getter)
for uri_type, group in itertools.groupby(uris, uri_type_getter):
batch = []
for uri in group:
if uri['key'] in _cache:
result[uri['uri']] = _cache[uri['key']]
else:
batch.append(uri)
if len(batch) >= _API_MAX_IDS_PER_REQUEST:
result.update(_process_uris(uri_type, batch))
batch = []
result.update(_process_uris(uri_type, batch))
return result
def _parse_uri(uri):
parsed_uri = urlparse.urlparse(uri)
uri_type, uri_id = None, None
if parsed_uri.scheme == 'spotify':
uri_type, uri_id = parsed_uri.path.split(':')[:2]
elif parsed_uri.scheme in ('http', 'https'):
if parsed_uri.netloc in ('open.spotify.com', 'play.spotify.com'):
uri_type, uri_id = parsed_uri.path.split('/')[1:3]
if uri_type and uri_type in ('track', 'album', 'artist') and uri_id:
return {'uri': uri, 'type': uri_type, 'id': uri_id,
'key': (uri_type, uri_id)}
raise ValueError('Could not parse %r as a Spotify URI' % uri)
def _process_uris(uri_type, uris):
result = {}
ids_to_uris = {u['id']: u for u in uris}
if not uris:
return result
try:
lookup_uri = _API_BASE_URI % (
uri_type, ','.join(sorted(ids_to_uris.keys())))
data = json.load(urllib2.urlopen(lookup_uri))
except (ValueError, IOError) as e:
error_msg = utils.locale_decode(e)
logger.debug('Fetching %s failed: %s', lookup_uri, error_msg)
return result
for item in data.get(uri_type + 's', []):
if not item:
continue
uri = ids_to_uris[item['id']]
if uri['key'] not in _cache:
if uri_type == 'track':
album_key = _parse_uri(item['album']['uri'])['key']
if album_key not in _cache:
_cache[album_key] = tuple(
_translate_image(i) for i in item['album']['images'])
_cache[uri['key']] = _cache[album_key]
else:
_cache[uri['key']] = tuple(
_translate_image(i) for i in item['images'])
result[uri['uri']] = _cache[uri['key']]
return result
def _translate_image(i):
return models.Image(uri=i['url'], height=i['height'], width=i['width'])
PK USGF/ mopidy_spotify/translator.pyfrom __future__ import unicode_literals
import collections
import logging
from mopidy import models
import spotify
logger = logging.getLogger(__name__)
class memoized(object):
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args, **kwargs):
# NOTE Only args, not kwargs, are part of the memoization key.
if not isinstance(args, collections.Hashable):
return self.func(*args, **kwargs)
if args in self.cache:
return self.cache[args]
else:
value = self.func(*args, **kwargs)
if value is not None:
self.cache[args] = value
return value
@memoized
def to_artist(sp_artist):
if not sp_artist.is_loaded:
return
return models.Artist(uri=sp_artist.link.uri, name=sp_artist.name)
@memoized
def to_artist_ref(sp_artist):
if not sp_artist.is_loaded:
return
return models.Ref.artist(uri=sp_artist.link.uri, name=sp_artist.name)
def to_artist_refs(sp_artists):
for sp_artist in sp_artists:
sp_artist.load()
ref = to_artist_ref(sp_artist)
if ref is not None:
yield ref
@memoized
def to_album(sp_album):
if not sp_album.is_loaded:
return
if sp_album.artist is not None and sp_album.artist.is_loaded:
artists = [to_artist(sp_album.artist)]
else:
artists = []
if sp_album.year is not None:
date = '%d' % sp_album.year
else:
date = None
return models.Album(
uri=sp_album.link.uri,
name=sp_album.name,
artists=artists,
date=date)
@memoized
def to_album_ref(sp_album):
if not sp_album.is_loaded:
return
if sp_album.artist is None or not sp_album.artist.is_loaded:
name = sp_album.name
else:
name = '%s - %s' % (sp_album.artist.name, sp_album.name)
return models.Ref.album(uri=sp_album.link.uri, name=name)
def to_album_refs(sp_albums):
for sp_album in sp_albums:
sp_album.load()
ref = to_album_ref(sp_album)
if ref is not None:
yield ref
@memoized
def to_track(sp_track, bitrate=None):
if not sp_track.is_loaded:
return
if sp_track.error != spotify.ErrorType.OK:
logger.debug(
'Error loading %s: %r', sp_track.link.uri, sp_track.error)
return
if sp_track.availability != spotify.TrackAvailability.AVAILABLE:
return
artists = [to_artist(sp_artist) for sp_artist in sp_track.artists]
artists = filter(None, artists)
album = to_album(sp_track.album)
return models.Track(
uri=sp_track.link.uri,
name=sp_track.name,
artists=artists,
album=album,
date=album.date,
length=sp_track.duration,
disc_no=sp_track.disc,
track_no=sp_track.index,
bitrate=bitrate)
@memoized
def to_track_ref(sp_track):
if not sp_track.is_loaded:
return
if sp_track.error != spotify.ErrorType.OK:
logger.debug(
'Error loading %s: %r', sp_track.link.uri, sp_track.error)
return
if sp_track.availability != spotify.TrackAvailability.AVAILABLE:
return
return models.Ref.track(uri=sp_track.link.uri, name=sp_track.name)
def to_track_refs(sp_tracks):
for sp_track in sp_tracks:
sp_track.load()
ref = to_track_ref(sp_track)
if ref is not None:
yield ref
def to_playlist(
sp_playlist, folders=None, username=None, bitrate=None,
as_ref=False, as_items=False):
if not isinstance(sp_playlist, spotify.Playlist):
return
if not sp_playlist.is_loaded:
return
if as_items:
return list(to_track_refs(sp_playlist.tracks))
name = sp_playlist.name
if not as_ref:
tracks = [
to_track(sp_track, bitrate=bitrate)
for sp_track in sp_playlist.tracks]
tracks = filter(None, tracks)
if name is None:
# Use same starred order as the Spotify client
tracks = list(reversed(tracks))
if name is None:
name = 'Starred'
if folders is not None:
name = '/'.join(folders + [name])
if username is not None and sp_playlist.owner.canonical_name != username:
name = '%s (by %s)' % (name, sp_playlist.owner.canonical_name)
if as_ref:
return models.Ref.playlist(uri=sp_playlist.link.uri, name=name)
else:
return models.Playlist(
uri=sp_playlist.link.uri, name=name, tracks=tracks)
def to_playlist_ref(sp_playlist, folders=None, username=None):
return to_playlist(
sp_playlist, folders=folders, username=username, as_ref=True)
# Maps from Mopidy search query field to Spotify search query field.
# `None` if there is no matching concept.
SEARCH_FIELD_MAP = {
'albumartist': 'artist',
'date': 'year',
'track_name': 'track',
'track_number': None,
}
def sp_search_query(query):
"""Translate a Mopidy search query to a Spotify search query"""
result = []
for (field, values) in query.items():
field = SEARCH_FIELD_MAP.get(field, field)
if field is None:
continue
for value in values:
if field == 'year':
value = _transform_year(value)
if value is not None:
result.append('%s:%d' % (field, value))
elif field == 'any':
result.append('"%s"' % value)
else:
result.append('%s:"%s"' % (field, value))
return ' '.join(result)
def _transform_year(date):
try:
return int(date.split('-')[0])
except ValueError:
logger.debug(
'Excluded year from search query: '
'Cannot parse date "%s"', date)
PK ZSGi) mopidy_spotify/__init__.pyfrom __future__ import unicode_literals
import os
from mopidy import config, ext
__version__ = '2.0.1'
class Extension(ext.Extension):
dist_name = 'Mopidy-Spotify'
ext_name = 'spotify'
version = __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['volume_normalization'] = config.Boolean()
schema['private_session'] = config.Boolean()
schema['timeout'] = config.Integer(minimum=0)
schema['cache_dir'] = config.Deprecated() # since 2.0
schema['settings_dir'] = config.Deprecated() # since 2.0
schema['allow_cache'] = config.Boolean()
schema['allow_network'] = config.Boolean()
schema['allow_playlists'] = config.Boolean()
schema['search_album_count'] = config.Integer(minimum=0, maximum=200)
schema['search_artist_count'] = config.Integer(minimum=0, maximum=200)
schema['search_track_count'] = config.Integer(minimum=0, maximum=200)
schema['toplist_countries'] = config.List(optional=True)
return schema
def setup(self, registry):
from mopidy_spotify.backend import SpotifyBackend
registry.add('backend', SpotifyBackend)
PK USGSt" " mopidy_spotify/backend.pyfrom __future__ import unicode_literals
import logging
import os
import threading
from mopidy import backend
import pykka
import spotify
from mopidy_spotify import Extension, library, playback, playlists
logger = logging.getLogger(__name__)
BITRATES = {
96: spotify.Bitrate.BITRATE_96k,
160: spotify.Bitrate.BITRATE_160k,
320: spotify.Bitrate.BITRATE_320k,
}
class SpotifyBackend(pykka.ThreadingActor, backend.Backend):
_logged_in = threading.Event()
_logged_out = threading.Event()
_logged_out.set()
def __init__(self, config, audio):
super(SpotifyBackend, self).__init__()
self._config = config
self._audio = audio
self._actor_proxy = None
self._session = None
self._event_loop = None
self._bitrate = None
self.library = library.SpotifyLibraryProvider(backend=self)
self.playback = playback.SpotifyPlaybackProvider(
audio=audio, backend=self)
if config['spotify']['allow_playlists']:
self.playlists = playlists.SpotifyPlaylistsProvider(backend=self)
else:
self.playlists = None
self.uri_schemes = ['spotify']
def on_start(self):
self._actor_proxy = self.actor_ref.proxy()
self._session = self._get_session(self._config)
self._event_loop = spotify.EventLoop(self._session)
self._event_loop.start()
self._session.login(
self._config['spotify']['username'],
self._config['spotify']['password'])
def on_stop(self):
logger.debug('Logging out of Spotify')
self._session.logout()
self._logged_out.wait()
self._event_loop.stop()
def _get_session(self, config):
session = spotify.Session(self._get_spotify_config(config))
session.connection.allow_network = config['spotify']['allow_network']
self._bitrate = config['spotify']['bitrate']
session.preferred_bitrate = BITRATES[self._bitrate]
session.volume_normalization = (
config['spotify']['volume_normalization'])
backend_actor_proxy = self._actor_proxy
session.on(
spotify.SessionEvent.CONNECTION_STATE_UPDATED,
on_connection_state_changed,
self._logged_in, self._logged_out, backend_actor_proxy)
session.on(
spotify.SessionEvent.PLAY_TOKEN_LOST,
on_play_token_lost, backend_actor_proxy)
return session
def _get_spotify_config(self, config):
ext = Extension()
spotify_config = spotify.Config()
spotify_config.load_application_key_file(
os.path.join(os.path.dirname(__file__), 'spotify_appkey.key'))
if config['spotify']['allow_cache']:
spotify_config.cache_location = ext.get_cache_dir(config)
else:
spotify_config.cache_location = None
spotify_config.settings_location = ext.get_data_dir(config)
return spotify_config
def on_logged_in(self):
if self._config['spotify']['private_session']:
logger.info('Spotify private session activated')
self._session.social.private_session = True
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.CONTAINER_LOADED,
playlists.on_container_loaded)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_ADDED,
playlists.on_playlist_added)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_REMOVED,
playlists.on_playlist_removed)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_MOVED,
playlists.on_playlist_moved)
def on_play_token_lost(self):
if self._session.player.state == spotify.PlayerState.PLAYING:
self.playback.pause()
logger.warning(
'Spotify has been paused because your account is '
'being used somewhere else.')
def on_connection_state_changed(
session, logged_in_event, logged_out_event, backend):
# Called from the pyspotify event loop, and not in an actor context.
if session.connection.state is spotify.ConnectionState.LOGGED_OUT:
logger.debug('Logged out of Spotify')
logged_in_event.clear()
logged_out_event.set()
elif session.connection.state is spotify.ConnectionState.LOGGED_IN:
logger.info('Logged in to Spotify in online mode')
logged_in_event.set()
logged_out_event.clear()
backend.on_logged_in()
elif session.connection.state is spotify.ConnectionState.DISCONNECTED:
logger.info('Disconnected from Spotify')
elif session.connection.state is spotify.ConnectionState.OFFLINE:
logger.info('Logged in to Spotify in offline mode')
logged_in_event.set()
logged_out_event.clear()
def on_play_token_lost(session, backend):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug('Spotify play token lost')
backend.on_play_token_lost()
PK USG;UP mopidy_spotify/lookup.pyfrom __future__ import unicode_literals
import logging
import spotify
from mopidy_spotify import translator, utils
logger = logging.getLogger(__name__)
_VARIOUS_ARTISTS_URIS = [
'spotify:artist:0LyfQWJT6nXafLPZqxe9Of',
]
def lookup(config, session, uri):
try:
sp_link = session.get_link(uri)
except ValueError as exc:
logger.info('Failed to lookup "%s": %s', uri, exc)
return []
try:
if sp_link.type is spotify.LinkType.TRACK:
return list(_lookup_track(config, sp_link))
elif sp_link.type is spotify.LinkType.ALBUM:
return list(_lookup_album(config, sp_link))
elif sp_link.type is spotify.LinkType.ARTIST:
with utils.time_logger('Artist lookup'):
return list(_lookup_artist(config, sp_link))
elif sp_link.type is spotify.LinkType.PLAYLIST:
return list(_lookup_playlist(config, sp_link))
elif sp_link.type is spotify.LinkType.STARRED:
return list(reversed(list(_lookup_playlist(config, sp_link))))
else:
logger.info(
'Failed to lookup "%s": Cannot handle %r',
uri, sp_link.type)
return []
except spotify.Error as exc:
logger.info('Failed to lookup "%s": %s', uri, exc)
return []
def _lookup_track(config, sp_link):
sp_track = sp_link.as_track()
sp_track.load()
track = translator.to_track(sp_track, bitrate=config['bitrate'])
if track is not None:
yield track
def _lookup_album(config, sp_link):
sp_album = sp_link.as_album()
sp_album_browser = sp_album.browse()
sp_album_browser.load()
for sp_track in sp_album_browser.tracks:
track = translator.to_track(
sp_track, bitrate=config['bitrate'])
if track is not None:
yield track
def _lookup_artist(config, sp_link):
sp_artist = sp_link.as_artist()
sp_artist_browser = sp_artist.browse(
type=spotify.ArtistBrowserType.NO_TRACKS)
sp_artist_browser.load()
# Get all album browsers we need first, so they can start retrieving
# data in the background.
sp_album_browsers = []
for sp_album in sp_artist_browser.albums:
sp_album.load()
if not sp_album.is_available:
continue
if sp_album.type is spotify.AlbumType.COMPILATION:
continue
if sp_album.artist.link.uri in _VARIOUS_ARTISTS_URIS:
continue
sp_album_browsers.append(sp_album.browse())
for sp_album_browser in sp_album_browsers:
sp_album_browser.load()
for sp_track in sp_album_browser.tracks:
track = translator.to_track(
sp_track, bitrate=config['bitrate'])
if track is not None:
yield track
def _lookup_playlist(config, sp_link):
sp_playlist = sp_link.as_playlist()
sp_playlist.load()
for sp_track in sp_playlist.tracks:
track = translator.to_track(
sp_track, bitrate=config['bitrate'])
if track is not None:
yield track
PK USG+~ mopidy_spotify/playlists.pyfrom __future__ import unicode_literals
import logging
from mopidy import backend
import spotify
from mopidy_spotify import translator, utils
logger = logging.getLogger(__name__)
class SpotifyPlaylistsProvider(backend.PlaylistsProvider):
def __init__(self, backend):
self._backend = backend
def as_list(self):
with utils.time_logger('playlists.as_list()'):
return (
list(self._get_starred_playlist_ref()) +
list(self._get_flattened_playlist_refs()))
def _get_starred_playlist_ref(self):
if self._backend._session is None:
return
sp_starred = self._backend._session.get_starred()
if sp_starred is None:
return
if (
self._backend._session.connection.state is
spotify.ConnectionState.LOGGED_IN):
sp_starred.load()
starred_ref = translator.to_playlist_ref(
sp_starred, username=self._backend._session.user_name)
if starred_ref is not None:
yield starred_ref
def _get_flattened_playlist_refs(self):
if self._backend._session is None:
return
if self._backend._session.playlist_container is None:
return
username = self._backend._session.user_name
folders = []
for sp_playlist in self._backend._session.playlist_container:
if isinstance(sp_playlist, spotify.PlaylistFolder):
if sp_playlist.type is spotify.PlaylistType.START_FOLDER:
folders.append(sp_playlist.name)
elif sp_playlist.type is spotify.PlaylistType.END_FOLDER:
folders.pop()
continue
playlist_ref = translator.to_playlist_ref(
sp_playlist, folders=folders, username=username)
if playlist_ref is not None:
yield playlist_ref
def get_items(self, uri):
with utils.time_logger('playlist.get_items(%s)' % uri):
return self._get_playlist(uri, as_items=True)
def lookup(self, uri):
with utils.time_logger('playlists.lookup(%s)' % uri):
return self._get_playlist(uri)
def _get_playlist(self, uri, as_items=False):
try:
sp_playlist = self._backend._session.get_playlist(uri)
except spotify.Error as exc:
logger.debug('Failed to lookup Spotify URI %s: %s', uri, exc)
return
if not sp_playlist.is_loaded:
logger.debug(
'Waiting for Spotify playlist to load: %s', sp_playlist)
sp_playlist.load()
username = self._backend._session.user_name
return translator.to_playlist(
sp_playlist, username=username, bitrate=self._backend._bitrate,
as_items=as_items)
def refresh(self):
pass # Not needed as long as we don't cache anything.
def create(self, name):
try:
sp_playlist = (
self._backend._session.playlist_container
.add_new_playlist(name))
except ValueError as exc:
logger.warning(
'Failed creating new Spotify playlist "%s": %s', name, exc)
except spotify.Error:
logger.warning('Failed creating new Spotify playlist "%s"', name)
else:
username = self._backend._session.user_name
return translator.to_playlist(sp_playlist, username=username)
def delete(self, uri):
pass # TODO
def save(self, playlist):
pass # TODO
def on_container_loaded(sp_playlist_container):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug('Spotify playlist container loaded')
# This event listener is also called after playlists are added, removed and
# moved, so since Mopidy currently only supports the "playlists_loaded"
# event this is the only place we need to trigger a Mopidy backend event.
backend.BackendListener.send('playlists_loaded')
def on_playlist_added(sp_playlist_container, sp_playlist, index):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
'Spotify playlist "%s" added to index %d', sp_playlist.name, index)
# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
def on_playlist_removed(sp_playlist_container, sp_playlist, index):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
'Spotify playlist "%s" removed from index %d', sp_playlist.name, index)
# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
def on_playlist_moved(
sp_playlist_container, sp_playlist, old_index, new_index):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
'Spotify playlist "%s" moved from index %d to %d',
sp_playlist.name, old_index, new_index)
# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
PK [9FMA A ! mopidy_spotify/spotify_appkey.keyωޟ!P3sRNG S"^=+ d/dK~8(%nG~L.$qsmŽy|30>t|d
ޢڷ+߲4λy/!u?d9W (qGHTM+ã+"gpvP24dKo bHW])õWtNY묙p,`_>Vt(],E o`%г0[a:Ⱦէ E{{&]nLBrIi}fωLAd_<6M=PK SG(t* t* . Mopidy_Spotify-2.0.1.dist-info/DESCRIPTION.rst**************
Mopidy-Spotify
**************
.. image:: https://img.shields.io/pypi/v/Mopidy-Spotify.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy-Spotify/
:alt: Latest PyPI version
.. image:: https://img.shields.io/pypi/dm/Mopidy-Spotify.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy-Spotify/
:alt: Number of PyPI downloads
.. image:: https://img.shields.io/travis/mopidy/mopidy-spotify/develop.svg?style=flat
:target: https://travis-ci.org/mopidy/mopidy-spotify
:alt: Travis CI build status
.. image:: https://img.shields.io/coveralls/mopidy/mopidy-spotify/develop.svg?style=flat
:target: https://coveralls.io/r/mopidy/mopidy-spotify?branch=develop
:alt: Test coverage
`Mopidy `_ extension for playing music from
`Spotify `_.
Dependencies
============
- A Spotify Premium subscription. Mopidy-Spotify **will not** work with Spotify
Free, just Spotify Premium.
- A non-Facebook Spotify username and password. If you created your account
through Facebook you'll need to create a "device password" to be able to use
Mopidy-Spotify. Go to http://www.spotify.com/account/set-device-password/,
login with your Facebook account, and follow the instructions.
- ``libspotify`` >= 12, < 13. The official C library from the `Spotify
developer site `_.
The package is available as ``libspotify12`` from
`apt.mopidy.com `__.
- ``pyspotify`` >= 2.0. The ``libspotify`` python wrapper. The package is
available as ``python-spotify`` from apt.mopidy.com or ``pyspotify`` on PyPI.
- ``Mopidy`` >= 1.1. The music server that Mopidy-Spotify extends.
If you install Mopidy-Spotify from apt.mopidy.com, AUR, or Homebrew, these
dependencies are installed automatically.
Installation
============
Debian/Ubuntu/Raspbian: Install the ``mopidy-spotify`` package from
`apt.mopidy.com `_::
sudo apt-get install mopidy-spotify
Arch Linux: Install the ``mopidy-spotify`` package from
`AUR `_::
yaourt -S mopidy-spotify
OS X: Install the ``mopidy-spotify`` package from the
`mopidy/mopidy `_ Homebrew tap::
brew install mopidy-spotify
Else: Install the dependencies listed above yourself, and then install the
package from PyPI::
pip install Mopidy-Spotify
Configuration
=============
Before starting Mopidy, you must add your Spotify Premium username and password
to your Mopidy configuration file::
[spotify]
username = alice
password = secret
The following configuration values are available:
- ``spotify/enabled``: If the Spotify extension should be enabled or not.
Defaults to ``true``.
- ``spotify/username``: Your Spotify Premium username. You *must* provide this.
- ``spotify/password``: Your Spotify Premium password. You *must* provide this.
- ``spotify/bitrate``: Audio bitrate in kbps. ``96``, ``160``, or ``320``.
Defaults to ``160``.
- ``spotify/volume_normalization``: Whether volume normalization is active or
not. Defaults to ``true``.
- ``spotify/timeout``: Seconds before giving up waiting for search results,
etc. Defaults to ``10``.
- ``spotify/allow_cache``: Whether to allow caching. The cache is stored in a
"spotify" directory within Mopidy's ``core/cache_dir``. Defaults to ``true``.
- ``spotify/allow_network``: Whether to allow network access or not. Defaults
to ``true``.
- ``spotify/allow_playlists``: Whether or not playlists should be exposed.
Defaults to ``true``.
- ``spotify/search_album_count``: Maximum number of albums returned in search
results. Number between 0 and 200. Defaults to 20.
- ``spotify/search_artist_count``: Maximum number of artists returned in search
results. Number between 0 and 200. Defaults to 10.
- ``spotify/search_track_count``: Maximum number of tracks returned in search
results. Number between 0 and 200. Defaults to 50.
- ``spotify/toplist_countries``: Comma separated list of two letter ISO country
codes to get toplists for. Defaults to blank, which is interpreted as all
countries that Spotify is available in.
- ``spotify/private_session``: Whether to use a private Spotify session. Turn
on private session to disable sharing of played tracks with friends through
the Spotify activity feed, Last.fm scrobbling, and Facebook. This only
affects social sharing done by Spotify, not by other Mopidy extensions.
Defaults to ``false``.
Project resources
=================
- `Source code `_
- `Issue tracker `_
- `Download development snapshot `_
Changelog
=========
v2.0.1 (2015-08-23)
-------------------
Bug fix release.
- Filter out ``None`` from ``library.get_distinct()`` return values. (Fixes:
#63)
v2.0.0 (2015-08-11)
-------------------
Rewrite using pyspotify 2. Should have feature parity with Mopidy-Spotify 1.
**Config**
- Add ``spotify/volume_normalization`` config. (Fixes: #13)
- Add ``spotify/allow_network`` config which can be used to force
Mopidy-Spotify to stay offline. This is mostly useful for testing during
development.
- Add ``spotify/allow_playlists`` config which can be used to disable all
access to playlists on the Spotify account. Useful where Mopidy is shared by
multiple users. (Fixes: #25)
- Make maximum number of returned results configurable through
``spotify/search_album_count``, ``spotify/search_artist_count``, and
``spotify/search_track_count``.
- Add ``spotify/private_session`` config.
- Change ``spotify/toplist_countries`` default value to blank, which is now
interpreted as all supported countries instead of no countries.
- Removed ``spotify/cache_dir`` and ``spotify/settings_dir`` config values. We
now use a "spotify" directory in the ``core/cache_dir`` and
``core/data_dir`` directories defined in Mopidy's configuration.
- Add ``spotify/allow_cache`` config value to make it possible to disable
caching.
**Browse**
- Add browsing of top albums and top artists, in additon to top tracks.
- Add browsing by current user's country, in addition to personal, global and
per-country browsing.
- Add browsing of artists, which includes the artist's top tracks and albums.
- Update list of countries Spotify is available in and provides toplists for.
**Lookup**
- Adding an artist by URI will now first find all albums by the artist and
then all tracks in the albums. This way, the returned tracks are grouped by
album and they are sorted by track number. (Fixes: #7)
- When adding an artist by URI, all albums that are marked as "compilations"
or where the album artist is "Various Artists" are now ignored. (Fixes: #5)
**Library**
- The library provider method ``get_distinct()`` is now supported. When called
without a query, the tracks in the user's playlists is used as the data
source. When called with a query, a Spotify search is used as the data
source. This addition makes the library view in some notable MPD clients,
like ncmpcpp, become quite fast and usable with Spotify. (Fixes: #50)
**Playback**
- If another Spotify client starts playback with the same account, we get a
"play token lost" event. Previously, Mopidy-Spotify would unconditionally
pause Mopidy playback if this happened. Now, we only pause playback if we're
currently playing music from Spotify. (Fixes: #1)
v1.4.0 (2015-05-19)
-------------------
- Update to not use deprecated Mopidy audio APIs.
- Use strings and not ints for the model's date field. This is required for
compatibility with the model validation added in Mopidy 1.1. (Fixes: #52)
- Fix error causing the image of every 50th URI in a ``library.get_images()``
call to not be looked up and returned.
- Fix handling of empty search queries. This was still using the removed
``playlists.playlists`` to fetch all your tracks.
- Update the ``SpotifyTrack`` proxy model to work with Mopidy 1.1 model
changes.
- Updated to work with the renaming of ``mopidy.utils`` to ``mopidy.internal``
in Mopidy 1.1.
v1.3.0 (2015-03-25)
-------------------
- Require Mopidy >= 1.0.
- Update to work with new playback API in Mopidy 1.0.
- Update to work with new playlists API in Mopidy 1.0.
- Update to work with new search API in Mopidy 1.0.
- Add ``library.get_images()`` support for cover art.
v1.2.0 (2014-07-21)
-------------------
- Add support for browsing playlists and albums. Needed to allow music
discovery extensions expose these in a clean way.
- Fix loss of audio when resuming from paused, when caused by another Spotify
client starting playback. (Fixes: #2, PR: #19)
v1.1.3 (2014-02-18)
-------------------
- Switch to new backend API locations, required by the upcoming Mopidy 0.19
release.
v1.1.2 (2014-02-18)
-------------------
- Wait for track to be loaded before playing it. This fixes playback of tracks
looked up directly by URI, and not through a playlist or search. (Fixes:
mopidy/mopidy#675)
v1.1.1 (2014-02-16)
-------------------
- Change requirement on pyspotify from ``>= 1.9, < 2`` to ``>= 1.9, < 1.999``,
so that it is parsed correctly and pyspotify 1.x is installed instead of 2.x.
v1.1.0 (2014-01-20)
-------------------
- Require Mopidy >= 0.18.
- Change ``library.lookup()`` to return tracks even if they are unplayable.
There's no harm in letting them be added to the tracklist, as Mopidy will
simply skip to the next track when failing to play the track. (Fixes:
mopidy/mopidy#606)
- Added basic library browsing support that exposes user, global and country
toplists.
v1.0.3 (2013-12-15)
-------------------
- Change search field ``track`` to ``track_name`` for compatibility with
Mopidy 0.17. (Fixes: mopidy/mopidy#610)
v1.0.2 (2013-11-19)
-------------------
- Add ``spotify/settings_dir`` config value so that libspotify settings can be
stored to another location than the libspotify cache. This also allows
``spotify/cache_dir`` to be unset, since settings are now using it's own
config value.
- Make the ``spotify/cache_dir`` config value optional, so that it can be set
to an empty string to disable caching.
v1.0.1 (2013-10-28)
-------------------
- Support searches from Mopidy that are using the ``albumartist`` field type,
added in Mopidy 0.16.
- Ignore the ``track_no`` field in search queries, added in Mopidy 0.16.
- Abort Spotify searches immediately if the search query is empty instead of
waiting for the 10s timeout before returning an empty search result.
v1.0.0 (2013-10-08)
-------------------
- Moved extension out of the main Mopidy project.
PK SG1 1 / Mopidy_Spotify-2.0.1.dist-info/entry_points.txt[mopidy.ext]
spotify = mopidy_spotify:Extension
PK SGؘT , Mopidy_Spotify-2.0.1.dist-info/metadata.json{"classifiers": ["Environment :: No Input/Output (Daemon)", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Topic :: Multimedia :: Sound/Audio :: Players"], "extensions": {"python.details": {"contacts": [{"email": "stein.magnus@jodal.no", "name": "Stein Magnus Jodal", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/mopidy/mopidy-spotify"}}, "python.exports": {"mopidy.ext": {"spotify": "mopidy_spotify:Extension"}}}, "extras": [], "generator": "bdist_wheel (0.24.0)", "license": "Apache License, Version 2.0", "metadata_version": "2.0", "name": "Mopidy-Spotify", "run_requires": [{"requires": ["setuptools", "Mopidy (>=1.1)", "Pykka (>=1.1)", "pyspotify (>=2.0.2)"]}], "summary": "Mopidy extension for playing music from Spotify", "version": "2.0.1"}PK SG]. . ' Mopidy_Spotify-2.0.1.dist-info/pbr.json{"is_release": true, "git_version": "ba773a0"}PK SGy , Mopidy_Spotify-2.0.1.dist-info/top_level.txtmopidy_spotify
PK SG3on n $ Mopidy_Spotify-2.0.1.dist-info/WHEELWheel-Version: 1.0
Generator: bdist_wheel (0.24.0)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any
PK SGbI- I- ' Mopidy_Spotify-2.0.1.dist-info/METADATAMetadata-Version: 2.0
Name: Mopidy-Spotify
Version: 2.0.1
Summary: Mopidy extension for playing music from Spotify
Home-page: https://github.com/mopidy/mopidy-spotify
Author: Stein Magnus Jodal
Author-email: stein.magnus@jodal.no
License: Apache License, Version 2.0
Platform: UNKNOWN
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
Requires-Dist: setuptools
Requires-Dist: Mopidy (>=1.1)
Requires-Dist: Pykka (>=1.1)
Requires-Dist: pyspotify (>=2.0.2)
**************
Mopidy-Spotify
**************
.. image:: https://img.shields.io/pypi/v/Mopidy-Spotify.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy-Spotify/
:alt: Latest PyPI version
.. image:: https://img.shields.io/pypi/dm/Mopidy-Spotify.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy-Spotify/
:alt: Number of PyPI downloads
.. image:: https://img.shields.io/travis/mopidy/mopidy-spotify/develop.svg?style=flat
:target: https://travis-ci.org/mopidy/mopidy-spotify
:alt: Travis CI build status
.. image:: https://img.shields.io/coveralls/mopidy/mopidy-spotify/develop.svg?style=flat
:target: https://coveralls.io/r/mopidy/mopidy-spotify?branch=develop
:alt: Test coverage
`Mopidy `_ extension for playing music from
`Spotify `_.
Dependencies
============
- A Spotify Premium subscription. Mopidy-Spotify **will not** work with Spotify
Free, just Spotify Premium.
- A non-Facebook Spotify username and password. If you created your account
through Facebook you'll need to create a "device password" to be able to use
Mopidy-Spotify. Go to http://www.spotify.com/account/set-device-password/,
login with your Facebook account, and follow the instructions.
- ``libspotify`` >= 12, < 13. The official C library from the `Spotify
developer site `_.
The package is available as ``libspotify12`` from
`apt.mopidy.com `__.
- ``pyspotify`` >= 2.0. The ``libspotify`` python wrapper. The package is
available as ``python-spotify`` from apt.mopidy.com or ``pyspotify`` on PyPI.
- ``Mopidy`` >= 1.1. The music server that Mopidy-Spotify extends.
If you install Mopidy-Spotify from apt.mopidy.com, AUR, or Homebrew, these
dependencies are installed automatically.
Installation
============
Debian/Ubuntu/Raspbian: Install the ``mopidy-spotify`` package from
`apt.mopidy.com `_::
sudo apt-get install mopidy-spotify
Arch Linux: Install the ``mopidy-spotify`` package from
`AUR `_::
yaourt -S mopidy-spotify
OS X: Install the ``mopidy-spotify`` package from the
`mopidy/mopidy `_ Homebrew tap::
brew install mopidy-spotify
Else: Install the dependencies listed above yourself, and then install the
package from PyPI::
pip install Mopidy-Spotify
Configuration
=============
Before starting Mopidy, you must add your Spotify Premium username and password
to your Mopidy configuration file::
[spotify]
username = alice
password = secret
The following configuration values are available:
- ``spotify/enabled``: If the Spotify extension should be enabled or not.
Defaults to ``true``.
- ``spotify/username``: Your Spotify Premium username. You *must* provide this.
- ``spotify/password``: Your Spotify Premium password. You *must* provide this.
- ``spotify/bitrate``: Audio bitrate in kbps. ``96``, ``160``, or ``320``.
Defaults to ``160``.
- ``spotify/volume_normalization``: Whether volume normalization is active or
not. Defaults to ``true``.
- ``spotify/timeout``: Seconds before giving up waiting for search results,
etc. Defaults to ``10``.
- ``spotify/allow_cache``: Whether to allow caching. The cache is stored in a
"spotify" directory within Mopidy's ``core/cache_dir``. Defaults to ``true``.
- ``spotify/allow_network``: Whether to allow network access or not. Defaults
to ``true``.
- ``spotify/allow_playlists``: Whether or not playlists should be exposed.
Defaults to ``true``.
- ``spotify/search_album_count``: Maximum number of albums returned in search
results. Number between 0 and 200. Defaults to 20.
- ``spotify/search_artist_count``: Maximum number of artists returned in search
results. Number between 0 and 200. Defaults to 10.
- ``spotify/search_track_count``: Maximum number of tracks returned in search
results. Number between 0 and 200. Defaults to 50.
- ``spotify/toplist_countries``: Comma separated list of two letter ISO country
codes to get toplists for. Defaults to blank, which is interpreted as all
countries that Spotify is available in.
- ``spotify/private_session``: Whether to use a private Spotify session. Turn
on private session to disable sharing of played tracks with friends through
the Spotify activity feed, Last.fm scrobbling, and Facebook. This only
affects social sharing done by Spotify, not by other Mopidy extensions.
Defaults to ``false``.
Project resources
=================
- `Source code `_
- `Issue tracker `_
- `Download development snapshot `_
Changelog
=========
v2.0.1 (2015-08-23)
-------------------
Bug fix release.
- Filter out ``None`` from ``library.get_distinct()`` return values. (Fixes:
#63)
v2.0.0 (2015-08-11)
-------------------
Rewrite using pyspotify 2. Should have feature parity with Mopidy-Spotify 1.
**Config**
- Add ``spotify/volume_normalization`` config. (Fixes: #13)
- Add ``spotify/allow_network`` config which can be used to force
Mopidy-Spotify to stay offline. This is mostly useful for testing during
development.
- Add ``spotify/allow_playlists`` config which can be used to disable all
access to playlists on the Spotify account. Useful where Mopidy is shared by
multiple users. (Fixes: #25)
- Make maximum number of returned results configurable through
``spotify/search_album_count``, ``spotify/search_artist_count``, and
``spotify/search_track_count``.
- Add ``spotify/private_session`` config.
- Change ``spotify/toplist_countries`` default value to blank, which is now
interpreted as all supported countries instead of no countries.
- Removed ``spotify/cache_dir`` and ``spotify/settings_dir`` config values. We
now use a "spotify" directory in the ``core/cache_dir`` and
``core/data_dir`` directories defined in Mopidy's configuration.
- Add ``spotify/allow_cache`` config value to make it possible to disable
caching.
**Browse**
- Add browsing of top albums and top artists, in additon to top tracks.
- Add browsing by current user's country, in addition to personal, global and
per-country browsing.
- Add browsing of artists, which includes the artist's top tracks and albums.
- Update list of countries Spotify is available in and provides toplists for.
**Lookup**
- Adding an artist by URI will now first find all albums by the artist and
then all tracks in the albums. This way, the returned tracks are grouped by
album and they are sorted by track number. (Fixes: #7)
- When adding an artist by URI, all albums that are marked as "compilations"
or where the album artist is "Various Artists" are now ignored. (Fixes: #5)
**Library**
- The library provider method ``get_distinct()`` is now supported. When called
without a query, the tracks in the user's playlists is used as the data
source. When called with a query, a Spotify search is used as the data
source. This addition makes the library view in some notable MPD clients,
like ncmpcpp, become quite fast and usable with Spotify. (Fixes: #50)
**Playback**
- If another Spotify client starts playback with the same account, we get a
"play token lost" event. Previously, Mopidy-Spotify would unconditionally
pause Mopidy playback if this happened. Now, we only pause playback if we're
currently playing music from Spotify. (Fixes: #1)
v1.4.0 (2015-05-19)
-------------------
- Update to not use deprecated Mopidy audio APIs.
- Use strings and not ints for the model's date field. This is required for
compatibility with the model validation added in Mopidy 1.1. (Fixes: #52)
- Fix error causing the image of every 50th URI in a ``library.get_images()``
call to not be looked up and returned.
- Fix handling of empty search queries. This was still using the removed
``playlists.playlists`` to fetch all your tracks.
- Update the ``SpotifyTrack`` proxy model to work with Mopidy 1.1 model
changes.
- Updated to work with the renaming of ``mopidy.utils`` to ``mopidy.internal``
in Mopidy 1.1.
v1.3.0 (2015-03-25)
-------------------
- Require Mopidy >= 1.0.
- Update to work with new playback API in Mopidy 1.0.
- Update to work with new playlists API in Mopidy 1.0.
- Update to work with new search API in Mopidy 1.0.
- Add ``library.get_images()`` support for cover art.
v1.2.0 (2014-07-21)
-------------------
- Add support for browsing playlists and albums. Needed to allow music
discovery extensions expose these in a clean way.
- Fix loss of audio when resuming from paused, when caused by another Spotify
client starting playback. (Fixes: #2, PR: #19)
v1.1.3 (2014-02-18)
-------------------
- Switch to new backend API locations, required by the upcoming Mopidy 0.19
release.
v1.1.2 (2014-02-18)
-------------------
- Wait for track to be loaded before playing it. This fixes playback of tracks
looked up directly by URI, and not through a playlist or search. (Fixes:
mopidy/mopidy#675)
v1.1.1 (2014-02-16)
-------------------
- Change requirement on pyspotify from ``>= 1.9, < 2`` to ``>= 1.9, < 1.999``,
so that it is parsed correctly and pyspotify 1.x is installed instead of 2.x.
v1.1.0 (2014-01-20)
-------------------
- Require Mopidy >= 0.18.
- Change ``library.lookup()`` to return tracks even if they are unplayable.
There's no harm in letting them be added to the tracklist, as Mopidy will
simply skip to the next track when failing to play the track. (Fixes:
mopidy/mopidy#606)
- Added basic library browsing support that exposes user, global and country
toplists.
v1.0.3 (2013-12-15)
-------------------
- Change search field ``track`` to ``track_name`` for compatibility with
Mopidy 0.17. (Fixes: mopidy/mopidy#610)
v1.0.2 (2013-11-19)
-------------------
- Add ``spotify/settings_dir`` config value so that libspotify settings can be
stored to another location than the libspotify cache. This also allows
``spotify/cache_dir`` to be unset, since settings are now using it's own
config value.
- Make the ``spotify/cache_dir`` config value optional, so that it can be set
to an empty string to disable caching.
v1.0.1 (2013-10-28)
-------------------
- Support searches from Mopidy that are using the ``albumartist`` field type,
added in Mopidy 0.16.
- Ignore the ``track_no`` field in search queries, added in Mopidy 0.16.
- Abort Spotify searches immediately if the search query is empty instead of
waiting for the 10s timeout before returning an empty search result.
v1.0.0 (2013-10-08)
-------------------
- Moved extension out of the main Mopidy project.
PK SG % Mopidy_Spotify-2.0.1.dist-info/RECORDMopidy_Spotify-2.0.1.dist-info/METADATA,sha256=LH68JOF2eHnmoXVxb-v_KDYbE0n5hwM02Xd7FGlaVjI,11593
Mopidy_Spotify-2.0.1.dist-info/DESCRIPTION.rst,sha256=ddhn7oviymcBp3gAdSag4_o-WgSaWDw_LerK4gAi51g,10868
Mopidy_Spotify-2.0.1.dist-info/RECORD,,
Mopidy_Spotify-2.0.1.dist-info/WHEEL,sha256=AvR0WeTpDaxT645bl5FQxUK6NPsTls2ttpcGJg3j1Xg,110
Mopidy_Spotify-2.0.1.dist-info/pbr.json,sha256=19lvGrydUgJNzVouq62xyhjgu6_2T8e6Rhm-Dxs9gPA,46
Mopidy_Spotify-2.0.1.dist-info/entry_points.txt,sha256=-s_A2d5bP90_tCxjW1TWXoGs0kxBjI7auv1y52nPaT8,49
Mopidy_Spotify-2.0.1.dist-info/metadata.json,sha256=mUuwJRJFgs8YrperqkOIAK-5rK7_4L2glYw4VosCGI0,949
Mopidy_Spotify-2.0.1.dist-info/top_level.txt,sha256=_5VdB0GVAfrfbDKiJgCgUldYBUu0yxwuSM1pC5Ie9Ks,15
mopidy_spotify/library.py,sha256=uB8c4yXwB8YCxw3sopCdU41xGAUlWKxXpdFPlydnOLQ,1001
mopidy_spotify/search.py,sha256=EiWoB5UlOyL4PJievHfqjGQUVkWERXW5nuhibD437Z0,1920
mopidy_spotify/distinct.py,sha256=96jcIniyy4cMnlp2NJIKUFTprUAuYvSHch5kwaqpRuU,4000
mopidy_spotify/browse.py,sha256=MzQifhZ4BAolPVSkgaueMJYoP9BQleh69dWjMotu_Nk,4231
mopidy_spotify/countries.py,sha256=tBG5btB_HmzmR6tyNreaqSmQebQhh8_m-I5U1KzGV4g,1513
mopidy_spotify/playback.py,sha256=Gr4GCUMUNmAjlFFGvedINWkO414wo4wGtBT_Y5nWifE,6130
mopidy_spotify/utils.py,sha256=I3OgOKe2wMbUIbrMF7SF0sesQYZVzHS_DtPmg8rySgg,514
mopidy_spotify/ext.conf,sha256=E8eUhWlKKz3-l_QqSIhtPU8RGPm60k74J64tZ5NkPP4,282
mopidy_spotify/images.py,sha256=JQ_4AJYY6PzhwbOkxQPsuQhU7Loh2S571Ws1sdQ9ea4,3169
mopidy_spotify/translator.py,sha256=GZoqHEzZvRScvetL46-OkrkG2DRf71ezVcjLGaqt9SU,5852
mopidy_spotify/__init__.py,sha256=Vghhos46ie88S0ZIrPSgG4WFW0oHXzpb00DT8Z4q54M,1538
mopidy_spotify/backend.py,sha256=cOMV9Wn8_wei3_6sCRQDsj7x84FuG_xEszuMYbsq8LI,5154
mopidy_spotify/lookup.py,sha256=o4wIH9REx-veVGe6Ftux3Y9vFdHRP3JpMGJVTky0rjA,3100
mopidy_spotify/playlists.py,sha256=s_uCPalHysZUOSmQP09ubYr_l35Sy-ZHjsCmoIpXljw,5138
mopidy_spotify/spotify_appkey.key,sha256=TZ-2ceWYTxKFZTSbRtwWccOK6dCJoGsfu_hCa24fvpY,321
PK USGr mopidy_spotify/library.pyPK USG\n mopidy_spotify/search.pyPK ZSG' mopidy_spotify/distinct.pyPK USG"jʇ mopidy_spotify/browse.pyPK USGB< k, mopidy_spotify/countries.pyPK USGܚ 2 mopidy_spotify/playback.pyPK USGSݭ J mopidy_spotify/utils.pyPK USG' = L mopidy_spotify/ext.confPK USG{a a =N mopidy_spotify/images.pyPK USGF/ Z mopidy_spotify/translator.pyPK ZSGi) q mopidy_spotify/__init__.pyPK USGSt" " $x mopidy_spotify/backend.pyPK USG;UP } mopidy_spotify/lookup.pyPK USG+~ Ϙ mopidy_spotify/playlists.pyPK [9FMA A ! mopidy_spotify/spotify_appkey.keyPK SG(t* t* . Mopidy_Spotify-2.0.1.dist-info/DESCRIPTION.rstPK SG1 1 / Z Mopidy_Spotify-2.0.1.dist-info/entry_points.txtPK SGؘT , Mopidy_Spotify-2.0.1.dist-info/metadata.jsonPK SG]. . ' Mopidy_Spotify-2.0.1.dist-info/pbr.jsonPK SGy , J Mopidy_Spotify-2.0.1.dist-info/top_level.txtPK SG3on n $ Mopidy_Spotify-2.0.1.dist-info/WHEELPK SGbI- I- ' S Mopidy_Spotify-2.0.1.dist-info/METADATAPK SG % Mopidy_Spotify-2.0.1.dist-info/RECORDPK