diff --git a/internal/service/media.go b/internal/service/media.go index 551eb4e..33eb291 100644 --- a/internal/service/media.go +++ b/internal/service/media.go @@ -198,8 +198,10 @@ func (s *MediaService) SearchMedia(ctx context.Context, query string, limit int) } func (s *MediaService) SearchMediaVisible(ctx context.Context, query string, limit int, visibility MediaVisibility) ([]model.Media, error) { - if limit <= 0 || limit > 200 { + if limit <= 0 { limit = 50 + } else if limit > 2000 { + limit = 2000 } return s.repo.Media.SearchFiltered(ctx, query, limit, repository.MediaQueryFilter{ IncludeNSFW: visibility.IncludeNSFW, diff --git a/internal/service/media_visibility_test.go b/internal/service/media_visibility_test.go index 4981d47..f97b571 100644 --- a/internal/service/media_visibility_test.go +++ b/internal/service/media_visibility_test.go @@ -1,6 +1,7 @@ package service import ( + "fmt" "slices" "testing" @@ -140,6 +141,41 @@ func TestConfiguredAdultLibrariesDoNotHideSafeLibraryWithNSFWItems(t *testing.T) } } +func TestSearchMediaVisibleHonorsLargePosterWallLimit(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.Library{}, &model.Media{}); err != nil { + t.Fatal(err) + } + repos := repository.New(db) + lib := model.Library{Name: "海报墙", Path: "/media/all", Type: "tv", Enabled: true} + if err := repos.Library.Create(t.Context(), &lib); err != nil { + t.Fatal(err) + } + rows := make([]model.Media, 260) + for i := range rows { + rows[i] = model.Media{ + LibraryID: lib.ID, + Title: fmt.Sprintf("节目 %03d", i), + Path: fmt.Sprintf("/media/all/show-%03d.mkv", i), + } + } + if err := db.Create(&rows).Error; err != nil { + t.Fatal(err) + } + + items, err := NewMediaService(&config.Config{}, zap.NewNop(), repos). + SearchMediaVisible(t.Context(), "", 240, MediaVisibility{IncludeNSFW: true}) + if err != nil { + t.Fatal(err) + } + if len(items) != 240 { + t.Fatalf("large poster wall search returned %d rows, want 240", len(items)) + } +} + func sortedMediaTitles(rows []model.Media) []string { out := make([]string, 0, len(rows)) for _, row := range rows { diff --git a/web/src/pages/PosterWallPage.tsx b/web/src/pages/PosterWallPage.tsx index e32094c..2f47a98 100644 --- a/web/src/pages/PosterWallPage.tsx +++ b/web/src/pages/PosterWallPage.tsx @@ -1,24 +1,55 @@ import { useEffect, useMemo, useState } from 'react' import { GalleryHorizontalEnd, Layers } from 'lucide-react' -import { mediaAPI } from '../api/library' +import { libraryAPI } from '../api/library' import { MediaCard } from '../components/MediaCard' import { groupSeries } from '../utils/groupSeries' import type { Media } from '../types' +const POSTER_WALL_PAGE_SIZE = 2000 + // PosterWallPage 把所有媒体的代表海报聚合到同一面墙,便于一目了然 // 浏览整个站点的内容。所有 episode 行会按剧集折叠,避免同一海报刷屏。 export function PosterWallPage() { const [items, setItems] = useState([]) const [loading, setLoading] = useState(true) + const [error, setError] = useState('') useEffect(() => { - // 拉得多一点(500),折叠后才能凑够展示量;后端 page_size 上限可能 - // 需要后续观察,暂以 500 试探。 - mediaAPI - .search('', 500) - .then((d) => setItems(d.items)) - .finally(() => setLoading(false)) + let cancelled = false + async function loadPosterWall() { + setLoading(true) + setError('') + try { + const libraries = await libraryAPI.list() + const all = ( + await Promise.all( + libraries.map(async (library) => { + const rows: Media[] = [] + let page = 1 + for (;;) { + const data = await libraryAPI.listMedia(library.id, page, POSTER_WALL_PAGE_SIZE) + rows.push(...(data.items || [])) + if (rows.length >= data.total || data.items.length === 0) break + page += 1 + } + return rows + }), + ) + ).flat() + if (!cancelled) setItems(all) + } catch (err) { + if (!cancelled) { + setError((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '海报墙加载失败') + } + } finally { + if (!cancelled) setLoading(false) + } + } + loadPosterWall() + return () => { + cancelled = true + } }, []) const cards = useMemo(() => groupSeries(items).slice(0, 240), [items]) @@ -36,7 +67,12 @@ export function PosterWallPage() { {loading &&

加载中…

} - {!loading && cards.length === 0 && ( + {!loading && error && ( +

+ {error} +

+ )} + {!loading && !error && cards.length === 0 && (

暂无媒体。请先添加媒体库并扫描。

)}