Added gui

This commit is contained in:
Gvidas Juknevičius 2024-04-22 19:14:48 +03:00
parent 9fef257bfc
commit b977e9cea5
Signed by: MCorange
GPG Key ID: 12B1346D720B7FBB
15 changed files with 3173 additions and 92 deletions

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
/out /out
/music_mgr/target /music_mgr/target
/music_mgr/config.json
/music_mgr/manifest.json
/.venv /.venv
/config.json /config.json

2874
music_mgr/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[package] [package]
name = "music_mgr" name = "mcmg"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
@ -10,6 +10,8 @@ anstyle = "1.0.6"
anyhow = "1.0.81" anyhow = "1.0.81"
camino = "1.1.6" camino = "1.1.6"
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
eframe = "0.27.2"
egui = "0.27.2"
env_logger = "0.11.3" env_logger = "0.11.3"
lazy_static = "1.4.0" lazy_static = "1.4.0"
libc = "0.2.153" libc = "0.2.153"

View File

@ -0,0 +1,4 @@
{
"format": "m4a",
"genres": {}
}

View File

@ -43,7 +43,7 @@ pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut
if should_download { if should_download {
downloader.download_song(cfg, &name, &song, &genre, manifest.get_format()).await?; downloader.download_song(cfg, &name, &song, &genre, manifest.get_format()).await?;
downloader.wait_for_procs(0).await?; crate::process_manager::wait_for_procs_untill(0).await?;
} }
Ok(()) Ok(())

View File

@ -0,0 +1,105 @@
mod nav_bar;
mod song_edit_window;
use egui::{Color32, Label, Sense};
use crate::manifest::Manifest;
use self::song_edit_window::GuiSongEditor;
pub struct Gui {
manifest: Manifest,
song_editor: GuiSongEditor
}
impl Gui {
fn new(_: &eframe::CreationContext<'_>, manifest: Manifest) -> Self {
Self {
manifest,
song_editor: GuiSongEditor {
is_open: false,
song: Default::default(),
ed_url: String::new(),
ed_name: String::new(),
},
}
}
pub fn start(manifest: Manifest) -> anyhow::Result<()> {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0])
.with_min_inner_size([300.0, 220.0]),
// .with_icon(
// // NOTE: Adding an icon is optional
// eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..])
// .expect("Failed to load icon"),
// ),
..Default::default()
};
if let Err(e) = eframe::run_native(
"eframe template",
native_options,
Box::new(|cc| Box::new(Gui::new(cc, manifest))),
) {
log::error!("Failed to create window: {e}");
};
Ok(())
}
}
impl eframe::App for Gui {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.draw_nav(ctx, frame);
self.draw_song_edit_window(ctx, frame);
egui::CentralPanel::default().show(ctx, |ui| {
// The central panel the region left after adding TopPanel's and SidePanel's
ui.heading(format!("Songs ({})", self.manifest.get_song_count()));
egui::ScrollArea::vertical()
.max_width(f32::INFINITY)
.auto_shrink(false)
.show(ui, |ui| {
for (genre, songs) in self.manifest.get_genres() {
for (song_name, song) in songs {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("[");
ui.hyperlink_to("link", song.get_url().unwrap());
ui.label("] ");
ui.colored_label(Color32::LIGHT_BLUE, genre);
ui.label(": ");
if ui.add(Label::new(song_name).sense(Sense::click())).clicked() {
self.song_editor.song = (
genre.clone(),
song_name.clone(),
);
log::debug!("Label pressed");
self.song_editor.is_open = true;
self.song_editor.ed_name = song_name.clone();
self.song_editor.ed_url = song.get_url_str().clone();
}
});
// ui.label(RichText::new(""))
}
}
});
ui.separator();
ui.add(egui::github_link_file!(
"https://github.com/emilk/eframe_template/blob/main/",
"Source code."
));
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
egui::warn_if_debug_build(ui);
});
});
}
}

View File

@ -0,0 +1,27 @@
use super::Gui;
impl Gui {
pub fn draw_nav(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
// The top panel is often a good place for a menu bar:
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
if ui.button("Save").clicked() {
if let Err(e) = self.manifest.save(None) {
log::error!("Failed to save manifest: {e}");
}
}
});
ui.add_space(16.0);
ui.with_layout(egui::Layout::bottom_up(egui::Align::RIGHT), |ui| {
egui::widgets::global_dark_light_mode_buttons(ui);
});
});
});
}
}

View File

