音乐openlist代理
This commit is contained in:
@@ -6763,6 +6763,7 @@ const MusicConfigComponent = ({
|
||||
OpenListCacheUsername: '',
|
||||
OpenListCachePassword: '',
|
||||
OpenListCachePath: '/music-cache',
|
||||
OpenListCacheProxyEnabled: true,
|
||||
});
|
||||
|
||||
// 从配置加载音乐设置
|
||||
@@ -6777,6 +6778,7 @@ const MusicConfigComponent = ({
|
||||
OpenListCacheUsername: config.MusicConfig.OpenListCacheUsername ?? '',
|
||||
OpenListCachePassword: config.MusicConfig.OpenListCachePassword ?? '',
|
||||
OpenListCachePath: config.MusicConfig.OpenListCachePath ?? '/music-cache',
|
||||
OpenListCacheProxyEnabled: config.MusicConfig.OpenListCacheProxyEnabled ?? true,
|
||||
});
|
||||
}
|
||||
}, [config]);
|
||||
@@ -7018,6 +7020,42 @@ const MusicConfigComponent = ({
|
||||
音乐缓存在 OpenList 中的存储目录(例如:/music-cache)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 缓存代理返回开关 */}
|
||||
<div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
启用缓存代理返回
|
||||
</label>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() =>
|
||||
setMusicSettings((prev) => ({
|
||||
...prev,
|
||||
OpenListCacheProxyEnabled: !prev.OpenListCacheProxyEnabled,
|
||||
}))
|
||||
}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
|
||||
musicSettings.OpenListCacheProxyEnabled
|
||||
? buttonStyles.toggleOn
|
||||
: buttonStyles.toggleOff
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full ${
|
||||
buttonStyles.toggleThumb
|
||||
} transition-transform ${
|
||||
musicSettings.OpenListCacheProxyEnabled
|
||||
? buttonStyles.toggleThumbOn
|
||||
: buttonStyles.toggleThumbOff
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
开启后,如果 OpenList 有缓存,将通过代理方式返回给前端,并设置永久缓存头,提升加载速度
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
|
||||
135
src/app/api/music/audio-proxy/route.ts
Normal file
135
src/app/api/music/audio-proxy/route.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { OpenListClient } from '@/lib/openlist.client';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
// 获取 OpenList 客户端
|
||||
async function getOpenListClient(): Promise<OpenListClient | null> {
|
||||
const config = await getConfig();
|
||||
const musicConfig = config?.MusicConfig;
|
||||
|
||||
if (!musicConfig?.OpenListCacheEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = musicConfig.OpenListCacheURL;
|
||||
const username = musicConfig.OpenListCacheUsername;
|
||||
const password = musicConfig.OpenListCachePassword;
|
||||
|
||||
if (!url || !username || !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new OpenListClient(url, username, password);
|
||||
}
|
||||
|
||||
// 代理OpenList缓存的音频文件
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const platform = searchParams.get('platform');
|
||||
const id = searchParams.get('id');
|
||||
const quality = searchParams.get('quality');
|
||||
|
||||
if (!platform || !id || !quality) {
|
||||
return NextResponse.json(
|
||||
{ error: '缺少必要参数: platform, id, quality' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 获取OpenList客户端
|
||||
const openListClient = await getOpenListClient();
|
||||
if (!openListClient) {
|
||||
return NextResponse.json(
|
||||
{ error: 'OpenList未配置或未启用' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
const config = await getConfig();
|
||||
const cachePath = config?.MusicConfig?.OpenListCachePath || '/music-cache';
|
||||
|
||||
// 构建音频文件路径
|
||||
const audioPath = `${cachePath}/${platform}/audio/${id}-${quality}.mp3`;
|
||||
|
||||
// 获取文件信息
|
||||
const fileResponse = await openListClient.getFile(audioPath);
|
||||
|
||||
if (fileResponse.code !== 200 || !fileResponse.data?.raw_url) {
|
||||
return NextResponse.json(
|
||||
{ error: '音频文件未找到' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否有 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',
|
||||
};
|
||||
|
||||
// 如果有 Range 请求,转发给上游
|
||||
if (range) {
|
||||
upstreamHeaders['Range'] = range;
|
||||
}
|
||||
|
||||
// 从OpenList获取音频流
|
||||
const response = await fetch(fileResponse.data.raw_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=31536000, immutable', // 永久缓存(1年)
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Accept-Ranges': acceptRanges || 'bytes',
|
||||
'X-Cache-Source': 'openlist-audio-proxy',
|
||||
};
|
||||
|
||||
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('代理OpenList音频失败:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: '代理请求失败',
|
||||
details: (error as Error).message,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,10 @@ async function replaceAudioUrlsWithOpenList(
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取配置,检查是否启用缓存代理
|
||||
const config = await getConfig();
|
||||
const cacheProxyEnabled = config?.MusicConfig?.OpenListCacheProxyEnabled ?? true;
|
||||
|
||||
// TuneHub 返回的数据结构是 { code: 0, data: { data: [...], total: 1 } }
|
||||
// 需要提取内层的 data 数组
|
||||
const songsData = data.data.data || data.data;
|
||||
@@ -143,7 +147,14 @@ async function replaceAudioUrlsWithOpenList(
|
||||
const fileResponse = await openListClient.getFile(audioPath);
|
||||
|
||||
if (fileResponse.code === 200 && fileResponse.data?.raw_url) {
|
||||
song.url = fileResponse.data.raw_url;
|
||||
// 如果启用缓存代理,返回代理URL;否则返回直接URL
|
||||
if (cacheProxyEnabled) {
|
||||
// 使用代理URL,通过我们的服务器代理OpenList的音频
|
||||
song.url = `/api/music/audio-proxy?platform=${platform}&id=${song.id}&quality=${quality}`;
|
||||
} else {
|
||||
// 直接使用OpenList的raw_url
|
||||
song.url = fileResponse.data.raw_url;
|
||||
}
|
||||
song.cached = true;
|
||||
} else {
|
||||
song.cached = false;
|
||||
|
||||
@@ -247,6 +247,7 @@ export interface AdminConfig {
|
||||
OpenListCacheUsername?: string; // OpenList用户名
|
||||
OpenListCachePassword?: string; // OpenList密码
|
||||
OpenListCachePath?: string; // OpenList缓存目录路径
|
||||
OpenListCacheProxyEnabled?: boolean; // 启用缓存代理返回(默认开启)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -553,6 +553,7 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
OpenListCacheUsername: '',
|
||||
OpenListCachePassword: '',
|
||||
OpenListCachePath: '/music-cache',
|
||||
OpenListCacheProxyEnabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user