feat: implement custom config

This commit is contained in:
shinya
2025-07-31 01:07:59 +08:00
parent fc24055bdc
commit 7b6867ed58
13 changed files with 967 additions and 49 deletions

View File

@@ -23,6 +23,13 @@ export interface AdminConfig {
from: 'config' | 'custom';
disabled?: boolean;
}[];
CustomCategories: {
name?: string;
type: 'movie' | 'tv';
query: string;
from: 'config' | 'custom';
disabled?: boolean;
}[];
}
export interface AdminConfigResult {

View File

@@ -17,6 +17,11 @@ interface ConfigFileStruct {
api_site: {
[key: string]: ApiSite;
};
custom_category?: {
name?: string;
type: 'movie' | 'tv';
query: string;
}[];
}
export const API_CONFIG = {
@@ -86,6 +91,7 @@ async function initConfig() {
// 从文件中获取源信息,用于补全源
const apiSiteEntries = Object.entries(fileConfig.api_site);
const customCategories = fileConfig.custom_category || [];
if (adminConfig) {
// 补全 SourceConfig
@@ -113,6 +119,37 @@ async function initConfig() {
}
});
// 确保 CustomCategories 被初始化
if (!adminConfig.CustomCategories) {
adminConfig.CustomCategories = [];
}
// 补全 CustomCategories
const existedCustomCategories = new Set(
adminConfig.CustomCategories.map((c) => c.query + c.type)
);
customCategories.forEach((category) => {
if (!existedCustomCategories.has(category.query + category.type)) {
adminConfig!.CustomCategories.push({
name: category.name,
type: category.type,
query: category.query,
from: 'config',
disabled: false,
});
}
});
// 检查现有 CustomCategories 是否在 fileConfig.custom_category 中,如果不在则标记为 custom
const customCategoriesKeys = new Set(
customCategories.map((c) => c.query + c.type)
);
adminConfig.CustomCategories.forEach((category) => {
if (!customCategoriesKeys.has(category.query + category.type)) {
category.from = 'custom';
}
});
const existedUsers = new Set(
(adminConfig.UserConfig.Users || []).map((u) => u.username)
);
@@ -173,6 +210,13 @@ async function initConfig() {
from: 'config',
disabled: false,
})),
CustomCategories: customCategories.map((category) => ({
name: category.name,
type: category.type,
query: category.query,
from: 'config',
disabled: false,
})),
};
}
@@ -212,6 +256,14 @@ async function initConfig() {
from: 'config',
disabled: false,
})),
CustomCategories:
fileConfig.custom_category?.map((category) => ({
name: category.name,
type: category.type,
query: category.query,
from: 'config',
disabled: false,
})) || [],
} as AdminConfig;
}
}
@@ -229,6 +281,11 @@ export async function getConfig(): Promise<AdminConfig> {
adminConfig = await (storage as any).getAdminConfig();
}
if (adminConfig) {
// 确保 CustomCategories 被初始化
if (!adminConfig.CustomCategories) {
adminConfig.CustomCategories = [];
}
// 合并一些环境变量配置
adminConfig.SiteConfig.SiteName = process.env.SITE_NAME || 'MoonTV';
adminConfig.SiteConfig.Announcement =
@@ -266,6 +323,33 @@ export async function getConfig(): Promise<AdminConfig> {
}
});
// 补全 CustomCategories
const customCategories = fileConfig.custom_category || [];
const existedCustomCategories = new Set(
adminConfig.CustomCategories.map((c) => c.query + c.type)
);
customCategories.forEach((category) => {
if (!existedCustomCategories.has(category.query + category.type)) {
adminConfig!.CustomCategories.push({
name: category.name,
type: category.type,
query: category.query,
from: 'config',
disabled: false,
});
}
});
// 检查现有 CustomCategories 是否在 fileConfig.custom_categories 中,如果不在则标记为 custom
const customCategoriesKeys = new Set(
customCategories.map((c) => c.query + c.type)
);
adminConfig.CustomCategories.forEach((category) => {
if (!customCategoriesKeys.has(category.query + category.type)) {
category.from = 'custom';
}
});
const ownerUser = process.env.USERNAME || '';
// 检查配置中的站长用户是否和 USERNAME 匹配,如果不匹配则降级为普通用户
let containOwner = false;
@@ -295,6 +379,7 @@ export async function getConfig(): Promise<AdminConfig> {
}
export async function resetConfig() {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
const storage = getStorage();
// 获取所有用户名,用于补全 Users
let userNames: string[] = [];
@@ -323,6 +408,7 @@ export async function resetConfig() {
// 从文件中获取源信息,用于补全源
const apiSiteEntries = Object.entries(fileConfig.api_site);
const customCategories = fileConfig.custom_category || [];
let allUsers = userNames.map((uname) => ({
username: uname,
role: 'user',
@@ -359,6 +445,16 @@ export async function resetConfig() {
from: 'config',
disabled: false,
})),
CustomCategories:
storageType === 'redis'
? customCategories?.map((category) => ({
name: category.name,
type: category.type,
query: category.query,
from: 'config',
disabled: false,
})) || []
: [],
} as AdminConfig;
if (storage && typeof (storage as any).setAdminConfig === 'function') {
@@ -371,6 +467,7 @@ export async function resetConfig() {
cachedConfig.SiteConfig = adminConfig.SiteConfig;
cachedConfig.UserConfig = adminConfig.UserConfig;
cachedConfig.SourceConfig = adminConfig.SourceConfig;
cachedConfig.CustomCategories = adminConfig.CustomCategories;
}
export async function getCacheTime(): Promise<number> {

View File

@@ -146,3 +146,82 @@ export async function getDoubanCategories(
return response.json();
}
}
interface DoubanListParams {
tag: string;
type: string;
pageLimit?: number;
pageStart?: number;
}
export async function getDoubanList(
params: DoubanListParams
): Promise<DoubanResult> {
const { tag, type, pageLimit = 20, pageStart = 0 } = params;
if (shouldUseDoubanClient()) {
// 使用客户端代理获取(当设置了代理 URL 时)
return fetchDoubanList(params);
} else {
const response = await fetch(
`/api/douban?tag=${tag}&type=${type}&limit=${pageLimit}&start=${pageStart}`
);
if (!response.ok) {
throw new Error('获取豆瓣列表数据失败');
}
return response.json();
}
}
export async function fetchDoubanList(
params: DoubanListParams
): Promise<DoubanResult> {
const { tag, type, pageLimit = 20, pageStart = 0 } = params;
// 验证参数
if (!tag || !type) {
throw new Error('tag 和 type 参数不能为空');
}
if (!['tv', 'movie'].includes(type)) {
throw new Error('type 参数必须是 tv 或 movie');
}
if (pageLimit < 1 || pageLimit > 100) {
throw new Error('pageLimit 必须在 1-100 之间');
}
if (pageStart < 0) {
throw new Error('pageStart 不能小于 0');
}
const target = `https://movie.douban.com/j/search_subjects?type=${type}&tag=${tag}&sort=recommend&page_limit=${pageLimit}&page_start=${pageStart}`;
try {
const response = await fetchWithTimeout(target);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const doubanData: DoubanCategoryApiResponse = await response.json();
// 转换数据格式
const list: DoubanItem[] = doubanData.items.map((item) => ({
id: item.id,
title: item.title,
poster: item.pic?.normal || item.pic?.large || '',
rate: item.rating?.value ? item.rating.value.toFixed(1) : '',
year: item.card_subtitle?.match(/(\d{4})/)?.[1] || '',
}));
return {
code: 200,
message: '获取成功',
list: list,
};
} catch (error) {
throw new Error(`获取豆瓣分类数据失败: ${(error as Error).message}`);
}
}

View File

@@ -2,7 +2,7 @@
'use client';
const CURRENT_VERSION = '20250730221204';
const CURRENT_VERSION = '20250731010759';
// 版本检查结果枚举
export enum UpdateStatus {