emby增加自定义ua和图片代理
This commit is contained in:
+20
-1
@@ -3512,6 +3512,7 @@ const EmbyConfigComponent = ({
|
|||||||
appendMediaSourceId: false,
|
appendMediaSourceId: false,
|
||||||
transcodeMp4: false,
|
transcodeMp4: false,
|
||||||
proxyPlay: false,
|
proxyPlay: false,
|
||||||
|
customUserAgent: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 从配置加载源列表
|
// 从配置加载源列表
|
||||||
@@ -3551,6 +3552,7 @@ const EmbyConfigComponent = ({
|
|||||||
appendMediaSourceId: false,
|
appendMediaSourceId: false,
|
||||||
transcodeMp4: false,
|
transcodeMp4: false,
|
||||||
proxyPlay: false,
|
proxyPlay: false,
|
||||||
|
customUserAgent: '',
|
||||||
});
|
});
|
||||||
setEditingSource(null);
|
setEditingSource(null);
|
||||||
setShowAddForm(false);
|
setShowAddForm(false);
|
||||||
@@ -4246,7 +4248,7 @@ const EmbyConfigComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 视频播放代理开关 */}
|
{/* 视频播放代理开关 */}
|
||||||
<div className='flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700'>
|
<div className='flex items-center justify-between mb-3'>
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<h4 className='text-sm font-medium text-gray-900 dark:text-white'>
|
<h4 className='text-sm font-medium text-gray-900 dark:text-white'>
|
||||||
视频播放代理
|
视频播放代理
|
||||||
@@ -4268,6 +4270,23 @@ const EmbyConfigComponent = ({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 自定义User-Agent */}
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||||
|
自定义User-Agent
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={formData.customUserAgent || ''}
|
||||||
|
onChange={(e) => setFormData({ ...formData, customUserAgent: e.target.value })}
|
||||||
|
placeholder='留空使用默认浏览器UA'
|
||||||
|
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white text-sm'
|
||||||
|
/>
|
||||||
|
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||||
|
用于登录、获取影片和代理视频时的User-Agent,留空则使用默认浏览器UA
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ async function handleSearch(client: EmbyClient, query: string) {
|
|||||||
const list = result.Items.map((item) => ({
|
const list = result.Items.map((item) => ({
|
||||||
vod_id: item.Id,
|
vod_id: item.Id,
|
||||||
vod_name: item.Name,
|
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_remarks: item.Type === 'Movie' ? '电影' : '剧集',
|
||||||
vod_year: item.ProductionYear?.toString() || '',
|
vod_year: item.ProductionYear?.toString() || '',
|
||||||
vod_content: item.Overview || '',
|
vod_content: item.Overview || '',
|
||||||
@@ -247,7 +247,7 @@ async function handleDetail(
|
|||||||
{
|
{
|
||||||
vod_id: item.Id,
|
vod_id: item.Id,
|
||||||
vod_name: item.Name,
|
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_remarks: item.Type === 'Movie' ? '电影' : '剧集',
|
||||||
vod_year: item.ProductionYear?.toString() || '',
|
vod_year: item.ProductionYear?.toString() || '',
|
||||||
vod_content: item.Overview || '',
|
vod_content: item.Overview || '',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { embyManager } from '@/lib/emby-manager';
|
import { embyManager } from '@/lib/emby-manager';
|
||||||
|
import { getProxyToken } from '@/lib/emby-token';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -19,6 +20,9 @@ export async function GET(request: NextRequest) {
|
|||||||
// 获取Emby客户端
|
// 获取Emby客户端
|
||||||
const client = await embyManager.getClient(embyKey);
|
const client = await embyManager.getClient(embyKey);
|
||||||
|
|
||||||
|
// 获取代理 token(如果启用了代理)
|
||||||
|
const proxyToken = client.isProxyEnabled() ? await getProxyToken(request) : null;
|
||||||
|
|
||||||
// 获取媒体详情
|
// 获取媒体详情
|
||||||
const item = await client.getItem(itemId);
|
const item = await client.getItem(itemId);
|
||||||
|
|
||||||
@@ -54,7 +58,7 @@ export async function GET(request: NextRequest) {
|
|||||||
title: item.Name,
|
title: item.Name,
|
||||||
type: item.Type === 'Movie' ? 'movie' : 'tv',
|
type: item.Type === 'Movie' ? 'movie' : 'tv',
|
||||||
overview: item.Overview || '',
|
overview: item.Overview || '',
|
||||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined),
|
||||||
year: item.ProductionYear?.toString() || '',
|
year: item.ProductionYear?.toString() || '',
|
||||||
rating: item.CommunityRating || 0,
|
rating: item.CommunityRating || 0,
|
||||||
playUrl: item.Type === 'Movie' ? await client.getStreamUrl(item.Id) : undefined,
|
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 { getCachedEmbyList, setCachedEmbyList } from '@/lib/emby-cache';
|
||||||
import { embyManager } from '@/lib/emby-manager';
|
import { embyManager } from '@/lib/emby-manager';
|
||||||
|
import { getProxyToken } from '@/lib/emby-token';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@ export async function GET(request: NextRequest) {
|
|||||||
// 获取Emby客户端
|
// 获取Emby客户端
|
||||||
const client = await embyManager.getClient(embyKey);
|
const client = await embyManager.getClient(embyKey);
|
||||||
|
|
||||||
|
// 获取代理 token(如果启用了代理)
|
||||||
|
const proxyToken = client.isProxyEnabled() ? await getProxyToken(request) : null;
|
||||||
|
|
||||||
// 获取媒体列表
|
// 获取媒体列表
|
||||||
const result = await client.getItems({
|
const result = await client.getItems({
|
||||||
ParentId: parentId,
|
ParentId: parentId,
|
||||||
@@ -46,7 +50,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const list = result.Items.map((item) => ({
|
const list = result.Items.map((item) => ({
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined),
|
||||||
year: item.ProductionYear?.toString() || '',
|
year: item.ProductionYear?.toString() || '',
|
||||||
rating: item.CommunityRating || 0,
|
rating: item.CommunityRating || 0,
|
||||||
mediaType: item.Type === 'Movie' ? 'movie' : 'tv',
|
mediaType: item.Type === 'Movie' ? 'movie' : 'tv',
|
||||||
|
|||||||
@@ -79,8 +79,10 @@ export async function GET(
|
|||||||
// 构建 Emby 原始播放链接(强制获取直接URL,避免代理循环)
|
// 构建 Emby 原始播放链接(强制获取直接URL,避免代理循环)
|
||||||
let embyStreamUrl = await client.getStreamUrl(itemId, true, true);
|
let embyStreamUrl = await client.getStreamUrl(itemId, true, true);
|
||||||
|
|
||||||
// 构建请求头,转发 Range 请求
|
// 构建请求头,转发 Range 请求,并添加自定义 User-Agent
|
||||||
const requestHeaders: HeadersInit = {};
|
const requestHeaders: HeadersInit = {
|
||||||
|
'User-Agent': client.getUserAgent(),
|
||||||
|
};
|
||||||
const rangeHeader = request.headers.get('range');
|
const rangeHeader = request.headers.get('range');
|
||||||
if (rangeHeader) {
|
if (rangeHeader) {
|
||||||
requestHeaders['Range'] = rangeHeader;
|
requestHeaders['Range'] = rangeHeader;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
|||||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||||
import { searchFromApi } from '@/lib/downstream';
|
import { searchFromApi } from '@/lib/downstream';
|
||||||
import { yellowWords } from '@/lib/yellow';
|
import { yellowWords } from '@/lib/yellow';
|
||||||
|
import { getProxyToken } from '@/lib/emby-token';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
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 count:', embySources.length);
|
||||||
console.log('[Search] Emby sources:', embySources.map(s => ({ key: s.config.key, name: s.config.name })));
|
console.log('[Search] Emby sources:', embySources.map(s => ({ key: s.config.key, name: s.config.name })));
|
||||||
|
|
||||||
|
// 获取代理 token(用于图片代理)
|
||||||
|
const proxyToken = await getProxyToken(request);
|
||||||
|
|
||||||
// 为每个 Emby 源创建搜索 Promise(全部并发,无限制)
|
// 为每个 Emby 源创建搜索 Promise(全部并发,无限制)
|
||||||
const embyPromises = embySources.map(({ client, config: embyConfig }) =>
|
const embyPromises = embySources.map(({ client, config: embyConfig }) =>
|
||||||
Promise.race([
|
Promise.race([
|
||||||
@@ -80,7 +84,7 @@ export async function GET(request: NextRequest) {
|
|||||||
source: sourceValue,
|
source: sourceValue,
|
||||||
source_name: sourceName,
|
source_name: sourceName,
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
poster: client.getImageUrl(item.Id, 'Primary', undefined, client.isProxyEnabled() ? proxyToken || undefined : undefined),
|
||||||
episodes: [],
|
episodes: [],
|
||||||
episodes_titles: [],
|
episodes_titles: [],
|
||||||
year: item.ProductionYear?.toString() || '',
|
year: item.ProductionYear?.toString() || '',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
|||||||
import { getAvailableApiSites, getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getConfig } from '@/lib/config';
|
||||||
import { searchFromApi } from '@/lib/downstream';
|
import { searchFromApi } from '@/lib/downstream';
|
||||||
import { yellowWords } from '@/lib/yellow';
|
import { yellowWords } from '@/lib/yellow';
|
||||||
|
import { getProxyToken } from '@/lib/emby-token';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -123,6 +124,9 @@ export async function GET(request: NextRequest) {
|
|||||||
const embySourcesMap = await embyManager.getAllClients();
|
const embySourcesMap = await embyManager.getAllClients();
|
||||||
const embySources = Array.from(embySourcesMap.values());
|
const embySources = Array.from(embySourcesMap.values());
|
||||||
|
|
||||||
|
// 获取代理 token(用于图片代理)
|
||||||
|
const proxyToken = await getProxyToken(request);
|
||||||
|
|
||||||
// 为每个 Emby 源并发搜索,并单独发送结果
|
// 为每个 Emby 源并发搜索,并单独发送结果
|
||||||
const embySearchPromises = embySources.map(async ({ client, config: embyConfig }) => {
|
const embySearchPromises = embySources.map(async ({ client, config: embyConfig }) => {
|
||||||
try {
|
try {
|
||||||
@@ -144,7 +148,7 @@ export async function GET(request: NextRequest) {
|
|||||||
source: sourceValue,
|
source: sourceValue,
|
||||||
source_name: sourceName,
|
source_name: sourceName,
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
poster: client.getImageUrl(item.Id, 'Primary', undefined, client.isProxyEnabled() ? proxyToken || undefined : undefined),
|
||||||
episodes: [],
|
episodes: [],
|
||||||
episodes_titles: [],
|
episodes_titles: [],
|
||||||
year: item.ProductionYear?.toString() || '',
|
year: item.ProductionYear?.toString() || '',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||||
import { searchFromApi } from '@/lib/downstream';
|
import { searchFromApi } from '@/lib/downstream';
|
||||||
|
import { getProxyToken } from '@/lib/emby-token';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -52,6 +53,9 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const client = await embyManager.getClient(embyKey);
|
const client = await embyManager.getClient(embyKey);
|
||||||
|
|
||||||
|
// 获取代理 token(如果启用了代理)
|
||||||
|
const proxyToken = client.isProxyEnabled() ? await getProxyToken(request) : null;
|
||||||
|
|
||||||
// 获取媒体详情
|
// 获取媒体详情
|
||||||
const item = await client.getItem(id);
|
const item = await client.getItem(id);
|
||||||
|
|
||||||
@@ -65,7 +69,7 @@ export async function GET(request: NextRequest) {
|
|||||||
source_name: sourceName,
|
source_name: sourceName,
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined),
|
||||||
year: item.ProductionYear?.toString() || '',
|
year: item.ProductionYear?.toString() || '',
|
||||||
douban_id: 0,
|
douban_id: 0,
|
||||||
desc: item.Overview || '',
|
desc: item.Overview || '',
|
||||||
@@ -99,7 +103,7 @@ export async function GET(request: NextRequest) {
|
|||||||
source_name: sourceName,
|
source_name: sourceName,
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
poster: client.getImageUrl(item.Id, 'Primary', undefined, proxyToken || undefined),
|
||||||
year: item.ProductionYear?.toString() || '',
|
year: item.ProductionYear?.toString() || '',
|
||||||
douban_id: 0,
|
douban_id: 0,
|
||||||
desc: item.Overview || '',
|
desc: item.Overview || '',
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ export interface AdminConfig {
|
|||||||
appendMediaSourceId?: boolean; // 拼接MediaSourceId参数
|
appendMediaSourceId?: boolean; // 拼接MediaSourceId参数
|
||||||
transcodeMp4?: boolean; // 转码mp4
|
transcodeMp4?: boolean; // 转码mp4
|
||||||
proxyPlay?: boolean; // 视频播放代理开关
|
proxyPlay?: boolean; // 视频播放代理开关
|
||||||
|
customUserAgent?: string; // 自定义User-Agent
|
||||||
}>;
|
}>;
|
||||||
// 旧格式:单源配置(向后兼容)
|
// 旧格式:单源配置(向后兼容)
|
||||||
Enabled?: boolean;
|
Enabled?: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
import { getAuthInfoFromCookie } from './auth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用于代理的 token
|
||||||
|
* 优先级:全局 token > 用户 token > null
|
||||||
|
*/
|
||||||
|
export async function getProxyToken(request?: NextRequest): Promise<string | null> {
|
||||||
|
// 1. 尝试获取全局 token
|
||||||
|
const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN;
|
||||||
|
if (globalToken) {
|
||||||
|
return globalToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 如果提供了 request,尝试从用户登录信息获取用户的 tvbox token
|
||||||
|
if (request) {
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (authInfo && authInfo.username) {
|
||||||
|
try {
|
||||||
|
const { db } = await import('./db');
|
||||||
|
const userInfo = await db.getUserInfoV2(authInfo.username);
|
||||||
|
if (userInfo && userInfo.tvboxToken && !userInfo.banned) {
|
||||||
|
return userInfo.tvboxToken;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略错误,继续
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 没有可用的 token
|
||||||
|
return null;
|
||||||
|
}
|
||||||
+26
-1
@@ -12,6 +12,7 @@ interface EmbyConfig {
|
|||||||
appendMediaSourceId?: boolean;
|
appendMediaSourceId?: boolean;
|
||||||
transcodeMp4?: boolean;
|
transcodeMp4?: boolean;
|
||||||
proxyPlay?: boolean; // 视频播放代理开关
|
proxyPlay?: boolean; // 视频播放代理开关
|
||||||
|
customUserAgent?: string; // 自定义User-Agent
|
||||||
key?: string; // Emby源的唯一标识
|
key?: string; // Emby源的唯一标识
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ export class EmbyClient {
|
|||||||
private transcodeMp4: boolean;
|
private transcodeMp4: boolean;
|
||||||
private proxyPlay: boolean;
|
private proxyPlay: boolean;
|
||||||
private embyKey?: string;
|
private embyKey?: string;
|
||||||
|
private customUserAgent: string;
|
||||||
|
|
||||||
constructor(config: EmbyConfig) {
|
constructor(config: EmbyConfig) {
|
||||||
let serverUrl = config.ServerURL.replace(/\/$/, '');
|
let serverUrl = config.ServerURL.replace(/\/$/, '');
|
||||||
@@ -85,6 +87,8 @@ export class EmbyClient {
|
|||||||
this.transcodeMp4 = config.transcodeMp4 || false;
|
this.transcodeMp4 = config.transcodeMp4 || false;
|
||||||
this.proxyPlay = config.proxyPlay || false;
|
this.proxyPlay = config.proxyPlay || false;
|
||||||
this.embyKey = config.key;
|
this.embyKey = config.key;
|
||||||
|
// 设置自定义UA,如果没有设置则使用默认浏览器UA
|
||||||
|
this.customUserAgent = config.customUserAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||||
|
|
||||||
// 如果 URL 不包含 /emby 路径,自动添加(除非启用了 removeEmbyPrefix)
|
// 如果 URL 不包含 /emby 路径,自动添加(除非启用了 removeEmbyPrefix)
|
||||||
if (!serverUrl.endsWith('/emby') && !this.removeEmbyPrefix) {
|
if (!serverUrl.endsWith('/emby') && !this.removeEmbyPrefix) {
|
||||||
@@ -122,6 +126,7 @@ export class EmbyClient {
|
|||||||
private getHeaders(): Record<string, string> {
|
private getHeaders(): Record<string, string> {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': this.customUserAgent,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.apiKey) {
|
if (this.apiKey) {
|
||||||
@@ -146,6 +151,7 @@ export class EmbyClient {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'X-Emby-Authorization': 'MediaBrowser Client="LunaTV", Device="Web", DeviceId="lunatv-web", Version="1.0.0"',
|
'X-Emby-Authorization': 'MediaBrowser Client="LunaTV", Device="Web", DeviceId="lunatv-web", Version="1.0.0"',
|
||||||
|
'User-Agent': this.customUserAgent,
|
||||||
},
|
},
|
||||||
body: params.toString(),
|
body: params.toString(),
|
||||||
});
|
});
|
||||||
@@ -420,7 +426,18 @@ export class EmbyClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getImageUrl(itemId: string, imageType: 'Primary' | 'Backdrop' | 'Logo' = 'Primary', maxWidth?: number): string {
|
getImageUrl(itemId: string, imageType: 'Primary' | 'Backdrop' | 'Logo' = 'Primary', maxWidth?: number, proxyToken?: string): string {
|
||||||
|
// 如果启用了代理播放且提供了 token,返回代理 URL
|
||||||
|
if (this.proxyPlay && proxyToken) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('imageType', imageType);
|
||||||
|
if (maxWidth) params.set('maxWidth', maxWidth.toString());
|
||||||
|
if (this.embyKey) params.set('embyKey', this.embyKey);
|
||||||
|
|
||||||
|
return `/api/emby/image/${proxyToken}/${itemId}?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则返回直连 URL
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
const token = this.apiKey || this.authToken;
|
const token = this.apiKey || this.authToken;
|
||||||
|
|
||||||
@@ -551,4 +568,12 @@ export class EmbyClient {
|
|||||||
|
|
||||||
return subtitles;
|
return subtitles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserAgent(): string {
|
||||||
|
return this.customUserAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProxyEnabled(): boolean {
|
||||||
|
return this.proxyPlay;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { SearchResult } from '@/lib/types';
|
|||||||
*/
|
*/
|
||||||
export async function getEmbyDetail(
|
export async function getEmbyDetail(
|
||||||
source: string,
|
source: string,
|
||||||
id: string
|
id: string,
|
||||||
|
proxyToken?: string | null
|
||||||
): Promise<SearchResult> {
|
): Promise<SearchResult> {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ export async function getEmbyDetail(
|
|||||||
source_name: sourceName,
|
source_name: sourceName,
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
poster: client.getImageUrl(item.Id, 'Primary', undefined, client.isProxyEnabled() ? proxyToken || undefined : undefined),
|
||||||
year: item.ProductionYear?.toString() || '',
|
year: item.ProductionYear?.toString() || '',
|
||||||
douban_id: 0,
|
douban_id: 0,
|
||||||
desc: item.Overview || '',
|
desc: item.Overview || '',
|
||||||
@@ -76,7 +77,7 @@ export async function getEmbyDetail(
|
|||||||
source_name: sourceName,
|
source_name: sourceName,
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
poster: client.getImageUrl(item.Id, 'Primary', undefined, client.isProxyEnabled() ? proxyToken || undefined : undefined),
|
||||||
year: item.ProductionYear?.toString() || '',
|
year: item.ProductionYear?.toString() || '',
|
||||||
douban_id: 0,
|
douban_id: 0,
|
||||||
desc: item.Overview || '',
|
desc: item.Overview || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user