优化音乐代理逻辑

This commit is contained in:
mtvpls
2026-04-28 00:14:11 +08:00
parent f265566536
commit ef433ba4df
+147 -34
View File
@@ -5,6 +5,124 @@ import { badRequest } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
const STREAM_URL_CACHE_TTL_MS = 10 * 60 * 1000;
type StreamUrlCacheValue = {
url: string;
expiresAt: number;
};
const globalMusicStreamCache = globalThis as typeof globalThis & {
__musicV2StreamUrlCache?: Map<string, StreamUrlCacheValue>;
__musicV2StreamUrlInflight?: Map<string, Promise<string>>;
};
const streamUrlCache = globalMusicStreamCache.__musicV2StreamUrlCache ?? new Map<string, StreamUrlCacheValue>();
const streamUrlInflight = globalMusicStreamCache.__musicV2StreamUrlInflight ?? new Map<string, Promise<string>>();
globalMusicStreamCache.__musicV2StreamUrlCache = streamUrlCache;
globalMusicStreamCache.__musicV2StreamUrlInflight = streamUrlInflight;
function getStreamCacheKey(song: ReturnType<typeof normalizeSong>, quality: string) {
return `${song.source}:${song.songId}:${quality}`;
}
function getCachedStreamUrl(cacheKey: string) {
const cached = streamUrlCache.get(cacheKey);
if (!cached) return null;
if (cached.expiresAt <= Date.now()) {
streamUrlCache.delete(cacheKey);
return null;
}
return cached.url;
}
function setCachedStreamUrl(cacheKey: string, url: string) {
streamUrlCache.set(cacheKey, {
url,
expiresAt: Date.now() + STREAM_URL_CACHE_TTL_MS,
});
}
async function resolveStreamUrl(
song: ReturnType<typeof normalizeSong>,
quality: string,
cacheKey: string
) {
const cachedUrl = getCachedStreamUrl(cacheKey);
if (cachedUrl) return cachedUrl;
const inflight = streamUrlInflight.get(cacheKey);
if (inflight) return inflight;
const resolverPromise = (async () => {
const urlResult = await lxPostJson<{ url?: string; error?: string }>(
'/api/music/url',
{
songInfo: {
id: song.songId,
name: song.name,
singer: song.artist,
source: song.source,
songmid: extractSongmid(song),
hash: song.hash,
interval: song.durationText,
copyrightId: song.copyrightId,
albumId: song.albumId,
lrcUrl: song.lrcUrl,
mrcUrl: song.mrcUrl,
trcUrl: song.trcUrl,
},
quality,
},
'auto'
);
const upstreamUrl = urlResult?.url;
if (!upstreamUrl) {
throw new Error(urlResult?.error || '获取音频流失败');
}
setCachedStreamUrl(cacheKey, upstreamUrl);
return upstreamUrl;
})();
streamUrlInflight.set(cacheKey, resolverPromise);
try {
return await resolverPromise;
} finally {
streamUrlInflight.delete(cacheKey);
}
}
function buildUpstreamHeaders(request: NextRequest, upstreamUrl: string) {
const headers = new Headers();
headers.set(
'User-Agent',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
);
const range = request.headers.get('range');
if (range) headers.set('Range', range);
const url = new URL(upstreamUrl);
const hostname = url.hostname.toLowerCase();
if (hostname.includes('kuwo.cn')) {
headers.set('Referer', 'http://www.kuwo.cn/');
headers.set('Origin', 'http://www.kuwo.cn');
} else if (hostname.includes('qqmusic.qq.com') || hostname === 'y.qq.com') {
headers.set('Referer', 'https://y.qq.com/');
headers.set('Origin', 'https://y.qq.com');
} else if (hostname.includes('music.163.com')) {
headers.set('Referer', 'https://music.163.com/');
headers.set('Origin', 'https://music.163.com');
}
return headers;
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
@@ -30,44 +148,39 @@ export async function GET(request: NextRequest) {
trcUrl: searchParams.get('trcUrl') || undefined,
});
const urlResult = await lxPostJson<{ url?: string; error?: string }>(
'/api/music/url',
{
songInfo: {
id: song.songId,
name: song.name,
singer: song.artist,
source: song.source,
songmid: extractSongmid(song),
hash: song.hash,
interval: song.durationText,
copyrightId: song.copyrightId,
albumId: song.albumId,
lrcUrl: song.lrcUrl,
mrcUrl: song.mrcUrl,
trcUrl: song.trcUrl,
},
quality,
},
'auto'
);
const cacheKey = getStreamCacheKey(song, quality);
let upstreamUrl = await resolveStreamUrl(song, quality, cacheKey);
let upstreamHeaders = buildUpstreamHeaders(request, upstreamUrl);
const upstreamUrl = urlResult?.url;
if (!upstreamUrl) {
return NextResponse.json({ success: false, error: { code: 'STREAM_FAILED', message: urlResult?.error || '获取音频流失败' } }, { status: 502 });
let upstream: Response;
try {
upstream = await fetch(upstreamUrl, {
headers: upstreamHeaders,
signal: AbortSignal.timeout(30000),
cache: 'no-store',
});
} catch (error) {
streamUrlCache.delete(cacheKey);
upstreamUrl = await resolveStreamUrl(song, quality, cacheKey);
upstreamHeaders = buildUpstreamHeaders(request, upstreamUrl);
upstream = await fetch(upstreamUrl, {
headers: upstreamHeaders,
signal: AbortSignal.timeout(30000),
cache: 'no-store',
});
console.warn('[music-v2/stream] upstream fetch failed once, retried with refreshed URL', {
source,
songId,
quality,
range: request.headers.get('range'),
reason: (error as Error).message,
});
}
const headers = new Headers();
headers.set('User-Agent', 'Mozilla/5.0');
const range = request.headers.get('range');
if (range) headers.set('Range', range);
const upstream = await fetch(upstreamUrl, {
headers,
signal: AbortSignal.timeout(30000),
});
if (!upstream.ok && upstream.status !== 206) {
if (upstream.status === 401 || upstream.status === 403 || upstream.status === 404) {
streamUrlCache.delete(cacheKey);
}
return NextResponse.json({ success: false, error: { code: 'STREAM_FAILED', message: '获取音频流失败' } }, { status: upstream.status });
}