Files
MoonTVPlus/src/app/api/cron/[password]/route.ts
T
2026-01-01 22:38:42 +08:00

354 lines
12 KiB
TypeScript

/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server';
import { getConfig, refineConfig } from '@/lib/config';
import { db, getStorage } from '@/lib/db';
import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
import { refreshLiveChannels } from '@/lib/live';
import { startOpenListRefresh } from '@/lib/openlist-refresh';
import { SearchResult } from '@/lib/types';
export const runtime = 'nodejs';
export async function GET(
request: NextRequest,
{ params }: { params: { password: string } }
) {
console.log(request.url);
const cronPassword = process.env.CRON_PASSWORD || 'mtvpls';
if (params.password !== cronPassword) {
return NextResponse.json(
{ success: false, message: 'Unauthorized' },
{ status: 401 }
);
}
try {
console.log('Cron job triggered:', new Date().toISOString());
cronJob();
return NextResponse.json({
success: true,
message: 'Cron job executed successfully',
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('Cron job failed:', error);
return NextResponse.json(
{
success: false,
message: 'Cron job failed',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
},
{ status: 500 }
);
}
}
async function cronJob() {
await refreshConfig();
await refreshAllLiveChannels();
await refreshOpenList();
await refreshRecordAndFavorites();
}
async function refreshAllLiveChannels() {
const config = await getConfig();
// 并发刷新所有启用的直播源
const refreshPromises = (config.LiveConfig || [])
.filter(liveInfo => !liveInfo.disabled)
.map(async (liveInfo) => {
try {
const nums = await refreshLiveChannels(liveInfo);
liveInfo.channelNumber = nums;
} catch (error) {
console.error(`刷新直播源失败 [${liveInfo.name || liveInfo.key}]:`, error);
liveInfo.channelNumber = 0;
}
});
// 等待所有刷新任务完成
await Promise.all(refreshPromises);
// 保存配置
await db.saveAdminConfig(config);
}
async function refreshConfig() {
let config = await getConfig();
if (config && config.ConfigSubscribtion && config.ConfigSubscribtion.URL && config.ConfigSubscribtion.AutoUpdate) {
try {
const response = await fetch(config.ConfigSubscribtion.URL);
if (!response.ok) {
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
}
const configContent = await response.text();
// 对 configContent 进行 base58 解码
let decodedContent;
try {
const bs58 = (await import('bs58')).default;
const decodedBytes = bs58.decode(configContent);
decodedContent = new TextDecoder().decode(decodedBytes);
} catch (decodeError) {
console.warn('Base58 解码失败:', decodeError);
throw decodeError;
}
try {
JSON.parse(decodedContent);
} catch (e) {
throw new Error('配置文件格式错误,请检查 JSON 语法');
}
config.ConfigFile = decodedContent;
config.ConfigSubscribtion.LastCheck = new Date().toISOString();
config = refineConfig(config);
await db.saveAdminConfig(config);
// 清除短剧视频源缓存(因为配置文件可能包含新的视频源)
try {
await db.deleteGlobalValue('duanju');
console.log('已清除短剧视频源缓存');
} catch (error) {
console.error('清除短剧视频源缓存失败:', error);
// 不影响主流程,继续执行
}
} catch (e) {
console.error('刷新配置失败:', e);
}
} else {
console.log('跳过刷新:未配置订阅地址或自动更新');
}
}
async function refreshRecordAndFavorites() {
try {
const users = await db.getAllUsers();
if (process.env.USERNAME && !users.includes(process.env.USERNAME)) {
users.push(process.env.USERNAME);
}
// 函数级缓存:key 为 `${source}+${id}`,值为 Promise<VideoDetail | null>
const detailCache = new Map<string, Promise<SearchResult | null>>();
// 获取详情 Promise(带缓存和错误处理)
const getDetail = async (
source: string,
id: string,
fallbackTitle: string
): Promise<SearchResult | null> => {
const key = `${source}+${id}`;
let promise = detailCache.get(key);
if (!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);
return null;
});
}
return promise;
};
for (const user of users) {
console.log(`开始处理用户: ${user}`);
const storage = getStorage();
// 播放记录
try {
const playRecords = await db.getAllPlayRecords(user);
const totalRecords = Object.keys(playRecords).length;
let processedRecords = 0;
for (const [key, record] of Object.entries(playRecords)) {
try {
const [source, id] = key.split('+');
if (!source || !id) {
console.warn(`跳过无效的播放记录键: ${key}`);
continue;
}
const detail = await getDetail(source, id, record.title);
if (!detail) {
console.warn(`跳过无法获取详情的播放记录: ${key}`);
continue;
}
const episodeCount = detail.episodes?.length || 0;
if (episodeCount > 0 && episodeCount !== record.total_episodes) {
await db.savePlayRecord(user, source, id, {
title: detail.title || record.title,
source_name: record.source_name,
cover: detail.poster || record.cover,
index: record.index,
total_episodes: episodeCount,
play_time: record.play_time,
year: detail.year || record.year,
total_time: record.total_time,
save_time: record.save_time,
search_title: record.search_title,
});
console.log(
`更新播放记录: ${record.title} (${record.total_episodes} -> ${episodeCount})`
);
}
processedRecords++;
} catch (err) {
console.error(`处理播放记录失败 (${key}):`, err);
// 继续处理下一个记录
}
}
console.log(`播放记录处理完成: ${processedRecords}/${totalRecords}`);
} catch (err) {
console.error(`获取用户播放记录失败 (${user}):`, err);
}
// 收藏
try {
let favorites = await db.getAllFavorites(user);
favorites = Object.fromEntries(
Object.entries(favorites).filter(([_, fav]) => fav.origin !== 'live')
);
const totalFavorites = Object.keys(favorites).length;
let processedFavorites = 0;
const now = Date.now();
for (const [key, fav] of Object.entries(favorites)) {
try {
const [source, id] = key.split('+');
if (!source || !id) {
console.warn(`跳过无效的收藏键: ${key}`);
continue;
}
const favDetail = await getDetail(source, id, fav.title);
if (!favDetail) {
console.warn(`跳过无法获取详情的收藏: ${key}`);
continue;
}
const favEpisodeCount = favDetail.episodes?.length || 0;
if (favEpisodeCount > 0 && favEpisodeCount !== fav.total_episodes) {
await db.saveFavorite(user, source, id, {
title: favDetail.title || fav.title,
source_name: fav.source_name,
cover: favDetail.poster || fav.cover,
year: favDetail.year || fav.year,
total_episodes: favEpisodeCount,
save_time: fav.save_time,
search_title: fav.search_title,
});
console.log(
`更新收藏: ${fav.title} (${fav.total_episodes} -> ${favEpisodeCount})`
);
// 创建通知
const notification = {
id: `fav_update_${source}_${id}_${now}`,
type: 'favorite_update' as const,
title: '收藏更新',
message: `《${fav.title}》有新集数更新!从 ${fav.total_episodes} 集更新到 ${favEpisodeCount} 集`,
timestamp: now,
read: false,
metadata: {
source,
id,
title: fav.title,
old_episodes: fav.total_episodes,
new_episodes: favEpisodeCount,
},
};
await storage.addNotification(user, notification);
console.log(`已为用户 ${user} 创建收藏更新通知: ${fav.title}`);
}
processedFavorites++;
} catch (err) {
console.error(`处理收藏失败 (${key}):`, err);
// 继续处理下一个收藏
}
}
console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`);
} catch (err) {
console.error(`获取用户收藏失败 (${user}):`, err);
}
}
console.log('刷新播放记录/收藏任务完成');
} catch (err) {
console.error('刷新播放记录/收藏任务启动失败', err);
}
}
async function refreshOpenList() {
try {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
// 检查功能是否启用
if (!openListConfig || !openListConfig.Enabled) {
console.log('跳过 OpenList 扫描:功能未启用');
return;
}
// 检查是否配置了 OpenList 和定时扫描
if (!openListConfig.URL || !openListConfig.Username || !openListConfig.Password) {
console.log('跳过 OpenList 扫描:未配置');
return;
}
const scanInterval = openListConfig.ScanInterval || 0;
if (scanInterval === 0) {
console.log('跳过 OpenList 扫描:定时扫描已关闭');
return;
}
// 检查间隔时间是否满足最低要求(60分钟)
if (scanInterval < 60) {
console.log(`跳过 OpenList 扫描:间隔时间 ${scanInterval} 分钟小于最低要求 60 分钟`);
return;
}
// 检查上次扫描时间
const lastRefreshTime = openListConfig.LastRefreshTime || 0;
const now = Date.now();
const timeSinceLastRefresh = now - lastRefreshTime;
const intervalMs = scanInterval * 60 * 1000;
if (timeSinceLastRefresh < intervalMs) {
const remainingMinutes = Math.ceil((intervalMs - timeSinceLastRefresh) / 60000);
console.log(`跳过 OpenList 扫描:距离上次扫描仅 ${Math.floor(timeSinceLastRefresh / 60000)} 分钟,还需等待 ${remainingMinutes} 分钟`);
return;
}
console.log(`开始 OpenList 定时扫描(间隔: ${scanInterval} 分钟)`);
// 直接调用扫描函数(立即扫描模式,不清空 metainfo)
const { taskId } = await startOpenListRefresh(false);
console.log('OpenList 定时扫描已启动,任务ID:', taskId);
} catch (err) {
console.error('OpenList 定时扫描失败:', err);
}
}