脚本实装
This commit is contained in:
@@ -48,10 +48,17 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const items = await listSourceScripts();
|
||||
return NextResponse.json({
|
||||
items,
|
||||
template: getDefaultSourceScriptTemplate(),
|
||||
});
|
||||
return NextResponse.json(
|
||||
{
|
||||
items,
|
||||
template: getDefaultSourceScriptTemplate(),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || '获取脚本列表失败' },
|
||||
@@ -80,19 +87,47 @@ export async function POST(request: NextRequest) {
|
||||
code: body.code,
|
||||
enabled: body.enabled,
|
||||
});
|
||||
return NextResponse.json({ ok: true, item: saved });
|
||||
return NextResponse.json(
|
||||
{ ok: true, item: saved },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
case 'delete': {
|
||||
await deleteSourceScript(body.id);
|
||||
return NextResponse.json({ ok: true });
|
||||
return NextResponse.json(
|
||||
{ ok: true },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
case 'toggle_enabled': {
|
||||
const item = await toggleSourceScriptEnabled(body.id);
|
||||
return NextResponse.json({ ok: true, item });
|
||||
return NextResponse.json(
|
||||
{ ok: true, item },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
case 'restore': {
|
||||
const item = await restoreSourceScriptHistory(body.id, body.version);
|
||||
return NextResponse.json({ ok: true, item });
|
||||
return NextResponse.json(
|
||||
{ ok: true, item },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
case 'test': {
|
||||
const result = await testSourceScript({
|
||||
@@ -108,13 +143,24 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(result, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json(result);
|
||||
return NextResponse.json(result, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
});
|
||||
}
|
||||
case 'import': {
|
||||
const imported = await importSourceScripts(
|
||||
Array.isArray(body.items) ? body.items : []
|
||||
);
|
||||
return NextResponse.json({ ok: true, items: imported });
|
||||
return NextResponse.json(
|
||||
{ ok: true, items: imported },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
default:
|
||||
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
||||
|
||||
@@ -3,6 +3,13 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { getDetailFromApi } from '@/lib/downstream';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
normalizeScriptDetailResult,
|
||||
resolveScriptDetailPlaybacks,
|
||||
normalizeScriptSources,
|
||||
parseScriptSourceValue,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -20,6 +27,55 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsedScriptSource = parseScriptSourceValue(sourceCode);
|
||||
if (parsedScriptSource) {
|
||||
try {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: parsedScriptSource.scriptKey,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
const sources = normalizeScriptSources(sourcesExecution.result);
|
||||
const sourceInfo =
|
||||
sources.find((item) => item.id === parsedScriptSource.sourceId) || {
|
||||
id: parsedScriptSource.sourceId,
|
||||
name: parsedScriptSource.sourceId,
|
||||
};
|
||||
|
||||
const detailExecution = await executeSavedSourceScript({
|
||||
key: parsedScriptSource.scriptKey,
|
||||
hook: 'detail',
|
||||
payload: {
|
||||
id,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
},
|
||||
});
|
||||
|
||||
const resolvedDetailResult = await resolveScriptDetailPlaybacks({
|
||||
scriptKey: parsedScriptSource.scriptKey,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
result: detailExecution.result,
|
||||
});
|
||||
|
||||
const normalized = normalizeScriptDetailResult({
|
||||
source: sourceCode,
|
||||
scriptKey: parsedScriptSource.scriptKey,
|
||||
scriptName: detailExecution.meta?.name || parsedScriptSource.scriptKey,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
sourceName: sourceInfo.name,
|
||||
detailId: id,
|
||||
result: resolvedDetailResult,
|
||||
});
|
||||
|
||||
return NextResponse.json(normalized);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理 openlist 源
|
||||
if (sourceCode === 'openlist') {
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,12 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
listEnabledSourceScripts,
|
||||
normalizeScriptSearchResults,
|
||||
normalizeScriptSources,
|
||||
} from '@/lib/source-script';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
@@ -37,6 +43,69 @@ export async function GET(request: NextRequest) {
|
||||
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||
|
||||
try {
|
||||
const enabledScripts = await listEnabledSourceScripts();
|
||||
const matchedScript = enabledScripts.find((item) => item.key === resourceId);
|
||||
if (matchedScript) {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: matchedScript.key,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
const sources = normalizeScriptSources(sourcesExecution.result);
|
||||
const scriptResults = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
const execution = await executeSavedSourceScript({
|
||||
key: matchedScript.key,
|
||||
hook: 'search',
|
||||
payload: {
|
||||
keyword: query,
|
||||
page: 1,
|
||||
sourceId: source.id,
|
||||
},
|
||||
});
|
||||
|
||||
return normalizeScriptSearchResults({
|
||||
scriptKey: matchedScript.key,
|
||||
scriptName: matchedScript.name,
|
||||
sourceId: source.id,
|
||||
sourceName: source.name,
|
||||
result: execution.result,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
let result = scriptResults.flat().filter((r) => r.title === query);
|
||||
if (!config.SiteConfig.DisableYellowFilter) {
|
||||
result = result.filter((item) => {
|
||||
const typeName = item.type_name || '';
|
||||
return !yellowWords.some((word: string) => typeName.includes(word));
|
||||
});
|
||||
}
|
||||
|
||||
const cacheTime = await getCacheTime();
|
||||
if (result.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: '未找到结果',
|
||||
result: null,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ results: result },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
|
||||
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
|
||||
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
|
||||
'Netlify-Vary': 'query',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 根据 resourceId 查找对应的 API 站点
|
||||
const targetSite = apiSites.find((site) => site.key === resourceId);
|
||||
if (!targetSite) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAvailableApiSites } from '@/lib/config';
|
||||
import { listEnabledSourceScripts } from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -11,8 +12,13 @@ export async function GET(request: NextRequest) {
|
||||
console.log('request', request.url);
|
||||
try {
|
||||
const apiSites = await getAvailableApiSites();
|
||||
const scriptSites = (await listEnabledSourceScripts()).map((item) => ({
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
script: true,
|
||||
}));
|
||||
|
||||
return NextResponse.json(apiSites);
|
||||
return NextResponse.json([...apiSites, ...scriptSites]);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: '获取资源失败' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -5,8 +5,14 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
listEnabledSourceScripts,
|
||||
normalizeScriptSearchResults,
|
||||
normalizeScriptSources,
|
||||
} from '@/lib/source-script';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -178,24 +184,76 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
);
|
||||
|
||||
const scriptSummaries = await listEnabledSourceScripts();
|
||||
const scriptPromises = scriptSummaries.map((script) =>
|
||||
Promise.race([
|
||||
(async () => {
|
||||
try {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: script.key,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
const sources = normalizeScriptSources(sourcesExecution.result);
|
||||
|
||||
const searchResults = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
const execution = await executeSavedSourceScript({
|
||||
key: script.key,
|
||||
hook: 'search',
|
||||
payload: {
|
||||
keyword: query,
|
||||
page: 1,
|
||||
sourceId: source.id,
|
||||
},
|
||||
});
|
||||
|
||||
return normalizeScriptSearchResults({
|
||||
scriptKey: script.key,
|
||||
scriptName: script.name,
|
||||
sourceId: source.id,
|
||||
sourceName: source.name,
|
||||
result: execution.result,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return searchResults.flat();
|
||||
} catch (error) {
|
||||
console.error(`[Search] 搜索脚本 ${script.name} 失败:`, error);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
new Promise<any[]>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`${script.name} timeout`)), 20000)
|
||||
),
|
||||
]).catch((error) => {
|
||||
console.error(`[Search] 搜索脚本 ${script.name} 超时:`, error);
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const allResults = await Promise.all([
|
||||
openlistPromise,
|
||||
...embyPromises,
|
||||
...searchPromises,
|
||||
...scriptPromises,
|
||||
]);
|
||||
|
||||
// 分离结果:第一个是 openlist,接下来是 emby 结果,最后是 api 结果
|
||||
// 添加安全检查,确保即使某个结果处理出错也不影响其他结果
|
||||
const openlistResults = Array.isArray(allResults[0]) ? allResults[0] : [];
|
||||
const embyResultsArray = allResults.slice(1, 1 + embyPromises.length);
|
||||
const apiResults = allResults.slice(1 + embyPromises.length);
|
||||
const apiResults = allResults.slice(1 + embyPromises.length, 1 + embyPromises.length + searchPromises.length);
|
||||
const scriptResults = allResults.slice(1 + embyPromises.length + searchPromises.length);
|
||||
|
||||
// 合并所有 Emby 结果,添加安全检查
|
||||
const embyResults = embyResultsArray.filter(Array.isArray).flat();
|
||||
const apiResultsFlat = apiResults.filter(Array.isArray).flat();
|
||||
const scriptResultsFlat = scriptResults.filter(Array.isArray).flat();
|
||||
|
||||
let flattenedResults = [...openlistResults, ...embyResults, ...apiResultsFlat];
|
||||
let flattenedResults = [...openlistResults, ...embyResults, ...apiResultsFlat, ...scriptResultsFlat];
|
||||
|
||||
flattenedResults = flattenedResults.map((result) => ({
|
||||
...result,
|
||||
|
||||
@@ -7,6 +7,12 @@ import { getAvailableApiSites, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
listEnabledSourceScripts,
|
||||
normalizeScriptSearchResults,
|
||||
normalizeScriptSources,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -61,6 +67,7 @@ export async function GET(request: NextRequest) {
|
||||
config.EmbyConfig.Sources.length > 0 &&
|
||||
config.EmbyConfig.Sources.some(s => s.enabled && s.ServerURL)
|
||||
);
|
||||
const enabledScripts = await listEnabledSourceScripts();
|
||||
|
||||
// 共享状态
|
||||
let streamClosed = false;
|
||||
@@ -103,7 +110,7 @@ export async function GET(request: NextRequest) {
|
||||
const startEvent = `data: ${JSON.stringify({
|
||||
type: 'start',
|
||||
query,
|
||||
totalSources: sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount,
|
||||
totalSources: sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount + enabledScripts.length,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
@@ -387,7 +394,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 检查是否所有源都已完成
|
||||
if (completedSources === sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount) {
|
||||
if (completedSources === sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount + enabledScripts.length) {
|
||||
if (!streamClosed) {
|
||||
// 发送最终完成事件
|
||||
const completeEvent = `data: ${JSON.stringify({
|
||||
@@ -409,8 +416,118 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
});
|
||||
|
||||
const scriptPromises = enabledScripts.map(async (script) => {
|
||||
try {
|
||||
const sourcesExecution = await Promise.race([
|
||||
executeSavedSourceScript({
|
||||
key: script.key,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`${script.name} timeout`)), 20000)
|
||||
),
|
||||
]);
|
||||
|
||||
const sources = normalizeScriptSources((sourcesExecution as any).result);
|
||||
const sourceResults = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
const execution = await Promise.race([
|
||||
executeSavedSourceScript({
|
||||
key: script.key,
|
||||
hook: 'search',
|
||||
payload: {
|
||||
keyword: query,
|
||||
page: 1,
|
||||
sourceId: source.id,
|
||||
},
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`${script.name}/${source.name} timeout`)), 20000)
|
||||
),
|
||||
]);
|
||||
|
||||
return normalizeScriptSearchResults({
|
||||
scriptKey: script.key,
|
||||
scriptName: script.name,
|
||||
sourceId: source.id,
|
||||
sourceName: source.name,
|
||||
result: (execution as any).result,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
let filteredResults = sourceResults.flat();
|
||||
if (!config.SiteConfig.DisableYellowFilter) {
|
||||
filteredResults = filteredResults.filter((result) => {
|
||||
const typeName = result.type_name || '';
|
||||
return !yellowWords.some((word: string) => typeName.includes(word));
|
||||
});
|
||||
}
|
||||
|
||||
completedSources++;
|
||||
|
||||
if (!streamClosed) {
|
||||
const sourceEvent = `data: ${JSON.stringify({
|
||||
type: 'source_result',
|
||||
source: `script:${script.key}`,
|
||||
sourceName: script.name,
|
||||
results: filteredResults,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
if (!safeEnqueue(encoder.encode(sourceEvent))) {
|
||||
streamClosed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredResults.length > 0) {
|
||||
allResults.push(...filteredResults);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`搜索脚本失败 ${script.name}:`, error);
|
||||
|
||||
completedSources++;
|
||||
|
||||
if (!streamClosed) {
|
||||
const errorEvent = `data: ${JSON.stringify({
|
||||
type: 'source_error',
|
||||
source: `script:${script.key}`,
|
||||
sourceName: script.name,
|
||||
error: error instanceof Error ? error.message : '搜索失败',
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
if (!safeEnqueue(encoder.encode(errorEvent))) {
|
||||
streamClosed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (completedSources === sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount + enabledScripts.length) {
|
||||
if (!streamClosed) {
|
||||
const completeEvent = `data: ${JSON.stringify({
|
||||
type: 'complete',
|
||||
totalResults: allResults.length,
|
||||
completedSources,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
if (safeEnqueue(encoder.encode(completeEvent))) {
|
||||
try {
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
console.warn('Failed to close controller:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 等待所有搜索完成
|
||||
await Promise.allSettled(searchPromises);
|
||||
await Promise.allSettled([...searchPromises, ...scriptPromises]);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
|
||||
@@ -6,6 +6,13 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { getDetailFromApiV2 } from '@/lib/downstream';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
normalizeScriptDetailResult,
|
||||
resolveScriptDetailPlaybacks,
|
||||
normalizeScriptSources,
|
||||
parseScriptSourceValue,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -28,6 +35,55 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsedScriptSource = parseScriptSourceValue(sourceCode);
|
||||
if (parsedScriptSource) {
|
||||
try {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: parsedScriptSource.scriptKey,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
const sources = normalizeScriptSources(sourcesExecution.result);
|
||||
const sourceInfo =
|
||||
sources.find((item) => item.id === parsedScriptSource.sourceId) || {
|
||||
id: parsedScriptSource.sourceId,
|
||||
name: parsedScriptSource.sourceId,
|
||||
};
|
||||
|
||||
const detailExecution = await executeSavedSourceScript({
|
||||
key: parsedScriptSource.scriptKey,
|
||||
hook: 'detail',
|
||||
payload: {
|
||||
id,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
},
|
||||
});
|
||||
|
||||
const resolvedDetailResult = await resolveScriptDetailPlaybacks({
|
||||
scriptKey: parsedScriptSource.scriptKey,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
result: detailExecution.result,
|
||||
});
|
||||
|
||||
const normalized = normalizeScriptDetailResult({
|
||||
source: sourceCode,
|
||||
scriptKey: parsedScriptSource.scriptKey,
|
||||
scriptName: detailExecution.meta?.name || parsedScriptSource.scriptKey,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
sourceName: sourceInfo.name,
|
||||
detailId: id,
|
||||
result: resolvedDetailResult,
|
||||
});
|
||||
|
||||
return NextResponse.json(normalized);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理 emby 源(支持多源)
|
||||
if (sourceCode === 'emby' || sourceCode.startsWith('emby_')) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user