新增基于lxserver的音乐功能
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user