添加新功能,完善项目

This commit is contained in:
truewhile
2026-09-22 16:22:29 +08:00
parent 47899284a5
commit b5ce4646cf
85 changed files with 7103 additions and 982 deletions
+74 -11
View File
@@ -12,11 +12,63 @@ import (
)
// ListMedia paginates media items inside a library.
// MediaListFilters 是列表接口的可选筛选条件(来自查询串或库内筛选面板)。
//
// 与 MediaVisibility 分开:可见性是权限约束(服务端强制),这些是用户主动
// 选择的浏览条件,两者在 SQL 层是「与」关系。
type MediaListFilters struct {
Genres []string
YearMin int
YearMax int
RatingMin float64
Unwatched bool
// UserID 是「未观看」判定所需的账号;为空时 Unwatched 被忽略。
UserID string
}
// empty 报告是否没有任何筛选条件。调用方据此走缓存友好的默认路径。
func (f MediaListFilters) empty() bool {
return len(f.Genres) == 0 && f.YearMin <= 0 && f.YearMax <= 0 &&
f.RatingMin <= 0 && !f.Unwatched
}
// apply 把筛选条件叠加到仓储过滤条件上。
func (f MediaListFilters) apply(filter repository.MediaQueryFilter) repository.MediaQueryFilter {
if len(f.Genres) > 0 {
filter.Genres = f.Genres
}
if f.YearMin > 0 {
filter.YearMin = f.YearMin
}
if f.YearMax > 0 {
filter.YearMax = f.YearMax
}
if f.RatingMin > 0 {
filter.RatingMin = f.RatingMin
}
if f.Unwatched && strings.TrimSpace(f.UserID) != "" {
filter.UnwatchedOnly = true
filter.UnwatchedUserID = f.UserID
}
return filter
}
func (s *MediaService) ListMedia(ctx context.Context, libraryID string, page, pageSize int) ([]model.Media, int64, error) {
return s.ListMediaVisible(ctx, libraryID, page, pageSize, MediaVisibility{IncludeNSFW: true})
}
func (s *MediaService) ListMediaVisible(ctx context.Context, libraryID string, page, pageSize int, visibility MediaVisibility) ([]model.Media, int64, error) {
return s.ListMediaVisibleFiltered(ctx, libraryID, page, pageSize, visibility, MediaListFilters{})
}
// ListMediaVisibleFiltered 在可见性之上叠加用户筛选条件。
func (s *MediaService) ListMediaVisibleFiltered(
ctx context.Context,
libraryID string,
page, pageSize int,
visibility MediaVisibility,
filters MediaListFilters,
) ([]model.Media, int64, error) {
if pageSize <= 0 {
pageSize = 50
}
@@ -31,11 +83,11 @@ func (s *MediaService) ListMediaVisible(ctx context.Context, libraryID string, p
if err != nil {
return nil, 0, err
}
filter := repository.MediaQueryFilter{
filter := filters.apply(repository.MediaQueryFilter{
IncludeNSFW: visibility.IncludeNSFW,
AllowedLibraryIDs: visibility.AllowedLibraryIDs,
HiddenLibraryIDs: visibility.HiddenLibraryIDs,
}
})
cacheKey := s.mediaListCacheKey(libraryID, libraryIDs, page, pageSize, filter)
var cached mediaListCacheValue
if s.cache != nil && s.cache.GetJSON(ctx, cacheKey, &cached) {
@@ -54,7 +106,6 @@ func (s *MediaService) ListMediaVisible(ctx context.Context, libraryID string, p
}
func (s *MediaService) ListMediaVisibleGrouped(ctx context.Context, libraryID string, page, pageSize int, visibility MediaVisibility) ([]MediaItem, int64, error) {
page, pageSize = normalizeGroupedMediaPage(page, pageSize)
grouped, err := s.GroupedMediaVisible(ctx, libraryID, visibility)
if err != nil {
return nil, 0, err
@@ -65,16 +116,29 @@ func (s *MediaService) ListMediaVisibleGrouped(ctx context.Context, libraryID st
// GroupedMediaVisible returns the complete version-grouped media list before pagination.
// The result is cached as an immutable slice; sort/pagination callers must copy it before mutating.
func (s *MediaService) GroupedMediaVisible(ctx context.Context, libraryID string, visibility MediaVisibility) ([]MediaItem, error) {
return s.GroupedMediaVisibleFiltered(ctx, libraryID, visibility, MediaListFilters{})
}
// GroupedMediaVisibleFiltered 在可见性之上叠加用户筛选条件。
//
// 版本分组的筛选必须作用在原始行上(先筛后分组):否则「按类型筛选」会把
// 同一部作品的不同版本拆到不同筛选结果里,出现重复卡片。
func (s *MediaService) GroupedMediaVisibleFiltered(
ctx context.Context,
libraryID string,
visibility MediaVisibility,
filters MediaListFilters,
) ([]MediaItem, error) {
visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility)
libraryIDs, err := MergedLibraryIDsForLibrary(ctx, s.repo, libraryID)
if err != nil {
return nil, err
}
filter := repository.MediaQueryFilter{
filter := filters.apply(repository.MediaQueryFilter{
IncludeNSFW: visibility.IncludeNSFW,
AllowedLibraryIDs: visibility.AllowedLibraryIDs,
HiddenLibraryIDs: visibility.HiddenLibraryIDs,
}
})
itemsCacheKey := s.groupedItemsCacheKey(libraryID, libraryIDs, filter)
value, err, _ := s.groupedMediaFlight.Do(itemsCacheKey, func() (any, error) {
if s.cache != nil {
@@ -86,7 +150,7 @@ func (s *MediaService) GroupedMediaVisible(ctx context.Context, libraryID string
}
loadCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute)
defer cancel()
items, err := s.listMediaVisibleForGrouping(loadCtx, libraryID, visibility)
items, err := s.listMediaVisibleForGrouping(loadCtx, libraryID, visibility, filters)
if err != nil {
return nil, err
}
@@ -105,17 +169,17 @@ func (s *MediaService) GroupedMediaVisible(ctx context.Context, libraryID string
return nil, nil
}
func (s *MediaService) listMediaVisibleForGrouping(ctx context.Context, libraryID string, visibility MediaVisibility) ([]model.Media, error) {
func (s *MediaService) listMediaVisibleForGrouping(ctx context.Context, libraryID string, visibility MediaVisibility, filters MediaListFilters) ([]model.Media, error) {
visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility)
libraryIDs, err := MergedLibraryIDsForLibrary(ctx, s.repo, libraryID)
if err != nil {
return nil, err
}
filter := repository.MediaQueryFilter{
filter := filters.apply(repository.MediaQueryFilter{
IncludeNSFW: visibility.IncludeNSFW,
AllowedLibraryIDs: visibility.AllowedLibraryIDs,
HiddenLibraryIDs: visibility.HiddenLibraryIDs,
}
})
// 版本分组的 URL 分页发生在 Go 进程内,响应里的 total 是分组后的数量,
// 不需要数据库再为原始行做一次 COUNT(*)。全量 COUNT 在超大媒体库上
// 会重复扫描整个 library_id 范围,而这里只关心是否存在截断风险。
@@ -133,8 +197,7 @@ func (s *MediaService) listMediaVisibleForGrouping(ctx context.Context, libraryI
}
// GetMedia returns a single media row.
func (s *MediaService) GetMedia(ctx context.Context, id string) (*model.Media, error) {
media, err := s.repo.Media.FindByID(ctx, id)
func (s *MediaService) GetMedia(ctx context.Context, id string) (*model.Media, error) { media, err := s.repo.Media.FindByID(ctx, id)
if err != nil || media == nil {
return media, err
}