From 2c805e25226037dbee4e77f62a6daedd9588fb3a Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Mon, 12 Jan 2026 21:06:45 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B1=82=E7=89=87=E5=A2=9E=E5=8A=A0=E5=BC=80?=
=?UTF-8?q?=E5=85=B3=E5=92=8C=E5=86=B7=E5=8D=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app/admin/page.tsx | 103 +++++++++++++++++++++++++++-
src/app/api/movie-requests/route.ts | 26 +++++--
src/app/layout.tsx | 5 +-
src/app/movie-request/page.tsx | 68 +++++++++++-------
src/app/private-library/page.tsx | 16 +++--
src/lib/admin.types.ts | 3 +
src/lib/redis-base.db.ts | 2 +
src/lib/upstash.db.ts | 13 +++-
8 files changed, 194 insertions(+), 42 deletions(-)
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 1fee446..5caab46 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -8726,7 +8726,7 @@ const XiaoyaConfigComponent = ({
};
// 求片列表组件
-const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => void }) => {
+const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise }) => {
const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [requests, setRequests] = useState([]);
@@ -8735,6 +8735,11 @@ const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig
const [fulfilledCount, setFulfilledCount] = useState(0);
const [loading, setLoading] = useState(true);
+ // 求片功能设置
+ const [enableMovieRequest, setEnableMovieRequest] = useState(config?.SiteConfig?.EnableMovieRequest ?? true);
+ const [movieRequestCooldown, setMovieRequestCooldown] = useState(config?.SiteConfig?.MovieRequestCooldown ?? 3600);
+ const [savingSettings, setSavingSettings] = useState(false);
+
useEffect(() => {
loadRequests();
loadCounts();
@@ -8795,9 +8800,100 @@ const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig
});
};
+ const handleSaveSettings = async () => {
+ setSavingSettings(true);
+ try {
+ if (!config) throw new Error('配置未加载');
+
+ const updatedConfig = {
+ ...config,
+ SiteConfig: {
+ ...config.SiteConfig,
+ EnableMovieRequest: enableMovieRequest,
+ MovieRequestCooldown: movieRequestCooldown,
+ },
+ };
+
+ const response = await fetch('/api/admin/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updatedConfig),
+ });
+
+ if (!response.ok) throw new Error('保存失败');
+
+ showSuccess('求片设置已保存', showAlert);
+ await refreshConfig();
+ } catch (err) {
+ showError(err instanceof Error ? err.message : '保存失败', showAlert);
+ } finally {
+ setSavingSettings(false);
+ }
+ };
+
return (
-
+ {/* 求片功能设置 */}
+
+
求片功能设置
+
+
+
+
+
+ 关闭后用户将无法访问求片页面
+
+
+
+
+
+
+
+
+ 用户两次求片之间的最小间隔时间,默认3600秒(1小时)
+
+
setMovieRequestCooldown(parseInt(e.target.value) || 0)}
+ className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100'
+ />
+
+ {movieRequestCooldown >= 3600
+ ? `约 ${Math.floor(movieRequestCooldown / 3600)} 小时 ${Math.floor((movieRequestCooldown % 3600) / 60)} 分钟`
+ : movieRequestCooldown >= 60
+ ? `约 ${Math.floor(movieRequestCooldown / 60)} 分钟`
+ : `${movieRequestCooldown} 秒`}
+
+
+
+
+
+
+
+ {/* 求片列表 */}
+
+
求片列表
+
)}
+
toggleTab('movieRequests')}
>
-
+
diff --git a/src/app/api/movie-requests/route.ts b/src/app/api/movie-requests/route.ts
index 4ea8879..aaad560 100644
--- a/src/app/api/movie-requests/route.ts
+++ b/src/app/api/movie-requests/route.ts
@@ -71,6 +71,14 @@ export async function POST(request: NextRequest) {
}
try {
+ // 检查求片功能是否启用并获取冷却时间
+ const { getConfig } = await import('@/lib/config');
+ const config = await getConfig();
+
+ if (config.SiteConfig.EnableMovieRequest === false) {
+ return NextResponse.json({ error: '求片功能已关闭' }, { status: 403 });
+ }
+
const body = await request.json();
const { tmdbId, title, year, mediaType, season, poster, overview } = body;
@@ -80,10 +88,11 @@ export async function POST(request: NextRequest) {
const storage = getStorage();
- // 检查频率限制
- const userInfo = await storage.getUserInfoV2(authInfo.username);
- const rateLimit = parseInt(process.env.MOVIE_REQUEST_RATE_LIMIT || '3600') * 1000;
+ // 检查频率限制 - 使用配置中的冷却时间
+ const cooldownSeconds = config.SiteConfig.MovieRequestCooldown ?? 3600;
+ const rateLimit = cooldownSeconds * 1000;
+ const userInfo = await storage.getUserInfoV2(authInfo.username);
if (userInfo?.last_movie_request_time) {
const elapsed = Date.now() - userInfo.last_movie_request_time;
if (elapsed < rateLimit) {
@@ -163,12 +172,17 @@ export async function POST(request: NextRequest) {
await storage.createMovieRequest(newRequest);
await storage.addUserMovieRequest(authInfo.username, newRequest.id);
- // 更新频率限制
- await storage.setGlobalValue(
- `user:${authInfo.username}:info:last_movie_request_time`,
+ // 更新频率限制 - 保存到用户信息的 hash 中
+ await storage.client.hSet(
+ `user:${authInfo.username}:info`,
+ 'last_movie_request_time',
Date.now().toString()
);
+ // 清除用户信息缓存,确保下次读取到最新数据
+ const { userInfoCache } = await import('@/lib/user-cache');
+ userInfoCache?.delete(authInfo.username);
+
// 给站长发送通知
const ownerUsername = process.env.USERNAME;
if (ownerUsername) {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index bb6c48c..62a91b1 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -82,6 +82,7 @@ export default async function RootLayout({
let aiEnablePlayPageEntry = false;
let aiDefaultMessageNoVideo = '';
let aiDefaultMessageWithVideo = '';
+ let enableMovieRequest = true;
let customCategories = [] as {
name: string;
type: 'movie' | 'tv';
@@ -124,6 +125,8 @@ export default async function RootLayout({
aiEnablePlayPageEntry = config.AIConfig?.EnablePlayPageEntry || false;
aiDefaultMessageNoVideo = config.AIConfig?.DefaultMessageNoVideo || '';
aiDefaultMessageWithVideo = config.AIConfig?.DefaultMessageWithVideo || '';
+ // 求片功能配置
+ enableMovieRequest = config.SiteConfig.EnableMovieRequest ?? true;
// 检查是否启用了 OpenList 功能
openListEnabled = !!(
config.OpenListConfig?.Enabled &&
@@ -178,7 +181,7 @@ export default async function RootLayout({
AI_ENABLE_PLAYPAGE_ENTRY: aiEnablePlayPageEntry,
AI_DEFAULT_MESSAGE_NO_VIDEO: aiDefaultMessageNoVideo,
AI_DEFAULT_MESSAGE_WITH_VIDEO: aiDefaultMessageWithVideo,
- ENABLE_SOURCE_SEARCH: process.env.NEXT_PUBLIC_ENABLE_SOURCE_SEARCH !== 'false',
+ ENABLE_MOVIE_REQUEST: enableMovieRequest,
};
return (
diff --git a/src/app/movie-request/page.tsx b/src/app/movie-request/page.tsx
index f52ab9d..a213ea2 100644
--- a/src/app/movie-request/page.tsx
+++ b/src/app/movie-request/page.tsx
@@ -49,6 +49,15 @@ export default function MovieRequestPage() {
const [myRequests, setMyRequests] = useState
([]);
const [loadingMyRequests, setLoadingMyRequests] = useState(true);
const [submitting, setSubmitting] = useState(false);
+ const [isFeatureEnabled, setIsFeatureEnabled] = useState(true);
+
+ // 检查求片功能是否启用
+ useEffect(() => {
+ const runtimeConfig = (window as any).RUNTIME_CONFIG;
+ if (runtimeConfig && runtimeConfig.ENABLE_MOVIE_REQUEST === false) {
+ setIsFeatureEnabled(false);
+ }
+ }, []);
// TMDB搜索
const handleSearch = async () => {
@@ -199,32 +208,43 @@ export default function MovieRequestPage() {
求片
- 搜索并提交您想看的影片
+ {isFeatureEnabled ? '搜索并提交您想看的影片' : '求片功能已关闭,仅可查看已求片列表'}
- {/* 搜索框 */}
-
-
-
setSearchKeyword(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') handleSearch();
- }}
- className='flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500'
- />
-
+ {/* 功能关闭提示 */}
+ {!isFeatureEnabled && (
+
+
+ 求片功能已被管理员关闭,您可以查看已提交的求片记录
+
-
+ )}
+
+ {/* 搜索框 - 仅在功能启用时显示 */}
+ {isFeatureEnabled && (
+
+
+ setSearchKeyword(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleSearch();
+ }}
+ className='flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500'
+ />
+
+
+
+ )}
{/* 我的求片列表 */}
{searchResults.length === 0 && (
@@ -308,10 +328,10 @@ export default function MovieRequestPage() {
diff --git a/src/app/private-library/page.tsx b/src/app/private-library/page.tsx
index 9c25414..2f36979 100644
--- a/src/app/private-library/page.tsx
+++ b/src/app/private-library/page.tsx
@@ -457,13 +457,15 @@ export default function PrivateLibraryPage() {
观看自我收藏的高清视频吧
-
+ {mounted && (
+
+ )}
{/* 第一级:源类型选择(OpenList / Emby / 小雅) */}
diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts
index c36a6ca..18a04cd 100644
--- a/src/lib/admin.types.ts
+++ b/src/lib/admin.types.ts
@@ -40,6 +40,9 @@ export interface AdminConfig {
TurnstileSiteKey?: string; // Cloudflare Turnstile Site Key
TurnstileSecretKey?: string; // Cloudflare Turnstile Secret Key
DefaultUserTags?: string[]; // 新注册用户的默认用户组
+ // 求片功能配置
+ EnableMovieRequest?: boolean; // 启用求片功能
+ MovieRequestCooldown?: number; // 求片冷却时间(秒),默认3600
// OIDC配置
EnableOIDCLogin?: boolean; // 启用OIDC登录
EnableOIDCRegistration?: boolean; // 启用OIDC注册
diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts
index 1a43eb6..ec1ecbb 100644
--- a/src/lib/redis-base.db.ts
+++ b/src/lib/redis-base.db.ts
@@ -659,6 +659,7 @@ export abstract class BaseRedisStorage implements IStorage {
playrecord_migrated?: boolean;
favorite_migrated?: boolean;
skip_migrated?: boolean;
+ last_movie_request_time?: number;
} | null> {
const userInfo = await this.withRetry(() =>
this.client.hGetAll(this.userInfoKey(userName))
@@ -678,6 +679,7 @@ export abstract class BaseRedisStorage implements IStorage {
playrecord_migrated: userInfo.playrecord_migrated === 'true',
favorite_migrated: userInfo.favorite_migrated === 'true',
skip_migrated: userInfo.skip_migrated === 'true',
+ last_movie_request_time: userInfo.last_movie_request_time ? parseInt(userInfo.last_movie_request_time, 10) : undefined,
};
}
diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts
index aa83dda..4c0345f 100644
--- a/src/lib/upstash.db.ts
+++ b/src/lib/upstash.db.ts
@@ -65,7 +65,12 @@ export class UpstashRedisStorage implements IStorage {
this._client = getUpstashRedisClient();
// 创建兼容Redis API的client包装器(支持camelCase和lowercase)
this.client = {
- hSet: (key: string, data: Record) => this._client.hset(key, data),
+ hSet: (key: string, field: string | Record, value?: string) => {
+ if (typeof field === 'string' && value !== undefined) {
+ return this._client.hset(key, { [field]: value });
+ }
+ return this._client.hset(key, field as Record);
+ },
hset: (key: string, data: Record) => this._client.hset(key, data),
zAdd: (key: string, member: { score: number; value: string }) => this._client.zadd(key, { score: member.score, member: member.value }),
zadd: (key: string, member: { score: number; value: string }) => this._client.zadd(key, { score: member.score, member: member.value }),
@@ -584,6 +589,7 @@ export class UpstashRedisStorage implements IStorage {
playrecord_migrated?: boolean;
favorite_migrated?: boolean;
skip_migrated?: boolean;
+ last_movie_request_time?: number;
} | null> {
// 先从缓存获取
const cached = userInfoCache?.get(userName);
@@ -677,6 +683,11 @@ export class UpstashRedisStorage implements IStorage {
playrecord_migrated,
favorite_migrated,
skip_migrated,
+ last_movie_request_time: userInfo.last_movie_request_time
+ ? (typeof userInfo.last_movie_request_time === 'number'
+ ? userInfo.last_movie_request_time
+ : parseInt(userInfo.last_movie_request_time as string, 10))
+ : undefined,
};
// 存入缓存