modified: Cargo.lock
modified: Cargo.toml modified: src/config.rs modified: src/db.rs modified: src/main_screen.rs modified: src/sync.rs
This commit is contained in:
parent
980f1da3ac
commit
495f53ef20
80
Cargo.lock
generated
80
Cargo.lock
generated
@ -112,6 +112,21 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.17.0"
|
version = "3.17.0"
|
||||||
@ -281,6 +296,16 @@ dependencies = [
|
|||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.10"
|
version = "0.20.10"
|
||||||
@ -325,6 +350,16 @@ dependencies = [
|
|||||||
"powerfmt",
|
"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]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@ -558,6 +593,16 @@ dependencies = [
|
|||||||
"slab",
|
"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]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@ -1081,6 +1126,7 @@ dependencies = [
|
|||||||
name = "lyrica"
|
name = "lyrica"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
@ -1090,6 +1136,8 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"itunesdb",
|
"itunesdb",
|
||||||
"libmath",
|
"libmath",
|
||||||
|
"md-5",
|
||||||
|
"puremp3",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"redb",
|
"redb",
|
||||||
"regex",
|
"regex",
|
||||||
@ -1104,6 +1152,16 @@ dependencies = [
|
|||||||
"ureq",
|
"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]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.4"
|
version = "2.7.4"
|
||||||
@ -1335,6 +1393,16 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.38"
|
version = "1.0.38"
|
||||||
@ -2205,6 +2273,12 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.16"
|
version = "1.0.16"
|
||||||
@ -2317,6 +2391,12 @@ version = "0.2.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -27,3 +27,6 @@ itunesdb = { version = "0.1.1", git = "https://gitea.awain.net/alterwain/ITunesD
|
|||||||
ureq = "3.0.5"
|
ureq = "3.0.5"
|
||||||
color-thief = "0.2"
|
color-thief = "0.2"
|
||||||
redb = "2.4.0"
|
redb = "2.4.0"
|
||||||
|
md-5 = "0.10.6"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
puremp3 = "0.1.0"
|
||||||
|
@ -17,7 +17,7 @@ pub fn get_temp_dl_dir() -> PathBuf {
|
|||||||
pub fn get_config_path() -> PathBuf {
|
pub fn get_config_path() -> PathBuf {
|
||||||
let mut p = get_configs_dir();
|
let mut p = get_configs_dir();
|
||||||
p.push("config");
|
p.push("config");
|
||||||
p.set_extension(".toml");
|
p.set_extension("toml");
|
||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
70
src/db.rs
70
src/db.rs
@ -1,13 +1,17 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
use md5::{Digest, Md5};
|
||||||
use redb::{Database, Error, ReadableTable, TableDefinition};
|
use redb::{Database, Error, ReadableTable, TableDefinition};
|
||||||
use serde::{Deserialize, Serialize};
|
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<u32, Vec<u8>> = TableDefinition::new("tracks");
|
const TRACKS: TableDefinition<u32, Vec<u8>> = TableDefinition::new("tracks");
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct Track {
|
pub struct Track {
|
||||||
unique_id: u32,
|
pub unique_id: u32,
|
||||||
filetype: u32,
|
filetype: u32,
|
||||||
stars: u8,
|
stars: u8,
|
||||||
last_modified_time: u32,
|
last_modified_time: u32,
|
||||||
@ -17,23 +21,58 @@ struct Track {
|
|||||||
bitrate: u32,
|
bitrate: u32,
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
play_count: u32,
|
play_count: u32,
|
||||||
dbid: u64,
|
pub dbid: u64,
|
||||||
bpm: u16,
|
bpm: u16,
|
||||||
skip_count: u32,
|
skip_count: u32,
|
||||||
has_artwork: u8,
|
has_artwork: u8,
|
||||||
media_type: u32,
|
media_type: u32,
|
||||||
title: String,
|
pub title: String,
|
||||||
location: String,
|
location: String,
|
||||||
album: String,
|
album: String,
|
||||||
artist: String,
|
artist: String,
|
||||||
genre: String,
|
genre: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_db() -> Database {
|
impl From<CloudTrack> 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()
|
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 write_txn = db.begin_write()?;
|
||||||
{
|
{
|
||||||
let mut table = write_txn.open_table(TRACKS)?;
|
let mut table = write_txn.open_table(TRACKS)?;
|
||||||
@ -45,10 +84,25 @@ fn insert_track(db: &Database, track: Track) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_track(db: &Database, id: u32) -> Result<Track, Error> {
|
pub fn get_track(db: &Database, id: u32) -> Result<Track, Error> {
|
||||||
let read_txn = db.begin_read()?;
|
let read_txn = db.begin_read()?;
|
||||||
let table = read_txn.open_table(TRACKS)?;
|
let table = read_txn.open_table(TRACKS)?;
|
||||||
let b = table.get(id)?.unwrap().value();
|
let b = table.get(id)?.unwrap().value();
|
||||||
let track: Track = bincode::deserialize(&b).unwrap();
|
let track: Track = bincode::deserialize(&b).unwrap();
|
||||||
Ok(track)
|
Ok(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_last_track_id(db: &Database) -> Result<u32, Error> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
@ -8,26 +8,26 @@ use ratatui::{
|
|||||||
widgets::{Block, Borders, Gauge, Paragraph, Row, Table, Tabs},
|
widgets::{Block, Borders, Gauge, Paragraph, Row, Table, Tabs},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use soundcloud::sobjects::CloudPlaylists;
|
use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists};
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::{dlp::DownloadProgress, screen::AppScreen, sync::AppEvent};
|
use crate::{dlp::DownloadProgress, screen::AppScreen, sync::AppEvent};
|
||||||
|
|
||||||
struct Playlist {
|
/*struct Playlist {
|
||||||
id: u64,
|
id: u64,
|
||||||
title: String,
|
title: String,
|
||||||
link: String,
|
link: String,
|
||||||
created_at: String,
|
created_at: String,
|
||||||
track_count: u32,
|
track_count: u32,
|
||||||
}
|
}*/
|
||||||
|
|
||||||
pub struct MainScreen {
|
pub struct MainScreen {
|
||||||
selected_tab: i8,
|
selected_tab: i8,
|
||||||
selected_row: i32,
|
selected_row: i32,
|
||||||
max_rows: i32,
|
max_rows: i32,
|
||||||
tab_titles: Vec<String>,
|
tab_titles: Vec<String>,
|
||||||
soundcloud: Option<Vec<Playlist>>,
|
soundcloud: Option<Vec<CloudPlaylist>>,
|
||||||
pub progress: Option<(u32, u32)>,
|
pub progress: Option<(u32, u32)>,
|
||||||
pub s_progress: Option<DownloadProgress>,
|
pub s_progress: Option<DownloadProgress>,
|
||||||
sender: UnboundedSender<AppEvent>,
|
sender: UnboundedSender<AppEvent>,
|
||||||
@ -160,31 +160,19 @@ impl MainScreen {
|
|||||||
fn download_row(&mut self) {
|
fn download_row(&mut self) {
|
||||||
if self.selected_tab == 1 {
|
if self.selected_tab == 1 {
|
||||||
// SC
|
// SC
|
||||||
let playlist_url = self
|
let playlist = self
|
||||||
.soundcloud
|
.soundcloud
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get(self.selected_row as usize)
|
.get(self.selected_row as usize)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.link
|
|
||||||
.clone();
|
.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) {
|
pub fn set_soundcloud_playlists(&mut self, pl: CloudPlaylists) {
|
||||||
self.soundcloud = Some(
|
self.soundcloud = Some(pl.collection);
|
||||||
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(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_progress(&self, frame: &mut Frame, area: Rect) {
|
fn render_progress(&self, frame: &mut Frame, area: Rect) {
|
||||||
|
24
src/sync.rs
24
src/sync.rs
@ -1,7 +1,7 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use itunesdb::xobjects::XDatabase;
|
use itunesdb::xobjects::XDatabase;
|
||||||
use soundcloud::sobjects::CloudPlaylists;
|
use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
@ -11,9 +11,9 @@ use tokio_util::sync::CancellationToken;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{
|
config::{
|
||||||
get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb,
|
get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration,
|
||||||
LyricaConfiguration,
|
|
||||||
},
|
},
|
||||||
|
db::{self, Track},
|
||||||
dlp::{self, DownloadProgress},
|
dlp::{self, DownloadProgress},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ pub enum AppEvent {
|
|||||||
ParseItunes(String),
|
ParseItunes(String),
|
||||||
ITunesParsed(XDatabase),
|
ITunesParsed(XDatabase),
|
||||||
SoundcloudGot(CloudPlaylists),
|
SoundcloudGot(CloudPlaylists),
|
||||||
DownloadPlaylist(String),
|
DownloadPlaylist(CloudPlaylist),
|
||||||
CurrentProgress(DownloadProgress),
|
CurrentProgress(DownloadProgress),
|
||||||
OverallProgress((u32, u32)),
|
OverallProgress((u32, u32)),
|
||||||
}
|
}
|
||||||
@ -37,6 +37,8 @@ pub fn initialize_async_service(
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = std::fs::create_dir_all(get_configs_dir());
|
let _ = std::fs::create_dir_all(get_configs_dir());
|
||||||
|
|
||||||
|
let database = db::init_db();
|
||||||
|
|
||||||
let mut receiver = receiver;
|
let mut receiver = receiver;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@ -85,11 +87,17 @@ pub fn initialize_async_service(
|
|||||||
|
|
||||||
let _ = sender.send(AppEvent::SoundcloudGot(playlists)).await;
|
let _ = sender.send(AppEvent::SoundcloudGot(playlists)).await;
|
||||||
},
|
},
|
||||||
AppEvent::DownloadPlaylist(playlist_url) => {
|
AppEvent::DownloadPlaylist(playlist) => {
|
||||||
if let Ok(()) = dlp::download_from_soundcloud(&playlist_url, &get_temp_dl_dir(), sender.clone()).await {
|
if let Ok(()) = dlp::download_from_soundcloud(&playlist.permalink_url, &get_temp_dl_dir(), sender.clone()).await {
|
||||||
// TODO: sync with db
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user