a b/src/cdplugins/tidal/tidalapi/__init__.py
1
# -*- coding: utf-8 -*-
2
#
3
# Copyright (C) 2014 Thomas Amland
4
#
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License as published by
7
# the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Lesser General Public License for more details.
14
#
15
# You should have received a copy of the GNU Lesser General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18
from __future__ import unicode_literals
19
20
import datetime
21
import json
22
import logging
23
import requests
24
from collections import namedtuple
25
from .models import Artist, Album, Track, Playlist, SearchResult, Category, Role
26
try:
27
    from urlparse import urljoin
28
except ImportError:
29
    from urllib.parse import urljoin
30
31
32
log = logging.getLogger(__name__)
33
34
Api = namedtuple('API', ['location', 'token'])
35
36
37
class Quality(object):
38
    lossless = 'LOSSLESS'
39
    high = 'HIGH'
40
    low = 'LOW'
41
42
43
class Config(object):
44
    def __init__(self, quality=Quality.high):
45
        self.quality = quality
46
        self.api_location = 'https://api.tidalhifi.com/v1/'
47
        self.api_token = 'P5Xbeo5LFvESeDy6' if self.quality == \
48
            Quality.lossless else 'wdgaB1CilGA-S_s2',
49
50
51
class Session(object):
52
53
    def __init__(self, config=Config()):
54
        self.session_id = None
55
        self.country_code = None
56
        self.user = None
57
        self._config = config
58
        """:type _config: :class:`Config`"""
59
60
    def load_session(self, session_id, country_code, user_id):
61
        self.session_id = session_id
62
        self.country_code = country_code
63
        self.user = User(self, id=user_id)
64
65
    def login(self, username, password):
66
        url = urljoin(self._config.api_location, 'login/username')
67
        params = {'token': self._config.api_token}
68
        payload = {
69
            'username': username,
70
            'password': password,
71
        }
72
        r = requests.post(url, data=payload, params=params)
73
        r.raise_for_status()
74
        body = r.json()
75
        self.session_id = body['sessionId']
76
        self.country_code = body['countryCode']
77
        self.user = User(self, id=body['userId'])
78
        return True
79
80
    def check_login(self):
81
        """ Returns true if current session is valid, false otherwise. """
82
        if self.user is None or not self.user.id or not self.session_id:
83
            return False
84
        url = urljoin(self._config.api_location, 'users/%s/subscription' % self.user.id)
85
        return requests.get(url, params={'sessionId': self.session_id}).ok
86
87
    def request(self, method, path, params=None, data=None):
88
        request_params = {
89
            'sessionId': self.session_id,
90
            'countryCode': self.country_code,
91
            'limit': '999',
92
        }
93
        if params:
94
            request_params.update(params)
95
        url = urljoin(self._config.api_location, path)
96
        r = requests.request(method, url, params=request_params, data=data)
97
        log.debug("request: %s" % r.request.url)
98
        r.raise_for_status()
99
        if r.content:
100
            log.debug("response: %s" % json.dumps(r.json(), indent=4))
101
        return r
102
103
    def get_user(self, user_id):
104
        return self._map_request('users/%s' % user_id, ret='user')
105
106
    def get_user_playlists(self, user_id):
107
        return self._map_request('users/%s/playlists' % user_id, ret='playlists')
108
109
    def get_playlist(self, playlist_id):
110
        return self._map_request('playlists/%s' % playlist_id, ret='playlist')
111
112
    def get_playlist_tracks(self, playlist_id):
113
        return self._map_request('playlists/%s/tracks' % playlist_id, ret='tracks')
114
115
    def get_album(self, album_id):
116
        return self._map_request('albums/%s' % album_id, ret='album')
117
118
    def get_album_tracks(self, album_id):
119
        return self._map_request('albums/%s/tracks' % album_id, ret='tracks')
120
121
    def get_artist(self, artist_id):
122
        return self._map_request('artists/%s' % artist_id, ret='artist')
123
124
    def get_artist_albums(self, artist_id):
125
        return self._map_request('artists/%s/albums' % artist_id, ret='albums')
126
127
    def get_artist_albums_ep_singles(self, artist_id):
128
        params = {'filter': 'EPSANDSINGLES'}
