From dbf1db040bf4a1e63643b5402207e708daa16927 Mon Sep 17 00:00:00 2001 From: Erik Kaju Date: Thu, 19 Jun 2025 23:52:37 +0300 Subject: [PATCH 1/3] Bump mp3 encoding to 320kbps, attach album cover and artist/album/title metadata to the file, handle file collision gracefully (useful for syncing playlists - download only missing files) --- Cargo.toml | 3 +++ src/channel_sink.rs | 4 ++-- src/download.rs | 20 +++++++++++--------- src/encoder/flac.rs | 14 +++++++------- src/encoder/mod.rs | 4 ++-- src/encoder/mp3.rs | 41 ++++++++++++++++++++++++++++++++++++++--- src/track.rs | 23 ++++++++++++++++++++++- 7 files changed, 85 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 394a95a..6ccd17c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ dirs = "5.0" mp3lame-encoder = { version = "0.1.5", optional = true } futures = "0.3" rayon = "1.10" +hex = "0.4" +reqwest = { version = "0.11", features = ["blocking", "json"] } +id3 = "0.6" [features] default = ["mp3"] diff --git a/src/channel_sink.rs b/src/channel_sink.rs index ec767df..4d1e11f 100644 --- a/src/channel_sink.rs +++ b/src/channel_sink.rs @@ -19,14 +19,14 @@ pub struct ChannelSink { impl ChannelSink { - pub fn new(track: TrackMetadata) -> (Self, SinkEventChannel) { + pub fn new(track: &TrackMetadata) -> (Self, SinkEventChannel) { let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); ( ChannelSink { sender: tx, bytes_sent: 0, - bytes_total: Self::convert_track_duration_to_size(&track), + bytes_total: Self::convert_track_duration_to_size(track), }, rx, ) diff --git a/src/download.rs b/src/download.rs index eea27f5..453e45a 100644 --- a/src/download.rs +++ b/src/download.rs @@ -77,7 +77,7 @@ impl Downloader { #[tracing::instrument(name = "download_track", skip(self))] async fn download_track(&self, track: Track, options: &DownloadOptions) -> Result<()> { let metadata = track.metadata(&self.session).await?; - tracing::info!("Downloading track: {:?}", metadata); + tracing::info!("Downloading track: {:?}", metadata.track_name); let file_name = self.get_file_name(&metadata); let path = options @@ -88,7 +88,12 @@ impl Downloader { .ok_or(anyhow::anyhow!("Could not set the output path"))? .to_string(); - let (sink, mut sink_channel) = ChannelSink::new(metadata); + if std::path::Path::new(&path).exists() { + println!("File already exists, skipping: {}", path); + return Ok(()); + } + + let (sink, mut sink_channel) = ChannelSink::new(&metadata); let file_size = sink.get_approximate_size(); @@ -129,15 +134,12 @@ impl Downloader { } } - tracing::info!("Encoding track: {:?}", file_name); - pb.set_message(format!("Encoding {}", &file_name)); + tracing::info!("Encoding and writing track: {:?}", file_name); + pb.set_message(format!("Encoding and writing {}", &file_name)); let samples = Samples::new(samples, 44100, 2, 16); let encoder = crate::encoder::get_encoder(options.format); - let stream = encoder.encode(samples).await?; - - pb.set_message(format!("Writing {}", &file_name)); - tracing::info!("Writing track: {:?} to file: {}", file_name, &path); - stream.write_to_file(&path).await?; + let output_path = &path; + encoder.encode(&samples, &metadata, output_path).await?; pb.finish_with_message(format!("Downloaded {}", &file_name)); Ok(()) diff --git a/src/encoder/flac.rs b/src/encoder/flac.rs index 50de691..cbd31b1 100644 --- a/src/encoder/flac.rs +++ b/src/encoder/flac.rs @@ -1,9 +1,6 @@ -use flacenc::bitsink::ByteSink; use flacenc::component::BitRepr; use flacenc::error::Verify; -use super::execute_with_result; -use super::EncodedStream; use super::Encoder; use super::Samples; @@ -12,7 +9,9 @@ pub struct FlacEncoder; #[async_trait::async_trait] impl Encoder for FlacEncoder { - async fn encode(&self, samples: Samples) -> anyhow::Result { + async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, output_path: &str) -> anyhow::Result<()> { + let file_name = &metadata.track_name; + tracing::info!("Writing track: {:?} to file: {}", file_name, output_path); let source = flacenc::source::MemSource::from_samples( &samples.samples, samples.channels as usize, @@ -26,7 +25,7 @@ impl Encoder for FlacEncoder { let (tx, rx) = tokio::sync::oneshot::channel(); - rayon::spawn(execute_with_result( + rayon::spawn(super::execute_with_result( move || { let flac_stream = flacenc::encode_with_fixed_block_size( &config, @@ -35,7 +34,7 @@ impl Encoder for FlacEncoder { ) .map_err(|e| anyhow::anyhow!("Failed to encode flac: {:?}", e))?; - let mut byte_sink = ByteSink::new(); + let mut byte_sink = flacenc::bitsink::ByteSink::new(); flac_stream .write(&mut byte_sink) .map_err(|e| anyhow::anyhow!("Failed to write flac stream: {:?}", e))?; @@ -47,6 +46,7 @@ impl Encoder for FlacEncoder { let byte_sink: Vec = rx.await??; - Ok(EncodedStream::new(byte_sink)) + let stream = super::EncodedStream::new(byte_sink); + stream.write_to_file(output_path).await } } diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 9e44278..b692de4 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -54,8 +54,8 @@ pub fn get_encoder(format: Format) -> &'static dyn Encoder { } #[async_trait::async_trait] -pub trait Encoder { - async fn encode(&self, samples: Samples) -> Result; +pub trait Encoder: Sync { + async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, output_path: &str) -> Result<()>; } pub struct Samples { diff --git a/src/encoder/mp3.rs b/src/encoder/mp3.rs index 87f0098..6ba62e7 100644 --- a/src/encoder/mp3.rs +++ b/src/encoder/mp3.rs @@ -3,6 +3,7 @@ use anyhow::Ok; use mp3lame_encoder::Builder; use mp3lame_encoder::FlushNoGap; use mp3lame_encoder::InterleavedPcm; +use id3::{Version, frame::{Picture, PictureType, Frame, Content}}; use super::execute_with_result; use super::EncodedStream; @@ -11,6 +12,8 @@ use super::Samples; pub struct Mp3Encoder; +unsafe impl Sync for Mp3Encoder {} + impl Mp3Encoder { fn build_encoder( &self, @@ -26,7 +29,7 @@ impl Mp3Encoder { anyhow::anyhow!("Failed to set number of channels for mp3 encoder: {}", e) })?; builder - .set_brate(mp3lame_encoder::Birtate::Kbps160) + .set_brate(mp3lame_encoder::Birtate::Kbps320) .map_err(|e| anyhow::anyhow!("Failed to set bitrate for mp3 encoder: {}", e))?; builder @@ -37,14 +40,46 @@ impl Mp3Encoder { #[async_trait::async_trait] impl Encoder for Mp3Encoder { - async fn encode(&self, samples: Samples) -> anyhow::Result { + async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, output_path: &str) -> anyhow::Result<()> { + let file_name = &metadata.track_name; + tracing::info!("Writing track: {:?} to file: {}", file_name, output_path); + let stream = self.encode_raw(samples).await?; + stream.write_to_file(output_path).await?; + + // Embed tags using id3 crate + let mut tag = id3::Tag::read_from_path(output_path).unwrap_or_else(|_| id3::Tag::new()); + tag.set_title(file_name); + tag.set_album(&metadata.album.name); + let artists = metadata.artists.iter().map(|a| a.name.as_str()).collect::>().join("\0"); + tag.set_artist(&artists); + + // Embed cover image + if let Some(image_bytes) = metadata.cover_image.as_deref() { + let picture = Picture { + mime_type: "image/jpeg".to_string(), + picture_type: PictureType::CoverFront, + description: "cover".to_string(), + data: image_bytes.to_vec(), + }; + let frame = Frame::with_content("APIC", Content::Picture(picture)); + tag.add_frame(frame); + } + tag.write_to_path(output_path, Version::Id3v24)?; + Ok(()) + } +} + +impl Mp3Encoder { + async fn encode_raw(&self, samples: &Samples) -> anyhow::Result { let mut mp3_encoder = self.build_encoder(samples.sample_rate, samples.channels)?; let (tx, rx) = tokio::sync::oneshot::channel(); + let samples_vec = samples.samples.clone(); + rayon::spawn(execute_with_result( move || { - let samples: Vec = samples.samples.iter().map(|&x| x as i16).collect(); + let samples: Vec = samples_vec.iter().map(|&x| x as i16).collect(); let input = InterleavedPcm(samples.as_slice()); let mut mp3_out_buffer = Vec::with_capacity(mp3lame_encoder::max_required_buffer_size(samples.len())); let encoded_size = mp3_encoder diff --git a/src/track.rs b/src/track.rs index c96dac9..047a693 100644 --- a/src/track.rs +++ b/src/track.rs @@ -93,7 +93,24 @@ impl Track { .await .map_err(|_| anyhow::anyhow!("Failed to get album"))?; - Ok(TrackMetadata::from(metadata, artists, album)) + // Fetch cover image bytes if available + let cover_image = if let Some(cover) = album.covers.first() { + let file_id_hex = hex::encode(cover.0); + let url = format!("https://i.scdn.co/image/{}", file_id_hex); + match reqwest::get(&url).await { + Ok(response) if response.status().is_success() => { + match response.bytes().await { + Ok(bytes) => Some(bytes.to_vec()), + Err(_) => None, + } + } + _ => None, + } + } else { + None + }; + + Ok(TrackMetadata::from(metadata, artists, album, cover_image)) } } @@ -178,6 +195,7 @@ pub struct TrackMetadata { pub track_name: String, pub album: AlbumMetadata, pub duration: i32, + pub cover_image: Option>, } impl TrackMetadata { @@ -185,11 +203,13 @@ impl TrackMetadata { track: librespot::metadata::Track, artists: Vec, album: librespot::metadata::Album, + cover_image: Option>, ) -> Self { let artists = artists .iter() .map(|artist| ArtistMetadata::from(artist.clone())) .collect(); + let album = AlbumMetadata::from(album); TrackMetadata { @@ -197,6 +217,7 @@ impl TrackMetadata { track_name: track.name.clone(), album, duration: track.duration, + cover_image, } } } From 54cb7f643376861c56ab06ad0c07d08beba5ec45 Mon Sep 17 00:00:00 2001 From: Erik Kaju Date: Fri, 20 Jun 2025 00:51:34 +0300 Subject: [PATCH 2/3] prompt tracks url interactively if not provided via command line argument --- src/main.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 389b35a..fc21e97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use structopt::StructOpt; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{fmt, EnvFilter}; +use std::io::{self, Write}; #[derive(Debug, StructOpt)] #[structopt( @@ -14,8 +15,7 @@ use tracing_subscriber::{fmt, EnvFilter}; )] struct Opt { #[structopt( - help = "A list of Spotify URIs or URLs (songs, podcasts, playlists or albums)", - required = true + help = "A list of Spotify URIs or URLs (songs, podcasts, playlists or albums)" )] tracks: Vec, #[structopt(short = "u", long = "username", help = "Your Spotify username")] @@ -72,12 +72,20 @@ pub fn create_destination_if_required(destination: Option) -> anyhow::Re async fn main() -> anyhow::Result<()> { configure_logger(); - let opt = Opt::from_args(); + let mut opt = Opt::from_args(); create_destination_if_required(opt.destination.clone())?; if opt.tracks.is_empty() { - eprintln!("No tracks provided"); - std::process::exit(1); + print!("Enter a Spotify URL or URI: "); + io::stdout().flush().unwrap(); + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + let input = input.trim(); + if input.is_empty() { + eprintln!("No tracks provided"); + std::process::exit(1); + } + opt.tracks.push(input.to_string()); } if opt.compression.is_some() { From 041a1cf54e333d19715520a0d5c5fdbe21043af6 Mon Sep 17 00:00:00 2001 From: Erik Kaju Date: Fri, 20 Jun 2025 16:46:00 +0300 Subject: [PATCH 3/3] bump up librespot version to 0.6.0, add support for year metadata, refactor cover image fetching, make username optional and take latest username automatically from credentials cache --- Cargo.toml | 4 ++- src/download.rs | 21 ++++++++++++-- src/encoder/flac.rs | 8 ++++- src/encoder/mod.rs | 4 +-- src/encoder/mp3.rs | 17 +++++++---- src/main.rs | 14 ++++++--- src/session.rs | 3 +- src/track.rs | 71 ++++++++++++++++----------------------------- 8 files changed, 79 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6ccd17c..f35162a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,11 @@ repository = "https://github.com/GuillemCastro/spotify-dl" description = "A command-line utility to download songs and playlists from Spotify" [dependencies] +bytes = "1" structopt = { version = "0.3", default-features = false } rpassword = "7.0" indicatif = "0.17" -librespot = { version = "0.4.2", default-features = false } +librespot = { version = "0.6.0", default-features = false } tokio = { version = "1", features = ["full", "tracing"] } flacenc = { version = "0.4" } audiotags = "0.5" @@ -31,6 +32,7 @@ rayon = "1.10" hex = "0.4" reqwest = { version = "0.11", features = ["blocking", "json"] } id3 = "0.6" +serde_json = "1.0.117" [features] default = ["mp3"] diff --git a/src/download.rs b/src/download.rs index 453e45a..f90df7a 100644 --- a/src/download.rs +++ b/src/download.rs @@ -2,6 +2,7 @@ use std::fmt::Write; use std::path::PathBuf; use std::time::Duration; +use bytes::Bytes; use anyhow::Result; use futures::StreamExt; use futures::TryStreamExt; @@ -97,7 +98,7 @@ impl Downloader { let file_size = sink.get_approximate_size(); - let (mut player, _) = Player::new( + let player = Player::new( self.player_config.clone(), self.session.clone(), self.volume_getter(), @@ -134,12 +135,16 @@ impl Downloader { } } + tracing::info!("Fetching album cover image: {:?}", file_name); + let cover_image = self.get_cover_image(&metadata).await?; + tracing::info!("Encoding and writing track: {:?}", file_name); pb.set_message(format!("Encoding and writing {}", &file_name)); let samples = Samples::new(samples, 44100, 2, 16); let encoder = crate::encoder::get_encoder(options.format); let output_path = &path; - encoder.encode(&samples, &metadata, output_path).await?; + + encoder.encode(&samples, &metadata, cover_image, output_path).await?; pb.finish_with_message(format!("Downloaded {}", &file_name)); Ok(()) @@ -188,4 +193,16 @@ impl Downloader { } clean } + + async fn get_cover_image(&self, metadata: &TrackMetadata) -> Result{ + match metadata.album.cover { + Some(ref cover) => { + self.session.spclient() + .get_image(&cover.id) + .await + .map_err(|e| anyhow::anyhow!("{:?}", e)) + } + None => Err(anyhow::anyhow!("No cover art!")) + } + } } diff --git a/src/encoder/flac.rs b/src/encoder/flac.rs index cbd31b1..cc1a0e3 100644 --- a/src/encoder/flac.rs +++ b/src/encoder/flac.rs @@ -1,6 +1,7 @@ use flacenc::component::BitRepr; use flacenc::error::Verify; +use bytes::Bytes; use super::Encoder; use super::Samples; @@ -9,7 +10,12 @@ pub struct FlacEncoder; #[async_trait::async_trait] impl Encoder for FlacEncoder { - async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, output_path: &str) -> anyhow::Result<()> { + async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, cover_image_bytes: Bytes, output_path: &str) -> anyhow::Result<()> { + + if !cover_image_bytes.is_empty() { + tracing::info!("Cover image found but not implemented in flac encoder"); + } + let file_name = &metadata.track_name; tracing::info!("Writing track: {:?} to file: {}", file_name, output_path); let source = flacenc::source::MemSource::from_samples( diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index b692de4..b5248f9 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -2,8 +2,8 @@ mod flac; #[cfg(feature = "mp3")] mod mp3; +use bytes::Bytes; use std::{path::Path, str::FromStr}; - use anyhow::Result; use tokio::sync::oneshot::Sender; @@ -55,7 +55,7 @@ pub fn get_encoder(format: Format) -> &'static dyn Encoder { #[async_trait::async_trait] pub trait Encoder: Sync { - async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, output_path: &str) -> Result<()>; + async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, cover_image_bytes: Bytes, output_path: &str) -> Result<()>; } pub struct Samples { diff --git a/src/encoder/mp3.rs b/src/encoder/mp3.rs index 6ba62e7..53b9fab 100644 --- a/src/encoder/mp3.rs +++ b/src/encoder/mp3.rs @@ -4,6 +4,7 @@ use mp3lame_encoder::Builder; use mp3lame_encoder::FlushNoGap; use mp3lame_encoder::InterleavedPcm; use id3::{Version, frame::{Picture, PictureType, Frame, Content}}; +use bytes::Bytes; use super::execute_with_result; use super::EncodedStream; @@ -40,7 +41,7 @@ impl Mp3Encoder { #[async_trait::async_trait] impl Encoder for Mp3Encoder { - async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, output_path: &str) -> anyhow::Result<()> { + async fn encode(&self, samples: &Samples, metadata: &crate::track::TrackMetadata, cover_image_bytes: Bytes, output_path: &str) -> anyhow::Result<()> { let file_name = &metadata.track_name; tracing::info!("Writing track: {:?} to file: {}", file_name, output_path); let stream = self.encode_raw(samples).await?; @@ -49,21 +50,25 @@ impl Encoder for Mp3Encoder { // Embed tags using id3 crate let mut tag = id3::Tag::read_from_path(output_path).unwrap_or_else(|_| id3::Tag::new()); tag.set_title(file_name); - tag.set_album(&metadata.album.name); + let artists = metadata.artists.iter().map(|a| a.name.as_str()).collect::>().join("\0"); tag.set_artist(&artists); + tag.set_album(&metadata.album.name); + + tag.add_frame(Frame::with_content("TDRC", Content::Text(metadata.album.year.to_string()))); + // Embed cover image - if let Some(image_bytes) = metadata.cover_image.as_deref() { + if !cover_image_bytes.is_empty() { let picture = Picture { mime_type: "image/jpeg".to_string(), picture_type: PictureType::CoverFront, description: "cover".to_string(), - data: image_bytes.to_vec(), + data: cover_image_bytes.to_vec(), }; - let frame = Frame::with_content("APIC", Content::Picture(picture)); - tag.add_frame(frame); + tag.add_frame(Frame::with_content("APIC", Content::Picture(picture))); } + tag.write_to_path(output_path, Version::Id3v24)?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index fc21e97..a059e72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ struct Opt { )] tracks: Vec, #[structopt(short = "u", long = "username", help = "Your Spotify username")] - username: String, + username: Option, #[structopt(short = "p", long = "password", help = "Your Spotify password")] password: Option, #[structopt( @@ -45,8 +45,8 @@ struct Opt { #[structopt( short = "f", long = "format", - help = "The format to download the tracks in. Default is flac.", - default_value = "flac" + help = "The format to download the tracks in. Default is mp3.", + default_value = "mp3" )] format: Format } @@ -92,8 +92,14 @@ async fn main() -> anyhow::Result<()> { eprintln!("Compression level is not supported yet. It will be ignored."); } - let session = create_session(opt.username, opt.password).await?; + let user_name = opt.username.or_else(|| { + println!("No username provided via arguments. Attempting to fetch from latest credentials cache."); + std::fs::read_to_string("credentials.json").ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + .and_then(|v| v.get("username")?.as_str().map(|s| s.to_string())) + }); + let session = create_session(user_name.unwrap(), opt.password).await?; let track = get_tracks(opt.tracks, &session).await?; let downloader = Downloader::new(session); diff --git a/src/session.rs b/src/session.rs index b8ca0d3..2f9feba 100644 --- a/src/session.rs +++ b/src/session.rs @@ -13,7 +13,8 @@ pub async fn create_session(username: String, password: Option) -> Resul cache.save_credentials(&credentials); - let (session, _) = Session::connect(session_config, credentials, Some(cache), false).await?; + let session = Session::new(session_config, Some(cache)); + session.connect(credentials, false).await?; Ok(session) } diff --git a/src/track.rs b/src/track.rs index 047a693..29ae142 100644 --- a/src/track.rs +++ b/src/track.rs @@ -15,19 +15,16 @@ pub async fn get_tracks(spotify_ids: Vec, session: &Session) -> Result = Vec::new(); for id in spotify_ids { tracing::debug!("Getting tracks for: {}", id); - let id = parse_uri_or_url(&id).ok_or(anyhow::anyhow!("Invalid track"))?; - let new_tracks = match id.audio_type { - librespot::core::spotify_id::SpotifyAudioType::Track => vec![Track::from_id(id)], - librespot::core::spotify_id::SpotifyAudioType::Podcast => vec![Track::from_id(id)], - librespot::core::spotify_id::SpotifyAudioType::NonPlayable => { - if Album::is_album(id, session).await { - Album::from_id(id).get_tracks(session).await - } else if Playlist::is_playlist(id, session).await { - Playlist::from_id(id).get_tracks(session).await - } else { - vec![] - } - } + let id = parse_uri_or_url(&id).ok_or(anyhow::anyhow!("Invalid track `{id}`"))?; + let new_tracks = match id.item_type { + librespot::core::spotify_id::SpotifyItemType::Track => vec![Track::from_id(id)], + librespot::core::spotify_id::SpotifyItemType::Episode => vec![Track::from_id(id)], + librespot::core::spotify_id::SpotifyItemType::Album => Album::from_id(id).get_tracks(session).await, + librespot::core::spotify_id::SpotifyItemType::Playlist => Playlist::from_id(id).get_tracks(session).await, + librespot::core::spotify_id::SpotifyItemType::Show => vec![], + librespot::core::spotify_id::SpotifyItemType::Artist => vec![], + librespot::core::spotify_id::SpotifyItemType::Local => vec![], + librespot::core::spotify_id::SpotifyItemType::Unknown => vec![], }; tracks.extend(new_tracks); } @@ -76,41 +73,24 @@ impl Track { } pub async fn metadata(&self, session: &Session) -> Result { - let metadata = librespot::metadata::Track::get(session, self.id) + let metadata = librespot::metadata::Track::get(session, &self.id) .await .map_err(|_| anyhow::anyhow!("Failed to get metadata"))?; let mut artists = Vec::new(); - for artist in &metadata.artists { + for artist in metadata.artists.iter() { artists.push( - librespot::metadata::Artist::get(session, *artist) + librespot::metadata::Artist::get(session, &artist.id) .await .map_err(|_| anyhow::anyhow!("Failed to get artist"))?, ); } - let album = librespot::metadata::Album::get(session, metadata.album) + let album = librespot::metadata::Album::get(session, &metadata.album.id) .await .map_err(|_| anyhow::anyhow!("Failed to get album"))?; - // Fetch cover image bytes if available - let cover_image = if let Some(cover) = album.covers.first() { - let file_id_hex = hex::encode(cover.0); - let url = format!("https://i.scdn.co/image/{}", file_id_hex); - match reqwest::get(&url).await { - Ok(response) if response.status().is_success() => { - match response.bytes().await { - Ok(bytes) => Some(bytes.to_vec()), - Err(_) => None, - } - } - _ => None, - } - } else { - None - }; - - Ok(TrackMetadata::from(metadata, artists, album, cover_image)) + Ok(TrackMetadata::from(metadata, artists, album)) } } @@ -136,19 +116,18 @@ impl Album { } pub async fn is_album(id: SpotifyId, session: &Session) -> bool { - librespot::metadata::Album::get(session, id).await.is_ok() + librespot::metadata::Album::get(session, &id).await.is_ok() } } #[async_trait::async_trait] impl TrackCollection for Album { async fn get_tracks(&self, session: &Session) -> Vec { - let album = librespot::metadata::Album::get(session, self.id) + let album = librespot::metadata::Album::get(session, &self.id) .await .expect("Failed to get album"); album - .tracks - .iter() + .tracks() .map(|track| Track::from_id(*track)) .collect() } @@ -169,7 +148,7 @@ impl Playlist { } pub async fn is_playlist(id: SpotifyId, session: &Session) -> bool { - librespot::metadata::Playlist::get(session, id) + librespot::metadata::Playlist::get(session, &id) .await .is_ok() } @@ -178,12 +157,11 @@ impl Playlist { #[async_trait::async_trait] impl TrackCollection for Playlist { async fn get_tracks(&self, session: &Session) -> Vec { - let playlist = librespot::metadata::Playlist::get(session, self.id) + let playlist = librespot::metadata::Playlist::get(session, &self.id) .await .expect("Failed to get playlist"); playlist - .tracks - .iter() + .tracks() .map(|track| Track::from_id(*track)) .collect() } @@ -195,7 +173,6 @@ pub struct TrackMetadata { pub track_name: String, pub album: AlbumMetadata, pub duration: i32, - pub cover_image: Option>, } impl TrackMetadata { @@ -203,7 +180,6 @@ impl TrackMetadata { track: librespot::metadata::Track, artists: Vec, album: librespot::metadata::Album, - cover_image: Option>, ) -> Self { let artists = artists .iter() @@ -217,7 +193,6 @@ impl TrackMetadata { track_name: track.name.clone(), album, duration: track.duration, - cover_image, } } } @@ -238,12 +213,16 @@ impl From for ArtistMetadata { #[derive(Clone, Debug)] pub struct AlbumMetadata { pub name: String, + pub year: i32, + pub cover: Option, } impl From for AlbumMetadata { fn from(album: librespot::metadata::Album) -> Self { AlbumMetadata { name: album.name.clone(), + year: album.date.as_utc().year(), + cover: album.covers.first().cloned() } } }