mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 00:44:41 +08:00
feat: add cron to refresh records and favorites
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"@cloudflare/next-on-pages": "^1.13.12",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@upstash/redis": "^1.25.0",
|
||||
"@vidstack/react": "^1.12.13",
|
||||
"clsx": "^2.0.0",
|
||||
"framer-motion": "^12.18.1",
|
||||
@@ -32,15 +33,15 @@
|
||||
"next": "^14.2.23",
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-cron": "^4.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"redis": "^4.6.7",
|
||||
"swiper": "^11.2.8",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vidstack": "^0.6.15",
|
||||
"@upstash/redis": "^1.25.0",
|
||||
"zod": "^3.24.1",
|
||||
"redis": "^4.6.7"
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.3.0",
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -47,6 +47,9 @@ importers:
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
node-cron:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
@@ -4735,6 +4738,10 @@ packages:
|
||||
no-case@3.0.4:
|
||||
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||
|
||||
node-cron@4.2.0:
|
||||
resolution: {integrity: sha512-nOdP7uH7u55w7ybQq9fusXtsResok+ErzvOBydJUPBBaQ9W+EfBaBWFPgJ8sOB7FWQednDvVBJtgP5xA0bME7Q==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
node-fetch@2.6.7:
|
||||
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
@@ -11612,6 +11619,8 @@ snapshots:
|
||||
lower-case: 2.0.2
|
||||
tslib: 2.8.1
|
||||
|
||||
node-cron@4.2.0: {}
|
||||
|
||||
node-fetch@2.6.7:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import '../lib/cron';
|
||||
|
||||
import './globals.css';
|
||||
|
||||
@@ -35,10 +36,7 @@ export default function RootLayout({
|
||||
|
||||
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
||||
const runtimeConfig = {
|
||||
STORAGE_TYPE:
|
||||
process.env.STORAGE_TYPE ||
|
||||
process.env.NEXT_PUBLIC_STORAGE_TYPE ||
|
||||
'localstorage',
|
||||
STORAGE_TYPE: process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage',
|
||||
ENABLE_REGISTER: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
|
||||
};
|
||||
|
||||
|
||||
30
src/lib/cron.ts
Normal file
30
src/lib/cron.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import cron from 'node-cron';
|
||||
|
||||
import refreshRecordAndFavorites from '@/lib/refreshRecordAndFavorites';
|
||||
|
||||
/*
|
||||
* 初始化定时任务:每个小时的 02 分执行一次。
|
||||
* 若需要添加更多任务,可在此文件中继续编写。
|
||||
*/
|
||||
|
||||
declare global {
|
||||
// 避免在开发热重载或多次导入时重复初始化
|
||||
// eslint-disable-next-line no-var
|
||||
var __moonTVCronInitialized: boolean | undefined;
|
||||
}
|
||||
|
||||
if (!global.__moonTVCronInitialized) {
|
||||
cron.schedule(
|
||||
'2 * * * *',
|
||||
async () => {
|
||||
refreshRecordAndFavorites();
|
||||
},
|
||||
{
|
||||
timezone: 'Asia/Shanghai',
|
||||
}
|
||||
);
|
||||
|
||||
global.__moonTVCronInitialized = true;
|
||||
}
|
||||
|
||||
export {}; // 仅用于确保这是一个模块
|
||||
@@ -61,6 +61,9 @@ export interface IStorage {
|
||||
getSearchHistory(): Promise<string[]>;
|
||||
addSearchHistory(keyword: string): Promise<void>;
|
||||
deleteSearchHistory(keyword?: string): Promise<void>;
|
||||
|
||||
// 用户列表
|
||||
getAllUsers(): Promise<string[]>;
|
||||
}
|
||||
|
||||
// ---------------- Redis 实现 ----------------
|
||||
@@ -203,6 +206,17 @@ class RedisStorage implements IStorage {
|
||||
await this.client.del(this.shKey);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 获取全部用户 ----------
|
||||
async getAllUsers(): Promise<string[]> {
|
||||
const keys = await this.client.keys('u:*:pwd');
|
||||
return keys
|
||||
.map((k) => {
|
||||
const match = k.match(/^u:(.+?):pwd$/);
|
||||
return match ? match[1] : undefined;
|
||||
})
|
||||
.filter((u): u is string => typeof u === 'string');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建存储实例
|
||||
@@ -368,6 +382,14 @@ export class DbManager {
|
||||
async deleteSearchHistory(keyword?: string): Promise<void> {
|
||||
await this.storage.deleteSearchHistory(keyword);
|
||||
}
|
||||
|
||||
// 获取全部用户名
|
||||
async getAllUsers(): Promise<string[]> {
|
||||
if (typeof (this.storage as any).getAllUsers === 'function') {
|
||||
return (this.storage as any).getAllUsers();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
|
||||
87
src/lib/refreshRecordAndFavorites.ts
Normal file
87
src/lib/refreshRecordAndFavorites.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { type VideoDetail, fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
||||
|
||||
const STORAGE_TYPE = process.env.NEXT_PUBLIC_STORAGE_TYPE ?? 'localstorage';
|
||||
|
||||
async function refreshRecordAndFavorites() {
|
||||
if (STORAGE_TYPE === 'localstorage') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await db.getAllUsers();
|
||||
// 函数级缓存:key 为 `${source}+${id}`,值为 Promise<VideoDetail>
|
||||
const detailCache = new Map<string, Promise<VideoDetail>>();
|
||||
|
||||
// 获取详情 Promise(带缓存)
|
||||
const getDetail = (
|
||||
source: string,
|
||||
id: string,
|
||||
fallbackTitle: string
|
||||
): Promise<VideoDetail> => {
|
||||
const key = `${source}+${id}`;
|
||||
let promise = detailCache.get(key);
|
||||
if (!promise) {
|
||||
promise = fetchVideoDetail({
|
||||
source,
|
||||
id,
|
||||
fallbackTitle,
|
||||
fallbackYear: '',
|
||||
});
|
||||
detailCache.set(key, promise);
|
||||
}
|
||||
return promise;
|
||||
};
|
||||
|
||||
for (const user of users) {
|
||||
// 播放记录
|
||||
const playRecords = await db.getAllPlayRecords(user);
|
||||
for (const [key, record] of Object.entries(playRecords)) {
|
||||
const [source, id] = key.split('+');
|
||||
if (!source || !id) continue;
|
||||
|
||||
const detail: VideoDetail = await getDetail(source, id, record.title);
|
||||
|
||||
const episodeCount = detail.episodes?.length || 0;
|
||||
if (episodeCount > 0 && episodeCount !== record.total_episodes) {
|
||||
await db.savePlayRecord(user, source, id, {
|
||||
title: record.title,
|
||||
source_name: record.source_name,
|
||||
cover: record.cover,
|
||||
index: record.index,
|
||||
total_episodes: episodeCount,
|
||||
play_time: record.play_time,
|
||||
total_time: record.total_time,
|
||||
save_time: record.save_time,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 收藏
|
||||
const favorites = await db.getAllFavorites(user);
|
||||
for (const [key, fav] of Object.entries(favorites)) {
|
||||
const [source, id] = key.split('+');
|
||||
if (!source || !id) continue;
|
||||
|
||||
const favDetail: VideoDetail = await getDetail(source, id, fav.title);
|
||||
|
||||
const favEpisodeCount = favDetail.episodes?.length || 0;
|
||||
if (favEpisodeCount > 0 && favEpisodeCount !== fav.total_episodes) {
|
||||
await db.saveFavorite(user, source, id, {
|
||||
title: fav.title,
|
||||
source_name: fav.source_name,
|
||||
cover: fav.cover,
|
||||
total_episodes: favEpisodeCount,
|
||||
save_time: fav.save_time,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('刷新播放记录/收藏失败', err);
|
||||
}
|
||||
}
|
||||
|
||||
export default refreshRecordAndFavorites;
|
||||
Reference in New Issue
Block a user