emby支持多源
This commit is contained in:
@@ -78,3 +78,54 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||
if (storageType === 'localstorage') {
|
||||
return NextResponse.json(
|
||||
{ error: '不支持本地存储进行管理员配置' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
const username = authInfo.username;
|
||||
|
||||
try {
|
||||
const newConfig = await request.json();
|
||||
|
||||
// 权限检查
|
||||
if (username !== process.env.USERNAME) {
|
||||
const { db } = await import('@/lib/db');
|
||||
const userInfoV2 = await db.getUserInfoV2(username);
|
||||
|
||||
if (!userInfoV2 || (userInfoV2.role !== 'admin' && userInfoV2.role !== 'owner') || userInfoV2.banned) {
|
||||
return NextResponse.json({ error: '权限不足' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const { db } = await import('@/lib/db');
|
||||
const { configSelfCheck, setCachedConfig } = await import('@/lib/config');
|
||||
|
||||
// 自检配置
|
||||
const checkedConfig = configSelfCheck(newConfig);
|
||||
|
||||
// 保存到数据库
|
||||
await db.saveAdminConfig(checkedConfig);
|
||||
|
||||
// 更新缓存
|
||||
await setCachedConfig(checkedConfig);
|
||||
|
||||
return NextResponse.json({ success: true, message: '配置已保存' });
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '保存配置失败: ' + (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,9 @@ export async function GET(
|
||||
|
||||
try {
|
||||
const config = await getConfig();
|
||||
const embyConfig = config.EmbyConfig;
|
||||
|
||||
// 验证 Emby 配置
|
||||
if (!embyConfig?.Enabled || !embyConfig.ServerURL) {
|
||||
// 验证 Emby 配置(多源)
|
||||
if (!config.EmbyConfig?.Sources || config.EmbyConfig.Sources.length === 0) {
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
msg: 'Emby 未配置或未启用',
|
||||
@@ -62,31 +61,18 @@ export async function GET(
|
||||
});
|
||||
}
|
||||
|
||||
const client = new EmbyClient(embyConfig);
|
||||
// 获取 embyKey 参数
|
||||
const embyKey = searchParams.get('embyKey') || undefined;
|
||||
|
||||
// 如果没有 UserId,需要先认证
|
||||
if (!embyConfig.UserId && embyConfig.Username && embyConfig.Password) {
|
||||
const authResult = await client.authenticate(embyConfig.Username, embyConfig.Password);
|
||||
embyConfig.UserId = authResult.User.Id;
|
||||
}
|
||||
|
||||
if (!embyConfig.UserId) {
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
msg: 'Emby 认证失败',
|
||||
page: 1,
|
||||
pagecount: 0,
|
||||
limit: 0,
|
||||
total: 0,
|
||||
list: [],
|
||||
});
|
||||
}
|
||||
// 使用 EmbyManager 获取客户端
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
const client = await embyManager.getClient(embyKey);
|
||||
|
||||
// 路由处理
|
||||
if (wd) {
|
||||
// 搜索模式
|
||||
if (ac === 'detail') {
|
||||
return await handleDetailBySearch(client, wd, requestToken, request);
|
||||
return await handleDetailBySearch(client, wd, requestToken, embyKey, request);
|
||||
}
|
||||
return await handleSearch(client, wd);
|
||||
} else if (ids || ac === 'detail') {
|
||||
@@ -102,7 +88,7 @@ export async function GET(
|
||||
list: [],
|
||||
});
|
||||
}
|
||||
return await handleDetail(client, ids, requestToken, request);
|
||||
return await handleDetail(client, ids, requestToken, embyKey, request);
|
||||
} else {
|
||||
// 列表模式
|
||||
return await handleSearch(client, '');
|
||||
@@ -161,6 +147,7 @@ async function handleDetailBySearch(
|
||||
client: EmbyClient,
|
||||
query: string,
|
||||
token: string,
|
||||
embyKey: string | undefined,
|
||||
request: NextRequest
|
||||
) {
|
||||
const result = await client.getItems({
|
||||
@@ -183,7 +170,7 @@ async function handleDetailBySearch(
|
||||
});
|
||||
}
|
||||
|
||||
return await handleDetail(client, result.Items[0].Id, token, request);
|
||||
return await handleDetail(client, result.Items[0].Id, token, embyKey, request);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,6 +180,7 @@ async function handleDetail(
|
||||
client: EmbyClient,
|
||||
itemId: string,
|
||||
token: string,
|
||||
embyKey: string | undefined,
|
||||
request: NextRequest
|
||||
) {
|
||||
const item = await client.getItem(itemId);
|
||||
@@ -203,11 +191,12 @@ async function handleDetail(
|
||||
(host?.includes('localhost') || host?.includes('127.0.0.1') ? 'http' : 'https');
|
||||
const baseUrl = process.env.SITE_BASE || `${proto}://${host}`;
|
||||
|
||||
const embyKeyParam = embyKey ? `&embyKey=${embyKey}` : '';
|
||||
let vodPlayUrl = '';
|
||||
|
||||
if (item.Type === 'Movie') {
|
||||
// 电影:单个播放链接(使用代理,添加 .mp4 扩展名)
|
||||
const proxyUrl = `${baseUrl}/api/emby/play/${encodeURIComponent(token)}/video.mp4?itemId=${item.Id}`;
|
||||
const proxyUrl = `${baseUrl}/api/emby/play/${encodeURIComponent(token)}/video.mp4?itemId=${item.Id}${embyKeyParam}`;
|
||||
vodPlayUrl = `正片$${proxyUrl}`;
|
||||
} else if (item.Type === 'Series') {
|
||||
// 剧集:获取所有集
|
||||
@@ -222,7 +211,7 @@ async function handleDetail(
|
||||
})
|
||||
.map((ep) => {
|
||||
const title = `第${ep.IndexNumber}集`;
|
||||
const proxyUrl = `${baseUrl}/api/emby/play/${encodeURIComponent(token)}/video.mp4?itemId=${ep.Id}`;
|
||||
const proxyUrl = `${baseUrl}/api/emby/play/${encodeURIComponent(token)}/video.mp4?itemId=${ep.Id}${embyKeyParam}`;
|
||||
return `${title}$${proxyUrl}`;
|
||||
});
|
||||
|
||||
|
||||
@@ -2,38 +2,22 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { EmbyClient } from '@/lib/emby.client';
|
||||
import { embyManager } from '@/lib/emby-manager';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const itemId = searchParams.get('id');
|
||||
const embyKey = searchParams.get('embyKey') || undefined;
|
||||
|
||||
if (!itemId) {
|
||||
return NextResponse.json({ error: '缺少媒体ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await getConfig();
|
||||
const embyConfig = config.EmbyConfig;
|
||||
|
||||
if (!embyConfig?.Enabled || !embyConfig.ServerURL) {
|
||||
return NextResponse.json({ error: 'Emby 未配置或未启用' }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = new EmbyClient(embyConfig);
|
||||
|
||||
// 如果没有 UserId,需要先认证
|
||||
if (!embyConfig.UserId && embyConfig.Username && embyConfig.Password) {
|
||||
const authResult = await client.authenticate(embyConfig.Username, embyConfig.Password);
|
||||
embyConfig.UserId = authResult.User.Id;
|
||||
}
|
||||
|
||||
if (!embyConfig.UserId) {
|
||||
return NextResponse.json({ error: 'Emby 认证失败' }, { status: 401 });
|
||||
}
|
||||
// 获取Emby客户端
|
||||
const client = await embyManager.getClient(embyKey);
|
||||
|
||||
// 获取媒体详情
|
||||
const item = await client.getItem(itemId);
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { EmbyClient } from '@/lib/emby.client';
|
||||
import { embyManager } from '@/lib/emby-manager';
|
||||
import { getCachedEmbyList, setCachedEmbyList } from '@/lib/emby-cache';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
@@ -13,56 +12,17 @@ export async function GET(request: NextRequest) {
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || '20');
|
||||
const parentId = searchParams.get('parentId') || undefined;
|
||||
const embyKey = searchParams.get('embyKey') || undefined;
|
||||
|
||||
try {
|
||||
// 检查缓存
|
||||
const cached = getCachedEmbyList(page, pageSize, parentId);
|
||||
const cached = getCachedEmbyList(page, pageSize, parentId, embyKey);
|
||||
if (cached) {
|
||||
return NextResponse.json(cached);
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
const embyConfig = config.EmbyConfig;
|
||||
|
||||
if (!embyConfig?.Enabled || !embyConfig.ServerURL) {
|
||||
return NextResponse.json({
|
||||
error: 'Emby 未配置或未启用',
|
||||
list: [],
|
||||
totalPages: 0,
|
||||
currentPage: page,
|
||||
total: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建 Emby 客户端
|
||||
const client = new EmbyClient(embyConfig);
|
||||
|
||||
// 如果使用用户名密码且没有 UserId,需要先认证
|
||||
if (!embyConfig.ApiKey && !embyConfig.UserId && embyConfig.Username && embyConfig.Password) {
|
||||
try {
|
||||
const authResult = await client.authenticate(embyConfig.Username, embyConfig.Password);
|
||||
embyConfig.UserId = authResult.User.Id;
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
error: 'Emby 认证失败: ' + (error as Error).message,
|
||||
list: [],
|
||||
totalPages: 0,
|
||||
currentPage: page,
|
||||
total: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 验证认证信息:必须有 ApiKey 或 UserId
|
||||
if (!embyConfig.ApiKey && !embyConfig.UserId) {
|
||||
return NextResponse.json({
|
||||
error: 'Emby 认证失败,请检查配置',
|
||||
list: [],
|
||||
totalPages: 0,
|
||||
currentPage: page,
|
||||
total: 0,
|
||||
});
|
||||
}
|
||||
// 获取Emby客户端
|
||||
const client = await embyManager.getClient(embyKey);
|
||||
|
||||
// 获取媒体列表
|
||||
const result = await client.getItems({
|
||||
@@ -96,7 +56,7 @@ export async function GET(request: NextRequest) {
|
||||
};
|
||||
|
||||
// 缓存结果
|
||||
setCachedEmbyList(page, pageSize, response, parentId);
|
||||
setCachedEmbyList(page, pageSize, response, parentId, embyKey);
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,51 +7,18 @@ import { getConfig } from '@/lib/config';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
// 内存缓存 Emby 配置,避免每次请求都读取配置
|
||||
let cachedEmbyConfig: {
|
||||
serverURL: string;
|
||||
apiKey: string;
|
||||
timestamp: number;
|
||||
} | null = null;
|
||||
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5分钟缓存
|
||||
|
||||
/**
|
||||
* 获取缓存的 Emby 配置
|
||||
* 获取 Emby 客户端
|
||||
*/
|
||||
async function getCachedEmbyConfig() {
|
||||
const now = Date.now();
|
||||
|
||||
// 如果缓存存在且未过期,直接返回
|
||||
if (cachedEmbyConfig && (now - cachedEmbyConfig.timestamp) < CACHE_TTL) {
|
||||
return cachedEmbyConfig;
|
||||
}
|
||||
|
||||
// 否则重新获取配置
|
||||
async function getEmbyClient(embyKey?: string) {
|
||||
const config = await getConfig();
|
||||
const embyConfig = config.EmbyConfig;
|
||||
|
||||
if (
|
||||
!embyConfig ||
|
||||
!embyConfig.Enabled ||
|
||||
!embyConfig.ServerURL
|
||||
) {
|
||||
if (!config.EmbyConfig?.Sources || config.EmbyConfig.Sources.length === 0) {
|
||||
throw new Error('Emby 未配置或未启用');
|
||||
}
|
||||
|
||||
const apiKey = embyConfig.ApiKey || embyConfig.AuthToken;
|
||||
if (!apiKey) {
|
||||
throw new Error('Emby 认证信息缺失');
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
cachedEmbyConfig = {
|
||||
serverURL: embyConfig.ServerURL,
|
||||
apiKey,
|
||||
timestamp: now,
|
||||
};
|
||||
|
||||
return cachedEmbyConfig;
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
return await embyManager.getClient(embyKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,16 +51,17 @@ export async function GET(
|
||||
}
|
||||
|
||||
const itemId = searchParams.get('itemId');
|
||||
const embyKey = searchParams.get('embyKey') || undefined;
|
||||
|
||||
if (!itemId) {
|
||||
return NextResponse.json({ error: '缺少 itemId 参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 使用缓存的配置
|
||||
const embyConfig = await getCachedEmbyConfig();
|
||||
// 获取 Emby 客户端
|
||||
let client = await getEmbyClient(embyKey);
|
||||
|
||||
// 构建 Emby 原始播放链接
|
||||
const embyStreamUrl = `${embyConfig.serverURL}/Videos/${itemId}/stream?Static=true&api_key=${embyConfig.apiKey}`;
|
||||
let embyStreamUrl = client.getStreamUrl(itemId);
|
||||
|
||||
// 构建请求头,转发 Range 请求
|
||||
const requestHeaders: HeadersInit = {};
|
||||
@@ -103,10 +71,22 @@ export async function GET(
|
||||
}
|
||||
|
||||
// 流式代理视频内容
|
||||
const videoResponse = await fetch(embyStreamUrl, {
|
||||
let videoResponse = await fetch(embyStreamUrl, {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
|
||||
// 如果返回 401,尝试重新认证并重试
|
||||
if (videoResponse.status === 401) {
|
||||
console.log('[Emby Play] 收到 401 错误,尝试重新认证');
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
embyManager.clearCache();
|
||||
client = await getEmbyClient(embyKey);
|
||||
embyStreamUrl = client.getStreamUrl(itemId);
|
||||
videoResponse = await fetch(embyStreamUrl, {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
if (!videoResponse.ok) {
|
||||
console.error('[Emby Play] 获取视频流失败:', {
|
||||
itemId,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { embyManager } from '@/lib/emby-manager';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* 获取所有启用的Emby源列表
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
const sources = await embyManager.getEnabledSources();
|
||||
|
||||
return NextResponse.json({
|
||||
sources: sources.map(s => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Emby Sources] 获取Emby源列表失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '获取Emby源列表失败', sources: [] },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,26 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { EmbyClient } from '@/lib/emby.client';
|
||||
import { embyManager } from '@/lib/emby-manager';
|
||||
import { getCachedEmbyViews, setCachedEmbyViews } from '@/lib/emby-cache';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// 检查缓存
|
||||
const cached = getCachedEmbyViews();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const embyKey = searchParams.get('embyKey') || undefined;
|
||||
|
||||
// 检查缓存(按embyKey缓存)
|
||||
const cacheKey = embyKey || 'default';
|
||||
const cached = getCachedEmbyViews(cacheKey);
|
||||
if (cached) {
|
||||
return NextResponse.json(cached);
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
const embyConfig = config.EmbyConfig;
|
||||
|
||||
if (!embyConfig?.Enabled || !embyConfig.ServerURL) {
|
||||
return NextResponse.json({
|
||||
error: 'Emby 未配置或未启用',
|
||||
views: [],
|
||||
});
|
||||
}
|
||||
|
||||
// 创建 Emby 客户端
|
||||
const client = new EmbyClient(embyConfig);
|
||||
|
||||
// 如果使用用户名密码且没有 UserId,需要先认证
|
||||
if (!embyConfig.ApiKey && !embyConfig.UserId && embyConfig.Username && embyConfig.Password) {
|
||||
try {
|
||||
const authResult = await client.authenticate(embyConfig.Username, embyConfig.Password);
|
||||
embyConfig.UserId = authResult.User.Id;
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
error: 'Emby 认证失败: ' + (error as Error).message,
|
||||
views: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 验证认证信息:必须有 ApiKey 或 UserId
|
||||
if (!embyConfig.ApiKey && !embyConfig.UserId) {
|
||||
return NextResponse.json({
|
||||
error: 'Emby 认证失败,请检查配置',
|
||||
views: [],
|
||||
});
|
||||
}
|
||||
// 获取Emby客户端
|
||||
const client = await embyManager.getClient(embyKey);
|
||||
|
||||
// 获取媒体库列表
|
||||
const views = await client.getUserViews();
|
||||
@@ -68,7 +40,7 @@ export async function GET() {
|
||||
};
|
||||
|
||||
// 缓存结果
|
||||
setCachedEmbyViews(response);
|
||||
setCachedEmbyViews(cacheKey, response);
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
|
||||
+61
-49
@@ -44,53 +44,57 @@ export async function GET(request: NextRequest) {
|
||||
config.OpenListConfig?.Password
|
||||
);
|
||||
|
||||
// 检查是否配置了 Emby
|
||||
const hasEmby = !!(
|
||||
config.EmbyConfig?.Enabled &&
|
||||
config.EmbyConfig?.ServerURL &&
|
||||
config.EmbyConfig?.UserId
|
||||
);
|
||||
// 获取所有启用的 Emby 源
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
const embySourcesMap = await embyManager.getAllClients();
|
||||
const embySources = Array.from(embySourcesMap.values());
|
||||
|
||||
// 搜索 Emby(如果配置了)- 异步带超时
|
||||
const embyPromise = hasEmby
|
||||
? Promise.race([
|
||||
(async () => {
|
||||
try {
|
||||
const { EmbyClient } = await import('@/lib/emby.client');
|
||||
const client = new EmbyClient(config.EmbyConfig!);
|
||||
const searchResult = await client.getItems({
|
||||
searchTerm: query,
|
||||
IncludeItemTypes: 'Movie,Series',
|
||||
Recursive: true,
|
||||
Fields: 'Overview,ProductionYear',
|
||||
Limit: 50,
|
||||
});
|
||||
return searchResult.Items.map((item) => ({
|
||||
id: item.Id,
|
||||
source: 'emby',
|
||||
source_name: 'Emby',
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
episodes: [],
|
||||
episodes_titles: [],
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
desc: item.Overview || '',
|
||||
type_name: item.Type === 'Movie' ? '电影' : '电视剧',
|
||||
douban_id: 0,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[Search] 搜索 Emby 失败:', error);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
new Promise<any[]>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Emby timeout')), 20000)
|
||||
),
|
||||
]).catch((error) => {
|
||||
console.error('[Search] 搜索 Emby 超时:', error);
|
||||
return [];
|
||||
})
|
||||
: Promise.resolve([]);
|
||||
console.log('[Search] Emby sources count:', embySources.length);
|
||||
console.log('[Search] Emby sources:', embySources.map(s => ({ key: s.config.key, name: s.config.name })));
|
||||
|
||||
// 为每个 Emby 源创建搜索 Promise(全部并发,无限制)
|
||||
const embyPromises = embySources.map(({ client, config: embyConfig }) =>
|
||||
Promise.race([
|
||||
(async () => {
|
||||
try {
|
||||
const searchResult = await client.getItems({
|
||||
searchTerm: query,
|
||||
IncludeItemTypes: 'Movie,Series',
|
||||
Recursive: true,
|
||||
Fields: 'Overview,ProductionYear',
|
||||
Limit: 50,
|
||||
});
|
||||
|
||||
// 如果只有一个Emby源,保持旧格式(向后兼容)
|
||||
const sourceValue = embySources.length === 1 ? 'emby' : `emby_${embyConfig.key}`;
|
||||
const sourceName = embySources.length === 1 ? 'Emby' : embyConfig.name;
|
||||
|
||||
return searchResult.Items.map((item) => ({
|
||||
id: item.Id,
|
||||
source: sourceValue,
|
||||
source_name: sourceName,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
episodes: [],
|
||||
episodes_titles: [],
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
desc: item.Overview || '',
|
||||
type_name: item.Type === 'Movie' ? '电影' : '电视剧',
|
||||
douban_id: 0,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`[Search] 搜索 ${embyConfig.name} 失败:`, error);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
new Promise<any[]>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`${embyConfig.name} timeout`)), 20000)
|
||||
),
|
||||
]).catch((error) => {
|
||||
console.error(`[Search] 搜索 ${embyConfig.name} 超时:`, error);
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
// 搜索 OpenList(如果配置了)- 异步带超时
|
||||
const openlistPromise = hasOpenList
|
||||
@@ -164,12 +168,20 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
|
||||
try {
|
||||
const [embyResults, openlistResults, ...apiResults] = await Promise.all([
|
||||
embyPromise,
|
||||
const allResults = await Promise.all([
|
||||
openlistPromise,
|
||||
...embyPromises,
|
||||
...searchPromises,
|
||||
]);
|
||||
let flattenedResults = [...embyResults, ...openlistResults, ...apiResults.flat()];
|
||||
|
||||
// 分离结果:第一个是 openlist,接下来是 emby 结果,最后是 api 结果
|
||||
const openlistResults = allResults[0];
|
||||
const embyResultsArray = allResults.slice(1, 1 + embyPromises.length);
|
||||
const apiResults = allResults.slice(1 + embyPromises.length);
|
||||
|
||||
// 合并所有 Emby 结果
|
||||
const embyResults = embyResultsArray.flat();
|
||||
let flattenedResults = [...openlistResults, ...embyResults, ...apiResults.flat()];
|
||||
if (!config.SiteConfig.DisableYellowFilter) {
|
||||
flattenedResults = flattenedResults.filter((result) => {
|
||||
const typeName = result.type_name || '';
|
||||
|
||||
+113
-71
@@ -41,11 +41,11 @@ export async function GET(request: NextRequest) {
|
||||
config.OpenListConfig?.Password
|
||||
);
|
||||
|
||||
// 检查是否配置了 Emby
|
||||
// 检查是否配置了 Emby(支持多源)
|
||||
const hasEmby = !!(
|
||||
config.EmbyConfig?.Enabled &&
|
||||
config.EmbyConfig?.ServerURL &&
|
||||
config.EmbyConfig?.UserId
|
||||
config.EmbyConfig?.Sources &&
|
||||
config.EmbyConfig.Sources.length > 0 &&
|
||||
config.EmbyConfig.Sources.some(s => s.enabled && s.ServerURL)
|
||||
);
|
||||
|
||||
// 共享状态
|
||||
@@ -73,11 +73,23 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取 Emby 源数量
|
||||
let embySourcesCount = 0;
|
||||
if (hasEmby) {
|
||||
try {
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
const embySourcesMap = await embyManager.getAllClients();
|
||||
embySourcesCount = embySourcesMap.size;
|
||||
} catch (error) {
|
||||
console.error('[Search WS] 获取 Emby 源数量失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送开始事件
|
||||
const startEvent = `data: ${JSON.stringify({
|
||||
type: 'start',
|
||||
query,
|
||||
totalSources: apiSites.length + (hasOpenList ? 1 : 0) + (hasEmby ? 1 : 0),
|
||||
totalSources: apiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
@@ -89,75 +101,105 @@ export async function GET(request: NextRequest) {
|
||||
let completedSources = 0;
|
||||
const allResults: any[] = [];
|
||||
|
||||
// 搜索 Emby(如果配置了)- 异步带超时
|
||||
// 搜索 Emby(如果配置了)- 异步带超时,支持多源
|
||||
if (hasEmby) {
|
||||
Promise.race([
|
||||
(async () => {
|
||||
try {
|
||||
const { EmbyClient } = await import('@/lib/emby.client');
|
||||
const client = new EmbyClient(config.EmbyConfig!);
|
||||
const searchResult = await client.getItems({
|
||||
searchTerm: query,
|
||||
IncludeItemTypes: 'Movie,Series',
|
||||
Recursive: true,
|
||||
Fields: 'Overview,ProductionYear',
|
||||
Limit: 50,
|
||||
});
|
||||
return searchResult.Items.map((item) => ({
|
||||
id: item.Id,
|
||||
source: 'emby',
|
||||
source_name: 'Emby',
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
episodes: [],
|
||||
episodes_titles: [],
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
desc: item.Overview || '',
|
||||
type_name: item.Type === 'Movie' ? '电影' : '电视剧',
|
||||
douban_id: 0,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[Search WS] 搜索 Emby 失败:', error);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Emby timeout')), 20000)
|
||||
),
|
||||
])
|
||||
.then((embyResults: any) => {
|
||||
completedSources++;
|
||||
if (!streamClosed) {
|
||||
const sourceEvent = `data: ${JSON.stringify({
|
||||
type: 'source_result',
|
||||
source: 'emby',
|
||||
sourceName: 'Emby',
|
||||
results: embyResults,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
if (!safeEnqueue(encoder.encode(sourceEvent))) {
|
||||
streamClosed = true;
|
||||
return;
|
||||
(async () => {
|
||||
let embyCompletedCount = 0;
|
||||
try {
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
const embySourcesMap = await embyManager.getAllClients();
|
||||
const embySources = Array.from(embySourcesMap.values());
|
||||
|
||||
// 为每个 Emby 源并发搜索,并单独发送结果
|
||||
const embySearchPromises = embySources.map(async ({ client, config: embyConfig }) => {
|
||||
try {
|
||||
const searchResult = await client.getItems({
|
||||
searchTerm: query,
|
||||
IncludeItemTypes: 'Movie,Series',
|
||||
Recursive: true,
|
||||
Fields: 'Overview,ProductionYear',
|
||||
Limit: 50,
|
||||
});
|
||||
|
||||
const sourceValue = embySources.length === 1 ? 'emby' : `emby_${embyConfig.key}`;
|
||||
const sourceName = embySources.length === 1 ? 'Emby' : embyConfig.name;
|
||||
|
||||
const results = searchResult.Items.map((item) => ({
|
||||
id: item.Id,
|
||||
source: sourceValue,
|
||||
source_name: sourceName,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
episodes: [],
|
||||
episodes_titles: [],
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
desc: item.Overview || '',
|
||||
type_name: item.Type === 'Movie' ? '电影' : '电视剧',
|
||||
douban_id: 0,
|
||||
}));
|
||||
|
||||
// 单独发送每个源的结果
|
||||
embyCompletedCount++;
|
||||
completedSources++;
|
||||
if (!streamClosed) {
|
||||
const sourceEvent = `data: ${JSON.stringify({
|
||||
type: 'source_result',
|
||||
source: sourceValue,
|
||||
sourceName: sourceName,
|
||||
results: results,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
if (safeEnqueue(encoder.encode(sourceEvent))) {
|
||||
if (results.length > 0) {
|
||||
allResults.push(...results);
|
||||
}
|
||||
} else {
|
||||
streamClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(`[Search WS] 搜索 ${embyConfig.name} 失败:`, error);
|
||||
embyCompletedCount++;
|
||||
completedSources++;
|
||||
// 发送空结果
|
||||
if (!streamClosed) {
|
||||
const sourceValue = embySources.length === 1 ? 'emby' : `emby_${embyConfig.key}`;
|
||||
const sourceName = embySources.length === 1 ? 'Emby' : embyConfig.name;
|
||||
const sourceEvent = `data: ${JSON.stringify({
|
||||
type: 'source_result',
|
||||
source: sourceValue,
|
||||
sourceName: sourceName,
|
||||
results: [],
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
safeEnqueue(encoder.encode(sourceEvent));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (embyResults.length > 0) {
|
||||
allResults.push(...embyResults);
|
||||
});
|
||||
|
||||
await Promise.all(embySearchPromises);
|
||||
} catch (error) {
|
||||
console.error('[Search WS] 搜索 Emby 整体失败:', error);
|
||||
// 如果整个 emby 搜索失败,需要补齐未完成的源
|
||||
const remainingSources = embySourcesCount - embyCompletedCount;
|
||||
for (let i = 0; i < remainingSources; i++) {
|
||||
completedSources++;
|
||||
if (!streamClosed) {
|
||||
const sourceEvent = `data: ${JSON.stringify({
|
||||
type: 'source_result',
|
||||
source: 'emby',
|
||||
sourceName: 'Emby',
|
||||
results: [],
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
safeEnqueue(encoder.encode(sourceEvent));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Search WS] 搜索 Emby 超时:', error);
|
||||
completedSources++;
|
||||
if (!streamClosed) {
|
||||
const sourceEvent = `data: ${JSON.stringify({
|
||||
type: 'source_result',
|
||||
source: 'emby',
|
||||
sourceName: 'Emby',
|
||||
results: [],
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
safeEnqueue(encoder.encode(sourceEvent));
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// 搜索 OpenList(如果配置了)- 异步带超时
|
||||
@@ -315,7 +357,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 检查是否所有源都已完成
|
||||
if (completedSources === apiSites.length + (hasOpenList ? 1 : 0) + (hasEmby ? 1 : 0)) {
|
||||
if (completedSources === apiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount) {
|
||||
if (!streamClosed) {
|
||||
// 发送最终完成事件
|
||||
const completeEvent = `data: ${JSON.stringify({
|
||||
|
||||
@@ -27,18 +27,29 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 特殊处理 emby 源
|
||||
if (sourceCode === 'emby') {
|
||||
// 特殊处理 emby 源(支持多源)
|
||||
if (sourceCode === 'emby' || sourceCode.startsWith('emby_')) {
|
||||
try {
|
||||
const config = await getConfig();
|
||||
const embyConfig = config.EmbyConfig;
|
||||
|
||||
if (!embyConfig || !embyConfig.Enabled || !embyConfig.ServerURL) {
|
||||
// 检查是否有启用的 Emby 源
|
||||
if (!config.EmbyConfig?.Sources || config.EmbyConfig.Sources.length === 0) {
|
||||
throw new Error('Emby 未配置或未启用');
|
||||
}
|
||||
|
||||
const { EmbyClient } = await import('@/lib/emby.client');
|
||||
const client = new EmbyClient(embyConfig);
|
||||
// 解析 embyKey
|
||||
let embyKey: string | undefined;
|
||||
if (sourceCode.startsWith('emby_')) {
|
||||
embyKey = sourceCode.substring(5); // 'emby_'.length = 5
|
||||
}
|
||||
|
||||
// 使用 EmbyManager 获取客户端和配置
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
const sources = await embyManager.getEnabledSources();
|
||||
const sourceConfig = sources.find(s => s.key === embyKey);
|
||||
const sourceName = sourceConfig?.name || 'Emby';
|
||||
|
||||
const client = await embyManager.getClient(embyKey);
|
||||
|
||||
// 获取媒体详情
|
||||
const item = await client.getItem(id);
|
||||
@@ -49,8 +60,8 @@ export async function GET(request: NextRequest) {
|
||||
const subtitles = client.getSubtitles(item);
|
||||
|
||||
const result = {
|
||||
source: 'emby',
|
||||
source_name: 'Emby',
|
||||
source: sourceCode, // 保持与请求一致(emby 或 emby_key)
|
||||
source_name: sourceName,
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
@@ -83,8 +94,8 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
|
||||
const result = {
|
||||
source: 'emby',
|
||||
source_name: 'Emby',
|
||||
source: sourceCode, // 保持与请求一致(emby 或 emby_key)
|
||||
source_name: sourceName,
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
|
||||
@@ -71,12 +71,9 @@ export async function GET(request: NextRequest) {
|
||||
config.OpenListConfig?.Password
|
||||
);
|
||||
|
||||
// 检查是否配置了 Emby
|
||||
const hasEmby = !!(
|
||||
config.EmbyConfig?.Enabled &&
|
||||
config.EmbyConfig?.ServerURL &&
|
||||
(config.EmbyConfig?.ApiKey || (config.EmbyConfig?.Username && config.EmbyConfig?.Password))
|
||||
);
|
||||
// 获取所有启用的 Emby 源
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
const embySources = await embyManager.getEnabledSources();
|
||||
|
||||
// 构建 OpenList 站点配置
|
||||
const openlistSites = hasOpenList ? [{
|
||||
@@ -90,17 +87,17 @@ export async function GET(request: NextRequest) {
|
||||
ext: '',
|
||||
}] : [];
|
||||
|
||||
// 构建 Emby 站点配置
|
||||
const embySites = hasEmby ? [{
|
||||
key: 'emby',
|
||||
name: 'Emby媒体库',
|
||||
// 构建 Emby 站点配置(为每个启用的Emby源生成独立站点)
|
||||
const embySites = embySources.map(source => ({
|
||||
key: `emby_${source.key}`,
|
||||
name: source.name || 'Emby媒体库',
|
||||
type: 1,
|
||||
api: `${baseUrl}/api/emby/cms-proxy/${encodeURIComponent(subscribeToken)}`,
|
||||
api: `${baseUrl}/api/emby/cms-proxy/${encodeURIComponent(subscribeToken)}?embyKey=${source.key}`,
|
||||
searchable: 1,
|
||||
quickSearch: 1,
|
||||
filterable: 1,
|
||||
ext: '',
|
||||
}] : [];
|
||||
}));
|
||||
|
||||
// 构建TVBOX订阅数据
|
||||
const tvboxSubscription = {
|
||||
|
||||
Reference in New Issue
Block a user