Files
MoonTVPlus/src/app/api/music/proxy/route.ts
mtvpls 9b217b8eae 206
2026-02-01 21:26:10 +08:00

122 lines
3.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 */
import { NextRequest, NextResponse } from 'next/server';
export const runtime = 'nodejs';
// 代理音频流
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const url = searchParams.get('url');
if (!url) {
return NextResponse.json(
{ error: '缺少 url 参数' },
{ status: 400 }
);
}
// 安全检查:只允许代理音乐平台的音频和图片 CDN
const allowedDomains = [
'sycdn.kuwo.cn',
'kwcdn.kuwo.cn',
'img1.kwcdn.kuwo.cn',
'img2.kwcdn.kuwo.cn',
'img3.kwcdn.kuwo.cn',
'img4.kwcdn.kuwo.cn',
'music.163.com',
'y.qq.com',
'ws.stream.qqmusic.qq.com',
'isure.stream.qqmusic.qq.com',
'dl.stream.qqmusic.qq.com',
];
let urlObj: URL;
try {
urlObj = new URL(url);
} catch {
return NextResponse.json(
{ error: '无效的 URL' },
{ status: 400 }
);
}
const isAllowed = allowedDomains.some(domain =>
urlObj.hostname === domain || urlObj.hostname.endsWith(`.${domain}`)
);
if (!isAllowed) {
console.warn(`拒绝代理音频请求: ${urlObj.hostname}`);
return NextResponse.json(
{ error: '不允许的目标域名' },
{ status: 403 }
);
}
// 检查是否有 Range 请求头
const range = request.headers.get('range');
// 构建上游请求头
const upstreamHeaders: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'http://www.kuwo.cn/',
};
// 如果有 Range 请求,转发给上游
if (range) {
upstreamHeaders['Range'] = range;
}
// 发起请求获取音频流
const response = await fetch(url, {
headers: upstreamHeaders,
});
if (!response.ok && response.status !== 206) {
return NextResponse.json(
{ error: '获取音频失败' },
{ status: response.status }
);
}
// 获取响应头
const contentType = response.headers.get('content-type') || 'audio/mpeg';
const contentLength = response.headers.get('content-length');
const contentRange = response.headers.get('content-range');
const acceptRanges = response.headers.get('accept-ranges');
// 创建响应头
const headers: Record<string, string> = {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=3600',
'Access-Control-Allow-Origin': '*',
'Accept-Ranges': acceptRanges || 'bytes',
};
if (contentLength) {
headers['Content-Length'] = contentLength;
}
// 如果上游返回了 Content-Range转发给客户端
if (contentRange) {
headers['Content-Range'] = contentRange;
}
// 返回音频流保持原始状态码200 或 206
return new NextResponse(response.body, {
status: response.status,
headers,
});
} catch (error) {
console.error('代理音频失败:', error);
return NextResponse.json(
{
error: '代理请求失败',
details: (error as Error).message,
},
{ status: 500 }
);
}
}