增加动漫数据源配置
This commit is contained in:
+165
-96
@@ -7,7 +7,9 @@ import { AdminConfig } from './admin.types';
|
||||
const BUILTIN_DANMAKU_API_BASE = 'https://mtvpls-danmu.netlify.app/87654321';
|
||||
const DEFAULT_LIVE_REFRESH_INTERVAL_HOURS = 12;
|
||||
|
||||
function normalizeLiveRefreshIntervalHours(refreshIntervalHours?: number): number {
|
||||
function normalizeLiveRefreshIntervalHours(
|
||||
refreshIntervalHours?: number
|
||||
): number {
|
||||
const normalizedInterval = Number(refreshIntervalHours);
|
||||
|
||||
if (!Number.isFinite(normalizedInterval) || normalizedInterval <= 0) {
|
||||
@@ -44,7 +46,7 @@ interface ConfigFileStruct {
|
||||
}[];
|
||||
lives?: {
|
||||
[key: string]: LiveCfg;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const API_CONFIG = {
|
||||
@@ -71,7 +73,6 @@ export const API_CONFIG = {
|
||||
let cachedConfig: AdminConfig;
|
||||
let configInitPromise: Promise<AdminConfig> | null = null;
|
||||
|
||||
|
||||
// 从配置文件补充管理员配置
|
||||
export function refineConfig(adminConfig: AdminConfig): AdminConfig {
|
||||
let fileConfig: ConfigFileStruct;
|
||||
@@ -197,19 +198,22 @@ export function refineConfig(adminConfig: AdminConfig): AdminConfig {
|
||||
return adminConfig;
|
||||
}
|
||||
|
||||
async function getInitConfig(configFile: string, subConfig: {
|
||||
URL: string;
|
||||
AutoUpdate: boolean;
|
||||
LastCheck: string;
|
||||
} = {
|
||||
URL: "",
|
||||
async function getInitConfig(
|
||||
configFile: string,
|
||||
subConfig: {
|
||||
URL: string;
|
||||
AutoUpdate: boolean;
|
||||
LastCheck: string;
|
||||
} = {
|
||||
URL: '',
|
||||
AutoUpdate: false,
|
||||
LastCheck: "",
|
||||
}): Promise<AdminConfig> {
|
||||
LastCheck: '',
|
||||
}
|
||||
): Promise<AdminConfig> {
|
||||
let cfgFile: ConfigFileStruct;
|
||||
|
||||
// 优先从环境变量读取订阅 URL
|
||||
const envSubUrl = process.env.CONFIG_SUBSCRIPTION_URL || "";
|
||||
const envSubUrl = process.env.CONFIG_SUBSCRIPTION_URL || '';
|
||||
|
||||
if (envSubUrl) {
|
||||
try {
|
||||
@@ -228,7 +232,7 @@ async function getInitConfig(configFile: string, subConfig: {
|
||||
}
|
||||
|
||||
// 优先从环境变量读取配置
|
||||
const envConfig = process.env.INIT_CONFIG || "";
|
||||
const envConfig = process.env.INIT_CONFIG || '';
|
||||
const configSource = envConfig || configFile;
|
||||
|
||||
try {
|
||||
@@ -254,23 +258,37 @@ async function getInitConfig(configFile: string, subConfig: {
|
||||
process.env.NEXT_PUBLIC_DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent',
|
||||
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
|
||||
DoubanImageProxyType:
|
||||
process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent',
|
||||
process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE ||
|
||||
'cmliussss-cdn-tencent',
|
||||
DoubanImageProxy: process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '',
|
||||
DisableYellowFilter:
|
||||
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true',
|
||||
FluidSearch:
|
||||
process.env.NEXT_PUBLIC_FLUID_SEARCH !== 'false',
|
||||
FluidSearch: process.env.NEXT_PUBLIC_FLUID_SEARCH !== 'false',
|
||||
// 弹幕配置
|
||||
DanmakuSourceType: hasCustomDanmakuEnv ? 'custom' : 'builtin',
|
||||
DanmakuApiBase:
|
||||
process.env.DANMAKU_API_BASE ||
|
||||
(hasCustomDanmakuEnv ? 'http://localhost:9321' : BUILTIN_DANMAKU_API_BASE),
|
||||
(hasCustomDanmakuEnv
|
||||
? 'http://localhost:9321'
|
||||
: BUILTIN_DANMAKU_API_BASE),
|
||||
DanmakuApiToken: process.env.DANMAKU_API_TOKEN || '87654321',
|
||||
DanmakuAutoLoadDefault: true,
|
||||
// TMDB配置
|
||||
TMDBApiKey: process.env.TMDB_API_KEY || '',
|
||||
TMDBProxy: process.env.TMDB_PROXY || '',
|
||||
TMDBReverseProxy: process.env.TMDB_REVERSE_PROXY || '',
|
||||
// 动漫/Bangumi配置
|
||||
BangumiDataSource:
|
||||
(process.env.NEXT_PUBLIC_BANGUMI_DATA_SOURCE as any) || 'direct',
|
||||
BangumiApiBaseUrl:
|
||||
process.env.BANGUMI_API_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_BANGUMI_API_BASE_URL ||
|
||||
'https://api.bgm.tv',
|
||||
BangumiImageBaseUrl:
|
||||
process.env.BANGUMI_IMAGE_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_BANGUMI_IMAGE_BASE_URL ||
|
||||
'',
|
||||
BangumiProxy: process.env.BANGUMI_PROXY || '',
|
||||
// Pansou配置
|
||||
PansouApiUrl: '',
|
||||
PansouUsername: '',
|
||||
@@ -365,7 +383,7 @@ export async function getConfig(): Promise<AdminConfig> {
|
||||
// localStorage 模式下直接从环境变量初始化
|
||||
if (storageType === 'localstorage') {
|
||||
console.log('localStorage 模式:从环境变量初始化配置');
|
||||
const adminConfig = await getInitConfig("");
|
||||
const adminConfig = await getInitConfig('');
|
||||
cachedConfig = configSelfCheck(adminConfig);
|
||||
configInitPromise = null;
|
||||
return cachedConfig;
|
||||
@@ -386,19 +404,20 @@ export async function getConfig(): Promise<AdminConfig> {
|
||||
if (dbReadFailed) {
|
||||
// 数据库读取失败,使用默认配置但不保存,避免覆盖数据库
|
||||
console.warn('数据库读取失败,使用临时默认配置(不会保存到数据库)');
|
||||
adminConfig = await getInitConfig("");
|
||||
adminConfig = await getInitConfig('');
|
||||
} else {
|
||||
// 数据库中确实没有配置,首次初始化并保存
|
||||
console.log('首次初始化配置');
|
||||
adminConfig = await getInitConfig("");
|
||||
adminConfig = await getInitConfig('');
|
||||
await db.saveAdminConfig(adminConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有旧格式Emby配置需要迁移
|
||||
const needsEmbyMigration = adminConfig.EmbyConfig &&
|
||||
adminConfig.EmbyConfig.ServerURL &&
|
||||
!adminConfig.EmbyConfig.Sources;
|
||||
const needsEmbyMigration =
|
||||
adminConfig.EmbyConfig &&
|
||||
adminConfig.EmbyConfig.ServerURL &&
|
||||
!adminConfig.EmbyConfig.Sources;
|
||||
|
||||
adminConfig = configSelfCheck(adminConfig);
|
||||
cachedConfig = adminConfig;
|
||||
@@ -544,19 +563,27 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (!adminConfig.UserConfig) {
|
||||
adminConfig.UserConfig = { Users: [] };
|
||||
}
|
||||
if (!adminConfig.UserConfig.Users || !Array.isArray(adminConfig.UserConfig.Users)) {
|
||||
if (
|
||||
!adminConfig.UserConfig.Users ||
|
||||
!Array.isArray(adminConfig.UserConfig.Users)
|
||||
) {
|
||||
adminConfig.UserConfig.Users = [];
|
||||
}
|
||||
if (!adminConfig.SourceConfig || !Array.isArray(adminConfig.SourceConfig)) {
|
||||
adminConfig.SourceConfig = [];
|
||||
}
|
||||
if (!adminConfig.CustomCategories || !Array.isArray(adminConfig.CustomCategories)) {
|
||||
if (
|
||||
!adminConfig.CustomCategories ||
|
||||
!Array.isArray(adminConfig.CustomCategories)
|
||||
) {
|
||||
adminConfig.CustomCategories = [];
|
||||
}
|
||||
if (!adminConfig.LiveConfig || !Array.isArray(adminConfig.LiveConfig)) {
|
||||
adminConfig.LiveConfig = [];
|
||||
}
|
||||
adminConfig.LiveRefreshIntervalHours = normalizeLiveRefreshIntervalHours(adminConfig.LiveRefreshIntervalHours);
|
||||
adminConfig.LiveRefreshIntervalHours = normalizeLiveRefreshIntervalHours(
|
||||
adminConfig.LiveRefreshIntervalHours
|
||||
);
|
||||
|
||||
if (adminConfig.OpenListConfig) {
|
||||
if (!adminConfig.OpenListConfig.RootPaths) {
|
||||
@@ -567,7 +594,9 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (!adminConfig.OpenListConfig.OfflineDownloadPath) {
|
||||
adminConfig.OpenListConfig.OfflineDownloadPath = '/';
|
||||
}
|
||||
if (adminConfig.OpenListConfig.OfflineDownloadUseCustomSource === undefined) {
|
||||
if (
|
||||
adminConfig.OpenListConfig.OfflineDownloadUseCustomSource === undefined
|
||||
) {
|
||||
adminConfig.OpenListConfig.OfflineDownloadUseCustomSource = false;
|
||||
}
|
||||
if (adminConfig.OpenListConfig.OfflineDownloadURL === undefined) {
|
||||
@@ -584,11 +613,13 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
// 用户信息已迁移到新版数据库
|
||||
// 这里只保留站长用户用于兼容性,其他用户从数据库读取
|
||||
const ownerUser = process.env.USERNAME;
|
||||
adminConfig.UserConfig.Users = [{
|
||||
username: ownerUser!,
|
||||
role: 'owner',
|
||||
banned: false,
|
||||
}];
|
||||
adminConfig.UserConfig.Users = [
|
||||
{
|
||||
username: ownerUser!,
|
||||
role: 'owner',
|
||||
banned: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 采集源去重
|
||||
const seenSourceKeys = new Set<string>();
|
||||
@@ -602,13 +633,15 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
|
||||
// 自定义分类去重
|
||||
const seenCustomCategoryKeys = new Set<string>();
|
||||
adminConfig.CustomCategories = adminConfig.CustomCategories.filter((category) => {
|
||||
if (seenCustomCategoryKeys.has(category.query + category.type)) {
|
||||
return false;
|
||||
adminConfig.CustomCategories = adminConfig.CustomCategories.filter(
|
||||
(category) => {
|
||||
if (seenCustomCategoryKeys.has(category.query + category.type)) {
|
||||
return false;
|
||||
}
|
||||
seenCustomCategoryKeys.add(category.query + category.type);
|
||||
return true;
|
||||
}
|
||||
seenCustomCategoryKeys.add(category.query + category.type);
|
||||
return true;
|
||||
});
|
||||
);
|
||||
|
||||
// 直播源去重
|
||||
const seenLiveKeys = new Set<string>();
|
||||
@@ -627,42 +660,52 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
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,
|
||||
}],
|
||||
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;
|
||||
adminConfig.EmbyConfig.Sources = adminConfig.EmbyConfig.Sources.filter(
|
||||
(source) => {
|
||||
if (seenEmbyKeys.has(source.key)) {
|
||||
return false;
|
||||
}
|
||||
seenEmbyKeys.add(source.key);
|
||||
return true;
|
||||
}
|
||||
seenEmbyKeys.add(source.key);
|
||||
return true;
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!adminConfig.SuwayomiConfig) {
|
||||
adminConfig.SuwayomiConfig = {
|
||||
Enabled: process.env.SUWAYOMI_ENABLED === 'true',
|
||||
ServerURL: process.env.SUWAYOMI_URL || process.env.NEXT_PUBLIC_SUWAYOMI_URL || '',
|
||||
AuthMode: (process.env.SUWAYOMI_AUTH_MODE as 'none' | 'basic_auth' | 'simple_login' | undefined) || 'none',
|
||||
ServerURL:
|
||||
process.env.SUWAYOMI_URL || process.env.NEXT_PUBLIC_SUWAYOMI_URL || '',
|
||||
AuthMode:
|
||||
(process.env.SUWAYOMI_AUTH_MODE as
|
||||
| 'none'
|
||||
| 'basic_auth'
|
||||
| 'simple_login'
|
||||
| undefined) || 'none',
|
||||
Username: process.env.SUWAYOMI_USERNAME || '',
|
||||
Password: process.env.SUWAYOMI_PASSWORD || '',
|
||||
DefaultLang: process.env.SUWAYOMI_DEFAULT_LANG || 'zh',
|
||||
@@ -694,7 +737,10 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (!Array.isArray(adminConfig.SuwayomiConfig.SourceIds)) {
|
||||
adminConfig.SuwayomiConfig.SourceIds = [];
|
||||
}
|
||||
if (adminConfig.SuwayomiConfig.MaxSources === undefined || Number.isNaN(adminConfig.SuwayomiConfig.MaxSources)) {
|
||||
if (
|
||||
adminConfig.SuwayomiConfig.MaxSources === undefined ||
|
||||
Number.isNaN(adminConfig.SuwayomiConfig.MaxSources)
|
||||
) {
|
||||
adminConfig.SuwayomiConfig.MaxSources = 10;
|
||||
}
|
||||
|
||||
@@ -715,19 +761,26 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
const envUrl = process.env.OPDS_URL || process.env.NEXT_PUBLIC_OPDS_URL;
|
||||
if (!envUrl) return [];
|
||||
|
||||
return [{
|
||||
id: 'default',
|
||||
name: process.env.OPDS_NAME || '默认书源',
|
||||
type: 'opds',
|
||||
url: envUrl,
|
||||
enabled: true,
|
||||
authMode: (process.env.OPDS_AUTH_MODE as 'none' | 'basic' | 'header' | undefined) || 'none',
|
||||
username: process.env.OPDS_USERNAME || '',
|
||||
password: process.env.OPDS_PASSWORD || '',
|
||||
headerName: process.env.OPDS_HEADER_NAME || '',
|
||||
headerValue: process.env.OPDS_HEADER_VALUE || '',
|
||||
searchTemplate: process.env.OPDS_SEARCH_TEMPLATE || '',
|
||||
}];
|
||||
return [
|
||||
{
|
||||
id: 'default',
|
||||
name: process.env.OPDS_NAME || '默认书源',
|
||||
type: 'opds',
|
||||
url: envUrl,
|
||||
enabled: true,
|
||||
authMode:
|
||||
(process.env.OPDS_AUTH_MODE as
|
||||
| 'none'
|
||||
| 'basic'
|
||||
| 'header'
|
||||
| undefined) || 'none',
|
||||
username: process.env.OPDS_USERNAME || '',
|
||||
password: process.env.OPDS_PASSWORD || '',
|
||||
headerName: process.env.OPDS_HEADER_NAME || '',
|
||||
headerValue: process.env.OPDS_HEADER_VALUE || '',
|
||||
searchTemplate: process.env.OPDS_SEARCH_TEMPLATE || '',
|
||||
},
|
||||
];
|
||||
})(),
|
||||
CacheTTL: Number(process.env.OPDS_CACHE_TTL_MS || 10 * 60 * 1000),
|
||||
};
|
||||
@@ -738,15 +791,22 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (!Array.isArray(adminConfig.OPDSConfig.Sources)) {
|
||||
adminConfig.OPDSConfig.Sources = [];
|
||||
}
|
||||
adminConfig.OPDSConfig.Sources = adminConfig.OPDSConfig.Sources.filter((source: any) => (source?.type || 'opds') === 'opds').map((source: any) => {
|
||||
adminConfig.OPDSConfig.Sources = adminConfig.OPDSConfig.Sources.filter(
|
||||
(source: any) => (source?.type || 'opds') === 'opds'
|
||||
).map((source: any) => {
|
||||
const { legado: _legado, ...rest } = source || {};
|
||||
return { ...rest, type: 'opds' };
|
||||
});
|
||||
if (!Array.isArray(adminConfig.OPDSConfig.LegadoSubscriptions)) {
|
||||
adminConfig.OPDSConfig.LegadoSubscriptions = [];
|
||||
}
|
||||
if (adminConfig.OPDSConfig.CacheTTL === undefined || Number.isNaN(adminConfig.OPDSConfig.CacheTTL)) {
|
||||
adminConfig.OPDSConfig.CacheTTL = Number(process.env.OPDS_CACHE_TTL_MS || 10 * 60 * 1000);
|
||||
if (
|
||||
adminConfig.OPDSConfig.CacheTTL === undefined ||
|
||||
Number.isNaN(adminConfig.OPDSConfig.CacheTTL)
|
||||
) {
|
||||
adminConfig.OPDSConfig.CacheTTL = Number(
|
||||
process.env.OPDS_CACHE_TTL_MS || 10 * 60 * 1000
|
||||
);
|
||||
}
|
||||
|
||||
if (!adminConfig.NetDiskConfig) {
|
||||
@@ -888,7 +948,10 @@ export async function resetConfig() {
|
||||
if (!originConfig) {
|
||||
originConfig = {} as AdminConfig;
|
||||
}
|
||||
const adminConfig = await getInitConfig(originConfig.ConfigFile, originConfig.ConfigSubscribtion);
|
||||
const adminConfig = await getInitConfig(
|
||||
originConfig.ConfigFile,
|
||||
originConfig.ConfigSubscribtion
|
||||
);
|
||||
cachedConfig = adminConfig;
|
||||
await db.saveAdminConfig(adminConfig);
|
||||
|
||||
@@ -923,13 +986,15 @@ export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
||||
// 优先根据用户自己的 enabledApis 配置查找
|
||||
if (userInfoV2.enabledApis && userInfoV2.enabledApis.length > 0) {
|
||||
const userApiSitesSet = new Set(userInfoV2.enabledApis);
|
||||
return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
proxyMode: s.proxyMode,
|
||||
}));
|
||||
return allApiSites
|
||||
.filter((s) => userApiSitesSet.has(s.key))
|
||||
.map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
proxyMode: s.proxyMode,
|
||||
}));
|
||||
}
|
||||
|
||||
// 如果没有 enabledApis 配置,则根据 tags 查找
|
||||
@@ -937,21 +1002,25 @@ export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
||||
const enabledApisFromTags = new Set<string>();
|
||||
|
||||
// 遍历用户的所有 tags,收集对应的 enabledApis
|
||||
userInfoV2.tags.forEach(tagName => {
|
||||
const tagConfig = config.UserConfig.Tags?.find(t => t.name === tagName);
|
||||
userInfoV2.tags.forEach((tagName) => {
|
||||
const tagConfig = config.UserConfig.Tags?.find((t) => t.name === tagName);
|
||||
if (tagConfig && tagConfig.enabledApis) {
|
||||
tagConfig.enabledApis.forEach(apiKey => enabledApisFromTags.add(apiKey));
|
||||
tagConfig.enabledApis.forEach((apiKey) =>
|
||||
enabledApisFromTags.add(apiKey)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (enabledApisFromTags.size > 0) {
|
||||
return allApiSites.filter((s) => enabledApisFromTags.has(s.key)).map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
proxyMode: s.proxyMode,
|
||||
}));
|
||||
return allApiSites
|
||||
.filter((s) => enabledApisFromTags.has(s.key))
|
||||
.map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
proxyMode: s.proxyMode,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user