mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-06-10 02:43:12 +08:00
feat: add admin account, add refresh log
This commit is contained in:
@@ -102,7 +102,6 @@ docker run -d --name moontv -p 3000:3000 ghcr.io/senshinya/moontv:latest
|
|||||||
|
|
||||||
访问 `http://服务器 IP:3000` 即可。
|
访问 `http://服务器 IP:3000` 即可。
|
||||||
|
|
||||||
|
|
||||||
## Docker Compose 最佳实践
|
## Docker Compose 最佳实践
|
||||||
|
|
||||||
若你使用 docker compose 部署,以下是一些 compose 示例
|
若你使用 docker compose 部署,以下是一些 compose 示例
|
||||||
@@ -169,12 +168,13 @@ networks:
|
|||||||
|
|
||||||
| 变量 | 说明 | 可选值 | 默认值 |
|
| 变量 | 说明 | 可选值 | 默认值 |
|
||||||
| ----------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| PASSWORD | 实例访问密码,留空则不启用密码保护 | 任意字符串 | (空) |
|
| USERNAME | redis 部署时的管理员账号 | 任意字符串 | (空) |
|
||||||
|
| PASSWORD | 默认部署时为唯一访问密码,redis 部署时为管理员密码 | 任意字符串 | (空) |
|
||||||
| SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
|
| SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
|
||||||
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
|
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
|
||||||
| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | localstorage(本地浏览器存储)、redis(仅 docker 支持) | localstorage |
|
| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | localstorage(本地浏览器存储)、redis(仅 docker 支持) | localstorage |
|
||||||
| REDIS_URL | redis 连接 url,若 NEXT_PUBLIC_STORAGE_TYPE 为 redis 则必填 | 连接 url | 空 |
|
| 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_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 |
|
||||||
| NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT | 搜索结果默认是否按标题和年份聚合 | true / false | true |
|
| NEXT_PUBLIC_AGGREGATE_SEARCH_RESULT | 搜索结果默认是否按标题和年份聚合 | true / false | true |
|
||||||
|
|
||||||
|
|||||||
16
config.json
16
config.json
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"cache_time": 7200,
|
"cache_time": 7200,
|
||||||
"api_site": {
|
"api_site": {
|
||||||
"dyttzy": {
|
"heimuer": {
|
||||||
"api": "http://caiji.dyttzyapi.com/api.php/provide/vod",
|
"api": "https://json.heimuer.xyz/api.php/provide/vod",
|
||||||
"name": "电影天堂资源",
|
"name": "黑木耳",
|
||||||
"detail": "http://caiji.dyttzyapi.com"
|
"detail": "https://heimuer.tv"
|
||||||
},
|
},
|
||||||
"ruyi": {
|
"ruyi": {
|
||||||
"api": "https://cj.rycjapi.com/api.php/provide/vod",
|
"api": "https://cj.rycjapi.com/api.php/provide/vod",
|
||||||
"name": "如意资源"
|
"name": "如意资源"
|
||||||
},
|
},
|
||||||
"heimuer": {
|
"dyttzy": {
|
||||||
"api": "https://json.heimuer.xyz/api.php/provide/vod",
|
"api": "http://caiji.dyttzyapi.com/api.php/provide/vod",
|
||||||
"name": "黑木耳",
|
"name": "电影天堂资源",
|
||||||
"detail": "https://heimuer.tv"
|
"detail": "http://caiji.dyttzyapi.com"
|
||||||
},
|
},
|
||||||
"bfzy": {
|
"bfzy": {
|
||||||
"api": "https://bfzyapi.com/api.php/provide/vod",
|
"api": "https://bfzyapi.com/api.php/provide/vod",
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: '密码不能为空' }, { status: 400 });
|
return NextResponse.json({ error: '密码不能为空' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 可能是管理员,直接读环境变量
|
||||||
|
if (
|
||||||
|
username === process.env.USERNAME &&
|
||||||
|
password === process.env.PASSWORD
|
||||||
|
) {
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
|
|
||||||
// 校验用户密码
|
// 校验用户密码
|
||||||
try {
|
try {
|
||||||
const pass = await db.verifyUser(username, password);
|
const pass = await db.verifyUser(username, password);
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: '密码不能为空' }, { status: 400 });
|
return NextResponse.json({ error: '密码不能为空' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否和管理员重复
|
||||||
|
if (username === process.env.USERNAME) {
|
||||||
|
return NextResponse.json({ error: '用户已存在' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查用户是否已存在
|
// 检查用户是否已存在
|
||||||
const exist = await db.checkUserExist(username);
|
const exist = await db.checkUserExist(username);
|
||||||
|
|||||||
@@ -12,15 +12,18 @@ async function refreshRecordAndFavorites() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const users = await db.getAllUsers();
|
const users = await db.getAllUsers();
|
||||||
// 函数级缓存:key 为 `${source}+${id}`,值为 Promise<VideoDetail>
|
if (process.env.USERNAME && !users.includes(process.env.USERNAME)) {
|
||||||
const detailCache = new Map<string, Promise<VideoDetail>>();
|
users.push(process.env.USERNAME);
|
||||||
|
}
|
||||||
|
// 函数级缓存:key 为 `${source}+${id}`,值为 Promise<VideoDetail | null>
|
||||||
|
const detailCache = new Map<string, Promise<VideoDetail | null>>();
|
||||||
|
|
||||||
// 获取详情 Promise(带缓存)
|
// 获取详情 Promise(带缓存和错误处理)
|
||||||
const getDetail = (
|
const getDetail = async (
|
||||||
source: string,
|
source: string,
|
||||||
id: string,
|
id: string,
|
||||||
fallbackTitle: string
|
fallbackTitle: string
|
||||||
): Promise<VideoDetail> => {
|
): Promise<VideoDetail | null> => {
|
||||||
const key = `${source}+${id}`;
|
const key = `${source}+${id}`;
|
||||||
let promise = detailCache.get(key);
|
let promise = detailCache.get(key);
|
||||||
if (!promise) {
|
if (!promise) {
|
||||||
@@ -28,58 +31,123 @@ async function refreshRecordAndFavorites() {
|
|||||||
source,
|
source,
|
||||||
id,
|
id,
|
||||||
fallbackTitle: fallbackTitle.trim(),
|
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;
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
|
console.log(`开始处理用户: ${user}`);
|
||||||
|
|
||||||
// 播放记录
|
// 播放记录
|
||||||
const playRecords = await db.getAllPlayRecords(user);
|
try {
|
||||||
for (const [key, record] of Object.entries(playRecords)) {
|
const playRecords = await db.getAllPlayRecords(user);
|
||||||
const [source, id] = key.split('+');
|
const totalRecords = Object.keys(playRecords).length;
|
||||||
if (!source || !id) continue;
|
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;
|
const detail = await getDetail(source, id, record.title);
|
||||||
if (episodeCount > 0 && episodeCount !== record.total_episodes) {
|
if (!detail) {
|
||||||
await db.savePlayRecord(user, source, id, {
|
console.warn(`跳过无法获取详情的播放记录: ${key}`);
|
||||||
title: record.title,
|
continue;
|
||||||
source_name: record.source_name,
|
}
|
||||||
cover: record.cover,
|
|
||||||
index: record.index,
|
const episodeCount = detail.episodes?.length || 0;
|
||||||
total_episodes: episodeCount,
|
if (episodeCount > 0 && episodeCount !== record.total_episodes) {
|
||||||
play_time: record.play_time,
|
await db.savePlayRecord(user, source, id, {
|
||||||
total_time: record.total_time,
|
title: record.title,
|
||||||
save_time: record.save_time,
|
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);
|
try {
|
||||||
for (const [key, fav] of Object.entries(favorites)) {
|
const favorites = await db.getAllFavorites(user);
|
||||||
const [source, id] = key.split('+');
|
const totalFavorites = Object.keys(favorites).length;
|
||||||
if (!source || !id) continue;
|
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;
|
const favDetail = await getDetail(source, id, fav.title);
|
||||||
if (favEpisodeCount > 0 && favEpisodeCount !== fav.total_episodes) {
|
if (!favDetail) {
|
||||||
await db.saveFavorite(user, source, id, {
|
console.warn(`跳过无法获取详情的收藏: ${key}`);
|
||||||
title: fav.title,
|
continue;
|
||||||
source_name: fav.source_name,
|
}
|
||||||
cover: fav.cover,
|
|
||||||
total_episodes: favEpisodeCount,
|
const favEpisodeCount = favDetail.episodes?.length || 0;
|
||||||
save_time: fav.save_time,
|
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) {
|
} catch (err) {
|
||||||
console.error('刷新播放记录/收藏失败', err);
|
console.error('刷新播放记录/收藏任务启动失败', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user