增加弹幕过滤功能
This commit is contained in:
+147
-1
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { getAuthInfoFromBrowserCookie } from './auth';
|
||||
import { SkipConfig } from './types';
|
||||
import { SkipConfig, DanmakuFilterConfig } from './types';
|
||||
|
||||
// 全局错误触发函数
|
||||
function triggerGlobalError(message: string) {
|
||||
@@ -66,6 +66,7 @@ interface UserCacheStore {
|
||||
favorites?: CacheData<Record<string, Favorite>>;
|
||||
searchHistory?: CacheData<string[]>;
|
||||
skipConfigs?: CacheData<Record<string, SkipConfig>>;
|
||||
danmakuFilterConfig?: CacheData<DanmakuFilterConfig>;
|
||||
}
|
||||
|
||||
// ---- 常量 ----
|
||||
@@ -340,6 +341,32 @@ class HybridCacheManager {
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹幕过滤配置缓存方法
|
||||
*/
|
||||
getCachedDanmakuFilterConfig(): DanmakuFilterConfig | null {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return null;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
const cached = userCache.danmakuFilterConfig;
|
||||
|
||||
if (cached && this.isCacheValid(cached)) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
cacheDanmakuFilterConfig(data: DanmakuFilterConfig): void {
|
||||
const username = this.getCurrentUsername();
|
||||
if (!username) return;
|
||||
|
||||
const userCache = this.getUserCache(username);
|
||||
userCache.danmakuFilterConfig = this.createCacheData(data);
|
||||
this.saveUserCache(username, userCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定用户的所有缓存
|
||||
*/
|
||||
@@ -1656,3 +1683,122 @@ export async function deleteSkipConfig(
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 弹幕过滤配置相关 API ----------------
|
||||
|
||||
/**
|
||||
* 获取弹幕过滤配置。
|
||||
* 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。
|
||||
*/
|
||||
export async function getDanmakuFilterConfig(): Promise<DanmakuFilterConfig | null> {
|
||||
// 服务器端渲染阶段直接返回空
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 数据库存储模式:使用混合缓存策略(包括 redis 和 upstash)
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
// 优先从缓存获取数据
|
||||
const cachedData = cacheManager.getCachedDanmakuFilterConfig();
|
||||
|
||||
if (cachedData) {
|
||||
// 返回缓存数据,同时后台异步更新
|
||||
fetchFromApi<DanmakuFilterConfig>(`/api/danmaku-filter`)
|
||||
.then((freshData) => {
|
||||
// 只有数据真正不同时才更新缓存
|
||||
if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) {
|
||||
cacheManager.cacheDanmakuFilterConfig(freshData);
|
||||
// 触发数据更新事件
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('danmakuFilterConfigUpdated', {
|
||||
detail: freshData,
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('后台同步弹幕过滤配置失败:', err);
|
||||
});
|
||||
|
||||
return cachedData;
|
||||
} else {
|
||||
// 缓存为空,直接从 API 获取并缓存
|
||||
try {
|
||||
const freshData = await fetchFromApi<DanmakuFilterConfig>(
|
||||
`/api/danmaku-filter`
|
||||
);
|
||||
cacheManager.cacheDanmakuFilterConfig(freshData);
|
||||
return freshData;
|
||||
} catch (err) {
|
||||
console.error('获取弹幕过滤配置失败:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// localStorage 模式
|
||||
try {
|
||||
const raw = localStorage.getItem('moontv_danmaku_filter_config');
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as DanmakuFilterConfig;
|
||||
} catch (err) {
|
||||
console.error('读取弹幕过滤配置失败:', err);
|
||||
triggerGlobalError('读取弹幕过滤配置失败');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存弹幕过滤配置。
|
||||
* 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。
|
||||
*/
|
||||
export async function saveDanmakuFilterConfig(
|
||||
config: DanmakuFilterConfig
|
||||
): Promise<void> {
|
||||
// 数据库存储模式:乐观更新策略(包括 redis 和 upstash)
|
||||
if (STORAGE_TYPE !== 'localstorage') {
|
||||
// 立即更新缓存
|
||||
cacheManager.cacheDanmakuFilterConfig(config);
|
||||
|
||||
// 触发立即更新事件
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('danmakuFilterConfigUpdated', {
|
||||
detail: config,
|
||||
})
|
||||
);
|
||||
|
||||
// 异步同步到数据库
|
||||
try {
|
||||
await fetchWithAuth('/api/danmaku-filter', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('保存弹幕过滤配置失败:', err);
|
||||
triggerGlobalError('保存弹幕过滤配置失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// localStorage 模式
|
||||
if (typeof window === 'undefined') {
|
||||
console.warn('无法在服务端保存弹幕过滤配置到 localStorage');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem('moontv_danmaku_filter_config', JSON.stringify(config));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('danmakuFilterConfigUpdated', {
|
||||
detail: config,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('保存弹幕过滤配置失败:', err);
|
||||
triggerGlobalError('保存弹幕过滤配置失败');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-1
@@ -3,7 +3,7 @@
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { KvrocksStorage } from './kvrocks.db';
|
||||
import { RedisStorage } from './redis.db';
|
||||
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
||||
import { Favorite, IStorage, PlayRecord, SkipConfig, DanmakuFilterConfig } from './types';
|
||||
import { UpstashRedisStorage } from './upstash.db';
|
||||
|
||||
// storage type 常量: 'localstorage' | 'redis' | 'upstash',默认 'localstorage'
|
||||
@@ -231,6 +231,29 @@ export class DbManager {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ---------- 弹幕过滤配置 ----------
|
||||
async getDanmakuFilterConfig(userName: string): Promise<DanmakuFilterConfig | null> {
|
||||
if (typeof (this.storage as any).getDanmakuFilterConfig === 'function') {
|
||||
return (this.storage as any).getDanmakuFilterConfig(userName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async setDanmakuFilterConfig(
|
||||
userName: string,
|
||||
config: DanmakuFilterConfig
|
||||
): Promise<void> {
|
||||
if (typeof (this.storage as any).setDanmakuFilterConfig === 'function') {
|
||||
await (this.storage as any).setDanmakuFilterConfig(userName, config);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDanmakuFilterConfig(userName: string): Promise<void> {
|
||||
if (typeof (this.storage as any).deleteDanmakuFilterConfig === 'function') {
|
||||
await (this.storage as any).deleteDanmakuFilterConfig(userName);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 数据清理 ----------
|
||||
async clearAllData(): Promise<void> {
|
||||
if (typeof (this.storage as any).clearAllData === 'function') {
|
||||
|
||||
@@ -378,6 +378,10 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
return `u:${user}:skip:${source}+${id}`;
|
||||
}
|
||||
|
||||
private danmakuFilterConfigKey(user: string) {
|
||||
return `u:${user}:danmaku_filter`;
|
||||
}
|
||||
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
@@ -443,6 +447,34 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
return configs;
|
||||
}
|
||||
|
||||
// ---------- 弹幕过滤配置 ----------
|
||||
async getDanmakuFilterConfig(
|
||||
userName: string
|
||||
): Promise<import('./types').DanmakuFilterConfig | null> {
|
||||
const val = await this.withRetry(() =>
|
||||
this.client.get(this.danmakuFilterConfigKey(userName))
|
||||
);
|
||||
return val ? (JSON.parse(val) as import('./types').DanmakuFilterConfig) : null;
|
||||
}
|
||||
|
||||
async setDanmakuFilterConfig(
|
||||
userName: string,
|
||||
config: import('./types').DanmakuFilterConfig
|
||||
): Promise<void> {
|
||||
await this.withRetry(() =>
|
||||
this.client.set(
|
||||
this.danmakuFilterConfigKey(userName),
|
||||
JSON.stringify(config)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async deleteDanmakuFilterConfig(userName: string): Promise<void> {
|
||||
await this.withRetry(() =>
|
||||
this.client.del(this.danmakuFilterConfigKey(userName))
|
||||
);
|
||||
}
|
||||
|
||||
// 清空所有数据
|
||||
async clearAllData(): Promise<void> {
|
||||
try {
|
||||
|
||||
@@ -81,6 +81,14 @@ export interface IStorage {
|
||||
deleteSkipConfig(userName: string, source: string, id: string): Promise<void>;
|
||||
getAllSkipConfigs(userName: string): Promise<{ [key: string]: SkipConfig }>;
|
||||
|
||||
// 弹幕过滤配置相关
|
||||
getDanmakuFilterConfig(userName: string): Promise<DanmakuFilterConfig | null>;
|
||||
setDanmakuFilterConfig(
|
||||
userName: string,
|
||||
config: DanmakuFilterConfig
|
||||
): Promise<void>;
|
||||
deleteDanmakuFilterConfig(userName: string): Promise<void>;
|
||||
|
||||
// 数据清理相关
|
||||
clearAllData(): Promise<void>;
|
||||
}
|
||||
@@ -122,3 +130,16 @@ export interface SkipConfig {
|
||||
intro_time: number; // 片头时间(秒)
|
||||
outro_time: number; // 片尾时间(秒)
|
||||
}
|
||||
|
||||
// 弹幕过滤规则数据结构
|
||||
export interface DanmakuFilterRule {
|
||||
keyword: string; // 关键字
|
||||
type: 'normal' | 'regex'; // 普通模式或正则模式
|
||||
enabled: boolean; // 是否启用
|
||||
id?: string; // 规则ID(用于前端管理)
|
||||
}
|
||||
|
||||
// 弹幕过滤配置数据结构
|
||||
export interface DanmakuFilterConfig {
|
||||
rules: DanmakuFilterRule[]; // 过滤规则列表
|
||||
}
|
||||
|
||||
@@ -282,6 +282,10 @@ export class UpstashRedisStorage implements IStorage {
|
||||
return `u:${user}:skip:${source}+${id}`;
|
||||
}
|
||||
|
||||
private danmakuFilterConfigKey(user: string) {
|
||||
return `u:${user}:danmaku_filter`;
|
||||
}
|
||||
|
||||
async getSkipConfig(
|
||||
userName: string,
|
||||
source: string,
|
||||
@@ -344,6 +348,31 @@ export class UpstashRedisStorage implements IStorage {
|
||||
return configs;
|
||||
}
|
||||
|
||||
// ---------- 弹幕过滤配置 ----------
|
||||
async getDanmakuFilterConfig(
|
||||
userName: string
|
||||
): Promise<import('./types').DanmakuFilterConfig | null> {
|
||||
const val = await withRetry(() =>
|
||||
this.client.get(this.danmakuFilterConfigKey(userName))
|
||||
);
|
||||
return val ? (val as import('./types').DanmakuFilterConfig) : null;
|
||||
}
|
||||
|
||||
async setDanmakuFilterConfig(
|
||||
userName: string,
|
||||
config: import('./types').DanmakuFilterConfig
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(this.danmakuFilterConfigKey(userName), config)
|
||||
);
|
||||
}
|
||||
|
||||
async deleteDanmakuFilterConfig(userName: string): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.del(this.danmakuFilterConfigKey(userName))
|
||||
);
|
||||
}
|
||||
|
||||
// 清空所有数据
|
||||
async clearAllData(): Promise<void> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user