Date: Wed, 24 Dec 2025 00:42:28 +0800
Subject: [PATCH 23/40] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E6=96=B0=E7=94=A8=E6=88=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/admin/page.tsx | 3 +-
.../api/admin/data_migration/export/route.ts | 36 +++++++++-
.../api/admin/data_migration/import/route.ts | 65 ++++++++++++++++++-
src/lib/config.ts | 25 +++++++
src/lib/db.ts | 1 +
src/lib/upstash.db.ts | 3 +
6 files changed, 127 insertions(+), 6 deletions(-)
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 5cdfb98..92943c3 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -427,6 +427,7 @@ interface UserConfigProps {
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
+ oidcSub?: string;
enabledApis?: string[];
created_at: number;
}> | null;
@@ -1453,7 +1454,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
{user.username}
- {(user as any).oidcSub && (
+ {user.oidcSub && (
OIDC
diff --git a/src/app/api/admin/data_migration/export/route.ts b/src/app/api/admin/data_migration/export/route.ts
index 1ccb281..77ea112 100644
--- a/src/app/api/admin/data_migration/export/route.ts
+++ b/src/app/api/admin/data_migration/export/route.ts
@@ -54,14 +54,26 @@ export async function POST(req: NextRequest) {
// 管理员配置
adminConfig: config,
// 所有用户数据
- userData: {} as { [username: string]: any }
+ userData: {} as { [username: string]: any },
+ // V2用户信息
+ usersV2: [] as any[]
}
};
- // 获取所有用户
+ // 获取所有V2用户
+ const usersV2Result = await db.getUserListV2(0, 10000, process.env.USERNAME);
+ exportData.data.usersV2 = usersV2Result.users;
+
+ // 获取所有用户(包括旧版用户)
let allUsers = await db.getAllUsers();
// 添加站长用户
allUsers.push(process.env.USERNAME);
+ // 添加V2用户
+ usersV2Result.users.forEach(user => {
+ if (!allUsers.includes(user.username)) {
+ allUsers.push(user.username);
+ }
+ });
allUsers = Array.from(new Set(allUsers));
// 为每个用户收集数据
@@ -76,7 +88,9 @@ export async function POST(req: NextRequest) {
// 跳过片头片尾配置
skipConfigs: await db.getAllSkipConfigs(username),
// 用户密码(通过验证空密码来检查用户是否存在,然后获取密码)
- password: await getUserPassword(username)
+ password: await getUserPassword(username),
+ // V2用户的加密密码
+ passwordV2: await getUserPasswordV2(username)
};
exportData.data.userData[username] = userData;
@@ -134,3 +148,19 @@ async function getUserPassword(username: string): Promise {
return null;
}
}
+
+// 辅助函数:获取V2用户的加密密码
+async function getUserPasswordV2(username: string): Promise {
+ try {
+ const storage = (db as any).storage;
+ if (storage && typeof storage.client?.hget === 'function') {
+ const userInfoKey = `user:${username}:info`;
+ const password = await storage.client.hget(userInfoKey, 'password');
+ return password;
+ }
+ return null;
+ } catch (error) {
+ console.error(`获取用户 ${username} V2密码失败:`, error);
+ return null;
+ }
+}
diff --git a/src/app/api/admin/data_migration/import/route.ts b/src/app/api/admin/data_migration/import/route.ts
index 73aafcb..7b246ac 100644
--- a/src/app/api/admin/data_migration/import/route.ts
+++ b/src/app/api/admin/data_migration/import/route.ts
@@ -94,13 +94,73 @@ export async function POST(req: NextRequest) {
// 不影响主流程,继续执行
}
+ // 导入V2用户信息
+ if (importData.data.usersV2 && Array.isArray(importData.data.usersV2)) {
+ for (const userV2 of importData.data.usersV2) {
+ try {
+ // 跳过站长(站长使用环境变量认证)
+ if (userV2.role === 'owner') {
+ continue;
+ }
+
+ // 获取用户的加密密码
+ const userData = importData.data.userData[userV2.username];
+ const passwordV2 = userData?.passwordV2;
+
+ if (passwordV2) {
+ // 直接使用加密后的密码创建用户
+ const storage = (db as any).storage;
+ if (storage && typeof storage.client?.hset === 'function') {
+ const userInfoKey = `user:${userV2.username}:info`;
+ const createdAt = userV2.created_at || Date.now();
+
+ const userInfo: any = {
+ role: userV2.role,
+ banned: userV2.banned,
+ password: passwordV2,
+ created_at: createdAt.toString(),
+ };
+
+ if (userV2.tags && userV2.tags.length > 0) {
+ userInfo.tags = JSON.stringify(userV2.tags);
+ }
+
+ if (userV2.oidcSub) {
+ userInfo.oidcSub = userV2.oidcSub;
+ // 创建OIDC映射
+ const oidcSubKey = `oidc:sub:${userV2.oidcSub}`;
+ await storage.client.set(oidcSubKey, userV2.username);
+ }
+
+ if (userV2.enabledApis && userV2.enabledApis.length > 0) {
+ userInfo.enabledApis = JSON.stringify(userV2.enabledApis);
+ }
+
+ await storage.client.hset(userInfoKey, userInfo);
+
+ // 添加到用户列表(Sorted Set)
+ const userListKey = 'user:list';
+ await storage.client.zadd(userListKey, {
+ score: createdAt,
+ member: userV2.username,
+ });
+
+ console.log(`V2用户 ${userV2.username} 导入成功`);
+ }
+ }
+ } catch (error) {
+ console.error(`导入V2用户 ${userV2.username} 失败:`, error);
+ }
+ }
+ }
+
// 导入用户数据
const userData = importData.data.userData;
for (const username in userData) {
const user = userData[username];
- // 重新注册用户(包含密码)
- if (user.password) {
+ // 重新注册用户(包含密码)- 仅用于旧版用户
+ if (user.password && !importData.data.usersV2?.find((u: any) => u.username === username)) {
await db.registerUser(username, user.password);
}
@@ -139,6 +199,7 @@ export async function POST(req: NextRequest) {
return NextResponse.json({
message: '数据导入成功',
importedUsers: Object.keys(userData).length,
+ importedUsersV2: importData.data.usersV2?.length || 0,
timestamp: importData.timestamp,
serverVersion: typeof importData.serverVersion === 'string' ? importData.serverVersion : '未知版本'
});
diff --git a/src/lib/config.ts b/src/lib/config.ts
index 67e5432..bfbebbf 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -328,6 +328,31 @@ export async function getConfig(): Promise {
}
adminConfig = configSelfCheck(adminConfig);
cachedConfig = adminConfig;
+
+ // 自动迁移用户(如果配置中有用户且V2存储支持)
+ // 过滤掉站长后检查是否有需要迁移的用户
+ const nonOwnerUsers = adminConfig.UserConfig.Users.filter(
+ (u) => u.username !== process.env.USERNAME
+ );
+ if (!dbReadFailed && nonOwnerUsers.length > 0) {
+ try {
+ // 检查是否支持V2存储
+ const storage = (db as any).storage;
+ if (storage && typeof storage.createUserV2 === 'function') {
+ console.log('检测到配置中有用户,开始自动迁移...');
+ await db.migrateUsersFromConfig(adminConfig);
+ // 迁移完成后,清空配置中的用户列表并保存
+ adminConfig.UserConfig.Users = [];
+ await db.saveAdminConfig(adminConfig);
+ cachedConfig = adminConfig;
+ console.log('用户自动迁移完成');
+ }
+ } catch (error) {
+ console.error('自动迁移用户失败:', error);
+ // 不影响主流程,继续执行
+ }
+ }
+
return cachedConfig;
}
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 7291d6a..06c12ce 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -234,6 +234,7 @@ export class DbManager {
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
+ oidcSub?: string;
enabledApis?: string[];
created_at: number;
}>;
diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts
index 4044f72..6580d9c 100644
--- a/src/lib/upstash.db.ts
+++ b/src/lib/upstash.db.ts
@@ -504,6 +504,7 @@ export class UpstashRedisStorage implements IStorage {
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
+ oidcSub?: string;
enabledApis?: string[];
created_at: number;
}>;
@@ -528,6 +529,7 @@ export class UpstashRedisStorage implements IStorage {
role: 'owner' as const,
banned: ownerInfo.banned,
tags: ownerInfo.tags,
+ oidcSub: ownerInfo.oidcSub,
enabledApis: ownerInfo.enabledApis,
created_at: ownerInfo.created_at,
});
@@ -549,6 +551,7 @@ export class UpstashRedisStorage implements IStorage {
role: userInfo.role,
banned: userInfo.banned,
tags: userInfo.tags,
+ oidcSub: userInfo.oidcSub,
enabledApis: userInfo.enabledApis,
created_at: userInfo.created_at,
});
From 3112e99395eea31bad48c56843f6705e789bdd66 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 00:55:37 +0800
Subject: [PATCH 24/40] =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=8A=A0=E9=94=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/api/register/route.ts | 144 ++++++++++++++++------------------
src/lib/lock.ts | 95 ++++++++++++++++++++++
src/lib/upstash.db.ts | 8 ++
3 files changed, 170 insertions(+), 77 deletions(-)
create mode 100644 src/lib/lock.ts
diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts
index 7240d97..543df9f 100644
--- a/src/app/api/register/route.ts
+++ b/src/app/api/register/route.ts
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';
+import { lockManager } from '@/lib/lock';
export const runtime = 'nodejs';
@@ -93,89 +94,78 @@ export async function POST(req: NextRequest) {
);
}
- // 检查用户是否已存在(优先使用新版本)
- let userExists = await db.checkUserExistV2(username);
- if (!userExists) {
- // 回退到旧版本检查
- userExists = await db.checkUserExist(username);
- }
- if (userExists) {
- return NextResponse.json(
- { error: '用户名已存在' },
- { status: 409 }
- );
- }
-
- // 检查配置中是否已存在
- const existingUser = config.UserConfig.Users.find((u) => u.username === username);
- if (existingUser) {
- return NextResponse.json(
- { error: '用户名已存在' },
- { status: 409 }
- );
- }
-
- // 如果开启了Turnstile验证
- if (siteConfig.RegistrationRequireTurnstile) {
- if (!turnstileToken) {
- return NextResponse.json(
- { error: '请完成人机验证' },
- { status: 400 }
- );
- }
-
- if (!siteConfig.TurnstileSecretKey) {
- console.error('Turnstile Secret Key未配置');
- return NextResponse.json(
- { error: '服务器配置错误' },
- { status: 500 }
- );
- }
-
- // 验证Turnstile Token
- const isValid = await verifyTurnstileToken(turnstileToken, siteConfig.TurnstileSecretKey);
- if (!isValid) {
- return NextResponse.json(
- { error: '人机验证失败,请重试' },
- { status: 400 }
- );
- }
- }
-
- // 创建用户
+ // 获取用户名锁,防止并发注册
+ let releaseLock: (() => void) | null = null;
try {
- // 1. 使用新版本创建用户(带SHA256加密)
- const defaultTags = siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0
- ? siteConfig.DefaultUserTags
- : undefined;
+ releaseLock = await lockManager.acquire(`register:${username}`);
+ } catch (error) {
+ return NextResponse.json(
+ { error: '服务器繁忙,请稍后重试' },
+ { status: 503 }
+ );
+ }
- await db.createUserV2(username, password, 'user', defaultTags);
-
- // 2. 同时在旧版本存储中创建(保持兼容性)
- await db.registerUser(username, password);
-
- // 3. 将用户添加到管理员配置的用户列表中(保持兼容性)
- const newUser: any = {
- username: username,
- role: 'user',
- banned: false,
- };
-
- // 4. 如果配置了默认用户组,分配给新用户
- if (defaultTags) {
- newUser.tags = defaultTags;
+ try {
+ // 检查用户是否已存在(只检查V2存储)
+ const userExists = await db.checkUserExistV2(username);
+ if (userExists) {
+ return NextResponse.json(
+ { error: '用户名已存在' },
+ { status: 409 }
+ );
}
- config.UserConfig.Users.push(newUser);
+ // 如果开启了Turnstile验证
+ if (siteConfig.RegistrationRequireTurnstile) {
+ if (!turnstileToken) {
+ return NextResponse.json(
+ { error: '请完成人机验证' },
+ { status: 400 }
+ );
+ }
- // 5. 保存更新后的配置
- await db.saveAdminConfig(config);
+ if (!siteConfig.TurnstileSecretKey) {
+ console.error('Turnstile Secret Key未配置');
+ return NextResponse.json(
+ { error: '服务器配置错误' },
+ { status: 500 }
+ );
+ }
- // 注册成功
- return NextResponse.json({ ok: true, message: '注册成功' });
- } catch (err) {
- console.error('创建用户失败', err);
- return NextResponse.json({ error: '注册失败,请稍后重试' }, { status: 500 });
+ // 验证Turnstile Token
+ const isValid = await verifyTurnstileToken(turnstileToken, siteConfig.TurnstileSecretKey);
+ if (!isValid) {
+ return NextResponse.json(
+ { error: '人机验证失败,请重试' },
+ { status: 400 }
+ );
+ }
+ }
+
+ // 创建用户
+ try {
+ // 使用新版本创建用户(带SHA256加密)
+ const defaultTags = siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0
+ ? siteConfig.DefaultUserTags
+ : undefined;
+
+ await db.createUserV2(username, password, 'user', defaultTags);
+
+ // 注册成功
+ return NextResponse.json({ ok: true, message: '注册成功' });
+ } catch (err: any) {
+ console.error('创建用户失败', err);
+ // 如果是用户已存在的错误,返回409
+ if (err.message === '用户已存在') {
+ return NextResponse.json({ error: '用户名已存在' }, { status: 409 });
+ }
+ return NextResponse.json({ error: '注册失败,请稍后重试' }, { status: 500 });
+ }
+ } finally {
+ // 释放锁
+ if (releaseLock) {
+ releaseLock();
+ }
}
} catch (error) {
console.error('注册接口异常', error);
diff --git a/src/lib/lock.ts b/src/lib/lock.ts
new file mode 100644
index 0000000..1bceac7
--- /dev/null
+++ b/src/lib/lock.ts
@@ -0,0 +1,95 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+// 简单的内存锁管理器
+class LockManager {
+ private locks: Map void> }> = new Map();
+ private readonly LOCK_TIMEOUT = 10000; // 10秒超时
+
+ async acquire(key: string): Promise<() => void> {
+ // 获取或创建锁对象
+ if (!this.locks.has(key)) {
+ this.locks.set(key, { locked: false, queue: [] });
+ }
+
+ const lock = this.locks.get(key)!;
+
+ // 如果锁未被占用,立即获取
+ if (!lock.locked) {
+ lock.locked = true;
+
+ // 设置超时自动释放
+ const timeoutId = setTimeout(() => {
+ this.release(key);
+ }, this.LOCK_TIMEOUT);
+
+ // 返回释放函数
+ return () => {
+ clearTimeout(timeoutId);
+ this.release(key);
+ };
+ }
+
+ // 如果锁已被占用,等待
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ // 超时,从队列中移除
+ const index = lock.queue.indexOf(callback);
+ if (index > -1) {
+ lock.queue.splice(index, 1);
+ }
+ reject(new Error('获取锁超时'));
+ }, this.LOCK_TIMEOUT);
+
+ const callback = () => {
+ clearTimeout(timeoutId);
+ lock.locked = true;
+
+ // 设置超时自动释放
+ const lockTimeoutId = setTimeout(() => {
+ this.release(key);
+ }, this.LOCK_TIMEOUT);
+
+ resolve(() => {
+ clearTimeout(lockTimeoutId);
+ this.release(key);
+ });
+ };
+
+ lock.queue.push(callback);
+ });
+ }
+
+ private release(key: string): void {
+ const lock = this.locks.get(key);
+ if (!lock) return;
+
+ // 如果队列中有等待者,唤醒下一个
+ if (lock.queue.length > 0) {
+ const next = lock.queue.shift();
+ if (next) {
+ next();
+ }
+ } else {
+ // 没有等待者,释放锁
+ lock.locked = false;
+ // 清理空的锁对象
+ this.locks.delete(key);
+ }
+ }
+
+ // 清理所有锁(用于测试或重置)
+ clear(): void {
+ this.locks.clear();
+ }
+}
+
+// 全局单例
+const globalKey = Symbol.for('__MOONTV_LOCK_MANAGER__');
+let lockManager: LockManager | undefined = (global as any)[globalKey];
+
+if (!lockManager) {
+ lockManager = new LockManager();
+ (global as any)[globalKey] = lockManager;
+}
+
+export { lockManager };
diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts
index 6580d9c..bd76783 100644
--- a/src/lib/upstash.db.ts
+++ b/src/lib/upstash.db.ts
@@ -252,6 +252,14 @@ export class UpstashRedisStorage implements IStorage {
oidcSub?: string,
enabledApis?: string[]
): Promise {
+ // 先检查用户是否已存在(原子性检查)
+ const exists = await withRetry(() =>
+ this.client.exists(this.userInfoKey(userName))
+ );
+ if (exists === 1) {
+ throw new Error('用户已存在');
+ }
+
const hashedPassword = await this.hashPassword(password);
const createdAt = Date.now();
From b64ce1c3f2c7c9b750dd58945c2d3858a8c18df6 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 09:47:46 +0800
Subject: [PATCH 25/40] =?UTF-8?q?=E7=A7=81=E4=BA=BA=E5=BD=B1=E5=BA=93?=
=?UTF-8?q?=E8=AF=A6=E6=83=85=E4=B8=8D=E5=86=8D=E4=BB=8Ehttp=E5=86=85?=
=?UTF-8?q?=E9=83=A8=E8=B0=83=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/api/detail/route.ts | 102 +++++++++++++++++++++++++++---------
1 file changed, 78 insertions(+), 24 deletions(-)
diff --git a/src/app/api/detail/route.ts b/src/app/api/detail/route.ts
index 580cd4a..8a03620 100644
--- a/src/app/api/detail/route.ts
+++ b/src/app/api/detail/route.ts
@@ -31,12 +31,12 @@ export async function GET(request: NextRequest) {
}
const rootPath = openListConfig.RootPath || '/';
+ const folderPath = `${rootPath}${rootPath.endsWith('/') ? '' : '/'}${id}`;
// 1. 读取 metainfo 获取元数据
let metaInfo: any = null;
try {
const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache');
- const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
const { db } = await import('@/lib/db');
metaInfo = getCachedMetaInfo(rootPath);
@@ -49,51 +49,105 @@ export async function GET(request: NextRequest) {
}
}
} catch (error) {
- console.error('[Detail] 从数据库读取 metainfo 失败:', error);
+ // 忽略错误
}
- // 2. 调用 openlist detail API
- const openlistResponse = await fetch(
- `${request.headers.get('x-forwarded-proto') || 'http'}://${request.headers.get('host')}/api/openlist/detail?folder=${encodeURIComponent(id)}`,
- {
- headers: {
- Cookie: request.headers.get('cookie') || '',
- },
- }
+ // 2. 直接调用 OpenList 客户端获取视频列表
+ const { OpenListClient } = await import('@/lib/openlist.client');
+ const { getCachedVideoInfo, setCachedVideoInfo } = await import('@/lib/openlist-cache');
+ const { parseVideoFileName } = await import('@/lib/video-parser');
+
+ const client = new OpenListClient(
+ openListConfig.URL,
+ openListConfig.Username,
+ openListConfig.Password
);
- if (!openlistResponse.ok) {
- throw new Error('获取 OpenList 视频详情失败');
+ let videoInfo = getCachedVideoInfo(folderPath);
+
+ if (!videoInfo) {
+ try {
+ const videoinfoPath = `${folderPath}/videoinfo.json`;
+ const fileResponse = await client.getFile(videoinfoPath);
+
+ if (fileResponse.code === 200 && fileResponse.data.raw_url) {
+ const contentResponse = await fetch(fileResponse.data.raw_url);
+ const content = await contentResponse.text();
+ videoInfo = JSON.parse(content);
+ if (videoInfo) {
+ setCachedVideoInfo(folderPath, videoInfo);
+ }
+ }
+ } catch (error) {
+ // 忽略错误
+ }
}
- const openlistData = await openlistResponse.json();
+ const listResponse = await client.listDirectory(folderPath);
- if (!openlistData.success) {
- throw new Error(openlistData.error || '获取视频详情失败');
+ if (listResponse.code !== 200) {
+ throw new Error('OpenList 列表获取失败');
}
+ const videoExtensions = ['.mp4', '.mkv', '.avi', '.m3u8', '.flv', '.ts', '.mov', '.wmv', '.webm', '.rmvb', '.rm', '.mpg', '.mpeg', '.3gp', '.f4v', '.m4v', '.vob'];
+ const videoFiles = listResponse.data.content.filter((item) => {
+ if (item.is_dir || item.name.startsWith('.') || item.name.endsWith('.json')) return false;
+ return videoExtensions.some(ext => item.name.toLowerCase().endsWith(ext));
+ });
+
+ if (!videoInfo) {
+ videoInfo = { episodes: {}, last_updated: Date.now() };
+ videoFiles.sort((a, b) => a.name.localeCompare(b.name));
+ for (let i = 0; i < videoFiles.length; i++) {
+ const file = videoFiles[i];
+ const parsed = parseVideoFileName(file.name);
+ videoInfo.episodes[file.name] = {
+ episode: parsed.episode || (i + 1),
+ season: parsed.season,
+ title: parsed.title,
+ parsed_from: 'filename',
+ };
+ }
+ setCachedVideoInfo(folderPath, videoInfo);
+ }
+
+ const episodes = videoFiles
+ .map((file, index) => {
+ const parsed = parseVideoFileName(file.name);
+ let episodeInfo;
+ if (parsed.episode) {
+ episodeInfo = { episode: parsed.episode, season: parsed.season, title: parsed.title, parsed_from: 'filename' };
+ } else {
+ episodeInfo = videoInfo!.episodes[file.name] || { episode: index + 1, season: undefined, title: undefined, parsed_from: 'filename' };
+ }
+ let displayTitle = episodeInfo.title;
+ if (!displayTitle && episodeInfo.episode) {
+ displayTitle = `第${episodeInfo.episode}集`;
+ }
+ if (!displayTitle) {
+ displayTitle = file.name;
+ }
+ return { fileName: file.name, episode: episodeInfo.episode || 0, season: episodeInfo.season, title: displayTitle };
+ })
+ .sort((a, b) => a.episode !== b.episode ? a.episode - b.episode : a.fileName.localeCompare(b.fileName));
+
// 3. 从 metainfo 中获取元数据
const folderMeta = metaInfo?.folders?.[id];
const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
- // 转换为标准格式(使用懒加载 URL)
const result = {
source: 'openlist',
source_name: '私人影库',
- id: openlistData.folder,
- title: folderMeta?.title || openlistData.folder,
+ id: id,
+ title: folderMeta?.title || id,
poster: folderMeta?.poster_path ? getTMDBImageUrl(folderMeta.poster_path) : '',
year: folderMeta?.release_date ? folderMeta.release_date.split('-')[0] : '',
douban_id: 0,
desc: folderMeta?.overview || '',
- episodes: openlistData.episodes.map((ep: any) =>
- `/api/openlist/play?folder=${encodeURIComponent(openlistData.folder)}&fileName=${encodeURIComponent(ep.fileName)}`
- ),
- episodes_titles: openlistData.episodes.map((ep: any) => ep.title || `第${ep.episode}集`),
+ episodes: episodes.map((ep) => `/api/openlist/play?folder=${encodeURIComponent(id)}&fileName=${encodeURIComponent(ep.fileName)}`),
+ episodes_titles: episodes.map((ep) => ep.title),
};
- console.log('[Detail] result.episodes_titles:', result.episodes_titles);
-
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
From 8cf8eac55f97c689f3d49cfb093c17f13d0e607d Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 09:55:58 +0800
Subject: [PATCH 26/40] fix typeerror
---
src/app/admin/page.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 92943c3..130c3a3 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -507,6 +507,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
enabledApis?: string[];
tags?: string[];
created_at?: number;
+ oidcSub?: string;
}> = !hasOldUserData && usersV2 ? usersV2 : (config?.UserConfig?.Users || []);
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算
From 21baa51fe7f183b6725325c8d3795cc7cc18cc5d Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 10:29:26 +0800
Subject: [PATCH 27/40] =?UTF-8?q?=E7=A7=BB=E9=99=A4oidc=E5=AF=B9=E6=97=A7?=
=?UTF-8?q?=E7=89=88=E6=B3=A8=E5=86=8C=E7=9A=84=E5=85=BC=E5=AE=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../api/auth/oidc/complete-register/route.ts | 21 -------------------
1 file changed, 21 deletions(-)
diff --git a/src/app/api/auth/oidc/complete-register/route.ts b/src/app/api/auth/oidc/complete-register/route.ts
index 7825353..bd11e0f 100644
--- a/src/app/api/auth/oidc/complete-register/route.ts
+++ b/src/app/api/auth/oidc/complete-register/route.ts
@@ -161,27 +161,6 @@ export async function POST(request: NextRequest) {
// 使用新版本创建用户(带SHA256加密和OIDC绑定)
await db.createUserV2(username, randomPassword, 'user', defaultTags, oidcSession.sub);
- // 同时在旧版本存储中创建(保持兼容性)
- await db.registerUser(username, randomPassword);
-
- // 将用户添加到配置中(保持兼容性)
- const newUser: any = {
- username: username,
- role: 'user',
- banned: false,
- oidcSub: oidcSession.sub, // 保存OIDC标识符
- };
-
- // 如果配置了默认用户组,分配给新用户
- if (defaultTags) {
- newUser.tags = defaultTags;
- }
-
- config.UserConfig.Users.push(newUser);
-
- // 保存配置
- await db.saveAdminConfig(config);
-
// 设置认证cookie
const response = NextResponse.json({ ok: true, message: '注册成功' });
const cookieValue = await generateAuthCookie(username, 'user');
From 9e6c98a8ff5aa0206970ef37e63e23eb92583337 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 11:53:32 +0800
Subject: [PATCH 28/40] =?UTF-8?q?=E7=A7=81=E4=BA=BA=E5=BD=B1=E5=BA=93?=
=?UTF-8?q?=E6=8D=A2=E6=BA=90=E4=B8=8D=E6=B5=8B=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/EpisodeSelector.tsx | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx
index 9a0b4d9..7e13ce5 100644
--- a/src/components/EpisodeSelector.tsx
+++ b/src/components/EpisodeSelector.tsx
@@ -266,7 +266,8 @@ const EpisodeSelector: React.FC = ({
if (
!optimizationEnabled || // 若关闭测速则直接退出
activeTab !== 'sources' ||
- availableSources.length === 0
+ availableSources.length === 0 ||
+ currentSource === 'openlist' // 私人影库不进行测速
)
return;
@@ -293,7 +294,7 @@ const EpisodeSelector: React.FC = ({
fetchVideoInfosInBatches();
// 依赖项保持与之前一致
- }, [activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted]);
+ }, [activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]);
// 升序分页标签
const categoriesAsc = useMemo(() => {
@@ -848,6 +849,11 @@ const EpisodeSelector: React.FC = ({
{/* 重新测试按钮 */}
{(() => {
+ // 私人影库不显示重新测试按钮
+ if (source.source === 'openlist') {
+ return null;
+ }
+
const sourceKey = `${source.source}-${source.id}`;
const isTesting = retestingSources.has(sourceKey);
const videoInfo = videoInfoMap.get(sourceKey);
From 187e6f1bd432da3f7fb26a795a0c10513fb7b11a Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 20:05:58 +0800
Subject: [PATCH 29/40] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A7=81=E4=BA=BA?=
=?UTF-8?q?=E5=BD=B1=E5=BA=93=E4=BB=8E=E6=90=9C=E7=B4=A2=E8=BF=9B=E6=97=A0?=
=?UTF-8?q?=E6=B3=95=E6=92=AD=E6=94=BE=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AE=8C?=
=?UTF-8?q?=E7=BB=93=E6=A0=87=E8=AF=86=E5=88=A4=E6=96=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/api/search/route.ts | 57 ++++++++++++++++++++++++++++-
src/app/play/page.tsx | 71 ++++++++++++++++++++++++++-----------
2 files changed, 106 insertions(+), 22 deletions(-)
diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts
index 229c62c..2b5a0ca 100644
--- a/src/app/api/search/route.ts
+++ b/src/app/api/search/route.ts
@@ -36,6 +36,61 @@ export async function GET(request: NextRequest) {
const config = await getConfig();
const apiSites = await getAvailableApiSites(authInfo.username);
+ // 检查是否配置了 OpenList
+ const hasOpenList = !!(config.OpenListConfig?.URL && config.OpenListConfig?.Username && config.OpenListConfig?.Password);
+
+ // 搜索 OpenList(如果配置了)
+ let openlistResults: any[] = [];
+ if (hasOpenList) {
+ try {
+ const { getCachedMetaInfo, setCachedMetaInfo } = await import('@/lib/openlist-cache');
+ const { getTMDBImageUrl } = await import('@/lib/tmdb.search');
+ const { db } = await import('@/lib/db');
+
+ const rootPath = config.OpenListConfig!.RootPath || '/';
+ let metaInfo = getCachedMetaInfo(rootPath);
+
+ // 如果没有缓存,尝试从数据库读取
+ if (!metaInfo) {
+ try {
+ const metainfoJson = await db.getGlobalValue('video.metainfo');
+ if (metainfoJson) {
+ metaInfo = JSON.parse(metainfoJson);
+ if (metaInfo) {
+ setCachedMetaInfo(rootPath, metaInfo);
+ }
+ }
+ } catch (error) {
+ console.error('[Search] 从数据库读取 metainfo 失败:', error);
+ }
+ }
+
+ if (metaInfo && metaInfo.folders) {
+ openlistResults = Object.entries(metaInfo.folders)
+ .filter(([folderName, info]: [string, any]) => {
+ const matchFolder = folderName.toLowerCase().includes(query.toLowerCase());
+ const matchTitle = info.title.toLowerCase().includes(query.toLowerCase());
+ return matchFolder || matchTitle;
+ })
+ .map(([folderName, info]: [string, any]) => ({
+ id: folderName,
+ source: 'openlist',
+ source_name: '私人影库',
+ title: info.title,
+ poster: getTMDBImageUrl(info.poster_path),
+ episodes: [],
+ episodes_titles: [],
+ year: info.release_date.split('-')[0] || '',
+ desc: info.overview,
+ type_name: info.media_type === 'movie' ? '电影' : '电视剧',
+ douban_id: 0,
+ }));
+ }
+ } catch (error) {
+ console.error('[Search] 搜索 OpenList 失败:', error);
+ }
+ }
+
// 添加超时控制和错误处理,避免慢接口拖累整体响应
const searchPromises = apiSites.map((site) =>
Promise.race([
@@ -54,7 +109,7 @@ export async function GET(request: NextRequest) {
const successResults = results
.filter((result) => result.status === 'fulfilled')
.map((result) => (result as PromiseFulfilledResult).value);
- let flattenedResults = successResults.flat();
+ let flattenedResults = [...openlistResults, ...successResults.flat()];
if (!config.SiteConfig.DisableYellowFilter) {
flattenedResults = flattenedResults.filter((result) => {
const typeName = result.type_name || '';
diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx
index ea15678..4ba0b0f 100644
--- a/src/app/play/page.tsx
+++ b/src/app/play/page.tsx
@@ -606,9 +606,9 @@ function PlayPageClient() {
// 工具函数(Utils)
// -----------------------------------------------------------------------------
- // 判断剧集是否已完结
- const isSeriesCompleted = (detail: SearchResult | null): boolean => {
- if (!detail) return false;
+ // 判断剧集状态
+ const getSeriesStatus = (detail: SearchResult | null): 'completed' | 'ongoing' | 'unknown' => {
+ if (!detail) return 'unknown';
// 方法1:通过 vod_remarks 判断
if (detail.vod_remarks) {
@@ -620,23 +620,27 @@ function PlayPageClient() {
// 如果包含连载关键词,则为连载中
if (ongoingKeywords.some(keyword => remarks.includes(keyword))) {
- return false;
+ return 'ongoing';
}
// 如果包含完结关键词,则为已完结
if (completedKeywords.some(keyword => remarks.includes(keyword))) {
- return true;
+ return 'completed';
}
}
// 方法2:通过 vod_total 和实际集数对比判断
if (detail.vod_total && detail.vod_total > 0 && detail.episodes && detail.episodes.length > 0) {
// 如果实际集数 >= 总集数,则为已完结
- return detail.episodes.length >= detail.vod_total;
+ if (detail.episodes.length >= detail.vod_total) {
+ return 'completed';
+ }
+ // 如果实际集数 < 总集数,则为连载中
+ return 'ongoing';
}
- // 无法判断,默认返回 false(连载中)
- return false;
+ // 无法判断,返回 unknown
+ return 'unknown';
};
// 播放源优选函数
@@ -1776,7 +1780,9 @@ function PlayPageClient() {
? result.year.toLowerCase() === videoYearRef.current.toLowerCase()
: true) &&
(searchType
- ? (searchType === 'tv' && result.episodes.length > 1) ||
+ ? // openlist 源跳过 episodes 长度检查,因为搜索时不返回详细播放列表
+ result.source === 'openlist' ||
+ (searchType === 'tv' && result.episodes.length > 1) ||
(searchType === 'movie' && result.episodes.length === 1)
: true)
);
@@ -1829,6 +1835,15 @@ function PlayPageClient() {
);
if (target) {
detailData = target;
+
+ // 如果是 openlist 源且 episodes 为空,需要调用 detail 接口获取完整信息
+ if (detailData.source === 'openlist' && (!detailData.episodes || detailData.episodes.length === 0)) {
+ console.log('[Play] OpenList source has no episodes, fetching detail...');
+ const detailSources = await fetchSourceDetail(currentSource, currentId);
+ if (detailSources.length > 0) {
+ detailData = detailSources[0];
+ }
+ }
} else {
setError('未找到匹配结果');
setLoading(false);
@@ -1849,6 +1864,15 @@ function PlayPageClient() {
console.log(detailData.source, detailData.id);
+ // 如果是 openlist 源且 episodes 为空,需要调用 detail 接口获取完整信息
+ if (detailData.source === 'openlist' && (!detailData.episodes || detailData.episodes.length === 0)) {
+ console.log('[Play] OpenList source has no episodes after selection, fetching detail...');
+ const detailSources = await fetchSourceDetail(detailData.source, detailData.id);
+ if (detailSources.length > 0) {
+ detailData = detailSources[0];
+ }
+ }
+
setNeedPrefer(false);
setCurrentSource(detailData.source);
setCurrentId(detailData.id);
@@ -2841,7 +2865,7 @@ function PlayPageClient() {
total_episodes: detailRef.current?.episodes.length || 1,
save_time: Date.now(),
search_title: searchTitle,
- is_completed: isSeriesCompleted(detailRef.current),
+ is_completed: getSeriesStatus(detailRef.current) === 'completed',
vod_remarks: detailRef.current?.vod_remarks,
});
setFavorited(true);
@@ -4408,17 +4432,22 @@ function PlayPageClient() {
)}
{/* 完结状态标识 */}
- {detail && totalEpisodes > 1 && (
-
- {isSeriesCompleted(detail) ? '已完结' : '连载中'}
-
- )}
+ {detail && totalEpisodes > 1 && (() => {
+ const status = getSeriesStatus(detail);
+ if (status === 'unknown') return null;
+
+ return (
+
+ {status === 'completed' ? '已完结' : '连载中'}
+
+ );
+ })()}
{/* 第二行:播放器和选集 */}
From 558ba174fef21d3d50dbbbe25db99b9bf0d64835 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 20:13:09 +0800
Subject: [PATCH 30/40] =?UTF-8?q?=E6=96=B0=E7=89=88=E7=94=A8=E6=88=B7?=
=?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=B0=86=E7=AB=99=E9=95=BF=E8=BD=AC=E6=8D=A2?=
=?UTF-8?q?=E4=B8=BA=E6=99=AE=E9=80=9A=E8=B4=A6=E5=8F=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../api/admin/data_migration/import/route.ts | 13 ++++++++++---
src/lib/db.ts | 17 +++++++++--------
2 files changed, 19 insertions(+), 11 deletions(-)
diff --git a/src/app/api/admin/data_migration/import/route.ts b/src/app/api/admin/data_migration/import/route.ts
index 7b246ac..5d2651b 100644
--- a/src/app/api/admin/data_migration/import/route.ts
+++ b/src/app/api/admin/data_migration/import/route.ts
@@ -98,8 +98,9 @@ export async function POST(req: NextRequest) {
if (importData.data.usersV2 && Array.isArray(importData.data.usersV2)) {
for (const userV2 of importData.data.usersV2) {
try {
- // 跳过站长(站长使用环境变量认证)
- if (userV2.role === 'owner') {
+ // 跳过环境变量中的站长(站长使用环境变量认证)
+ if (userV2.username === process.env.USERNAME) {
+ console.log(`跳过站长 ${userV2.username} 的导入`);
continue;
}
@@ -108,6 +109,12 @@ export async function POST(req: NextRequest) {
const passwordV2 = userData?.passwordV2;
if (passwordV2) {
+ // 将站长角色转换为普通角色
+ const importedRole = userV2.role === 'owner' ? 'user' : userV2.role;
+ if (userV2.role === 'owner') {
+ console.log(`用户 ${userV2.username} 的角色从 owner 转换为 user`);
+ }
+
// 直接使用加密后的密码创建用户
const storage = (db as any).storage;
if (storage && typeof storage.client?.hset === 'function') {
@@ -115,7 +122,7 @@ export async function POST(req: NextRequest) {
const createdAt = userV2.created_at || Date.now();
const userInfo: any = {
- role: userV2.role,
+ role: importedRole,
banned: userV2.banned,
password: passwordV2,
created_at: createdAt.toString(),
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 06c12ce..7bcfb2b 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -274,8 +274,8 @@ export class DbManager {
for (const user of users) {
try {
- // 跳过站长(站长使用环境变量认证,不需要迁移)
- if (user.role === 'owner') {
+ // 跳过环境变量中的站长(站长使用环境变量认证,不需要迁移)
+ if (user.username === process.env.USERNAME) {
console.log(`跳过站长 ${user.username} 的迁移`);
continue;
}
@@ -295,11 +295,6 @@ export class DbManager {
password = crypto.randomUUID();
console.log(`用户 ${user.username} (OIDC用户) 使用随机密码迁移`);
}
- // 如果是站长,使用环境变量中的密码
- else if (user.username === process.env.USERNAME && process.env.PASSWORD) {
- password = process.env.PASSWORD;
- console.log(`用户 ${user.username} (站长) 使用环境变量密码迁移`);
- }
// 尝试从旧的存储中获取密码
else {
try {
@@ -322,11 +317,17 @@ export class DbManager {
}
}
+ // 将站长角色转换为普通角色
+ const migratedRole = user.role === 'owner' ? 'user' : user.role;
+ if (user.role === 'owner') {
+ console.log(`用户 ${user.username} 的角色从 owner 转换为 user`);
+ }
+
// 创建新用户
await this.createUserV2(
user.username,
password,
- user.role,
+ migratedRole,
user.tags,
(user as any).oidcSub,
user.enabledApis
From c425db7e0e473a787117ca88a4d2c26b550d7c00 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 20:49:00 +0800
Subject: [PATCH 31/40] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?=
=?UTF-8?q?=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/admin/page.tsx | 4 +--
src/lib/redis-base.db.ts | 75 ++++++++++++++++++++++++++++++--------
src/lib/upstash.db.ts | 77 ++++++++++++++++++++++++++++++++--------
src/lib/user-cache.ts | 60 ++++++++++++++++++++++++++++++-
4 files changed, 184 insertions(+), 32 deletions(-)
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 130c3a3..0cde3f3 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -7982,8 +7982,8 @@ function AdminPageClient() {
// 刷新配置和用户列表
const refreshConfigAndUsers = useCallback(async () => {
await fetchConfig();
- await fetchUsersV2();
- }, [fetchConfig, fetchUsersV2]);
+ await fetchUsersV2(userPage); // 保持当前页码
+ }, [fetchConfig, fetchUsersV2, userPage]);
useEffect(() => {
// 首次加载时显示骨架
diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts
index 94aa0ba..9a93c48 100644
--- a/src/lib/redis-base.db.ts
+++ b/src/lib/redis-base.db.ts
@@ -372,6 +372,12 @@ export abstract class BaseRedisStorage implements IStorage {
score: createdAt,
value: userName,
}));
+
+ // 如果创建的是站长用户,清除站长存在状态缓存
+ if (userName === process.env.USERNAME) {
+ const { ownerExistenceCache } = await import('./user-cache');
+ ownerExistenceCache.delete(userName);
+ }
}
// 验证用户密码(新版本)
@@ -498,27 +504,68 @@ export abstract class BaseRedisStorage implements IStorage {
total: number;
}> {
// 获取总数
- const total = await this.withRetry(() => this.client.zCard(this.userListKey()));
+ let total = await this.withRetry(() => this.client.zCard(this.userListKey()));
+
+ // 检查站长是否在数据库中(使用缓存)
+ let ownerInfo = null;
+ let ownerInDatabase = false;
+ if (ownerUsername) {
+ // 先检查缓存
+ const { ownerExistenceCache } = await import('./user-cache');
+ const cachedExists = ownerExistenceCache.get(ownerUsername);
+
+ if (cachedExists !== null) {
+ // 使用缓存的结果
+ ownerInDatabase = cachedExists;
+ if (ownerInDatabase) {
+ // 如果站长在数据库中,获取详细信息
+ ownerInfo = await this.getUserInfoV2(ownerUsername);
+ }
+ } else {
+ // 缓存未命中,查询数据库
+ ownerInfo = await this.getUserInfoV2(ownerUsername);
+ ownerInDatabase = !!ownerInfo;
+ // 更新缓存
+ ownerExistenceCache.set(ownerUsername, ownerInDatabase);
+ }
+
+ // 如果站长不在数据库中,总数+1(无论在哪一页都要加)
+ if (!ownerInDatabase) {
+ total += 1;
+ }
+ }
+
+ // 如果站长不在数据库中且在第一页,需要调整获取的用户数量和偏移量
+ let actualOffset = offset;
+ let actualLimit = limit;
+
+ if (ownerUsername && !ownerInDatabase) {
+ if (offset === 0) {
+ // 第一页:只获取 limit-1 个用户,为站长留出位置
+ actualLimit = limit - 1;
+ } else {
+ // 其他页:偏移量需要减1,因为站长占据了第一页的一个位置
+ actualOffset = offset - 1;
+ }
+ }
// 获取用户列表(按注册时间升序)
const usernames = await this.withRetry(() =>
- this.client.zRange(this.userListKey(), offset, offset + limit - 1)
+ this.client.zRange(this.userListKey(), actualOffset, actualOffset + actualLimit - 1)
);
const users = [];
- // 如果有站长,确保站长始终在第一位
+ // 如果有站长且在第一页,确保站长始终在第一位
if (ownerUsername && offset === 0) {
- const ownerInfo = await this.getUserInfoV2(ownerUsername);
- if (ownerInfo) {
- users.push({
- username: ownerUsername,
- role: 'owner' as const,
- banned: ownerInfo.banned,
- tags: ownerInfo.tags,
- created_at: ownerInfo.created_at,
- });
- }
+ // 即使站长不在数据库中,也要添加站长(站长使用环境变量认证)
+ users.push({
+ username: ownerUsername,
+ role: 'owner' as const,
+ banned: ownerInfo?.banned || false,
+ tags: ownerInfo?.tags,
+ created_at: ownerInfo?.created_at || 0,
+ });
}
// 获取其他用户信息
@@ -557,7 +604,7 @@ export abstract class BaseRedisStorage implements IStorage {
// 删除用户信息Hash
await this.withRetry(() => this.client.del(this.userInfoKey(userName)));
- // 从用户列表中移除
+ // 从用���列表中移除
await this.withRetry(() => this.client.zRem(this.userListKey(), userName));
// 删除用户的其他数据(播放记录、收藏等)
diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts
index bd76783..b8ed0b1 100644
--- a/src/lib/upstash.db.ts
+++ b/src/lib/upstash.db.ts
@@ -292,6 +292,12 @@ export class UpstashRedisStorage implements IStorage {
score: createdAt,
member: userName,
}));
+
+ // 如果创建的是站长用户,清除站长存在状态缓存
+ if (userName === process.env.USERNAME) {
+ const { ownerExistenceCache } = await import('./user-cache');
+ ownerExistenceCache.delete(userName);
+ }
}
// 验证用户密码(新版本)
@@ -519,29 +525,70 @@ export class UpstashRedisStorage implements IStorage {
total: number;
}> {
// 获取总数
- const total = await withRetry(() => this.client.zcard(this.userListKey()));
+ let total = await withRetry(() => this.client.zcard(this.userListKey()));
+
+ // 检查站长是否在数据库中(使用缓存)
+ let ownerInfo = null;
+ let ownerInDatabase = false;
+ if (ownerUsername) {
+ // 先检查缓存
+ const { ownerExistenceCache } = await import('./user-cache');
+ const cachedExists = ownerExistenceCache.get(ownerUsername);
+
+ if (cachedExists !== null) {
+ // 使用缓存的结果
+ ownerInDatabase = cachedExists;
+ if (ownerInDatabase) {
+ // 如果站长在数据库中,获取详细信息
+ ownerInfo = await this.getUserInfoV2(ownerUsername);
+ }
+ } else {
+ // 缓存未命中,查询数据库
+ ownerInfo = await this.getUserInfoV2(ownerUsername);
+ ownerInDatabase = !!ownerInfo;
+ // 更���缓存
+ ownerExistenceCache.set(ownerUsername, ownerInDatabase);
+ }
+
+ // 如果站长不在数据库中,总数+1(无论在哪一页都要加)
+ if (!ownerInDatabase) {
+ total += 1;
+ }
+ }
+
+ // 如果站长不在数据库中且在第一页,需要调整获取的用户数量和偏移量
+ let actualOffset = offset;
+ let actualLimit = limit;
+
+ if (ownerUsername && !ownerInDatabase) {
+ if (offset === 0) {
+ // 第一页:只获取 limit-1 个用户,为站长留出位置
+ actualLimit = limit - 1;
+ } else {
+ // 其他页:偏移量需要减1,因为站长占据了第一页的一个位置
+ actualOffset = offset - 1;
+ }
+ }
// 获取用户列表(按注册时间升序)
const usernames = await withRetry(() =>
- this.client.zrange(this.userListKey(), offset, offset + limit - 1)
+ this.client.zrange(this.userListKey(), actualOffset, actualOffset + actualLimit - 1)
);
const users = [];
- // 如果有站长,确保站长始终在第一位
+ // 如果有站长且在第一页,确保站长始终在第一位
if (ownerUsername && offset === 0) {
- const ownerInfo = await this.getUserInfoV2(ownerUsername);
- if (ownerInfo) {
- users.push({
- username: ownerUsername,
- role: 'owner' as const,
- banned: ownerInfo.banned,
- tags: ownerInfo.tags,
- oidcSub: ownerInfo.oidcSub,
- enabledApis: ownerInfo.enabledApis,
- created_at: ownerInfo.created_at,
- });
- }
+ // 即使站长不在数据库中,也要添加站长(站长使用环境变量认证)
+ users.push({
+ username: ownerUsername,
+ role: 'owner' as const,
+ banned: ownerInfo?.banned || false,
+ tags: ownerInfo?.tags,
+ oidcSub: ownerInfo?.oidcSub,
+ enabledApis: ownerInfo?.enabledApis,
+ created_at: ownerInfo?.created_at || 0,
+ });
}
// 获取其他用户信息
diff --git a/src/lib/user-cache.ts b/src/lib/user-cache.ts
index 92517b8..9c71e18 100644
--- a/src/lib/user-cache.ts
+++ b/src/lib/user-cache.ts
@@ -55,6 +55,51 @@ class UserInfoCache {
}
}
+// 站长存在状态缓存
+class OwnerExistenceCache {
+ private cache: Map = new Map();
+ private readonly TTL = 10 * 60 * 1000; // 10分钟过期
+
+ get(ownerUsername: string): boolean | null {
+ const cached = this.cache.get(ownerUsername);
+ if (!cached) return null;
+
+ // 检查是否过期
+ if (Date.now() - cached.cachedAt > this.TTL) {
+ this.cache.delete(ownerUsername);
+ return null;
+ }
+
+ return cached.exists;
+ }
+
+ set(ownerUsername: string, exists: boolean): void {
+ this.cache.set(ownerUsername, {
+ exists,
+ cachedAt: Date.now(),
+ });
+ }
+
+ delete(ownerUsername: string): void {
+ this.cache.delete(ownerUsername);
+ }
+
+ clear(): void {
+ this.cache.clear();
+ }
+
+ // 清理过期的缓存
+ cleanup(): void {
+ const now = Date.now();
+ const entries = Array.from(this.cache.entries());
+ for (const [username, cached] of entries) {
+ if (now - cached.cachedAt > this.TTL) {
+ this.cache.delete(username);
+ }
+ }
+ }
+}
+
// 全局单例
const globalKey = Symbol.for('__MOONTV_USER_INFO_CACHE__');
let userInfoCache: UserInfoCache | undefined = (global as any)[globalKey];
@@ -69,4 +114,17 @@ if (!userInfoCache) {
}, 60 * 1000);
}
-export { userInfoCache };
+const ownerExistenceGlobalKey = Symbol.for('__MOONTV_OWNER_EXISTENCE_CACHE__');
+let ownerExistenceCache: OwnerExistenceCache | undefined = (global as any)[ownerExistenceGlobalKey];
+
+if (!ownerExistenceCache) {
+ ownerExistenceCache = new OwnerExistenceCache();
+ (global as any)[ownerExistenceGlobalKey] = ownerExistenceCache;
+
+ // 每分钟清理一次过期缓存
+ setInterval(() => {
+ ownerExistenceCache?.cleanup();
+ }, 60 * 1000);
+}
+
+export { userInfoCache, ownerExistenceCache };
From e11f162b87aa50f9ca5cbcb76062829dd4d65d07 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 20:59:17 +0800
Subject: [PATCH 32/40] =?UTF-8?q?=E4=BA=91=E7=9B=98=E6=90=9C=E7=B4=A2?=
=?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=8F=98=E6=9B=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/play/page.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx
index 4ba0b0f..f913a6c 100644
--- a/src/app/play/page.tsx
+++ b/src/app/play/page.tsx
@@ -2,7 +2,7 @@
'use client';
-import { Heart, Search, X } from 'lucide-react';
+import { Heart, Search, X, Cloud } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense, useEffect, useRef, useState } from 'react';
@@ -4932,7 +4932,7 @@ function PlayPageClient() {
className='flex-shrink-0 hover:opacity-80 transition-opacity'
title='搜索网盘资源'
>
-
+
{/* 豆瓣评分显示 */}
{doubanRating && doubanRating.value > 0 && (
From bc8ac693b429d422ceb95571767d3a1c4a464ad7 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 21:55:27 +0800
Subject: [PATCH 33/40] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=83=AD=E5=8A=9B?=
=?UTF-8?q?=E5=9B=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/play/page.tsx | 75 +++++++++++++++++++++++++++++++++++++
src/components/UserMenu.tsx | 39 +++++++++++++++++++
2 files changed, 114 insertions(+)
diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx
index f913a6c..3e08715 100644
--- a/src/app/play/page.tsx
+++ b/src/app/play/page.tsx
@@ -293,6 +293,32 @@ function PlayPageClient() {
const danmakuPluginRef = useRef(null);
const danmakuSettingsRef = useRef(danmakuSettings);
+ // 弹幕热力图完全禁用开关(默认不禁用,即启用热力图功能)
+ const [danmakuHeatmapDisabled, setDanmakuHeatmapDisabled] = useState(() => {
+ if (typeof window !== 'undefined') {
+ const v = localStorage.getItem('danmaku_heatmap_disabled');
+ if (v !== null) return v === 'true';
+ }
+ return false; // 默认不禁用
+ });
+ const danmakuHeatmapDisabledRef = useRef(danmakuHeatmapDisabled);
+ useEffect(() => {
+ danmakuHeatmapDisabledRef.current = danmakuHeatmapDisabled;
+ }, [danmakuHeatmapDisabled]);
+
+ // 弹幕热力图开关(默认开启)
+ const [danmakuHeatmapEnabled, setDanmakuHeatmapEnabled] = useState(() => {
+ if (typeof window !== 'undefined') {
+ const v = localStorage.getItem('danmaku_heatmap_enabled');
+ if (v !== null) return v === 'true';
+ }
+ return true; // 默认开启
+ });
+ const danmakuHeatmapEnabledRef = useRef(danmakuHeatmapEnabled);
+ useEffect(() => {
+ danmakuHeatmapEnabledRef.current = danmakuHeatmapEnabled;
+ }, [danmakuHeatmapEnabled]);
+
// 多条弹幕匹配结果
const [danmakuMatches, setDanmakuMatches] = useState([]);
const [showDanmakuSourceSelector, setShowDanmakuSourceSelector] = useState(false);
@@ -2311,6 +2337,17 @@ function PlayPageClient() {
setDanmakuCount(0);
} finally {
setDanmakuLoading(false);
+
+ // 弹幕加载完成后,根据用户设置显示或隐藏热力图(仅在未禁用热力图时)
+ if (!danmakuHeatmapDisabledRef.current) {
+ const heatmapElement = document.querySelector('.art-control-heatmap') as HTMLElement;
+ if (heatmapElement) {
+ const isEnabled = danmakuHeatmapEnabledRef.current;
+ heatmapElement.style.opacity = isEnabled ? '1' : '0';
+ heatmapElement.style.pointerEvents = isEnabled ? 'auto' : 'none';
+ console.log('弹幕加载完成,热力图状态:', isEnabled ? '显示' : '隐藏');
+ }
+ }
}
};
@@ -3136,6 +3173,7 @@ function PlayPageClient() {
antiOverlap: true,
synchronousPlayback: danmakuSettingsRef.current.synchronousPlayback,
emitter: false,
+ heatmap: !danmakuHeatmapDisabledRef.current, // 根据禁用状态决定是否创建热力图
// 主题
theme: 'dark',
filter: (danmu: any) => {
@@ -3216,6 +3254,33 @@ function PlayPageClient() {
return '打开设置';
},
},
+ // 只有在未禁用热力图时才显示热力图开关
+ ...(!danmakuHeatmapDisabledRef.current ? [{
+ name: '弹幕热力',
+ html: '弹幕热力',
+ icon: '',
+ switch: danmakuHeatmapEnabledRef.current,
+ onSwitch: function (item: any) {
+ const newVal = !item.switch;
+ try {
+ localStorage.setItem('danmaku_heatmap_enabled', String(newVal));
+ setDanmakuHeatmapEnabled(newVal);
+
+ // 使用 opacity 控制热力图显示/隐藏
+ const heatmapElement = document.querySelector('.art-control-heatmap') as HTMLElement;
+ if (heatmapElement) {
+ heatmapElement.style.opacity = newVal ? '1' : '0';
+ heatmapElement.style.pointerEvents = newVal ? 'auto' : 'none';
+ console.log('弹幕热力已', newVal ? '开启' : '关闭');
+ } else {
+ console.warn('未找到热力图元素');
+ }
+ } catch (err) {
+ console.error('切换弹幕热力失败:', err);
+ }
+ return newVal;
+ },
+ }] : []),
...(webGPUSupported ? [
{
name: 'Anime4K超分',
@@ -3900,6 +3965,16 @@ function PlayPageClient() {
danmakuPluginRef.current.hide();
}
+ // 初始隐藏热力图,等待弹幕加载完成后再显示(仅在未禁用热力图时)
+ if (!danmakuHeatmapDisabledRef.current) {
+ const heatmapElement = document.querySelector('.art-control-heatmap') as HTMLElement;
+ if (heatmapElement) {
+ heatmapElement.style.opacity = '0';
+ heatmapElement.style.pointerEvents = 'none';
+ console.log('热力图初始状态: 隐藏(等待弹幕加载)');
+ }
+ }
+
// 自动搜索并加载弹幕
await autoSearchDanmaku();
}
diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx
index 802fc81..a31c53b 100644
--- a/src/components/UserMenu.tsx
+++ b/src/components/UserMenu.tsx
@@ -86,6 +86,7 @@ export const UserMenu: React.FC = () => {
const [enableOptimization, setEnableOptimization] = useState(true);
const [fluidSearch, setFluidSearch] = useState(true);
const [liveDirectConnect, setLiveDirectConnect] = useState(false);
+ const [danmakuHeatmapDisabled, setDanmakuHeatmapDisabled] = useState(false);
const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent');
const [doubanImageProxyType, setDoubanImageProxyType] = useState('cmliussss-cdn-tencent');
const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState('');
@@ -300,6 +301,11 @@ export const UserMenu: React.FC = () => {
if (savedLiveDirectConnect !== null) {
setLiveDirectConnect(JSON.parse(savedLiveDirectConnect));
}
+
+ const savedDanmakuHeatmapDisabled = localStorage.getItem('danmaku_heatmap_disabled');
+ if (savedDanmakuHeatmapDisabled !== null) {
+ setDanmakuHeatmapDisabled(savedDanmakuHeatmapDisabled === 'true');
+ }
}
}, []);
@@ -497,6 +503,13 @@ export const UserMenu: React.FC = () => {
}
};
+ const handleDanmakuHeatmapDisabledToggle = (value: boolean) => {
+ setDanmakuHeatmapDisabled(value);
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('danmaku_heatmap_disabled', String(value));
+ }
+ };
+
const handleDoubanDataSourceChange = (value: string) => {
setDoubanDataSource(value);
if (typeof window !== 'undefined') {
@@ -553,6 +566,7 @@ export const UserMenu: React.FC = () => {
setEnableOptimization(true);
setFluidSearch(defaultFluidSearch);
setLiveDirectConnect(false);
+ setDanmakuHeatmapDisabled(false);
setDoubanProxyUrl(defaultDoubanProxy);
setDoubanDataSource(defaultDoubanProxyType);
setDoubanImageProxyType(defaultDoubanImageProxyType);
@@ -563,6 +577,7 @@ export const UserMenu: React.FC = () => {
localStorage.setItem('enableOptimization', JSON.stringify(true));
localStorage.setItem('fluidSearch', JSON.stringify(defaultFluidSearch));
localStorage.setItem('liveDirectConnect', JSON.stringify(false));
+ localStorage.setItem('danmaku_heatmap_disabled', 'false');
localStorage.setItem('doubanProxyUrl', defaultDoubanProxy);
localStorage.setItem('doubanDataSource', defaultDoubanProxyType);
localStorage.setItem('doubanImageProxyType', defaultDoubanImageProxyType);
@@ -1150,6 +1165,30 @@ export const UserMenu: React.FC = () => {
+ {/* 禁用弹幕热力 */}
+
+
+
+ 禁用弹幕热力图
+
+
+ 完全关闭弹幕热力图功能以提升性能(需手动刷新页面生效)
+
+
+
+
+
{/* 分割线 */}
From c7b9097447d55b988edbcf1c5694db6dbbf6790d Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 23:19:06 +0800
Subject: [PATCH 34/40] fix typeeror
---
src/lib/lock.ts | 11 ++++++-----
src/lib/user-cache.ts | 24 +++++++++++++-----------
2 files changed, 19 insertions(+), 16 deletions(-)
diff --git a/src/lib/lock.ts b/src/lib/lock.ts
index 1bceac7..e98f794 100644
--- a/src/lib/lock.ts
+++ b/src/lib/lock.ts
@@ -85,11 +85,12 @@ class LockManager {
// 全局单例
const globalKey = Symbol.for('__MOONTV_LOCK_MANAGER__');
-let lockManager: LockManager | undefined = (global as any)[globalKey];
+let _lockManager: LockManager | undefined = (global as any)[globalKey];
-if (!lockManager) {
- lockManager = new LockManager();
- (global as any)[globalKey] = lockManager;
+if (!_lockManager) {
+ _lockManager = new LockManager();
+ (global as any)[globalKey] = _lockManager;
}
-export { lockManager };
+// TypeScript doesn't recognize that lockManager is always defined after the if block
+export const lockManager = _lockManager as LockManager;
diff --git a/src/lib/user-cache.ts b/src/lib/user-cache.ts
index 9c71e18..f1ec2f3 100644
--- a/src/lib/user-cache.ts
+++ b/src/lib/user-cache.ts
@@ -102,29 +102,31 @@ class OwnerExistenceCache {
// 全局单例
const globalKey = Symbol.for('__MOONTV_USER_INFO_CACHE__');
-let userInfoCache: UserInfoCache | undefined = (global as any)[globalKey];
+let _userInfoCache: UserInfoCache | undefined = (global as any)[globalKey];
-if (!userInfoCache) {
- userInfoCache = new UserInfoCache();
- (global as any)[globalKey] = userInfoCache;
+if (!_userInfoCache) {
+ _userInfoCache = new UserInfoCache();
+ (global as any)[globalKey] = _userInfoCache;
// 每分钟清理一次过期缓存
setInterval(() => {
- userInfoCache?.cleanup();
+ _userInfoCache?.cleanup();
}, 60 * 1000);
}
+export const userInfoCache = _userInfoCache as UserInfoCache;
+
const ownerExistenceGlobalKey = Symbol.for('__MOONTV_OWNER_EXISTENCE_CACHE__');
-let ownerExistenceCache: OwnerExistenceCache | undefined = (global as any)[ownerExistenceGlobalKey];
+let _ownerExistenceCache: OwnerExistenceCache | undefined = (global as any)[ownerExistenceGlobalKey];
-if (!ownerExistenceCache) {
- ownerExistenceCache = new OwnerExistenceCache();
- (global as any)[ownerExistenceGlobalKey] = ownerExistenceCache;
+if (!_ownerExistenceCache) {
+ _ownerExistenceCache = new OwnerExistenceCache();
+ (global as any)[ownerExistenceGlobalKey] = _ownerExistenceCache;
// 每分钟清理一次过期缓存
setInterval(() => {
- ownerExistenceCache?.cleanup();
+ _ownerExistenceCache?.cleanup();
}, 60 * 1000);
}
-export { userInfoCache, ownerExistenceCache };
+export const ownerExistenceCache = _ownerExistenceCache as OwnerExistenceCache;
From e26c412c47134ba7dc34563cc3026a2103ad117e Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Wed, 24 Dec 2025 23:43:28 +0800
Subject: [PATCH 35/40] =?UTF-8?q?=E4=BF=AE=E6=AD=A3getAllUsers()=EF=BC=8C?=
=?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=90=AF=E5=8A=A8=E6=97=B6=E8=87=AA=E5=8A=A8?=
=?UTF-8?q?=E8=BF=81=E7=A7=BB=E6=89=A7=E8=A1=8C=E5=A4=9A=E6=AC=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/lib/config.ts | 105 ++++++++++++++++++++++-----------------
src/lib/redis-base.db.ts | 13 +++--
src/lib/upstash.db.ts | 13 +++--
3 files changed, 71 insertions(+), 60 deletions(-)
diff --git a/src/lib/config.ts b/src/lib/config.ts
index bfbebbf..4762d6e 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -55,6 +55,7 @@ export const API_CONFIG = {
// 在模块加载时根据环境决定配置来源
let cachedConfig: AdminConfig;
+let configInitPromise: Promise | null = null;
// 从配置文件补充管理员配置
@@ -303,57 +304,69 @@ export async function getConfig(): Promise {
return cachedConfig;
}
- // 读 db
- let adminConfig: AdminConfig | null = null;
- let dbReadFailed = false;
- try {
- adminConfig = await db.getAdminConfig();
- } catch (e) {
- console.error('获取管理员配置失败:', e);
- dbReadFailed = true;
+ // 如果正在初始化,等待初始化完成
+ if (configInitPromise) {
+ return configInitPromise;
}
- // db 中无配置,执行一次初始化
- if (!adminConfig) {
- if (dbReadFailed) {
- // 数据库读取失败,使用默认配置但不保存,避免覆盖数据库
- console.warn('数据库读取失败,使用临时默认配置(不会保存到数据库)');
- adminConfig = await getInitConfig("");
- } else {
- // 数据库中确实没有配置,首次初始化并保存
- console.log('首次初始化配置');
- adminConfig = await getInitConfig("");
- await db.saveAdminConfig(adminConfig);
- }
- }
- adminConfig = configSelfCheck(adminConfig);
- cachedConfig = adminConfig;
-
- // 自动迁移用户(如果配置中有用户且V2存储支持)
- // 过滤掉站长后检查是否有需要迁移的用户
- const nonOwnerUsers = adminConfig.UserConfig.Users.filter(
- (u) => u.username !== process.env.USERNAME
- );
- if (!dbReadFailed && nonOwnerUsers.length > 0) {
+ // 创建初始化 Promise
+ configInitPromise = (async () => {
+ // 读 db
+ let adminConfig: AdminConfig | null = null;
+ let dbReadFailed = false;
try {
- // 检查是否支持V2存储
- const storage = (db as any).storage;
- if (storage && typeof storage.createUserV2 === 'function') {
- console.log('检测到配置中有用户,开始自动迁移...');
- await db.migrateUsersFromConfig(adminConfig);
- // 迁移完成后,清空配置中的用户列表并保存
- adminConfig.UserConfig.Users = [];
- await db.saveAdminConfig(adminConfig);
- cachedConfig = adminConfig;
- console.log('用户自动迁移完成');
- }
- } catch (error) {
- console.error('自动迁移用户失败:', error);
- // 不影响主流程,继续执行
+ adminConfig = await db.getAdminConfig();
+ } catch (e) {
+ console.error('获取管理员配置失败:', e);
+ dbReadFailed = true;
}
- }
- return cachedConfig;
+ // db 中无配置,执行一次初始化
+ if (!adminConfig) {
+ if (dbReadFailed) {
+ // 数据库读取失败,使用默认配置但不保存,避免覆盖数据库
+ console.warn('数据库读取失败,使用临时默认配置(不会保存到数据库)');
+ adminConfig = await getInitConfig("");
+ } else {
+ // 数据库中确实没有配置,首次初始化并保存
+ console.log('首次初始化配置');
+ adminConfig = await getInitConfig("");
+ await db.saveAdminConfig(adminConfig);
+ }
+ }
+ adminConfig = configSelfCheck(adminConfig);
+ cachedConfig = adminConfig;
+
+ // 自动迁移用户(如果配置中有用户且V2存储支持)
+ // 过滤掉站长后检查是否有需要迁移的用户
+ const nonOwnerUsers = adminConfig.UserConfig.Users.filter(
+ (u) => u.username !== process.env.USERNAME
+ );
+ if (!dbReadFailed && nonOwnerUsers.length > 0) {
+ try {
+ // 检查是否支持V2存储
+ const storage = (db as any).storage;
+ if (storage && typeof storage.createUserV2 === 'function') {
+ console.log('检测到配置中有用户,开始自动迁移...');
+ await db.migrateUsersFromConfig(adminConfig);
+ // 迁移完成后,清空配置中的用户列表并保存
+ adminConfig.UserConfig.Users = [];
+ await db.saveAdminConfig(adminConfig);
+ cachedConfig = adminConfig;
+ console.log('用户自动迁移完成');
+ }
+ } catch (error) {
+ console.error('自动迁移用户失败:', error);
+ // 不影响主流程,继续执行
+ }
+ }
+
+ // 清除初始化 Promise
+ configInitPromise = null;
+ return cachedConfig;
+ })();
+
+ return configInitPromise;
}
export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts
index 9a93c48..e513c67 100644
--- a/src/lib/redis-base.db.ts
+++ b/src/lib/redis-base.db.ts
@@ -645,13 +645,12 @@ export abstract class BaseRedisStorage implements IStorage {
// ---------- 获取全部用户 ----------
async getAllUsers(): Promise {
- const keys = await this.withRetry(() => this.client.keys('u:*:pwd'));
- return keys
- .map((k) => {
- const match = k.match(/^u:(.+?):pwd$/);
- return match ? ensureString(match[1]) : undefined;
- })
- .filter((u): u is string => typeof u === 'string');
+ // 从新版用户列表获取
+ const userListKey = this.userListKey();
+ const users = await this.withRetry(() =>
+ this.client.zRange(userListKey, 0, -1)
+ );
+ return users.map(u => ensureString(u));
}
// ---------- 管理员配置 ----------
diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts
index b8ed0b1..a0856ed 100644
--- a/src/lib/upstash.db.ts
+++ b/src/lib/upstash.db.ts
@@ -673,13 +673,12 @@ export class UpstashRedisStorage implements IStorage {
// ---------- 获取全部用户 ----------
async getAllUsers(): Promise {
- const keys = await withRetry(() => this.client.keys('u:*:pwd'));
- return keys
- .map((k) => {
- const match = k.match(/^u:(.+?):pwd$/);
- return match ? ensureString(match[1]) : undefined;
- })
- .filter((u): u is string => typeof u === 'string');
+ // 从新版用户列表获取
+ const userListKey = this.userListKey();
+ const users = await withRetry(() =>
+ this.client.zrange(userListKey, 0, -1)
+ );
+ return users.map(u => ensureString(u));
}
// ---------- 管理员配置 ----------
From 012a3beb5e2ddfe689b853950fad2de50f69a4c4 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Thu, 25 Dec 2025 00:00:22 +0800
Subject: [PATCH 36/40] =?UTF-8?q?=E4=BF=AE=E6=AD=A3getAllUsers()=E4=B8=8D?=
=?UTF-8?q?=E5=8C=85=E5=90=AB=E7=AB=99=E9=95=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/api/debug/watch-room-config/route.ts | 57 --------------------
src/lib/redis-base.db.ts | 10 +++-
src/lib/upstash.db.ts | 10 +++-
3 files changed, 18 insertions(+), 59 deletions(-)
delete mode 100644 src/app/api/debug/watch-room-config/route.ts
diff --git a/src/app/api/debug/watch-room-config/route.ts b/src/app/api/debug/watch-room-config/route.ts
deleted file mode 100644
index d21f201..0000000
--- a/src/app/api/debug/watch-room-config/route.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/* eslint-disable no-console */
-
-import { NextRequest, NextResponse } from 'next/server';
-
-import { getConfig } from '@/lib/config';
-
-export const runtime = 'nodejs';
-
-export async function GET(request: NextRequest) {
- try {
- const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
-
- // 调试信息
- const debugInfo = {
- storageType,
- envVars: {
- hasRedisUrl: !!process.env.REDIS_URL,
- hasUpstashUrl: !!process.env.UPSTASH_REDIS_REST_URL,
- hasUpstashToken: !!process.env.UPSTASH_REDIS_REST_TOKEN,
- hasKvrocksUrl: !!process.env.KVROCKS_URL,
- watchRoomEnabled: process.env.WATCH_ROOM_ENABLED,
- watchRoomServerType: process.env.WATCH_ROOM_SERVER_TYPE,
- hasWatchRoomExternalUrl: !!process.env.WATCH_ROOM_EXTERNAL_SERVER_URL,
- hasWatchRoomExternalAuth: !!process.env.WATCH_ROOM_EXTERNAL_SERVER_AUTH,
- },
- watchRoomConfig: {
- enabled: process.env.WATCH_ROOM_ENABLED === 'true',
- serverType: process.env.WATCH_ROOM_SERVER_TYPE || 'internal',
- externalServerUrl: process.env.WATCH_ROOM_EXTERNAL_SERVER_URL,
- externalServerAuth: process.env.WATCH_ROOM_EXTERNAL_SERVER_AUTH ? '***' : undefined,
- },
- configReadError: null as string | null,
- };
-
- // 尝试读取配置(验证数据库连接)
- try {
- await getConfig();
- } catch (error) {
- debugInfo.configReadError = (error as Error).message;
- }
-
- return NextResponse.json(debugInfo, {
- headers: {
- 'Cache-Control': 'no-store',
- },
- });
- } catch (error) {
- console.error('Debug API error:', error);
- return NextResponse.json(
- {
- error: 'Failed to get debug info',
- details: (error as Error).message,
- },
- { status: 500 }
- );
- }
-}
diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts
index e513c67..c63320d 100644
--- a/src/lib/redis-base.db.ts
+++ b/src/lib/redis-base.db.ts
@@ -650,7 +650,15 @@ export abstract class BaseRedisStorage implements IStorage {
const users = await this.withRetry(() =>
this.client.zRange(userListKey, 0, -1)
);
- return users.map(u => ensureString(u));
+ const userList = users.map(u => ensureString(u));
+
+ // 确保站长在列表中(站长可能不在数据库中,使用环境变量认证)
+ const ownerUsername = process.env.USERNAME;
+ if (ownerUsername && !userList.includes(ownerUsername)) {
+ userList.unshift(ownerUsername);
+ }
+
+ return userList;
}
// ---------- 管理员配置 ----------
diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts
index a0856ed..830f259 100644
--- a/src/lib/upstash.db.ts
+++ b/src/lib/upstash.db.ts
@@ -678,7 +678,15 @@ export class UpstashRedisStorage implements IStorage {
const users = await withRetry(() =>
this.client.zrange(userListKey, 0, -1)
);
- return users.map(u => ensureString(u));
+ const userList = users.map(u => ensureString(u));
+
+ // 确保站长在列表中(站长可能不在数据库中,使用环境变量认证)
+ const ownerUsername = process.env.USERNAME;
+ if (ownerUsername && !userList.includes(ownerUsername)) {
+ userList.unshift(ownerUsername);
+ }
+
+ return userList;
}
// ---------- 管理员配置 ----------
From 2e545466cbff9d16c44d329b7b9406c36bd0b5ad Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Thu, 25 Dec 2025 00:12:03 +0800
Subject: [PATCH 37/40] =?UTF-8?q?=E6=8F=90=E9=AB=98=E6=89=80=E6=9C=89?=
=?UTF-8?q?=E5=BC=B9=E5=B9=95=E6=8E=A5=E5=8F=A3=E7=9A=84=E8=B6=85=E6=97=B6?=
=?UTF-8?q?=E6=97=B6=E9=97=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/api/danmaku/episodes/route.ts | 2 +-
src/app/api/danmaku/match/route.ts | 2 +-
src/app/api/danmaku/search/route.ts | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/app/api/danmaku/episodes/route.ts b/src/app/api/danmaku/episodes/route.ts
index 112ee85..b28e064 100644
--- a/src/app/api/danmaku/episodes/route.ts
+++ b/src/app/api/danmaku/episodes/route.ts
@@ -40,7 +40,7 @@ export async function GET(request: NextRequest) {
// 添加超时控制和重试机制
const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 10秒超时
try {
const response = await fetch(apiUrl, {
diff --git a/src/app/api/danmaku/match/route.ts b/src/app/api/danmaku/match/route.ts
index 194a66c..972bf00 100644
--- a/src/app/api/danmaku/match/route.ts
+++ b/src/app/api/danmaku/match/route.ts
@@ -37,7 +37,7 @@ export async function POST(request: NextRequest) {
// 添加超时控制
const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 10秒超时
try {
const response = await fetch(apiUrl, {
diff --git a/src/app/api/danmaku/search/route.ts b/src/app/api/danmaku/search/route.ts
index 670381a..ecda7ef 100644
--- a/src/app/api/danmaku/search/route.ts
+++ b/src/app/api/danmaku/search/route.ts
@@ -36,7 +36,7 @@ export async function GET(request: NextRequest) {
// 添加超时控制
const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 10秒超时
try {
const response = await fetch(apiUrl, {
From 35fc89b33ebce6acc5ce0fc8d6e16b1470d7c3ba Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Thu, 25 Dec 2025 00:39:30 +0800
Subject: [PATCH 38/40] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=81=E7=A7=BB?=
=?UTF-8?q?=E4=B8=A2=E5=A4=B1enabledApis=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/lib/redis-base.db.ts | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts
index c63320d..fe98a43 100644
--- a/src/lib/redis-base.db.ts
+++ b/src/lib/redis-base.db.ts
@@ -342,7 +342,8 @@ export abstract class BaseRedisStorage implements IStorage {
password: string,
role: 'owner' | 'admin' | 'user' = 'user',
tags?: string[],
- oidcSub?: string
+ oidcSub?: string,
+ enabledApis?: string[]
): Promise {
const hashedPassword = await this.hashPassword(password);
const createdAt = Date.now();
@@ -359,6 +360,10 @@ export abstract class BaseRedisStorage implements IStorage {
userInfo.tags = JSON.stringify(tags);
}
+ if (enabledApis && enabledApis.length > 0) {
+ userInfo.enabledApis = JSON.stringify(enabledApis);
+ }
+
if (oidcSub) {
userInfo.oidcSub = oidcSub;
// 创建OIDC映射
@@ -400,6 +405,7 @@ export abstract class BaseRedisStorage implements IStorage {
banned: boolean;
tags?: string[];
oidcSub?: string;
+ enabledApis?: string[];
created_at: number;
} | null> {
const userInfo = await this.withRetry(() =>
@@ -415,6 +421,7 @@ export abstract class BaseRedisStorage implements IStorage {
banned: userInfo.banned === 'true',
tags: userInfo.tags ? JSON.parse(userInfo.tags) : undefined,
oidcSub: userInfo.oidcSub,
+ enabledApis: userInfo.enabledApis ? JSON.parse(userInfo.enabledApis) : undefined,
created_at: parseInt(userInfo.created_at || '0', 10),
};
}
@@ -427,6 +434,7 @@ export abstract class BaseRedisStorage implements IStorage {
banned?: boolean;
tags?: string[];
oidcSub?: string;
+ enabledApis?: string[];
}
): Promise {
const userInfo: Record = {};
@@ -448,6 +456,15 @@ export abstract class BaseRedisStorage implements IStorage {
}
}
+ if (updates.enabledApis !== undefined) {
+ if (updates.enabledApis.length > 0) {
+ userInfo.enabledApis = JSON.stringify(updates.enabledApis);
+ } else {
+ // 删除enabledApis字段
+ await this.withRetry(() => this.client.hDel(this.userInfoKey(userName), 'enabledApis'));
+ }
+ }
+
if (updates.oidcSub !== undefined) {
const oldInfo = await this.getUserInfoV2(userName);
if (oldInfo?.oidcSub && oldInfo.oidcSub !== updates.oidcSub) {
@@ -499,6 +516,8 @@ export abstract class BaseRedisStorage implements IStorage {
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
+ oidcSub?: string;
+ enabledApis?: string[];
created_at: number;
}>;
total: number;
From b0da4e90e7122f1846a90071de62ecd2c9a31a84 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Thu, 25 Dec 2025 01:02:22 +0800
Subject: [PATCH 39/40] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E7=A7=81=E4=BA=BA?=
=?UTF-8?q?=E5=BD=B1=E5=BA=93=E5=85=B3=E9=97=AD=E5=90=8E=E8=BF=98=E8=83=BD?=
=?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=88=B0=E7=A7=81=E4=BA=BA=E5=BD=B1=E5=BA=93?=
=?UTF-8?q?=E7=9A=84=E8=A7=86=E9=A2=91=E5=92=8C=E6=92=AD=E6=94=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/api/detail/route.ts | 10 ++++++++--
src/app/api/openlist/correct/route.ts | 10 ++++++++--
src/app/api/openlist/delete/route.ts | 8 ++++++--
src/app/api/openlist/detail/route.ts | 10 ++++++++--
src/app/api/openlist/list/route.ts | 10 ++++++++--
src/app/api/openlist/play/route.ts | 10 ++++++++--
src/app/api/openlist/refresh-video/route.ts | 10 ++++++++--
src/app/api/openlist/refresh/route.ts | 10 ++++++++--
src/app/api/search/route.ts | 7 ++++++-
src/app/api/search/ws/route.ts | 7 ++++++-
10 files changed, 74 insertions(+), 18 deletions(-)
diff --git a/src/app/api/detail/route.ts b/src/app/api/detail/route.ts
index 8a03620..f5e993d 100644
--- a/src/app/api/detail/route.ts
+++ b/src/app/api/detail/route.ts
@@ -26,8 +26,14 @@ export async function GET(request: NextRequest) {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
- if (!openListConfig || !openListConfig.URL || !openListConfig.Username || !openListConfig.Password) {
- throw new Error('OpenList 未配置');
+ if (
+ !openListConfig ||
+ !openListConfig.Enabled ||
+ !openListConfig.URL ||
+ !openListConfig.Username ||
+ !openListConfig.Password
+ ) {
+ throw new Error('OpenList 未配置或未启用');
}
const rootPath = openListConfig.RootPath || '/';
diff --git a/src/app/api/openlist/correct/route.ts b/src/app/api/openlist/correct/route.ts
index e956b2a..beb6e95 100644
--- a/src/app/api/openlist/correct/route.ts
+++ b/src/app/api/openlist/correct/route.ts
@@ -39,9 +39,15 @@ export async function POST(request: NextRequest) {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
- if (!openListConfig || !openListConfig.URL || !openListConfig.Username || !openListConfig.Password) {
+ if (
+ !openListConfig ||
+ !openListConfig.Enabled ||
+ !openListConfig.URL ||
+ !openListConfig.Username ||
+ !openListConfig.Password
+ ) {
return NextResponse.json(
- { error: 'OpenList 未配置' },
+ { error: 'OpenList 未配置或未启用' },
{ status: 400 }
);
}
diff --git a/src/app/api/openlist/delete/route.ts b/src/app/api/openlist/delete/route.ts
index 7a04e04..e3fc1f7 100644
--- a/src/app/api/openlist/delete/route.ts
+++ b/src/app/api/openlist/delete/route.ts
@@ -38,9 +38,13 @@ export async function POST(request: NextRequest) {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
- if (!openListConfig || !openListConfig.URL) {
+ if (
+ !openListConfig ||
+ !openListConfig.Enabled ||
+ !openListConfig.URL
+ ) {
return NextResponse.json(
- { error: 'OpenList 未配置' },
+ { error: 'OpenList 未配置或未启用' },
{ status: 400 }
);
}
diff --git a/src/app/api/openlist/detail/route.ts b/src/app/api/openlist/detail/route.ts
index 300afea..68c91ee 100644
--- a/src/app/api/openlist/detail/route.ts
+++ b/src/app/api/openlist/detail/route.ts
@@ -35,8 +35,14 @@ export async function GET(request: NextRequest) {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
- if (!openListConfig || !openListConfig.URL || !openListConfig.Username || !openListConfig.Password) {
- return NextResponse.json({ error: 'OpenList 未配置' }, { status: 400 });
+ if (
+ !openListConfig ||
+ !openListConfig.Enabled ||
+ !openListConfig.URL ||
+ !openListConfig.Username ||
+ !openListConfig.Password
+ ) {
+ return NextResponse.json({ error: 'OpenList 未配置或未启用' }, { status: 400 });
}
const rootPath = openListConfig.RootPath || '/';
diff --git a/src/app/api/openlist/list/route.ts b/src/app/api/openlist/list/route.ts
index 8c67b18..ae32a3a 100644
--- a/src/app/api/openlist/list/route.ts
+++ b/src/app/api/openlist/list/route.ts
@@ -35,9 +35,15 @@ export async function GET(request: NextRequest) {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
- if (!openListConfig || !openListConfig.URL || !openListConfig.Username || !openListConfig.Password) {
+ if (
+ !openListConfig ||
+ !openListConfig.Enabled ||
+ !openListConfig.URL ||
+ !openListConfig.Username ||
+ !openListConfig.Password
+ ) {
return NextResponse.json(
- { error: 'OpenList 未配置', list: [], total: 0 },
+ { error: 'OpenList 未配置或未启用', list: [], total: 0 },
{ status: 200 }
);
}
diff --git a/src/app/api/openlist/play/route.ts b/src/app/api/openlist/play/route.ts
index 731db4f..95955b5 100644
--- a/src/app/api/openlist/play/route.ts
+++ b/src/app/api/openlist/play/route.ts
@@ -31,8 +31,14 @@ export async function GET(request: NextRequest) {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
- if (!openListConfig || !openListConfig.URL || !openListConfig.Username || !openListConfig.Password) {
- return NextResponse.json({ error: 'OpenList 未配置' }, { status: 400 });
+ if (
+ !openListConfig ||
+ !openListConfig.Enabled ||
+ !openListConfig.URL ||
+ !openListConfig.Username ||
+ !openListConfig.Password
+ ) {
+ return NextResponse.json({ error: 'OpenList 未配置或未启用' }, { status: 400 });
}
const rootPath = openListConfig.RootPath || '/';
diff --git a/src/app/api/openlist/refresh-video/route.ts b/src/app/api/openlist/refresh-video/route.ts
index 50527e3..1c27877 100644
--- a/src/app/api/openlist/refresh-video/route.ts
+++ b/src/app/api/openlist/refresh-video/route.ts
@@ -30,8 +30,14 @@ export async function POST(request: NextRequest) {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
- if (!openListConfig || !openListConfig.URL || !openListConfig.Username || !openListConfig.Password) {
- return NextResponse.json({ error: 'OpenList 未配置' }, { status: 400 });
+ if (
+ !openListConfig ||
+ !openListConfig.Enabled ||
+ !openListConfig.URL ||
+ !openListConfig.Username ||
+ !openListConfig.Password
+ ) {
+ return NextResponse.json({ error: 'OpenList 未配置或未启用' }, { status: 400 });
}
const rootPath = openListConfig.RootPath || '/';
diff --git a/src/app/api/openlist/refresh/route.ts b/src/app/api/openlist/refresh/route.ts
index 4fa3527..d815371 100644
--- a/src/app/api/openlist/refresh/route.ts
+++ b/src/app/api/openlist/refresh/route.ts
@@ -43,9 +43,15 @@ export async function POST(request: NextRequest) {
const config = await getConfig();
const openListConfig = config.OpenListConfig;
- if (!openListConfig || !openListConfig.URL || !openListConfig.Username || !openListConfig.Password) {
+ if (
+ !openListConfig ||
+ !openListConfig.Enabled ||
+ !openListConfig.URL ||
+ !openListConfig.Username ||
+ !openListConfig.Password
+ ) {
return NextResponse.json(
- { error: 'OpenList 未配置' },
+ { error: 'OpenList 未配置或未启用' },
{ status: 400 }
);
}
diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts
index 2b5a0ca..67c290b 100644
--- a/src/app/api/search/route.ts
+++ b/src/app/api/search/route.ts
@@ -37,7 +37,12 @@ export async function GET(request: NextRequest) {
const apiSites = await getAvailableApiSites(authInfo.username);
// 检查是否配置了 OpenList
- const hasOpenList = !!(config.OpenListConfig?.URL && config.OpenListConfig?.Username && config.OpenListConfig?.Password);
+ const hasOpenList = !!(
+ config.OpenListConfig?.Enabled &&
+ config.OpenListConfig?.URL &&
+ config.OpenListConfig?.Username &&
+ config.OpenListConfig?.Password
+ );
// 搜索 OpenList(如果配置了)
let openlistResults: any[] = [];
diff --git a/src/app/api/search/ws/route.ts b/src/app/api/search/ws/route.ts
index c896d41..f404723 100644
--- a/src/app/api/search/ws/route.ts
+++ b/src/app/api/search/ws/route.ts
@@ -34,7 +34,12 @@ export async function GET(request: NextRequest) {
const apiSites = await getAvailableApiSites(authInfo.username);
// 检查是否配置了 OpenList
- const hasOpenList = !!(config.OpenListConfig?.URL && config.OpenListConfig?.Username && config.OpenListConfig?.Password);
+ const hasOpenList = !!(
+ config.OpenListConfig?.Enabled &&
+ config.OpenListConfig?.URL &&
+ config.OpenListConfig?.Username &&
+ config.OpenListConfig?.Password
+ );
// 共享状态
let streamClosed = false;
From 8999577e80dd4064bdf3a27f95295e21fcf00ddb Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Thu, 25 Dec 2025 20:24:21 +0800
Subject: [PATCH 40/40] =?UTF-8?q?=E7=BC=96=E5=86=99v204=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E6=97=A5=E5=BF=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG | 16 ++++++++++++++++
VERSION.txt | 2 +-
src/lib/changelog.ts | 22 +++++++++++++++++++++-
src/lib/version.ts | 2 +-
4 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
index ac18ec3..3a89e19 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,19 @@
+## [204.0.0] - 2025-12-25
+
+### Added
+- ⚠️⚠️⚠️更新此版本前前务必进行备份!!!⚠️⚠️⚠️
+- 新增私人影视库功能(实验性)
+- 增加弹幕热力图
+- 增加盘搜搜索资源
+
+### Changed
+- 完全重构用户数据存储结构
+- 提高所有弹幕接口的超时时间
+- 优化完结标识判断
+- 即将上映移动端字体大小调整
+- tmdb增加代理支持
+- 剧集更新检测改为服务器后台定时执行
+
## [203.2.2] - 2025-12-20
### Fixed
diff --git a/VERSION.txt b/VERSION.txt
index 617b5b5..dc6ffb7 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1 +1 @@
-203.2.2
\ No newline at end of file
+204.0.0
\ No newline at end of file
diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts
index e1ea012..7820890 100644
--- a/src/lib/changelog.ts
+++ b/src/lib/changelog.ts
@@ -11,6 +11,25 @@ export interface ChangelogEntry {
export const changelog: ChangelogEntry[] = [
{
+ version: '204.0.0',
+ date: '2025-12-20',
+ added: [
+ "新增私人影视库功能(实验性)",
+ "增加弹幕热力图",
+ "增加盘搜搜索资源"
+ ],
+ changed: [
+ "完全重构用户数据存储结构",
+ "提高所有弹幕接口的超时时间",
+ "优化完结标识判断",
+ "即将上映移动端字体大小调整",
+ "tmdb增加代理支持",
+ "剧集更新检测改为服务器后台定时执行"
+ ],
+ fixed: [
+ ]
+ },
+ {
version: '203.2.2',
date: '2025-12-20',
added: [
@@ -21,7 +40,8 @@ export const changelog: ChangelogEntry[] = [
"修复IOS端换集报错播放器初始化失败",
"修复超分切换时重复渲染"
]
- },{
+ },
+ {
version: '203.2.0',
date: '2025-12-19',
added: [
diff --git a/src/lib/version.ts b/src/lib/version.ts
index 607ddb9..2888093 100644
--- a/src/lib/version.ts
+++ b/src/lib/version.ts
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
-const CURRENT_VERSION = '203.2.2';
+const CURRENT_VERSION = '204.0.0';
// 导出当前版本号供其他地方使用
export { CURRENT_VERSION };
|