feat: 添加成人内容过滤功能

- 新增用户设置系统支持内容过滤开关
- 扩展类型定义支持成人内容标记
- 实现用户设置API端点(GET/PATCH/PUT)
- 增强搜索API支持内容分组和过滤
- 创建AdultContentFilter UI组件
- 添加用户设置页面和认证检查
- 更新配置示例和README文档
- 实现LocalStorage和Redis存储后端
- 默认启用过滤确保安全性
This commit is contained in:
katelya
2025-09-04 21:11:02 +08:00
parent c9429efba6
commit 86ebbb2cf6
12 changed files with 658 additions and 17 deletions
+1
View File
@@ -22,6 +22,7 @@ export interface AdminConfig {
detail?: string;
from: 'config' | 'custom';
disabled?: boolean;
is_adult?: boolean; // 新增:是否为成人内容资源站
}[];
}
+23 -2
View File
@@ -378,9 +378,30 @@ export async function getCacheTime(): Promise<number> {
return config.SiteConfig.SiteInterfaceCacheTime || 7200;
}
export async function getAvailableApiSites(): Promise<ApiSite[]> {
export async function getAvailableApiSites(filterAdult = false): Promise<ApiSite[]> {
const config = await getConfig();
return config.SourceConfig.filter((s) => !s.disabled).map((s) => ({
let sites = config.SourceConfig.filter((s) => !s.disabled);
// 如果需要过滤成人内容,则排除标记为成人内容的资源站
if (filterAdult) {
sites = sites.filter((s) => !s.is_adult);
}
return sites.map((s) => ({
key: s.key,
name: s.name,
api: s.api,
detail: s.detail,
}));
}
// 获取成人内容资源站
export async function getAdultApiSites(): Promise<ApiSite[]> {
const config = await getConfig();
const adultSites = config.SourceConfig
.filter((s) => !s.disabled && s.is_adult);
return adultSites.map((s) => ({
key: s.key,
name: s.name,
api: s.api,
+52 -2
View File
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { AdminConfig } from './admin.types';
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types';
/**
* LocalStorage 存储实现
@@ -290,6 +290,56 @@ export class LocalStorage implements IStorage {
}
}
// ---------- 用户设置 ----------
async getUserSettings(userName: string): Promise<UserSettings | null> {
if (typeof window === 'undefined') return null;
try {
const storageKey = this.getStorageKey('settings', userName);
const data = localStorage.getItem(storageKey);
if (data) {
return JSON.parse(data);
}
// 如果用户设置不存在,返回默认设置
const defaultSettings: UserSettings = {
filter_adult_content: true, // 默认开启成人内容过滤
theme: 'auto',
language: 'zh-CN',
auto_play: true,
video_quality: 'auto'
};
return defaultSettings;
} catch (error) {
console.error('Error getting user settings:', error);
return null;
}
}
async setUserSettings(userName: string, settings: UserSettings): Promise<void> {
if (typeof window === 'undefined') return;
try {
const storageKey = this.getStorageKey('settings', userName);
localStorage.setItem(storageKey, JSON.stringify(settings));
} catch (error) {
console.error('Error setting user settings:', error);
}
}
async updateUserSettings(userName: string, settings: Partial<UserSettings>): Promise<void> {
if (typeof window === 'undefined') return;
try {
const currentSettings = await this.getUserSettings(userName);
const updatedSettings = { ...currentSettings, ...settings };
await this.setUserSettings(userName, updatedSettings as UserSettings);
} catch (error) {
console.error('Error updating user settings:', error);
}
}
// ---------- 管理员功能 ----------
async getAllUsers(): Promise<string[]> {
if (typeof window === 'undefined') return [];
@@ -365,7 +415,7 @@ export class LocalStorage implements IStorage {
localStorage.removeItem(userKey);
// 删除用户相关的所有数据
const prefixes = ['playrecord', 'favorite', 'searchhistory', 'skipconfig'];
const prefixes = ['playrecord', 'favorite', 'searchhistory', 'skipconfig', 'settings'];
for (const prefix of prefixes) {
const dataPrefix = this.getStorageKey(prefix, userName);
+45 -1
View File
@@ -3,7 +3,7 @@
import { createClient, RedisClientType } from 'redis';
import { AdminConfig } from './admin.types';
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types';
// 搜索历史最大条数
const SEARCH_HISTORY_LIMIT = 20;
@@ -223,6 +223,50 @@ export class RedisStorage implements IStorage {
if (favoriteKeys.length > 0) {
await withRetry(() => this.client.del(favoriteKeys));
}
// 删除用户设置
await withRetry(() => this.client.del(this.userSettingsKey(userName)));
}
// ---------- 用户设置 ----------
private userSettingsKey(user: string) {
return `u:${user}:settings`; // u:username:settings
}
async getUserSettings(userName: string): Promise<UserSettings | null> {
const data = await withRetry(() =>
this.client.get(this.userSettingsKey(userName))
);
if (data) {
return JSON.parse(ensureString(data));
}
// 如果用户设置不存在,返回默认设置
const defaultSettings: UserSettings = {
filter_adult_content: true, // 默认开启成人内容过滤
theme: 'auto',
language: 'zh-CN',
auto_play: true,
video_quality: 'auto'
};
return defaultSettings;
}
async setUserSettings(userName: string, settings: UserSettings): Promise<void> {
await withRetry(() =>
this.client.set(
this.userSettingsKey(userName),
JSON.stringify(settings)
)
);
}
async updateUserSettings(userName: string, settings: Partial<UserSettings>): Promise<void> {
const currentSettings = await this.getUserSettings(userName);
const updatedSettings = { ...currentSettings, ...settings };
await this.setUserSettings(userName, updatedSettings as UserSettings);
}
// ---------- 搜索历史 ----------
+37
View File
@@ -70,6 +70,11 @@ export interface IStorage {
// 删除用户(包括密码、搜索历史、播放记录、收藏夹)
deleteUser(userName: string): Promise<void>;
// 用户设置相关
getUserSettings(userName: string): Promise<UserSettings | null>;
setUserSettings(userName: string, settings: UserSettings): Promise<void>;
updateUserSettings(userName: string, settings: Partial<UserSettings>): Promise<void>;
// 搜索历史相关
getSearchHistory(userName: string): Promise<string[]>;
addSearchHistory(userName: string, keyword: string): Promise<void>;
@@ -119,6 +124,38 @@ export interface DoubanResult {
list: DoubanItem[];
}
// 资源站配置
export interface ApiSite {
api: string;
name: string;
detail?: string;
type?: number;
playMode?: 'parse' | 'direct';
is_adult?: boolean; // 新增:是否为成人内容资源站
}
// 配置文件结构
export interface Config {
cache_time: number;
api_site: { [key: string]: ApiSite };
}
// 用户设置
export interface UserSettings {
filter_adult_content: boolean; // 是否过滤成人内容,默认为 true
theme: 'light' | 'dark' | 'auto';
language: string;
auto_play: boolean;
video_quality: string;
[key: string]: string | boolean | number; // 允许其他设置
}
// 搜索结果(支持成人内容分组)
export interface GroupedSearchResults {
regular_results: SearchResult[];
adult_results?: SearchResult[];
}
// Runtime配置类型
export interface RuntimeConfig {
STORAGE_TYPE?: string;
+1 -1
View File
@@ -3,7 +3,7 @@
import { Redis } from '@upstash/redis';
import { AdminConfig } from './admin.types';
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types';
// 搜索历史最大条数
const SEARCH_HISTORY_LIMIT = 20;