外部播放器使用代理m3u8时增加鉴权

This commit is contained in:
mtvpls
2025-12-05 18:06:23 +08:00
parent ccc20aee3a
commit 40da6ae5b2
4 changed files with 37 additions and 35 deletions

View File

@@ -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

View File

@@ -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 选项解释:

View File

@@ -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);

View File

@@ -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(