Added downloading capabilities in the gui

This commit is contained in:
Gvidas Juknevičius 2024-09-14 16:50:07 +03:00
parent 266b580df7
commit 52a55d8be2
Signed by: MCorange
GPG Key ID: 12B1346D720B7FBB
15 changed files with 2062 additions and 1323 deletions

1549
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,9 @@ camino = "1.1.6"
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
eframe = "0.27.2" eframe = "0.27.2"
egui = "0.27.2" egui = "0.27.2"
egui_extras = "0.27.2"
env_logger = "0.11.3" env_logger = "0.11.3"
futures = "0.3.30"
lazy_static = "1.4.0" lazy_static = "1.4.0"
libc = "0.2.153" libc = "0.2.153"
log = "0.4.21" log = "0.4.21"

File diff suppressed because it is too large Load Diff

View File

@ -4,19 +4,19 @@ use crate::{config::ConfigWrapper, downloader::Downloader, manifest::{song::Song
pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downloader, url: &Option<String>, name: &Option<String>, genre: &Option<String>) -> anyhow::Result<()> { pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downloader, url: &Option<String>, name: &Option<String>, playlist: &Option<String>) -> anyhow::Result<()> {
log::debug!("Genre: {genre:?}"); log::debug!("Playlist: {playlist:?}");
log::debug!("url: {url:?}"); log::debug!("url: {url:?}");
log::debug!("name: {name:?}"); log::debug!("name: {name:?}");
let mut genres = manifest.get_playlists().keys().map(|f| f.clone()).collect::<Vec<String>>(); let mut playlists = manifest.get_playlists().keys().map(|f| f.clone()).collect::<Vec<String>>();
genres.sort(); playlists.sort();
let genre = genre.clone().unwrap_or_else( || { let playlist = playlist.clone().unwrap_or_else( || {
let g = crate::prompt::prompt_with_list_or_str("Enter song genre", &genres); let g = crate::prompt::prompt_with_list_or_str("Enter song playlist", &playlists);
log::info!("Genre: {g}"); log::info!("Playlist: {g}");
g g
}); });
@ -36,14 +36,14 @@ pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut
); );
let song = Song::from_url_str(url)?; let song = Song::from_url_str(url)?;
manifest.add_song(genre.clone(), name.clone(), song.clone()); manifest.add_song(playlist.clone(), name.clone(), song.clone());
manifest.save(None)?; manifest.save(None)?;
let should_download = crate::prompt::prompt_bool("Download song now?", Some(false)); let should_download = crate::prompt::prompt_bool("Download song now?", Some(false));
if should_download { if should_download {
downloader.download_song(cfg, &name, &song, &genre, manifest.get_format()).await?; downloader.download_song(cfg, &name, &song, &playlist, manifest.get_format())?;
crate::process_manager::wait_for_procs_untill(0).await?; crate::process_manager::wait_for_procs_untill(0)?;
} }
Ok(()) Ok(())

View File

