增加剧集更新检查

This commit is contained in:
mtvpls
2025-12-18 23:44:14 +08:00
parent 0ae5923b4b
commit c60e25ded6
12 changed files with 864 additions and 9 deletions
@@ -0,0 +1,159 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getStorage } from '@/lib/db';
import { getAvailableApiSites } from '@/lib/config';
import { getDetailFromApi } from '@/lib/downstream';
import { Notification } from '@/lib/types';
export const runtime = 'nodejs';
export async function POST(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const storage = getStorage();
const username = authInfo.username;
const now = Date.now();
console.log(`用户 ${username} 请求检查收藏更新`);
console.log(`当前时间: ${new Date(now).toLocaleString('zh-CN')}`);
console.log(`开始检查收藏更新...`);
// 获取所有收藏
const favorites = await storage.getAllFavorites(username);
const favoriteKeys = Object.keys(favorites);
if (favoriteKeys.length === 0) {
return NextResponse.json({
message: '没有收藏',
updates: [],
});
}
// 获取可用的 API 站点
const apiSites = await getAvailableApiSites(username);
// 检查每个收藏的更新
const updates: Array<{
source: string;
id: string;
title: string;
old_episodes: number;
new_episodes: number;
}> = [];
// 限制并发请求数量,避免过载
const BATCH_SIZE = 5;
for (let i = 0; i < favoriteKeys.length; i += BATCH_SIZE) {
const batch = favoriteKeys.slice(i, i + BATCH_SIZE);
await Promise.all(
batch.map(async (key) => {
try {
const favorite = favorites[key];
// 跳过 live 类型的收藏
if (favorite.origin === 'live') {
return;
}
// 跳过已完结的收藏
if (favorite.is_completed) {
console.log(`跳过已完结的收藏: ${favorite.title}`);
return;
}
// 解析 source 和 id
const [source, id] = key.split('+');
if (!source || !id) {
return;
}
// 查找对应的 API 站点
const apiSite = apiSites.find((site) => site.key === source);
if (!apiSite) {
return;
}
// 获取最新详情
const detail = await getDetailFromApi(apiSite, id);
// 比较集数
const oldEpisodes = favorite.total_episodes;
const newEpisodes = detail.episodes.length;
console.log(`检查收藏: ${favorite.title} (${source}+${id})`);
console.log(` 旧集数: ${oldEpisodes}, 新集数: ${newEpisodes}`);
console.log(` 是否完结: ${favorite.is_completed}, 备注: ${favorite.vod_remarks}`);
if (newEpisodes > oldEpisodes) {
updates.push({
source,
id,
title: favorite.title,
old_episodes: oldEpisodes,
new_episodes: newEpisodes,
});
// 更新收藏的集数和完结状态
await storage.setFavorite(username, key, {
...favorite,
total_episodes: newEpisodes,
is_completed: detail.vod_remarks
? ['全', '完结', '大结局', 'end', '完'].some((keyword) =>
detail.vod_remarks!.toLowerCase().includes(keyword)
)
: false,
vod_remarks: detail.vod_remarks,
});
}
} catch (error) {
console.error(`检查收藏更新失败 (${key}):`, error);
// 继续处理其他收藏
}
})
);
}
console.log(`检查完成,发现 ${updates.length} 个更新`);
// 如果有更新,创建通知
if (updates.length > 0) {
for (const update of updates) {
const notification: Notification = {
id: `fav_update_${update.source}_${update.id}_${now}`,
type: 'favorite_update',
title: '收藏更新',
message: `${update.title}》有新集数更新!从 ${update.old_episodes} 集更新到 ${update.new_episodes}`,
timestamp: now,
read: false,
metadata: {
source: update.source,
id: update.id,
title: update.title,
old_episodes: update.old_episodes,
new_episodes: update.new_episodes,
},
};
await storage.addNotification(username, notification);
}
}
return NextResponse.json({
message: updates.length > 0 ? `发现 ${updates.length} 个更新` : '没有更新',
updates,
checked: favoriteKeys.length,
});
} catch (error) {
console.error('检查收藏更新失败:', error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 }
);
}
}
+69
View File
@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getStorage } from '@/lib/db';
export const runtime = 'nodejs';
// GET: 获取所有通知
export async function GET(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const storage = getStorage();
const notifications = await storage.getNotifications(authInfo.username);
const unreadCount = await storage.getUnreadNotificationCount(authInfo.username);
return NextResponse.json({
notifications,
unreadCount,
});
} catch (error) {
console.error('获取通知失败:', error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 }
);
}
}
// POST: 标记通知为已读或删除通知
export async function POST(request: NextRequest) {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const body = await request.json();
const { action, notificationId } = body;
const storage = getStorage();
if (action === 'mark_read' && notificationId) {
await storage.markNotificationAsRead(authInfo.username, notificationId);
return NextResponse.json({ message: '已标记为已读' });
}
if (action === 'delete' && notificationId) {
await storage.deleteNotification(authInfo.username, notificationId);
return NextResponse.json({ message: '已删除' });
}
if (action === 'clear_all') {
await storage.clearAllNotifications(authInfo.username);
return NextResponse.json({ message: '已清空所有通知' });
}
return NextResponse.json({ error: '无效的操作' }, { status: 400 });
} catch (error) {
console.error('操作通知失败:', error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 }
);
}
}
+49
View File
@@ -57,6 +57,55 @@ function HomeClient() {
}
}, [announcement]);
// 首次进入时检查收藏更新(带前端冷却检查)
useEffect(() => {
const checkFavoriteUpdates = async () => {
try {
// 检查冷却时间(前端 localStorage
const COOLDOWN_TIME = 30 * 60 * 1000; // 30分钟
const lastCheckTime = localStorage.getItem('lastFavoriteCheckTime');
const now = Date.now();
if (lastCheckTime) {
const timeSinceLastCheck = now - parseInt(lastCheckTime, 10);
if (timeSinceLastCheck < COOLDOWN_TIME) {
const remainingMinutes = Math.ceil((COOLDOWN_TIME - timeSinceLastCheck) / 1000 / 60);
console.log(`收藏更新检查冷却中,还需等待 ${remainingMinutes} 分钟`);
return;
}
}
console.log('开始检查收藏更新...');
const response = await fetch('/api/favorites/check-updates', {
method: 'POST',
});
if (response.ok) {
// 更新本地检查时间
localStorage.setItem('lastFavoriteCheckTime', now.toString());
const data = await response.json();
if (data.updates && data.updates.length > 0) {
console.log(`发现 ${data.updates.length} 个收藏更新`);
// 触发通知更新事件
window.dispatchEvent(new Event('notificationsUpdated'));
} else {
console.log('没有收藏更新');
}
}
} catch (error) {
console.error('检查收藏更新失败:', error);
}
};
// 延迟3秒后检查,避免影响首页加载
const timer = setTimeout(() => {
checkFavoriteUpdates();
}, 3000);
return () => clearTimeout(timer);
}, []);
// 收藏夹数据
type FavoriteItem = {
id: string;
+57 -8
View File
@@ -602,6 +602,39 @@ function PlayPageClient() {
// 工具函数(Utils
// -----------------------------------------------------------------------------
// 判断剧集是否已完结
const isSeriesCompleted = (detail: SearchResult | null): boolean => {
if (!detail) return false;
// 方法1:通过 vod_remarks 判断
if (detail.vod_remarks) {
const remarks = detail.vod_remarks.toLowerCase();
// 判定为完结的关键词
const completedKeywords = ['全', '完结', '大结局', 'end', '完'];
// 判定为连载的关键词
const ongoingKeywords = ['更新至', '连载', '第', '更新到'];
// 如果包含连载关键词,则为连载中
if (ongoingKeywords.some(keyword => remarks.includes(keyword))) {
return false;
}
// 如果包含完结关键词,则为已完结
if (completedKeywords.some(keyword => remarks.includes(keyword))) {
return true;
}
}
// 方法2:通过 vod_total 和实际集数对比判断
if (detail.vod_total && detail.vod_total > 0 && detail.episodes && detail.episodes.length > 0) {
// 如果实际集数 >= 总集数,则为已完结
return detail.episodes.length >= detail.vod_total;
}
// 无法判断,默认返回 false(连载中)
return false;
};
// 播放源优选函数
const preferBestSource = async (
sources: SearchResult[]
@@ -2758,11 +2791,13 @@ function PlayPageClient() {
await saveFavorite(currentSourceRef.current, currentIdRef.current, {
title: videoTitleRef.current,
source_name: detailRef.current?.source_name || '',
year: detailRef.current?.year,
year: detailRef.current?.year || 'unknown',
cover: detailRef.current?.poster || '',
total_episodes: detailRef.current?.episodes.length || 1,
save_time: Date.now(),
search_title: searchTitle,
is_completed: isSeriesCompleted(detailRef.current),
vod_remarks: detailRef.current?.vod_remarks,
});
setFavorited(true);
}
@@ -4288,14 +4323,28 @@ function PlayPageClient() {
<div className='flex flex-col gap-3 py-4 px-5 lg:px-[3rem] 2xl:px-20'>
{/* 第一行:影片标题 */}
<div className='py-1'>
<h1 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
{videoTitle || '影片标题'}
{totalEpisodes > 1 && (
<span className='text-gray-500 dark:text-gray-400'>
{` > ${
detail?.episodes_titles?.[currentEpisodeIndex] ||
`${currentEpisodeIndex + 1}`
<h1 className='text-xl font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2 flex-wrap'>
<span>
{videoTitle || '影片标题'}
{totalEpisodes > 1 && (
<span className='text-gray-500 dark:text-gray-400'>
{` > ${
detail?.episodes_titles?.[currentEpisodeIndex] ||
`${currentEpisodeIndex + 1}`
}`}
</span>
)}
</span>
{/* 完结状态标识 */}
{detail && totalEpisodes > 1 && (
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
isSeriesCompleted(detail)
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
}`}
>
{isSeriesCompleted(detail) ? '已完结' : '连载中'}
</span>
)}
</h1>