Files
silo-server/internal/usercollections/types.go
T
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

97 lines
3.2 KiB
Go

// Package usercollections owns the import + sync logic for personal,
// profile-scoped collections (the user-facing analogue of the admin
// library_collections subsystem).
//
// User collections live in user_personal_collections and use external sources
// (TMDB / Trakt / MDBList) the same way admin collections do, but resolved
// against the entire catalog. Per-user library access is enforced at read
// time by the catalog resolver, so the sync service does not have to scope
// item resolution to a particular set of libraries.
package usercollections
import (
"encoding/json"
"errors"
"fmt"
"github.com/Silo-Server/silo-server/internal/collectionutil"
)
// NormalizeMDBListURL accepts either a list page URL
// (https://mdblist.com/lists/user/slug) or its JSON variant and returns the
// canonical JSON URL. Trailing slashes are tolerated. Empty input is
// returned unchanged so callers can keep their own validation.
func NormalizeMDBListURL(url string) string {
return collectionutil.NormalizeMDBListURL(url)
}
type SourceMode string
const (
SourceModeMDBList SourceMode = "mdblist_json"
SourceModeTMDBPreset SourceMode = "tmdb_preset"
SourceModeTraktPreset SourceMode = "trakt_preset"
)
// MinSyncIntervalHours is the smallest interval (in hours between fires) that
// a user is allowed to schedule. Stricter than admin to keep TMDB/Trakt API
// quota bounded across many users.
const MinSyncIntervalHours = 24
var ErrSyncUnsupported = errors.New("collection cannot be synced")
type SourceConfig struct {
Mode SourceMode `json:"mode"`
URL string `json:"url,omitempty"`
Preset string `json:"preset,omitempty"`
Provider string `json:"provider,omitempty"`
MediaType string `json:"media_type,omitempty"`
TimeWindow string `json:"time_window,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
Limit *int `json:"limit,omitempty"`
// LibraryIDs narrows sync resolution to these libraries. Empty/nil means
// resolve against every library the requesting user can access.
LibraryIDs []int `json:"library_ids,omitempty"`
}
func MarshalSourceConfig(cfg SourceConfig) (string, error) {
raw, err := json.Marshal(cfg)
if err != nil {
return "", err
}
return string(raw), nil
}
func ParseSourceConfig(raw string) (SourceConfig, error) {
var cfg SourceConfig
if raw == "" {
return cfg, nil
}
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
return cfg, err
}
return cfg, nil
}
// DisplayURL produces a stable, human-friendly identifier for the source —
// stored on the row so list views can render it without reparsing
// SourceConfig. Mirrors the admin tmdb://… / trakt://… scheme.
func (c SourceConfig) DisplayURL() string {
switch c.Mode {
case SourceModeMDBList:
return c.URL
case SourceModeTMDBPreset:
if c.Preset == "trending" {
return fmt.Sprintf("tmdb://%s/%s/%s", c.Preset, c.MediaType, c.TimeWindow)
}
return fmt.Sprintf("tmdb://%s/%s", c.Preset, c.MediaType)
case SourceModeTraktPreset:
if c.Preset == "recommended" {
return fmt.Sprintf("trakt://%s/%s/%s", c.Preset, c.MediaType, c.ProfileID)
}
return fmt.Sprintf("trakt://%s/%s", c.Preset, c.MediaType)
default:
return ""
}
}