@ -1,33 +1,44 @@
mod nav_bar; mod nav_bar;
mod song_edit_window; mod song_edit_window;
use egui::{Color32, Label, Sense};
use crate::manifest::Manifest; use egui::{Color32, Label, Sense};
use egui_extras::{Column, TableBuilder};
use crate::{config::{Config, ConfigWrapper}, downloader::Downloader, manifest::{song::SongType, Manifest}};
use self::song_edit_window::GuiSongEditor; use self::song_edit_window::GuiSongEditor;
#[derive(Debug)]
pub struct Gui { pub struct Gui {
manifest: Manifest, manifest: Manifest,
song_editor: GuiSongEditor song_editor: GuiSongEditor,
filter: String,
downloader: Downloader,
cfg: ConfigWrapper
} }
impl Gui { impl Gui {
fn new(_: &eframe::CreationContext<'_>, manifest: Manifest) -> Self { fn new(_: &eframe::CreationContext<'_>, manifest: Manifest, downloader: Downloader, cfg: ConfigWrapper) -> Self {
Self { Self {
manifest, manifest,
song_editor: GuiSongEditor { song_editor: GuiSongEditor {
is_new_open: false,
is_open: false, is_open: false,
song: Default::default(), song: Default::default(),
ed_url: String::new(), ed_url: String::new(),
ed_name: String::new(), ed_name: String::new(),
ed_playlist: Some(String::new()),
ed_type: SongType::Youtube
}, },
filter: String::new(),
downloader,
cfg,
} }
} }
pub fn start(manifest: Manifest) -> anyhow::Result<()> { pub fn start(manifest: Manifest, downloader: Downloader, cfg: ConfigWrapper) -> anyhow::Result<()> {
let native_options = eframe::NativeOptions { let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0]) .with_inner_size([400.0, 300.0])
@ -43,7 +54,7 @@ impl Gui {
if let Err(e) = eframe::run_native( if let Err(e) = eframe::run_native(
"eframe template", "eframe template",
native_options, native_options,
Box::new(|cc| Box::new(Gui::new(cc, manifest))), Box::new(|cc| Box::new(Gui::new(cc, manifest, downloader, cfg))),
) { ) {
log::error!("Failed to create window: {e}"); log::error!("Failed to create window: {e}");
}; };
@ -56,46 +67,129 @@ impl eframe::App for Gui {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.draw_nav(ctx, frame); self.draw_nav(ctx, frame);
self.draw_song_edit_window(ctx, frame); self.draw_song_edit_window(ctx, frame);
self.draw_new_song_window(ctx, frame);
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
// The central panel the region left after adding TopPanel's and SidePanel's // The central panel the region left after adding TopPanel's and SidePanel's
ui.heading(format!("Songs ({})", self.manifest.get_song_count())); //ui.heading(format!("Songs ({})", self.manifest.get_song_count()));
let fltr_by;
let filter_clean;
if self.filter.starts_with("playlist:") {
fltr_by = "playlist";
filter_clean = self.filter.strip_prefix("playlist:").unwrap_or("").to_string().to_lowercase();
} else if self.filter.starts_with("source:") {
fltr_by = "source";
filter_clean = self.filter.strip_prefix("source:").unwrap_or("").to_string().to_lowercase();
} else if self.filter.starts_with("url:") {
fltr_by = "url";
filter_clean = self.filter.strip_prefix("url:").unwrap_or("").to_string();
} else {
fltr_by = "";
filter_clean = self.filter.clone();
}
egui::ScrollArea::vertical() ui.vertical(|ui| {
.max_width(f32::INFINITY) ui.horizontal(|ui| {
.auto_shrink(false) ui.colored_label(Color32::BLUE, "Filter: ");
.show(ui, |ui| { ui.text_edit_singleline(&mut self.filter);
for (genre, songs) in self.manifest.get_playlists() { });
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.vertical(|ui| {
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.resizable(true)
.column(Column::auto())
.column(Column::auto())
//.column(
// Column::remainder()
// .at_least(40.0)
// .clip(true)
// .resizable(true),
//)
.column(Column::auto())
.column(Column::remainder())
//.column(Column::remainder())
.min_scrolled_height(0.0)
.max_scroll_height(available_height)
.sense(egui::Sense::click());
let playlists = self.manifest.get_playlists().clone();
let songs = {
let mut songs = Vec::new();
for (pname, p) in playlists {
for (sname, s) in p {
songs.push((pname.clone(), sname, s))
}
}
songs
};
table.header(20.0, |mut header| {
header.col(|_|{});
header.col(|ui| {
ui.strong("Playlist");
});
header.col(|ui| {
ui.strong("Source");
});
header.col(|ui| {
ui.strong("Name");
});
}).body(|mut body| {
for (pname, sname, s) in songs {
if fltr_by == "playlist" && !filter_clean.is_empty() {
if !pname.to_lowercase().contains(&filter_clean) {
continue;
}
} else if fltr_by == "type" && !filter_clean.is_empty(){
if !s.get_type().to_string().to_lowercase().contains(&filter_clean) {
continue;
}
} else if fltr_by == "url" && !filter_clean.is_empty(){
if !s.get_url_str().contains(&filter_clean) {
continue;
}
} else if !filter_clean.is_empty() {
if !sname.to_lowercase().contains(&filter_clean) {
continue;
}
}
body.row(18.0, |mut row| {
row.col(|ui| {
if ui.add(Label::new("[edit]").sense(Sense::click())).clicked() {
self.song_editor.song = (
pname.clone(),
sname.clone(),
);
log::debug!("Label pressed");
self.song_editor.is_open = true;
self.song_editor.ed_name = sname.clone();
self.song_editor.ed_url = s.get_url_str().clone();
}
});
row.col(|ui| {
ui.label(pname.clone());
});
row.col(|ui| {
ui.label(s.get_type().to_string());
});
row.col(|ui| {
ui.hyperlink_to(sname.clone(), s.get_url_str());
});
})
}
});
});
ui.separator(); 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| { ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
egui::warn_if_debug_build(ui); egui::warn_if_debug_build(ui);

View File

@ -1,3 +1,5 @@
use egui::Hyperlink;
use super::Gui; use super::Gui;
@ -8,20 +10,45 @@ impl Gui {
// The top panel is often a good place for a menu bar: // The top panel is often a good place for a menu bar:
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() { if ui.button("Source").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.open_url(egui::OpenUrl::new_tab("https://git.mcorangehq.xyz/XOR64/music"));
} }
if ui.button("Save").clicked() { if ui.button("Save").clicked() {
if let Err(e) = self.manifest.save(None) { if let Err(e) = self.manifest.save(None) {
log::error!("Failed to save manifest: {e}"); log::error!("Failed to save manifest: {e}");
} }
} }
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.menu_button("Song", |ui| {
if ui.button("Add New").clicked() {
log::debug!("NEW SONG");
self.song_editor.is_new_open = true;
}
});
ui.menu_button("Downloader", |ui| {
if ui.button("Download All").clicked() {
if let Err(e) = self.downloader.download_all_nb(&self.manifest, &self.cfg) {
log::error!("Err: {e}");
}
}
}); });
ui.add_space(16.0); ui.add_space(16.0);
ui.with_layout(egui::Layout::bottom_up(egui::Align::RIGHT), |ui| { ui.with_layout(egui::Layout::bottom_up(egui::Align::RIGHT), |ui| {
egui::widgets::global_dark_light_mode_buttons(ui); ui.horizontal(|ui| {
if self.downloader.get_songs_left_nb() > 0 {
ui.label(format!("Downloading: {}/{}", self.downloader.get_songs_left_nb(), self.downloader.get_initial_song_count_nb()));
}
let _ = self.downloader.download_all_nb_poll(&self.cfg);
egui::widgets::global_dark_light_mode_buttons(ui);
});
}); });
}); });
}); });
} }
} }

