Skip to main content

soundcloud_rs/client/
tracks.rs

1use 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}