diff options
author | Mike Crute <mike@crute.us> | 2017-10-07 04:34:52 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2017-10-07 04:37:42 +0000 |
commit | 954816d5c27fd03e2896e7d79b7cb1175bb037ce (patch) | |
tree | 1556f9f162f5852d111d2e64aa1811649f6c6fd1 | |
parent | 488b22889733d8c3b4ac4f8c7c73c72379e64132 (diff) | |
download | pydora-954816d5c27fd03e2896e7d79b7cb1175bb037ce.tar.bz2 pydora-954816d5c27fd03e2896e7d79b7cb1175bb037ce.tar.xz pydora-954816d5c27fd03e2896e7d79b7cb1175bb037ce.zip |
Refactor audio URL extraction
This generalizes the audio URL extraction logic into a synthesized field
that can be extracted from the PlaylistModel class. It also removes a
few transmogrifiers that are no longer needed in the general case.
Technically this breaks a publicly exposed API within playlists models
but it was always considered an implementation detail so nobody should
be relying on it.
-rw-r--r-- | pandora/models/__init__.py | 23 | ||||
-rw-r--r-- | pandora/models/pandora.py | 75 |
2 files changed, 33 insertions, 65 deletions
diff --git a/pandora/models/__init__.py b/pandora/models/__init__.py index d4e3b87..6a14f45 100644 --- a/pandora/models/__init__.py +++ b/pandora/models/__init__.py | |||
@@ -12,6 +12,22 @@ class Field(namedtuple("Field", ["field", "default", "formatter"])): | |||
12 | return super(Field, cls).__new__(cls, field, default, formatter) | 12 | return super(Field, cls).__new__(cls, field, default, formatter) |
13 | 13 | ||
14 | 14 | ||
15 | class SyntheticField(namedtuple("SyntheticField", ["field"])): | ||
16 | """Field That Requires Synthesis | ||
17 | |||
18 | Synthetic fields may exist in the data but generally do not and require | ||
19 | additional synthesis to arrive ate a sane value. Subclasses must define | ||
20 | a formatter method that receives an API client, field name, and full data | ||
21 | payload. | ||
22 | """ | ||
23 | |||
24 | default = None | ||
25 | |||
26 | @staticmethod | ||
27 | def formatter(api_client, field, data): # pragma: no cover | ||
28 | raise NotImplementedError | ||
29 | |||
30 | |||
15 | class ModelMetaClass(type): | 31 | class ModelMetaClass(type): |
16 | 32 | ||
17 | def __new__(cls, name, parents, dct): | 33 | def __new__(cls, name, parents, dct): |
@@ -22,7 +38,7 @@ class ModelMetaClass(type): | |||
22 | if key.startswith("__"): | 38 | if key.startswith("__"): |
23 | continue | 39 | continue |
24 | 40 | ||
25 | if isinstance(val, Field): | 41 | if isinstance(val, Field) or isinstance(val, SyntheticField): |
26 | fields[key] = val | 42 | fields[key] = val |
27 | del new_dct[key] | 43 | del new_dct[key] |
28 | 44 | ||
@@ -57,6 +73,11 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): | |||
57 | for key, value in instance.__class__._fields.items(): | 73 | for key, value in instance.__class__._fields.items(): |
58 | newval = data.get(value.field, value.default) | 74 | newval = data.get(value.field, value.default) |
59 | 75 | ||
76 | if isinstance(value, SyntheticField): | ||
77 | newval = value.formatter(api_client, value.field, data, newval) | ||
78 | setattr(instance, key, newval) | ||
79 | continue | ||
80 | |||
60 | if newval and value.formatter: | 81 | if newval and value.formatter: |
61 | newval = value.formatter(api_client, newval) | 82 | newval = value.formatter(api_client, newval) |
62 | 83 | ||
diff --git a/pandora/models/pandora.py b/pandora/models/pandora.py index 455bcf3..d7d2bf1 100644 --- a/pandora/models/pandora.py +++ b/pandora/models/pandora.py | |||
@@ -1,5 +1,6 @@ | |||
1 | from ..client import BaseAPIClient | 1 | from ..client import BaseAPIClient |
2 | from ..errors import ParameterMissing | 2 | from ..errors import ParameterMissing |
3 | from . import SyntheticField | ||
3 | from . import Field, PandoraModel, PandoraListModel, PandoraDictListModel | 4 | from . import Field, PandoraModel, PandoraListModel, PandoraDictListModel |
4 | 5 | ||
5 | 6 | ||
@@ -51,42 +52,15 @@ class StationList(PandoraListModel): | |||
51 | return checksum != self.checksum | 52 | return checksum != self.checksum |
52 | 53 | ||
53 | 54 | ||
54 | class PlaylistModel(PandoraModel): | 55 | class AudioField(SyntheticField): |
55 | |||
56 | @classmethod | ||
57 | def from_json(cls, api_client, data): | ||
58 | self = cls(api_client) | ||
59 | |||
60 | for key, value in cls._fields.items(): | ||
61 | newval = data.get(value.field, value.default) | ||
62 | |||
63 | if value.field == "audioUrl" and newval is None: | ||
64 | newval = cls.get_audio_url( | ||
65 | data, api_client.default_audio_quality) | ||
66 | |||
67 | if value.field == "bitrate" and newval is None: | ||
68 | newval = cls.get_audio_bitrate( | ||
69 | data, api_client.default_audio_quality) | ||
70 | 56 | ||
71 | if value.field == "encoding" and newval is None: | 57 | @staticmethod |
72 | newval = cls.get_audio_encoding( | 58 | def formatter(api_client, field, data, value): |
73 | data, api_client.default_audio_quality) | ||
74 | |||
75 | if newval and value.formatter: | ||
76 | newval = value.formatter(newval) | ||
77 | |||
78 | setattr(self, key, newval) | ||
79 | |||
80 | return self | ||
81 | |||
82 | @classmethod | ||
83 | def get_audio_field(cls, data, field, preferred_quality): | ||
84 | """Get audio-related fields | 59 | """Get audio-related fields |
85 | 60 | ||
86 | Try to find fields for the audio url for specified preferred quality | 61 | Try to find fields for the audio url for specified preferred quality |
87 | level, or next-lowest available quality url otherwise. | 62 | level, or next-lowest available quality url otherwise. |
88 | """ | 63 | """ |
89 | audio_url = None | ||
90 | url_map = data.get("audioUrlMap") | 64 | url_map = data.get("audioUrlMap") |
91 | audio_url = data.get("audioUrl") | 65 | audio_url = data.get("audioUrl") |
92 | 66 | ||
@@ -103,8 +77,7 @@ class PlaylistModel(PandoraModel): | |||
103 | "encoding": "aacplus", | 77 | "encoding": "aacplus", |
104 | } | 78 | } |
105 | } | 79 | } |
106 | # No audio url available (e.g. ad tokens) | 80 | elif not url_map: # No audio url available (e.g. ad tokens) |
107 | elif not url_map: | ||
108 | return None | 81 | return None |
109 | 82 | ||
110 | valid_audio_formats = [BaseAPIClient.HIGH_AUDIO_QUALITY, | 83 | valid_audio_formats = [BaseAPIClient.HIGH_AUDIO_QUALITY, |
@@ -115,6 +88,7 @@ class PlaylistModel(PandoraModel): | |||
115 | # from the beginning of the list if nothing is found. Ensures that the | 88 | # from the beginning of the list if nothing is found. Ensures that the |
116 | # bitrate used will always be the same or lower quality than was | 89 | # bitrate used will always be the same or lower quality than was |
117 | # specified to prevent audio from skipping for slow connections. | 90 | # specified to prevent audio from skipping for slow connections. |
91 | preferred_quality = api_client.default_audio_quality | ||
118 | if preferred_quality in valid_audio_formats: | 92 | if preferred_quality in valid_audio_formats: |
119 | i = valid_audio_formats.index(preferred_quality) | 93 | i = valid_audio_formats.index(preferred_quality) |
120 | valid_audio_formats = valid_audio_formats[i:] | 94 | valid_audio_formats = valid_audio_formats[i:] |
@@ -127,35 +101,8 @@ class PlaylistModel(PandoraModel): | |||
127 | 101 | ||
128 | return audio_url[field] if audio_url else None | 102 | return audio_url[field] if audio_url else None |
129 | 103 | ||
130 | @classmethod | ||
131 | def get_audio_url(cls, data, | ||
132 | preferred_quality=BaseAPIClient.MED_AUDIO_QUALITY): | ||
133 | """Get audio url | ||
134 | |||
135 | Try to find audio url for specified preferred quality level, or | ||
136 | next-lowest available quality url otherwise. | ||
137 | """ | ||
138 | return cls.get_audio_field(data, "audioUrl", preferred_quality) | ||
139 | |||
140 | @classmethod | ||
141 | def get_audio_bitrate(cls, data, | ||
142 | preferred_quality=BaseAPIClient.MED_AUDIO_QUALITY): | ||
143 | """Get audio bitrate | ||
144 | |||
145 | Try to find bitrate of audio url for specified preferred quality level, | ||
146 | or next-lowest available quality url otherwise. | ||
147 | """ | ||
148 | return cls.get_audio_field(data, "bitrate", preferred_quality) | ||
149 | |||
150 | @classmethod | ||
151 | def get_audio_encoding(cls, data, | ||
152 | preferred_quality=BaseAPIClient.MED_AUDIO_QUALITY): | ||
153 | """Get audio encoding | ||
154 | 104 | ||
155 | Try to find encoding of audio url for specified preferred quality | 105 | class PlaylistModel(PandoraModel): |
156 | level, or next-lowest available quality url otherwise. | ||
157 | """ | ||
158 | return cls.get_audio_field(data, "encoding", preferred_quality) | ||
159 | 106 | ||
160 | def get_is_playable(self): | 107 | def get_is_playable(self): |
161 | if not self.audio_url: | 108 | if not self.audio_url: |
@@ -195,9 +142,9 @@ class PlaylistItem(PlaylistModel): | |||
195 | track_gain = Field("trackGain") | 142 | track_gain = Field("trackGain") |
196 | track_length = Field("trackLength") | 143 | track_length = Field("trackLength") |
197 | track_token = Field("trackToken") | 144 | track_token = Field("trackToken") |
198 | audio_url = Field("audioUrl") | 145 | audio_url = AudioField("audioUrl") |
199 | bitrate = Field("bitrate") | 146 | bitrate = AudioField("bitrate") |
200 | encoding = Field("encoding") | 147 | encoding = AudioField("encoding") |
201 | album_art_url = Field("albumArtUrl") | 148 | album_art_url = Field("albumArtUrl") |
202 | allow_feedback = Field("allowFeedback") | 149 | allow_feedback = Field("allowFeedback") |
203 | station_id = Field("stationId") | 150 | station_id = Field("stationId") |
@@ -245,7 +192,7 @@ class AdItem(PlaylistModel): | |||
245 | title = Field("title") | 192 | title = Field("title") |
246 | company_name = Field("companyName") | 193 | company_name = Field("companyName") |
247 | tracking_tokens = Field("adTrackingTokens") | 194 | tracking_tokens = Field("adTrackingTokens") |
248 | audio_url = Field("audioUrl") | 195 | audio_url = AudioField("audioUrl") |
249 | image_url = Field("imageUrl") | 196 | image_url = Field("imageUrl") |
250 | click_through_url = Field("clickThroughUrl") | 197 | click_through_url = Field("clickThroughUrl") |
251 | station_id = None | 198 | station_id = None |