From 86ffdd03d110fb9e90adf395b825d0ae8471f841 Mon Sep 17 00:00:00 2001 From: Tan Kian-ting Date: Thu, 14 Aug 2025 02:00:04 +0800 Subject: [PATCH] add modification --- Cargo.lock | 92 +++++++++++++++++++ Cargo.toml | 2 + src/main.rs | 256 ++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 334 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19219fd..acc5791 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -47,12 +56,28 @@ dependencies = [ "target-lexicon", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -402,6 +427,12 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "memchr" version = "2.7.5" @@ -486,6 +517,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc_version" version = "0.4.1" @@ -495,6 +555,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "semver" version = "1.0.26" @@ -618,6 +691,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "which" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +dependencies = [ + "env_home", + "rustix", + "winsafe", +] + [[package]] name = "windows-link" version = "0.1.3" @@ -707,10 +791,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "yt-snd" version = "0.1.0" dependencies = [ "gio", "gtk4", + "regex", + "which", ] diff --git a/Cargo.toml b/Cargo.toml index 4ebdc43..718464b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" [dependencies] gio = "0.21.1" gtk4 = { version = "0.10.0", features = ["v4_12", "v4_14", "v4_10"] } +regex = "1.11.1" +which = "8.0.0" diff --git a/src/main.rs b/src/main.rs index e9b7445..913e9e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,16 +4,168 @@ use gio::glib::property::PropertyGet; use gtk4 as gtk; use gtk::prelude::*; use gtk::{glib, Application, ApplicationWindow, Button, ScrolledWindow}; +use gio::ListModel; +use regex::Regex; +use std::process::Command; +use which::which; +use crate::glib::GString; + +#[derive(Clone, Copy)] +enum OutputFormat { + FLAC, + MP3, + OGG, +} fn main() -> glib::ExitCode { + fn show_multi_select_error(v: ListModel, w: Option<&ApplicationWindow>, path_entry: gtk4::Entry){ + let orig_path = path_entry.buffer().text(); + let warning = gtk::AlertDialog::builder() + .modal(true) + .cancel_button(2) + .message("Muitlple folder choosing is not permited!") + .detail(format!("Only one folder should be selected, but you selected {} item(s). None of them will be selected.", v.n_items())) + .default_button(1) + .build(); + + warning.show(w); + path_entry.buffer().set_text(orig_path); + } + + fn show_generic_error(w : Option<&ApplicationWindow>, msg : &str, detail : &str){ + let err_dialog = gtk::AlertDialog::builder() + .modal(true) + .message(msg) + .detail(detail) + .build(); + + err_dialog.show(w); + } + + fn show_empty_output_error(w: Option<&ApplicationWindow>){ + show_generic_error(w, "Empty path is not permitted!", "Please insert the video urls in the box"); + } + + //let mut links = vec!(); + + fn download_and_convert(mut links : &Vec<&'static str>, + output_path : &str, + format : OutputFormat, + tmp_window_clone : &ApplicationWindow, + status_info_label: >k4::Label){ + let tmp_window_clone = tmp_window_clone.clone(); + let tmp_window_clone2: ApplicationWindow = tmp_window_clone.clone(); + + fn real_download(output_path: &str, format: OutputFormat, tmp_window_clone: &ApplicationWindow, status_info_label: >k4::Label, lnk: String, is_processed: bool) { + let output_format = match format { + OutputFormat::FLAC => "flac", + OutputFormat::MP3 => "mp3", + OutputFormat::OGG => "vorbis", + }; + + if is_processed == true { + let res = Command::new("yt-dlp") + .arg(&lnk) + .arg("--paths") + .arg(output_path) + .arg("--extract-audio") + .arg("--audio-format") + .arg(output_format) + .output(); + + match res { + Ok(_) => status_info_label.set_text(format!("\"{}\" downloaded!", &lnk).as_str()), + Err(masg) => { + let title = "Downloading or converting failed!"; + let failed_detail_format = format!("\"{}\" failed to download or convert. More information:\n{}", lnk, masg); + let failed_detail = failed_detail_format.as_str(); + status_info_label.set_text(failed_detail); + show_generic_error(Some(&tmp_window_clone) ,title , failed_detail); + }, + } + } + } + + + + let yt_dlp_result = which("yt-dlp"); + let ffmpeg_result = which("ffmpeg"); + + let mut lack_of_program: Vec<&'static str> = vec!(); + + match yt_dlp_result { + Ok(_) => (), + Err(_) => lack_of_program.push("yt-dlp"), + } + + + match ffmpeg_result { + Ok(_) => (), + Err(_) => lack_of_program.push("ffmpeg"), + } + + + if lack_of_program.len() > 0{ + let lack_of_program_str_joined = lack_of_program.join(", "); + + let detail = format!("Please install the following program(s):\n{}", lack_of_program_str_joined); + let detail_str = detail.as_str(); + show_generic_error(Some(&tmp_window_clone), "Dependent program lacked", detail_str); + } + + let mut links = links.clone(); + for lnk in links.iter_mut(){ + + let re = Regex::new(r"list=").unwrap(); + let capturing = re.captures(lnk); + + let mut is_processed = true; + + let format_clone = format.clone(); + let lnk_clone = lnk.clone(); + match capturing{ + None =>{is_processed = true; real_download(output_path.clone(), format_clone, &tmp_window_clone, status_info_label, lnk.to_string(), is_processed);}, + Some(v) => { + is_processed = false; + let dialog = gtk::AlertDialog::builder() + //.modal(true) + .cancel_button(1) + .message("\"list\" contained in one of the url") + .detail(format!("It will download and convert ALL the videoes!")) + .default_button(0) + .buttons(vec!["Continue".to_string(), "Cancel".to_string()]) + .build(); + + + + + dialog.choose(Some(&tmp_window_clone), gio::Cancellable::NONE, move |result| { + if result.is_err() || result.unwrap() != 1 { + println!("continue"); + let output_path = Box::new(output_path); + real_download(*output_path, format, &tmp_window_clone2, status_info_label, lnk_clone.to_string(), is_processed); + }else{ + println!("cancel"); + is_processed = false; + return; + } + }); + + + + } + } + + } +} _ = gtk::init(); let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0); - let label_box_of_links: gtk4::Label = gtk::Label::new(Some("Enter links from Youtube, seperated by newline character (Enter)")); + let label_box_of_links: gtk4::Label = gtk::Label::new(Some("Enter links from Youtube, seperated by newline character (Enter):")); label_box_of_links.set_halign(gtk::Align::Start); // align from starting (left) let scrolled_window = ScrolledWindow::new(); @@ -40,7 +192,7 @@ fn main() -> glib::ExitCode { }; - + @@ -48,7 +200,7 @@ fn main() -> glib::ExitCode { let buffer = path_entry.buffer(); let home_path2 = format!("{}", home_path.clone()); - let home_path3 = format!("{}", home_path.clone()); + //let home_path3 = format!("{}", home_path.clone()); buffer.set_text(home_path2.to_string()); @@ -68,9 +220,18 @@ fn main() -> glib::ExitCode { let format_run_box: gtk4::Box = gtk::Box::new(gtk::Orientation::Horizontal, 0); let format_label: gtk4::Label = gtk::Label::new(Some("Format:")); let flac_checker = gtk::CheckButton::with_label("flac"); + let use_flac = flac_checker.is_active(); + let mp3_checker = gtk::CheckButton::with_label("mp3"); + mp3_checker.set_active(true); + let use_mp3: bool = mp3_checker.is_active(); + let ogg_checker: gtk4::CheckButton = gtk::CheckButton::with_label("ogg"); - let dummy_label: gtk4::Label = gtk::Label::new(Some("")); + let use_ogg = ogg_checker.is_active(); + + //println!("mp3 {} | ogg {}", useMp3 , useOgg); + + let dummy_label: gtk4::Label = gtk::Label::new(Some("")); // interface workaround dummy_label.set_hexpand(true); let convert_button = Button::builder() @@ -82,6 +243,8 @@ fn main() -> glib::ExitCode { .halign(gtk::Align::End) .build(); + + flac_checker.set_halign(gtk::Align::Start); mp3_checker.set_halign(gtk::Align::Start); ogg_checker.set_halign(gtk::Align::Start); @@ -108,6 +271,10 @@ fn main() -> glib::ExitCode { main_box.append(&path_box); main_box.append(&format_run_box); + let status_info_label: gtk4::Label = gtk::Label::new(Some("Ready")); // interface workaround + status_info_label.set_halign(gtk::Align::Start); + main_box.append(&status_info_label); + let app = Application::builder() .application_id("info.kianting.yt-snd") @@ -116,7 +283,7 @@ fn main() -> glib::ExitCode { - let cancellable: Option<&gio::Cancellable> = None; + let cancellable: Option<&gio::Cancellable> = None; @@ -129,6 +296,50 @@ fn main() -> glib::ExitCode { .child(&main_box) .build(); + let path_entry_cloned_for_converting = path_entry.clone(); + + let text_buffer2 = text_buffer.clone(); + let tmp_window_clone: ApplicationWindow = window.clone(); + let start_iter = text_buffer2.start_iter(); + let end_iter = text_buffer2.end_iter(); + //let mut links = text_buffer2.text(&start_iter, &end_iter, true); + + let status_info_label_cloned = status_info_label.clone(); + convert_button.connect_clicked(move|_|{ + let new_line_pattern = Regex::new(r"(\r?\n)+").expect("invalid regex"); + + let mut links = text_buffer2.text(&start_iter, &end_iter, true); + let link_as_str = links.as_str(); + + let link_vector = new_line_pattern.split(link_as_str).collect(); + + + if link_vector == [""]{ + show_empty_output_error(Some(&tmp_window_clone)); + } + + let output_path = path_entry_cloned_for_converting.buffer().text(); + + let format: OutputFormat; + + + if use_mp3 == true{ + format = OutputFormat::MP3; + } + else if use_ogg == true{ + format = OutputFormat::OGG; + }else { + format = OutputFormat::FLAC; + } + + download_and_convert(link_vector, output_path.as_str(), + format, + &tmp_window_clone, + &status_info_label_cloned); + + + }); + let window_clone = window.clone(); let path_entry_cloned = path_entry.clone(); @@ -139,27 +350,40 @@ fn main() -> glib::ExitCode { let opened_directory_wrapped = gio::File::for_path(&opened_directory); folder_dialog.set_initial_folder(Some(&opened_directory_wrapped)); folder_dialog.set_modal(false); - folder_dialog.select_multiple_folders(Some(&window_clone), cancellable, |x| { + let window_clone2 = window_clone.clone(); + let path_entry_cloned2 = path_entry_cloned.clone(); + folder_dialog.select_multiple_folders(Some(&window_clone), cancellable, move |x | { - println!( "{}", match x { - Ok(v) => {let a = format!("==={:?}", v.item(0)); - let b = v.item(0).unwrap().downcast::().unwrap().path().unwrap(); - - println!("{:?}", b); - - a} + match x { + Ok(v) => { + let link_list_length = v.n_items(); + if link_list_length > 1{ + show_multi_select_error(v.clone(), Some(&window_clone2), path_entry_cloned2); + "multiple select error".to_string() + }else{ + + let path_ = v.item(0).unwrap().downcast::().unwrap().path(); + let path2 = path_.unwrap(); + let path3 = path2.to_str().unwrap(); + + + path_entry_cloned2.buffer().set_text(path3); + + "Ok".to_string() + } + } Err(_) => "Error!".to_string(), - }); + }; }); - //folder_dialog.save(Some(&window_clone), cancellable, move |folder|{println!("~~~~~~")}) }); window.present(); - // Show the window. }); app.run() } + +