优化音乐代理逻辑
This commit is contained in:
@@ -5,6 +5,124 @@ import { badRequest } from '@/lib/music-v2-api';
|
|||||||
|
|
||||||
export const runtime = 'nodejs';
|
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) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@@ -30,44 +148,39 @@ export async function GET(request: NextRequest) {
|
|||||||
trcUrl: searchParams.get('trcUrl') || undefined,
|
trcUrl: searchParams.get('trcUrl') || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const urlResult = await lxPostJson<{ url?: string; error?: string }>(
|
const cacheKey = getStreamCacheKey(song, quality);
|
||||||
'/api/music/url',
|
let upstreamUrl = await resolveStreamUrl(song, quality, cacheKey);
|
||||||
{
|
let upstreamHeaders = buildUpstreamHeaders(request, upstreamUrl);
|
||||||
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;
|
let upstream: Response;
|
||||||
if (!upstreamUrl) {
|
try {
|
||||||
return NextResponse.json({ success: false, error: { code: 'STREAM_FAILED', message: urlResult?.error || '获取音频流失败' } }, { status: 502 });
|
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.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 });
|
return NextResponse.json({ success: false, error: { code: 'STREAM_FAILED', message: '获取音频流失败' } }, { status: upstream.status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user