@ -0,0 +1,79 @@
use egui::Color32;
use crate::manifest::{GenreName, SongName};
use super::Gui;
pub struct GuiSongEditor {
pub is_open: bool,
pub song: (GenreName, SongName),
pub ed_url: String,
pub ed_name: String,
}
impl Gui {
pub fn draw_song_edit_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
let mut save = false;
let (genre, song_name) = self.song_editor.song.clone();
let Some(song) = self.manifest.get_song(genre.clone(), &song_name) else {
return;
};
let song = song.clone();
egui::Window::new("Song editor")
.open(&mut self.song_editor.is_open)
.show(ctx,
|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("[");
ui.hyperlink_to("link", song.get_url().unwrap());
ui.label("] ");
ui.colored_label(Color32::LIGHT_BLUE, &genre);
ui.label(": ");
ui.label(&song_name)
});
ui.horizontal(|ui| {
ui.label("Type: ");
ui.label(&song.get_type().to_string());
});
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut self.song_editor.ed_name);
});
ui.horizontal(|ui| {
ui.label("Url: ");
ui.text_edit_singleline(&mut self.song_editor.ed_url);
});
if ui.button("Save").clicked() {
save = true;
}
});
if save {
{
let Some(song) = self.manifest.get_song_mut(genre.clone(), &song_name) else {
return;
};
*song.get_url_str_mut() = self.song_editor.ed_url.clone();
}
let Some(genre) = self.manifest.get_genre_mut(genre.clone()) else {
return;
};
genre.remove(&song_name);
genre.insert(self.song_editor.ed_name.clone(), song);
let _ = self.manifest.save(None);
}
}
}

View File

@ -1,13 +1,17 @@
mod add; mod add;
pub mod gui;
use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest}; use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest};
pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow::Result<()> { pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow::Result<()> {
log::info!("Is in term: {}", cfg.isatty);
//std::fs::write("./isatty", format!("{}\n", cfg.isatty))?;
let mut downloader = Downloader::new(); let mut downloader = Downloader::new();
match &cfg.cli.command { match (&cfg.cli.command, cfg.isatty) {
None | Some(CliCommand::Download) => { (None | Some(CliCommand::Download), true) => {
match downloader.download_all(manifest, &cfg).await { match downloader.download_all(manifest, &cfg).await {
Ok(count) => log::info!("Downloaded {count} songs"), Ok(count) => log::info!("Downloaded {count} songs"),
Err(e) => { Err(e) => {
@ -16,7 +20,7 @@ pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow
} }
} }
}, },
Some(c) => { (Some(c), _) => {
match c { match c {
CliCommand::Download => unreachable!(), CliCommand::Download => unreachable!(),
CliCommand::Add { url, name, genre } => { CliCommand::Add { url, name, genre } => {
@ -24,8 +28,14 @@ pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow
log::error!("Failed to run 'add' command: {e}"); log::error!("Failed to run 'add' command: {e}");
} }
} }
CliCommand::Gui => {
gui::Gui::start(manifest.clone())?;
},
} }
} }
(None, false) => {
gui::Gui::start(manifest.clone())?;
},
} }
Ok(()) Ok(())

View File

@ -25,9 +25,8 @@ pub struct CliArgs {
} }
#[derive(Debug, Subcommand, Default)] #[derive(Debug, Subcommand)]
pub enum CliCommand { pub enum CliCommand {
#[default]
Download, Download,
Add { Add {
#[arg(long, short)] #[arg(long, short)]
@ -36,5 +35,6 @@ pub enum CliCommand {
name: Option<String>, name: Option<String>,
#[arg(long, short)] #[arg(long, short)]
genre: Option<String> genre: Option<String>
} },
Gui
} }

View File

@ -20,14 +20,12 @@ lazy_static!(
pub struct Downloader { pub struct Downloader {
count: usize, count: usize,
id_itr: usize,
} }
impl Downloader { impl Downloader {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
count: 0, count: 0,
id_itr: 0,
} }
} }
@ -37,10 +35,10 @@ impl Downloader {
for (genre, songs) in manifest.get_genres() { for (genre, songs) in manifest.get_genres() {
for (song_name, song) in songs { for (song_name, song) in songs {
self.download_song(cfg, song_name, song, &genre, format).await?; self.download_song(cfg, song_name, song, &genre, format).await?;
self.wait_for_procs(10).await?; self.count += crate::process_manager::wait_for_procs_untill(10).await?;
} }
} }
self.wait_for_procs(0).await?; self.count += crate::process_manager::wait_for_procs_untill(0).await?;
Ok(self.count) Ok(self.count)
} }
@ -91,51 +89,7 @@ impl Downloader {
cmd.stdout(Stdio::null()).stderr(Stdio::null()); cmd.stdout(Stdio::null()).stderr(Stdio::null());
}; };
let mut proc = cmd.spawn()?; crate::process_manager::add_proc(cmd, format!("Downloaded {dl_file}")).await?;
let id = self.id_itr;
tokio::spawn(async move {
let id = id;
proc.wait().await
.expect("child process encountered an error");
PROCESSES.lock().await.write().await.get_mut(&id).unwrap().finished = true;
});
log::info!("Downloading {dl_file}");
PROCESSES.lock().await.write().await.insert(id, Proc {
url: song.get_url_str().clone(),
path: dl_file,
finished: false,
});
self.id_itr += 1;
Ok(())
}
pub async fn wait_for_procs(&mut self, until: usize) -> anyhow::Result<()> {
// NOTE: This looks really fucked because i dont want to deadlock the processes so i lock PROCESSES for as little as possible
// NOTE: So its also kinda really slow
loop {
{
if PROCESSES.lock().await.read().await.len() <= until {
return Ok(());
}
}
let procs = {
PROCESSES.lock().await.read().await.clone()
};
for (idx, proc) in procs {
if proc.finished {
{
PROCESSES.lock().await.write().await.remove(&idx);
}
log::info!("Finished downloading {}", proc.path);
self.count += 1;
}
}
}
#[allow(unreachable_code)] //? rust_analyzer not smart enough for this
Ok(()) Ok(())
} }
} }

