From ec1ebb750c972cc90cab2cfd3ff239faa975d0a0 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Tue, 3 Feb 2026 20:37:01 +0800
Subject: [PATCH] =?UTF-8?q?=E9=9F=B3=E4=B9=90openlist=E4=BB=A3=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/admin/page.tsx | 38 +++++++
src/app/api/music/audio-proxy/route.ts | 135 +++++++++++++++++++++++++
src/app/api/music/route.ts | 13 ++-
src/lib/admin.types.ts | 1 +
src/lib/config.ts | 1 +
5 files changed, 187 insertions(+), 1 deletion(-)
create mode 100644 src/app/api/music/audio-proxy/route.ts
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 609ae6b..8e6a8cd 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -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)
+
+ {/* 缓存代理返回开关 */}
+
+
+
+
+
+
+ 开启后,如果 OpenList 有缓存,将通过代理方式返回给前端,并设置永久缓存头,提升加载速度
+
+
{/* 操作按钮 */}
diff --git a/src/app/api/music/audio-proxy/route.ts b/src/app/api/music/audio-proxy/route.ts
new file mode 100644
index 0000000..37ed724
--- /dev/null
+++ b/src/app/api/music/audio-proxy/route.ts
@@ -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 {
+ 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 = {
+ '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 = {
+ '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 }
+ );
+ }
+}
diff --git a/src/app/api/music/route.ts b/src/app/api/music/route.ts
index 34c0dcc..f0abf32 100644
--- a/src/app/api/music/route.ts
+++ b/src/app/api/music/route.ts
@@ -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;
diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts
index 4066aa8..9e64574 100644
--- a/src/lib/admin.types.ts
+++ b/src/lib/admin.types.ts
@@ -247,6 +247,7 @@ export interface AdminConfig {
OpenListCacheUsername?: string; // OpenList用户名
OpenListCachePassword?: string; // OpenList密码
OpenListCachePath?: string; // OpenList缓存目录路径
+ OpenListCacheProxyEnabled?: boolean; // 启用缓存代理返回(默认开启)
};
}
diff --git a/src/lib/config.ts b/src/lib/config.ts
index 58cc3ff..be14316 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -553,6 +553,7 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
OpenListCacheUsername: '',
OpenListCachePassword: '',
OpenListCachePath: '/music-cache',
+ OpenListCacheProxyEnabled: true,
};
}