mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix(media): preserve yt-dlp output names
This commit is contained in:
@@ -306,11 +306,14 @@ pub enum QueueDirection {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../../src/bindings/")]
|
||||
pub struct DownloadStateEvent {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub error: Option<String>,
|
||||
#[ts(optional)]
|
||||
pub file_name: Option<String>,
|
||||
}
|
||||
|
||||
impl DownloadStateEvent {
|
||||
@@ -319,6 +322,7 @@ impl DownloadStateEvent {
|
||||
id: id.into(),
|
||||
status: status.as_str().to_string(),
|
||||
error: None,
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +331,16 @@ impl DownloadStateEvent {
|
||||
id: id.into(),
|
||||
status: DownloadStatus::Failed.as_str().to_string(),
|
||||
error: Some(error.into()),
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completed_with_file(id: impl Into<String>, file_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
status: DownloadStatus::Completed.as_str().to_string(),
|
||||
error: None,
|
||||
file_name: Some(file_name.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +351,7 @@ impl DownloadStateEvent {
|
||||
id: id.into(),
|
||||
status: DownloadStatus::Retrying.as_str().to_string(),
|
||||
error: Some(reason.into()),
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+69
-22
@@ -65,6 +65,18 @@ fn is_media_processing_line(line: &str) -> bool {
|
||||
|| lower.contains("post-process")
|
||||
}
|
||||
|
||||
fn media_output_template(
|
||||
resolved_dest: &std::path::Path,
|
||||
safe_filename: &str,
|
||||
format_selector: Option<&str>,
|
||||
) -> std::path::PathBuf {
|
||||
if format_selector.is_none() && std::path::Path::new(safe_filename).extension().is_none() {
|
||||
resolved_dest.join("%(title).200B [%(id)s].%(ext)s")
|
||||
} else {
|
||||
resolved_dest.join(safe_filename)
|
||||
}
|
||||
}
|
||||
|
||||
fn json_str<'a>(value: &'a serde_json::Value, key: &str) -> Option<&'a str> {
|
||||
value
|
||||
.get(key)
|
||||
@@ -2399,7 +2411,7 @@ pub(crate) async fn start_media_download_internal(
|
||||
user_agent: Option<String>,
|
||||
max_tries: Option<i32>,
|
||||
cancel_rx: &mut tokio::sync::watch::Receiver<bool>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<std::path::PathBuf, String> {
|
||||
let safe_filename = crate::download_ownership::canonical_download_filename(&filename);
|
||||
|
||||
let resolved_dest = resolve_path(&destination, &app_handle);
|
||||
@@ -2413,6 +2425,8 @@ pub(crate) async fn start_media_download_internal(
|
||||
}
|
||||
|
||||
let out_path = resolved_dest.join(&safe_filename);
|
||||
let output_template =
|
||||
media_output_template(&resolved_dest, &safe_filename, format_selector.as_deref());
|
||||
|
||||
let total_tracks: f64 = if let Some(ref format) = format_selector {
|
||||
if format.contains('+') {
|
||||
@@ -2519,8 +2533,10 @@ pub(crate) async fn start_media_download_internal(
|
||||
.arg("--continue")
|
||||
.arg("--compat-options")
|
||||
.arg("no-youtube-unavailable-videos")
|
||||
.arg("--print")
|
||||
.arg("after_move:%(filepath)s")
|
||||
.arg("-o")
|
||||
.arg(out_path.to_string_lossy().to_string())
|
||||
.arg(output_template.to_string_lossy().to_string())
|
||||
.env("PATH", &trusted_path);
|
||||
|
||||
if let Some(limit) = speed_limit.as_ref() {
|
||||
@@ -2611,6 +2627,7 @@ pub(crate) async fn start_media_download_internal(
|
||||
log::info!("yt-dlp spawned for id: {} (strike {})", id, strike);
|
||||
|
||||
let mut stderr_tail = String::new();
|
||||
let mut final_output_path: Option<std::path::PathBuf> = None;
|
||||
let failure_reason = loop {
|
||||
tokio::select! {
|
||||
_ = cancel_rx.changed() => {
|
||||
@@ -2626,30 +2643,38 @@ pub(crate) async fn start_media_download_internal(
|
||||
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 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,
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(tauri_plugin_shell::process::CommandEvent::Stderr(line_bytes)) => {
|
||||
@@ -2713,8 +2738,13 @@ 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 {
|
||||
log::info!("yt-dlp completed successfully id: {}", id);
|
||||
let completed_path = final_output_path
|
||||
.as_ref()
|
||||
.filter(|path| path.is_file())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| out_path.clone());
|
||||
if let Ok(metadata) = tokio::fs::metadata(&completed_path).await {
|
||||
if metadata.is_file() {
|
||||
let _ = app_handle.emit("download-progress", DownloadProgressEvent {
|
||||
id: id.to_string(),
|
||||
@@ -2726,7 +2756,7 @@ pub(crate) async fn start_media_download_internal(
|
||||
});
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
return Ok(completed_path);
|
||||
}
|
||||
log::error!("yt-dlp exited with non-zero code {:?} for id: {}", payload.code, id);
|
||||
cleanup_media_artifacts(&out_path, false).await;
|
||||
@@ -4199,13 +4229,30 @@ fn set_extension_frontend_ready(state: tauri::State<'_, AppState>, ready: bool)
|
||||
mod tests {
|
||||
use super::{
|
||||
aggregate_media_fraction, build_media_format_options, collect_download_uris,
|
||||
is_excluded_yt_dlp_format, json_lower, media_progress_speed,
|
||||
is_excluded_yt_dlp_format, json_lower, media_output_template, media_progress_speed,
|
||||
normalize_speed_limit_for_aria2, parse_firelink_deep_link, parse_media_progress_line,
|
||||
redact_log_line, FirelinkDeepLink, MediaProgress, MEDIA_PROGRESS_PREFIX,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[test]
|
||||
fn media_metadata_fallback_lets_ytdlp_choose_extension() {
|
||||
let destination = std::path::Path::new("/tmp/firelink");
|
||||
let template = media_output_template(destination, "1234567890", None);
|
||||
assert_eq!(
|
||||
template,
|
||||
destination.join("%(title).200B [%(id)s].%(ext)s")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_media_format_keeps_requested_output_path() {
|
||||
let destination = std::path::Path::new("/tmp/firelink");
|
||||
let template = media_output_template(destination, "clip.mp4", Some("best"));
|
||||
assert_eq!(template, destination.join("clip.mp4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_bare_global_speed_limits_as_kib_per_second() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1221,20 +1221,21 @@ impl SidecarSpawner for ProductionSpawner {
|
||||
&mut cancel_rx,
|
||||
)
|
||||
.await;
|
||||
if outcome.is_ok() {
|
||||
if let Ok(path) = crate::download_ownership::expected_primary_path(
|
||||
&self.app_handle,
|
||||
&payload.destination,
|
||||
&payload.filename,
|
||||
) {
|
||||
let _ = crate::download_ownership::set_primary_path(&self.app_handle, id, &path);
|
||||
if let Ok(path) = outcome.as_ref() {
|
||||
let _ = crate::download_ownership::set_primary_path(&self.app_handle, id, path);
|
||||
if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) {
|
||||
use tauri::Emitter;
|
||||
let _ = self.app_handle.emit(
|
||||
"download-state",
|
||||
crate::ipc::DownloadStateEvent::completed_with_file(id, file_name),
|
||||
);
|
||||
}
|
||||
}
|
||||
let _ = state
|
||||
.download_coordinator
|
||||
.finish_media(id.to_string())
|
||||
.await;
|
||||
outcome
|
||||
outcome.map(|_| ())
|
||||
}
|
||||
|
||||
async fn run_native(&self, id: &str, payload: &SpawnPayload) -> Result<(), String> {
|
||||
|
||||
@@ -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 DownloadStateEvent = { id: string, status: string, error: string | null, };
|
||||
export type DownloadStateEvent = { id: string, status: string, error: string | null, fileName?: string, };
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { DownloadProgressEvent } from '../bindings/DownloadProgressEvent';
|
||||
import type { DownloadStatus } from '../bindings/DownloadStatus';
|
||||
import { listenEvent as listen } from '../ipc';
|
||||
import type { DownloadItem } from '../bindings/DownloadItem';
|
||||
import { categoryForFileName } from '../utils/downloads';
|
||||
|
||||
interface DownloadProgressState {
|
||||
progressMap: Record<string, DownloadProgressEvent>;
|
||||
@@ -70,6 +71,10 @@ export async function initDownloadListener() {
|
||||
status,
|
||||
...(progress ? { fraction: progress.fraction } : {})
|
||||
};
|
||||
if (payload.fileName && payload.fileName !== current.fileName) {
|
||||
updates.fileName = payload.fileName;
|
||||
updates.category = categoryForFileName(payload.fileName);
|
||||
}
|
||||
if (status !== 'downloading') {
|
||||
updates.speed = '-';
|
||||
updates.eta = '-';
|
||||
|
||||
Reference in New Issue
Block a user