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
+27 -10
View File
@@ -156,16 +156,33 @@ export interface AdminConfig {
SystemPrompt?: string; // 自定义系统提示词
};
EmbyConfig?: {
Enabled: boolean; // 是否启用Emby媒体库功能
ServerURL: string; // Emby服务器地址
ApiKey?: string; // API Key(推荐方式)
Username?: string; // 用户名(或使用API Key
Password?: string; // 密码
UserId?: string; // 用户ID(登录后获取)
AuthToken?: string; // 认证令牌(用户名密码登录后获取
Libraries?: string[]; // 要显示的媒体库ID(可选,默认全部
LastSyncTime?: number; // 最后同步时间戳
ItemCount?: number; // 媒体项数量
// 新格式:多源配置(推荐)
Sources?: Array<{
key: string; // 唯一标识,如 'emby1', 'emby2'
name: string; // 显示名称,如 '家庭Emby', '公司Emby'
enabled: boolean; // 是否启用
ServerURL: string; // Emby服务器地址
ApiKey?: string; // API Key(推荐方式
Username?: string; // 用户名(或使用API Key
Password?: string; // 密码
UserId?: string; // 用户ID(登录后获取)
AuthToken?: string; // 认证令牌(用户名密码登录后获取)
Libraries?: string[]; // 要显示的媒体库ID(可选,默认全部)
LastSyncTime?: number; // 最后同步时间戳
ItemCount?: number; // 媒体项数量
isDefault?: boolean; // 是否为默认源(用于向后兼容)
}>;
// 旧格式:单源配置(向后兼容)
Enabled?: boolean;
ServerURL?: string;
ApiKey?: string;
Username?: string;
Password?: string;
UserId?: string;
AuthToken?: string;
Libraries?: string[];
LastSyncTime?: number;
ItemCount?: number;
};
}
+54
View File
@@ -335,9 +335,25 @@ export async function getConfig(): Promise<AdminConfig> {
await db.saveAdminConfig(adminConfig);
}
}
// 检查是否有旧格式Emby配置需要迁移
const needsEmbyMigration = adminConfig.EmbyConfig &&
adminConfig.EmbyConfig.ServerURL &&
!adminConfig.EmbyConfig.Sources;
adminConfig = configSelfCheck(adminConfig);
cachedConfig = adminConfig;
// 如果进行了Emby配置迁移,保存到数据库
if (!dbReadFailed && needsEmbyMigration) {
try {
await db.saveAdminConfig(adminConfig);
console.log('[Config] Emby配置迁移已保存到数据库');
} catch (error) {
console.error('[Config] 保存迁移后的配置失败:', error);
}
}
// 自动迁移用户(如果配置中有用户且V2存储支持)
// 过滤掉站长后检查是否有需要迁移的用户
const nonOwnerUsers = adminConfig.UserConfig.Users.filter(
@@ -476,6 +492,44 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
return true;
});
// Emby配置迁移:将旧格式迁移到新格式
if (adminConfig.EmbyConfig) {
// 如果是旧格式(有ServerURL但没有Sources
if (adminConfig.EmbyConfig.ServerURL && !adminConfig.EmbyConfig.Sources) {
console.log('[Config] 检测到旧格式Emby配置,自动迁移到新格式');
const oldConfig = adminConfig.EmbyConfig;
adminConfig.EmbyConfig = {
Sources: [{
key: 'default',
name: 'Emby',
enabled: oldConfig.Enabled ?? false,
ServerURL: oldConfig.ServerURL,
ApiKey: oldConfig.ApiKey,
Username: oldConfig.Username,
Password: oldConfig.Password,
UserId: oldConfig.UserId,
AuthToken: oldConfig.AuthToken,
Libraries: oldConfig.Libraries,
LastSyncTime: oldConfig.LastSyncTime,
ItemCount: oldConfig.ItemCount,
isDefault: true,
}],
};
}
// Emby源去重
if (adminConfig.EmbyConfig.Sources) {
const seenEmbyKeys = new Set<string>();
adminConfig.EmbyConfig.Sources = adminConfig.EmbyConfig.Sources.filter((source) => {
if (seenEmbyKeys.has(source.key)) {
return false;
}
seenEmbyKeys.add(source.key);
return true;
});
}
}
return adminConfig;
}
+16 -11
View File
@@ -15,8 +15,9 @@ const EMBY_VIEWS_CACHE_KEY = 'emby:views';
/**
* 生成 Emby 列表缓存键
*/
function makeListCacheKey(page: number, pageSize: number, parentId?: string): string {
return parentId ? `emby:list:${page}:${pageSize}:${parentId}` : `emby:list:${page}:${pageSize}`;
function makeListCacheKey(page: number, pageSize: number, parentId?: string, embyKey?: string): string {
const keyPrefix = embyKey ? `emby:${embyKey}` : 'emby';
return parentId ? `${keyPrefix}:list:${page}:${pageSize}:${parentId}` : `${keyPrefix}:list:${page}:${pageSize}`;
}
/**
@@ -25,9 +26,10 @@ function makeListCacheKey(page: number, pageSize: number, parentId?: string): st
export function getCachedEmbyList(
page: number,
pageSize: number,
parentId?: string
parentId?: string,
embyKey?: string
): any | null {
const key = makeListCacheKey(page, pageSize, parentId);
const key = makeListCacheKey(page, pageSize, parentId, embyKey);
const entry = EMBY_CACHE.get(key);
if (!entry) return null;
@@ -47,10 +49,11 @@ export function setCachedEmbyList(
page: number,
pageSize: number,
data: any,
parentId?: string
parentId?: string,
embyKey?: string
): void {
const now = Date.now();
const key = makeListCacheKey(page, pageSize, parentId);
const key = makeListCacheKey(page, pageSize, parentId, embyKey);
EMBY_CACHE.set(key, {
expiresAt: now + EMBY_CACHE_TTL_MS,
data,
@@ -69,13 +72,14 @@ export function clearEmbyCache(): { cleared: number } {
/**
* 获取缓存的 Emby 媒体库列表
*/
export function getCachedEmbyViews(): any | null {
const entry = EMBY_CACHE.get(EMBY_VIEWS_CACHE_KEY);
export function getCachedEmbyViews(embyKey: string = 'default'): any | null {
const cacheKey = `${EMBY_VIEWS_CACHE_KEY}:${embyKey}`;
const entry = EMBY_CACHE.get(cacheKey);
if (!entry) return null;
// 检查是否过期
if (entry.expiresAt <= Date.now()) {
EMBY_CACHE.delete(EMBY_VIEWS_CACHE_KEY);
EMBY_CACHE.delete(cacheKey);
return null;
}
@@ -85,9 +89,10 @@ export function getCachedEmbyViews(): any | null {
/**
* 设置缓存的 Emby 媒体库列表
*/
export function setCachedEmbyViews(data: any): void {
export function setCachedEmbyViews(embyKey: string = 'default', data: any): void {
const now = Date.now();
EMBY_CACHE.set(EMBY_VIEWS_CACHE_KEY, {
const cacheKey = `${EMBY_VIEWS_CACHE_KEY}:${embyKey}`;
EMBY_CACHE.set(cacheKey, {
expiresAt: now + EMBY_VIEWS_CACHE_TTL_MS,
data,
});
+182
View File
@@ -0,0 +1,182 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { EmbyClient } from './emby.client';
import { getConfig } from './config';
import { AdminConfig } from './admin.types';
interface EmbySourceConfig {
key: string;
name: string;
enabled: boolean;
ServerURL: string;
ApiKey?: string;
Username?: string;
Password?: string;
UserId?: string;
AuthToken?: string;
Libraries?: string[];
LastSyncTime?: number;
ItemCount?: number;
isDefault?: boolean;
}
class EmbyManager {
private static instance: EmbyManager;
private clients: Map<string, EmbyClient> = new Map();
private constructor() {}
static getInstance(): EmbyManager {
if (!EmbyManager.instance) {
EmbyManager.instance = new EmbyManager();
}
return EmbyManager.instance;
}
/**
* 从配置中获取所有Emby源(支持新旧格式)
*/
private async getSources(): Promise<EmbySourceConfig[]> {
const config = await getConfig();
// 如果是新格式(Sources数组)
if (config.EmbyConfig?.Sources && Array.isArray(config.EmbyConfig.Sources)) {
return config.EmbyConfig.Sources;
}
// 如果是旧格式(单源配置),转换为数组格式
if (config.EmbyConfig?.ServerURL) {
return [{
key: 'default',
name: 'Emby',
enabled: config.EmbyConfig.Enabled ?? false,
ServerURL: config.EmbyConfig.ServerURL,
ApiKey: config.EmbyConfig.ApiKey,
Username: config.EmbyConfig.Username,
Password: config.EmbyConfig.Password,
UserId: config.EmbyConfig.UserId,
AuthToken: config.EmbyConfig.AuthToken,
Libraries: config.EmbyConfig.Libraries,
LastSyncTime: config.EmbyConfig.LastSyncTime,
ItemCount: config.EmbyConfig.ItemCount,
isDefault: true,
}];
}
return [];
}
/**
* 获取指定key的EmbyClient
* @param key Emby源的key,如果不指定则使用默认源
*/
async getClient(key?: string): Promise<EmbyClient> {
const sources = await this.getSources();
if (sources.length === 0) {
throw new Error('未配置 Emby 源');
}
// 如果没有指定key,使用默认源(第一个或标记为default的)
if (!key) {
const defaultSource = sources.find(s => s.isDefault) || sources[0];
key = defaultSource.key;
}
// 从缓存获取或创建新实例
if (!this.clients.has(key)) {
const sourceConfig = sources.find(s => s.key === key);
if (!sourceConfig) {
throw new Error(`未找到 Emby 源: ${key}`);
}
if (!sourceConfig.enabled) {
throw new Error(`Emby 源已禁用: ${sourceConfig.name}`);
}
this.clients.set(key, new EmbyClient(sourceConfig));
}
return this.clients.get(key)!;
}
/**
* 获取所有启用的EmbyClient
*/
async getAllClients(): Promise<Map<string, { client: EmbyClient; config: EmbySourceConfig }>> {
const sources = await this.getSources();
const enabledSources = sources.filter(s => s.enabled);
const result = new Map<string, { client: EmbyClient; config: EmbySourceConfig }>();
for (const source of enabledSources) {
if (!this.clients.has(source.key)) {
this.clients.set(source.key, new EmbyClient(source));
}
result.set(source.key, {
client: this.clients.get(source.key)!,
config: source,
});
}
return result;
}
/**
* 获取所有启用的Emby源配置
*/
async getEnabledSources(): Promise<EmbySourceConfig[]> {
const sources = await this.getSources();
return sources.filter(s => s.enabled);
}
/**
* 检查是否配置了Emby
*/
async hasEmby(): Promise<boolean> {
const sources = await this.getSources();
return sources.some(s => s.enabled && s.ServerURL);
}
/**
* 清除缓存的客户端实例
*/
clearCache() {
this.clients.clear();
}
}
export const embyManager = EmbyManager.getInstance();
/**
* 配置迁移函数:将旧格式配置迁移到新格式
*/
export function migrateEmbyConfig(config: AdminConfig): AdminConfig {
// 如果已经是新格式,直接返回
if (config.EmbyConfig?.Sources) {
return config;
}
// 如果是旧格式,迁移到新格式
if (config.EmbyConfig && config.EmbyConfig.ServerURL) {
const oldConfig = config.EmbyConfig;
config.EmbyConfig = {
Sources: [{
key: 'default',
name: 'Emby',
enabled: oldConfig.Enabled ?? false,
ServerURL: oldConfig.ServerURL,
ApiKey: oldConfig.ApiKey,
Username: oldConfig.Username,
Password: oldConfig.Password,
UserId: oldConfig.UserId,
AuthToken: oldConfig.AuthToken,
Libraries: oldConfig.Libraries,
LastSyncTime: oldConfig.LastSyncTime,
ItemCount: oldConfig.ItemCount,
isDefault: true,
}],
};
}
return config;
}