From 85df857678ae8bdf581c308f70389c089f6da4db Mon Sep 17 00:00:00 2001 From: NimBold Date: Fri, 19 Jun 2026 16:41:33 +0330 Subject: [PATCH] fix(ytdlp): correct media size estimates --- src-tauri/src/download.rs | 1 + src-tauri/src/lib.rs | 170 ++++++++++++++------------ src/bindings/DownloadProgressEvent.ts | 2 +- src/bindings/MediaFormat.ts | 2 +- src/components/AddDownloadsModal.tsx | 23 ++-- src/store/downloadStore.ts | 3 +- 6 files changed, 110 insertions(+), 91 deletions(-) diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index 88e5d75..f442e28 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -161,6 +161,7 @@ impl CoordinatorEventSink { speed: format_speed(speed_bytes), eta, size: total.map(|t| format_size(t as f64)), + size_is_final: false, }, ); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 27f7ea8..a5a50c4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -33,6 +33,8 @@ pub struct MediaFormat { pub fps: Option, #[ts(type = "number | null")] pub filesize: Option, + #[ts(type = "number | null")] + pub filesize_approx: Option, } #[derive(Debug, Serialize, serde::Deserialize, Clone, TS)] @@ -76,6 +78,30 @@ fn media_filesize(value: &serde_json::Value) -> Option { json_u64(value, "filesize").or_else(|| json_u64(value, "filesize_approx")) } +fn media_exact_filesize(value: &serde_json::Value) -> Option { + json_u64(value, "filesize") +} + +fn media_approx_filesize(value: &serde_json::Value) -> Option { + media_exact_filesize(value).is_none().then(|| json_u64(value, "filesize_approx")).flatten() +} + +fn media_sized_bytes(value: &serde_json::Value) -> Option<(u64, bool)> { + if let Some(size) = media_exact_filesize(value) { + Some((size, false)) + } else { + media_approx_filesize(value).map(|size| (size, true)) + } +} + +fn split_size_estimate(bytes: Option<(u64, bool)>) -> (Option, Option) { + match bytes { + Some((bytes, false)) => (Some(bytes), None), + Some((bytes, true)) => (None, Some(bytes)), + None => (None, None), + } +} + fn codec_is_present(codec: Option<&str>) -> bool { codec.map(str::trim).filter(|codec| !codec.is_empty()).map(|codec| codec.to_lowercase() != "none").unwrap_or(false) } @@ -218,27 +244,27 @@ 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 { - let mut total = 0_u64; - let mut known = false; - - if let Some(size) = video.and_then(media_filesize) { - total = total.saturating_add(size); - known = true; - } - if let Some(size) = audio.and_then(media_filesize) { - total = total.saturating_add(size); - known = true; - } - - known.then_some(total) +fn estimated_merged_size(video: Option<&serde_json::Value>, audio: Option<&serde_json::Value>) -> (Option, Option) { + let Some(video) = video else { + return (None, None); + }; + let Some((video_size, video_approx)) = media_sized_bytes(video) else { + return (None, None); + }; + let Some(audio) = audio else { + return split_size_estimate(Some((video_size, video_approx))); + }; + let Some((audio_size, audio_approx)) = media_sized_bytes(audio) else { + return (None, None); + }; + split_size_estimate(Some((video_size.saturating_add(audio_size), video_approx || audio_approx))) } fn raw_media_format(value: &serde_json::Value) -> 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 = media_filesize(value); + let (filesize, filesize_approx) = split_size_estimate(media_sized_bytes(value)); 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()) @@ -254,7 +280,7 @@ fn raw_media_format(value: &serde_json::Value) -> Option { joined_format_label(&ext, None, json_str(value, "acodec")) }; - Some(MediaFormat { format_id, resolution, ext, format_label, fps, filesize }) + Some(MediaFormat { format_id, resolution, ext, format_label, fps, filesize, filesize_approx }) } fn build_media_format_options(formats_arr: &[serde_json::Value]) -> Vec { @@ -266,13 +292,15 @@ fn build_media_format_options(formats_arr: &[serde_json::Value]) -> Vec = [2160_u64, 1440, 1080, 720, 480, 360] @@ -283,37 +311,43 @@ fn build_media_format_options(formats_arr: &[serde_json::Value]) -> Vec Vec bool { - if !path.is_file() { - return false; - } - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - path.metadata() - .map(|metadata| metadata.permissions().mode() & 0o111 != 0) - .unwrap_or(false) - } - - #[cfg(not(unix))] - { - true - } -} - -fn find_yt_dlp_on_path() -> Option { - if let Ok(override_path) = std::env::var("FIRELINK_YTDLP_PATH") { - let path = PathBuf::from(override_path); - if executable_exists(&path) { - return Some(path); - } - } - - if let Some(path) = std::env::var_os("PATH").and_then(|path| { - std::env::split_paths(&path) - .map(|dir| dir.join("yt-dlp")) - .find(|candidate| executable_exists(candidate)) - }) { - return Some(path); - } - - ["/opt/homebrew/bin/yt-dlp", "/usr/local/bin/yt-dlp", "/usr/bin/yt-dlp"] - .into_iter() - .map(PathBuf::from) - .find(|candidate| executable_exists(candidate)) -} - fn resolve_metadata_ytdlp_path(app_handle: &tauri::AppHandle) -> Result<(PathBuf, &'static str), String> { - if let Some(path) = find_yt_dlp_on_path() { - return Ok((path, "system")); - } - resolve_bundled_binary_path(app_handle, "yt-dlp") .map(|path| (path, "bundled")) .map_err(|e| format!("failed to find bundled yt-dlp: {e}")) @@ -1177,6 +1172,7 @@ pub struct DownloadProgressEvent { speed: String, eta: String, size: Option, + size_is_final: bool, } #[derive(Debug, Clone, Serialize, TS)] @@ -2053,10 +2049,11 @@ pub(crate) async fn start_media_download_internal( let _ = app_handle.emit("download-progress", DownloadProgressEvent { id: id.to_string(), fraction: overall_fraction, - speed, - eta: progress.eta, - size: progress.size, - }); + speed, + eta: progress.eta, + size: progress.size, + size_is_final: false, + }); last_progress_at = now; } } @@ -2077,10 +2074,11 @@ pub(crate) async fn start_media_download_internal( let _ = app_handle.emit("download-progress", DownloadProgressEvent { id: id.to_string(), fraction: overall_fraction, - speed, - eta: progress.eta, - size: progress.size, - }); + speed, + eta: progress.eta, + size: progress.size, + size_is_final: false, + }); last_progress_at = now; } } @@ -2099,6 +2097,7 @@ pub(crate) async fn start_media_download_internal( speed: "Processing".to_string(), eta: "-".to_string(), size: None, + size_is_final: false, }); } let lower = line.to_lowercase(); @@ -2117,6 +2116,18 @@ pub(crate) async fn start_media_download_internal( Some(tauri_plugin_shell::process::CommandEvent::Terminated(payload)) => { if payload.code == Some(0) { log::info!("yt-dlp completed successfully for id: {}", id); + if let Ok(metadata) = tokio::fs::metadata(&out_path).await { + if metadata.is_file() { + let _ = app_handle.emit("download-progress", DownloadProgressEvent { + id: id.to_string(), + fraction: 1.0, + speed: "-".to_string(), + eta: "-".to_string(), + size: Some(crate::download::format_size(metadata.len() as f64)), + size_is_final: true, + }); + } + } return Ok(()); } log::error!("yt-dlp exited with non-zero code {:?} for id: {}", payload.code, id); @@ -3412,9 +3423,10 @@ pub fn run() { id, fraction, speed, - eta, - size, - }); + eta, + size, + size_is_final: false, + }); } } } diff --git a/src/bindings/DownloadProgressEvent.ts b/src/bindings/DownloadProgressEvent.ts index 3cb1505..cd05f2f 100644 --- a/src/bindings/DownloadProgressEvent.ts +++ b/src/bindings/DownloadProgressEvent.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DownloadProgressEvent = { id: string, fraction: number, speed: string, eta: string, size: string | null, }; +export type DownloadProgressEvent = { id: string, fraction: number, speed: string, eta: string, size: string | null, size_is_final: boolean, }; diff --git a/src/bindings/MediaFormat.ts b/src/bindings/MediaFormat.ts index 2149b92..4f25ebd 100644 --- a/src/bindings/MediaFormat.ts +++ b/src/bindings/MediaFormat.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type MediaFormat = { format_id: string, resolution: string, ext: string, format_label: string, fps: number | null, filesize: number | null, }; +export type MediaFormat = { format_id: string, resolution: string, ext: string, format_label: string, fps: number | null, filesize: number | null, filesize_approx: number | null, }; diff --git a/src/components/AddDownloadsModal.tsx b/src/components/AddDownloadsModal.tsx index 07e5573..6551b40 100644 --- a/src/components/AddDownloadsModal.tsx +++ b/src/components/AddDownloadsModal.tsx @@ -28,6 +28,7 @@ interface MediaFormat { detail: string; type: string; bytes: number; + isApproximate?: boolean; } interface ParsedDownloadItem { @@ -383,16 +384,20 @@ export const AddDownloadsModal = () => { password: keychainPassword }); if (mediaData && mediaData.formats.length > 0) { - const mappedFormats = mediaData.formats.map(f => { - const quality = f.resolution || 'Best'; - const container = f.ext.toUpperCase(); - const bytes = f.filesize || 0; - return { - name: `${quality} ${container}`, - ext: f.ext, - bytes, + const mappedFormats = mediaData.formats.map(f => { + const quality = f.resolution || 'Best'; + const container = f.ext.toUpperCase(); + const exactBytes = f.filesize || 0; + const approxBytes = f.filesize_approx || 0; + const bytes = exactBytes || approxBytes; + const isApproximate = !exactBytes && approxBytes > 0; + return { + name: `${quality} ${container}`, + ext: f.ext, + bytes, + isApproximate, formatLabel: f.format_label || f.ext.toUpperCase(), - detail: bytes ? formatBytes(bytes) : 'Unknown size', + detail: bytes ? `${isApproximate ? '~' : ''}${formatBytes(bytes)}` : 'Unknown size', selector: f.format_id, type: quality.toLowerCase().includes('audio') ? 'Audio' : 'Video' }; diff --git a/src/store/downloadStore.ts b/src/store/downloadStore.ts index ad3346b..41582d6 100644 --- a/src/store/downloadStore.ts +++ b/src/store/downloadStore.ts @@ -35,11 +35,12 @@ export async function initDownloadListener() { const mainStore = useDownloadStore.getState(); const current = mainStore.downloads.find(d => d.id === payload.id); if (current) { + const shouldUpdateSize = Boolean(payload.size && (!current.isMedia || payload.size_is_final)); mainStore.updateDownload(payload.id, { fraction: payload.fraction, speed: payload.speed, eta: payload.eta, - ...(payload.size ? { size: payload.size } : {}), + ...(shouldUpdateSize ? { size: payload.size! } : {}), }); } });