feat: 添加成人内容过滤功能
- 新增用户设置系统支持内容过滤开关 - 扩展类型定义支持成人内容标记 - 实现用户设置API端点(GET/PATCH/PUT) - 增强搜索API支持内容分组和过滤 - 创建AdultContentFilter UI组件 - 添加用户设置页面和认证检查 - 更新配置示例和README文档 - 实现LocalStorage和Redis存储后端 - 默认启用过滤确保安全性
This commit is contained in:
@@ -22,6 +22,7 @@ export interface AdminConfig {
|
||||
detail?: string;
|
||||
from: 'config' | 'custom';
|
||||
disabled?: boolean;
|
||||
is_adult?: boolean; // 新增:是否为成人内容资源站
|
||||
}[];
|
||||
}
|
||||
|
||||
|
||||
+23
-2
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user