Added gui
This commit is contained in:
parent
9fef257bfc
commit
b977e9cea5
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,6 @@
|
|||
/out
|
||||
/music_mgr/target
|
||||
/music_mgr/config.json
|
||||
/music_mgr/manifest.json
|
||||
/.venv
|
||||
/config.json
|
2874
music_mgr/Cargo.lock
generated
2874
music_mgr/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "music_mgr"
|
||||
name = "mcmg"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -10,6 +10,8 @@ anstyle = "1.0.6"
|
|||
anyhow = "1.0.81"
|
||||
camino = "1.1.6"
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
eframe = "0.27.2"
|
||||
egui = "0.27.2"
|
||||
env_logger = "0.11.3"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2.153"
|
||||
|
|
4
music_mgr/manifest.default.json
Normal file
4
music_mgr/manifest.default.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"format": "m4a",
|
||||
"genres": {}
|
||||
}
|
|
@ -43,7 +43,7 @@ pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut
|
|||
|
||||
if should_download {
|
||||
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(())
|
||||
|
|
105
music_mgr/src/commands/gui/mod.rs
Normal file
105
music_mgr/src/commands/gui/mod.rs
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
27
music_mgr/src/commands/gui/nav_bar.rs
Normal file
27
music_mgr/src/commands/gui/nav_bar.rs
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
79
music_mgr/src/commands/gui/song_edit_window.rs
Normal file
79
music_mgr/src/commands/gui/song_edit_window.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
mod add;
|
||||
pub mod gui;
|
||||
|
||||
use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest};
|
||||
|
||||
|
||||
|
||||
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();
|
||||
match &cfg.cli.command {
|
||||
None | Some(CliCommand::Download) => {
|
||||
match (&cfg.cli.command, cfg.isatty) {
|
||||
(None | Some(CliCommand::Download), true) => {
|
||||
match downloader.download_all(manifest, &cfg).await {
|
||||
Ok(count) => log::info!("Downloaded {count} songs"),
|
||||
Err(e) => {
|
||||
|
@ -16,7 +20,7 @@ pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow
|
|||
}
|
||||
}
|
||||
},
|
||||
Some(c) => {
|
||||
(Some(c), _) => {
|
||||
match c {
|
||||
CliCommand::Download => unreachable!(),
|
||||
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}");
|
||||
}
|
||||
}
|
||||
CliCommand::Gui => {
|
||||
gui::Gui::start(manifest.clone())?;
|
||||
},
|
||||
}
|
||||
}
|
||||
(None, false) => {
|
||||
gui::Gui::start(manifest.clone())?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -25,9 +25,8 @@ pub struct CliArgs {
|
|||
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand, Default)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum CliCommand {
|
||||
#[default]
|
||||
Download,
|
||||
Add {
|
||||
#[arg(long, short)]
|
||||
|
@ -36,5 +35,6 @@ pub enum CliCommand {
|
|||
name: Option<String>,
|
||||
#[arg(long, short)]
|
||||
genre: Option<String>
|
||||
}
|
||||
},
|
||||
Gui
|
||||
}
|
||||
|
|
|
@ -20,14 +20,12 @@ lazy_static!(
|
|||
|
||||
pub struct Downloader {
|
||||
count: usize,
|
||||
id_itr: usize,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
count: 0,
|
||||
id_itr: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,10 +35,10 @@ impl Downloader {
|
|||
for (genre, songs) in manifest.get_genres() {
|
||||
for (song_name, song) in songs {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -91,51 +89,7 @@ impl Downloader {
|
|||
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
||||
};
|
||||
|
||||
let mut proc = cmd.spawn()?;
|
||||
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
|
||||
crate::process_manager::add_proc(cmd, format!("Downloaded {dl_file}")).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ mod commands;
|
|||
mod prompt;
|
||||
mod config;
|
||||
mod constants;
|
||||
mod process_manager;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
|
|
@ -8,6 +8,9 @@ use std::{collections::HashMap, fmt::{Debug, Display}, path::PathBuf};
|
|||
use anyhow::{bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
const DEFAULT_MANIFEST: &'static str = include_str!("../../manifest.default.json");
|
||||
|
||||
pub type GenreName = String;
|
||||
pub type SongName = String;
|
||||
pub type Genre = HashMap<SongName, song::Song>;
|
||||
|
@ -60,6 +63,13 @@ impl Manifest {
|
|||
pub fn get_genres_mut(&mut self) -> &mut HashMap<GenreName, Genre> {
|
||||
&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<()> {
|
||||
let path = p.unwrap_or(&self.path);
|
||||
log::debug!("Path: {path:?}");
|
||||
|
@ -79,6 +89,11 @@ impl Manifest {
|
|||
Ok(())
|
||||
}
|
||||
pub fn load_new(p: &PathBuf) -> Result<Self> {
|
||||
|
||||
if !p.exists() {
|
||||
std::fs::write(p, DEFAULT_MANIFEST)?;
|
||||
}
|
||||
|
||||
let mut s = Self::default();
|
||||
log::debug!("Path: {p:?}");
|
||||
s.path = p.clone();
|
||||
|
|
|
@ -11,6 +11,17 @@ pub enum SongType {
|
|||
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)]
|
||||
pub struct Song {
|
||||
url: String,
|
||||
|
|
67
music_mgr/src/process_manager.rs
Normal file
67
music_mgr/src/process_manager.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user