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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- **Linux launcher category** - Add `AudioVideo`, `Video`, and `Network` categories to the Linux desktop entry so Youwee no longer appears under "Other" in application menus
- **Age-restricted YouTube quality notice** - Log a clear explanation when YouTube requires account age-verification for an age-restricted video, which limits yt-dlp to low-quality formats (usually 360p) instead of silently downloading them

## [0.20.4] - 2026-09-20

Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Sửa lỗi
- **Danh mục launcher Linux** - Thêm các danh mục `AudioVideo`, `Video` và `Network` vào desktop entry trên Linux để Youwee không còn nằm trong mục "Other" của menu ứng dụng
- **Thông báo chất lượng video YouTube giới hạn độ tuổi** - Ghi log giải thích rõ ràng khi YouTube yêu cầu xác minh tuổi tài khoản cho video giới hạn độ tuổi, khiến yt-dlp chỉ lấy được định dạng chất lượng thấp (thường là 360p) thay vì âm thầm tải bản đó

## [0.20.4] - 2026-09-20

Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Youwee 的所有重要更改都将记录在此文件中。

### 修复
- **Linux 启动器分类** - 为 Linux 桌面入口添加 `AudioVideo`、`Video` 和 `Network` 分类,使 Youwee 不再显示在应用菜单的“其他”分类中
- **YouTube 年龄限制视频画质提示** - 当 YouTube 要求账号进行年龄验证时记录清晰的日志说明;此时 yt-dlp 只能获取低画质格式(通常为 360p),不再静默下载而不提示

## [0.20.4] - 2026-09-20

Expand Down
47 changes: 47 additions & 0 deletions src-tauri/src/commands/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,16 @@ mod playlist_chapter_tests {
);
}

#[test]
fn detects_youtube_age_verification_notice() {
assert!(is_youtube_age_verification_notice(
"[youtube] YCuT6oXJX7A: This video is age-restricted and YouTube is requiring account age-verification; some formats may be missing"
));
assert!(!is_youtube_age_verification_notice(
"[youtube] YCuT6oXJX7A: This video is age-restricted; some formats may be missing without authentication."
));
}

#[test]
fn output_path_arg_uses_ytdlp_home_path_slot() {
assert_eq!(
Expand Down Expand Up @@ -1108,6 +1118,26 @@ fn is_aria2_not_found_line(line: &str) -> bool {
|| lower.contains("is not recognized"))
}

/// yt-dlp prints this when a logged-in account is not age-verified for an
/// age-restricted YouTube video. It then falls back to clients that need a
/// GVS PO Token, so without one only format 18 (360p) remains.
fn is_youtube_age_verification_notice(line: &str) -> bool {
line.contains("age-restricted") && line.contains("requiring account age-verification")
}

fn log_youtube_age_verification_notice(line: &str, logged: &AtomicBool, url: &str) {
if !is_youtube_age_verification_notice(line) || logged.swap(true, Ordering::SeqCst) {
return;
}
add_log_internal(
"info",
"YouTube requires account age-verification for this age-restricted video; only low-quality formats (usually 360p) may be available. Verify your age on YouTube with the account used for cookies, then refresh cookies.",
None,
Some(url),
)
.ok();
}

fn download_cancelled_error() -> BackendError {
BackendError::new(crate::types::code::DOWNLOAD_CANCELLED, "Download cancelled")
.with_retryable(false)
Expand Down Expand Up @@ -1784,6 +1814,7 @@ pub async fn download_video(
let mut printed_filepaths: Vec<String> = Vec::new();
let mut observed_filepaths: Vec<String> = Vec::new();
let mut recent_output: VecDeque<String> = VecDeque::new();
let age_verification_logged = AtomicBool::new(false);

let quality_display = match quality.as_str() {
"8k" => Some("8K".to_string()),
Expand Down Expand Up @@ -1812,6 +1843,7 @@ pub async fn download_video(
if let Some(filepath) = parse_ytdlp_output_filepath(&line) {
push_unique_filepath(&mut observed_filepaths, &filepath);
}
log_youtube_age_verification_notice(&line, &age_verification_logged, &url);

// Parse playlist item info
if line.contains("Downloading item") {
Expand Down Expand Up @@ -1935,6 +1967,11 @@ pub async fn download_video(
if let Some(filepath) = parse_ytdlp_output_filepath(&stderr_line) {
push_unique_filepath(&mut observed_filepaths, &filepath);
}
log_youtube_age_verification_notice(
&stderr_line,
&age_verification_logged,
&url,
);

if let Some((percent, speed, eta, pi, pc, downloaded_size, elapsed_time)) =
parse_progress(&stderr_line)
Expand Down Expand Up @@ -2424,6 +2461,10 @@ async fn handle_tokio_download(
let stderr_url = url.clone();
let stderr_recent_output = recent_output.clone();
let stderr_observed_filepaths = observed_filepaths.clone();
// yt-dlp prints the age-verification notice to stdout, but check stderr too
// in case output is redirected (e.g. quiet/logtostderr); log it only once.
let age_verification_logged = Arc::new(AtomicBool::new(false));
let stderr_age_verification_logged = age_verification_logged.clone();
let stderr_task = if let Some(stderr_handle) = stderr {
Some(tokio::spawn(async move {
let mut stderr_reader = BufReader::new(stderr_handle);
Expand All @@ -2449,6 +2490,11 @@ async fn handle_tokio_download(
push_unique_filepath(&mut paths, &filepath);
}
}
log_youtube_age_verification_notice(
&line,
&stderr_age_verification_logged,
&stderr_url,
);

// Parse progress from stderr (live streams output here)
if let Some((percent, speed, eta, pi, pc, downloaded_size, elapsed_time)) =
Expand Down Expand Up @@ -2516,6 +2562,7 @@ async fn handle_tokio_download(
push_unique_filepath(&mut paths, &filepath);
}
}
log_youtube_age_verification_notice(&line, &age_verification_logged, &url);

// Parse progress and emit events
if let Some((percent, speed, eta, pi, pc, downloaded_size, elapsed_time)) =
Expand Down
Loading