diff options
Diffstat (limited to 'pandora')
-rw-r--r-- | pandora/client.py | 215 | ||||
-rw-r--r-- | pandora/clientbuilder.py | 42 | ||||
-rw-r--r-- | pandora/errors.py | 9 | ||||
-rw-r--r-- | pandora/models/_base.py | 12 | ||||
-rw-r--r-- | pandora/models/ad.py | 5 | ||||
-rw-r--r-- | pandora/models/playlist.py | 36 | ||||
-rw-r--r-- | pandora/models/search.py | 15 | ||||
-rw-r--r-- | pandora/models/station.py | 9 | ||||
-rw-r--r-- | pandora/transport.py | 35 |
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 | ||
77 | class APIClientBuilder: | 76 | class 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 | ||
117 | class SettingsDict(TranslatingDict): | 122 | class 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 | ||
73 | class ModelMetaClass(type): | 73 | class 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 | ||
7 | class AdditionalAudioUrl(Enum): | 7 | class 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 | ||
19 | class PandoraType(Enum): | 19 | class 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 | ||
37 | class AudioField(SyntheticField): | 38 | class 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 | ||
86 | class AdditionalUrlField(SyntheticField): | 88 | class 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 | ||
107 | class PlaylistModel(PandoraModel): | 108 | class 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 | ||
43 | class ArtistSearchResultItem(SearchResultItem): | 46 | class 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 | ||
87 | class StationList(PandoraListModel): | 86 | class 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 | ||
112 | class GenreStationList(PandoraDictListModel): | 113 | class 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 | ||
102 | class APITransport: | 105 | class 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 | ||