Skip to content

Commit

Permalink
Merge pull request #12 from adamcik/feature/browsing
Browse files Browse the repository at this point in the history
Basic browsing support
  • Loading branch information
jodal committed Jan 19, 2014
2 parents 1d9d562 + 96c67f2 commit 4e7aac0
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 14 deletions.
5 changes: 5 additions & 0 deletions README.rst
Expand Up @@ -82,6 +82,8 @@ The following configuration values are available:
- ``spotify/settings_dir``: The dir where the Spotify extension stores
libspotify settings. Defaults to ``$XDG_CONFIG_DIR/mopidy/spotify``, which
usually means ``~/.config/mopidy/spotify``.
- ``spotify/toplist_countries``: Comma separated list of two letter country
domains to get toplists for.


Project resources
Expand All @@ -103,6 +105,9 @@ v1.0.4 (UNRELEASED)
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)
-------------------

Expand Down
5 changes: 3 additions & 2 deletions mopidy_spotify/__init__.py
Expand Up @@ -26,8 +26,9 @@ def get_config_schema(self):
schema['timeout'] = config.Integer(minimum=0)
schema['cache_dir'] = config.Path(optional=True)
schema['settings_dir'] = config.Path()
schema['toplist_countries'] = config.List(optional=True)
return schema

def get_backend_classes(self):
def setup(self, registry):
from .backend import SpotifyBackend
return [SpotifyBackend]
registry.add('backend', SpotifyBackend)
1 change: 1 addition & 0 deletions mopidy_spotify/ext.conf
Expand Up @@ -6,3 +6,4 @@ bitrate = 160
timeout = 10
cache_dir = $XDG_CACHE_DIR/mopidy/spotify
settings_dir = $XDG_CONFIG_DIR/mopidy/spotify
toplist_countries = AD,AR,AU,AT,BE,CO,CY,DK,EE,FI,FR,DE,GR,HK,IS,IE,IT,LV,LI,LT,LU,MY,MX,MC,NL,NZ,NO,PT,ES,SG,SE,CH,TW,TR,GB,US
94 changes: 92 additions & 2 deletions mopidy_spotify/library.py
@@ -1,19 +1,58 @@
from __future__ import unicode_literals

import logging
import threading
import time
import urllib

import pykka
from spotify import Link, SpotifyError
from spotify import Link, SpotifyError, ToplistBrowser

from mopidy.backends import base
from mopidy.models import Track, SearchResult
from mopidy.models import Ref, Track, SearchResult

from . import translator

logger = logging.getLogger('mopidy_spotify')

SPOTIFY_COUNTRIES = {
'AD': 'Andorra',
'AR': 'Argentina',
'AT': 'Austria',
'AU': 'Australia',
'BE': 'Belgium',
'CH': 'Switzerland',
'CO': 'Colombia',
'CY': 'Cyprus',
'DE': 'Germany',
'DK': 'Denmark',
'EE': 'Estonia',
'ES': 'Spain',
'FI': 'Finland',
'FR': 'France',
'GB': 'United Kingdom',
'GR': 'Greece',
'HK': 'Hong Kong',
'IE': 'Ireland',
'IS': 'Iceland',
'IT': 'Italy',
'LI': 'Liechtenstein',
'LT': 'Lithuania',
'LU': 'Luxembourg',
'LV': 'Latvia',
'MC': 'Monaco',
'MX': 'Mexico',
'MY': 'Malaysia',
'NL': 'Netherlands',
'NO': 'Norway',
'NZ': 'New Zealand',
'PT': 'Portugal',
'SE': 'Sweden',
'SG': 'Singapore',
'TR': 'Turkey',
'TW': 'Taiwan',
'US': 'United States'}


class SpotifyTrack(Track):
"""Proxy object for unloaded Spotify tracks."""
Expand Down Expand Up @@ -56,10 +95,61 @@ def copy(self, **values):


class SpotifyLibraryProvider(base.BaseLibraryProvider):
root_directory = Ref.directory(uri='spotify:directory', name='Spotify')

def __init__(self, *args, **kwargs):
super(SpotifyLibraryProvider, self).__init__(*args, **kwargs)
self._timeout = self.backend.config['spotify']['timeout']

# TODO: add /artists/{top/tracks,albums/tracks} and /users?
self._root = [Ref.directory(uri='spotify:toplist:current',
name='Personal top tracks'),
Ref.directory(uri='spotify:toplist:all',
name='Global top tracks')]
self._countries = []

if not self.backend.config['spotify']['toplist_countries']:
return

