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}