129
        return self._map_request('artists/%s/albums' % artist_id, params, ret='albums')
130
131
    def get_artist_albums_other(self, artist_id):
132
        params = {'filter': 'COMPILATIONS'}
133
        return self._map_request('artists/%s/albums' % artist_id, params, ret='albums')
134
135
    def get_artist_top_tracks(self, artist_id):
136
        return self._map_request('artists/%s/toptracks' % artist_id, ret='tracks')
137
138
    def get_artist_bio(self, artist_id):
139
        return self.request('GET', 'artists/%s/bio' % artist_id).json()['text']
140
141
    def get_artist_similar(self, artist_id):
142
        return self._map_request('artists/%s/similar' % artist_id, ret='artists')
143
144
    def get_artist_radio(self, artist_id):
145
        return self._map_request('artists/%s/radio' % artist_id, params={'limit': 100}, ret='tracks')
146
147
    def get_featured(self):
148
        items = self.request('GET', 'promotions').json()['items']
149
        return [_parse_featured_playlist(item) for item in items if item['type'] == 'PLAYLIST']
150
151
    def get_featured_items(self, content_type, group):
152
        return self._map_request('/'.join(['featured', group, content_type]), ret=content_type)
153
154
    def get_moods(self):
155
        return map(_parse_moods, self.request('GET', 'moods').json())
156
157
    def get_mood_playlists(self, mood_id):
158
        return self._map_request('/'.join(['moods', mood_id, 'playlists']), ret='playlists')
159
160
    def get_genres(self):
161
        return map(_parse_genres, self.request('GET', 'genres').json())
162
163
    def get_genre_items(self, genre_id, content_type):
164
        return self._map_request('/'.join(['genres', genre_id, content_type]), ret=content_type)
165
166
    def get_track_radio(self, track_id):
167
        return self._map_request('tracks/%s/radio' % track_id, params={'limit': 100}, ret='tracks')
168
169
    def get_track(self, track_id):
170
        return self._map_request('tracks/%s' % track_id, ret='track')
171
172
    def _map_request(self, url, params=None, ret=None):
173
        json_obj = self.request('GET', url, params).json()
174
        parse = None
175
        if ret.startswith('artist'):
176
            parse = _parse_artist
177
        elif ret.startswith('album'):
178
            parse = _parse_album
179
        elif ret.startswith('track'):
180
            parse = _parse_track
181
        elif ret.startswith('user'):
182
            raise NotImplementedError()
183
        elif ret.startswith('playlist'):
184
            parse = _parse_playlist
185
186
        items = json_obj.get('items')
187
        if items is None:
188
            return parse(json_obj)
189
        elif len(items) > 0 and 'item' in items[0]:
190
            return list(map(parse, [item['item'] for item in items]))
191
        else:
192
            return list(map(parse, items))
193
194
    def get_media_url(self, track_id):
195
        params = {'soundQuality': self._config.quality}
196
        r = self.request('GET', 'tracks/%s/streamUrl' % track_id, params)
197
        return r.json()['url']
198
199
    def search(self, field, value):
200
        params = {
201
            'query': value,
202
            'limit': 50,
203
        }
204
        if field not in ['artist', 'album', 'playlist', 'track']:
205
            raise ValueError('Unknown field \'%s\'' % field)
206
207
        ret_type = field + 's'
208
        url = 'search/' + field + 's'
209
        result = self._map_request(url, params, ret=ret_type)
210
        return SearchResult(**{ret_type: result})
211
212
213
def _parse_artist(json_obj):
214
    return Artist(id=json_obj['id'], name=json_obj['name'], role=Role(json_obj['type']))
215
216
217
def _parse_artists(json_obj):
218
    return list(map(_parse_artist, json_obj))
219
220
221
def _parse_album(json_obj, artist=None, artists=None):
222
    if artist is None:
223
        artist = _parse_artist(json_obj['artist'])
224
    if artists is None:
225
        artists = _parse_artists(json_obj['artists'])
226
    kwargs = {
227
        'id': json_obj['id'],
228
        'name': json_obj['title'],
229
        'num_tracks': json_obj.get('numberOfTracks'),
230
        'duration': json_obj.get('duration'),
231
        'artist': artist,
232
        'artists': artists,
233
    }