View File

@ -1,28 +1,34 @@
use egui::Color32; use egui::{Color32, TextBuffer};
use crate::manifest::{GenreName, SongName};
use crate::manifest::{playlist::Playlist, song::{Song, SongType}};
use super::Gui; use super::Gui;
#[derive(Debug)]
pub struct GuiSongEditor { pub struct GuiSongEditor {
pub is_open: bool, pub is_open: bool,
pub song: (GenreName, SongName), pub is_new_open: bool,
pub song: (String, String),
pub ed_url: String, pub ed_url: String,
pub ed_name: String, pub ed_name: String,
pub ed_playlist: Option<String>,
pub ed_type: SongType,
} }
impl Gui { impl Gui {
pub fn draw_song_edit_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { pub fn draw_song_edit_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
let mut save = false; let mut save = false;
let (genre, song_name) = self.song_editor.song.clone(); let (playlist, song_name) = self.song_editor.song.clone();
let Some(song) = self.manifest.get_song(genre.clone(), &song_name) else { let Some(song) = self.manifest.get_song(playlist.clone(), &song_name) else {
return; return;
}; };
let song = song.clone(); let song = song.clone();
egui::Window::new("Song editor") egui::Window::new("Song editor")
.open(&mut self.song_editor.is_open) .open(&mut self.song_editor.is_open)
.show(ctx, .show(ctx,
@ -33,7 +39,7 @@ impl Gui {
ui.label("["); ui.label("[");
ui.hyperlink_to("link", song.get_url().unwrap()); ui.hyperlink_to("link", song.get_url().unwrap());
ui.label("] "); ui.label("] ");
ui.colored_label(Color32::LIGHT_BLUE, &genre); ui.colored_label(Color32::LIGHT_BLUE, &playlist);
ui.label(": "); ui.label(": ");
ui.label(&song_name) ui.label(&song_name)
}); });
@ -59,21 +65,81 @@ impl Gui {
if save { if save {
{ {
let Some(song) = self.manifest.get_song_mut(genre.clone(), &song_name) else { let Some(song) = self.manifest.get_song_mut(playlist.clone(), &song_name) else {
return; return;
}; };
*song.get_url_str_mut() = self.song_editor.ed_url.clone(); *song.get_url_str_mut() = self.song_editor.ed_url.clone();
} }
let Some(genre) = self.manifest.get_playlist_mut(genre.clone()) else { let Some(playlist) = self.manifest.get_playlist_mut(playlist.clone()) else {
return; return;
}; };
genre.remove(&song_name); playlist.remove_song(&song_name);
genre.insert(self.song_editor.ed_name.clone(), song); playlist.add_song(self.song_editor.ed_name.clone(), song);
let _ = self.manifest.save(None); let _ = self.manifest.save(None);
} }
} }
pub fn draw_new_song_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
let mut save = false;
egui::Window::new("New song")
.open(&mut self.song_editor.is_new_open)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Type: ");
egui::ComboBox::from_id_source("new_song_window_type")
.selected_text(format!("{:?}", self.song_editor.ed_type))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.song_editor.ed_type, SongType::Youtube, "Youtube");
ui.selectable_value(&mut self.song_editor.ed_type, SongType::Spotify, "Spotify");
ui.selectable_value(&mut self.song_editor.ed_type, SongType::Soundcloud, "Soundcloud");
}
);
});
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut self.song_editor.ed_name);
});
ui.horizontal(|ui| {
ui.label("Playlist: ");
egui::ComboBox::from_id_source("new_song_window_playlist")
.selected_text(format!("{}", self.song_editor.ed_playlist.clone().unwrap()))
.show_ui(ui, |ui| {
for p in self.manifest.get_playlists().keys() {
ui.selectable_value(&mut self.song_editor.ed_playlist, Option::Some(p.clone()), p.as_str());
}
}
);
});
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(playlist) = self.manifest.get_playlist_mut(self.song_editor.ed_playlist.clone().unwrap()) else {
panic!("couldnt find playlist from a preset playlist list????????????");
};
playlist.add_song(
self.song_editor.ed_name.clone(),
Song::from_url_str(self.song_editor.ed_url.clone()).unwrap().set_type(self.song_editor.ed_type.clone()).clone()
);
let _ = self.manifest.save(None);
save = false;
self.song_editor.is_new_open = false;
}
}
} }

