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