mirror of https://github.com/djteang/OrangeTV.git
532 lines
16 KiB
TypeScript
532 lines
16 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion */
|
||
|
||
import { db } from '@/lib/db';
|
||
|
||
import { AdminConfig } from './admin.types';
|
||
|
||
export interface ApiSite {
|
||
key: string;
|
||
api: string;
|
||
name: string;
|
||
detail?: string;
|
||
}
|
||
|
||
export interface LiveCfg {
|
||
name: string;
|
||
url: string;
|
||
ua?: string;
|
||
epg?: string; // 节目单
|
||
}
|
||
|
||
interface ConfigFileStruct {
|
||
cache_time?: number;
|
||
api_site?: {
|
||
[key: string]: ApiSite;
|
||
};
|
||
custom_category?: {
|
||
name?: string;
|
||
type: 'movie' | 'tv';
|
||
query: string;
|
||
}[];
|
||
lives?: {
|
||
[key: string]: LiveCfg;
|
||
}
|
||
}
|
||
|
||
export const API_CONFIG = {
|
||
search: {
|
||
path: '?ac=videolist&wd=',
|
||
pagePath: '?ac=videolist&wd={query}&pg={page}',
|
||
headers: {
|
||
'User-Agent':
|
||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||
Accept: 'application/json',
|
||
},
|
||
},
|
||
detail: {
|
||
path: '?ac=videolist&ids=',
|
||
headers: {
|
||
'User-Agent':
|
||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||
Accept: 'application/json',
|
||
},
|
||
},
|
||
shortdrama: {
|
||
baseUrl: 'https://api.r2afosne.dpdns.org',
|
||
headers: {
|
||
'User-Agent':
|
||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||
Accept: 'application/json',
|
||
},
|
||
},
|
||
};
|
||
|
||
// 在模块加载时根据环境决定配置来源
|
||
let cachedConfig: AdminConfig;
|
||
|
||
|
||
// 从配置文件补充管理员配置
|
||
export function refineConfig(adminConfig: AdminConfig): AdminConfig {
|
||
let fileConfig: ConfigFileStruct;
|
||
try {
|
||
fileConfig = JSON.parse(adminConfig.ConfigFile) as ConfigFileStruct;
|
||
} catch (e) {
|
||
fileConfig = {} as ConfigFileStruct;
|
||
}
|
||
|
||
// 合并文件中的源信息
|
||
const apiSitesFromFile = Object.entries(fileConfig.api_site || []);
|
||
const currentApiSites = new Map(
|
||
(adminConfig.SourceConfig || []).map((s) => [s.key, s])
|
||
);
|
||
|
||
// 用于跟踪已存在的API地址,避免重复
|
||
const existingApiUrls = new Set(
|
||
Array.from(currentApiSites.values()).map(s => s.api.toLowerCase().trim())
|
||
);
|
||
|
||
apiSitesFromFile.forEach(([key, site]) => {
|
||
const existingSource = currentApiSites.get(key);
|
||
const normalizedApiUrl = site.api.toLowerCase().trim();
|
||
|
||
if (existingSource) {
|
||
// 如果已存在,只覆盖 name、api、detail 和 from
|
||
existingSource.name = site.name;
|
||
existingSource.api = site.api;
|
||
existingSource.detail = site.detail;
|
||
existingSource.from = 'config';
|
||
// 更新API地址记录
|
||
existingApiUrls.add(normalizedApiUrl);
|
||
} else {
|
||
// 检查API地址是否已存在
|
||
if (existingApiUrls.has(normalizedApiUrl)) {
|
||
console.warn(`跳过重复的API地址: ${site.api} (key: ${key})`);
|
||
return; // 跳过重复的API地址
|
||
}
|
||
|
||
// 如果不存在,创建新条目
|
||
currentApiSites.set(key, {
|
||
key,
|
||
name: site.name,
|
||
api: site.api,
|
||
detail: site.detail,
|
||
from: 'config',
|
||
disabled: false,
|
||
});
|
||
existingApiUrls.add(normalizedApiUrl);
|
||
}
|
||
});
|
||
|
||
// 检查现有源是否在 fileConfig.api_site 中,如果不在则标记为 custom
|
||
const apiSitesFromFileKey = new Set(apiSitesFromFile.map(([key]) => key));
|
||
currentApiSites.forEach((source) => {
|
||
if (!apiSitesFromFileKey.has(source.key)) {
|
||
source.from = 'custom';
|
||
}
|
||
});
|
||
|
||
// 将 Map 转换回数组
|
||
adminConfig.SourceConfig = Array.from(currentApiSites.values());
|
||
|
||
// 覆盖 CustomCategories
|
||
const customCategoriesFromFile = fileConfig.custom_category || [];
|
||
const currentCustomCategories = new Map(
|
||
(adminConfig.CustomCategories || []).map((c) => [c.query + c.type, c])
|
||
);
|
||
|
||
customCategoriesFromFile.forEach((category) => {
|
||
const key = category.query + category.type;
|
||
const existedCategory = currentCustomCategories.get(key);
|
||
if (existedCategory) {
|
||
existedCategory.name = category.name;
|
||
existedCategory.query = category.query;
|
||
existedCategory.type = category.type;
|
||
existedCategory.from = 'config';
|
||
} else {
|
||
currentCustomCategories.set(key, {
|
||
name: category.name,
|
||
type: category.type,
|
||
query: category.query,
|
||
from: 'config',
|
||
disabled: false,
|
||
});
|
||
}
|
||
});
|
||
|
||
// 检查现有 CustomCategories 是否在 fileConfig.custom_category 中,如果不在则标记为 custom
|
||
const customCategoriesFromFileKeys = new Set(
|
||
customCategoriesFromFile.map((c) => c.query + c.type)
|
||
);
|
||
currentCustomCategories.forEach((category) => {
|
||
if (!customCategoriesFromFileKeys.has(category.query + category.type)) {
|
||
category.from = 'custom';
|
||
}
|
||
});
|
||
|
||
// 将 Map 转换回数组
|
||
adminConfig.CustomCategories = Array.from(currentCustomCategories.values());
|
||
|
||
const livesFromFile = Object.entries(fileConfig.lives || []);
|
||
const currentLives = new Map(
|
||
(adminConfig.LiveConfig || []).map((l) => [l.key, l])
|
||
);
|
||
livesFromFile.forEach(([key, site]) => {
|
||
const existingLive = currentLives.get(key);
|
||
if (existingLive) {
|
||
existingLive.name = site.name;
|
||
existingLive.url = site.url;
|
||
existingLive.ua = site.ua;
|
||
existingLive.epg = site.epg;
|
||
} else {
|
||
// 如果不存在,创建新条目
|
||
currentLives.set(key, {
|
||
key,
|
||
name: site.name,
|
||
url: site.url,
|
||
ua: site.ua,
|
||
epg: site.epg,
|
||
channelNumber: 0,
|
||
from: 'config',
|
||
disabled: false,
|
||
});
|
||
}
|
||
});
|
||
|
||
// 检查现有 LiveConfig 是否在 fileConfig.lives 中,如果不在则标记为 custom
|
||
const livesFromFileKeys = new Set(livesFromFile.map(([key]) => key));
|
||
currentLives.forEach((live) => {
|
||
if (!livesFromFileKeys.has(live.key)) {
|
||
live.from = 'custom';
|
||
}
|
||
});
|
||
|
||
// 将 Map 转换回数组
|
||
adminConfig.LiveConfig = Array.from(currentLives.values());
|
||
|
||
return adminConfig;
|
||
}
|
||
|
||
async function getInitConfig(configFile: string, subConfig: {
|
||
URL: string;
|
||
AutoUpdate: boolean;
|
||
LastCheck: string;
|
||
} = {
|
||
URL: "",
|
||
AutoUpdate: false,
|
||
LastCheck: "",
|
||
}): Promise<AdminConfig> {
|
||
let cfgFile: ConfigFileStruct;
|
||
try {
|
||
cfgFile = JSON.parse(configFile) as ConfigFileStruct;
|
||
} catch (e) {
|
||
cfgFile = {} as ConfigFileStruct;
|
||
}
|
||
const adminConfig: AdminConfig = {
|
||
ConfigFile: configFile,
|
||
ConfigSubscribtion: subConfig,
|
||
SiteConfig: {
|
||
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'OrangeTV',
|
||
Announcement:
|
||
process.env.ANNOUNCEMENT ||
|
||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||
SearchDownstreamMaxPage:
|
||
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
|
||
SiteInterfaceCacheTime: cfgFile.cache_time || 7200,
|
||
DoubanProxyType:
|
||
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',
|
||
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',
|
||
RequireDeviceCode:
|
||
process.env.NEXT_PUBLIC_REQUIRE_DEVICE_CODE !== 'false',
|
||
},
|
||
UserConfig: {
|
||
Users: [],
|
||
},
|
||
SourceConfig: [],
|
||
CustomCategories: [],
|
||
LiveConfig: [],
|
||
};
|
||
|
||
// 补充用户信息
|
||
let userNames: string[] = [];
|
||
try {
|
||
userNames = await db.getAllUsers();
|
||
} catch (e) {
|
||
console.error('获取用户列表失败:', e);
|
||
}
|
||
const allUsers = userNames.filter((u) => u !== process.env.USERNAME).map((u) => ({
|
||
username: u,
|
||
role: 'user',
|
||
banned: false,
|
||
}));
|
||
allUsers.unshift({
|
||
username: process.env.USERNAME!,
|
||
role: 'owner',
|
||
banned: false,
|
||
});
|
||
adminConfig.UserConfig.Users = allUsers as any;
|
||
|
||
// 从配置文件中补充源信息
|
||
Object.entries(cfgFile.api_site || []).forEach(([key, site]) => {
|
||
adminConfig.SourceConfig.push({
|
||
key: key,
|
||
name: site.name,
|
||
api: site.api,
|
||
detail: site.detail,
|
||
from: 'config',
|
||
disabled: false,
|
||
});
|
||
});
|
||
|
||
// 从配置文件中补充自定义分类信息
|
||
cfgFile.custom_category?.forEach((category) => {
|
||
adminConfig.CustomCategories.push({
|
||
name: category.name || category.query,
|
||
type: category.type,
|
||
query: category.query,
|
||
from: 'config',
|
||
disabled: false,
|
||
});
|
||
});
|
||
|
||
// 从配置文件中补充直播源信息
|
||
Object.entries(cfgFile.lives || []).forEach(([key, live]) => {
|
||
if (!adminConfig.LiveConfig) {
|
||
adminConfig.LiveConfig = [];
|
||
}
|
||
adminConfig.LiveConfig.push({
|
||
key,
|
||
name: live.name,
|
||
url: live.url,
|
||
ua: live.ua,
|
||
epg: live.epg,
|
||
channelNumber: 0,
|
||
from: 'config',
|
||
disabled: false,
|
||
});
|
||
});
|
||
|
||
return adminConfig;
|
||
}
|
||
|
||
export async function getConfig(): Promise<AdminConfig> {
|
||
// 直接使用内存缓存
|
||
if (cachedConfig) {
|
||
return cachedConfig;
|
||
}
|
||
|
||
// 读 db
|
||
let adminConfig: AdminConfig | null = null;
|
||
try {
|
||
adminConfig = await db.getAdminConfig();
|
||
} catch (e) {
|
||
console.error('获取管理员配置失败:', e);
|
||
}
|
||
|
||
// db 中无配置,执行一次初始化
|
||
if (!adminConfig) {
|
||
adminConfig = await getInitConfig("");
|
||
}
|
||
adminConfig = configSelfCheck(adminConfig);
|
||
cachedConfig = adminConfig;
|
||
db.saveAdminConfig(cachedConfig);
|
||
return cachedConfig;
|
||
}
|
||
|
||
export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||
// 确保必要的属性存在和初始化
|
||
if (!adminConfig.UserConfig) {
|
||
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)) {
|
||
adminConfig.CustomCategories = [];
|
||
}
|
||
if (!adminConfig.LiveConfig || !Array.isArray(adminConfig.LiveConfig)) {
|
||
adminConfig.LiveConfig = [];
|
||
}
|
||
|
||
// 确保 SiteConfig 及其属性存在
|
||
if (!adminConfig.SiteConfig) {
|
||
adminConfig.SiteConfig = {
|
||
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'OrangeTV',
|
||
Announcement: process.env.ANNOUNCEMENT || '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
|
||
SearchDownstreamMaxPage: Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
|
||
SiteInterfaceCacheTime: 7200,
|
||
DoubanProxyType: 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',
|
||
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',
|
||
RequireDeviceCode: process.env.NEXT_PUBLIC_REQUIRE_DEVICE_CODE !== 'false',
|
||
};
|
||
}
|
||
|
||
// 确保 RequireDeviceCode 属性存在
|
||
if (adminConfig.SiteConfig.RequireDeviceCode === undefined) {
|
||
adminConfig.SiteConfig.RequireDeviceCode = process.env.NEXT_PUBLIC_REQUIRE_DEVICE_CODE !== 'false';
|
||
}
|
||
|
||
// 确保 ThemeConfig 存在
|
||
if (!adminConfig.ThemeConfig) {
|
||
adminConfig.ThemeConfig = {
|
||
defaultTheme: 'default',
|
||
customCSS: '',
|
||
allowUserCustomization: true,
|
||
};
|
||
}
|
||
|
||
// 站长变更自检
|
||
const ownerUser = process.env.USERNAME;
|
||
|
||
// 去重
|
||
const seenUsernames = new Set<string>();
|
||
adminConfig.UserConfig.Users = adminConfig.UserConfig.Users.filter((user) => {
|
||
if (seenUsernames.has(user.username)) {
|
||
return false;
|
||
}
|
||
seenUsernames.add(user.username);
|
||
return true;
|
||
});
|
||
// 过滤站长
|
||
const originOwnerCfg = adminConfig.UserConfig.Users.find((u) => u.username === ownerUser);
|
||
adminConfig.UserConfig.Users = adminConfig.UserConfig.Users.filter((user) => user.username !== ownerUser);
|
||
// 其他用户不得拥有 owner 权限
|
||
adminConfig.UserConfig.Users.forEach((user) => {
|
||
if (user.role === 'owner') {
|
||
user.role = 'user';
|
||
}
|
||
});
|
||
// 重新添加回站长
|
||
adminConfig.UserConfig.Users.unshift({
|
||
username: ownerUser!,
|
||
role: 'owner',
|
||
banned: false,
|
||
enabledApis: originOwnerCfg?.enabledApis || undefined,
|
||
tags: originOwnerCfg?.tags || undefined,
|
||
});
|
||
|
||
// 采集源去重
|
||
const seenSourceKeys = new Set<string>();
|
||
adminConfig.SourceConfig = adminConfig.SourceConfig.filter((source) => {
|
||
if (seenSourceKeys.has(source.key)) {
|
||
return false;
|
||
}
|
||
seenSourceKeys.add(source.key);
|
||
return true;
|
||
});
|
||
|
||
// 自定义分类去重
|
||
const seenCustomCategoryKeys = new Set<string>();
|
||
adminConfig.CustomCategories = adminConfig.CustomCategories.filter((category) => {
|
||
if (seenCustomCategoryKeys.has(category.query + category.type)) {
|
||
return false;
|
||
}
|
||
seenCustomCategoryKeys.add(category.query + category.type);
|
||
return true;
|
||
});
|
||
|
||
// 直播源去重
|
||
const seenLiveKeys = new Set<string>();
|
||
adminConfig.LiveConfig = adminConfig.LiveConfig.filter((live) => {
|
||
if (seenLiveKeys.has(live.key)) {
|
||
return false;
|
||
}
|
||
seenLiveKeys.add(live.key);
|
||
return true;
|
||
});
|
||
|
||
return adminConfig;
|
||
}
|
||
|
||
export async function resetConfig() {
|
||
let originConfig: AdminConfig | null = null;
|
||
try {
|
||
originConfig = await db.getAdminConfig();
|
||
} catch (e) {
|
||
console.error('获取管理员配置失败:', e);
|
||
}
|
||
if (!originConfig) {
|
||
originConfig = {} as AdminConfig;
|
||
}
|
||
const adminConfig = await getInitConfig(originConfig.ConfigFile, originConfig.ConfigSubscribtion);
|
||
cachedConfig = adminConfig;
|
||
await db.saveAdminConfig(adminConfig);
|
||
|
||
return;
|
||
}
|
||
|
||
export async function getCacheTime(): Promise<number> {
|
||
const config = await getConfig();
|
||
return config.SiteConfig.SiteInterfaceCacheTime || 7200;
|
||
}
|
||
|
||
export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
||
const config = await getConfig();
|
||
const allApiSites = config.SourceConfig.filter((s) => !s.disabled);
|
||
|
||
if (!user) {
|
||
return allApiSites;
|
||
}
|
||
|
||
const userConfig = config.UserConfig.Users.find((u) => u.username === user);
|
||
if (!userConfig) {
|
||
return allApiSites;
|
||
}
|
||
|
||
// 优先根据用户自己的 enabledApis 配置查找
|
||
if (userConfig.enabledApis && userConfig.enabledApis.length > 0) {
|
||
const userApiSitesSet = new Set(userConfig.enabledApis);
|
||
return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({
|
||
key: s.key,
|
||
name: s.name,
|
||
api: s.api,
|
||
detail: s.detail,
|
||
}));
|
||
}
|
||
|
||
// 如果没有 enabledApis 配置,则根据 tags 查找
|
||
if (userConfig.tags && userConfig.tags.length > 0 && config.UserConfig.Tags) {
|
||
const enabledApisFromTags = new Set<string>();
|
||
|
||
// 遍历用户的所有 tags,收集对应的 enabledApis
|
||
userConfig.tags.forEach(tagName => {
|
||
const tagConfig = config.UserConfig.Tags?.find(t => t.name === tagName);
|
||
if (tagConfig && tagConfig.enabledApis) {
|
||
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,
|
||
}));
|
||
}
|
||
}
|
||
|
||
// 如果都没有配置,返回所有可用的 API 站点
|
||
return allApiSites;
|
||
}
|
||
|
||
export async function setCachedConfig(config: AdminConfig) {
|
||
cachedConfig = config;
|
||
}
|
||
|
||
export function clearCachedConfig() {
|
||
cachedConfig = undefined as any;
|
||
} |