完善小雅
This commit is contained in:
50
src/app/api/douban/search/route.ts
Normal file
50
src/app/api/douban/search/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { fetchDoubanData } from '@/lib/douban';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
interface DoubanSearchResult {
|
||||
id: string;
|
||||
title: string;
|
||||
year: string;
|
||||
type?: string;
|
||||
sub_title?: string;
|
||||
episode?: string;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
interface DoubanSearchResponse {
|
||||
code: number;
|
||||
data?: DoubanSearchResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/douban/search?q=<query>
|
||||
* 搜索豆瓣影视作品
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const query = searchParams.get('q');
|
||||
|
||||
if (!query) {
|
||||
return NextResponse.json({ error: '缺少搜索关键词' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const target = `https://movie.douban.com/j/subject_suggest?q=${encodeURIComponent(query)}`;
|
||||
const data = await fetchDoubanData<DoubanSearchResult[]>(target);
|
||||
|
||||
const response: DoubanSearchResponse = {
|
||||
code: 200,
|
||||
data: data,
|
||||
};
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: '搜索豆瓣数据失败', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,58 @@ 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 {
|
||||
@@ -44,10 +93,70 @@ export async function GET(request: NextRequest) {
|
||||
xiaoyaConfig.Token
|
||||
);
|
||||
|
||||
const playUrl = await client.getDownloadUrl(path);
|
||||
// 优先尝试视频预览流方法
|
||||
try {
|
||||
const token = await client.getToken();
|
||||
|
||||
// 返回 302 重定向到播放链接
|
||||
return NextResponse.redirect(playUrl);
|
||||
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 },
|
||||
|
||||
@@ -4,13 +4,12 @@ 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';
|
||||
|
||||
/**
|
||||
* GET /api/xiaoya/search?keyword=<keyword>&page=<page>
|
||||
* 搜索小雅视频
|
||||
* GET /api/xiaoya/search?keyword=<keyword>&type=<type>
|
||||
* 搜索小雅视频(使用小雅的网页搜索引擎)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
@@ -21,7 +20,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const keyword = searchParams.get('keyword');
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const type = searchParams.get('type') || 'video'; // video, music, ebook, all
|
||||
|
||||
if (!keyword) {
|
||||
return NextResponse.json({ error: '缺少搜索关键词' }, { status: 400 });
|
||||
@@ -38,34 +37,59 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '小雅未配置或未启用' }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = new XiaoyaClient(
|
||||
xiaoyaConfig.ServerURL,
|
||||
xiaoyaConfig.Username,
|
||||
xiaoyaConfig.Password,
|
||||
xiaoyaConfig.Token
|
||||
);
|
||||
// 使用小雅的搜索引擎
|
||||
const searchUrl = `${xiaoyaConfig.ServerURL}/search?box=${encodeURIComponent(keyword)}&type=${type}&url=`;
|
||||
|
||||
const result = await client.search(keyword, page, 50);
|
||||
const response = await fetch(searchUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
},
|
||||
});
|
||||
|
||||
// 只返回视频文件
|
||||
const videoExtensions = ['.mp4', '.mkv', '.avi', '.m3u8', '.flv', '.ts', '.mov', '.wmv', '.webm'];
|
||||
if (!response.ok) {
|
||||
throw new Error(`搜索请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const videos = result.content
|
||||
.filter(item =>
|
||||
!item.is_dir &&
|
||||
videoExtensions.some(ext => item.name.toLowerCase().endsWith(ext))
|
||||
)
|
||||
.map(item => ({
|
||||
name: item.name,
|
||||
path: item.name, // Alist 搜索返回的是完整路径
|
||||
}));
|
||||
const html = await response.text();
|
||||
|
||||
// 解析 HTML 中的链接
|
||||
// 格式: <a href=/path/to/file>path/to/file</a>
|
||||
const linkRegex = /<a href=([^>]+)>([^<]+)<\/a>/g;
|
||||
const results: Array<{ name: string; path: string }> = [];
|
||||
|
||||
let match;
|
||||
while ((match = linkRegex.exec(html)) !== null) {
|
||||
let path = match[1];
|
||||
const displayText = match[2];
|
||||
|
||||
// 跳过返回首页和频道链接
|
||||
if (path === '/' || path.startsWith('http')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// URL 解码路径
|
||||
try {
|
||||
path = decodeURIComponent(path);
|
||||
} catch (e) {
|
||||
console.error('URL 解码失败:', path, e);
|
||||
}
|
||||
|
||||
// 提取文件名(路径的最后一部分)
|
||||
const pathParts = displayText.split('/');
|
||||
const fileName = pathParts[pathParts.length - 1];
|
||||
|
||||
results.push({
|
||||
name: fileName,
|
||||
path: path,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
videos,
|
||||
total: result.total,
|
||||
page,
|
||||
videos: results,
|
||||
total: results.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('小雅搜索失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
|
||||
Reference in New Issue
Block a user