新增基于lxserver的音乐功能

This commit is contained in:
mtvpls
2026-04-13 23:17:57 +08:00
parent 7469cc1537
commit c82d7b3841
30 changed files with 2740 additions and 546 deletions
+12 -36
View File
@@ -29,38 +29,20 @@ export async function POST(request: NextRequest) {
const username = authInfo.username;
const {
TuneHubEnabled,
TuneHubBaseUrl,
TuneHubApiKey,
OpenListCacheEnabled,
OpenListCacheURL,
OpenListCacheUsername,
OpenListCachePassword,
OpenListCachePath,
OpenListCacheProxyEnabled,
Enabled,
BaseUrl,
Token,
} = body as {
TuneHubEnabled?: boolean;
TuneHubBaseUrl?: string;
TuneHubApiKey?: string;
OpenListCacheEnabled?: boolean;
OpenListCacheURL?: string;
OpenListCacheUsername?: string;
OpenListCachePassword?: string;
OpenListCachePath?: string;
OpenListCacheProxyEnabled?: boolean;
Enabled?: boolean;
BaseUrl?: string;
Token?: string;
};
// 参数校验
if (
(TuneHubEnabled !== undefined && typeof TuneHubEnabled !== 'boolean') ||
(TuneHubBaseUrl !== undefined && typeof TuneHubBaseUrl !== 'string') ||
(TuneHubApiKey !== undefined && typeof TuneHubApiKey !== 'string') ||
(OpenListCacheEnabled !== undefined && typeof OpenListCacheEnabled !== 'boolean') ||
(OpenListCacheURL !== undefined && typeof OpenListCacheURL !== 'string') ||
(OpenListCacheUsername !== undefined && typeof OpenListCacheUsername !== 'string') ||
(OpenListCachePassword !== undefined && typeof OpenListCachePassword !== 'string') ||
(OpenListCachePath !== undefined && typeof OpenListCachePath !== 'string') ||
(OpenListCacheProxyEnabled !== undefined && typeof OpenListCacheProxyEnabled !== 'boolean')
(Enabled !== undefined && typeof Enabled !== 'boolean') ||
(BaseUrl !== undefined && typeof BaseUrl !== 'string') ||
(Token !== undefined && typeof Token !== 'string')
) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -77,15 +59,9 @@ export async function POST(request: NextRequest) {
// 更新缓存中的音乐配置
adminConfig.MusicConfig = {
TuneHubEnabled,
TuneHubBaseUrl,
TuneHubApiKey,
OpenListCacheEnabled,
OpenListCacheURL,
OpenListCacheUsername,
OpenListCachePassword,
OpenListCachePath,
OpenListCacheProxyEnabled,
Enabled,
BaseUrl,
Token,
};
// 写入数据库
@@ -0,0 +1,38 @@
import { NextRequest, NextResponse } from 'next/server';
import { isMusicSource, lxGetJson, normalizeLxSong, unwrapLxArray } from '@/lib/music-v2';
import { badRequest, internalError } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const source = searchParams.get('source') || 'kw';
const boardId = searchParams.get('boardId') || searchParams.get('bangid') || '';
const page = Number(searchParams.get('page') || '1');
if (!isMusicSource(source)) return badRequest('不支持的音源');
if (!boardId) return badRequest('缺少榜单 ID');
const payload = await lxGetJson<any>(`/api/music/leaderboard/list?source=${source}&bangid=${encodeURIComponent(boardId)}&page=${page}`, 'none');
const list = unwrapLxArray<any>(payload);
const total =
payload?.total ??
payload?.data?.total ??
payload?.data?.data?.total ??
list.length;
return NextResponse.json({
success: true,
data: {
board: { id: boardId },
list: list.map(normalizeLxSong),
total,
page,
},
});
} catch (error) {
return internalError('获取榜单歌曲失败', (error as Error).message);
}
}
@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server';
import { isMusicSource, lxGetJson, unwrapLxArray } from '@/lib/music-v2';
import { badRequest, internalError } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const source = searchParams.get('source') || 'kw';
if (!isMusicSource(source)) return badRequest('不支持的音源');
const fallbackSources = [source, 'kg', 'kw', 'tx', 'wy', 'mg'].filter(
(item, index, arr) => arr.indexOf(item) === index
);
let actualSource = source as typeof source;
let list: Array<{ id?: string; bangid?: string; name: string; img?: string }> = [];
const errors: string[] = [];
for (const candidate of fallbackSources) {
try {
const candidatePayload = await lxGetJson<any>(
`/api/music/leaderboard/boards?source=${candidate}`,
'none'
);
const candidateList = unwrapLxArray<{ id?: string; bangid?: string; name: string; img?: string }>(candidatePayload);
if (Array.isArray(candidateList) && candidateList.length > 0) {
actualSource = candidate as typeof source;
list = candidateList;
break;
}
} catch (error) {
const message = (error as Error).message;
errors.push(`${candidate}: ${message}`);
console.error(`[music-v2] 获取榜单源失败: ${candidate}`, error);
}
}
return NextResponse.json({
success: true,
data: {
list: list.map(item => ({
id: item.bangid || item.id || '',
name: item.name,
cover: item.img,
source: actualSource,
})),
source: actualSource,
errors,
},
});
} catch (error) {
console.error('[music-v2] 获取榜单失败:', error);
return internalError('获取榜单失败', (error as Error).message);
}
}
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
import { isMusicSource, lxGetJson } from '@/lib/music-v2';
import { badRequest, internalError } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const source = searchParams.get('source') || 'mg';
if (!isMusicSource(source)) return badRequest('不支持的音源');
const list = await lxGetJson<Array<{ name: string; singer?: string; source: string }>>(`/api/music/hotSearch?source=${source}`, 'none');
return NextResponse.json({
success: true,
data: {
list: list.map(item => ({
keyword: item.name,
name: item.name,
artist: item.singer || '',
source: item.source,
})),
},
});
} catch (error) {
return internalError('获取热搜失败', (error as Error).message);
}
}
+80
View File
@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { MusicV2HistoryRecord, normalizeSong } from '@/lib/music-v2';
import { badRequest, getMusicV2Username, internalError, unauthorized } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
function toHistoryRecord(input: any, previous?: MusicV2HistoryRecord): MusicV2HistoryRecord {
const song = normalizeSong(input.song || input);
const now = Date.now();
return {
...song,
playProgressSec: Number(input.playProgressSec ?? input.play_progress_sec ?? previous?.playProgressSec ?? 0),
lastPlayedAt: Number(input.lastPlayedAt ?? input.last_played_at ?? now),
playCount: Number(input.playCount ?? input.play_count ?? ((previous?.playCount || 0) + 1)),
lastQuality: input.lastQuality || input.last_quality || previous?.lastQuality,
createdAt: Number(input.createdAt ?? input.created_at ?? previous?.createdAt ?? now),
updatedAt: now,
};
}
export async function GET(request: NextRequest) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const records = await db.listMusicV2History(username);
return NextResponse.json({ success: true, data: { records } });
} catch (error) {
return internalError('获取播放历史失败', (error as Error).message);
}
}
export async function POST(request: NextRequest) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const body = await request.json();
const existingRecords = await db.listMusicV2History(username);
const existingMap = new Map(existingRecords.map(record => [record.songId, record]));
if (Array.isArray(body.records)) {
const records = body.records
.map((item: any) => toHistoryRecord(item, existingMap.get(item.song?.songId || item.songId)))
.filter((item: MusicV2HistoryRecord) => item.songId && item.source && item.name && item.artist);
await db.batchUpsertMusicV2History(username, records);
return NextResponse.json({ success: true, data: { count: records.length } });
}
const record = toHistoryRecord(body.record || body, existingMap.get(body.song?.songId || body.songId));
if (!record.songId || !record.source || !record.name || !record.artist) {
return badRequest('历史记录数据不完整');
}
await db.upsertMusicV2History(username, record);
return NextResponse.json({ success: true, data: { record } });
} catch (error) {
return internalError('保存播放历史失败', (error as Error).message);
}
}
export async function DELETE(request: NextRequest) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const { searchParams } = new URL(request.url);
const songId = searchParams.get('songId');
if (songId) {
await db.deleteMusicV2History(username, songId);
} else {
await db.clearMusicV2History(username);
}
return NextResponse.json({ success: true });
} catch (error) {
return internalError('删除播放历史失败', (error as Error).message);
}
}
+23
View File
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server';
import { fetchLxLyric, normalizeSong } from '@/lib/music-v2';
import { badRequest, internalError } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const song = normalizeSong(body?.song || body?.songInfo || {});
if (!song.source || !song.songId) {
return badRequest(`歌曲信息不完整: songId=${song.songId || ''}, source=${song.source || ''}`);
}
const data = await fetchLxLyric(song);
return NextResponse.json({ success: true, data: { lyric: data.lyric || '', tlyric: data.tlyric || '' } });
} catch (error) {
console.error('[music-v2] lyric route error:', error);
return internalError('获取歌词失败', (error as Error).message);
}
}
+165
View File
@@ -0,0 +1,165 @@
import { NextRequest, NextResponse } from 'next/server';
import { extractSongmid, fetchLxLyric, MusicQuality, normalizeMusicQuality, normalizeSong, lxPostJson } from '@/lib/music-v2';
import { badRequest, internalError } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
const PLAY_META_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
type PlayMetaPayload = {
song: ReturnType<typeof normalizeSong>;
lyric: {
lyric?: string;
tlyric?: string;
};
meta: {
attempts: any[];
};
};
const globalMusicPlayMetaCache = globalThis as typeof globalThis & {
__musicV2PlayMetaCache?: Map<string, { expiresAt: number; payload: PlayMetaPayload }>;
};
const playMetaCache = globalMusicPlayMetaCache.__musicV2PlayMetaCache ?? new Map<string, { expiresAt: number; payload: PlayMetaPayload }>();
globalMusicPlayMetaCache.__musicV2PlayMetaCache = playMetaCache;
function buildStableStreamUrl(song: ReturnType<typeof normalizeSong>, quality: string) {
const params = new URLSearchParams({
songId: song.songId,
source: song.source,
quality,
songmid: extractSongmid(song),
name: song.name,
artist: song.artist,
});
if (song.durationText) params.set('durationText', song.durationText);
if (song.hash) params.set('hash', song.hash);
if (song.copyrightId) params.set('copyrightId', song.copyrightId);
if (song.albumId) params.set('albumId', song.albumId);
if (song.lrcUrl) params.set('lrcUrl', song.lrcUrl);
if (song.mrcUrl) params.set('mrcUrl', song.mrcUrl);
if (song.trcUrl) params.set('trcUrl', song.trcUrl);
return `/api/music/v2/stream?${params.toString()}`;
}
function getPlayMetaCacheKey(song: ReturnType<typeof normalizeSong>, quality: string) {
return `${song.source}:${song.songId}:${quality}`;
}
function getCachedPlayMeta(cacheKey: string) {
const cached = playMetaCache.get(cacheKey);
if (!cached) return null;
if (cached.expiresAt <= Date.now()) {
playMetaCache.delete(cacheKey);
return null;
}
return cached.payload;
}
function setCachedPlayMeta(cacheKey: string, payload: PlayMetaPayload) {
playMetaCache.set(cacheKey, {
expiresAt: Date.now() + PLAY_META_CACHE_TTL_MS,
payload,
});
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const requestedQuality = ((body?.quality || '320k') as MusicQuality);
const quality = normalizeMusicQuality(requestedQuality);
const includeUrl = body?.includeUrl !== false;
const song = normalizeSong(body?.song || {});
if (!song.songId || !song.source || !song.name || !song.artist) {
return badRequest(`歌曲信息不完整: songId=${song.songId || ''}, source=${song.source || ''}, name=${song.name || ''}, artist=${song.artist || ''}`);
}
const cacheKey = getPlayMetaCacheKey(song, quality);
let cachedMeta = getCachedPlayMeta(cacheKey);
if (!cachedMeta) {
let lyric: { lyric?: string; tlyric?: string } = { lyric: '', tlyric: '' };
try {
lyric = await fetchLxLyric(song);
} catch {
// ignore lyric failure
}
cachedMeta = {
song,
lyric,
meta: {
attempts: [],
},
};
setCachedPlayMeta(cacheKey, cachedMeta);
}
let play: {
url: string;
directUrl: string;
quality: string;
requestedQuality: MusicQuality;
} | undefined;
let attempts = cachedMeta.meta.attempts || [];
if (includeUrl) {
const urlResult = await lxPostJson<{ url?: string; type?: string; attempts?: any[]; error?: string }>(
'/api/music/url',
{
songInfo: {
id: song.songId,
name: song.name,
singer: song.artist,
source: song.source,
songmid: song.songmid || song.songId.split('_').slice(1).join('_'),
},
quality,
},
'auto'
);
if (!urlResult?.url) {
return NextResponse.json({
success: false,
error: {
code: 'MUSIC_PLAY_FAILED',
message: urlResult?.error || '获取播放地址失败',
},
}, { status: 502 });
}
attempts = urlResult.attempts || attempts;
play = {
url: buildStableStreamUrl(song, quality),
directUrl: urlResult.url,
quality: urlResult.type || quality,
requestedQuality,
};
}
return NextResponse.json({
success: true,
data: {
song: cachedMeta.song,
...(play ? { play } : {}),
lyric: {
lyric: cachedMeta.lyric.lyric || '',
tlyric: cachedMeta.lyric.tlyric || '',
},
meta: {
attempts,
includeUrl,
},
},
});
} catch (error) {
console.error('[music-v2] play route error:', error);
return internalError('获取播放信息失败', (error as Error).message);
}
}
@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { badRequest, getMusicV2Username, internalError, unauthorized } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ playlistId: string }> }) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const { playlistId } = await params;
const playlist = await db.getMusicV2Playlist(playlistId);
if (!playlist) return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: '歌单不存在' } }, { status: 404 });
if (playlist.username !== username) return NextResponse.json({ success: false, error: { code: 'FORBIDDEN', message: '无权限操作此歌单' } }, { status: 403 });
const body = await request.json();
await db.updateMusicV2Playlist(playlistId, {
name: body?.name,
description: body?.description,
cover: body?.cover,
});
const updated = await db.getMusicV2Playlist(playlistId);
return NextResponse.json({ success: true, data: { playlist: updated } });
} catch (error) {
return internalError('更新歌单失败', (error as Error).message);
}
}
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ playlistId: string }> }) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const { playlistId } = await params;
const playlist = await db.getMusicV2Playlist(playlistId);
if (!playlist) return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: '歌单不存在' } }, { status: 404 });
if (playlist.username !== username) return NextResponse.json({ success: false, error: { code: 'FORBIDDEN', message: '无权限操作此歌单' } }, { status: 403 });
await db.deleteMusicV2Playlist(playlistId);
return NextResponse.json({ success: true });
} catch (error) {
return internalError('删除歌单失败', (error as Error).message);
}
}
@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { MusicV2PlaylistItem, normalizeSong } from '@/lib/music-v2';
import { badRequest, getMusicV2Username, internalError, unauthorized } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function GET(request: NextRequest, { params }: { params: Promise<{ playlistId: string }> }) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const { playlistId } = await params;
const playlist = await db.getMusicV2Playlist(playlistId);
if (!playlist) return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: '歌单不存在' } }, { status: 404 });
if (playlist.username !== username) return NextResponse.json({ success: false, error: { code: 'FORBIDDEN', message: '无权限访问此歌单' } }, { status: 403 });
const songs = await db.listMusicV2PlaylistItems(playlistId);
return NextResponse.json({ success: true, data: { songs } });
} catch (error) {
return internalError('获取歌单歌曲失败', (error as Error).message);
}
}
export async function POST(request: NextRequest, { params }: { params: Promise<{ playlistId: string }> }) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const { playlistId } = await params;
const playlist = await db.getMusicV2Playlist(playlistId);
if (!playlist) return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: '歌单不存在' } }, { status: 404 });
if (playlist.username !== username) return NextResponse.json({ success: false, error: { code: 'FORBIDDEN', message: '无权限操作此歌单' } }, { status: 403 });
const body = await request.json();
const song = normalizeSong(body?.song || {});
if (!song.songId || !song.source || !song.name || !song.artist) return badRequest('歌曲信息不完整');
const exists = await db.hasMusicV2PlaylistItem(playlistId, song.songId);
if (exists) return badRequest('歌曲已在歌单中', 'DUPLICATE_SONG');
const item: MusicV2PlaylistItem = {
...song,
playlistId,
sortOrder: Number(body?.sortOrder || 0),
addedAt: Date.now(),
updatedAt: Date.now(),
};
await db.addMusicV2PlaylistItem(playlistId, item);
return NextResponse.json({ success: true });
} catch (error) {
return internalError('添加歌曲失败', (error as Error).message);
}
}
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ playlistId: string }> }) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const { playlistId } = await params;
const playlist = await db.getMusicV2Playlist(playlistId);
if (!playlist) return NextResponse.json({ success: false, error: { code: 'NOT_FOUND', message: '歌单不存在' } }, { status: 404 });
if (playlist.username !== username) return NextResponse.json({ success: false, error: { code: 'FORBIDDEN', message: '无权限操作此歌单' } }, { status: 403 });
const { searchParams } = new URL(request.url);
const songId = searchParams.get('songId');
if (!songId) return badRequest('缺少 songId');
await db.removeMusicV2PlaylistItem(playlistId, songId);
return NextResponse.json({ success: true });
} catch (error) {
return internalError('删除歌曲失败', (error as Error).message);
}
}
+41
View File
@@ -0,0 +1,41 @@
import { randomUUID } from 'crypto';
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { badRequest, getMusicV2Username, internalError, unauthorized } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const playlists = await db.listMusicV2Playlists(username);
return NextResponse.json({ success: true, data: { playlists } });
} catch (error) {
return internalError('获取歌单失败', (error as Error).message);
}
}
export async function POST(request: NextRequest) {
const username = await getMusicV2Username(request);
if (!username) return unauthorized();
try {
const body = await request.json();
const name = body?.name?.trim();
if (!name) return badRequest('歌单名称不能为空');
const playlistId = randomUUID();
await db.createMusicV2Playlist(username, {
id: playlistId,
name,
description: body?.description?.trim(),
});
const playlist = await db.getMusicV2Playlist(playlistId);
return NextResponse.json({ success: true, data: { playlist } });
} catch (error) {
return internalError('创建歌单失败', (error as Error).message);
}
}
+33
View File
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server';
import { isMusicSource, lxGetJson, LxServerSong, normalizeLxSong } from '@/lib/music-v2';
import { badRequest, internalError } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const q = searchParams.get('q')?.trim() || '';
const source = searchParams.get('source') || 'kw';
const page = Number(searchParams.get('page') || '1');
const limit = Number(searchParams.get('limit') || '20');
if (!q) return badRequest('缺少搜索关键词');
if (!isMusicSource(source)) return badRequest('不支持的音源');
const list = await lxGetJson<LxServerSong[]>(`/api/music/search?name=${encodeURIComponent(q)}&source=${source}&page=${page}&limit=${limit}`, 'none');
return NextResponse.json({
success: true,
data: {
list: list.map(normalizeLxSong),
page,
limit,
hasMore: Array.isArray(list) && list.length >= limit,
},
});
} catch (error) {
return internalError('搜索歌曲失败', (error as Error).message);
}
}
+93
View File
@@ -0,0 +1,93 @@
import { NextRequest, NextResponse } from 'next/server';
import { extractSongmid, isMusicSource, lxPostJson, normalizeMusicQuality, normalizeSong } from '@/lib/music-v2';
import { badRequest } from '@/lib/music-v2-api';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const source = searchParams.get('source') || '';
const songId = searchParams.get('songId') || '';
const quality = normalizeMusicQuality(searchParams.get('quality') || '320k');
if (!isMusicSource(source)) return badRequest('不支持的音源');
if (!songId) return badRequest('缺少歌曲ID');
const song = normalizeSong({
songId,
source,
songmid: searchParams.get('songmid') || undefined,
name: searchParams.get('name') || '',
artist: searchParams.get('artist') || '',
durationText: searchParams.get('durationText') || undefined,
hash: searchParams.get('hash') || undefined,
copyrightId: searchParams.get('copyrightId') || undefined,
albumId: searchParams.get('albumId') || undefined,
lrcUrl: searchParams.get('lrcUrl') || undefined,
mrcUrl: searchParams.get('mrcUrl') || undefined,
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 upstreamUrl = urlResult?.url;
if (!upstreamUrl) {
return NextResponse.json({ success: false, error: { code: 'STREAM_FAILED', message: urlResult?.error || '获取音频流失败' } }, { status: 502 });
}
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) {
return NextResponse.json({ success: false, error: { code: 'STREAM_FAILED', message: '获取音频流失败' } }, { status: upstream.status });
}
const responseHeaders = new Headers();
responseHeaders.set('Content-Type', upstream.headers.get('content-type') || 'audio/mpeg');
responseHeaders.set('Cache-Control', 'public, max-age=31536000, immutable');
responseHeaders.set('Accept-Ranges', upstream.headers.get('accept-ranges') || 'bytes');
responseHeaders.set('Access-Control-Allow-Origin', '*');
const copyHeaders = ['content-length', 'content-range', 'etag', 'last-modified'];
for (const header of copyHeaders) {
const value = upstream.headers.get(header);
if (value) responseHeaders.set(header, value);
}
return new NextResponse(upstream.body, {
status: upstream.status,
headers: responseHeaders,
});
} catch (error) {
return NextResponse.json({ success: false, error: { code: 'STREAM_FAILED', message: (error as Error).message } }, { status: 400 });
}
}