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';
}
}