emby支持多源
This commit is contained in:
+27
-10
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user