定时任务性能优化
This commit is contained in:
@@ -339,6 +339,7 @@ dockge/komodo 等 docker compose UI 也有自动更新功能
|
|||||||
| USERNAME | 站长账号 | 任意字符串 | 无默认,必填字段 |
|
| USERNAME | 站长账号 | 任意字符串 | 无默认,必填字段 |
|
||||||
| PASSWORD | 站长密码 | 任意字符串 | 无默认,必填字段 |
|
| PASSWORD | 站长密码 | 任意字符串 | 无默认,必填字段 |
|
||||||
| CRON_PASSWORD | 定时任务 API 访问密码(用于保护 /api/cron 端点) | 任意字符串 | mtvpls |
|
| CRON_PASSWORD | 定时任务 API 访问密码(用于保护 /api/cron 端点) | 任意字符串 | mtvpls |
|
||||||
|
| CRON_USER_BATCH_SIZE | 定时任务用户批处理大小(控制并发处理的用户数量,影响播放记录和收藏更新任务的并发性能) | 正整数 | 3 |
|
||||||
| SITE_BASE | 站点 url | 形如 https://example.com | 空 |
|
| SITE_BASE | 站点 url | 形如 https://example.com | 空 |
|
||||||
| NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
|
| NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
|
||||||
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
|
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
|
||||||
|
|||||||
@@ -54,11 +54,16 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cronJob() {
|
async function cronJob() {
|
||||||
|
// 先刷新配置,确保其他任务使用最新配置
|
||||||
await refreshConfig();
|
await refreshConfig();
|
||||||
await refreshAllLiveChannels();
|
|
||||||
await refreshOpenList();
|
// 其余任务并行执行
|
||||||
await refreshRecordAndFavorites();
|
await Promise.all([
|
||||||
await checkAnimeSubscriptions();
|
refreshAllLiveChannels(),
|
||||||
|
refreshOpenList(),
|
||||||
|
refreshRecordAndFavorites(),
|
||||||
|
checkAnimeSubscriptions(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAllLiveChannels() {
|
async function refreshAllLiveChannels() {
|
||||||
@@ -164,26 +169,28 @@ async function refreshRecordAndFavorites() {
|
|||||||
const key = `${source}+${id}`;
|
const key = `${source}+${id}`;
|
||||||
let promise = detailCache.get(key);
|
let promise = detailCache.get(key);
|
||||||
if (!promise) {
|
if (!promise) {
|
||||||
|
// 立即缓存Promise,避免并发时的竞态条件
|
||||||
promise = fetchVideoDetail({
|
promise = fetchVideoDetail({
|
||||||
source,
|
source,
|
||||||
id,
|
id,
|
||||||
fallbackTitle: fallbackTitle.trim(),
|
fallbackTitle: fallbackTitle.trim(),
|
||||||
})
|
})
|
||||||
.then((detail) => {
|
.then((detail) => {
|
||||||
// 成功时才缓存结果
|
|
||||||
const successPromise = Promise.resolve(detail);
|
|
||||||
detailCache.set(key, successPromise);
|
|
||||||
return detail;
|
return detail;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`获取视频详情失败 (${source}+${id}):`, err);
|
console.error(`获取视频详情失败 (${source}+${id}):`, err);
|
||||||
|
// 失败时从缓存中移除,下次可以重试
|
||||||
|
detailCache.delete(key);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
detailCache.set(key, promise);
|
||||||
}
|
}
|
||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const user of users) {
|
// 处理单个用户的函数
|
||||||
|
const processUser = async (user: string) => {
|
||||||
console.log(`开始处理用户: ${user}`);
|
console.log(`开始处理用户: ${user}`);
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
|
|
||||||
@@ -342,44 +349,54 @@ async function refreshRecordAndFavorites() {
|
|||||||
|
|
||||||
console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`);
|
console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`);
|
||||||
|
|
||||||
// 如果有更新,发送汇总邮件
|
// 如果有更新,异步发送汇总邮件(不阻塞主流程)
|
||||||
if (userUpdates.length > 0) {
|
if (userUpdates.length > 0) {
|
||||||
try {
|
(async () => {
|
||||||
const userEmail = storage.getUserEmail ? await storage.getUserEmail(user) : null;
|
try {
|
||||||
const emailNotifications = storage.getEmailNotificationPreference
|
const userEmail = storage.getUserEmail ? await storage.getUserEmail(user) : null;
|
||||||
? await storage.getEmailNotificationPreference(user)
|
const emailNotifications = storage.getEmailNotificationPreference
|
||||||
: false;
|
? await storage.getEmailNotificationPreference(user)
|
||||||
|
: false;
|
||||||
|
|
||||||
if (userEmail && emailNotifications) {
|
if (userEmail && emailNotifications) {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const emailConfig = config?.EmailConfig;
|
const emailConfig = config?.EmailConfig;
|
||||||
|
|
||||||
if (emailConfig?.enabled) {
|
if (emailConfig?.enabled) {
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||||
const siteName = config?.SiteConfig?.SiteName || 'MoonTVPlus';
|
const siteName = config?.SiteConfig?.SiteName || 'MoonTVPlus';
|
||||||
|
|
||||||
await EmailService.send(emailConfig, {
|
await EmailService.send(emailConfig, {
|
||||||
to: userEmail,
|
to: userEmail,
|
||||||
subject: `📺 收藏更新汇总 - ${userUpdates.length} 部影片有更新`,
|
subject: `📺 收藏更新汇总 - ${userUpdates.length} 部影片有更新`,
|
||||||
html: getBatchFavoriteUpdateEmailTemplate(
|
html: getBatchFavoriteUpdateEmailTemplate(
|
||||||
user,
|
user,
|
||||||
userUpdates,
|
userUpdates,
|
||||||
siteUrl,
|
siteUrl,
|
||||||
siteName
|
siteName
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`邮件汇总已发送至: ${userEmail} (${userUpdates.length} 个更新)`);
|
console.log(`邮件汇总已发送至: ${userEmail} (${userUpdates.length} 个更新)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (emailError) {
|
||||||
|
console.error(`发送邮件汇总失败 (${user}):`, emailError);
|
||||||
}
|
}
|
||||||
} catch (emailError) {
|
})().catch(err => console.error(`邮件发送异步任务失败 (${user}):`, err));
|
||||||
console.error(`发送邮件汇总失败 (${user}):`, emailError);
|
|
||||||
// 邮件发送失败不影响主流程
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`获取用户收藏失败 (${user}):`, 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('刷新播放记录/收藏任务完成');
|
console.log('刷新播放记录/收藏任务完成');
|
||||||
|
|||||||
@@ -361,31 +361,51 @@ export async function checkSubscription(subscription: AnimeSubscription) {
|
|||||||
* 检查所有订阅(定时任务调用)
|
* 检查所有订阅(定时任务调用)
|
||||||
*/
|
*/
|
||||||
export async function checkAnimeSubscriptions() {
|
export async function checkAnimeSubscriptions() {
|
||||||
|
console.log('[AnimeSubscription] 开始检查动漫订阅');
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const animeConfig = config.AnimeSubscriptionConfig;
|
const animeConfig = config.AnimeSubscriptionConfig;
|
||||||
|
|
||||||
if (!animeConfig?.Enabled) {
|
if (!animeConfig?.Enabled) {
|
||||||
|
console.log('[AnimeSubscription] 动漫订阅功能未启用,跳过检查');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscriptions = animeConfig.Subscriptions || [];
|
const subscriptions = animeConfig.Subscriptions || [];
|
||||||
|
console.log(`[AnimeSubscription] 共有 ${subscriptions.length} 个订阅`);
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const MIN_CHECK_INTERVAL = 30 * 60 * 1000; // 30分钟
|
const MIN_CHECK_INTERVAL = 30 * 60 * 1000; // 30分钟
|
||||||
let configChanged = false;
|
let configChanged = false;
|
||||||
|
let checkedCount = 0;
|
||||||
|
let skippedCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
for (const sub of subscriptions) {
|
for (const sub of subscriptions) {
|
||||||
if (!sub.enabled) continue;
|
if (!sub.enabled) {
|
||||||
|
console.log(`[AnimeSubscription] 跳过已禁用的订阅: ${sub.title}`);
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否距离上次检查超过30分钟
|
// 检查是否距离上次检查超过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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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;
|
configChanged = true;
|
||||||
|
checkedCount++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[AnimeSubscription] ${sub.title}: 检查失败`, error);
|
console.error(`[AnimeSubscription] ${sub.title}: 检查失败`, error);
|
||||||
|
errorCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,5 +413,8 @@ export async function checkAnimeSubscriptions() {
|
|||||||
if (configChanged) {
|
if (configChanged) {
|
||||||
await db.saveAdminConfig(config);
|
await db.saveAdminConfig(config);
|
||||||
await setCachedConfig(config);
|
await setCachedConfig(config);
|
||||||
|
console.log('[AnimeSubscription] 配置已更新并保存');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[AnimeSubscription] 检查完成 - 总计: ${subscriptions.length}, 已检查: ${checkedCount}, 跳过: ${skippedCount}, 失败: ${errorCount}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user