From 4dfea503d55fe00403b4340eff793ff13846e7d6 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sun, 22 Feb 2026 23:55:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E6=80=A7?= =?UTF-8?q?=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + src/app/api/cron/[password]/route.ts | 87 +++++++++++++++++----------- src/lib/anime-subscription.ts | 29 +++++++++- 3 files changed, 79 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index c3db67d..082d8d2 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ dockge/komodo 等 docker compose UI 也有自动更新功能 | USERNAME | 站长账号 | 任意字符串 | 无默认,必填字段 | | PASSWORD | 站长密码 | 任意字符串 | 无默认,必填字段 | | CRON_PASSWORD | 定时任务 API 访问密码(用于保护 /api/cron 端点) | 任意字符串 | mtvpls | +| CRON_USER_BATCH_SIZE | 定时任务用户批处理大小(控制并发处理的用户数量,影响播放记录和收藏更新任务的并发性能) | 正整数 | 3 | | SITE_BASE | 站点 url | 形如 https://example.com | 空 | | NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV | | ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 | diff --git a/src/app/api/cron/[password]/route.ts b/src/app/api/cron/[password]/route.ts index cd40b34..3885d50 100644 --- a/src/app/api/cron/[password]/route.ts +++ b/src/app/api/cron/[password]/route.ts @@ -54,11 +54,16 @@ export async function GET( } async function cronJob() { + // 先刷新配置,确保其他任务使用最新配置 await refreshConfig(); - await refreshAllLiveChannels(); - await refreshOpenList(); - await refreshRecordAndFavorites(); - await checkAnimeSubscriptions(); + + // 其余任务并行执行 + await Promise.all([ + refreshAllLiveChannels(), + refreshOpenList(), + refreshRecordAndFavorites(), + checkAnimeSubscriptions(), + ]); } async function refreshAllLiveChannels() { @@ -164,26 +169,28 @@ async function refreshRecordAndFavorites() { const key = `${source}+${id}`; let promise = detailCache.get(key); if (!promise) { + // 立即缓存Promise,避免并发时的竞态条件 promise = fetchVideoDetail({ source, id, fallbackTitle: fallbackTitle.trim(), }) .then((detail) => { - // 成功时才缓存结果 - const successPromise = Promise.resolve(detail); - detailCache.set(key, successPromise); return detail; }) .catch((err) => { console.error(`获取视频详情失败 (${source}+${id}):`, err); + // 失败时从缓存中移除,下次可以重试 + detailCache.delete(key); return null; }); + detailCache.set(key, promise); } return promise; }; - for (const user of users) { + // 处理单个用户的函数 + const processUser = async (user: string) => { console.log(`开始处理用户: ${user}`); const storage = getStorage(); @@ -342,44 +349,54 @@ async function refreshRecordAndFavorites() { console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`); - // 如果有更新,发送汇总邮件 + // 如果有更新,异步发送汇总邮件(不阻塞主流程) if (userUpdates.length > 0) { - try { - const userEmail = storage.getUserEmail ? await storage.getUserEmail(user) : null; - const emailNotifications = storage.getEmailNotificationPreference - ? await storage.getEmailNotificationPreference(user) - : false; + (async () => { + try { + const userEmail = storage.getUserEmail ? await storage.getUserEmail(user) : null; + const emailNotifications = storage.getEmailNotificationPreference + ? await storage.getEmailNotificationPreference(user) + : false; - if (userEmail && emailNotifications) { - const config = await getConfig(); - const emailConfig = config?.EmailConfig; + if (userEmail && emailNotifications) { + const config = await getConfig(); + const emailConfig = config?.EmailConfig; - if (emailConfig?.enabled) { - const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; - const siteName = config?.SiteConfig?.SiteName || 'MoonTVPlus'; + if (emailConfig?.enabled) { + const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; + const siteName = config?.SiteConfig?.SiteName || 'MoonTVPlus'; - await EmailService.send(emailConfig, { - to: userEmail, - subject: `📺 收藏更新汇总 - ${userUpdates.length} 部影片有更新`, - html: getBatchFavoriteUpdateEmailTemplate( - user, - userUpdates, - siteUrl, - siteName - ), - }); + await EmailService.send(emailConfig, { + to: userEmail, + subject: `📺 收藏更新汇总 - ${userUpdates.length} 部影片有更新`, + html: getBatchFavoriteUpdateEmailTemplate( + user, + userUpdates, + siteUrl, + siteName + ), + }); - console.log(`邮件汇总已发送至: ${userEmail} (${userUpdates.length} 个更新)`); + console.log(`邮件汇总已发送至: ${userEmail} (${userUpdates.length} 个更新)`); + } } + } catch (emailError) { + console.error(`发送邮件汇总失败 (${user}):`, emailError); } - } catch (emailError) { - console.error(`发送邮件汇总失败 (${user}):`, emailError); - // 邮件发送失败不影响主流程 - } + })().catch(err => console.error(`邮件发送异步任务失败 (${user}):`, err)); } } catch (err) { console.error(`获取用户收藏失败 (${user}):`, err); } + }; + + // 分批并行处理用户,避免并发过高 + // 可通过环境变量 CRON_USER_BATCH_SIZE 配置批处理大小,默认为 3 + const BATCH_SIZE = parseInt(process.env.CRON_USER_BATCH_SIZE || '3', 10); + for (let i = 0; i < users.length; i += BATCH_SIZE) { + const batch = users.slice(i, i + BATCH_SIZE); + console.log(`处理用户批次 ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(users.length / BATCH_SIZE)}: ${batch.join(', ')}`); + await Promise.all(batch.map(user => processUser(user))); } console.log('刷新播放记录/收藏任务完成'); diff --git a/src/lib/anime-subscription.ts b/src/lib/anime-subscription.ts index 01c1a69..d83e936 100644 --- a/src/lib/anime-subscription.ts +++ b/src/lib/anime-subscription.ts @@ -361,31 +361,51 @@ export async function checkSubscription(subscription: AnimeSubscription) { * 检查所有订阅(定时任务调用) */ export async function checkAnimeSubscriptions() { + console.log('[AnimeSubscription] 开始检查动漫订阅'); + const config = await getConfig(); const animeConfig = config.AnimeSubscriptionConfig; if (!animeConfig?.Enabled) { + console.log('[AnimeSubscription] 动漫订阅功能未启用,跳过检查'); return; } const subscriptions = animeConfig.Subscriptions || []; + console.log(`[AnimeSubscription] 共有 ${subscriptions.length} 个订阅`); + const now = Date.now(); const MIN_CHECK_INTERVAL = 30 * 60 * 1000; // 30分钟 let configChanged = false; + let checkedCount = 0; + let skippedCount = 0; + let errorCount = 0; for (const sub of subscriptions) { - if (!sub.enabled) continue; + if (!sub.enabled) { + console.log(`[AnimeSubscription] 跳过已禁用的订阅: ${sub.title}`); + skippedCount++; + continue; + } // 检查是否距离上次检查超过30分钟 - if (now - sub.lastCheckTime < MIN_CHECK_INTERVAL) { + const timeSinceLastCheck = now - sub.lastCheckTime; + if (timeSinceLastCheck < MIN_CHECK_INTERVAL) { + const remainingMinutes = Math.ceil((MIN_CHECK_INTERVAL - timeSinceLastCheck) / 60000); + console.log(`[AnimeSubscription] 跳过 ${sub.title}: 距离上次检查仅 ${Math.floor(timeSinceLastCheck / 60000)} 分钟,还需等待 ${remainingMinutes} 分钟`); + skippedCount++; continue; } try { - await checkSubscription(sub); + console.log(`[AnimeSubscription] 检查订阅: ${sub.title} (源: ${sub.source}, 上次集数: ${sub.lastEpisode})`); + const result = await checkSubscription(sub); + console.log(`[AnimeSubscription] ${sub.title}: 找到 ${result.found} 个新集数,成功下载 ${result.downloaded} 个`); configChanged = true; + checkedCount++; } catch (error) { console.error(`[AnimeSubscription] ${sub.title}: 检查失败`, error); + errorCount++; } } @@ -393,5 +413,8 @@ export async function checkAnimeSubscriptions() { if (configChanged) { await db.saveAdminConfig(config); await setCachedConfig(config); + console.log('[AnimeSubscription] 配置已更新并保存'); } + + console.log(`[AnimeSubscription] 检查完成 - 总计: ${subscriptions.length}, 已检查: ${checkedCount}, 跳过: ${skippedCount}, 失败: ${errorCount}`); }