动漫订阅增加排除关键词

This commit is contained in:
mtvpls
2026-07-17 09:56:33 +08:00
parent 9f63e18550
commit bd779b237c
6 changed files with 59 additions and 5 deletions
@@ -40,6 +40,10 @@ export async function PUT(
if (updates.filterText !== undefined) { if (updates.filterText !== undefined) {
subscription.filterText = updates.filterText.trim(); subscription.filterText = updates.filterText.trim();
} }
if (updates.excludeText !== undefined) {
subscription.excludeText =
typeof updates.excludeText === 'string' ? updates.excludeText.trim() : '';
}
if (updates.source !== undefined) { if (updates.source !== undefined) {
if (!['acgrip', 'mikan', 'dmhy', 'nyaa'].includes(updates.source)) { if (!['acgrip', 'mikan', 'dmhy', 'nyaa'].includes(updates.source)) {
return NextResponse.json({ error: '无效的搜索源' }, { status: 400 }); return NextResponse.json({ error: '无效的搜索源' }, { status: 400 });
@@ -52,7 +52,7 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: '无权限访问' }, { status: 403 }); return NextResponse.json({ error: '无权限访问' }, { status: 403 });
} }
const { title, filterText, source, enabled, lastEpisode } = const { title, filterText, excludeText, source, enabled, lastEpisode } =
await req.json(); await req.json();
// 验证必填字段 // 验证必填字段
@@ -93,6 +93,7 @@ export async function POST(req: NextRequest) {
id: crypto.randomUUID(), id: crypto.randomUUID(),
title: title.trim(), title: title.trim(),
filterText: filterText.trim(), filterText: filterText.trim(),
excludeText: typeof excludeText === 'string' ? excludeText.trim() : '',
source, source,
enabled: enabled ?? true, enabled: enabled ?? true,
lastCheckTime: 0, lastCheckTime: 0,
+24 -1
View File
@@ -179,6 +179,7 @@ export default function AnimeSubscriptionComponent({
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
title: '', title: '',
filterText: '', filterText: '',
excludeText: '',
source: 'mikan' as 'acgrip' | 'mikan' | 'dmhy' | 'nyaa', source: 'mikan' as 'acgrip' | 'mikan' | 'dmhy' | 'nyaa',
lastEpisode: 0, lastEpisode: 0,
enabled: true, enabled: true,
@@ -198,6 +199,7 @@ export default function AnimeSubscriptionComponent({
setFormData({ setFormData({
title: '', title: '',
filterText: '', filterText: '',
excludeText: '',
source: 'mikan', source: 'mikan',
lastEpisode: 0, lastEpisode: 0,
enabled: true, enabled: true,
@@ -273,6 +275,7 @@ export default function AnimeSubscriptionComponent({
setFormData({ setFormData({
title: sub.title, title: sub.title,
filterText: sub.filterText, filterText: sub.filterText,
excludeText: sub.excludeText || '',
source: sub.source, source: sub.source,
lastEpisode: sub.lastEpisode, lastEpisode: sub.lastEpisode,
enabled: sub.enabled, enabled: sub.enabled,
@@ -489,6 +492,7 @@ export default function AnimeSubscriptionComponent({
<p> </p> <p> </p>
<p> OpenList离线下载根目录//</p> <p> OpenList离线下载根目录//</p>
<p> </p> <p> </p>
<p> ,,PV</p>
<p> </p> <p> </p>
</div> </div>
</div> </div>
@@ -534,10 +538,25 @@ export default function AnimeSubscriptionComponent({
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500' className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500'
/> />
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
</p> </p>
</div> </div>
</div> </div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
</label>
<input
type='text'
value={formData.excludeText}
onChange={(e) => setFormData({ ...formData, excludeText: e.target.value })}
placeholder='先行版,预告,PV'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
</p>
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'> <div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
@@ -625,6 +644,7 @@ export default function AnimeSubscriptionComponent({
</div> </div>
<div className='text-sm text-gray-600 dark:text-gray-400 space-y-1'> <div className='text-sm text-gray-600 dark:text-gray-400 space-y-1'>
<p>{sub.filterText}</p> <p>{sub.filterText}</p>
{sub.excludeText ? <p>{sub.excludeText}</p> : null}
<p> {sub.lastEpisode} </p> <p> {sub.lastEpisode} </p>
<p>{formatTime(sub.lastCheckTime)}</p> <p>{formatTime(sub.lastCheckTime)}</p>
</div> </div>
@@ -687,6 +707,9 @@ export default function AnimeSubscriptionComponent({
</div> </div>
<div className='text-sm text-gray-600 dark:text-gray-400 space-y-1'> <div className='text-sm text-gray-600 dark:text-gray-400 space-y-1'>
<p className='break-all'>{sub.filterText}</p> <p className='break-all'>{sub.filterText}</p>
{sub.excludeText ? (
<p className='break-all'>{sub.excludeText}</p>
) : null}
<p> {sub.lastEpisode} · {formatTime(sub.lastCheckTime)}</p> <p> {sub.lastEpisode} · {formatTime(sub.lastCheckTime)}</p>
</div> </div>
<div className='flex items-center gap-2 pt-1'> <div className='flex items-center gap-2 pt-1'>
+1
View File
@@ -382,6 +382,7 @@ export interface AdminConfig {
id: string; id: string;
title: string; title: string;
filterText: string; filterText: string;
excludeText?: string; // 排除关键词,逗号分隔;标题包含任一则跳过
source: 'acgrip' | 'mikan' | 'dmhy' | 'nyaa'; source: 'acgrip' | 'mikan' | 'dmhy' | 'nyaa';
enabled: boolean; enabled: boolean;
lastCheckTime: number; lastCheckTime: number;
+26 -3
View File
@@ -58,17 +58,39 @@ export function extractEpisode(title: string): number | null {
} }
/** /**
* 检查标题是否匹配过滤条件 * 解析逗号分隔关键词(兼容中文逗号)
*/
function parseKeywords(text: string): string[] {
return text
.replace(//g, ',')
.split(',')
.map((k) => k.trim())
.filter(Boolean);
}
/**
* 检查标题是否匹配过滤条件(包含关键词,AND:必须全部命中)
*/ */
export function matchesFilter(title: string, filterText: string): boolean { export function matchesFilter(title: string, filterText: string): boolean {
if (!filterText) return true; if (!filterText) return true;
// 支持多个关键词,用逗号分隔,必须全部匹配 // 支持多个关键词,用逗号分隔,必须全部匹配
const keywords = filterText.split(',').map((k) => k.trim()).filter(Boolean); const keywords = parseKeywords(filterText);
return keywords.every((keyword) => title.includes(keyword)); return keywords.every((keyword) => title.includes(keyword));
} }
/**
* 检查标题是否命中排除关键词(OR:任一命中即排除)
*/
export function matchesExclude(title: string, excludeText?: string): boolean {
if (!excludeText) return false;
const keywords = parseKeywords(excludeText);
return keywords.some((keyword) => title.includes(keyword));
}
/** /**
* 搜索 ACG 资源(直接调用搜索逻辑,不通过 HTTP) * 搜索 ACG 资源(直接调用搜索逻辑,不通过 HTTP)
*/ */
@@ -318,9 +340,10 @@ export async function checkSubscription(subscription: AnimeSubscription) {
// 1. 搜索资源 // 1. 搜索资源
const results = await searchACG(subscription.title, subscription.source); const results = await searchACG(subscription.title, subscription.source);
// 2. 过滤并解析集数 // 2. 过滤并解析集数(包含关键词 AND,排除关键词 OR)
const newEpisodes = results const newEpisodes = results
.filter((item: any) => matchesFilter(item.title, subscription.filterText)) .filter((item: any) => matchesFilter(item.title, subscription.filterText))
.filter((item: any) => !matchesExclude(item.title, subscription.excludeText))
.map((item: any) => ({ .map((item: any) => ({
episode: extractEpisode(item.title), episode: extractEpisode(item.title),
...item, ...item,
+2
View File
@@ -2,6 +2,8 @@ export interface AnimeSubscription {
id: string; id: string;
title: string; title: string;
filterText: string; filterText: string;
/** 排除关键词,逗号分隔;标题包含任一关键词则跳过,例如:先行版,预告 */
excludeText?: string;
source: 'acgrip' | 'mikan' | 'dmhy' | 'nyaa'; source: 'acgrip' | 'mikan' | 'dmhy' | 'nyaa';
enabled: boolean; enabled: boolean;
lastCheckTime: number; lastCheckTime: number;