pandora_api/json/
ad.rs

1/*!
2Ad support methods.
3
4Pandora is ad-free for Pandora One users. For all other account types, the
5playlist returned by (TODO: link to station::get_playlist()) Retrieve playlist
6or (TODO: link to auth::user_login()) User login will contain tracks with
7adToken values for the advertisements that should be played (if audioUrl is
8provided) or displayed using imageUrl and bannerAdMap.
9*/
10// SPDX-License-Identifier: MIT AND WTFPL
11use std::collections::HashMap;
12
13use pandora_api_derive::PandoraJsonRequest;
14use serde::{Deserialize, Serialize};
15
16use crate::errors::Error;
17use crate::json::{PandoraJsonApiRequest, PandoraSession};
18
19/// Retrieve the metadata for the associated advertisement token (usually provided by one of the other methods responsible for retrieving the playlist).
20///
21/// | Name | Type | Description |
22/// | ---- | ---- | ----------- |
23/// | adToken | string | The adToken to retrieve the metadata for. (see Retrieve playlist) |
24/// | returnAdTrackingTokens | boolean | (optional - but the adTrackingTokens are required by Register advertisement ) |
25/// | supportAudioAds | boolean | audioUrl links for the ads are included in the results if set to ‘True’. (optional) |
26/// | includeBannerAd | boolean | bannerAdMap containing an HTML fragment that can be embedded in web pages is included in the results if set to ‘True’. (optional) |
27#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
28#[pandora_request(encrypted = true)]
29#[serde(rename_all = "camelCase")]
30pub struct GetAdMetadata {
31    /// The ad token associated with the ad for which metadata is being requested.
32    pub ad_token: String,
33    /// Optional parameters on the call
34    #[serde(flatten)]
35    pub optional: HashMap<String, serde_json::value::Value>,
36}
37
38impl GetAdMetadata {
39    /// Convenience function for setting boolean flags in the request. (Chaining call)
40    pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
41        self.optional
42            .insert(option.to_string(), serde_json::value::Value::from(value));
43        self
44    }
45
46    /// Whether request should include ad tracking tokens in the response. (Chaining call)
47    pub fn return_ad_tracking_tokens(self, value: bool) -> Self {
48        self.and_boolean_option("returnAdTrackingTokens", value)
49    }
50
51    /// Inform pandora whether audio ads are supported. (Chaining call)
52    pub fn support_audio_ads(self, value: bool) -> Self {
53        self.and_boolean_option("supportAudioAds", value)
54    }
55
56    /// Whether request should include banner ads in the response. (Chaining call)
57    pub fn include_banner_ad(self, value: bool) -> Self {
58        self.and_boolean_option("includeBannerAd", value)
59    }
60}
61
62impl<TS: ToString> From<&TS> for GetAdMetadata {
63    fn from(ad_token: &TS) -> Self {
64        Self {
65            ad_token: ad_token.to_string(),
66            optional: HashMap::new(),
67        }
68    }
69}
70
71/// ``` json
72/// {
73///    "stat": "ok",
74///    "result": {
75///        "clickThroughUrl": "http://adclick.g.doubleclick.net/aclk?sa=L&ai=BN-k_Zu53Vsr-F8-MlALj2rngAdjY8PcIAAAAEAEgADgAWPiivbTzAmDJBoIBF2NhLXB1Yi0yMTY0NjIyMzg3Njg3ODgysgEYd3d3LmRjbGstZGVmYXVsdC1yZWYuY29tugEJZ2ZwX2ltYWdlyAEJ2gEgaHR0cDovL3d3dy5kY2xrLWRlZmF1bHQtcmVmLmNvbS-YApNYwAIC4AIA6gIhNDIwNC9wYW5kLmFuZHJvaWQvcHJvZC5ub3dwbGF5aW5n-AKB0h6QA6QDmAOkA6gDAeAEAaAGINgHAA&num=0&sig=AOD64_1dqywjcCPaB_sDzcmIjy7yPRJRbQ&client=ca-pub-2164622387687882&adurl=https://www.att.com/shop/wireless/devices/prepaidphones.html",
76///        "imageUrl": "http://cont-1.p-cdn.com/images/public/devicead/g/e/3/f/daarv2828725klf3eg_500W_500H.jpg",
77///        "audioUrlMap": {
78///            "highQuality": {
79///                "bitrate": "64",
80///                "encoding": "aacplus",
81///                "audioUrl": "http://audio-ch1-t2-1-v4v6.pandora.com/access/4070162610719767146.mp4?version=4&lid=1797938999&token=CQ7xvDEck%2FutSGT4CwBfabSJD9DGqEv%2Bl5etfRYIcRtr6aQHd4ske3UE2%2FqzigYDNXjm6Mnh8CECeE%2F%2BQOGhTLY2zKBF260WCb7gTEgdPyFZOLSWfwV6Pi%2FPkF0BtBFGaCmIRLeo0H%2Fu3gyLDuySYPeIBO36SCttM%2B%2BriDe0IDv8EqoAj6BbM3frQiXF3vh%2BNCQoHBBrhLLaqocNu1pAOajQgyMGHMBy%2BKW8%2BhdRPr656jh81KwV%2FcUz%2BX%2Bri0udeRI8iSWR1bewgJdGtMQe3pzSZ1w3V16DAk%2Bi2hTOJXGCdNOLPQjC1GUBKVhdRJTU0uXk9dE8a%2Bn%2Bp2kuMcnRqaXro9Ya%2Ff4U0676v0JwseMng%2FGQp9ehJlbPzwtx5n0H",
82///                "protocol": "http"
83///            },
84///            "mediumQuality": {
85///                "bitrate": "64",
86///                "encoding": "aacplus",
87///                "audioUrl": "http://audio-ch1-t2-2-v4v6.pandora.com/access/2108163933346668833.mp4?version=4&lid=1797938999&token=CQ7xvDEck%2FutSGT4CwBfabSJD9DGqEv%2Bl5etfRYIcRtr6aQHd4ske3UE2%2FqzigYDNXjm6Mnh8CECeE%2F%2BQOGhTLY2zKBF260WCb7gTEgdPyFZOLSWfwV6Pi%2FPkF0BtBFGaCmIRLeo0H%2Fu3gyLDuySYPeIBO36SCttM%2B%2BriDe0IDv8EqoAj6BbM3frQiXF3vh%2BNCQoHBBrhLLaqocNu1pAOajQgyMGHMBy%2BKW8%2BhdRPr656jh81KwV%2FcUz%2BX%2Bri0udeRI8iSWR1bewgJdGtMQe3pzSZ1w3V16DAk%2Bi2hTOJXGCdNOLPQjC1GUBKVhdRJTU0uXk9dE8a%2Bn%2Bp2kuMcnRqaXro9Ya%2Ff4U0676v0JwseMng%2FGQp9ehJlbPzwtx5n0H",
88///                "protocol": "http"
89///            },
90///            "lowQuality": {
91///                "bitrate": "32",
92///                "encoding": "aacplus",
93///                "audioUrl": "http://audio-sv5-t2-1-v4v6.pandora.com/access/226734167372417065.mp4?version=4&lid=1797938999&token=CQ7xvDEck%2FutSGT4CwBfabSJD9DGqEv%2Bl5etfRYIcRtr6aQHd4ske3UE2%2FqzigYDSj6TIFMvq1a13lVZ0wkrCiMwbctJJs%2BhvJ17tqP3A9ul0dtwC0a%2B6wUWZ2h8MX4gC%2B96puCfQBcEH0hgBBlNTn%2F21lc2gGheE1ls6fAfUXa6P%2FoNRYtruiAJ%2Bne99iqzUCVNGl1Tyolgep7izpcdT4k86qVYiSfhTlXG8HatSCco0hkoqgi8JjFG00WXvx1eWJfBdZQ%2B2h9CBArHUbzIqs59BsFo%2Fq4oFOmAm2dVGZjEnZbQURqPpFFU08iw2tZP2t7lrh%2Bpeqvpe9rpz3g%2BQcC13H0vHTyhrD7esVz3ifAVb5IbjE4tSOCWqkuvRTi9",
94///                "protocol": "http"
95///            }
96///        },
97///        "adTrackingTokens": [
98///            "ADU-1797938999-42-232-pod/1/1/0--0-1450700391437",
99///            "ADU-1797938999-42-232-pod/1/1/0--1-1450700391437"
100///        ],
101///        "bannerAdMap": {
102///            "html": "\n\t\t<!-- xplatformAudioAdWith300x250Banner -->\n\t\t<body style=\"padding:0px;margin-left:0px;margin-right:0px;margin-top:0px;margin-bottom:0px;background-color:transparent;text-align:center\">\n\t\t\t<script type='text/javascript'>\n\t\t\t\tvar withoutBorderWeb = '<a href=\"http://adclick.g.doubleclick.net/aclk?sa=L&ai=BN-k_Zu53Vsr-F8-MlALj2rngAdjY8PcIAAAAEAEgADgAWPiivbTzAmDJBoIBF2NhLXB1Yi0yMTY0NjIyMzg3Njg3ODgysgEYd3d3LmRjbGstZGVmYXVsdC1yZWYuY29tugEJZ2ZwX2ltYWdlyAEJ2gEgaHR0cDovL3d3dy5kY2xrLWRlZmF1bHQtcmVmLmNvbS-YApNYwAIC4AIA6gIhNDIwNC9wYW5kLmFuZHJvaWQvcHJvZC5ub3dwbGF5aW5n-AKB0h6QA6QDmAOkA6gDAeAEAaAGINgHAA&num=0&sig=AOD64_1dqywjcCPaB_sDzcmIjy7yPRJRbQ&client=ca-pub-2164622387687882&adurl=https://www.att.com/shop/wireless/devices/prepaidphones.html\" target=\"_blank\"><img src=\"http://www.pandora.com/util/mediaserverPublicRedirect.jsp?type=file&file=ads/d/2015/12/5/2/7/828725/asset_750814.jpg\" width=\"300\" height=\"250\" border=\"0\" /></a>';\n\t\t\t\t\tvar withoutBorderMobile = withoutBorderWeb;\n\t\t\t\tvar withBorderMobile = '<table width=\"320\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\"><tr><td colspan=\"3\"><img src=\"http://www.pandora.com/static/ads/mobile_300x250_template/shell300x250_01_top.png\" name=\"BorderTop\" width=\"320\" height=\"11\" id=\"BorderTop\" /></td></tr><tr><td width=\"10\"><img src=\"http://www.pandora.com/static/ads/mobile_300x250_template/shell300x250_02_left.png\" name=\"BorderLeft\" width=\"10\" height=\"250\" id=\"BorderLeft\" /></td><td>' + withoutBorderMobile + '</td><td width=\"10\"><img src=\"http://www.pandora.com/static/ads/mobile_300x250_template/shell300x250_04_rght.png\" name=\"BorderRight\" width=\"10\" height=\"250\" id=\"BorderRight\" /></td></tr><tr><td colspan=\"3\"><img src=\"http://www.pandora.com/static/ads/mobile_300x250_template/shell300x250_05_bttm.png\" name=\"BorderBottom\" width=\"320\" height=\"11\" id=\"BorderBottom\" /></td></tr></table>';\n\t\t\t\tif (typeof PandoraApp == \"object\") {\n\t\t\t\t\tvar isIPad =navigator.userAgent.match(/iPad/i);\n\t\t\t\t\tif (isIPad) {\n\t\t\t\t\t\tdocument.write(withoutBorderMobile);\n\t\t\t\t\t\tPandoraApp.setViewportHeight(250);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdocument.write(withBorderMobile);\n\t\t\t\t\t\tPandoraApp.setViewportHeight(272);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tmedium_rectangle();\n                    if(parent.setActiveStyleSheet) parent.setActiveStyleSheet(\"default\");\n\t\t\t\t\tdocument.write(withoutBorderWeb);\n\t\t\t\t}\n\t\t\t</script>\n\t\t</body>\n                            "
103///        },
104///        "companyName": "",
105///        "trackGain": "0.0",
106///        "title": ""
107///    }
108///}
109///```
110#[derive(Debug, Clone, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct GetAdMetadataResponse {
113    /// Unknown field.
114    pub ad_tracking_tokens: Vec<String>,
115    /// Unknown field.
116    #[serde(default)]
117    pub audio_url_map: HashMap<String, AudioStream>,
118    /// Unknown field.
119    #[serde(default)]
120    pub banner_ad_map: HashMap<String, String>,
121    /// Additional, optional fields in the response
122    #[serde(flatten)]
123    pub optional: HashMap<String, serde_json::value::Value>,
124}
125
126/// A description of an audio stream.  Where to get it, and how to decode it.
127#[derive(Debug, Clone, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct AudioStream {
130    /// The bitrate for this audio stream.
131    pub bitrate: String,
132    /// The encoding used for this audio stream.
133    pub encoding: String,
134    /// The url from which the audio may be streamed.
135    pub audio_url: String,
136    /// The url scheme/protocol for the stream transport.
137    pub protocol: String,
138}
139
140/// Convenience function to do a basic getAdMetadata call.
141pub async fn get_ad_metadata(
142    session: &mut PandoraSession,
143    ad_token: &str,
144) -> Result<GetAdMetadataResponse, Error> {
145    GetAdMetadata::from(&ad_token)
146        .return_ad_tracking_tokens(false)
147        .support_audio_ads(false)
148        .include_banner_ad(false)
149        .response(session)
150        .await
151}
152
153/// Register the tracking tokens associated with the advertisement. The theory is that this should be done just as the advertisement is about to play.
154///
155/// | Name | Type | Description |
156/// | stationId | string | The ID of an existing station (see Retrieve extended station information) to register the ads against (optional) |
157/// | adTrackingTokens | string | The tokens of the ads to register (see Retrieve ad metadata) |
158/// ``` json
159/// {
160///    "stat": "ok"
161/// }
162/// ```
163#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
164#[pandora_request(encrypted = true)]
165#[serde(rename_all = "camelCase")]
166pub struct RegisterAd {
167    /// The station id token that the ad is associated with.
168    pub station_id: String,
169    /// The ad tracking tokens for the ad.
170    pub ad_tracking_tokens: Vec<String>,
171}
172
173impl RegisterAd {
174    /// Add a tracking token to the list of ad tracking tokens for this request. (Chaining call)
175    pub fn and_tracking_token(mut self, token: &str) -> Self {
176        self.ad_tracking_tokens.push(token.to_string());
177        self
178    }
179}
180
181impl<TS: ToString> From<&TS> for RegisterAd {
182    fn from(station_id: &TS) -> Self {
183        Self {
184            station_id: station_id.to_string(),
185            ad_tracking_tokens: Vec::new(),
186        }
187    }
188}
189
190/// There's no known response to data to this request.
191#[derive(Debug, Clone, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct RegisterAdResponse {
194    /// The fields of the registerAd response are unknown.
195    #[serde(flatten)]
196    pub optional: HashMap<String, serde_json::value::Value>,
197}
198
199/// Convenience function to do a basic registerAd call.
200pub async fn register_ad(
201    session: &mut PandoraSession,
202    station_id: &str,
203    ad_tracking_tokens: Vec<String>,
204) -> Result<RegisterAdResponse, Error> {
205    let mut request = RegisterAd::from(&station_id);
206    request.ad_tracking_tokens = ad_tracking_tokens;
207    request.response(session).await
208}
209
210#[cfg(test)]
211mod tests {
212    use crate::json::{
213        station::get_playlist, tests::session_login, user::get_station_list, Partner,
214    };
215
216    use super::*;
217
218    #[tokio::test]
219    async fn ad_test() {
220        let partner = Partner::default();
221        let mut session = session_login(&partner)
222            .await
223            .expect("Failed initializing login session");
224
225        for station in get_station_list(&mut session)
226            .await
227            .expect("Failed getting station list to look up a track to bookmark")
228            .stations
229        {
230            for ad in get_playlist(&mut session, &station.station_token)
231                .await
232                .expect("Failed completing request for playlist")
233                .items
234                .iter()
235                .flat_map(|p| p.get_ad())
236            {
237                let ad_metadata = GetAdMetadata::from(&ad.ad_token)
238                    .return_ad_tracking_tokens(true)
239                    .support_audio_ads(true)
240                    .include_banner_ad(true)
241                    .response(&mut session)
242                    .await
243                    .expect("Failed getting ad metadata");
244
245                if !ad_metadata.ad_tracking_tokens.is_empty() {
246                    let _ad_registered = register_ad(
247                        &mut session,
248                        &station.station_id,
249                        ad_metadata.ad_tracking_tokens,
250                    )
251                    .await
252                    .expect("Failed registering ad");
253                }
254            }
255        }
256    }
257}