Format code using Zed auto-formatter, display stderr along with stdout

in youtube downloader, use mp3 template instead of -x and --audio-format
in the command,
This commit is contained in:
csd4ni3l
2026-05-01 22:03:07 +02:00
parent ff947b57a8
commit 74d54d6927
2 changed files with 201 additions and 114 deletions
+193 -106
View File
@@ -1,7 +1,19 @@
use bevy::{log::Level, prelude::*};
use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui::{self, Context, TextBuffer, Ui, ecolor::Color32}};
use bevy_egui::{
EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet,
egui::{self, Context, TextBuffer, Ui, ecolor::Color32},
};
use std::{collections::HashMap, fs::{File, create_dir, exists, rename}, io::{BufReader, Read, Seek}, path::Path, process::{Command, Stdio}, sync::{Arc, Mutex}, thread, time::Instant};
use std::{
collections::HashMap,
fs::{File, create_dir, exists, rename},
io::{BufReader, Read, Seek},
path::Path,
process::{Command, Stdio},
sync::{Arc, Mutex},
thread,
time::Instant,
};
use serde::{Deserialize, Serialize};
@@ -46,7 +58,7 @@ struct YoutubeDownloaderState {
current_filename: String,
download_directory: String,
yt_dlp_running: bool,
yt_dlp_stdout_text: Arc<Mutex<String>>
yt_dlp_stdout_text: Arc<Mutex<String>>,
}
#[derive(Resource)]
@@ -60,7 +72,7 @@ struct AppState {
is_virt_output_used: HashMap<String, bool>,
last_virt_output_update: Instant,
current_view: String,
youtube_downloader_state: YoutubeDownloaderState
youtube_downloader_state: YoutubeDownloaderState,
}
const ALLOWED_FILE_EXTENSIONS: [&str; 4] = ["mp3", "wav", "flac", "ogg"];
@@ -163,8 +175,8 @@ fn main() {
current_filename: String::new(),
download_directory: String::new(),
yt_dlp_running: false,
yt_dlp_stdout_text: Arc::new(Mutex::new(String::new()))
}
yt_dlp_stdout_text: Arc::new(Mutex::new(String::new())),
},
})
.add_systems(
PreStartup,
@@ -179,7 +191,8 @@ fn main() {
}
fn update(mut app_state: ResMut<AppState>) {
#[cfg(target_os = "linux")] {
#[cfg(target_os = "linux")]
{
if app_state.last_virt_output_update.elapsed().as_secs_f32() >= 1.5 {
app_state.last_virt_output_update = Instant::now();
app_state.virt_outputs = list_outputs();
@@ -187,14 +200,21 @@ fn update(mut app_state: ResMut<AppState>) {
for virt_output in &app_state.virt_outputs.clone() {
if !is_virt_output_used.contains_key(&virt_output.1) {
app_state.is_virt_output_used.insert(virt_output.1.clone(), false);
app_state
.is_virt_output_used
.insert(virt_output.1.clone(), false);
}
if app_state.is_virt_output_used[&virt_output.1] {
linux_lib::move_output_to_sink(virt_output.1.clone(), linux_lib::get_soundboard_sink_index());
}
else {
linux_lib::move_output_to_sink(virt_output.1.clone(), linux_lib::get_default_source());
linux_lib::move_output_to_sink(
virt_output.1.clone(),
linux_lib::get_soundboard_sink_index(),
);
} else {
linux_lib::move_output_to_sink(
virt_output.1.clone(),
linux_lib::get_default_source(),
);
}
}
}
@@ -258,7 +278,8 @@ fn update_ui_scale_factor_system(egui_context: Single<(&mut EguiContextSettings,
egui_settings.scale_factor = 1.5 / camera.target_scaling_factor().unwrap_or(1.5);
}
fn get_duration<R>(decoder: &mut rodio::Decoder<R>) -> f32 // get_duration is needed cause some MP3 files dont provide duration metadata so we need to count
fn get_duration<R>(decoder: &mut rodio::Decoder<R>) -> f32
// get_duration is needed cause some MP3 files dont provide duration metadata so we need to count
where
R: Read + Seek,
{
@@ -276,7 +297,13 @@ where
fn play_sound(file_path: String, app_state: &mut AppState) {
let file = File::open(&file_path).unwrap();
let mut src = Decoder::new(BufReader::new(file)).unwrap();
let decoded = Decoder::new(BufReader::new(file));
let mut src;
if decoded.is_ok() {
src = decoded.unwrap();
} else {
return;
}
let length = get_duration(&mut src);
// need to recreate since get_duration seeks to the end and nothing is left
@@ -306,12 +333,21 @@ fn play_sound(file_path: String, app_state: &mut AppState) {
app_state.currently_playing.push(playing_sound);
}
fn create_virtual_mic_ui(ui: &mut Ui, app_state: &mut ResMut<AppState>, available_width: f32, available_height: f32) {
#[cfg(target_os = "linux")] {
fn create_virtual_mic_ui(
ui: &mut Ui,
app_state: &mut ResMut<AppState>,
available_width: f32,
available_height: f32,
) {
#[cfg(target_os = "linux")]
{
if app_state.is_virt_output_used.len() != 0 {
let outputs = app_state.virt_outputs.clone();
for output in &outputs {
let current_value = *app_state.is_virt_output_used.get(&output.1).unwrap_or(&false);
let current_value = *app_state
.is_virt_output_used
.get(&output.1)
.unwrap_or(&false);
if ui
.add_sized(
[available_width, available_height / 30.0],
@@ -319,11 +355,13 @@ fn create_virtual_mic_ui(ui: &mut Ui, app_state: &mut ResMut<AppState>, availabl
)
.clicked()
{
*app_state.is_virt_output_used.entry(output.1.clone()).or_insert(false) = !current_value;
*app_state
.is_virt_output_used
.entry(output.1.clone())
.or_insert(false) = !current_value;
}
}
}
else {
} else {
ui.add(egui::Button::new("No apps found to use.".to_string()));
}
@@ -331,7 +369,9 @@ fn create_virtual_mic_ui(ui: &mut Ui, app_state: &mut ResMut<AppState>, availabl
}
#[allow(unreachable_code)]
{
ui.add(egui::Button::new("Unsupported. Select inside apps.".to_string()));
ui.add(egui::Button::new(
"Unsupported. Select inside apps.".to_string(),
));
}
}
@@ -461,42 +501,82 @@ fn main_ui(ctx: &Context, mut app_state: ResMut<AppState>) {
fn download_youtube_sound(app_state: &mut ResMut<AppState>) {
let filename = app_state.youtube_downloader_state.current_filename.clone();
let download_directory = app_state.youtube_downloader_state.download_directory.clone();
let download_directory = app_state
.youtube_downloader_state
.download_directory
.clone();
let current_url = app_state.youtube_downloader_state.current_url.clone();
let stdout_text = Arc::clone(&app_state.youtube_downloader_state.yt_dlp_stdout_text);
app_state.youtube_downloader_state.yt_dlp_running = true;
thread::spawn(move || {
let mut command = Command::new(get_yt_dlp_path())
.args(&["-x", "--audio-format", "mp3", "-o", "sound.mp3", current_url.as_str()])
let mut child = Command::new(get_yt_dlp_path())
.args(&[
"-x",
"--audio-format",
"mp3",
"-o",
"sound.mp3",
current_url.as_str(),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to execute process");
if let Some(mut stdout) = command.stdout.take() {
let mut buffer = String::new();
loop {
let mut chunk = vec![0u8; 1024];
match stdout.read(&mut chunk) {
Ok(0) => break,
Ok(n) => {
if let Ok(text) = String::from_utf8(chunk[..n].to_vec()) {
buffer.push_str(&text);
if let Ok(mut locked) = stdout_text.lock() {
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let stdout_text_for_thread = stdout_text.clone();
let stdout_handle = thread::spawn(move || {
if let Some(mut stdout) = stdout {
let mut buf = [0u8; 4096];
let mut buffer = String::new();
loop {
match stdout.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
buffer.push_str(&String::from_utf8_lossy(&buf[..n]));
if let Ok(mut locked) = stdout_text_for_thread.lock() {
*locked = buffer.clone();
}
}
Err(_) => break,
}
Err(_) => break,
}
}
}
let _ = command.wait();
});
let stderr_text_for_thread = stdout_text.clone();
let stderr_handle = thread::spawn(move || {
if let Some(mut stderr) = stderr {
let mut buf = [0u8; 4096];
let mut buffer = String::new();
loop {
match stderr.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
buffer.push_str(&String::from_utf8_lossy(&buf[..n]));
if let Ok(mut locked) = stderr_text_for_thread.lock() {
*locked = buffer.clone();
}
}
Err(_) => break,
}
}
}
});
let _status = child.wait();
let _ = stdout_handle.join();
let _ = stderr_handle.join();
let path = Path::new(&download_directory).join(filename);
let _ = rename("sound.mp3", path.to_string_lossy().as_str());
let _ = rename("sound.mp3", path.to_string_lossy().as_ref());
});
}
@@ -508,7 +588,12 @@ fn youtube_downloader_ui(ctx: &Context, mut app_state: ResMut<AppState>) {
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
ui.heading("Directory");
egui::ComboBox::from_id_salt("Download Directory Selector")
.selected_text(app_state.youtube_downloader_state.download_directory.clone())
.selected_text(
app_state
.youtube_downloader_state
.download_directory
.clone(),
)
.width(available_width)
.height(available_height / 15.0)
.show_ui(ui, |ui| {
@@ -522,10 +607,18 @@ fn youtube_downloader_ui(ctx: &Context, mut app_state: ResMut<AppState>) {
});
ui.heading("Filename");
ui.add_sized([available_width, available_height / 20.0], egui::TextEdit::singleline(&mut app_state.youtube_downloader_state.current_filename));
ui.add_sized(
[available_width, available_height / 20.0],
egui::TextEdit::singleline(
&mut app_state.youtube_downloader_state.current_filename,
),
);
ui.heading("Youtube URL");
ui.add_sized([available_width, available_height / 20.0], egui::TextEdit::singleline(&mut app_state.youtube_downloader_state.current_url));
ui.add_sized(
[available_width, available_height / 20.0],
egui::TextEdit::singleline(&mut app_state.youtube_downloader_state.current_url),
);
});
if let Ok(text) = app_state.youtube_downloader_state.yt_dlp_stdout_text.lock() {
@@ -534,10 +627,7 @@ fn youtube_downloader_ui(ctx: &Context, mut app_state: ResMut<AppState>) {
if ui
.add_sized(
[
available_width as f32,
available_height / 15.0,
],
[available_width as f32, available_height / 15.0],
egui::Button::new("Download Sound"),
)
.clicked()
@@ -568,8 +658,7 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
ui.heading("csd4ni3l Soundboard");
});
}
else {
} else {
ui.heading("csd4ni3l Soundboard");
}
});
@@ -579,72 +668,70 @@ fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
egui::TopBottomPanel::bottom("currently_playing")
.exact_height(window_height * 0.1)
.show(ctx, |ui| {
ui.vertical(|ui| {
for playing_sound in &mut app_state.currently_playing {
ui.horizontal(|ui| {
ui.label(format!(
"{} - {:.2} / {:.2}",
playing_sound.file_path,
playing_sound.sink.get_pos().as_secs_f32(),
playing_sound.length
));
let available_width = ui.available_width();
let available_height = ui.available_height();
if ui
.add_sized(
[
available_width / 2 as f32,
available_height,
],
egui::Button::new("Stop"),
)
.clicked()
{
playing_sound.to_remove = true;
};
if ui
.add_sized(
[
available_width / 2 as f32,
available_height,
],
egui::Button::new(if playing_sound.sink.is_paused() {"Resume"} else {"Pause"}),
)
.clicked()
{
if playing_sound.sink.is_paused() {
playing_sound.sink.play();
}
else {
playing_sound.sink.pause();
}
};
});
ui.vertical(|ui| {
for playing_sound in &mut app_state.currently_playing {
ui.horizontal(|ui| {
ui.label(format!(
"{} - {:.2} / {:.2}",
playing_sound.file_path,
playing_sound.sink.get_pos().as_secs_f32(),
playing_sound.length
));
let available_width = ui.available_width();
let available_height = ui.available_height();
if ui
.add_sized(
[available_width / 2 as f32, available_height],
egui::Button::new("Stop"),
)
.clicked()
{
playing_sound.to_remove = true;
};
if ui
.add_sized(
[available_width / 2 as f32, available_height],
egui::Button::new(if playing_sound.sink.is_paused() {
"Resume"
} else {
"Pause"
}),
)
.clicked()
{
if playing_sound.sink.is_paused() {
playing_sound.sink.play();
} else {
playing_sound.sink.pause();
}
};
});
}
});
let available_width = ui.available_width();
let available_height = ui.available_height();
if ui
.add_sized(
[available_width, available_height / 15.0],
egui::Button::new("Stop all"),
)
.clicked()
{
app_state.currently_playing.clear();
}
});
let available_width = ui.available_width();
let available_height = ui.available_height();
if ui
.add_sized(
[available_width, available_height / 15.0],
egui::Button::new("Stop all"),
)
.clicked()
{
app_state.currently_playing.clear();
}
});
app_state.currently_playing.retain(|playing_sound| { // retains happen the next cycle, not in the current one because of borrowing and im lazy to fix
playing_sound.sink.get_pos().as_secs_f32() <= (playing_sound.length - 0.01) && !playing_sound.to_remove // 0.01 offset needed here because of floating point errors and so its not exact
app_state.currently_playing.retain(|playing_sound| {
// retains happen the next cycle, not in the current one because of borrowing and im lazy to fix
playing_sound.sink.get_pos().as_secs_f32() <= (playing_sound.length - 0.01)
&& !playing_sound.to_remove // 0.01 offset needed here because of floating point errors and so its not exact
});
if app_state.current_view == "main".to_string() {
main_ui(ctx, app_state);
}
else if app_state.current_view == "youtube_downloader".to_string() {
} else if app_state.current_view == "youtube_downloader".to_string() {
youtube_downloader_ui(ctx, app_state);
}