mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 05:46:38 +08:00
优化首页/媒体库加载速度与 SQLite 查询性能
优化首页/媒体库加载速度与 SQLite 查询性能
This commit is contained in:
@@ -4,9 +4,7 @@ name: AuTo Docker Image
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
# 保留手动触发作为备选
|
# 保留手动触发作为备选
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func setDefaults(v *viper.Viper) {
|
|||||||
v.SetDefault("database.dsn", "")
|
v.SetDefault("database.dsn", "")
|
||||||
v.SetDefault("database.wal_mode", true)
|
v.SetDefault("database.wal_mode", true)
|
||||||
v.SetDefault("database.busy_timeout", 5000)
|
v.SetDefault("database.busy_timeout", 5000)
|
||||||
v.SetDefault("database.cache_size", -20000)
|
v.SetDefault("database.cache_size", -40000)
|
||||||
v.SetDefault("database.max_open_conns", defaultDatabaseMaxOpenConns)
|
v.SetDefault("database.max_open_conns", defaultDatabaseMaxOpenConns)
|
||||||
v.SetDefault("database.max_idle_conns", defaultDatabaseMaxIdleConns)
|
v.SetDefault("database.max_idle_conns", defaultDatabaseMaxIdleConns)
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,19 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if isSQLite(db) {
|
if isSQLite(db) {
|
||||||
return ensureMediaSearchIndex(db)
|
if err := ensureMediaSearchIndex(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ensureSQLiteQueryOptimizer(db)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureSQLiteQueryOptimizer(db *gorm.DB) error {
|
||||||
|
// Refresh planner statistics so indexes on large media tables are used.
|
||||||
|
return db.Exec("ANALYZE").Error
|
||||||
|
}
|
||||||
|
|
||||||
func ensurePostgresColumnCompatibility(db *gorm.DB) error {
|
func ensurePostgresColumnCompatibility(db *gorm.DB) error {
|
||||||
if !isPostgres(db) {
|
if !isPostgres(db) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -114,7 +114,10 @@ func buildSQLiteDSN(cfg *config.Config) string {
|
|||||||
if cfg.Database.CacheSize != 0 {
|
if cfg.Database.CacheSize != 0 {
|
||||||
dsn += fmt.Sprintf("&_pragma=cache_size(%d)", cfg.Database.CacheSize)
|
dsn += fmt.Sprintf("&_pragma=cache_size(%d)", cfg.Database.CacheSize)
|
||||||
}
|
}
|
||||||
dsn += "&_pragma=temp_store(MEMORY)&_pragma=mmap_size(268435456)"
|
dsn += "&_pragma=temp_store(MEMORY)&_pragma=mmap_size(536870912)"
|
||||||
|
if cfg.Database.WALMode {
|
||||||
|
dsn += "&_pragma=wal_autocheckpoint(1000)"
|
||||||
|
}
|
||||||
return dsn
|
return dsn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -87,6 +89,18 @@ func (r *MediaRepository) ListByLibraryFiltered(ctx context.Context, libraryID s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *MediaRepository) ListByLibrariesFiltered(ctx context.Context, libraryIDs []string, offset, limit int, filter MediaQueryFilter) ([]model.Media, int64, error) {
|
func (r *MediaRepository) ListByLibrariesFiltered(ctx context.Context, libraryIDs []string, offset, limit int, filter MediaQueryFilter) ([]model.Media, int64, error) {
|
||||||
|
items, total, err := r.listByLibrariesFiltered(ctx, libraryIDs, offset, limit, filter, true)
|
||||||
|
return items, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByLibrariesFilteredNoCount skips the COUNT query when the caller already
|
||||||
|
// knows totals or only needs a bounded slice (e.g. home-page previews).
|
||||||
|
func (r *MediaRepository) ListByLibrariesFilteredNoCount(ctx context.Context, libraryIDs []string, offset, limit int, filter MediaQueryFilter) ([]model.Media, error) {
|
||||||
|
items, _, err := r.listByLibrariesFiltered(ctx, libraryIDs, offset, limit, filter, false)
|
||||||
|
return items, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MediaRepository) listByLibrariesFiltered(ctx context.Context, libraryIDs []string, offset, limit int, filter MediaQueryFilter, withCount bool) ([]model.Media, int64, error) {
|
||||||
var items []model.Media
|
var items []model.Media
|
||||||
var total int64
|
var total int64
|
||||||
if len(libraryIDs) == 0 {
|
if len(libraryIDs) == 0 {
|
||||||
@@ -99,8 +113,10 @@ func (r *MediaRepository) ListByLibrariesFiltered(ctx context.Context, libraryID
|
|||||||
q = q.Where("library_id IN ?", libraryIDs)
|
q = q.Where("library_id IN ?", libraryIDs)
|
||||||
}
|
}
|
||||||
q = applyMediaQueryFilter(q, filter)
|
q = applyMediaQueryFilter(q, filter)
|
||||||
if err := q.Count(&total).Error; err != nil {
|
if withCount {
|
||||||
return nil, 0, err
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 多级排序消除"随机"观感:
|
// 多级排序消除"随机"观感:
|
||||||
// 1. release_date desc — 精确上映/首播日期新→旧
|
// 1. release_date desc — 精确上映/首播日期新→旧
|
||||||
@@ -114,6 +130,75 @@ func (r *MediaRepository) ListByLibrariesFiltered(ctx context.Context, libraryID
|
|||||||
return items, total, err
|
return items, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rankedMediaRow struct {
|
||||||
|
model.Media
|
||||||
|
MmtlRN int `gorm:"column:mmtl_rn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRecentByLibraries returns up to perLibrary recent items for each library
|
||||||
|
// in a single query using a window function (avoids N+1 on home preview).
|
||||||
|
func (r *MediaRepository) ListRecentByLibraries(ctx context.Context, libraryIDs []string, perLibrary int, filter MediaQueryFilter) (map[string][]model.Media, error) {
|
||||||
|
out := make(map[string][]model.Media, len(libraryIDs))
|
||||||
|
if len(libraryIDs) == 0 || perLibrary <= 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var libClause string
|
||||||
|
var args []interface{}
|
||||||
|
if len(libraryIDs) == 1 {
|
||||||
|
libClause = "library_id = ?"
|
||||||
|
args = append(args, libraryIDs[0])
|
||||||
|
} else {
|
||||||
|
libClause = "library_id IN ?"
|
||||||
|
args = append(args, libraryIDs)
|
||||||
|
}
|
||||||
|
where := "deleted_at IS NULL AND " + libClause
|
||||||
|
if filterSQL, filterArgs := mediaQueryFilterSQL(filter); filterSQL != "" {
|
||||||
|
where += " AND " + filterSQL
|
||||||
|
args = append(args, filterArgs...)
|
||||||
|
}
|
||||||
|
args = append(args, perLibrary)
|
||||||
|
|
||||||
|
sql := fmt.Sprintf(`
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT *, ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY library_id
|
||||||
|
ORDER BY release_date DESC, year DESC, updated_at DESC, created_at DESC, id DESC
|
||||||
|
) AS mmtl_rn
|
||||||
|
FROM media
|
||||||
|
WHERE %s
|
||||||
|
) ranked
|
||||||
|
WHERE mmtl_rn <= ?
|
||||||
|
`, where)
|
||||||
|
|
||||||
|
var rows []rankedMediaRow
|
||||||
|
if err := r.db.WithContext(ctx).Raw(sql, args...).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
out[row.LibraryID] = append(out[row.LibraryID], row.Media)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaQueryFilterSQL(filter MediaQueryFilter) (string, []interface{}) {
|
||||||
|
var parts []string
|
||||||
|
var args []interface{}
|
||||||
|
if !filter.IncludeNSFW {
|
||||||
|
parts = append(parts, "nsfw = ?")
|
||||||
|
args = append(args, false)
|
||||||
|
}
|
||||||
|
if len(filter.HiddenLibraryIDs) > 0 {
|
||||||
|
parts = append(parts, "library_id NOT IN ?")
|
||||||
|
args = append(args, filter.HiddenLibraryIDs)
|
||||||
|
}
|
||||||
|
if len(filter.AllowedLibraryIDs) > 0 {
|
||||||
|
parts = append(parts, "library_id IN ?")
|
||||||
|
args = append(args, filter.AllowedLibraryIDs)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
type libraryCountRow struct {
|
type libraryCountRow struct {
|
||||||
LibraryID string `gorm:"column:library_id"`
|
LibraryID string `gorm:"column:library_id"`
|
||||||
Total int64 `gorm:"column:total"`
|
Total int64 `gorm:"column:total"`
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/ShukeBta/MMTL/internal/database"
|
||||||
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListRecentByLibraries(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := database.AutoMigrate(db); err != nil {
|
||||||
|
t.Fatalf("migrate: %v", err)
|
||||||
|
}
|
||||||
|
repos := New(db)
|
||||||
|
|
||||||
|
lib1 := model.Library{Name: "电影", Path: "/media/movies", Type: "movie", Enabled: true}
|
||||||
|
if err := repos.Library.Create(t.Context(), &lib1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lib2 := model.Library{Name: "动漫", Path: "/media/anime", Type: "anime", Enabled: true}
|
||||||
|
if err := repos.Library.Create(t.Context(), &lib2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Date(2026, 7, 2, 12, 0, 0, 0, time.UTC)
|
||||||
|
var rows []model.Media
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
rows = append(rows, model.Media{
|
||||||
|
Base: model.Base{ID: fmt.Sprintf("movie-%02d", i), CreatedAt: now.Add(time.Duration(i) * time.Hour)},
|
||||||
|
LibraryID: lib1.ID,
|
||||||
|
Title: fmt.Sprintf("电影%d", i),
|
||||||
|
Path: fmt.Sprintf("/media/movies/电影%d/movie%d.mp4", i, i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for i := 1; i <= 8; i++ {
|
||||||
|
rows = append(rows, model.Media{
|
||||||
|
Base: model.Base{ID: fmt.Sprintf("anime-ep-%02d", i), CreatedAt: now.Add(time.Duration(i) * time.Minute)},
|
||||||
|
LibraryID: lib2.ID,
|
||||||
|
Title: fmt.Sprintf("某动漫 第%d集", i),
|
||||||
|
Path: fmt.Sprintf("/media/anime/某动漫/Season 01/某动漫.S01E%02d.mp4", i),
|
||||||
|
SeasonNum: 1,
|
||||||
|
EpisodeNum: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := repos.DB.Create(&rows).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := MediaQueryFilter{IncludeNSFW: true}
|
||||||
|
got, err := repos.Media.ListRecentByLibraries(t.Context(), []string{lib1.ID, lib2.ID}, 3, filter)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListRecentByLibraries failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(got[lib1.ID]) != 3 {
|
||||||
|
t.Fatalf("lib1 recent count = %d, want 3", len(got[lib1.ID]))
|
||||||
|
}
|
||||||
|
if len(got[lib2.ID]) != 3 {
|
||||||
|
t.Fatalf("lib2 recent count = %d, want 3", len(got[lib2.ID]))
|
||||||
|
}
|
||||||
|
if got[lib1.ID][0].ID != "movie-05" {
|
||||||
|
t.Fatalf("lib1 newest = %q, want movie-05", got[lib1.ID][0].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,40 @@ func (s *MediaService) mediaListCacheKey(libraryID string, libraryIDs []string,
|
|||||||
return "media:list:" + hex.EncodeToString(sum[:])
|
return "media:list:" + hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MediaService) libraryPreviewCacheKey(libraries []model.Library, cardLimit int, filter repository.MediaQueryFilter) string {
|
||||||
|
libIDs := make([]string, len(libraries))
|
||||||
|
for i, lib := range libraries {
|
||||||
|
libIDs[i] = lib.ID
|
||||||
|
}
|
||||||
|
sort.Strings(libIDs)
|
||||||
|
allowed := append([]string(nil), filter.AllowedLibraryIDs...)
|
||||||
|
hidden := append([]string(nil), filter.HiddenLibraryIDs...)
|
||||||
|
sort.Strings(allowed)
|
||||||
|
sort.Strings(hidden)
|
||||||
|
sum := sha1.Sum([]byte(strings.Join([]string{
|
||||||
|
"preview",
|
||||||
|
strings.Join(libIDs, ","),
|
||||||
|
fmt.Sprintf("%d:%t", cardLimit, filter.IncludeNSFW),
|
||||||
|
strings.Join(allowed, ","),
|
||||||
|
strings.Join(hidden, ","),
|
||||||
|
}, "|")))
|
||||||
|
return "media:preview:" + hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MediaService) seriesCardsCacheKey(libraryID string, visibility MediaVisibility) string {
|
||||||
|
allowed := append([]string(nil), visibility.AllowedLibraryIDs...)
|
||||||
|
hidden := append([]string(nil), visibility.HiddenLibraryIDs...)
|
||||||
|
sort.Strings(allowed)
|
||||||
|
sort.Strings(hidden)
|
||||||
|
sum := sha1.Sum([]byte(strings.Join([]string{
|
||||||
|
libraryID,
|
||||||
|
fmt.Sprintf("%t", visibility.IncludeNSFW),
|
||||||
|
strings.Join(allowed, ","),
|
||||||
|
strings.Join(hidden, ","),
|
||||||
|
}, "|")))
|
||||||
|
return "media:series-cards:" + hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MediaService) mediaCacheTTLSeconds() int {
|
func (s *MediaService) mediaCacheTTLSeconds() int {
|
||||||
if s == nil || s.cfg == nil || s.cfg.Cache.MediaTTLSeconds < 1 {
|
if s == nil || s.cfg == nil || s.cfg.Cache.MediaTTLSeconds < 1 {
|
||||||
return 15
|
return 15
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ShukeBta/MMTL/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
"github.com/ShukeBta/MMTL/internal/repository"
|
"github.com/ShukeBta/MMTL/internal/repository"
|
||||||
@@ -14,6 +15,10 @@ type LibraryPreviewItem struct {
|
|||||||
Cards []SeriesCard `json:"cards"`
|
Cards []SeriesCard `json:"cards"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type libraryPreviewCacheValue struct {
|
||||||
|
Items []LibraryPreviewItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
// ListLibraries returns every library configured on the server.
|
// ListLibraries returns every library configured on the server.
|
||||||
func (s *MediaService) ListLibraries(ctx context.Context) ([]model.Library, error) {
|
func (s *MediaService) ListLibraries(ctx context.Context) ([]model.Library, error) {
|
||||||
return s.repo.Library.List(ctx)
|
return s.repo.Library.List(ctx)
|
||||||
@@ -29,6 +34,18 @@ func (s *MediaService) ListLibrariesWithPreview(ctx context.Context, libraries [
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility)
|
||||||
|
filter := repository.MediaQueryFilter{
|
||||||
|
IncludeNSFW: visibility.IncludeNSFW,
|
||||||
|
AllowedLibraryIDs: visibility.AllowedLibraryIDs,
|
||||||
|
HiddenLibraryIDs: visibility.HiddenLibraryIDs,
|
||||||
|
}
|
||||||
|
cacheKey := s.libraryPreviewCacheKey(libraries, cardLimit, filter)
|
||||||
|
var cached libraryPreviewCacheValue
|
||||||
|
if s.cache != nil && s.cache.GetJSON(ctx, cacheKey, &cached) {
|
||||||
|
return cached.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
libIDs := make([]string, 0, len(libraries))
|
libIDs := make([]string, 0, len(libraries))
|
||||||
for i, lib := range libraries {
|
for i, lib := range libraries {
|
||||||
out[i] = LibraryPreviewItem{
|
out[i] = LibraryPreviewItem{
|
||||||
@@ -39,13 +56,6 @@ func (s *MediaService) ListLibrariesWithPreview(ctx context.Context, libraries [
|
|||||||
libIDs = append(libIDs, lib.ID)
|
libIDs = append(libIDs, lib.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility)
|
|
||||||
filter := repository.MediaQueryFilter{
|
|
||||||
IncludeNSFW: visibility.IncludeNSFW,
|
|
||||||
AllowedLibraryIDs: visibility.AllowedLibraryIDs,
|
|
||||||
HiddenLibraryIDs: visibility.HiddenLibraryIDs,
|
|
||||||
}
|
|
||||||
|
|
||||||
counts, err := s.repo.Media.CountByLibraries(ctx, libIDs, filter)
|
counts, err := s.repo.Media.CountByLibraries(ctx, libIDs, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -64,15 +74,32 @@ func (s *MediaService) ListLibrariesWithPreview(ctx context.Context, libraries [
|
|||||||
fetchCount = 200
|
fetchCount = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recentByLibrary, err := s.repo.Media.ListRecentByLibraries(ctx, libIDs, fetchCount, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allPreviewItems := make([]model.Media, 0, len(libIDs)*fetchCount)
|
||||||
for i := range out {
|
for i := range out {
|
||||||
if out[i].Total == 0 {
|
if out[i].Total == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
items, _, err := s.repo.Media.ListByLibrariesFiltered(ctx, []string{out[i].ID}, 0, fetchCount, filter)
|
items := recentByLibrary[out[i].ID]
|
||||||
if err != nil {
|
if len(items) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allPreviewItems = append(allPreviewItems, items...)
|
||||||
|
}
|
||||||
|
s.attachLibraryMetadata(ctx, allPreviewItems)
|
||||||
|
|
||||||
|
for i := range out {
|
||||||
|
if out[i].Total == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items := recentByLibrary[out[i].ID]
|
||||||
|
if len(items) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.attachLibraryMetadata(ctx, items)
|
|
||||||
cards := groupMediaSeriesCards(items)
|
cards := groupMediaSeriesCards(items)
|
||||||
if len(cards) > cardLimit {
|
if len(cards) > cardLimit {
|
||||||
cards = cards[:cardLimit]
|
cards = cards[:cardLimit]
|
||||||
@@ -83,6 +110,10 @@ func (s *MediaService) ListLibrariesWithPreview(ctx context.Context, libraries [
|
|||||||
out[i].Cards = cards
|
out[i].Cards = cards
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.cache != nil {
|
||||||
|
s.cache.SetJSON(ctx, cacheKey, libraryPreviewCacheValue{Items: out}, time.Duration(s.mediaCacheTTLSeconds())*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import (
|
|||||||
"github.com/ShukeBta/MMTL/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type seriesCardsCacheValue struct {
|
||||||
|
Cards []SeriesCard `json:"cards"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
type SeriesCard struct {
|
type SeriesCard struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Rep model.Media `json:"rep"`
|
Rep model.Media `json:"rep"`
|
||||||
@@ -25,12 +30,25 @@ type seriesCardGroup struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *MediaService) ListLibrarySeriesCards(ctx context.Context, libraryID string, visibility MediaVisibility) ([]SeriesCard, int64, error) {
|
func (s *MediaService) ListLibrarySeriesCards(ctx context.Context, libraryID string, visibility MediaVisibility) ([]SeriesCard, int64, error) {
|
||||||
|
visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility)
|
||||||
|
cacheKey := s.seriesCardsCacheKey(libraryID, visibility)
|
||||||
|
var cached seriesCardsCacheValue
|
||||||
|
if s.cache != nil && s.cache.GetJSON(ctx, cacheKey, &cached) {
|
||||||
|
return cached.Cards, cached.Total, nil
|
||||||
|
}
|
||||||
rows, _, err := s.listAllMediaVisible(ctx, libraryID, visibility)
|
rows, _, err := s.listAllMediaVisible(ctx, libraryID, visibility)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
cards := groupMediaSeriesCards(rows)
|
cards := groupMediaSeriesCards(rows)
|
||||||
return cards, int64(len(cards)), nil
|
if cards == nil {
|
||||||
|
cards = []SeriesCard{}
|
||||||
|
}
|
||||||
|
total := int64(len(cards))
|
||||||
|
if s.cache != nil {
|
||||||
|
s.cache.SetJSON(ctx, cacheKey, seriesCardsCacheValue{Cards: cards, Total: total}, time.Duration(s.mediaCacheTTLSeconds())*time.Second)
|
||||||
|
}
|
||||||
|
return cards, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MediaService) ListRecentSeriesCards(ctx context.Context, limit int, visibility MediaVisibility) ([]SeriesCard, error) {
|
func (s *MediaService) ListRecentSeriesCards(ctx context.Context, limit int, visibility MediaVisibility) ([]SeriesCard, error) {
|
||||||
|
|||||||
@@ -45,9 +45,12 @@ export function LibraryPage() {
|
|||||||
const [historyMap, setHistoryMap] = useState<Map<string, string>>(new Map())
|
const [historyMap, setHistoryMap] = useState<Map<string, string>>(new Map())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (sortField !== 'last_played') return
|
||||||
|
let cancelled = false
|
||||||
historyAPI
|
historyAPI
|
||||||
.list(1000)
|
.list(1000)
|
||||||
.then((historyItems) => {
|
.then((historyItems) => {
|
||||||
|
if (cancelled) return
|
||||||
const map = new Map<string, string>()
|
const map = new Map<string, string>()
|
||||||
for (const item of historyItems ?? []) {
|
for (const item of historyItems ?? []) {
|
||||||
if (item.media_id && item.watched_at) {
|
if (item.media_id && item.watched_at) {
|
||||||
@@ -59,7 +62,10 @@ export function LibraryPage() {
|
|||||||
setHistoryMap(map)
|
setHistoryMap(map)
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}, [])
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [sortField])
|
||||||
|
|
||||||
const handleSortChange = (field: SortField, order: SortOrder) => {
|
const handleSortChange = (field: SortField, order: SortOrder) => {
|
||||||
setSortField(field)
|
setSortField(field)
|
||||||
|
|||||||
@@ -145,6 +145,16 @@ function isSeriesLibraryType(type?: string) {
|
|||||||
return type === 'tv' || type === 'anime' || type === 'variety'
|
return type === 'tv' || type === 'anime' || type === 'variety'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function yieldToBrowser(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (typeof requestIdleCallback !== 'undefined') {
|
||||||
|
requestIdleCallback(() => resolve(), { timeout: 48 })
|
||||||
|
} else {
|
||||||
|
setTimeout(resolve, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAllSeriesCards(
|
async function loadAllSeriesCards(
|
||||||
libraryID: string,
|
libraryID: string,
|
||||||
isRemoteEmby: boolean | undefined,
|
isRemoteEmby: boolean | undefined,
|
||||||
@@ -161,6 +171,7 @@ async function loadAllSeriesCards(
|
|||||||
onPage({ items: collected, total: data.total ?? collected.length, firstPage: page === 1 })
|
onPage({ items: collected, total: data.total ?? collected.length, firstPage: page === 1 })
|
||||||
if (collected.length >= (data.total ?? 0) || pageItems.length < pageSize) break
|
if (collected.length >= (data.total ?? 0) || pageItems.length < pageSize) break
|
||||||
page += 1
|
page += 1
|
||||||
|
await yieldToBrowser()
|
||||||
}
|
}
|
||||||
return { items: collected }
|
return { items: collected }
|
||||||
}
|
}
|
||||||
@@ -181,6 +192,7 @@ async function loadAllMedia(
|
|||||||
onPage({ items: collected, total: data.total ?? collected.length, firstPage: page === 1 })
|
onPage({ items: collected, total: data.total ?? collected.length, firstPage: page === 1 })
|
||||||
if (collected.length >= (data.total ?? 0) || pageItems.length < pageSize) break
|
if (collected.length >= (data.total ?? 0) || pageItems.length < pageSize) break
|
||||||
page += 1
|
page += 1
|
||||||
|
await yieldToBrowser()
|
||||||
}
|
}
|
||||||
return { items: collected }
|
return { items: collected }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user