The History page's "Episodes" media filter was structurally empty: both history read paths collapse episode watch events into their series display item, and the resulting series ids were then filtered against type='episode' — guaranteed zero results. The episode media scope now resolves history at episode granularity: - loadPersonalSourceIDs keeps each watched episode's own id (deduped to the most recent watch, most-recent-first) instead of collapsing to the series; ListFilters and SearchFacet share the loader, so facets on the episode-scoped history view work too. - fetchAccessibleItemsByID gains an episode-scope path through the episode catalog query executor, since episode rows never hydrate from media_items via the browse repository. Every other scope keeps the existing series-collapse behavior. Fixes #299 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
82 lines
2.3 KiB
Go
82 lines
2.3 KiB
Go
package catalog
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/userstore"
|
|
)
|
|
|
|
// HistoryEpisodeScopeIDs returns history entry ids for the episode-scoped
|
|
// history view: each watched item keeps its own id (episodes are NOT collapsed
|
|
// into their series), deduplicated to the most recent watch — entries arrive
|
|
// most-recent-first from ListHistory. Non-episode ids (movies, audiobooks)
|
|
// pass through unchanged; the episode catalog relation drops them at
|
|
// hydration, so no episodes lookup is needed here.
|
|
func HistoryEpisodeScopeIDs(entries []userstore.WatchHistoryEntry) []string {
|
|
ids := make([]string, 0, len(entries))
|
|
seen := make(map[string]struct{}, len(entries))
|
|
for _, entry := range entries {
|
|
mediaItemID := strings.TrimSpace(entry.MediaItemID)
|
|
if mediaItemID == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[mediaItemID]; ok {
|
|
continue
|
|
}
|
|
seen[mediaItemID] = struct{}{}
|
|
ids = append(ids, mediaItemID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func ResolveHistoryDisplayIDs(ctx context.Context, entries []userstore.WatchHistoryEntry, episodeRepo *EpisodeRepository) ([]string, error) {
|
|
episodeSeriesByID := make(map[string]string)
|
|
if episodeRepo != nil {
|
|
episodeIDs := make([]string, 0, len(entries))
|
|
seenEpisodeIDs := make(map[string]struct{}, len(entries))
|
|
for _, entry := range entries {
|
|
mediaItemID := strings.TrimSpace(entry.MediaItemID)
|
|
if mediaItemID == "" {
|
|
continue
|
|
}
|
|
if _, ok := seenEpisodeIDs[mediaItemID]; ok {
|
|
continue
|
|
}
|
|
seenEpisodeIDs[mediaItemID] = struct{}{}
|
|
episodeIDs = append(episodeIDs, mediaItemID)
|
|
}
|
|
|
|
episodes, err := episodeRepo.GetByIDs(ctx, episodeIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, episode := range episodes {
|
|
if episode == nil {
|
|
continue
|
|
}
|
|
if seriesID := strings.TrimSpace(episode.SeriesID); seriesID != "" {
|
|
episodeSeriesByID[episode.ContentID] = seriesID
|
|
}
|
|
}
|
|
}
|
|
|
|
ids := make([]string, 0, len(entries))
|
|
seen := make(map[string]struct{}, len(entries))
|
|
for _, entry := range entries {
|
|
displayID := strings.TrimSpace(entry.MediaItemID)
|
|
if seriesID, ok := episodeSeriesByID[displayID]; ok {
|
|
displayID = seriesID
|
|
}
|
|
if displayID == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[displayID]; ok {
|
|
continue
|
|
}
|
|
seen[displayID] = struct{}{}
|
|
ids = append(ids, displayID)
|
|
}
|
|
return ids, nil
|
|
}
|