soundcloud_rs/client/
tracks.rs1use ffmpeg_sidecar::command::FfmpegCommand;
2use ffmpeg_sidecar::download;
3use std::path::{Path, PathBuf};
4
5use crate::models::client::Client;
6use crate::models::client::Identifier;
7use crate::models::error::Error;
8use crate::models::query::{Paging, TracksQuery};
9use crate::models::response::{Track, Tracks};
10use crate::response::{Stream, StreamType, Transcoding, Waveform};
11
12impl Client {
13 pub async fn search_tracks(
14 &self,
15 query: Option<&TracksQuery>,
16 ) -> Result<Tracks, Error> {
17 let tracks: Tracks = self.get("search/tracks", query).await?;
18 Ok(tracks)
19 }
20
21 pub async fn get_track(
22 &self,
23 identifier: &Identifier,
24 ) -> Result<Track, Error> {
25 let url = format!("tracks/{identifier}");
26 let resp: Track = self.get(&url, None::<&()>).await?;
27 Ok(resp)
28 }
29
30 pub async fn get_track_related(
31 &self,
32 identifier: &Identifier,
33 pagination: Option<&Paging>,
34 ) -> Result<Tracks, Error> {
35 let url = format!("tracks/{identifier}/related");
36 let resp: Tracks = self.get(&url, pagination).await?;
37 Ok(resp)
38 }
39
40 pub async fn download_track(
41 &self,
42 identifier: &Identifier,
43 stream_type: Option<&StreamType>,
44 destination: Option<&str>,
45 filename: Option<&str>,
46 ) -> Result<(), Error> {
47 let track = self.get_track(identifier).await?;
48
49 let stream = match stream_type {
50 Some(stream_type) => stream_type,
51 None => &StreamType::Progressive,
52 };
53
54 if track.title.is_none() {
55 return Err(Error::new("Track title is missing"));
56 }
57
58 let title = match filename {
59 Some(filename) => filename,
60 None => track.title.as_ref().expect("Missing track title"),
61 };
62
63 let output_path = match destination {
64 Some(destination) => PathBuf::from(destination).join(format!("{title}.mp3")),
65 None => PathBuf::from(format!("{title}.mp3")),
66 };
67 if let Some(parent) = output_path.parent() {
68 if !parent.exists() {
69 std::fs::create_dir_all(parent)?;
70 }
71 }
72
73 let transcoding = self.get_transcoding_by_stream_type(&track, stream).await?;
74 let stream_url = self.get_stream_url(identifier, Some(stream)).await?;
75
76 match transcoding
77 .format
78 .as_ref()
79 .expect("Missing transcoding format")
80 .protocol
81 .as_ref()
82 {
83 Some(StreamType::Progressive) => {
84 self.download_progressive(&stream_url, &output_path).await?
85 }
86 Some(StreamType::Hls) => self.download_hls(&stream_url, &output_path).await?,
87 _ => return Err(Error::new("Invalid Stream Type")),
88 }
89
90 Ok(())
91 }
92
93 pub async fn get_track_waveform(
94 &self,
95 identifier: &Identifier,
96 ) -> Result<Waveform, Error> {
97 let track = self.get_track(identifier).await?;
98 let waveform_url = track.waveform_url.as_ref().expect("Missing waveform URL");
99 let response = reqwest::get(waveform_url).await?;
100 let waveform: Waveform = response.json::<Waveform>().await?;
101 Ok(waveform)
102 }
103
104 pub async fn get_stream_url(
105 &self,
106 identifier: &Identifier,
107 stream_type: Option<&StreamType>,
108 ) -> Result<String, Error> {
109 let track = self.get_track(identifier).await?;
110 let stream = match stream_type {
111 Some(stream_type) => stream_type,
112 None => &StreamType::Progressive,
113 };
114 let transcoding = self.get_transcoding_by_stream_type(&track, stream).await?;
115 let path = transcoding.url.as_ref().ok_or_else(|| Error::new("Missing transcoding URL"))?;
116 let client_id = self.get_client_id_value().await;
117 let (stream, _): (Stream, _) = Self::get_json(path, None, None::<&()>, &client_id).await?;
118 stream.url.ok_or_else(|| Error::new("Missing resolved stream URL"))
119 }
120
121 async fn get_transcoding_by_stream_type(
122 &self,
123 track: &Track,
124 stream_type: &StreamType,
125 ) -> Result<Transcoding, Error> {
126 let transcodings = track
127 .media
128 .as_ref()
129 .expect("Missing media")
130 .transcodings
131 .as_ref()
132 .expect("Missing transcodings");
133 if transcodings.is_empty() {
134 return Err(Error::new("No available download options"));
135 }
136
137 let transcoding: Option<Transcoding> = {
138 for t in transcodings {
139 let protocol = match t.format.as_ref().and_then(|f| f.protocol.as_ref()) {
140 Some(p) => p,
141 None => continue,
142 };
143 if *protocol != *stream_type {
144 continue;
145 }
146
147 let path = match t.url.as_ref() {
148 Some(u) => u,
149 None => continue,
150 };
151
152 let client_id = self.get_client_id_value().await;
153 let (stream, _): (Stream, _) =
154 Self::get_json(path, None, None::<&()>, &client_id).await?;
155 if stream.url.is_some() {
156 return Ok(t.clone());
157 }
158 }
159 None
160 };
161 Ok(transcoding.expect("No available download options"))
162 }
163
164 async fn download_progressive(
165 &self,
166 stream_url: &str,
167 output_path: &Path,
168 ) -> Result<(), Error> {
169 let response = reqwest::get(stream_url).await?;
170 let bytes = response.bytes().await?;
171 tokio::fs::write(output_path, &bytes).await?;
172 Ok(())
173 }
174
175 async fn download_hls(
176 &self,
177 stream_url: &str,
178 output_path: &Path,
179 ) -> Result<(), Error> {
180 download::auto_download().map_err(|e| Error::new(format!("FFmpeg download failed: {}", e)))?;
181 let status = FfmpegCommand::new()
182 .input(stream_url)
183 .output(
184 output_path
185 .to_str()
186 .expect("Failed to convert output path to string"),
187 )
188 .args(["-c", "copy"])
189 .spawn()
190 .map_err(|e| Error::new(format!("FFmpeg spawn failed: {}", e)))?
191 .wait()
192 .map_err(|e| Error::new(format!("FFmpeg wait failed: {}", e)))?;
193
194 if !status.success() {
195 return Err(Error::new("Download HLS Failed"));
196 }
197 Ok(())
198 }
199}