增加弹幕过滤功能

This commit is contained in:
mtvpls
2025-12-03 21:34:05 +08:00
parent 98d36986fc
commit 820fc972df
8 changed files with 668 additions and 7 deletions
+101
View File
@@ -0,0 +1,101 @@
/* eslint-disable no-console */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';
import { DanmakuFilterConfig } from '@/lib/types';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
try {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: '未登录' }, { status: 401 });
}
const config = await getConfig();
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
// 获取弹幕过滤配置
const filterConfig = await db.getDanmakuFilterConfig(authInfo.username);
// 如果没有配置,返回默认值
if (!filterConfig) {
return NextResponse.json({ rules: [] });
}
return NextResponse.json(filterConfig);
} catch (error) {
console.error('获取弹幕过滤配置失败:', error);
return NextResponse.json(
{ error: '获取弹幕过滤配置失败' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: '未登录' }, { status: 401 });
}
const adminConfig = await getConfig();
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = adminConfig.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
const body = await request.json();
const config: DanmakuFilterConfig = body;
if (!config || !Array.isArray(config.rules)) {
return NextResponse.json({ error: '配置格式错误' }, { status: 400 });
}
// 验证每个规则的格式
const validatedRules = config.rules.map((rule) => ({
keyword: String(rule.keyword || ''),
type: (rule.type === 'regex' || rule.type === 'normal') ? rule.type : 'normal',
enabled: Boolean(rule.enabled),
id: rule.id || undefined,
}));
const validatedConfig: DanmakuFilterConfig = {
rules: validatedRules,
};
await db.setDanmakuFilterConfig(authInfo.username, validatedConfig);
return NextResponse.json({ success: true });
} catch (error) {
console.error('保存弹幕过滤配置失败:', error);
return NextResponse.json(
{ error: '保存弹幕过滤配置失败' },
{ status: 500 }
);
}
}
+67 -5
View File
@@ -19,6 +19,7 @@ import {
savePlayRecord,
saveSkipConfig,
subscribeToDataUpdates,
getDanmakuFilterConfig,
} from '@/lib/db.client';
import {
convertDanmakuFormat,
@@ -31,12 +32,13 @@ import {
searchAnime,
} from '@/lib/danmaku/api';
import type { DanmakuAnime, DanmakuSelection, DanmakuSettings } from '@/lib/danmaku/types';
import { SearchResult } from '@/lib/types';
import { SearchResult, DanmakuFilterConfig } from '@/lib/types';
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
import EpisodeSelector from '@/components/EpisodeSelector';
import PageLayout from '@/components/PageLayout';
import DoubanComments from '@/components/DoubanComments';
import DanmakuFilterSettings from '@/components/DanmakuFilterSettings';
import { useEnableComments } from '@/hooks/useEnableComments';
// 扩展 HTMLVideoElement 类型以支持 hls 属性
@@ -168,6 +170,8 @@ function PlayPageClient() {
const [danmakuSettings, setDanmakuSettings] = useState<DanmakuSettings>(
loadDanmakuSettings()
);
const [danmakuFilterConfig, setDanmakuFilterConfig] = useState<DanmakuFilterConfig | null>(null);
const danmakuFilterConfigRef = useRef<DanmakuFilterConfig | null>(null);
const [currentDanmakuSelection, setCurrentDanmakuSelection] =
useState<DanmakuSelection | null>(null);
const [danmakuEpisodesList, setDanmakuEpisodesList] = useState<
@@ -181,11 +185,38 @@ function PlayPageClient() {
// 多条弹幕匹配结果
const [danmakuMatches, setDanmakuMatches] = useState<DanmakuAnime[]>([]);
const [showDanmakuSourceSelector, setShowDanmakuSourceSelector] = useState(false);
const [showDanmakuFilterSettings, setShowDanmakuFilterSettings] = useState(false);
useEffect(() => {
danmakuSettingsRef.current = danmakuSettings;
}, [danmakuSettings]);
// 加载弹幕过滤配置
useEffect(() => {
const loadFilterConfig = async () => {
try {
const config = await getDanmakuFilterConfig();
if (config) {
setDanmakuFilterConfig(config);
danmakuFilterConfigRef.current = config;
} else {
// 如果没有配置,设置默认空配置
const defaultConfig: DanmakuFilterConfig = { rules: [] };
setDanmakuFilterConfig(defaultConfig);
danmakuFilterConfigRef.current = defaultConfig;
}
} catch (error) {
console.error('加载弹幕过滤配置失败:', error);
}
};
loadFilterConfig();
}, []);
// 同步弹幕过滤配置到ref
useEffect(() => {
danmakuFilterConfigRef.current = danmakuFilterConfig;
}, [danmakuFilterConfig]);
// 视频基本信息
const [videoTitle, setVideoTitle] = useState(searchParams.get('title') || '');
const [videoYear, setVideoYear] = useState(searchParams.get('year') || '');
@@ -2098,11 +2129,23 @@ function PlayPageClient() {
theme: 'dark',
filter: (danmu: any) => {
// 应用过滤规则
if (danmakuSettingsRef.current.filterRules.length > 0) {
for (const rule of danmakuSettingsRef.current.filterRules) {
const filterConfig = danmakuFilterConfigRef.current;
if (filterConfig && filterConfig.rules.length > 0) {
for (const rule of filterConfig.rules) {
// 跳过未启用的规则
if (!rule.enabled) continue;
try {
if (new RegExp(rule).test(danmu.text)) {
return false;
if (rule.type === 'normal') {
// 普通模式:字符串包含匹配
if (danmu.text.includes(rule.keyword)) {
return false;
}
} else if (rule.type === 'regex') {
// 正则模式:正则表达式匹配
if (new RegExp(rule.keyword).test(danmu.text)) {
return false;
}
}
} catch (e) {
console.error('弹幕过滤规则错误:', e);
@@ -2144,6 +2187,15 @@ function PlayPageClient() {
return newVal ? '当前开启' : '当前关闭';
},
},
{
html: '弹幕过滤',
icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" fill="#ffffff"/><path d="M8 12h8" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/></svg>',
tooltip: '配置弹幕过滤规则',
onClick() {
setShowDanmakuFilterSettings(true);
return '打开设置';
},
},
...(webGPUSupported ? [
{
name: 'Anime4K超分',
@@ -3344,6 +3396,16 @@ function PlayPageClient() {
</div>
)}
</div>
{/* 弹幕过滤设置对话框 */}
<DanmakuFilterSettings
isOpen={showDanmakuFilterSettings}
onClose={() => setShowDanmakuFilterSettings(false)}
onConfigUpdate={(config) => {
setDanmakuFilterConfig(config);
danmakuFilterConfigRef.current = config;
}}
/>
</PageLayout>
);
}