add modification

This commit is contained in:
Tan, Kian-ting 2025-08-14 02:00:04 +08:00
parent c3a225ff77
commit 86ffdd03d1
3 changed files with 334 additions and 16 deletions

92
Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@ -47,12 +56,28 @@ dependencies = [
"target-lexicon", "target-lexicon",
] ]
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"
@ -402,6 +427,12 @@ version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.5" version = "2.7.5"
@ -486,6 +517,35 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -495,6 +555,19 @@ dependencies = [
"semver", "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]] [[package]]
name = "semver" name = "semver"
version = "1.0.26" version = "1.0.26"
@ -618,6 +691,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" 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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.1.3" version = "0.1.3"
@ -707,10 +791,18 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]] [[package]]
name = "yt-snd" name = "yt-snd"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"gio", "gio",
"gtk4", "gtk4",
"regex",
"which",
] ]

View file

@ -6,3 +6,5 @@ edition = "2021"
[dependencies] [dependencies]
gio = "0.21.1" gio = "0.21.1"
gtk4 = { version = "0.10.0", features = ["v4_12", "v4_14", "v4_10"] } gtk4 = { version = "0.10.0", features = ["v4_12", "v4_14", "v4_10"] }
regex = "1.11.1"
which = "8.0.0"

View file

@ -4,16 +4,168 @@ use gio::glib::property::PropertyGet;
use gtk4 as gtk; use gtk4 as gtk;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{glib, Application, ApplicationWindow, Button, ScrolledWindow}; 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 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: &gtk4::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: &gtk4::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(); _ = gtk::init();
let main_box = gtk::Box::new(gtk::Orientation::Vertical, 0); 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) label_box_of_links.set_halign(gtk::Align::Start); // align from starting (left)
let scrolled_window = ScrolledWindow::new(); let scrolled_window = ScrolledWindow::new();
@ -48,7 +200,7 @@ fn main() -> glib::ExitCode {
let buffer = path_entry.buffer(); let buffer = path_entry.buffer();
let home_path2 = format!("{}", home_path.clone()); 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()); 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_run_box: gtk4::Box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
let format_label: gtk4::Label = gtk::Label::new(Some("Format:")); let format_label: gtk4::Label = gtk::Label::new(Some("Format:"));
let flac_checker = gtk::CheckButton::with_label("flac"); let flac_checker = gtk::CheckButton::with_label("flac");
let use_flac = flac_checker.is_active();
let mp3_checker = gtk::CheckButton::with_label("mp3"); 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 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); dummy_label.set_hexpand(true);
let convert_button = Button::builder() let convert_button = Button::builder()
@ -82,6 +243,8 @@ fn main() -> glib::ExitCode {
.halign(gtk::Align::End) .halign(gtk::Align::End)
.build(); .build();
flac_checker.set_halign(gtk::Align::Start); flac_checker.set_halign(gtk::Align::Start);
mp3_checker.set_halign(gtk::Align::Start); mp3_checker.set_halign(gtk::Align::Start);
ogg_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(&path_box);
main_box.append(&format_run_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() let app = Application::builder()
.application_id("info.kianting.yt-snd") .application_id("info.kianting.yt-snd")
@ -129,6 +296,50 @@ fn main() -> glib::ExitCode {
.child(&main_box) .child(&main_box)
.build(); .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 window_clone = window.clone();
let path_entry_cloned = path_entry.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); let opened_directory_wrapped = gio::File::for_path(&opened_directory);
folder_dialog.set_initial_folder(Some(&opened_directory_wrapped)); folder_dialog.set_initial_folder(Some(&opened_directory_wrapped));
folder_dialog.set_modal(false); 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 { match x {
Ok(v) => {let a = format!("==={:?}", v.item(0)); Ok(v) => {
let b = v.item(0).unwrap().downcast::<gio::File>().unwrap().path().unwrap(); 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{
println!("{:?}", b); let path_ = v.item(0).unwrap().downcast::<gio::File>().unwrap().path();
let path2 = path_.unwrap();
let path3 = path2.to_str().unwrap();
a}
path_entry_cloned2.buffer().set_text(path3);
"Ok".to_string()
}
}
Err(_) => "Error!".to_string(), Err(_) => "Error!".to_string(),
};
}); });
}); });
//folder_dialog.save(Some(&window_clone), cancellable, move |folder|{println!("~~~~~~")})
});
window.present(); window.present();
// Show the window.
}); });
app.run() app.run()
} }