音乐openlist代理
This commit is contained in:
@@ -6763,6 +6763,7 @@ const MusicConfigComponent = ({
|
|||||||
OpenListCacheUsername: '',
|
OpenListCacheUsername: '',
|
||||||
OpenListCachePassword: '',
|
OpenListCachePassword: '',
|
||||||
OpenListCachePath: '/music-cache',
|
OpenListCachePath: '/music-cache',
|
||||||
|
OpenListCacheProxyEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 从配置加载音乐设置
|
// 从配置加载音乐设置
|
||||||
@@ -6777,6 +6778,7 @@ const MusicConfigComponent = ({
|
|||||||
OpenListCacheUsername: config.MusicConfig.OpenListCacheUsername ?? '',
|
OpenListCacheUsername: config.MusicConfig.OpenListCacheUsername ?? '',
|
||||||
OpenListCachePassword: config.MusicConfig.OpenListCachePassword ?? '',
|
OpenListCachePassword: config.MusicConfig.OpenListCachePassword ?? '',
|
||||||
OpenListCachePath: config.MusicConfig.OpenListCachePath ?? '/music-cache',
|
OpenListCachePath: config.MusicConfig.OpenListCachePath ?? '/music-cache',
|
||||||
|
OpenListCacheProxyEnabled: config.MusicConfig.OpenListCacheProxyEnabled ?? true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
@@ -7018,6 +7020,42 @@ const MusicConfigComponent = ({
|
|||||||
音乐缓存在 OpenList 中的存储目录(例如:/music-cache)
|
音乐缓存在 OpenList 中的存储目录(例如:/music-cache)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
|
|||||||
@@ -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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取配置,检查是否启用缓存代理
|
||||||
|
const config = await getConfig();
|
||||||
|
const cacheProxyEnabled = config?.MusicConfig?.OpenListCacheProxyEnabled ?? true;
|
||||||
|
|
||||||
// TuneHub 返回的数据结构是 { code: 0, data: { data: [...], total: 1 } }
|
// TuneHub 返回的数据结构是 { code: 0, data: { data: [...], total: 1 } }
|
||||||
// 需要提取内层的 data 数组
|
// 需要提取内层的 data 数组
|
||||||
const songsData = data.data.data || data.data;
|
const songsData = data.data.data || data.data;
|
||||||
@@ -143,7 +147,14 @@ async function replaceAudioUrlsWithOpenList(
|
|||||||
const fileResponse = await openListClient.getFile(audioPath);
|
const fileResponse = await openListClient.getFile(audioPath);
|
||||||
|
|
||||||
if (fileResponse.code === 200 && fileResponse.data?.raw_url) {
|
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;
|
song.cached = true;
|
||||||
} else {
|
} else {
|
||||||
song.cached = false;
|
song.cached = false;
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ export interface AdminConfig {
|
|||||||
OpenListCacheUsername?: string; // OpenList用户名
|
OpenListCacheUsername?: string; // OpenList用户名
|
||||||
OpenListCachePassword?: string; // OpenList密码
|
OpenListCachePassword?: string; // OpenList密码
|
||||||
OpenListCachePath?: string; // OpenList缓存目录路径
|
OpenListCachePath?: string; // OpenList缓存目录路径
|
||||||
|
OpenListCacheProxyEnabled?: boolean; // 启用缓存代理返回(默认开启)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -553,6 +553,7 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
|||||||
OpenListCacheUsername: '',
|
OpenListCacheUsername: '',
|
||||||
OpenListCachePassword: '',
|
OpenListCachePassword: '',
|
||||||
OpenListCachePath: '/music-cache',
|
OpenListCachePath: '/music-cache',
|
||||||
|
OpenListCacheProxyEnabled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user