From 40da6ae5b25af35d0d031299ef07b8d79f862824 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Fri, 5 Dec 2025 18:06:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=96=E9=83=A8=E6=92=AD=E6=94=BE=E5=99=A8?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=BB=A3=E7=90=86m3u8=E6=97=B6=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=89=B4=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 22 ---------------------- README.md | 5 +++-- src/app/api/proxy-m3u8/route.ts | 21 +++++++++++++++++---- src/app/play/page.tsx | 24 +++++++++++++++++------- 4 files changed, 37 insertions(+), 35 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 72f28e0..0000000 --- a/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -# 环境变量配置示例 -# 复制此文件为 .env.local 并修改配置 - -# ========================================== -# 弹幕 API 配置 -# ========================================== - -# 弹幕 API 服务地址(默认: http://localhost:9321) -# 用于后端代理转发弹幕请求 -DANMAKU_API_BASE=http://localhost:9321 - -# 弹幕 API Token(默认: 87654321) -# 如果您的 danmu_api 使用了自定义 token,请在此配置 -DANMAKU_API_TOKEN=87654321 - -# ========================================== -# 注意事项 -# ========================================== -# 1. 弹幕请求通过后端代理转发,前端不会直接访问 danmu_api -# 2. 确保 danmu_api 服务在配置的地址上正常运行 -# 3. 如果 danmu_api 和 LunaTV 在同一台机器上,使用 localhost 即可 -# 4. 如果 danmu_api 在其他机器上,请使用完整的 URL(如 http://192.168.1.100:9321) diff --git a/README.md b/README.md index e390fae..5a2a82d 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ ## 🎉 相对原版新增内容 - 🎮 **外部播放器跳转**:支持 PotPlayer、VLC、MPV、MX Player、nPlayer、IINA 等多种外部播放器 -- ✨ **视频超分 (Anime4K)**:使用 WebGPU 技术实现实时视频画质增强(支持 2x/3x/4x 超分) -- 💬 **弹幕系统**:完整的弹幕搜索、匹配、加载功能,支持弹幕设置持久化 +- ✨ **视频超分 (Anime4K)**:使用 WebGPU 技术实现实时视频画质增强(支持 1.5x/2x/3x/4x 超分) +- 💬 **弹幕系统**:完整的弹幕搜索、匹配、加载功能,支持弹幕设置持久化、弹幕屏蔽 - 📝 **豆瓣评论抓取**:自动抓取并展示豆瓣电影短评,支持分页加载 ## ✨ 功能特性 @@ -249,6 +249,7 @@ dockge/komodo 等 docker compose UI 也有自动更新功能 | NEXT_PUBLIC_DOUBAN_IMAGE_PROXY | 自定义豆瓣图片代理 URL | url prefix | (空) | | NEXT_PUBLIC_DISABLE_YELLOW_FILTER | 关闭色情内容过滤 | true/false | false | | NEXT_PUBLIC_FLUID_SEARCH | 是否开启搜索接口流式输出 | true/ false | true | +| NEXT_PUBLIC_PROXY_M3U8_TOKEN | M3U8 代理 API 鉴权 Token(外部播放器跳转时的鉴权token,不填为无鉴权) | 任意字符串 | (空) | NEXT_PUBLIC_DOUBAN_PROXY_TYPE 选项解释: diff --git a/src/app/api/proxy-m3u8/route.ts b/src/app/api/proxy-m3u8/route.ts index f1a86cc..de806b6 100644 --- a/src/app/api/proxy-m3u8/route.ts +++ b/src/app/api/proxy-m3u8/route.ts @@ -7,13 +7,25 @@ export const runtime = 'nodejs'; /** * M3U8 代理接口 * 用于外部播放器访问,会执行去广告逻辑并处理相对链接 - * GET /api/proxy-m3u8?url=<原始m3u8地址>&source=<播放源> + * GET /api/proxy-m3u8?url=<原始m3u8地址>&source=<播放源>&token=<鉴权token> */ export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); const m3u8Url = searchParams.get('url'); const source = searchParams.get('source') || ''; + const token = searchParams.get('token'); + + // Token 鉴权:如果环境变量设置了 token,则必须验证 + const envToken = process.env.NEXT_PUBLIC_PROXY_M3U8_TOKEN; + if (envToken && envToken.trim() !== '') { + if (!token || token !== envToken) { + return NextResponse.json( + { error: '无效的访问令牌' }, + { status: 401 } + ); + } + } if (!m3u8Url) { return NextResponse.json( @@ -70,7 +82,7 @@ export async function GET(request: Request) { } // 处理 m3u8 中的相对链接 - m3u8Content = resolveM3u8Links(m3u8Content, m3u8Url, source, origin); + m3u8Content = resolveM3u8Links(m3u8Content, m3u8Url, source, origin, token || ''); // 返回处理后的 m3u8 内容 return new NextResponse(m3u8Content, { @@ -134,7 +146,7 @@ function filterAdsFromM3U8Default(type: string, m3u8Content: string): string { /** * 将 m3u8 中的相对链接转换为绝对链接,并将子 m3u8 链接转为代理链接 */ -function resolveM3u8Links(m3u8Content: string, baseUrl: string, source: string, proxyOrigin: string): string { +function resolveM3u8Links(m3u8Content: string, baseUrl: string, source: string, proxyOrigin: string, token: string): string { const lines = m3u8Content.split('\n'); const resolvedLines = []; @@ -203,7 +215,8 @@ function resolveM3u8Links(m3u8Content: string, baseUrl: string, source: string, // 2. 检查是否是子 m3u8,如果是,转换为代理链接 const isM3u8 = url.includes('.m3u8') || isNextLineUrl; if (isM3u8) { - url = `${proxyOrigin}/api/proxy-m3u8?url=${encodeURIComponent(url)}${source ? `&source=${encodeURIComponent(source)}` : ''}`; + const tokenParam = token ? `&token=${encodeURIComponent(token)}` : ''; + url = `${proxyOrigin}/api/proxy-m3u8?url=${encodeURIComponent(url)}${source ? `&source=${encodeURIComponent(source)}` : ''}${tokenParam}`; } resolvedLines.push(url); diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 3abf6ff..e251628 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -62,6 +62,9 @@ function PlayPageClient() { const searchParams = useSearchParams(); const enableComments = useEnableComments(); + // 获取 Proxy M3U8 Token + const proxyToken = typeof window !== 'undefined' ? process.env.NEXT_PUBLIC_PROXY_M3U8_TOKEN || '' : ''; + // ----------------------------------------------------------------------------- // 状态变量(State) // ----------------------------------------------------------------------------- @@ -3420,8 +3423,9 @@ function PlayPageClient() { onClick={(e) => { e.preventDefault(); // 使用代理 URL + const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; const proxyUrl = externalPlayerAdBlock - ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}` + ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}` : videoUrl; const isM3u8 = videoUrl.toLowerCase().includes('.m3u8') || videoUrl.toLowerCase().includes('/m3u8/'); @@ -3479,8 +3483,9 @@ function PlayPageClient() { onClick={(e) => { e.preventDefault(); // 使用代理 URL + const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; const proxyUrl = externalPlayerAdBlock - ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}` + ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}` : videoUrl; // URL encode 避免冒号被吃掉 window.open(`potplayer://${proxyUrl}`, '_blank'); @@ -3503,8 +3508,9 @@ function PlayPageClient() { onClick={(e) => { e.preventDefault(); // 使用代理 URL + const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; const proxyUrl = externalPlayerAdBlock - ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}` + ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}` : videoUrl; // URL encode 避免冒号被吃掉 window.open(`vlc://${proxyUrl}`, '_blank'); @@ -3527,8 +3533,9 @@ function PlayPageClient() { onClick={(e) => { e.preventDefault(); // 使用代理 URL + const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; const proxyUrl = externalPlayerAdBlock - ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}` + ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}` : videoUrl; // URL encode 避免冒号被吃掉 window.open(`mpv://${proxyUrl}`, '_blank'); @@ -3551,8 +3558,9 @@ function PlayPageClient() { onClick={(e) => { e.preventDefault(); // 使用代理 URL + const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; const proxyUrl = externalPlayerAdBlock - ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}` + ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}` : videoUrl; window.open( `intent://${proxyUrl}#Intent;package=com.mxtech.videoplayer.ad;S.title=${encodeURIComponent( @@ -3579,8 +3587,9 @@ function PlayPageClient() { onClick={(e) => { e.preventDefault(); // 使用代理 URL + const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; const proxyUrl = externalPlayerAdBlock - ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}` + ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}` : videoUrl; window.open(`nplayer-${proxyUrl}`, '_blank'); }} @@ -3602,8 +3611,9 @@ function PlayPageClient() { onClick={(e) => { e.preventDefault(); // 使用代理 URL + const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : ''; const proxyUrl = externalPlayerAdBlock - ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}` + ? `${window.location.origin}/api/proxy-m3u8?url=${encodeURIComponent(videoUrl)}&source=${encodeURIComponent(currentSource)}${tokenParam}` : videoUrl; window.open( `iina://weblink?url=${encodeURIComponent(