动漫订阅增加排除关键词
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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'>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
Reference in New Issue
Block a user