diff options
Diffstat (limited to 'pandora/models/pandora.py')
-rw-r--r-- | pandora/models/pandora.py | 475 |
1 files changed, 0 insertions, 475 deletions
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 | ||