availability

package
v0.3.4 Latest Latest
Warning

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

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

README

Availability monitoring

Ether observes recent releases and reuses successful resolver health checks. It never removes catalogue/search results because evidence is missing. Movie dates and episode air dates still use the catalogue's existing release eligibility; quality, CAM acceptance, identities, blocklists and episode targeting use the resolver's existing policies rather than a second rules engine.

Owner controls

Sources → Availability provides monitoring, selective validation, participating feeds, debrid participation, preference for checked recommendations and optional newly available shelves. Monitoring and validation start disabled. Presentation preferences start enabled and can benefit from ordinary playback observations.

Default daily background allowances (UTC, shared across nodes):

  • Per indexer: 10% (Gentle 5%; Unlimited up to 100 − reservePercent) of its daily API limit: the owner's, else the one it reports (Newznab apilimits), else 1,000 (the strictest private tier), so 100 feed and search requests by default. The day's remaining head polls are reserved, so gap pages and title searches cannot starve the feeds; a feed whose allowance cannot fit 30-minute polls in half of it is polled less often. Requests to one indexer are at least Deps.Pace (3 s) apart.
  • In total, the sum of those (or the owner's custom total).
  • 100 validation API requests, 20 candidate checks and 20 descriptor grabs.
  • 512 MiB of conservative data reservations. NNTP BODY reservations use the provider's maximum article size, so actual transferred bytes can be much lower.

An allowance of zero disables that work. Foreground requests still have their normal account limits. Every feed page, provider API request and source read goes through admission before network work. Ordinary playback does not use the background allowance. Shared worker contexts retain the admission hook.

Discovery and evidence

Newznab/Torznab recent-release pages use an empty t=search request, following the incremental model documented by Radarr. Built-in sites list their latest page (a keywordless search, as Prowlarr does); Stremio addons and plugin sources are search-only. One page describes up to 100 releases. Head polls default to every 30 minutes. A sync reads from the head back to the previous head: one page usually, otherwise only the pages the gap needs (at most MaxPages per sync), resuming an unfinished gap every minute while its allowance has room. A first sync starts at the head; catch-up (optional) also walks recent history. Failed requests do not advance the cursor.

Feeds detect; title searches are events. A movie in the window is searched by id once when it enters the window, once when its digital or physical date arrives, and once after its found release proves unplayable (plus finishing a search an outage or allowance cut short). A search asks one source at a time in priority order and stops at the first acceptable release. Nothing is searched on a timer.

Matching scans local title identities and resolves episodes locally; it does not call TMDB for every feed item. Conflicting identifiers and ambiguous title matches are rejected. A season pack only contributes known, already-aired episodes, and its health check is targeted to the chosen episode using the existing resolver.

The queue prioritizes popular titles, checks one candidate per title per pass, and defers extra candidates when the title already has fresh successful evidence. It never searches the entire catalogue in the background. Playback attempts can reuse discovered descriptors before normal search, with a five-second shortlist budget. Viewer/device policy is always reapplied on that path.

Debrid cache lookups are batched and capability-dependent. Background validation can inspect existing account items and validate a bounded HTTPS range; it never starts an uncached download or adds torrents. Unsupported cache APIs leave availability unknown. A debrid account by itself is not a global discovery feed; recent torrent discovery requires a configured Torznab source. Usenet, debrid, both, and no playback provider are supported configurations.

Successful evidence is source-, title-, policy- and configuration-specific. It expires after six hours. Configuration changes invalidate old positives. A source failure never creates a title-wide negative. Timeout, exhausted allowance and unsupported checks remain unknown. Normal resolver health-cache hits do not renew evidence timestamps. Sampled health is useful evidence, not a guarantee of all bytes or all clients.

Discovery filtering captures one isolated settings value, policy fingerprint, source list and clock reading per operation. Nested episode and retained-release reads, pagination exclusions, recommendation priority and newly available shelves reuse those inputs. The next operation sees settings and source-generation changes; existing observation invalidation still prevents stale memo writes. config.Store.Get retains its deep-copy isolation. Background admission remains live so owner pauses and allowance changes take effect during background work. Resolver candidate checks still apply their existing policy; this snapshot does not bypass access, identity or quality checks or promise to remove all Home delay.

Library pages and section hubs never judge feed releases inside a request. They read the last computed hidden set for the evidence window; a set older than fifteen seconds, or older than a new observation or invalidation, is served while one background refresh recomputes it. Before the first set exists every title in the window stays hidden, as it would until evidence arrives. Each refresh asks the resolver only about (title, release) pairs without a durable verdict under the current policy key (profile, resolver settings, catalogue language, rule fields): verdicts live in availability_verdicts, bound to the release descriptor and the title facts matching reads, pruned with feed items and capped at 60,000 rows. A refresh stores its verdicts before it publishes its set. A browse request that judges queues its verdicts for one writer instead of writing, and reads count the queue as stored, so no section load judges a release twice. Short-lived gates (blocklist marks, a brief health-failure skip) are reapplied on every read; hosted ranking sessions make every verdict short-lived.

Only fresh checked movies move ahead within popular/trending/recommendation shelves; no items are removed. Explicit chronology and Continue Watching retain their order. Newly available shelves mean first successfully checked by this server, not necessarily a film's worldwide premiere.

Operations

The existing cluster leader runs one scheduler; cursors, observations and atomic usage counters are durable in the shared database. Hot settings are respected by admission. Checks pause while local playback is active. Each cycle is bounded, cancellation propagates, transient outcomes back off, and descriptors expire after 30 days with a hard retention cap of 30,000 observations. Signed release locators are sealed using the config store; owner status never exposes them.

Tests use fake feeds/providers and local HTTP fixtures. They cover admission, account reserve, midnight reset, restart/overlap/gaps, identity conflicts, source/policy invalidation, stale evidence, disabled-provider behavior, diverse candidate selection, resolver reuse, and budget propagation across shared workers.

Documentation

Overview

Package availability discovers releases incrementally and spends a bounded background allowance validating useful candidates. Recent releases require evidence for discovery; older catalogue entries need no background search.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EvidenceWindow added in v0.2.9

func EvidenceWindow(t core.Title, now time.Time) bool

EvidenceWindow is a proof window, not a delay: an eligible observed release unlocks a title immediately. Older catalogue entries do not need a scan.

func WithDiscoveryMemo added in v0.3.0

func WithDiscoveryMemo(ctx context.Context) context.Context

WithDiscoveryMemo shares one availability snapshot for the rest of this request. Home authorizes the same titles many times, and each check was cloning config and feed settings.

Types

type Catalog

type Catalog interface {
	ReleaseMatches(context.Context, []core.Release) (map[string][]core.Title, error)
	CachedTitle(context.Context, core.RatingKey) (*core.Title, error)
	CachedTitles(context.Context, []core.RatingKey) ([]core.Title, error)
}

type Deps

type Deps struct {
	DB         *sql.DB
	Config     func() config.Config
	Feeds      Feeds
	Catalog    Catalog
	Validator  Validator
	Seal       func(string) (string, error)
	Open       func(string) (string, error)
	CacheCheck func(context.Context, []string) (map[string]bool, bool)
	// Search is a budgeted id search across indexers and configured addons.
	Search           func(context.Context, core.SearchRequest) ([]core.Release, error)
	Watchlist        func(context.Context) (map[core.RatingKey]bool, error)
	Active           func() bool
	Now              func() time.Time
	Logger           *slog.Logger
	ConfigGeneration func() uint64
	// Pace is the least time between two background requests to one
	// indexer (zero: no spacing), so feeds, gap pages and title searches
	// never burst at an indexer.
	Pace time.Duration
	// Async runs evidence refreshes on the service's own goroutines, off
	// the request path; Close stops them. False runs each refresh inline,
	// before the request that found its answer stale.
	Async bool
}

type FeedStatus

type FeedStatus struct {
	core.ReleaseFeed
	Selected    bool    `json:"selected"`
	LastAttempt int64   `json:"lastAttempt"`
	LastSync    int64   `json:"lastSync"`
	Gap         bool    `json:"gap"`
	Message     string  `json:"message"`
	Matched     int     `json:"matched"`
	Queued      bool    `json:"queued"`
	Progress    float64 `json:"progress,omitempty"`
	ETASeconds  int64   `json:"etaSeconds,omitempty"`
}

type Feeds

type Feeds interface {
	FeedSources() []core.ReleaseFeed
	Recent(context.Context, string, int, int) ([]core.Release, error)
}

type NewRelease added in v0.3.0

type NewRelease struct {
	Key        string `json:"key"`
	Title      string `json:"title"`
	Year       int    `json:"year,omitempty"`
	Poster     string `json:"poster,omitempty"`
	Theatrical int64  `json:"theatrical,omitempty"`
	Digital    int64  `json:"digital,omitempty"`
	Code       string `json:"code"`
	Source     string `json:"source,omitempty"`
	NextCheck  int64  `json:"nextCheck,omitempty"`
	Reason     string `json:"reason,omitempty"`
}

NewRelease is one movie inside the evidence window, for the owner list.

type Observation

type Observation struct {
	Key           core.RatingKey `json:"key"`
	Title         string         `json:"title"`
	Release       string         `json:"release"`
	Protocol      string         `json:"protocol"`
	State         string         `json:"state"`
	Reason        string         `json:"reason"`
	FirstSeen     int64          `json:"firstSeen"`
	FirstVerified int64          `json:"firstVerified"`
	VerifiedAt    int64          `json:"verifiedAt"`
	NextCheck     int64          `json:"nextCheck"`
}

type Service

type Service struct {
	// contains filtered or unexported fields
}

func New

func New(d Deps) (*Service, error)

func (*Service) Candidates

func (s *Service) Candidates(ctx context.Context, t *core.Title) []core.Release

Candidates serves remembered descriptors locally; playback still applies its exact viewer/device policy and validates the selected source.

func (*Service) Close added in v0.3.3

func (s *Service) Close()

Close stops background evidence refreshes and waits for them.

func (*Service) Cycle

func (s *Service) Cycle(ctx context.Context) error

func (*Service) DigitalDiscovery added in v0.3.4

func (s *Service) DigitalDiscovery(ctx context.Context) (bool, bool)

DigitalDiscovery reports the discovery policy for the current request.

func (*Service) Explain added in v0.3.0

func (s *Service) Explain(ctx context.Context, key core.RatingKey) (TitleEvidence, error)

func (*Service) FilterDiscovery added in v0.2.9

func (s *Service) FilterDiscovery(ctx context.Context, items []core.Title) []core.Title

A short local memo keeps repeated Plex artwork/list requests inexpensive. New observations and invalidations clear it immediately. Other cluster nodes see changes within fifteen seconds, without a provider request.

func (*Service) FilterDiscoveryCurrent added in v0.3.0

func (s *Service) FilterDiscoveryCurrent(ctx context.Context, items []core.Title) []core.Title

FilterDiscoveryCurrent bypasses node-local visibility and resolver memos for one bounded policy preflight batch. It performs no provider lookup.

func (*Service) Handler

func (s *Service) Handler() http.Handler

Handler is mounted only inside the authenticated owner API.

func (*Service) HiddenEpisodes added in v0.3.4

func (s *Service) HiddenEpisodes(ctx context.Context) []core.RatingKey

HiddenEpisodes is the first-day episode evidence decision for SQL paging.

func (*Service) HiddenMovies added in v0.2.9

func (s *Service) HiddenMovies(ctx context.Context) []core.RatingKey

HiddenMovies supplies SQL pagination with exclusions from the recent local mirror only. No per-title provider lookup or historical catalogue walk occurs.

func (*Service) HiddenShows added in v0.2.9

func (s *Service) HiddenShows(ctx context.Context) []core.RatingKey

HiddenShows excludes first-day series before SQL pagination, just as movies.

func (*Service) Hubs

func (s *Service) Hubs(ctx context.Context, limit int) []core.Hub

func (*Service) Invalidate

func (s *Service) Invalidate(ctx context.Context, id string)

Invalidate retracts positive evidence immediately when playback or the owner invalidates a release. The title and its other sources remain discoverable.

func (*Service) NewReleases added in v0.3.0

func (s *Service) NewReleases(ctx context.Context) ([]NewRelease, error)

func (*Service) Observe

func (s *Service) Observe(ctx context.Context, t *core.Title, r core.Release)

Observe reuses an actual successful resolver health check. Optimistic starts and prepared sessions without health evidence do not call this method.

func (*Service) PagingFilter added in v0.3.4

func (s *Service) PagingFilter(ctx context.Context, f core.Filter) (core.Filter, error)

PagingFilter captures the current immutable SQL generations once. On a new installation the first set is computed before it can be used for a page.

func (*Service) Prioritize

func (s *Service) Prioritize(ctx context.Context, items []core.Title) []core.Title

func (*Service) RecordSearch added in v0.2.9

func (s *Service) RecordSearch(ctx context.Context, t *core.Title, rs []core.Release)

RecordSearch retains descriptors from an actual requested search. It does not initiate searches, grab media or claim health. Policy is reevaluated on reads.

func (*Service) Run

func (s *Service) Run(ctx context.Context)

func (*Service) Status

func (s *Service) Status(ctx context.Context) (Status, error)

func (*Service) Summary added in v0.3.0

func (s *Service) Summary(ctx context.Context) (Summary, error)

type Status

type Status struct {
	Mode                  string           `json:"mode"`
	Message               string           `json:"message"`
	Feeds                 []FeedStatus     `json:"feeds"`
	Usage                 map[string]int64 `json:"usage"`
	Counts                map[string]int   `json:"counts"`
	Recent                []Observation    `json:"recent"`
	Day                   string           `json:"day"`
	Ceilings              map[string]int64 `json:"ceilings,omitempty"`
	Effort                string           `json:"effort"`
	EffectiveFeedRequests int              `json:"effectiveFeedRequests"`
	EffectivePollMinutes  int              `json:"effectivePollMinutes"`
	EffectiveMaxPages     int              `json:"effectiveMaxPages"`
	OverAllowance         bool             `json:"overAllowance"`
}

type Summary added in v0.3.0

type Summary struct {
	Ready       int   `json:"ready"`
	Waiting     int   `json:"waiting"`
	LastChecked int64 `json:"lastChecked,omitempty"`
}

Summary is the quiet status line for "When they can play": how many in-window movies are ready, how many are still waiting, and when Ether last looked.

type TitleEvidence added in v0.3.0

type TitleEvidence struct {
	Code      string `json:"code"`
	CheckedAt int64  `json:"checkedAt,omitempty"`
	NextCheck int64  `json:"nextCheck,omitempty"`
	Reason    string `json:"reason,omitempty"`
}

TitleEvidence is why a movie is shown or hidden. Codes are for the owner console; they are not sentences.

type Validator

type Validator interface {
	FeedCandidate(context.Context, *core.Title, core.Release) (core.Release, string, error)
	VerifyFeedCandidate(context.Context, *core.Title, core.Release) error
}

Jump to

Keyboard shortcuts

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