定时任务增加鉴权
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user