openlist支持视频预览方式播放
This commit is contained in:
@@ -9,9 +9,59 @@ import { OpenListClient } from '@/lib/openlist.client';
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* GET /api/openlist/play?folder=xxx&fileName=xxx
|
||||
* 获取单个视频文件的播放链接(懒加载)
|
||||
* 返回重定向到真实播放 URL
|
||||
* 使用 HEAD 请求跟随重定向获取最终 URL(直连方法 - 降级使用)
|
||||
*/
|
||||
async function getFinalUrl(url: string, maxRedirects = 5): Promise<string> {
|
||||
let currentUrl = url;
|
||||
let redirectCount = 0;
|
||||
|
||||
while (redirectCount < maxRedirects) {
|
||||
try {
|
||||
const response = await fetch(currentUrl, {
|
||||
method: 'HEAD',
|
||||
redirect: 'manual',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
const location = response.headers.get('location');
|
||||
if (!location) {
|
||||
return currentUrl;
|
||||
}
|
||||
|
||||
if (location.startsWith('http://') || location.startsWith('https://')) {
|
||||
currentUrl = location;
|
||||
} else if (location.startsWith('/')) {
|
||||
const urlObj = new URL(currentUrl);
|
||||
currentUrl = `${urlObj.protocol}//${urlObj.host}${location}`;
|
||||
} else {
|
||||
const urlObj = new URL(currentUrl);
|
||||
const pathParts = urlObj.pathname.split('/');
|
||||
pathParts.pop();
|
||||
pathParts.push(location);
|
||||
currentUrl = `${urlObj.protocol}//${urlObj.host}${pathParts.join('/')}`;
|
||||
}
|
||||
|
||||
redirectCount++;
|
||||
} else {
|
||||
return currentUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[openlist/play] 获取最终 URL 失败:', error);
|
||||
return currentUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return currentUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/openlist/play?folder=xxx&fileName=xxx&format=json
|
||||
* 获取单个视频文件的播放链接(优先使用视频预览流,失败时降级到直连)
|
||||
* format=json: 返回 JSON 格式(用于 play 页面)
|
||||
* 默认: 返回重定向(用于 tvbox 等)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -23,6 +73,7 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const folderName = searchParams.get('folder');
|
||||
const fileName = searchParams.get('fileName');
|
||||
const format = searchParams.get('format'); // 新增 format 参数
|
||||
|
||||
if (!folderName || !fileName) {
|
||||
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
||||
@@ -51,23 +102,71 @@ export async function GET(request: NextRequest) {
|
||||
openListConfig.Password
|
||||
);
|
||||
|
||||
// 获取文件的播放链接
|
||||
const fileResponse = await client.getFile(filePath);
|
||||
// 优先尝试视频预览流方法
|
||||
try {
|
||||
const data = await client.getVideoPreview(filePath);
|
||||
|
||||
if (fileResponse.code !== 200 || !fileResponse.data.raw_url) {
|
||||
console.error('[OpenList Play] 获取播放URL失败:', {
|
||||
fileName,
|
||||
code: fileResponse.code,
|
||||
message: fileResponse.message,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: '获取播放链接失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
const taskList = data.data?.video_preview_play_info?.live_transcoding_task_list;
|
||||
if (!taskList || taskList.length === 0) {
|
||||
throw new Error('未找到可用的播放链接');
|
||||
}
|
||||
|
||||
const qualityOrder: Record<string, number> = {
|
||||
'FHD': 1,
|
||||
'HD': 2,
|
||||
'LD': 3,
|
||||
'SD': 4,
|
||||
};
|
||||
|
||||
const qualities = taskList
|
||||
.filter((task: any) => task.status === 'finished')
|
||||
.map((task: any) => ({
|
||||
name: task.template_id,
|
||||
url: task.url,
|
||||
}))
|
||||
.sort((a: any, b: any) => (qualityOrder[a.name] || 999) - (qualityOrder[b.name] || 999));
|
||||
|
||||
if (qualities.length === 0) {
|
||||
throw new Error('未找到已完成的播放链接');
|
||||
}
|
||||
|
||||
// 如果指定了 format=json,返回 JSON 格式
|
||||
if (format === 'json') {
|
||||
return NextResponse.json({
|
||||
url: qualities[0].url,
|
||||
qualities
|
||||
});
|
||||
}
|
||||
|
||||
// 默认返回重定向(用于 tvbox)
|
||||
return NextResponse.redirect(qualities[0].url);
|
||||
} catch (error) {
|
||||
// 视频预览流失败,降级到直连方法
|
||||
console.log('[openlist/play] 视频预览流失败,降级到直连方法:', (error as Error).message);
|
||||
|
||||
const fileResponse = await client.getFile(filePath);
|
||||
|
||||
if (fileResponse.code !== 200 || !fileResponse.data.raw_url) {
|
||||
console.error('[OpenList Play] 获取播放URL失败:', {
|
||||
fileName,
|
||||
code: fileResponse.code,
|
||||
message: fileResponse.message,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: '获取播放链接失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 如果指定了 format=json,使用 getFinalUrl 并返回 JSON
|
||||
if (format === 'json') {
|
||||
const finalUrl = await getFinalUrl(fileResponse.data.raw_url);
|
||||
return NextResponse.json({ url: finalUrl });
|
||||
}
|
||||
|
||||
// 默认返回重定向(用于 tvbox)
|
||||
return NextResponse.redirect(fileResponse.data.raw_url);
|
||||
}
|
||||
|
||||
// 返回重定向到真实播放 URL
|
||||
return NextResponse.redirect(fileResponse.data.raw_url);
|
||||
} catch (error) {
|
||||
console.error('获取播放链接失败:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -58,9 +58,11 @@ async function getFinalUrl(url: string, maxRedirects = 5): Promise<string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/xiaoya/play?path=<path>
|
||||
* GET /api/xiaoya/play?path=<path>&format=json
|
||||
* 获取小雅视频的播放链接(优先使用视频预览流,失败时降级到直连)
|
||||
* path参数为base58编码的路径
|
||||
* format=json: 返回 JSON 格式(用于 play 页面)
|
||||
* 默认: 返回重定向(用于 tvbox 等)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -71,6 +73,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const encodedPath = searchParams.get('path');
|
||||
const format = searchParams.get('format'); // 新增 format 参数
|
||||
|
||||
if (!encodedPath) {
|
||||
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
||||
@@ -149,18 +152,30 @@ export async function GET(request: NextRequest) {
|
||||
throw new Error('未找到已完成的播放链接');
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
url: qualities[0].url,
|
||||
qualities
|
||||
});
|
||||
// 如果指定了 format=json,返回 JSON 格式
|
||||
if (format === 'json') {
|
||||
return NextResponse.json({
|
||||
url: qualities[0].url,
|
||||
qualities
|
||||
});
|
||||
}
|
||||
|
||||
// 默认返回重定向(用于 tvbox)
|
||||
return NextResponse.redirect(qualities[0].url);
|
||||
} catch (error) {
|
||||
// 视频预览流失败,降级到直连方法
|
||||
console.log('[xiaoya/play] 视频预览流失败,降级到直连方法:', (error as Error).message);
|
||||
|
||||
const playUrl = await client.getDownloadUrl(path);
|
||||
const finalUrl = await getFinalUrl(playUrl);
|
||||
|
||||
return NextResponse.json({ url: finalUrl });
|
||||
// 如果指定了 format=json,使用 getFinalUrl 并返回 JSON
|
||||
if (format === 'json') {
|
||||
const finalUrl = await getFinalUrl(playUrl);
|
||||
return NextResponse.json({ url: finalUrl });
|
||||
}
|
||||
|
||||
// 默认返回重定向(用于 tvbox)
|
||||
return NextResponse.redirect(playUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1498,10 +1498,14 @@ function PlayPageClient() {
|
||||
|
||||
let newUrl = detailData?.episodes[episodeIndex] || '';
|
||||
|
||||
// 如果是小雅接口,先请求获取真实 URL
|
||||
if (newUrl.startsWith('/api/xiaoya/play')) {
|
||||
// 如果是小雅或 openlist 接口,先请求获取真实 URL
|
||||
if (newUrl.startsWith('/api/xiaoya/play') || newUrl.startsWith('/api/openlist/play')) {
|
||||
try {
|
||||
const response = await fetch(newUrl);
|
||||
// 添加 format=json 参数
|
||||
const separator = newUrl.includes('?') ? '&' : '?';
|
||||
const fetchUrl = `${newUrl}${separator}format=json`;
|
||||
|
||||
const response = await fetch(fetchUrl);
|
||||
const data = await response.json();
|
||||
if (data.url) {
|
||||
newUrl = data.url;
|
||||
@@ -1513,11 +1517,11 @@ function PlayPageClient() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取小雅播放链接失败:', error);
|
||||
console.error('获取播放链接失败:', error);
|
||||
setVideoQualities([]);
|
||||
}
|
||||
} else {
|
||||
// 非小雅源,清空清晰度列表
|
||||
// 非小雅/openlist 源,清空清晰度列表
|
||||
setVideoQualities([]);
|
||||
}
|
||||
|
||||
|
||||
@@ -280,6 +280,31 @@ export class OpenListClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取视频预览流
|
||||
async getVideoPreview(path: string): Promise<any> {
|
||||
const response = await this.fetchWithRetry(`${this.baseURL}/api/fs/other`, {
|
||||
method: 'POST',
|
||||
headers: await this.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
path: path,
|
||||
method: 'video_preview',
|
||||
password: '',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`视频预览请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.code !== 200) {
|
||||
throw new Error(`视频预览失败: ${data.message}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 检查连通性
|
||||
async checkConnectivity(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user