diff --git a/src/app/api/admin/anime-subscription/[id]/route.ts b/src/app/api/admin/anime-subscription/[id]/route.ts index 0c2a500..4ee810c 100644 --- a/src/app/api/admin/anime-subscription/[id]/route.ts +++ b/src/app/api/admin/anime-subscription/[id]/route.ts @@ -40,6 +40,10 @@ export async function PUT( if (updates.filterText !== undefined) { subscription.filterText = updates.filterText.trim(); } + if (updates.excludeText !== undefined) { + subscription.excludeText = + typeof updates.excludeText === 'string' ? updates.excludeText.trim() : ''; + } if (updates.source !== undefined) { if (!['acgrip', 'mikan', 'dmhy', 'nyaa'].includes(updates.source)) { return NextResponse.json({ error: '无效的搜索源' }, { status: 400 }); diff --git a/src/app/api/admin/anime-subscription/route.ts b/src/app/api/admin/anime-subscription/route.ts index 2c7138f..61777a2 100644 --- a/src/app/api/admin/anime-subscription/route.ts +++ b/src/app/api/admin/anime-subscription/route.ts @@ -52,7 +52,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: '无权限访问' }, { status: 403 }); } - const { title, filterText, source, enabled, lastEpisode } = + const { title, filterText, excludeText, source, enabled, lastEpisode } = await req.json(); // 验证必填字段 @@ -93,6 +93,7 @@ export async function POST(req: NextRequest) { id: crypto.randomUUID(), title: title.trim(), filterText: filterText.trim(), + excludeText: typeof excludeText === 'string' ? excludeText.trim() : '', source, enabled: enabled ?? true, lastCheckTime: 0, diff --git a/src/components/AnimeSubscriptionComponent.tsx b/src/components/AnimeSubscriptionComponent.tsx index 910b41e..3dc9685 100644 --- a/src/components/AnimeSubscriptionComponent.tsx +++ b/src/components/AnimeSubscriptionComponent.tsx @@ -179,6 +179,7 @@ export default function AnimeSubscriptionComponent({ const [formData, setFormData] = useState({ title: '', filterText: '', + excludeText: '', source: 'mikan' as 'acgrip' | 'mikan' | 'dmhy' | 'nyaa', lastEpisode: 0, enabled: true, @@ -198,6 +199,7 @@ export default function AnimeSubscriptionComponent({ setFormData({ title: '', filterText: '', + excludeText: '', source: 'mikan', lastEpisode: 0, enabled: true, @@ -273,6 +275,7 @@ export default function AnimeSubscriptionComponent({ setFormData({ title: sub.title, filterText: sub.filterText, + excludeText: sub.excludeText || '', source: sub.source, lastEpisode: sub.lastEpisode, enabled: sub.enabled, @@ -489,6 +492,7 @@ export default function AnimeSubscriptionComponent({

• 定时任务会自动检查订阅更新

• 下载路径:OpenList离线下载根目录/番剧名称/

• 过滤关键词支持多个,用逗号分隔,只会下载包含这些关键字的资源,可以用来过滤字幕组或是字幕种类

+

• 排除关键词支持多个,用逗号分隔,标题包含任一关键词则跳过,例如:先行版,预告,PV

• 当前集数:已看到第几集,只下载更新的集数

@@ -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' />

- 多个关键词用逗号分隔 + 多个关键词用逗号分隔,需全部包含

+
+ + 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' + /> +

+ 可选;多个关键词用逗号分隔,标题包含任一则跳过 +

+

过滤条件:{sub.filterText}

+ {sub.excludeText ?

排除条件:{sub.excludeText}

: null}

当前集数:第 {sub.lastEpisode} 集

上次检查:{formatTime(sub.lastCheckTime)}

@@ -687,6 +707,9 @@ export default function AnimeSubscriptionComponent({

过滤:{sub.filterText}

+ {sub.excludeText ? ( +

排除:{sub.excludeText}

+ ) : null}

集数:第 {sub.lastEpisode} 集 · {formatTime(sub.lastCheckTime)}

diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index 230359f..513c24f 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -382,6 +382,7 @@ export interface AdminConfig { id: string; title: string; filterText: string; + excludeText?: string; // 排除关键词,逗号分隔;标题包含任一则跳过 source: 'acgrip' | 'mikan' | 'dmhy' | 'nyaa'; enabled: boolean; lastCheckTime: number; diff --git a/src/lib/anime-subscription.ts b/src/lib/anime-subscription.ts index 0e74e0b..fc7e5b2 100644 --- a/src/lib/anime-subscription.ts +++ b/src/lib/anime-subscription.ts @@ -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 { 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)); } +/** + * 检查标题是否命中排除关键词(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) */ @@ -318,9 +340,10 @@ export async function checkSubscription(subscription: AnimeSubscription) { // 1. 搜索资源 const results = await searchACG(subscription.title, subscription.source); - // 2. 过滤并解析集数 + // 2. 过滤并解析集数(包含关键词 AND,排除关键词 OR) const newEpisodes = results .filter((item: any) => matchesFilter(item.title, subscription.filterText)) + .filter((item: any) => !matchesExclude(item.title, subscription.excludeText)) .map((item: any) => ({ episode: extractEpisode(item.title), ...item, diff --git a/src/types/anime-subscription.ts b/src/types/anime-subscription.ts index b124ab0..7a0059c 100644 --- a/src/types/anime-subscription.ts +++ b/src/types/anime-subscription.ts @@ -2,6 +2,8 @@ export interface AnimeSubscription { id: string; title: string; filterText: string; + /** 排除关键词,逗号分隔;标题包含任一关键词则跳过,例如:先行版,预告 */ + excludeText?: string; source: 'acgrip' | 'mikan' | 'dmhy' | 'nyaa'; enabled: boolean; lastCheckTime: number;