diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 11d2095..d4a439c 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -358,6 +358,7 @@ interface DataSource {
detail?: string;
disabled?: boolean;
from: 'config' | 'custom';
+ proxyMode?: boolean;
}
// 直播源数据类型
@@ -3490,6 +3491,53 @@ const VideoSourceConfig = ({
});
};
+ const handleToggleProxyMode = (key: string) => {
+ const target = sources.find((s) => s.key === key);
+ if (!target) return;
+
+ // 更新本地状态
+ setSources((prev) =>
+ prev.map((s) =>
+ s.key === key ? { ...s, proxyMode: !s.proxyMode } : s
+ )
+ );
+
+ // 调用API更新
+ withLoading(`toggleProxyMode_${key}`, async () => {
+ try {
+ const response = await fetch('/api/admin/source', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ action: 'toggle_proxy_mode',
+ key,
+ }),
+ });
+
+ if (!response.ok) {
+ const data = await response.json().catch(() => ({}));
+ throw new Error(data.error || `操作失败: ${response.status}`);
+ }
+
+ await refreshConfig();
+ } catch (error) {
+ // 失败时回滚本地状态
+ setSources((prev) =>
+ prev.map((s) =>
+ s.key === key ? { ...s, proxyMode: !s.proxyMode } : s
+ )
+ );
+ showError(
+ error instanceof Error ? error.message : '切换代理模式失败',
+ showAlert
+ );
+ throw error;
+ }
+ }).catch(() => {
+ console.error('操作失败', 'toggle_proxy_mode', key);
+ });
+ };
+
const handleAddSource = () => {
if (!newSource.name || !newSource.key || !newSource.api) return;
withLoading('addSource', async () => {
@@ -3777,6 +3825,31 @@ const VideoSourceConfig = ({
{!source.disabled ? '启用中' : '已禁用'}
+
+
+ |
{(() => {
const status = getValidationStatus(source.key);
@@ -4128,6 +4201,9 @@ const VideoSourceConfig = ({
|
状态
|
+
+ 代理模式
+ |
有效性
|
diff --git a/src/app/api/admin/source/route.ts b/src/app/api/admin/source/route.ts
index f5c9ca4..f980255 100644
--- a/src/app/api/admin/source/route.ts
+++ b/src/app/api/admin/source/route.ts
@@ -9,7 +9,7 @@ import { db } from '@/lib/db';
export const runtime = 'nodejs';
// 支持的操作类型
-type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort' | 'batch_disable' | 'batch_enable' | 'batch_delete';
+type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort' | 'batch_disable' | 'batch_enable' | 'batch_delete' | 'toggle_proxy_mode';
interface BaseBody {
action?: Action;
@@ -37,7 +37,7 @@ export async function POST(request: NextRequest) {
const username = authInfo.username;
// 基础校验
- const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort', 'batch_disable', 'batch_enable', 'batch_delete'];
+ const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort', 'batch_disable', 'batch_enable', 'batch_delete', 'toggle_proxy_mode'];
if (!username || !action || !ACTIONS.includes(action)) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -226,6 +226,16 @@ export async function POST(request: NextRequest) {
adminConfig.SourceConfig = newList;
break;
}
+ case 'toggle_proxy_mode': {
+ const { key } = body as { key?: string };
+ if (!key)
+ return NextResponse.json({ error: '缺少 key 参数' }, { status: 400 });
+ const entry = adminConfig.SourceConfig.find((s) => s.key === key);
+ if (!entry)
+ return NextResponse.json({ error: '源不存在' }, { status: 404 });
+ entry.proxyMode = !entry.proxyMode;
+ break;
+ }
default:
return NextResponse.json({ error: '未知操作' }, { status: 400 });
}
diff --git a/src/app/api/detail/route.ts b/src/app/api/detail/route.ts
index bc043f3..278db00 100644
--- a/src/app/api/detail/route.ts
+++ b/src/app/api/detail/route.ts
@@ -143,6 +143,7 @@ export async function GET(request: NextRequest) {
desc: folderMeta?.overview || '',
episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(folderName)}&fileName=${encodeURIComponent(ep.fileName)}`),
episodes_titles: episodes.map((ep) => ep.title),
+ proxyMode: false, // openlist 源不使用代理模式
};
return NextResponse.json(result);
@@ -167,9 +168,16 @@ export async function GET(request: NextRequest) {
}
const result = await getDetailFromApi(apiSite, id);
+
+ // 添加 proxyMode 到返回结果
+ const resultWithProxy = {
+ ...result,
+ proxyMode: apiSite.proxyMode || false,
+ };
+
const cacheTime = await getCacheTime();
- return NextResponse.json(result, {
+ return NextResponse.json(resultWithProxy, {
headers: {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
diff --git a/src/app/api/proxy/vod/key/route.ts b/src/app/api/proxy/vod/key/route.ts
new file mode 100644
index 0000000..eb7762d
--- /dev/null
+++ b/src/app/api/proxy/vod/key/route.ts
@@ -0,0 +1,58 @@
+/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
+
+import { NextResponse } from "next/server";
+
+import { getConfig } from "@/lib/config";
+
+export const runtime = 'nodejs';
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const url = searchParams.get('url');
+ const source = searchParams.get('source');
+
+ if (!url) {
+ return NextResponse.json({ error: 'Missing url' }, { status: 400 });
+ }
+
+ if (!source) {
+ return NextResponse.json({ error: 'Missing source' }, { status: 400 });
+ }
+
+ // 检查该视频源是否启用了代理模式
+ const config = await getConfig();
+ const videoSource = config.SourceConfig?.find((s: any) => s.key === source);
+
+ if (!videoSource) {
+ return NextResponse.json({ error: 'Source not found' }, { status: 404 });
+ }
+
+ if (!videoSource.proxyMode) {
+ return NextResponse.json({ error: 'Proxy mode not enabled for this source' }, { status: 403 });
+ }
+
+ try {
+ const decodedUrl = decodeURIComponent(url);
+ const response = await fetch(decodedUrl, {
+ headers: {
+ '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',
+ 'Referer': decodedUrl,
+ },
+ });
+
+ if (!response.ok) {
+ return NextResponse.json({ error: 'Failed to fetch key' }, { status: 500 });
+ }
+
+ const headers = new Headers();
+ headers.set('Content-Type', response.headers.get('Content-Type') || 'application/octet-stream');
+ headers.set('Access-Control-Allow-Origin', '*');
+ headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
+ headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
+ headers.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
+
+ return new Response(response.body, { headers });
+ } catch (error) {
+ return NextResponse.json({ error: 'Failed to fetch key' }, { status: 500 });
+ }
+}
diff --git a/src/app/api/proxy/vod/m3u8/route.ts b/src/app/api/proxy/vod/m3u8/route.ts
new file mode 100644
index 0000000..9e3590e
--- /dev/null
+++ b/src/app/api/proxy/vod/m3u8/route.ts
@@ -0,0 +1,191 @@
+/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
+
+import { NextResponse } from "next/server";
+
+import { getConfig } from "@/lib/config";
+import { getBaseUrl, resolveUrl } from "@/lib/live";
+
+export const runtime = 'nodejs';
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const url = searchParams.get('url');
+ const source = searchParams.get('source'); // 视频源key
+
+ if (!url) {
+ return NextResponse.json({ error: 'Missing url' }, { status: 400 });
+ }
+
+ if (!source) {
+ return NextResponse.json({ error: 'Missing source' }, { status: 400 });
+ }
+
+ // 检查该视频源是否启用了代理模式
+ const config = await getConfig();
+ const videoSource = config.SourceConfig?.find((s: any) => s.key === source);
+
+ if (!videoSource) {
+ return NextResponse.json({ error: 'Source not found' }, { status: 404 });
+ }
+
+ if (!videoSource.proxyMode) {
+ return NextResponse.json({ error: 'Proxy mode not enabled for this source' }, { status: 403 });
+ }
+
+ let response: Response | null = null;
+ let responseUsed = false;
+
+ try {
+ const decodedUrl = decodeURIComponent(url);
+
+ response = await fetch(decodedUrl, {
+ cache: 'no-cache',
+ redirect: 'follow',
+ credentials: 'same-origin',
+ headers: {
+ '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',
+ 'Referer': decodedUrl,
+ },
+ });
+
+ if (!response.ok) {
+ return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
+ }
+
+ const contentType = response.headers.get('Content-Type') || '';
+ // rewrite m3u8
+ if (contentType.toLowerCase().includes('mpegurl') || contentType.toLowerCase().includes('octet-stream') || decodedUrl.includes('.m3u8')) {
+ // 获取最终的响应URL(处理重定向后的URL)
+ const finalUrl = response.url;
+ const m3u8Content = await response.text();
+ responseUsed = true; // 标记 response 已被使用
+
+ // 使用最终的响应URL作为baseUrl,而不是原始的请求URL
+ const baseUrl = getBaseUrl(finalUrl);
+
+ // 重写 M3U8 内容
+ const modifiedContent = rewriteM3U8Content(m3u8Content, baseUrl, request, source);
+
+ const headers = new Headers();
+ headers.set('Content-Type', contentType || 'application/vnd.apple.mpegurl');
+ headers.set('Access-Control-Allow-Origin', '*');
+ headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
+ headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
+ headers.set('Cache-Control', 'no-cache');
+ headers.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
+ return new Response(modifiedContent, { headers });
+ }
+ // just proxy
+ const headers = new Headers();
+ headers.set('Content-Type', response.headers.get('Content-Type') || 'application/vnd.apple.mpegurl');
+ headers.set('Access-Control-Allow-Origin', '*');
+ headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
+ headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
+ headers.set('Cache-Control', 'no-cache');
+ headers.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
+
+ // 直接返回视频流
+ return new Response(response.body, {
+ status: 200,
+ headers,
+ });
+ } catch (error) {
+ return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
+ } finally {
+ // 确保 response 被正确关闭以释放资源
+ if (response && !responseUsed) {
+ try {
+ response.body?.cancel();
+ } catch (error) {
+ // 忽略关闭时的错误
+ console.warn('Failed to close response body:', error);
+ }
+ }
+ }
+}
+
+function rewriteM3U8Content(content: string, baseUrl: string, req: Request, source: string) {
+ // 从 referer 头提取协议信息
+ const referer = req.headers.get('referer');
+ let protocol = 'http';
+ if (referer) {
+ try {
+ const refererUrl = new URL(referer);
+ protocol = refererUrl.protocol.replace(':', '');
+ } catch (error) {
+ // ignore
+ }
+ }
+
+ const host = req.headers.get('host');
+ const proxyBase = `${protocol}://${host}/api/proxy/vod`;
+
+ const lines = content.split('\n');
+ const rewrittenLines: string[] = [];
+
+ for (let i = 0; i < lines.length; i++) {
+ let line = lines[i].trim();
+
+ // 处理 TS 片段 URL 和其他媒体文件
+ if (line && !line.startsWith('#')) {
+ const resolvedUrl = resolveUrl(baseUrl, line);
+ const proxyUrl = `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}&source=${source}`;
+ rewrittenLines.push(proxyUrl);
+ continue;
+ }
+
+ // 处理 EXT-X-MAP 标签中的 URI
+ if (line.startsWith('#EXT-X-MAP:')) {
+ line = rewriteMapUri(line, baseUrl, proxyBase, source);
+ }
+
+ // 处理 EXT-X-KEY 标签中的 URI
+ if (line.startsWith('#EXT-X-KEY:')) {
+ line = rewriteKeyUri(line, baseUrl, proxyBase, source);
+ }
+
+ // 处理嵌套的 M3U8 文件 (EXT-X-STREAM-INF)
+ if (line.startsWith('#EXT-X-STREAM-INF:')) {
+ rewrittenLines.push(line);
+ // 下一行通常是 M3U8 URL
+ if (i + 1 < lines.length) {
+ i++;
+ const nextLine = lines[i].trim();
+ if (nextLine && !nextLine.startsWith('#')) {
+ const resolvedUrl = resolveUrl(baseUrl, nextLine);
+ const proxyUrl = `${proxyBase}/m3u8?url=${encodeURIComponent(resolvedUrl)}&source=${source}`;
+ rewrittenLines.push(proxyUrl);
+ } else {
+ rewrittenLines.push(nextLine);
+ }
+ }
+ continue;
+ }
+
+ rewrittenLines.push(line);
+ }
+
+ return rewrittenLines.join('\n');
+}
+
+function rewriteMapUri(line: string, baseUrl: string, proxyBase: string, source: string) {
+ const uriMatch = line.match(/URI="([^"]+)"/);
+ if (uriMatch) {
+ const originalUri = uriMatch[1];
+ const resolvedUrl = resolveUrl(baseUrl, originalUri);
+ const proxyUrl = `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}&source=${source}`;
+ return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
+ }
+ return line;
+}
+
+function rewriteKeyUri(line: string, baseUrl: string, proxyBase: string, source: string) {
+ const uriMatch = line.match(/URI="([^"]+)"/);
+ if (uriMatch) {
+ const originalUri = uriMatch[1];
+ const resolvedUrl = resolveUrl(baseUrl, originalUri);
+ const proxyUrl = `${proxyBase}/key?url=${encodeURIComponent(resolvedUrl)}&source=${source}`;
+ return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
+ }
+ return line;
+}
diff --git a/src/app/api/proxy/vod/segment/route.ts b/src/app/api/proxy/vod/segment/route.ts
new file mode 100644
index 0000000..8b8a734
--- /dev/null
+++ b/src/app/api/proxy/vod/segment/route.ts
@@ -0,0 +1,153 @@
+/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
+
+import { NextResponse } from "next/server";
+
+import { getConfig } from "@/lib/config";
+
+export const runtime = 'nodejs';
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const url = searchParams.get('url');
+ const source = searchParams.get('source');
+
+ if (!url) {
+ return NextResponse.json({ error: 'Missing url' }, { status: 400 });
+ }
+
+ if (!source) {
+ return NextResponse.json({ error: 'Missing source' }, { status: 400 });
+ }
+
+ // 检查该视频源是否启用了代理模式
+ const config = await getConfig();
+ const videoSource = config.SourceConfig?.find((s: any) => s.key === source);
+
+ if (!videoSource) {
+ return NextResponse.json({ error: 'Source not found' }, { status: 404 });
+ }
+
+ if (!videoSource.proxyMode) {
+ return NextResponse.json({ error: 'Proxy mode not enabled for this source' }, { status: 403 });
+ }
+
+ let response: Response | null = null;
+ let reader: ReadableStreamDefaultReader | null = null;
+
+ try {
+ const decodedUrl = decodeURIComponent(url);
+ response = await fetch(decodedUrl, {
+ headers: {
+ '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',
+ 'Referer': decodedUrl,
+ },
+ });
+ if (!response.ok) {
+ return NextResponse.json({ error: 'Failed to fetch segment' }, { status: 500 });
+ }
+
+ const headers = new Headers();
+ headers.set('Content-Type', response.headers.get('Content-Type') || 'video/mp2t');
+ headers.set('Access-Control-Allow-Origin', '*');
+ headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
+ headers.set('Access-Control-Allow-Headers', 'Content-Type, Range, Origin, Accept');
+ headers.set('Accept-Ranges', 'bytes');
+ headers.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range');
+ const contentLength = response.headers.get('content-length');
+ if (contentLength) {
+ headers.set('Content-Length', contentLength);
+ }
+
+ // 使用流式传输,避免占用内存
+ const stream = new ReadableStream({
+ start(controller) {
+ if (!response?.body) {
+ controller.close();
+ return;
+ }
+
+ reader = response.body.getReader();
+ const isCancelled = false;
+
+ function pump() {
+ if (isCancelled || !reader) {
+ return;
+ }
+
+ reader.read().then(({ done, value }) => {
+ if (isCancelled) {
+ return;
+ }
+
+ if (done) {
+ controller.close();
+ cleanup();
+ return;
+ }
+
+ controller.enqueue(value);
+ pump();
+ }).catch((error) => {
+ if (!isCancelled) {
+ controller.error(error);
+ cleanup();
+ }
+ });
+ }
+
+ function cleanup() {
+ if (reader) {
+ try {
+ reader.releaseLock();
+ } catch (e) {
+ // reader 可能已经被释放,忽略错误
+ }
+ reader = null;
+ }
+ }
+
+ pump();
+ },
+ cancel() {
+ // 当流被取消时,确保释放所有资源
+ if (reader) {
+ try {
+ reader.releaseLock();
+ } catch (e) {
+ // reader 可能已经被释放,忽略错误
+ }
+ reader = null;
+ }
+
+ if (response?.body) {
+ try {
+ response.body.cancel();
+ } catch (e) {
+ // 忽略取消时的错误
+ }
+ }
+ }
+ });
+
+ return new Response(stream, { headers });
+ } catch (error) {
+ // 确保在错误情况下也释放资源
+ if (reader) {
+ try {
+ (reader as ReadableStreamDefaultReader).releaseLock();
+ } catch (e) {
+ // 忽略错误
+ }
+ }
+
+ if (response?.body) {
+ try {
+ response.body.cancel();
+ } catch (e) {
+ // 忽略错误
+ }
+ }
+
+ return NextResponse.json({ error: 'Failed to fetch segment' }, { status: 500 });
+ }
+}
diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx
index 90aabe8..4c65317 100644
--- a/src/app/play/page.tsx
+++ b/src/app/play/page.tsx
@@ -681,6 +681,9 @@ function PlayPageClient() {
// 视频播放地址
const [videoUrl, setVideoUrl] = useState('');
+ // 视频源代理模式状态
+ const [sourceProxyMode, setSourceProxyMode] = useState(false);
+
// 总集数
const totalEpisodes = detail?.episodes?.length || 0;
@@ -1048,9 +1051,13 @@ function PlayPageClient() {
const hasLocalFile = await checkLocalDownload(currentSource, currentId, episodeIndex);
if (hasLocalFile) {
- // 使用本地代理接口,URL以.m3u8结尾以便Artplayer自动识别
+ // 使用本地代理接口,URL以.m3u8结尾以便Artplayer自动识别
newUrl = `/api/offline-download/local/${currentSource}/${currentId}/${episodeIndex}/playlist.m3u8`;
console.log('使用本地下载文件播放:', newUrl);
+ } else if (sourceProxyMode && newUrl) {
+ // 如果视频源启用了代理模式,且不是本地下载,则通过代理播放
+ newUrl = `/api/proxy/vod/m3u8?url=${encodeURIComponent(newUrl)}&source=${encodeURIComponent(currentSource)}`;
+ console.log('使用代理模式播放:', newUrl);
}
if (newUrl !== videoUrl) {
@@ -2041,6 +2048,7 @@ function PlayPageClient() {
setVideoCover(detailData.poster);
setVideoDoubanId(detailData.douban_id || 0);
setDetail(detailData);
+ setSourceProxyMode(detailData.proxyMode || false); // 从 detail 数据中读取代理模式
if (currentEpisodeIndex >= detailData.episodes.length) {
setCurrentEpisodeIndex(0);
}
@@ -2145,6 +2153,7 @@ function PlayPageClient() {
setVideoCover(targetSource.poster);
setVideoDoubanId(targetSource.douban_id || 0);
setDetail(targetSource);
+ setSourceProxyMode(targetSource.proxyMode || false); // 从 detail 数据中读取代理模式
// 更新集数
if (targetEpisode >= 0 && targetEpisode < targetSource.episodes.length) {
@@ -2274,6 +2283,7 @@ function PlayPageClient() {
setCurrentSource(newSource);
setCurrentId(newId);
setDetail(newDetail);
+ setSourceProxyMode(newDetail.proxyMode || false); // 从 detail 数据中读取代理模式
setCurrentEpisodeIndex(targetIndex);
} catch (err) {
// 隐藏换源加载状态
diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts
index 070c40d..7b35077 100644
--- a/src/lib/admin.types.ts
+++ b/src/lib/admin.types.ts
@@ -72,6 +72,7 @@ export interface AdminConfig {
detail?: string;
from: 'config' | 'custom';
disabled?: boolean;
+ proxyMode?: boolean; // 代理模式开关:启用后由服务器代理m3u8和ts分片
}[];
CustomCategories: {
name?: string;
diff --git a/src/lib/config.ts b/src/lib/config.ts
index 4762d6e..665a914 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -9,6 +9,7 @@ export interface ApiSite {
api: string;
name: string;
detail?: string;
+ proxyMode?: boolean;
}
export interface LiveCfg {
@@ -522,6 +523,7 @@ export async function getAvailableApiSites(user?: string): Promise {
name: s.name,
api: s.api,
detail: s.detail,
+ proxyMode: s.proxyMode,
}));
}
@@ -543,6 +545,7 @@ export async function getAvailableApiSites(user?: string): Promise {
name: s.name,
api: s.api,
detail: s.detail,
+ proxyMode: s.proxyMode,
}));
}
}
diff --git a/src/lib/downstream.ts b/src/lib/downstream.ts
index 348b9e6..9652f61 100644
--- a/src/lib/downstream.ts
+++ b/src/lib/downstream.ts
@@ -117,6 +117,7 @@ async function searchWithCache(
douban_id: item.vod_douban_id,
vod_remarks: item.vod_remarks,
vod_total: item.vod_total,
+ proxyMode: apiSite.proxyMode || false,
};
});
@@ -288,6 +289,7 @@ export async function getDetailFromApi(
douban_id: videoDetail.vod_douban_id,
vod_remarks: videoDetail.vod_remarks,
vod_total: videoDetail.vod_total,
+ proxyMode: apiSite.proxyMode || false,
};
}
@@ -370,5 +372,6 @@ async function handleSpecialSourceDetail(
douban_id: 0,
vod_remarks: undefined,
vod_total: undefined,
+ proxyMode: apiSite.proxyMode || false,
};
}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index df20898..0b47b4f 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -128,6 +128,7 @@ export interface SearchResult {
douban_id?: number;
vod_remarks?: string; // 视频备注信息(如"全80集"、"更新至25集"等)
vod_total?: number; // 总集数
+ proxyMode?: boolean; // 代理模式:启用后由服务器代理m3u8和ts分片
}
// 豆瓣数据结构