small update

This commit is contained in:
Michael Wain 2025-02-16 03:00:08 +03:00
parent b29ef4901e
commit 92d3f7ba77
6 changed files with 226 additions and 109 deletions

102
Cargo.lock generated
View File

@ -112,15 +112,6 @@ 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"
@ -266,16 +257,6 @@ 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"
@ -320,16 +301,6 @@ 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"
@ -547,16 +518,6 @@ 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"
@ -975,8 +936,8 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "itunesdb"
version = "0.1.11"
source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#4204fdcda886438d815da7b70f736506e06a22e3"
version = "0.1.19"
source = "git+https://gitea.awain.net/alterwain/ITunesDB.git#e1e8d0a12ca2c3825191ff1d815645a704e1b646"
dependencies = [
"bincode",
"env_logger",
@ -1071,15 +1032,15 @@ dependencies = [
name = "lyrica"
version = "0.1.0"
dependencies = [
"bincode",
"chrono",
"color-eyre",
"crossterm",
"dirs",
"futures",
"itunesdb",
"md-5",
"mp3-duration",
"puremp3",
"rand",
"ratatui",
"regex",
"rusb",
@ -1091,16 +1052,6 @@ dependencies = [
"toml",
]
[[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"
@ -1134,6 +1085,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "mp3-duration"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "348bdc7300502f0801e5b57c448815713cd843b744ef9bda252a2698fdf90a0f"
dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "native-tls"
version = "0.2.13"
@ -1411,7 +1371,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror",
"thiserror 2.0.11",
]
[[package]]
@ -1883,13 +1843,33 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.11",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -2114,12 +2094,6 @@ 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"
@ -2196,12 +2170,6 @@ 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"

View File

@ -12,7 +12,6 @@ dirs = "6.0.0"
toml = "0.8.20"
serde = "1.0.217"
serde_json = "1.0"
bincode = "1.3.3"
regex = "1.11.1"
ratatui = { version = "0.29.0", features = ["all-widgets"] }
color-eyre = "0.6.3"
@ -21,6 +20,7 @@ futures = "0.3"
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7.12", features = ["codec"] }
soundcloud = { version = "0.1.8", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" }
itunesdb = { version = "0.1.11", git = "https://gitea.awain.net/alterwain/ITunesDB.git" }
md-5 = "0.10.6"
itunesdb = { version = "0.1.19", git = "https://gitea.awain.net/alterwain/ITunesDB.git" }
puremp3 = "0.1.0"
mp3-duration = "0.1.10"
rand = "0.8.5"

View File

@ -1,15 +1,71 @@
use crate::{screen::AppScreen, theme::Theme};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::prelude::{Color, Line, Style, Stylize};
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table};
use ratatui::Frame;
#[derive(Default)]
pub struct FileSystem {}
impl AppScreen for FileSystem {
fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) {}
fn render(&self, frame: &mut ratatui::Frame, theme: &Theme) {
todo!()
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0), // Main content area
Constraint::Length(1), // Status bar
])
.split(frame.area());
self.render_main(frame, chunks[0]);
// Render Status Bar
let status_bar = Paragraph::new(Line::from(vec![
"<F5> SAVE AS PLAYLIST".bold(),
" | ".dark_gray(),
"<F6> SAVE AS IS".bold(),
" | ".dark_gray(),
"<F8> SELECT".bold(),
" | ".dark_gray(),
"<F9> DESELECT".bold(),
" | ".dark_gray(),
"<Q> QUIT".bold(),
]))
.centered();
frame.render_widget(status_bar, chunks[1]); // Render into third chunk
}
fn as_any(&mut self) -> &mut dyn std::any::Any {
todo!()
self
}
}
impl FileSystem {
fn render_main(&self, frame: &mut Frame, area: Rect) {
let mut v = vec![Row::new(vec!["Name", "Type", "Size", "Modified"])
.style(Style::default().fg(Color::Gray))];
// move this out to make hdd not suffer
let paths = std::fs::read_dir("~/Documents").unwrap();
for path in paths {
v.push();
}
let table = Table::new(
v,
&[
Constraint::Percentage(50),
Constraint::Length(5),
Constraint::Percentage(20),
Constraint::Percentage(30),
],
)
.block(Block::default().borders(Borders::ALL).title(" Documents "))
.style(Style::default().fg(Color::Black));
frame.render_widget(table, area);
}
}

