/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ 'use client'; import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState, useRef, useMemo } from 'react'; import CapsuleSwitch from '@/components/CapsuleSwitch'; import PageLayout from '@/components/PageLayout'; import VideoCard from '@/components/VideoCard'; type LibrarySourceType = 'openlist' | 'emby' | 'xiaoya' | `emby:${string}` | `emby_${string}`; interface EmbySourceOption { key: string; name: string; } interface Video { id: string; folder?: string; tmdbId?: number; title: string; poster: string; releaseDate?: string; year?: string; overview?: string; voteAverage?: number; rating?: number; mediaType: 'movie' | 'tv'; } interface EmbyView { id: string; name: string; type: string; } export default function PrivateLibraryPage() { const router = useRouter(); const searchParams = useSearchParams(); // 获取运行时配置 const runtimeConfig = useMemo(() => { if (typeof window !== 'undefined' && (window as any).RUNTIME_CONFIG) { return (window as any).RUNTIME_CONFIG; } return { OPENLIST_ENABLED: false, EMBY_ENABLED: false, XIAOYA_ENABLED: false }; }, []); // 解析URL中的source参数(支持 emby:emby1 格式) const parseSourceParam = (sourceParam: string | null): { sourceType: LibrarySourceType; embyKey?: string } => { if (!sourceParam) return { sourceType: 'openlist' }; if (sourceParam.includes(':')) { const [type, key] = sourceParam.split(':'); return { sourceType: type as LibrarySourceType, embyKey: key }; } return { sourceType: sourceParam as LibrarySourceType }; }; const [sourceType, setSourceType] = useState('openlist'); const [embyKey, setEmbyKey] = useState(); const [embySourceOptions, setEmbySourceOptions] = useState([]); const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(''); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [embyViews, setEmbyViews] = useState([]); const [selectedView, setSelectedView] = useState('all'); const [loadingViews, setLoadingViews] = useState(false); // 小雅相关状态 const [xiaoyaPath, setXiaoyaPath] = useState('/'); const [xiaoyaFolders, setXiaoyaFolders] = useState>([]); const [xiaoyaFiles, setXiaoyaFiles] = useState>([]); const pageSize = 20; const observerTarget = useRef(null); const isFetchingRef = useRef(false); const abortControllerRef = useRef(null); const scrollContainerRef = useRef(null); const embyScrollContainerRef = useRef(null); const isDraggingRef = useRef(false); const startXRef = useRef(0); const scrollLeftRef = useRef(0); const isInitializedRef = useRef(false); const hasRestoredViewRef = useRef(false); // 从URL初始化状态,并检查配置自动跳转 useEffect(() => { const urlSourceParam = searchParams.get('source'); // 解析source参数 const parsed = parseSourceParam(urlSourceParam); // 如果 OpenList 未配置但 Emby 已配置,强制使用 Emby if (!runtimeConfig.OPENLIST_ENABLED && runtimeConfig.EMBY_ENABLED) { setSourceType('emby'); } else if (parsed.sourceType) { setSourceType(parsed.sourceType); if (parsed.embyKey) { setEmbyKey(parsed.embyKey); } } isInitializedRef.current = true; }, [searchParams, runtimeConfig]); // 获取Emby源列表 useEffect(() => { const fetchEmbySources = async () => { try { const response = await fetch('/api/emby/sources'); if (response.ok) { const data = await response.json(); setEmbySourceOptions(data.sources || []); // 如果没有设置embyKey,使用第一个源 if (!embyKey && data.sources && data.sources.length > 0) { setEmbyKey(data.sources[0].key); } } } catch (error) { console.error('获取Emby源列表失败:', error); } }; if (sourceType === 'emby') { fetchEmbySources(); } }, [sourceType]); // 更新URL参数 useEffect(() => { if (!isInitializedRef.current) return; const params = new URLSearchParams(); // 构建source参数 if (sourceType === 'emby' && embyKey && embySourceOptions.length > 1) { params.set('source', `emby:${embyKey}`); } else { params.set('source', sourceType); } if (sourceType === 'emby' && selectedView !== 'all') { params.set('view', selectedView); } router.replace(`/private-library?${params.toString()}`, { scroll: false }); }, [sourceType, embyKey, selectedView, router, embySourceOptions.length]); // 切换源类型时重置所有状态(但不在初始化时执行) useEffect(() => { if (!isInitializedRef.current) return; setPage(1); setVideos([]); setHasMore(true); setError(''); setSelectedView('all'); setLoading(false); setLoadingMore(false); isFetchingRef.current = false; }, [sourceType, embyKey]); // 切换分类时重置状态(但不在初始化时执行) useEffect(() => { if (!isInitializedRef.current) return; setPage(1); setVideos([]); setHasMore(true); setError(''); setLoading(false); setLoadingMore(false); isFetchingRef.current = false; }, [selectedView]); // 获取 Emby 媒体库列表 useEffect(() => { if (sourceType !== 'emby' || !embyKey) return; const fetchEmbyViews = async () => { setLoadingViews(true); try { const params = new URLSearchParams({ embyKey }); const response = await fetch(`/api/emby/views?${params.toString()}`); const data = await response.json(); if (data.error) { console.error('获取 Emby 媒体库列表失败:', data.error); setEmbyViews([]); } else { setEmbyViews(data.views || []); // 分类加载完成后,检查URL中是否有view参数(只在第一次加载时恢复) if (!hasRestoredViewRef.current) { const urlView = searchParams.get('view'); if (urlView && data.views && data.views.length > 0) { // 检查该view是否存在于分类列表中 const viewExists = data.views.some((v: EmbyView) => v.id === urlView); if (viewExists) { setSelectedView(urlView); } } hasRestoredViewRef.current = true; } } } catch (err) { console.error('获取 Emby 媒体库列表失败:', err); setEmbyViews([]); } finally { setLoadingViews(false); } }; fetchEmbyViews(); }, [sourceType, embyKey]); // 鼠标拖动滚动 const handleMouseDown = (e: React.MouseEvent) => { if (!scrollContainerRef.current) return; isDraggingRef.current = true; startXRef.current = e.pageX - scrollContainerRef.current.offsetLeft; scrollLeftRef.current = scrollContainerRef.current.scrollLeft; scrollContainerRef.current.style.cursor = 'grabbing'; scrollContainerRef.current.style.userSelect = 'none'; }; const handleMouseLeave = () => { if (!scrollContainerRef.current) return; isDraggingRef.current = false; scrollContainerRef.current.style.cursor = 'grab'; scrollContainerRef.current.style.userSelect = 'auto'; }; const handleMouseUp = () => { if (!scrollContainerRef.current) return; isDraggingRef.current = false; scrollContainerRef.current.style.cursor = 'grab'; scrollContainerRef.current.style.userSelect = 'auto'; }; const handleMouseMove = (e: React.MouseEvent) => { if (!isDraggingRef.current || !scrollContainerRef.current) return; e.preventDefault(); const x = e.pageX - scrollContainerRef.current.offsetLeft; const walk = (x - startXRef.current) * 2; // 滚动速度倍数 scrollContainerRef.current.scrollLeft = scrollLeftRef.current - walk; }; // 加载数据的函数 useEffect(() => { const fetchVideos = async () => { const isInitial = page === 1; // 取消之前的请求 if (abortControllerRef.current) { abortControllerRef.current.abort(); } // 如果选择了 openlist 但未配置,不发起请求 if (sourceType === 'openlist' && !runtimeConfig.OPENLIST_ENABLED) { setLoading(false); return; } // 如果选择了 emby 但未配置或没有embyKey,不发起请求 if (sourceType === 'emby' && (!runtimeConfig.EMBY_ENABLED || !embyKey)) { setLoading(false); return; } // 如果选择了 xiaoya 但未配置,不发起请求 if (sourceType === 'xiaoya' && !runtimeConfig.XIAOYA_ENABLED) { setLoading(false); return; } // 创建新的 AbortController const abortController = new AbortController(); abortControllerRef.current = abortController; isFetchingRef.current = true; try { if (isInitial) { setLoading(true); } else { setLoadingMore(true); } setError(''); const endpoint = sourceType === 'openlist' ? `/api/openlist/list?page=${page}&pageSize=${pageSize}` : sourceType === 'xiaoya' ? `/api/xiaoya/browse?path=${encodeURIComponent(xiaoyaPath)}` : `/api/emby/list?page=${page}&pageSize=${pageSize}${selectedView !== 'all' ? `&parentId=${selectedView}` : ''}&embyKey=${embyKey}`; const response = await fetch(endpoint, { signal: abortController.signal }); if (!response.ok) { throw new Error('获取视频列表失败'); } const data = await response.json(); if (data.error) { setError(data.error); if (isInitial) { setVideos([]); } } else { // 小雅返回的是文件夹和文件列表 if (sourceType === 'xiaoya') { setXiaoyaFolders(data.folders || []); setXiaoyaFiles(data.files || []); setVideos([]); // 小雅不使用 videos 状态 setHasMore(false); // 小雅不需要分页 } else { const newVideos = data.list || []; if (isInitial) { setVideos(newVideos); } else { setVideos((prev) => [...prev, ...newVideos]); } // 检查是否还有更多数据 const currentPage = data.page || page; const totalPages = data.totalPages || 1; const hasMoreData = currentPage < totalPages; setHasMore(hasMoreData); } } } catch (err: any) { // 忽略取消请求的错误 if (err.name === 'AbortError') { return; } console.error('获取视频列表失败:', err); setError('获取视频列表失败'); if (isInitial) { setVideos([]); } } finally { // 只有当这个请求没有被取消时才更新状态 if (!abortController.signal.aborted) { if (isInitial) { setLoading(false); } else { setLoadingMore(false); } isFetchingRef.current = false; } } }; fetchVideos(); // 清理函数:组件卸载时取消请求 return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, [sourceType, embyKey, page, selectedView, xiaoyaPath, runtimeConfig]); const handleVideoClick = (video: Video) => { // 构建source参数 let sourceParam = sourceType; if (sourceType === 'emby' && embyKey && embySourceOptions.length > 1) { sourceParam = `emby:${embyKey}`; } // 跳转到播放页面 router.push(`/play?source=${sourceParam}&id=${encodeURIComponent(video.id)}`); }; // 使用 Intersection Observer 监听滚动 useEffect(() => { if (!observerTarget.current) { return; } const observer = new IntersectionObserver( (entries) => { const entry = entries[0]; // 当目标元素可见且还有更多数据且没有正在加载时,加载下一页 if (entry.isIntersecting && hasMore && !loadingMore && !loading && !isFetchingRef.current) { setPage((prev) => prev + 1); } }, { threshold: 0.1, rootMargin: '100px' } ); const currentTarget = observerTarget.current; observer.observe(currentTarget); return () => { if (currentTarget) { observer.unobserve(currentTarget); } }; }, [hasMore, loadingMore, loading, page]); return (

私人影库

观看自我收藏的高清视频吧

{/* 第一级:源类型选择(OpenList / Emby / 小雅) */}
setSourceType(value as LibrarySourceType)} />
{/* 第二级:Emby源选择(仅当选择Emby且有多个源时显示) */} {sourceType === 'emby' && embySourceOptions.length > 1 && (
服务
{ if (!embyScrollContainerRef.current) return; isDraggingRef.current = true; startXRef.current = e.pageX - embyScrollContainerRef.current.offsetLeft; scrollLeftRef.current = embyScrollContainerRef.current.scrollLeft; embyScrollContainerRef.current.style.cursor = 'grabbing'; embyScrollContainerRef.current.style.userSelect = 'none'; }} onMouseLeave={() => { if (!embyScrollContainerRef.current) return; isDraggingRef.current = false; embyScrollContainerRef.current.style.cursor = 'grab'; embyScrollContainerRef.current.style.userSelect = 'auto'; }} onMouseUp={() => { if (!embyScrollContainerRef.current) return; isDraggingRef.current = false; embyScrollContainerRef.current.style.cursor = 'grab'; embyScrollContainerRef.current.style.userSelect = 'auto'; }} onMouseMove={(e) => { if (!isDraggingRef.current || !embyScrollContainerRef.current) return; e.preventDefault(); const x = e.pageX - embyScrollContainerRef.current.offsetLeft; const walk = (x - startXRef.current) * 2; embyScrollContainerRef.current.scrollLeft = scrollLeftRef.current - walk; }} >
{embySourceOptions.map((option) => ( ))}
)} {/* 第三级:Emby 媒体库分类选择器 */} {sourceType === 'emby' && (
分类
{loadingViews ? (
) : embyViews.length > 0 ? (
{embyViews.map((view) => ( ))}
) : null}
)} {error && (

{error}

)} {loading ? (
{Array.from({ length: pageSize }).map((_, index) => (
))}
) : sourceType === 'xiaoya' ? ( // 小雅浏览模式
{/* 面包屑导航 */}
{xiaoyaPath.split('/').filter(Boolean).map((part, index, arr) => { const path = '/' + arr.slice(0, index + 1).join('/'); return ( / ); })}
{/* 文件夹列表 */} {xiaoyaFolders.length > 0 && (

文件夹

{xiaoyaFolders.map((folder) => ( ))}
)} {/* 视频文件列表 */} {xiaoyaFiles.length > 0 && (

视频文件

{xiaoyaFiles.map((file) => { // 从当前路径提取文件夹名作为标题 const pathParts = xiaoyaPath.split('/').filter(Boolean); const folderName = pathParts[pathParts.length - 1] || ''; // 清理文件夹名(移除年份和 TMDb ID) const title = folderName .replace(/\s*\(\d{4}\)\s*\{tmdb-\d+\}$/i, '') .trim() || file.name; return ( ); })}
)} {xiaoyaFolders.length === 0 && xiaoyaFiles.length === 0 && (

此目录为空

)}
) : videos.length === 0 ? (

{sourceType === 'openlist' ? '暂无视频,请在管理面板配置 OpenList 并刷新' : '暂无视频,请在管理面板配置 Emby'}

) : ( <>
{videos.map((video) => { // 构建source参数用于VideoCard // 如果是emby源且有embyKey,使用下划线格式 let sourceParam = sourceType; if (sourceType === 'emby' && embyKey) { sourceParam = `emby_${embyKey}`; } return ( 0 ? video.voteAverage.toFixed(1) : '' } from='search' /> ); })}
{/* 滚动加载指示器 - 始终渲染以便 observer 可以监听 */}
{loadingMore && (
加载中...
)} {!hasMore && videos.length > 0 && !loadingMore && (
已加载全部内容
)}
)}
); }