Files
MeBox/internal/repository/media_repository.go
T
truewhile 6aefe18caa 5
2026-08-26 12:52:10 +08:00

134 lines
4.5 KiB
Go

package repository
import (
"context"
"errors"
"sync"
"gorm.io/gorm"
"github.com/ShukeBta/MMTL/internal/model"
)
// MediaRepository persists model.Media records.
type MediaRepository struct {
db *gorm.DB
searchIndexOnce sync.Once
searchIndexAvailable bool
searchBackend MediaSearchBackend
}
type MediaSearchBackend interface {
SearchMediaIDs(ctx context.Context, query string, offset, limit int, filter MediaQueryFilter) ([]string, int64, error)
}
type MediaSearchSyncBackend interface {
MediaSearchBackend
EnsureIndex(ctx context.Context) error
IndexMedia(ctx context.Context, rows []model.Media) error
}
func (r *MediaRepository) SetSearchBackend(backend MediaSearchBackend) {
if r != nil {
r.searchBackend = backend
}
}
// MediaQueryFilter is applied to user-facing media queries so NSFW items and
// profile-restricted libraries are filtered in SQL instead of only in React.
type MediaQueryFilter struct {
IncludeNSFW bool
AllowedLibraryIDs []string
HiddenLibraryIDs []string
}
func applyMediaQueryFilter(q *gorm.DB, filter MediaQueryFilter) *gorm.DB {
if !filter.IncludeNSFW {
q = q.Where("nsfw = ?", false)
}
if len(filter.HiddenLibraryIDs) > 0 {
q = q.Where("library_id NOT IN ?", filter.HiddenLibraryIDs)
}
if len(filter.AllowedLibraryIDs) > 0 {
q = q.Where("library_id IN ?", filter.AllowedLibraryIDs)
}
return q
}
func (r *MediaRepository) indexMediaBestEffort(ctx context.Context, media model.Media) {
backend, ok := r.searchBackend.(MediaSearchSyncBackend)
if !ok {
return
}
_ = backend.IndexMedia(ctx, []model.Media{media})
}
// FindByID returns the media row or (nil, nil).
func (r *MediaRepository) FindByID(ctx context.Context, id string) (*model.Media, error) {
var m model.Media
err := r.db.WithContext(ctx).Where("id = ?", id).First(&m).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &m, nil
}
// ListByLibrary returns paginated media items for a library.
func (r *MediaRepository) ListByLibrary(ctx context.Context, libraryID string, offset, limit int) ([]model.Media, int64, error) {
return r.ListByLibraryFiltered(ctx, libraryID, offset, limit, MediaQueryFilter{IncludeNSFW: true})
}
func (r *MediaRepository) ListByLibraryFiltered(ctx context.Context, libraryID string, offset, limit int, filter MediaQueryFilter) ([]model.Media, int64, error) {
return r.ListByLibrariesFiltered(ctx, []string{libraryID}, offset, limit, filter)
}
func (r *MediaRepository) ListByLibrariesFiltered(ctx context.Context, libraryIDs []string, offset, limit int, filter MediaQueryFilter) ([]model.Media, int64, error) {
var items []model.Media
var total int64
if len(libraryIDs) == 0 {
return items, 0, nil
}
q := r.db.WithContext(ctx).Model(&model.Media{})
if len(libraryIDs) == 1 {
q = q.Where("library_id = ?", libraryIDs[0])
} else {
q = q.Where("library_id IN ?", libraryIDs)
}
q = applyMediaQueryFilter(q, filter)
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
// 多级排序消除"随机"观感:
// 1. release_date desc — 精确上映/首播日期新→旧
// 2. year desc — 老数据没有完整日期时仍按年份新→旧
// 3. updated_at desc — 同日期/同年按最近更新兜底
// 4. created_at desc — 再按入库时间
// 5. id desc — 稳定 tie-breaker:云盘批量扫描同批 created_at 相同时,
// 没有它 DB 返回顺序不确定,正是"随机排序"的根因。
err := q.Order("release_date DESC, year DESC, updated_at DESC, created_at DESC, id DESC").
Offset(offset).Limit(limit).Find(&items).Error
return items, total, err
}
// DeleteByLibrary purges all media tied to a library.
func (r *MediaRepository) DeleteByLibrary(ctx context.Context, libraryID string) error {
// FTS 行由 media 表上的触发器同步清理(物理删除触发 FTS 清理)。
return r.db.WithContext(ctx).Unscoped().Where("library_id = ?", libraryID).Delete(&model.Media{}).Error
}
func (r *MediaRepository) DeleteByLibraryRoot(ctx context.Context, libraryID, rootID string) error {
return r.db.WithContext(ctx).Unscoped().
Where("library_id = ? AND library_root_id = ?", libraryID, rootID).
Delete(&model.Media{}).Error
}
// PurgeByLibrary permanently removes media tied to a library. Used for virtual
// cloud mounts where "remove mount" must not populate the recycle bin.
func (r *MediaRepository) PurgeByLibrary(ctx context.Context, libraryID string) error {
return r.db.WithContext(ctx).Unscoped().Where("library_id = ?", libraryID).Delete(&model.Media{}).Error
}