Skip to main content

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(JsonError::new(
773                None,
774                Some(String::from("Unsupported audioUrlMap format")),
775            ).into()),
776        }
777    }
778
779    /// Determine the associated file extension for this format.
780    pub fn get_extension(&self) -> String {
781        match self {
782            // TODO: verify container format for all aac types
783            Self::AacMono40 => String::from("m4a"),
784            Self::Aac64 => String::from("m4a"),
785            Self::AacPlus32 => String::from("m4a"),
786            Self::AacPlus64 => String::from("m4a"),
787            Self::AacPlusAdts24 => String::from("aac"),
788            Self::AacPlusAdts32 => String::from("aac"),
789            Self::AacPlusAdts64 => String::from("aac"),
790            Self::Mp3128 => String::from("mp3"),
791            Self::Wma32 => String::from("wma"),
792        }
793    }
794
795    /// Determine the encoded audio bitrate for this format.
796    pub fn get_bitrate(&self) -> u32 {
797        match self {
798            Self::AacMono40 => 40,
799            Self::Aac64 => 64,
800            Self::AacPlus32 => 32,
801            Self::AacPlus64 => 64,
802            Self::AacPlusAdts24 => 24,
803            Self::AacPlusAdts32 => 32,
804            Self::AacPlusAdts64 => 64,
805            Self::Mp3128 => 128,
806            Self::Wma32 => 32,
807        }
808    }
809
810    /// Estimator of relative audio quality. The actual numbers don't
811    /// mean anything, it's just for assigning an ordering.
812    fn get_quality_weight(&self) -> u8 {
813        match self {
814            Self::AacPlusAdts64 => 10,
815            Self::AacPlus64 => 9,
816            // MP3 at 128kbps using a high quality encoder is estimated
817            // to be equivalent to AAC-HE at 64kbps.  Because we don't
818            // know the quality of the mp3 encoder, we weigh it below 64kbps
819            // AacPlus, but above 64kbps Aac.
820            // https://en.wikipedia.org/wiki/High-Efficiency_Advanced_Audio_Coding
821            Self::Mp3128 => 8,
822            Self::Aac64 => 7,
823            Self::AacPlusAdts32 => 6,
824            Self::AacPlus32 => 5,
825            Self::AacPlusAdts24 => 4,
826            // Aac is a good codec, but AacPlus holds up much better at low
827            // bitrates, plus this is monoaural.
828            Self::AacMono40 => 2,
829            // 32kbps is an incredibly low bitrate, on an old codec
830            // so this is theorized to be the lowest quality
831            Self::Wma32 => 1,
832        }
833    }
834}
835
836impl PartialOrd for AudioFormat {
837    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
838        Some(self.get_quality_weight().cmp(&other.get_quality_weight()))
839    }
840}
841
842impl std::fmt::Display for AudioFormat {
843    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
844        match self {
845            AudioFormat::AacMono40 => write!(f, "HTTP_40_AAC_MONO"),
846            AudioFormat::Aac64 => write!(f, "HTTP_64_AAC"),
847            AudioFormat::AacPlus32 => write!(f, "HTTP_32_AACPLUS"),
848            AudioFormat::AacPlus64 => write!(f, "HTTP_64_AACPLUS"),
849            AudioFormat::AacPlusAdts24 => write!(f, "HTTP_24_AACPLUS_ADTS"),
850            AudioFormat::AacPlusAdts32 => write!(f, "HTTP_32_AACPLUS_ADTS"),
851            AudioFormat::AacPlusAdts64 => write!(f, "HTTP_64_AACPLUS_ADTS"),
852            AudioFormat::Mp3128 => write!(f, "HTTP_128_MP3"),
853            AudioFormat::Wma32 => write!(f, "HTTP_32_WMA"),
854        }
855    }
856}
857
858impl TryFrom<&str> for AudioFormat {
859    type Error = Error;
860    fn try_from(fmt: &str) -> std::result::Result<Self, Self::Error> {
861        match fmt {
862            "HTTP_40_AAC_MONO" => Ok(AudioFormat::AacMono40),
863            "HTTP_64_AAC" => Ok(AudioFormat::Aac64),
864            "HTTP_32_AACPLUS" => Ok(AudioFormat::AacPlus32),
865            "HTTP_64_AACPLUS" => Ok(AudioFormat::AacPlus64),
866            "HTTP_24_AACPLUS_ADTS" => Ok(AudioFormat::AacPlusAdts24),
867            "HTTP_32_AACPLUS_ADTS" => Ok(AudioFormat::AacPlusAdts32),
868            "HTTP_64_AACPLUS_ADTS" => Ok(AudioFormat::AacPlusAdts64),
869            "HTTP_128_MP3" => Ok(AudioFormat::Mp3128),
870            "HTTP_32_WMA" => Ok(AudioFormat::Wma32),
871            x => Err(Self::Error::InvalidAudioFormat(x.to_string())),
872        }
873    }
874}
875
876impl TryFrom<String> for AudioFormat {
877    type Error = Error;
878    fn try_from(fmt: String) -> std::result::Result<Self, Self::Error> {
879        Self::try_from(fmt.as_str())
880    }
881}
882
883/// | Name | Type | Description |
884/// | items.additionalAudioUrl | array/string | List of additional audio urls in the requested order or single string if only one format was requested |
885/// | items.songRating | int | 1 if song was given a thumbs up, 0 if song was not rated yet |
886/// | items.audioUrlMap | object | Song audio format and bitrates returned differ based on what partner credentials are used. |
887/// ``` json
888/// {
889///      "stat": "ok",
890///      "result": {
891///          "items": [{
892///              "trackToken": "40b892bc5376e695c2e5c2b347227b85af2761b6aa417f736d9a79319b8f4cb97c9695a5f9a9a32aa2abaed43571235c",
893///              "artistName": "Cannabich, Christian",
894///              "albumName": "London Mozart Players, Christian Cannabich: Symphonies",
895///              "amazonAlbumUrl": "http://www.amazon.com/dp/B000GW8ATU/?tag=wwwpandoracom-20",
896///              "songExplorerUrl": "http://www.pandora.com/xml/music/song/london-mozart-players/christian-cannabich-symphonies/2-andantino?explicit=false",
897///              "albumArtUrl": "http://cont-sv5-2.pandora.com/images/public/amz/5/2/9/7/095115137925_500W_488H.jpg",
898///              "artistDetailUrl": "http://www.pandora.com/christian-cannabich?...",
899///              "audioUrlMap": {
900///                  "highQuality": {
901///                      "bitrate": "64",
902///                      "encoding": "aacplus",
903///                      "audioUrl": "http://audio-sjl-t1-2.pandora.com/access/166132182435087962.mp4?...",
904///                      "protocol": "http"
905///                  },
906///                  "mediumQuality": {
907///                      "bitrate": "64",
908///                      "encoding": "aacplus",
909///                      "audioUrl": "http://t1-2.cdn.pandora.com/access/4127124196771074419.mp4?...",
910///                      "protocol": "http"
911///                  },
912///                  "lowQuality": {
913///                      "bitrate": "32",
914///                      "encoding": "aacplus",
915///                      "audioUrl": "http://audio-sv5-t1-1.pandora.com/access/3464788359714661029.mp4?...",
916///                      "protocol": "http"
917///                  }
918///              },
919///              "itunesSongUrl": "http://click.linksynergy.com/fs-bin/stat?...",
920///              "additionalAudioUrl": [
921///                  "http://t1-2.cdn.pandora.com/access/6705986462049243054.mp4?...",
922///                  "http://audio-sjl-t1-1.pandora.com/access/2473529637452270302.mp4?..."
923///              ],
924///              "amazonAlbumAsin": "B000GW8ATU",
925///              "amazonAlbumDigitalAsin": "B003H37NN4",
926///              "artistExplorerUrl": "http://www.pandora.com/xml/music/composer/christian-cannabich?explicit=false",
927///              "songName": "Symphony In G Major",
928///              "albumDetailUrl": "http://www.pandora.com/london-mozart-players/christian-cannabich-symphonies?...",
929///              "songDetailUrl": "http://www.pandora.com/london-mozart-players/christian-cannabich-symphonies/2-andantino?...",
930///              "stationId": "121193154444133035",
931///              "songRating": 0,
932///              "trackGain": "10.09",
933///              "albumExplorerUrl": "http://www.pandora.com/xml/music/album/london-mozart-players/christian-cannabich-symphonies?explicit=false",
934///              "allowFeedback": true,
935///              "amazonSongDigitalAsin": "B003H39AGW",
936///              "nowPlayingStationAdUrl": "http://ad.doubleclick.net/pfadx/pand.android/prod.nowplaying..."
937///          }, {
938///              "adToken": "121193154444133035-none"
939///          },
940///          ]
941///      }
942/// }
943/// ```
944#[derive(Debug, Clone, Deserialize)]
945#[serde(rename_all = "camelCase")]
946pub struct GetPlaylistResponse {
947    /// Contains a list of playlist entries, each being either a song/track or
948    /// an ad.
949    pub items: Vec<PlaylistEntry>,
950}
951
952/// Responses can be either a track or an ad.
953/// The responses don't have a standard tag identifying which type it is,
954/// but ads have only one value: adToken: String.
955#[derive(Debug, Clone, Deserialize)]
956#[serde(rename_all = "camelCase", untagged)]
957pub enum PlaylistEntry {
958    /// Playlist entry representing an ad.
959    PlaylistAd(PlaylistAd),
960    /// Playlist entry representing a song/track.
961    PlaylistTrack(Box<PlaylistTrack>),
962}
963
964impl PlaylistEntry {
965    /// Returns whether the playlist entry is an ad
966    pub fn is_ad(&self) -> bool {
967        matches!(self, PlaylistEntry::PlaylistAd(_))
968    }
969
970    /// Returns whether the playlist entry is a track
971    pub fn is_track(&self) -> bool {
972        matches!(self, PlaylistEntry::PlaylistTrack(_))
973    }
974
975    /// Returns the PlaylistAd object for this entry, if any
976    pub fn get_ad(&self) -> Option<PlaylistAd> {
977        match self {
978            PlaylistEntry::PlaylistAd(a) => Some(a.clone()),
979            _ => None,
980        }
981    }
982
983    /// Returns the PlaylistTrack object for this entry, if any
984    pub fn get_track(&self) -> Option<PlaylistTrack> {
985        match self {
986            PlaylistEntry::PlaylistTrack(t) => Some(*t.clone()),
987            _ => None,
988        }
989    }
990}
991
992/// Represents an ad entry in a playlist.
993#[derive(Debug, Clone, Deserialize)]
994#[serde(rename_all = "camelCase")]
995pub struct PlaylistAd {
996    /// The unique id (token) for the ad which should be played.
997    pub ad_token: String,
998    /// Additional, optional fields in the response
999    #[serde(flatten)]
1000    pub optional: HashMap<String, serde_json::value::Value>,
1001}
1002
1003/// Represents a track (song) entry in a playlist.
1004#[derive(Debug, Clone, Deserialize)]
1005#[serde(rename_all = "camelCase")]
1006pub struct PlaylistTrack {
1007    /// The unique id (token) for the track to be played.
1008    pub track_token: String,
1009    /// The music id (token) used with GetTrack to request additional track
1010    /// information.
1011    pub music_id: String,
1012    /// The unique id (token) for the station from which this track was
1013    /// requested.
1014    pub station_id: String,
1015    /// The default audio streams available for this track.
1016    pub audio_url_map: AudioQuality,
1017    /// The name of the artist for this track.
1018    pub artist_name: String,
1019    /// The name of the album for this track.
1020    pub album_name: String,
1021    /// The name of the song for this track.
1022    pub song_name: String,
1023    /// The rating of the song for this track.
1024    pub song_rating: u32,
1025    /// Additional, optional fields in the response
1026    #[serde(flatten)]
1027    pub optional: HashMap<String, serde_json::value::Value>,
1028}
1029
1030///                  "lowQuality": {
1031///                      "bitrate": "32",
1032///                      "encoding": "aacplus",
1033///                      "audioUrl": "http://audio-sv5-t1-1.pandora.com/access/3464788359714661029.mp4?...",
1034///                      "protocol": "http"
1035///                  }
1036#[derive(Debug, Clone, Deserialize)]
1037#[serde(rename_all = "camelCase")]
1038pub struct AudioQuality {
1039    /// Attributes for the high quality audio stream.
1040    pub high_quality: AudioStream,
1041    /// Attributes for the medium quality audio stream.
1042    pub medium_quality: AudioStream,
1043    /// Attributes for the low quality audio stream.
1044    pub low_quality: AudioStream,
1045}
1046
1047/// Playback/decoding attributes of an available audio stream.
1048#[derive(Debug, Clone, Deserialize)]
1049#[serde(rename_all = "camelCase")]
1050pub struct AudioStream {
1051    /// The audio bitrate/quality for this stream.
1052    pub bitrate: String,
1053    /// The audio encoding format for this stream.
1054    pub encoding: String,
1055    /// The url to stream audio from.
1056    pub audio_url: String,
1057    /// The protocol to use with the audio URL.
1058    pub protocol: String,
1059}
1060
1061/// Convenience function to do a basic getPlaylist call.
1062pub async fn get_playlist(
1063    session: &mut PandoraSession,
1064    station_token: &str,
1065) -> Result<GetPlaylistResponse, Error> {
1066    GetPlaylist::from(&station_token)
1067        .station_is_starting(false)
1068        .include_track_length(false)
1069        .include_audio_token(false)
1070        .xplatform_ad_capable(false)
1071        .include_audio_receipt_url(false)
1072        .include_backstage_ad_url(false)
1073        .include_sharing_ad_url(false)
1074        .include_social_ad_url(false)
1075        .include_competitive_sep_indicator(false)
1076        .include_complete_playlist(false)
1077        .include_track_options(false)
1078        .audio_ad_pod_capable(false)
1079        .response(session)
1080        .await
1081}
1082
1083/// Extended station information includes seeds and feedback.
1084///
1085/// | Name | Type | Description |
1086/// | stationToken | string |  |
1087/// | includeExtendedAttributes | bool |  |
1088/// ``` json
1089/// {
1090///     "stationToken": "374145764047334893",
1091///     "includeExtendedAttributes": true,
1092///     "userAuthToken": "XXX",
1093///     "syncTime": 1404910732
1094/// }
1095/// ```
1096#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
1097#[pandora_request(encrypted = true)]
1098#[serde(rename_all = "camelCase")]
1099pub struct GetStation {
1100    /// The unique id (token) for the station to request information on.
1101    pub station_token: String,
1102    /// The fields of the createStation response are unknown.
1103    #[serde(flatten)]
1104    pub optional: HashMap<String, serde_json::value::Value>,
1105}
1106
1107impl GetStation {
1108    /// Convenience function for setting boolean flags in the request. (Chaining call)
1109    pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
1110        self.optional
1111            .insert(option.to_string(), serde_json::value::Value::from(value));
1112        self
1113    }
1114
1115    /// Whether request should include extended station attributes in the response. (Chaining call)
1116    pub fn include_extended_attributes(self, value: bool) -> Self {
1117        self.and_boolean_option("includeExtendedAttributes", value)
1118    }
1119}
1120
1121impl<TS: ToString> From<&TS> for GetStation {
1122    fn from(station_token: &TS) -> Self {
1123        GetStation {
1124            station_token: station_token.to_string(),
1125            optional: HashMap::new(),
1126        }
1127    }
1128}
1129
1130/// | Name | Type | Description |
1131/// | music | object | Station seeds, see Add seed |
1132/// | music.songs | list | Song seeds |
1133/// | music.artists | list | Artist seeds |
1134/// | feedback | object | Feedback added by Rate track |
1135/// | feedback.thumbsUp | list |   |
1136/// | feedback.thumbsDown | list |   |
1137/// ``` json
1138/// {
1139///     "stat": "ok",
1140///     "result": {
1141///         "suppressVideoAds": false,
1142///         "stationId": "374145764047334893",
1143///         "allowAddMusic": true,
1144///         "dateCreated": {
1145///             "date": 15,
1146///             "day": 6,
1147///             "hours": 7,
1148///             "minutes": 34,
1149///             "month": 0,
1150///             "nanos": 874000000,
1151///             "seconds": 21,
1152///             "time": 1295105661874,
1153///             "timezoneOffset": 480,
1154///             "year": 111
1155///         },
1156///         "stationDetailUrl": "https://www.pandora.com/login?target=%2Fstations%2Fc644756145fc3f5df1916901125ee697495159685ae39575",
1157///         "artUrl": "http://cont-1.p-cdn.com/images/public/amz/5/2/8/5/075678235825_500W_498H.jpg",
1158///         "requiresCleanAds": false,
1159///         "stationToken": "374145764047334893",
1160///         "stationName": "Winter Radio",
1161///         "music": {
1162///             "songs": [{
1163///                 "seedId": "428301990230109677",
1164///                 "artistName": "Tori Amos",
1165///                 "artUrl": "http://cont-sjl-1.pandora.com/images/public/amz/5/2/8/5/075678235825_130W_130H.jpg",
1166///                 "songName": "Winter",
1167///                 "musicToken": "87ef9db1c3f04330"
1168///             }],
1169///             "artists": [{
1170///                 "artistName": "Jason Derulo",
1171///                 "musicToken": "563f577e00d837a5",
1172///                 "seedId": "31525199612287328",
1173///                 "artUrl": "http://mediaserver-cont-sv5-1-v4v6.pandora.com/images/public/amg/portrait/pic200/drQ300/Q366/Q36675SDAPJ.jpg"
1174///             }],
1175///             "genres": [{
1176///                 "musicToken": "cc021b31a48b8acf",
1177///                 "genreName": "Today's Hits",
1178///                 "seedId": "31525199599467854"
1179///             }]
1180///         },
1181///         "isShared": false,
1182///         "allowDelete": true,
1183///         "genre": ["Rock"],
1184///         "isQuickMix": false,
1185///         "allowRename": true,
1186///         "stationSharingUrl": "https://www.pandora.com/login?target=%2Fshare%2Fstation%2Fc644756145fc3f5df1916901125ee697495159685ae39575",
1187///         "allowEditDescription": true,
1188///         "feedback": {
1189///             "thumbsUp": [{
1190///                 "dateCreated": {
1191///                     "date": 28,
1192///                     "day": 5,
1193///                     "hours": 13,
1194///                     "minutes": 57,
1195///                     "month": 2,
1196///                     "nanos": 760000000,
1197///                     "seconds": 49,
1198///                     "time": 1396040269760,
1199///                     "timezoneOffset": 420,
1200///                     "year": 114
1201///                 },
1202///                 "albumArtUrl": "http://cont-1.p-cdn.com/images/public/amz/9/7/1/4/900004179_130W_130H.jpg",
1203///                 "musicToken": "d33dd0c199ebaf28425ba2910f7abf8b",
1204///                 "songName": "Hey Lover",
1205///                 "artistName": "Keri Noble",
1206///                 "feedbackId": "-7239441039566426643",
1207///                 "isPositive": true
1208///             }],
1209///             "totalThumbsUp": 20,
1210///             "totalThumbsDown": 5,
1211///             "thumbsDown": [{
1212///                 "dateCreated": {
1213///                     "date": 28,
1214///                     "day": 5,
1215///                     "hours": 10,
1216///                     "minutes": 43,
1217///                     "month": 2,
1218///                     "nanos": 637000000,
1219///                     "seconds": 30,
1220///                     "time": 1396028610637,
1221///                     "timezoneOffset": 420,
1222///                     "year": 114
1223///                 },
1224///                 "albumArtUrl": "http://cont-ch1-1.pandora.com/images/public/amz/9/0/5/1/724383771509_130W_130H.jpg",
1225///                 "musicToken": "5a0018da7876f6e7",
1226///                 "songName": "Talk Show Host",
1227///                 "artistName": "Radiohead",
1228///                 "feedbackId": "-7241622182873125395",
1229///                 "isPositive": false
1230///             }]
1231///         }
1232///     }
1233/// }
1234/// ```
1235#[derive(Debug, Clone, Deserialize)]
1236#[serde(rename_all = "camelCase")]
1237pub struct GetStationResponse {
1238    /// The unique id (token) for the station for which information was
1239    /// requested. The stationId (station_id) and stationToken (station_token)
1240    /// attributes appear to be duplicates.
1241    pub station_id: String,
1242    /// The unique id (token) for the station for which information was
1243    /// requested. The stationId (station_id) and stationToken (station_token)
1244    /// attributes appear to be duplicates.
1245    pub station_token: String,
1246    /// The user-created name of the station.
1247    pub station_name: String,
1248    /// Whether the station allows adding music to it.
1249    pub allow_add_music: Option<bool>,
1250    /// Unknown
1251    pub suppress_video_ads: Option<bool>,
1252    /// When the station was created.
1253    pub date_created: Timestamp,
1254    /// Unknown
1255    pub station_detail_url: Option<String>,
1256    /// Unknown
1257    pub art_url: Option<String>,
1258    /// Unknown
1259    pub requires_clean_ads: Option<bool>,
1260    /// Station music seeds.
1261    pub music: Option<StationSeeds>,
1262    /// Whether the station is visible for sharing.
1263    pub is_shared: Option<bool>,
1264    /// Whether the station can be deleted.
1265    pub allow_delete: Option<bool>,
1266    /// The genre(s) the station belongs to.
1267    #[serde(default)]
1268    pub genre: Vec<String>,
1269    /// Whether this is a QuickMix station.
1270    pub is_quick_mix: Option<bool>,
1271    /// Whether the station may be renamed.
1272    pub allow_rename: Option<bool>,
1273    /// The URL to use for sharing this station.
1274    pub station_sharing_url: Option<String>,
1275    /// Whether the description for this station may be edited.
1276    pub allow_edit_description: Option<bool>,
1277    /// Feedback submitted for tracks on this station.
1278    pub feedback: Option<StationFeedback>,
1279}
1280
1281/// ``` json
1282///         "music": {
1283///             "songs": [],
1284///             "artists": [],
1285///             "genres": []
1286///         },
1287/// ```
1288#[derive(Debug, Clone, Deserialize)]
1289#[serde(rename_all = "camelCase")]
1290pub struct StationSeeds {
1291    /// Songs used as seeds for this station.
1292    pub songs: Vec<SongSeed>,
1293    /// Atrists used as seeds for this station.
1294    pub artists: Vec<ArtistSeed>,
1295    /// Genres used as seeds for this station.
1296    pub genres: Vec<GenreSeed>,
1297}
1298
1299/// Attributes of a song seed for a station.
1300/// ``` json
1301///             "songs": [{
1302///                 "seedId": "5629501782357373",
1303///                 "musicToken": "9d8f932edea76ed8425ba2910f7abf8b",
1304///                 "songName": "Soul Finger",
1305///                 "artistName": "The Bar-Kays",
1306///                 "pandoraType": "TR",
1307///                 "pandoraId": "TR:852695",
1308///                 "artUrl": "http://.../081227857165_130W_130H.jpg",
1309///             }],
1310/// ```
1311#[derive(Debug, Clone, Deserialize)]
1312#[serde(rename_all = "camelCase")]
1313pub struct SongSeed {
1314    /// Unique identifier/handle for this seed.
1315    pub seed_id: String,
1316    /// Identifier for the song used for this seed.
1317    pub music_token: String,
1318    /// Name of the song used for this seed.
1319    pub song_name: String,
1320    /// Name of the artist for the song used for this seed.
1321    pub artist_name: String,
1322    /// The type of Pandora object described by the Pandora ID.
1323    pub pandora_type: String,
1324    /// An identifier for this Pandora object that is unique across all types of Pandora
1325    /// objects.
1326    pub pandora_id: String,
1327    /// Unknown
1328    pub art_url: String,
1329    /// Unknown fields in the response, if any
1330    #[serde(flatten)]
1331    pub optional: HashMap<String, serde_json::value::Value>,
1332}
1333
1334/// Attributes of an artist seed for a station.
1335/// ``` json
1336///             "artists": [{
1337///                 "seedId": "5629501764244877",
1338///                 "musicToken": "2858b602eb1adfa8",
1339///                 "artistName": "Michael Bublé",
1340///                 "pandoraType": "AR"
1341///                 "pandoraId": "AR:6533",
1342///                 "artUrl": "http://.../90W_90H.jpg",
1343///                 "icon": {"dominantColor": "602d30","artUrl": ""},
1344///             ],}
1345/// ```
1346#[derive(Debug, Clone, Deserialize)]
1347#[serde(rename_all = "camelCase")]
1348pub struct ArtistSeed {
1349    /// Unique identifier/handle for this seed.
1350    pub seed_id: String,
1351    /// Identifier for the artist used for this seed.
1352    pub music_token: String,
1353    /// Name of the artist used for this seed.
1354    pub artist_name: String,
1355    /// The type of Pandora object described by the Pandora ID.
1356    pub pandora_type: String,
1357    /// An identifier for this Pandora object that is unique across all types of Pandora
1358    /// objects.
1359    pub pandora_id: String,
1360    /// Artist icon
1361    pub icon: HashMap<String, String>,
1362    /// Unknown fields in the response, if any
1363    #[serde(flatten)]
1364    pub optional: HashMap<String, serde_json::value::Value>,
1365}
1366
1367/// Attributes of a genre seed for a station.
1368/// ``` json
1369///             "genres": [{
1370///                 "musicToken": "cc021b31a48b8acf",
1371///                 "genreName": "Today's Hits",
1372///                 "seedId": "31525199599467854"
1373///             }]
1374/// ```
1375#[derive(Debug, Clone, Deserialize)]
1376#[serde(rename_all = "camelCase")]
1377pub struct GenreSeed {
1378    /// Unique identifier/handle for this seed.
1379    pub seed_id: String,
1380    /// Identifier for the genre used for this seed.
1381    pub music_token: String,
1382    /// Name of the genre used for this seed.
1383    pub genre_name: String,
1384    /// Unknown fields in the response, if any
1385    #[serde(flatten)]
1386    pub optional: HashMap<String, serde_json::value::Value>,
1387}
1388
1389/// ``` json
1390///         "feedback": {
1391///             "thumbsUp": [],
1392///             "totalThumbsUp": 20,
1393///             "totalThumbsDown": 5,
1394///             "thumbsDown": []
1395///         }
1396/// ```
1397#[derive(Debug, Clone, Deserialize)]
1398#[serde(rename_all = "camelCase")]
1399pub struct StationFeedback {
1400    /// A list of positive feedback submitted to a station.
1401    pub thumbs_up: Vec<TrackFeedback>,
1402    /// The total number of positive submissions to a station.
1403    pub total_thumbs_up: u32,
1404    /// A list of negative feedback submitted to a station.
1405    pub thumbs_down: Vec<TrackFeedback>,
1406    /// The total number of negative submissions to a station.
1407    pub total_thumbs_down: u32,
1408}
1409
1410/// ``` json
1411///             "thumbsDown": [{
1412///                 "dateCreated": {
1413///                     "date": 28,
1414///                     "day": 5,
1415///                     "hours": 10,
1416///                     "minutes": 43,
1417///                     "month": 2,
1418///                     "nanos": 637000000,
1419///                     "seconds": 30,
1420///                     "time": 1396028610637,
1421///                     "timezoneOffset": 420,
1422///                     "year": 114
1423///                 },
1424///                 "albumArtUrl": "http://cont-ch1-1.pandora.com/images/public/amz/9/0/5/1/724383771509_130W_130H.jpg",
1425///                 "musicToken": "5a0018da7876f6e7",
1426///                 "songName": "Talk Show Host",
1427///                 "artistName": "Radiohead",
1428///                 "feedbackId": "-7241622182873125395",
1429///                 "isPositive": false
1430///             }]
1431/// ```
1432#[derive(Debug, Clone, Deserialize)]
1433#[serde(rename_all = "camelCase")]
1434pub struct TrackFeedback {
1435    /// Unique identifier/handle referring to this feedback submission.
1436    pub feedback_id: String,
1437    /// Name of the song that was rated.
1438    pub song_name: String,
1439    /// Name of the artist for the song that was rated.
1440    pub artist_name: String,
1441    /// Whether the rating is positive (true) or negative (false).
1442    pub is_positive: bool,
1443    /// A token referring to the song that was rated.
1444    pub music_token: String,
1445    /// Date the feedback was created.
1446    pub date_created: Timestamp,
1447    /// Unknown
1448    pub album_art_url: String,
1449}
1450
1451/// Convenience function to do a basic getStation call.
1452pub async fn get_station(
1453    session: &mut PandoraSession,
1454    station_token: &str,
1455) -> Result<GetStationResponse, Error> {
1456    GetStation::from(&station_token)
1457        .include_extended_attributes(false)
1458        .response(session)
1459        .await
1460}
1461
1462/// **Unsupported!**
1463/// Undocumented method
1464/// [station.publishStationShare()](https://6xq.net/pandora-apidoc/json/methods/)
1465pub struct PublishStationShareUnsupported {}
1466
1467/// | Name   | Type |   Description |
1468/// | stationToken  |  string | Existing station, see Retrieve station list |
1469/// | stationName | string | New station name |
1470#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
1471#[pandora_request(encrypted = true)]
1472#[serde(rename_all = "camelCase")]
1473pub struct RenameStation {
1474    /// The unique id (token) for the station that should be renamed.
1475    /// Also sometimes referred to as a stationId.
1476    pub station_token: String,
1477    /// The new name that should be used for this station.
1478    pub station_name: String,
1479}
1480
1481impl RenameStation {
1482    /// Create a new RenameStation with some initial values.
1483    pub fn new(station_token: &str, station_name: &str) -> Self {
1484        Self {
1485            station_token: station_token.to_string(),
1486            station_name: station_name.to_string(),
1487        }
1488    }
1489}
1490
1491/// There's no known response data to this request.
1492#[derive(Debug, Clone, Deserialize)]
1493#[serde(rename_all = "camelCase")]
1494pub struct RenameStationResponse {
1495    /// The fields of the renameStation response, if any, are unknown.
1496    #[serde(flatten)]
1497    pub optional: HashMap<String, serde_json::value::Value>,
1498}
1499
1500/// Convenience function to do a basic renameStation call.
1501pub async fn rename_station(
1502    session: &mut PandoraSession,
1503    station_token: &str,
1504    station_name: &str,
1505) -> Result<RenameStationResponse, Error> {
1506    RenameStation::new(station_token, station_name)
1507        .response(session)
1508        .await
1509}
1510
1511/// Shares a station with the specified email addresses. that emails is a string array
1512///
1513/// | Name  |  Type |   Description |
1514/// | stationId |  string | See Retrieve station list |
1515/// | stationToken |   string | See Retrieve station list |
1516/// | emails | string[] |   A list of emails to share the station with |
1517#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
1518#[pandora_request(encrypted = true)]
1519#[serde(rename_all = "camelCase")]
1520pub struct ShareStation {
1521    /// The unique id (token) for the station that should be shared.
1522    /// Also sometimes referred to as a stationId.
1523    pub station_id: String,
1524    /// The unique id (token) for the station that should be shared.
1525    /// Also sometimes referred to as a stationId.
1526    pub station_token: String,
1527    /// A list of emails to share the station with.
1528    pub emails: Vec<String>,
1529}
1530
1531impl ShareStation {
1532    /// Create a new RenameStation with some initial values.  Call
1533    /// add_recipient() to add recipient emails to the request.
1534    pub fn new(station_id: &str, station_token: &str) -> Self {
1535        Self {
1536            station_id: station_id.to_string(),
1537            station_token: station_token.to_string(),
1538            emails: Vec::new(),
1539        }
1540    }
1541
1542    /// Add a recipient email to the request.
1543    pub fn add_recipient(&mut self, recipient: &str) {
1544        self.emails.push(recipient.to_string());
1545    }
1546}
1547
1548/// There's no known response data to this request.
1549#[derive(Debug, Clone, Deserialize)]
1550#[serde(rename_all = "camelCase")]
1551pub struct ShareStationResponse {
1552    /// The fields of the shareStation response, if any, are unknown.
1553    #[serde(flatten)]
1554    pub optional: HashMap<String, serde_json::value::Value>,
1555}
1556
1557/// Convenience function to do a basic shareStation call.
1558pub async fn share_station(
1559    session: &mut PandoraSession,
1560    station_id: &str,
1561    station_token: &str,
1562    emails: Vec<String>,
1563) -> Result<ShareStationResponse, Error> {
1564    let mut request = ShareStation::new(station_id, station_token);
1565    request.emails = emails;
1566    request.response(session).await
1567}
1568
1569/// Stations created by other users are added as reference to the user’s
1570/// station list. These stations cannot be modified (i.e. rate tracks) unless
1571/// transformed.
1572///
1573/// | Name   |  Type  |   Description |
1574/// | stationToken  |   string |  See Retrieve station list |
1575#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
1576#[pandora_request(encrypted = true)]
1577#[serde(rename_all = "camelCase")]
1578pub struct TransformSharedStation {
1579    /// The unique id (token) for the shared station that should be converted to
1580    /// a personal station.
1581    /// Also sometimes referred to as a stationId.
1582    pub station_token: String,
1583}
1584
1585impl<TS: ToString> From<&TS> for TransformSharedStation {
1586    fn from(station_token: &TS) -> Self {
1587        Self {
1588            station_token: station_token.to_string(),
1589        }
1590    }
1591}
1592
1593/// There's no known response data to this request.
1594#[derive(Debug, Clone, Deserialize)]
1595#[serde(rename_all = "camelCase")]
1596pub struct TransformSharedStationResponse {
1597    /// The fields of the transformSharedStation response, if any, are unknown.
1598    #[serde(flatten)]
1599    pub optional: HashMap<String, serde_json::value::Value>,
1600}
1601
1602/// Convenience function to do a basic transformSharedStation call.
1603pub async fn transform_shared_station(
1604    session: &mut PandoraSession,
1605    station_token: &str,
1606) -> Result<TransformSharedStationResponse, Error> {
1607    TransformSharedStation::from(&station_token)
1608        .response(session)
1609        .await
1610}
1611
1612#[cfg(test)]
1613mod tests {
1614    use std::collections::HashSet;
1615
1616    use super::*;
1617    use crate::json::{
1618        music::search, music::ArtistMatch, tests::session_login, user::get_station_list, Partner,
1619    };
1620
1621    // TODO: share_station, transform_shared_station,
1622    #[tokio::test]
1623    async fn station_ops_test() {
1624        // TODO: ensure that the station we intend to create didn't get leaked
1625        // by a previous, failed test execution, look for stations named either
1626        // "INXS Radio" or "XSNI Radio"
1627        let partner = Partner::default();
1628        let mut session = session_login(&partner)
1629            .await
1630            .expect("Failed initializing login session");
1631
1632        let artist_search = search(&mut session, "INXS")
1633            .await
1634            .expect("Failed completing artist search request");
1635
1636        let additional_artist_search = search(&mut session, "Panic! At the Disco")
1637            .await
1638            .expect("Failed completing artist search request");
1639
1640        if let Some(ArtistMatch { music_token, .. }) = artist_search
1641            .artists
1642            .iter()
1643            .filter(|am| am.score == 100)
1644            .next()
1645        {
1646            let created_station = create_station_from_music_token(&mut session, &music_token)
1647                .await
1648                .expect("Failed creating station from search result");
1649
1650            let _renamed_station =
1651                rename_station(&mut session, &created_station.station_token, "XSNI Radio")
1652                    .await
1653                    .expect("Failed renaming station");
1654
1655            if let Some(ArtistMatch { music_token, .. }) = additional_artist_search
1656                .artists
1657                .iter()
1658                .filter(|am| am.score == 100)
1659                .next()
1660            {
1661                let added_music =
1662                    add_music(&mut session, &created_station.station_token, music_token)
1663                        .await
1664                        .expect("Failed adding music to station");
1665
1666                let _del_music = delete_music(&mut session, &added_music.seed_id)
1667                    .await
1668                    .expect("Failed deleting music from station");
1669            }
1670
1671            let _del_station = delete_station(&mut session, &created_station.station_token)
1672                .await
1673                .expect("Failed deleting station");
1674        }
1675    }
1676
1677    /* This test is very demanding on the server, so we disable it until we want
1678     * to retest.
1679    #[tokio::test]
1680    async fn genre_stations_test() {
1681        let partner = Partner::default();
1682        let mut session = session_login(&partner).await.expect("Failed initializing login session");
1683
1684        let genre_stations = get_genre_stations(&mut session).await
1685            .expect("Failed getting genre stations");
1686
1687        let genre_stations_checksum = get_genre_stations_checksum(&mut session).await
1688            .expect("Failed getting genre stations checksum");
1689    }
1690    */
1691
1692    #[tokio::test]
1693    async fn station_feedback_test() {
1694        let partner = Partner::default();
1695        let mut session = session_login(&partner)
1696            .await
1697            .expect("Failed initializing login session");
1698
1699        for station in get_station_list(&mut session)
1700            .await
1701            .expect("Failed getting station list to look up a track to bookmark")
1702            .stations
1703        {
1704            // Look through feedback on the station and build up a list of
1705            // already-rated songs so that we don't mess with any pre-existing
1706            // ratings during this test.  This also exercises get_station.
1707            let station = GetStation::from(&station.station_token)
1708                .include_extended_attributes(true)
1709                .response(&mut session)
1710                .await
1711                .expect("Failed getting station attributes");
1712
1713            let mut protected_tracks: HashSet<String> = HashSet::new();
1714            protected_tracks.extend(
1715                station
1716                    .feedback
1717                    .iter()
1718                    .flat_map(|f| f.thumbs_up.iter())
1719                    .map(|tf| tf.song_name.clone()),
1720            );
1721            protected_tracks.extend(
1722                station
1723                    .feedback
1724                    .iter()
1725                    .flat_map(|f| f.thumbs_down.iter())
1726                    .map(|tf| tf.song_name.clone()),
1727            );
1728
1729            for track in get_playlist(&mut session, &station.station_token)
1730                .await
1731                .expect("Failed completing request for playlist")
1732                .items
1733                .iter()
1734                .flat_map(|p| p.get_track())
1735            {
1736                if protected_tracks.contains(&track.song_name) {
1737                    continue;
1738                }
1739
1740                // Thumbs-up track
1741                let feedback = add_feedback(
1742                    &mut session,
1743                    &station.station_token,
1744                    &track.track_token,
1745                    true,
1746                )
1747                .await
1748                .expect("Failed adding positive feedback to track");
1749                // And delete
1750                let _del_feedback = delete_feedback(&mut session, &feedback.feedback_id)
1751                    .await
1752                    .expect("Failed deleting positive feedback from track");
1753                // Thumbs-down track
1754                let feedback = add_feedback(
1755                    &mut session,
1756                    &station.station_token,
1757                    &track.track_token,
1758                    false,
1759                )
1760                .await
1761                .expect("Failed adding negative feedback to track");
1762                // And delete
1763                let _del_feedback = delete_feedback(&mut session, &feedback.feedback_id)
1764                    .await
1765                    .expect("Failed deleting negative feedback from track");
1766
1767                // Finished test, stop looping through
1768                return;
1769            }
1770        }
1771        panic!("Station list request returned no results, so no feedback-capable content.");
1772    }
1773}