Files
MoonTVPlus/src/app/api/proxy/m3u8/route.ts
2025-08-24 17:23:27 +08:00

153 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 allowCORS = searchParams.get('allowCORS') === 'true';
const source = searchParams.get('moontv-source');
if (!url) {
return NextResponse.json({ error: 'Missing url' }, { status: 400 });
}
const config = await getConfig();
const liveSource = config.LiveConfig?.find((s: any) => s.key === source);
if (!liveSource) {
return NextResponse.json({ error: 'Source not found' }, { status: 404 });
}
const ua = liveSource.ua || 'AptvPlayer/1.4.10';
try {
const decodedUrl = decodeURIComponent(url);
const response = await fetch(decodedUrl, {
cache: 'no-cache',
redirect: 'follow',
credentials: 'same-origin',
headers: {
'User-Agent': ua,
},
});
if (!response.ok) {
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
}
// 获取最终的响应URL处理重定向后的URL
const finalUrl = response.url;
const m3u8Content = await response.text();
// 使用最终的响应URL作为baseUrl而不是原始的请求URL
const baseUrl = getBaseUrl(finalUrl);
// 重写 M3U8 内容
const modifiedContent = rewriteM3U8Content(m3u8Content, baseUrl, request, allowCORS);
const headers = new Headers();
headers.set('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(modifiedContent, { headers });
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
}
}
function rewriteM3U8Content(content: string, baseUrl: string, req: Request, allowCORS: boolean) {
// 从 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`;
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);
// 检查是否为 mp4 格式
const isMp4 = resolvedUrl.toLowerCase().includes('.mp4') || resolvedUrl.toLowerCase().includes('mp4');
const proxyUrl = (isMp4 || allowCORS) ? resolvedUrl : `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}`;
rewrittenLines.push(proxyUrl);
continue;
}
// 处理 EXT-X-MAP 标签中的 URI
if (line.startsWith('#EXT-X-MAP:')) {
line = rewriteMapUri(line, baseUrl, proxyBase);
}
// 处理 EXT-X-KEY 标签中的 URI
if (line.startsWith('#EXT-X-KEY:')) {
line = rewriteKeyUri(line, baseUrl, proxyBase);
}
// 处理嵌套的 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)}`;
rewrittenLines.push(proxyUrl);
} else {
rewrittenLines.push(nextLine);
}
}
continue;
}
rewrittenLines.push(line);
}
return rewrittenLines.join('\n');
}
function rewriteMapUri(line: string, baseUrl: string, proxyBase: string) {
const uriMatch = line.match(/URI="([^"]+)"/);
if (uriMatch) {
const originalUri = uriMatch[1];
const resolvedUrl = resolveUrl(baseUrl, originalUri);
// 检查是否为 mp4 格式,如果是则走 proxyBase
const isMp4 = resolvedUrl.toLowerCase().includes('.mp4') || resolvedUrl.toLowerCase().includes('mp4');
const proxyUrl = isMp4 ? `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}` : `${proxyBase}/segment?url=${encodeURIComponent(resolvedUrl)}`;
return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
}
return line;
}
function rewriteKeyUri(line: string, baseUrl: string, proxyBase: string) {
const uriMatch = line.match(/URI="([^"]+)"/);
if (uriMatch) {
const originalUri = uriMatch[1];
const resolvedUrl = resolveUrl(baseUrl, originalUri);
const proxyUrl = `${proxyBase}/key?url=${encodeURIComponent(resolvedUrl)}`;
return line.replace(uriMatch[0], `URI="${proxyUrl}"`);
}
return line;
}