diff --git a/README.rst b/README.rst index fa66e62..ddc6ff8 100644 --- a/README.rst +++ b/README.rst @@ -86,6 +86,7 @@ Configuration [soundcloud] auth_token = 1-1111-1111111 explore_songs = 25 + stream_entries = 100 Project resources diff --git a/mopidy_soundcloud/__init__.py b/mopidy_soundcloud/__init__.py index 5ffa58b..aa92999 100644 --- a/mopidy_soundcloud/__init__.py +++ b/mopidy_soundcloud/__init__.py @@ -25,6 +25,7 @@ def get_config_schema(self): schema['auth_token'] = config.Secret() schema['explore'] = config.Deprecated() schema['explore_pages'] = config.Deprecated() + schema['stream_entries'] = config.Integer() return schema def validate_config(self, config): # no_coverage diff --git a/mopidy_soundcloud/ext.conf b/mopidy_soundcloud/ext.conf index 9f0a4e8..efa494b 100644 --- a/mopidy_soundcloud/ext.conf +++ b/mopidy_soundcloud/ext.conf @@ -5,4 +5,7 @@ enabled = True auth_token = # Number of songs to fetch in explore section -explore_songs = 25 \ No newline at end of file +explore_songs = 25 + +# Number of tracks and playlists to fetch from user stream +stream_entries = 100 \ No newline at end of file diff --git a/mopidy_soundcloud/library.py b/mopidy_soundcloud/library.py index f9f3ecc..646f8e3 100644 --- a/mopidy_soundcloud/library.py +++ b/mopidy_soundcloud/library.py @@ -11,7 +11,6 @@ from mopidy_soundcloud.soundcloud import safe_url - logger = logging.getLogger(__name__) @@ -27,7 +26,6 @@ def new_folder(name, path): def simplify_search_query(query): - if isinstance(query, dict): r = [] for v in query.values(): @@ -84,6 +82,22 @@ def list_liked(self): vfs_list[set_id] = new_folder(name, ['sets', set_id]) return vfs_list.values() + def list_stream(self): + vfs_list = collections.OrderedDict() + for data in self.backend.remote.get_user_stream(): + try: + name, set_id = data + except (TypeError, ValueError): + logger.debug( + 'Adding track "%s" from stream to vfs' % data.name) + vfs_list[data.name] = models.Ref.track( + uri=data.uri, name=data.name + ) + else: + logger.debug('Adding playlist "%s" from stream to vfs' % name) + vfs_list[set_id] = new_folder(name, ['sets', set_id]) + return vfs_list.values() + def list_user_follows(self): sets_vfs = collections.OrderedDict() for (name, user_id) in self.backend.remote.get_followings(): @@ -166,9 +180,7 @@ def browse(self, uri): return self.list_liked() # User stream if 'stream' == req_type: - return self.tracklist_to_vfs( - self.backend.remote.get_user_stream() - ) + return self.list_stream() # root directory return self.vfs.get(uri, {}).values() diff --git a/mopidy_soundcloud/soundcloud.py b/mopidy_soundcloud/soundcloud.py index d7c4b9d..77cb3bd 100644 --- a/mopidy_soundcloud/soundcloud.py +++ b/mopidy_soundcloud/soundcloud.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import collections import logging import re import string @@ -13,7 +12,6 @@ import requests - logger = logging.getLogger(__name__) @@ -71,6 +69,7 @@ def __init__(self, config): super(SoundCloudClient, self).__init__() token = config['auth_token'] self.explore_songs = config.get('explore_songs', 10) + self.stream_entries = config.get('stream_entries', 100) self.http_client = requests.Session() self.http_client.headers.update({'Authorization': 'OAuth %s' % token}) @@ -90,23 +89,24 @@ def user(self): @cache() def get_user_stream(self): - # User timeline like playlist which uses undocumented api - # https://api.soundcloud.com/e1/me/stream.json?offset=0 - # returns five elements per request - tracks = [] - for sid in xrange(0, 2): - stream = self._get('e1/me/stream.json?offset=%s' % sid * 5) - for data in stream.get('collection'): - kind = data.get('type') - # multiple types of track with same data - if 'track' in kind: - tracks.append(self.parse_track(data.get('track'))) - if kind == 'playlist': - playlist = data.get('playlist').get('tracks') - if isinstance(playlist, collections.Iterable): - tracks.extend(self.parse_results(playlist)) - - return self.sanitize_tracks(tracks) + # User timeline like playlist which uses undocumented api: + # https://api.soundcloud.com/e1/me/stream.json?limit=0 + # Additional parameters are: 'promoted_playlist=true' + # or 'offset=00000152-cc97-84a0-0000-00002a293ab5' + tracks_and_playlists = [] + stream = self._get('e1/me/stream.json?limit=%s' % self.stream_entries) + for data in stream.get('collection'): + kind = data.get('type') + # multiple types of track with same data + if 'track' in kind: + tracks_and_playlists.append( + self.parse_track(data.get('track'))) + if 'playlist' in kind: + playlist = data.get('playlist') + tracks_and_playlists.append( + (playlist.get('title'), str(playlist.get('id')))) + + return self.sanitize_tracks(tracks_and_playlists) @cache() def get_explore_categories(self): diff --git a/tests/fixtures/sc-stream.yaml b/tests/fixtures/sc-stream.yaml index 37d8503..38030a0 100644 --- a/tests/fixtures/sc-stream.yaml +++ b/tests/fixtures/sc-stream.yaml @@ -6,38 +6,9 @@ interactions: Accept-Encoding: ['gzip, deflate'] Authorization: [!!python/unicode 'OAuth 1-35204-61921957-55796ebef403996'] Connection: [keep-alive] - User-Agent: [python-requests/2.4.3 CPython/2.7.8 Linux/3.16.0-28-generic] + User-Agent: [python-requests/2.9.1] method: GET - uri: https://api.soundcloud.com:443/e1/me/stream.json?offset=0e1/me/stream.json?offset=0e1/me/stream.json?offset=0e1/me/stream.json?offset=0e1/me/stream.json?offset=0&client_id=93e33e327fd8a9b77becd179652272e2 - response: - body: - string: !!binary | - H4sIAAAAAAAAAATBwQqAIAwA0H/ZOVxehehDIkJsoqFOdDtF/957L0QVHXSlQREcJJE+HaLv2UzW - dofCepvAFcliJZwyyFfzTG57yTXLZldYIHApFCRzA3ec3w8AAP//AwCP9XBDVwAAAA== - headers: - access-control-allow-headers: ['Accept, Authorization, Content-Type, Origin'] - access-control-allow-methods: ['GET, PUT, POST, DELETE'] - access-control-allow-origin: ['*'] - access-control-expose-headers: [Date] - cache-control: ['private, max-age=0'] - content-encoding: [gzip] - content-length: ['106'] - content-type: [application/json] - date: ['Fri, 02 Jan 2015 17:03:54 GMT'] - server: [am/2] - set-cookie: [_session_auth_key="iuo9QNN6oaPVa3gj9XyKTHZtsxE="] - status: {code: 200, message: OK} -- request: - body: null - headers: - Accept: ['*/*'] - Accept-Encoding: ['gzip, deflate'] - Authorization: [!!python/unicode 'OAuth 1-35204-61921957-55796ebef403996'] - Connection: [keep-alive] - Cookie: [_session_auth_key="iuo9QNN6oaPVa3gj9XyKTHZtsxE="] - User-Agent: [python-requests/2.4.3 CPython/2.7.8 Linux/3.16.0-28-generic] - method: GET - uri: https://api.soundcloud.com:443/e1/me/stream.json?offset=1e1/me/stream.json?offset=1e1/me/stream.json?offset=1e1/me/stream.json?offset=1e1/me/stream.json?offset=1&client_id=93e33e327fd8a9b77becd179652272e2 + uri: https://api.soundcloud.com/e1/me/stream.json?limit=10&client_id=93e33e327fd8a9b77becd179652272e2 response: body: string: !!binary | @@ -52,7 +23,7 @@ interactions: content-encoding: [gzip] content-length: ['106'] content-type: [application/json] - date: ['Fri, 02 Jan 2015 17:03:54 GMT'] + date: ['Sat, 20 Feb 2016 19:14:11 GMT'] server: [am/2] status: {code: 200, message: OK} version: 1 diff --git a/tests/test_api.py b/tests/test_api.py index cc220b3..c095c6e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -18,6 +18,7 @@ def setUp(self): config = SoundCloudExtension().get_config_schema() config['auth_token'] = '1-35204-61921957-55796ebef403996' config['explore_songs'] = 10 + config['stream_entries'] = 10 # using this user http://maildrop.cc/inbox/mopidytestuser self.api = SoundCloudClient(config) @@ -75,8 +76,8 @@ def test_get_user_liked(self): @vcr.use_cassette('tests/fixtures/sc-stream.yaml') def test_get_user_stream(self): - tracks = self.api.get_user_stream() - self.assertIsInstance(tracks, list) + tracks_and_playlists = self.api.get_user_stream() + self.assertIsInstance(tracks_and_playlists, list) @vcr.use_cassette('tests/fixtures/sc-explore.yaml') def test_get_explore(self): diff --git a/tests/test_extension.py b/tests/test_extension.py index 225c51c..5ffd2c6 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -22,3 +22,4 @@ def test_get_config_schema(self): self.assertIn('auth_token', schema) self.assertIn('explore_songs', schema) + self.assertIn('stream_entries', schema)