From 0762f9fb5c2e8aecf80438b5bff21eafc77ef998 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Sat, 27 Jun 2026 03:39:15 +0800 Subject: [PATCH] split media service read paths --- internal/service/media.go | 207 ------------------------------ internal/service/media_library.go | 43 +++++++ internal/service/media_listing.go | 109 ++++++++++++++++ internal/service/media_search.go | 78 +++++++++++ 4 files changed, 230 insertions(+), 207 deletions(-) create mode 100644 internal/service/media_library.go create mode 100644 internal/service/media_listing.go create mode 100644 internal/service/media_search.go diff --git a/internal/service/media.go b/internal/service/media.go index 1b398b5..165ce8d 100644 --- a/internal/service/media.go +++ b/internal/service/media.go @@ -2,9 +2,6 @@ package service import ( - "context" - "time" - "go.uber.org/zap" "github.com/ShukeBta/MediaStationGo/internal/config" @@ -63,207 +60,3 @@ func (s *MediaService) SetRuntimeCache(cache *RuntimeCacheService) *MediaService } return s } - -// ListLibraries returns every library configured on the server. -func (s *MediaService) ListLibraries(ctx context.Context) ([]model.Library, error) { - return s.repo.Library.List(ctx) -} - -// DeleteLibrary removes a library and its media rows. The on-disk files are -// left untouched. -func (s *MediaService) DeleteLibrary(ctx context.Context, id string) error { - lib, err := s.repo.Library.FindByID(ctx, id) - if err != nil { - return err - } - if lib != nil { - if _, ok := ParseCloudLibraryMount(lib.Path); ok { - if err := s.repo.Media.PurgeByLibrary(ctx, id); err != nil { - return err - } - _ = s.repo.DB.WithContext(ctx).Where("library_id = ?", id).Delete(&model.LibraryRoot{}).Error - err := s.repo.DB.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.Library{}).Error - if err == nil { - s.invalidateMediaCache(ctx) - } - return err - } - } - _ = s.repo.DB.WithContext(ctx).Where("library_id = ?", id).Delete(&model.LibraryRoot{}).Error - if err := s.repo.Media.DeleteByLibrary(ctx, id); err != nil { - return err - } - err = s.repo.Library.Delete(ctx, id) - if err == nil { - s.invalidateMediaCache(ctx) - } - return err -} - -// ListMedia paginates media items inside a library. -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) { - if pageSize <= 0 { - pageSize = 50 - } - if pageSize > 2000 { - pageSize = 2000 - } - if page < 1 { - page = 1 - } - visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility) - libraryIDs, err := MergedLibraryIDsForLibrary(ctx, s.repo, libraryID) - if err != nil { - return nil, 0, err - } - filter := 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) { - s.attachLibraryMetadata(ctx, cached.Items) - return cached.Items, cached.Total, nil - } - items, total, err := s.repo.Media.ListByLibrariesFiltered(ctx, libraryIDs, (page-1)*pageSize, pageSize, filter) - if err != nil { - return nil, 0, err - } - s.attachLibraryMetadata(ctx, items) - if s.cache != nil { - s.cache.SetJSON(ctx, cacheKey, mediaListCacheValue{Items: items, Total: total}, time.Duration(s.mediaCacheTTLSeconds())*time.Second) - } - return items, total, nil -} - -func (s *MediaService) ListMediaVisibleGrouped(ctx context.Context, libraryID string, page, pageSize int, visibility MediaVisibility) ([]MediaItem, int64, error) { - page, pageSize = normalizeGroupedMediaPage(page, pageSize) - items, err := s.listMediaVisibleForGrouping(ctx, libraryID, visibility) - if err != nil { - return nil, 0, err - } - grouped := groupMediaVersions(items) - return paginateMediaItems(grouped, page, pageSize), int64(len(grouped)), nil -} - -func (s *MediaService) listMediaVisibleForGrouping(ctx context.Context, libraryID string, visibility MediaVisibility) ([]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{ - IncludeNSFW: visibility.IncludeNSFW, - AllowedLibraryIDs: visibility.AllowedLibraryIDs, - HiddenLibraryIDs: visibility.HiddenLibraryIDs, - } - cacheKey := s.mediaListCacheKey(libraryID, libraryIDs, 0, maxMediaSearchLimit, filter) + ":group-source" - var cached mediaListCacheValue - if s.cache != nil && s.cache.GetJSON(ctx, cacheKey, &cached) { - s.attachLibraryMetadata(ctx, cached.Items) - return cached.Items, nil - } - items, total, err := s.repo.Media.ListByLibrariesFiltered(ctx, libraryIDs, 0, maxMediaSearchLimit, filter) - if err != nil { - return nil, err - } - if total > int64(len(items)) && s.log != nil { - s.log.Warn("media version grouping truncated by safety limit", - zap.String("library_id", libraryID), - zap.Int64("total", total), - zap.Int("limit", maxMediaSearchLimit)) - } - s.attachLibraryMetadata(ctx, items) - if s.cache != nil { - s.cache.SetJSON(ctx, cacheKey, mediaListCacheValue{Items: items, Total: total}, time.Duration(s.mediaCacheTTLSeconds())*time.Second) - } - return items, nil -} - -// SearchMedia performs a simple LIKE search across titles. -func (s *MediaService) SearchMedia(ctx context.Context, query string, limit int) ([]model.Media, error) { - return s.SearchMediaVisible(ctx, query, limit, MediaVisibility{IncludeNSFW: true}) -} - -func (s *MediaService) SearchMediaVisible(ctx context.Context, query string, limit int, visibility MediaVisibility) ([]model.Media, error) { - if limit <= 0 { - limit = 50 - } else if limit > maxMediaSearchLimit { - limit = maxMediaSearchLimit - } - visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility) - items, err := s.repo.Media.SearchFiltered(ctx, query, limit, repository.MediaQueryFilter{ - IncludeNSFW: visibility.IncludeNSFW, - AllowedLibraryIDs: visibility.AllowedLibraryIDs, - HiddenLibraryIDs: visibility.HiddenLibraryIDs, - }) - if err != nil { - return nil, err - } - s.attachLibraryMetadata(ctx, items) - return items, nil -} - -func (s *MediaService) SearchMediaVisibleGrouped(ctx context.Context, query string, limit int, visibility MediaVisibility) ([]MediaItem, error) { - if limit <= 0 { - limit = 50 - } else if limit > maxMediaSearchLimit { - limit = maxMediaSearchLimit - } - items, err := s.SearchMediaVisible(ctx, query, maxMediaSearchLimit, visibility) - if err != nil { - return nil, err - } - return firstMediaItems(groupMediaVersions(items), limit), nil -} - -func (s *MediaService) SearchMediaVisiblePage(ctx context.Context, query string, page, pageSize int, visibility MediaVisibility) ([]model.Media, int64, error) { - if pageSize <= 0 { - pageSize = 50 - } - if pageSize > maxMediaSearchPageSize { - pageSize = maxMediaSearchPageSize - } - if page < 1 { - page = 1 - } - visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility) - items, total, err := s.repo.Media.SearchFilteredPage(ctx, query, (page-1)*pageSize, pageSize, repository.MediaQueryFilter{ - IncludeNSFW: visibility.IncludeNSFW, - AllowedLibraryIDs: visibility.AllowedLibraryIDs, - HiddenLibraryIDs: visibility.HiddenLibraryIDs, - }) - if err != nil { - return nil, 0, err - } - s.attachLibraryMetadata(ctx, items) - return items, total, nil -} - -func (s *MediaService) SearchMediaVisiblePageGrouped(ctx context.Context, query string, page, pageSize int, visibility MediaVisibility) ([]MediaItem, int64, error) { - page, pageSize = normalizeGroupedMediaPage(page, pageSize) - items, err := s.SearchMediaVisible(ctx, query, maxMediaSearchLimit, visibility) - if err != nil { - return nil, 0, err - } - grouped := groupMediaVersions(items) - return paginateMediaItems(grouped, page, pageSize), int64(len(grouped)), nil -} - -// 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) - if err != nil || media == nil { - return media, err - } - items := []model.Media{*media} - s.attachLibraryMetadata(ctx, items) - *media = items[0] - return media, nil -} diff --git a/internal/service/media_library.go b/internal/service/media_library.go new file mode 100644 index 0000000..b37cc22 --- /dev/null +++ b/internal/service/media_library.go @@ -0,0 +1,43 @@ +package service + +import ( + "context" + + "github.com/ShukeBta/MediaStationGo/internal/model" +) + +// ListLibraries returns every library configured on the server. +func (s *MediaService) ListLibraries(ctx context.Context) ([]model.Library, error) { + return s.repo.Library.List(ctx) +} + +// DeleteLibrary removes a library and its media rows. The on-disk files are +// left untouched. +func (s *MediaService) DeleteLibrary(ctx context.Context, id string) error { + lib, err := s.repo.Library.FindByID(ctx, id) + if err != nil { + return err + } + if lib != nil { + if _, ok := ParseCloudLibraryMount(lib.Path); ok { + if err := s.repo.Media.PurgeByLibrary(ctx, id); err != nil { + return err + } + _ = s.repo.DB.WithContext(ctx).Where("library_id = ?", id).Delete(&model.LibraryRoot{}).Error + err := s.repo.DB.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.Library{}).Error + if err == nil { + s.invalidateMediaCache(ctx) + } + return err + } + } + _ = s.repo.DB.WithContext(ctx).Where("library_id = ?", id).Delete(&model.LibraryRoot{}).Error + if err := s.repo.Media.DeleteByLibrary(ctx, id); err != nil { + return err + } + err = s.repo.Library.Delete(ctx, id) + if err == nil { + s.invalidateMediaCache(ctx) + } + return err +} diff --git a/internal/service/media_listing.go b/internal/service/media_listing.go new file mode 100644 index 0000000..b3afa5c --- /dev/null +++ b/internal/service/media_listing.go @@ -0,0 +1,109 @@ +package service + +import ( + "context" + "time" + + "go.uber.org/zap" + + "github.com/ShukeBta/MediaStationGo/internal/model" + "github.com/ShukeBta/MediaStationGo/internal/repository" +) + +// ListMedia paginates media items inside a library. +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) { + if pageSize <= 0 { + pageSize = 50 + } + if pageSize > 2000 { + pageSize = 2000 + } + if page < 1 { + page = 1 + } + visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility) + libraryIDs, err := MergedLibraryIDsForLibrary(ctx, s.repo, libraryID) + if err != nil { + return nil, 0, err + } + filter := 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) { + s.attachLibraryMetadata(ctx, cached.Items) + return cached.Items, cached.Total, nil + } + items, total, err := s.repo.Media.ListByLibrariesFiltered(ctx, libraryIDs, (page-1)*pageSize, pageSize, filter) + if err != nil { + return nil, 0, err + } + s.attachLibraryMetadata(ctx, items) + if s.cache != nil { + s.cache.SetJSON(ctx, cacheKey, mediaListCacheValue{Items: items, Total: total}, time.Duration(s.mediaCacheTTLSeconds())*time.Second) + } + return items, total, nil +} + +func (s *MediaService) ListMediaVisibleGrouped(ctx context.Context, libraryID string, page, pageSize int, visibility MediaVisibility) ([]MediaItem, int64, error) { + page, pageSize = normalizeGroupedMediaPage(page, pageSize) + items, err := s.listMediaVisibleForGrouping(ctx, libraryID, visibility) + if err != nil { + return nil, 0, err + } + grouped := groupMediaVersions(items) + return paginateMediaItems(grouped, page, pageSize), int64(len(grouped)), nil +} + +func (s *MediaService) listMediaVisibleForGrouping(ctx context.Context, libraryID string, visibility MediaVisibility) ([]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{ + IncludeNSFW: visibility.IncludeNSFW, + AllowedLibraryIDs: visibility.AllowedLibraryIDs, + HiddenLibraryIDs: visibility.HiddenLibraryIDs, + } + cacheKey := s.mediaListCacheKey(libraryID, libraryIDs, 0, maxMediaSearchLimit, filter) + ":group-source" + var cached mediaListCacheValue + if s.cache != nil && s.cache.GetJSON(ctx, cacheKey, &cached) { + s.attachLibraryMetadata(ctx, cached.Items) + return cached.Items, nil + } + items, total, err := s.repo.Media.ListByLibrariesFiltered(ctx, libraryIDs, 0, maxMediaSearchLimit, filter) + if err != nil { + return nil, err + } + if total > int64(len(items)) && s.log != nil { + s.log.Warn("media version grouping truncated by safety limit", + zap.String("library_id", libraryID), + zap.Int64("total", total), + zap.Int("limit", maxMediaSearchLimit)) + } + s.attachLibraryMetadata(ctx, items) + if s.cache != nil { + s.cache.SetJSON(ctx, cacheKey, mediaListCacheValue{Items: items, Total: total}, time.Duration(s.mediaCacheTTLSeconds())*time.Second) + } + return items, nil +} + +// 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) + if err != nil || media == nil { + return media, err + } + items := []model.Media{*media} + s.attachLibraryMetadata(ctx, items) + *media = items[0] + return media, nil +} diff --git a/internal/service/media_search.go b/internal/service/media_search.go new file mode 100644 index 0000000..f70d03b --- /dev/null +++ b/internal/service/media_search.go @@ -0,0 +1,78 @@ +package service + +import ( + "context" + + "github.com/ShukeBta/MediaStationGo/internal/model" + "github.com/ShukeBta/MediaStationGo/internal/repository" +) + +// SearchMedia performs a simple LIKE search across titles. +func (s *MediaService) SearchMedia(ctx context.Context, query string, limit int) ([]model.Media, error) { + return s.SearchMediaVisible(ctx, query, limit, MediaVisibility{IncludeNSFW: true}) +} + +func (s *MediaService) SearchMediaVisible(ctx context.Context, query string, limit int, visibility MediaVisibility) ([]model.Media, error) { + if limit <= 0 { + limit = 50 + } else if limit > maxMediaSearchLimit { + limit = maxMediaSearchLimit + } + visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility) + items, err := s.repo.Media.SearchFiltered(ctx, query, limit, repository.MediaQueryFilter{ + IncludeNSFW: visibility.IncludeNSFW, + AllowedLibraryIDs: visibility.AllowedLibraryIDs, + HiddenLibraryIDs: visibility.HiddenLibraryIDs, + }) + if err != nil { + return nil, err + } + s.attachLibraryMetadata(ctx, items) + return items, nil +} + +func (s *MediaService) SearchMediaVisibleGrouped(ctx context.Context, query string, limit int, visibility MediaVisibility) ([]MediaItem, error) { + if limit <= 0 { + limit = 50 + } else if limit > maxMediaSearchLimit { + limit = maxMediaSearchLimit + } + items, err := s.SearchMediaVisible(ctx, query, maxMediaSearchLimit, visibility) + if err != nil { + return nil, err + } + return firstMediaItems(groupMediaVersions(items), limit), nil +} + +func (s *MediaService) SearchMediaVisiblePage(ctx context.Context, query string, page, pageSize int, visibility MediaVisibility) ([]model.Media, int64, error) { + if pageSize <= 0 { + pageSize = 50 + } + if pageSize > maxMediaSearchPageSize { + pageSize = maxMediaSearchPageSize + } + if page < 1 { + page = 1 + } + visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility) + items, total, err := s.repo.Media.SearchFilteredPage(ctx, query, (page-1)*pageSize, pageSize, repository.MediaQueryFilter{ + IncludeNSFW: visibility.IncludeNSFW, + AllowedLibraryIDs: visibility.AllowedLibraryIDs, + HiddenLibraryIDs: visibility.HiddenLibraryIDs, + }) + if err != nil { + return nil, 0, err + } + s.attachLibraryMetadata(ctx, items) + return items, total, nil +} + +func (s *MediaService) SearchMediaVisiblePageGrouped(ctx context.Context, query string, page, pageSize int, visibility MediaVisibility) ([]MediaItem, int64, error) { + page, pageSize = normalizeGroupedMediaPage(page, pageSize) + items, err := s.SearchMediaVisible(ctx, query, maxMediaSearchLimit, visibility) + if err != nil { + return nil, 0, err + } + grouped := groupMediaVersions(items) + return paginateMediaItems(grouped, page, pageSize), int64(len(grouped)), nil +}