From a724560d580215a50264902c919cd56100e50bf5 Mon Sep 17 00:00:00 2001 From: "alterwain@protonmail.com" Date: Sat, 22 Feb 2025 04:14:26 +0300 Subject: [PATCH] artworks implemented for soundcloud --- Cargo.lock | 14 ++++++- Cargo.toml | 5 ++- src/config.rs | 5 +++ src/sync.rs | 106 ++++++++++++++++++++++++++++++++------------------ src/util.rs | 10 +++++ 5 files changed, 99 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eed6b25..1c46e90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1284,8 +1284,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "itunesdb" -version = "0.1.89" -source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#2a867778b7bbd82956247f2f17b98fdb6865a910" +version = "0.1.90" +source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#d37957a9a0b66577dadf3b305b02c72f1ca50a62" dependencies = [ "bincode", "env_logger", @@ -1441,6 +1441,7 @@ dependencies = [ "tokio-util", "toml", "tui-big-text", + "twox-hash", ] [[package]] @@ -2789,6 +2790,15 @@ dependencies = [ "ratatui", ] +[[package]] +name = "twox-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" +dependencies = [ + "rand", +] + [[package]] name = "unicode-ident" version = "1.0.16" diff --git a/Cargo.toml b/Cargo.toml index c850c24..4b3310a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,10 @@ futures = "0.3" tokio = { version = "1", features = ["full"] } tokio-util = { version = "0.7.12", features = ["codec"] } soundcloud = { version = "0.1.8", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } -itunesdb = { version = "0.1.89", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } +itunesdb = { version = "0.1.90", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } rand = "0.8.5" tui-big-text = "0.7.1" throbber-widgets-tui = "0.8.0" audiotags = "0.5.0" -image = "0.25.5" \ No newline at end of file +image = "0.25.5" +twox-hash = "2.1.0" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 5344cf6..ff2806f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,6 +14,11 @@ pub fn get_temp_dl_dir() -> PathBuf { p } +pub fn clear_temp_dl_dir() { + let path = get_temp_dl_dir(); + let _ = std::fs::remove_dir_all(path); +} + pub fn get_config_path() -> PathBuf { let mut p = get_configs_dir(); p.push("config"); diff --git a/src/sync.rs b/src/sync.rs index 94d7b16..52bdf12 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,7 +1,7 @@ -use audiotags::{Picture, Tag}; +use audiotags::Tag; use color_eyre::owo_colors::OwoColorize; use image::imageops::FilterType; -use image::ImageReader; +use image::{GenericImageView, ImageReader}; use itunesdb::artworkdb::aobjects::ADatabase; use itunesdb::objects::{ListSortOrder, PlaylistItem}; use itunesdb::serializer; @@ -51,15 +51,20 @@ pub struct DBPlaylist { pub tracks: Vec, } -async fn track_from_soundcloud(value: &CloudTrack) -> Option { +async fn track_from_soundcloud(value: &CloudTrack, ipod_path: String) -> Option { let mut track_path = get_temp_dl_dir(); track_path.push(value.id.to_string()); track_path.set_extension("mp3"); + let mut image_path = get_temp_dl_dir(); + image_path.push(value.id.to_string()); + image_path.set_extension("jpg"); let audio_file = audio_file_info::from_path(track_path.to_str().unwrap()) .await .unwrap(); let audio_info = &audio_file.audio_file.tracks.track; + let song_dbid = util::hash_from_path(track_path); + let mut track = XTrackItem::new( value.id as u32, audio_info.audio_bytes as u32, @@ -67,10 +72,34 @@ async fn track_from_soundcloud(value: &CloudTrack) -> Option { 0, (audio_info.bit_rate / 1000) as u32, audio_info.sample_rate as u32, - hash(), + song_dbid, 0, ); + if image_path.exists() { + let mut adb = get_artwork_db(&ipod_path); + + let image_data = std::fs::read(image_path).unwrap(); + + let (small_img_name, large_img_name) = adb.add_images(song_dbid, util::hash(&image_data)); + + let mut dst = PathBuf::from(&ipod_path); + dst.push("iPod_Control"); + dst.push("Artwork"); + + let size = image_data.len(); + + make_cover_image(&image_data, &ipod_path, &small_img_name, (100, 100)); + make_cover_image(&image_data, &ipod_path, &large_img_name, (200, 200)); + + write_artwork_db(adb, &ipod_path); + + track.data.artwork_size = size as u32; + track.data.mhii_link = 0; + track.data.has_artwork = 1; + track.data.artwork_count = 1; + } + audio_file.modify_xtrack(&mut track); track.set_title(value.title.clone().unwrap()); @@ -84,11 +113,6 @@ async fn track_from_soundcloud(value: &CloudTrack) -> Option { Some(track) } -// note: this hash function is used to make unique ids for each track. It doesn't aim to generate secure ones. -fn hash() -> u64 { - rand::random::() -} - fn get_track_location(unique_id: u32, extension: &str) -> String { let mut tp = PathBuf::new(); tp.push(":iPod_Control"); @@ -330,7 +354,7 @@ async fn load_from_fs( .unwrap(); let audio_info = &audio_file.audio_file.tracks.track; - let song_dbid = hash(); + let song_dbid = util::hash_from_path(path.clone()); let mut track = XTrackItem::new( id, @@ -362,18 +386,20 @@ async fn load_from_fs( if let Some(cover) = tag.album_cover() { let mut adb = get_artwork_db(&ipod_path); - let (small_img_name, large_img_name) = adb.add_images(song_dbid, id); + let (small_img_name, large_img_name) = adb.add_images(song_dbid, util::hash(cover.data)); let mut dst = PathBuf::from(&ipod_path); dst.push("iPod_Control"); dst.push("Artwork"); - make_small_image(cover.clone(), &ipod_path, &small_img_name); - make_large_image(cover, &ipod_path, &large_img_name); + let size = cover.data.len(); + + make_cover_image(cover.data, &ipod_path, &small_img_name, (100, 100)); + make_cover_image(cover.data, &ipod_path, &large_img_name, (200, 200)); write_artwork_db(adb, &ipod_path); - track.data.artwork_size = 1134428; + track.data.artwork_size = size as u32; track.data.mhii_link = 0; track.data.has_artwork = 1; track.data.artwork_count = 1; @@ -441,32 +467,34 @@ fn get_artwork_db(ipod_path: &str) -> ADatabase { itunesdb::artworkdb::deserializer::new_db() } -fn make_small_image(cover: Picture, ipod_path: &str, file_name: &str) { - let img: IPodImage = ImageReader::new(Cursor::new(cover.data)) +fn make_cover_image(cover: &[u8], ipod_path: &str, file_name: &str, dim: (u32, u32)) { + let mut dynamic_im = ImageReader::new(Cursor::new(cover)) .with_guessed_format() .unwrap() .decode() - .unwrap() - .resize_exact(100, 100, FilterType::Lanczos3) - .into(); + .unwrap(); - let mut dst = PathBuf::from(ipod_path); - dst.push("iPod_Control"); - dst.push("Artwork"); + if dynamic_im.height() != dynamic_im.width() { + let side = if dynamic_im.height() < dynamic_im.width() { + dynamic_im.height() + } else { + dynamic_im.width() + }; + let x = if dynamic_im.height() < dynamic_im.width() { + (dynamic_im.width() - side) / 2 + } else { + 0 + }; + let y = if dynamic_im.height() < dynamic_im.width() { + 0 + } else { + (dynamic_im.height() - side) / 2 + }; + dynamic_im = dynamic_im.crop(x, y, side, side); + } - let _ = std::fs::create_dir_all(dst.clone()); - - dst.push(file_name); - img.write(dst); -} - -fn make_large_image(cover: Picture, ipod_path: &str, file_name: &str) { - let img: IPodImage = ImageReader::new(Cursor::new(cover.data)) - .with_guessed_format() - .unwrap() - .decode() - .unwrap() - .resize_exact(200, 200, FilterType::Lanczos3) + let img: IPodImage = dynamic_im + .resize_exact(dim.0, dim.1, FilterType::Lanczos3) .into(); let mut dst = PathBuf::from(ipod_path); @@ -494,7 +522,7 @@ async fn download_track( { let p: PathBuf = Path::new(&ipod_path).into(); - if let Some(mut t) = track_from_soundcloud(&track).await { + if let Some(mut t) = track_from_soundcloud(&track, ipod_path.clone()).await { t.data.unique_id = database.get_unique_id(); t.set_location(get_track_location(t.data.unique_id, "mp3")); let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3"); @@ -518,6 +546,8 @@ async fn download_track( .await; overwrite_database(database, &ipod_path); + + crate::config::clear_temp_dl_dir(); } async fn download_playlist( @@ -542,7 +572,7 @@ async fn download_playlist( if track.title.is_none() { continue; } - if let Some(mut t) = track_from_soundcloud(&track).await { + if let Some(mut t) = track_from_soundcloud(&track, ipod_path.clone()).await { t.data.unique_id = database.get_unique_id(); new_playlist.add_elem(t.data.unique_id); t.set_location(get_track_location(t.data.unique_id, "mp3")); @@ -569,6 +599,8 @@ async fn download_playlist( .await; overwrite_database(database, &ipod_path); + + crate::config::clear_temp_dl_dir(); } fn get_playlists(db: &mut XDatabase) -> Vec { diff --git a/src/util.rs b/src/util.rs index 0e9241b..f1305e0 100644 --- a/src/util.rs +++ b/src/util.rs @@ -3,6 +3,7 @@ use regex::Regex; use std::io::Write; use std::path::PathBuf; use std::{error::Error, process::Command, str, str::FromStr}; +use twox_hash::XxHash3_64; const VENDOR_ID: u16 = 1452; const PRODUCT_ID: u16 = 4617; @@ -110,6 +111,15 @@ fn get_ipod_path() -> Option { } } +// note: this hash function is used to make unique ids for each track. It doesn't aim to generate secure ones. +pub fn hash(data: &[u8]) -> u64 { + XxHash3_64::oneshot(data) +} + +pub fn hash_from_path(path: PathBuf) -> u64 { + hash(&std::fs::read(path).unwrap()) +} + pub struct IPodImage { pixels: Vec, }