Files
silo-server/internal/catalog/history_source.go
T

222 lines
5.6 KiB
Go

package catalog
import (
"context"
"fmt"
"strings"
"time"
)
func historySourceCanUseOptimizedPageQuery(req CatalogRequest) bool {
if req.Source != CatalogSourceHistory || !req.UseSourceOrder {
return false
}
if strings.TrimSpace(req.SearchQuery) != "" || strings.TrimSpace(req.NamePrefix) != "" {
return false
}
def := req.Query.Normalize()
return def.MediaScope == "" &&
len(def.LibraryIDs) == 0 &&
len(def.Groups) == 0
}
func (r *CatalogResolver) resolveHistorySourcePage(
ctx context.Context,
req CatalogRequest,
access AccessFilter,
) (*CatalogResult, error) {
snapshot := time.Now().UTC()
if req.SnapshotAt != nil {
snapshot = *req.SnapshotAt
}
displayIDs, total, hasMore, err := r.loadHistoryDisplayPage(
ctx,
access,
req.Limit,
req.Offset,
!req.SkipTotal,
&snapshot,
)
if err != nil {
return nil, err
}
items, err := r.fetchAccessibleItemsByID(ctx, displayIDs, req, access)
if err != nil {
return nil, err
}
return &CatalogResult{
Items: items,
Total: total,
HasMore: hasMore,
TotalExact: !req.SkipTotal,
SnapshotAt: snapshot,
}, nil
}
func (r *CatalogResolver) loadHistoryDisplayPage(
ctx context.Context,
access AccessFilter,
limit int,
offset int,
includeTotal bool,
snapshot *time.Time,
) ([]string, int, bool, error) {
if r == nil || r.itemRepo == nil || r.itemRepo.pool == nil {
return nil, 0, false, fmt.Errorf("catalog resolver requires an item repository")
}
if access.UserID <= 0 || strings.TrimSpace(access.ProfileID) == "" {
return nil, 0, false, fmt.Errorf("%w: history source requires active user scope", ErrInvalidCatalogRequest)
}
if limit <= 0 {
limit = 20
}
if offset < 0 {
offset = 0
}
baseQuery, baseArgs := buildHistoryDisplayBaseQuery(access, snapshot)
total := 0
if includeTotal {
countQuery := fmt.Sprintf(`WITH history_display AS (%s) SELECT COUNT(*) FROM history_display`, baseQuery)
if err := r.itemRepo.pool.QueryRow(ctx, countQuery, baseArgs...).Scan(&total); err != nil {
return nil, 0, false, fmt.Errorf("counting history display rows: %w", err)
}
if total == 0 {
return []string{}, 0, false, nil
}
}
queryLimit := limit
if !includeTotal {
queryLimit++
}
args := append([]any{}, baseArgs...)
limitArgIdx := len(args) + 1
args = append(args, queryLimit)
offsetClause := ""
if offset > 0 {
offsetArgIdx := len(args) + 1
offsetClause = fmt.Sprintf(" OFFSET $%d", offsetArgIdx)
args = append(args, offset)
}
pageQuery := fmt.Sprintf(
`WITH history_display AS (%s)
SELECT display_id
FROM history_display
ORDER BY watched_at DESC, display_id ASC
LIMIT $%d%s`,
baseQuery,
limitArgIdx,
offsetClause,
)
rows, err := r.itemRepo.pool.Query(ctx, pageQuery, args...)
if err != nil {
return nil, 0, false, fmt.Errorf("querying history display page: %w", err)
}
defer rows.Close()
displayIDs := make([]string, 0, limit)
for rows.Next() {
var displayID string
if err := rows.Scan(&displayID); err != nil {
return nil, 0, false, fmt.Errorf("scanning history display row: %w", err)
}
displayIDs = append(displayIDs, displayID)
}
if err := rows.Err(); err != nil {
return nil, 0, false, fmt.Errorf("iterating history display rows: %w", err)
}
hasMore := false
if includeTotal {
hasMore = total > offset+len(displayIDs)
return displayIDs, total, hasMore, nil
}
if len(displayIDs) > limit {
hasMore = true
displayIDs = displayIDs[:limit]
}
return displayIDs, 0, hasMore, nil
}
func buildHistoryDisplayBaseQuery(access AccessFilter, snapshot *time.Time) (string, []any) {
args := []any{access.UserID, access.ProfileID}
argIdx := 3
conditions := []string{
"h.user_id = $1",
"h.profile_id = $2",
`NOT EXISTS (
SELECT 1
FROM user_history_hidden_items hhi
WHERE hhi.user_id = h.user_id
AND hhi.profile_id = h.profile_id
AND hhi.media_item_id = h.media_item_id
AND h.watched_at <= hhi.hidden_before
)`,
}
if snapshot != nil {
conditions = append(conditions, fmt.Sprintf("h.watched_at <= $%d", argIdx))
args = append(args, *snapshot)
argIdx++
}
if access.AllowedContentIDs != nil {
if len(access.AllowedContentIDs) == 0 {
conditions = append(conditions, "1 = 0")
} else {
conditions = append(conditions, fmt.Sprintf("mi.content_id = ANY($%d)", argIdx))
args = append(args, access.AllowedContentIDs)
argIdx++
}
}
if len(access.AllowedLibraryIDs) > 0 {
conditions = append(conditions, fmt.Sprintf(`EXISTS (
SELECT 1
FROM media_item_libraries mil
WHERE mil.content_id = mi.content_id
AND mil.media_folder_id = ANY($%d)
)`, argIdx))
args = append(args, access.AllowedLibraryIDs)
argIdx++
} else if access.AllowedLibraryIDs != nil {
conditions = append(conditions, "1 = 0")
}
if len(access.DisabledLibraryIDs) > 0 {
conditions = append(conditions, fmt.Sprintf(`NOT EXISTS (
SELECT 1
FROM media_item_libraries mil_disabled
WHERE mil_disabled.content_id = mi.content_id
AND mil_disabled.media_folder_id = ANY($%d)
)`, argIdx))
args = append(args, access.DisabledLibraryIDs)
argIdx++
}
ApplySectionAccessFilter("mi", access, &conditions, &args, &argIdx)
return fmt.Sprintf(
`SELECT DISTINCT ON (history_events.display_id) history_events.display_id, history_events.watched_at
FROM (
SELECT COALESCE(NULLIF(e.series_id, ''), h.media_item_id) AS display_id, h.watched_at
FROM user_watch_history h
LEFT JOIN episodes e ON e.content_id = h.media_item_id
JOIN media_items mi ON mi.content_id = COALESCE(NULLIF(e.series_id, ''), h.media_item_id)
WHERE %s
) history_events
ORDER BY history_events.display_id ASC, history_events.watched_at DESC`,
strings.Join(conditions, " AND "),
), args
}