upd
This commit is contained in:
parent
7a0468d0a8
commit
e6dcb1bae6
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -433,7 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1021,8 +1021,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "itunesdb"
|
||||
version = "0.1.2"
|
||||
source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#2db99df934c29f03b842a43245f1e93d7d4ade27"
|
||||
version = "0.1.6"
|
||||
source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#eac020520b7efa2173065772105ab0b2c4ba6da6"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"env_logger",
|
||||
@ -1662,7 +1662,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2029,7 +2029,7 @@ dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2532,7 +2532,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -23,7 +23,7 @@ tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7.12", features = ["codec"] }
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
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"
|
||||
color-thief = "0.2"
|
||||
redb = "2.4.0"
|
||||
|
74
src/db.rs
74
src/db.rs
@ -1,6 +1,6 @@
|
||||
use std::fs::File;
|
||||
|
||||
use itunesdb::xobjects::{XArgument, XTrackItem};
|
||||
use itunesdb::xobjects::{XArgument, XPlaylist, XTrackItem};
|
||||
use md5::{Digest, Md5};
|
||||
use redb::{Database, Error, ReadableTable, TableDefinition};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -9,6 +9,7 @@ use soundcloud::sobjects::CloudTrack;
|
||||
use crate::config::{get_db, get_temp_dl_dir};
|
||||
|
||||
const TRACKS: TableDefinition<u32, Vec<u8>> = TableDefinition::new("tracks");
|
||||
const PLAYLISTS: TableDefinition<u64, Vec<u8>> = TableDefinition::new("playlists");
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Track {
|
||||
@ -17,7 +18,7 @@ pub struct Track {
|
||||
stars: u8,
|
||||
last_modified_time: u32,
|
||||
size: u32,
|
||||
length: u32,
|
||||
pub length: u32,
|
||||
year: u32,
|
||||
pub bitrate: u32,
|
||||
sample_rate: u32,
|
||||
@ -31,7 +32,25 @@ pub struct Track {
|
||||
location: String,
|
||||
album: 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 {
|
||||
@ -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 {
|
||||
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> {
|
||||
let write_txn = db.begin_write()?;
|
||||
{
|
||||
|
@ -111,9 +111,9 @@ impl App {
|
||||
AppEvent::IPodNotFound => {
|
||||
let _ = self.sender.send(AppEvent::SearchIPod);
|
||||
},
|
||||
AppEvent::ITunesParsed(tracks) => {
|
||||
AppEvent::ITunesParsed(playlists) => {
|
||||
let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen);
|
||||
screen.tracks = Some(tracks);
|
||||
screen.set_itunes(playlists);
|
||||
},
|
||||
AppEvent::SoundcloudGot(playlists) => {
|
||||
let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen);
|
||||
|
@ -1,4 +1,4 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
@ -11,6 +11,7 @@ use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{db::Track, screen::AppScreen, sync::AppEvent, theme::Theme};
|
||||
use crate::db::DBPlaylist;
|
||||
|
||||
pub struct MainScreen {
|
||||
mode: bool,
|
||||
@ -21,7 +22,7 @@ pub struct MainScreen {
|
||||
max_songs: i32,
|
||||
tab_titles: Vec<String>,
|
||||
soundcloud: Option<Vec<CloudPlaylist>>,
|
||||
pub tracks: Option<Vec<Track>>,
|
||||
playlists: Option<Vec<DBPlaylist>>,
|
||||
sender: UnboundedSender<AppEvent>,
|
||||
}
|
||||
|
||||
@ -97,12 +98,12 @@ impl MainScreen {
|
||||
max_pls: 0,
|
||||
max_songs: 0,
|
||||
soundcloud: None,
|
||||
tracks: None,
|
||||
playlists: None,
|
||||
selected_tab: 0,
|
||||
tab_titles: vec![
|
||||
"YouTube".to_string(),
|
||||
"SoundCloud".to_string(),
|
||||
"Local Playlists".to_string(),
|
||||
"iPod".to_string(),
|
||||
"Settings".to_string(),
|
||||
],
|
||||
sender,
|
||||
@ -115,7 +116,7 @@ impl MainScreen {
|
||||
self.max_songs = 0;
|
||||
self.max_pls = match self.selected_tab {
|
||||
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,
|
||||
}
|
||||
.try_into()
|
||||
@ -201,6 +202,13 @@ impl MainScreen {
|
||||
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) {
|
||||
let rows = match self.selected_tab {
|
||||
@ -233,17 +241,18 @@ impl MainScreen {
|
||||
// local
|
||||
let mut v = Vec::new();
|
||||
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)),
|
||||
);
|
||||
if let Some(s) = &self.tracks {
|
||||
for (i, track) in s.iter().enumerate() {
|
||||
if let Some(s) = &self.playlists {
|
||||
for (i, playlist) in s.iter().enumerate() {
|
||||
let date = Utc.timestamp_millis_opt(playlist.timestamp as i64).unwrap();
|
||||
let mut row = Row::new(vec![
|
||||
track.unique_id.to_string(),
|
||||
track.title.clone(),
|
||||
track.artist.clone(),
|
||||
track.bitrate.to_string(),
|
||||
format!("{:X}", track.dbid),
|
||||
playlist.persistent_playlist_id.to_string(),
|
||||
"".to_string(),
|
||||
playlist.tracks.len().to_string(),
|
||||
format!("{}", date.format("%Y-%m-%d %H:%M")),
|
||||
"YES".to_string(),
|
||||
]);
|
||||
if self.selected_playlist == i as i32 {
|
||||
row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White));
|
||||
@ -282,7 +291,7 @@ impl MainScreen {
|
||||
|
||||
let rows = match self.selected_tab {
|
||||
1 => {
|
||||
// local
|
||||
// sc
|
||||
let mut v = Vec::new();
|
||||
v.push(
|
||||
Row::new(vec!["Id", "Title", "Artist", "Duration", "Genre"])
|
||||
@ -310,6 +319,31 @@ impl MainScreen {
|
||||
}
|
||||
}
|
||||
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(),
|
||||
};
|
||||
|
50
src/sync.rs
50
src/sync.rs
@ -10,19 +10,15 @@ use tokio::{
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration,
|
||||
},
|
||||
db::{self, Track},
|
||||
dlp::{self, DownloadProgress},
|
||||
AppState,
|
||||
};
|
||||
use crate::{config::{
|
||||
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};
|
||||
|
||||
pub enum AppEvent {
|
||||
SearchIPod,
|
||||
IPodNotFound,
|
||||
ITunesParsed(Vec<Track>),
|
||||
ITunesParsed(Vec<DBPlaylist>),
|
||||
SoundcloudGot(CloudPlaylists),
|
||||
DownloadPlaylist(CloudPlaylist),
|
||||
CurrentProgress(DownloadProgress),
|
||||
@ -38,6 +34,8 @@ pub fn initialize_async_service(
|
||||
tokio::spawn(async move {
|
||||
let _ = std::fs::create_dir_all(get_configs_dir());
|
||||
|
||||
let mut ipod_db = None;
|
||||
|
||||
let database = db::init_db();
|
||||
|
||||
let mut receiver = receiver;
|
||||
@ -49,13 +47,13 @@ pub fn initialize_async_service(
|
||||
if let Some(request) = r {
|
||||
match request {
|
||||
AppEvent::SearchIPod => {
|
||||
/*if let Some(p) = util::search_ipod() {
|
||||
let _ = sender.send(AppEvent::IPodFound(p)).await;
|
||||
if let Some(p) = util::search_ipod() {
|
||||
let _ = sender.send(AppEvent::SwitchScreen(AppState::MainScreen)).await;
|
||||
ipod_db = Some(p.clone());
|
||||
parse_itunes(&database, &sender, p).await;
|
||||
} else {
|
||||
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,
|
||||
_ => {}
|
||||
@ -92,12 +90,11 @@ async fn download_playlist(
|
||||
}
|
||||
|
||||
async fn parse_itunes(database: &Database, sender: &Sender<AppEvent>, path: String) {
|
||||
// todo: parse itunes
|
||||
let cd = get_temp_itunesdb();
|
||||
let p: PathBuf = Path::new(&path).into();
|
||||
// p.push("iPod_Control");
|
||||
// p.push("iTunes");
|
||||
// p.set_file_name("iTunesDB");
|
||||
let mut p: PathBuf = Path::new(&path).into();
|
||||
p.push("iPod_Control");
|
||||
p.push("iTunes");
|
||||
p.set_file_name("iTunesDB");
|
||||
let _ = std::fs::copy(p, &cd);
|
||||
let mut file = File::open(cd).await.unwrap();
|
||||
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
|
||||
.send(AppEvent::ITunesParsed(
|
||||
db::get_all_tracks(database).unwrap(),
|
||||
db::get_all_playlists(database).unwrap(),
|
||||
))
|
||||
.await;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user