音乐openlist代理

This commit is contained in:
mtvpls
2026-02-03 20:37:01 +08:00
parent 54cdd87bcc
commit ec1ebb750c
5 changed files with 187 additions and 1 deletions

View File

@@ -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>
{/* 操作按钮 */}

View 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 }
);
}
}

View File

@@ -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;

View File

@@ -247,6 +247,7 @@ export interface AdminConfig {
OpenListCacheUsername?: string; // OpenList用户名
OpenListCachePassword?: string; // OpenList密码
OpenListCachePath?: string; // OpenList缓存目录路径
OpenListCacheProxyEnabled?: boolean; // 启用缓存代理返回(默认开启)
};
}

View File

@@ -553,6 +553,7 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
OpenListCacheUsername: '',
OpenListCachePassword: '',
OpenListCachePath: '/music-cache',
OpenListCacheProxyEnabled: true,
};
}