diff options
Diffstat (limited to 'pandora/models/playlist.py')
-rw-r--r-- | pandora/models/playlist.py | 197 |
1 files changed, 197 insertions, 0 deletions
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 @@ | |||
1 | from enum import Enum | ||
2 | |||
3 | from ..client import BaseAPIClient | ||
4 | from ._base import Field, SyntheticField, PandoraModel, PandoraListModel | ||
5 | |||
6 | |||
7 | class 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 | |||
19 | class 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 | |||
37 | class 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 | |||
86 | class 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 | |||
107 | class 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 | |||
138 | class 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 | |||
194 | class Playlist(PandoraListModel): | ||
195 | |||
196 | __list_key__ = "items" | ||
197 | __list_model__ = PlaylistItem | ||