1use std::{
2 fmt::Write,
3 time::{Duration, Instant},
4};
5
6use bytes::Bytes;
7use data_encoding::HEXUPPER_PERMISSIVE;
8use futures_util::future::IntoStream;
9use http::header::HeaderValue;
10use hyper::{
11 header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
12 HeaderMap, Method, Request,
13};
14use hyper_util::client::legacy::ResponseFuture;
15use protobuf::{Enum, Message, MessageFull};
16use rand::RngCore;
17use sysinfo::System;
18use thiserror::Error;
19
20use crate::config::{os_version, OS};
21use crate::{
22 apresolve::SocketAddress,
23 cdn_url::CdnUrl,
24 config::SessionConfig,
25 error::ErrorKind,
26 protocol::{
27 canvaz::EntityCanvazRequest,
28 clienttoken_http::{
29 ChallengeAnswer, ChallengeType, ClientTokenRequest, ClientTokenRequestType,
30 ClientTokenResponse, ClientTokenResponseType,
31 },
32 connect::PutStateRequest,
33 extended_metadata::BatchedEntityRequest,
34 },
35 token::Token,
36 util,
37 version::spotify_semantic_version,
38 Error, FileId, SpotifyId,
39};
40
41component! {
42 SpClient : SpClientInner {
43 accesspoint: Option<SocketAddress> = None,
44 strategy: RequestStrategy = RequestStrategy::default(),
45 client_token: Option<Token> = None,
46 }
47}
48
49pub type SpClientResult = Result<Bytes, Error>;
50
51#[allow(clippy::declare_interior_mutable_const)]
52pub const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-token");
53
54#[derive(Debug, Error)]
55pub enum SpClientError {
56 #[error("missing attribute {0}")]
57 Attribute(String),
58}
59
60impl From<SpClientError> for Error {
61 fn from(err: SpClientError) -> Self {
62 Self::failed_precondition(err)
63 }
64}
65
66#[derive(Copy, Clone, Debug)]
67pub enum RequestStrategy {
68 TryTimes(usize),
69 Infinitely,
70}
71
72impl Default for RequestStrategy {
73 fn default() -> Self {
74 RequestStrategy::TryTimes(10)
75 }
76}
77
78impl SpClient {
79 pub fn set_strategy(&self, strategy: RequestStrategy) {
80 self.lock(|inner| inner.strategy = strategy)
81 }
82
83 pub async fn flush_accesspoint(&self) {
84 self.lock(|inner| inner.accesspoint = None)
85 }
86
87 pub async fn get_accesspoint(&self) -> Result<SocketAddress, Error> {
88 let ap = self.lock(|inner| inner.accesspoint.clone());
90 let tuple = match ap {
91 Some(tuple) => tuple,
92 None => {
93 let tuple = self.session().apresolver().resolve("spclient").await?;
94 self.lock(|inner| inner.accesspoint = Some(tuple.clone()));
95 info!(
96 "Resolved \"{}:{}\" as spclient access point",
97 tuple.0, tuple.1
98 );
99 tuple
100 }
101 };
102 Ok(tuple)
103 }
104
105 pub async fn base_url(&self) -> Result<String, Error> {
106 let ap = self.get_accesspoint().await?;
107 Ok(format!("https://{}:{}", ap.0, ap.1))
108 }
109
110 async fn client_token_request<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
111 let body = message.write_to_bytes()?;
112
113 let request = Request::builder()
114 .method(&Method::POST)
115 .uri("https://clienttoken.spotify.com/v1/clienttoken")
116 .header(ACCEPT, HeaderValue::from_static("application/x-protobuf"))
117 .body(body.into())?;
118
119 self.session().http_client().request_body(request).await
120 }
121
122 pub async fn client_token(&self) -> Result<String, Error> {
123 let client_token = self.lock(|inner| {
124 if let Some(token) = &inner.client_token {
125 if token.is_expired() {
126 inner.client_token = None;
127 }
128 }
129 inner.client_token.clone()
130 });
131
132 if let Some(client_token) = client_token {
133 return Ok(client_token.access_token);
134 }
135
136 debug!("Client token unavailable or expired, requesting new token.");
137
138 let mut request = ClientTokenRequest::new();
139 request.request_type = ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST.into();
140
141 let client_data = request.mut_client_data();
142
143 client_data.client_version = spotify_semantic_version();
144
145 let os = OS;
152 let client_id = match os {
153 "macos" | "windows" => self.session().client_id(),
154 os => SessionConfig::default_for_os(os).client_id,
155 };
156 client_data.client_id = client_id;
157
158 let connectivity_data = client_data.mut_connectivity_sdk_data();
159 connectivity_data.device_id = self.session().device_id().to_string();
160
161 let platform_data = connectivity_data
162 .platform_specific_data
163 .mut_or_insert_default();
164
165 let os_version = os_version();
166 let kernel_version = System::kernel_version().unwrap_or_else(|| String::from("0"));
167
168 match os {
169 "windows" => {
170 let os_version = os_version.parse::<f32>().unwrap_or(10.) as i32;
171 let kernel_version = kernel_version.parse::<i32>().unwrap_or(21370);
172
173 let (pe, image_file) = match std::env::consts::ARCH {
174 "arm" => (448, 452),
175 "aarch64" => (43620, 452),
176 "x86_64" => (34404, 34404),
177 _ => (332, 332), };
179
180 let windows_data = platform_data.mut_desktop_windows();
181 windows_data.os_version = os_version;
182 windows_data.os_build = kernel_version;
183 windows_data.platform_id = 2;
184 windows_data.unknown_value_6 = 9;
185 windows_data.image_file_machine = image_file;
186 windows_data.pe_machine = pe;
187 windows_data.unknown_value_10 = true;
188 }
189 "ios" => {
190 let ios_data = platform_data.mut_ios();
191 ios_data.user_interface_idiom = 0;
192 ios_data.target_iphone_simulator = false;
193 ios_data.hw_machine = "iPhone14,5".to_string();
194 ios_data.system_version = os_version;
195 }
196 "android" => {
197 let android_data = platform_data.mut_android();
198 android_data.android_version = os_version;
199 android_data.api_version = 31;
200 "Pixel".clone_into(&mut android_data.device_name);
201 "GF5KQ".clone_into(&mut android_data.model_str);
202 "Google".clone_into(&mut android_data.vendor);
203 }
204 "macos" => {
205 let macos_data = platform_data.mut_desktop_macos();
206 macos_data.system_version = os_version;
207 macos_data.hw_model = "iMac21,1".to_string();
208 macos_data.compiled_cpu_type = std::env::consts::ARCH.to_string();
209 }
210 _ => {
211 let linux_data = platform_data.mut_desktop_linux();
212 linux_data.system_name = "Linux".to_string();
213 linux_data.system_release = kernel_version;
214 linux_data.system_version = os_version;
215 linux_data.hardware = std::env::consts::ARCH.to_string();
216 }
217 }
218
219 let mut response = self.client_token_request(&request).await?;
220 let mut count = 0;
221 const MAX_TRIES: u8 = 3;
222
223 let token_response = loop {
224 count += 1;
225
226 let message = ClientTokenResponse::parse_from_bytes(&response)?;
227
228 match ClientTokenResponseType::from_i32(message.response_type.value()) {
229 Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => {
232 debug!("Received a granted token");
233 break message;
234 }
235 Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => {
236 debug!("Received a hash cash challenge, solving...");
237
238 let challenges = message.challenges().clone();
239 let state = challenges.state;
240 if let Some(challenge) = challenges.challenges.first() {
241 let hash_cash_challenge = challenge.evaluate_hashcash_parameters();
242
243 let ctx = vec![];
244 let prefix = HEXUPPER_PERMISSIVE
245 .decode(hash_cash_challenge.prefix.as_bytes())
246 .map_err(|e| {
247 Error::failed_precondition(format!(
248 "Unable to decode hash cash challenge: {e}"
249 ))
250 })?;
251 let length = hash_cash_challenge.length;
252
253 let mut suffix = [0u8; 0x10];
254 let answer = util::solve_hash_cash(&ctx, &prefix, length, &mut suffix);
255
256 match answer {
257 Ok(_) => {
258 let suffix = HEXUPPER_PERMISSIVE.encode(&suffix);
260
261 let mut answer_message = ClientTokenRequest::new();
262 answer_message.request_type =
263 ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST
264 .into();
265
266 let challenge_answers = answer_message.mut_challenge_answers();
267
268 let mut challenge_answer = ChallengeAnswer::new();
269 challenge_answer.mut_hash_cash().suffix = suffix;
270 challenge_answer.ChallengeType =
271 ChallengeType::CHALLENGE_HASH_CASH.into();
272
273 challenge_answers.state = state.to_string();
274 challenge_answers.answers.push(challenge_answer);
275
276 trace!("Answering hash cash challenge");
277 match self.client_token_request(&answer_message).await {
278 Ok(token) => {
279 response = token;
280 continue;
281 }
282 Err(e) => {
283 trace!(
284 "Answer not accepted {}/{}: {}",
285 count,
286 MAX_TRIES,
287 e
288 );
289 }
290 }
291 }
292 Err(e) => trace!(
293 "Unable to solve hash cash challenge {}/{}: {}",
294 count,
295 MAX_TRIES,
296 e
297 ),
298 }
299
300 if count < MAX_TRIES {
301 response = self.client_token_request(&request).await?;
302 } else {
303 return Err(Error::failed_precondition(format!(
304 "Unable to solve any of {MAX_TRIES} hash cash challenges"
305 )));
306 }
307 } else {
308 return Err(Error::failed_precondition("No challenges found"));
309 }
310 }
311
312 Some(unknown) => {
313 return Err(Error::unimplemented(format!(
314 "Unknown client token response type: {unknown:?}"
315 )))
316 }
317 None => return Err(Error::failed_precondition("No client token response type")),
318 }
319 };
320
321 let granted_token = token_response.granted_token();
322 let access_token = granted_token.token.to_owned();
323
324 self.lock(|inner| {
325 let client_token = Token {
326 access_token: access_token.clone(),
327 expires_in: Duration::from_secs(
328 granted_token
329 .refresh_after_seconds
330 .try_into()
331 .unwrap_or(7200),
332 ),
333 token_type: "client-token".to_string(),
334 scopes: granted_token
335 .domains
336 .iter()
337 .map(|d| d.domain.clone())
338 .collect(),
339 timestamp: Instant::now(),
340 };
341
342 inner.client_token = Some(client_token);
343 });
344
345 trace!("Got client token: {:?}", granted_token);
346
347 Ok(access_token)
348 }
349
350 pub async fn request_with_protobuf<M: Message + MessageFull>(
351 &self,
352 method: &Method,
353 endpoint: &str,
354 headers: Option<HeaderMap>,
355 message: &M,
356 ) -> SpClientResult {
357 let body = protobuf::text_format::print_to_string(message);
358
359 let mut headers = headers.unwrap_or_default();
360 headers.insert(
361 CONTENT_TYPE,
362 HeaderValue::from_static("application/x-protobuf"),
363 );
364
365 self.request(method, endpoint, Some(headers), Some(&body))
366 .await
367 }
368
369 pub async fn request_as_json(
370 &self,
371 method: &Method,
372 endpoint: &str,
373 headers: Option<HeaderMap>,
374 body: Option<&str>,
375 ) -> SpClientResult {
376 let mut headers = headers.unwrap_or_default();
377 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
378
379 self.request(method, endpoint, Some(headers), body).await
380 }
381
382 pub async fn request(
383 &self,
384 method: &Method,
385 endpoint: &str,
386 headers: Option<HeaderMap>,
387 body: Option<&str>,
388 ) -> SpClientResult {
389 let mut tries: usize = 0;
390 let mut last_response;
391
392 let body = body.unwrap_or_default();
393
394 loop {
395 tries += 1;
396
397 let mut url = self.base_url().await?;
400 url.push_str(endpoint);
401
402 let separator = match url.find('?') {
403 Some(_) => "&",
404 None => "?",
405 };
406
407 let _ = write!(
412 url,
413 "{}product=0&country={}",
414 separator,
415 self.session().country()
416 );
417
418 if !url.contains("salt=") {
420 let _ = write!(url, "&salt={}", rand::thread_rng().next_u32());
421 }
422
423 let mut request = Request::builder()
424 .method(method)
425 .uri(url)
426 .body(body.to_owned().into())?;
427
428 let token = self.session().login5().auth_token().await?;
430
431 let headers_mut = request.headers_mut();
432 if let Some(ref hdrs) = headers {
433 *headers_mut = hdrs.clone();
434 }
435 headers_mut.insert(
436 AUTHORIZATION,
437 HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?,
438 );
439
440 match self.client_token().await {
441 Ok(client_token) => {
442 let _ = headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?);
443 }
444 Err(e) => {
445 warn!("Unable to get client token: {e} Trying to continue without...")
447 }
448 }
449
450 last_response = self.session().http_client().request_body(request).await;
451
452 if last_response.is_ok() {
453 return last_response;
454 }
455
456 if let RequestStrategy::TryTimes(max_tries) = self.lock(|inner| inner.strategy) {
459 if tries >= max_tries {
460 break;
461 }
462 }
463
464 if let Err(ref network_error) = last_response {
467 match network_error.kind {
468 ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => {
469 if tries % 3 == 0 {
471 self.flush_accesspoint().await
472 }
473 }
474 _ => break, }
476 }
477
478 debug!("Error was: {:?}", last_response);
479 }
480
481 last_response
482 }
483
484 pub async fn put_connect_state(
485 &self,
486 connection_id: &str,
487 state: &PutStateRequest,
488 ) -> SpClientResult {
489 let endpoint = format!("/connect-state/v1/devices/{}", self.session().device_id());
490
491 let mut headers = HeaderMap::new();
492 headers.insert("X-Spotify-Connection-Id", connection_id.parse()?);
493
494 self.request_with_protobuf(&Method::PUT, &endpoint, Some(headers), state)
495 .await
496 }
497
498 pub async fn get_metadata(&self, scope: &str, id: &SpotifyId) -> SpClientResult {
499 let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()?);
500 self.request(&Method::GET, &endpoint, None, None).await
501 }
502
503 pub async fn get_track_metadata(&self, track_id: &SpotifyId) -> SpClientResult {
504 self.get_metadata("track", track_id).await
505 }
506
507 pub async fn get_episode_metadata(&self, episode_id: &SpotifyId) -> SpClientResult {
508 self.get_metadata("episode", episode_id).await
509 }
510
511 pub async fn get_album_metadata(&self, album_id: &SpotifyId) -> SpClientResult {
512 self.get_metadata("album", album_id).await
513 }
514
515 pub async fn get_artist_metadata(&self, artist_id: &SpotifyId) -> SpClientResult {
516 self.get_metadata("artist", artist_id).await
517 }
518
519 pub async fn get_show_metadata(&self, show_id: &SpotifyId) -> SpClientResult {
520 self.get_metadata("show", show_id).await
521 }
522
523 pub async fn get_lyrics(&self, track_id: &SpotifyId) -> SpClientResult {
524 let endpoint = format!("/color-lyrics/v2/track/{}", track_id.to_base62()?);
525
526 self.request_as_json(&Method::GET, &endpoint, None, None)
527 .await
528 }
529
530 pub async fn get_lyrics_for_image(
531 &self,
532 track_id: &SpotifyId,
533 image_id: &FileId,
534 ) -> SpClientResult {
535 let endpoint = format!(
536 "/color-lyrics/v2/track/{}/image/spotify:image:{}",
537 track_id.to_base62()?,
538 image_id
539 );
540
541 self.request_as_json(&Method::GET, &endpoint, None, None)
542 .await
543 }
544
545 pub async fn get_playlist(&self, playlist_id: &SpotifyId) -> SpClientResult {
546 let endpoint = format!("/playlist/v2/playlist/{}", playlist_id.to_base62()?);
547
548 self.request(&Method::GET, &endpoint, None, None).await
549 }
550
551 pub async fn get_user_profile(
552 &self,
553 username: &str,
554 playlist_limit: Option<u32>,
555 artist_limit: Option<u32>,
556 ) -> SpClientResult {
557 let mut endpoint = format!("/user-profile-view/v3/profile/{username}");
558
559 if playlist_limit.is_some() || artist_limit.is_some() {
560 let _ = write!(endpoint, "?");
561
562 if let Some(limit) = playlist_limit {
563 let _ = write!(endpoint, "playlist_limit={limit}");
564 if artist_limit.is_some() {
565 let _ = write!(endpoint, "&");
566 }
567 }
568
569 if let Some(limit) = artist_limit {
570 let _ = write!(endpoint, "artist_limit={limit}");
571 }
572 }
573
574 self.request_as_json(&Method::GET, &endpoint, None, None)
575 .await
576 }
577
578 pub async fn get_user_followers(&self, username: &str) -> SpClientResult {
579 let endpoint = format!("/user-profile-view/v3/profile/{username}/followers");
580
581 self.request_as_json(&Method::GET, &endpoint, None, None)
582 .await
583 }
584
585 pub async fn get_user_following(&self, username: &str) -> SpClientResult {
586 let endpoint = format!("/user-profile-view/v3/profile/{username}/following");
587
588 self.request_as_json(&Method::GET, &endpoint, None, None)
589 .await
590 }
591
592 pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult {
593 let endpoint = format!(
594 "/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json",
595 track_id.to_uri()?
596 );
597
598 self.request_as_json(&Method::GET, &endpoint, None, None)
599 .await
600 }
601
602 pub async fn get_apollo_station(
612 &self,
613 scope: &str,
614 context_uri: &str,
615 count: Option<usize>,
616 previous_tracks: Vec<SpotifyId>,
617 autoplay: bool,
618 ) -> SpClientResult {
619 let mut endpoint = format!("/radio-apollo/v3/{scope}/{context_uri}?autoplay={autoplay}");
620
621 if let Some(count) = count {
623 let _ = write!(endpoint, "&count={count}");
624 }
625
626 let previous_track_str = previous_tracks
627 .iter()
628 .map(|track| track.to_base62())
629 .collect::<Result<Vec<_>, _>>()?
630 .join(",");
631 if !previous_track_str.is_empty() {
633 let _ = write!(endpoint, "&prev_tracks={previous_track_str}");
634 }
635
636 self.request_as_json(&Method::GET, &endpoint, None, None)
637 .await
638 }
639
640 pub async fn get_next_page(&self, next_page_uri: &str) -> SpClientResult {
641 let endpoint = next_page_uri.trim_start_matches("hm:/");
642 self.request_as_json(&Method::GET, endpoint, None, None)
643 .await
644 }
645
646 pub async fn get_canvases(&self, request: EntityCanvazRequest) -> SpClientResult {
651 let endpoint = "/canvaz-cache/v0/canvases";
652 self.request_with_protobuf(&Method::POST, endpoint, None, &request)
653 .await
654 }
655
656 pub async fn get_extended_metadata(&self, request: BatchedEntityRequest) -> SpClientResult {
657 let endpoint = "/extended-metadata/v0/extended-metadata";
658 self.request_with_protobuf(&Method::POST, endpoint, None, &request)
659 .await
660 }
661
662 pub async fn get_audio_storage(&self, file_id: &FileId) -> SpClientResult {
663 let endpoint = format!(
664 "/storage-resolve/files/audio/interactive/{}",
665 file_id.to_base16()?
666 );
667 self.request(&Method::GET, &endpoint, None, None).await
668 }
669
670 pub fn stream_from_cdn(
671 &self,
672 cdn_url: &CdnUrl,
673 offset: usize,
674 length: usize,
675 ) -> Result<IntoStream<ResponseFuture>, Error> {
676 let url = cdn_url.try_get_url()?;
677 let req = Request::builder()
678 .method(&Method::GET)
679 .uri(url)
680 .header(
681 RANGE,
682 HeaderValue::from_str(&format!("bytes={}-{}", offset, offset + length - 1))?,
683 )
684 .body(Bytes::new())?;
685
686 let stream = self.session().http_client().request_stream(req)?;
687
688 Ok(stream)
689 }
690
691 pub async fn request_url(&self, url: &str) -> SpClientResult {
692 let request = Request::builder()
693 .method(&Method::GET)
694 .uri(url)
695 .body(Bytes::new())?;
696
697 self.session().http_client().request_body(request).await
698 }
699
700 pub async fn get_audio_preview(&self, preview_id: &FileId) -> SpClientResult {
702 let attribute = "audio-preview-url-template";
703 let template = self
704 .session()
705 .get_user_attribute(attribute)
706 .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?;
707
708 let mut url = template.replace("{id}", &preview_id.to_base16()?);
709 let separator = match url.find('?') {
710 Some(_) => "&",
711 None => "?",
712 };
713 let _ = write!(url, "{}cid={}", separator, self.session().client_id());
714
715 self.request_url(&url).await
716 }
717
718 pub async fn get_head_file(&self, file_id: &FileId) -> SpClientResult {
720 let attribute = "head-files-url";
721 let template = self
722 .session()
723 .get_user_attribute(attribute)
724 .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?;
725
726 let url = template.replace("{file_id}", &file_id.to_base16()?);
727
728 self.request_url(&url).await
729 }
730
731 pub async fn get_image(&self, image_id: &FileId) -> SpClientResult {
732 let attribute = "image-url";
733 let template = self
734 .session()
735 .get_user_attribute(attribute)
736 .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?;
737 let url = template.replace("{file_id}", &image_id.to_base16()?);
738
739 self.request_url(&url).await
740 }
741}