Files
silo-server/internal/usercollections/listing.go
87159b0a38 feat(collections): add profile-scoped display filters (#191)
* feat(collections): add profile-scoped display filters

* refactor(collections): dedup display-filter helpers per review

Address code-review feedback on the profile-scoped display filters
without changing behavior:

- Widen CompletedHistoryItemMap to accept ProgressCompletionStore and
  drop the duplicate completedHistoryItemMapForProgress copy.
- Extract the duplicated MDBList candidate retry loop into a generic
  collectionutil.FetchMDBListWithFallback helper, used by both the user
  and library collection syncers, and cover it with unit tests.
- Reuse validateOptionalLibraryIDs in HandleUpdateCollection instead of
  an inline positive-ID loop.
- Import the shared COLLECTION_{WATCH,MEDIA}_FILTER_OPTIONS in the
  template config form rather than redefining them locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(collections): sanitize query_definition library_ids fallback

readSourceConfigLibraryIDs validated source_config.library_ids (finite,
positive, truncated, deduplicated) but returned the query_definition
fallback raw, so legacy rows could surface zero/negative/duplicate IDs
that the backend now rejects on save. Extract a shared sanitizer and
apply it to both paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(docs): This makes the agents annoying to work with

* Improve playback session handling

* Support collection source order in catalog filters

* fix(collections): address display filter review feedback

* refactor(catalog): remove duplicate collection query params

* Hide episode media scope for collection overlays

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:03:38 -04:00

93 lines
3.6 KiB
Go

package usercollections
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// ServerVisibleCollection is the per-user view of a personal collection the
// owner has opted into their own library Collections tab. Personal collections
// are private to their owner; this list never crosses user boundaries.
type ServerVisibleCollection struct {
ID string `json:"id"`
CreatorProfileID string `json:"creator_profile_id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
CollectionType string `json:"collection_type"`
ItemCount int `json:"item_count"`
PosterPath string `json:"-"`
PosterURL string `json:"poster_url,omitempty"`
PosterThumbhash string `json:"poster_thumbhash,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// serverVisibleListLimit caps how many opt-in collections a single library tab
// will surface so a user with hundreds of published collections can't blow up
// the response.
const serverVisibleListLimit = 500
// ListServerVisibleByLibrary returns the current user's personal collections
// that have opted into their library Collections tab and whose library scope
// matches the requested library. Imported exact collections read library_ids
// from source_config, while legacy rows can fall back to query_definition. A
// collection with no library_ids is treated as library-agnostic and therefore
// visible in every library tab. Other users' collections are never returned.
func ListServerVisibleByLibrary(ctx context.Context, pool *pgxpool.Pool, userID int, profileID string, libraryID int) ([]ServerVisibleCollection, error) {
rows, err := pool.Query(ctx,
`WITH visible_collections AS (
SELECT upc.id, upc.creator_profile_id, upc.name, upc.description, upc.collection_type,
upc.item_count, upc.poster_url, upc.poster_thumbhash, upc.created_at, upc.updated_at,
CASE
WHEN upc.collection_type = 'smart' THEN upc.query_definition
WHEN upc.source_config ? 'library_ids' THEN upc.source_config
ELSE upc.query_definition
END AS scope_config
FROM user_personal_collections upc
WHERE upc.user_id = $1
AND upc.include_in_server_collections = TRUE
AND EXISTS (
SELECT 1 FROM user_personal_collection_profiles vp
WHERE vp.user_id = upc.user_id
AND vp.collection_id = upc.id
AND vp.profile_id = $2
)
)
SELECT id, creator_profile_id, name, description, collection_type, item_count,
poster_url, poster_thumbhash, created_at, updated_at
FROM visible_collections
WHERE TRUE
AND (
NOT (scope_config ? 'library_ids')
OR jsonb_array_length(COALESCE(scope_config->'library_ids', '[]'::jsonb)) = 0
OR scope_config->'library_ids' @> to_jsonb($3::int)
)
ORDER BY name ASC
LIMIT $4`,
userID, profileID, libraryID, serverVisibleListLimit,
)
if err != nil {
return nil, fmt.Errorf("listing server-visible user collections: %w", err)
}
defer rows.Close()
var out []ServerVisibleCollection
for rows.Next() {
var c ServerVisibleCollection
var createdAt, updatedAt time.Time
if err := rows.Scan(
&c.ID, &c.CreatorProfileID, &c.Name, &c.Description, &c.CollectionType,
&c.ItemCount, &c.PosterPath, &c.PosterThumbhash, &createdAt, &updatedAt,
); err != nil {
return nil, fmt.Errorf("scanning server-visible user collection: %w", err)
}
c.CreatedAt = createdAt.UTC().Format(time.RFC3339)
c.UpdatedAt = updatedAt.UTC().Format(time.RFC3339)
out = append(out, c)
}
return out, rows.Err()
}