aboutsummaryrefslogtreecommitdiff
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
parent8d316038441194526d6183c0496f765c6ff5a418 (diff)
downloadpydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.tar.bz2
pydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.tar.xz
pydora-6cc2aa2d628da3bd5df4f765529a1c54ec40561b.zip
Reorganize models
-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
-rw-r--r--tests/test_pandora/test_client.py3
-rw-r--r--tests/test_pandora/test_models.py78
-rw-r--r--tests/test_pydora/test_utils.py4
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 @@
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
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
2from unittest.mock import Mock, call, patch 2from unittest.mock import Mock, call, patch
3 3
4from pandora import errors 4from pandora import errors
5from pandora.models.pandora import AdItem, AdditionalAudioUrl 5from pandora.models.ad import AdItem
6from pandora.models.playlist import AdditionalAudioUrl
6from pandora.client import APIClient, BaseAPIClient 7from pandora.client import APIClient, BaseAPIClient
7from tests.test_pandora.test_models import TestAdItem 8from 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
5from pandora.client import APIClient 5from pandora.client import APIClient
6from pandora.errors import ParameterMissing 6from pandora.errors import ParameterMissing
7 7
8import pandora.models.ad as am
8import pandora.models._base as m 9import pandora.models._base as m
9import pandora.models.pandora as pm 10import pandora.models.search as sm
11import pandora.models.station as stm
12import pandora.models.bookmark as bm
13import pandora.models.playlist as plm
10 14
11 15
12class TestField(TestCase): 16class 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):
293class TestPlaylistModel(TestCase): 297class 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
680class TestPandoraType(TestCase): 684class 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
4from pandora.client import APIClient 4from pandora.client import APIClient
5from pandora.errors import InvalidAuthToken, ParameterMissing 5from pandora.errors import InvalidAuthToken, ParameterMissing
6from pandora.models.pandora import Station, AdItem, PlaylistItem 6from pandora.models.ad import AdItem
7from pandora.models.station import Station
8from pandora.models.playlist import PlaylistItem
7from pydora.utils import iterate_forever 9from pydora.utils import iterate_forever
8 10
9 11