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, }; }