emby支持多源

This commit is contained in:
mtvpls
2026-01-08 00:31:03 +08:00
parent b61db02478
commit 2736c9e845
22 changed files with 1316 additions and 572 deletions
+15 -26
View File
@@ -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}`;
});
+4 -20
View File
@@ -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);
+6 -46
View File
@@ -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,
+27
View File
@@ -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 }
);
}
}
+12 -40
View File
@@ -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) {