aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2017-10-07 04:34:52 +0000
committerMike Crute <mike@crute.us>2017-10-07 04:37:42 +0000
commit954816d5c27fd03e2896e7d79b7cb1175bb037ce (patch)
tree1556f9f162f5852d111d2e64aa1811649f6c6fd1
parent488b22889733d8c3b4ac4f8c7c73c72379e64132 (diff)
downloadpydora-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__.py23
-rw-r--r--pandora/models/pandora.py75
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
15class 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
15class ModelMetaClass(type): 31class 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 @@
1from ..client import BaseAPIClient 1from ..client import BaseAPIClient
2from ..errors import ParameterMissing 2from ..errors import ParameterMissing
3from . import SyntheticField
3from . import Field, PandoraModel, PandoraListModel, PandoraDictListModel 4from . 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
54class PlaylistModel(PandoraModel): 55class 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 105class 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