self._root.append(Ref.directory(uri='spotify:toplist:countries',
name='Country top tracks'))
for code in self.backend.config['spotify']['toplist_countries']:
code = code.upper()
self._countries.append(Ref.directory(
uri='spotify:toplist:%s' % code.lower(),
name=SPOTIFY_COUNTRIES.get(code, code)))

def browse(self, uri):
if uri == self.root_directory.uri:
return self._root

variant, identifier = translator.parse_uri(uri.lower())
if variant != 'toplist':
return []

if identifier == 'countries':
return self._countries

if identifier not in ('all', 'current'):
identifier = identifier.upper()
if identifier not in SPOTIFY_COUNTRIES:
return []

result = []
done = threading.Event()

def callback(browser, userdata):
for track in browser:
result.append(translator.to_mopidy_track_ref(track))
done.set()

logger.debug('Performing toplist browse for %s', identifier)
ToplistBrowser(b'tracks', bytes(identifier), callback, None)
if not done.wait(self._timeout):
logger.warning('%s toplist browse timed out.', identifier)

return result

def find_exact(self, query=None, uris=None):
return self.search(query=query, uris=uris)

Expand Down
29 changes: 24 additions & 5 deletions mopidy_spotify/translator.py
@@ -1,10 +1,11 @@
from __future__ import unicode_literals

import logging
import re

import spotify

from mopidy.models import Artist, Album, Track, Playlist
from mopidy.models import Artist, Album, Playlist, Ref, Track


logger = logging.getLogger('mopidy_spotify')
Expand All @@ -16,14 +17,21 @@
TRACK_AVAILABLE = 1


def parse_uri(uri):
result = re.findall(r'^spotify:([a-z]+)(?::(\w+))?$', uri)
if result:
return result[0]
return None, None


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...]')
return Artist(uri=uri, name='[loading] %s' % uri)
artist_cache[uri] = Artist(uri=uri, name=spotify_artist.name())
return artist_cache[uri]

Expand All @@ -35,7 +43,7 @@ def to_mopidy_album(spotify_album):
if uri in album_cache:
return album_cache[uri]
if not spotify_album.is_loaded():
return Album(uri=uri, name='[loading...]')
return Album(uri=uri, name='[loading] %s' % uri)
album_cache[uri] = Album(
uri=uri,
name=spotify_album.name(),
Expand All @@ -44,14 +52,25 @@ def to_mopidy_album(spotify_album):
return album_cache[uri]


def to_mopidy_track_ref(spotify_track):
uri = str(spotify.Link.from_track(spotify_track, 0))
if not spotify_track.is_loaded():
return Ref.track(uri=uri, name='[loading] %s' % uri)

name = spotify_track.name()
if spotify_track.availability() != TRACK_AVAILABLE:
name = '[unplayable] %s' % name
return Ref.track(uri=uri, name=name)


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...]')
return Track(uri=uri, name='[loading] %s' % uri)
name = spotify_track.name()
if spotify_track.availability() != TRACK_AVAILABLE:
name = '[unplayable] %s' % name
Expand Down Expand Up @@ -82,7 +101,7 @@ def to_mopidy_playlist(
logger.debug('Spotify playlist translation error: %s', e)
return
if not spotify_playlist.is_loaded():
return Playlist(uri=uri, name='[loading...]')
return Playlist(uri=uri, name='[loading] %s' % uri)
name = spotify_playlist.name()
if folders:
folder_names = '/'.join(folder.name() for folder in folders)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -24,7 +24,7 @@ def get_version(filename):
include_package_data=True,
install_requires=[
'setuptools',
'Mopidy >= 0.17',
'Mopidy >= 0.18',
'Pykka >= 1.1',
'pyspotify >= 1.9, < 2',
],
Expand Down
10 changes: 6 additions & 4 deletions tests/test_extension.py
@@ -1,3 +1,4 @@
import mock
import unittest

from mopidy_spotify import Extension, backend as backend_lib
Expand All @@ -24,9 +25,10 @@ def test_get_config_schema(self):
self.assertIn('timeout', schema)
self.assertIn('cache_dir', schema)

def test_get_backend_classes(self):
ext = Extension()
def test_setup(self):
registry = mock.Mock()

backends = ext.get_backend_classes()
ext = Extension()
ext.setup(registry)

self.assertIn(backend_lib.SpotifyBackend, backends)
registry.add.assert_called_with('backend', backend_lib.SpotifyBackend)

0 comments on commit 4e7aac0

Please sign in to comment.