This commit is contained in:
Michael Wain 2025-02-13 18:06:09 +03:00
parent 7a0468d0a8
commit e6dcb1bae6
6 changed files with 156 additions and 48 deletions

12
Cargo.lock generated
View File

@ -433,7 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -1021,8 +1021,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]] [[package]]
name = "itunesdb" name = "itunesdb"
version = "0.1.2" version = "0.1.6"
source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#2db99df934c29f03b842a43245f1e93d7d4ade27" source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#eac020520b7efa2173065772105ab0b2c4ba6da6"
dependencies = [ dependencies = [
"bincode", "bincode",
"env_logger", "env_logger",
@ -1662,7 +1662,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2029,7 +2029,7 @@ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2532,7 +2532,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]

View File

@ -23,7 +23,7 @@ tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7.12", features = ["codec"] } tokio-util = { version = "0.7.12", features = ["codec"] }
strum = { version = "0.27", features = ["derive"] } strum = { version = "0.27", features = ["derive"] }
soundcloud = { version = "0.1.8", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" } soundcloud = { version = "0.1.8", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" }
itunesdb = { version = "0.1.2", git = "https://gitea.awain.net/alterwain/ITunesDB.git" } itunesdb = { version = "0.1.6", git = "https://gitea.awain.net/alterwain/ITunesDB.git" }
ureq = "3.0.5" ureq = "3.0.5"
color-thief = "0.2" color-thief = "0.2"
redb = "2.4.0" redb = "2.4.0"

View File

@ -1,6 +1,6 @@
use std::fs::File; use std::fs::File;
use itunesdb::xobjects::{XArgument, XTrackItem}; use itunesdb::xobjects::{XArgument, XPlaylist, XTrackItem};
use md5::{Digest, Md5}; use md5::{Digest, Md5};
use redb::{Database, Error, ReadableTable, TableDefinition}; use redb::{Database, Error, ReadableTable, TableDefinition};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,6 +9,7 @@ use soundcloud::sobjects::CloudTrack;
use crate::config::{get_db, get_temp_dl_dir}; 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");
const PLAYLISTS: TableDefinition<u64, Vec<u8>> = TableDefinition::new("playlists");
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Track { pub struct Track {
@ -17,7 +18,7 @@ pub struct Track {
stars: u8, stars: u8,
last_modified_time: u32, last_modified_time: u32,
size: u32, size: u32,
length: u32, pub length: u32,
year: u32, year: u32,
pub bitrate: u32, pub bitrate: u32,
sample_rate: u32, sample_rate: u32,
@ -31,7 +32,25 @@ pub struct Track {
location: String, location: String,
album: String, album: String,
pub artist: String, pub artist: String,
genre: String, pub genre: String,
}
#[derive(Serialize, Deserialize)]
pub struct DBPlaylist {
pub persistent_playlist_id: u64,
pub title: String,
pub timestamp: u32,
pub is_master: bool,
pub tracks: Vec<Track>,
}
#[derive(Serialize, Deserialize)]
pub struct Playlist {
pub persistent_playlist_id: u64,
pub title: String,
pub timestamp: u32,
pub is_master: bool,
pub tracks: Vec<u32>,
} }
impl From<CloudTrack> for Track { impl From<CloudTrack> for Track {
@ -100,12 +119,57 @@ impl From<XTrackItem> for Track {
} }
} }
// TODO: implement From (or Into) for Track, convert from Soundcloud Audio or iTunes
pub fn init_db() -> Database { pub fn init_db() -> Database {
Database::create(get_db()).unwrap() Database::create(get_db()).unwrap()
} }
pub fn insert_playlist(db: &Database, playlist: Playlist) -> Result<(), Error> {
let write_txn = db.begin_write()?;
{
let mut table = write_txn.open_table(PLAYLISTS)?;
let uid = playlist.persistent_playlist_id;
let data = bincode::serialize(&playlist).unwrap();
table.insert(uid, data)?;
}
write_txn.commit()?;
Ok(())
}
pub fn get_playlist(db: &Database, id: u64) -> Result<DBPlaylist, Error> {
let read_txn = db.begin_read()?;
let table = read_txn.open_table(PLAYLISTS)?;
let b = table.get(id)?.unwrap().value();
let value: Playlist = bincode::deserialize(&b).unwrap();
let playlist = DBPlaylist {
persistent_playlist_id: value.persistent_playlist_id,
timestamp: value.timestamp,
title: value.title,
is_master: value.is_master,
tracks: value.tracks.iter().map(|id| get_track(db, *id)).filter(|t| t.is_ok()).map(|t| t.unwrap()).collect(),
};
Ok(playlist.into())
}
pub fn get_all_playlists(db: &Database) -> Result<Vec<DBPlaylist>, Error> {
let read_txn = db.begin_read()?;
let table = read_txn.open_table(PLAYLISTS)?;
Ok(table
.iter()
.unwrap()
.flatten()
.map(|d| bincode::deserialize(&d.1.value()).unwrap())
.collect::<Vec<Playlist>>()
.iter()
.map(|p| DBPlaylist{
persistent_playlist_id: p.persistent_playlist_id,
timestamp: p.timestamp,
title: p.title.clone(),
is_master: p.is_master,
tracks: p.tracks.iter().map(|id| get_track(db, *id)).filter(|t| t.is_ok()).map(|t| t.unwrap()).collect()
})
.collect())
}
pub 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()?;
{ {

View File

@ -111,9 +111,9 @@ impl App {
AppEvent::IPodNotFound => { AppEvent::IPodNotFound => {
let _ = self.sender.send(AppEvent::SearchIPod); let _ = self.sender.send(AppEvent::SearchIPod);
}, },
AppEvent::ITunesParsed(tracks) => { AppEvent::ITunesParsed(playlists) => {
let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen); let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen);
screen.tracks = Some(tracks); screen.set_itunes(playlists);
}, },
AppEvent::SoundcloudGot(playlists) => { AppEvent::SoundcloudGot(playlists) => {
let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen); let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen);

View File

@ -1,4 +1,4 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, TimeZone, Utc};
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{ use ratatui::{
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
@ -11,6 +11,7 @@ use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use crate::{db::Track, screen::AppScreen, sync::AppEvent, theme::Theme}; use crate::{db::Track, screen::AppScreen, sync::AppEvent, theme::Theme};
use crate::db::DBPlaylist;
pub struct MainScreen { pub struct MainScreen {
mode: bool, mode: bool,
@ -21,7 +22,7 @@ pub struct MainScreen {
max_songs: i32, max_songs: i32,
tab_titles: Vec<String>, tab_titles: Vec<String>,
soundcloud: Option<Vec<CloudPlaylist>>, soundcloud: Option<Vec<CloudPlaylist>>,
pub tracks: Option<Vec<Track>>, playlists: Option<Vec<DBPlaylist>>,
sender: UnboundedSender<AppEvent>, sender: UnboundedSender<AppEvent>,
} }
@ -97,12 +98,12 @@ impl MainScreen {
max_pls: 0, max_pls: 0,
max_songs: 0, max_songs: 0,
soundcloud: None, soundcloud: None,
tracks: None, playlists: None,
selected_tab: 0, selected_tab: 0,
tab_titles: vec![ tab_titles: vec![
"YouTube".to_string(), "YouTube".to_string(),
"SoundCloud".to_string(), "SoundCloud".to_string(),
"Local Playlists".to_string(), "iPod".to_string(),
"Settings".to_string(), "Settings".to_string(),
], ],
sender, sender,
@ -115,7 +116,7 @@ impl MainScreen {
self.max_songs = 0; self.max_songs = 0;
self.max_pls = match self.selected_tab { self.max_pls = match self.selected_tab {
1 => self.soundcloud.as_deref().unwrap_or(&[]).len(), 1 => self.soundcloud.as_deref().unwrap_or(&[]).len(),
2 => self.tracks.as_deref().unwrap_or(&[]).len(), 2 => self.playlists.as_deref().unwrap_or(&[]).len(),
_ => 0, _ => 0,
} }
.try_into() .try_into()
@ -201,6 +202,13 @@ impl MainScreen {
self.update_max_rows(); self.update_max_rows();
} }
} }
pub fn set_itunes(&mut self, pl: Vec<DBPlaylist>) {
self.playlists = Some(pl);
if self.selected_tab == 2 {
self.update_max_rows();
}
}
fn render_tab(&self, frame: &mut Frame, area: Rect) { fn render_tab(&self, frame: &mut Frame, area: Rect) {
let rows = match self.selected_tab { let rows = match self.selected_tab {
@ -233,17 +241,18 @@ impl MainScreen {
// local // local
let mut v = Vec::new(); let mut v = Vec::new();
v.push( v.push(
Row::new(vec!["Id", "Title", "Artist", "Bitrate", "Hash"]) Row::new(vec!["Id", "Title", "Songs Count", "Date", "IS"])
.style(Style::default().fg(Color::Gray)), .style(Style::default().fg(Color::Gray)),
); );
if let Some(s) = &self.tracks { if let Some(s) = &self.playlists {
for (i, track) in s.iter().enumerate() { for (i, playlist) in s.iter().enumerate() {
let date = Utc.timestamp_millis_opt(playlist.timestamp as i64).unwrap();
let mut row = Row::new(vec![ let mut row = Row::new(vec![
track.unique_id.to_string(), playlist.persistent_playlist_id.to_string(),
track.title.clone(), "".to_string(),
track.artist.clone(), playlist.tracks.len().to_string(),
track.bitrate.to_string(), format!("{}", date.format("%Y-%m-%d %H:%M")),
format!("{:X}", track.dbid), "YES".to_string(),
]); ]);
if self.selected_playlist == i as i32 { if self.selected_playlist == i as i32 {
row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White)); row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White));
@ -282,7 +291,7 @@ impl MainScreen {
let rows = match self.selected_tab { let rows = match self.selected_tab {
1 => { 1 => {
// local // sc
let mut v = Vec::new(); let mut v = Vec::new();
v.push( v.push(
Row::new(vec!["Id", "Title", "Artist", "Duration", "Genre"]) Row::new(vec!["Id", "Title", "Artist", "Duration", "Genre"])
@ -310,6 +319,31 @@ impl MainScreen {
} }
} }
v v
},
2 => {
// local
let mut v = Vec::new();
v.push(
Row::new(vec!["Id", "Title", "Artist", "Bitrate", "Genre"])
.style(Style::default().fg(Color::Gray)),
);
if let Some(pls) = &self.playlists {
let s = &pls.get(self.selected_playlist as usize).unwrap().tracks;
for (i, track) in s.iter().enumerate() {
let mut row = Row::new(vec![
track.unique_id.to_string(),
track.title.clone(),
track.artist.clone(),
track.bitrate.to_string(),
track.genre.clone(),
]);
if self.selected_song == i as i32 {
row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White));
}
v.push(row);
}
}
v
} }
_ => Vec::new(), _ => Vec::new(),
}; };

View File

@ -10,19 +10,15 @@ use tokio::{
}; };
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{ use crate::{config::{
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}, util, AppState};
}, use crate::db::{DBPlaylist, Playlist};
db::{self, Track},
dlp::{self, DownloadProgress},
AppState,
};
pub enum AppEvent { pub enum AppEvent {
SearchIPod, SearchIPod,
IPodNotFound, IPodNotFound,
ITunesParsed(Vec<Track>), ITunesParsed(Vec<DBPlaylist>),
SoundcloudGot(CloudPlaylists), SoundcloudGot(CloudPlaylists),
DownloadPlaylist(CloudPlaylist), DownloadPlaylist(CloudPlaylist),
CurrentProgress(DownloadProgress), CurrentProgress(DownloadProgress),
@ -38,6 +34,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 mut ipod_db = None;
let database = db::init_db(); let database = db::init_db();
let mut receiver = receiver; let mut receiver = receiver;
@ -49,13 +47,13 @@ pub fn initialize_async_service(
if let Some(request) = r { if let Some(request) = r {
match request { match request {
AppEvent::SearchIPod => { AppEvent::SearchIPod => {
/*if let Some(p) = util::search_ipod() { if let Some(p) = util::search_ipod() {
let _ = sender.send(AppEvent::IPodFound(p)).await; let _ = sender.send(AppEvent::SwitchScreen(AppState::MainScreen)).await;
ipod_db = Some(p.clone());
parse_itunes(&database, &sender, p).await;
} else { } else {
let _ = sender.send(AppEvent::IPodNotFound).await; let _ = sender.send(AppEvent::IPodNotFound).await;
}*/ }
let _ = sender.send(AppEvent::SwitchScreen(AppState::MainScreen)).await;
parse_itunes(&database, &sender, "/Users/michael/Documents/ipod/iTunes/iTunesDB".to_string()).await;
}, },
AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, &database, &sender).await, AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, &database, &sender).await,
_ => {} _ => {}
@ -92,12 +90,11 @@ async fn download_playlist(
} }
async fn parse_itunes(database: &Database, sender: &Sender<AppEvent>, path: String) { async fn parse_itunes(database: &Database, sender: &Sender<AppEvent>, path: String) {
// todo: parse itunes
let cd = get_temp_itunesdb(); let cd = get_temp_itunesdb();
let p: PathBuf = Path::new(&path).into(); let mut p: PathBuf = Path::new(&path).into();
// p.push("iPod_Control"); p.push("iPod_Control");
// p.push("iTunes"); p.push("iTunes");
// p.set_file_name("iTunesDB"); p.set_file_name("iTunesDB");
let _ = std::fs::copy(p, &cd); let _ = std::fs::copy(p, &cd);
let mut file = File::open(cd).await.unwrap(); let mut file = File::open(cd).await.unwrap();
let mut contents = vec![]; let mut contents = vec![];
@ -111,9 +108,22 @@ async fn parse_itunes(database: &Database, sender: &Sender<AppEvent>, path: Stri
} }
} }
if let XSomeList::Playlists(playlists) = &xdb.find_dataset(3).child {
for playlist in playlists {
let pl = Playlist {
persistent_playlist_id: playlist.data.persistent_playlist_id,
timestamp: playlist.data.timestamp,
title: String::new() ,
is_master: playlist.data.is_master_playlist_flag != 0,
tracks: playlist.elems.iter().map(|e| e.0.track_id).collect()
};
let _ = db::insert_playlist(database, pl);
}
}
let _ = sender let _ = sender
.send(AppEvent::ITunesParsed( .send(AppEvent::ITunesParsed(
db::get_all_tracks(database).unwrap(), db::get_all_playlists(database).unwrap(),
)) ))
.await; .await;