{"record":{"id":"69f4bef123361159","repo":"tonhowtf/omniget","slug":"download-cancelled-legacy","errorCode":null,"errorMessage":"Download cancelled","messagePattern":"Download cancelled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"src-tauri/omniget-core/src/platforms/bilibili/legacy.rs","lineNumber":270,"sourceCode":"async fn download_playlist(\n    info: &MediaInfo,\n    opts: &DownloadOptions,\n    progress: mpsc::Sender<ProgressUpdate>,\n    ytdlp_path: &std::path::Path,\n) -> anyhow::Result<DownloadResult> {\n    let total = info.available_qualities.len().max(1);\n    let mut last_result = DownloadResult {\n        file_path: opts.output_dir.clone(),\n        file_size_bytes: 0,\n        duration_seconds: 0.0,\n        torrent_id: None,\n    };\n    let mut success_count: usize = 0;\n    let mut first_error: Option<anyhow::Error> = None;\n\n    for (i, quality) in info.available_qualities.iter().enumerate() {\n        if opts.cancel_token.is_cancelled() {\n            return Err(anyhow!(\"Download cancelled\"));\n        }\n\n        let (entry_tx, mut entry_rx) = mpsc::channel::<ProgressUpdate>(16);\n        let progress_clone = progress.clone();\n        let total_f = total as f64;\n        let idx = i as f64;\n\n        tokio::spawn(async move {\n            while let Some(p) = entry_rx.recv().await {\n                let overall = (idx + p.percent / 100.0) / total_f * 100.0;\n                let _ = progress_clone\n                    .send(ProgressUpdate::rich(overall, None, None, p.speed_bps, None))\n                    .await;\n            }\n        });\n\n        let extra = vec![\"--no-playlist\".to_string()];\n","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/omniget-core/src/platforms/bilibili/legacy.rs#L252-L288","documentation":"download_playlist() checks a shared cancellation token before processing each playlist item; if the user (or the app) cancelled the operation, it aborts the whole playlist with 'Download cancelled'. This is a normal control-flow error used to unwind a long-running download, not a malfunction.","triggerScenarios":"Calling download() on a playlist-type bilibili MediaInfo while opts.cancel_token.is_cancelled() becomes true between playlist items.","commonSituations":"User clicks Cancel in the UI mid-playlist; the app shuts down and cancels in-flight tasks; a caller drops/short-circuits and cancels the token after a timeout.","solutions":["Treat this error as expected cancellation: catch it and stop without retrying or surfacing as a failure to the user.","If cancellation is unintentional, ensure the cancel token is not shared with another long-lived operation and is reset before starting a new download.","Persist completed item count (success_count) so a retry can resume from where it left off."],"exampleFix":"// before\nmatch download(...).await {\n    Err(e) => show_error(e),\n    Ok(r) => show_done(r),\n}\n// after\nmatch download(...).await {\n    Err(e) if e.to_string().contains(\"Download cancelled\") => show_cancelled(),\n    Err(e) => show_error(e),\n    Ok(r) => show_done(r),\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match download(...).await {\n    Err(e) if e.to_string().contains(\"Download cancelled\") => { /* treat as normal stop */ }\n    Err(e) => report_error(e),\n    Ok(r) => report_done(r),\n}","preventionTips":["Never reuse a cancel token that may already be cancelled for a new download.","Reset the cancel token before starting each download run.","Track success_count to enable resumable retries after cancellation."],"tags":["bilibili","cancellation","playlist","control-flow"],"backgroundTag":"operation-cancelled","analyzedSha":"8600b91f4246848bac346874daa9e61c1fc5677a","analyzedAt":"2026-09-12T14:29:19.317Z","contentChangedAt":"2026-09-12T14:29:19.317Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}