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}