mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
1
This commit is contained in:
@@ -432,7 +432,9 @@ export default function HomeScreen() {
|
||||
<Ionicons name="search" size={14} color="#999" />
|
||||
<Text style={styles.searchPlaceholder}>搜索视频、UP主...</Text>
|
||||
</TouchableOpacity>
|
||||
<DownloadProgressBtn onPress={() => router.push('/downloads' as any)} />
|
||||
<DownloadProgressBtn
|
||||
onPress={() => router.push("/downloads" as any)}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
<View style={styles.tabRow}>
|
||||
|
||||
@@ -4,34 +4,28 @@ import { AppState } from 'react-native';
|
||||
import { useDownloadStore } from '../store/downloadStore';
|
||||
import { getPlayUrlForDownload } from '../services/bilibili';
|
||||
|
||||
// 模块级进度节流
|
||||
const lastReportedProgress: Record<string, number> = {};
|
||||
|
||||
const QUALITY_LABELS: Record<number, string> = {
|
||||
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<void> {
|
||||
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<number | undefined> {
|
||||
try {
|
||||
@@ -44,21 +38,14 @@ async function readFileSize(uri: string): Promise<number | undefined> {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user