View File

@ -23,20 +23,20 @@ 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, playlist } => {
if let Err(e) = add::add(cfg, manifest, &mut downloader, url, name, genre).await { if let Err(e) = add::add(cfg, manifest, &mut downloader, url, name, playlist).await {
log::error!("Failed to run 'add' command: {e}"); log::error!("Failed to run 'add' command: {e}");
} }
} }
CliCommand::Gui => { CliCommand::Gui => {
gui::Gui::start(manifest.clone())?; gui::Gui::start(manifest.clone(), downloader, cfg.clone())?;
}, },
} }
} }
(None, false) => { (None, false) => {
gui::Gui::start(manifest.clone())?; gui::Gui::start(manifest.clone(), downloader, cfg.clone())?;
}, },
} }
Ok(()) Ok(())
} }

View File

@ -2,7 +2,7 @@ use camino::Utf8PathBuf;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
#[derive(Debug, Parser, Default)] #[derive(Debug, Parser, Default, Clone)]
pub struct CliArgs { pub struct CliArgs {
/// Show more info /// Show more info
#[arg(long, short)] #[arg(long, short)]
@ -25,7 +25,7 @@ pub struct CliArgs {
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand, Clone)]
pub enum CliCommand { pub enum CliCommand {
Download, Download,
Add { Add {
@ -34,7 +34,7 @@ pub enum CliCommand {
#[arg(long, short)] #[arg(long, short)]
name: Option<String>, name: Option<String>,
#[arg(long, short)] #[arg(long, short)]
genre: Option<String> playlist: Option<String>
}, },
Gui Gui
} }

View File

@ -12,25 +12,25 @@ use self::cli::CliArgs;
// const YTDLP_DL_URL: &'static str = "https://github.com/yt-dlp/yt-dlp/archive/refs/heads/master.zip"; // const YTDLP_DL_URL: &'static str = "https://github.com/yt-dlp/yt-dlp/archive/refs/heads/master.zip";
// const SPOTDL_DL_URL: &'static str = "https://github.com/spotDL/spotify-downloader/archive/refs/heads/master.zip"; // const SPOTDL_DL_URL: &'static str = "https://github.com/spotDL/spotify-downloader/archive/refs/heads/master.zip";
#[derive(Debug, Default)] #[derive(Debug, Default, Clone)]
pub struct ConfigWrapper { pub struct ConfigWrapper {
pub cfg: Config, pub cfg: Config,
pub cli: cli::CliArgs, pub cli: cli::CliArgs,
pub isatty: bool pub isatty: bool
} }
#[derive(Debug, Serialize, Deserialize, Default)] #[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Config { pub struct Config {
pub ytdlp: ConfigYtdlp, pub ytdlp: ConfigYtdlp,
pub spotdl: ConfigSpotdl, pub spotdl: ConfigSpotdl,
} }
#[derive(Debug, Serialize, Deserialize, Default)] #[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct ConfigYtdlp { pub struct ConfigYtdlp {
pub path: PathBuf, pub path: PathBuf,
} }
#[derive(Debug, Serialize, Deserialize, Default)] #[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct ConfigSpotdl { pub struct ConfigSpotdl {
pub path: PathBuf, pub path: PathBuf,
} }

