私人影库增加追番订阅功能

This commit is contained in:
mtvpls
2026-02-18 17:20:37 +08:00
parent e4a76b3f9d
commit 96b08dc0bd
11 changed files with 1455 additions and 1 deletions

View File

@@ -25,6 +25,7 @@ import {
AlertCircle,
AlertTriangle,
Bot,
Cat,
Check,
CheckCircle,
ChevronDown,
@@ -50,6 +51,7 @@ import { createPortal } from 'react-dom';
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
import AnimeSubscriptionComponent from '@/components/AnimeSubscriptionComponent';
import CorrectDialog from '@/components/CorrectDialog';
import DataMigration from '@/components/DataMigration';
import PageLayout from '@/components/PageLayout';
@@ -11597,6 +11599,7 @@ function AdminPageClient() {
openListConfig: false,
embyConfig: false,
xiaoyaConfig: false,
animeSubscription: false,
aiConfig: false,
liveSource: false,
webLive: false,
@@ -12049,6 +12052,18 @@ function AdminPageClient() {
>
<MovieRequestsComponent config={config} refreshConfig={fetchConfig} />
</CollapsibleTab>
{/* 追番订阅子标签 */}
<CollapsibleTab
title='追番订阅'
icon={
<Cat size={20} className='text-gray-600 dark:text-gray-400' />
}
isExpanded={expandedTabs.animeSubscription}
onToggle={() => toggleTab('animeSubscription')}
>
<AnimeSubscriptionComponent config={config} refreshConfig={fetchConfig} />
</CollapsibleTab>
</div>
</CollapsibleTab>

View File

@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server';
import { checkSubscription } from '@/lib/anime-subscription';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';
export const runtime = 'nodejs';
/**
* POST /api/admin/anime-subscription/[id]/check
* 手动触发检查单个订阅
*/
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// 权限检查
const authInfo = getAuthInfoFromCookie(req);
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
return NextResponse.json({ error: '无权限访问' }, { status: 403 });
}
const config = await getConfig();
const subscriptions = config.AnimeSubscriptionConfig?.Subscriptions || [];
const subscription = subscriptions.find((sub) => sub.id === params.id);
if (!subscription) {
return NextResponse.json({ error: '订阅不存在' }, { status: 404 });
}
// 执行检查逻辑(忽略时间间隔限制)
const result = await checkSubscription(subscription);
// 保存配置
await db.saveAdminConfig(config);
return NextResponse.json({
success: true,
...result,
});
} catch (error: any) {
console.error('检查追番订阅失败:', error);
return NextResponse.json(
{ error: error.message || '检查失败' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,112 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';
export const runtime = 'nodejs';
/**
* PUT /api/admin/anime-subscription/[id]
* 更新订阅
*/
export async function PUT(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// 权限检查
const authInfo = getAuthInfoFromCookie(req);
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
return NextResponse.json({ error: '无权限访问' }, { status: 403 });
}
const config = await getConfig();
const subscriptions = config.AnimeSubscriptionConfig?.Subscriptions || [];
const index = subscriptions.findIndex((sub) => sub.id === params.id);
if (index === -1) {
return NextResponse.json({ error: '订阅不存在' }, { status: 404 });
}
const updates = await req.json();
const subscription = subscriptions[index];
// 更新字段
if (updates.title !== undefined) {
subscription.title = updates.title.trim();
}
if (updates.filterText !== undefined) {
subscription.filterText = updates.filterText.trim();
}
if (updates.source !== undefined) {
if (!['acgrip', 'mikan', 'dmhy'].includes(updates.source)) {
return NextResponse.json({ error: '无效的搜索源' }, { status: 400 });
}
subscription.source = updates.source;
}
if (updates.enabled !== undefined) {
subscription.enabled = updates.enabled;
}
if (updates.lastEpisode !== undefined) {
// 验证集数为非负整数
const episode = parseInt(String(updates.lastEpisode), 10);
if (isNaN(episode) || episode < 0) {
return NextResponse.json(
{ error: '集数必须是非负整数' },
{ status: 400 }
);
}
subscription.lastEpisode = episode;
}
subscription.updatedAt = Date.now();
await db.saveAdminConfig(config);
return NextResponse.json(subscription);
} catch (error: any) {
console.error('更新追番订阅失败:', error);
return NextResponse.json(
{ error: error.message || '更新订阅失败' },
{ status: 500 }
);
}
}
/**
* DELETE /api/admin/anime-subscription/[id]
* 删除订阅
*/
export async function DELETE(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// 权限检查
const authInfo = getAuthInfoFromCookie(req);
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
return NextResponse.json({ error: '无权限访问' }, { status: 403 });
}
const config = await getConfig();
const subscriptions = config.AnimeSubscriptionConfig?.Subscriptions || [];
const index = subscriptions.findIndex((sub) => sub.id === params.id);
if (index === -1) {
return NextResponse.json({ error: '订阅不存在' }, { status: 404 });
}
subscriptions.splice(index, 1);
await db.saveAdminConfig(config);
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('删除追番订阅失败:', error);
return NextResponse.json(
{ error: error.message || '删除订阅失败' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,106 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';
import { AnimeSubscription } from '@/types/anime-subscription';
export const runtime = 'nodejs';
/**
* GET /api/admin/anime-subscription
* 获取订阅列表和配置
*/
export async function GET(req: NextRequest) {
try {
// 权限检查
const authInfo = getAuthInfoFromCookie(req);
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
return NextResponse.json({ error: '无权限访问' }, { status: 403 });
}
const config = await getConfig();
const animeConfig = config.AnimeSubscriptionConfig || {
Enabled: false,
Subscriptions: [],
};
return NextResponse.json(animeConfig);
} catch (error: any) {
console.error('获取追番订阅配置失败:', error);
return NextResponse.json(
{ error: error.message || '获取配置失败' },
{ status: 500 }
);
}
}
/**
* POST /api/admin/anime-subscription
* 创建新订阅
*/
export async function POST(req: NextRequest) {
try {
// 权限检查
const authInfo = getAuthInfoFromCookie(req);
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
return NextResponse.json({ error: '无权限访问' }, { status: 403 });
}
const { title, filterText, source, enabled, lastEpisode } =
await req.json();
// 验证必填字段
if (!title || !filterText || !source) {
return NextResponse.json({ error: '缺少必填字段' }, { status: 400 });
}
// 验证 source
if (!['acgrip', 'mikan', 'dmhy'].includes(source)) {
return NextResponse.json({ error: '无效的搜索源' }, { status: 400 });
}
const config = await getConfig();
if (!config.AnimeSubscriptionConfig) {
config.AnimeSubscriptionConfig = { Enabled: false, Subscriptions: [] };
}
// 验证集数
let episodeNum = 0;
if (lastEpisode !== undefined) {
episodeNum = parseInt(String(lastEpisode), 10);
if (isNaN(episodeNum) || episodeNum < 0) {
return NextResponse.json(
{ error: '集数必须是非负整数' },
{ status: 400 }
);
}
}
// 创建新订阅
const newSubscription: AnimeSubscription = {
id: crypto.randomUUID(),
title: title.trim(),
filterText: filterText.trim(),
source,
enabled: enabled ?? true,
lastCheckTime: 0,
lastEpisode: episodeNum,
createdAt: Date.now(),
updatedAt: Date.now(),
createdBy: authInfo.username || 'unknown',
};
config.AnimeSubscriptionConfig.Subscriptions.push(newSubscription);
await db.saveAdminConfig(config);
return NextResponse.json(newSubscription);
} catch (error: any) {
console.error('创建追番订阅失败:', error);
return NextResponse.json(
{ error: error.message || '创建订阅失败' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,47 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';
export const runtime = 'nodejs';
/**
* PUT /api/admin/anime-subscription/toggle
* 切换追番功能启用状态
*/
export async function PUT(req: NextRequest) {
try {
// 权限检查
const authInfo = getAuthInfoFromCookie(req);
if (!authInfo || (authInfo.role !== 'admin' && authInfo.role !== 'owner')) {
return NextResponse.json({ error: '无权限访问' }, { status: 403 });
}
const { enabled } = await req.json();
if (typeof enabled !== 'boolean') {
return NextResponse.json(
{ error: 'enabled 必须是布尔值' },
{ status: 400 }
);
}
const config = await getConfig();
if (!config.AnimeSubscriptionConfig) {
config.AnimeSubscriptionConfig = { Enabled: false, Subscriptions: [] };
}
config.AnimeSubscriptionConfig.Enabled = enabled;
await db.saveAdminConfig(config);
return NextResponse.json({ success: true, enabled });
} catch (error: any) {
console.error('切换追番功能状态失败:', error);
return NextResponse.json(
{ error: error.message || '切换状态失败' },
{ status: 500 }
);
}
}

View File

@@ -2,6 +2,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { checkAnimeSubscriptions } from '@/lib/anime-subscription';
import { getConfig, refineConfig } from '@/lib/config';
import { db, getStorage } from '@/lib/db';
import { EmailService } from '@/lib/email.service';
@@ -57,6 +58,7 @@ async function cronJob() {
await refreshAllLiveChannels();
await refreshOpenList();
await refreshRecordAndFavorites();
await checkAnimeSubscriptions();
}
async function refreshAllLiveChannels() {