diff options
author | Mike Crute <mike@crute.us> | 2019-04-02 03:17:18 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2019-04-02 03:24:37 +0000 |
commit | 6cc2aa2d628da3bd5df4f765529a1c54ec40561b (patch) | |
tree | e026f777604bd1cd4a426d611aa97a4f0fc968cf | |
parent | 8d316038441194526d6183c0496f765c6ff5a418 (diff) | |
download | pydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.tar.bz2 pydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.tar.xz pydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.zip |
Reorganize models
-rw-r--r-- | pandora/client.py | 16 | ||||
-rw-r--r-- | pandora/models/ad.py | 36 | ||||
-rw-r--r-- | pandora/models/bookmark.py | 32 | ||||
-rw-r--r-- | pandora/models/pandora.py | 475 | ||||
-rw-r--r-- | pandora/models/playlist.py | 197 | ||||
-rw-r--r-- | pandora/models/search.py | 93 | ||||
-rw-r--r-- | pandora/models/station.py | 123 | ||||
-rw-r--r-- | tests/test_pandora/test_client.py | 3 | ||||
-rw-r--r-- | tests/test_pandora/test_models.py | 78 | ||||
-rw-r--r-- | tests/test_pydora/test_utils.py | 4 |
10 files changed, 535 insertions, 522 deletions
diff --git a/pandora/client.py b/pandora/client.py index 658fb0d..b077954 100644 --- a/pandora/client.py +++ b/pandora/client.py | |||
@@ -103,7 +103,7 @@ class APIClient(BaseAPIClient): | |||
103 | """ | 103 | """ |
104 | 104 | ||
105 | def get_station_list(self): # pragma: no cover | 105 | def get_station_list(self): # pragma: no cover |
106 | from .models.pandora import StationList | 106 | from .models.station import StationList |
107 | 107 | ||
108 | return StationList.from_json(self, | 108 | return StationList.from_json(self, |
109 | self("user.getStationList", | 109 | self("user.getStationList", |
@@ -113,7 +113,7 @@ class APIClient(BaseAPIClient): | |||
113 | return self("user.getStationListChecksum")["checksum"] | 113 | return self("user.getStationListChecksum")["checksum"] |
114 | 114 | ||
115 | def get_playlist(self, station_token, additional_urls=None): | 115 | def get_playlist(self, station_token, additional_urls=None): |
116 | from .models.pandora import Playlist | 116 | from .models.playlist import Playlist |
117 | 117 | ||
118 | if additional_urls is None: | 118 | if additional_urls is None: |
119 | additional_urls = [] | 119 | additional_urls = [] |
@@ -143,13 +143,13 @@ class APIClient(BaseAPIClient): | |||
143 | return playlist | 143 | return playlist |
144 | 144 | ||
145 | def get_bookmarks(self): # pragma: no cover | 145 | def get_bookmarks(self): # pragma: no cover |
146 | from .models.pandora import BookmarkList | 146 | from .models.bookmark import BookmarkList |
147 | 147 | ||
148 | return BookmarkList.from_json(self, | 148 | return BookmarkList.from_json(self, |
149 | self("user.getBookmarks")) | 149 | self("user.getBookmarks")) |
150 | 150 | ||
151 | def get_station(self, station_token): # pragma: no cover | 151 | def get_station(self, station_token): # pragma: no cover |
152 | from .models.pandora import Station | 152 | from .models.station import Station |
153 | 153 | ||
154 | return Station.from_json(self, | 154 | return Station.from_json(self, |
155 | self("station.getStation", | 155 | self("station.getStation", |
@@ -175,7 +175,7 @@ class APIClient(BaseAPIClient): | |||
175 | def search(self, search_text, | 175 | def search(self, search_text, |
176 | include_near_matches=False, | 176 | include_near_matches=False, |
177 | include_genre_stations=False): # pragma: no cover | 177 | include_genre_stations=False): # pragma: no cover |
178 | from .models.pandora import SearchResult | 178 | from .models.search import SearchResult |
179 | 179 | ||
180 | return SearchResult.from_json( | 180 | return SearchResult.from_json( |
181 | self, | 181 | self, |
@@ -197,7 +197,7 @@ class APIClient(BaseAPIClient): | |||
197 | 197 | ||
198 | def create_station(self, search_token=None, artist_token=None, | 198 | def create_station(self, search_token=None, artist_token=None, |
199 | track_token=None): | 199 | track_token=None): |
200 | from .models.pandora import Station | 200 | from .models.station import Station |
201 | 201 | ||
202 | kwargs = {} | 202 | kwargs = {} |
203 | 203 | ||
@@ -226,7 +226,7 @@ class APIClient(BaseAPIClient): | |||
226 | stationToken=station_token) | 226 | stationToken=station_token) |
227 | 227 | ||
228 | def get_genre_stations(self): | 228 | def get_genre_stations(self): |
229 | from .models.pandora import GenreStationList | 229 | from .models.station import GenreStationList |
230 | 230 | ||
231 | genre_stations = GenreStationList.from_json( | 231 | genre_stations = GenreStationList.from_json( |
232 | self, self("station.getGenreStations")) | 232 | self, self("station.getGenreStations")) |
@@ -270,7 +270,7 @@ class APIClient(BaseAPIClient): | |||
270 | email=emails[0]) | 270 | email=emails[0]) |
271 | 271 | ||
272 | def get_ad_item(self, station_id, ad_token): | 272 | def get_ad_item(self, station_id, ad_token): |
273 | from .models.pandora import AdItem | 273 | from .models.ad import AdItem |
274 | 274 | ||
275 | if not station_id: | 275 | if not station_id: |
276 | raise errors.ParameterMissing("The 'station_id' param must be " | 276 | raise errors.ParameterMissing("The 'station_id' param must be " |
diff --git a/pandora/models/ad.py b/pandora/models/ad.py new file mode 100644 index 0000000..d5bcd89 --- /dev/null +++ b/pandora/models/ad.py | |||
@@ -0,0 +1,36 @@ | |||
1 | from ..errors import ParameterMissing | ||
2 | from ._base import Field | ||
3 | from .playlist import PlaylistModel, AudioField | ||
4 | |||
5 | |||
6 | class AdItem(PlaylistModel): | ||
7 | |||
8 | title = Field("title") | ||
9 | company_name = Field("companyName") | ||
10 | tracking_tokens = Field("adTrackingTokens") | ||
11 | audio_url = AudioField("audioUrl") | ||
12 | image_url = Field("imageUrl") | ||
13 | click_through_url = Field("clickThroughUrl") | ||
14 | station_id = None | ||
15 | ad_token = None | ||
16 | |||
17 | @property | ||
18 | def is_ad(self): | ||
19 | return True | ||
20 | |||
21 | def register_ad(self, station_id=None): | ||
22 | if not station_id: | ||
23 | station_id = self.station_id | ||
24 | if self.tracking_tokens: | ||
25 | self._api_client.register_ad(station_id, self.tracking_tokens) | ||
26 | else: | ||
27 | raise ParameterMissing('No ad tracking tokens provided for ' | ||
28 | 'registration.') | ||
29 | |||
30 | def prepare_playback(self): | ||
31 | try: | ||
32 | self.register_ad(self.station_id) | ||
33 | except ParameterMissing as exc: | ||
34 | if self.tracking_tokens: | ||
35 | raise exc | ||
36 | return super(AdItem, self).prepare_playback() | ||
diff --git a/pandora/models/bookmark.py b/pandora/models/bookmark.py new file mode 100644 index 0000000..810348b --- /dev/null +++ b/pandora/models/bookmark.py | |||
@@ -0,0 +1,32 @@ | |||
1 | from ._base import PandoraModel, Field, DateField | ||
2 | |||
3 | |||
4 | class Bookmark(PandoraModel): | ||
5 | |||
6 | music_token = Field("musicToken") | ||
7 | artist_name = Field("artistName") | ||
8 | art_url = Field("artUrl") | ||
9 | bookmark_token = Field("bookmarkToken") | ||
10 | date_created = DateField("dateCreated") | ||
11 | |||
12 | # song only | ||
13 | sample_url = Field("sampleUrl") | ||
14 | sample_gain = Field("sampleGain") | ||
15 | album_name = Field("albumName") | ||
16 | song_name = Field("songName") | ||
17 | |||
18 | @property | ||
19 | def is_song_bookmark(self): | ||
20 | return self.song_name is not None | ||
21 | |||
22 | def delete(self): | ||
23 | if self.is_song_bookmark: | ||
24 | self._api_client.delete_song_bookmark(self.bookmark_token) | ||
25 | else: | ||
26 | self._api_client.delete_artist_bookmark(self.bookmark_token) | ||
27 | |||
28 | |||
29 | class BookmarkList(PandoraModel): | ||
30 | |||
31 | songs = Field("songs", model=Bookmark) | ||
32 | artists = Field("artists", model=Bookmark) | ||
diff --git a/pandora/models/pandora.py b/pandora/models/pandora.py deleted file mode 100644 index 341b0be..0000000 --- a/pandora/models/pandora.py +++ /dev/null | |||
@@ -1,475 +0,0 @@ | |||
1 | from enum import Enum | ||
2 | |||
3 | from ..client import BaseAPIClient | ||
4 | from ..errors import ParameterMissing | ||
5 | from ._base import Field, DateField, SyntheticField | ||
6 | from ._base import PandoraModel, PandoraListModel, PandoraDictListModel | ||
7 | |||
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 | |||
21 | class PandoraType(Enum): | ||
22 | |||
23 | TRACK = "TR" | ||
24 | ARTIST = "AR" | ||
25 | GENRE = "GR" | ||
26 | |||
27 | @staticmethod | ||
28 | def from_model(client, value): | ||
29 | return PandoraType.from_string(value) | ||
30 | |||
31 | @staticmethod | ||
32 | def from_string(value): | ||
33 | return { | ||
34 | "TR": PandoraType.TRACK, | ||
35 | "AR": PandoraType.ARTIST, | ||
36 | }.get(value, PandoraType.GENRE) | ||
37 | |||
38 | |||
39 | class Icon(PandoraModel): | ||
40 | |||
41 | dominant_color = Field("dominantColor") | ||
42 | art_url = Field("artUrl") | ||
43 | |||
44 | |||
45 | class StationSeed(PandoraModel): | ||
46 | |||
47 | seed_id = Field("seedId") | ||
48 | music_token = Field("musicToken") | ||
49 | pandora_id = Field("pandoraId") | ||
50 | pandora_type = Field("pandoraType", formatter=PandoraType.from_model) | ||
51 | |||
52 | genre_name = Field("genreName") | ||
53 | song_name = Field("songName") | ||
54 | artist_name = Field("artistName") | ||
55 | art_url = Field("artUrl") | ||
56 | icon = Field("icon", model=Icon) | ||
57 | |||
58 | |||
59 | class StationSeeds(PandoraModel): | ||
60 | |||
61 | genres = Field("genres", model=StationSeed) | ||
62 | songs = Field("songs", model=StationSeed) | ||
63 | artists = Field("artists", model=StationSeed) | ||
64 | |||
65 | |||
66 | class SongFeedback(PandoraModel): | ||
67 | |||
68 | feedback_id = Field("feedbackId") | ||
69 | song_identity = Field("songIdentity") | ||
70 | is_positive = Field("isPositive") | ||
71 | pandora_id = Field("pandoraId") | ||
72 | album_art_url = Field("albumArtUrl") | ||
73 | music_token = Field("musicToken") | ||
74 | song_name = Field("songName") | ||
75 | artist_name = Field("artistName") | ||
76 | pandora_type = Field("pandoraType", formatter=PandoraType.from_model) | ||
77 | date_created = DateField("dateCreated") | ||
78 | |||
79 | |||
80 | class StationFeedback(PandoraModel): | ||
81 | |||
82 | total_thumbs_up = Field("totalThumbsUp") | ||
83 | total_thumbs_down = Field("totalThumbsDown") | ||
84 | thumbs_up = Field("thumbsUp", model=SongFeedback) | ||
85 | thumbs_down = Field("thumbsDown", model=SongFeedback) | ||
86 | |||
87 | |||
88 | class Station(PandoraModel): | ||
89 | |||
90 | can_add_music = Field("allowAddMusic") | ||
91 | can_delete = Field("allowDelete") | ||
92 | can_rename = Field("allowRename") | ||
93 | can_edit_description = Field("allowEditDescription") | ||
94 | process_skips = Field("processSkips") | ||
95 | is_shared = Field("isShared") | ||
96 | is_quickmix = Field("isQuickMix") | ||
97 | is_genre_station = Field("isGenreStation") | ||
98 | is_thumbprint_station = Field("isThumbprint") | ||
99 | |||
100 | art_url = Field("artUrl") | ||
101 | date_created = DateField("dateCreated") | ||
102 | detail_url = Field("stationDetailUrl") | ||
103 | id = Field("stationId") | ||
104 | name = Field("stationName") | ||
105 | sharing_url = Field("stationSharingUrl") | ||
106 | thumb_count = Field("thumbCount") | ||
107 | token = Field("stationToken") | ||
108 | |||
109 | genre = Field("genre", []) | ||
110 | quickmix_stations = Field("quickMixStationIds", []) | ||
111 | |||
112 | seeds = Field("music", model=StationSeeds) | ||
113 | feedback = Field("feedback", model=StationFeedback) | ||
114 | |||
115 | def get_playlist(self, additional_urls=None): | ||
116 | return iter(self._api_client.get_playlist(self.token, | ||
117 | additional_urls)) | ||
118 | |||
119 | |||
120 | class GenreStation(PandoraModel): | ||
121 | |||
122 | id = Field("stationId") | ||
123 | name = Field("stationName") | ||
124 | token = Field("stationToken") | ||
125 | category = Field("categoryName") | ||
126 | |||
127 | def get_playlist(self): # pragma: no cover | ||
128 | raise NotImplementedError("Genre stations do not have playlists. " | ||
129 | "Create a real station using the token.") | ||
130 | |||
131 | |||
132 | class StationList(PandoraListModel): | ||
133 | |||
134 | checksum = Field("checksum") | ||
135 | |||
136 | __index_key__ = "id" | ||
137 | __list_key__ = "stations" | ||
138 | __list_model__ = Station | ||
139 | |||
140 | def has_changed(self): | ||
141 | checksum = self._api_client.get_station_list_checksum() | ||
142 | return checksum != self.checksum | ||
143 | |||
144 | |||
145 | class AudioField(SyntheticField): | ||
146 | |||
147 | def formatter(self, api_client, data, value): | ||
148 | """Get audio-related fields | ||
149 | |||
150 | Try to find fields for the audio url for specified preferred quality | ||
151 | level, or next-lowest available quality url otherwise. | ||
152 | """ | ||
153 | url_map = data.get("audioUrlMap") | ||
154 | audio_url = data.get("audioUrl") | ||
155 | |||
156 | # Only an audio URL, not a quality map. This happens for most of the | ||
157 | # mobile client tokens and some of the others now. In this case | ||
158 | # substitute the empirically determined default values in the format | ||
159 | # used by the rest of the function so downstream consumers continue to | ||
160 | # work. | ||
161 | if audio_url and not url_map: | ||
162 | url_map = { | ||
163 | BaseAPIClient.HIGH_AUDIO_QUALITY: { | ||
164 | "audioUrl": audio_url, | ||
165 | "bitrate": 64, | ||
166 | "encoding": "aacplus", | ||
167 | } | ||
168 | } | ||
169 | elif not url_map: # No audio url available (e.g. ad tokens) | ||
170 | return None | ||
171 | |||
172 | valid_audio_formats = [BaseAPIClient.HIGH_AUDIO_QUALITY, | ||
173 | BaseAPIClient.MED_AUDIO_QUALITY, | ||
174 | BaseAPIClient.LOW_AUDIO_QUALITY] | ||
175 | |||
176 | # Only iterate over sublist, starting at preferred audio quality, or | ||
177 | # from the beginning of the list if nothing is found. Ensures that the | ||
178 | # bitrate used will always be the same or lower quality than was | ||
179 | # specified to prevent audio from skipping for slow connections. | ||
180 | preferred_quality = api_client.default_audio_quality | ||
181 | if preferred_quality in valid_audio_formats: | ||
182 | i = valid_audio_formats.index(preferred_quality) | ||
183 | valid_audio_formats = valid_audio_formats[i:] | ||
184 | |||
185 | for quality in valid_audio_formats: | ||
186 | audio_url = url_map.get(quality) | ||
187 | |||
188 | if audio_url: | ||
189 | return audio_url[self.field] | ||
190 | |||
191 | return audio_url[self.field] if audio_url else None | ||
192 | |||
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 | |||
215 | class PlaylistModel(PandoraModel): | ||
216 | |||
217 | def get_is_playable(self): | ||
218 | if not self.audio_url: | ||
219 | return False | ||
220 | return self._api_client.transport.test_url(self.audio_url) | ||
221 | |||
222 | def prepare_playback(self): | ||
223 | """Prepare Track for Playback | ||
224 | |||
225 | This method must be called by clients before beginning playback | ||
226 | otherwise the track recieved may not be playable. | ||
227 | """ | ||
228 | return self | ||
229 | |||
230 | def thumbs_up(self): # pragma: no cover | ||
231 | raise NotImplementedError | ||
232 | |||
233 | def thumbs_down(self): # pragma: no cover | ||
234 | raise NotImplementedError | ||
235 | |||
236 | def bookmark_song(self): # pragma: no cover | ||
237 | raise NotImplementedError | ||
238 | |||
239 | def bookmark_artist(self): # pragma: no cover | ||
240 | raise NotImplementedError | ||
241 | |||
242 | def sleep(self): # pragma: no cover | ||
243 | raise NotImplementedError | ||
244 | |||
245 | |||
246 | class PlaylistItem(PlaylistModel): | ||
247 | |||
248 | artist_name = Field("artistName") | ||
249 | album_name = Field("albumName") | ||
250 | song_name = Field("songName") | ||
251 | song_rating = Field("songRating") | ||
252 | track_gain = Field("trackGain") | ||
253 | track_length = Field("trackLength") | ||
254 | track_token = Field("trackToken") | ||
255 | audio_url = AudioField("audioUrl") | ||
256 | bitrate = AudioField("bitrate") | ||
257 | encoding = AudioField("encoding") | ||
258 | album_art_url = Field("albumArtUrl") | ||
259 | allow_feedback = Field("allowFeedback") | ||
260 | station_id = Field("stationId") | ||
261 | |||
262 | ad_token = Field("adToken") | ||
263 | |||
264 | album_detail_url = Field("albumDetailUrl") | ||
265 | album_explore_url = Field("albumExplorerUrl") | ||
266 | |||
267 | amazon_album_asin = Field("amazonAlbumAsin") | ||
268 | amazon_album_digital_asin = Field("amazonAlbumDigitalAsin") | ||
269 | amazon_album_url = Field("amazonAlbumUrl") | ||
270 | amazon_song_digital_asin = Field("amazonSongDigitalAsin") | ||
271 | |||
272 | artist_detail_url = Field("artistDetailUrl") | ||
273 | artist_explore_url = Field("artistExplorerUrl") | ||
274 | |||
275 | itunes_song_url = Field("itunesSongUrl") | ||
276 | |||
277 | song_detail_url = Field("songDetailUrl") | ||
278 | song_explore_url = Field("songExplorerUrl") | ||
279 | |||
280 | additional_audio_urls = AdditionalUrlField("additionalAudioUrl") | ||
281 | |||
282 | @property | ||
283 | def is_ad(self): | ||
284 | return self.ad_token is not None | ||
285 | |||
286 | def thumbs_up(self): # pragma: no cover | ||
287 | return self._api_client.add_feedback(self.track_token, True) | ||
288 | |||
289 | def thumbs_down(self): # pragma: no cover | ||
290 | return self._api_client.add_feedback(self.track_token, False) | ||
291 | |||
292 | def bookmark_song(self): # pragma: no cover | ||
293 | return self._api_client.add_song_bookmark(self.track_token) | ||
294 | |||
295 | def bookmark_artist(self): # pragma: no cover | ||
296 | return self._api_client.add_artist_bookmark(self.track_token) | ||
297 | |||
298 | def sleep(self): # pragma: no cover | ||
299 | return self._api_client.sleep_song(self.track_token) | ||
300 | |||
301 | |||
302 | class AdItem(PlaylistModel): | ||
303 | |||
304 | title = Field("title") | ||
305 | company_name = Field("companyName") | ||
306 | tracking_tokens = Field("adTrackingTokens") | ||
307 | audio_url = AudioField("audioUrl") | ||
308 | image_url = Field("imageUrl") | ||
309 | click_through_url = Field("clickThroughUrl") | ||
310 | station_id = None | ||
311 | ad_token = None | ||
312 | |||
313 | @property | ||
314 | def is_ad(self): | ||
315 | return True | ||
316 | |||
317 | def register_ad(self, station_id=None): | ||
318 | if not station_id: | ||
319 | station_id = self.station_id | ||
320 | if self.tracking_tokens: | ||
321 | self._api_client.register_ad(station_id, self.tracking_tokens) | ||
322 | else: | ||
323 | raise ParameterMissing('No ad tracking tokens provided for ' | ||
324 | 'registration.') | ||
325 | |||
326 | def prepare_playback(self): | ||
327 | try: | ||
328 | self.register_ad(self.station_id) | ||
329 | except ParameterMissing as exc: | ||
330 | if self.tracking_tokens: | ||
331 | raise exc | ||
332 | return super(AdItem, self).prepare_playback() | ||
333 | |||
334 | |||
335 | class Playlist(PandoraListModel): | ||
336 | |||
337 | __list_key__ = "items" | ||
338 | __list_model__ = PlaylistItem | ||
339 | |||
340 | |||
341 | class Bookmark(PandoraModel): | ||
342 | |||
343 | music_token = Field("musicToken") | ||
344 | artist_name = Field("artistName") | ||
345 | art_url = Field("artUrl") | ||
346 | bookmark_token = Field("bookmarkToken") | ||
347 | date_created = DateField("dateCreated") | ||
348 | |||
349 | # song only | ||
350 | sample_url = Field("sampleUrl") | ||
351 | sample_gain = Field("sampleGain") | ||
352 | album_name = Field("albumName") | ||
353 | song_name = Field("songName") | ||
354 | |||
355 | @property | ||
356 | def is_song_bookmark(self): | ||
357 | return self.song_name is not None | ||
358 | |||
359 | def delete(self): | ||
360 | if self.is_song_bookmark: | ||
361 | self._api_client.delete_song_bookmark(self.bookmark_token) | ||
362 | else: | ||
363 | self._api_client.delete_artist_bookmark(self.bookmark_token) | ||
364 | |||
365 | |||
366 | class BookmarkList(PandoraModel): | ||
367 | |||
368 | songs = Field("songs", model=Bookmark) | ||
369 | artists = Field("artists", model=Bookmark) | ||
370 | |||
371 | |||
372 | class SearchResultItem(PandoraModel): | ||
373 | |||
374 | score = Field("score") | ||
375 | token = Field("musicToken") | ||
376 | |||
377 | @property | ||
378 | def is_song(self): | ||
379 | return isinstance(self, SongSearchResultItem) | ||
380 | |||
381 | @property | ||
382 | def is_artist(self): | ||
383 | return isinstance(self, ArtistSearchResultItem) and \ | ||
384 | self.token.startswith("R") | ||
385 | |||
386 | @property | ||
387 | def is_composer(self): | ||
388 | return isinstance(self, ArtistSearchResultItem) and \ | ||
389 | self.token.startswith("C") | ||
390 | |||
391 | @property | ||
392 | def is_genre_station(self): | ||
393 | return isinstance(self, GenreStationSearchResultItem) | ||
394 | |||
395 | def create_station(self): # pragma: no cover | ||
396 | raise NotImplementedError | ||
397 | |||
398 | @classmethod | ||
399 | def from_json(cls, api_client, data): | ||
400 | if data["musicToken"].startswith("S"): | ||
401 | return SongSearchResultItem.from_json(api_client, data) | ||
402 | elif data["musicToken"].startswith(("R", "C")): | ||
403 | return ArtistSearchResultItem.from_json(api_client, data) | ||
404 | elif data["musicToken"].startswith("G"): | ||
405 | return GenreStationSearchResultItem.from_json(api_client, data) | ||
406 | else: | ||
407 | raise NotImplementedError("Unknown result token type '{}'" | ||
408 | .format(data["musicToken"])) | ||
409 | |||
410 | |||
411 | class ArtistSearchResultItem(SearchResultItem): | ||
412 | |||
413 | score = Field("score") | ||
414 | token = Field("musicToken") | ||
415 | artist = Field("artistName") | ||
416 | likely_match = Field("likelyMatch", default=False) | ||
417 | |||
418 | def create_station(self): | ||
419 | self._api_client.create_station(artist_token=self.token) | ||
420 | |||
421 | @classmethod | ||
422 | def from_json(cls, api_client, data): | ||
423 | return super(SearchResultItem, cls).from_json(api_client, data) | ||
424 | |||
425 | |||
426 | class SongSearchResultItem(SearchResultItem): | ||
427 | |||
428 | score = Field("score") | ||
429 | token = Field("musicToken") | ||
430 | artist = Field("artistName") | ||
431 | song_name = Field("songName") | ||
432 | |||
433 | def create_station(self): | ||
434 | self._api_client.create_station(track_token=self.token) | ||
435 | |||
436 | @classmethod | ||
437 | def from_json(cls, api_client, data): | ||
438 | return super(SearchResultItem, cls).from_json(api_client, data) | ||
439 | |||
440 | |||
441 | class GenreStationSearchResultItem(SearchResultItem): | ||
442 | |||
443 | score = Field("score") | ||
444 | token = Field("musicToken") | ||
445 | station_name = Field("stationName") | ||
446 | |||
447 | def create_station(self): | ||
448 | self._api_client.create_station(search_token=self.token) | ||
449 | |||
450 | @classmethod | ||
451 | def from_json(cls, api_client, data): | ||
452 | return super(SearchResultItem, cls).from_json(api_client, data) | ||
453 | |||
454 | |||
455 | class SearchResult(PandoraModel): | ||
456 | |||
457 | nearest_matches_available = Field("nearMatchesAvailable") | ||
458 | explanation = Field("explanation") | ||
459 | songs = Field("songs", model=SongSearchResultItem) | ||
460 | artists = Field("artists", model=ArtistSearchResultItem) | ||
461 | genre_stations = Field("genreStations", model=GenreStationSearchResultItem) | ||
462 | |||
463 | |||
464 | class GenreStationList(PandoraDictListModel): | ||
465 | |||
466 | checksum = Field("checksum") | ||
467 | |||
468 | __dict_list_key__ = "categories" | ||
469 | __dict_key__ = "categoryName" | ||
470 | __list_key__ = "stations" | ||
471 | __list_model__ = GenreStation | ||
472 | |||
473 | def has_changed(self): | ||
474 | checksum = self._api_client.get_station_list_checksum() | ||
475 | return checksum != self.checksum | ||
diff --git a/pandora/models/playlist.py b/pandora/models/playlist.py new file mode 100644 index 0000000..08510f1 --- /dev/null +++ b/pandora/models/playlist.py | |||
@@ -0,0 +1,197 @@ | |||
1 | from enum import Enum | ||
2 | |||
3 | from ..client import BaseAPIClient | ||
4 | from ._base import Field, SyntheticField, PandoraModel, PandoraListModel | ||
5 | |||
6 | |||
7 | class AdditionalAudioUrl(Enum): | ||
8 | HTTP_40_AAC_MONO = 'HTTP_40_AAC_MONO' | ||
9 | HTTP_64_AAC = 'HTTP_64_AAC' | ||
10 | HTTP_32_AACPLUS = 'HTTP_32_AACPLUS' | ||
11 | HTTP_64_AACPLUS = 'HTTP_64_AACPLUS' | ||
12 | HTTP_24_AACPLUS_ADTS = 'HTTP_24_AACPLUS_ADTS' | ||
13 | HTTP_32_AACPLUS_ADTS = 'HTTP_32_AACPLUS_ADTS' | ||
14 | HTTP_64_AACPLUS_ADTS = 'HTTP_64_AACPLUS_ADTS' | ||
15 | HTTP_128_MP3 = 'HTTP_128_MP3' | ||
16 | HTTP_32_WMA = 'HTTP_32_WMA' | ||
17 | |||
18 | |||
19 | class PandoraType(Enum): | ||
20 | |||
21 | TRACK = "TR" | ||
22 | ARTIST = "AR" | ||
23 | GENRE = "GR" | ||
24 | |||
25 | @staticmethod | ||
26 | def from_model(client, value): | ||
27 | return PandoraType.from_string(value) | ||
28 | |||
29 | @staticmethod | ||
30 | def from_string(value): | ||
31 | return { | ||
32 | "TR": PandoraType.TRACK, | ||
33 | "AR": PandoraType.ARTIST, | ||
34 | }.get(value, PandoraType.GENRE) | ||
35 | |||
36 | |||
37 | class AudioField(SyntheticField): | ||
38 | |||
39 | def formatter(self, api_client, data, value): | ||
40 | """Get audio-related fields | ||
41 | |||
42 | Try to find fields for the audio url for specified preferred quality | ||
43 | level, or next-lowest available quality url otherwise. | ||
44 | """ | ||
45 | url_map = data.get("audioUrlMap") | ||
46 | audio_url = data.get("audioUrl") | ||
47 | |||
48 | # Only an audio URL, not a quality map. This happens for most of the | ||
49 | # mobile client tokens and some of the others now. In this case | ||
50 | # substitute the empirically determined default values in the format | ||
51 | # used by the rest of the function so downstream consumers continue to | ||
52 | # work. | ||
53 | if audio_url and not url_map: | ||
54 | url_map = { | ||
55 | BaseAPIClient.HIGH_AUDIO_QUALITY: { | ||
56 | "audioUrl": audio_url, | ||
57 | "bitrate": 64, | ||
58 | "encoding": "aacplus", | ||
59 | } | ||
60 | } | ||
61 | elif not url_map: # No audio url available (e.g. ad tokens) | ||
62 | return None | ||
63 | |||
64 | valid_audio_formats = [BaseAPIClient.HIGH_AUDIO_QUALITY, | ||
65 | BaseAPIClient.MED_AUDIO_QUALITY, | ||
66 | BaseAPIClient.LOW_AUDIO_QUALITY] | ||
67 | |||
68 | # Only iterate over sublist, starting at preferred audio quality, or | ||
69 | # from the beginning of the list if nothing is found. Ensures that the | ||
70 | # bitrate used will always be the same or lower quality than was | ||
71 | # specified to prevent audio from skipping for slow connections. | ||
72 | preferred_quality = api_client.default_audio_quality | ||
73 | if preferred_quality in valid_audio_formats: | ||
74 | i = valid_audio_formats.index(preferred_quality) | ||
75 | valid_audio_formats = valid_audio_formats[i:] | ||
76 | |||
77 | for quality in valid_audio_formats: | ||
78 | audio_url = url_map.get(quality) | ||
79 | |||
80 | if audio_url: | ||
81 | return audio_url[self.field] | ||
82 | |||
83 | return audio_url[self.field] if audio_url else None | ||
84 | |||
85 | |||
86 | class AdditionalUrlField(SyntheticField): | ||
87 | |||
88 | def formatter(self, api_client, data, value): | ||
89 | """Parse additional url fields and map them to inputs | ||
90 | |||
91 | Attempt to create a dictionary with keys being user input, and | ||
92 | response being the returned URL | ||
93 | """ | ||
94 | if value is None: | ||
95 | return None | ||
96 | |||
97 | user_param = data['_paramAdditionalUrls'] | ||
98 | urls = {} | ||
99 | if isinstance(value, str): | ||
100 | urls[user_param[0]] = value | ||
101 | else: | ||
102 | for key, url in zip(user_param, value): | ||
103 | urls[key] = url | ||
104 | return urls | ||
105 | |||
106 | |||
107 | class PlaylistModel(PandoraModel): | ||
108 | |||
109 | def get_is_playable(self): | ||
110 | if not self.audio_url: | ||
111 | return False | ||
112 | return self._api_client.transport.test_url(self.audio_url) | ||
113 | |||
114 | def prepare_playback(self): | ||
115 | """Prepare Track for Playback | ||
116 | |||
117 | This method must be called by clients before beginning playback | ||
118 | otherwise the track recieved may not be playable. | ||
119 | """ | ||
120 | return self | ||
121 | |||
122 | def thumbs_up(self): # pragma: no cover | ||
123 | raise NotImplementedError | ||
124 | |||
125 | def thumbs_down(self): # pragma: no cover | ||
126 | raise NotImplementedError | ||
127 | |||
128 | def bookmark_song(self): # pragma: no cover | ||
129 | raise NotImplementedError | ||
130 | |||
131 | def bookmark_artist(self): # pragma: no cover | ||
132 | raise NotImplementedError | ||
133 | |||
134 | def sleep(self): # pragma: no cover | ||
135 | raise NotImplementedError | ||
136 | |||
137 | |||
138 | class PlaylistItem(PlaylistModel): | ||
139 | |||
140 | artist_name = Field("artistName") | ||
141 | album_name = Field("albumName") | ||
142 | song_name = Field("songName") | ||
143 | song_rating = Field("songRating") | ||
144 | track_gain = Field("trackGain") | ||
145 | track_length = Field("trackLength") | ||
146 | track_token = Field("trackToken") | ||
147 | audio_url = AudioField("audioUrl") | ||
148 | bitrate = AudioField("bitrate") | ||
149 | encoding = AudioField("encoding") | ||
150 | album_art_url = Field("albumArtUrl") | ||
151 | allow_feedback = Field("allowFeedback") | ||
152 | station_id = Field("stationId") | ||
153 | |||
154 | ad_token = Field("adToken") | ||
155 | |||
156 | album_detail_url = Field("albumDetailUrl") | ||
157 | album_explore_url = Field("albumExplorerUrl") | ||
158 | |||
159 | amazon_album_asin = Field("amazonAlbumAsin") | ||
160 | amazon_album_digital_asin = Field("amazonAlbumDigitalAsin") | ||
161 | amazon_album_url = Field("amazonAlbumUrl") | ||
162 | amazon_song_digital_asin = Field("amazonSongDigitalAsin") | ||
163 | |||
164 | artist_detail_url = Field("artistDetailUrl") | ||
165 | artist_explore_url = Field("artistExplorerUrl") | ||
166 | |||
167 | itunes_song_url = Field("itunesSongUrl") | ||
168 | |||
169 | song_detail_url = Field("songDetailUrl") | ||
170 | song_explore_url = Field("songExplorerUrl") | ||
171 | |||
172 | additional_audio_urls = AdditionalUrlField("additionalAudioUrl") | ||
173 | |||
174 | @property | ||
175 | def is_ad(self): | ||
176 | return self.ad_token is not None | ||
177 | |||
178 | def thumbs_up(self): # pragma: no cover | ||
179 | return self._api_client.add_feedback(self.track_token, True) | ||
180 | |||
181 | def thumbs_down(self): # pragma: no cover | ||
182 | return self._api_client.add_feedback(self.track_token, False) | ||
183 | |||
184 | def bookmark_song(self): # pragma: no cover | ||
185 | return self._api_client.add_song_bookmark(self.track_token) | ||
186 | |||
187 | def bookmark_artist(self): # pragma: no cover | ||
188 | return self._api_client.add_artist_bookmark(self.track_token) | ||
189 | |||
190 | def sleep(self): # pragma: no cover | ||
191 | return self._api_client.sleep_song(self.track_token) | ||
192 | |||
193 | |||
194 | class Playlist(PandoraListModel): | ||
195 | |||
196 | __list_key__ = "items" | ||
197 | __list_model__ = PlaylistItem | ||
diff --git a/pandora/models/search.py b/pandora/models/search.py new file mode 100644 index 0000000..89def8e --- /dev/null +++ b/pandora/models/search.py | |||
@@ -0,0 +1,93 @@ | |||
1 | from ._base import Field, PandoraModel | ||
2 | |||
3 | |||
4 | class SearchResultItem(PandoraModel): | ||
5 | |||
6 | score = Field("score") | ||
7 | token = Field("musicToken") | ||
8 | |||
9 | @property | ||
10 | def is_song(self): | ||
11 | return isinstance(self, SongSearchResultItem) | ||
12 | |||
13 | @property | ||
14 | def is_artist(self): | ||
15 | return isinstance(self, ArtistSearchResultItem) and \ | ||
16 | self.token.startswith("R") | ||
17 | |||
18 | @property | ||
19 | def is_composer(self): | ||
20 | return isinstance(self, ArtistSearchResultItem) and \ | ||
21 | self.token.startswith("C") | ||
22 | |||
23 | @property | ||
24 | def is_genre_station(self): | ||
25 | return isinstance(self, GenreStationSearchResultItem) | ||
26 | |||
27 | def create_station(self): # pragma: no cover | ||
28 | raise NotImplementedError | ||
29 | |||
30 | @classmethod | ||
31 | def from_json(cls, api_client, data): | ||
32 | if data["musicToken"].startswith("S"): | ||
33 | return SongSearchResultItem.from_json(api_client, data) | ||
34 | elif data["musicToken"].startswith(("R", "C")): | ||
35 | return ArtistSearchResultItem.from_json(api_client, data) | ||
36 | elif data["musicToken"].startswith("G"): | ||
37 | return GenreStationSearchResultItem.from_json(api_client, data) | ||
38 | else: | ||
39 | raise NotImplementedError("Unknown result token type '{}'" | ||
40 | .format(data["musicToken"])) | ||
41 | |||
42 | |||
43 | class ArtistSearchResultItem(SearchResultItem): | ||
44 | |||
45 | score = Field("score") | ||
46 | token = Field("musicToken") | ||
47 | artist = Field("artistName") | ||
48 | likely_match = Field("likelyMatch", default=False) | ||
49 | |||
50 | def create_station(self): | ||
51 | self._api_client.create_station(artist_token=self.token) | ||
52 | |||
53 | @classmethod | ||
54 | def from_json(cls, api_client, data): | ||
55 | return super(SearchResultItem, cls).from_json(api_client, data) | ||
56 | |||
57 | |||
58 | class SongSearchResultItem(SearchResultItem): | ||
59 | |||
60 | score = Field("score") | ||
61 | token = Field("musicToken") | ||
62 | artist = Field("artistName") | ||
63 | song_name = Field("songName") | ||
64 | |||
65 | def create_station(self): | ||
66 | self._api_client.create_station(track_token=self.token) | ||
67 | |||
68 | @classmethod | ||
69 | def from_json(cls, api_client, data): | ||
70 | return super(SearchResultItem, cls).from_json(api_client, data) | ||
71 | |||
72 | |||
73 | class GenreStationSearchResultItem(SearchResultItem): | ||
74 | |||
75 | score = Field("score") | ||
76 | token = Field("musicToken") | ||
77 | station_name = Field("stationName") | ||
78 | |||
79 | def create_station(self): | ||
80 | self._api_client.create_station(search_token=self.token) | ||
81 | |||
82 | @classmethod | ||
83 | def from_json(cls, api_client, data): | ||
84 | return super(SearchResultItem, cls).from_json(api_client, data) | ||
85 | |||
86 | |||
87 | class SearchResult(PandoraModel): | ||
88 | |||
89 | nearest_matches_available = Field("nearMatchesAvailable") | ||
90 | explanation = Field("explanation") | ||
91 | songs = Field("songs", model=SongSearchResultItem) | ||
92 | artists = Field("artists", model=ArtistSearchResultItem) | ||
93 | genre_stations = Field("genreStations", model=GenreStationSearchResultItem) | ||
diff --git a/pandora/models/station.py b/pandora/models/station.py new file mode 100644 index 0000000..d4f4846 --- /dev/null +++ b/pandora/models/station.py | |||
@@ -0,0 +1,123 @@ | |||
1 | from ._base import Field, DateField | ||
2 | from ._base import PandoraModel, PandoraListModel, PandoraDictListModel | ||
3 | from .playlist import PandoraType | ||
4 | |||
5 | |||
6 | class Icon(PandoraModel): | ||
7 | |||
8 | dominant_color = Field("dominantColor") | ||
9 | art_url = Field("artUrl") | ||
10 | |||
11 | |||
12 | class StationSeed(PandoraModel): | ||
13 | |||
14 | seed_id = Field("seedId") | ||
15 | music_token = Field("musicToken") | ||
16 | pandora_id = Field("pandoraId") | ||
17 | pandora_type = Field("pandoraType", formatter=PandoraType.from_model) | ||
18 | |||
19 | genre_name = Field("genreName") | ||
20 | song_name = Field("songName") | ||
21 | artist_name = Field("artistName") | ||
22 | art_url = Field("artUrl") | ||
23 | icon = Field("icon", model=Icon) | ||
24 | |||
25 | |||
26 | class StationSeeds(PandoraModel): | ||
27 | |||
28 | genres = Field("genres", model=StationSeed) | ||
29 | songs = Field("songs", model=StationSeed) | ||
30 | artists = Field("artists", model=StationSeed) | ||
31 | |||
32 | |||
33 | class SongFeedback(PandoraModel): | ||
34 | |||
35 | feedback_id = Field("feedbackId") | ||
36 | song_identity = Field("songIdentity") | ||
37 | is_positive = Field("isPositive") | ||
38 | pandora_id = Field("pandoraId") | ||
39 | album_art_url = Field("albumArtUrl") | ||
40 | music_token = Field("musicToken") | ||
41 | song_name = Field("songName") | ||
42 | artist_name = Field("artistName") | ||
43 | pandora_type = Field("pandoraType", formatter=PandoraType.from_model) | ||
44 | date_created = DateField("dateCreated") | ||
45 | |||
46 | |||
47 | class StationFeedback(PandoraModel): | ||
48 | |||
49 | total_thumbs_up = Field("totalThumbsUp") | ||
50 | total_thumbs_down = Field("totalThumbsDown") | ||
51 | thumbs_up = Field("thumbsUp", model=SongFeedback) | ||
52 | thumbs_down = Field("thumbsDown", model=SongFeedback) | ||
53 | |||
54 | |||
55 | class Station(PandoraModel): | ||
56 | |||
57 | can_add_music = Field("allowAddMusic") | ||
58 | can_delete = Field("allowDelete") | ||
59 | can_rename = Field("allowRename") | ||
60 | can_edit_description = Field("allowEditDescription") | ||
61 | process_skips = Field("processSkips") | ||
62 | is_shared = Field("isShared") | ||
63 | is_quickmix = Field("isQuickMix") | ||
64 | is_genre_station = Field("isGenreStation") | ||
65 | is_thumbprint_station = Field("isThumbprint") | ||
66 | |||
67 | art_url = Field("artUrl") | ||
68 | date_created = DateField("dateCreated") | ||
69 | detail_url = Field("stationDetailUrl") | ||
70 | id = Field("stationId") | ||
71 | name = Field("stationName") | ||
72 | sharing_url = Field("stationSharingUrl") | ||
73 | thumb_count = Field("thumbCount") | ||
74 | token = Field("stationToken") | ||
75 | |||
76 | genre = Field("genre", []) | ||
77 | quickmix_stations = Field("quickMixStationIds", []) | ||
78 | |||
79 | seeds = Field("music", model=StationSeeds) | ||
80 | feedback = Field("feedback", model=StationFeedback) | ||
81 | |||
82 | def get_playlist(self, additional_urls=None): | ||
83 | return iter(self._api_client.get_playlist(self.token, | ||
84 | additional_urls)) | ||
85 | |||
86 | |||
87 | class StationList(PandoraListModel): | ||
88 | |||
89 | checksum = Field("checksum") | ||
90 | |||
91 | __index_key__ = "id" | ||
92 | __list_key__ = "stations" | ||
93 | __list_model__ = Station | ||
94 | |||
95 | def has_changed(self): | ||
96 | checksum = self._api_client.get_station_list_checksum() | ||
97 | return checksum != self.checksum | ||
98 | |||
99 | |||
100 | class GenreStation(PandoraModel): | ||
101 | |||
102 | id = Field("stationId") | ||
103 | name = Field("stationName") | ||
104 | token = Field("stationToken") | ||
105 | category = Field("categoryName") | ||
106 | |||
107 | def get_playlist(self): # pragma: no cover | ||
108 | raise NotImplementedError("Genre stations do not have playlists. " | ||
109 | "Create a real station using the token.") | ||
110 | |||
111 | |||
112 | class GenreStationList(PandoraDictListModel): | ||
113 | |||
114 | checksum = Field("checksum") | ||
115 | |||
116 | __dict_list_key__ = "categories" | ||
117 | __dict_key__ = "categoryName" | ||
118 | __list_key__ = "stations" | ||
119 | __list_model__ = GenreStation | ||
120 | |||
121 | def has_changed(self): | ||
122 | checksum = self._api_client.get_station_list_checksum() | ||
123 | return checksum != self.checksum | ||
diff --git a/tests/test_pandora/test_client.py b/tests/test_pandora/test_client.py index 5bd200c..901e072 100644 --- a/tests/test_pandora/test_client.py +++ b/tests/test_pandora/test_client.py | |||
@@ -2,7 +2,8 @@ from unittest import TestCase | |||
2 | from unittest.mock import Mock, call, patch | 2 | from unittest.mock import Mock, call, patch |
3 | 3 | ||
4 | from pandora import errors | 4 | from pandora import errors |
5 | from pandora.models.pandora import AdItem, AdditionalAudioUrl | 5 | from pandora.models.ad import AdItem |
6 | from pandora.models.playlist import AdditionalAudioUrl | ||
6 | from pandora.client import APIClient, BaseAPIClient | 7 | from pandora.client import APIClient, BaseAPIClient |
7 | from tests.test_pandora.test_models import TestAdItem | 8 | from tests.test_pandora.test_models import TestAdItem |
8 | 9 | ||
diff --git a/tests/test_pandora/test_models.py b/tests/test_pandora/test_models.py index 8e0e0b5..8cfbd9d 100644 --- a/tests/test_pandora/test_models.py +++ b/tests/test_pandora/test_models.py | |||
@@ -5,8 +5,12 @@ from unittest.mock import Mock, patch | |||
5 | from pandora.client import APIClient | 5 | from pandora.client import APIClient |
6 | from pandora.errors import ParameterMissing | 6 | from pandora.errors import ParameterMissing |
7 | 7 | ||
8 | import pandora.models.ad as am | ||
8 | import pandora.models._base as m | 9 | import pandora.models._base as m |
9 | import pandora.models.pandora as pm | 10 | import pandora.models.search as sm |
11 | import pandora.models.station as stm | ||
12 | import pandora.models.bookmark as bm | ||
13 | import pandora.models.playlist as plm | ||
10 | 14 | ||
11 | 15 | ||
12 | class TestField(TestCase): | 16 | class TestField(TestCase): |
@@ -59,7 +63,7 @@ class TestAdditionalUrlField(TestCase): | |||
59 | '_paramAdditionalUrls': ['foo'] | 63 | '_paramAdditionalUrls': ['foo'] |
60 | } | 64 | } |
61 | 65 | ||
62 | field = pm.AdditionalUrlField("additionalAudioUrl") | 66 | field = plm.AdditionalUrlField("additionalAudioUrl") |
63 | 67 | ||
64 | ret = field.formatter(None, dummy_data, 'test') | 68 | ret = field.formatter(None, dummy_data, 'test') |
65 | 69 | ||
@@ -73,7 +77,7 @@ class TestAdditionalUrlField(TestCase): | |||
73 | ] | 77 | ] |
74 | } | 78 | } |
75 | 79 | ||
76 | field = pm.AdditionalUrlField("additionalAudioUrl") | 80 | field = plm.AdditionalUrlField("additionalAudioUrl") |
77 | 81 | ||
78 | ret = field.formatter(None, dummy_data, ['foo', 'bar']) | 82 | ret = field.formatter(None, dummy_data, ['foo', 'bar']) |
79 | 83 | ||
@@ -274,7 +278,7 @@ class TestPlaylistItemModel(TestCase): | |||
274 | WEIRD_FORMAT = {"audioUrlMap": {"highQuality": {}}} | 278 | WEIRD_FORMAT = {"audioUrlMap": {"highQuality": {}}} |
275 | 279 | ||
276 | def test_audio_url_without_map(self): | 280 | def test_audio_url_without_map(self): |
277 | item = pm.PlaylistItem.from_json(Mock(), self.AUDIO_URL_NO_MAP) | 281 | item = plm.PlaylistItem.from_json(Mock(), self.AUDIO_URL_NO_MAP) |
278 | self.assertEqual(item.bitrate, 64) | 282 | self.assertEqual(item.bitrate, 64) |
279 | self.assertEqual(item.encoding, "aacplus") | 283 | self.assertEqual(item.encoding, "aacplus") |
280 | self.assertEqual(item.audio_url, "foo") | 284 | self.assertEqual(item.audio_url, "foo") |
@@ -284,7 +288,7 @@ class TestPlaylistItemModel(TestCase): | |||
284 | # valid url... but I didn't add the original code so just going to test it | 288 | # valid url... but I didn't add the original code so just going to test it |
285 | # and leave it alone for now ~mcrute | 289 | # and leave it alone for now ~mcrute |
286 | def test_empty_quality_map_url_is_map(self): | 290 | def test_empty_quality_map_url_is_map(self): |
287 | item = pm.PlaylistItem.from_json(Mock(), self.WEIRD_FORMAT) | 291 | item = plm.PlaylistItem.from_json(Mock(), self.WEIRD_FORMAT) |
288 | self.assertIsNone(item.bitrate) | 292 | self.assertIsNone(item.bitrate) |
289 | self.assertIsNone(item.encoding) | 293 | self.assertIsNone(item.encoding) |
290 | self.assertIsNone(item.audio_url) | 294 | self.assertIsNone(item.audio_url) |
@@ -293,13 +297,13 @@ class TestPlaylistItemModel(TestCase): | |||
293 | class TestPlaylistModel(TestCase): | 297 | class TestPlaylistModel(TestCase): |
294 | 298 | ||
295 | def test_unplayable_get_is_playable(self): | 299 | def test_unplayable_get_is_playable(self): |
296 | playlist = pm.PlaylistModel(Mock()) | 300 | playlist = plm.PlaylistModel(Mock()) |
297 | playlist.audio_url = "" | 301 | playlist.audio_url = "" |
298 | self.assertFalse(playlist.get_is_playable()) | 302 | self.assertFalse(playlist.get_is_playable()) |
299 | 303 | ||
300 | def test_playable_get_is_playable(self): | 304 | def test_playable_get_is_playable(self): |
301 | client = Mock() | 305 | client = Mock() |
302 | playlist = pm.PlaylistModel(client) | 306 | playlist = plm.PlaylistModel(client) |
303 | playlist.audio_url = "foo" | 307 | playlist.audio_url = "foo" |
304 | playlist.get_is_playable() | 308 | playlist.get_is_playable() |
305 | client.transport.test_url.assert_called_with("foo") | 309 | client.transport.test_url.assert_called_with("foo") |
@@ -339,7 +343,7 @@ class TestAdItem(TestCase): | |||
339 | def setUp(self): | 343 | def setUp(self): |
340 | api_client_mock = Mock(spec=APIClient) | 344 | api_client_mock = Mock(spec=APIClient) |
341 | api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY | 345 | api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY |
342 | self.result = pm.AdItem.from_json(api_client_mock, self.JSON_DATA) | 346 | self.result = am.AdItem.from_json(api_client_mock, self.JSON_DATA) |
343 | self.result.station_id = 'station_id_mock' | 347 | self.result.station_id = 'station_id_mock' |
344 | self.result.ad_token = 'token_mock' | 348 | self.result.ad_token = 'token_mock' |
345 | 349 | ||
@@ -355,14 +359,14 @@ class TestAdItem(TestCase): | |||
355 | def test_register_ad_raises_if_no_tracking_tokens_available(self): | 359 | def test_register_ad_raises_if_no_tracking_tokens_available(self): |
356 | with self.assertRaises(ParameterMissing): | 360 | with self.assertRaises(ParameterMissing): |
357 | self.result.tracking_tokens = [] | 361 | self.result.tracking_tokens = [] |
358 | self.result._api_client.register_ad = Mock(spec=pm.AdItem) | 362 | self.result._api_client.register_ad = Mock(spec=am.AdItem) |
359 | 363 | ||
360 | self.result.register_ad('id_mock') | 364 | self.result.register_ad('id_mock') |
361 | 365 | ||
362 | assert self.result._api_client.register_ad.called | 366 | assert self.result._api_client.register_ad.called |
363 | 367 | ||
364 | def test_prepare_playback(self): | 368 | def test_prepare_playback(self): |
365 | with patch.object(pm.PlaylistModel, 'prepare_playback') as super_mock: | 369 | with patch.object(plm.PlaylistModel, 'prepare_playback') as super_mock: |
366 | 370 | ||
367 | self.result.register_ad = Mock() | 371 | self.result.register_ad = Mock() |
368 | self.result.prepare_playback() | 372 | self.result.prepare_playback() |
@@ -370,7 +374,7 @@ class TestAdItem(TestCase): | |||
370 | assert super_mock.called | 374 | assert super_mock.called |
371 | 375 | ||
372 | def test_prepare_playback_raises_paramater_missing(self): | 376 | def test_prepare_playback_raises_paramater_missing(self): |
373 | with patch.object(pm.PlaylistModel, 'prepare_playback') as super_mock: | 377 | with patch.object(plm.PlaylistModel, 'prepare_playback') as super_mock: |
374 | 378 | ||
375 | self.result.register_ad = Mock(side_effect=ParameterMissing( | 379 | self.result.register_ad = Mock(side_effect=ParameterMissing( |
376 | 'No ad tracking tokens provided for registration.') | 380 | 'No ad tracking tokens provided for registration.') |
@@ -380,7 +384,7 @@ class TestAdItem(TestCase): | |||
380 | assert not super_mock.called | 384 | assert not super_mock.called |
381 | 385 | ||
382 | def test_prepare_playback_handles_paramater_missing_if_no_tokens(self): | 386 | def test_prepare_playback_handles_paramater_missing_if_no_tokens(self): |
383 | with patch.object(pm.PlaylistModel, 'prepare_playback') as super_mock: | 387 | with patch.object(plm.PlaylistModel, 'prepare_playback') as super_mock: |
384 | 388 | ||
385 | self.result.tracking_tokens = [] | 389 | self.result.tracking_tokens = [] |
386 | self.result.register_ad = Mock(side_effect=ParameterMissing( | 390 | self.result.register_ad = Mock(side_effect=ParameterMissing( |
@@ -431,7 +435,7 @@ class TestSearchResultItem(TestCase): | |||
431 | APIClient.HIGH_AUDIO_QUALITY | 435 | APIClient.HIGH_AUDIO_QUALITY |
432 | 436 | ||
433 | def test_is_song(self): | 437 | def test_is_song(self): |
434 | result = pm.SearchResultItem.from_json( | 438 | result = sm.SearchResultItem.from_json( |
435 | self.api_client_mock, self.SONG_JSON_DATA) | 439 | self.api_client_mock, self.SONG_JSON_DATA) |
436 | assert result.is_song | 440 | assert result.is_song |
437 | assert not result.is_artist | 441 | assert not result.is_artist |
@@ -439,7 +443,7 @@ class TestSearchResultItem(TestCase): | |||
439 | assert not result.is_genre_station | 443 | assert not result.is_genre_station |
440 | 444 | ||
441 | def test_is_artist(self): | 445 | def test_is_artist(self): |
442 | result = pm.SearchResultItem.from_json( | 446 | result = sm.SearchResultItem.from_json( |
443 | self.api_client_mock, self.ARTIST_JSON_DATA) | 447 | self.api_client_mock, self.ARTIST_JSON_DATA) |
444 | assert not result.is_song | 448 | assert not result.is_song |
445 | assert result.is_artist | 449 | assert result.is_artist |
@@ -447,7 +451,7 @@ class TestSearchResultItem(TestCase): | |||
447 | assert not result.is_genre_station | 451 | assert not result.is_genre_station |
448 | 452 | ||
449 | def test_is_composer(self): | 453 | def test_is_composer(self): |
450 | result = pm.SearchResultItem.from_json( | 454 | result = sm.SearchResultItem.from_json( |
451 | self.api_client_mock, self.COMPOSER_JSON_DATA) | 455 | self.api_client_mock, self.COMPOSER_JSON_DATA) |
452 | assert not result.is_song | 456 | assert not result.is_song |
453 | assert not result.is_artist | 457 | assert not result.is_artist |
@@ -455,7 +459,7 @@ class TestSearchResultItem(TestCase): | |||
455 | assert not result.is_genre_station | 459 | assert not result.is_genre_station |
456 | 460 | ||
457 | def test_is_genre_station(self): | 461 | def test_is_genre_station(self): |
458 | result = pm.SearchResultItem.from_json( | 462 | result = sm.SearchResultItem.from_json( |
459 | self.api_client_mock, self.GENRE_JSON_DATA) | 463 | self.api_client_mock, self.GENRE_JSON_DATA) |
460 | assert not result.is_song | 464 | assert not result.is_song |
461 | assert not result.is_artist | 465 | assert not result.is_artist |
@@ -464,7 +468,7 @@ class TestSearchResultItem(TestCase): | |||
464 | 468 | ||
465 | def test_fails_if_unknown(self): | 469 | def test_fails_if_unknown(self): |
466 | with self.assertRaises(NotImplementedError): | 470 | with self.assertRaises(NotImplementedError): |
467 | pm.SearchResultItem.from_json( | 471 | sm.SearchResultItem.from_json( |
468 | self.api_client_mock, self.UNKNOWN_JSON_DATA) | 472 | self.api_client_mock, self.UNKNOWN_JSON_DATA) |
469 | 473 | ||
470 | 474 | ||
@@ -490,14 +494,14 @@ class TestArtistSearchResultItem(TestCase): | |||
490 | APIClient.HIGH_AUDIO_QUALITY | 494 | APIClient.HIGH_AUDIO_QUALITY |
491 | 495 | ||
492 | def test_repr(self): | 496 | def test_repr(self): |
493 | result = pm.SearchResultItem.from_json( | 497 | result = sm.SearchResultItem.from_json( |
494 | self.api_client_mock, self.ARTIST_JSON_DATA) | 498 | self.api_client_mock, self.ARTIST_JSON_DATA) |
495 | expected = ( | 499 | expected = ( |
496 | "ArtistSearchResultItem(artist='artist_name_mock', " | 500 | "ArtistSearchResultItem(artist='artist_name_mock', " |
497 | "likely_match=False, score=100, token='R0000000')") | 501 | "likely_match=False, score=100, token='R0000000')") |
498 | self.assertEqual(expected, repr(result)) | 502 | self.assertEqual(expected, repr(result)) |
499 | 503 | ||
500 | result = pm.SearchResultItem.from_json( | 504 | result = sm.SearchResultItem.from_json( |
501 | self.api_client_mock, self.COMPOSER_JSON_DATA) | 505 | self.api_client_mock, self.COMPOSER_JSON_DATA) |
502 | expected = ( | 506 | expected = ( |
503 | "ArtistSearchResultItem(artist='composer_name_mock', " | 507 | "ArtistSearchResultItem(artist='composer_name_mock', " |
@@ -505,7 +509,7 @@ class TestArtistSearchResultItem(TestCase): | |||
505 | self.assertEqual(expected, repr(result)) | 509 | self.assertEqual(expected, repr(result)) |
506 | 510 | ||
507 | def test_create_station(self): | 511 | def test_create_station(self): |
508 | result = pm.SearchResultItem.from_json( | 512 | result = sm.SearchResultItem.from_json( |
509 | self.api_client_mock, self.ARTIST_JSON_DATA) | 513 | self.api_client_mock, self.ARTIST_JSON_DATA) |
510 | result._api_client.create_station = Mock() | 514 | result._api_client.create_station = Mock() |
511 | 515 | ||
@@ -529,7 +533,7 @@ class TestSongSearchResultItem(TestCase): | |||
529 | APIClient.HIGH_AUDIO_QUALITY | 533 | APIClient.HIGH_AUDIO_QUALITY |
530 | 534 | ||
531 | def test_repr(self): | 535 | def test_repr(self): |
532 | result = pm.SearchResultItem.from_json( | 536 | result = sm.SearchResultItem.from_json( |
533 | self.api_client_mock, self.SONG_JSON_DATA) | 537 | self.api_client_mock, self.SONG_JSON_DATA) |
534 | expected = ( | 538 | expected = ( |
535 | "SongSearchResultItem(artist='artist_name_mock', score=100, " | 539 | "SongSearchResultItem(artist='artist_name_mock', score=100, " |
@@ -537,7 +541,7 @@ class TestSongSearchResultItem(TestCase): | |||
537 | self.assertEqual(expected, repr(result)) | 541 | self.assertEqual(expected, repr(result)) |
538 | 542 | ||
539 | def test_create_station(self): | 543 | def test_create_station(self): |
540 | result = pm.SearchResultItem.from_json( | 544 | result = sm.SearchResultItem.from_json( |
541 | self.api_client_mock, self.SONG_JSON_DATA) | 545 | self.api_client_mock, self.SONG_JSON_DATA) |
542 | result._api_client.create_station = Mock() | 546 | result._api_client.create_station = Mock() |
543 | 547 | ||
@@ -560,7 +564,7 @@ class TestGenreStationSearchResultItem(TestCase): | |||
560 | APIClient.HIGH_AUDIO_QUALITY | 564 | APIClient.HIGH_AUDIO_QUALITY |
561 | 565 | ||
562 | def test_repr(self): | 566 | def test_repr(self): |
563 | result = pm.SearchResultItem.from_json( | 567 | result = sm.SearchResultItem.from_json( |
564 | self.api_client_mock, self.GENRE_JSON_DATA) | 568 | self.api_client_mock, self.GENRE_JSON_DATA) |
565 | expected = ( | 569 | expected = ( |
566 | "GenreStationSearchResultItem(score=100, " | 570 | "GenreStationSearchResultItem(score=100, " |
@@ -568,7 +572,7 @@ class TestGenreStationSearchResultItem(TestCase): | |||
568 | self.assertEqual(expected, repr(result)) | 572 | self.assertEqual(expected, repr(result)) |
569 | 573 | ||
570 | def test_create_station(self): | 574 | def test_create_station(self): |
571 | result = pm.SearchResultItem.from_json( | 575 | result = sm.SearchResultItem.from_json( |
572 | self.api_client_mock, self.GENRE_JSON_DATA) | 576 | self.api_client_mock, self.GENRE_JSON_DATA) |
573 | result._api_client.create_station = Mock() | 577 | result._api_client.create_station = Mock() |
574 | 578 | ||
@@ -604,7 +608,7 @@ class TestSearchResult(TestCase): | |||
604 | def setUp(self): | 608 | def setUp(self): |
605 | api_client_mock = Mock(spec=APIClient) | 609 | api_client_mock = Mock(spec=APIClient) |
606 | api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY | 610 | api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY |
607 | self.result = pm.SearchResult.from_json( | 611 | self.result = sm.SearchResult.from_json( |
608 | api_client_mock, self.JSON_DATA) | 612 | api_client_mock, self.JSON_DATA) |
609 | 613 | ||
610 | def test_repr(self): | 614 | def test_repr(self): |
@@ -634,7 +638,7 @@ class TestGenreStationList(TestCase): | |||
634 | api_client = Mock() | 638 | api_client = Mock() |
635 | api_client.get_station_list_checksum.return_value = "foo" | 639 | api_client.get_station_list_checksum.return_value = "foo" |
636 | 640 | ||
637 | stations = pm.GenreStationList.from_json(api_client, self.TEST_DATA) | 641 | stations = stm.GenreStationList.from_json(api_client, self.TEST_DATA) |
638 | self.assertTrue(stations.has_changed()) | 642 | self.assertTrue(stations.has_changed()) |
639 | 643 | ||
640 | 644 | ||
@@ -649,7 +653,7 @@ class TestStationList(TestCase): | |||
649 | api_client = Mock() | 653 | api_client = Mock() |
650 | api_client.get_station_list_checksum.return_value = "foo" | 654 | api_client.get_station_list_checksum.return_value = "foo" |
651 | 655 | ||
652 | stations = pm.StationList.from_json(api_client, self.TEST_DATA) | 656 | stations = stm.StationList.from_json(api_client, self.TEST_DATA) |
653 | self.assertTrue(stations.has_changed()) | 657 | self.assertTrue(stations.has_changed()) |
654 | 658 | ||
655 | 659 | ||
@@ -662,31 +666,31 @@ class TestBookmark(TestCase): | |||
662 | self.client = Mock() | 666 | self.client = Mock() |
663 | 667 | ||
664 | def test_is_song_bookmark(self): | 668 | def test_is_song_bookmark(self): |
665 | s = pm.Bookmark.from_json(self.client, self.SONG_BOOKMARK) | 669 | s = bm.Bookmark.from_json(self.client, self.SONG_BOOKMARK) |
666 | a = pm.Bookmark.from_json(self.client, self.ARTIST_BOOKMARK) | 670 | a = bm.Bookmark.from_json(self.client, self.ARTIST_BOOKMARK) |
667 | 671 | ||
668 | self.assertTrue(s.is_song_bookmark) | 672 | self.assertTrue(s.is_song_bookmark) |
669 | self.assertFalse(a.is_song_bookmark) | 673 | self.assertFalse(a.is_song_bookmark) |
670 | 674 | ||
671 | def test_delete_song_bookmark(self): | 675 | def test_delete_song_bookmark(self): |
672 | pm.Bookmark.from_json(self.client, self.SONG_BOOKMARK).delete() | 676 | bm.Bookmark.from_json(self.client, self.SONG_BOOKMARK).delete() |
673 | self.client.delete_song_bookmark.assert_called_with("token") | 677 | self.client.delete_song_bookmark.assert_called_with("token") |
674 | 678 | ||
675 | def test_delete_artist_bookmark(self): | 679 | def test_delete_artist_bookmark(self): |
676 | pm.Bookmark.from_json(self.client, self.ARTIST_BOOKMARK).delete() | 680 | bm.Bookmark.from_json(self.client, self.ARTIST_BOOKMARK).delete() |
677 | self.client.delete_artist_bookmark.assert_called_with("token") | 681 | self.client.delete_artist_bookmark.assert_called_with("token") |
678 | 682 | ||
679 | 683 | ||
680 | class TestPandoraType(TestCase): | 684 | class TestPandoraType(TestCase): |
681 | 685 | ||
682 | def test_it_can_be_built_from_a_model(self): | 686 | def test_it_can_be_built_from_a_model(self): |
683 | pt = pm.PandoraType.from_model(None, "TR") | 687 | pt = plm.PandoraType.from_model(None, "TR") |
684 | self.assertIs(pm.PandoraType.TRACK, pt) | 688 | self.assertIs(plm.PandoraType.TRACK, pt) |
685 | 689 | ||
686 | def test_it_can_be_built_from_string(self): | 690 | def test_it_can_be_built_from_string(self): |
687 | pt = pm.PandoraType.from_string("TR") | 691 | pt = plm.PandoraType.from_string("TR") |
688 | self.assertIs(pm.PandoraType.TRACK, pt) | 692 | self.assertIs(plm.PandoraType.TRACK, pt) |
689 | 693 | ||
690 | def test_it_returns_genre_for_unknown_string(self): | 694 | def test_it_returns_genre_for_unknown_string(self): |
691 | pt = pm.PandoraType.from_string("FOO") | 695 | pt = plm.PandoraType.from_string("FOO") |
692 | self.assertIs(pm.PandoraType.GENRE, pt) | 696 | self.assertIs(plm.PandoraType.GENRE, pt) |
diff --git a/tests/test_pydora/test_utils.py b/tests/test_pydora/test_utils.py index 5dca83c..475a7b5 100644 --- a/tests/test_pydora/test_utils.py +++ b/tests/test_pydora/test_utils.py | |||
@@ -3,7 +3,9 @@ from unittest.mock import Mock, patch | |||
3 | 3 | ||
4 | from pandora.client import APIClient | 4 | from pandora.client import APIClient |
5 | from pandora.errors import InvalidAuthToken, ParameterMissing | 5 | from pandora.errors import InvalidAuthToken, ParameterMissing |
6 | from pandora.models.pandora import Station, AdItem, PlaylistItem | 6 | from pandora.models.ad import AdItem |
7 | from pandora.models.station import Station | ||
8 | from pandora.models.playlist import PlaylistItem | ||
7 | from pydora.utils import iterate_forever | 9 | from pydora.utils import iterate_forever |
8 | 10 | ||
9 | 11 | ||