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, } } }