167 lines
4.7 KiB
TypeScript
167 lines
4.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
|
||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||
import { getConfig } from '@/lib/config';
|
||
import { XiaoyaClient } from '@/lib/xiaoya.client';
|
||
|
||
export const runtime = 'nodejs';
|
||
|
||
/**
|
||
* 使用 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('[xiaoya/play] 获取最终 URL 失败:', error);
|
||
return currentUrl;
|
||
}
|
||
}
|
||
|
||
return currentUrl;
|
||
}
|
||
|
||
/**
|
||
* GET /api/xiaoya/play?path=<path>
|
||
* 获取小雅视频的播放链接(优先使用视频预览流,失败时降级到直连)
|
||
*/
|
||
export async function GET(request: NextRequest) {
|
||
try {
|
||
const authInfo = getAuthInfoFromCookie(request);
|
||
if (!authInfo || !authInfo.username) {
|
||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||
}
|
||
|
||
const { searchParams } = new URL(request.url);
|
||
const path = searchParams.get('path');
|
||
|
||
if (!path) {
|
||
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
||
}
|
||
|
||
const config = await getConfig();
|
||
const xiaoyaConfig = config.XiaoyaConfig;
|
||
|
||
if (
|
||
!xiaoyaConfig ||
|
||
!xiaoyaConfig.Enabled ||
|
||
!xiaoyaConfig.ServerURL
|
||
) {
|
||
return NextResponse.json({ error: '小雅未配置或未启用' }, { status: 400 });
|
||
}
|
||
|
||
const client = new XiaoyaClient(
|
||
xiaoyaConfig.ServerURL,
|
||
xiaoyaConfig.Username,
|
||
xiaoyaConfig.Password,
|
||
xiaoyaConfig.Token
|
||
);
|
||
|
||
// 优先尝试视频预览流方法
|
||
try {
|
||
const token = await client.getToken();
|
||
|
||
const response = await fetch(`${xiaoyaConfig.ServerURL}/api/fs/other`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': token,
|
||
},
|
||
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}`);
|
||
}
|
||
|
||
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,
|
||
'SD': 3,
|
||
'LD': 4,
|
||
};
|
||
|
||
const qualities = taskList
|
||
.filter((task: any) => task.status === 'finished')
|
||
.map((task: any) => ({
|
||
name: task.template_id,
|
||
url: task.url,
|
||
}))
|
||
.sort((a, b) => (qualityOrder[a.name] || 999) - (qualityOrder[b.name] || 999));
|
||
|
||
if (qualities.length === 0) {
|
||
throw new Error('未找到已完成的播放链接');
|
||
}
|
||
|
||
return NextResponse.json({
|
||
url: qualities[0].url,
|
||
qualities
|
||
});
|
||
} 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 });
|
||
}
|
||
} catch (error) {
|
||
return NextResponse.json(
|
||
{ error: (error as Error).message },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|