View File

@ -1,5 +1,4 @@
use std::{collections::HashMap, error::Error, io};
use crate::file_system::FileSystem;
use crate::theme::Theme;
use color_eyre::Result;
use crossterm::{
@ -19,6 +18,7 @@ use ratatui::{
Frame, Terminal,
};
use screen::AppScreen;
use std::{collections::HashMap, error::Error, io};
use sync::AppEvent;
use tokio::sync::mpsc::{self, Receiver, UnboundedSender};
use tokio_util::sync::CancellationToken;
@ -66,6 +66,7 @@ impl Default for App {
screens.insert(AppState::IPodWait, Box::new(WaitScreen::default()));
screens.insert(AppState::MainScreen, Box::new(MainScreen::new(jx.clone())));
screens.insert(AppState::LoadingScreen, Box::new(LoadingScreen::default()));
screens.insert(AppState::FileSystem, Box::new(FileSystem::default()));
Self {
receiver: rx,

View File

@ -1,6 +1,5 @@
use chrono::{DateTime, TimeZone, Utc};
use crossterm::event::{KeyCode, KeyEvent};
use itunesdb::xobjects::XPlaylist;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
@ -11,8 +10,8 @@ use ratatui::{
use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists};
use tokio::sync::mpsc::UnboundedSender;
use crate::{screen::AppScreen, sync::AppEvent, theme::Theme};
use crate::sync::DBPlaylist;
use crate::{screen::AppScreen, sync::AppEvent, theme::Theme, AppState};
pub struct MainScreen {
mode: bool,
@ -36,6 +35,10 @@ impl AppScreen for MainScreen {
KeyCode::Down => self.next_row(),
KeyCode::F(6) => self.download_row(),
KeyCode::Tab => self.switch_mode(),
KeyCode::F(2) => {
self.sender
.send(AppEvent::SwitchScreen(AppState::FileSystem));
}
_ => {}
}
}
@ -82,7 +85,7 @@ impl AppScreen for MainScreen {
"<Q> QUIT".bold(),
]))
.centered();
frame.render_widget(status_bar, chunks[2]); // Render into third chunk
frame.render_widget(status_bar, chunks[2]);
}
fn as_any(&mut self) -> &mut dyn std::any::Any {
@ -203,7 +206,7 @@ impl MainScreen {
self.update_max_rows();
}
}
pub fn set_itunes(&mut self, pl: Vec<DBPlaylist>) {
self.playlists = Some(pl);
if self.selected_tab == 2 {
@ -247,11 +250,11 @@ impl MainScreen {
);
if let Some(s) = &self.playlists {
for (i, playlist) in s.iter().enumerate() {
let date = Utc.timestamp_millis_opt(playlist.data.timestamp as i64).unwrap();
let date = Utc.timestamp_millis_opt(playlist.timestamp as i64).unwrap();
let mut row = Row::new(vec![
playlist.data.persistent_playlist_id.to_string(),
playlist.id.to_string(),
"".to_string(),
playlist.elems.len().to_string(),
playlist.tracks.len().to_string(),
format!("{}", date.format("%Y-%m-%d %H:%M")),
"YES".to_string(),
]);
@ -320,7 +323,7 @@ impl MainScreen {
}
}
v
},
}
2 => {
// local
let mut v = Vec::new();
@ -329,14 +332,14 @@ impl MainScreen {
.style(Style::default().fg(Color::Gray)),
);
if let Some(pls) = &self.playlists {
let s = &pls.get(self.selected_playlist as usize).unwrap().elems;
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.location.clone(),
track.bitrate.to_string(),
track.genre.clone(),
track.data.unique_id.to_string(),
track.get_title(),
track.get_location(),
track.data.bitrate.to_string(),
track.get_genre(),
]);
if self.selected_song == i as i32 {
row = row.style(Style::default().bg(Color::LightBlue).fg(Color::White));

View File

@ -1,7 +1,9 @@
use itunesdb::objects::{ListSortOrder, PlaylistItem};
use itunesdb::serializer;
use itunesdb::xobjects::{XDatabase, XPlArgument, XPlaylist, XTrackItem};
use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists, CloudTrack};
use std::io::Write;
use std::path::{Path, PathBuf};
use itunesdb::xobjects::{XDatabase, XTrackItem};
use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists};
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncWriteExt},
@ -9,9 +11,13 @@ use tokio::{
};
use tokio_util::sync::CancellationToken;
use crate::{config::{
get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration,
}, dlp::{self, DownloadProgress}, util, AppState};
use crate::{
config::{
get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration,
},
dlp::{self, DownloadProgress},
util, AppState,
};
pub enum AppEvent {
SearchIPod,
@ -28,7 +34,54 @@ pub struct DBPlaylist {
pub id: u64,
pub title: String,
pub timestamp: u32,
pub tracks: Vec<XTrackItem>
pub tracks: Vec<XTrackItem>,
}
fn track_from_soundcloud(value: &CloudTrack) -> XTrackItem {
let mut track_path = get_temp_dl_dir();
track_path.push(value.id.to_string());
track_path.set_extension("mp3");
let f = std::fs::File::open(&track_path).unwrap();
let mut data = &std::fs::read(&track_path).unwrap()[..];
let (header, _samples) = puremp3::read_mp3(data).unwrap();
let duration = mp3_duration::from_read(&mut data).unwrap();
let mut track = XTrackItem::new(
value.id as u32,
f.metadata().unwrap().len() as u32,
duration.as_millis() as u32,
0,
header.bitrate.bps() / 1000,
header.sample_rate.hz(),
hash(),
0,
);
track.set_title(value.title.clone().unwrap());
track.set_artist(
value
.user
.clone()
.map_or(String::new(), |a| a.username.unwrap_or(a.permalink)),
);
track.set_genre(value.genre.clone().unwrap());
track.update_arg(6, String::from("MPEG audio file"));
track
}
// note: this hash function is used to make unique ids for each track. It doesn't aim to generate secure ones.
fn hash() -> u64 {
rand::random::<u64>()
}
fn overwrite_database(database: &mut XDatabase, ipod_path: &String) {
let data = serializer::to_bytes(database);
let mut p: PathBuf = Path::new(ipod_path).into();
p.push("iPod_Control");
p.push("iTunes");
p.push("iTunesDB");
let mut file = std::fs::File::create(p).unwrap();
let _ = file.write(&data);
}
pub fn initialize_async_service(
@ -60,7 +113,8 @@ pub fn initialize_async_service(
let _ = sender.send(AppEvent::IPodNotFound).await;
}
},
AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, &mut database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await,
AppEvent::DownloadPlaylist(playlist) => download_playlist(playlist, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await,
AppEvent::SwitchScreen(state) => { let _ = sender.send(AppEvent::SwitchScreen(state)).await;},
_ => {}
}
}
@ -74,7 +128,7 @@ async fn download_playlist(
playlist: CloudPlaylist,
database: &mut XDatabase,
sender: &Sender<AppEvent>,
ipod_path: String
ipod_path: String,
) {
if let Ok(()) =
dlp::download_from_soundcloud(&playlist.permalink_url, &get_temp_dl_dir(), sender.clone())
@ -82,35 +136,61 @@ async fn download_playlist(
{
let tracks = playlist.tracks;
let mut p: PathBuf = Path::new(&ipod_path).into();
let p: PathBuf = Path::new(&ipod_path).into();
let mut new_playlist = XPlaylist::new(rand::random(), ListSortOrder::SongTitle);
new_playlist.set_title(playlist.title);
for track in tracks {
if track.title.is_none() {
continue;
}
let mut t: XTrackItem = track.into();
let mut t: XTrackItem = track_from_soundcloud(&track);
t.data.unique_id = database.get_unique_id();
new_playlist.add_elem(t.data.unique_id);
let mut tp = PathBuf::new();
tp.push("iPod_Control");
tp.push(":iPod_Control");
tp.push("Music");
tp.push(["F", &format!("{:02}", &(t.data.unique_id % 100))].concat());
tp.push(format!("{:X}", t.data.unique_id));
tp.set_extension("mp3");
t.set_location(tp.to_str().unwrap().to_string().replace("/", ":").to_string());
t.set_location(
tp.to_str()
.unwrap()
.to_string()
.replace("/", ":")
.to_string(),
);
let mut dest = p.clone();
dest.push(tp);
dest.push("iPod_Control");
dest.push("Music");
dest.push(["F", &format!("{:02}", &(t.data.unique_id % 100))].concat());
let _ = std::fs::create_dir_all(dest.to_str().unwrap());
dest.push(format!("{:X}", t.data.unique_id));
dest.set_extension("mp3");
let mut track_path = get_temp_dl_dir();
track_path.push(track.id.to_string());
track_path.set_extension("mp3");
let _ = std::fs::copy(track_path, dest);
let _ = std::fs::copy(track_path.to_str().unwrap(), dest.to_str().unwrap());
let _ = database.add_track(t);
database.add_track(t);
}
database.add_playlist(new_playlist);
}
let _ = sender
.send(AppEvent::SwitchScreen(AppState::MainScreen))
.await;
let _ = sender
.send(AppEvent::ITunesParsed(get_playlists(database)))
.await;
overwrite_database(database, &ipod_path);
}
fn get_playlists(db: &mut XDatabase) -> Vec<DBPlaylist> {
@ -120,7 +200,19 @@ fn get_playlists(db: &mut XDatabase) -> Vec<DBPlaylist> {
id: t.data.persistent_playlist_id,
title: t.get_title(),
timestamp: t.data.timestamp,
tracks: t.elems.iter().map(|(i, _a)| db.get_track(i.track_id)).filter(|t| t.is_some()).map(|t| t.unwrap().clone()).collect()}).collect()
tracks: to_tracks(db, t.elems.clone()),
})
.collect()
}
fn to_tracks(db: &mut XDatabase, elems: Vec<(PlaylistItem, Vec<XPlArgument>)>) -> Vec<XTrackItem> {
elems
.iter()
.map(|(i, _a)| i.track_id)
.map(|id| db.get_track(id))
.filter(|i| i.is_some())
.map(|i| i.unwrap().clone())
.collect()
}
async fn parse_itunes(sender: &Sender<AppEvent>, path: String) -> XDatabase {
@ -129,7 +221,6 @@ async fn parse_itunes(sender: &Sender<AppEvent>, path: String) -> XDatabase {
p.push("iPod_Control");
p.push("iTunes");
p.push("iTunesDB");
println!("{}", p.to_str().unwrap());
let _ = std::fs::copy(p, &cd);
let mut file = File::open(cd).await.unwrap();
let mut contents = vec![];
@ -137,9 +228,7 @@ async fn parse_itunes(sender: &Sender<AppEvent>, path: String) -> XDatabase {
let mut database = itunesdb::deserializer::parse_bytes(&contents);
let _ = sender
.send(AppEvent::ITunesParsed(
get_playlists(&mut database),
))
.send(AppEvent::ITunesParsed(get_playlists(&mut database)))
.await;
let p = get_config_path();