spotify_dl/
track.rs

1use std::pin::Pin;
2use std::sync::Arc;
3
4use anyhow::Result;
5use bytes::Bytes;
6use lazy_static::lazy_static;
7use librespot::core::session::Session;
8use librespot::core::spotify_id::SpotifyId;
9use librespot::metadata::Metadata;
10use librespot::metadata::image::Image;
11use regex::Regex;
12
13use crate::encoder::tags::Tags;
14use crate::utils::clean_invalid_characters;
15
16pub type AsyncFn<T> =
17    Arc<dyn Fn() -> Pin<Box<dyn Future<Output = Option<T>> + Send>> + Send + Sync>;
18
19#[async_trait::async_trait]
20trait TrackCollection {
21    async fn get_tracks(&self, session: &Session) -> Vec<Track>;
22}
23
24#[tracing::instrument(name = "get_tracks", skip(session), level = "debug")]
25pub async fn get_tracks(spotify_ids: Vec<String>, session: &Session) -> Result<Vec<Track>> {
26    let mut tracks: Vec<Track> = Vec::new();
27    for id in spotify_ids {
28        tracing::debug!("Getting tracks for: {}", id);
29        let id = parse_uri_or_url(&id).ok_or(anyhow::anyhow!("Invalid track"))?;
30        let new_tracks = match id.item_type {
31            librespot::core::spotify_id::SpotifyItemType::Track => vec![Track::from_id(id)],
32            librespot::core::spotify_id::SpotifyItemType::Episode => vec![Track::from_id(id)],
33            librespot::core::spotify_id::SpotifyItemType::Album => {
34                Album::from_id(id).get_tracks(session).await
35            }
36            librespot::core::spotify_id::SpotifyItemType::Playlist => {
37                Playlist::from_id(id).get_tracks(session).await
38            }
39            _ => {
40                tracing::warn!("Unsupported item type: {:?}", id.item_type);
41                vec![]
42            }
43        };
44        tracks.extend(new_tracks);
45    }
46    tracing::debug!("Got tracks: {:?}", tracks);
47    Ok(tracks)
48}
49
50fn parse_uri_or_url(track: &str) -> Option<SpotifyId> {
51    parse_uri(track).or_else(|| parse_url(track))
52}
53
54fn parse_uri(track_uri: &str) -> Option<SpotifyId> {
55    let res = SpotifyId::from_uri(track_uri);
56    tracing::info!("Parsed URI: {:?}", res);
57    res.ok()
58}
59
60fn parse_url(track_url: &str) -> Option<SpotifyId> {
61    let results = SPOTIFY_URL_REGEX.captures(track_url)?;
62    let uri = format!(
63        "spotify:{}:{}",
64        results.get(1)?.as_str(),
65        results.get(2)?.as_str()
66    );
67    SpotifyId::from_uri(&uri).ok()
68}
69
70#[derive(Clone, Debug)]
71pub struct Track {
72    pub id: SpotifyId,
73}
74
75lazy_static! {
76    static ref SPOTIFY_URL_REGEX: Regex =
77        Regex::new(r"https://open\.spotify\.com(?:/intl-[a-z]{2})?/(\w+)/([a-zA-Z0-9]+)").unwrap();
78}
79
80impl Track {
81    pub fn new(track: &str) -> Result<Self> {
82        let id = parse_uri_or_url(track).ok_or(anyhow::anyhow!("Invalid track"))?;
83        Ok(Track { id })
84    }
85
86    pub fn from_id(id: SpotifyId) -> Self {
87        Track { id }
88    }
89
90    pub async fn metadata(&self, session: &Session) -> Result<TrackMetadata> {
91        let metadata = librespot::metadata::Track::get(session, &self.id)
92            .await
93            .map_err(|_| anyhow::anyhow!("Failed to get metadata"))?;
94
95        let mut artists = Vec::new();
96        for artist in metadata.artists.iter() {
97            artists.push(
98                librespot::metadata::Artist::get(session, &artist.id)
99                    .await
100                    .map_err(|_| anyhow::anyhow!("Failed to get artist"))?,
101            );
102        }
103
104        let album = librespot::metadata::Album::get(session, &metadata.album.id)
105            .await
106            .map_err(|_| anyhow::anyhow!("Failed to get album"))?;
107
108        let covers = album.covers.clone();
109        let session = session.clone();
110
111        let image_retriever: AsyncFn<Bytes> = Arc::new(move || {
112            let covers = covers.clone();
113            let session = session.clone();
114
115            Box::pin(async move {
116                let cover = covers.first()?;
117                session.spclient().get_image(&cover.id).await.ok()
118            })
119        });
120
121        Ok(TrackMetadata::from(
122            metadata,
123            artists,
124            album,
125            image_retriever,
126        ))
127    }
128}
129
130#[async_trait::async_trait]
131impl TrackCollection for Track {
132    async fn get_tracks(&self, _session: &Session) -> Vec<Track> {
133        vec![self.clone()]
134    }
135}
136
137pub struct Album {
138    id: SpotifyId,
139}
140
141impl Album {
142    pub fn new(album: &str) -> Result<Self> {
143        let id = parse_uri_or_url(album).ok_or(anyhow::anyhow!("Invalid album"))?;
144        Ok(Album { id })
145    }
146
147    pub fn from_id(id: SpotifyId) -> Self {
148        Album { id }
149    }
150
151    pub async fn is_album(id: SpotifyId, session: &Session) -> bool {
152        librespot::metadata::Album::get(session, &id).await.is_ok()
153    }
154}
155
156#[async_trait::async_trait]
157impl TrackCollection for Album {
158    async fn get_tracks(&self, session: &Session) -> Vec<Track> {
159        let album = librespot::metadata::Album::get(session, &self.id)
160            .await
161            .expect("Failed to get album");
162        album.tracks().map(|track| Track::from_id(*track)).collect()
163    }
164}
165
166pub struct Playlist {
167    id: SpotifyId,
168}
169
170impl Playlist {
171    pub fn new(playlist: &str) -> Result<Self> {
172        let id = parse_uri_or_url(playlist).ok_or(anyhow::anyhow!("Invalid playlist"))?;
173        Ok(Playlist { id })
174    }
175
176    pub fn from_id(id: SpotifyId) -> Self {
177        Playlist { id }
178    }
179
180    pub async fn is_playlist(id: SpotifyId, session: &Session) -> bool {
181        librespot::metadata::Playlist::get(session, &id)
182            .await
183            .is_ok()
184    }
185}
186
187#[async_trait::async_trait]
188impl TrackCollection for Playlist {
189    async fn get_tracks(&self, session: &Session) -> Vec<Track> {
190        let playlist = librespot::metadata::Playlist::get(session, &self.id)
191            .await
192            .expect("Failed to get playlist");
193        playlist
194            .tracks()
195            .map(|track| Track::from_id(*track))
196            .collect()
197    }
198}
199
200#[derive(Clone)]
201pub struct TrackMetadata {
202    pub artists: Vec<ArtistMetadata>,
203    pub track_name: String,
204    pub album: AlbumMetadata,
205    pub duration: i32,
206    image_retriever: AsyncFn<Bytes>,
207}
208
209impl TrackMetadata {
210    pub fn from(
211        track: librespot::metadata::Track,
212        artists: Vec<librespot::metadata::Artist>,
213        album: librespot::metadata::Album,
214        image_retriever: AsyncFn<Bytes>,
215    ) -> Self {
216        let artists = artists
217            .iter()
218            .map(|artist| ArtistMetadata::from(artist.clone()))
219            .collect();
220        let album = AlbumMetadata::from(album);
221
222        TrackMetadata {
223            artists,
224            track_name: track.name.clone(),
225            album,
226            duration: track.duration,
227            image_retriever,
228        }
229    }
230
231    pub fn approx_size(&self) -> usize {
232        let duration = self.duration / 1000;
233        let sample_rate = 44100;
234        let channels = 2;
235        let bits_per_sample = 32;
236        let bytes_per_sample = bits_per_sample / 8;
237        (duration as usize) * sample_rate * channels * bytes_per_sample
238    }
239
240    pub async fn tags(&self) -> Result<Tags> {
241        let tags = Tags {
242            title: self.track_name.clone(),
243            artists: self.artists.iter().map(|a| a.name.clone()).collect(),
244            album_title: self.album.name.clone(),
245            album_cover: (self.image_retriever)().await,
246        };
247        Ok(tags)
248    }
249}
250
251impl ToString for TrackMetadata {
252    fn to_string(&self) -> String {
253        if self.artists.len() > 3 {
254            let artists_name = self
255                .artists
256                .iter()
257                .take(3)
258                .map(|artist| artist.name.clone())
259                .collect::<Vec<String>>()
260                .join(", ");
261            return clean_invalid_characters(format!(
262                "{}, ... - {}",
263                artists_name, self.track_name
264            ));
265        }
266
267        let artists_name = self
268            .artists
269            .iter()
270            .map(|artist| artist.name.clone())
271            .collect::<Vec<String>>()
272            .join(", ");
273        clean_invalid_characters(format!("{} - {}", artists_name, self.track_name))
274    }
275}
276
277#[derive(Clone, Debug)]
278pub struct ArtistMetadata {
279    pub name: String,
280}
281
282impl From<librespot::metadata::Artist> for ArtistMetadata {
283    fn from(artist: librespot::metadata::Artist) -> Self {
284        ArtistMetadata {
285            name: artist.name.clone(),
286        }
287    }
288}
289
290#[derive(Clone, Debug)]
291pub struct AlbumMetadata {
292    pub name: String,
293    pub cover: Option<Image>,
294}
295
296impl From<librespot::metadata::Album> for AlbumMetadata {
297    fn from(album: librespot::metadata::Album) -> Self {
298        AlbumMetadata {
299            name: album.name.clone(),
300            cover: album.covers.first().cloned(),
301        }
302    }
303}