From 495f53ef20cfdf74a310096e08ed339c63541f97 Mon Sep 17 00:00:00 2001 From: "alterwain@protonmail.com" Date: Wed, 12 Feb 2025 05:45:20 +0300 Subject: [PATCH] modified: Cargo.lock modified: Cargo.toml modified: src/config.rs modified: src/db.rs modified: src/main_screen.rs modified: src/sync.rs --- Cargo.lock | 80 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 ++ src/config.rs | 2 +- src/db.rs | 70 +++++++++++++++++++++++++++++++++++----- src/main_screen.rs | 26 ++++----------- src/sync.rs | 24 +++++++++----- 6 files changed, 169 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b341ff..439ed22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,21 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +[[package]] +name = "bitstream-io" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614aa3f2bac03707e62a84d18a48dd3d9ea6171313fd5e6a53b5054d8ae74601" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -281,6 +296,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.10" @@ -325,6 +350,16 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "6.0.0" @@ -558,6 +593,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1081,6 +1126,7 @@ dependencies = [ name = "lyrica" version = "0.1.0" dependencies = [ + "base64", "bincode", "chrono", "color-eyre", @@ -1090,6 +1136,8 @@ dependencies = [ "futures", "itunesdb", "libmath", + "md-5", + "puremp3", "ratatui", "redb", "regex", @@ -1104,6 +1152,16 @@ dependencies = [ "ureq", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1335,6 +1393,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "puremp3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b7efbb39e373af70c139e0611375fa6cad751fb93d528a610b55302710d883" +dependencies = [ + "bitstream-io", + "byteorder", +] + [[package]] name = "quote" version = "1.0.38" @@ -2205,6 +2273,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.16" @@ -2317,6 +2391,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index d5eecf9..6ae0a0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,6 @@ itunesdb = { version = "0.1.1", git = "https://gitea.awain.net/alterwain/ITunesD ureq = "3.0.5" color-thief = "0.2" redb = "2.4.0" +md-5 = "0.10.6" +base64 = "0.22.1" +puremp3 = "0.1.0" diff --git a/src/config.rs b/src/config.rs index 30f6cd6..5344cf6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,7 +17,7 @@ pub fn get_temp_dl_dir() -> PathBuf { pub fn get_config_path() -> PathBuf { let mut p = get_configs_dir(); p.push("config"); - p.set_extension(".toml"); + p.set_extension("toml"); p } diff --git a/src/db.rs b/src/db.rs index 5b8c046..4013485 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,13 +1,17 @@ +use std::fs::File; + +use md5::{Digest, Md5}; use redb::{Database, Error, ReadableTable, TableDefinition}; use serde::{Deserialize, Serialize}; +use soundcloud::sobjects::CloudTrack; -use crate::config::get_db; +use crate::config::{get_db, get_temp_dl_dir}; const TRACKS: TableDefinition> = TableDefinition::new("tracks"); #[derive(Serialize, Deserialize)] -struct Track { - unique_id: u32, +pub struct Track { + pub unique_id: u32, filetype: u32, stars: u8, last_modified_time: u32, @@ -17,23 +21,58 @@ struct Track { bitrate: u32, sample_rate: u32, play_count: u32, - dbid: u64, + pub dbid: u64, bpm: u16, skip_count: u32, has_artwork: u8, media_type: u32, - title: String, + pub title: String, location: String, album: String, artist: String, genre: String, } -fn init_db() -> Database { +impl From for Track { + fn from(value: CloudTrack) -> Self { + let mut track_path = get_temp_dl_dir(); + track_path.push(value.id.to_string()); + track_path.set_extension("mp3"); + let f = File::open(&track_path).unwrap(); + let data = &std::fs::read(&track_path).unwrap()[..]; + let (header, _samples) = puremp3::read_mp3(data).unwrap(); + Track { + unique_id: 0, + filetype: 0, + stars: 0, + last_modified_time: 0, + size: f.metadata().unwrap().len() as u32, + length: 0, + year: 0, + bitrate: header.bitrate.bps(), + sample_rate: header.sample_rate.hz(), + play_count: 0, + dbid: hash(data), + bpm: 0, + skip_count: 0, + has_artwork: 0, + media_type: 0, + title: value.title.unwrap(), + location: String::new(), + album: String::new(), + artist: "Soundcloud".to_string(), + genre: value.genre.unwrap_or_default(), + } + } +} + +// TODO: implement From (or Into) for Track, convert from Soundcloud Audio or iTunes + +pub fn init_db() -> Database { Database::create(get_db()).unwrap() } -fn insert_track(db: &Database, track: Track) -> Result<(), Error> { +pub fn insert_track(db: &Database, track: Track) -> Result<(), Error> { let write_txn = db.begin_write()?; { let mut table = write_txn.open_table(TRACKS)?; @@ -45,10 +84,25 @@ fn insert_track(db: &Database, track: Track) -> Result<(), Error> { Ok(()) } -fn get_track(db: &Database, id: u32) -> Result { +pub fn get_track(db: &Database, id: u32) -> Result { let read_txn = db.begin_read()?; let table = read_txn.open_table(TRACKS)?; let b = table.get(id)?.unwrap().value(); let track: Track = bincode::deserialize(&b).unwrap(); Ok(track) } + +pub fn get_last_track_id(db: &Database) -> Result { + let read_txn = db.begin_read()?; + let table = read_txn.open_table(TRACKS)?; + let l = table.last()?.map_or(80u32, |v| v.0.value()); + Ok(l) +} + +// note: this hash function is used to make unique ids for each track. It doesn't aim to generate secure ones. +fn hash(buf: &[u8]) -> u64 { + let mut hasher = Md5::new(); + hasher.update(buf); + let arr = hasher.finalize()[..8].try_into().unwrap(); + u64::from_be_bytes(arr) +} diff --git a/src/main_screen.rs b/src/main_screen.rs index bc3b001..d7116a8 100644 --- a/src/main_screen.rs +++ b/src/main_screen.rs @@ -8,26 +8,26 @@ use ratatui::{ widgets::{Block, Borders, Gauge, Paragraph, Row, Table, Tabs}, Frame, }; -use soundcloud::sobjects::CloudPlaylists; +use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists}; use strum::IntoEnumIterator; use tokio::sync::mpsc::UnboundedSender; use crate::{dlp::DownloadProgress, screen::AppScreen, sync::AppEvent}; -struct Playlist { +/*struct Playlist { id: u64, title: String, link: String, created_at: String, track_count: u32, -} +}*/ pub struct MainScreen { selected_tab: i8, selected_row: i32, max_rows: i32, tab_titles: Vec, - soundcloud: Option>, + soundcloud: Option>, pub progress: Option<(u32, u32)>, pub s_progress: Option, sender: UnboundedSender, @@ -160,31 +160,19 @@ impl MainScreen { fn download_row(&mut self) { if self.selected_tab == 1 { // SC - let playlist_url = self + let playlist = self .soundcloud .as_ref() .unwrap() .get(self.selected_row as usize) .unwrap() - .link .clone(); - let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist_url)); + let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist)); } } pub fn set_soundcloud_playlists(&mut self, pl: CloudPlaylists) { - self.soundcloud = Some( - pl.collection - .iter() - .map(|p| Playlist { - id: p.id, - created_at: p.created_at.clone(), - title: p.title.clone(), - link: p.permalink_url.clone(), - track_count: p.track_count, - }) - .collect(), - ); + self.soundcloud = Some(pl.collection); } fn render_progress(&self, frame: &mut Frame, area: Rect) { diff --git a/src/sync.rs b/src/sync.rs index 29e43d5..78d47df 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use itunesdb::xobjects::XDatabase; -use soundcloud::sobjects::CloudPlaylists; +use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists}; use tokio::{ fs::File, io::{AsyncReadExt, AsyncWriteExt}, @@ -11,9 +11,9 @@ use tokio_util::sync::CancellationToken; use crate::{ config::{ - get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, - LyricaConfiguration, + get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration, }, + db::{self, Track}, dlp::{self, DownloadProgress}, }; @@ -24,7 +24,7 @@ pub enum AppEvent { ParseItunes(String), ITunesParsed(XDatabase), SoundcloudGot(CloudPlaylists), - DownloadPlaylist(String), + DownloadPlaylist(CloudPlaylist), CurrentProgress(DownloadProgress), OverallProgress((u32, u32)), } @@ -37,6 +37,8 @@ pub fn initialize_async_service( tokio::spawn(async move { let _ = std::fs::create_dir_all(get_configs_dir()); + let database = db::init_db(); + let mut receiver = receiver; loop { @@ -85,11 +87,17 @@ pub fn initialize_async_service( let _ = sender.send(AppEvent::SoundcloudGot(playlists)).await; }, - AppEvent::DownloadPlaylist(playlist_url) => { - if let Ok(()) = dlp::download_from_soundcloud(&playlist_url, &get_temp_dl_dir(), sender.clone()).await { - // TODO: sync with db + AppEvent::DownloadPlaylist(playlist) => { + if let Ok(()) = dlp::download_from_soundcloud(&playlist.permalink_url, &get_temp_dl_dir(), sender.clone()).await { + let tracks = playlist.tracks; + for track in tracks { + if track.title.is_none() { continue; } + let mut t: Track = track.into(); + t.unique_id = db::get_last_track_id(&database).unwrap_or(80) + 1; + let _ = db::insert_track(&database, t); + } } - } + }, _ => {} } }