Documentation
¶
Overview ¶
Package newznab implements core.Indexer against Newznab-compatible Usenet indexers, with per-indexer daily budgets and a result cache.
Index ¶
- Constants
- func DiscoverProwlarr(ctx context.Context, cfg config.Prowlarr, opts Options) ([]config.Indexer, error)
- func InfoHashFromMagnet(magnet string) string
- func MagnetFor(infoHash, name string, trackers []string) string
- func NormalizeInfoHash(s string) string
- func ProwlarrIDPrefix(cfg config.Prowlarr) string
- func Test(ctx context.Context, cfg config.Indexer, policy config.ClientIdentity) error
- func TestWithOptions(ctx context.Context, cfg config.Indexer, opts Options) error
- type APIError
- type Backend
- type Caps
- type Client
- func (c *Client) Caps(ctx context.Context) (*Caps, error)
- func (c *Client) Config() config.Indexer
- func (c *Client) Exhausted(ctx context.Context) bool
- func (c *Client) FetchNZB(ctx context.Context, r core.Release) ([]byte, error)
- func (c *Client) Name() string
- func (c *Client) Remaining(ctx context.Context) (Remaining, error)
- func (c *Client) ResolveTorrent(ctx context.Context, r core.Release) (core.Release, error)
- func (c *Client) Search(ctx context.Context, req core.SearchRequest) ([]core.Release, error)
- func (c *Client) SearchRescue(ctx context.Context, req core.SearchRequest) ([]core.Release, error)
- func (c *Client) SearchWith(ctx context.Context, req core.SearchRequest, arrived func([]core.Release)) ([]core.Release, error)
- func (c *Client) SetCacheTTL(d time.Duration)
- func (c *Client) SetClientIdentity(p config.ClientIdentity)
- func (c *Client) UpdateConfig(cfg config.Indexer)
- type Event
- type Manager
- func (m *Manager) ApplyConfig(cfg config.Config)
- func (m *Manager) CachedReleases(ctx context.Context) ([]core.Release, error)
- func (m *Manager) Clients() []*Client
- func (m *Manager) FeedSources() []core.ReleaseFeed
- func (m *Manager) FetchNZB(ctx context.Context, r core.Release) ([]byte, error)
- func (m *Manager) HasTorrentIndexer() bool
- func (m *Manager) Indexers() []core.Indexer
- func (m *Manager) MissingSources() error
- func (m *Manager) Name() string
- func (m *Manager) Prune(ctx context.Context) (int64, error)
- func (m *Manager) PurgeLegacyCache(ctx context.Context) (int64, error)
- func (m *Manager) Recent(ctx context.Context, id string, offset, limit int) ([]core.Release, error)
- func (m *Manager) Reload(cfgs []config.Indexer)
- func (m *Manager) Remaining(ctx context.Context) ([]Remaining, error)
- func (m *Manager) ResolveTorrent(ctx context.Context, r core.Release) (core.Release, error)
- func (m *Manager) Search(ctx context.Context, req core.SearchRequest) ([]core.Release, error)
- func (m *Manager) SearchRescue(ctx context.Context, req core.SearchRequest) ([]core.Release, error)
- func (m *Manager) SearchRescueWith(ctx context.Context, req core.SearchRequest, arrived func([]core.Release)) ([]core.Release, error)
- func (m *Manager) SearchTimings() []SearchTiming
- func (m *Manager) SearchWith(ctx context.Context, req core.SearchRequest, arrived func([]core.Release)) ([]core.Release, error)
- func (m *Manager) Subscribe(fn func(Event)) func()
- type Options
- type ProwlarrDiscovery
- type ProwlarrTag
- type RateLimitError
- type Remaining
- type SearchTiming
- type TorrentMeta
Constants ¶
const MaxRateLimitPause = maxRateLimitPause
MaxRateLimitPause is the shared cap on a service-requested pause, exported so HTTP handlers cap Retry-After exactly like this client instead of duplicating a literal bound.
Variables ¶
This section is empty.
Functions ¶
func DiscoverProwlarr ¶
func DiscoverProwlarr(ctx context.Context, cfg config.Prowlarr, opts Options) ([]config.Indexer, error)
DiscoverProwlarr imports enabled Usenet (Newznab) and torrent (Torznab) indexers; torrent indexers get Protocol "torrent" and stream through a debrid provider. Each receives a stable source-scoped ID and its own Ether hit/grab budget, cache and reputation. Prowlarr's /{id}/api alias is intentionally used so ordinary Newznab URL construction and all existing budget enforcement apply unchanged.
func InfoHashFromMagnet ¶
InfoHashFromMagnet extracts the v1 info hash from a magnet URI's xt=urn:btih: parameter; "" when absent.
func NormalizeInfoHash ¶
NormalizeInfoHash returns a lower-case 40-character hex BitTorrent v1 info hash, converting a 32-character base32 hash; "" when s is neither.
func ProwlarrIDPrefix ¶ added in v0.2.34
ProwlarrIDPrefix returns the ID prefix of indexers imported from cfg's Prowlarr instance ("prowlarr-<scope>-"), or "" when the URL is not a usable base URL (same validation as discovery).
Types ¶
type APIError ¶
type APIError struct {
Code int
Description string
Budget bool // daily limit / rate limited; unwraps to core.ErrBudgetExhausted
}
APIError is a typed Newznab error document (<error code=".." description=".."/>) or an HTTP-level failure translated into the same shape.
type Backend ¶
type Backend interface {
Search(ctx context.Context, req core.SearchRequest, reserve func(context.Context, string) error) ([]core.Release, error)
FetchNZB(ctx context.Context, r core.Release, reserve func(context.Context, string) error) ([]byte, error)
ResolveTorrent(ctx context.Context, r core.Release, reserve func(context.Context, string) error) (core.Release, error)
}
Backend searches a site that is not a Newznab API, such as a built-in site run from an owner-installed definition. The Client keeps budgets, the result cache and events around it: a backend calls reserve with "hit" or "grab" before each request that counts against a daily budget.
type Caps ¶
type Caps struct {
ServerTitle string `json:"serverTitle,omitempty"`
ServerType string `json:"serverType,omitempty"`
LimitDefault int `json:"limitDefault,omitempty"`
LimitMax int `json:"limitMax,omitempty"`
SearchAvail bool `json:"searchAvailable"`
SearchParams []string `json:"searchParams,omitempty"`
MovieAvail bool `json:"movieSearchAvailable"`
MovieParams []string `json:"movieParams,omitempty"`
TVAvail bool `json:"tvSearchAvailable"`
TVParams []string `json:"tvParams,omitempty"`
Categories []int `json:"categories,omitempty"`
}
Caps is a parsed Newznab capabilities document.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is one configured Newznab indexer. It implements core.Indexer.
func New ¶
New builds a Client for one indexer. db may be nil (budgets and cache then live in memory).
func (*Client) Caps ¶
Caps returns the indexer's capabilities, cached for 24h per endpoint (URL and API key): an indexer edited to point elsewhere fetches anew, and a response from the old endpoint still in flight is not kept.
func (*Client) FetchNZB ¶
FetchNZB implements core.Indexer. Every call returns a slice of its own, which the caller may keep or modify.
func (*Client) ResolveTorrent ¶
ResolveTorrent fills a torrent release's InfoHash and MagnetURL by downloading its .torrent link (counting as a grab) when the indexer only supplied a download URL. Releases that already carry both are returned unchanged without a request.
func (*Client) SearchRescue ¶ added in v0.2.4
SearchRescue runs broader keyword, alias, OriginalTitle, miniseries and season-pack queries when the primary Search results had no playable candidate. Results are cached under a separate rescue key and respect the indexer's daily budget and rate-limit state.
func (*Client) SearchWith ¶ added in v0.2.9
func (c *Client) SearchWith(ctx context.Context, req core.SearchRequest, arrived func([]core.Release)) ([]core.Release, error)
SearchWith delivers complete items while the HTTP body is still arriving. Cache/backend answers use the same callback; each ID is emitted once even when fallback queries overlap. The cache only receives complete responses.
func (*Client) SetCacheTTL ¶
SetCacheTTL changes the result-cache TTL (called on config reload). A negative value disables the cache.
func (*Client) SetClientIdentity ¶ added in v0.3.2
func (c *Client) SetClientIdentity(p config.ClientIdentity)
SetClientIdentity resolves the owner's policy into the four identities this indexer presents, honoring the indexer's own override.
func (*Client) UpdateConfig ¶
UpdateConfig swaps the indexer config (called on config reload).
type Event ¶
type Event struct {
Indexer string
Kind string
Success bool
BudgetExhausted bool
Results int
Duration time.Duration
AverageAge time.Duration
At time.Time
}
Event records one indexer's observed operation; no URLs or keys are exposed.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager holds all configured indexers and spreads searches across them within their budgets. It implements core.Indexer so the resolver can treat the fleet as one indexer; FetchNZB routes by the release's Indexer name.
func NewManager ¶
NewManager builds a Manager from indexer configs.
func (*Manager) ApplyConfig ¶
ApplyConfig reloads indexers and the search-cache TTL from a full config snapshot; subscribe it to config.Store changes.
func (*Manager) CachedReleases ¶ added in v0.2.9
CachedReleases reuses a bounded set of already-paid-for search/feed results. It performs no requests, capability discovery, cache refresh, or downloads.
func (*Manager) FeedSources ¶ added in v0.2.8
func (m *Manager) FeedSources() []core.ReleaseFeed
FeedSources reports capabilities without making a request. Built-in sites remain searchable but are not treated as paginated feeds without that contract.
func (*Manager) HasTorrentIndexer ¶ added in v0.2.20
HasTorrentIndexer reports whether an enabled indexer returns torrents (Torznab, a Stremio addon or a torrent built-in site).
func (*Manager) MissingSources ¶ added in v0.2.20
MissingSources reports core.ErrNoSources when no indexer is enabled: a search would return nothing whatever the title.
func (*Manager) Prune ¶
Prune deletes budget rows older than budgetRetentionDays and any expired search-cache rows (normally removed on write, but an idle indexer never writes). It is meant to be called periodically.
func (*Manager) PurgeLegacyCache ¶ added in v0.2.4
PurgeLegacyCache deletes search-cache rows written before cached links were tokenized (they contain indexer API keys) and returns how many it removed. Call it once no node running an older release writes the cache.
func (*Manager) Recent ¶ added in v0.2.8
Recent fetches one page, charging the same account-wide limits as Search. It never invokes title searches, capability discovery, or implicit retries.
func (*Manager) Reload ¶
Reload replaces the indexer set (call on config change). Existing clients with an unchanged ID keep their in-memory caches.
func (*Manager) ResolveTorrent ¶
ResolveTorrent implements the debrid indexer's torrent resolution by routing to the release's originating indexer.
func (*Manager) Search ¶
Search implements core.Indexer: it queries enabled indexers in parallel (up to MaxParallel), letting each client check its cache before budgets, merges by priority order and de-duplicates by normalised name + size.
func (*Manager) SearchRescue ¶ added in v0.2.4
SearchRescue runs a second-pass rescue fan-out across all enabled indexers when the primary Search results had no playable candidate.
func (*Manager) SearchRescueWith ¶ added in v0.2.4
func (m *Manager) SearchRescueWith(ctx context.Context, req core.SearchRequest, arrived func([]core.Release)) ([]core.Release, error)
SearchRescueWith is SearchRescue with per-indexer early arrival callbacks.
func (*Manager) SearchTimings ¶
func (m *Manager) SearchTimings() []SearchTiming
SearchTimings returns a copy of the most recent per-indexer search timings.
func (*Manager) SearchWith ¶ added in v0.2.4
func (m *Manager) SearchWith(ctx context.Context, req core.SearchRequest, arrived func([]core.Release)) ([]core.Release, error)
SearchWith is Search, also handing each indexer's results to arrived as soon as that indexer answers (concurrently, from its own goroutine), so callers can start follow-up work, such as debrid cache checks, before the slowest indexer is done.
type Options ¶
type Options struct {
HTTPClient *http.Client
Logger *slog.Logger
SearchCacheTTL time.Duration // <0 disables cache, 0 = default 6h
NZBDir string // directory for downloaded NZBs; "" keeps them in memory only
MaxNZBBytes int64
Now func() time.Time // injected clock (tests)
MaxParallel int // Manager fan-out; 0 dispatches every configured indexer
// ClientIdentity is the owner's policy for the client Ether presents to
// indexers; the zero value keeps the Ether identity.
ClientIdentity config.ClientIdentity
// Backend builds the search backend for an indexer whose Kind is not
// Newznab (built-in sites, internal/cardigann). Nil disables them.
Backend func(config.Indexer) (Backend, error)
}
Options tunes a Client or Manager. Zero values get sensible defaults.
type ProwlarrDiscovery ¶ added in v0.2.34
type ProwlarrDiscovery struct {
Indexers []config.Indexer `json:"indexers"`
SkippedUnknownProtocol int `json:"skippedUnknownProtocol"`
Warnings []string `json:"warnings,omitempty"`
// PresentIDs are the scoped IDs of every enabled indexer the source
// reported, including ones filtered out below (unknown protocol or,
// when tags are set, a tag the owner did not choose). It is diagnostic
// only. Tag sync decides what to stop using from Indexers, never from
// this list, and an empty tag list stays additive.
PresentIDs []string `json:"presentIds,omitempty"`
}
ProwlarrDiscovery is the result of a successful Prowlarr discovery: the importable indexers plus what was left out and why, so a preview can report skips instead of dropping them silently (same pattern as the quality-profile import's Skipped/Warnings).
func DiscoverProwlarrReport ¶ added in v0.2.34
func DiscoverProwlarrReport(ctx context.Context, cfg config.Prowlarr, opts Options) (ProwlarrDiscovery, error)
DiscoverProwlarrReport is DiscoverProwlarr plus a count (and warning) for enabled indexers skipped because Prowlarr reported a protocol Ether cannot use.
It only reads Prowlarr's indexer list (GET /api/v1/indexer). It does not search or download, so indexer hit and grab quotas are not spent. cfg.Tags empty keeps every enabled Usenet and torrent indexer. With tags set, an indexer is kept only when its tags field shares one of those ids.
type ProwlarrTag ¶ added in v0.3.0
ProwlarrTag is one tag from Prowlarr's /api/v1/tag.
func ListProwlarrTags ¶ added in v0.3.0
func ListProwlarrTags(ctx context.Context, cfg config.Prowlarr, opts Options) ([]ProwlarrTag, error)
ListProwlarrTags reads Prowlarr's tag catalogue (GET /api/v1/tag) so the owner can pick tags. It does not search or download.
type RateLimitError ¶ added in v0.2.17
RateLimitError is a backend's rate-limit refusal (HTTP 429). The Client pauses the indexer until RetryAfter, a Retry-After header value ("" for the default pause), exactly as for a Newznab 429.
func (*RateLimitError) Error ¶ added in v0.2.17
func (e *RateLimitError) Error() string
func (*RateLimitError) Unwrap ¶ added in v0.2.17
func (e *RateLimitError) Unwrap() []error
type Remaining ¶
type Remaining struct {
Indexer string `json:"indexer"`
Day string `json:"day"`
HitsUsed int `json:"hitsUsed"`
HitsLimit int `json:"hitsLimit"`
HitsRemaining int `json:"hitsRemaining"`
GrabsUsed int `json:"grabsUsed"`
GrabsLimit int `json:"grabsLimit"`
GrabsRemaining int `json:"grabsRemaining"`
RetryAt int64 `json:"retryAt,omitempty"` // service-reported throttling; automatic retry (Unix seconds)
// Enabled and Torrent let the dashboard warn when nothing can be
// searched, or when debrid has no torrent indexer to search.
Enabled bool `json:"enabled"`
Torrent bool `json:"torrent"` // Torznab, a Stremio addon or a torrent built-in site
}
Remaining reports one indexer's budget state for today (UTC). A remaining value of -1 means there is no personal cap; service limits still apply.
type SearchTiming ¶
SearchTiming records the most recent aggregate search's per-indexer cost.
type TorrentMeta ¶
TorrentMeta is what a .torrent file contributes to a release.
func ParseTorrent ¶
func ParseTorrent(data []byte) (TorrentMeta, error)
ParseTorrent computes the info hash (SHA-1 of the bencoded info dictionary) and reads the name and trackers of a .torrent file.