emby增加自定义ua和图片代理
This commit is contained in:
@@ -140,7 +140,7 @@ async function handleSearch(client: EmbyClient, query: string) {
|
||||
const list = result.Items.map((item) => ({
|
||||
vod_id: item.Id,
|
||||
vod_name: item.Name,
|
||||
vod_pic: client.getImageUrl(item.Id, 'Primary'),
|
||||
vod_pic: client.getImageUrl(item.Id, 'Primary', undefined, requestToken),
|
||||
vod_remarks: item.Type === 'Movie' ? '电影' : '剧集',
|
||||
vod_year: item.ProductionYear?.toString() || '',
|
||||
vod_content: item.Overview || '',
|
||||
@@ -247,7 +247,7 @@ async function handleDetail(
|
||||
{
|
||||
vod_id: item.Id,
|
||||
vod_name: item.Name,
|
||||
vod_pic: client.getImageUrl(item.Id, 'Primary'),
|
||||
vod_pic: client.getImageUrl(item.Id, 'Primary', undefined, requestToken),
|
||||
vod_remarks: item.Type === 'Movie' ? '电影' : '剧集',
|
||||
vod_year: item.ProductionYear?.toString() || '',
|
||||
vod_content: item.Overview || '',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { embyManager } from '@/lib/emby-manager';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -19,6 +20,9 @@ export async function GET(request: NextRequest) {
|
||||
// 获取Emby客户端
|
||||
const client = await embyManager.getClient(embyKey);
|
||||
|
||||
// 获取代理 token(如果启用了代理)
|
||||
const proxyToken = client.isProxyEnabled() ? await getProxyToken(request) : null;
|
||||
|
||||
// 获取媒体详情
|
||||
const item = await client.getItem(itemId);
|
||||
|
||||
@@ -54,7 +58,7 @@ export async function GET(request: NextRequest) {
|
||||
title: item.Name,
|
||||
type: item.Type === 'Movie' ? 'movie' : 'tv',
|
||||
overview: item.Overview || '',
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined),
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
rating: item.CommunityRating || 0,
|
||||
playUrl: item.Type === 'Movie' ? await client.getStreamUrl(item.Id) : undefined,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getConfig } from '@/lib/config';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* 获取 Emby 客户端
|
||||
*/
|
||||
async function getEmbyClient(embyKey?: string) {
|
||||
const config = await getConfig();
|
||||
|
||||
if (!config.EmbyConfig?.Sources || config.EmbyConfig.Sources.length === 0) {
|
||||
throw new Error('Emby 未配置或未启用');
|
||||
}
|
||||
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
return await embyManager.getClient(embyKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/emby/image/{token}/{itemId}?imageType=Primary&maxWidth=300&embyKey=xxx
|
||||
* 代理 Emby 图片
|
||||
*
|
||||
* 权限验证:TVBox Token(路径参数) 或 用户登录(满足其一即可)
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { token: string; itemId: string } }
|
||||
) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// 双重验证:TVBox Token(全局或用户) 或 用户登录
|
||||
const requestToken = params.token;
|
||||
const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN;
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
|
||||
// 验证 TVBox Token(全局token或用户token)
|
||||
let hasValidToken = false;
|
||||
if (globalToken && requestToken === globalToken) {
|
||||
// 全局token
|
||||
hasValidToken = true;
|
||||
} else {
|
||||
// 检查是否是用户token
|
||||
const { db } = await import('@/lib/db');
|
||||
const username = await db.getUsernameByTvboxToken(requestToken);
|
||||
if (username) {
|
||||
// 检查用户是否被封禁
|
||||
const userInfo = await db.getUserInfoV2(username);
|
||||
if (userInfo && !userInfo.banned) {
|
||||
hasValidToken = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证用户登录
|
||||
const hasValidAuth = authInfo && authInfo.username;
|
||||
|
||||
// 两者至少满足其一
|
||||
if (!hasValidToken && !hasValidAuth) {
|
||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||
}
|
||||
|
||||
const itemId = params.itemId;
|
||||
const imageType = (searchParams.get('imageType') || 'Primary') as 'Primary' | 'Backdrop' | 'Logo';
|
||||
const maxWidth = searchParams.get('maxWidth') ? parseInt(searchParams.get('maxWidth')!) : undefined;
|
||||
const embyKey = searchParams.get('embyKey') || undefined;
|
||||
|
||||
// 获取 Emby 客户端
|
||||
const client = await getEmbyClient(embyKey);
|
||||
|
||||
// 获取图片 URL
|
||||
const imageUrl = client.getImageUrl(itemId, imageType, maxWidth);
|
||||
|
||||
// 构建请求头,添加自定义 User-Agent
|
||||
const requestHeaders: HeadersInit = {
|
||||
'User-Agent': client.getUserAgent(),
|
||||
};
|
||||
|
||||
// 请求图片
|
||||
const imageResponse = await fetch(imageUrl, {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
|
||||
if (!imageResponse.ok) {
|
||||
console.error('[Emby Image] 获取图片失败:', {
|
||||
itemId,
|
||||
imageType,
|
||||
status: imageResponse.status,
|
||||
statusText: imageResponse.statusText,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: '获取图片失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 获取 Content-Type
|
||||
const contentType = imageResponse.headers.get('content-type') || 'image/jpeg';
|
||||
|
||||
// 构建响应头
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Type', contentType);
|
||||
|
||||
// 复制重要的响应头
|
||||
const contentLength = imageResponse.headers.get('content-length');
|
||||
if (contentLength) {
|
||||
headers.set('Content-Length', contentLength);
|
||||
}
|
||||
|
||||
// 设置缓存头
|
||||
headers.set('Cache-Control', 'public, max-age=86400'); // 缓存1天
|
||||
|
||||
// 返回图片内容
|
||||
return new NextResponse(imageResponse.body, {
|
||||
status: imageResponse.status,
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Emby Image] 错误:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '获取图片失败', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getCachedEmbyList, setCachedEmbyList } from '@/lib/emby-cache';
|
||||
import { embyManager } from '@/lib/emby-manager';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -31,6 +32,9 @@ export async function GET(request: NextRequest) {
|
||||
// 获取Emby客户端
|
||||
const client = await embyManager.getClient(embyKey);
|
||||
|
||||
// 获取代理 token(如果启用了代理)
|
||||
const proxyToken = client.isProxyEnabled() ? await getProxyToken(request) : null;
|
||||
|
||||
// 获取媒体列表
|
||||
const result = await client.getItems({
|
||||
ParentId: parentId,
|
||||
@@ -46,7 +50,7 @@ export async function GET(request: NextRequest) {
|
||||
const list = result.Items.map((item) => ({
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined),
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
rating: item.CommunityRating || 0,
|
||||
mediaType: item.Type === 'Movie' ? 'movie' : 'tv',
|
||||
|
||||
@@ -79,8 +79,10 @@ export async function GET(
|
||||
// 构建 Emby 原始播放链接(强制获取直接URL,避免代理循环)
|
||||
let embyStreamUrl = await client.getStreamUrl(itemId, true, true);
|
||||
|
||||
// 构建请求头,转发 Range 请求
|
||||
const requestHeaders: HeadersInit = {};
|
||||
// 构建请求头,转发 Range 请求,并添加自定义 User-Agent
|
||||
const requestHeaders: HeadersInit = {
|
||||
'User-Agent': client.getUserAgent(),
|
||||
};
|
||||
const rangeHeader = request.headers.get('range');
|
||||
if (rangeHeader) {
|
||||
requestHeaders['Range'] = rangeHeader;
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -58,6 +59,9 @@ export async function GET(request: NextRequest) {
|
||||
console.log('[Search] Emby sources count:', embySources.length);
|
||||
console.log('[Search] Emby sources:', embySources.map(s => ({ key: s.config.key, name: s.config.name })));
|
||||
|
||||
// 获取代理 token(用于图片代理)
|
||||
const proxyToken = await getProxyToken(request);
|
||||
|
||||
// 为每个 Emby 源创建搜索 Promise(全部并发,无限制)
|
||||
const embyPromises = embySources.map(({ client, config: embyConfig }) =>
|
||||
Promise.race([
|
||||
@@ -80,7 +84,7 @@ export async function GET(request: NextRequest) {
|
||||
source: sourceValue,
|
||||
source_name: sourceName,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
poster: client.getImageUrl(item.Id, 'Primary', undefined, client.isProxyEnabled() ? proxyToken || undefined : undefined),
|
||||
episodes: [],
|
||||
episodes_titles: [],
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -123,6 +124,9 @@ export async function GET(request: NextRequest) {
|
||||
const embySourcesMap = await embyManager.getAllClients();
|
||||
const embySources = Array.from(embySourcesMap.values());
|
||||
|
||||
// 获取代理 token(用于图片代理)
|
||||
const proxyToken = await getProxyToken(request);
|
||||
|
||||
// 为每个 Emby 源并发搜索,并单独发送结果
|
||||
const embySearchPromises = embySources.map(async ({ client, config: embyConfig }) => {
|
||||
try {
|
||||
@@ -144,7 +148,7 @@ export async function GET(request: NextRequest) {
|
||||
source: sourceValue,
|
||||
source_name: sourceName,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
poster: client.getImageUrl(item.Id, 'Primary', undefined, client.isProxyEnabled() ? proxyToken || undefined : undefined),
|
||||
episodes: [],
|
||||
episodes_titles: [],
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -52,6 +53,9 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const client = await embyManager.getClient(embyKey);
|
||||
|
||||
// 获取代理 token(如果启用了代理)
|
||||
const proxyToken = client.isProxyEnabled() ? await getProxyToken(request) : null;
|
||||
|
||||
// 获取媒体详情
|
||||
const item = await client.getItem(id);
|
||||
|
||||
@@ -65,7 +69,7 @@ export async function GET(request: NextRequest) {
|
||||
source_name: sourceName,
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined),
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
douban_id: 0,
|
||||
desc: item.Overview || '',
|
||||
@@ -99,7 +103,7 @@ export async function GET(request: NextRequest) {
|
||||
source_name: sourceName,
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined),
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
douban_id: 0,
|
||||
desc: item.Overview || '',
|
||||
|
||||
Reference in New Issue
Block a user