pandora_api/json/station.rs
1/*!
2Station support methods.
3
4A station is a collection of one or more user-supplied seeds. Artists or tracks
5can be used as seed. Based on the seeds Pandora decides which music to play.
6*/
7// SPDX-License-Identifier: MIT AND WTFPL
8use std::collections::HashMap;
9use std::convert::TryFrom;
10
11use pandora_api_derive::PandoraJsonRequest;
12use serde::{Deserialize, Serialize};
13
14use crate::errors::Error;
15use crate::json::errors::JsonError;
16use crate::json::{PandoraJsonApiRequest, PandoraSession, Timestamp};
17
18/// Songs can be “loved” or “banned”. Both influence the music played on the
19/// station. Banned songs are never played again on this particular station.
20///
21/// | Name | Type | Description |
22/// | ------------ | ------- | ------------------ |
23/// | stationToken | string | |
24/// | trackToken | string | |
25/// | isPositive | boolean | `false` bans track |
26///
27/// ``` json
28/// {
29/// "stationToken": "374145764047334893",
30/// "trackToken": "fcc2298ec4b1c93e73ad4b2813ceca0dba565bbbe03d8a78bad65ee89a7aaf4d0b3b11954fe6ab08794283f8ef1d44bfc32ce9f8e0513bec",
31/// "isPositive": false,
32/// "userAuthToken": "XXX",
33/// "syncTime": 1404911036
34/// }
35/// ```
36#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
37#[pandora_request(encrypted = true)]
38#[serde(rename_all = "camelCase")]
39pub struct AddFeedback {
40 /// The unique id (token) for the station on which the track should be rated.
41 /// Also sometimes referred to as a stationId.
42 pub station_token: String,
43 /// The unique id (token) for the track to be rated.
44 pub track_token: String,
45 /// Whether feedback is positive (true) or negative (false).
46 pub is_positive: bool,
47}
48
49impl AddFeedback {
50 /// Create a new AddFeedback with some values.
51 pub fn new(station_token: &str, track_token: &str, is_positive: bool) -> Self {
52 Self {
53 station_token: station_token.to_string(),
54 track_token: track_token.to_string(),
55 is_positive,
56 }
57 }
58
59 /// Create a new AddFeedback with some values, and positive feedback.
60 pub fn new_positive(station_token: &str, track_token: &str) -> Self {
61 Self::new(station_token, track_token, true)
62 }
63
64 /// Create a new AddFeedback with some values, and negative feedback.
65 pub fn new_negative(station_token: &str, track_token: &str) -> Self {
66 Self::new(station_token, track_token, false)
67 }
68}
69
70///
71/// | Name | Type | Description |
72/// | ------------- | ------- | ---------------------------- |
73/// | dateCreated | object | |
74/// | musicToken | string | |
75/// | songName | string | |
76/// | totalThumbsUp | int | |
77/// | feedbackId | string | See `station-deleteFeedback` |
78/// | isPositive | boolean | |
79///
80/// ``` json
81/// {
82/// "stat": "ok",
83/// "result": {
84/// "totalThumbsDown": 4,
85/// "stationPersonalizationPercent": 57,
86/// "dateCreated": {
87/// "date": 9,
88/// "day": 3,
89/// "hours": 6,
90/// "minutes": 3,
91/// "month": 6,
92/// "seconds": 56,
93/// "time": 1404911036840,
94/// "timezoneOffset": 420,
95/// "year": 114
96/// },
97/// "albumArtUrl": "http://cont-sv5-2.pandora.com/images/public/amz/2/2/9/5/094632175922_130W_130H.jpg",
98/// "musicToken": "23234b0abdbeb37d",
99/// "songName": "Nothing Compares 2 U",
100/// "artistName": "Sinead O'Connor",
101/// "totalThumbsUp": 20,
102/// "feedbackId": "21955050420286614",
103/// "isPositive": false
104/// }
105/// }
106/// ```
107#[derive(Debug, Clone, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct AddFeedbackResponse {
110 /// Timestamp for when the bookmark was created.
111 pub date_created: Timestamp,
112 /// The unique id (token) for the artist. Artist tokens start with 'R',
113 /// composers with 'C', songs with 'S', and genres with 'G'.
114 pub music_token: String,
115 /// Total positive feedback submissions (for this user across stations? across all users?).
116 pub total_thumbs_up: u32,
117 /// Total negative feedback submissions (for this user across stations? across all users?).
118 pub total_thumbs_down: u32,
119 /// The unique id (token) for the submitted feedback.
120 pub feedback_id: String,
121 /// Whether feedback is positive (true) or negative (false).
122 pub is_positive: bool,
123 /// The name of the song being rated.
124 pub song_name: String,
125 /// The name of the artist being rated.
126 pub artist_name: String,
127 /// A link to an image of the artist.
128 pub album_art_url: String,
129 /// Unknown
130 pub station_personalization_percent: u8,
131}
132
133/// Convenience function to do a basic addFeedback call.
134pub async fn add_feedback(
135 session: &mut PandoraSession,
136 station_token: &str,
137 track_token: &str,
138 is_positive: bool,
139) -> Result<AddFeedbackResponse, Error> {
140 AddFeedback::new(station_token, track_token, is_positive)
141 .response(session)
142 .await
143}
144
145/// music-search results can be used to add new seeds to an existing station.
146///
147/// | Name | Type | Description |
148/// | stationToken | string | Existing station, see user::get_station_list() |
149/// | musicToken | string | See music::search() |
150/// ``` json
151/// {
152/// "musicToken": "R1119",
153/// "stationToken": "1181753543028256237",
154/// "userAuthToken": "XXX",
155/// "syncTime": 1404912202
156/// }
157/// ```
158#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
159#[pandora_request(encrypted = true)]
160#[serde(rename_all = "camelCase")]
161pub struct AddMusic {
162 /// The unique id (token) for the station on which the track should be rated.
163 pub station_token: String,
164 /// The unique id (token) for the artist/composer/song/genre to be added to
165 /// the station. Artist tokens start with 'R', composers with 'C', songs
166 /// with 'S', and genres with 'G'.
167 pub music_token: String,
168}
169
170impl AddMusic {
171 /// Create a new AddMusic with some values.
172 pub fn new(station_token: &str, music_token: &str) -> Self {
173 Self {
174 station_token: station_token.to_string(),
175 music_token: music_token.to_string(),
176 }
177 }
178}
179
180///
181/// | Name | Type | Description |
182/// | seedId | string | Can be used to remove seed with station::delete_music() |
183/// ``` json
184/// {
185/// "stat": "ok",
186/// "result": {
187/// "artistName": "Foo Fighters",
188/// "musicToken": "3bcf3f314419f974",
189/// "seedId": "2123197691273031149",
190/// "artUrl": "http://cont-dc6-1.pandora.com/images/public/amg/portrait/pic200/drP900/P972/P97242B3S6P.jpg"
191/// }
192/// }
193/// ```
194#[derive(Debug, Clone, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct AddMusicResponse {
197 /// The name of the artist being rated.
198 pub artist_name: String,
199 /// The unique id (token) for the music object added. Artist tokens start with 'R',
200 /// composers with 'C', songs with 'S', and genres with 'G'.
201 pub music_token: String,
202 /// Unknown
203 pub seed_id: String,
204 /// A link to an image of the added object.
205 pub art_url: String,
206}
207
208/// Convenience function to do a basic addMusic call.
209pub async fn add_music(
210 session: &mut PandoraSession,
211 station_token: &str,
212 music_token: &str,
213) -> Result<AddMusicResponse, Error> {
214 AddMusic::new(station_token, music_token)
215 .response(session)
216 .await
217}
218
219/// Stations can either be created with a musicToken obtained by Search or
220/// trackToken from playlists (Retrieve playlist). The latter needs a musicType
221/// to specify whether the track itself or its artist should be used as seed.
222///
223/// | Name | Type | Description |
224/// | trackToken | string | See Retrieve playlist |
225/// | musicType | string | “song” or “artist” (“song” for genre stations) |
226/// | musicToken | string | See Search |
227#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
228#[pandora_request(encrypted = true)]
229#[serde(rename_all = "camelCase")]
230pub struct CreateStation {
231 /// The unique id (token) for the track around which the station should
232 /// be created.
233 pub track_token: String,
234 /// Whether the artist or the song referred to by the trackToken should be
235 /// used to create the station.
236 pub music_type: MusicType,
237 /// The unique id (token) for the artist/composer/song/genre to be added to
238 /// the station. Artist tokens start with 'R', composers with 'C', songs
239 /// with 'S', and genres with 'G'.
240 pub music_token: String,
241}
242
243impl CreateStation {
244 /// Create a new station from a track, usually from a playlist.
245 pub fn new_from_track(track_token: &str, music_type: MusicType) -> Self {
246 Self {
247 track_token: track_token.to_string(),
248 music_type,
249 music_token: String::new(),
250 }
251 }
252
253 /// Create a new station from a musicToken, usually returned by a search.
254 pub fn new_from_music_token(music_token: &str) -> Self {
255 Self {
256 track_token: String::new(),
257 music_type: MusicType::Artist,
258 music_token: music_token.to_string(),
259 }
260 }
261
262 /// Create a new CreateStation for a song with some values.
263 pub fn new_from_track_song(track_token: &str) -> Self {
264 Self::new_from_track(track_token, MusicType::Song)
265 }
266
267 /// Create a new CreateStation for an artist with some values.
268 pub fn new_from_track_artist(track_token: &str) -> Self {
269 Self::new_from_track(track_token, MusicType::Artist)
270 }
271}
272
273/// Used for selecting whether a musicToken should be interpreted
274/// as referring to the associated artist or the associated song.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub enum MusicType {
278 /// Use the song referred by the musicToken
279 Song,
280 /// Use the artist for the song referred by the musicToken
281 Artist,
282}
283
284/// station.createStation has no known response
285#[derive(Debug, Clone, Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct CreateStationResponse {
288 /// The unique id (token) for the just-created station.
289 pub station_token: String,
290 /// The fields of the createStation response are unknown.
291 #[serde(flatten)]
292 pub optional: HashMap<String, serde_json::value::Value>,
293}
294
295/// Convenience function to do a basic createStation call.
296pub async fn create_station_from_track_song(
297 session: &mut PandoraSession,
298 track_token: &str,
299) -> Result<CreateStationResponse, Error> {
300 CreateStation::new_from_track_song(track_token)
301 .response(session)
302 .await
303}
304
305/// Convenience function to do a basic createStation call.
306pub async fn create_station_from_artist(
307 session: &mut PandoraSession,
308 track_token: &str,
309) -> Result<CreateStationResponse, Error> {
310 CreateStation::new_from_track_artist(track_token)
311 .response(session)
312 .await
313}
314
315/// Convenience function to do a basic createStation call.
316pub async fn create_station_from_music_token(
317 session: &mut PandoraSession,
318 music_token: &str,
319) -> Result<CreateStationResponse, Error> {
320 CreateStation::new_from_music_token(music_token)
321 .response(session)
322 .await
323}
324
325/// Feedback added by Rate track can be removed from the station.
326///
327/// | Name | Type | Description |
328/// | feedbackId | string | See Retrieve extended station information |
329/// ``` json
330/// {
331/// "feedbackId": "3738252050522320365",
332/// "userAuthToken": "XXX",
333/// "syncTime": 1404910760
334/// }
335/// ```
336#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
337#[pandora_request(encrypted = true)]
338#[serde(rename_all = "camelCase")]
339pub struct DeleteFeedback {
340 /// The unique id (token) for the feedback submission that should be deleted.
341 pub feedback_id: String,
342}
343
344impl<TS: ToString> From<&TS> for DeleteFeedback {
345 fn from(feedback_id: &TS) -> Self {
346 Self {
347 feedback_id: feedback_id.to_string(),
348 }
349 }
350}
351
352/// This method does not return data.
353#[derive(Debug, Clone, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub struct DeleteFeedbackResponse {
356 /// The fields of the deleteFeedback response are unknown.
357 #[serde(flatten)]
358 pub optional: HashMap<String, serde_json::value::Value>,
359}
360
361/// Convenience function to do a basic deleteFeedback call.
362pub async fn delete_feedback(
363 session: &mut PandoraSession,
364 feedback_id: &str,
365) -> Result<DeleteFeedbackResponse, Error> {
366 DeleteFeedback::from(&feedback_id).response(session).await
367}
368
369/// Seeds can be removed from a station, except for the last one.
370///
371/// | Name | Type | Description |
372/// | seedId | string | See Retrieve extended station information and Add seed |
373/// ``` json
374/// {
375/// "seedId": "1230715903914683885",
376/// "userAuthToken": "XXX",
377/// "syncTime": 1404912023
378/// }
379/// ```
380#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
381#[pandora_request(encrypted = true)]
382#[serde(rename_all = "camelCase")]
383pub struct DeleteMusic {
384 /// The unique id (token) for the music seed that should be deleted
385 /// from this station.
386 pub seed_id: String,
387}
388
389impl<TS: ToString> From<&TS> for DeleteMusic {
390 fn from(seed_id: &TS) -> Self {
391 Self {
392 seed_id: seed_id.to_string(),
393 }
394 }
395}
396
397/// This method does not return data.
398#[derive(Debug, Clone, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct DeleteMusicResponse {
401 /// The fields of the deleteMusic response are unknown.
402 #[serde(flatten)]
403 pub optional: HashMap<String, serde_json::value::Value>,
404}
405
406/// Convenience function to do a basic deleteMusic call.
407pub async fn delete_music(
408 session: &mut PandoraSession,
409 seed_id: &str,
410) -> Result<DeleteMusicResponse, Error> {
411 DeleteMusic::from(&seed_id).response(session).await
412}
413
414/// | Name | Type | Description |
415/// | stationToken | string | Existing station, see Retrieve station list |
416/// ``` json
417/// {
418/// "stationToken": "374145764047334893",
419/// "userAuthToken": "XXX",
420/// "syncTime": 1404911699
421/// }
422/// ```
423#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
424#[pandora_request(encrypted = true)]
425#[serde(rename_all = "camelCase")]
426pub struct DeleteStation {
427 /// The unique id (token) for the station that should be deleted.
428 pub station_token: String,
429}
430
431impl<TS: ToString> From<&TS> for DeleteStation {
432 fn from(station_token: &TS) -> Self {
433 Self {
434 station_token: station_token.to_string(),
435 }
436 }
437}
438
439/// No data is returned in response.
440#[derive(Debug, Clone, Deserialize)]
441#[serde(rename_all = "camelCase")]
442pub struct DeleteStationResponse {
443 /// The fields of the deleteStation response are unknown.
444 #[serde(flatten)]
445 pub optional: HashMap<String, serde_json::value::Value>,
446}
447
448/// Convenience function to do a basic deleteStation call.
449pub async fn delete_station(
450 session: &mut PandoraSession,
451 station_token: &str,
452) -> Result<DeleteStationResponse, Error> {
453 DeleteStation::from(&station_token).response(session).await
454}
455
456/// Check to see if the list of genre stations has changed.
457///
458/// | Name | Type | Description |
459/// | includeGenreCategoryAdUrl | bool | (optional) |
460#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
461#[pandora_request(encrypted = true)]
462#[serde(rename_all = "camelCase")]
463pub struct GetGenreStationsChecksum {
464 /// The fields of the deleteStation response are unknown.
465 #[serde(flatten)]
466 pub optional: HashMap<String, serde_json::value::Value>,
467}
468
469impl GetGenreStationsChecksum {
470 /// Create a new GetGenreStationsChecksum with some default values.
471 pub fn new() -> Self {
472 Self::default()
473 }
474
475 /// Convenience function for setting boolean flags in the request. (Chaining call)
476 pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
477 self.optional
478 .insert(option.to_string(), serde_json::value::Value::from(value));
479 self
480 }
481
482 /// Whether to request that genre category ad url should be included in the reply. (Chaining call)
483 pub fn include_genre_category_ad_url(self, value: bool) -> Self {
484 self.and_boolean_option("includeGenreCategoryAdUrl", value)
485 }
486}
487
488/// | Name | Type | Description |
489/// | checksum | string | |
490#[derive(Debug, Clone, Deserialize)]
491#[serde(rename_all = "camelCase")]
492pub struct GetGenreStationsChecksumResponse {
493 /// The checksum for the list of genre stations. This is useful to detect
494 /// when the list of genre stations has changed so that it can be requested
495 /// and refreshed for the user. This also allows for app caching of the
496 /// list across session.
497 pub checksum: String,
498}
499
500/// Convenience function to do a basic getGenreStationsChecksum call.
501pub async fn get_genre_stations_checksum(
502 session: &mut PandoraSession,
503) -> Result<GetGenreStationsChecksumResponse, Error> {
504 GetGenreStationsChecksum::default()
505 .include_genre_category_ad_url(false)
506 .response(session)
507 .await
508}
509
510/// Pandora provides a list of predefined stations ("genre stations").
511/// The request has no parameters.
512#[derive(Debug, Clone, Default, Serialize, PandoraJsonRequest)]
513#[pandora_request(encrypted = true)]
514#[serde(rename_all = "camelCase")]
515pub struct GetGenreStations {}
516
517impl GetGenreStations {
518 /// Create a new GetGenreStations.
519 pub fn new() -> Self {
520 Self::default()
521 }
522}
523
524/// Each station belongs to one category, usually a genre name. stationToken
525/// can be used as musicToken to create a new station with Create.
526///
527/// | Name | Type | Description |
528/// | categories | array | List of categories |
529/// | categories.stations | array | List of stations in category |
530/// | categories.stations.stationToken | string | Actually a musicToken, see Create |
531/// | categories.categoryName | string | Category name |
532/// ``` json
533/// {
534/// "stat": "ok",
535/// "result": {
536/// "categories": [{
537/// "stations": [{
538/// "stationToken": "G165",
539/// "stationName": "90s Alternative ",
540/// "stationId": "G165"
541/// }],
542/// "categoryName": "Alternative"
543/// }]
544/// }
545/// }
546/// ```
547#[derive(Debug, Clone, Deserialize)]
548#[serde(rename_all = "camelCase")]
549pub struct GetGenreStationsResponse {
550 /// The checksum for the list of genre stations. This is useful to detect
551 /// when the list of genre stations has changed so that it can be requested
552 /// and refreshed for the user. This also allows for app caching of the
553 /// list across session.
554 pub categories: Vec<GenreCategory>,
555}
556
557/// A collection of stations that fall in a broad genre category
558#[derive(Debug, Clone, Deserialize)]
559#[serde(rename_all = "camelCase")]
560pub struct GenreCategory {
561 /// Genre/music category name
562 pub category_name: String,
563 /// List of stations in the category
564 pub stations: Vec<GenreStation>,
565}
566
567/// A specific genre station
568#[derive(Debug, Clone, Deserialize)]
569#[serde(rename_all = "camelCase")]
570pub struct GenreStation {
571 /// Actually a musicToken, which can be used with station.createStation.
572 pub station_token: String,
573 /// User-friendly name for the station.
574 pub station_name: String,
575 /// Unknown
576 pub station_id: String,
577}
578
579/// Convenience function to do a basic getGenreStations call.
580pub async fn get_genre_stations(
581 session: &mut PandoraSession,
582) -> Result<GetGenreStationsResponse, Error> {
583 GetGenreStations::default().response(session).await
584}
585
586/// This method must be sent over a TLS-encrypted connection.
587///
588/// | Name | Type | Description |
589/// | stationToken | string | station token from Retrieve station list |
590/// | additionalAudioUrl | string | Comma separated list of additional audio formats to return. (optional) |
591/// | stationIsStarting | boolean | (optional) |
592/// | includeTrackLength | boolean | (optional) |
593/// | includeAudioToken | boolean | (optional) |
594/// | xplatformAdCapable | boolean | (optional) |
595/// | includeAudioReceiptUrl | boolean | (optional) |
596/// | includeBackstageAdUrl | boolean | (optional) |
597/// | includeSharingAdUrl | boolean | (optional) |
598/// | includeSocialAdUrl | boolean | (optional) |
599/// | includeCompetitiveSepIndicator | boolean | (optional) |
600/// | includeCompletePlaylist | boolean | (optional) |
601/// | includeTrackOptions | boolean | (optional) |
602/// | audioAdPodCapable | boolean | (optional) |
603///
604/// Valid values for additionalAudioUrl are:
605///
606/// * HTTP_40_AAC_MONO
607/// * HTTP_64_AAC
608/// * HTTP_32_AACPLUS
609/// * HTTP_64_AACPLUS
610/// * HTTP_24_AACPLUS_ADTS
611/// * HTTP_32_AACPLUS_ADTS
612/// * HTTP_64_AACPLUS_ADTS
613/// * HTTP_128_MP3
614/// * HTTP_32_WMA
615///
616/// Usually a playlist contains four tracks.
617/// ``` json
618/// {
619/// "userAuthToken": "XXX",
620/// "additionalAudioUrl": "HTTP_32_AACPLUS_ADTS,HTTP_64_AACPLUS_ADTS",
621/// "syncTime": 1335841463,
622/// "stationToken": "121193154444133035"
623/// }
624/// ```
625#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
626#[pandora_request(encrypted = true)]
627#[serde(rename_all = "camelCase")]
628pub struct GetPlaylist {
629 /// The unique id (token) for the station to request a playlist from
630 pub station_token: String,
631 /// Optional parameters on the call
632 #[serde(flatten)]
633 pub optional: HashMap<String, serde_json::value::Value>,
634}
635
636impl GetPlaylist {
637 /// Convenience function for setting boolean flags in the request. (Chaining call)
638 pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
639 self.optional
640 .insert(option.to_string(), serde_json::value::Value::from(value));
641 self
642 }
643
644 /// Additional (non-default) audio formats that should be included in the
645 /// response. Repeat call to include additional formats. (Chaining call)
646 pub fn additional_audio_url(mut self, value: &str) -> Self {
647 // TODO: Verify this logic works
648 self.optional
649 .entry("additionalAudioUrl".to_string())
650 .and_modify(|s| {
651 if let serde_json::value::Value::String(s) = s {
652 s.push(',');
653 s.push_str(value);
654 }
655 })
656 .or_insert_with(|| serde_json::value::Value::from(value));
657 self
658 }
659
660 /// Whether request should also mark the station as starting. (Chaining call)
661 pub fn station_is_starting(self, value: bool) -> Self {
662 self.and_boolean_option("stationIsStarting", value)
663 }
664
665 /// Whether playlist entries should include the track length in the response. (Chaining call)
666 pub fn include_track_length(self, value: bool) -> Self {
667 self.and_boolean_option("includeTrackLength", value)
668 }
669
670 /// Whether playlist entries should include the audio token in the response. (Chaining call)
671 pub fn include_audio_token(self, value: bool) -> Self {
672 self.and_boolean_option("includeAudioToken", value)
673 }
674
675 /// Whether the client is cross-platform ad capable. (Chaining call)
676 pub fn xplatform_ad_capable(self, value: bool) -> Self {
677 self.and_boolean_option("xplatformAdCapable", value)
678 }
679
680 /// Whether to include audio receipt url in the response. (Chaining call)
681 pub fn include_audio_receipt_url(self, value: bool) -> Self {
682 self.and_boolean_option("includeAudioReceiptUrl", value)
683 }
684
685 /// Whether to include backstage ad url in the response. (Chaining call)
686 pub fn include_backstage_ad_url(self, value: bool) -> Self {
687 self.and_boolean_option("includeBackstageAdUrl", value)
688 }
689
690 /// Whether to include sharing ad url in the response. (Chaining call)
691 pub fn include_sharing_ad_url(self, value: bool) -> Self {
692 self.and_boolean_option("includeSharingAdUrl", value)
693 }
694
695 /// Whether to include social ad url in the response. (Chaining call)
696 pub fn include_social_ad_url(self, value: bool) -> Self {
697 self.and_boolean_option("includeSocialAdUrl", value)
698 }
699
700 /// Whether to include competitive sep indicator in the response. (Chaining call)
701 pub fn include_competitive_sep_indicator(self, value: bool) -> Self {
702 self.and_boolean_option("includeCompetitiveSepIndicator", value)
703 }
704
705 /// Whether to include complete playlist in the response. (Chaining call)
706 pub fn include_complete_playlist(self, value: bool) -> Self {
707 self.and_boolean_option("includeCompletePlaylist", value)
708 }
709
710 /// Whether to include track options in the response. (Chaining call)
711 pub fn include_track_options(self, value: bool) -> Self {
712 self.and_boolean_option("includeTrackOptions", value)
713 }
714
715 /// Indicate to Pandora whether the client is audio ad pod capable. (Chaining call)
716 pub fn audio_ad_pod_capable(self, value: bool) -> Self {
717 self.and_boolean_option("audioAdPodCapable", value)
718 }
719}
720
721impl<TS: ToString> From<&TS> for GetPlaylist {
722 fn from(station_token: &TS) -> Self {
723 Self {
724 station_token: station_token.to_string(),
725 optional: HashMap::new(),
726 }
727 .additional_audio_url(&AudioFormat::Mp3128.to_string())
728 }
729}
730
731/// Valid values for additionalAudioUrl are:
732///
733/// * HTTP_40_AAC_MONO
734/// * HTTP_64_AAC
735/// * HTTP_32_AACPLUS
736/// * HTTP_64_AACPLUS
737/// * HTTP_24_AACPLUS_ADTS
738/// * HTTP_32_AACPLUS_ADTS
739/// * HTTP_64_AACPLUS_ADTS
740/// * HTTP_128_MP3
741/// * HTTP_32_WMA
742#[derive(Debug, Clone, PartialEq)]
743pub enum AudioFormat {
744 /// AAC format, monaural audio, 40kbps
745 AacMono40,
746 /// AAC format, 64kbps
747 Aac64,
748 /// AACPlus format, 32kbps
749 AacPlus32,
750 /// AACPlus format, 64kbps
751 AacPlus64,
752 /// AACPlus format in an ADTS container, 24kbps
753 AacPlusAdts24,
754 /// AACPlus format in an ADTS container, 32kbps
755 AacPlusAdts32,
756 /// AACPlus format in an ADTS container, 64kbps
757 AacPlusAdts64,
758 /// MP3 format, 128kbps
759 Mp3128,
760 /// WMA format, 32kbps
761 Wma32,
762}
763
764impl AudioFormat {
765 /// Determine the audio format from the encoding and bitrate information
766 /// returned as part of a playlist track.
767 pub fn new_from_audio_url_map(encoding: &str, bitrate: &str) -> Result<Self, Error> {
768 match (encoding, bitrate) {
769 ("aac", "64") => Ok(Self::AacPlus64),
770 ("aacplus", "32") => Ok(Self::AacPlus32),
771 ("aacplus", "64") => Ok(Self::AacPlus64),
772 _ => Err(
773 JsonError::new(None, Some(String::from("Unsupported audioUrlMap format"))).into(),
774 ),
775 }
776 }
777
778 /// Determine the associated file extension for this format.
779 pub fn get_extension(&self) -> String {
780 match self {
781 // TODO: verify container format for all aac types
782 Self::AacMono40 => String::from("m4a"),
783 Self::Aac64 => String::from("m4a"),
784 Self::AacPlus32 => String::from("m4a"),
785 Self::AacPlus64 => String::from("m4a"),
786 Self::AacPlusAdts24 => String::from("aac"),
787 Self::AacPlusAdts32 => String::from("aac"),
788 Self::AacPlusAdts64 => String::from("aac"),
789 Self::Mp3128 => String::from("mp3"),
790 Self::Wma32 => String::from("wma"),
791 }
792 }
793
794 /// Determine the encoded audio bitrate for this format.
795 pub fn get_bitrate(&self) -> u32 {
796 match self {
797 Self::AacMono40 => 40,
798 Self::Aac64 => 64,
799 Self::AacPlus32 => 32,
800 Self::AacPlus64 => 64,
801 Self::AacPlusAdts24 => 24,
802 Self::AacPlusAdts32 => 32,
803 Self::AacPlusAdts64 => 64,
804 Self::Mp3128 => 128,
805 Self::Wma32 => 32,
806 }
807 }
808
809 /// Estimator of relative audio quality. The actual numbers don't
810 /// mean anything, it's just for assigning an ordering.
811 fn get_quality_weight(&self) -> u8 {
812 match self {
813 Self::AacPlusAdts64 => 10,
814 Self::AacPlus64 => 9,
815 // MP3 at 128kbps using a high quality encoder is estimated
816 // to be equivalent to AAC-HE at 64kbps. Because we don't
817 // know the quality of the mp3 encoder, we weigh it below 64kbps
818 // AacPlus, but above 64kbps Aac.
819 // https://en.wikipedia.org/wiki/High-Efficiency_Advanced_Audio_Coding
820 Self::Mp3128 => 8,
821 Self::Aac64 => 7,
822 Self::AacPlusAdts32 => 6,
823 Self::AacPlus32 => 5,
824 Self::AacPlusAdts24 => 4,
825 // Aac is a good codec, but AacPlus holds up much better at low
826 // bitrates, plus this is monoaural.
827 Self::AacMono40 => 2,
828 // 32kbps is an incredibly low bitrate, on an old codec
829 // so this is theorized to be the lowest quality
830 Self::Wma32 => 1,
831 }
832 }
833}
834
835impl PartialOrd for AudioFormat {
836 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
837 Some(self.get_quality_weight().cmp(&other.get_quality_weight()))
838 }
839}
840
841impl std::fmt::Display for AudioFormat {
842 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
843 match self {
844 AudioFormat::AacMono40 => write!(f, "HTTP_40_AAC_MONO"),
845 AudioFormat::Aac64 => write!(f, "HTTP_64_AAC"),
846 AudioFormat::AacPlus32 => write!(f, "HTTP_32_AACPLUS"),
847 AudioFormat::AacPlus64 => write!(f, "HTTP_64_AACPLUS"),
848 AudioFormat::AacPlusAdts24 => write!(f, "HTTP_24_AACPLUS_ADTS"),
849 AudioFormat::AacPlusAdts32 => write!(f, "HTTP_32_AACPLUS_ADTS"),
850 AudioFormat::AacPlusAdts64 => write!(f, "HTTP_64_AACPLUS_ADTS"),
851 AudioFormat::Mp3128 => write!(f, "HTTP_128_MP3"),
852 AudioFormat::Wma32 => write!(f, "HTTP_32_WMA"),
853 }
854 }
855}
856
857impl TryFrom<&str> for AudioFormat {
858 type Error = Error;
859 fn try_from(fmt: &str) -> std::result::Result<Self, Self::Error> {
860 match fmt {
861 "HTTP_40_AAC_MONO" => Ok(AudioFormat::AacMono40),
862 "HTTP_64_AAC" => Ok(AudioFormat::Aac64),
863 "HTTP_32_AACPLUS" => Ok(AudioFormat::AacPlus32),
864 "HTTP_64_AACPLUS" => Ok(AudioFormat::AacPlus64),
865 "HTTP_24_AACPLUS_ADTS" => Ok(AudioFormat::AacPlusAdts24),
866 "HTTP_32_AACPLUS_ADTS" => Ok(AudioFormat::AacPlusAdts32),
867 "HTTP_64_AACPLUS_ADTS" => Ok(AudioFormat::AacPlusAdts64),
868 "HTTP_128_MP3" => Ok(AudioFormat::Mp3128),
869 "HTTP_32_WMA" => Ok(AudioFormat::Wma32),
870 x => Err(Self::Error::InvalidAudioFormat(x.to_string())),
871 }
872 }
873}
874
875impl TryFrom<String> for AudioFormat {
876 type Error = Error;
877 fn try_from(fmt: String) -> std::result::Result<Self, Self::Error> {
878 Self::try_from(fmt.as_str())
879 }
880}
881
882/// | Name | Type | Description |
883/// | items.additionalAudioUrl | array/string | List of additional audio urls in the requested order or single string if only one format was requested |
884/// | items.songRating | int | 1 if song was given a thumbs up, 0 if song was not rated yet |
885/// | items.audioUrlMap | object | Song audio format and bitrates returned differ based on what partner credentials are used. |
886/// ``` json
887/// {
888/// "stat": "ok",
889/// "result": {
890/// "items": [{
891/// "trackToken": "40b892bc5376e695c2e5c2b347227b85af2761b6aa417f736d9a79319b8f4cb97c9695a5f9a9a32aa2abaed43571235c",
892/// "artistName": "Cannabich, Christian",
893/// "albumName": "London Mozart Players, Christian Cannabich: Symphonies",
894/// "amazonAlbumUrl": "http://www.amazon.com/dp/B000GW8ATU/?tag=wwwpandoracom-20",
895/// "songExplorerUrl": "http://www.pandora.com/xml/music/song/london-mozart-players/christian-cannabich-symphonies/2-andantino?explicit=false",
896/// "albumArtUrl": "http://cont-sv5-2.pandora.com/images/public/amz/5/2/9/7/095115137925_500W_488H.jpg",
897/// "artistDetailUrl": "http://www.pandora.com/christian-cannabich?...",
898/// "audioUrlMap": {
899/// "highQuality": {
900/// "bitrate": "64",
901/// "encoding": "aacplus",
902/// "audioUrl": "http://audio-sjl-t1-2.pandora.com/access/166132182435087962.mp4?...",
903/// "protocol": "http"
904/// },
905/// "mediumQuality": {
906/// "bitrate": "64",
907/// "encoding": "aacplus",
908/// "audioUrl": "http://t1-2.cdn.pandora.com/access/4127124196771074419.mp4?...",
909/// "protocol": "http"
910/// },
911/// "lowQuality": {
912/// "bitrate": "32",
913/// "encoding": "aacplus",
914/// "audioUrl": "http://audio-sv5-t1-1.pandora.com/access/3464788359714661029.mp4?...",
915/// "protocol": "http"
916/// }
917/// },
918/// "itunesSongUrl": "http://click.linksynergy.com/fs-bin/stat?...",
919/// "additionalAudioUrl": [
920/// "http://t1-2.cdn.pandora.com/access/6705986462049243054.mp4?...",
921/// "http://audio-sjl-t1-1.pandora.com/access/2473529637452270302.mp4?..."
922/// ],
923/// "amazonAlbumAsin": "B000GW8ATU",
924/// "amazonAlbumDigitalAsin": "B003H37NN4",
925/// "artistExplorerUrl": "http://www.pandora.com/xml/music/composer/christian-cannabich?explicit=false",
926/// "songName": "Symphony In G Major",
927/// "albumDetailUrl": "http://www.pandora.com/london-mozart-players/christian-cannabich-symphonies?...",
928/// "songDetailUrl": "http://www.pandora.com/london-mozart-players/christian-cannabich-symphonies/2-andantino?...",
929/// "stationId": "121193154444133035",
930/// "songRating": 0,
931/// "trackGain": "10.09",
932/// "albumExplorerUrl": "http://www.pandora.com/xml/music/album/london-mozart-players/christian-cannabich-symphonies?explicit=false",
933/// "allowFeedback": true,
934/// "amazonSongDigitalAsin": "B003H39AGW",
935/// "nowPlayingStationAdUrl": "http://ad.doubleclick.net/pfadx/pand.android/prod.nowplaying..."
936/// }, {
937/// "adToken": "121193154444133035-none"
938/// },
939/// ]
940/// }
941/// }
942/// ```
943#[derive(Debug, Clone, Deserialize)]
944#[serde(rename_all = "camelCase")]
945pub struct GetPlaylistResponse {
946 /// Contains a list of playlist entries, each being either a song/track or
947 /// an ad.
948 pub items: Vec<PlaylistEntry>,
949}
950
951/// Responses can be either a track or an ad.
952/// The responses don't have a standard tag identifying which type it is,
953/// but ads have only one value: adToken: String.
954#[derive(Debug, Clone, Deserialize)]
955#[serde(rename_all = "camelCase", untagged)]
956pub enum PlaylistEntry {
957 /// Playlist entry representing an ad.
958 PlaylistAd(PlaylistAd),
959 /// Playlist entry representing a song/track.
960 PlaylistTrack(Box<PlaylistTrack>),
961}
962
963impl PlaylistEntry {
964 /// Returns whether the playlist entry is an ad
965 pub fn is_ad(&self) -> bool {
966 matches!(self, PlaylistEntry::PlaylistAd(_))
967 }
968
969 /// Returns whether the playlist entry is a track
970 pub fn is_track(&self) -> bool {
971 matches!(self, PlaylistEntry::PlaylistTrack(_))
972 }
973
974 /// Returns the PlaylistAd object for this entry, if any
975 pub fn get_ad(&self) -> Option<PlaylistAd> {
976 match self {
977 PlaylistEntry::PlaylistAd(a) => Some(a.clone()),
978 _ => None,
979 }
980 }
981
982 /// Returns the PlaylistTrack object for this entry, if any
983 pub fn get_track(&self) -> Option<PlaylistTrack> {
984 match self {
985 PlaylistEntry::PlaylistTrack(t) => Some(*t.clone()),
986 _ => None,
987 }
988 }
989}
990
991/// Represents an ad entry in a playlist.
992#[derive(Debug, Clone, Deserialize)]
993#[serde(rename_all = "camelCase")]
994pub struct PlaylistAd {
995 /// The unique id (token) for the ad which should be played.
996 pub ad_token: String,
997 /// Additional, optional fields in the response
998 #[serde(flatten)]
999 pub optional: HashMap<String, serde_json::value::Value>,
1000}
1001
1002/// Represents a track (song) entry in a playlist.
1003#[derive(Debug, Clone, Deserialize)]
1004#[serde(rename_all = "camelCase")]
1005pub struct PlaylistTrack {
1006 /// The unique id (token) for the track to be played.
1007 pub track_token: String,
1008 /// The music id (token) used with GetTrack to request additional track
1009 /// information.
1010 pub music_id: String,
1011 /// The unique id (token) for the station from which this track was
1012 /// requested.
1013 pub station_id: String,
1014 /// The default audio streams available for this track.
1015 pub audio_url_map: AudioQuality,
1016 /// The name of the artist for this track.
1017 pub artist_name: String,
1018 /// The name of the album for this track.
1019 pub album_name: String,
1020 /// The name of the song for this track.
1021 pub song_name: String,
1022 /// The rating of the song for this track.
1023 pub song_rating: u32,
1024 /// Additional, optional fields in the response
1025 #[serde(flatten)]
1026 pub optional: HashMap<String, serde_json::value::Value>,
1027}
1028
1029/// "lowQuality": {
1030/// "bitrate": "32",
1031/// "encoding": "aacplus",
1032/// "audioUrl": "http://audio-sv5-t1-1.pandora.com/access/3464788359714661029.mp4?...",
1033/// "protocol": "http"
1034/// }
1035#[derive(Debug, Clone, Deserialize)]
1036#[serde(rename_all = "camelCase")]
1037pub struct AudioQuality {
1038 /// Attributes for the high quality audio stream.
1039 pub high_quality: AudioStream,
1040 /// Attributes for the medium quality audio stream.
1041 pub medium_quality: AudioStream,
1042 /// Attributes for the low quality audio stream.
1043 pub low_quality: AudioStream,
1044}
1045
1046/// Playback/decoding attributes of an available audio stream.
1047#[derive(Debug, Clone, Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049pub struct AudioStream {
1050 /// The audio bitrate/quality for this stream.
1051 pub bitrate: String,
1052 /// The audio encoding format for this stream.
1053 pub encoding: String,
1054 /// The url to stream audio from.
1055 pub audio_url: String,
1056 /// The protocol to use with the audio URL.
1057 pub protocol: String,
1058}
1059
1060/// Convenience function to do a basic getPlaylist call.
1061pub async fn get_playlist(
1062 session: &mut PandoraSession,
1063 station_token: &str,
1064) -> Result<GetPlaylistResponse, Error> {
1065 GetPlaylist::from(&station_token)
1066 .station_is_starting(false)
1067 .include_track_length(false)
1068 .include_audio_token(false)
1069 .xplatform_ad_capable(false)
1070 .include_audio_receipt_url(false)
1071 .include_backstage_ad_url(false)
1072 .include_sharing_ad_url(false)
1073 .include_social_ad_url(false)
1074 .include_competitive_sep_indicator(false)
1075 .include_complete_playlist(false)
1076 .include_track_options(false)
1077 .audio_ad_pod_capable(false)
1078 .response(session)
1079 .await
1080}
1081
1082/// Extended station information includes seeds and feedback.
1083///
1084/// | Name | Type | Description |
1085/// | stationToken | string | |
1086/// | includeExtendedAttributes | bool | |
1087/// ``` json
1088/// {
1089/// "stationToken": "374145764047334893",
1090/// "includeExtendedAttributes": true,
1091/// "userAuthToken": "XXX",
1092/// "syncTime": 1404910732
1093/// }
1094/// ```
1095#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
1096#[pandora_request(encrypted = true)]
1097#[serde(rename_all = "camelCase")]
1098pub struct GetStation {
1099 /// The unique id (token) for the station to request information on.
1100 pub station_token: String,
1101 /// The fields of the createStation response are unknown.
1102 #[serde(flatten)]
1103 pub optional: HashMap<String, serde_json::value::Value>,
1104}
1105
1106impl GetStation {
1107 /// Convenience function for setting boolean flags in the request. (Chaining call)
1108 pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
1109 self.optional
1110 .insert(option.to_string(), serde_json::value::Value::from(value));
1111 self
1112 }
1113
1114 /// Whether request should include extended station attributes in the response. (Chaining call)
1115 pub fn include_extended_attributes(self, value: bool) -> Self {
1116 self.and_boolean_option("includeExtendedAttributes", value)
1117 }
1118}
1119
1120impl<TS: ToString> From<&TS> for GetStation {
1121 fn from(station_token: &TS) -> Self {
1122 GetStation {
1123 station_token: station_token.to_string(),
1124 optional: HashMap::new(),
1125 }
1126 }
1127}
1128
1129/// | Name | Type | Description |
1130/// | music | object | Station seeds, see Add seed |
1131/// | music.songs | list | Song seeds |
1132/// | music.artists | list | Artist seeds |
1133/// | feedback | object | Feedback added by Rate track |
1134/// | feedback.thumbsUp | list | |
1135/// | feedback.thumbsDown | list | |
1136/// ``` json
1137/// {
1138/// "stat": "ok",
1139/// "result": {
1140/// "suppressVideoAds": false,
1141/// "stationId": "374145764047334893",
1142/// "allowAddMusic": true,
1143/// "dateCreated": {
1144/// "date": 15,
1145/// "day": 6,
1146/// "hours": 7,
1147/// "minutes": 34,
1148/// "month": 0,
1149/// "nanos": 874000000,
1150/// "seconds": 21,
1151/// "time": 1295105661874,
1152/// "timezoneOffset": 480,
1153/// "year": 111
1154/// },
1155/// "stationDetailUrl": "https://www.pandora.com/login?target=%2Fstations%2Fc644756145fc3f5df1916901125ee697495159685ae39575",
1156/// "artUrl": "http://cont-1.p-cdn.com/images/public/amz/5/2/8/5/075678235825_500W_498H.jpg",
1157/// "requiresCleanAds": false,
1158/// "stationToken": "374145764047334893",
1159/// "stationName": "Winter Radio",
1160/// "music": {
1161/// "songs": [{
1162/// "seedId": "428301990230109677",
1163/// "artistName": "Tori Amos",
1164/// "artUrl": "http://cont-sjl-1.pandora.com/images/public/amz/5/2/8/5/075678235825_130W_130H.jpg",
1165/// "songName": "Winter",
1166/// "musicToken": "87ef9db1c3f04330"
1167/// }],
1168/// "artists": [{
1169/// "artistName": "Jason Derulo",
1170/// "musicToken": "563f577e00d837a5",
1171/// "seedId": "31525199612287328",
1172/// "artUrl": "http://mediaserver-cont-sv5-1-v4v6.pandora.com/images/public/amg/portrait/pic200/drQ300/Q366/Q36675SDAPJ.jpg"
1173/// }],
1174/// "genres": [{
1175/// "musicToken": "cc021b31a48b8acf",
1176/// "genreName": "Today's Hits",
1177/// "seedId": "31525199599467854"
1178/// }]
1179/// },
1180/// "isShared": false,
1181/// "allowDelete": true,
1182/// "genre": ["Rock"],
1183/// "isQuickMix": false,
1184/// "allowRename": true,
1185/// "stationSharingUrl": "https://www.pandora.com/login?target=%2Fshare%2Fstation%2Fc644756145fc3f5df1916901125ee697495159685ae39575",
1186/// "allowEditDescription": true,
1187/// "feedback": {
1188/// "thumbsUp": [{
1189/// "dateCreated": {
1190/// "date": 28,
1191/// "day": 5,
1192/// "hours": 13,
1193/// "minutes": 57,
1194/// "month": 2,
1195/// "nanos": 760000000,
1196/// "seconds": 49,
1197/// "time": 1396040269760,
1198/// "timezoneOffset": 420,
1199/// "year": 114
1200/// },
1201/// "albumArtUrl": "http://cont-1.p-cdn.com/images/public/amz/9/7/1/4/900004179_130W_130H.jpg",
1202/// "musicToken": "d33dd0c199ebaf28425ba2910f7abf8b",
1203/// "songName": "Hey Lover",
1204/// "artistName": "Keri Noble",
1205/// "feedbackId": "-7239441039566426643",
1206/// "isPositive": true
1207/// }],
1208/// "totalThumbsUp": 20,
1209/// "totalThumbsDown": 5,
1210/// "thumbsDown": [{
1211/// "dateCreated": {
1212/// "date": 28,
1213/// "day": 5,
1214/// "hours": 10,
1215/// "minutes": 43,
1216/// "month": 2,
1217/// "nanos": 637000000,
1218/// "seconds": 30,
1219/// "time": 1396028610637,
1220/// "timezoneOffset": 420,
1221/// "year": 114
1222/// },
1223/// "albumArtUrl": "http://cont-ch1-1.pandora.com/images/public/amz/9/0/5/1/724383771509_130W_130H.jpg",
1224/// "musicToken": "5a0018da7876f6e7",
1225/// "songName": "Talk Show Host",
1226/// "artistName": "Radiohead",
1227/// "feedbackId": "-7241622182873125395",
1228/// "isPositive": false
1229/// }]
1230/// }
1231/// }
1232/// }
1233/// ```
1234#[derive(Debug, Clone, Deserialize)]
1235#[serde(rename_all = "camelCase")]
1236pub struct GetStationResponse {
1237 /// The unique id (token) for the station for which information was
1238 /// requested. The stationId (station_id) and stationToken (station_token)
1239 /// attributes appear to be duplicates.
1240 pub station_id: String,
1241 /// The unique id (token) for the station for which information was
1242 /// requested. The stationId (station_id) and stationToken (station_token)
1243 /// attributes appear to be duplicates.
1244 pub station_token: String,
1245 /// The user-created name of the station.
1246 pub station_name: String,
1247 /// Whether the station allows adding music to it.
1248 pub allow_add_music: Option<bool>,
1249 /// Unknown
1250 pub suppress_video_ads: Option<bool>,
1251 /// When the station was created.
1252 pub date_created: Timestamp,
1253 /// Unknown
1254 pub station_detail_url: Option<String>,
1255 /// Unknown
1256 pub art_url: Option<String>,
1257 /// Unknown
1258 pub requires_clean_ads: Option<bool>,
1259 /// Station music seeds.
1260 pub music: Option<StationSeeds>,
1261 /// Whether the station is visible for sharing.
1262 pub is_shared: Option<bool>,
1263 /// Whether the station can be deleted.
1264 pub allow_delete: Option<bool>,
1265 /// The genre(s) the station belongs to.
1266 #[serde(default)]
1267 pub genre: Vec<String>,
1268 /// Whether this is a QuickMix station.
1269 pub is_quick_mix: Option<bool>,
1270 /// Whether the station may be renamed.
1271 pub allow_rename: Option<bool>,
1272 /// The URL to use for sharing this station.
1273 pub station_sharing_url: Option<String>,
1274 /// Whether the description for this station may be edited.
1275 pub allow_edit_description: Option<bool>,
1276 /// Feedback submitted for tracks on this station.
1277 pub feedback: Option<StationFeedback>,
1278}
1279
1280/// ``` json
1281/// "music": {
1282/// "songs": [],
1283/// "artists": [],
1284/// "genres": []
1285/// },
1286/// ```
1287#[derive(Debug, Clone, Deserialize)]
1288#[serde(rename_all = "camelCase")]
1289pub struct StationSeeds {
1290 /// Songs used as seeds for this station.
1291 pub songs: Vec<SongSeed>,
1292 /// Atrists used as seeds for this station.
1293 pub artists: Vec<ArtistSeed>,
1294 /// Genres used as seeds for this station.
1295 pub genres: Vec<GenreSeed>,
1296}
1297
1298/// Attributes of a song seed for a station.
1299/// ``` json
1300/// "songs": [{
1301/// "seedId": "5629501782357373",
1302/// "musicToken": "9d8f932edea76ed8425ba2910f7abf8b",
1303/// "songName": "Soul Finger",
1304/// "artistName": "The Bar-Kays",
1305/// "pandoraType": "TR",
1306/// "pandoraId": "TR:852695",
1307/// "artUrl": "http://.../081227857165_130W_130H.jpg",
1308/// }],
1309/// ```
1310#[derive(Debug, Clone, Deserialize)]
1311#[serde(rename_all = "camelCase")]
1312pub struct SongSeed {
1313 /// Unique identifier/handle for this seed.
1314 pub seed_id: String,
1315 /// Identifier for the song used for this seed.
1316 pub music_token: String,
1317 /// Name of the song used for this seed.
1318 pub song_name: String,
1319 /// Name of the artist for the song used for this seed.
1320 pub artist_name: String,
1321 /// The type of Pandora object described by the Pandora ID.
1322 pub pandora_type: String,
1323 /// An identifier for this Pandora object that is unique across all types of Pandora
1324 /// objects.
1325 pub pandora_id: String,
1326 /// Unknown
1327 pub art_url: String,
1328 /// Unknown fields in the response, if any
1329 #[serde(flatten)]
1330 pub optional: HashMap<String, serde_json::value::Value>,
1331}
1332
1333/// Attributes of an artist seed for a station.
1334/// ``` json
1335/// "artists": [{
1336/// "seedId": "5629501764244877",
1337/// "musicToken": "2858b602eb1adfa8",
1338/// "artistName": "Michael Bublé",
1339/// "pandoraType": "AR"
1340/// "pandoraId": "AR:6533",
1341/// "artUrl": "http://.../90W_90H.jpg",
1342/// "icon": {"dominantColor": "602d30","artUrl": ""},
1343/// ],}
1344/// ```
1345#[derive(Debug, Clone, Deserialize)]
1346#[serde(rename_all = "camelCase")]
1347pub struct ArtistSeed {
1348 /// Unique identifier/handle for this seed.
1349 pub seed_id: String,
1350 /// Identifier for the artist used for this seed.
1351 pub music_token: String,
1352 /// Name of the artist used for this seed.
1353 pub artist_name: String,
1354 /// The type of Pandora object described by the Pandora ID.
1355 pub pandora_type: String,
1356 /// An identifier for this Pandora object that is unique across all types of Pandora
1357 /// objects.
1358 pub pandora_id: String,
1359 /// Artist icon
1360 pub icon: HashMap<String, String>,
1361 /// Unknown fields in the response, if any
1362 #[serde(flatten)]
1363 pub optional: HashMap<String, serde_json::value::Value>,
1364}
1365
1366/// Attributes of a genre seed for a station.
1367/// ``` json
1368/// "genres": [{
1369/// "musicToken": "cc021b31a48b8acf",
1370/// "genreName": "Today's Hits",
1371/// "seedId": "31525199599467854"
1372/// }]
1373/// ```
1374#[derive(Debug, Clone, Deserialize)]
1375#[serde(rename_all = "camelCase")]
1376pub struct GenreSeed {
1377 /// Unique identifier/handle for this seed.
1378 pub seed_id: String,
1379 /// Identifier for the genre used for this seed.
1380 pub music_token: String,
1381 /// Name of the genre used for this seed.
1382 pub genre_name: String,
1383 /// Unknown fields in the response, if any
1384 #[serde(flatten)]
1385 pub optional: HashMap<String, serde_json::value::Value>,
1386}
1387
1388/// ``` json
1389/// "feedback": {
1390/// "thumbsUp": [],
1391/// "totalThumbsUp": 20,
1392/// "totalThumbsDown": 5,
1393/// "thumbsDown": []
1394/// }
1395/// ```
1396#[derive(Debug, Clone, Deserialize)]
1397#[serde(rename_all = "camelCase")]
1398pub struct StationFeedback {
1399 /// A list of positive feedback submitted to a station.
1400 pub thumbs_up: Vec<TrackFeedback>,
1401 /// The total number of positive submissions to a station.
1402 pub total_thumbs_up: u32,
1403 /// A list of negative feedback submitted to a station.
1404 pub thumbs_down: Vec<TrackFeedback>,
1405 /// The total number of negative submissions to a station.
1406 pub total_thumbs_down: u32,
1407}
1408
1409/// ``` json
1410/// "thumbsDown": [{
1411/// "dateCreated": {
1412/// "date": 28,
1413/// "day": 5,
1414/// "hours": 10,
1415/// "minutes": 43,
1416/// "month": 2,
1417/// "nanos": 637000000,
1418/// "seconds": 30,
1419/// "time": 1396028610637,
1420/// "timezoneOffset": 420,
1421/// "year": 114
1422/// },
1423/// "albumArtUrl": "http://cont-ch1-1.pandora.com/images/public/amz/9/0/5/1/724383771509_130W_130H.jpg",
1424/// "musicToken": "5a0018da7876f6e7",
1425/// "songName": "Talk Show Host",
1426/// "artistName": "Radiohead",
1427/// "feedbackId": "-7241622182873125395",
1428/// "isPositive": false
1429/// }]
1430/// ```
1431#[derive(Debug, Clone, Deserialize)]
1432#[serde(rename_all = "camelCase")]
1433pub struct TrackFeedback {
1434 /// Unique identifier/handle referring to this feedback submission.
1435 pub feedback_id: String,
1436 /// Name of the song that was rated.
1437 pub song_name: String,
1438 /// Name of the artist for the song that was rated.
1439 pub artist_name: String,
1440 /// Whether the rating is positive (true) or negative (false).
1441 pub is_positive: bool,
1442 /// A token referring to the song that was rated.
1443 pub music_token: String,
1444 /// Date the feedback was created.
1445 pub date_created: Timestamp,
1446 /// Unknown
1447 pub album_art_url: String,
1448}
1449
1450/// Convenience function to do a basic getStation call.
1451pub async fn get_station(
1452 session: &mut PandoraSession,
1453 station_token: &str,
1454) -> Result<GetStationResponse, Error> {
1455 GetStation::from(&station_token)
1456 .include_extended_attributes(false)
1457 .response(session)
1458 .await
1459}
1460
1461/// **Unsupported!**
1462/// Undocumented method
1463/// [station.publishStationShare()](https://6xq.net/pandora-apidoc/json/methods/)
1464pub struct PublishStationShareUnsupported {}
1465
1466/// | Name | Type | Description |
1467/// | stationToken | string | Existing station, see Retrieve station list |
1468/// | stationName | string | New station name |
1469#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
1470#[pandora_request(encrypted = true)]
1471#[serde(rename_all = "camelCase")]
1472pub struct RenameStation {
1473 /// The unique id (token) for the station that should be renamed.
1474 /// Also sometimes referred to as a stationId.
1475 pub station_token: String,
1476 /// The new name that should be used for this station.
1477 pub station_name: String,
1478}
1479
1480impl RenameStation {
1481 /// Create a new RenameStation with some initial values.
1482 pub fn new(station_token: &str, station_name: &str) -> Self {
1483 Self {
1484 station_token: station_token.to_string(),
1485 station_name: station_name.to_string(),
1486 }
1487 }
1488}
1489
1490/// There's no known response data to this request.
1491#[derive(Debug, Clone, Deserialize)]
1492#[serde(rename_all = "camelCase")]
1493pub struct RenameStationResponse {
1494 /// The fields of the renameStation response, if any, are unknown.
1495 #[serde(flatten)]
1496 pub optional: HashMap<String, serde_json::value::Value>,
1497}
1498
1499/// Convenience function to do a basic renameStation call.
1500pub async fn rename_station(
1501 session: &mut PandoraSession,
1502 station_token: &str,
1503 station_name: &str,
1504) -> Result<RenameStationResponse, Error> {
1505 RenameStation::new(station_token, station_name)
1506 .response(session)
1507 .await
1508}
1509
1510/// Shares a station with the specified email addresses. that emails is a string array
1511///
1512/// | Name | Type | Description |
1513/// | stationId | string | See Retrieve station list |
1514/// | stationToken | string | See Retrieve station list |
1515/// | emails | string[] | A list of emails to share the station with |
1516#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
1517#[pandora_request(encrypted = true)]
1518#[serde(rename_all = "camelCase")]
1519pub struct ShareStation {
1520 /// The unique id (token) for the station that should be shared.
1521 /// Also sometimes referred to as a stationId.
1522 pub station_id: String,
1523 /// The unique id (token) for the station that should be shared.
1524 /// Also sometimes referred to as a stationId.
1525 pub station_token: String,
1526 /// A list of emails to share the station with.
1527 pub emails: Vec<String>,
1528}
1529
1530impl ShareStation {
1531 /// Create a new RenameStation with some initial values. Call
1532 /// add_recipient() to add recipient emails to the request.
1533 pub fn new(station_id: &str, station_token: &str) -> Self {
1534 Self {
1535 station_id: station_id.to_string(),
1536 station_token: station_token.to_string(),
1537 emails: Vec::new(),
1538 }
1539 }
1540
1541 /// Add a recipient email to the request.
1542 pub fn add_recipient(&mut self, recipient: &str) {
1543 self.emails.push(recipient.to_string());
1544 }
1545}
1546
1547/// There's no known response data to this request.
1548#[derive(Debug, Clone, Deserialize)]
1549#[serde(rename_all = "camelCase")]
1550pub struct ShareStationResponse {
1551 /// The fields of the shareStation response, if any, are unknown.
1552 #[serde(flatten)]
1553 pub optional: HashMap<String, serde_json::value::Value>,
1554}
1555
1556/// Convenience function to do a basic shareStation call.
1557pub async fn share_station(
1558 session: &mut PandoraSession,
1559 station_id: &str,
1560 station_token: &str,
1561 emails: Vec<String>,
1562) -> Result<ShareStationResponse, Error> {
1563 let mut request = ShareStation::new(station_id, station_token);
1564 request.emails = emails;
1565 request.response(session).await
1566}
1567
1568/// Stations created by other users are added as reference to the user’s
1569/// station list. These stations cannot be modified (i.e. rate tracks) unless
1570/// transformed.
1571///
1572/// | Name | Type | Description |
1573/// | stationToken | string | See Retrieve station list |
1574#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
1575#[pandora_request(encrypted = true)]
1576#[serde(rename_all = "camelCase")]
1577pub struct TransformSharedStation {
1578 /// The unique id (token) for the shared station that should be converted to
1579 /// a personal station.
1580 /// Also sometimes referred to as a stationId.
1581 pub station_token: String,
1582}
1583
1584impl<TS: ToString> From<&TS> for TransformSharedStation {
1585 fn from(station_token: &TS) -> Self {
1586 Self {
1587 station_token: station_token.to_string(),
1588 }
1589 }
1590}
1591
1592/// There's no known response data to this request.
1593#[derive(Debug, Clone, Deserialize)]
1594#[serde(rename_all = "camelCase")]
1595pub struct TransformSharedStationResponse {
1596 /// The fields of the transformSharedStation response, if any, are unknown.
1597 #[serde(flatten)]
1598 pub optional: HashMap<String, serde_json::value::Value>,
1599}
1600
1601/// Convenience function to do a basic transformSharedStation call.
1602pub async fn transform_shared_station(
1603 session: &mut PandoraSession,
1604 station_token: &str,
1605) -> Result<TransformSharedStationResponse, Error> {
1606 TransformSharedStation::from(&station_token)
1607 .response(session)
1608 .await
1609}
1610
1611#[cfg(test)]
1612mod tests {
1613 use std::collections::HashSet;
1614
1615 use super::*;
1616 use crate::json::{
1617 music::search, music::ArtistMatch, tests::session_login, user::get_station_list, Partner,
1618 };
1619
1620 // TODO: share_station, transform_shared_station,
1621 #[tokio::test]
1622 async fn station_ops_test() {
1623 // TODO: ensure that the station we intend to create didn't get leaked
1624 // by a previous, failed test execution, look for stations named either
1625 // "INXS Radio" or "XSNI Radio"
1626 let partner = Partner::default();
1627 let mut session = session_login(&partner)
1628 .await
1629 .expect("Failed initializing login session");
1630
1631 let artist_search = search(&mut session, "INXS")
1632 .await
1633 .expect("Failed completing artist search request");
1634
1635 let additional_artist_search = search(&mut session, "Panic! At the Disco")
1636 .await
1637 .expect("Failed completing artist search request");
1638
1639 if let Some(ArtistMatch { music_token, .. }) = artist_search
1640 .artists
1641 .iter()
1642 .filter(|am| am.score == 100)
1643 .next()
1644 {
1645 let created_station = create_station_from_music_token(&mut session, &music_token)
1646 .await
1647 .expect("Failed creating station from search result");
1648
1649 let _renamed_station =
1650 rename_station(&mut session, &created_station.station_token, "XSNI Radio")
1651 .await
1652 .expect("Failed renaming station");
1653
1654 if let Some(ArtistMatch { music_token, .. }) = additional_artist_search
1655 .artists
1656 .iter()
1657 .filter(|am| am.score == 100)
1658 .next()
1659 {
1660 let added_music =
1661 add_music(&mut session, &created_station.station_token, music_token)
1662 .await
1663 .expect("Failed adding music to station");
1664
1665 let _del_music = delete_music(&mut session, &added_music.seed_id)
1666 .await
1667 .expect("Failed deleting music from station");
1668 }
1669
1670 let _del_station = delete_station(&mut session, &created_station.station_token)
1671 .await
1672 .expect("Failed deleting station");
1673 }
1674 }
1675
1676 /* This test is very demanding on the server, so we disable it until we want
1677 * to retest.
1678 #[tokio::test]
1679 async fn genre_stations_test() {
1680 let partner = Partner::default();
1681 let mut session = session_login(&partner).await.expect("Failed initializing login session");
1682
1683 let genre_stations = get_genre_stations(&mut session).await
1684 .expect("Failed getting genre stations");
1685
1686 let genre_stations_checksum = get_genre_stations_checksum(&mut session).await
1687 .expect("Failed getting genre stations checksum");
1688 }
1689 */
1690
1691 #[tokio::test]
1692 async fn station_feedback_test() {
1693 let partner = Partner::default();
1694 let mut session = session_login(&partner)
1695 .await
1696 .expect("Failed initializing login session");
1697
1698 for station in get_station_list(&mut session)
1699 .await
1700 .expect("Failed getting station list to look up a track to bookmark")
1701 .stations
1702 {
1703 // Look through feedback on the station and build up a list of
1704 // already-rated songs so that we don't mess with any pre-existing
1705 // ratings during this test. This also exercises get_station.
1706 let station = GetStation::from(&station.station_token)
1707 .include_extended_attributes(true)
1708 .response(&mut session)
1709 .await
1710 .expect("Failed getting station attributes");
1711
1712 let mut protected_tracks: HashSet<String> = HashSet::new();
1713 protected_tracks.extend(
1714 station
1715 .feedback
1716 .iter()
1717 .flat_map(|f| f.thumbs_up.iter())
1718 .map(|tf| tf.song_name.clone()),
1719 );
1720 protected_tracks.extend(
1721 station
1722 .feedback
1723 .iter()
1724 .flat_map(|f| f.thumbs_down.iter())
1725 .map(|tf| tf.song_name.clone()),
1726 );
1727
1728 for track in get_playlist(&mut session, &station.station_token)
1729 .await
1730 .expect("Failed completing request for playlist")
1731 .items
1732 .iter()
1733 .flat_map(|p| p.get_track())
1734 {
1735 if protected_tracks.contains(&track.song_name) {
1736 continue;
1737 }
1738
1739 // Thumbs-up track
1740 let feedback = add_feedback(
1741 &mut session,
1742 &station.station_token,
1743 &track.track_token,
1744 true,
1745 )
1746 .await
1747 .expect("Failed adding positive feedback to track");
1748 // And delete
1749 let _del_feedback = delete_feedback(&mut session, &feedback.feedback_id)
1750 .await
1751 .expect("Failed deleting positive feedback from track");
1752 // Thumbs-down track
1753 let feedback = add_feedback(
1754 &mut session,
1755 &station.station_token,
1756 &track.track_token,
1757 false,
1758 )
1759 .await
1760 .expect("Failed adding negative feedback to track");
1761 // And delete
1762 let _del_feedback = delete_feedback(&mut session, &feedback.feedback_id)
1763 .await
1764 .expect("Failed deleting negative feedback from track");
1765
1766 // Finished test, stop looping through
1767 return;
1768 }
1769 }
1770 panic!("Station list request returned no results, so no feedback-capable content.");
1771 }
1772}