Compare commits
6 Commits
266b580df7
...
0.0.2b
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e70d38f7f | |||
|
affafed441
|
|||
|
7d6d560d2b
|
|||
|
776a88c4cf
|
|||
|
29c7e452b0
|
|||
|
52a55d8be2
|
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker="aarch64-linux-gnu-gcc"
|
||||
2031
Cargo.lock
generated
2031
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -12,11 +12,15 @@ camino = "1.1.6"
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
eframe = "0.27.2"
|
||||
egui = "0.27.2"
|
||||
egui_extras = "0.27.2"
|
||||
env_logger = "0.11.3"
|
||||
futures = "0.3.30"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2.153"
|
||||
log = "0.4.21"
|
||||
reqwest = "0.12.3"
|
||||
notify-rust = "4.11.3"
|
||||
open = "5.3.0"
|
||||
reqwest = { version = "0.12.3", features = ["h2", "http2", "rustls-tls"], default-features = false }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.115"
|
||||
# serde_traitobject = "0.2.8"
|
||||
|
||||
1282
manifest.json
1282
manifest.json
File diff suppressed because it is too large
Load Diff
18
scripts/build-release.sh
Executable file
18
scripts/build-release.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
echo "Please suppy a version: 0.0.0[a | b | rc-0]"
|
||||
exit
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
cargo build --release --target x86_64-unknown-linux-gnu
|
||||
cargo build --release --target aarch64-unknown-linux-gnu
|
||||
|
||||
cp ./target/x86_64-pc-windows-gnu/release/mcmg.exe ./target/mcmg_win32.exe
|
||||
cp ./target/x86_64-unknown-linux-gnu/release/mcmg ./target/mcmg_linux_x86_64
|
||||
cp ./target/aarch64-unknown-linux-gnu/release/mcmg ./target/mcmg_linux_aarch64
|
||||
cp ./scripts/setup-template.sh "./target/mcmg-setup-$1.sh"
|
||||
cp ./scripts/setup-template.ps1 "./target/mcmg-setup-$1.ps1"
|
||||
23
scripts/setup-template.ps1
Normal file
23
scripts/setup-template.ps1
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
$MyInvocation.MyCommand.Name -match '([0-9]+\.[0-9]+\.[0-9]+([ab]|(rc[-]*[0-9]*)))'
|
||||
$Ver = $Matches[1]
|
||||
|
||||
|
||||
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
|
||||
winget install Gyan.FFmpeg
|
||||
}
|
||||
|
||||
if (-not (Get-Command "yt-dlp" -ErrorAction SilentlyContinue)) {
|
||||
winget install "yt-dlp.yt-dlp"
|
||||
}
|
||||
|
||||
if (-not (Get-Command spotdl -ErrorAction SilentlyContinue)) {
|
||||
if (-not (Get-Command python -ErrorAction SilentlyContinue)) {
|
||||
winget install "Python.Python.3.12"
|
||||
}
|
||||
python -m pip install spotdl
|
||||
}
|
||||
|
||||
$url = "https://git.mcorangehq.xyz/XOR64/music/releases/download/$Ver/mcmg_win32.exe"
|
||||
|
||||
Invoke-WebRequest -Uri $url -OutFile "mcmg.exe"
|
||||
42
scripts/setup-template.sh
Executable file
42
scripts/setup-template.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
PROG_VER=$(echo $0 | grep -o -E "[0-9]+\.[0-9]+\.[0-9]+([ab]|(rc[-]*[0-9]*))")
|
||||
|
||||
echo $PROG_VER
|
||||
|
||||
function cmd_exists() {
|
||||
if ! command -v $1 &> /dev/null
|
||||
then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
if cmd_exists "pacman"; then
|
||||
if cmd_exists "yay"; then
|
||||
yay -Sy --needed ffmpeg yt-dlp spotdl curl
|
||||
else
|
||||
sudo pacman -Sy --needed ffmpeg yt-dlp python python-pip python-pipx curl
|
||||
pipx install spotdl
|
||||
fi
|
||||
fi
|
||||
|
||||
if cmd_exists "apt"; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3 python3-pip ffmpeg curl
|
||||
|
||||
# updates all python packages, uncomment if you get errors for packages
|
||||
# pip3 freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip3 install -U
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install spotdl
|
||||
python3 -m pip install yt-dlp
|
||||
fi
|
||||
|
||||
curl "https://git.mcorangehq.xyz/XOR64/music/releases/download/${PROG_VER}/mcmg_linux_x86_64" -o mcmg
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::bail;
|
||||
|
||||
use crate::{config::ConfigWrapper, downloader::Downloader, manifest::{song::Song, Manifest}, util::is_supported_host};
|
||||
|
||||
|
||||
|
||||
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: &String, name: &String, playlist: &String) -> anyhow::Result<()> {
|
||||
|
||||
log::debug!("Genre: {genre:?}");
|
||||
log::debug!("url: {url:?}");
|
||||
log::debug!("name: {name:?}");
|
||||
let mut playlists = manifest.get_playlists().keys().map(|f| f.clone()).collect::<Vec<String>>();
|
||||
|
||||
let mut genres = manifest.get_playlists().keys().map(|f| f.clone()).collect::<Vec<String>>();
|
||||
|
||||
genres.sort();
|
||||
|
||||
let genre = genre.clone().unwrap_or_else( || {
|
||||
let g = crate::prompt::prompt_with_list_or_str("Enter song genre", &genres);
|
||||
log::info!("Genre: {g}");
|
||||
g
|
||||
});
|
||||
|
||||
|
||||
let url = url.clone().unwrap_or_else( ||
|
||||
crate::prompt::simple_prompt("Enter song youtube url, make sure its not a playlist, (yt only for now)")
|
||||
);
|
||||
playlists.sort();
|
||||
|
||||
if !is_supported_host(url::Url::from_str(&url)?) {
|
||||
log::error!("Invalid or unsupported host name");
|
||||
@@ -31,20 +18,38 @@ pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut
|
||||
}
|
||||
|
||||
|
||||
let name = name.clone().unwrap_or_else( ||
|
||||
crate::prompt::simple_prompt("Enter song name with like this: {Author} - {Song name}")
|
||||
);
|
||||
|
||||
let song = Song::from_url_str(url)?;
|
||||
manifest.add_song(genre.clone(), name.clone(), song.clone());
|
||||
let song = Song::from_url_str(url.clone())?;
|
||||
manifest.add_song(playlist, name.clone(), song.clone());
|
||||
manifest.save(None)?;
|
||||
|
||||
let should_download = crate::prompt::prompt_bool("Download song now?", Some(false));
|
||||
|
||||
if should_download {
|
||||
downloader.download_song(cfg, &name, &song, &genre, manifest.get_format()).await?;
|
||||
crate::process_manager::wait_for_procs_untill(0).await?;
|
||||
downloader.download_song(cfg, &name, &song, &playlist, manifest.get_format())?;
|
||||
crate::process_manager::wait_for_procs_untill(0)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_playlist(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downloader, url: &String, name: &String) -> anyhow::Result<()> {
|
||||
let songs = downloader.download_playlist_nb(cfg, url, name, manifest.get_format())?;
|
||||
|
||||
if manifest.get_playlist(name).is_some() {
|
||||
log::error!("Playlist {name} already exists");
|
||||
bail!("")
|
||||
}
|
||||
|
||||
manifest.add_playlist(name.clone());
|
||||
|
||||
let playlist = manifest.get_playlist_mut(name).expect("Unreachable");
|
||||
|
||||
for (sname, song) in songs {
|
||||
playlist.add_song(sname, song);
|
||||
}
|
||||
manifest.save(None)?;
|
||||
|
||||
while downloader.download_all_nb_poll(cfg)?.is_some() {};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
mod nav_bar;
|
||||
mod song_edit_window;
|
||||
|
||||
use egui::{Color32, Label, Sense};
|
||||
|
||||
use crate::manifest::Manifest;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use egui::{Button, Color32, Label, RichText, Sense};
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
use song_edit_window::{GuiError, GuiImportPlaylist, GuiNewSong};
|
||||
|
||||
use crate::{config::{Config, ConfigWrapper}, downloader::Downloader, manifest::{song::{Song, SongType}, Manifest}};
|
||||
|
||||
use self::song_edit_window::GuiSongEditor;
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Gui {
|
||||
manifest: Manifest,
|
||||
song_editor: GuiSongEditor
|
||||
song_edit_w: GuiSongEditor,
|
||||
new_song_w: GuiNewSong,
|
||||
import_playlist_w: GuiImportPlaylist,
|
||||
error_w: GuiError,
|
||||
filter: String,
|
||||
downloader: Downloader,
|
||||
cfg: ConfigWrapper,
|
||||
downloading: bool,
|
||||
}
|
||||
|
||||
impl Gui {
|
||||
fn new(_: &eframe::CreationContext<'_>, manifest: Manifest) -> Self {
|
||||
fn new(_: &eframe::CreationContext<'_>, manifest: Manifest, downloader: Downloader, cfg: ConfigWrapper) -> Self {
|
||||
Self {
|
||||
manifest,
|
||||
song_editor: GuiSongEditor {
|
||||
is_open: false,
|
||||
song: Default::default(),
|
||||
ed_url: String::new(),
|
||||
ed_name: String::new(),
|
||||
},
|
||||
downloader,
|
||||
cfg,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(manifest: Manifest) -> anyhow::Result<()> {
|
||||
pub fn start(manifest: Manifest, downloader: Downloader, cfg: ConfigWrapper) -> anyhow::Result<()> {
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([400.0, 300.0])
|
||||
@@ -43,59 +51,200 @@ impl Gui {
|
||||
if let Err(e) = eframe::run_native(
|
||||
"eframe template",
|
||||
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}");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn throw_error(&mut self, text: impl ToString) {
|
||||
self.error_w.is_open = true;
|
||||
self.error_w.text = text.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
self.draw_new_song_window(ctx, frame);
|
||||
self.draw_import_playlist_window(ctx, frame);
|
||||
self.draw_error_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()));
|
||||
//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()
|
||||
.max_width(f32::INFINITY)
|
||||
.auto_shrink(false)
|
||||
.show(ui, |ui| {
|
||||
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| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(Color32::from_hex("#4444aa").unwrap(), "Filter: ");
|
||||
ui.text_edit_singleline(&mut self.filter);
|
||||
});
|
||||
});
|
||||
|
||||
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| {
|
||||
ui.label(pname.clone())
|
||||
.context_menu(|ui| context_menu(self, ui, &pname, &sname, &s));
|
||||
});
|
||||
row.col(|ui| {
|
||||
let color =
|
||||
match s.get_type() {
|
||||
SongType::Youtube => Color32::from_hex("#FF0000").unwrap(),
|
||||
SongType::Spotify => Color32::from_hex("#1db954").unwrap(),
|
||||
SongType::Soundcloud => Color32::from_hex("#F26F23").unwrap()
|
||||
};
|
||||
|
||||
ui.colored_label(color, s.get_type().to_string())
|
||||
.context_menu(|ui| context_menu(self, ui, &pname, &sname, &s));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.hyperlink_to(sname.clone(), s.get_url_str())
|
||||
.context_menu(|ui| context_menu(self, ui, &pname, &sname, &s));
|
||||
});
|
||||
|
||||
row.response()
|
||||
.context_menu(|ui| context_menu(self, ui, &pname, &sname, &s));
|
||||
|
||||
fn context_menu(this: &mut Gui, ui: &mut egui::Ui, pname: &String, sname: &String, song: &Song) {
|
||||
if ui.button("Edit").clicked() {
|
||||
this.song_edit_w.song = (
|
||||
pname.clone(),
|
||||
sname.clone(),
|
||||
);
|
||||
this.song_edit_w.is_open = true;
|
||||
this.song_edit_w.ed_name = sname.clone();
|
||||
this.song_edit_w.ed_url = song.get_url_str().clone();
|
||||
ui.close_menu()
|
||||
}
|
||||
|
||||
if ui.button("Download").clicked() {
|
||||
if let Err(e) = this.downloader.download_song_nb(&this.cfg, pname, sname, song, this.manifest.get_format()) {
|
||||
log::error!("{e}");
|
||||
this.throw_error(format!("Failed to download song {sname}: {e}"));
|
||||
}
|
||||
ui.close_menu()
|
||||
}
|
||||
|
||||
if ui.button("Open Source").clicked() {
|
||||
if let Err(e) = open::that(song.get_url_str()) {
|
||||
log::error!("{e}");
|
||||
this.throw_error(format!("Failed to open song source: {e}"));
|
||||
}
|
||||
ui.close_menu()
|
||||
}
|
||||
if ui.button("Play").clicked() {
|
||||
let p = crate::util::get_song_path(pname, sname, this.manifest.get_format());
|
||||
|
||||
if !p.exists() {
|
||||
this.throw_error(format!("Song does not exist on disk"));
|
||||
} else if let Err(e) = open::that(p) {
|
||||
log::error!("{e}");
|
||||
this.throw_error(format!("Failed to play song: {e}"));
|
||||
}
|
||||
ui.close_menu()
|
||||
}
|
||||
if ui.button("Delete from disk").clicked() {
|
||||
let p = crate::util::get_song_path(pname, sname, this.manifest.get_format());
|
||||
if p.exists() {
|
||||
if let Err(e) = std::fs::remove_file(p) {
|
||||
this.throw_error(format!("Failed to delete file: {e}"));
|
||||
}
|
||||
}
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button(RichText::new("Delete").color(Color32::RED)).clicked() {
|
||||
this.throw_error("TODO");
|
||||
ui.close_menu()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
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);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use egui::Hyperlink;
|
||||
|
||||
use super::Gui;
|
||||
|
||||
|
||||
@@ -8,20 +10,56 @@ impl Gui {
|
||||
// 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("Source").clicked() {
|
||||
ctx.open_url(egui::OpenUrl::new_tab("https://git.mcorangehq.xyz/XOR64/music"));
|
||||
}
|
||||
if ui.button("Save").clicked() {
|
||||
if let Err(e) = self.manifest.save(None) {
|
||||
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() {
|
||||
self.new_song_w.is_open = true;
|
||||
}
|
||||
});
|
||||
|
||||
ui.menu_button("Playlist", |ui| {
|
||||
if ui.button("Import").clicked() {
|
||||
self.import_playlist_w.is_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.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 {
|
||||
self.downloading = true;
|
||||
ui.label(format!("Downloading: {}/{}", self.downloader.get_songs_left_nb(), self.downloader.get_initial_song_count_nb()));
|
||||
} else if self.downloading {
|
||||
let _ = notify_rust::Notification::new()
|
||||
.summary("Done downloading")
|
||||
.body("Your music has been downloaded")
|
||||
.show();
|
||||
self.downloading = false;
|
||||
}
|
||||
let _ = self.downloader.download_all_nb_poll(&self.cfg);
|
||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,56 @@
|
||||
|
||||
use egui::Color32;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::manifest::{GenreName, SongName};
|
||||
use egui::{text::{LayoutJob, TextWrapping}, Color32, Label, RichText, Style, TextBuffer, TextFormat, TextStyle};
|
||||
|
||||
|
||||
use crate::manifest::{playlist::Playlist, song::{Song, SongType}};
|
||||
|
||||
use super::Gui;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GuiSongEditor {
|
||||
pub is_open: bool,
|
||||
pub song: (GenreName, SongName),
|
||||
pub song: (String, String),
|
||||
pub ed_url: String,
|
||||
pub ed_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GuiNewSong {
|
||||
pub is_open: bool,
|
||||
ed_type: SongType,
|
||||
ed_name: String,
|
||||
ed_playlist: Option<String>,
|
||||
ed_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GuiImportPlaylist {
|
||||
pub is_open: bool,
|
||||
ed_name: String,
|
||||
ed_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GuiError {
|
||||
pub is_open: bool,
|
||||
pub text: 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 (playlist, song_name) = self.song_edit_w.song.clone();
|
||||
|
||||
let Some(song) = self.manifest.get_song(genre.clone(), &song_name) else {
|
||||
let Some(song) = self.manifest.get_song(&playlist, &song_name) else {
|
||||
return;
|
||||
};
|
||||
let song = song.clone();
|
||||
|
||||
|
||||
egui::Window::new("Song editor")
|
||||
.open(&mut self.song_editor.is_open)
|
||||
.open(&mut self.song_edit_w.is_open)
|
||||
.show(ctx,
|
||||
|ui| {
|
||||
|
||||
@@ -33,7 +59,7 @@ impl Gui {
|
||||
ui.label("[");
|
||||
ui.hyperlink_to("link", song.get_url().unwrap());
|
||||
ui.label("] ");
|
||||
ui.colored_label(Color32::LIGHT_BLUE, &genre);
|
||||
ui.colored_label(Color32::LIGHT_BLUE, &playlist);
|
||||
ui.label(": ");
|
||||
ui.label(&song_name)
|
||||
});
|
||||
@@ -45,11 +71,11 @@ impl Gui {
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name: ");
|
||||
ui.text_edit_singleline(&mut self.song_editor.ed_name);
|
||||
ui.text_edit_singleline(&mut self.song_edit_w.ed_name);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Url: ");
|
||||
ui.text_edit_singleline(&mut self.song_editor.ed_url);
|
||||
ui.text_edit_singleline(&mut self.song_edit_w.ed_url);
|
||||
});
|
||||
|
||||
if ui.button("Save").clicked() {
|
||||
@@ -59,21 +85,142 @@ impl Gui {
|
||||
|
||||
if save {
|
||||
{
|
||||
let Some(song) = self.manifest.get_song_mut(genre.clone(), &song_name) else {
|
||||
let Some(song) = self.manifest.get_song_mut(&playlist, &song_name) else {
|
||||
return;
|
||||
};
|
||||
|
||||
*song.get_url_str_mut() = self.song_editor.ed_url.clone();
|
||||
*song.get_url_str_mut() = self.song_edit_w.ed_url.clone();
|
||||
}
|
||||
|
||||
let Some(genre) = self.manifest.get_playlist_mut(genre.clone()) else {
|
||||
let Some(playlist) = self.manifest.get_playlist_mut(&playlist) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
genre.remove(&song_name);
|
||||
genre.insert(self.song_editor.ed_name.clone(), song);
|
||||
playlist.remove_song(&song_name);
|
||||
playlist.add_song(self.song_edit_w.ed_name.clone(), song);
|
||||
self.song_edit_w.is_open = false;
|
||||
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.new_song_w.is_open)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Type: ");
|
||||
egui::ComboBox::from_id_source("new_song_window_type")
|
||||
.selected_text(format!("{:?}", self.new_song_w.ed_type))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.new_song_w.ed_type, SongType::Youtube, "Youtube");
|
||||
ui.selectable_value(&mut self.new_song_w.ed_type, SongType::Spotify, "Spotify");
|
||||
ui.selectable_value(&mut self.new_song_w.ed_type, SongType::Soundcloud, "Soundcloud");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name: ");
|
||||
ui.text_edit_singleline(&mut self.new_song_w.ed_name);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Playlist: ");
|
||||
egui::ComboBox::from_id_source("new_song_window_playlist")
|
||||
.selected_text(format!("{}", self.new_song_w.ed_playlist.clone().unwrap_or("".to_string())))
|
||||
.show_ui(ui, |ui| {
|
||||
for p in self.manifest.get_playlists().keys() {
|
||||
ui.selectable_value(&mut self.new_song_w.ed_playlist, Option::Some(p.clone()), p.as_str());
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Url: ");
|
||||
ui.text_edit_singleline(&mut self.new_song_w.ed_url);
|
||||
});
|
||||
|
||||
if ui.button("Save").clicked() {
|
||||
save = true;
|
||||
}
|
||||
});
|
||||
|
||||
if save {
|
||||
let Some(playlist) = self.manifest.get_playlist_mut(&self.new_song_w.ed_playlist.clone().unwrap()) else {
|
||||
panic!("couldnt find playlist from a preset playlist list????????????");
|
||||
};
|
||||
|
||||
playlist.add_song(
|
||||
self.new_song_w.ed_name.clone(),
|
||||
Song::from_url_str(self.new_song_w.ed_url.clone()).unwrap().set_type(self.new_song_w.ed_type.clone()).clone()
|
||||
);
|
||||
|
||||
|
||||
let _ = self.manifest.save(None);
|
||||
self.new_song_w.is_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_import_playlist_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
|
||||
let mut save = false;
|
||||
egui::Window::new("Import Playlist")
|
||||
.open(&mut self.import_playlist_w.is_open)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Type: Youtube");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name: ");
|
||||
ui.text_edit_singleline(&mut self.import_playlist_w.ed_name);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Url: ");
|
||||
ui.text_edit_singleline(&mut self.import_playlist_w.ed_url);
|
||||
});
|
||||
|
||||
if ui.button("Import").clicked() {
|
||||
save = true;
|
||||
}
|
||||
});
|
||||
|
||||
if save {
|
||||
let name = self.import_playlist_w.ed_name.clone();
|
||||
let url = self.import_playlist_w.ed_url.clone();
|
||||
|
||||
if self.manifest.get_playlist(&name).is_some() {
|
||||
log::error!("Playlist {name} already exists");
|
||||
self.throw_error(format!("Playlist {name} already exists"));
|
||||
}
|
||||
|
||||
let songs = self.downloader.download_playlist_nb(&self.cfg, &url, &name, &self.manifest.get_format()).unwrap();
|
||||
self.manifest.add_playlist(name.clone());
|
||||
|
||||
let playlist = self.manifest.get_playlist_mut(&name).expect("Unreachable");
|
||||
|
||||
for (sname, song) in songs {
|
||||
log::info!("Added: {sname}");
|
||||
playlist.add_song(sname, song);
|
||||
}
|
||||
let _ = self.manifest.save(None);
|
||||
self.import_playlist_w.is_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_error_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
|
||||
egui::Window::new("ERROR!!!! D:")
|
||||
.open(&mut self.error_w.is_open)
|
||||
.show(ctx, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label(RichText::new("Error:").size(30.0).color(Color32::RED));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(Label::new(self.error_w.text.clone()).wrap(true));
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -23,20 +23,25 @@ pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow
|
||||
(Some(c), _) => {
|
||||
match c {
|
||||
CliCommand::Download => unreachable!(),
|
||||
CliCommand::Add { url, name, genre } => {
|
||||
if let Err(e) = add::add(cfg, manifest, &mut downloader, url, name, genre).await {
|
||||
CliCommand::AddPlaylist { url, name } => {
|
||||
if let Err(e) = add::add_playlist(cfg, manifest, &mut downloader, url, name).await {
|
||||
log::error!("Failed to run 'add-playlist' commmand: {e}");
|
||||
}
|
||||
}
|
||||
CliCommand::Add { url, name, playlist } => {
|
||||
if let Err(e) = add::add(cfg, manifest, &mut downloader, url, name, playlist).await {
|
||||
log::error!("Failed to run 'add' command: {e}");
|
||||
}
|
||||
}
|
||||
CliCommand::Gui => {
|
||||
gui::Gui::start(manifest.clone())?;
|
||||
gui::Gui::start(manifest.clone(), downloader, cfg.clone())?;
|
||||
},
|
||||
}
|
||||
}
|
||||
(None, false) => {
|
||||
gui::Gui::start(manifest.clone())?;
|
||||
gui::Gui::start(manifest.clone(), downloader, cfg.clone())?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use camino::Utf8PathBuf;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
|
||||
#[derive(Debug, Parser, Default)]
|
||||
#[derive(Debug, Parser, Default, Clone)]
|
||||
pub struct CliArgs {
|
||||
/// Show more info
|
||||
#[arg(long, short)]
|
||||
@@ -25,16 +25,22 @@ pub struct CliArgs {
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[derive(Debug, Subcommand, Clone)]
|
||||
pub enum CliCommand {
|
||||
Download,
|
||||
Add {
|
||||
#[arg(long, short)]
|
||||
url: Option<String>,
|
||||
url: String,
|
||||
#[arg(long, short)]
|
||||
name: Option<String>,
|
||||
name: String,
|
||||
#[arg(long, short)]
|
||||
genre: Option<String>
|
||||
playlist: String
|
||||
},
|
||||
AddPlaylist {
|
||||
#[arg(long, short)]
|
||||
url: String,
|
||||
#[arg(long, short)]
|
||||
name: String
|
||||
},
|
||||
Gui
|
||||
}
|
||||
|
||||
@@ -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 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 cfg: Config,
|
||||
pub cli: cli::CliArgs,
|
||||
pub isatty: bool
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||
pub struct Config {
|
||||
pub ytdlp: ConfigYtdlp,
|
||||
pub spotdl: ConfigSpotdl,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||
pub struct ConfigYtdlp {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||
pub struct ConfigSpotdl {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
@@ -18,34 +18,123 @@ lazy_static!(
|
||||
static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new()));
|
||||
);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Downloader {
|
||||
count: usize,
|
||||
nb_initial_song_count: usize,
|
||||
nb_cache: Vec<(String, String, Song, Format)>
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
pub fn new() -> 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() + crate::process_manager::proc_count()
|
||||
}
|
||||
|
||||
pub fn download_song_nb(&mut self, cfg: &ConfigWrapper, pname: &String, sname: &String, song: &Song, format: &Format) -> anyhow::Result<()> {
|
||||
self.nb_cache.push((pname.clone(), sname.clone(), song.clone(), format.clone()));
|
||||
self.nb_initial_song_count += 1;
|
||||
self.download_all_nb_poll(cfg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn download_all_nb(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<Option<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<Option<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)?;
|
||||
}
|
||||
}
|
||||
if self.get_songs_left_nb() == 0 {
|
||||
self.nb_initial_song_count = 0;
|
||||
}
|
||||
if crate::process_manager::proc_count() == 0 && self.nb_cache.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(crate::process_manager::purge_done_procs()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub async fn download_all(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<usize> {
|
||||
let format = manifest.get_format();
|
||||
|
||||
for (genre, songs) in manifest.get_playlists() {
|
||||
for (song_name, song) in songs {
|
||||
self.download_song(cfg, song_name, song, &genre, format).await?;
|
||||
self.count += crate::process_manager::wait_for_procs_untill(10).await?;
|
||||
for (name, playlist) in manifest.get_playlists() {
|
||||
for (song_name, song) in playlist.get_songs() {
|
||||
self.download_song(cfg, song_name, song, &name, format)?;
|
||||
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)
|
||||
}
|
||||
|
||||
pub async fn download_song(&mut self, cfg: &ConfigWrapper, name: &String, song: &Song, genre: &String, format: &Format) -> anyhow::Result<()> {
|
||||
let dl_dir = format!("{}/{genre}", cfg.cli.output);
|
||||
let dl_file = format!("{dl_dir}/{}.{}", name, &format);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn download_playlist(&mut self, cfg: &ConfigWrapper, url: &String, pname: &String, format: &Format) -> anyhow::Result<usize> {
|
||||
self.download_playlist_nb(cfg, url, pname, format)?;
|
||||
let mut count = 0;
|
||||
while let Some(c) = self.download_all_nb_poll(cfg)? {
|
||||
count += c;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub fn download_playlist_nb(&mut self, cfg: &ConfigWrapper, url: &String, pname: &String, format: &Format) -> anyhow::Result<HashMap<String, Song>> {
|
||||
log::warn!("This automatically assumes its a youtube link as it is currently the only supported playlist source");
|
||||
let mut cmd = tokio::process::Command::new(&cfg.cfg.ytdlp.path);
|
||||
cmd.args([
|
||||
"--flat-playlist",
|
||||
"--simulate",
|
||||
"-O", "%(url)s|%(title)s",
|
||||
url.as_str()
|
||||
]);
|
||||
cmd
|
||||
.stderr(Stdio::null())
|
||||
.stdout(Stdio::piped());
|
||||
|
||||
let ftr = cmd.output();
|
||||
|
||||
let mut ret = HashMap::new();
|
||||
|
||||
let out = futures::executor::block_on(ftr)?.stdout;
|
||||
let out = String::from_utf8(out)?;
|
||||
for line in out.lines() {
|
||||
let mut split_text = line.split("|").collect::<Vec<&str>>();
|
||||
let url = split_text.swap_remove(0).to_string();
|
||||
let sname = split_text.join("|");
|
||||
let song = Song::from_url_str(url)?.set_type(SongType::Youtube).clone();
|
||||
self.nb_cache.push((pname.clone(), sname.clone(), song.clone(), format.clone()));
|
||||
ret.insert(sname, song.clone());
|
||||
}
|
||||
self.nb_initial_song_count += out.lines().count();
|
||||
self.download_all_nb_poll(cfg)?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn download_song(&mut self, cfg: &ConfigWrapper, name: &String, song: &Song, playlist: &String, format: &Format) -> anyhow::Result<()> {
|
||||
let dl_dir = format!("{}/{playlist}", cfg.cli.output);
|
||||
let dl_file = format!("{dl_dir}/{}.{}", name, &format);
|
||||
log::debug!("Checking: {dl_file}");
|
||||
if PathBuf::from(&dl_file).exists() {
|
||||
log::debug!("File {dl_file} exists, skipping");
|
||||
return Ok(())
|
||||
@@ -89,7 +178,7 @@ impl Downloader {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// pub mod v1;
|
||||
|
||||
pub mod song;
|
||||
pub mod playlist;
|
||||
use song::Song;
|
||||
|
||||
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");
|
||||
|
||||
pub type GenreName = String;
|
||||
|
||||
pub type SongName = String;
|
||||
pub type Genre = HashMap<SongName, song::Song>;
|
||||
|
||||
@@ -31,36 +32,39 @@ pub struct Manifest {
|
||||
#[serde(skip)]
|
||||
path: PathBuf,
|
||||
format: Format,
|
||||
playlists: HashMap<GenreName, Genre>
|
||||
playlists: HashMap<String, playlist::Playlist>,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Manifest {
|
||||
pub fn get_format(&self) -> &Format {
|
||||
&self.format
|
||||
}
|
||||
pub fn add_song(&mut self, genre: GenreName, name: SongName, song: Song) -> Option<Song> {
|
||||
self.get_playlist_mut(genre)?.insert(name, song)
|
||||
pub fn add_song(&mut self, playlist_name: &String, name: SongName, song: Song) -> Option<Song> {
|
||||
self.get_playlist_mut(playlist_name)?.add_song(name, song)
|
||||
}
|
||||
pub fn get_song(&self, genre: GenreName, name: &SongName) -> Option<&Song> {
|
||||
self.get_playlist(genre)?.get(name)
|
||||
pub fn get_song(&self, playlist_name: &String, name: &String) -> Option<&Song> {
|
||||
self.get_playlist(playlist_name)?.get_song(name)
|
||||
}
|
||||
pub fn get_song_mut(&mut self, genre: GenreName, name: &SongName) -> Option<&mut Song> {
|
||||
self.get_playlist_mut(genre)?.get_mut(name)
|
||||
pub fn get_song_mut(&mut self, playlist_name: &String, name: &String) -> Option<&mut Song> {
|
||||
self.get_playlist_mut(playlist_name)?.get_song_mut(name)
|
||||
}
|
||||
pub fn add_playlist(&mut self, name: GenreName) {
|
||||
self.playlists.insert(name, Default::default());
|
||||
pub fn add_playlist(&mut self, playlist_name: String) {
|
||||
self.playlists.insert(playlist_name, Default::default());
|
||||
}
|
||||
pub fn get_playlist(&self, name: GenreName) -> Option<&Genre> {
|
||||
self.playlists.get(&name)
|
||||
pub fn get_playlist(&self, playlist_name: &String) -> Option<&playlist::Playlist> {
|
||||
self.playlists.get(playlist_name)
|
||||
}
|
||||
pub fn get_playlist_mut(&mut self, name: GenreName) -> Option<&mut Genre> {
|
||||
self.playlists.get_mut(&name)
|
||||
pub fn get_playlist_mut(&mut self, playlist_name: &String) -> Option<&mut playlist::Playlist> {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
pub fn get_song_count(&self) -> usize {
|
||||
|
||||
56
src/manifest/playlist.rs
Normal file
56
src/manifest/playlist.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ use anyhow::{bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
|
||||
pub enum SongType {
|
||||
#[default]
|
||||
Youtube,
|
||||
Spotify,
|
||||
Soundcloud,
|
||||
@@ -33,13 +34,17 @@ impl Song {
|
||||
pub fn from_url_str(url: String) -> Result<Self> {
|
||||
Self::from_url(url::Url::from_str(url.as_str())?)
|
||||
}
|
||||
|
||||
pub fn from_url(url: url::Url) -> Result<Self> {
|
||||
Ok(Self {
|
||||
url: url.to_string(),
|
||||
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> {
|
||||
Ok(url::Url::from_str(&self.url)?)
|
||||
}
|
||||
@@ -59,6 +64,7 @@ impl Song {
|
||||
|
||||
|
||||
|
||||
|
||||
impl TryFrom<url::Url> for SongType {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
|
||||
@@ -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 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;
|
||||
proc.wait().await
|
||||
.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,
|
||||
msg,
|
||||
});
|
||||
@@ -38,30 +38,42 @@ pub async fn add_proc(mut cmd: Command, msg: String) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn proc_count() -> usize {
|
||||
PROCESSES.lock().unwrap().read().unwrap().len()
|
||||
}
|
||||
|
||||
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`
|
||||
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: 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;
|
||||
}
|
||||
if !is_proc_queue_full(max) {
|
||||
return Ok(finish_count);
|
||||
}
|
||||
finish_count += purge_done_procs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
src/util.rs
30
src/util.rs
@@ -1,6 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{any::Any, path::PathBuf};
|
||||
|
||||
use crate::constants;
|
||||
use crate::{constants, manifest::Format};
|
||||
|
||||
pub(crate) fn is_supported_host(url: url::Url) -> bool {
|
||||
let host = url.host_str();
|
||||
@@ -52,6 +52,11 @@ pub(crate) fn isatty() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn as_any_mut<T: Any>(val: &mut T) -> &mut dyn Any {
|
||||
val as &mut dyn Any
|
||||
}
|
||||
|
||||
// pub async fn dl_to_file(url: &str, p: PathBuf) -> anyhow::Result<()> {
|
||||
// log::info!("Downloading {} -> {:?}", url, p);
|
||||
// let ytdlp_req = reqwest::get(url).await?.bytes().await?;
|
||||
@@ -60,4 +65,23 @@ pub(crate) fn isatty() -> bool {
|
||||
// fd.write(&ytdlp_req)?;
|
||||
// log::debug!("Finished writing {:?}", p);
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn get_song_path/*<P: TryInto<PathBuf>>*/(/*basepath: Option<P>,*/ pname: &String, sname: &String, format: &Format) -> PathBuf {
|
||||
// let mut path: PathBuf;
|
||||
/*if let Some(bp) = basepath {
|
||||
if let Ok(bp) = bp.try_into() {
|
||||
path = bp;
|
||||
} else {
|
||||
path = std::env::current_dir().unwrap_or(PathBuf::new());
|
||||
}
|
||||
} else {*/
|
||||
let mut path = std::env::current_dir().unwrap_or(PathBuf::new());
|
||||
//}
|
||||
// TODO: Get this from cfg
|
||||
path.push("out");
|
||||
path.push(pname);
|
||||
path.push(sname);
|
||||
path.set_extension(format.to_string());
|
||||
path
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user