emby分类获取
This commit is contained in:
@@ -12,10 +12,11 @@ export async function GET(request: NextRequest) {
|
|||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const page = parseInt(searchParams.get('page') || '1');
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
const pageSize = parseInt(searchParams.get('pageSize') || '20');
|
const pageSize = parseInt(searchParams.get('pageSize') || '20');
|
||||||
|
const parentId = searchParams.get('parentId') || undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
const cached = getCachedEmbyList(page, pageSize);
|
const cached = getCachedEmbyList(page, pageSize, parentId);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return NextResponse.json(cached);
|
return NextResponse.json(cached);
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,7 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// 获取媒体列表
|
// 获取媒体列表
|
||||||
const result = await client.getItems({
|
const result = await client.getItems({
|
||||||
|
ParentId: parentId,
|
||||||
IncludeItemTypes: 'Movie,Series',
|
IncludeItemTypes: 'Movie,Series',
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
Fields: 'Overview,ProductionYear',
|
Fields: 'Overview,ProductionYear',
|
||||||
@@ -94,7 +96,7 @@ export async function GET(request: NextRequest) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 缓存结果
|
// 缓存结果
|
||||||
setCachedEmbyList(page, pageSize, response);
|
setCachedEmbyList(page, pageSize, response, parentId);
|
||||||
|
|
||||||
return NextResponse.json(response);
|
return NextResponse.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { EmbyClient } from '@/lib/emby.client';
|
||||||
|
import { getCachedEmbyViews, setCachedEmbyViews } from '@/lib/emby-cache';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// 检查缓存
|
||||||
|
const cached = getCachedEmbyViews();
|
||||||
|
if (cached) {
|
||||||
|
return NextResponse.json(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await getConfig();
|
||||||
|
const embyConfig = config.EmbyConfig;
|
||||||
|
|
||||||
|
if (!embyConfig?.Enabled || !embyConfig.ServerURL) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: 'Emby 未配置或未启用',
|
||||||
|
views: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Emby 客户端
|
||||||
|
const client = new EmbyClient(embyConfig);
|
||||||
|
|
||||||
|
// 如果使用用户名密码且没有 UserId,需要先认证
|
||||||
|
if (!embyConfig.ApiKey && !embyConfig.UserId && embyConfig.Username && embyConfig.Password) {
|
||||||
|
try {
|
||||||
|
const authResult = await client.authenticate(embyConfig.Username, embyConfig.Password);
|
||||||
|
embyConfig.UserId = authResult.User.Id;
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: 'Emby 认证失败: ' + (error as Error).message,
|
||||||
|
views: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证认证信息:必须有 ApiKey 或 UserId
|
||||||
|
if (!embyConfig.ApiKey && !embyConfig.UserId) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: 'Emby 认证失败,请检查配置',
|
||||||
|
views: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取媒体库列表
|
||||||
|
const views = await client.getUserViews();
|
||||||
|
|
||||||
|
// 过滤出电影和电视剧媒体库
|
||||||
|
const filteredViews = views.filter(
|
||||||
|
(view) => view.CollectionType === 'movies' || view.CollectionType === 'tvshows'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
views: filteredViews.map((view) => ({
|
||||||
|
id: view.Id,
|
||||||
|
name: view.Name,
|
||||||
|
type: view.CollectionType,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
setCachedEmbyViews(response);
|
||||||
|
|
||||||
|
return NextResponse.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 Emby 媒体库列表失败:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
error: '获取 Emby 媒体库列表失败: ' + (error as Error).message,
|
||||||
|
views: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
|
|
||||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||||
@@ -25,8 +25,15 @@ interface Video {
|
|||||||
mediaType: 'movie' | 'tv';
|
mediaType: 'movie' | 'tv';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EmbyView {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function PrivateLibraryPage() {
|
export default function PrivateLibraryPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const [source, setSource] = useState<LibrarySource>('openlist');
|
const [source, setSource] = useState<LibrarySource>('openlist');
|
||||||
const [videos, setVideos] = useState<Video[]>([]);
|
const [videos, setVideos] = useState<Video[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -34,19 +41,138 @@ export default function PrivateLibraryPage() {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
const [embyViews, setEmbyViews] = useState<EmbyView[]>([]);
|
||||||
|
const [selectedView, setSelectedView] = useState<string>('all');
|
||||||
|
const [loadingViews, setLoadingViews] = useState(false);
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const observerTarget = useRef<HTMLDivElement>(null);
|
const observerTarget = useRef<HTMLDivElement>(null);
|
||||||
const isFetchingRef = useRef(false);
|
const isFetchingRef = useRef(false);
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isDraggingRef = useRef(false);
|
||||||
|
const startXRef = useRef(0);
|
||||||
|
const scrollLeftRef = useRef(0);
|
||||||
|
const isInitializedRef = useRef(false);
|
||||||
|
|
||||||
// 切换源时重置所有状态
|
// 从URL初始化状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const urlSource = searchParams.get('source') as LibrarySource;
|
||||||
|
const urlView = searchParams.get('view');
|
||||||
|
|
||||||
|
if (urlSource && (urlSource === 'openlist' || urlSource === 'emby')) {
|
||||||
|
setSource(urlSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlView) {
|
||||||
|
setSelectedView(urlView);
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitializedRef.current = true;
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInitializedRef.current) return;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('source', source);
|
||||||
|
if (source === 'emby' && selectedView !== 'all') {
|
||||||
|
params.set('view', selectedView);
|
||||||
|
}
|
||||||
|
router.replace(`/private-library?${params.toString()}`, { scroll: false });
|
||||||
|
}, [source, selectedView, router]);
|
||||||
|
|
||||||
|
// 切换源时重置所有状态(但不在初始化时执行)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInitializedRef.current) return;
|
||||||
|
|
||||||
|
setPage(1);
|
||||||
|
setVideos([]);
|
||||||
|
setHasMore(true);
|
||||||
|
setError('');
|
||||||
|
setSelectedView('all');
|
||||||
|
isFetchingRef.current = false;
|
||||||
|
}, [source]);
|
||||||
|
|
||||||
|
// 切换分类时重置状态(但不在初始化时执行)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInitializedRef.current) return;
|
||||||
|
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setVideos([]);
|
setVideos([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
setError('');
|
setError('');
|
||||||
isFetchingRef.current = false;
|
isFetchingRef.current = false;
|
||||||
|
}, [selectedView]);
|
||||||
|
|
||||||
|
// 获取 Emby 媒体库列表
|
||||||
|
useEffect(() => {
|
||||||
|
if (source !== 'emby') return;
|
||||||
|
|
||||||
|
const fetchEmbyViews = async () => {
|
||||||
|
setLoadingViews(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/emby/views');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
console.error('获取 Emby 媒体库列表失败:', data.error);
|
||||||
|
setEmbyViews([]);
|
||||||
|
} else {
|
||||||
|
setEmbyViews(data.views || []);
|
||||||
|
|
||||||
|
// 分类加载完成后,检查URL中是否有view参数
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取 Emby 媒体库列表失败:', err);
|
||||||
|
setEmbyViews([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingViews(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchEmbyViews();
|
||||||
}, [source]);
|
}, [source]);
|
||||||
|
|
||||||
|
// 鼠标拖动滚动
|
||||||
|
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
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<HTMLDivElement>) => {
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchVideos = async () => {
|
const fetchVideos = async () => {
|
||||||
@@ -69,7 +195,7 @@ export default function PrivateLibraryPage() {
|
|||||||
|
|
||||||
const endpoint = source === 'openlist'
|
const endpoint = source === 'openlist'
|
||||||
? `/api/openlist/list?page=${page}&pageSize=${pageSize}`
|
? `/api/openlist/list?page=${page}&pageSize=${pageSize}`
|
||||||
: `/api/emby/list?page=${page}&pageSize=${pageSize}`;
|
: `/api/emby/list?page=${page}&pageSize=${pageSize}${selectedView !== 'all' ? `&parentId=${selectedView}` : ''}`;
|
||||||
|
|
||||||
const response = await fetch(endpoint);
|
const response = await fetch(endpoint);
|
||||||
|
|
||||||
@@ -116,7 +242,7 @@ export default function PrivateLibraryPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchVideos();
|
fetchVideos();
|
||||||
}, [source, page]);
|
}, [source, page, selectedView]);
|
||||||
|
|
||||||
const handleVideoClick = (video: Video) => {
|
const handleVideoClick = (video: Video) => {
|
||||||
// 跳转到播放页面
|
// 跳转到播放页面
|
||||||
@@ -175,6 +301,54 @@ export default function PrivateLibraryPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Emby 分类选择器 */}
|
||||||
|
{source === 'emby' && (
|
||||||
|
<div className='mb-6'>
|
||||||
|
{loadingViews ? (
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<div className='w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin' />
|
||||||
|
</div>
|
||||||
|
) : embyViews.length > 0 ? (
|
||||||
|
<div className='relative'>
|
||||||
|
<div
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
className='overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing'
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 px-4 min-w-min'>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedView('all')}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap flex-shrink-0 ${
|
||||||
|
selectedView === 'all'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</button>
|
||||||
|
{embyViews.map((view) => (
|
||||||
|
<button
|
||||||
|
key={view.id}
|
||||||
|
onClick={() => setSelectedView(view.id)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap flex-shrink-0 ${
|
||||||
|
selectedView === view.id
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{view.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6'>
|
<div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6'>
|
||||||
<p className='text-red-800 dark:text-red-200'>{error}</p>
|
<p className='text-red-800 dark:text-red-200'>{error}</p>
|
||||||
|
|||||||
+37
-6
@@ -8,13 +8,15 @@ export interface EmbyCachedEntry<T> {
|
|||||||
|
|
||||||
// 缓存配置
|
// 缓存配置
|
||||||
const EMBY_CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6小时
|
const EMBY_CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6小时
|
||||||
|
const EMBY_VIEWS_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 1天
|
||||||
const EMBY_CACHE: Map<string, EmbyCachedEntry<any>> = new Map();
|
const EMBY_CACHE: Map<string, EmbyCachedEntry<any>> = new Map();
|
||||||
|
const EMBY_VIEWS_CACHE_KEY = 'emby:views';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成 Emby 列表缓存键
|
* 生成 Emby 列表缓存键
|
||||||
*/
|
*/
|
||||||
function makeListCacheKey(page: number, pageSize: number): string {
|
function makeListCacheKey(page: number, pageSize: number, parentId?: string): string {
|
||||||
return `emby:list:${page}:${pageSize}`;
|
return parentId ? `emby:list:${page}:${pageSize}:${parentId}` : `emby:list:${page}:${pageSize}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,9 +24,10 @@ function makeListCacheKey(page: number, pageSize: number): string {
|
|||||||
*/
|
*/
|
||||||
export function getCachedEmbyList(
|
export function getCachedEmbyList(
|
||||||
page: number,
|
page: number,
|
||||||
pageSize: number
|
pageSize: number,
|
||||||
|
parentId?: string
|
||||||
): any | null {
|
): any | null {
|
||||||
const key = makeListCacheKey(page, pageSize);
|
const key = makeListCacheKey(page, pageSize, parentId);
|
||||||
const entry = EMBY_CACHE.get(key);
|
const entry = EMBY_CACHE.get(key);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
|
|
||||||
@@ -43,10 +46,11 @@ export function getCachedEmbyList(
|
|||||||
export function setCachedEmbyList(
|
export function setCachedEmbyList(
|
||||||
page: number,
|
page: number,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
data: any
|
data: any,
|
||||||
|
parentId?: string
|
||||||
): void {
|
): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const key = makeListCacheKey(page, pageSize);
|
const key = makeListCacheKey(page, pageSize, parentId);
|
||||||
EMBY_CACHE.set(key, {
|
EMBY_CACHE.set(key, {
|
||||||
expiresAt: now + EMBY_CACHE_TTL_MS,
|
expiresAt: now + EMBY_CACHE_TTL_MS,
|
||||||
data,
|
data,
|
||||||
@@ -62,6 +66,33 @@ export function clearEmbyCache(): { cleared: number } {
|
|||||||
return { cleared: size };
|
return { cleared: size };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的 Emby 媒体库列表
|
||||||
|
*/
|
||||||
|
export function getCachedEmbyViews(): any | null {
|
||||||
|
const entry = EMBY_CACHE.get(EMBY_VIEWS_CACHE_KEY);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (entry.expiresAt <= Date.now()) {
|
||||||
|
EMBY_CACHE.delete(EMBY_VIEWS_CACHE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存的 Emby 媒体库列表
|
||||||
|
*/
|
||||||
|
export function setCachedEmbyViews(data: any): void {
|
||||||
|
const now = Date.now();
|
||||||
|
EMBY_CACHE.set(EMBY_VIEWS_CACHE_KEY, {
|
||||||
|
expiresAt: now + EMBY_VIEWS_CACHE_TTL_MS,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取缓存统计信息
|
* 获取缓存统计信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ interface GetItemsParams {
|
|||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EmbyView {
|
||||||
|
Id: string;
|
||||||
|
Name: string;
|
||||||
|
CollectionType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class EmbyClient {
|
export class EmbyClient {
|
||||||
private serverUrl: string;
|
private serverUrl: string;
|
||||||
private apiKey?: string;
|
private apiKey?: string;
|
||||||
@@ -165,6 +171,49 @@ export class EmbyClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserViews(): Promise<EmbyView[]> {
|
||||||
|
await this.ensureAuthenticated();
|
||||||
|
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Error('未配置 Emby 用户 ID,请在管理面板重新保存 Emby 配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.apiKey || this.authToken;
|
||||||
|
const url = `${this.serverUrl}/Users/${this.userId}/Views${token ? `?api_key=${token}` : ''}`;
|
||||||
|
|
||||||
|
console.log('[EmbyClient] getUserViews - URL:', url);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
// 如果是 401 错误且有用户名密码,尝试重新认证
|
||||||
|
if (response.status === 401 && this.username && this.password && !this.apiKey) {
|
||||||
|
console.log('[EmbyClient] Token expired, re-authenticating...');
|
||||||
|
const authResult = await this.authenticate(this.username, this.password);
|
||||||
|
this.authToken = authResult.AccessToken;
|
||||||
|
this.userId = authResult.User.Id;
|
||||||
|
|
||||||
|
// 重试请求
|
||||||
|
const retryUrl = `${this.serverUrl}/Users/${this.userId}/Views?api_key=${this.authToken}`;
|
||||||
|
const retryResponse = await fetch(retryUrl);
|
||||||
|
|
||||||
|
if (!retryResponse.ok) {
|
||||||
|
const errorText = await retryResponse.text();
|
||||||
|
throw new Error(`获取 Emby 媒体库列表失败 (${retryResponse.status}): ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryData = await retryResponse.json();
|
||||||
|
return retryData.Items || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`获取 Emby 媒体库列表失败 (${response.status}): ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.Items || [];
|
||||||
|
}
|
||||||
|
|
||||||
async getItems(params: GetItemsParams): Promise<EmbyItemsResult> {
|
async getItems(params: GetItemsParams): Promise<EmbyItemsResult> {
|
||||||
await this.ensureAuthenticated();
|
await this.ensureAuthenticated();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user