pluginitems

package
v0.3.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2026 License: GPL-3.0 Imports: 22 Imported by: 0

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

View Source
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).

View Source
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)).

View Source
const (
	DefaultWalkPageLimit = 200
	DefaultChangesLimit  = 500
	MaxUpsertBatch       = plugins.MaxKindUpsertItems
)
View Source
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

View Source
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

func FoldSortTitle(s string) string

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

func SortTitle(s string) string

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

type FacetCount struct {
	Value string `json:"value"`
	Count int    `json:"count"`
}

FacetCount is a single facet value and item count.

type HealthReporter

type HealthReporter interface {
	RecordFailure(ctx context.Context, pluginID, reason string)
}

HealthReporter records plugin health issues discovered by sync or item validation (satisfied by *plugins.Manager via RecordFailure, or custom hooks).

type IndexedFieldValue

type IndexedFieldValue struct {
	Field string
	Text  string
	Int   int64
	Real  float64
}

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 New

func New(ctx context.Context, db *sql.DB, o Options) (*Service, error)

New opens the pluginitems Service over a migrated database.

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

func (s *Service) ClearManualMatch(ctx context.Context, itemRef core.ItemRef) error

ClearManualMatch removes manual pin rows for itemRef.

func (*Service) Close

func (s *Service) Close()

Close stops all background sync workers.

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

func (s *Service) Generation() uint64

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) GetKind

func (s *Service) GetKind(ctx context.Context, pluginID, kindKey string) (KindRecord, error)

GetKind returns the installed KindRecord for (pluginID, kindKey).

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

func (s *Service) InvalidateResolution(ctx context.Context, ref core.ItemRef) error

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

func (s *Service) ListKinds(ctx context.Context, pluginID string) ([]KindRecord, error)

ListKinds returns all installed kinds for pluginID (or all plugins when pluginID == "").

func (*Service) ListMatches

func (s *Service) ListMatches(ctx context.Context, itemRef core.ItemRef) ([]MatchRecord, error)

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

func (s *Service) PurgeKind(ctx context.Context, pluginID, kindKey string) error

PurgeKind removes all items, fields, art, tallies, matches, resolutions, sync state and kind registration for one (pluginID, kindKey) (spec §2.5).

func (*Service) PurgePlugin

func (s *Service) PurgePlugin(ctx context.Context, pluginID string) error

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

func (s *Service) SaveNegativeResolution(ctx context.Context, ref core.ItemRef) error

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) SetCaller

func (s *Service) SetCaller(c Caller)

SetCaller sets or replaces the plugin route caller after construction.

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

func (s *Service) StopKindSync(pluginID, kindKey string)

StopKindSync stops the background sync worker for (pluginID, kindKey) and waits for any in-flight pass to exit.

func (*Service) StopPluginSync

func (s *Service) StopPluginSync(pluginID string)

StopPluginSync stops all background sync workers belonging to pluginID.

func (*Service) SyncGeneration

func (s *Service) SyncGeneration() uint64

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:

  1. 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).
  2. 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.
  3. 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.
  4. 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

func (s *Service) SyncStateOf(ctx context.Context, pluginID, kindKey string) (SyncState, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL