222 lines
5.6 KiB
Go
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
|
|
}
|