mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
28aa466107
- 新增 6 位临时密码生成与验证接口,支持 TV 及客户端快速登录 - 媒体及剧集详情补充 People 演职员信息解析与返回 - 优化剧集背景图与海报标签继承机制及播放时长兜底逻辑 - 增加临时密码登录相关的单元测试
233 lines
6.9 KiB
Go
233 lines
6.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
)
|
|
|
|
// 请求级 payload 构建缓存:/Items 列表为每行构建 payload 时,
|
|
// mediaShouldBeEpisode 需要库类型、剧集 payload 需要 series 标题。
|
|
// 一次页面请求内这些值高度重复(同一库、同一部剧),挂在 ctx 上的
|
|
// 小缓存可以把每条目 2-3 次 DB 查询降为整个请求各 1 次预取。
|
|
|
|
type embyPayloadCacheKey struct{}
|
|
|
|
type embyLibraryTypeEntry struct {
|
|
typ string
|
|
found bool // 库不存在时 found=false,调用方可退回计数启发式
|
|
}
|
|
|
|
type embyPayloadSeriesEntry struct {
|
|
title string
|
|
posterURL string
|
|
backdropURL string
|
|
found bool
|
|
}
|
|
|
|
type embyPayloadCache struct {
|
|
mu sync.Mutex
|
|
libTypes map[string]embyLibraryTypeEntry
|
|
series map[string]embyPayloadSeriesEntry
|
|
}
|
|
|
|
func (c *embyPayloadCache) libraryType(id string) (embyLibraryTypeEntry, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry, ok := c.libTypes[id]
|
|
return entry, ok
|
|
}
|
|
|
|
func (c *embyPayloadCache) setLibraryType(id string, entry embyLibraryTypeEntry) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.libTypes[id] = entry
|
|
}
|
|
|
|
func (c *embyPayloadCache) seriesTitle(id string) (string, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry, ok := c.series[id]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
return entry.title, true
|
|
}
|
|
|
|
func (c *embyPayloadCache) seriesEntry(id string) (embyPayloadSeriesEntry, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry, ok := c.series[id]
|
|
return entry, ok
|
|
}
|
|
|
|
func (c *embyPayloadCache) setSeriesEntry(id string, entry embyPayloadSeriesEntry) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.series[id] = entry
|
|
}
|
|
|
|
func (c *embyPayloadCache) setSeriesTitle(id, title string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry := c.series[id]
|
|
entry.title = title
|
|
entry.found = true
|
|
c.series[id] = entry
|
|
}
|
|
|
|
// withPayloadCache attaches a fresh request-scoped cache if none exists yet.
|
|
func (e *EmbyService) withPayloadCache(ctx context.Context) context.Context {
|
|
if e == nil || e.repo == nil {
|
|
return ctx
|
|
}
|
|
if ctx.Value(embyPayloadCacheKey{}) != nil {
|
|
return ctx
|
|
}
|
|
return context.WithValue(ctx, embyPayloadCacheKey{}, &embyPayloadCache{
|
|
libTypes: map[string]embyLibraryTypeEntry{},
|
|
series: map[string]embyPayloadSeriesEntry{},
|
|
})
|
|
}
|
|
|
|
// prefetchPayloadCache warms the cache for the given media rows with two bulk
|
|
// queries (library types, series titles) instead of per-item lookups.
|
|
func (e *EmbyService) prefetchPayloadCache(ctx context.Context, rows []model.Media) {
|
|
cache, ok := ctx.Value(embyPayloadCacheKey{}).(*embyPayloadCache)
|
|
if !ok || len(rows) == 0 {
|
|
return
|
|
}
|
|
libIDs := make([]string, 0, 8)
|
|
seriesIDs := make([]string, 0, 8)
|
|
seenLib := map[string]struct{}{}
|
|
seenSeries := map[string]struct{}{}
|
|
for i := range rows {
|
|
row := &rows[i]
|
|
if id := strings.TrimSpace(row.LibraryID); id != "" {
|
|
if _, done := seenLib[id]; !done {
|
|
// 已在缓存中的库不必再查。
|
|
if _, hit := cache.libraryType(id); !hit {
|
|
seenLib[id] = struct{}{}
|
|
libIDs = append(libIDs, id)
|
|
}
|
|
}
|
|
}
|
|
if id := strings.TrimSpace(row.SeriesID); id != "" {
|
|
if _, done := seenSeries[id]; !done {
|
|
if _, hit := cache.seriesTitle(id); !hit {
|
|
seenSeries[id] = struct{}{}
|
|
seriesIDs = append(seriesIDs, id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(libIDs) > 0 {
|
|
var libs []model.Library
|
|
if err := e.repo.DB.WithContext(ctx).Select("id, type").Where("id IN ?", libIDs).Find(&libs).Error; err == nil {
|
|
found := map[string]string{}
|
|
for _, lib := range libs {
|
|
found[lib.ID] = lib.Type
|
|
}
|
|
for _, id := range libIDs {
|
|
typ, ok := found[id]
|
|
cache.setLibraryType(id, embyLibraryTypeEntry{typ: typ, found: ok})
|
|
}
|
|
}
|
|
}
|
|
if len(seriesIDs) > 0 {
|
|
var series []model.Series
|
|
if err := e.repo.DB.WithContext(ctx).Select("id, title, poster_url, backdrop_url").Where("id IN ?", seriesIDs).Find(&series).Error; err == nil {
|
|
for _, s := range series {
|
|
cache.setSeriesEntry(s.ID, embyPayloadSeriesEntry{
|
|
title: s.Title,
|
|
posterURL: s.PosterURL,
|
|
backdropURL: s.BackdropURL,
|
|
found: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// payloadLibraryType resolves a library type through the request cache,
|
|
// falling back to a direct lookup when no cache is attached. found=false
|
|
// means the library row does not exist (soft-deleted or orphaned id).
|
|
func (e *EmbyService) payloadLibraryType(ctx context.Context, libraryID string) (typ string, found bool, err error) {
|
|
if cache, ok := ctx.Value(embyPayloadCacheKey{}).(*embyPayloadCache); ok {
|
|
if entry, hit := cache.libraryType(libraryID); hit {
|
|
return entry.typ, entry.found, nil
|
|
}
|
|
var lib model.Library
|
|
if dbErr := e.repo.DB.WithContext(ctx).Select("id, type").Where("id = ?", libraryID).First(&lib).Error; dbErr != nil {
|
|
cache.setLibraryType(libraryID, embyLibraryTypeEntry{})
|
|
return "", false, nil
|
|
}
|
|
cache.setLibraryType(lib.ID, embyLibraryTypeEntry{typ: lib.Type, found: true})
|
|
return lib.Type, true, nil
|
|
}
|
|
var lib model.Library
|
|
if err = e.repo.DB.WithContext(ctx).Select("id, type").Where("id = ?", libraryID).First(&lib).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", false, nil
|
|
}
|
|
return "", false, err
|
|
}
|
|
return lib.Type, true, nil
|
|
}
|
|
|
|
// payloadSeriesTitle resolves a series title through the request cache,
|
|
// falling back to a direct lookup when no cache is attached.
|
|
func (e *EmbyService) payloadSeriesTitle(ctx context.Context, seriesID string) (string, bool, error) {
|
|
entry, ok, err := e.payloadSeriesEntry(ctx, seriesID)
|
|
if err != nil || !ok {
|
|
return "", false, err
|
|
}
|
|
return entry.title, true, nil
|
|
}
|
|
|
|
// payloadSeriesEntry resolves a series entry through the request cache,
|
|
// falling back to a direct lookup when no cache is attached.
|
|
func (e *EmbyService) payloadSeriesEntry(ctx context.Context, seriesID string) (embyPayloadSeriesEntry, bool, error) {
|
|
if cache, ok := ctx.Value(embyPayloadCacheKey{}).(*embyPayloadCache); ok {
|
|
if entry, hit := cache.seriesEntry(seriesID); hit {
|
|
return entry, entry.found, nil
|
|
}
|
|
if e.repo != nil && e.repo.Series != nil {
|
|
s, err := e.repo.Series.FindByID(ctx, seriesID)
|
|
if err != nil || s == nil {
|
|
cache.setSeriesEntry(seriesID, embyPayloadSeriesEntry{})
|
|
return embyPayloadSeriesEntry{}, false, err
|
|
}
|
|
entry := embyPayloadSeriesEntry{
|
|
title: s.Title,
|
|
posterURL: s.PosterURL,
|
|
backdropURL: s.BackdropURL,
|
|
found: true,
|
|
}
|
|
cache.setSeriesEntry(seriesID, entry)
|
|
return entry, true, nil
|
|
}
|
|
}
|
|
if e.repo != nil && e.repo.Series != nil {
|
|
series, err := e.repo.Series.FindByID(ctx, seriesID)
|
|
if err != nil {
|
|
return embyPayloadSeriesEntry{}, false, err
|
|
}
|
|
if series == nil {
|
|
return embyPayloadSeriesEntry{}, false, nil
|
|
}
|
|
return embyPayloadSeriesEntry{
|
|
title: series.Title,
|
|
posterURL: series.PosterURL,
|
|
backdropURL: series.BackdropURL,
|
|
found: true,
|
|
}, true, nil
|
|
}
|
|
return embyPayloadSeriesEntry{}, false, nil
|
|
}
|