Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src-tauri/src/commands/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/commands/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 6 additions & 18 deletions src-tauri/src/services/ytdlp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) {
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();
Expand Down Expand Up @@ -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]
Expand Down
99 changes: 99 additions & 0 deletions src-tauri/src/utils/filename.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! 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 = 180;
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.as_bytes().len();
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<String>, 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"), 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]
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()));
}
}
2 changes: 2 additions & 0 deletions src-tauri/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
mod command;
mod extract;
mod filename;
mod format;
mod path;
mod progress;
mod security;

pub use command::*;
pub use extract::*;
pub use filename::*;
pub use format::*;
pub use path::*;
pub use progress::*;
Expand Down
Loading