From 44ce8241c6a4e5587abd09fea1a5c67a7a7bdd72 Mon Sep 17 00:00:00 2001 From: shinya Date: Fri, 4 Jul 2025 20:50:12 +0800 Subject: [PATCH] feat: add admin account, add refresh log --- README.md | 6 +- config.json | 16 +-- src/app/api/login/route.ts | 8 ++ src/app/api/register/route.ts | 5 + src/lib/refreshRecordAndFavorites.ts | 146 ++++++++++++++++++++------- 5 files changed, 131 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index bf84877..c28de9e 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,6 @@ docker run -d --name moontv -p 3000:3000 ghcr.io/senshinya/moontv:latest 访问 `http://服务器 IP:3000` 即可。 - ## Docker Compose 最佳实践 若你使用 docker compose 部署,以下是一些 compose 示例 @@ -169,12 +168,13 @@ networks: | 变量 | 说明 | 可选值 | 默认值 | | ----------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| PASSWORD | 实例访问密码,留空则不启用密码保护 | 任意字符串 | (空) | +| USERNAME | redis 部署时的管理员账号 | 任意字符串 | (空) | +| PASSWORD | 默认部署时为唯一访问密码,redis 部署时为管理员密码 | 任意字符串 | (空) | | SITE_NAME | 站点名称 | 任意字符串 | MoonTV | | ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 | | NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | localstorage(本地浏览器存储)、redis(仅 docker 支持) | localstorage | | REDIS_URL | redis 连接 url,若 NEXT_PUBLIC_STORAGE_TYPE 为 redis 则必填 | 连接 url | 空 | -| NEXT_PUBLIC_ENABLE_REGISTER | 是否开放注册,建议首次运行时设置 true,注册初始账号后可关闭 | true / false | false | +| NEXT_PUBLIC_ENABLE_REGISTER | 是否开放注册,仅在 redis 部署时生效 | true / false | false | | NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 | | NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT | 搜索结果默认是否按标题和年份聚合 | true / false | true | diff --git a/config.json b/config.json index 86de4c6..0984250 100644 --- a/config.json +++ b/config.json @@ -1,19 +1,19 @@ { "cache_time": 7200, "api_site": { - "dyttzy": { - "api": "http://caiji.dyttzyapi.com/api.php/provide/vod", - "name": "电影天堂资源", - "detail": "http://caiji.dyttzyapi.com" + "heimuer": { + "api": "https://json.heimuer.xyz/api.php/provide/vod", + "name": "黑木耳", + "detail": "https://heimuer.tv" }, "ruyi": { "api": "https://cj.rycjapi.com/api.php/provide/vod", "name": "如意资源" }, - "heimuer": { - "api": "https://json.heimuer.xyz/api.php/provide/vod", - "name": "黑木耳", - "detail": "https://heimuer.tv" + "dyttzy": { + "api": "http://caiji.dyttzyapi.com/api.php/provide/vod", + "name": "电影天堂资源", + "detail": "http://caiji.dyttzyapi.com" }, "bfzy": { "api": "https://bfzyapi.com/api.php/provide/vod", diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts index 70e80e8..0bea865 100644 --- a/src/app/api/login/route.ts +++ b/src/app/api/login/route.ts @@ -44,6 +44,14 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: '密码不能为空' }, { status: 400 }); } + // 可能是管理员,直接读环境变量 + if ( + username === process.env.USERNAME && + password === process.env.PASSWORD + ) { + return NextResponse.json({ ok: true }); + } + // 校验用户密码 try { const pass = await db.verifyUser(username, password); diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts index 4a214b2..91eccbb 100644 --- a/src/app/api/register/route.ts +++ b/src/app/api/register/route.ts @@ -34,6 +34,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: '密码不能为空' }, { status: 400 }); } + // 检查是否和管理员重复 + if (username === process.env.USERNAME) { + return NextResponse.json({ error: '用户已存在' }, { status: 400 }); + } + try { // 检查用户是否已存在 const exist = await db.checkUserExist(username); diff --git a/src/lib/refreshRecordAndFavorites.ts b/src/lib/refreshRecordAndFavorites.ts index d183875..8e0da31 100644 --- a/src/lib/refreshRecordAndFavorites.ts +++ b/src/lib/refreshRecordAndFavorites.ts @@ -12,15 +12,18 @@ async function refreshRecordAndFavorites() { try { const users = await db.getAllUsers(); - // 函数级缓存:key 为 `${source}+${id}`,值为 Promise - const detailCache = new Map>(); + if (process.env.USERNAME && !users.includes(process.env.USERNAME)) { + users.push(process.env.USERNAME); + } + // 函数级缓存:key 为 `${source}+${id}`,值为 Promise + const detailCache = new Map>(); - // 获取详情 Promise(带缓存) - const getDetail = ( + // 获取详情 Promise(带缓存和错误处理) + const getDetail = async ( source: string, id: string, fallbackTitle: string - ): Promise => { + ): Promise => { const key = `${source}+${id}`; let promise = detailCache.get(key); if (!promise) { @@ -28,58 +31,123 @@ async function refreshRecordAndFavorites() { source, id, fallbackTitle: fallbackTitle.trim(), - }); - detailCache.set(key, promise); + }) + .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 playRecords = await db.getAllPlayRecords(user); - for (const [key, record] of Object.entries(playRecords)) { - const [source, id] = key.split('+'); - if (!source || !id) continue; + try { + const playRecords = await db.getAllPlayRecords(user); + const totalRecords = Object.keys(playRecords).length; + let processedRecords = 0; - const detail: VideoDetail = await getDetail(source, id, record.title); + for (const [key, record] of Object.entries(playRecords)) { + try { + const [source, id] = key.split('+'); + if (!source || !id) { + console.warn(`跳过无效的播放记录键: ${key}`); + continue; + } - 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 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: 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, + }); + 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); } // 收藏 - const favorites = await db.getAllFavorites(user); - for (const [key, fav] of Object.entries(favorites)) { - const [source, id] = key.split('+'); - if (!source || !id) continue; + try { + const favorites = await db.getAllFavorites(user); + const totalFavorites = Object.keys(favorites).length; + let processedFavorites = 0; - const favDetail: VideoDetail = await getDetail(source, id, fav.title); + for (const [key, fav] of Object.entries(favorites)) { + try { + const [source, id] = key.split('+'); + if (!source || !id) { + console.warn(`跳过无效的收藏键: ${key}`); + continue; + } - 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, - }); + 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: fav.title, + source_name: fav.source_name, + cover: fav.cover, + total_episodes: favEpisodeCount, + save_time: fav.save_time, + }); + console.log( + `更新收藏: ${fav.title} (${fav.total_episodes} -> ${favEpisodeCount})` + ); + } + + processedFavorites++; + } catch (err) { + console.error(`处理收藏失败 (${key}):`, err); + // 继续处理下一个收藏 + } } + + console.log(`收藏处理完成: ${processedFavorites}/${totalFavorites}`); + } catch (err) { + console.error(`获取用户收藏失败 (${user}):`, err); } } + + console.log('刷新播放记录/收藏任务完成'); } catch (err) { - console.error('刷新播放记录/收藏失败', err); + console.error('刷新播放记录/收藏任务启动失败', err); } }