From 53bc7890ab433d65627610bf8dd5aa4c1291987e Mon Sep 17 00:00:00 2001 From: blueyi Date: Sun, 5 Jul 2026 20:24:56 +0800 Subject: [PATCH 1/2] fix(windows): improve yt-dlp filename safety without changing defaults Add --windows-filenames on Windows and compute --trim-filenames from the output directory length so long Facebook/Instagram titles stay within path limits. Keeps the existing %(title)s output template unchanged. --- src-tauri/src/commands/download.rs | 2 +- src-tauri/src/commands/metadata.rs | 2 +- src-tauri/src/services/ytdlp.rs | 24 +++------ src-tauri/src/utils/filename.rs | 86 ++++++++++++++++++++++++++++++ src-tauri/src/utils/mod.rs | 2 + 5 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 src-tauri/src/utils/filename.rs diff --git a/src-tauri/src/commands/download.rs b/src-tauri/src/commands/download.rs index 70db2696..3398d599 100644 --- a/src-tauri/src/commands/download.rs +++ b/src-tauri/src/commands/download.rs @@ -1033,7 +1033,7 @@ pub async fn download_video( "--file-access-retries".to_string(), "2".to_string(), ]; - add_safe_filename_args(&mut args); + add_safe_filename_args(&mut args, Some(&sanitized_path)); if split_embedded_chapters { args.push("--split-chapters".to_string()); diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index bc6e05e5..f10f5eb0 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -750,7 +750,7 @@ pub async fn fetch_metadata( "-o".to_string(), output_template.clone(), ]; - add_safe_filename_args(&mut args); + add_safe_filename_args(&mut args, Some(&sanitized_path)); // Description output template - yt-dlp adds .description automatically if write_description { diff --git a/src-tauri/src/services/ytdlp.rs b/src-tauri/src/services/ytdlp.rs index cb26cbd8..eae88c3d 100644 --- a/src-tauri/src/services/ytdlp.rs +++ b/src-tauri/src/services/ytdlp.rs @@ -928,15 +928,7 @@ pub fn build_cookie_args( args } -/// Keep yt-dlp output filenames below common filesystem limits. -/// -/// Some extractors, especially Facebook Reels, expose long captions as titles. -/// macOS returns ENAMETOOLONG before yt-dlp can create the .part file unless -/// yt-dlp trims the generated filename first. -pub fn add_safe_filename_args(args: &mut Vec) { - args.push("--trim-filenames".to_string()); - args.push("180".to_string()); -} +pub use crate::utils::add_safe_filename_args; fn parse_cookie_skip_rule(rule: &str) -> Option<(String, String)> { let trimmed = rule.trim(); @@ -1235,16 +1227,12 @@ mod tests { fn safe_filename_args_trim_long_titles_before_writing_files() { let mut args = vec!["--newline".to_string()]; - add_safe_filename_args(&mut args); + add_safe_filename_args(&mut args, None); - assert_eq!( - args, - vec![ - "--newline".to_string(), - "--trim-filenames".to_string(), - "180".to_string(), - ] - ); + assert!(args.contains(&"--newline".to_string())); + assert!(args.contains(&"--trim-filenames".to_string())); + #[cfg(windows)] + assert!(args.contains(&"--windows-filenames".to_string())); } #[test] diff --git a/src-tauri/src/utils/filename.rs b/src-tauri/src/utils/filename.rs new file mode 100644 index 00000000..6fcb63db --- /dev/null +++ b/src-tauri/src/utils/filename.rs @@ -0,0 +1,86 @@ +//! yt-dlp filename safety helpers for long paths and Windows-invalid characters. + +const WINDOWS_MAX_PATH: usize = 260; +const RESERVED_SUFFIX_BYTES: usize = 40; +const MIN_TRIM_FILENAMES: u32 = 50; +const MAX_TRIM_FILENAMES: u32 = 200; +const DEFAULT_TRIM_FILENAMES: u32 = 180; + +/// Compute a safe `--trim-filenames` byte limit from the output directory length. +/// +/// Reserves space for the directory prefix so the final path stays within common +/// filesystem limits (especially `MAX_PATH` on Windows). +pub fn calc_trim_filenames_bytes(output_path: &str) -> u32 { + let path_len = output_path.chars().count(); + let available = WINDOWS_MAX_PATH + .saturating_sub(path_len) + .saturating_sub(RESERVED_SUFFIX_BYTES); + (available as u32).clamp(MIN_TRIM_FILENAMES, MAX_TRIM_FILENAMES) +} + +/// Append yt-dlp flags that keep generated filenames safe on the target platform. +pub fn add_safe_filename_args(args: &mut Vec, output_path: Option<&str>) { + #[cfg(windows)] + { + args.push("--windows-filenames".to_string()); + } + + let trim = output_path + .map(calc_trim_filenames_bytes) + .unwrap_or(DEFAULT_TRIM_FILENAMES); + + args.push("--trim-filenames".to_string()); + args.push(trim.to_string()); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn calc_trim_filenames_respects_long_output_paths() { + let long_path = "G:\\Youwee\\Downloads\\very-long-folder-name"; + assert!(calc_trim_filenames_bytes(long_path) < DEFAULT_TRIM_FILENAMES); + } + + #[test] + fn calc_trim_filenames_clamps_short_paths_to_default_cap() { + assert_eq!(calc_trim_filenames_bytes("/tmp"), MAX_TRIM_FILENAMES); + } + + #[test] + fn add_safe_filename_args_uses_path_aware_trim() { + let mut args = Vec::new(); + add_safe_filename_args(&mut args, Some("G:\\Youwee")); + + assert!(args.contains(&"--trim-filenames".to_string())); + let trim_index = args.iter().position(|arg| arg == "--trim-filenames").unwrap(); + let trim_value: u32 = args[trim_index + 1].parse().unwrap(); + assert!(trim_value >= MIN_TRIM_FILENAMES); + assert!(trim_value <= MAX_TRIM_FILENAMES); + } + + #[test] + fn add_safe_filename_args_falls_back_to_default_trim() { + let mut args = Vec::new(); + add_safe_filename_args(&mut args, None); + + assert_eq!( + args, + vec![ + #[cfg(windows)] + "--windows-filenames".to_string(), + "--trim-filenames".to_string(), + DEFAULT_TRIM_FILENAMES.to_string(), + ] + ); + } + + #[cfg(windows)] + #[test] + fn add_safe_filename_args_enables_windows_filenames() { + let mut args = Vec::new(); + add_safe_filename_args(&mut args, None); + assert!(args.contains(&"--windows-filenames".to_string())); + } +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index c954d19b..53eb4148 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,5 +1,6 @@ mod command; mod extract; +mod filename; mod format; mod path; mod progress; @@ -7,6 +8,7 @@ mod security; pub use command::*; pub use extract::*; +pub use filename::*; pub use format::*; pub use path::*; pub use progress::*; From a24a49a4e5d13e77b7319dacb807389312cedbfc Mon Sep 17 00:00:00 2001 From: blueyi Date: Mon, 6 Jul 2026 02:16:38 +0800 Subject: [PATCH 2/2] fix(windows): cap trim at 180 and measure path length in bytes Address PR review: keep the effective --trim-filenames cap at 180 for short output paths, and use output_path.as_bytes().len() so non-ASCII folder names reserve path space correctly. --- src-tauri/src/utils/filename.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/utils/filename.rs b/src-tauri/src/utils/filename.rs index 6fcb63db..8b5542da 100644 --- a/src-tauri/src/utils/filename.rs +++ b/src-tauri/src/utils/filename.rs @@ -3,7 +3,7 @@ const WINDOWS_MAX_PATH: usize = 260; const RESERVED_SUFFIX_BYTES: usize = 40; const MIN_TRIM_FILENAMES: u32 = 50; -const MAX_TRIM_FILENAMES: u32 = 200; +const MAX_TRIM_FILENAMES: u32 = 180; const DEFAULT_TRIM_FILENAMES: u32 = 180; /// Compute a safe `--trim-filenames` byte limit from the output directory length. @@ -11,7 +11,7 @@ const DEFAULT_TRIM_FILENAMES: u32 = 180; /// Reserves space for the directory prefix so the final path stays within common /// filesystem limits (especially `MAX_PATH` on Windows). pub fn calc_trim_filenames_bytes(output_path: &str) -> u32 { - let path_len = output_path.chars().count(); + let path_len = output_path.as_bytes().len(); let available = WINDOWS_MAX_PATH .saturating_sub(path_len) .saturating_sub(RESERVED_SUFFIX_BYTES); @@ -45,7 +45,20 @@ mod tests { #[test] fn calc_trim_filenames_clamps_short_paths_to_default_cap() { - assert_eq!(calc_trim_filenames_bytes("/tmp"), MAX_TRIM_FILENAMES); + assert_eq!(calc_trim_filenames_bytes("/tmp"), DEFAULT_TRIM_FILENAMES); + } + + #[test] + fn calc_trim_filenames_uses_byte_length_for_non_ascii_paths() { + let path = "G:\\下载\\Youwee\\very-long-non-ascii-output-directory-name"; + assert!(path.as_bytes().len() > path.chars().count()); + + let expected = (WINDOWS_MAX_PATH + .saturating_sub(path.as_bytes().len()) + .saturating_sub(RESERVED_SUFFIX_BYTES) as u32) + .clamp(MIN_TRIM_FILENAMES, MAX_TRIM_FILENAMES); + assert_eq!(calc_trim_filenames_bytes(path), expected); + assert!(expected < DEFAULT_TRIM_FILENAMES); } #[test]