增加离线下载功能

This commit is contained in:
mtvpls
2025-12-14 00:22:38 +08:00
parent d467c5067b
commit e711d165eb
11 changed files with 2039 additions and 18 deletions
+107 -9
View File
@@ -9,6 +9,7 @@ import { Suspense, useEffect, useRef, useState } from 'react';
import { usePlaySync } from '@/hooks/usePlaySync';
import { getDoubanDetail } from '@/lib/douban.client';
import { useDownload } from '@/contexts/DownloadContext';
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
import {
deleteFavorite,
@@ -71,6 +72,15 @@ function PlayPageClient() {
// 获取 Proxy M3U8 Token
const proxyToken = typeof window !== 'undefined' ? process.env.NEXT_PUBLIC_PROXY_M3U8_TOKEN || '' : '';
// 获取用户认证信息
const authInfo = typeof window !== 'undefined' ? getAuthInfoFromBrowserCookie() : null;
// 离线下载功能配置
const enableOfflineDownload = typeof window !== 'undefined'
? process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true'
: false;
const hasOfflinePermission = authInfo?.role === 'owner' || authInfo?.role === 'admin';
// -----------------------------------------------------------------------------
// 状态变量(State
// -----------------------------------------------------------------------------
@@ -739,8 +749,34 @@ function PlayPageClient() {
return Math.round(score * 100) / 100; // 保留两位小数
};
// 检查是否有本地下载的视频
const checkLocalDownload = async (
source: string,
videoId: string,
episodeIndex: number
): Promise<boolean> => {
if (!enableOfflineDownload || !hasOfflinePermission) {
return false;
}
try {
const response = await fetch(
`/api/offline-download?action=check&source=${encodeURIComponent(source)}&videoId=${encodeURIComponent(videoId)}&episodeIndex=${episodeIndex}`
);
if (response.ok) {
const data = await response.json();
return data.downloaded || false;
}
} catch (error) {
console.error('检查本地下载失败:', error);
}
return false;
};
// 更新视频地址
const updateVideoUrl = (
const updateVideoUrl = async (
detailData: SearchResult | null,
episodeIndex: number
) => {
@@ -752,14 +788,25 @@ function PlayPageClient() {
setVideoUrl('');
return;
}
const newUrl = detailData?.episodes[episodeIndex] || '';
let newUrl = detailData?.episodes[episodeIndex] || '';
// 检查是否有本地下载的文件
const hasLocalFile = await checkLocalDownload(currentSource, currentId, episodeIndex);
if (hasLocalFile) {
// 使用本地代理接口,URL以.m3u8结尾以便Artplayer自动识别
newUrl = `/api/offline-download/local/${currentSource}/${currentId}/${episodeIndex}/playlist.m3u8`;
console.log('使用本地下载文件播放:', newUrl);
}
if (newUrl !== videoUrl) {
setVideoUrl(newUrl);
}
};
// 处理下载指定集数(支持批量下载)
const handleDownloadEpisode = async (episodeIndexes: number[]) => {
const handleDownloadEpisode = async (episodeIndexes: number[], offlineMode = false) => {
if (!detail || !detail.episodes || episodeIndexes.length === 0) {
if (artPlayerRef.current) {
artPlayerRef.current.notice.show = '无法获取视频地址';
@@ -781,12 +828,55 @@ function PlayPageClient() {
}
const episodeUrl = detail.episodes[episodeIndex];
const proxyUrl = externalPlayerAdBlock
? `${origin}/api/proxy-m3u8?url=${encodeURIComponent(episodeUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}`
: episodeUrl;
// 离线下载模式:无论是否开启去广告,都走非去广告逻辑
const proxyUrl = offlineMode
? episodeUrl // 离线下载不使用代理,直接使用原始URL
: (externalPlayerAdBlock
? `${origin}/api/proxy-m3u8?url=${encodeURIComponent(episodeUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}`
: episodeUrl);
const isM3u8 = episodeUrl.toLowerCase().includes('.m3u8') || episodeUrl.toLowerCase().includes('/m3u8/');
if (isM3u8) {
if (offlineMode && isM3u8) {
// 离线下载模式 - 调用服务器API
try {
const downloadTitle = `${videoTitle}_第${episodeIndex + 1}`;
const response = await fetch('/api/offline-download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: currentSource,
videoId: currentId,
episodeIndex,
title: downloadTitle,
m3u8Url: proxyUrl,
metadata: detail ? {
videoTitle: detail.title,
cover: detail.poster,
description: detail.desc,
year: detail.year,
rating: undefined, // SearchResult 没有 rating 字段
totalEpisodes: detail.episodes?.length,
} : undefined,
}),
});
const data = await response.json();
if (response.ok) {
successCount++;
} else {
console.error(`离线下载任务创建失败 (第${episodeIndex + 1}集):`, data.error);
failCount++;
}
} catch (error) {
console.error(`离线下载任务创建失败 (第${episodeIndex + 1}集):`, error);
failCount++;
}
} else if (isM3u8) {
// M3U8格式 - 使用新的下载器,TS 格式
try {
const downloadTitle = `${videoTitle}_第${episodeIndex + 1}`;
@@ -819,7 +909,9 @@ function PlayPageClient() {
// 显示结果通知
if (artPlayerRef.current) {
if (failCount === 0) {
artPlayerRef.current.notice.show = `已添加 ${successCount} 个下载任务!`;
artPlayerRef.current.notice.show = offlineMode
? `已创建 ${successCount} 个离线下载任务!`
: `已添加 ${successCount} 个下载任务!`;
} else if (successCount === 0) {
artPlayerRef.current.notice.show = '下载失败,请重试';
} else {
@@ -2692,6 +2784,10 @@ function PlayPageClient() {
if (video.hls) {
video.hls.destroy();
}
// 每次创建HLS实例时,都读取最新的blockAdEnabled状态
const shouldUseCustomLoader = blockAdEnabledRef.current;
const hls = new Hls({
debug: false, // 关闭日志
enableWorker: true, // WebWorker 解码,降低主线程压力
@@ -2703,7 +2799,7 @@ function PlayPageClient() {
maxBufferSize: 60 * 1000 * 1000, // 约 60MB,超出后触发清理
/* 自定义loader */
loader: (blockAdEnabledRef.current
loader: (shouldUseCustomLoader
? CustomHlsJsLoader
: Hls.DefaultConfig.loader) as any,
});
@@ -4171,6 +4267,8 @@ function PlayPageClient() {
videoTitle={videoTitle}
currentEpisodeIndex={currentEpisodeIndex}
onDownload={handleDownloadEpisode}
enableOfflineDownload={enableOfflineDownload}
hasOfflinePermission={hasOfflinePermission}
/>
{/* 弹幕过滤设置对话框 */}