调整视频源脚本播放流程
This commit is contained in:
@@ -6,7 +6,6 @@ import { getDetailFromApi } from '@/lib/downstream';
|
|||||||
import {
|
import {
|
||||||
executeSavedSourceScript,
|
executeSavedSourceScript,
|
||||||
normalizeScriptDetailResult,
|
normalizeScriptDetailResult,
|
||||||
resolveScriptDetailPlaybacks,
|
|
||||||
normalizeScriptSources,
|
normalizeScriptSources,
|
||||||
parseScriptSourceValue,
|
parseScriptSourceValue,
|
||||||
} from '@/lib/source-script';
|
} from '@/lib/source-script';
|
||||||
@@ -51,12 +50,6 @@ export async function GET(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedDetailResult = await resolveScriptDetailPlaybacks({
|
|
||||||
scriptKey: parsedScriptSource.scriptKey,
|
|
||||||
sourceId: parsedScriptSource.sourceId,
|
|
||||||
result: detailExecution.result,
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalized = normalizeScriptDetailResult({
|
const normalized = normalizeScriptDetailResult({
|
||||||
source: sourceCode,
|
source: sourceCode,
|
||||||
scriptKey: parsedScriptSource.scriptKey,
|
scriptKey: parsedScriptSource.scriptKey,
|
||||||
@@ -64,7 +57,7 @@ export async function GET(request: NextRequest) {
|
|||||||
sourceId: parsedScriptSource.sourceId,
|
sourceId: parsedScriptSource.sourceId,
|
||||||
sourceName: sourceInfo.name,
|
sourceName: sourceInfo.name,
|
||||||
detailId: id,
|
detailId: id,
|
||||||
result: resolvedDetailResult,
|
result: detailExecution.result,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(normalized);
|
return NextResponse.json(normalized);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { getProxyToken } from '@/lib/emby-token';
|
|||||||
import {
|
import {
|
||||||
executeSavedSourceScript,
|
executeSavedSourceScript,
|
||||||
normalizeScriptDetailResult,
|
normalizeScriptDetailResult,
|
||||||
resolveScriptDetailPlaybacks,
|
|
||||||
normalizeScriptSources,
|
normalizeScriptSources,
|
||||||
parseScriptSourceValue,
|
parseScriptSourceValue,
|
||||||
} from '@/lib/source-script';
|
} from '@/lib/source-script';
|
||||||
@@ -59,12 +58,6 @@ export async function GET(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedDetailResult = await resolveScriptDetailPlaybacks({
|
|
||||||
scriptKey: parsedScriptSource.scriptKey,
|
|
||||||
sourceId: parsedScriptSource.sourceId,
|
|
||||||
result: detailExecution.result,
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalized = normalizeScriptDetailResult({
|
const normalized = normalizeScriptDetailResult({
|
||||||
source: sourceCode,
|
source: sourceCode,
|
||||||
scriptKey: parsedScriptSource.scriptKey,
|
scriptKey: parsedScriptSource.scriptKey,
|
||||||
@@ -72,7 +65,7 @@ export async function GET(request: NextRequest) {
|
|||||||
sourceId: parsedScriptSource.sourceId,
|
sourceId: parsedScriptSource.sourceId,
|
||||||
sourceName: sourceInfo.name,
|
sourceName: sourceInfo.name,
|
||||||
detailId: id,
|
detailId: id,
|
||||||
result: resolvedDetailResult,
|
result: detailExecution.result,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(normalized);
|
return NextResponse.json(normalized);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
import {
|
||||||
|
parseScriptPlayUrlValue,
|
||||||
|
resolveSavedScriptPlayUrl,
|
||||||
|
} from '@/lib/source-script';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/source-script/play?key=xxx&sourceId=xxx&lineId=xxx&episodeIndex=0&playUrl=base64url&format=json
|
||||||
|
* format=json: 返回 JSON 格式(用于 play 页面)
|
||||||
|
* 默认: 返回重定向(用于播放器或外部调用)
|
||||||
|
*/
|
||||||
|
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 key = searchParams.get('key');
|
||||||
|
const sourceId = searchParams.get('sourceId');
|
||||||
|
const lineId = searchParams.get('lineId') || 'default';
|
||||||
|
const episodeIndexRaw = searchParams.get('episodeIndex');
|
||||||
|
const playUrlEncoded = searchParams.get('playUrl');
|
||||||
|
const format = searchParams.get('format');
|
||||||
|
|
||||||
|
if (!key || !sourceId || !episodeIndexRaw || !playUrlEncoded) {
|
||||||
|
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const episodeIndex = Number.parseInt(episodeIndexRaw, 10);
|
||||||
|
if (!Number.isInteger(episodeIndex) || episodeIndex < 0) {
|
||||||
|
return NextResponse.json({ error: '无效的 episodeIndex' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const playUrl = parseScriptPlayUrlValue(playUrlEncoded);
|
||||||
|
if (!playUrl) {
|
||||||
|
return NextResponse.json({ error: '无效的播放地址' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await resolveSavedScriptPlayUrl({
|
||||||
|
key,
|
||||||
|
sourceId,
|
||||||
|
lineId,
|
||||||
|
episodeIndex,
|
||||||
|
playUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.url || result.url.trim() === '') {
|
||||||
|
throw new Error('获取到的播放链接为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
return NextResponse.json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.redirect(result.url);
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: (error as Error).message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
-17
@@ -2127,10 +2127,17 @@ function PlayPageClient() {
|
|||||||
let newUrl = detailData?.episodes[episodeIndex] || '';
|
let newUrl = detailData?.episodes[episodeIndex] || '';
|
||||||
|
|
||||||
// 如果是小雅或 openlist 接口,先请求获取真实 URL
|
// 如果是小雅或 openlist 接口,先请求获取真实 URL
|
||||||
if (newUrl.startsWith('/api/xiaoya/play') || newUrl.startsWith('/api/openlist/play')) {
|
const isSpecialLazyPlayUrl =
|
||||||
|
newUrl.startsWith('/api/xiaoya/play') ||
|
||||||
|
newUrl.startsWith('/api/openlist/play') ||
|
||||||
|
newUrl.startsWith('/api/source-script/play');
|
||||||
|
|
||||||
|
if (isSpecialLazyPlayUrl) {
|
||||||
try {
|
try {
|
||||||
// 保存原始URL(用于后续刷新)
|
// 保存原始URL(用于后续刷新)
|
||||||
currentXiaoyaUrlRef.current = newUrl;
|
if (newUrl.startsWith('/api/xiaoya/play') || newUrl.startsWith('/api/openlist/play')) {
|
||||||
|
currentXiaoyaUrlRef.current = newUrl;
|
||||||
|
}
|
||||||
|
|
||||||
// 添加 format=json 参数
|
// 添加 format=json 参数
|
||||||
const separator = newUrl.includes('?') ? '&' : '?';
|
const separator = newUrl.includes('?') ? '&' : '?';
|
||||||
@@ -3247,6 +3254,42 @@ function PlayPageClient() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCachedSourcesData = (query: string): SearchResult[] => {
|
||||||
|
if (typeof window === 'undefined' || !query.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheKey = `search_cache_${query.trim()}`;
|
||||||
|
const cached = sessionStorage.getItem(cacheKey);
|
||||||
|
if (!cached) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedData = JSON.parse(cached);
|
||||||
|
const results = cachedData.filter(
|
||||||
|
(result: SearchResult) =>
|
||||||
|
normalizeTitle(result.title).toLowerCase() ===
|
||||||
|
normalizeTitle(videoTitleRef.current).toLowerCase() &&
|
||||||
|
(videoYearRef.current
|
||||||
|
? result.year.toLowerCase() === videoYearRef.current.toLowerCase() ||
|
||||||
|
!result.year ||
|
||||||
|
result.year.trim() === '' ||
|
||||||
|
result.year === 'unknown' ||
|
||||||
|
!/^\d{4}$/.test(result.year)
|
||||||
|
: true) &&
|
||||||
|
(searchType
|
||||||
|
? getType(result) === searchType
|
||||||
|
: true)
|
||||||
|
);
|
||||||
|
|
||||||
|
return applyCorrectionsToSources(results);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Play] 读取缓存失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const initAll = async () => {
|
const initAll = async () => {
|
||||||
if (currentSource === 'directplay') {
|
if (currentSource === 'directplay') {
|
||||||
if (!currentId) {
|
if (!currentId) {
|
||||||
@@ -3333,22 +3376,34 @@ function PlayPageClient() {
|
|||||||
let sourcesInfo: SearchResult[] = [];
|
let sourcesInfo: SearchResult[] = [];
|
||||||
|
|
||||||
if (currentSource && currentId) {
|
if (currentSource && currentId) {
|
||||||
// 先快速获取当前源的详情
|
const cachedSources = getCachedSourcesData(searchTitle || videoTitle);
|
||||||
try {
|
const cachedTarget = cachedSources.find(
|
||||||
// currentSource 已经是完整格式(如 'emby_wumei')
|
(source) => source.source === currentSource && source.id === currentId
|
||||||
// 如果是小雅源且有fileName参数,传递给API
|
);
|
||||||
const currentSourceDetail = await fetchSourceDetail(
|
|
||||||
currentSource,
|
if (cachedTarget?.episodes?.length) {
|
||||||
currentId,
|
detailData = cachedTarget;
|
||||||
searchTitle || videoTitle,
|
sourcesInfo = cachedSources;
|
||||||
currentSource === 'xiaoya' ? fileName : undefined
|
setAvailableSources(cachedSources);
|
||||||
);
|
setSourceSearchLoading(false);
|
||||||
if (currentSourceDetail.length > 0) {
|
} else {
|
||||||
detailData = currentSourceDetail[0];
|
// 先快速获取当前源的详情
|
||||||
sourcesInfo = currentSourceDetail;
|
try {
|
||||||
|
// currentSource 已经是完整格式(如 'emby_wumei')
|
||||||
|
// 如果是小雅源且有fileName参数,传递给API
|
||||||
|
const currentSourceDetail = await fetchSourceDetail(
|
||||||
|
currentSource,
|
||||||
|
currentId,
|
||||||
|
searchTitle || videoTitle,
|
||||||
|
currentSource === 'xiaoya' ? fileName : undefined
|
||||||
|
);
|
||||||
|
if (currentSourceDetail.length > 0) {
|
||||||
|
detailData = currentSourceDetail[0];
|
||||||
|
sourcesInfo = currentSourceDetail;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取当前源详情失败:', err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('获取当前源详情失败:', err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 异步获取其他源信息,不阻塞播放
|
// 异步获取其他源信息,不阻塞播放
|
||||||
|
|||||||
+117
-17
@@ -738,22 +738,39 @@ export function normalizeScriptSearchResults(input: {
|
|||||||
result: any;
|
result: any;
|
||||||
}) {
|
}) {
|
||||||
const list = Array.isArray(input.result?.list) ? input.result.list : [];
|
const list = Array.isArray(input.result?.list) ? input.result.list : [];
|
||||||
return list.map((item: any) => ({
|
return list.map((item: any) => {
|
||||||
id: String(item.id),
|
const titles = Array.isArray(item.episodes_titles) ? item.episodes_titles : [];
|
||||||
title: String(item.title || ''),
|
const episodes = Array.isArray(item.episodes)
|
||||||
poster: item.poster || '',
|
? item.episodes.map((episode: any, index: number) => {
|
||||||
episodes: Array.isArray(item.episodes) ? item.episodes : [],
|
const playUrl =
|
||||||
episodes_titles: Array.isArray(item.episodes_titles)
|
typeof episode === 'string'
|
||||||
? item.episodes_titles
|
? episode
|
||||||
: [],
|
: String(episode?.playUrl || episode?.url || '');
|
||||||
source: buildScriptSourceValue(input.scriptKey, input.sourceId),
|
return buildScriptPlayUrl({
|
||||||
source_name: `${input.scriptName} / ${input.sourceName}`,
|
scriptKey: input.scriptKey,
|
||||||
year: item.year || '',
|
sourceId: input.sourceId,
|
||||||
desc: item.desc || '',
|
lineId: String(episode?.lineId || item?.lineId || 'default'),
|
||||||
type_name: item.type_name || '',
|
episodeIndex: index,
|
||||||
douban_id: item.douban_id || 0,
|
playUrl,
|
||||||
vod_remarks: item.vod_remarks,
|
});
|
||||||
}));
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(item.id),
|
||||||
|
title: String(item.title || ''),
|
||||||
|
poster: item.poster || '',
|
||||||
|
episodes,
|
||||||
|
episodes_titles: titles,
|
||||||
|
source: buildScriptSourceValue(input.scriptKey, input.sourceId),
|
||||||
|
source_name: `${input.scriptName} / ${input.sourceName}`,
|
||||||
|
year: item.year || '',
|
||||||
|
desc: item.desc || '',
|
||||||
|
type_name: item.type_name || '',
|
||||||
|
douban_id: item.douban_id || 0,
|
||||||
|
vod_remarks: item.vod_remarks,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeScriptDetailResult(input: {
|
export function normalizeScriptDetailResult(input: {
|
||||||
@@ -790,7 +807,7 @@ export function normalizeScriptDetailResult(input: {
|
|||||||
const episodes = Array.isArray(playback.episodes) ? playback.episodes : [];
|
const episodes = Array.isArray(playback.episodes) ? playback.episodes : [];
|
||||||
|
|
||||||
episodes.forEach((episode: any, index: number) => {
|
episodes.forEach((episode: any, index: number) => {
|
||||||
const playUrl =
|
const rawPlayUrl =
|
||||||
typeof episode === 'string'
|
typeof episode === 'string'
|
||||||
? episode
|
? episode
|
||||||
: String(episode?.playUrl || episode?.url || '');
|
: String(episode?.playUrl || episode?.url || '');
|
||||||
@@ -799,6 +816,16 @@ export function normalizeScriptDetailResult(input: {
|
|||||||
? String(episode.title)
|
? String(episode.title)
|
||||||
: String(titles[index] || `第${index + 1}集`);
|
: String(titles[index] || `第${index + 1}集`);
|
||||||
|
|
||||||
|
const playbackSourceId = String(playback.sourceId || input.sourceId);
|
||||||
|
const lineId = String(playback.lineId || 'default');
|
||||||
|
const playUrl = buildScriptPlayUrl({
|
||||||
|
scriptKey: input.scriptKey,
|
||||||
|
sourceId: playbackSourceId,
|
||||||
|
lineId,
|
||||||
|
episodeIndex: index,
|
||||||
|
playUrl: rawPlayUrl,
|
||||||
|
});
|
||||||
|
|
||||||
flattenedEpisodes.push(playUrl);
|
flattenedEpisodes.push(playUrl);
|
||||||
flattenedTitles.push(`${playbackSourceName} / ${lineName} / ${episodeTitle}`);
|
flattenedTitles.push(`${playbackSourceName} / ${lineName} / ${episodeTitle}`);
|
||||||
});
|
});
|
||||||
@@ -896,3 +923,76 @@ export async function resolveScriptDetailPlaybacks(input: {
|
|||||||
playbacks: resolvedPlaybacks,
|
playbacks: resolvedPlaybacks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeBase64Url(value: string) {
|
||||||
|
return Buffer.from(value, 'utf8')
|
||||||
|
.toString('base64')
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeBase64Url(value: string) {
|
||||||
|
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const padding = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
|
||||||
|
return Buffer.from(`${normalized}${padding}`, 'base64').toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildScriptPlayUrl(input: {
|
||||||
|
scriptKey: string;
|
||||||
|
sourceId: string;
|
||||||
|
lineId: string;
|
||||||
|
episodeIndex: number;
|
||||||
|
playUrl: string;
|
||||||
|
}) {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
key: input.scriptKey,
|
||||||
|
sourceId: input.sourceId,
|
||||||
|
lineId: input.lineId,
|
||||||
|
episodeIndex: String(input.episodeIndex),
|
||||||
|
playUrl: encodeBase64Url(input.playUrl),
|
||||||
|
});
|
||||||
|
return `/api/source-script/play?${searchParams.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseScriptPlayUrlValue(value: string) {
|
||||||
|
return decodeBase64Url(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveSavedScriptPlayUrl(input: {
|
||||||
|
key: string;
|
||||||
|
sourceId: string;
|
||||||
|
lineId: string;
|
||||||
|
episodeIndex: number;
|
||||||
|
playUrl: string;
|
||||||
|
configValues?: Record<string, string>;
|
||||||
|
}) {
|
||||||
|
const script = await getEnabledSourceScriptByKey(input.key);
|
||||||
|
const { compiled, ctx } = await compileSourceScript(script, input.configValues);
|
||||||
|
|
||||||
|
if (typeof compiled.resolvePlayUrl !== 'function') {
|
||||||
|
return {
|
||||||
|
url: input.playUrl,
|
||||||
|
type: 'auto',
|
||||||
|
headers: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await withTimeout(
|
||||||
|
Promise.resolve(
|
||||||
|
compiled.resolvePlayUrl(ctx, {
|
||||||
|
playUrl: input.playUrl,
|
||||||
|
sourceId: input.sourceId,
|
||||||
|
lineId: input.lineId,
|
||||||
|
episodeIndex: input.episodeIndex,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
DEFAULT_TIMEOUT_MS
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: result?.url || input.playUrl,
|
||||||
|
type: result?.type || 'auto',
|
||||||
|
headers: result?.headers || {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user