234
    if 'releaseDate' in json_obj:
235
        try:
236
            kwargs['release_date'] = datetime.datetime(*map(int, json_obj['releaseDate'].split('-')))
237
        except ValueError:
238
            pass
239
    return Album(**kwargs)
240
241
242
def _parse_featured_playlist(json_obj):
243
    kwargs = {
244
        'id': json_obj['artifactId'],
245
        'name': json_obj['header'],
246
        'description': json_obj['text'],
247
    }
248
    return Playlist(**kwargs)
249
250
251
def _parse_playlist(json_obj):
252
    kwargs = {
253
        'id': json_obj['uuid'],
254
        'name': json_obj['title'],
255
        'description': json_obj['description'],
256
        'num_tracks': int(json_obj['numberOfTracks']),
257
        'duration': int(json_obj['duration']),
258
        'is_public': json_obj['publicPlaylist'],
259
        #TODO 'creator': _parse_user(json_obj['creator']),
260
    }
261
    return Playlist(**kwargs)
262
263
264
def _parse_track(json_obj):
265
    artist = _parse_artist(json_obj['artist'])
266
    artists = _parse_artists(json_obj['artists'])
267
    album = _parse_album(json_obj['album'], artist, artists)
268
    kwargs = {
269
        'id': json_obj['id'],
270
        'name': json_obj['title'],
271
        'duration': json_obj['duration'],
272
        'track_num': json_obj['trackNumber'],
273
        'disc_num': json_obj['volumeNumber'],
274
        'popularity': json_obj['popularity'],
275
        'artist': artist,
276
        'artists': artists,
277
        'album': album,
278
        'available': bool(json_obj['streamReady']),
279
    }
280
    return Track(**kwargs)
281
282
283
def _parse_genres(json_obj):
284
    image = "http://resources.wimpmusic.com/images/%s/460x306.jpg" \
285
            % json_obj['image'].replace('-', '/')
286
    return Category(id=json_obj['path'], name=json_obj['name'], image=image)
287
288
289
def _parse_moods(json_obj):
290
    image = "http://resources.wimpmusic.com/images/%s/342x342.jpg" \
291
            % json_obj['image'].replace('-', '/')
292
    return Category(id=json_obj['path'], name=json_obj['name'], image=image)
293
294
295
class Favorites(object):
296
297
    def __init__(self, session, user_id):
298
        self._session = session
299
        self._base_url = 'users/%s/favorites' % user_id
300
301
    def add_artist(self, artist_id):
302
        return self._session.request('POST', self._base_url + '/artists', data={'artistId': artist_id}).ok
303
304
    def add_album(self, album_id):
305
        return self._session.request('POST', self._base_url + '/albums', data={'albumId': album_id}).ok
306
307
    def add_track(self, track_id):
308
        return self._session.request('POST', self._base_url + '/tracks', data={'trackId': track_id}).ok
309
310
    def remove_artist(self, artist_id):
311
        return self._session.request('DELETE', self._base_url + '/artists/%s' % artist_id).ok
312
313
    def remove_album(self, album_id):
314
        return self._session.request('DELETE', self._base_url + '/albums/%s' % album_id).ok
315
316
    def remove_track(self, track_id):
317
        return self._session.request('DELETE', self._base_url + '/tracks/%s' % track_id).ok
318
319
    def artists(self):
320
        return self._session._map_request(self._base_url + '/artists', ret='artists')
321
322
    def albums(self):
323
        return self._session._map_request(self._base_url + '/albums', ret='albums')
324
325
    def playlists(self):
326
        return self._session._map_request(self._base_url + '/playlists', ret='playlists')
327
328
    def tracks(self):
329
        r = self._session.request('GET', self._base_url + '/tracks')
330
        return [_parse_track(item['item']) for item in r.json()['items']]
331
332
333
class User(object):
334
335
    favorites = None
336
337
    def __init__(self, session, id):
338
        """
339
        :type session: :class:`Session`
340
        :param id: The user ID
341
        """
342
        self._session = session
343
        self.id = id
344
        self.favorites = Favorites(session, self.id)
345
346
    def playlists(self):
347
        return self._session.get_user_playlists(self.id)