From 965a6b411f085160340b7c54b228fe8092f060f2 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 19 Mar 2026 12:55:23 +0800 Subject: [PATCH] 1 --- app/index.tsx | 4 +- hooks/useDownload.ts | 137 ++++++++++++++++++++++++------------------- 2 files changed, 80 insertions(+), 61 deletions(-) diff --git a/app/index.tsx b/app/index.tsx index dcfa51c..fcdeac2 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -432,7 +432,9 @@ export default function HomeScreen() { 搜索视频、UP主... - router.push('/downloads' as any)} /> + router.push("/downloads" as any)} + /> diff --git a/hooks/useDownload.ts b/hooks/useDownload.ts index 0d7c762..0a92909 100644 --- a/hooks/useDownload.ts +++ b/hooks/useDownload.ts @@ -4,34 +4,28 @@ import { AppState } from 'react-native'; import { useDownloadStore } from '../store/downloadStore'; import { getPlayUrlForDownload } from '../services/bilibili'; -// 模块级进度节流 const lastReportedProgress: Record = {}; const QUALITY_LABELS: Record = { - 16: '360P', - 32: '480P', - 64: '720P', - 80: '1080P', - 112: '1080P+', - 116: '1080P60', + 16: '360P', 32: '480P', 64: '720P', + 80: '1080P', 112: '1080P+', 116: '1080P60', }; /** 等待 App 回到前台 */ function waitForActive(): Promise { return new Promise((resolve) => { - if (AppState.currentState === 'active') { - resolve(); - return; - } - const sub = AppState.addEventListener('change', (nextState) => { - if (nextState === 'active') { - sub.remove(); - resolve(); - } + if (AppState.currentState === 'active') { resolve(); return; } + const sub = AppState.addEventListener('change', (s) => { + if (s === 'active') { sub.remove(); resolve(); } }); }); } +/** 当前是否在后台 */ +function isBackground() { + return AppState.currentState !== 'active'; +} + /** 读取本地文件实际大小 */ async function readFileSize(uri: string): Promise { try { @@ -44,21 +38,14 @@ async function readFileSize(uri: string): Promise { export function useDownload() { const { tasks, addTask, updateTask, removeTask } = useDownloadStore(); - function taskKey(bvid: string, qn: number) { - return `${bvid}_${qn}`; - } - + function taskKey(bvid: string, qn: number) { return `${bvid}_${qn}`; } function localPath(bvid: string, qn: number) { return `${FileSystem.documentDirectory}${bvid}_${qn}.mp4`; } async function startDownload( - bvid: string, - cid: number, - qn: number, - qdesc: string, - title: string, - cover: string, + bvid: string, cid: number, qn: number, + qdesc: string, title: string, cover: string, ) { const key = taskKey(bvid, qn); if (tasks[key]?.status === 'downloading') return; @@ -66,11 +53,25 @@ export function useDownload() { addTask(key, { bvid, title, cover, qn, qdesc: qdesc || QUALITY_LABELS[qn] || String(qn), - status: 'downloading', - progress: 0, - createdAt: Date.now(), + status: 'downloading', progress: 0, createdAt: Date.now(), }); + // 最多重新拉取 URL 并重试一次(应对后台时 URL 过期的情况) + for (let attempt = 0; attempt < 2; attempt++) { + const success = await attemptDownload(key, bvid, cid, qn); + if (success !== 'retry') return; + // 需要重试:重新拉取 URL + updateTask(key, { status: 'downloading', progress: 0 }); + lastReportedProgress[key] = -1; + } + + updateTask(key, { status: 'error', error: '下载失败,请重试' }); + } + + /** 执行一次下载尝试。返回 'done' | 'error' | 'retry' */ + async function attemptDownload( + key: string, bvid: string, cid: number, qn: number, + ): Promise<'done' | 'error' | 'retry'> { try { const [url, buvid3, sessdata] = await Promise.all([ getPlayUrlForDownload(bvid, cid, qn), @@ -86,8 +87,7 @@ export function useDownload() { const headers = { Referer: 'https://www.bilibili.com', Origin: 'https://www.bilibili.com', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', ...(cookies.length > 0 && { Cookie: cookies.join('; ') }), }; @@ -104,52 +104,69 @@ export function useDownload() { const resumable = FileSystem.createDownloadResumable(url, dest, { headers }, progressCallback); - // ── 后台暂停 / 前台续传 ── - let pausedByBackground = false; - const appStateSub = AppState.addEventListener('change', async (nextState) => { - if ((nextState === 'background' || nextState === 'inactive') && !pausedByBackground) { - pausedByBackground = true; + // 进入后台时主动暂停,抢在 OS 断连之前 + let bgPaused = false; + const appStateSub = AppState.addEventListener('change', async (state) => { + if ((state === 'background' || state === 'inactive') && !bgPaused) { + bgPaused = true; try { await resumable.pauseAsync(); } catch {} } }); - let result = await resumable.downloadAsync(); + let result: FileSystem.DownloadResult | null = null; - // 如果是因为进入后台被暂停(result 为 null 或抛出连接中断),等回前台续传 - if (!result?.uri && pausedByBackground) { - await waitForActive(); - pausedByBackground = false; + try { + result = await resumable.downloadAsync(); + } catch (e: any) { + // downloadAsync 抛出:多为后台断连(connection abort)或被 pauseAsync 中断 + if (!isBackground() && !bgPaused) { + // 真实网络错误,非后台原因 + appStateSub.remove(); + delete lastReportedProgress[key]; + const msg = e?.message ?? '下载失败'; + updateTask(key, { status: 'error', error: msg.length > 40 ? msg.slice(0, 40) + '...' : msg }); + return 'error'; + } + // 后台引发的中断,走下面的续传逻辑 + result = null; + } finally { + appStateSub.remove(); + } + + // ── 续传逻辑:result 为 null 说明被暂停或中断 ── + if (!result?.uri) { + // 等 App 回到前台 + if (isBackground()) await waitForActive(); + + // 尝试从断点续传 try { result = await resumable.resumeAsync(); } catch { result = null; } + + // 续传仍失败(URL 可能过期),通知上层重试 + if (!result?.uri) { + delete lastReportedProgress[key]; + return 'retry'; + } } - appStateSub.remove(); + // ── 下载完成 ── delete lastReportedProgress[key]; - - if (result?.uri) { - const fileSize = await readFileSize(result.uri); - updateTask(key, { - status: 'done', progress: 1, localUri: result.uri, - ...(fileSize ? { fileSize } : {}), - }); - } else { - updateTask(key, { status: 'error', error: '下载失败' }); - } + const fileSize = await readFileSize(result.uri); + updateTask(key, { + status: 'done', progress: 1, localUri: result.uri, + ...(fileSize ? { fileSize } : {}), + }); + return 'done'; } catch (e: any) { delete lastReportedProgress[key]; console.error('[Download] failed:', e); - - // 连接中断 + 已被后台暂停 → 不报错,等回前台后让用户手动重试即可 - const isConnectionAbort = (e?.message ?? '').includes('connection a'); - const msg = isConnectionAbort ? '已暂停,返回应用后可重试' : (e?.message ?? '下载失败'); - updateTask(key, { - status: 'error', - error: msg.length > 40 ? msg.slice(0, 40) + '...' : msg, - }); + const msg = e?.message ?? '下载失败'; + updateTask(key, { status: 'error', error: msg.length > 40 ? msg.slice(0, 40) + '...' : msg }); + return 'error'; } }