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"
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]]

View File

@ -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"

View File

@ -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()?;
{

View File

@ -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);

View File

@ -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(),
};

View File

@ -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;