pandora_api/json/music.rs
1/*!
2Music support methods.
3*/
4// SPDX-License-Identifier: MIT AND WTFPL
5use std::collections::HashMap;
6
7use pandora_api_derive::PandoraJsonRequest;
8use serde::{Deserialize, Serialize};
9
10use crate::errors::Error;
11use crate::json::{PandoraJsonApiRequest, PandoraSession};
12
13/// **Unsupported!**
14/// Undocumented method
15/// [music.getSearchRecommendations()](https://6xq.net/pandora-apidoc/json/methods/)
16pub struct GetSearchRecommendationsUnsupported {}
17
18/// This method returns a description of the track associated with the provided
19/// musicId included with each track in a playlist.
20/// | musicId | String | as returned from a playlist that has not yet expired |
21///
22/// [music.getTrack()](https://github.com/pithos/pithos/issues/351)
23#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
24#[pandora_request(encrypted = true)]
25#[serde(rename_all = "camelCase")]
26pub struct GetTrack {
27 /// The token for the track as returned by the playlist
28 pub music_id: String,
29}
30
31impl<TS: ToString> From<&TS> for GetTrack {
32 fn from(music_id: &TS) -> Self {
33 Self {
34 music_id: music_id.to_string(),
35 }
36 }
37}
38
39/// Get extended information for a track as returned by a playlist.
40///
41/// See https://github.com/pithos/pithos/issues/351 for additional
42/// information
43/// [music.getTrack()](
44///
45/// | Name | Type | Description |
46/// | artistName | String | |
47/// | albumName | String | |
48/// | songName | String | |
49/// | trackToken | String | |
50/// | musicId | String | |
51/// | musicToken | String | |
52/// ``` json
53/// {
54/// "stat": "ok",
55/// "result": {
56/// 'albumName': 'Lukas Graham',
57/// 'trackToken': 'S5264080',
58/// 'artistName': 'Lukas Graham',
59/// 'albumArtUrl':
60/// 'http://mediaserver-cont-dc6-2-v4v6.pandora.com/images/public/gracenote/albumart/9/6/6/9/800079669_500W_500H.jpg',
61/// 'score': '',
62/// 'songName': '7 Years',
63/// 'musicId': 'S5264080',
64/// 'songDetailUrl':
65/// 'http://www.pandora.com/lukas-graham/lukas-graham/7-years',
66/// 'musicToken': '2b0dc86c994aa1e9425ba2910f7abf8b'
67/// }
68/// }
69/// ```
70#[derive(Debug, Clone, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct GetTrackResponse {
73 /// The name of the song for the provided token.
74 pub song_name: String,
75 /// The name of the artist for the provided token.
76 pub artist_name: String,
77 /// The name of the album for the provided token.
78 pub album_name: String,
79 /// The track token that is unique to the playlist is was provided with.
80 pub track_token: String,
81 /// The unique id (token) for the song. Artist tokens start with 'R',
82 /// composers with 'C', songs with 'S', and genres with 'G'.
83 pub music_id: String,
84 /// A unique token for a song/track.
85 pub music_token: String,
86 /// Additional optional or undocumented fields of a GetTrack response.
87 #[serde(flatten)]
88 pub optional: HashMap<String, serde_json::value::Value>,
89}
90
91/// Convenience function to do a basic getTrack call.
92pub async fn get_track(
93 session: &mut PandoraSession,
94 track_token: &str,
95) -> Result<GetTrackResponse, Error> {
96 GetTrack::from(&track_token).response(session).await
97}
98
99/// **Unsupported!**
100/// Undocumented method
101/// [music.publishSongShare()](https://6xq.net/pandora-apidoc/json/methods/)
102pub struct PublishSongShareUnsupported {}
103
104/// This is a free text search that matches artist and track names.
105///
106/// | Name | Type | Description |
107/// |searchText | string | Artist name or track title |
108/// |includeNearMatches | bool | (optional) |
109/// |includeGenreStations | bool | (optional) |
110/// ``` json
111/// {
112/// "searchText": "encore",
113/// "userAuthToken": "XXX",
114/// "syncTime": 1335869287
115/// }
116/// ```
117#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
118#[pandora_request(encrypted = true)]
119#[serde(rename_all = "camelCase")]
120pub struct Search {
121 /// The text to search for in artist names or track titles.
122 pub search_text: String,
123 /// Optional parameters on the call
124 #[serde(flatten)]
125 pub optional: HashMap<String, serde_json::value::Value>,
126}
127
128impl Search {
129 /// Convenience function for setting boolean flags in the request. (Chaining call)
130 pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
131 self.optional
132 .insert(option.to_string(), serde_json::value::Value::from(value));
133 self
134 }
135
136 /// Whether request should include partial matches in the response. (Chaining call)
137 pub fn include_near_matches(self, value: bool) -> Self {
138 self.and_boolean_option("includeNearMatches", value)
139 }
140
141 /// Whether request should include genre stations in the response. (Chaining call)
142 pub fn include_genre_stations(self, value: bool) -> Self {
143 self.and_boolean_option("includeGenreStations", value)
144 }
145}
146
147impl<TS: ToString> From<&TS> for Search {
148 fn from(search_text: &TS) -> Self {
149 Self {
150 search_text: search_text.to_string(),
151 optional: HashMap::new(),
152 }
153 }
154}
155
156/// Convenience function to do a basic addSongBookmark call.
157pub async fn search(
158 session: &mut PandoraSession,
159 search_text: &str,
160) -> Result<SearchResponse, Error> {
161 Search::from(&search_text)
162 .include_near_matches(false)
163 .include_genre_stations(false)
164 .response(session)
165 .await
166}
167
168/// Matching songs, artists, and genre stations are returned in three separate lists.
169///
170/// | Name | Type | Description |
171/// | songs.musicToken | string | Token starts with ‘S’ followed by one or more digits (e.g. ‘S1234567’). |
172/// | artists.musicToken | string | Results can be either for artists (token starts with ‘R’) or composers (token starts with ‘C’). |
173/// | genreStations.musicToken | string | Token starts with ‘G’ followed by one or more digits (e.g. ‘G123’). |
174/// ``` json
175/// {
176/// "stat": "ok",
177/// "result": {
178/// "nearMatchesAvailable": true,
179/// "explanation": "",
180/// "songs": [{
181/// "artistName": "Jason DeRulo",
182/// "musicToken": "S1508963",
183/// "songName": "Encore",
184/// "score": 100
185/// }],
186/// "artists": [{
187/// "artistName": "Encore",
188/// "musicToken": "R175304",
189/// "likelyMatch": false,
190/// "score": 100
191/// }],
192/// "genreStations": [{
193/// "musicToken": "G123",
194/// "score": 100,
195/// "stationName": "Today's Encore"
196/// }]
197/// }
198/// }
199/// ```
200#[derive(Debug, Clone, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct SearchResponse {
203 /// Songs matching the search.
204 #[serde(default)]
205 pub songs: Vec<SongMatch>,
206 /// Artists matching the search.
207 #[serde(default)]
208 pub artists: Vec<ArtistMatch>,
209 /// Genre stations matching the search.
210 #[serde(default)]
211 pub genre_stations: Vec<GenreMatch>,
212 /// Additional optional fields that may appear in the response.
213 #[serde(flatten)]
214 pub optional: HashMap<String, serde_json::value::Value>,
215}
216
217/// Structure collecting the song information returned
218/// by searches.
219#[derive(Debug, Clone, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct SongMatch {
222 /// Name of the matched song.
223 pub song_name: String,
224 /// The name of the artist found in the search.
225 pub artist_name: String,
226 /// The unique id (token) for the song. Artist tokens start with 'R',
227 /// composers with 'C', songs with 'S', and genres with 'G'.
228 pub music_token: String,
229 /// A rating of how close the match is.
230 pub score: u8,
231}
232
233/// Structure collecting the artist information returned
234/// by searches.
235#[derive(Debug, Clone, Deserialize)]
236#[serde(rename_all = "camelCase")]
237pub struct ArtistMatch {
238 /// The name of the artist found in the search.
239 pub artist_name: String,
240 /// The unique id (token) for the song. Artist tokens start with 'R',
241 /// composers with 'C', songs with 'S', and genres with 'G'.
242 pub music_token: String,
243 /// Whether the match is just a close, but not perfect, match.
244 pub likely_match: bool,
245 /// A rating of how close the match is.
246 pub score: u8,
247}
248
249/// Structure collecting the genre-station information returned
250/// by searches.
251#[derive(Debug, Clone, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct GenreMatch {
254 /// The unique id (token) for the song. Artist tokens start with 'R',
255 /// composers with 'C', songs with 'S', and genres with 'G'.
256 pub music_token: String,
257 /// A rating of how close the match is.
258 pub score: u8,
259 /// The name of the genre station found in the search.
260 pub station_name: String,
261}
262
263/// **Unsupported!**
264/// Undocumented method
265/// [music.shareMusic()](https://6xq.net/pandora-apidoc/json/methods/)
266pub struct ShareMusicUnsupported {}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::json::{
272 station::get_playlist, tests::session_login, user::get_station_list, Partner,
273 };
274
275 #[tokio::test]
276 async fn search_test() {
277 let partner = Partner::default();
278 let mut session = session_login(&partner)
279 .await
280 .expect("Failed initializing login session");
281
282 let _search_response = search(&mut session, "INXS")
283 .await
284 .expect("Failed completing search request");
285 let _search_response: SearchResponse = Search::from(&"Alternative")
286 .include_genre_stations(true)
287 .response(&mut session)
288 .await
289 .expect("Failed completing search request");
290 }
291
292 #[tokio::test]
293 async fn get_track_test() {
294 let partner = Partner::default();
295 let mut session = session_login(&partner)
296 .await
297 .expect("Failed initializing login session");
298
299 for station in get_station_list(&mut session)
300 .await
301 .expect("Failed getting station list to look up a track to bookmark")
302 .stations
303 {
304 for track in get_playlist(&mut session, &station.station_token)
305 .await
306 .expect("Failed completing request for playlist")
307 .items
308 .iter()
309 .flat_map(|p| p.get_track())
310 {
311 if let Some(serde_json::value::Value::String(music_id)) =
312 track.optional.get("musicId")
313 {
314 let _response = get_track(&mut session, &music_id)
315 .await
316 .expect("Failed getting track information");
317 }
318 }
319 }
320 }
321}