Skip to content

Commit 795b85a

Browse files
committed
Add the ability to list videos and get urls. Resolves #15
Also adds tests for videos and urls. Adds a new config option to set video quality.
1 parent 7b5a349 commit 795b85a

4 files changed

Lines changed: 107 additions & 13 deletions

File tree

tests/test_api.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ def test_get_artist_albums_other(session):
5050
albums = session.get_artist_albums_other(16147)
5151
assert any([a.name == 'Dance History 1.0' for a in albums])
5252

53+
def test_get_artist_videos(session):
54+
videos = session.get_artist_videos(3502112)
55+
assert any([v.name == 'Call on Me' for v in videos])
56+
5357
def test_album(session):
5458
album_id = 17927863
5559
album = session.get_album(album_id)
@@ -66,11 +70,35 @@ def test_get_album_tracks(session):
6670
assert tracks[0].name == 'Take-Off'
6771
assert tracks[0].track_num == 1
6872
assert tracks[0].duration == 56
73+
assert tracks[0].artist.name == 'Lasgo'
74+
assert tracks[0].album.name == 'Smile'
6975
assert tracks[-1].name == 'Gone'
7076
assert tracks[-1].track_num == 13
7177
assert tracks[-1].duration == 210
72-
assert tracks[0].artist.name == 'Lasgo'
73-
assert tracks[0].album.name == 'Smile'
78+
79+
80+
def test_get_album_videos(session):
81+
videos = session.get_album_videos(108046179)
82+
assert videos[0].name == 'Formation (Choreography Version)'
83+
assert videos[0].track_num == 14
84+
assert videos[0].duration == 262
85+
assert videos[0].artist.name == 'Beyoncé'
86+
assert videos[0].album.name == 'Lemonade'
87+
assert videos[1].name == 'Lemonade Film'
88+
assert videos[1].track_num == 15
89+
assert videos[1].duration == 3955
90+
91+
def test_get_album_items(session):
92+
items = session.get_album_items(108046179)
93+
assert items[0].name == 'Pray You Catch Me'
94+
assert items[0].track_num == 1
95+
assert items[0].duration == 196
96+
assert items[0].artist.name == 'Beyoncé'
97+
assert items[0].album.name == 'Lemonade'
98+
assert items[-1].name == 'Lemonade Film'
99+
assert items[-1].track_num == 15
100+
assert items[-1].duration == 3955
101+
assert items[-1].type == 'Music Video'
74102

75103
def test_artist_radio(session):
76104
tracks = session.get_artist_radio(16147)
@@ -91,3 +119,11 @@ def test_album_image(session):
91119
def test_playlist_image(session):
92120
playlist = session.get_playlist('33136f5a-d93a-4469-9353-8365897aaf94')
93121
assert requests.get(playlist.image).status_code == 200
122+
123+
def test_get_track_url(session):
124+
track = session.get_track(108043415)
125+
newurl = session.get_track_url(track.id)
126+
127+
def test_get_video_url(session):
128+
video = session.get_video(108046194)
129+
newurl = session.get_video_url(video.id)

tidalapi/__init__.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import logging
2323
import requests
2424
from collections import namedtuple
25-
from .models import Artist, Album, Track, Playlist, SearchResult, Category, Role
25+
from .models import Artist, Album, Track, Video, Playlist, SearchResult, Category, Role
2626
try:
2727
from urlparse import urljoin
2828
except ImportError:
@@ -39,10 +39,15 @@ class Quality(object):
3939
high = 'HIGH'
4040
low = 'LOW'
4141

42+
class videoQuality(object):
43+
high = 'HIGH'
44+
medium = 'MEDIUM'
45+
low = 'LOW'
4246

4347
class Config(object):
44-
def __init__(self, quality=Quality.high):
48+
def __init__(self, quality=Quality.high, videoQuality=videoQuality.high):
4549
self.quality = quality
50+
self.videoQuality = videoQuality
4651
self.api_location = 'https://api.tidalhifi.com/v1/'
4752
self.api_token = 'BI218mwp9ERZ3PFI' if self.quality == \
4853
Quality.lossless else '4zx46pyr9o8qZNRw',
@@ -112,12 +117,25 @@ def get_playlist(self, playlist_id):
112117
def get_playlist_tracks(self, playlist_id):
113118
return self._map_request('playlists/%s/tracks' % playlist_id, ret='tracks')
114119

120+
def get_playlist_videos(self, playlist_id):
121+
return self._map_request('playlists/%s/items' % playlist_id, ret='video')
122+
123+
def get_playlist_items(self, playlist_id):
124+
return self._get_items('playlists/%s/items' % playlist_id, ret='items')
125+
115126
def get_album(self, album_id):
116127
return self._map_request('albums/%s' % album_id, ret='album')
117128

118129
def get_album_tracks(self, album_id):
119130
return self._map_request('albums/%s/tracks' % album_id, ret='tracks')
120131

132+
def get_album_videos(self, album_id):
133+
items = self._get_items('albums/%s/items' % album_id, ret='videos')
134+
return [item for item in items if isinstance(item, Video)]
135+
136+
def get_album_items(self, album_id):
137+
return self._get_items('albums/%s/items' % album_id, ret='items')
138+
121139
def get_artist(self, artist_id):
122140
return self._map_request('artists/%s' % artist_id, ret='artist')
123141

