1use std::{
2 fmt::Write,
3 time::{Duration, SystemTime},
4};
5
6use crate::config::{OS, os_version};
7use crate::{
8 Error, FileId, SpotifyId, SpotifyUri,
9 apresolve::SocketAddress,
10 config::SessionConfig,
11 dealer::protocol::TransferOptions,
12 error::ErrorKind,
13 protocol::{
14 autoplay_context_request::AutoplayContextRequest,
15 clienttoken_http::{
16 ChallengeAnswer, ChallengeType, ClientTokenRequest, ClientTokenRequestType,
17 ClientTokenResponse, ClientTokenResponseType,
18 },
19 connect::PutStateRequest,
20 context::Context,
21 extended_metadata::BatchedEntityRequest,
22 extended_metadata::{BatchedExtensionResponse, EntityRequest, ExtensionQuery},
23 extension_kind::ExtensionKind,
24 },
25 token::Token,
26 util,
27 version::spotify_semantic_version,
28};
29use bytes::Bytes;
30use data_encoding::HEXUPPER_PERMISSIVE;
31use futures_util::future::IntoStream;
32use http::{Uri, header::HeaderValue};
33use hyper::{
34 HeaderMap, Method, Request,
35 header::{ACCEPT, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, HeaderName, RANGE},
36};
37use hyper_util::client::legacy::ResponseFuture;
38use protobuf::{Enum, EnumOrUnknown, Message, MessageFull};
39use rand::RngCore;
40use serde::Serialize;
41use sysinfo::System;
42use thiserror::Error;
43
44component! {
45 SpClient : SpClientInner {
46 accesspoint: Option<SocketAddress> = None,
47 strategy: RequestStrategy = RequestStrategy::default(),
48 client_token: Option<Token> = None,
49 }
50}
51
52pub type SpClientResult = Result<Bytes, Error>;
53
54#[allow(clippy::declare_interior_mutable_const)]
55pub const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-token");
56#[allow(clippy::declare_interior_mutable_const)]
57const CONNECTION_ID: HeaderName = HeaderName::from_static("x-spotify-connection-id");
58
59const NO_METRICS_AND_SALT: RequestOptions = RequestOptions {
60 metrics: false,
61 salt: false,
62 base_url: None,
63};
64
65#[derive(Debug, Error)]
66pub enum SpClientError {
67 #[error("missing attribute {0}")]
68 Attribute(String),
69 #[error("expected data but received none")]
70 NoData,
71 #[error("expected an entry to exist in {0}")]
72 ExpectedEntry(&'static str),
73}
74
75impl From<SpClientError> for Error {
76 fn from(err: SpClientError) -> Self {
77 Self::failed_precondition(err)
78 }
79}
80
81#[derive(Copy, Clone, Debug)]
82pub enum RequestStrategy {
83 TryTimes(usize),
84 Infinitely,
85}
86
87impl Default for RequestStrategy {
88 fn default() -> Self {
89 RequestStrategy::TryTimes(10)
90 }
91}
92
93pub struct RequestOptions {
94 metrics: bool,
95 salt: bool,
96 base_url: Option<&'static str>,
97}
98
99impl Default for RequestOptions {
100 fn default() -> Self {
101 Self {
102 metrics: true,
103 salt: true,
104 base_url: None,
105 }
106 }
107}
108
109#[derive(Debug, Serialize)]
110pub struct TransferRequest {
111 pub transfer_options: TransferOptions,
112}
113
114impl SpClient {
115 pub fn set_strategy(&self, strategy: RequestStrategy) {
116 self.lock(|inner| inner.strategy = strategy)
117 }
118
119 pub async fn flush_accesspoint(&self) {
120 self.lock(|inner| inner.accesspoint = None)
121 }
122
123 pub async fn get_accesspoint(&self) -> Result<SocketAddress, Error> {
124 let ap = self.lock(|inner| inner.accesspoint.clone());
126 let tuple = match ap {
127 Some(tuple) => tuple,
128 None => {
129 let tuple = self.session().apresolver().resolve("spclient").await?;
130 self.lock(|inner| inner.accesspoint = Some(tuple.clone()));
131 info!(
132 "Resolved \"{}:{}\" as spclient access point",
133 tuple.0, tuple.1
134 );
135 tuple
136 }
137 };
138 Ok(tuple)
139 }
140
141 pub async fn base_url(&self) -> Result<String, Error> {
142 let ap = self.get_accesspoint().await?;
143 Ok(format!("https://{}:{}", ap.0, ap.1))
144 }
145
146 async fn client_token_request<M: Message>(&self, message: &M) -> Result<Bytes, Error> {
147 let body = message.write_to_bytes()?;
148
149 let request = Request::builder()
150 .method(&Method::POST)
151 .uri("https://clienttoken.spotify.com/v1/clienttoken")
152 .header(ACCEPT, HeaderValue::from_static("application/x-protobuf"))
153 .body(body.into())?;
154
155 self.session().http_client().request_body(request).await
156 }
157
158 pub async fn client_token(&self) -> Result<String, Error> {
159 let client_token = self.lock(|inner| {
160 if let Some(token) = &inner.client_token {
161 if token.is_expired() {
162 inner.client_token = None;
163 }
164 }
165 inner.client_token.clone()
166 });
167
168 if let Some(client_token) = client_token {
169 return Ok(client_token.access_token);
170 }
171
172 debug!("Client token unavailable or expired, requesting new token.");
173
174 let mut request = ClientTokenRequest::new();
175 request.request_type = ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST.into();
176
177 let client_data = request.mut_client_data();
178
179 client_data.client_version = spotify_semantic_version();
180
181 let os = OS;
188 let client_id = match os {
189 "macos" | "windows" => self.session().client_id(),
190 os => SessionConfig::default_for_os(os).client_id,
191 };
192 client_data.client_id = client_id;
193
194 let connectivity_data = client_data.mut_connectivity_sdk_data();
195 connectivity_data.device_id = self.session().device_id().to_string();
196
197 let platform_data = connectivity_data
198 .platform_specific_data
199 .mut_or_insert_default();
200
201 let os_version = os_version();
202 let kernel_version = System::kernel_version().unwrap_or_else(|| String::from("0"));
203
204 match os {
205 "windows" => {
206 let os_version = os_version.parse::<f32>().unwrap_or(10.) as i32;
207 let kernel_version = kernel_version.parse::<i32>().unwrap_or(21370);
208
209 let (pe, image_file) = match std::env::consts::ARCH {
210 "arm" => (448, 452),
211 "aarch64" => (43620, 452),
212 "x86_64" => (34404, 34404),
213 _ => (332, 332), };
215
216 let windows_data = platform_data.mut_desktop_windows();
217 windows_data.os_version = os_version;
218 windows_data.os_build = kernel_version;
219 windows_data.platform_id = 2;
220 windows_data.unknown_value_6 = 9;
221 windows_data.image_file_machine = image_file;
222 windows_data.pe_machine = pe;
223 windows_data.unknown_value_10 = true;
224 }
225 "ios" => {
226 let ios_data = platform_data.mut_ios();
227 ios_data.user_interface_idiom = 0;
228 ios_data.target_iphone_simulator = false;
229 ios_data.hw_machine = "iPhone14,5".to_string();
230 ios_data.system_version = os_version;
231 }
232 "android" => {
233 let android_data = platform_data.mut_android();
234 android_data.android_version = os_version;
235 android_data.api_version = 31;
236 "Pixel".clone_into(&mut android_data.device_name);
237 "GF5KQ".clone_into(&mut android_data.model_str);
238 "Google".clone_into(&mut android_data.vendor);
239 }
240 "macos" => {
241 let macos_data = platform_data.mut_desktop_macos();
242 macos_data.system_version = os_version;
243 macos_data.hw_model = "iMac21,1".to_string();
244 macos_data.compiled_cpu_type = std::env::consts::ARCH.to_string();
245 }
246 _ => {
247 let linux_data = platform_data.mut_desktop_linux();
248 linux_data.system_name = "Linux".to_string();
249 linux_data.system_release = kernel_version;
250 linux_data.system_version = os_version;
251 linux_data.hardware = std::env::consts::ARCH.to_string();
252 }
253 }
254
255 let mut response = self.client_token_request(&request).await?;
256 let mut count = 0;
257 const MAX_TRIES: u8 = 3;
258
259 let token_response = loop {
260 count += 1;
261
262 let message = ClientTokenResponse::parse_from_bytes(&response)?;
263
264 match ClientTokenResponseType::from_i32(message.response_type.value()) {
265 Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => {
268 debug!("Received a granted token");
269 break message;
270 }
271 Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => {
272 debug!("Received a hash cash challenge, solving...");
273
274 let challenges = message.challenges().clone();
275 let state = challenges.state;
276 if let Some(challenge) = challenges.challenges.first() {
277 let hash_cash_challenge = challenge.evaluate_hashcash_parameters();
278
279 let ctx = vec![];
280 let prefix = HEXUPPER_PERMISSIVE
281 .decode(hash_cash_challenge.prefix.as_bytes())
282 .map_err(|e| {
283 Error::failed_precondition(format!(
284 "Unable to decode hash cash challenge: {e}"
285 ))
286 })?;
287 let length = hash_cash_challenge.length;
288
289 let mut suffix = [0u8; 0x10];
290 let answer = util::solve_hash_cash(&ctx, &prefix, length, &mut suffix);
291
292 match answer {
293 Ok(_) => {
294 let suffix = HEXUPPER_PERMISSIVE.encode(&suffix);
296
297 let mut answer_message = ClientTokenRequest::new();
298 answer_message.request_type =
299 ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST
300 .into();
301
302 let challenge_answers = answer_message.mut_challenge_answers();
303
304 let mut challenge_answer = ChallengeAnswer::new();
305 challenge_answer.mut_hash_cash().suffix = suffix;
306 challenge_answer.ChallengeType =
307 ChallengeType::CHALLENGE_HASH_CASH.into();
308
309 challenge_answers.state = state.to_string();
310 challenge_answers.answers.push(challenge_answer);
311
312 trace!("Answering hash cash challenge");
313 match self.client_token_request(&answer_message).await {
314 Ok(token) => {
315 response = token;
316 continue;
317 }
318 Err(e) => {
319 trace!("Answer not accepted {count}/{MAX_TRIES}: {e}");
320 }
321 }
322 }
323 Err(e) => trace!(
324 "Unable to solve hash cash challenge {count}/{MAX_TRIES}: {e}"
325 ),
326 }
327
328 if count < MAX_TRIES {
329 response = self.client_token_request(&request).await?;
330 } else {
331 return Err(Error::failed_precondition(format!(
332 "Unable to solve any of {MAX_TRIES} hash cash challenges"
333 )));
334 }
335 } else {
336 return Err(Error::failed_precondition("No challenges found"));
337 }
338 }
339
340 Some(unknown) => {
341 return Err(Error::unimplemented(format!(
342 "Unknown client token response type: {unknown:?}"
343 )));
344 }
345 None => return Err(Error::failed_precondition("No client token response type")),
346 }
347 };
348
349 let granted_token = token_response.granted_token();
350 let access_token = granted_token.token.to_owned();
351
352 self.lock(|inner| {
353 let client_token = Token {
354 access_token: access_token.clone(),
355 expires_in: Duration::from_secs(
356 granted_token
357 .refresh_after_seconds
358 .try_into()
359 .unwrap_or(7200),
360 ),
361 token_type: "client-token".to_string(),
362 scopes: granted_token
363 .domains
364 .iter()
365 .map(|d| d.domain.clone())
366 .collect(),
367 timestamp: SystemTime::now(),
368 };
369
370 inner.client_token = Some(client_token);
371 });
372
373 trace!("Got client token: {granted_token:?}");
374
375 Ok(access_token)
376 }
377
378 pub async fn request_with_protobuf<M: Message + MessageFull>(
379 &self,
380 method: &Method,
381 endpoint: &str,
382 headers: Option<HeaderMap>,
383 message: &M,
384 ) -> SpClientResult {
385 self.request_with_protobuf_and_options(
386 method,
387 endpoint,
388 headers,
389 message,
390 &Default::default(),
391 )
392 .await
393 }
394
395 pub async fn request_with_protobuf_and_options<M: Message + MessageFull>(
396 &self,
397 method: &Method,
398 endpoint: &str,
399 headers: Option<HeaderMap>,
400 message: &M,
401 options: &RequestOptions,
402 ) -> SpClientResult {
403 let body = message.write_to_bytes()?;
404
405 let mut headers = headers.unwrap_or_default();
406 headers.insert(
407 CONTENT_TYPE,
408 HeaderValue::from_static("application/x-protobuf"),
409 );
410
411 self.request_with_options(method, endpoint, Some(headers), Some(&body), options)
412 .await
413 }
414
415 pub async fn request_as_json(
416 &self,
417 method: &Method,
418 endpoint: &str,
419 headers: Option<HeaderMap>,
420 body: Option<&str>,
421 ) -> SpClientResult {
422 let mut headers = headers.unwrap_or_default();
423 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
424
425 self.request(method, endpoint, Some(headers), body.map(|s| s.as_bytes()))
426 .await
427 }
428
429 pub async fn request(
430 &self,
431 method: &Method,
432 endpoint: &str,
433 headers: Option<HeaderMap>,
434 body: Option<&[u8]>,
435 ) -> SpClientResult {
436 self.request_with_options(method, endpoint, headers, body, &Default::default())
437 .await
438 }
439
440 pub async fn request_with_options(
441 &self,
442 method: &Method,
443 endpoint: &str,
444 headers: Option<HeaderMap>,
445 body: Option<&[u8]>,
446 options: &RequestOptions,
447 ) -> SpClientResult {
448 let mut tries: usize = 0;
449 let mut last_response;
450
451 let body = body.unwrap_or_default();
452
453 loop {
454 tries += 1;
455
456 let mut url = match options.base_url {
459 Some(base_url) => base_url.to_string(),
460 None => self.base_url().await?,
461 };
462 url.push_str(endpoint);
463
464 if options.metrics && !url.contains("product=0") {
469 let _ = write!(
470 url,
471 "{}product=0&country={}",
472 util::get_next_query_separator(&url),
473 self.session().country()
474 );
475 }
476
477 if options.salt && !url.contains("salt=") {
479 let _ = write!(
480 url,
481 "{}salt={}",
482 util::get_next_query_separator(&url),
483 rand::rng().next_u32()
484 );
485 }
486
487 let mut request = Request::builder()
488 .method(method)
489 .uri(url)
490 .header(CONTENT_LENGTH, body.len())
491 .body(Bytes::copy_from_slice(body))?;
492
493 let token = self.session().login5().auth_token().await?;
495
496 let headers_mut = request.headers_mut();
497 if let Some(ref headers) = headers {
498 for (name, value) in headers {
499 headers_mut.insert(name, value.clone());
500 }
501 }
502
503 headers_mut.insert(
504 AUTHORIZATION,
505 HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?,
506 );
507
508 match self.client_token().await {
509 Ok(client_token) => {
510 let _ = headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?);
511 }
512 Err(e) => {
513 warn!("Unable to get client token: {e} Trying to continue without...")
515 }
516 }
517
518 last_response = self.session().http_client().request_body(request).await;
519
520 if last_response.is_ok() {
521 return last_response;
522 }
523
524 if let RequestStrategy::TryTimes(max_tries) = self.lock(|inner| inner.strategy) {
527 if tries >= max_tries {
528 break;
529 }
530 }
531
532 if let Err(ref network_error) = last_response {
535 match network_error.kind {
536 ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => {
537 if tries % 3 == 0 {
539 self.flush_accesspoint().await
540 }
541 }
542 _ => break, }
544 }
545
546 debug!("Error was: {last_response:?}");
547 }
548
549 last_response
550 }
551
552 pub async fn put_connect_state_request(&self, state: &PutStateRequest) -> SpClientResult {
553 let endpoint = format!("/connect-state/v1/devices/{}", self.session().device_id());
554
555 let mut headers = HeaderMap::new();
556 headers.insert(CONNECTION_ID, self.session().connection_id().parse()?);
557
558 self.request_with_protobuf(&Method::PUT, &endpoint, Some(headers), state)
559 .await
560 }
561
562 pub async fn delete_connect_state_request(&self) -> SpClientResult {
563 let endpoint = format!("/connect-state/v1/devices/{}", self.session().device_id());
564 self.request(&Method::DELETE, &endpoint, None, None).await
565 }
566
567 pub async fn put_connect_state_inactive(&self, notify: bool) -> SpClientResult {
568 let endpoint = format!(
569 "/connect-state/v1/devices/{}/inactive?notify={notify}",
570 self.session().device_id()
571 );
572
573 let mut headers = HeaderMap::new();
574 headers.insert(CONNECTION_ID, self.session().connection_id().parse()?);
575
576 self.request(&Method::PUT, &endpoint, Some(headers), None)
577 .await
578 }
579
580 pub async fn get_extended_metadata(
581 &self,
582 request: BatchedEntityRequest,
583 ) -> Result<BatchedExtensionResponse, Error> {
584 let res = self
585 .request_with_protobuf(
586 &Method::POST,
587 "/extended-metadata/v0/extended-metadata",
588 None,
589 &request,
590 )
591 .await?;
592 Ok(BatchedExtensionResponse::parse_from_bytes(&res)?)
593 }
594
595 pub async fn get_metadata(&self, kind: ExtensionKind, id: &SpotifyUri) -> SpClientResult {
596 let req = BatchedEntityRequest {
597 entity_request: vec![EntityRequest {
598 entity_uri: id.to_uri()?,
599 query: vec![ExtensionQuery {
600 extension_kind: EnumOrUnknown::new(kind),
601 ..Default::default()
602 }],
603 ..Default::default()
604 }],
605 ..Default::default()
606 };
607
608 let mut res = self.get_extended_metadata(req).await?;
609 let mut extended_metadata = res
610 .extended_metadata
611 .pop()
612 .ok_or(SpClientError::ExpectedEntry("extended_metadata"))?;
613
614 let mut data = extended_metadata
615 .extension_data
616 .pop()
617 .ok_or(SpClientError::ExpectedEntry("extension_data"))?;
618
619 match data.extension_data.take() {
620 None => Err(SpClientError::ExpectedEntry("data").into()),
621 Some(data) => Ok(Bytes::from(data.value)),
622 }
623 }
624
625 pub async fn get_track_metadata(&self, track_uri: &SpotifyUri) -> SpClientResult {
626 self.get_metadata(ExtensionKind::TRACK_V4, track_uri).await
627 }
628
629 pub async fn get_episode_metadata(&self, episode_uri: &SpotifyUri) -> SpClientResult {
630 self.get_metadata(ExtensionKind::EPISODE_V4, episode_uri)
631 .await
632 }
633
634 pub async fn get_album_metadata(&self, album_uri: &SpotifyUri) -> SpClientResult {
635 self.get_metadata(ExtensionKind::ALBUM_V4, album_uri).await
636 }
637
638 pub async fn get_artist_metadata(&self, artist_uri: &SpotifyUri) -> SpClientResult {
639 self.get_metadata(ExtensionKind::ARTIST_V4, artist_uri)
640 .await
641 }
642
643 pub async fn get_show_metadata(&self, show_uri: &SpotifyUri) -> SpClientResult {
644 self.get_metadata(ExtensionKind::SHOW_V4, show_uri).await
645 }
646
647 pub async fn get_lyrics(&self, track_id: &SpotifyId) -> SpClientResult {
648 let endpoint = format!("/color-lyrics/v2/track/{}", track_id.to_base62()?);
649
650 self.request_as_json(&Method::GET, &endpoint, None, None)
651 .await
652 }
653
654 pub async fn get_lyrics_for_image(
655 &self,
656 track_id: &SpotifyId,
657 image_id: &FileId,
658 ) -> SpClientResult {
659 let endpoint = format!(
660 "/color-lyrics/v2/track/{}/image/spotify:image:{}",
661 track_id.to_base62()?,
662 image_id
663 );
664
665 self.request_as_json(&Method::GET, &endpoint, None, None)
666 .await
667 }
668
669 pub async fn get_playlist(&self, playlist_id: &SpotifyId) -> SpClientResult {
670 let endpoint = format!("/playlist/v2/playlist/{}", playlist_id.to_base62()?);
671
672 self.request(&Method::GET, &endpoint, None, None).await
673 }
674
675 pub async fn get_user_profile(
676 &self,
677 username: &str,
678 playlist_limit: Option<u32>,
679 artist_limit: Option<u32>,
680 ) -> SpClientResult {
681 let mut endpoint = format!("/user-profile-view/v3/profile/{username}");
682
683 if playlist_limit.is_some() || artist_limit.is_some() {
684 let _ = write!(endpoint, "?");
685
686 if let Some(limit) = playlist_limit {
687 let _ = write!(endpoint, "playlist_limit={limit}");
688 if artist_limit.is_some() {
689 let _ = write!(endpoint, "&");
690 }
691 }
692
693 if let Some(limit) = artist_limit {
694 let _ = write!(endpoint, "artist_limit={limit}");
695 }
696 }
697
698 self.request_as_json(&Method::GET, &endpoint, None, None)
699 .await
700 }
701
702 pub async fn get_user_followers(&self, username: &str) -> SpClientResult {
703 let endpoint = format!("/user-profile-view/v3/profile/{username}/followers");
704
705 self.request_as_json(&Method::GET, &endpoint, None, None)
706 .await
707 }
708
709 pub async fn get_user_following(&self, username: &str) -> SpClientResult {
710 let endpoint = format!("/user-profile-view/v3/profile/{username}/following");
711
712 self.request_as_json(&Method::GET, &endpoint, None, None)
713 .await
714 }
715
716 pub async fn get_radio_for_track(&self, track_uri: &SpotifyUri) -> SpClientResult {
717 let endpoint = format!(
718 "/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json",
719 track_uri.to_uri()?
720 );
721
722 self.request_as_json(&Method::GET, &endpoint, None, None)
723 .await
724 }
725
726 pub async fn get_apollo_station(
736 &self,
737 scope: &str,
738 context_uri: &str,
739 count: Option<usize>,
740 previous_tracks: Vec<SpotifyId>,
741 autoplay: bool,
742 ) -> SpClientResult {
743 let mut endpoint = format!("/radio-apollo/v3/{scope}/{context_uri}?autoplay={autoplay}");
744
745 if let Some(count) = count {
747 let _ = write!(endpoint, "&count={count}");
748 }
749
750 let previous_track_str = previous_tracks
751 .iter()
752 .map(|track| track.to_base62())
753 .collect::<Result<Vec<_>, _>>()?
754 .join(",");
755 if !previous_track_str.is_empty() {
757 let _ = write!(endpoint, "&prev_tracks={previous_track_str}");
758 }
759
760 self.request_as_json(&Method::GET, &endpoint, None, None)
761 .await
762 }
763
764 pub async fn get_next_page(&self, next_page_uri: &str) -> SpClientResult {
765 let endpoint = next_page_uri.trim_start_matches("hm:/");
766 self.request_as_json(&Method::GET, endpoint, None, None)
767 .await
768 }
769
770 pub async fn get_audio_storage(&self, file_id: &FileId) -> SpClientResult {
774 let endpoint = format!(
775 "/storage-resolve/files/audio/interactive/{}",
776 file_id.to_base16()?
777 );
778 self.request(&Method::GET, &endpoint, None, None).await
779 }
780
781 pub fn stream_from_cdn<U>(
782 &self,
783 cdn_url: U,
784 offset: usize,
785 length: usize,
786 ) -> Result<IntoStream<ResponseFuture>, Error>
787 where
788 U: TryInto<Uri>,
789 <U as TryInto<Uri>>::Error: Into<http::Error>,
790 {
791 let req = Request::builder()
792 .method(&Method::GET)
793 .uri(cdn_url)
794 .header(
795 RANGE,
796 HeaderValue::from_str(&format!("bytes={}-{}", offset, offset + length - 1))?,
797 )
798 .body(Bytes::new())?;
799
800 let stream = self.session().http_client().request_stream(req)?;
801
802 Ok(stream)
803 }
804
805 pub async fn request_url(&self, url: &str) -> SpClientResult {
806 let request = Request::builder()
807 .method(&Method::GET)
808 .uri(url)
809 .body(Bytes::new())?;
810
811 self.session().http_client().request_body(request).await
812 }
813
814 pub async fn get_audio_preview(&self, preview_id: &FileId) -> SpClientResult {
816 const ATTRIBUTE: &str = "audio-preview-url-template";
817 let template = self
818 .session()
819 .get_user_attribute(ATTRIBUTE)
820 .ok_or_else(|| SpClientError::Attribute(ATTRIBUTE.to_string()))?;
821
822 let mut url = template.replace("{id}", &preview_id.to_base16()?);
823 let separator = match url.find('?') {
824 Some(_) => "&",
825 None => "?",
826 };
827 let _ = write!(url, "{}cid={}", separator, self.session().client_id());
828
829 self.request_url(&url).await
830 }
831
832 pub async fn get_head_file(&self, file_id: &FileId) -> SpClientResult {
834 const ATTRIBUTE: &str = "head-files-url";
835 let template = self
836 .session()
837 .get_user_attribute(ATTRIBUTE)
838 .ok_or_else(|| SpClientError::Attribute(ATTRIBUTE.to_string()))?;
839
840 let url = template.replace("{file_id}", &file_id.to_base16()?);
841
842 self.request_url(&url).await
843 }
844
845 pub async fn get_image(&self, image_id: &FileId) -> SpClientResult {
846 const ATTRIBUTE: &str = "image-url";
847 let template = self
848 .session()
849 .get_user_attribute(ATTRIBUTE)
850 .ok_or_else(|| SpClientError::Attribute(ATTRIBUTE.to_string()))?;
851 let url = template.replace("{file_id}", &image_id.to_base16()?);
852
853 self.request_url(&url).await
854 }
855
856 pub async fn get_context(&self, uri: &str) -> Result<Context, Error> {
880 let uri = format!("/context-resolve/v1/{uri}");
881
882 let res = self
883 .request_with_options(&Method::GET, &uri, None, None, &NO_METRICS_AND_SALT)
884 .await?;
885 let ctx_json = String::from_utf8(res.to_vec())?;
886 if ctx_json.is_empty() {
887 Err(SpClientError::NoData)?
888 }
889
890 let ctx = protobuf_json_mapping::parse_from_str::<Context>(&ctx_json);
891
892 if ctx.is_err() {
893 trace!("failed parsing context: {ctx_json}")
894 }
895
896 Ok(ctx?)
897 }
898
899 pub async fn get_autoplay_context(
900 &self,
901 context_request: &AutoplayContextRequest,
902 ) -> Result<Context, Error> {
903 let res = self
904 .request_with_protobuf_and_options(
905 &Method::POST,
906 "/context-resolve/v1/autoplay",
907 None,
908 context_request,
909 &NO_METRICS_AND_SALT,
910 )
911 .await?;
912
913 let ctx_json = String::from_utf8(res.to_vec())?;
914 if ctx_json.is_empty() {
915 Err(SpClientError::NoData)?
916 }
917
918 let ctx = protobuf_json_mapping::parse_from_str::<Context>(&ctx_json);
919
920 if ctx.is_err() {
921 trace!("failed parsing context: {ctx_json}")
922 }
923
924 Ok(ctx?)
925 }
926
927 pub async fn get_rootlist(&self, from: usize, length: Option<usize>) -> SpClientResult {
928 let length = length.unwrap_or(120);
929 let user = self.session().username();
930 let endpoint = format!(
931 "/playlist/v2/user/{user}/rootlist?decorate=revision,attributes,length,owner,capabilities,status_code&from={from}&length={length}"
932 );
933
934 self.request(&Method::GET, &endpoint, None, None).await
935 }
936
937 pub async fn transfer(
942 &self,
943 from_device_id: &str,
944 to_device_id: &str,
945 transfer_request: Option<&TransferRequest>,
946 ) -> SpClientResult {
947 let body = transfer_request.map(serde_json::to_string).transpose()?;
948
949 let endpoint =
950 format!("/connect-state/v1/connect/transfer/from/{from_device_id}/to/{to_device_id}");
951 self.request_with_options(
952 &Method::POST,
953 &endpoint,
954 None,
955 body.as_deref().map(|s| s.as_bytes()),
956 &NO_METRICS_AND_SALT,
957 )
958 .await
959 }
960}