From 340ef09efaae05868a0ac25dab21d4fde647152e Mon Sep 17 00:00:00 2001 From: NimBold Date: Sun, 21 Jun 2026 00:07:29 +0330 Subject: [PATCH] fix(media): correct YouTube size estimates Estimate missing stream sizes from bitrate and duration, bind displayed options to exact yt-dlp stream IDs, and propagate approximate totals consistently to download rows. --- src-tauri/src/lib.rs | 223 ++++++++++++++++++++++++++------ src/components/QualityModal.tsx | 5 +- 2 files changed, 188 insertions(+), 40 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 242ba13..9773219 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -94,6 +94,28 @@ fn media_sized_bytes(value: &serde_json::Value) -> Option<(u64, bool)> { } } +fn estimated_stream_bytes( + value: &serde_json::Value, + duration_seconds: Option, +) -> Option<(u64, bool)> { + if let Some(size) = media_sized_bytes(value) { + return Some(size); + } + + let duration_seconds = duration_seconds.filter(|duration| *duration > 0.0)?; + let bitrate_kbps = if has_video_stream(value) && !has_audio_stream(value) { + json_f64(value, "vbr").or_else(|| json_f64(value, "tbr")) + } else if has_audio_stream(value) && !has_video_stream(value) { + json_f64(value, "abr").or_else(|| json_f64(value, "tbr")) + } else { + json_f64(value, "tbr") + } + .filter(|bitrate| *bitrate > 0.0)?; + + let bytes = bitrate_kbps * 1_000.0 * duration_seconds / 8.0; + (bytes.is_finite() && bytes > 0.0).then_some((bytes.round() as u64, true)) +} + fn split_size_estimate(bytes: Option<(u64, bool)>) -> (Option, Option) { match bytes { Some((bytes, false)) => (Some(bytes), None), @@ -244,25 +266,62 @@ fn joined_format_label(container: &str, video_codec: Option<&str>, audio_codec: } } -fn estimated_merged_size(video: Option<&serde_json::Value>, audio: Option<&serde_json::Value>) -> (Option, Option) { - let v_bytes = video.and_then(media_sized_bytes); - let a_bytes = audio.and_then(media_sized_bytes); - - match (v_bytes, a_bytes) { - (Some((v_size, v_approx)), Some((a_size, a_approx))) => { - split_size_estimate(Some((v_size.saturating_add(a_size), v_approx || a_approx))) +fn selected_format_id( + video: Option<&serde_json::Value>, + audio: Option<&serde_json::Value>, +) -> Option { + match ( + video.and_then(|format| json_str(format, "format_id")), + audio.and_then(|format| json_str(format, "format_id")), + ) { + (Some(video_id), Some(audio_id)) if video_id != audio_id => { + Some(format!("{video_id}+{audio_id}")) } - (Some((v_size, _v_approx)), None) => split_size_estimate(Some((v_size, true))), - (None, Some((a_size, _a_approx))) => split_size_estimate(Some((a_size, true))), - (None, None) => (None, None), + (Some(video_id), _) => Some(video_id.to_string()), + (None, Some(audio_id)) => Some(audio_id.to_string()), + (None, None) => None, } } -fn raw_media_format(value: &serde_json::Value) -> Option { +fn estimated_merged_size( + video: Option<&serde_json::Value>, + audio: Option<&serde_json::Value>, + duration_seconds: Option, +) -> (Option, Option) { + let video_has_audio = video.is_some_and(|format| has_audio_stream(format)); + let v_bytes = video.and_then(|format| estimated_stream_bytes(format, duration_seconds)); + let a_bytes = if video_has_audio { + None + } else { + audio.and_then(|format| estimated_stream_bytes(format, duration_seconds)) + }; + + match (video, audio, v_bytes, a_bytes, video_has_audio) { + (Some(_), _, Some((v_size, v_approx)), _, true) => { + split_size_estimate(Some((v_size, v_approx))) + } + (Some(_), Some(_), Some((v_size, v_approx)), Some((a_size, a_approx)), false) => { + split_size_estimate(Some((v_size.saturating_add(a_size), v_approx || a_approx))) + } + (Some(_), None, Some((v_size, v_approx)), None, false) => { + split_size_estimate(Some((v_size, v_approx))) + } + (None, Some(_), None, Some((a_size, a_approx)), false) => { + split_size_estimate(Some((a_size, a_approx))) + } + _ => (None, None), + } +} + +fn raw_media_format( + value: &serde_json::Value, + duration_seconds: Option, +) -> Option { let format_id = json_str(value, "format_id")?.to_string(); let ext = json_str(value, "ext").unwrap_or("mkv").to_string(); let fps = json_f64(value, "fps"); - let (filesize, filesize_approx) = split_size_estimate(media_sized_bytes(value)); + let (filesize, filesize_approx) = + split_size_estimate(estimated_stream_bytes(value, duration_seconds)); let resolution = if has_video_stream(value) { format_height(value).map(|height| format!("{height}p")).or_else(|| json_str(value, "resolution").map(ToOwned::to_owned)).unwrap_or_else(|| "Video".to_string()) @@ -281,18 +340,32 @@ fn raw_media_format(value: &serde_json::Value) -> Option { Some(MediaFormat { format_id, resolution, ext, format_label, fps, filesize, filesize_approx }) } -fn build_media_format_options(formats_arr: &[serde_json::Value]) -> Vec { +fn build_media_format_options( + formats_arr: &[serde_json::Value], + duration_seconds: Option, + requested_formats: Option<&[serde_json::Value]>, +) -> Vec { let clean_formats: Vec<&serde_json::Value> = formats_arr.iter().filter(|format| !is_excluded_yt_dlp_format(format)).collect(); let has_video = clean_formats.iter().any(|format| has_video_stream(format)); let has_audio = clean_formats.iter().any(|format| has_audio_stream(format)); let mut options = Vec::new(); if has_video { - let best_video = best_matching_format(&clean_formats, has_video_stream); - let best_audio = best_audio_format(&clean_formats, None); - let (filesize, filesize_approx) = estimated_merged_size(best_video, best_audio); + let requested_video = requested_formats + .and_then(|formats| formats.iter().find(|format| has_video_stream(format))); + let requested_audio = requested_formats + .and_then(|formats| formats.iter().find(|format| has_audio_stream(format) && !has_video_stream(format))); + let best_video = requested_video.or_else(|| best_matching_format(&clean_formats, has_video_stream)); + let best_audio = if best_video.is_some_and(|format| has_audio_stream(format)) { + None + } else { + requested_audio.or_else(|| best_audio_format(&clean_formats, None)) + }; + let (filesize, filesize_approx) = + estimated_merged_size(best_video, best_audio, duration_seconds); options.push(MediaFormat { - format_id: "bestvideo+bestaudio/best".to_string(), + format_id: selected_format_id(best_video, best_audio) + .unwrap_or_else(|| "bestvideo+bestaudio/best".to_string()), resolution: "Best".to_string(), ext: "mkv".to_string(), format_label: joined_format_label("mkv", best_video.and_then(|format| json_str(format, "vcodec")), best_audio.and_then(|format| json_str(format, "acodec"))), @@ -308,10 +381,12 @@ fn build_media_format_options(formats_arr: &[serde_json::Value]) -> Vec Vec Vec Vec Vec Vec Vec { const settings = useSettingsStore.getState(); const id = crypto.randomUUID(); const filename = `${activeMetadata.title}.${format.ext}`.replace(/[\/\\?%*:|"<>]/g, '-'); + const estimatedBytes = format.filesize || format.filesize_approx || 0; const category = categoryForFileName(filename); const destination = await resolveCategoryDestination(settings, category); @@ -43,7 +44,9 @@ export const QualityModal = React.memo(() => { fileName: filename, destination, fraction: 0, - size: format.filesize ? formatBytes(format.filesize) : 'Unknown', + size: estimatedBytes + ? `${format.filesize ? '' : '~'}${formatBytes(estimatedBytes)}` + : 'Unknown', speed: '-', eta: '-', category,