@@ -135,6 +153,9 @@ def get_artist_albums_other(self, artist_id):
135153
def get_artist_top_tracks(self, artist_id):
136154
return self._map_request('artists/%s/toptracks' % artist_id, ret='tracks')
137155

156+
def get_artist_videos(self, artist_id):
157+
return self._map_request('artists/%s/videos' % artist_id, ret='videos')
158+
138159
def get_artist_bio(self, artist_id):
139160
return self.request('GET', 'artists/%s/bio' % artist_id).json()['text']
140161

@@ -169,6 +190,9 @@ def get_track_radio(self, track_id):
169190
def get_track(self, track_id):
170191
return self._map_request('tracks/%s' % track_id, ret='track')
171192

193+
def get_video(self, video_id):
194+
return self._map_request('videos/%s' % video_id, ret = 'video')
195+
172196
def _map_request(self, url, params=None, ret=None):
173197
json_obj = self.request('GET', url, params).json()
174198
parse = None
@@ -177,9 +201,13 @@ def _map_request(self, url, params=None, ret=None):
177201
elif ret.startswith('album'):
178202
parse = _parse_album
179203
elif ret.startswith('track'):
180-
parse = _parse_track
204+
parse = _parse_media
181205
elif ret.startswith('user'):
182206
raise NotImplementedError()
207+
elif ret.startswith('video'):
208+
parse = _parse_media
209+
elif ret.startswith('item'):
210+
parse = _parse_media
183211
elif ret.startswith('playlist'):
184212
parse = _parse_playlist
185213

@@ -191,11 +219,31 @@ def _map_request(self, url, params=None, ret=None):
191219
else:
192220
return list(map(parse, items))
193221

222+
def _get_items(self, url, ret=None):
223+
remaining = 100
224+
offset = 0
225+
while remaining is 100:
226+
items = self._map_request(url, params={'offset': offset, 'limit': 100}, ret=ret)
227+
remaining = len(items)
228+
return items
229+
194230
def get_media_url(self, track_id):
195231
params = {'soundQuality': self._config.quality}
196232
r = self.request('GET', 'tracks/%s/streamUrl' % track_id, params)
197233
return r.json()['url']
198234

235+
def get_track_url(self, track_id):
236+
self.get_media_url(track_id)
237+
238+
def get_video_url(self, video_id):
239+
params = {
240+
'urlusagemode': 'STREAM',
241+
'videoquality': self._config.videoQuality,
242+
'assetpresentation': 'FULL'
243+
}
244+
r = self.request('GET', 'videos/%s/urlpostpaywall' % video_id, params)
245+
return r.json()['urls'][0]
246+
199247
def search(self, field, value):
200248
params = {
201249
'query': value,
@@ -265,11 +313,13 @@ def _parse_playlist(json_obj):
265313
}
266314
return Playlist(**kwargs)
267315

268-
269-
def _parse_track(json_obj):
316+
def _parse_media(json_obj):
270317
artist = _parse_artist(json_obj['artist'])
271318
artists = _parse_artists(json_obj['artists'])
272-
album = _parse_album(json_obj['album'], artist, artists)
319+
album = None
320+
if (json_obj['album']):
321+
album = _parse_album(json_obj['album'], artist, artists)
322+
273323
kwargs = {
274324
'id': json_obj['id'],
275325
'name': json_obj['title'],
@@ -281,9 +331,13 @@ def _parse_track(json_obj):
281331
'artists': artists,
282332
'album': album,
283333
'available': bool(json_obj['streamReady']),
334+
'type': json_obj.get('type'),
284335
}
285-
return Track(**kwargs)
286336

337+
if kwargs['type'] == 'Music Video':
338+
return Video(**kwargs)
339+
else:
340+
return Track(**kwargs)
287341

288342
def _parse_genres(json_obj):
289343
image = "http://resources.wimpmusic.com/images/%s/460x306.jpg" \
@@ -332,7 +386,7 @@ def playlists(self):
332386

333387
def tracks(self):
334388
r = self._session.request('GET', self._base_url + '/tracks')
335-
return [_parse_track(item['item']) for item in r.json()['items']]
389+
return [_parse_media(item['item']) for item in r.json()['items']]
336390

337391

338392
class User(object):

tidalapi/models.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ class Playlist(Model):
6565
def image(self, width=512, height=512):
6666
return IMG_URL.format(width=width, height=height, id=self.id, id_type='uuid')
6767

68-
69-
class Track(Model):
68+
class Media(Model):
7069
duration = -1
7170
track_num = -1
7271
disc_num = 1
@@ -76,6 +75,11 @@ class Track(Model):
7675
album = None
7776
available = True
7877

78+
class Track(Media):
79+
pass
80+
81+
class Video(Media):
82+
type = None
7983

8084
class SearchResult(Model):
8185
artists = []

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ envlist = py27,py34,py36,py37,pypy,pypy3
44
[testenv]
55
passenv = TIDAL_PASSWORD TIDAL_USERNAME
66
deps = pytest
7-
commands = pytest tests
7+
commands = pytest {posargs}

0 commit comments

Comments
 (0)