mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-03 07:59:29 +00:00
fix(media): show live yt-dlp progress (#8)
Buffer yt-dlp output chunks before parsing progress events and update the download row from live progress state instead of imperative DOM writes.
This commit is contained in:
+240
-109
@@ -700,6 +700,40 @@ fn progress_json_string(progress: &serde_json::Value, key: &str) -> Option<Strin
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn drain_media_output_lines(buffer: &mut String, chunk: &str) -> Vec<String> {
|
||||
buffer.push_str(chunk);
|
||||
|
||||
let mut lines = Vec::new();
|
||||
while let Some(index) = match (buffer.find('\n'), buffer.find('\r')) {
|
||||
(Some(line_feed), Some(carriage_return)) => Some(line_feed.min(carriage_return)),
|
||||
(Some(line_feed), None) => Some(line_feed),
|
||||
(None, Some(carriage_return)) => Some(carriage_return),
|
||||
(None, None) => None,
|
||||
} {
|
||||
let mut line: String = buffer.drain(..=index).collect();
|
||||
while line.ends_with('\n') || line.ends_with('\r') {
|
||||
line.pop();
|
||||
}
|
||||
if !line.trim().is_empty() {
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if lines.is_empty()
|
||||
&& buffer.contains(MEDIA_PROGRESS_PREFIX)
|
||||
&& parse_media_progress_line(buffer).is_some()
|
||||
{
|
||||
lines.push(std::mem::take(buffer));
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn flush_media_output_line(buffer: &mut String) -> Option<String> {
|
||||
let line = std::mem::take(buffer);
|
||||
(!line.trim().is_empty()).then_some(line)
|
||||
}
|
||||
|
||||
fn parse_media_progress_line(line: &str) -> Option<MediaProgress> {
|
||||
if let Some(prefix_index) = line.find(MEDIA_PROGRESS_PREFIX) {
|
||||
let progress: serde_json::Value =
|
||||
@@ -723,8 +757,12 @@ fn parse_media_progress_line(line: &str) -> Option<MediaProgress> {
|
||||
} else if total > 0.0 {
|
||||
downloaded / total
|
||||
} else {
|
||||
progress_json_string(&progress, "_percent_str")
|
||||
.and_then(|percent| percent.trim_end_matches('%').trim().parse::<f64>().ok())
|
||||
progress_json_number(&progress, "_percent")
|
||||
.or_else(|| {
|
||||
progress_json_string(&progress, "_percent_str").and_then(|percent| {
|
||||
percent.trim_end_matches('%').trim().parse::<f64>().ok()
|
||||
})
|
||||
})
|
||||
.unwrap_or(0.0)
|
||||
/ 100.0
|
||||
};
|
||||
@@ -895,6 +933,45 @@ fn aggregate_media_fraction(
|
||||
((*current_track + *last_fraction) / total_tracks).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn emit_media_progress(
|
||||
app_handle: &tauri::AppHandle,
|
||||
id: &str,
|
||||
progress: MediaProgress,
|
||||
total_tracks: f64,
|
||||
current_track: &mut f64,
|
||||
last_fraction: &mut f64,
|
||||
last_speed_sample: &mut Option<(Instant, f64)>,
|
||||
last_progress_at: &mut Instant,
|
||||
) {
|
||||
let previous_track = *current_track;
|
||||
let overall_fraction = aggregate_media_fraction(
|
||||
total_tracks,
|
||||
current_track,
|
||||
last_fraction,
|
||||
progress.fraction,
|
||||
);
|
||||
if *current_track != previous_track {
|
||||
*last_speed_sample = None;
|
||||
}
|
||||
let (speed, eta) = media_progress_speed(&progress, Instant::now(), last_speed_sample);
|
||||
|
||||
let now = Instant::now();
|
||||
if now.duration_since(*last_progress_at) >= Duration::from_millis(200) {
|
||||
let _ = app_handle.emit(
|
||||
"download-progress",
|
||||
DownloadProgressEvent {
|
||||
id: id.to_string(),
|
||||
fraction: overall_fraction,
|
||||
speed,
|
||||
eta,
|
||||
size: progress.size,
|
||||
size_is_final: false,
|
||||
},
|
||||
);
|
||||
*last_progress_at = now;
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup_media_processing_artifacts(out_path: &std::path::Path) {
|
||||
cleanup_media_artifacts(out_path, true).await;
|
||||
}
|
||||
@@ -1230,7 +1307,7 @@ async fn fetch_metadata(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if size_bytes == 0 {
|
||||
if let Some(len) = res.headers().get(reqwest::header::CONTENT_LENGTH) {
|
||||
if let Ok(len_str) = len.to_str() {
|
||||
@@ -2834,6 +2911,8 @@ pub(crate) async fn start_media_download_internal(
|
||||
|
||||
let mut stderr_tail = String::new();
|
||||
let mut final_output_path: Option<std::path::PathBuf> = None;
|
||||
let mut stdout_buffer = String::new();
|
||||
let mut stderr_buffer = String::new();
|
||||
let failure_reason = loop {
|
||||
tokio::select! {
|
||||
_ = cancel_rx.changed() => {
|
||||
@@ -2847,101 +2926,124 @@ pub(crate) async fn start_media_download_internal(
|
||||
event = rx.recv() => {
|
||||
match event {
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Stdout(line_bytes)) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
if let Some(progress) = parse_media_progress_line(&line) {
|
||||
let previous_track = current_track;
|
||||
let overall_fraction = aggregate_media_fraction(
|
||||
total_tracks,
|
||||
&mut current_track,
|
||||
&mut last_fraction,
|
||||
progress.fraction,
|
||||
);
|
||||
if current_track != previous_track {
|
||||
last_speed_sample = None;
|
||||
}
|
||||
let (speed, eta) = media_progress_speed(&progress, std::time::Instant::now(), &mut last_speed_sample);
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
if now.duration_since(last_progress_at) >= std::time::Duration::from_millis(200) {
|
||||
let _ = app_handle.emit("download-progress", DownloadProgressEvent {
|
||||
id: id.to_string(),
|
||||
fraction: overall_fraction,
|
||||
speed,
|
||||
eta,
|
||||
size: progress.size,
|
||||
size_is_final: false,
|
||||
});
|
||||
last_progress_at = now;
|
||||
}
|
||||
} else {
|
||||
let candidate = line.trim();
|
||||
if !candidate.is_empty() {
|
||||
let candidate_path = std::path::PathBuf::from(candidate);
|
||||
if candidate_path.is_absolute() {
|
||||
final_output_path = Some(candidate_path);
|
||||
let chunk = String::from_utf8_lossy(&line_bytes);
|
||||
for line in drain_media_output_lines(&mut stdout_buffer, &chunk) {
|
||||
if let Some(progress) = parse_media_progress_line(&line) {
|
||||
emit_media_progress(
|
||||
&app_handle,
|
||||
id,
|
||||
progress,
|
||||
total_tracks,
|
||||
&mut current_track,
|
||||
&mut last_fraction,
|
||||
&mut last_speed_sample,
|
||||
&mut last_progress_at,
|
||||
);
|
||||
} else {
|
||||
let candidate = line.trim();
|
||||
if !candidate.is_empty() {
|
||||
let candidate_path = std::path::PathBuf::from(candidate);
|
||||
if candidate_path.is_absolute() {
|
||||
final_output_path = Some(candidate_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Stderr(line_bytes)) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
if let Some(progress) = parse_media_progress_line(&line) {
|
||||
let previous_track = current_track;
|
||||
let overall_fraction = aggregate_media_fraction(
|
||||
total_tracks,
|
||||
&mut current_track,
|
||||
&mut last_fraction,
|
||||
progress.fraction,
|
||||
);
|
||||
if current_track != previous_track {
|
||||
last_speed_sample = None;
|
||||
}
|
||||
let (speed, eta) = media_progress_speed(&progress, std::time::Instant::now(), &mut last_speed_sample);
|
||||
let now = std::time::Instant::now();
|
||||
if now.duration_since(last_progress_at) >= std::time::Duration::from_millis(200) {
|
||||
let _ = app_handle.emit("download-progress", DownloadProgressEvent {
|
||||
id: id.to_string(),
|
||||
fraction: overall_fraction,
|
||||
speed,
|
||||
eta,
|
||||
size: progress.size,
|
||||
size_is_final: false,
|
||||
});
|
||||
last_progress_at = now;
|
||||
}
|
||||
}
|
||||
if !processing_started && is_media_processing_line(&line) {
|
||||
processing_started = true;
|
||||
let _ = app_handle.emit(
|
||||
"download-state",
|
||||
DownloadStateEvent::new(
|
||||
id,
|
||||
crate::ipc::DownloadStatus::Processing,
|
||||
),
|
||||
);
|
||||
let _ = app_handle.emit("download-progress", DownloadProgressEvent {
|
||||
id: id.to_string(),
|
||||
fraction: 1.0,
|
||||
speed: "Processing".to_string(),
|
||||
eta: "-".to_string(),
|
||||
size: None,
|
||||
size_is_final: false,
|
||||
});
|
||||
}
|
||||
let lower = line.to_lowercase();
|
||||
if lower.contains("error") || lower.contains("critical") {
|
||||
log::error!("yt-dlp stderr [{}]: {}", id, line.trim());
|
||||
}
|
||||
stderr_tail.push_str(&line);
|
||||
let chunk = String::from_utf8_lossy(&line_bytes);
|
||||
stderr_tail.push_str(&chunk);
|
||||
if stderr_tail.len() > STDERR_TAIL {
|
||||
stderr_tail = stderr_tail.split_off(stderr_tail.len() - STDERR_TAIL);
|
||||
}
|
||||
for line in drain_media_output_lines(&mut stderr_buffer, &chunk) {
|
||||
if let Some(progress) = parse_media_progress_line(&line) {
|
||||
emit_media_progress(
|
||||
&app_handle,
|
||||
id,
|
||||
progress,
|
||||
total_tracks,
|
||||
&mut current_track,
|
||||
&mut last_fraction,
|
||||
&mut last_speed_sample,
|
||||
&mut last_progress_at,
|
||||
);
|
||||
}
|
||||
if !processing_started && is_media_processing_line(&line) {
|
||||
processing_started = true;
|
||||
let _ = app_handle.emit(
|
||||
"download-state",
|
||||
DownloadStateEvent::new(
|
||||
id,
|
||||
crate::ipc::DownloadStatus::Processing,
|
||||
),
|
||||
);
|
||||
let _ = app_handle.emit("download-progress", DownloadProgressEvent {
|
||||
id: id.to_string(),
|
||||
fraction: 1.0,
|
||||
speed: "Processing".to_string(),
|
||||
eta: "-".to_string(),
|
||||
size: None,
|
||||
size_is_final: false,
|
||||
});
|
||||
}
|
||||
let lower = line.to_lowercase();
|
||||
if lower.contains("error") || lower.contains("critical") {
|
||||
log::error!("yt-dlp stderr [{}]: {}", id, line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Error(err)) => {
|
||||
log::error!("yt-dlp shell error [{}]: {}", id, err);
|
||||
break err;
|
||||
}
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Terminated(payload)) => {
|
||||
if let Some(line) = flush_media_output_line(&mut stdout_buffer) {
|
||||
if let Some(progress) = parse_media_progress_line(&line) {
|
||||
emit_media_progress(
|
||||
&app_handle,
|
||||
id,
|
||||
progress,
|
||||
total_tracks,
|
||||
&mut current_track,
|
||||
&mut last_fraction,
|
||||
&mut last_speed_sample,
|
||||
&mut last_progress_at,
|
||||
);
|
||||
} else {
|
||||
let candidate = line.trim();
|
||||
if !candidate.is_empty() {
|
||||
let candidate_path = std::path::PathBuf::from(candidate);
|
||||
if candidate_path.is_absolute() {
|
||||
final_output_path = Some(candidate_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(line) = flush_media_output_line(&mut stderr_buffer) {
|
||||
if let Some(progress) = parse_media_progress_line(&line) {
|
||||
emit_media_progress(
|
||||
&app_handle,
|
||||
id,
|
||||
progress,
|
||||
total_tracks,
|
||||
&mut current_track,
|
||||
&mut last_fraction,
|
||||
&mut last_speed_sample,
|
||||
&mut last_progress_at,
|
||||
);
|
||||
}
|
||||
if !processing_started && is_media_processing_line(&line) {
|
||||
processing_started = true;
|
||||
let _ = app_handle.emit(
|
||||
"download-state",
|
||||
DownloadStateEvent::new(
|
||||
id,
|
||||
crate::ipc::DownloadStatus::Processing,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if payload.code == Some(0) {
|
||||
log::info!("yt-dlp completed successfully id: {}", id);
|
||||
let completed_path = final_output_path
|
||||
@@ -3442,7 +3544,7 @@ async fn detach_download_for_reconfigure(
|
||||
serde_json::json!([gid]),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
if let Err(e) = pause_res {
|
||||
if !e.contains("cannot be paused now") {
|
||||
return Err(e);
|
||||
@@ -3579,20 +3681,20 @@ fn update_dock_badge(app_handle: tauri::AppHandle, count: i32) {
|
||||
|
||||
let _ = app_handle.run_on_main_thread(move || {
|
||||
unsafe {
|
||||
let app_class = class!(NSApplication);
|
||||
let app: *mut Object = msg_send![app_class, sharedApplication];
|
||||
let dock_tile: *mut Object = msg_send![app, dockTile];
|
||||
let label = if count > 0 {
|
||||
count.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let c_label = CString::new(label).unwrap();
|
||||
let ns_string_class = class!(NSString);
|
||||
let ns_label: *mut Object = msg_send![ns_string_class, alloc];
|
||||
let ns_label: *mut Object = msg_send![ns_label, initWithUTF8String: c_label.as_ptr()];
|
||||
let _: () = msg_send![dock_tile, setBadgeLabel: ns_label];
|
||||
let _: () = msg_send![ns_label, release];
|
||||
let app_class = class!(NSApplication);
|
||||
let app: *mut Object = msg_send![app_class, sharedApplication];
|
||||
let dock_tile: *mut Object = msg_send![app, dockTile];
|
||||
let label = if count > 0 {
|
||||
count.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let c_label = CString::new(label).unwrap();
|
||||
let ns_string_class = class!(NSString);
|
||||
let ns_label: *mut Object = msg_send![ns_string_class, alloc];
|
||||
let ns_label: *mut Object = msg_send![ns_label, initWithUTF8String: c_label.as_ptr()];
|
||||
let _: () = msg_send![dock_tile, setBadgeLabel: ns_label];
|
||||
let _: () = msg_send![ns_label, release];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -3663,25 +3765,25 @@ fn create_sleep_preventer() -> Result<SleepPreventer, String> {
|
||||
let cstr = CString::new(s).unwrap();
|
||||
macos_sleep::CFStringCreateWithCString(null(), cstr.as_ptr(), 0x08000100)
|
||||
};
|
||||
|
||||
|
||||
let type_sys = create_cf_string("PreventSystemSleep");
|
||||
let type_net = create_cf_string("NetworkClientActive");
|
||||
let name = create_cf_string("Firelink active download");
|
||||
|
||||
|
||||
let mut sys_id: u32 = 0;
|
||||
let mut net_id: u32 = 0;
|
||||
|
||||
|
||||
let res1 = macos_sleep::IOPMAssertionCreateWithDescription(
|
||||
type_sys, name, null(), null(), null(), 0.0, null(), &mut sys_id
|
||||
);
|
||||
let res2 = macos_sleep::IOPMAssertionCreateWithDescription(
|
||||
type_net, name, null(), null(), null(), 0.0, null(), &mut net_id
|
||||
);
|
||||
|
||||
|
||||
macos_sleep::CFRelease(type_sys);
|
||||
macos_sleep::CFRelease(type_net);
|
||||
macos_sleep::CFRelease(name);
|
||||
|
||||
|
||||
if res1 == 0 && res2 == 0 {
|
||||
Ok(SleepPreventer::Mac { system_sleep_id: sys_id, network_client_id: net_id })
|
||||
} else {
|
||||
@@ -3935,16 +4037,16 @@ fn check_automation_permission() -> Result<(), String> {
|
||||
let ns_string_class = class!(NSString);
|
||||
let script_str: *mut Object = msg_send![ns_string_class, alloc];
|
||||
let script_str: *mut Object = msg_send![script_str, initWithUTF8String: c_script.as_ptr()];
|
||||
|
||||
|
||||
let ns_apple_script: *mut Object = msg_send![class!(NSAppleScript), alloc];
|
||||
let ns_apple_script: *mut Object = msg_send![ns_apple_script, initWithSource: script_str];
|
||||
|
||||
|
||||
let mut error_dict: *mut Object = null_mut();
|
||||
let result: *mut Object = msg_send![ns_apple_script, executeAndReturnError: &mut error_dict];
|
||||
|
||||
|
||||
let _: () = msg_send![script_str, release];
|
||||
let _: () = msg_send![ns_apple_script, release];
|
||||
|
||||
|
||||
if result.is_null() {
|
||||
return Err("Automation permission was not granted".to_string());
|
||||
}
|
||||
@@ -4483,7 +4585,7 @@ fn set_extension_frontend_ready(state: tauri::State<'_, AppState>, ready: bool)
|
||||
mod tests {
|
||||
use super::{
|
||||
aggregate_media_fraction, append_ytdlp_http_headers, build_media_format_options,
|
||||
collect_download_uris, filename_from_content_disposition,
|
||||
collect_download_uris, drain_media_output_lines, filename_from_content_disposition,
|
||||
filename_from_url_disposition_query, filename_from_url_path, is_excluded_yt_dlp_format,
|
||||
is_browser_cookie_extraction_error, json_lower, media_metadata_cache_key,
|
||||
media_output_template, media_progress_speed, normalize_speed_limit_for_aria2,
|
||||
@@ -4929,6 +5031,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_chunked_structured_ytdlp_progress() {
|
||||
let mut buffer = String::new();
|
||||
let first = format!("{MEDIA_PROGRESS_PREFIX}{{\"downloaded_bytes\":5242880,");
|
||||
let second = "\"total_bytes\":10485760,\"_percent\":50.0,\"_speed_str\":\"1.00MiB/s\"}\n";
|
||||
|
||||
assert!(drain_media_output_lines(&mut buffer, &first).is_empty());
|
||||
let lines = drain_media_output_lines(&mut buffer, second);
|
||||
|
||||
assert_eq!(lines.len(), 1);
|
||||
assert_eq!(
|
||||
parse_media_progress_line(&lines[0]).map(|progress| progress.fraction),
|
||||
Some(0.5)
|
||||
);
|
||||
assert!(buffer.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_structured_ytdlp_numeric_percent_without_total() {
|
||||
let line = format!(
|
||||
"{MEDIA_PROGRESS_PREFIX}{{\"downloaded_bytes\":5242880,\"_percent\":37.5,\"_speed_str\":\"1.00MiB/s\"}}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_media_progress_line(&line).map(|progress| progress.fraction),
|
||||
Some(0.375)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_ffmpeg_snapshot_version_without_collapsing_to_n() {
|
||||
let output = "ffmpeg version N-125385-ge2e889d9da-https://www.martin-riedl.de Copyright (c) 2000-2026 the FFmpeg developers";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useDownloadStore } from '../store/useDownloadStore';
|
||||
import { useDownloadProgressStore } from '../store/downloadStore';
|
||||
@@ -45,47 +45,26 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
.map(candidate => candidate.id);
|
||||
}));
|
||||
const moveInQueue = useDownloadStore(state => state.moveInQueue);
|
||||
const liveProgress = useDownloadProgressStore(state => state.progressMap[downloadId]);
|
||||
const queueIndex = queueItems.indexOf(downloadId);
|
||||
|
||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
||||
const statusTextRef = useRef<HTMLSpanElement>(null);
|
||||
const speedTextRef = useRef<HTMLSpanElement>(null);
|
||||
const etaTextRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!download || download.status !== 'downloading') return;
|
||||
|
||||
const applyProgress = (progress: any) => {
|
||||
if (!progress) return;
|
||||
if (progressBarRef.current) {
|
||||
progressBarRef.current.style.width = `${progress.fraction * 100}%`;
|
||||
}
|
||||
if (statusTextRef.current) {
|
||||
statusTextRef.current.innerText = `${(progress.fraction * 100).toFixed(0)}%`;
|
||||
statusTextRef.current.title = `${(progress.fraction * 100).toFixed(0)}%`;
|
||||
}
|
||||
if (speedTextRef.current) {
|
||||
speedTextRef.current.innerText = progress.speed;
|
||||
speedTextRef.current.title = progress.speed;
|
||||
}
|
||||
if (etaTextRef.current) {
|
||||
etaTextRef.current.innerText = progress.eta;
|
||||
etaTextRef.current.title = progress.eta;
|
||||
}
|
||||
};
|
||||
|
||||
// Apply immediate state on mount
|
||||
applyProgress(useDownloadProgressStore.getState().progressMap[downloadId]);
|
||||
|
||||
const unsubscribe = useDownloadProgressStore.subscribe((state) => {
|
||||
applyProgress(state.progressMap[downloadId]);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [downloadId, download?.status]);
|
||||
|
||||
if (!download) return null;
|
||||
|
||||
const displayFraction = download.status === 'downloading'
|
||||
? liveProgress?.fraction ?? download.fraction ?? 0
|
||||
: download.fraction ?? 0;
|
||||
const displayPercent = `${(displayFraction * 100).toFixed(0)}%`;
|
||||
const displaySpeed = download.status === 'downloading'
|
||||
? liveProgress?.speed ?? download.speed
|
||||
: download.status === 'processing'
|
||||
? 'Processing...'
|
||||
: '-';
|
||||
const displayEta = download.status === 'downloading'
|
||||
? liveProgress?.eta ?? download.eta
|
||||
: download.status === 'processing'
|
||||
? 'Muxing...'
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`download-row group cursor-default relative ${index % 2 !== 0 ? 'striped' : ''} ${selectedIds.has(downloadId) ? 'is-selected' : ''}`}
|
||||
@@ -118,26 +97,24 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
<>
|
||||
<div className="download-progress-track">
|
||||
<div
|
||||
ref={progressBarRef}
|
||||
className={`download-progress-fill ${
|
||||
download.status === 'paused' ? 'paused' :
|
||||
download.status === 'processing' ? 'processing' :
|
||||
download.status === 'queued' || download.status === 'staged' ? 'queued' :
|
||||
download.status === 'retrying' ? 'retrying' : ''
|
||||
}`}
|
||||
style={{ width: `${(download.fraction || 0) * 100}%` }}
|
||||
style={{ width: `${displayFraction * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
key={`status-${download.status}`}
|
||||
ref={statusTextRef}
|
||||
title={
|
||||
download.lastError && (download.status === 'failed' || download.status === 'retrying')
|
||||
? download.lastError
|
||||
: (download.status === 'queued' || download.status === 'staged') && queueIndex !== -1
|
||||
? `${download.status === 'staged' ? 'In queue' : 'Queued'} #${queueIndex + 1}`
|
||||
: download.status === 'downloading'
|
||||
? `${((download.fraction || 0) * 100).toFixed(0)}%`
|
||||
? displayPercent
|
||||
: download.status === 'processing'
|
||||
? 'Processing'
|
||||
: download.status.charAt(0).toUpperCase() + download.status.slice(1)
|
||||
@@ -159,7 +136,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
</span>
|
||||
</>
|
||||
) : download.status === 'downloading' ? (
|
||||
`${((download.fraction || 0) * 100).toFixed(0)}%`
|
||||
displayPercent
|
||||
) : download.status === 'processing' ? (
|
||||
'Processing'
|
||||
) : (
|
||||
@@ -173,22 +150,20 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
<div className="download-cell-truncate">
|
||||
<span
|
||||
key={`speed-${download.status}`}
|
||||
ref={speedTextRef}
|
||||
className="tabular-nums"
|
||||
title={download.status === 'downloading' ? download.speed : download.status === 'processing' ? 'Processing…' : '-'}
|
||||
title={displaySpeed}
|
||||
>
|
||||
{download.status === 'downloading' ? download.speed : download.status === 'processing' ? 'Processing…' : '-'}
|
||||
{displaySpeed}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="download-cell-truncate">
|
||||
<span
|
||||
key={`eta-${download.status}`}
|
||||
ref={etaTextRef}
|
||||
className="tabular-nums"
|
||||
title={download.status === 'downloading' ? download.eta : download.status === 'processing' ? 'Muxing…' : '-'}
|
||||
title={displayEta}
|
||||
>
|
||||
{download.status === 'downloading' ? download.eta : download.status === 'processing' ? 'Muxing…' : '-'}
|
||||
{displayEta}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -46,8 +46,17 @@ export async function initDownloadListener() {
|
||||
const current = mainStore.downloads.find(d => d.id === payload.id);
|
||||
if (current) {
|
||||
const shouldUpdateSize = Boolean(payload.size && (!current.isMedia || payload.size_is_final));
|
||||
const updates: Partial<DownloadItem> = {};
|
||||
if (current.status === 'downloading' || current.status === 'processing') {
|
||||
updates.fraction = payload.fraction;
|
||||
updates.speed = payload.speed;
|
||||
updates.eta = payload.eta;
|
||||
}
|
||||
if (shouldUpdateSize && current.size !== payload.size) {
|
||||
mainStore.updateDownload(payload.id, { size: payload.size! });
|
||||
updates.size = payload.size!;
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
mainStore.updateDownload(payload.id, updates);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user