View File

@ -18,32 +18,66 @@ lazy_static!(
static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new())); static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new()));
); );
#[derive(Debug, Default)]
pub struct Downloader { pub struct Downloader {
count: usize, count: usize,
nb_initial_song_count: usize,
nb_cache: Vec<(String, String, Song, Format)>
} }
impl Downloader { impl Downloader {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
count: 0, ..Default::default()
} }
} }
pub fn get_initial_song_count_nb(&self) -> usize {
self.nb_initial_song_count
}
pub fn get_songs_left_nb(&self) -> usize {
self.nb_cache.len()
}
pub fn download_all_nb(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<usize> {
for (pname, playlist) in manifest.get_playlists() {
for (sname, song) in playlist.get_songs() {
self.nb_cache.push((pname.clone(), sname.clone(), song.clone(), manifest.get_format().clone()));
}
}
self.nb_initial_song_count = self.nb_cache.len();
self.download_all_nb_poll(cfg)
}
pub fn download_all_nb_poll(&mut self, cfg: &ConfigWrapper) -> anyhow::Result<usize> {
if !crate::process_manager::is_proc_queue_full(10) {
if let Some((pname, sname, song, format)) = self.nb_cache.pop() {
self.download_song(cfg, &sname, &song, &pname, &format)?;
}
}
Ok(crate::process_manager::purge_done_procs())
}
pub async fn download_all(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<usize> { pub async fn download_all(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<usize> {
let format = manifest.get_format(); let format = manifest.get_format();
for (genre, songs) in manifest.get_playlists() { for (name, playlist) in manifest.get_playlists() {
for (song_name, song) in songs { for (song_name, song) in playlist.get_songs() {
self.download_song(cfg, song_name, song, &genre, format).await?; self.download_song(cfg, song_name, song, &name, format)?;
self.count += crate::process_manager::wait_for_procs_untill(10).await?; self.count += crate::process_manager::wait_for_procs_untill(10)?;
} }
} }
self.count += crate::process_manager::wait_for_procs_untill(0).await?; self.count += crate::process_manager::wait_for_procs_untill(0)?;
Ok(self.count) Ok(self.count)
} }
pub async fn download_song(&mut self, cfg: &ConfigWrapper, name: &String, song: &Song, genre: &String, format: &Format) -> anyhow::Result<()> { pub fn download_song(&mut self, cfg: &ConfigWrapper, name: &String, song: &Song, playlist: &String, format: &Format) -> anyhow::Result<()> {
let dl_dir = format!("{}/{genre}", cfg.cli.output); let dl_dir = format!("{}/{playlist}", cfg.cli.output);
let dl_file = format!("{dl_dir}/{}.{}", name, &format); let dl_file = format!("{dl_dir}/{}.{}", name, &format);
if PathBuf::from(&dl_file).exists() { if PathBuf::from(&dl_file).exists() {
@ -89,7 +123,7 @@ impl Downloader {
cmd.stdout(Stdio::null()).stderr(Stdio::null()); cmd.stdout(Stdio::null()).stderr(Stdio::null());
}; };
crate::process_manager::add_proc(cmd, format!("Downloaded {dl_file}")).await?; crate::process_manager::add_proc(cmd, format!("Downloaded {dl_file}"))?;
Ok(()) Ok(())
} }
} }

View File

@ -1,6 +1,7 @@
// pub mod v1; // pub mod v1;
pub mod song; pub mod song;
pub mod playlist;
use song::Song; use song::Song;
use std::{collections::HashMap, fmt::{Debug, Display}, path::PathBuf}; use std::{collections::HashMap, fmt::{Debug, Display}, path::PathBuf};
@ -11,7 +12,7 @@ use serde::{Deserialize, Serialize};
const DEFAULT_MANIFEST: &'static str = include_str!("../../manifest.default.json"); const DEFAULT_MANIFEST: &'static str = include_str!("../../manifest.default.json");
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>;
@ -31,36 +32,39 @@ pub struct Manifest {
#[serde(skip)] #[serde(skip)]
path: PathBuf, path: PathBuf,
format: Format, format: Format,
playlists: HashMap<GenreName, Genre> playlists: HashMap<String, playlist::Playlist>,
} }
#[allow(dead_code)] #[allow(dead_code)]
impl Manifest { impl Manifest {
pub fn get_format(&self) -> &Format { pub fn get_format(&self) -> &Format {
&self.format &self.format
} }
pub fn add_song(&mut self, genre: GenreName, name: SongName, song: Song) -> Option<Song> { pub fn add_song(&mut self, playlist_name: String, name: SongName, song: Song) -> Option<Song> {
self.get_playlist_mut(genre)?.insert(name, song) self.get_playlist_mut(playlist_name)?.add_song(name, song)
} }
pub fn get_song(&self, genre: GenreName, name: &SongName) -> Option<&Song> { pub fn get_song(&self, playlist_name: String, name: &String) -> Option<&Song> {
self.get_playlist(genre)?.get(name) self.get_playlist(playlist_name)?.get_song(name)
} }
pub fn get_song_mut(&mut self, genre: GenreName, name: &SongName) -> Option<&mut Song> { pub fn get_song_mut(&mut self, playlist_name: String, name: &String) -> Option<&mut Song> {
self.get_playlist_mut(genre)?.get_mut(name) self.get_playlist_mut(playlist_name)?.get_song_mut(name)
} }
pub fn add_playlist(&mut self, name: GenreName) { pub fn add_playlist(&mut self, playlist_name: String) {
self.playlists.insert(name, Default::default()); self.playlists.insert(playlist_name, Default::default());
} }
pub fn get_playlist(&self, name: GenreName) -> Option<&Genre> { pub fn get_playlist(&self, playlist_name: String) -> Option<&playlist::Playlist> {
self.playlists.get(&name) self.playlists.get(&playlist_name)
} }
pub fn get_playlist_mut(&mut self, name: GenreName) -> Option<&mut Genre> { pub fn get_playlist_mut(&mut self, playlist_name: String) -> Option<&mut playlist::Playlist> {
self.playlists.get_mut(&name) self.playlists.get_mut(&playlist_name)
} }
pub fn get_playlists(&self) -> &HashMap<GenreName, Genre> { pub fn get_playlists(&self) -> &HashMap<String, playlist::Playlist> {
&self.playlists &self.playlists
} }
pub fn get_playlists_mut(&mut self) -> &mut HashMap<GenreName, Genre> { pub fn get_playlists_mut(&mut self) -> &mut HashMap<String, playlist::Playlist> {
&mut self.playlists &mut self.playlists
} }
pub fn get_song_count(&self) -> usize { pub fn get_song_count(&self) -> usize {

56
src/manifest/playlist.rs Normal file
View File

@ -0,0 +1,56 @@
use egui::ahash::HashMap;
use serde::{Deserialize, Serialize};
use super::song::Song;
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Playlist {
songs: HashMap<String, Song>
}
impl Playlist {
pub fn new() -> Self {
Self { ..Default::default() }
}
pub fn add_song(&mut self, name: String, song: Song) -> Option<Song> {
self.songs.insert(name, song)
}
pub fn remove_song(&mut self, name: &String) -> Option<Song> {
self.songs.remove(name)
}
pub fn get_song(&self, name: &String) -> Option<&Song> {
self.songs.get(name)
}
pub fn get_songs(&self) -> &HashMap<String, Song> {
&self.songs
}
pub fn get_songs_mut(&mut self) -> &mut HashMap<String, Song> {
&mut self.songs
}
pub fn get_song_mut(&mut self, name: &String) -> Option<&mut Song> {
self.songs.get_mut(name)
}
pub fn len(&self) -> usize {
self.songs.len()
}
}
impl IntoIterator for Playlist {
type Item = (String, Song);
type IntoIter = std::collections::hash_map::IntoIter<String, Song>;
fn into_iter(self) -> Self::IntoIter {
self.songs.into_iter()
}
}

View File

@ -4,7 +4,7 @@ use anyhow::{bail, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum SongType { pub enum SongType {
Youtube, Youtube,
Spotify, Spotify,
@ -33,13 +33,17 @@ impl Song {
pub fn from_url_str(url: String) -> Result<Self> { pub fn from_url_str(url: String) -> Result<Self> {
Self::from_url(url::Url::from_str(url.as_str())?) Self::from_url(url::Url::from_str(url.as_str())?)
} }
pub fn from_url(url: url::Url) -> Result<Self> { pub fn from_url(url: url::Url) -> Result<Self> {
Ok(Self { Ok(Self {
url: url.to_string(), url: url.to_string(),
typ: url.try_into()?, typ: url.try_into()?,
}) })
} }
pub fn set_type(&mut self, typ: SongType) -> &mut Self {
self.typ = typ;
self
}
pub fn get_url(&self) -> Result<url::Url> { pub fn get_url(&self) -> Result<url::Url> {
Ok(url::Url::from_str(&self.url)?) Ok(url::Url::from_str(&self.url)?)
} }

View File

@ -1,6 +1,6 @@
use std::{collections::HashMap, sync::atomic::{AtomicUsize, Ordering}}; use std::{collections::HashMap, sync::{atomic::{AtomicUsize, Ordering}, Mutex, RwLock}};
use tokio::{process::Command, sync::{Mutex, RwLock}}; use tokio::process::Command;
@ -19,7 +19,7 @@ static PROC_INC: AtomicUsize = AtomicUsize::new(0);
pub async fn add_proc(mut cmd: Command, msg: String) -> anyhow::Result<()> { pub fn add_proc(mut cmd: Command, msg: String) -> anyhow::Result<()> {
let mut proc = cmd.spawn()?; let mut proc = cmd.spawn()?;
let id = PROC_INC.fetch_add(1, Ordering::AcqRel); let id = PROC_INC.fetch_add(1, Ordering::AcqRel);
@ -27,10 +27,10 @@ pub async fn add_proc(mut cmd: Command, msg: String) -> anyhow::Result<()> {
let id = id; let id = id;
proc.wait().await proc.wait().await
.expect("child process encountered an error"); .expect("child process encountered an error");
PROCESSES.lock().await.write().await.get_mut(&id).unwrap().finished = true; PROCESSES.lock().unwrap().write().unwrap().get_mut(&id).unwrap().finished = true;
}); });
PROCESSES.lock().await.write().await.insert(id, Proc { PROCESSES.lock().unwrap().write().unwrap().insert(id, Proc {
finished: false, finished: false,
msg, msg,
}); });
@ -38,30 +38,39 @@ pub async fn add_proc(mut cmd: Command, msg: String) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
pub fn is_proc_queue_full(max: usize) -> bool {
let proc_cnt = PROCESSES.lock().unwrap().read().unwrap().len();
proc_cnt >= max
}
pub fn purge_done_procs() -> usize {
let mut finish_count = 0;
let procs = {
PROCESSES.lock().unwrap().read().unwrap().clone()
};
for (idx, proc) in procs {
if proc.finished {
{
PROCESSES.lock().unwrap().write().unwrap().remove(&idx);
}
log::info!("{}", proc.msg);
finish_count += 1;
}
}
finish_count
}
/// Waits for processes to finish untill the proc count is lower or equal to `max` /// 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> { pub 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: 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 // NOTE: So its also kinda really slow
let mut finish_count = 0; let mut finish_count = 0;
loop { loop {
{ if !is_proc_queue_full(max) {
if PROCESSES.lock().await.read().await.len() <= max { return Ok(finish_count);
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;
}
} }
finish_count += purge_done_procs();
} }
} }