aboutsummaryrefslogtreecommitdiff
path: root/pandora
diff options
context:
space:
mode:
Diffstat (limited to 'pandora')
-rw-r--r--pandora/client.py215
-rw-r--r--pandora/clientbuilder.py42
-rw-r--r--pandora/errors.py9
-rw-r--r--pandora/models/_base.py12
-rw-r--r--pandora/models/ad.py5
-rw-r--r--pandora/models/playlist.py36
-rw-r--r--pandora/models/search.py15
-rw-r--r--pandora/models/station.py9
-rw-r--r--pandora/transport.py35
9 files changed, 212 insertions, 166 deletions
diff --git a/pandora/client.py b/pandora/client.py
index e0d3da9..a98056c 100644
--- a/pandora/client.py
+++ b/pandora/client.py
@@ -29,8 +29,14 @@ class BaseAPIClient:
29 29
30 ALL_QUALITIES = [LOW_AUDIO_QUALITY, MED_AUDIO_QUALITY, HIGH_AUDIO_QUALITY] 30 ALL_QUALITIES = [LOW_AUDIO_QUALITY, MED_AUDIO_QUALITY, HIGH_AUDIO_QUALITY]
31 31
32 def __init__(self, transport, partner_user, partner_password, device, 32 def __init__(
33 default_audio_quality=MED_AUDIO_QUALITY): 33 self,
34 transport,
35 partner_user,
36 partner_password,
37 device,
38 default_audio_quality=MED_AUDIO_QUALITY,
39 ):
34 self.transport = transport 40 self.transport = transport
35 self.partner_user = partner_user 41 self.partner_user = partner_user
36 self.partner_password = partner_password 42 self.partner_password = partner_password
@@ -40,11 +46,13 @@ class BaseAPIClient:
40 self.password = None 46 self.password = None
41 47
42 def _partner_login(self): 48 def _partner_login(self):
43 partner = self.transport("auth.partnerLogin", 49 partner = self.transport(
44 username=self.partner_user, 50 "auth.partnerLogin",
45 password=self.partner_password, 51 username=self.partner_user,
46 deviceModel=self.device, 52 password=self.partner_password,
47 version=self.transport.API_VERSION) 53 deviceModel=self.device,
54 version=self.transport.API_VERSION,
55 )
48 56
49 self.transport.set_partner(partner) 57 self.transport.set_partner(partner)
50 58
@@ -59,16 +67,18 @@ class BaseAPIClient:
59 self._partner_login() 67 self._partner_login()
60 68
61 try: 69 try:
62 user = self.transport("auth.userLogin", 70 user = self.transport(
63 loginType="user", 71 "auth.userLogin",
64 username=self.username, 72 loginType="user",
65 password=self.password, 73 username=self.username,
66 includePandoraOneInfo=True, 74 password=self.password,
67 includeSubscriptionExpiration=True, 75 includePandoraOneInfo=True,
68 returnCapped=True, 76 includeSubscriptionExpiration=True,
69 includeAdAttributes=True, 77 returnCapped=True,
70 includeAdvertiserAttributes=True, 78 includeAdAttributes=True,
71 xplatformAdCapable=True) 79 includeAdvertiserAttributes=True,
80 xplatformAdCapable=True,
81 )
72 except errors.InvalidPartnerLogin: 82 except errors.InvalidPartnerLogin:
73 raise errors.InvalidUserLogin() 83 raise errors.InvalidUserLogin()
74 84
@@ -80,7 +90,7 @@ class BaseAPIClient:
80 def get_qualities(cls, start_at, return_all_if_invalid=True): 90 def get_qualities(cls, start_at, return_all_if_invalid=True):
81 try: 91 try:
82 idx = cls.ALL_QUALITIES.index(start_at) 92 idx = cls.ALL_QUALITIES.index(start_at)
83 return cls.ALL_QUALITIES[:idx + 1] 93 return cls.ALL_QUALITIES[: idx + 1]
84 except ValueError: 94 except ValueError:
85 if return_all_if_invalid: 95 if return_all_if_invalid:
86 return cls.ALL_QUALITIES[:] 96 return cls.ALL_QUALITIES[:]
@@ -105,9 +115,9 @@ class APIClient(BaseAPIClient):
105 def get_station_list(self): 115 def get_station_list(self):
106 from .models.station import StationList 116 from .models.station import StationList
107 117
108 return StationList.from_json(self, 118 return StationList.from_json(
109 self("user.getStationList", 119 self, self("user.getStationList", includeStationArtUrl=True)
110 includeStationArtUrl=True)) 120 )
111 121
112 def get_station_list_checksum(self): 122 def get_station_list_checksum(self):
113 return self("user.getStationListChecksum")["checksum"] 123 return self("user.getStationListChecksum")["checksum"]
@@ -119,19 +129,21 @@ class APIClient(BaseAPIClient):
119 additional_urls = [] 129 additional_urls = []
120 130
121 if isinstance(additional_urls, str): 131 if isinstance(additional_urls, str):
122 raise TypeError('Additional urls should be a list') 132 raise TypeError("Additional urls should be a list")
123 133
124 urls = [getattr(url, "value", url) for url in additional_urls] 134 urls = [getattr(url, "value", url) for url in additional_urls]
125 135
126 resp = self("station.getPlaylist", 136 resp = self(
127 stationToken=station_token, 137 "station.getPlaylist",
128 includeTrackLength=True, 138 stationToken=station_token,
129 xplatformAdCapable=True, 139 includeTrackLength=True,
130 audioAdPodCapable=True, 140 xplatformAdCapable=True,
131 additionalAudioUrl=','.join(urls)) 141 audioAdPodCapable=True,
142 additionalAudioUrl=",".join(urls),
143 )
132 144
133 for item in resp['items']: 145 for item in resp["items"]:
134 item['_paramAdditionalUrls'] = additional_urls 146 item["_paramAdditionalUrls"] = additional_urls
135 147
136 playlist = Playlist.from_json(self, resp) 148 playlist = Playlist.from_json(self, resp)
137 149
@@ -145,58 +157,69 @@ class APIClient(BaseAPIClient):
145 def get_bookmarks(self): 157 def get_bookmarks(self):
146 from .models.bookmark import BookmarkList 158 from .models.bookmark import BookmarkList
147 159
148 return BookmarkList.from_json(self, 160 return BookmarkList.from_json(self, self("user.getBookmarks"))
149 self("user.getBookmarks"))
150 161
151 def get_station(self, station_token): 162 def get_station(self, station_token):
152 from .models.station import Station 163 from .models.station import Station
153 164
154 return Station.from_json(self, 165 return Station.from_json(
155 self("station.getStation", 166 self,
156 stationToken=station_token, 167 self(
157 includeExtendedAttributes=True)) 168 "station.getStation",
169 stationToken=station_token,
170 includeExtendedAttributes=True,
171 ),
172 )
158 173
159 def add_artist_bookmark(self, track_token): 174 def add_artist_bookmark(self, track_token):
160 return self("bookmark.addArtistBookmark", 175 return self("bookmark.addArtistBookmark", trackToken=track_token)
161 trackToken=track_token)
162 176
163 def add_song_bookmark(self, track_token): 177 def add_song_bookmark(self, track_token):
164 return self("bookmark.addSongBookmark", 178 return self("bookmark.addSongBookmark", trackToken=track_token)
165 trackToken=track_token)
166 179
167 def delete_song_bookmark(self, bookmark_token): 180 def delete_song_bookmark(self, bookmark_token):
168 return self("bookmark.deleteSongBookmark", 181 return self(
169 bookmarkToken=bookmark_token) 182 "bookmark.deleteSongBookmark", bookmarkToken=bookmark_token
183 )
170 184
171 def delete_artist_bookmark(self, bookmark_token): 185 def delete_artist_bookmark(self, bookmark_token):
172 return self("bookmark.deleteArtistBookmark", 186 return self(
173 bookmarkToken=bookmark_token) 187 "bookmark.deleteArtistBookmark", bookmarkToken=bookmark_token
188 )
174 189
175 def search(self, search_text, 190 def search(
176 include_near_matches=False, 191 self,
177 include_genre_stations=False): 192 search_text,
193 include_near_matches=False,
194 include_genre_stations=False,
195 ):
178 from .models.search import SearchResult 196 from .models.search import SearchResult
179 197
180 return SearchResult.from_json( 198 return SearchResult.from_json(
181 self, 199 self,
182 self("music.search", 200 self(
183 searchText=search_text, 201 "music.search",
184 includeNearMatches=include_near_matches, 202 searchText=search_text,
185 includeGenreStations=include_genre_stations) 203 includeNearMatches=include_near_matches,
204 includeGenreStations=include_genre_stations,
205 ),
186 ) 206 )
187 207
188 def add_feedback(self, track_token, positive): 208 def add_feedback(self, track_token, positive):
189 return self("station.addFeedback", 209 return self(
190 trackToken=track_token, 210 "station.addFeedback", trackToken=track_token, isPositive=positive
191 isPositive=positive) 211 )
192 212
193 def add_music(self, music_token, station_token): 213 def add_music(self, music_token, station_token):
194 return self("station.addMusic", 214 return self(
195 musicToken=music_token, 215 "station.addMusic",
196 stationToken=station_token) 216 musicToken=music_token,
217 stationToken=station_token,
218 )
197 219
198 def create_station(self, search_token=None, artist_token=None, 220 def create_station(
199 track_token=None): 221 self, search_token=None, artist_token=None, track_token=None
222 ):
200 from .models.station import Station 223 from .models.station import Station
201 224
202 kwargs = {} 225 kwargs = {}
@@ -210,26 +233,23 @@ class APIClient(BaseAPIClient):
210 else: 233 else:
211 raise KeyError("Must pass a type of token") 234 raise KeyError("Must pass a type of token")
212 235
213 return Station.from_json(self, 236 return Station.from_json(self, self("station.createStation", **kwargs))
214 self("station.createStation", **kwargs))
215 237
216 def delete_feedback(self, feedback_id): 238 def delete_feedback(self, feedback_id):
217 return self("station.deleteFeedback", 239 return self("station.deleteFeedback", feedbackId=feedback_id)
218 feedbackId=feedback_id)
219 240
220 def delete_music(self, seed_id): 241 def delete_music(self, seed_id):
221 return self("station.deleteMusic", 242 return self("station.deleteMusic", seedId=seed_id)
222 seedId=seed_id)
223 243
224 def delete_station(self, station_token): 244 def delete_station(self, station_token):
225 return self("station.deleteStation", 245 return self("station.deleteStation", stationToken=station_token)
226 stationToken=station_token)
227 246
228 def get_genre_stations(self): 247 def get_genre_stations(self):
229 from .models.station import GenreStationList 248 from .models.station import GenreStationList
230 249
231 genre_stations = GenreStationList.from_json( 250 genre_stations = GenreStationList.from_json(
232 self, self("station.getGenreStations")) 251 self, self("station.getGenreStations")
252 )
233 genre_stations.checksum = self.get_genre_stations_checksum() 253 genre_stations.checksum = self.get_genre_stations_checksum()
234 254
235 return genre_stations 255 return genre_stations
@@ -238,44 +258,45 @@ class APIClient(BaseAPIClient):
238 return self("station.getGenreStationsChecksum")["checksum"] 258 return self("station.getGenreStationsChecksum")["checksum"]
239 259
240 def rename_station(self, station_token, name): 260 def rename_station(self, station_token, name):
241 return self("station.renameStation", 261 return self(
242 stationToken=station_token, 262 "station.renameStation",
243 stationName=name) 263 stationToken=station_token,
264 stationName=name,
265 )
244 266
245 def explain_track(self, track_token): 267 def explain_track(self, track_token):
246 return self("track.explainTrack", 268 return self("track.explainTrack", trackToken=track_token)
247 trackToken=track_token)
248 269
249 def set_quick_mix(self, *args): 270 def set_quick_mix(self, *args):
250 return self("user.setQuickMix", 271 return self("user.setQuickMix", quickMixStationIds=args)
251 quickMixStationIds=args)
252 272
253 def sleep_song(self, track_token): 273 def sleep_song(self, track_token):
254 return self("user.sleepSong", 274 return self("user.sleepSong", trackToken=track_token)
255 trackToken=track_token)
256 275
257 def share_station(self, station_id, station_token, *emails): 276 def share_station(self, station_id, station_token, *emails):
258 return self("station.shareStation", 277 return self(
259 stationId=station_id, 278 "station.shareStation",
260 stationToken=station_token, 279 stationId=station_id,
261 emails=emails) 280 stationToken=station_token,
281 emails=emails,
282 )
262 283
263 def transform_shared_station(self, station_token): 284 def transform_shared_station(self, station_token):
264 return self("station.transformSharedStation", 285 return self(
265 stationToken=station_token) 286 "station.transformSharedStation", stationToken=station_token
287 )
266 288
267 def share_music(self, music_token, *emails): 289 def share_music(self, music_token, *emails):
268 return self("music.shareMusic", 290 return self(
269 musicToken=music_token, 291 "music.shareMusic", musicToken=music_token, email=emails[0]
270 email=emails[0]) 292 )
271 293
272 def get_ad_item(self, station_id, ad_token): 294 def get_ad_item(self, station_id, ad_token):
273 from .models.ad import AdItem 295 from .models.ad import AdItem
274 296
275 if not station_id: 297 if not station_id:
276 raise errors.ParameterMissing("The 'station_id' param must be " 298 msg = "The 'station_id' param must be defined, got: '{}'"
277 "defined, got: '{}'" 299 raise errors.ParameterMissing(msg.format(station_id))
278 .format(station_id))
279 300
280 ad_item = AdItem.from_json(self, self.get_ad_metadata(ad_token)) 301 ad_item = AdItem.from_json(self, self.get_ad_metadata(ad_token))
281 ad_item.station_id = station_id 302 ad_item.station_id = station_id
@@ -283,12 +304,14 @@ class APIClient(BaseAPIClient):
283 return ad_item 304 return ad_item
284 305
285 def get_ad_metadata(self, ad_token): 306 def get_ad_metadata(self, ad_token):
286 return self("ad.getAdMetadata", 307 return self(
287 adToken=ad_token, 308 "ad.getAdMetadata",
288 returnAdTrackingTokens=True, 309 adToken=ad_token,
289 supportAudioAds=True) 310 returnAdTrackingTokens=True,
311 supportAudioAds=True,
312 )
290 313
291 def register_ad(self, station_id, tokens): 314 def register_ad(self, station_id, tokens):
292 return self("ad.registerAd", 315 return self(
293 stationId=station_id, 316 "ad.registerAd", stationId=station_id, adTrackingTokens=tokens
294 adTrackingTokens=tokens) 317 )
diff --git a/pandora/clientbuilder.py b/pandora/clientbuilder.py
index bc55cec..2b11ffb 100644
--- a/pandora/clientbuilder.py
+++ b/pandora/clientbuilder.py
@@ -70,8 +70,7 @@ class TranslatingDict(dict):
70 70
71 def __setitem__(self, key, value): 71 def __setitem__(self, key, value):
72 key = self.translate_key(key) 72 key = self.translate_key(key)
73 super().__setitem__( 73 super().__setitem__(key, self.translate_value(key, value))
74 key, self.translate_value(key, value))
75 74
76 75
77class APIClientBuilder: 76class APIClientBuilder:
@@ -99,19 +98,25 @@ class APIClientBuilder:
99 self.client_class = client_class or self.DEFAULT_CLIENT_CLASS 98 self.client_class = client_class or self.DEFAULT_CLIENT_CLASS
100 99
101 def build_from_settings_dict(self, settings): 100 def build_from_settings_dict(self, settings):
102 enc = Encryptor(settings["DECRYPTION_KEY"], 101 enc = Encryptor(settings["DECRYPTION_KEY"], settings["ENCRYPTION_KEY"])
103 settings["ENCRYPTION_KEY"])
104 102
105 trans = APITransport(enc, 103 trans = APITransport(
106 settings.get("API_HOST", DEFAULT_API_HOST), 104 enc,
107 settings.get("PROXY", None)) 105 settings.get("API_HOST", DEFAULT_API_HOST),
106 settings.get("PROXY", None),
107 )
108 108
109 quality = settings.get("AUDIO_QUALITY", 109 quality = settings.get(
110 self.client_class.MED_AUDIO_QUALITY) 110 "AUDIO_QUALITY", self.client_class.MED_AUDIO_QUALITY
111 )
111 112
112 return self.client_class(trans, settings["PARTNER_USER"], 113 return self.client_class(
113 settings["PARTNER_PASSWORD"], 114 trans,
114 settings["DEVICE"], quality) 115 settings["PARTNER_USER"],
116 settings["PARTNER_PASSWORD"],
117 settings["DEVICE"],
118 quality,
119 )
115 120
116 121
117class SettingsDict(TranslatingDict): 122class SettingsDict(TranslatingDict):
@@ -185,8 +190,9 @@ class FileBasedClientBuilder(APIClientBuilder):
185 client = self.build_from_settings_dict(config) 190 client = self.build_from_settings_dict(config)
186 191
187 if self.authenticate: 192 if self.authenticate:
188 client.login(config["USER"]["USERNAME"], 193 client.login(
189 config["USER"]["PASSWORD"]) 194 config["USER"]["USERNAME"], config["USER"]["PASSWORD"]
195 )
190 196
191 return client 197 return client
192 198
@@ -201,8 +207,9 @@ class PydoraConfigFileBuilder(FileBasedClientBuilder):
201 207
202 @staticmethod 208 @staticmethod
203 def cfg_to_dict(cfg, key, kind=SettingsDict): 209 def cfg_to_dict(cfg, key, kind=SettingsDict):
204 return kind((k.strip().upper(), v.strip()) 210 return kind(
205 for k, v in cfg.items(key, raw=True)) 211 (k.strip().upper(), v.strip()) for k, v in cfg.items(key, raw=True)
212 )
206 213
207 def parse_config(self): 214 def parse_config(self):
208 cfg = ConfigParser() 215 cfg = ConfigParser()
@@ -212,7 +219,8 @@ class PydoraConfigFileBuilder(FileBasedClientBuilder):
212 219
213 settings = PydoraConfigFileBuilder.cfg_to_dict(cfg, "api") 220 settings = PydoraConfigFileBuilder.cfg_to_dict(cfg, "api")
214 settings["user"] = PydoraConfigFileBuilder.cfg_to_dict( 221 settings["user"] = PydoraConfigFileBuilder.cfg_to_dict(
215 cfg, "user", dict) 222 cfg, "user", dict
223 )
216 224
217 return settings 225 return settings
218 226
diff --git a/pandora/errors.py b/pandora/errors.py
index c978708..3b03db2 100644
--- a/pandora/errors.py
+++ b/pandora/errors.py
@@ -86,10 +86,11 @@ class PandoraException(Exception):
86 for code, api_message in __API_EXCEPTIONS__.items(): 86 for code, api_message in __API_EXCEPTIONS__.items():
87 name = PandoraException._format_name(api_message) 87 name = PandoraException._format_name(api_message)
88 88
89 exception = type(name, (PandoraException,), { 89 exception = type(
90 "code": code, 90 name,
91 "message": api_message, 91 (PandoraException,),
92 }) 92 {"code": code, "message": api_message,},
93 )
93 94
94 export_to[name] = __API_EXCEPTIONS__[code] = exception 95 export_to[name] = __API_EXCEPTIONS__[code] = exception
95 96
diff --git a/pandora/models/_base.py b/pandora/models/_base.py
index 6975d0a..8904be1 100644
--- a/pandora/models/_base.py
+++ b/pandora/models/_base.py
@@ -71,7 +71,6 @@ class DateField(SyntheticField):
71 71
72 72
73class ModelMetaClass(type): 73class ModelMetaClass(type):
74
75 def __new__(cls, name, parents, dct): 74 def __new__(cls, name, parents, dct):
76 dct["_fields"] = fields = {} 75 dct["_fields"] = fields = {}
77 new_dct = dct.copy() 76 new_dct = dct.copy()
@@ -159,7 +158,8 @@ class PandoraModel(metaclass=ModelMetaClass):
159 """ 158 """
160 items = [ 159 items = [
161 "=".join((key, repr(getattr(self, key)))) 160 "=".join((key, repr(getattr(self, key))))
162 for key in sorted(self._fields.keys())] 161 for key in sorted(self._fields.keys())
162 ]
163 163
164 if items: 164 if items:
165 output = ", ".join(items) 165 output = ", ".join(items)
@@ -167,8 +167,9 @@ class PandoraModel(metaclass=ModelMetaClass):
167 output = None 167 output = None
168 168
169 if and_also: 169 if and_also:
170 return "{}({}, {})".format(self.__class__.__name__, 170 return "{}({}, {})".format(
171 output, and_also) 171 self.__class__.__name__, output, and_also
172 )
172 else: 173 else:
173 return "{}({})".format(self.__class__.__name__, output) 174 return "{}({})".format(self.__class__.__name__, output)
174 175
@@ -301,7 +302,8 @@ class PandoraDictListModel(PandoraModel, dict):
301 302
302 for part in item[self.__list_key__]: 303 for part in item[self.__list_key__]:
303 self[key].append( 304 self[key].append(
304 cls.__list_model__.from_json(api_client, part)) 305 cls.__list_model__.from_json(api_client, part)
306 )
305 307
306 return self 308 return self
307 309
diff --git a/pandora/models/ad.py b/pandora/models/ad.py
index ad4b7b0..48cd302 100644
--- a/pandora/models/ad.py
+++ b/pandora/models/ad.py
@@ -24,8 +24,9 @@ class AdItem(PlaylistModel):
24 if self.tracking_tokens: 24 if self.tracking_tokens:
25 self._api_client.register_ad(station_id, self.tracking_tokens) 25 self._api_client.register_ad(station_id, self.tracking_tokens)
26 else: 26 else:
27 raise ParameterMissing('No ad tracking tokens provided for ' 27 raise ParameterMissing(
28 'registration.') 28 "No ad tracking tokens provided for registration."
29 )
29 30
30 def prepare_playback(self): 31 def prepare_playback(self):
31 try: 32 try:
diff --git a/pandora/models/playlist.py b/pandora/models/playlist.py
index 38afb00..8e0d22e 100644
--- a/pandora/models/playlist.py
+++ b/pandora/models/playlist.py
@@ -5,15 +5,15 @@ from ._base import Field, SyntheticField, PandoraModel, PandoraListModel
5 5
6 6
7class AdditionalAudioUrl(Enum): 7class AdditionalAudioUrl(Enum):
8 HTTP_40_AAC_MONO = 'HTTP_40_AAC_MONO' 8 HTTP_40_AAC_MONO = "HTTP_40_AAC_MONO"
9 HTTP_64_AAC = 'HTTP_64_AAC' 9 HTTP_64_AAC = "HTTP_64_AAC"
10 HTTP_32_AACPLUS = 'HTTP_32_AACPLUS' 10 HTTP_32_AACPLUS = "HTTP_32_AACPLUS"
11 HTTP_64_AACPLUS = 'HTTP_64_AACPLUS' 11 HTTP_64_AACPLUS = "HTTP_64_AACPLUS"
12 HTTP_24_AACPLUS_ADTS = 'HTTP_24_AACPLUS_ADTS' 12 HTTP_24_AACPLUS_ADTS = "HTTP_24_AACPLUS_ADTS"
13 HTTP_32_AACPLUS_ADTS = 'HTTP_32_AACPLUS_ADTS' 13 HTTP_32_AACPLUS_ADTS = "HTTP_32_AACPLUS_ADTS"
14 HTTP_64_AACPLUS_ADTS = 'HTTP_64_AACPLUS_ADTS' 14 HTTP_64_AACPLUS_ADTS = "HTTP_64_AACPLUS_ADTS"
15 HTTP_128_MP3 = 'HTTP_128_MP3' 15 HTTP_128_MP3 = "HTTP_128_MP3"
16 HTTP_32_WMA = 'HTTP_32_WMA' 16 HTTP_32_WMA = "HTTP_32_WMA"
17 17
18 18
19class PandoraType(Enum): 19class PandoraType(Enum):
@@ -28,14 +28,14 @@ class PandoraType(Enum):
28 28
29 @staticmethod 29 @staticmethod
30 def from_string(value): 30 def from_string(value):
31 return { 31 types = {
32 "TR": PandoraType.TRACK, 32 "TR": PandoraType.TRACK,
33 "AR": PandoraType.ARTIST, 33 "AR": PandoraType.ARTIST,
34 }.get(value, PandoraType.GENRE) 34 }
35 return types.get(value, PandoraType.GENRE)
35 36
36 37
37class AudioField(SyntheticField): 38class AudioField(SyntheticField):
38
39 def formatter(self, api_client, data, newval): 39 def formatter(self, api_client, data, newval):
40 """Get audio-related fields 40 """Get audio-related fields
41 41
@@ -61,9 +61,11 @@ class AudioField(SyntheticField):
61 elif not url_map: # No audio url available (e.g. ad tokens) 61 elif not url_map: # No audio url available (e.g. ad tokens)
62 return None 62 return None
63 63
64 valid_audio_formats = [BaseAPIClient.HIGH_AUDIO_QUALITY, 64 valid_audio_formats = [
65 BaseAPIClient.MED_AUDIO_QUALITY, 65 BaseAPIClient.HIGH_AUDIO_QUALITY,
66 BaseAPIClient.LOW_AUDIO_QUALITY] 66 BaseAPIClient.MED_AUDIO_QUALITY,
67 BaseAPIClient.LOW_AUDIO_QUALITY,
68 ]
67 69
68 # Only iterate over sublist, starting at preferred audio quality, or 70 # Only iterate over sublist, starting at preferred audio quality, or
69 # from the beginning of the list if nothing is found. Ensures that the 71 # from the beginning of the list if nothing is found. Ensures that the
@@ -84,7 +86,6 @@ class AudioField(SyntheticField):
84 86
85 87
86class AdditionalUrlField(SyntheticField): 88class AdditionalUrlField(SyntheticField):
87
88 def formatter(self, api_client, data, newval): 89 def formatter(self, api_client, data, newval):
89 """Parse additional url fields and map them to inputs 90 """Parse additional url fields and map them to inputs
90 91
@@ -94,7 +95,7 @@ class AdditionalUrlField(SyntheticField):
94 if newval is None: 95 if newval is None:
95 return None 96 return None
96 97
97 user_param = data['_paramAdditionalUrls'] 98 user_param = data["_paramAdditionalUrls"]
98 urls = {} 99 urls = {}
99 if isinstance(newval, str): 100 if isinstance(newval, str):
100 urls[user_param[0]] = newval 101 urls[user_param[0]] = newval
@@ -105,7 +106,6 @@ class AdditionalUrlField(SyntheticField):
105 106
106 107
107class PlaylistModel(PandoraModel): 108class PlaylistModel(PandoraModel):
108
109 def get_is_playable(self): 109 def get_is_playable(self):
110 if not self.audio_url: 110 if not self.audio_url:
111 return False 111 return False
diff --git a/pandora/models/search.py b/pandora/models/search.py
index 94e6ee6..fe31561 100644
--- a/pandora/models/search.py
+++ b/pandora/models/search.py
@@ -12,13 +12,15 @@ class SearchResultItem(PandoraModel):
12 12
13 @property 13 @property
14 def is_artist(self): 14 def is_artist(self):
15 return isinstance(self, ArtistSearchResultItem) and \ 15 return isinstance(
16 self.token.startswith("R") 16 self, ArtistSearchResultItem
17 ) and self.token.startswith("R")
17 18
18 @property 19 @property
19 def is_composer(self): 20 def is_composer(self):
20 return isinstance(self, ArtistSearchResultItem) and \ 21 return isinstance(
21 self.token.startswith("C") 22 self, ArtistSearchResultItem
23 ) and self.token.startswith("C")
22 24
23 @property 25 @property
24 def is_genre_station(self): 26 def is_genre_station(self):
@@ -36,8 +38,9 @@ class SearchResultItem(PandoraModel):
36 elif data["musicToken"].startswith("G"): 38 elif data["musicToken"].startswith("G"):
37 return GenreStationSearchResultItem.from_json(api_client, data) 39 return GenreStationSearchResultItem.from_json(api_client, data)
38 else: 40 else:
39 raise NotImplementedError("Unknown result token type '{}'" 41 raise NotImplementedError(
40 .format(data["musicToken"])) 42 "Unknown result token type '{}'".format(data["musicToken"])
43 )
41 44
42 45
43class ArtistSearchResultItem(SearchResultItem): 46class ArtistSearchResultItem(SearchResultItem):
diff --git a/pandora/models/station.py b/pandora/models/station.py
index a1880ec..d3f6552 100644
--- a/pandora/models/station.py
+++ b/pandora/models/station.py
@@ -80,8 +80,7 @@ class Station(PandoraModel):
80 feedback = Field("feedback", model=StationFeedback) 80 feedback = Field("feedback", model=StationFeedback)
81 81
82 def get_playlist(self, additional_urls=None): 82 def get_playlist(self, additional_urls=None):
83 return iter(self._api_client.get_playlist(self.token, 83 return iter(self._api_client.get_playlist(self.token, additional_urls))
84 additional_urls))
85 84
86 85
87class StationList(PandoraListModel): 86class StationList(PandoraListModel):
@@ -105,8 +104,10 @@ class GenreStation(PandoraModel):
105 category = Field("categoryName") 104 category = Field("categoryName")
106 105
107 def get_playlist(self): 106 def get_playlist(self):
108 raise NotImplementedError("Genre stations do not have playlists. " 107 raise NotImplementedError(
109 "Create a real station using the token.") 108 "Genre stations do not have playlists. "
109 "Create a real station using the token."
110 )
110 111
111 112
112class GenreStationList(PandoraDictListModel): 113class GenreStationList(PandoraDictListModel):
diff --git a/pandora/transport.py b/pandora/transport.py
index edec8a8..84f153b 100644
--- a/pandora/transport.py
+++ b/pandora/transport.py
@@ -40,6 +40,7 @@ def retries(max_tries, exceptions=(Exception,)):
40 function will only be retried if it raises one of the specified 40 function will only be retried if it raises one of the specified
41 exceptions. 41 exceptions.
42 """ 42 """
43
43 def decorator(func): 44 def decorator(func):
44 def function(*args, **kwargs): 45 def function(*args, **kwargs):
45 46
@@ -55,8 +56,9 @@ def retries(max_tries, exceptions=(Exception,)):
55 if isinstance(exc, PandoraException): 56 if isinstance(exc, PandoraException):
56 raise 57 raise
57 if retries_left > 0: 58 if retries_left > 0:
58 time.sleep(delay_exponential( 59 time.sleep(
59 0.5, 2, max_tries - retries_left)) 60 delay_exponential(0.5, 2, max_tries - retries_left)
61 )
60 else: 62 else:
61 raise 63 raise
62 64
@@ -76,11 +78,12 @@ def delay_exponential(base, growth_factor, attempts):
76 Base must be greater than 0, otherwise a ValueError will be 78 Base must be greater than 0, otherwise a ValueError will be
77 raised. 79 raised.
78 """ 80 """
79 if base == 'rand': 81 if base == "rand":
80 base = random.random() 82 base = random.random()
81 elif base <= 0: 83 elif base <= 0:
82 raise ValueError("The 'base' param must be greater than 0, " 84 raise ValueError(
83 "got: {}".format(base)) 85 "The 'base' param must be greater than 0, got: {}".format(base)
86 )
84 time_to_sleep = base * (growth_factor ** (attempts - 1)) 87 time_to_sleep = base * (growth_factor ** (attempts - 1))
85 return time_to_sleep 88 return time_to_sleep
86 89
@@ -95,8 +98,8 @@ class RetryingSession(requests.Session):
95 98
96 def __init__(self): 99 def __init__(self):
97 super().__init__() 100 super().__init__()
98 self.mount('https://', HTTPAdapter(max_retries=3)) 101 self.mount("https://", HTTPAdapter(max_retries=3))
99 self.mount('http://', HTTPAdapter(max_retries=3)) 102 self.mount("http://", HTTPAdapter(max_retries=3))
100 103
101 104
102class APITransport: 105class APITransport:
@@ -109,10 +112,14 @@ class APITransport:
109 112
110 API_VERSION = "5" 113 API_VERSION = "5"
111 114
112 REQUIRE_RESET = ("auth.partnerLogin", ) 115 REQUIRE_RESET = ("auth.partnerLogin",)
113 NO_ENCRYPT = ("auth.partnerLogin", ) 116 NO_ENCRYPT = ("auth.partnerLogin",)
114 REQUIRE_TLS = ("auth.partnerLogin", "auth.userLogin", 117 REQUIRE_TLS = (
115 "station.getPlaylist", "user.createUser") 118 "auth.partnerLogin",
119 "auth.userLogin",
120 "station.getPlaylist",
121 "user.createUser",
122 )
116 123
117 def __init__(self, cryptor, api_host=DEFAULT_API_HOST, proxy=None): 124 def __init__(self, cryptor, api_host=DEFAULT_API_HOST, proxy=None):
118 self.cryptor = cryptor 125 self.cryptor = cryptor
@@ -199,8 +206,8 @@ class APITransport:
199 206
200 def _build_url(self, method): 207 def _build_url(self, method):
201 return "{}://{}".format( 208 return "{}://{}".format(
202 "https" if method in self.REQUIRE_TLS else "http", 209 "https" if method in self.REQUIRE_TLS else "http", self.api_host
203 self.api_host) 210 )
204 211
205 def _build_data(self, method, data): 212 def _build_data(self, method, data):
206 data["userAuthToken"] = self.user_auth_token 213 data["userAuthToken"] = self.user_auth_token
@@ -260,7 +267,7 @@ class BlowfishCryptor:
260 267
261 computed = b"".join([chr(pad_size).encode("ascii")] * pad_size) 268 computed = b"".join([chr(pad_size).encode("ascii")] * pad_size)
262 if not data[-pad_size:] == computed: 269 if not data[-pad_size:] == computed:
263 raise ValueError('Invalid padding') 270 raise ValueError("Invalid padding")
264 271
265 return data[:-pad_size] 272 return data[:-pad_size]
266 273