Documentation
¶
Overview ¶
Package pluginitems owns the storage, keyset browse, full-text search, sync engine, Title projection, matching and resolution cache for plugin content kinds (spec §2.2-2.5, §3.1-3.6, §4.4, §6.3, §7).
Plugin items never enter catalog_titles; every query is scoped by library_id first, and item identity is the namespaced core.ItemRef ("pk:<pluginID>:<kindKey>:<externalID>") paired with a stored, never-reused RatingKey sequence number (core.PluginKey).
Index ¶
- Constants
- Variables
- func FoldSortTitle(s string) string
- func Project(item core.KindItem, decl plugins.MediaKindMeta, present core.Presentations) core.Title
- func SortTitle(s string) string
- type BrowsePage
- type BrowseQuery
- type Caller
- type FacetCount
- type HealthReporter
- type IndexedFieldValue
- type KindRecord
- type MatchRecord
- type Options
- type SearchQuery
- type Service
- func (s *Service) BestMatch(ctx context.Context, itemRef core.ItemRef, threshold float64) (MatchRecord, bool, error)
- func (s *Service) Browse(ctx context.Context, q BrowseQuery) (BrowsePage, error)
- func (s *Service) Children(ctx context.Context, parentRef core.ItemRef, cursor string, limit int) (BrowsePage, error)
- func (s *Service) ClearManualMatch(ctx context.Context, itemRef core.ItemRef) error
- func (s *Service) Close()
- func (s *Service) DeleteItems(ctx context.Context, pluginID, kindKey string, externalIDs []string) error
- func (s *Service) Facets(ctx context.Context, libraryID core.LibraryID, level, fieldKey string, ...) ([]FacetCount, error)
- func (s *Service) Generation() uint64
- func (s *Service) GetByExternal(ctx context.Context, pluginID, kindKey, externalID string) (core.KindItem, core.LibraryID, error)
- func (s *Service) GetByKey(ctx context.Context, key core.RatingKey) (core.KindItem, core.LibraryID, error)
- func (s *Service) GetByRef(ctx context.Context, ref core.ItemRef) (core.KindItem, core.LibraryID, error)
- func (s *Service) GetKind(ctx context.Context, pluginID, kindKey string) (KindRecord, error)
- func (s *Service) GetKindByLibrary(ctx context.Context, libraryID core.LibraryID) (KindRecord, error)
- func (s *Service) GetResolution(ctx context.Context, ref core.ItemRef) (core.PlayableSource, bool, error)
- func (s *Service) InvalidateResolution(ctx context.Context, ref core.ItemRef) error
- func (s *Service) KindBackends() plugins.KindBackends
- func (s *Service) KindDelete(ctx context.Context, pluginID, kindKey string, externalIDs []string) error
- func (s *Service) KindItems(ctx context.Context, pluginID, kindKey, cursor string, limit int) (core.KindPage, error)
- func (s *Service) KindUpsert(ctx context.Context, pluginID, kindKey string, items []core.KindItem) (int, error)
- func (s *Service) LibraryTally(ctx context.Context, libraryID core.LibraryID) (map[string]int, error)
- func (s *Service) ListKinds(ctx context.Context, pluginID string) ([]KindRecord, error)
- func (s *Service) ListMatches(ctx context.Context, itemRef core.ItemRef) ([]MatchRecord, error)
- func (s *Service) MatchItem(ctx context.Context, caller Caller, matcherPluginID, kindKey string, ...) ([]core.KindMatchScore, error)
- func (s *Service) PinMatch(ctx context.Context, itemRef core.ItemRef, candidateRef string, ...) error
- func (s *Service) PurgeKind(ctx context.Context, pluginID, kindKey string) error
- func (s *Service) PurgePlugin(ctx context.Context, pluginID string) error
- func (s *Service) SaveMatches(ctx context.Context, itemRef core.ItemRef, source string, ...) error
- func (s *Service) SaveNegativeResolution(ctx context.Context, ref core.ItemRef) error
- func (s *Service) SaveResolution(ctx context.Context, ref core.ItemRef, src core.PlayableSource) error
- func (s *Service) Search(ctx context.Context, q SearchQuery) (BrowsePage, error)
- func (s *Service) SetCaller(c Caller)
- func (s *Service) SetHealth(h HealthReporter)
- func (s *Service) StartKindSync(parentCtx context.Context, pluginID, kindKey string, interval time.Duration)
- func (s *Service) StopKindSync(pluginID, kindKey string)
- func (s *Service) StopPluginSync(pluginID string)
- func (s *Service) SyncGeneration() uint64
- func (s *Service) SyncKind(ctx context.Context, pluginID, kindKey string, opts SyncOptions) (SyncResult, error)
- func (s *Service) SyncStateOf(ctx context.Context, pluginID, kindKey string) (SyncState, error)
- func (s *Service) UpsertItems(ctx context.Context, pluginID, kindKey string, items []core.KindItem) (int, error)
- func (s *Service) UpsertKind(ctx context.Context, pluginID string, libraryID core.LibraryID, ...) (KindRecord, error)
- type SyncOptions
- type SyncResult
- type SyncState
- type ValidatedItem
Constants ¶
const ( DefaultMaxPluginItems = 2_000_000 MaxTitleRunes = 500 MaxSummaryRunes = 20_000 MaxFieldRunes = 4_000 MaxArtRoles = plugins.MaxKindArtRoles MaxHintsBytes = plugins.MaxKindHintsBytes MaxFTSTextBytes = 8 << 10 DefaultBrowseLimit = 200 MaxBrowseLimit = 1000 MaxBrowseBodyBytes = 8 << 20 MaxSyncConcurrency = 4 DeleteGuardRatio = 0.5 )
Store and item validation caps (spec §2.4, §3.2, §3.3, §3.5, §10).
const ( ResolutionCacheMaxTTL = 15 * time.Minute ResolutionNegativeCacheTTL = 60 * time.Second )
ResolutionCacheMaxTTL is the maximum positive cache window for a resolved PlayableSource (spec §4.4: min(expiresAt, now+15m)).
const ( DefaultWalkPageLimit = 200 DefaultChangesLimit = 500 MaxUpsertBatch = plugins.MaxKindUpsertItems )
const DefaultMatchThreshold = 0.85
DefaultMatchThreshold is the default confidence score at or above which an automatic match is accepted as chosen when no manual pin exists (spec §7.2).
Variables ¶
var ( // ErrMaxItemsExceeded is returned when an upsert would push a plugin // past its total item cap across kinds (spec §2.4). ErrMaxItemsExceeded = fmt.Errorf("%w: plugin item cap exceeded", plugins.ErrQuotaExceeded) // ErrInvalidItem is returned when an item fails envelope or field validation. ErrInvalidItem = errors.New("pluginitems: invalid item") // ErrDeleteGuard is returned when a sync walk or changes batch would // delete more than 50% of a library's items (spec §3.5). ErrDeleteGuard = errors.New("pluginitems: sync aborted by 50% delete guard") )
Functions ¶
func FoldSortTitle ¶
FoldSortTitle normalizes an explicit sort title to lowercase with Latin diacritics and ligatures folded to their base ASCII letters.
func Project ¶
func Project(item core.KindItem, decl plugins.MediaKindMeta, present core.Presentations) core.Title
Project maps a plugin KindItem into a core.Title for legacy Plex/Jellyfin/Emby frontends per §6.3.
Mapping rules:
- Title.Key = item.Key (a PluginKey(seq))
- Title.Type:
- Single-level playable kind -> TypeMovie
- 2-level hierarchy (e.g. channel -> video) -> root = TypeShow, leaf = TypeEpisode
- 3-level hierarchy (e.g. series -> season -> episode) -> TypeShow / TypeSeason / TypeEpisode
- Title.Title = item.Title
- Title.SortTitle = SortTitle(item.Title) or FoldSortTitle(item.SortTitle)
- Title.Summary = item.Summary + formatted declared non-empty fields at the end
- Title.Year = item.Year
- Title.Released = time.Unix(item.Released, 0)
- Title.Runtime = duration field (e.g. "duration" / "runtime") when present
- Title.Genres = item.Fields["genre"/"genres"/"tags"] + facet fields
- Title.PosterPath = art["poster"] (falling back to art["thumb"])
- Title.BackdropPath = art["backdrop"]
- Title.StillPath = art["thumb"] (for episode-like items)
- Title.LogoPath = art["logo"]
func SortTitle ¶
SortTitle derives the normalized lowercase sort key for a display title, matching catalog.SortTitle rules (spec §3.2): strips invisible format characters, removes a leading English article ("the ", "a ", "an "), and folds Latin accents/diacritics and ligatures to base ASCII letters.
Types ¶
type BrowsePage ¶
type BrowsePage struct {
Items []core.KindItem `json:"items"`
Total int `json:"total"`
NextCursor string `json:"nextCursor,omitempty"`
}
BrowsePage is a keyset page of plugin items.
type BrowseQuery ¶
type BrowseQuery struct {
LibraryID core.LibraryID
ParentRef core.ItemRef
Level string
Sort string // title (default), added, released, year, rating, index
Desc bool // false = default direction for Sort; true = reverse
Facets map[string][]string // field key -> required values (OR within key, AND across keys)
Cursor string // opaque keyset cursor
Limit int
}
BrowseQuery specifies a keyset-paged browse over a plugin library and level (spec §2.3, §3.4).
type Caller ¶
type Caller interface {
Call(ctx context.Context, pluginID string, req plugins.Request) (plugins.Response, error)
}
Caller invokes one plugin route (satisfied by *plugins.Manager).
type FacetCount ¶
FacetCount is a single facet value and item count.
type HealthReporter ¶
HealthReporter records plugin health issues discovered by sync or item validation (satisfied by *plugins.Manager via RecordFailure, or custom hooks).
type IndexedFieldValue ¶
IndexedFieldValue is one typed EAV row for plugin_item_fields (spec §2.3).
type KindRecord ¶
type KindRecord struct {
PluginID string
KindKey string
LibraryID core.LibraryID
Decl plugins.MediaKindMeta
DeclHash string
SchemaVersion int64
}
KindRecord is one installed (plugin, kind) row in plugin_kinds (spec §2.3).
type MatchRecord ¶
type MatchRecord struct {
ItemRef core.ItemRef `json:"itemRef"`
CandidateRef string `json:"candidateRef"`
Score float64 `json:"score"`
Reasons []string `json:"reasons,omitempty"`
Source string `json:"source"` // "auto" | "manual" | pluginID
Chosen bool `json:"chosen"`
UpdatedAt int64 `json:"updatedAt"`
}
MatchRecord is a persisted cross-kind match row in plugin_matches (spec §2.3, §7).
type Options ¶
type Options struct {
Logger *slog.Logger
Now func() time.Time
Caller Caller
Health HealthReporter
IsLeader func() bool
MaxPluginItems int
// OnAttention is an optional hook notified when a sync guard or
// contract error flags a plugin for owner attention (e.g. 50%-delete
// guard, unknown fields dropped, auth required).
OnAttention func(ctx context.Context, pluginID, reason string)
}
Options configures the pluginitems Service.
type SearchQuery ¶
type SearchQuery struct {
LibraryID core.LibraryID
Query string
Level string
Facets map[string][]string
Limit int
}
SearchQuery scopes a full-text search to a single library (spec §2.3, §3.4).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service manages plugin kinds, items, browse, search, sync, projections, matches and resolutions.
func (*Service) BestMatch ¶
func (s *Service) BestMatch(ctx context.Context, itemRef core.ItemRef, threshold float64) (MatchRecord, bool, error)
BestMatch returns the active match for itemRef: either a chosen/manual pin or the highest-scoring automated match with score >= threshold (spec §7.2).
func (*Service) Browse ¶
func (s *Service) Browse(ctx context.Context, q BrowseQuery) (BrowsePage, error)
Browse returns a keyset-paged slice of items in a library (spec §2.3, §3.4). Unfiltered totals are read from plugin_item_tally in O(1); filtered or child-scoped totals execute a bounded COUNT(*) with the same WHERE clauses.
func (*Service) Children ¶
func (s *Service) Children(ctx context.Context, parentRef core.ItemRef, cursor string, limit int) (BrowsePage, error)
Children returns the direct children of parentRef ordered by (idx ASC, sort_title ASC, item_ref ASC).
func (*Service) ClearManualMatch ¶
ClearManualMatch removes manual pin rows for itemRef.
func (*Service) DeleteItems ¶
func (s *Service) DeleteItems(ctx context.Context, pluginID, kindKey string, externalIDs []string) error
DeleteItems deletes the named externalIDs (and any descendant items) from (pluginID, kindKey), maintaining tallies, FTS, matches and resolutions.
func (*Service) Facets ¶
func (s *Service) Facets(ctx context.Context, libraryID core.LibraryID, level, fieldKey string, limit int) ([]FacetCount, error)
Facets returns the distinct values and counts for a declared facetable field within a library (optionally scoped to a level).
func (*Service) Generation ¶
Generation returns the monotonic sync generation across all plugin kinds (spec §3.5, §6.4: feeds frontend.PageFreshness.KindSync).
func (*Service) GetByExternal ¶
func (s *Service) GetByExternal(ctx context.Context, pluginID, kindKey, externalID string) (core.KindItem, core.LibraryID, error)
GetByExternal returns one stored KindItem and its LibraryID by (pluginID, kindKey, externalID).
func (*Service) GetByKey ¶
func (s *Service) GetByKey(ctx context.Context, key core.RatingKey) (core.KindItem, core.LibraryID, error)
GetByKey returns one stored KindItem and its LibraryID by its PluginKey RatingKey.
func (*Service) GetByRef ¶
func (s *Service) GetByRef(ctx context.Context, ref core.ItemRef) (core.KindItem, core.LibraryID, error)
GetByRef returns one stored KindItem and its LibraryID by ItemRef.
func (*Service) GetKindByLibrary ¶
func (s *Service) GetKindByLibrary(ctx context.Context, libraryID core.LibraryID) (KindRecord, error)
GetKindByLibrary returns the installed KindRecord for libraryID.
func (*Service) GetResolution ¶
func (s *Service) GetResolution(ctx context.Context, ref core.ItemRef) (core.PlayableSource, bool, error)
GetResolution looks up a non-expired cached resolution for ref (spec §4.4). If a negative cache entry is active, it returns (zero, true, core.ErrNotAvailable).
func (*Service) InvalidateResolution ¶
InvalidateResolution deletes the cached resolution for ref, e.g. on a mid-stream upstream failure (spec §4.4).
func (*Service) KindBackends ¶
func (s *Service) KindBackends() plugins.KindBackends
KindBackends returns a plugins.KindBackends struct with KindUpsert, KindDelete and KindItems wired to this store (spec §3.6, §12 slice C).
func (*Service) KindDelete ¶
func (s *Service) KindDelete(ctx context.Context, pluginID, kindKey string, externalIDs []string) error
KindDelete implements the store side of plugins.KindBackends.KindDelete (spec §3.6).
func (*Service) KindItems ¶
func (s *Service) KindItems(ctx context.Context, pluginID, kindKey, cursor string, limit int) (core.KindPage, error)
KindItems implements the store side of plugins.KindBackends.KindItems (spec §3.6): reads the plugin's own library back, keyset-paged across all levels by external_id.
func (*Service) KindUpsert ¶
func (s *Service) KindUpsert(ctx context.Context, pluginID, kindKey string, items []core.KindItem) (int, error)
KindUpsert implements the store side of plugins.KindBackends.KindUpsert (spec §3.6): validates and upserts items into the kind's library, updates tallies and FTS, and bumps the sync generation.
func (*Service) LibraryTally ¶
func (s *Service) LibraryTally(ctx context.Context, libraryID core.LibraryID) (map[string]int, error)
LibraryTally returns the maintained per-level item counts for libraryID from plugin_item_tally (spec §2.3, §3.4).
func (*Service) ListKinds ¶
ListKinds returns all installed kinds for pluginID (or all plugins when pluginID == "").
func (*Service) ListMatches ¶
ListMatches returns all stored match candidates for itemRef, ordered with chosen / manual pins first, then descending score.
func (*Service) MatchItem ¶
func (s *Service) MatchItem(ctx context.Context, caller Caller, matcherPluginID, kindKey string, item core.KindItem, candidates []core.SourceCandidate, settings map[string]any, threshold float64) ([]core.KindMatchScore, error)
MatchItem invokes a matcher plugin's POST /ether/v1/kinds/{kind}/match route for an item against candidate releases and persists the resulting scores in plugin_matches (spec §7).
func (*Service) PinMatch ¶
func (s *Service) PinMatch(ctx context.Context, itemRef core.ItemRef, candidateRef string, reasons []string) error
PinMatch sets a manual match override (source='manual', chosen=1, score=1.0) for itemRef and clears chosen on any other candidates for itemRef (spec §7.2).
func (*Service) PurgeKind ¶
PurgeKind removes all items, fields, art, tallies, matches, resolutions, sync state and kind registration for one (pluginID, kindKey) (spec §2.5).
func (*Service) PurgePlugin ¶
PurgePlugin is the idempotent removal sequence for all kinds owned by pluginID (spec §2.5): stops sync goroutines, deletes watch-state rows for the plugin's RatingKeys, and purges items, fields, art, FTS, tallies, matches, resolutions, sync state and plugin_kinds rows.
func (*Service) SaveMatches ¶
func (s *Service) SaveMatches(ctx context.Context, itemRef core.ItemRef, source string, scores []core.KindMatchScore, threshold float64) error
SaveMatches persists automated match scores for itemRef from source (e.g. "auto" or matcherPluginID). Rows with source='manual' or a manual pin on itemRef are never overwritten by automated matcher runs (spec §7.2).
func (*Service) SaveNegativeResolution ¶
SaveNegativeResolution caches a not_available resolution result for 60s so a dead kind cannot stampede the plugin (spec §4.4).
func (*Service) SaveResolution ¶
func (s *Service) SaveResolution(ctx context.Context, ref core.ItemRef, src core.PlayableSource) error
SaveResolution caches a successful PlayableSource for ref until min(src.ExpiresAt, now+15m) (spec §4.4).
func (*Service) Search ¶
func (s *Service) Search(ctx context.Context, q SearchQuery) (BrowsePage, error)
Search executes a library-scoped full-text search over plugin items (SQLite FTS5 or PostgreSQL tsvector, spec §2.3, §3.4, §10).
func (*Service) SetHealth ¶
func (s *Service) SetHealth(h HealthReporter)
SetHealth sets or replaces the health reporter after construction.
func (*Service) StartKindSync ¶
func (s *Service) StartKindSync(parentCtx context.Context, pluginID, kindKey string, interval time.Duration)
StartKindSync starts (or restarts) a background pull-sync worker for a kind.
func (*Service) StopKindSync ¶
StopKindSync stops the background sync worker for (pluginID, kindKey) and waits for any in-flight pass to exit.
func (*Service) StopPluginSync ¶
StopPluginSync stops all background sync workers belonging to pluginID.
func (*Service) SyncGeneration ¶
SyncGeneration is an alias for Generation (feeds PageFreshness.KindSync).
func (*Service) SyncKind ¶
func (s *Service) SyncKind(ctx context.Context, pluginID, kindKey string, opts SyncOptions) (SyncResult, error)
SyncKind executes a pull sync for a registered plugin kind (spec §3.5, §4.4).
Sync algorithm:
- Initial sync (when cursor is empty and the store has 0 items for the kind): walks /ether/v1/kinds/{kind}/browse from the root level (parentId="") with limit=200, then breadth-first into child levels with bounded concurrency (4).
- Delta sync (when cursor != "" and storedCount > 0 and !opts.FullWalk): calls POST /ether/v1/kinds/{kind}/changes with {"cursor": cursor, "limit": 500}. If the plugin returns 404/501 (changes not implemented), falls back to re-walk diff. Otherwise chains while complete == false, and records nextCursor in plugin_sync_state.
- Fallback re-walk (when changes is not supported or FullWalk is requested on an already-populated kind): re-walks /browse, diffs by etag (skipping unchanged items), resets missed_walks=0 on seen items, increments missed_walks on unseen items, and deletes items unseen in 2 consecutive complete walks.
- Guardrail: if >50% of existing items in a library would be deleted in a single pass (and the library has at least 2 items) and !opts.Force, the delete is aborted, plugin_sync_state records the error, OnAttention/Health is notified, and ErrDeleteGuard is returned.
func (*Service) SyncStateOf ¶
SyncStateOf returns the current SyncState row for (pluginID, kindKey).
func (*Service) UpsertItems ¶
func (s *Service) UpsertItems(ctx context.Context, pluginID, kindKey string, items []core.KindItem) (int, error)
UpsertItems validates and writes a batch of KindItems for (pluginID, kindKey) in one transaction, maintaining tallies, typed EAV fields, art and FTS.
func (*Service) UpsertKind ¶
func (s *Service) UpsertKind(ctx context.Context, pluginID string, libraryID core.LibraryID, decl plugins.MediaKindMeta, replaceContents bool) (KindRecord, error)
UpsertKind installs or upgrades one (plugin, kind) declaration in plugin_kinds (spec §2.3, §2.5). On upgrade, decl_hash is compared against the stored declaration: additive changes bump schema_version; non-additive changes are refused with a ValidationError unless replaceContents is true, in which case existing items for that kind are purged and sync state reset.
type SyncOptions ¶
type SyncOptions struct {
Force bool // bypass the 50%-delete guard (admin override)
FullWalk bool // force a full browse walk even if a change cursor exists
Concurrency int // max concurrent child browse requests (default MaxSyncConcurrency = 4)
Caller Caller // optional override for Service's configured Caller
}
SyncOptions controls a single SyncKind invocation.
type SyncResult ¶
type SyncResult struct {
Upserted int
Deleted int
Skipped int
NextCursor string
RetryAfter time.Duration
}
SyncResult reports the outcome of a SyncKind run.
type SyncState ¶
type SyncState struct {
PluginID string `json:"pluginId"`
KindKey string `json:"kindKey"`
Cursor string `json:"cursor,omitempty"`
Generation uint64 `json:"generation"`
LastStarted time.Time `json:"lastStarted,omitzero"`
LastOK time.Time `json:"lastOk,omitzero"`
LastError string `json:"lastError,omitempty"`
Items int64 `json:"items"`
}
SyncState is the persisted sync status for one (plugin, kind) (spec §2.3).
type ValidatedItem ¶
type ValidatedItem struct {
Item core.KindItem
IndexedFields []IndexedFieldValue
SearchFields string
DroppedFields int
}
ValidatedItem holds a cleaned KindItem plus its extracted indexed field rows and FTS search text.
func ValidateItem ¶
func ValidateItem(decl plugins.MediaKindMeta, it core.KindItem, parentLevel string) (ValidatedItem, error)
ValidateItem validates one KindItem against its MediaKindMeta declaration (spec §3.3): level and optional parentLevel, field types and caps, string lengths, art roles and URLs, hints JSON object cap, and SortTitle derivation. Unknown field keys are dropped and counted in DroppedFields; wrong field types or violated caps reject the item with an error wrapping ErrInvalidItem.