mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix(downloads): harden add modal and retry state
Route explicit no-limit speed overrides without inheriting the global cap. Preserve dotted media titles when switching formats and drain retry gid completions through remember_gid.
This commit is contained in:
+13
-12
@@ -2837,10 +2837,11 @@ pub(crate) async fn start_media_download_internal(
|
||||
.arg(output_template.to_string_lossy().to_string())
|
||||
.env("PATH", &trusted_path);
|
||||
|
||||
if let Some(limit) = speed_limit.as_ref() {
|
||||
if !limit.is_empty() {
|
||||
cmd = cmd.arg("--limit-rate").arg(limit);
|
||||
}
|
||||
if let Some(limit) = speed_limit
|
||||
.as_deref()
|
||||
.and_then(normalize_speed_limit_for_aria2)
|
||||
{
|
||||
cmd = cmd.arg("--limit-rate").arg(limit);
|
||||
}
|
||||
|
||||
if let Some(p) = proxy.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
|
||||
@@ -3280,6 +3281,13 @@ async fn resume_download(
|
||||
queue_manager.release_permit(&id_clone).await;
|
||||
return;
|
||||
}
|
||||
let _ = app_handle_clone.emit(
|
||||
"download-state",
|
||||
crate::ipc::DownloadStateEvent::new(
|
||||
&id_clone,
|
||||
crate::ipc::DownloadStatus::Downloading,
|
||||
),
|
||||
);
|
||||
let result = match rpc_call(
|
||||
aria2_port,
|
||||
&aria2_secret,
|
||||
@@ -3332,13 +3340,6 @@ async fn resume_download(
|
||||
return;
|
||||
}
|
||||
log::info!("aria2 resume [{}]: unpaused gid {}", id_clone, gid_clone);
|
||||
let _ = app_handle_clone.emit(
|
||||
"download-state",
|
||||
crate::ipc::DownloadStateEvent::new(
|
||||
&id_clone,
|
||||
crate::ipc::DownloadStatus::Downloading,
|
||||
),
|
||||
);
|
||||
});
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -3970,7 +3971,7 @@ async fn set_concurrent_limit(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_speed_limit_for_aria2(limit: &str) -> Option<String> {
|
||||
pub(crate) fn normalize_speed_limit_for_aria2(limit: &str) -> Option<String> {
|
||||
let trimmed = limit.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
|
||||
+9
-19
@@ -596,20 +596,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
removed.last().cloned()
|
||||
}
|
||||
|
||||
/// Overwrite a stale aria2 gid with the fresh gid minted by a retry
|
||||
/// `addUri`. Failing to call this after re-add leaks the semaphore permit.
|
||||
pub fn rotate_aria2_gid(&self, id: &str, stale_gid: &str, new_gid: &str) {
|
||||
let mut gids = self.aria2_gids.write().unwrap();
|
||||
gids.remove(stale_gid);
|
||||
gids.insert(new_gid.to_string(), id.to_string());
|
||||
log::info!(
|
||||
"aria2 gid transition [{}]: rotated {} -> {}",
|
||||
id,
|
||||
stale_gid,
|
||||
new_gid
|
||||
);
|
||||
}
|
||||
|
||||
async fn wait_permit_released(self: &Arc<Self>, id: &str) {
|
||||
loop {
|
||||
if !self.active_permits.lock().await.contains_key(id) {
|
||||
@@ -695,7 +681,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
}
|
||||
|
||||
let this = Arc::clone(self);
|
||||
let stale_gid = gid.to_string();
|
||||
let id_for_task = id.clone();
|
||||
let error_for_emit = error.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -752,11 +737,12 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.rotate_aria2_gid(&id_for_task, &stale_gid, &new_gid);
|
||||
let retained_gid = new_gid.clone();
|
||||
this.remember_gid(id_for_task.clone(), new_gid).await;
|
||||
log::warn!(
|
||||
"aria2 retry cancellation [{}]: retained late gid {} mapping for remove retry",
|
||||
id_for_task,
|
||||
new_gid
|
||||
retained_gid
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -764,8 +750,8 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
.lock()
|
||||
.await
|
||||
.insert(id_for_task.clone(), strike + 1);
|
||||
this.rotate_aria2_gid(&id_for_task, &stale_gid, &new_gid);
|
||||
this.emit_state(&id_for_task, DownloadStatus::Downloading);
|
||||
this.remember_gid(id_for_task.clone(), new_gid).await;
|
||||
}
|
||||
Err(retry_error) => {
|
||||
this.apply_completion(&id_for_task, PendingOutcome::Error(retry_error))
|
||||
@@ -1204,7 +1190,11 @@ impl SidecarSpawner for ProductionSpawner {
|
||||
options.insert("max-tries".to_string(), serde_json::json!(mt.to_string()));
|
||||
options.insert("retry-wait".to_string(), serde_json::json!("2"));
|
||||
options.insert("continue".to_string(), serde_json::json!("true"));
|
||||
if let Some(speed) = &payload.speed_limit {
|
||||
if let Some(speed) = payload
|
||||
.speed_limit
|
||||
.as_deref()
|
||||
.and_then(crate::normalize_speed_limit_for_aria2)
|
||||
{
|
||||
options.insert("max-download-limit".to_string(), serde_json::json!(speed));
|
||||
}
|
||||
if let Some(user) = &payload.username {
|
||||
|
||||
@@ -18,15 +18,14 @@
|
||||
//! ### aria2 GID-rotation contract (CRITICAL)
|
||||
//!
|
||||
//! When aria2 retries via a fresh `aria2.addUri`, it mints a **brand-new GID**.
|
||||
//! The caller MUST overwrite the stale GID → download-id mapping in
|
||||
//! `QueueManager::aria2_gids` with the new GID on every successful re-add.
|
||||
//! Failing to do so detaches subsequent `onDownloadComplete` /
|
||||
//! `onDownloadError` WebSocket events from the original id, which leaks the
|
||||
//! semaphore permit permanently. Concretely, after every retry-driven
|
||||
//! `addUri` that returns `new_gid`:
|
||||
//! The caller MUST store the new GID through `QueueManager::remember_gid` on
|
||||
//! every successful re-add. Failing to do so detaches subsequent
|
||||
//! `onDownloadComplete` / `onDownloadError` WebSocket events from the original
|
||||
//! id, or strands a terminal event that arrived before the fresh GID was stored.
|
||||
//! Concretely, after every retry-driven `addUri` that returns `new_gid`:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! queue_manager.rotate_aria2_gid(&id, &stale_gid, &new_gid);
|
||||
//! queue_manager.remember_gid(id.clone(), new_gid).await;
|
||||
//! ```
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
Reference in New Issue
Block a user