fix(catalog): include user sort metrics

This commit is contained in:
zZebrahz
2026-05-30 02:55:05 +00:00
parent a2d8118978
commit 20da980cc8
3 changed files with 186 additions and 0 deletions
+3
View File
@@ -78,12 +78,15 @@ func (h *CatalogHandler) HandleGetCatalog(w http.ResponseWriter, r *http.Request
userStates := h.itemsH.listItemUserStates(r, result.Items)
episodeMetadata := h.itemsH.listEpisodeBrowseMetadata(r.Context(), result.Items)
sortField := catalog.NormalizeQuerySort(req.Query.Sort).Field
store, profileID, _ := h.itemsH.userStoreForRequest(r)
sortMetrics := h.itemsH.listSortMetrics(
r.Context(),
result.Items,
sortField,
h.itemsH.accessFilter(r),
overlaySummaries,
store,
profileID,
)
items := make([]itemListResponse, 0, len(result.Items))
for _, item := range result.Items {
+124
View File
@@ -847,6 +847,8 @@ func (h *ItemsHandler) listSortMetrics(
sortField string,
filter catalog.AccessFilter,
overlaySummaries map[string]*models.OverlaySummary,
store userstore.UserStore,
profileID string,
) map[string]*sortMetricsResponse {
metrics := make(map[string]*sortMetricsResponse, len(items))
switch sortField {
@@ -885,10 +887,132 @@ func (h *ItemsHandler) listSortMetrics(
metrics[contentID] = &sortMetricsResponse{BitrateKbps: &value}
}
}
case "progress", "date_viewed", "plays":
h.listUserSortMetrics(ctx, items, sortField, store, profileID, metrics)
}
return metrics
}
func (h *ItemsHandler) listUserSortMetrics(
ctx context.Context,
items []*models.MediaItem,
sortField string,
store userstore.UserStore,
profileID string,
metrics map[string]*sortMetricsResponse,
) {
if store == nil || profileID == "" || len(items) == 0 {
return
}
contentIDs := uniqueItemContentIDs(items)
if len(contentIDs) == 0 {
return
}
progressMap, err := store.ListProgressByMediaItems(ctx, profileID, contentIDs)
if err != nil {
return
}
switch sortField {
case "progress":
for _, item := range items {
if item == nil || item.ContentID == "" {
continue
}
progress, ok := progressMap[item.ContentID]
if !ok || progress.Completed || progress.PositionSeconds <= 0 || progress.DurationSeconds <= 0 {
continue
}
ratio := progress.PositionSeconds / progress.DurationSeconds
metrics[item.ContentID] = &sortMetricsResponse{ProgressRatio: &ratio}
}
case "date_viewed", "plays":
history, err := listCompletedHistoryForItems(ctx, store, profileID, contentIDs)
if err != nil {
return
}
historyCounts := make(map[string]int, len(contentIDs))
historyViewedAt := make(map[string]string, len(contentIDs))
for _, entry := range history {
if entry.MediaItemID == "" {
continue
}
historyCounts[entry.MediaItemID]++
if entry.WatchedAt > historyViewedAt[entry.MediaItemID] {
historyViewedAt[entry.MediaItemID] = entry.WatchedAt
}
}
for _, item := range items {
if item == nil || item.ContentID == "" {
continue
}
resp := &sortMetricsResponse{}
if sortField == "date_viewed" {
viewedAt := historyViewedAt[item.ContentID]
if progress, ok := progressMap[item.ContentID]; ok && progress.Completed && progress.UpdatedAt > viewedAt {
viewedAt = progress.UpdatedAt
}
if viewedAt == "" {
continue
}
resp.ViewedAt = viewedAt
} else {
playCount := historyCounts[item.ContentID]
if progress, ok := progressMap[item.ContentID]; ok && progress.Completed && playCount < 1 {
playCount = 1
}
if playCount <= 0 {
continue
}
resp.PlayCount = &playCount
}
metrics[item.ContentID] = resp
}
}
}
func uniqueItemContentIDs(items []*models.MediaItem) []string {
contentIDs := make([]string, 0, len(items))
seen := make(map[string]struct{}, len(items))
for _, item := range items {
if item == nil || item.ContentID == "" {
continue
}
if _, ok := seen[item.ContentID]; ok {
continue
}
seen[item.ContentID] = struct{}{}
contentIDs = append(contentIDs, item.ContentID)
}
return contentIDs
}
func listCompletedHistoryForItems(
ctx context.Context,
store userstore.UserStore,
profileID string,
contentIDs []string,
) ([]userstore.WatchHistoryEntry, error) {
const pageSize = 500
var all []userstore.WatchHistoryEntry
for offset := 0; ; offset += pageSize {
page, err := store.ListCompletedHistory(ctx, userstore.CompletedHistoryQuery{
ProfileID: profileID,
MediaItemIDs: contentIDs,
Limit: pageSize,
Offset: offset,
})
if err != nil {
return nil, err
}
all = append(all, page...)
if len(page) < pageSize {
return all, nil
}
}
}
func (h *ItemsHandler) listBrowseItemFiles(ctx context.Context, items []*models.MediaItem, filter catalog.AccessFilter) map[string][]*models.MediaFile {
grouped := make(map[string][]*models.MediaFile, len(items))
if h.fileRepo == nil || len(items) == 0 {
@@ -0,0 +1,59 @@
package handlers
import (
"context"
"testing"
"time"
"github.com/Silo-Server/silo-server/internal/catalog"
"github.com/Silo-Server/silo-server/internal/models"
"github.com/Silo-Server/silo-server/internal/userstore"
)
func TestListSortMetricsIncludesUserStateSorts(t *testing.T) {
ctx := context.Background()
store := newProfileTestStore(t)
items := []*models.MediaItem{
{ContentID: "movie-progress", Type: "movie", Title: "Progress"},
{ContentID: "movie-viewed", Type: "movie", Title: "Viewed"},
{ContentID: "movie-plays", Type: "movie", Title: "Plays"},
}
handler := &ItemsHandler{}
if err := store.SetProgress(ctx, "profile-1", "movie-progress", 900, 3600, userstore.ProgressThresholds{}); err != nil {
t.Fatalf("SetProgress(progress): %v", err)
}
completedAt := time.Date(2026, 5, 29, 18, 30, 0, 0, time.UTC)
if err := store.SetProgressAt(ctx, "profile-1", "movie-viewed", 3600, 3600, true, completedAt); err != nil {
t.Fatalf("SetProgressAt(viewed): %v", err)
}
for _, watchedAt := range []string{"2026-05-27T10:00:00Z", "2026-05-28T10:00:00Z"} {
if err := store.AddHistory(ctx, userstore.WatchHistoryEntry{
ProfileID: "profile-1",
MediaItemID: "movie-plays",
WatchedAt: watchedAt,
DurationSeconds: 3600,
Completed: true,
Source: userstore.WatchHistorySourcePlayback,
}); err != nil {
t.Fatalf("AddHistory(%s): %v", watchedAt, err)
}
}
progressMetrics := handler.listSortMetrics(ctx, items, "progress", catalog.AccessFilter{}, nil, store, "profile-1")
progress := progressMetrics["movie-progress"]
if progress == nil || progress.ProgressRatio == nil || *progress.ProgressRatio != 0.25 {
t.Fatalf("progress metrics = %#v", progress)
}
viewedMetrics := handler.listSortMetrics(ctx, items, "date_viewed", catalog.AccessFilter{}, nil, store, "profile-1")
if got := viewedMetrics["movie-viewed"]; got == nil || got.ViewedAt != "2026-05-29T18:30:00Z" {
t.Fatalf("viewed metrics = %#v", got)
}
playsMetrics := handler.listSortMetrics(ctx, items, "plays", catalog.AccessFilter{}, nil, store, "profile-1")
plays := playsMetrics["movie-plays"]
if plays == nil || plays.PlayCount == nil || *plays.PlayCount != 2 {
t.Fatalf("plays metrics = %#v", plays)
}
}