[codex] feat(jellycompat): use catalog search provider (#236)

* feat(jellycompat): use catalog search provider

* fix(jellycompat): route video search buckets to provider
This commit is contained in:
Quick
2026-06-27 20:10:48 -04:00
committed by GitHub
parent 4d2a990de1
commit a0fbe2cdda
13 changed files with 442 additions and 37 deletions
+11
View File
@@ -2185,6 +2185,17 @@ func main() {
compatDeps.WatchCompletionObserver = deps.WatchCompletionObserver
compatDeps.SettingsRepo = settingsRepo
compatDeps.PersonRepo = personRepo
compatSearchService := catalog.NewCatalogSearchService(
appCtx,
settingsRepo,
itemRepo,
catalog.NewSearchIndexEventRepository(deps.DB),
deps.CatalogSearchVectorizer,
)
if compatSearchService != nil {
compatSearchService.StartCoverageRefresh(appCtx)
compatDeps.CatalogSearchProvider = compatSearchService.Provider()
}
if deps.S3Public != nil {
compatDeps.PosterPresigner = deps.S3Public
+3 -7
View File
@@ -449,14 +449,10 @@ func NewRouter(deps Dependencies) chi.Router {
ebookAnnotationStore = handlers.NewPGEbookReaderAnnotationStore(deps.DB)
browseRepo := catalog.NewBrowseRepository(deps.DB)
itemRepo = catalog.NewItemRepository(deps.DB)
catalogSearchSettings, err := catalog.LoadCatalogSearchSettings(context.Background(), settingsRepo)
if err != nil {
slog.Warn("catalog search: failed to load settings; using postgres", "err", err)
catalogSearchSettings = catalog.DefaultCatalogSearchSettings()
}
searchIndexEvents := catalog.NewSearchIndexEventRepository(deps.DB)
catalogSearchService = catalog.NewCatalogSearchServiceFromSettings(
catalogSearchSettings,
catalogSearchService = catalog.NewCatalogSearchService(
context.Background(),
settingsRepo,
itemRepo,
searchIndexEvents,
deps.CatalogSearchVectorizer,
+1 -1
View File
@@ -110,7 +110,7 @@ func (s *stubContentService) BrowseItems(context.Context, *Session, url.Values)
panic("unused")
}
func (s *stubContentService) SearchItems(context.Context, *Session, string, []string, int, int, *int) (*upstreamBrowseResponse, error) {
func (s *stubContentService) SearchItems(context.Context, *Session, SearchItemsOptions) (*upstreamBrowseResponse, error) {
panic("unused")
}
+42 -14
View File
@@ -195,6 +195,7 @@ type episodeListSource interface {
type directContentService struct {
browseRepo browseSource
itemRepo itemAccessSource
searchProvider catalog.CatalogSearchProvider
seasonRepo seasonListSource
episodeRepo episodeListSource
detailSvc *catalog.DetailService
@@ -214,16 +215,18 @@ func newDirectContentService(
folderRepo folderListSource,
storeProvider userstore.UserStoreProvider,
accessFilter AccessFilterResolver,
searchProvider catalog.CatalogSearchProvider,
) *directContentService {
return &directContentService{
browseRepo: browseRepo,
itemRepo: itemRepo,
seasonRepo: seasonRepo,
episodeRepo: episodeRepo,
detailSvc: detailSvc,
folderRepo: folderRepo,
storeProvider: storeProvider,
accessFilter: accessFilter,
browseRepo: browseRepo,
itemRepo: itemRepo,
searchProvider: searchProvider,
seasonRepo: seasonRepo,
episodeRepo: episodeRepo,
detailSvc: detailSvc,
folderRepo: folderRepo,
storeProvider: storeProvider,
accessFilter: accessFilter,
}
}
@@ -456,12 +459,37 @@ func parseContentIDParam(raw string) []string {
return ids
}
func (s *directContentService) SearchItems(ctx context.Context, session *Session, query string, itemTypes []string, limit, offset int, libraryID *int) (*upstreamBrowseResponse, error) {
filter := applyCompatPresentationLibrary(s.resolveFilter(ctx, session), libraryID)
func (s *directContentService) SearchItems(ctx context.Context, session *Session, opts SearchItemsOptions) (*upstreamBrowseResponse, error) {
filter := applyCompatPresentationLibrary(s.resolveFilter(ctx, session), opts.LibraryID)
itemTypes := compatScopedSearchTypes(opts.ItemTypes)
items, total, err := s.itemRepo.Search(ctx, query, compatScopedSearchTypes(itemTypes), limit, offset, filter)
if err != nil {
return nil, fmt.Errorf("search items: %w", err)
var items []*models.MediaItem
var total int
var hasMore bool
if s.searchProvider != nil {
result, err := s.searchProvider.Search(ctx, catalog.CatalogSearchRequest{
Query: opts.Query,
ItemTypes: itemTypes,
Limit: opts.Limit,
Offset: opts.Offset,
Access: filter,
SkipTotal: opts.SkipTotal,
})
if err != nil {
return nil, fmt.Errorf("search items: %w", err)
}
if result != nil {
items = result.Items
total = result.Total
hasMore = result.HasMore
}
} else {
var err error
items, total, err = s.itemRepo.Search(ctx, opts.Query, itemTypes, opts.Limit, opts.Offset, filter)
if err != nil {
return nil, fmt.Errorf("search items: %w", err)
}
hasMore = opts.Offset+len(items) < total
}
listItems := make([]upstreamListItem, 0, len(items))
@@ -478,7 +506,7 @@ func (s *directContentService) SearchItems(ctx context.Context, session *Session
return &upstreamBrowseResponse{
Total: total,
HasMore: offset+len(listItems) < total,
HasMore: hasMore,
Items: listItems,
}, nil
}
+129
View File
@@ -460,6 +460,44 @@ func (s *stubBrowseSource) ListGenres(_ context.Context, _ catalog.BrowseFilters
return nil, nil
}
type recordingCatalogSearchProvider struct {
requests []catalog.CatalogSearchRequest
result *catalog.CatalogSearchResult
}
func (p *recordingCatalogSearchProvider) Search(_ context.Context, req catalog.CatalogSearchRequest) (*catalog.CatalogSearchResult, error) {
p.requests = append(p.requests, req)
if p.result != nil {
return p.result, nil
}
return &catalog.CatalogSearchResult{}, nil
}
type recordingItemAccessSource struct {
searchQueries []string
searchTypes [][]string
searchLimits []int
searchOffsets []int
items []*models.MediaItem
total int
}
func (s *recordingItemAccessSource) EnsureAccessible(context.Context, string, catalog.AccessFilter) error {
return nil
}
func (s *recordingItemAccessSource) Search(_ context.Context, query string, itemTypes []string, limit, offset int, _ catalog.AccessFilter) ([]*models.MediaItem, int, error) {
s.searchQueries = append(s.searchQueries, query)
s.searchTypes = append(s.searchTypes, append([]string(nil), itemTypes...))
s.searchLimits = append(s.searchLimits, limit)
s.searchOffsets = append(s.searchOffsets, offset)
return append([]*models.MediaItem(nil), s.items...), s.total, nil
}
func (s *recordingItemAccessSource) GetByIDs(context.Context, []string) ([]*models.MediaItem, error) {
return nil, nil
}
// newDirectContentServiceForTest builds a directContentService with stubbed
// catalog dependencies. Useful for behavioral tests that don't need real
// Postgres state.
@@ -470,6 +508,97 @@ func newDirectContentServiceForTest(browse browseSource, provider userstore.User
}
}
func TestSearchItemsUsesCatalogSearchProviderWithCompatScope(t *testing.T) {
libraryID := 7
provider := &recordingCatalogSearchProvider{
result: &catalog.CatalogSearchResult{
Items: []*models.MediaItem{{
ContentID: "movie-1",
Type: "movie",
Title: "Dune",
}},
Total: 12,
HasMore: true,
},
}
svc := &directContentService{
searchProvider: provider,
accessFilter: func(context.Context, int, string) catalog.AccessFilter {
return catalog.AccessFilter{
AllowedLibraryIDs: []int{1, 2},
ExcludedMediaTypes: []string{"ebook"},
MaxContentRating: "PG-13",
}
},
}
result, err := svc.SearchItems(context.Background(), &Session{
StreamAppUserID: 22,
ProfileID: "profile-1",
}, SearchItemsOptions{
Query: "dune",
Limit: 5,
Offset: 10,
LibraryID: &libraryID,
SkipTotal: true,
})
if err != nil {
t.Fatalf("SearchItems error: %v", err)
}
if len(provider.requests) != 1 {
t.Fatalf("provider requests = %d, want 1", len(provider.requests))
}
req := provider.requests[0]
if req.Query != "dune" || req.Limit != 5 || req.Offset != 10 || !req.SkipTotal {
t.Fatalf("provider request shape = %#v", req)
}
if want := []string{"movie", "series", "episode"}; !slices.Equal(req.ItemTypes, want) {
t.Fatalf("ItemTypes = %#v, want %#v", req.ItemTypes, want)
}
if req.Access.PresentationLibraryID == nil || *req.Access.PresentationLibraryID != libraryID {
t.Fatalf("PresentationLibraryID = %#v, want %d", req.Access.PresentationLibraryID, libraryID)
}
for _, mediaType := range []string{"ebook", "audiobook", "podcast"} {
if !slices.Contains(req.Access.ExcludedMediaTypes, mediaType) {
t.Fatalf("ExcludedMediaTypes = %#v, missing %q", req.Access.ExcludedMediaTypes, mediaType)
}
}
if result.Total != 12 || !result.HasMore || len(result.Items) != 1 || result.Items[0].Title != "Dune" {
t.Fatalf("result = %#v", result)
}
}
func TestSearchItemsFallsBackToItemRepoWhenProviderMissing(t *testing.T) {
itemRepo := &recordingItemAccessSource{
items: []*models.MediaItem{{
ContentID: "movie-1",
Type: "movie",
Title: "Fallback",
}},
total: 3,
}
svc := &directContentService{itemRepo: itemRepo}
result, err := svc.SearchItems(context.Background(), &Session{}, SearchItemsOptions{
Query: "fallback",
ItemTypes: []string{"MusicAlbum"},
Limit: 2,
Offset: 1,
})
if err != nil {
t.Fatalf("SearchItems error: %v", err)
}
if len(itemRepo.searchQueries) != 1 || itemRepo.searchQueries[0] != "fallback" {
t.Fatalf("search queries = %#v", itemRepo.searchQueries)
}
if want := []string{compatNoMatchType}; !slices.Equal(itemRepo.searchTypes[0], want) {
t.Fatalf("search types = %#v, want %#v", itemRepo.searchTypes[0], want)
}
if result.Total != 3 || !result.HasMore || len(result.Items) != 1 || result.Items[0].Title != "Fallback" {
t.Fatalf("result = %#v", result)
}
}
// TestBrowseItems_DoesNotFetchProgressWhenNoPlayedFilter verifies that
// BrowseItems does NOT call ListProgressByMediaItems on the user store when
// the is_played filter is empty. The handler-level resolveUserStateForContentIDs
+10 -1
View File
@@ -9,7 +9,7 @@ import (
type ContentService interface {
ListUserLibraries(ctx context.Context, session *Session) ([]upstreamUserLibrary, error)
BrowseItems(ctx context.Context, session *Session, params url.Values) (*upstreamBrowseResponse, error)
SearchItems(ctx context.Context, session *Session, query string, itemTypes []string, limit, offset int, libraryID *int) (*upstreamBrowseResponse, error)
SearchItems(ctx context.Context, session *Session, opts SearchItemsOptions) (*upstreamBrowseResponse, error)
GetItemDetail(ctx context.Context, session *Session, contentID string, libraryID *int) (*upstreamItemDetail, error)
ListSeasons(ctx context.Context, session *Session, seriesID string, libraryID *int) ([]upstreamSeason, error)
GetSeason(ctx context.Context, session *Session, seriesID string, seasonNumber int, libraryID *int) (*upstreamSeason, error)
@@ -18,6 +18,15 @@ type ContentService interface {
ListItemFilters(ctx context.Context, session *Session, params url.Values) (*upstreamItemFiltersResponse, error)
}
type SearchItemsOptions struct {
Query string
ItemTypes []string
Limit int
Offset int
LibraryID *int
SkipTotal bool
}
// UserDataService provides favorites/progress/watched operations.
type UserDataService interface {
ListFavorites(ctx context.Context, session *Session, limit, offset int) ([]upstreamListItem, error)
+23 -2
View File
@@ -272,6 +272,17 @@ func itemTypesContain(itemTypes []string, target string) bool {
return false
}
func searchItemTypesForQuery(query itemsQuery) []string {
itemTypes := append([]string(nil), query.itemTypes...)
if query.mediaTypesExplicit && !query.mediaTypesSet["video"] {
return []string{compatNoMatchType}
}
if query.hasItemTypeFilter && len(itemTypes) == 0 {
return []string{compatNoMatchType}
}
return itemTypes
}
// HandleItem serves GET /Items/{id}.
func (h *ItemsHandler) HandleItem(w http.ResponseWriter, r *http.Request) {
session := SessionFromContext(r.Context())
@@ -1605,7 +1616,10 @@ func (h *ItemsHandler) HandleSearchHints(w http.ResponseWriter, r *http.Request)
}
limit := parsePositiveInt(q.Get("Limit"), 20)
result, err := h.content.SearchItems(r.Context(), session, query, nil, limit, 0, nil)
result, err := h.content.SearchItems(r.Context(), session, SearchItemsOptions{
Query: query,
Limit: limit,
})
if err != nil {
writeCompatUpstreamError(w, err)
return
@@ -1864,7 +1878,14 @@ func (h *ItemsHandler) handleFavoriteItems(w http.ResponseWriter, r *http.Reques
}
func (h *ItemsHandler) handleSearchItems(w http.ResponseWriter, r *http.Request, session *Session, query itemsQuery) {
result, err := h.content.SearchItems(r.Context(), session, query.searchTerm, query.itemTypes, query.limit, query.startIndex, libraryIDPtr(query.parentLibraryID))
result, err := h.content.SearchItems(r.Context(), session, SearchItemsOptions{
Query: query.searchTerm,
ItemTypes: searchItemTypesForQuery(query),
Limit: query.limit,
Offset: query.startIndex,
LibraryID: libraryIDPtr(query.parentLibraryID),
SkipTotal: !query.enableTotalRecordCount,
})
if err != nil {
writeCompatUpstreamError(w, err)
return
+142 -1
View File
@@ -50,7 +50,7 @@ func (s *countingContentService) BrowseItems(context.Context, *Session, url.Valu
panic("unused")
}
func (s *countingContentService) SearchItems(context.Context, *Session, string, []string, int, int, *int) (*upstreamBrowseResponse, error) {
func (s *countingContentService) SearchItems(context.Context, *Session, SearchItemsOptions) (*upstreamBrowseResponse, error) {
panic("unused")
}
@@ -81,6 +81,20 @@ func (s *countingContentService) ListItemFilters(context.Context, *Session, url.
panic("unused")
}
type recordingSearchContentService struct {
countingContentService
options []SearchItemsOptions
result *upstreamBrowseResponse
}
func (s *recordingSearchContentService) SearchItems(_ context.Context, _ *Session, opts SearchItemsOptions) (*upstreamBrowseResponse, error) {
s.options = append(s.options, opts)
if s.result != nil {
return s.result, nil
}
return &upstreamBrowseResponse{Items: []upstreamListItem{}}, nil
}
func TestHandleItems_SeriesParentSeasonFilterReturnsPagedSeasons(t *testing.T) {
codec := NewResourceIDCodec()
seriesContentID := "series-1"
@@ -140,6 +154,133 @@ func TestHandleItems_SeriesParentSeasonFilterReturnsPagedSeasons(t *testing.T) {
}
}
func TestHandleItemsSearchPropagatesEnableTotalRecordCount(t *testing.T) {
for _, tc := range []struct {
name string
querySuffix string
wantSkipTotal bool
}{
{name: "default includes total", wantSkipTotal: false},
{name: "disabled skips total", querySuffix: "&EnableTotalRecordCount=false", wantSkipTotal: true},
} {
t.Run(tc.name, func(t *testing.T) {
codec := NewResourceIDCodec()
contentSvc := &recordingSearchContentService{}
h := &ItemsHandler{
content: contentSvc,
userData: &mockUserDataService{},
codec: codec,
mapper: newMapper(codec, &config.Config{}),
images: NewImageCache(time.Hour, time.Now),
}
req := httptest.NewRequest("GET", "/Users/test/Items?SearchTerm=dune&Limit=5&StartIndex=2"+tc.querySuffix, nil)
req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, &Session{
StreamAppUserID: 1,
ProfileID: "profile-1",
}))
rec := httptest.NewRecorder()
h.HandleItems(rec, req)
if rec.Code != 200 {
t.Fatalf("expected status 200; got %d, body=%s", rec.Code, rec.Body.String())
}
if len(contentSvc.options) != 1 {
t.Fatalf("SearchItems calls = %d, want 1", len(contentSvc.options))
}
opts := contentSvc.options[0]
if opts.Query != "dune" || opts.Limit != 5 || opts.Offset != 2 {
t.Fatalf("SearchItems options = %#v", opts)
}
if opts.SkipTotal != tc.wantSkipTotal {
t.Fatalf("SkipTotal = %v, want %v", opts.SkipTotal, tc.wantSkipTotal)
}
})
}
}
func TestHandleItemsSearchMediaTypesVideoExcludeMovieEpisodeSearchesSeries(t *testing.T) {
codec := NewResourceIDCodec()
contentSvc := &recordingSearchContentService{}
h := &ItemsHandler{
content: contentSvc,
userData: &mockUserDataService{},
codec: codec,
mapper: newMapper(codec, &config.Config{}),
images: NewImageCache(time.Hour, time.Now),
}
req := httptest.NewRequest("GET", "/Items?SearchTerm=sponge+bob&Limit=100"+
"&ExcludeItemTypes=Movie&ExcludeItemTypes=Episode&ExcludeItemTypes=TvChannel"+
"&MediaTypes=Video&EnableTotalRecordCount=false", nil)
req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, &Session{
StreamAppUserID: 1,
ProfileID: "profile-1",
}))
rec := httptest.NewRecorder()
h.HandleItems(rec, req)
if rec.Code != 200 {
t.Fatalf("expected status 200; got %d, body=%s", rec.Code, rec.Body.String())
}
if len(contentSvc.options) != 1 {
t.Fatalf("SearchItems calls = %d, want 1", len(contentSvc.options))
}
opts := contentSvc.options[0]
if opts.Query != "sponge bob" || opts.Limit != 100 || !opts.SkipTotal {
t.Fatalf("SearchItems options = %#v", opts)
}
if len(opts.ItemTypes) != 1 || opts.ItemTypes[0] != "series" {
t.Fatalf("ItemTypes = %v, want [series]", opts.ItemTypes)
}
var result queryResultDTO
if err := json.NewDecoder(rec.Body).Decode(&result); err != nil {
t.Fatalf("decode response: %v", err)
}
if result.TotalRecordCount != 0 || len(result.Items) != 0 {
t.Fatalf("result = total %d items %d, want empty", result.TotalRecordCount, len(result.Items))
}
}
func TestHandleItemsSearchSeriesScopeReachesProvider(t *testing.T) {
codec := NewResourceIDCodec()
contentSvc := &recordingSearchContentService{}
h := &ItemsHandler{
content: contentSvc,
userData: &mockUserDataService{},
codec: codec,
mapper: newMapper(codec, &config.Config{}),
images: NewImageCache(time.Hour, time.Now),
}
req := httptest.NewRequest("GET", "/Items?SearchTerm=spongebob&Limit=100"+
"&IncludeItemTypes=Series&EnableTotalRecordCount=false", nil)
req = req.WithContext(context.WithValue(req.Context(), compatSessionKey, &Session{
StreamAppUserID: 1,
ProfileID: "profile-1",
}))
rec := httptest.NewRecorder()
h.HandleItems(rec, req)
if rec.Code != 200 {
t.Fatalf("expected status 200; got %d, body=%s", rec.Code, rec.Body.String())
}
if len(contentSvc.options) != 1 {
t.Fatalf("SearchItems calls = %d, want 1", len(contentSvc.options))
}
opts := contentSvc.options[0]
if opts.Query != "spongebob" || opts.Limit != 100 || !opts.SkipTotal {
t.Fatalf("SearchItems options = %#v", opts)
}
if len(opts.ItemTypes) != 1 || opts.ItemTypes[0] != "series" {
t.Fatalf("ItemTypes = %v, want [series]", opts.ItemTypes)
}
}
// TestHandleItem_Episode_FetchesSeriesDetailForStableParentImageTags verifies
// that episode detail responses fetch parent series image metadata even when
// image URLs are already cached. Cached URLs are not enough to build stable
+6 -1
View File
@@ -147,7 +147,12 @@ func (h *PersonsHandler) shouldSuppressSearchPeople(ctx context.Context, session
return false
}
media, err := h.content.SearchItems(ctx, session, raw, []string{"movie", "series"}, 5, 0, nil)
media, err := h.content.SearchItems(ctx, session, SearchItemsOptions{
Query: raw,
ItemTypes: []string{"movie", "series"},
Limit: 5,
SkipTotal: true,
})
if err != nil || media == nil || len(media.Items) == 0 {
return false
}
+42 -3
View File
@@ -28,7 +28,7 @@ type itemsQuery struct {
genreName string
isFavorite bool
isResumable bool
hasItemTypeFilter bool // true when IncludeItemTypes was present in the request
hasItemTypeFilter bool // true when IncludeItemTypes or ExcludeItemTypes was present in the request
wantsBoxSets bool // true when IncludeItemTypes contains BoxSet
wantsViews bool // true when IncludeItemTypes contains CollectionFolder
sortExplicit bool // true when SortBy was present in the request
@@ -110,8 +110,9 @@ func parseItemsQuery(r *http.Request, codec *ResourceIDCodec) itemsQuery {
}
rawItemTypes := q.Values("IncludeItemTypes")
result.hasItemTypeFilter = len(rawItemTypes) > 0 && strings.TrimSpace(strings.Join(rawItemTypes, "")) != ""
result.itemTypes = mapIncludeItemTypes(rawItemTypes)
rawExcludedItemTypes := q.Values("ExcludeItemTypes")
result.hasItemTypeFilter = hasNonEmptyValues(rawItemTypes) || hasNonEmptyValues(rawExcludedItemTypes)
result.itemTypes = effectiveItemTypes(rawItemTypes, rawExcludedItemTypes)
result.wantsBoxSets = includeItemTypesContain(rawItemTypes, "boxset")
result.wantsViews = includeItemTypesContain(rawItemTypes, "collectionfolder")
result.sortExplicit = strings.TrimSpace(q.Get("SortBy")) != ""
@@ -342,6 +343,44 @@ func mapIncludeItemTypes(rawValues []string) []string {
return result
}
func effectiveItemTypes(rawIncluded, rawExcluded []string) []string {
included := mapIncludeItemTypes(rawIncluded)
excluded := mapIncludeItemTypes(rawExcluded)
if len(excluded) == 0 {
return included
}
base := included
if len(base) == 0 && !hasNonEmptyValues(rawIncluded) {
base = compatVideoTypeList
}
if len(base) == 0 {
return nil
}
excludedSet := make(map[string]struct{}, len(excluded))
for _, itemType := range excluded {
excludedSet[itemType] = struct{}{}
}
result := make([]string, 0, len(base))
for _, itemType := range base {
if _, skip := excludedSet[itemType]; skip {
continue
}
result = append(result, itemType)
}
return result
}
func hasNonEmptyValues(values []string) bool {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return true
}
}
return false
}
// includeItemTypesContain reports whether a raw IncludeItemTypes value list
// contains the given (lowercase) type, before mapIncludeItemTypes drops
// entries it cannot map to catalog types (e.g. BoxSet).
+24
View File
@@ -40,6 +40,30 @@ func TestBuildBrowseParamsPropagatesEnableTotalRecordCount(t *testing.T) {
}
}
func TestParseItemsQueryAppliesExcludeItemTypesToDefaultVideoScope(t *testing.T) {
req := httptest.NewRequest("GET", "/Items?SearchTerm=sponge+bob"+
"&ExcludeItemTypes=Movie&ExcludeItemTypes=Episode&ExcludeItemTypes=TvChannel", nil)
query := parseItemsQuery(req, NewResourceIDCodec())
if !query.hasItemTypeFilter {
t.Fatal("expected ExcludeItemTypes to count as an item type filter")
}
if len(query.itemTypes) != 1 || query.itemTypes[0] != "series" {
t.Fatalf("itemTypes = %v, want [series]", query.itemTypes)
}
}
func TestParseItemsQuerySubtractsExcludeItemTypesFromIncludeItemTypes(t *testing.T) {
req := httptest.NewRequest("GET", "/Items?IncludeItemTypes=Movie,Series&ExcludeItemTypes=Movie", nil)
query := parseItemsQuery(req, NewResourceIDCodec())
if len(query.itemTypes) != 1 || query.itemTypes[0] != "series" {
t.Fatalf("itemTypes = %v, want [series]", query.itemTypes)
}
}
func TestMapSortByReleaseDate(t *testing.T) {
tests := []string{
"PremiereDate",
+1
View File
@@ -310,6 +310,7 @@ func withDefaults(deps Dependencies) Dependencies {
deps.FolderRepo,
deps.UserStoreProvider,
deps.AccessFilterFn,
deps.CatalogSearchProvider,
)
if deps.PosterPresigner != nil {
svc.posterPresigner = deps.PosterPresigner
+8 -7
View File
@@ -62,13 +62,14 @@ type Dependencies struct {
ScanQueue scantrigger.Queuer
// Catalog repos (for ContentService construction)
BrowseRepo *catalog.BrowseRepository
ItemRepo *catalog.ItemRepository
SeasonRepo *catalog.SeasonRepository
EpisodeRepo *catalog.EpisodeRepository
ProviderIDRepo *catalog.ProviderIDRepository
DetailSvc *catalog.DetailService
FolderRepo *catalog.FolderRepository
BrowseRepo *catalog.BrowseRepository
ItemRepo *catalog.ItemRepository
SeasonRepo *catalog.SeasonRepository
EpisodeRepo *catalog.EpisodeRepository
ProviderIDRepo *catalog.ProviderIDRepository
DetailSvc *catalog.DetailService
FolderRepo *catalog.FolderRepository
CatalogSearchProvider catalog.CatalogSearchProvider
// Person repository
PersonRepo *catalog.PersonRepository