diff options
author | Skybound1 <mgupta94@hotmail.co.uk> | 2018-12-10 19:10:49 +0100 |
---|---|---|
committer | Mike Crute <crutem@amazon.com> | 2018-12-10 10:10:49 -0800 |
commit | 9e49c086a86deff4ae85d802e9d57278f1ac4636 (patch) | |
tree | 1b68ce4c19b7dcd6281118acd702b920bc59417d | |
parent | d74a05605401926b6b6fc694753c7de19f67c419 (diff) | |
download | pydora-9e49c086a86deff4ae85d802e9d57278f1ac4636.tar.bz2 pydora-9e49c086a86deff4ae85d802e9d57278f1ac4636.tar.xz pydora-9e49c086a86deff4ae85d802e9d57278f1ac4636.zip |
Enhancement: Adds additional audio urls (#58)
* Adds support for additionalAudioUrl for station.getPlaylist
* Fixes broken tests
* Reworks use of iterables for additional audio urls and adds associated tests
* Moves parsing additional url response into a syntethic field
* Adds tests for additional urls field
-rw-r--r-- | pandora/client.py | 27 | ||||
-rw-r--r-- | pandora/models/pandora.py | 40 | ||||
-rw-r--r-- | tests/test_pandora/test_client.py | 68 | ||||
-rw-r--r-- | tests/test_pandora/test_models.py | 33 |
4 files changed, 158 insertions, 10 deletions
diff --git a/pandora/client.py b/pandora/client.py index 01b7654..df09144 100644 --- a/pandora/client.py +++ b/pandora/client.py | |||
@@ -127,15 +127,28 @@ class APIClient(BaseAPIClient): | |||
127 | def get_station_list_checksum(self): # pragma: no cover | 127 | def get_station_list_checksum(self): # pragma: no cover |
128 | return self("user.getStationListChecksum")["checksum"] | 128 | return self("user.getStationListChecksum")["checksum"] |
129 | 129 | ||
130 | def get_playlist(self, station_token): | 130 | def get_playlist(self, station_token, additional_urls=None): |
131 | from .models.pandora import Playlist | 131 | from .models.pandora import Playlist |
132 | 132 | ||
133 | playlist = Playlist.from_json(self, | 133 | if additional_urls is None: |
134 | self("station.getPlaylist", | 134 | additional_urls = [] |
135 | stationToken=station_token, | 135 | |
136 | includeTrackLength=True, | 136 | if isinstance(additional_urls, str): |
137 | xplatformAdCapable=True, | 137 | raise TypeError('Additional urls should be a list') |
138 | audioAdPodCapable=True)) | 138 | |
139 | urls = [getattr(url, "value", url) for url in additional_urls] | ||
140 | |||
141 | resp = self("station.getPlaylist", | ||
142 | stationToken=station_token, | ||
143 | includeTrackLength=True, | ||
144 | xplatformAdCapable=True, | ||
145 | audioAdPodCapable=True, | ||
146 | additionalAudioUrl=','.join(urls)) | ||
147 | |||
148 | for item in resp['items']: | ||
149 | item['_paramAdditionalUrls'] = additional_urls | ||
150 | |||
151 | playlist = Playlist.from_json(self, resp) | ||
139 | 152 | ||
140 | for i, track in enumerate(playlist): | 153 | for i, track in enumerate(playlist): |
141 | if track.is_ad: | 154 | if track.is_ad: |
diff --git a/pandora/models/pandora.py b/pandora/models/pandora.py index cc57eac..ecc1225 100644 --- a/pandora/models/pandora.py +++ b/pandora/models/pandora.py | |||
@@ -6,6 +6,18 @@ from . import Field, DateField, SyntheticField | |||
6 | from . import PandoraModel, PandoraListModel, PandoraDictListModel | 6 | from . import PandoraModel, PandoraListModel, PandoraDictListModel |
7 | 7 | ||
8 | 8 | ||
9 | class AdditionalAudioUrl(Enum): | ||
10 | HTTP_40_AAC_MONO = 'HTTP_40_AAC_MONO' | ||
11 | HTTP_64_AAC = 'HTTP_64_AAC' | ||
12 | HTTP_32_AACPLUS = 'HTTP_32_AACPLUS' | ||
13 | HTTP_64_AACPLUS = 'HTTP_64_AACPLUS' | ||
14 | HTTP_24_AACPLUS_ADTS = 'HTTP_24_AACPLUS_ADTS' | ||
15 | HTTP_32_AACPLUS_ADTS = 'HTTP_32_AACPLUS_ADTS' | ||
16 | HTTP_64_AACPLUS_ADTS = 'HTTP_64_AACPLUS_ADTS' | ||
17 | HTTP_128_MP3 = 'HTTP_128_MP3' | ||
18 | HTTP_32_WMA = 'HTTP_32_WMA' | ||
19 | |||
20 | |||
9 | class PandoraType(Enum): | 21 | class PandoraType(Enum): |
10 | 22 | ||
11 | TRACK = "TR" | 23 | TRACK = "TR" |
@@ -100,8 +112,9 @@ class Station(PandoraModel): | |||
100 | seeds = Field("music", model=StationSeeds) | 112 | seeds = Field("music", model=StationSeeds) |
101 | feedback = Field("feedback", model=StationFeedback) | 113 | feedback = Field("feedback", model=StationFeedback) |
102 | 114 | ||
103 | def get_playlist(self): | 115 | def get_playlist(self, additional_urls=None): |
104 | return iter(self._api_client.get_playlist(self.token)) | 116 | return iter(self._api_client.get_playlist(self.token, |
117 | additional_urls)) | ||
105 | 118 | ||
106 | 119 | ||
107 | class GenreStation(PandoraModel): | 120 | class GenreStation(PandoraModel): |
@@ -178,6 +191,27 @@ class AudioField(SyntheticField): | |||
178 | return audio_url[self.field] if audio_url else None | 191 | return audio_url[self.field] if audio_url else None |
179 | 192 | ||
180 | 193 | ||
194 | class AdditionalUrlField(SyntheticField): | ||
195 | |||
196 | def formatter(self, api_client, data, value): | ||
197 | """Parse additional url fields and map them to inputs | ||
198 | |||
199 | Attempt to create a dictionary with keys being user input, and | ||
200 | response being the returned URL | ||
201 | """ | ||
202 | if value is None: | ||
203 | return None | ||
204 | |||
205 | user_param = data['_paramAdditionalUrls'] | ||
206 | urls = {} | ||
207 | if isinstance(value, str): | ||
208 | urls[user_param[0]] = value | ||
209 | else: | ||
210 | for key, url in zip(user_param, value): | ||
211 | urls[key] = url | ||
212 | return urls | ||
213 | |||
214 | |||
181 | class PlaylistModel(PandoraModel): | 215 | class PlaylistModel(PandoraModel): |
182 | 216 | ||
183 | def get_is_playable(self): | 217 | def get_is_playable(self): |
@@ -243,6 +277,8 @@ class PlaylistItem(PlaylistModel): | |||
243 | song_detail_url = Field("songDetailUrl") | 277 | song_detail_url = Field("songDetailUrl") |
244 | song_explore_url = Field("songExplorerUrl") | 278 | song_explore_url = Field("songExplorerUrl") |
245 | 279 | ||
280 | additional_audio_urls = AdditionalUrlField("additionalAudioUrl") | ||
281 | |||
246 | @property | 282 | @property |
247 | def is_ad(self): | 283 | def is_ad(self): |
248 | return self.ad_token is not None | 284 | return self.ad_token is not None |
diff --git a/tests/test_pandora/test_client.py b/tests/test_pandora/test_client.py index 6c61355..d24ea28 100644 --- a/tests/test_pandora/test_client.py +++ b/tests/test_pandora/test_client.py | |||
@@ -1,7 +1,7 @@ | |||
1 | from unittest import TestCase | 1 | from unittest import TestCase |
2 | 2 | ||
3 | from pandora import errors | 3 | from pandora import errors |
4 | from pandora.models.pandora import AdItem | 4 | from pandora.models.pandora import AdItem, AdditionalAudioUrl |
5 | from pandora.client import APIClient, BaseAPIClient | 5 | from pandora.client import APIClient, BaseAPIClient |
6 | from pandora.py2compat import Mock, call, patch | 6 | from pandora.py2compat import Mock, call, patch |
7 | from tests.test_pandora.test_models import TestAdItem | 7 | from tests.test_pandora.test_models import TestAdItem |
@@ -87,6 +87,7 @@ class TestCallingAPIClient(TestCase): | |||
87 | client.get_playlist('token_mock') | 87 | client.get_playlist('token_mock') |
88 | 88 | ||
89 | playlist_mock.assert_has_calls([call("station.getPlaylist", | 89 | playlist_mock.assert_has_calls([call("station.getPlaylist", |
90 | additionalAudioUrl='', | ||
90 | audioAdPodCapable=True, | 91 | audioAdPodCapable=True, |
91 | includeTrackLength=True, | 92 | includeTrackLength=True, |
92 | stationToken='token_mock', | 93 | stationToken='token_mock', |
@@ -186,3 +187,68 @@ class TestCreatingGenreStation(TestCase): | |||
186 | client = APIClient(Mock(), None, None, None, None) | 187 | client = APIClient(Mock(), None, None, None, None) |
187 | station = client.get_genre_stations() | 188 | station = client.get_genre_stations() |
188 | self.assertEqual(station.checksum, "foo") | 189 | self.assertEqual(station.checksum, "foo") |
190 | |||
191 | |||
192 | class TestAdditionalUrls(TestCase): | ||
193 | |||
194 | def test_non_iterable_string(self): | ||
195 | with self.assertRaises(TypeError): | ||
196 | transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) | ||
197 | |||
198 | client = APIClient(transport, None, None, None, None) | ||
199 | client._authenticate = Mock() | ||
200 | |||
201 | client.get_playlist('token_mock', additional_urls='') | ||
202 | |||
203 | def test_non_iterable_other(self): | ||
204 | with self.assertRaises(TypeError): | ||
205 | transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) | ||
206 | |||
207 | client = APIClient(transport, None, None, None, None) | ||
208 | client._authenticate = Mock() | ||
209 | |||
210 | client.get_playlist('token_mock', | ||
211 | additional_urls=AdditionalAudioUrl.HTTP_32_WMA) | ||
212 | |||
213 | def test_without_enum(self): | ||
214 | with patch.object(APIClient, '__call__') as playlist_mock: | ||
215 | transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) | ||
216 | |||
217 | client = APIClient(transport, None, None, None, None) | ||
218 | client._authenticate = Mock() | ||
219 | |||
220 | urls = ['HTTP_128_MP3', | ||
221 | 'HTTP_24_AACPLUS_ADTS'] | ||
222 | |||
223 | desired = 'HTTP_128_MP3,HTTP_24_AACPLUS_ADTS' | ||
224 | |||
225 | client.get_playlist('token_mock', additional_urls=urls) | ||
226 | |||
227 | playlist_mock.assert_has_calls([call("station.getPlaylist", | ||
228 | additionalAudioUrl=desired, | ||
229 | audioAdPodCapable=True, | ||
230 | includeTrackLength=True, | ||
231 | stationToken='token_mock', | ||
232 | xplatformAdCapable=True)]) | ||
233 | |||
234 | |||
235 | def test_with_enum(self): | ||
236 | with patch.object(APIClient, '__call__') as playlist_mock: | ||
237 | transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) | ||
238 | |||
239 | client = APIClient(transport, None, None, None, None) | ||
240 | client._authenticate = Mock() | ||
241 | |||
242 | urls = [AdditionalAudioUrl.HTTP_128_MP3, | ||
243 | AdditionalAudioUrl.HTTP_24_AACPLUS_ADTS] | ||
244 | |||
245 | desired = 'HTTP_128_MP3,HTTP_24_AACPLUS_ADTS' | ||
246 | |||
247 | client.get_playlist('token_mock', additional_urls=urls) | ||
248 | |||
249 | playlist_mock.assert_has_calls([call("station.getPlaylist", | ||
250 | additionalAudioUrl=desired, | ||
251 | audioAdPodCapable=True, | ||
252 | includeTrackLength=True, | ||
253 | stationToken='token_mock', | ||
254 | xplatformAdCapable=True)]) | ||
diff --git a/tests/test_pandora/test_models.py b/tests/test_pandora/test_models.py index ce71ef8..410b0de 100644 --- a/tests/test_pandora/test_models.py +++ b/tests/test_pandora/test_models.py | |||
@@ -52,6 +52,39 @@ class TestDateField(TestCase): | |||
52 | self.assertEqual(expected, model.date_field.replace(microsecond=0)) | 52 | self.assertEqual(expected, model.date_field.replace(microsecond=0)) |
53 | 53 | ||
54 | 54 | ||
55 | class TestAdditionalUrlField(TestCase): | ||
56 | |||
57 | def test_single_url(self): | ||
58 | dummy_data = { | ||
59 | '_paramAdditionalUrls': ['foo'] | ||
60 | } | ||
61 | |||
62 | field = pm.AdditionalUrlField("additionalAudioUrl") | ||
63 | |||
64 | ret = field.formatter(None, dummy_data, 'test') | ||
65 | |||
66 | self.assertEqual(ret, {'foo': 'test'}) | ||
67 | |||
68 | def test_multiple_urls(self): | ||
69 | dummy_data = { | ||
70 | '_paramAdditionalUrls': [ | ||
71 | 'abc', | ||
72 | 'def', | ||
73 | ] | ||
74 | } | ||
75 | |||
76 | field = pm.AdditionalUrlField("additionalAudioUrl") | ||
77 | |||
78 | ret = field.formatter(None, dummy_data, ['foo', 'bar']) | ||
79 | |||
80 | expected = { | ||
81 | 'abc': 'foo', | ||
82 | 'def': 'bar', | ||
83 | } | ||
84 | |||
85 | self.assertEqual(ret, expected) | ||
86 | |||
87 | |||
55 | class TestPandoraModel(TestCase): | 88 | class TestPandoraModel(TestCase): |
56 | 89 | ||
57 | JSON_DATA = { | 90 | JSON_DATA = { |