diff options
author | Mike Crute <mcrute@gmail.com> | 2016-01-09 13:30:33 -0800 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2016-01-09 13:30:33 -0800 |
commit | 0705462ee5644d57d825e37a295a5f698247aba0 (patch) | |
tree | 3489d4e608e7747807ab78e8dab0ffbc0b8aa581 | |
parent | eb91a5c00701689085b6f427d62e487f417c6f51 (diff) | |
parent | 4d05e233de4cbb77f5998ad86be7f921d521ff37 (diff) | |
download | pydora-0705462ee5644d57d825e37a295a5f698247aba0.tar.bz2 pydora-0705462ee5644d57d825e37a295a5f698247aba0.tar.xz pydora-0705462ee5644d57d825e37a295a5f698247aba0.zip |
Merge pull request #40 from jcass77/develop
Resolving ad playback issues
-rw-r--r-- | pandora/client.py | 20 | ||||
-rw-r--r-- | pandora/models/pandora.py | 27 | ||||
-rw-r--r-- | pandora/transport.py | 6 | ||||
-rw-r--r-- | pydora/utils.py | 9 | ||||
-rw-r--r-- | tests/test_pandora/test_client.py | 15 | ||||
-rw-r--r-- | tests/test_pandora/test_models.py | 46 | ||||
-rw-r--r-- | tests/test_pandora/test_transport.py | 2 | ||||
-rw-r--r-- | tests/test_pydora/test_utils.py | 14 |
8 files changed, 94 insertions, 45 deletions
diff --git a/pandora/client.py b/pandora/client.py index fafd872..403216e 100644 --- a/pandora/client.py +++ b/pandora/client.py | |||
@@ -219,9 +219,10 @@ class APIClient(BaseAPIClient): | |||
219 | from .models.pandora import GenreStationList | 219 | from .models.pandora import GenreStationList |
220 | 220 | ||
221 | genres = self("station.getGenreStations") | 221 | genres = self("station.getGenreStations") |
222 | genres["checksum"] = self.get_genre_stations_checksum() | ||
223 | 222 | ||
224 | return GenreStationList.from_json(self, genres) | 223 | genre_stations = GenreStationList.from_json(self, genres) |
224 | genre_stations.checksum = self.get_genre_stations_checksum() | ||
225 | return genre_stations | ||
225 | 226 | ||
226 | def get_genre_stations_checksum(self): | 227 | def get_genre_stations_checksum(self): |
227 | return self("station.getGenreStationsChecksum")["checksum"] | 228 | return self("station.getGenreStationsChecksum")["checksum"] |
@@ -262,13 +263,14 @@ class APIClient(BaseAPIClient): | |||
262 | from .models.pandora import AdItem | 263 | from .models.pandora import AdItem |
263 | 264 | ||
264 | if not station_id: | 265 | if not station_id: |
265 | raise ValueError("The 'station_id' param must be defined, " | 266 | raise errors.ParameterMissing("The 'station_id' param must be " |
266 | "got: '{}'".format(station_id)) | 267 | "defined, got: '{}'" |
267 | 268 | .format(station_id)) | |
268 | ad_metadata = self.get_ad_metadata(ad_token) | 269 | |
269 | ad_metadata["stationId"] = station_id | 270 | ad_item = AdItem.from_json(self, self.get_ad_metadata(ad_token)) |
270 | 271 | ad_item.station_id = station_id | |
271 | return AdItem.from_json(self, ad_metadata) | 272 | ad_item.ad_token = ad_token |
273 | return ad_item | ||
272 | 274 | ||
273 | def get_ad_metadata(self, ad_token): | 275 | def get_ad_metadata(self, ad_token): |
274 | return self("ad.getAdMetadata", | 276 | return self("ad.getAdMetadata", |
diff --git a/pandora/models/pandora.py b/pandora/models/pandora.py index 40b368a..7c134e1 100644 --- a/pandora/models/pandora.py +++ b/pandora/models/pandora.py | |||
@@ -1,6 +1,7 @@ | |||
1 | from .. import BaseAPIClient | 1 | from .. import BaseAPIClient |
2 | from . import with_metaclass, ModelMetaClass | 2 | from . import with_metaclass, ModelMetaClass |
3 | from . import Field, PandoraModel, PandoraListModel, PandoraDictListModel | 3 | from . import Field, PandoraModel, PandoraListModel, PandoraDictListModel |
4 | from ..errors import ParameterMissing | ||
4 | 5 | ||
5 | 6 | ||
6 | class Station(PandoraModel): | 7 | class Station(PandoraModel): |
@@ -130,6 +131,8 @@ class PlaylistModel(PandoraModel): | |||
130 | return cls.get_audio_field(data, "bitrate", preferred_quality) | 131 | return cls.get_audio_field(data, "bitrate", preferred_quality) |
131 | 132 | ||
132 | def get_is_playable(self): | 133 | def get_is_playable(self): |
134 | if not self.audio_url: | ||
135 | return False | ||
133 | return self._api_client.transport.test_url(self.audio_url) | 136 | return self._api_client.transport.test_url(self.audio_url) |
134 | 137 | ||
135 | def prepare_playback(self): | 138 | def prepare_playback(self): |
@@ -217,17 +220,33 @@ class AdItem(PlaylistModel): | |||
217 | audio_url = Field("audioUrl") | 220 | audio_url = Field("audioUrl") |
218 | image_url = Field("imageUrl") | 221 | image_url = Field("imageUrl") |
219 | click_through_url = Field("clickThroughUrl") | 222 | click_through_url = Field("clickThroughUrl") |
220 | station_id = Field("stationId") | 223 | station_id = None |
224 | ad_token = None | ||
221 | 225 | ||
222 | @property | 226 | @property |
223 | def is_ad(self): | 227 | def is_ad(self): |
224 | return True | 228 | return True |
225 | 229 | ||
226 | def register_ad(self, station_id): | 230 | def register_ad(self, station_id=None): |
227 | self._api_client.register_ad(station_id, self.tracking_tokens) | 231 | if not station_id: |
232 | station_id = self.station_id | ||
233 | if self.tracking_tokens: | ||
234 | self._api_client.register_ad(station_id, self.tracking_tokens) | ||
235 | else: | ||
236 | raise ParameterMissing('No ad tracking tokens provided for ' | ||
237 | 'registration.') | ||
228 | 238 | ||
229 | def prepare_playback(self): | 239 | def prepare_playback(self): |
230 | self.register_ad(self.station_id) | 240 | try: |
241 | self.register_ad(self.station_id) | ||
242 | except ParameterMissing as e: | ||
243 | if not self.tracking_tokens: | ||
244 | # Ignore registration attempts if no ad tracking tokens are | ||
245 | # available | ||
246 | pass | ||
247 | else: | ||
248 | # Unexpected error, re-raise. | ||
249 | raise e | ||
231 | return super(AdItem, self).prepare_playback() | 250 | return super(AdItem, self).prepare_playback() |
232 | 251 | ||
233 | 252 | ||
diff --git a/pandora/transport.py b/pandora/transport.py index 1a2ee69..8fab7af 100644 --- a/pandora/transport.py +++ b/pandora/transport.py | |||
@@ -46,7 +46,11 @@ def retries(max_tries, exceptions=(Exception,)): | |||
46 | retries_left -= 1 | 46 | retries_left -= 1 |
47 | return func(*args, **kwargs) | 47 | return func(*args, **kwargs) |
48 | 48 | ||
49 | except exceptions: | 49 | except exceptions as e: |
50 | # Don't retry for PandoraExceptions - unlikely that result | ||
51 | # will change for same set of input parameters. | ||
52 | if isinstance(e, PandoraException): | ||
53 | continue | ||
50 | if retries_left > 0: | 54 | if retries_left > 0: |
51 | time.sleep(delay_exponential( | 55 | time.sleep(delay_exponential( |
52 | 0.5, 2, max_tries - retries_left)) | 56 | 0.5, 2, max_tries - retries_left)) |
diff --git a/pydora/utils.py b/pydora/utils.py index 756810a..275c386 100644 --- a/pydora/utils.py +++ b/pydora/utils.py | |||
@@ -125,15 +125,6 @@ def iterate_forever(func, *args, **kwargs): | |||
125 | yield playlistItem | 125 | yield playlistItem |
126 | except StopIteration: | 126 | except StopIteration: |
127 | output = func(*args, **kwargs) | 127 | output = func(*args, **kwargs) |
128 | except errors.ParameterMissing as e: | ||
129 | if isinstance(playlistItem, AdItem): | ||
130 | if (not playlistItem.tracking_tokens or | ||
131 | len(playlistItem.tracking_tokens) == 0): | ||
132 | # Ad item does not contain any tracking tokens, yield | ||
133 | # without registering | ||
134 | yield playlistItem | ||
135 | # Something else went wrong, re-raise | ||
136 | raise e | ||
137 | 128 | ||
138 | 129 | ||
139 | class SilentPopen(subprocess.Popen): | 130 | class SilentPopen(subprocess.Popen): |
diff --git a/tests/test_pandora/test_client.py b/tests/test_pandora/test_client.py index 24d9734..cf486e0 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.client import APIClient, BaseAPIClient | 3 | from pandora.client import APIClient, BaseAPIClient |
4 | from pandora.errors import InvalidAuthToken | 4 | from pandora.errors import InvalidAuthToken, ParameterMissing |
5 | from pandora.py2compat import Mock, call, patch | 5 | from pandora.py2compat import Mock, call, patch |
6 | from tests.test_pandora.test_models import TestAdItem | 6 | from tests.test_pandora.test_models import TestAdItem |
7 | 7 | ||
@@ -64,12 +64,12 @@ class TestCallingAPIClient(TestCase): | |||
64 | client = APIClient(transport, None, None, None, None) | 64 | client = APIClient(transport, None, None, None, None) |
65 | client._authenticate = Mock() | 65 | client._authenticate = Mock() |
66 | 66 | ||
67 | client.get_playlist('mock_token') | 67 | client.get_playlist('token_mock') |
68 | 68 | ||
69 | playlist_mock.assert_has_calls([call("station.getPlaylist", | 69 | playlist_mock.assert_has_calls([call("station.getPlaylist", |
70 | audioAdPodCapable=True, | 70 | audioAdPodCapable=True, |
71 | includeTrackLength=True, | 71 | includeTrackLength=True, |
72 | stationToken='mock_token', | 72 | stationToken='token_mock', |
73 | xplatformAdCapable=True)]) | 73 | xplatformAdCapable=True)]) |
74 | 74 | ||
75 | 75 | ||
@@ -103,11 +103,12 @@ class TestGettingAds(TestCase): | |||
103 | client = APIClient(transport, None, None, None, None) | 103 | client = APIClient(transport, None, None, None, None) |
104 | client._authenticate = Mock() | 104 | client._authenticate = Mock() |
105 | 105 | ||
106 | ad_item = client.get_ad_item('mock_id', 'mock_token') | 106 | ad_item = client.get_ad_item('id_mock', 'token_mock') |
107 | assert ad_item.station_id == 'mock_id' | 107 | assert ad_item.station_id == 'id_mock' |
108 | assert ad_item.ad_token == 'token_mock' | ||
108 | 109 | ||
109 | ad_metadata_mock.assert_has_calls([call("ad.getAdMetadata", | 110 | ad_metadata_mock.assert_has_calls([call("ad.getAdMetadata", |
110 | adToken='mock_token', | 111 | adToken='token_mock', |
111 | returnAdTrackingTokens=True, | 112 | returnAdTrackingTokens=True, |
112 | supportAudioAds=True)]) | 113 | supportAudioAds=True)]) |
113 | 114 | ||
@@ -117,4 +118,4 @@ class TestGettingAds(TestCase): | |||
117 | client = APIClient(transport, None, None, None, None) | 118 | client = APIClient(transport, None, None, None, None) |
118 | client.get_ad_metadata = Mock() | 119 | client.get_ad_metadata = Mock() |
119 | 120 | ||
120 | self.assertRaises(ValueError, client.get_ad_item, '', 'mock_token') | 121 | self.assertRaises(ParameterMissing, client.get_ad_item, '', 'token_mock') |
diff --git a/tests/test_pandora/test_models.py b/tests/test_pandora/test_models.py index ecd8cf1..ec6e9d5 100644 --- a/tests/test_pandora/test_models.py +++ b/tests/test_pandora/test_models.py | |||
@@ -3,6 +3,7 @@ from datetime import datetime | |||
3 | from pandora.py2compat import Mock, patch | 3 | from pandora.py2compat import Mock, patch |
4 | from pandora import APIClient | 4 | from pandora import APIClient |
5 | from pandora.models.pandora import AdItem, PlaylistModel | 5 | from pandora.models.pandora import AdItem, PlaylistModel |
6 | from pandora.errors import ParameterMissing | ||
6 | 7 | ||
7 | import pandora.models as m | 8 | import pandora.models as m |
8 | 9 | ||
@@ -205,35 +206,46 @@ class TestAdItem(TestCase): | |||
205 | JSON_DATA = { | 206 | JSON_DATA = { |
206 | 'audioUrlMap': { | 207 | 'audioUrlMap': { |
207 | 'mediumQuality': { | 208 | 'mediumQuality': { |
208 | 'audioUrl': 'mock_med_url', 'bitrate': '64', 'protocol': 'http', 'encoding': 'aacplus' | 209 | 'audioUrl': 'med_url_mock', 'bitrate': '64', 'protocol': 'http', 'encoding': 'aacplus' |
209 | }, | 210 | }, |
210 | 'highQuality': { | 211 | 'highQuality': { |
211 | 'audioUrl': 'mock_high_url', 'bitrate': '64', 'protocol': 'http', 'encoding': 'aacplus' | 212 | 'audioUrl': 'high_url_mock', 'bitrate': '64', 'protocol': 'http', 'encoding': 'aacplus' |
212 | }, | 213 | }, |
213 | 'lowQuality': { | 214 | 'lowQuality': { |
214 | 'audioUrl': 'mock_low_url', 'bitrate': '32', 'protocol': 'http', 'encoding': 'aacplus'}}, | 215 | 'audioUrl': 'low_url_mock', 'bitrate': '32', 'protocol': 'http', 'encoding': 'aacplus'}}, |
215 | 'clickThroughUrl': 'mock_click_url', | 216 | 'clickThroughUrl': 'click_url_mock', |
216 | 'imageUrl': 'mock_img_url', | 217 | 'imageUrl': 'img_url_mock', |
217 | 'companyName': '', | 218 | 'companyName': '', |
218 | 'title': '', | 219 | 'title': '', |
219 | 'trackGain': '0.0', | 220 | 'trackGain': '0.0', |
220 | 'adTrackingTokens': ['mock_token_1', 'mock_token_2'] | 221 | 'adTrackingTokens': ['token_1_mock', 'token_2_mock'] |
221 | } | 222 | } |
222 | 223 | ||
223 | def setUp(self): | 224 | def setUp(self): |
224 | api_client_mock = Mock(spec=APIClient) | 225 | api_client_mock = Mock(spec=APIClient) |
225 | api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY | 226 | api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY |
226 | self.result = AdItem.from_json(api_client_mock, self.JSON_DATA) | 227 | self.result = AdItem.from_json(api_client_mock, self.JSON_DATA) |
228 | self.result.station_id = 'station_id_mock' | ||
229 | self.result.ad_token = 'token_mock' | ||
227 | 230 | ||
228 | def test_is_ad_is_true(self): | 231 | def test_is_ad_is_true(self): |
229 | assert self.result.is_ad is True | 232 | assert self.result.is_ad is True |
230 | 233 | ||
231 | def test_register_ad(self): | 234 | def test_register_ad(self): |
232 | self.result._api_client.register_ad = Mock() | 235 | self.result._api_client.register_ad = Mock() |
233 | self.result.register_ad('id_dummy') | 236 | self.result.register_ad('id_mock') |
234 | 237 | ||
235 | assert self.result._api_client.register_ad.called | 238 | assert self.result._api_client.register_ad.called |
236 | 239 | ||
240 | def test_register_ad_raises_exception_if_no_tracking_tokens_available(self): | ||
241 | with self.assertRaises(ParameterMissing): | ||
242 | self.result.tracking_tokens = [] | ||
243 | self.result._api_client.register_ad = Mock(spec=AdItem) | ||
244 | |||
245 | self.result.register_ad('id_mock') | ||
246 | |||
247 | assert self.result._api_client.register_ad.called | ||
248 | |||
237 | def test_prepare_playback(self): | 249 | def test_prepare_playback(self): |
238 | with patch.object(PlaylistModel, 'prepare_playback') as super_mock: | 250 | with patch.object(PlaylistModel, 'prepare_playback') as super_mock: |
239 | 251 | ||
@@ -241,3 +253,23 @@ class TestAdItem(TestCase): | |||
241 | self.result.prepare_playback() | 253 | self.result.prepare_playback() |
242 | assert self.result.register_ad.called | 254 | assert self.result.register_ad.called |
243 | assert super_mock.called | 255 | assert super_mock.called |
256 | |||
257 | def test_prepare_playback_raises_paramater_missing(self): | ||
258 | with patch.object(PlaylistModel, 'prepare_playback') as super_mock: | ||
259 | |||
260 | self.result.register_ad = Mock(side_effect=ParameterMissing('No ad tracking tokens provided for ' | ||
261 | 'registration.') | ||
262 | ) | ||
263 | self.assertRaises(ParameterMissing, self.result.prepare_playback) | ||
264 | assert self.result.register_ad.called | ||
265 | assert not super_mock.called | ||
266 | |||
267 | def test_prepare_playback_handles_paramater_missing_if_no_tokens(self): | ||
268 | with patch.object(PlaylistModel, 'prepare_playback') as super_mock: | ||
269 | |||
270 | self.result.tracking_tokens = [] | ||
271 | self.result.register_ad = Mock(side_effect=ParameterMissing('No ad tracking tokens provided for ' | ||
272 | 'registration.')) | ||
273 | self.result.prepare_playback() | ||
274 | assert self.result.register_ad.called | ||
275 | assert super_mock.called | ||
diff --git a/tests/test_pandora/test_transport.py b/tests/test_pandora/test_transport.py index 59d4cea..6596a0e 100644 --- a/tests/test_pandora/test_transport.py +++ b/tests/test_pandora/test_transport.py | |||
@@ -18,7 +18,7 @@ class TestTransport(TestCase): | |||
18 | 18 | ||
19 | time.sleep = Mock() | 19 | time.sleep = Mock() |
20 | client.transport._make_http_request = Mock( | 20 | client.transport._make_http_request = Mock( |
21 | side_effect=SysCallError("mock_error")) | 21 | side_effect=SysCallError("error_mock")) |
22 | client.transport._start_request = Mock() | 22 | client.transport._start_request = Mock() |
23 | 23 | ||
24 | client("method") | 24 | client("method") |
diff --git a/tests/test_pydora/test_utils.py b/tests/test_pydora/test_utils.py index 64cdf74..ae6fa0b 100644 --- a/tests/test_pydora/test_utils.py +++ b/tests/test_pydora/test_utils.py | |||
@@ -18,22 +18,22 @@ class TestIterateForever(TestCase): | |||
18 | with patch.object(APIClient, 'get_playlist') as get_playlist_mock: | 18 | with patch.object(APIClient, 'get_playlist') as get_playlist_mock: |
19 | with patch.object(APIClient, 'register_ad', side_effect=ParameterMissing("ParameterMissing")): | 19 | with patch.object(APIClient, 'register_ad', side_effect=ParameterMissing("ParameterMissing")): |
20 | 20 | ||
21 | station = Station.from_json(self.client, {'stationToken': 'dummy_token'}) | 21 | station = Station.from_json(self.client, {'stationToken': 'token_mock'}) |
22 | dummy_ad = AdItem.from_json(self.client, {'station_id': 'dummy_id'}) | 22 | ad_mock = AdItem.from_json(self.client, {'station_id': 'id_mock'}) |
23 | get_playlist_mock.return_value=iter([dummy_ad]) | 23 | get_playlist_mock.return_value=iter([ad_mock]) |
24 | 24 | ||
25 | station_iter = iterate_forever(station.get_playlist) | 25 | station_iter = iterate_forever(station.get_playlist) |
26 | 26 | ||
27 | next_track = station_iter.next() | 27 | next_track = station_iter.next() |
28 | self.assertEqual(dummy_ad, next_track) | 28 | self.assertEqual(ad_mock, next_track) |
29 | 29 | ||
30 | def test_reraise_missing_params_exception(self): | 30 | def test_reraise_missing_params_exception(self): |
31 | with patch.object(APIClient, 'get_playlist', side_effect=ParameterMissing("ParameterMissing")) as get_playlist_mock: | 31 | with patch.object(APIClient, 'get_playlist', side_effect=ParameterMissing("ParameterMissing")) as get_playlist_mock: |
32 | with self.assertRaises(ParameterMissing): | 32 | with self.assertRaises(ParameterMissing): |
33 | 33 | ||
34 | station = Station.from_json(self.client, {'stationToken': 'dummy_token'}) | 34 | station = Station.from_json(self.client, {'stationToken': 'token_mock'}) |
35 | dummy_track = PlaylistItem.from_json(self.client, {'token': 'dummy_token'}) | 35 | track_mock = PlaylistItem.from_json(self.client, {'token': 'token_mock'}) |
36 | get_playlist_mock.return_value=iter([dummy_track]) | 36 | get_playlist_mock.return_value=iter([track_mock]) |
37 | 37 | ||
38 | station_iter = iterate_forever(station.get_playlist) | 38 | station_iter = iterate_forever(station.get_playlist) |
39 | station_iter.next() | 39 | station_iter.next() |