@@ -10932,6 +10937,53 @@ const LiveSourceConfig = ({
});
};
+ const handleSetProxyMode = (key: string, mode: 'full' | 'm3u8-only' | 'direct') => {
+ withLoading(`setLiveProxyMode_${key}`, async () => {
+ // 保存旧值用于回滚
+ const oldMode = liveSources.find((s) => s.key === key)?.proxyMode;
+
+ // 乐观更新本地状态
+ setLiveSources((prev) =>
+ prev.map((s) =>
+ s.key === key ? { ...s, proxyMode: mode } : s
+ )
+ );
+
+ try {
+ const response = await fetch('/api/admin/live', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ action: 'set_proxy_mode',
+ key,
+ proxyMode: mode,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error('设置代理模式失败');
+ }
+
+ // 成功后刷新配置
+ await refreshConfig();
+ } catch (error) {
+ // 失败时回滚本地状态
+ setLiveSources((prev) =>
+ prev.map((s) =>
+ s.key === key ? { ...s, proxyMode: oldMode } : s
+ )
+ );
+ showError(
+ error instanceof Error ? error.message : '设置代理模式失败',
+ showAlert
+ );
+ throw error;
+ }
+ }).catch(() => {
+ console.error('操作失败', 'set_proxy_mode', key);
+ });
+ };
+
const handleDelete = (key: string) => {
withLoading(`deleteLiveSource_${key}`, () =>
callLiveSourceApi({ action: 'delete', key })
@@ -11108,6 +11160,24 @@ const LiveSourceConfig = ({
{!liveSource.disabled ? '启用中' : '已禁用'}
+
+
+ |
{/* 播放器 */}
{/* 播放器容器 */}
@@ -7629,6 +7946,28 @@ function PlayPageClient() {
>
重试
+ {/* 直链播放 CORS 失败时,显示"使用代理播放"按钮 */}
+ {!proxyAttemptedRef.current && (corsFailedUrl || (isDirectPlay && videoUrl && !videoUrl.includes('/api/proxy-m3u8'))) && (
+ {
+ const originalUrl = corsFailedUrl || videoUrl;
+ // 记忆域名到 localStorage
+ addDirectplayProxyDomain(originalUrl);
+ // 构建代理 URL
+ const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : '';
+ const proxyUrl = `/api/proxy-m3u8?url=${encodeURIComponent(originalUrl)}&source=directplay${tokenParam}`;
+ // 清除错误状态并重新播放
+ setVideoError(null);
+ setCorsFailedUrl(null);
+ setIsVideoLoading(true);
+ proxyAttemptedRef.current = true;
+ setVideoUrl(proxyUrl);
+ }}
+ className='mt-4 ml-3 px-6 py-2 bg-gradient-to-r from-blue-500 to-indigo-600 text-white rounded-lg hover:from-blue-600 hover:to-indigo-700 transition-all duration-200'
+ >
+ 使用代理播放
+
+ )}
>
) : (
@@ -7675,8 +8014,8 @@ function PlayPageClient() {
@@ -7797,12 +8136,12 @@ function PlayPageClient() {
{/* 去广告开关 */}
setExternalPlayerAdBlock(!externalPlayerAdBlock)}
- className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-200 shadow-sm hover:shadow-md cursor-pointer border flex-shrink-0 ${
- externalPlayerAdBlock
- ? 'bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white border-blue-400'
- : 'bg-white hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 border-gray-300 dark:border-gray-600'
- }`}
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-200 shadow-sm hover:shadow-md cursor-pointer border flex-shrink-0 ${externalPlayerAdBlock
+ ? 'bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white border-blue-400'
+ : 'bg-white hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 border-gray-300 dark:border-gray-600'
+ }`}
title={externalPlayerAdBlock ? '去广告已开启' : '去广告已关闭'}
>
)}
+
+ {/* AI评论区域 */}
+ {videoTitle && enableAIComments && (
+
+
+ {/* 标题 */}
+
+
+ {/* 评论内容 */}
+
+
+
+ )}
>
)}
@@ -8537,18 +8895,18 @@ function PlayPageClient() {
// 特殊源使用 tmdb,其他使用 cms(通过 doubanId)
// 如果有豆瓣ID且不为0,传入doubanId
detail.source === 'openlist' ||
- detail.source?.startsWith('emby') ||
- detail.source === 'xiaoya'
+ detail.source?.startsWith('emby') ||
+ detail.source === 'xiaoya'
? undefined
: detail.douban_id && detail.douban_id !== 0
- ? detail.douban_id
- : undefined
+ ? detail.douban_id
+ : undefined
}
tmdbId={
// 特殊源使用 tmdb
detail.source === 'openlist' ||
- detail.source?.startsWith('emby') ||
- detail.source === 'xiaoya'
+ detail.source?.startsWith('emby') ||
+ detail.source === 'xiaoya'
? detail.tmdb_id
: undefined
}
@@ -8558,14 +8916,14 @@ function PlayPageClient() {
// 非特殊源使用 cms 数据
// 但如果有豆瓣ID且不为0,则不传入cmsData,优先使用豆瓣数据
detail.source !== 'openlist' &&
- !detail.source?.startsWith('emby') &&
- detail.source !== 'xiaoya' &&
- !(detail.douban_id && detail.douban_id !== 0)
+ !detail.source?.startsWith('emby') &&
+ detail.source !== 'xiaoya' &&
+ !(detail.douban_id && detail.douban_id !== 0)
? {
- desc: detail.desc,
- episodes: detail.episodes,
- episodes_titles: detail.episodes_titles,
- }
+ desc: detail.desc,
+ episodes: detail.episodes,
+ episodes_titles: detail.episodes_titles,
+ }
: undefined
}
sourceId={detail.id}
diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx
index 5f05588..39bc6ca 100644
--- a/src/app/search/page.tsx
+++ b/src/app/search/page.tsx
@@ -1,9 +1,26 @@
/* eslint-disable react-hooks/exhaustive-deps, @typescript-eslint/no-explicit-any,@typescript-eslint/no-non-null-assertion,no-empty */
'use client';
-import { ChevronUp, Film, HardDrive, Magnet,RefreshCw, Search, X } from 'lucide-react';
+import {
+ ChevronUp,
+ Film,
+ Grid2x2,
+ HardDrive,
+ List,
+ Magnet,
+ RefreshCw,
+ Search,
+ X,
+} from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
-import React, { startTransition, Suspense, useEffect, useMemo, useRef, useState } from 'react';
+import React, {
+ startTransition,
+ Suspense,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
import {
@@ -14,12 +31,16 @@ import {
subscribeToDataUpdates,
} from '@/lib/db.client';
import { SearchResult } from '@/lib/types';
+import { processImageUrl } from '@/lib/utils';
import AcgSearch from '@/components/AcgSearch';
import CapsuleSwitch from '@/components/CapsuleSwitch';
+import ImageViewer from '@/components/ImageViewer';
import PageLayout from '@/components/PageLayout';
import PansouSearch from '@/components/PansouSearch';
-import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter';
+import SearchResultFilter, {
+ SearchFilterCategory,
+} from '@/components/SearchResultFilter';
import SearchSuggestions from '@/components/SearchSuggestions';
import VideoCard, { VideoCardHandle } from '@/components/VideoCard';
import VirtualScrollableGrid from '@/components/VirtualScrollableGrid';
@@ -30,13 +51,17 @@ function SearchPageClient() {
// 返回顶部按钮显示状态
const [showBackToTop, setShowBackToTop] = useState(false);
// 选项卡状态: 'video' 或 'pansou' 或 'acg'
- const [activeTab, setActiveTab] = useState<'video' | 'pansou' | 'acg'>('video');
+ const [activeTab, setActiveTab] = useState<'video' | 'pansou' | 'acg'>(
+ 'video'
+ );
// Pansou 搜索触发标志
const [triggerPansouSearch, setTriggerPansouSearch] = useState(false);
// ACG 搜索触发标志
const [triggerAcgSearch, setTriggerAcgSearch] = useState(false);
// 用户权限
- const [userRole, setUserRole] = useState<'owner' | 'admin' | 'user' | null>(null);
+ const [userRole, setUserRole] = useState<'owner' | 'admin' | 'user' | null>(
+ null
+ );
// 繁体转简体转换器
const converterRef = useRef<((text: string) => string) | null>(null);
// 转换器是否已初始化
@@ -57,8 +82,15 @@ function SearchPageClient() {
const flushTimerRef = useRef(null);
const [useFluidSearch, setUseFluidSearch] = useState(true);
// 聚合卡片 refs 与聚合统计缓存
- const groupRefs = useRef |