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(
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}