aboutsummaryrefslogtreecommitdiff
path: root/pandora
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2019-04-02 03:17:18 +0000
committerMike Crute <mike@crute.us>2019-04-02 03:24:37 +0000
commit6cc2aa2d628da3bd5df4f765529a1c54ec40561b (patch)
treee026f777604bd1cd4a426d611aa97a4f0fc968cf /pandora
parent8d316038441194526d6183c0496f765c6ff5a418 (diff)
downloadpydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.tar.bz2
pydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.tar.xz
pydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.zip
Reorganize models
Diffstat (limited to 'pandora')
-rw-r--r--pandora/client.py16
-rw-r--r--pandora/models/ad.py36
-rw-r--r--pandora/models/bookmark.py32
-rw-r--r--pandora/models/pandora.py475
-rw-r--r--pandora/models/playlist.py197
-rw-r--r--pandora/models/search.py93
-rw-r--r--pandora/models/station.py123
7 files changed, 489 insertions, 483 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 @@
1from ..errors import ParameterMissing
2from ._base import Field
3from .playlist import PlaylistModel, AudioField
4
5
6class 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 @@
1from ._base import PandoraModel, Field, DateField
2
3
4class 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
29class 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 @@
1from enum import Enum
2
3from ..client import BaseAPIClient
4from ..errors import ParameterMissing
5from ._base import Field, DateField, SyntheticField
6from ._base import PandoraModel, PandoraListModel, PandoraDictListModel
7
8
9class 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
21class 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
39class Icon(PandoraModel):
40
41 dominant_color = Field("dominantColor")
42 art_url = Field("artUrl")
43
44
45class 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
59class StationSeeds(PandoraModel):
60
61 genres = Field("genres", model=StationSeed)
62 songs = Field("songs", model=StationSeed)
63 artists = Field("artists", model=StationSeed)
64
65
66class 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
80class 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
88class 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
120class 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
132class 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
145class 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
194class 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
215class 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
246class 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
302class 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
335class Playlist(PandoraListModel):
336
337 __list_key__ = "items"
338 __list_model__ = PlaylistItem
339
340
341class 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
366class BookmarkList(PandoraModel):
367
368 songs = Field("songs", model=Bookmark)
369 artists = Field("artists", model=Bookmark)
370
371
372class 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
411class 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
426class 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
441class 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
455class 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
464class 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 @@
1from enum import Enum
2
3from ..client import BaseAPIClient
4from ._base import Field, SyntheticField, PandoraModel, PandoraListModel
5
6
7class 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
19class 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
37class 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
86class 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
107class 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
138class 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
194class 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 @@
1from ._base import Field, PandoraModel
2
3
4class 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
43class 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
58class 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
73class 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
87class 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 @@
1from ._base import Field, DateField
2from ._base import PandoraModel, PandoraListModel, PandoraDictListModel
3from .playlist import PandoraType
4
5
6class Icon(PandoraModel):
7
8 dominant_color = Field("dominantColor")
9 art_url = Field("artUrl")
10
11
12class 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
26class StationSeeds(PandoraModel):
27
28 genres = Field("genres", model=StationSeed)
29 songs = Field("songs", model=StationSeed)
30 artists = Field("artists", model=StationSeed)
31
32
33class 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
47class 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
55class 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
87class 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
100class 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
112class 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