View File

@ -10,6 +10,7 @@ mod commands;
mod prompt; mod prompt;
mod config; mod config;
mod constants; mod constants;
mod process_manager;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {

View File

@ -8,6 +8,9 @@ use std::{collections::HashMap, fmt::{Debug, Display}, path::PathBuf};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const DEFAULT_MANIFEST: &'static str = include_str!("../../manifest.default.json");
pub type GenreName = String; pub type GenreName = String;
pub type SongName = String; pub type SongName = String;
pub type Genre = HashMap<SongName, song::Song>; pub type Genre = HashMap<SongName, song::Song>;
@ -60,6 +63,13 @@ impl Manifest {
pub fn get_genres_mut(&mut self) -> &mut HashMap<GenreName, Genre> { pub fn get_genres_mut(&mut self) -> &mut HashMap<GenreName, Genre> {
&mut self.genres &mut self.genres
} }
pub fn get_song_count(&self) -> usize {
let mut count = 0;
for (_, v) in &self.genres {
count += v.len();
}
count
}
pub fn load(&mut self, p: Option<&PathBuf>) -> Result<()> { pub fn load(&mut self, p: Option<&PathBuf>) -> Result<()> {
let path = p.unwrap_or(&self.path); let path = p.unwrap_or(&self.path);
log::debug!("Path: {path:?}"); log::debug!("Path: {path:?}");
@ -79,6 +89,11 @@ impl Manifest {
Ok(()) Ok(())
} }
pub fn load_new(p: &PathBuf) -> Result<Self> { pub fn load_new(p: &PathBuf) -> Result<Self> {
if !p.exists() {
std::fs::write(p, DEFAULT_MANIFEST)?;
}
let mut s = Self::default(); let mut s = Self::default();
log::debug!("Path: {p:?}"); log::debug!("Path: {p:?}");
s.path = p.clone(); s.path = p.clone();

View File

@ -11,6 +11,17 @@ pub enum SongType {
Soundcloud, Soundcloud,
} }
impl ToString for SongType {
fn to_string(&self) -> String {
let s = match self {
SongType::Youtube => "Youtube",
SongType::Spotify => "Spotify",
SongType::Soundcloud => "Soundcloud",
};
String::from(s)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Song { pub struct Song {
url: String, url: String,

View File

@ -0,0 +1,67 @@
use std::{collections::HashMap, sync::atomic::{AtomicUsize, Ordering}};
use tokio::{process::Command, sync::{Mutex, RwLock}};
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Proc {
msg: String,
finished: bool
}
lazy_static::lazy_static!(
static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new()));
);
static PROC_INC: AtomicUsize = AtomicUsize::new(0);
pub async fn add_proc(mut cmd: Command, msg: String) -> anyhow::Result<()> {
let mut proc = cmd.spawn()?;
let id = PROC_INC.fetch_add(1, Ordering::AcqRel);
tokio::spawn(async move {
let id = id;
proc.wait().await
.expect("child process encountered an error");
PROCESSES.lock().await.write().await.get_mut(&id).unwrap().finished = true;
});
PROCESSES.lock().await.write().await.insert(id, Proc {
finished: false,
msg,
});
Ok(())
}
/// Waits for processes to finish untill the proc count is lower or equal to `max`
pub async fn wait_for_procs_untill(max: usize) -> anyhow::Result<usize> {
// NOTE: This looks really fucked because i dont want to deadlock the processes so i lock PROCESSES for as little as possible
// NOTE: So its also kinda really slow
let mut finish_count = 0;
loop {
{
if PROCESSES.lock().await.read().await.len() <= max {
return Ok(finish_count);
}
}
let procs = {
PROCESSES.lock().await.read().await.clone()
};
for (idx, proc) in procs {
if proc.finished {
{
PROCESSES.lock().await.write().await.remove(&idx);
}
log::info!("{}", proc.msg);
finish_count += 1;
}
}
}
}