api

package
v0.1.151 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: GPL-2.0, GPL-3.0 Imports: 17 Imported by: 0

Documentation

Overview

Package api defines the internal contracts between subflux components. All cross-component calls go through these interfaces, enabling testability (mock any component) and swappability (e.g. SQLite → PostgreSQL).

This package imports no subflux implementation packages — only stdlib and shared external libraries (cplieger/arrapi, cplieger/auth, cplieger/webhttp). Implementation packages import api, never the reverse.

This file contains consumer contracts: interfaces that consuming code depends on (Store, AuthStore, ConfigProvider, SearchEngine). Implementation/provider contracts live in interfaces_provider.go.

Index

Constants

View Source
const (

	// KeyStatus is the canonical JSON key for operation result status
	// responses (StatusResponse is the typed carrier).
	// Used by server, scanning, and confighandlers packages.
	KeyStatus = "status"

	// ErrMsgSonarrNotConfigured is the canonical error message when Sonarr is nil.
	ErrMsgSonarrNotConfigured = "sonarr not configured"

	// ErrMsgRadarrNotConfigured is the canonical error message when Radarr is nil.
	ErrMsgRadarrNotConfigured = "radarr not configured"

	// HeaderXAPIKey is the canonical HTTP header name for API key authentication.
	// Used by both inbound (subflux API key verification) and outbound (Sonarr/Radarr) requests.
	HeaderXAPIKey = "X-Api-Key" //nolint:gosec // G101 false positive: header name, not a credential
)

Canonical JSON response keys and sentinel error messages shared across every helper below. Extracted to silence goconst warnings and to make a wire-format rename land in one place.

View Source
const (
	VariantStandard Variant = "standard"
	DefaultVariant          = VariantStandard
	VariantHI       Variant = "hi"
	VariantForced   Variant = "forced"

	// VariantAliasSDH is the alternative name for HI subtitles used in external
	// subtitle filenames and provider metadata.
	VariantAliasSDH = "sdh"
	// VariantAliasForeign is the alternative name for forced subtitles used in
	// external subtitle filenames and provider metadata.
	VariantAliasForeign = "foreign"
)

Subtitle variant identifiers. Shared across api, search, server, and cli so the auto and manual download paths agree on a single vocabulary.

View Source
const DefaultDownloadMaxAttempts = 3

DefaultDownloadMaxAttempts is the fallback number of download attempts per search when the config value is zero.

View Source
const DefaultManualProviderTimeout = 30 * time.Second

DefaultManualProviderTimeout is the per-provider search timeout for manual searches (the /api/search path, which also serves the remote CLI's search subcommand).

View Source
const DefaultProviderConcurrency = 4

DefaultProviderConcurrency is the default maximum number of parallel provider searches when the config value is zero.

View Source
const DefaultProviderPriority = 99

DefaultProviderPriority is used when no priority is configured for a provider.

View Source
const DefaultSyncMinConfidence = 0.6

DefaultSyncMinConfidence is the default minimum confidence for auto-sync to be applied. Used by config and syncing packages.

View Source
const MaxSafeFileBytes = 10 << 20

MaxSafeFileBytes is the maximum file size (10 MB) for config files, subtitle files, and other user-supplied payloads. Prevents OOM from oversized or malicious inputs. Shared across config, subsync, and provider packages.

View Source
const SubtitleExtSRT = ".srt"

SubtitleExtSRT is the extension every subflux writer emits. The capability-scoped extension authority lives in internal/subtitleext (api stays stdlib-only); its coverage test pins this constant to the writerOutput view so the two cannot drift.

Variables

View Source
var DefaultScores = Scores{
	Hash:             100,
	Source:           28,
	ReleaseGroup:     23,
	StreamingService: 14,
	VideoCodec:       10,
	HDR:              8,
	Edition:          15,
	SeasonPack:       15,
}

DefaultScores for release attribute matching.

Movie (98): source=28, release_group=23, streaming_service=14, edition=15, video_codec=10, hdr=8. Episode (98): source=28, release_group=23, streaming_service=14, season_pack=15, video_codec=10, hdr=8.

Hash match = 100 directly, bypasses attribute scoring.

View Source
var ErrBinaryData = errors.New("binary archive data, not a subtitle")

ErrBinaryData indicates the downloaded data is a binary archive that could not be extracted, not a subtitle file.

Functions

func AudioLanguages added in v0.1.106

func AudioLanguages(mi *arrapi.MediaInfo) []string

AudioLanguages parses a file's MediaInfo audio-languages string into deduplicated ISO 639-1 codes. Returns nil when mi is nil or empty.

func BadGatewayC

func BadGatewayC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

BadGatewayC writes a 502 with the given code and message.

func BadRequestC

func BadRequestC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

BadRequestC writes a 400 with the given code and message.

func BuildEpisodeID

func BuildEpisodeID(tvdbID int, imdbID string, season, episode int) string

BuildEpisodeID returns the canonical media ID for an episode. Prefer TVDB (Sonarr's canonical source), fall back to IMDB.

func BuildMediaID

func BuildMediaID(req *SearchRequest) string

BuildMediaID creates a stable identifier for a media item from a search request. Movies use TMDB ID (canonical for Radarr), episodes use TVDB ID (canonical for Sonarr). IMDB is only used as a last resort fallback.

func BuildMovieID

func BuildMovieID(tmdbID int, imdbID string) string

BuildMovieID returns the canonical media ID for a movie. Prefer TMDB (Radarr's canonical source), fall back to IMDB.

func BuildSeasonIDPrefix added in v0.1.148

func BuildSeasonIDPrefix(tvdbID int, imdbID string, season int) string

BuildSeasonIDPrefix returns the media ID prefix shared by every episode of one season, mirroring BuildEpisodeID's format through the episode separator (e.g. "tvdb-123-s05e"). BuildEpisodeID(tvdb, imdb, season, N) has this prefix for every N in 0-99, so backoff and state rows for a season are one prefix scan. Returns "" when neither ID is available (matching BuildEpisodeID's unidentified-media fallback shape only per season would be meaninglessly broad).

func BuildSeriesPrefix

func BuildSeriesPrefix(tvdbID int, imdbID string) string

BuildSeriesPrefix returns the media ID prefix for all episodes of a series. Used by coverage and missing-count queries to match all episodes via LIKE.

func ConflictC

func ConflictC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

ConflictC writes a 409 with the given code and message.

func CountNonTextBytes

func CountNonTextBytes(data []byte) int

CountNonTextBytes returns the number of bytes in data that are not printable text (control characters below TAB, or between CR and SPACE, excluding ESC). Used by ValidateSubtitleData and archive extraction.

func ForbiddenC

func ForbiddenC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

ForbiddenC writes a 403 with the given code and message.

func HasExcludeTag

func HasExcludeTag(tags []int, excludeIDs map[int]struct{}) bool

HasExcludeTag reports whether any of the item's tags are in the exclude set.

func InternalErrorC

func InternalErrorC(w http.ResponseWriter, r *http.Request, err error, code ErrorCode, logAttrs ...any)

InternalErrorC writes a 500 with the given code and a generic message, and logs the raw error at ERROR level. The raw error is never echoed to the client to avoid leaking internal details.

func IsValidMediaPrefix

func IsValidMediaPrefix(prefix string) bool

IsValidMediaPrefix checks that a prefix parameter matches expected media ID formats. Prevents arbitrary prefix queries that could produce confusing results if the app is exposed without auth.

func JSONError

func JSONError(w http.ResponseWriter, err error, code int)

JSONError writes {"error": err.Error()} at the given status code.

SECURITY: err.Error() is echoed verbatim to the client. Only use when the error text is author-controlled or explicitly user-safe (e.g. errors.New("short, static message")). For wrapped internal errors, use InternalError(w, err, ...) which logs the raw error and returns a generic message. For user-facing validation failures, prefer BadRequest(w, r, code, msg) with an author-controlled message.

func JSONErrorWithCode

func JSONErrorWithCode(w http.ResponseWriter, r *http.Request, status int, code ErrorCode, msg string)

JSONErrorWithCode is JSONError + an error-code envelope. Use when the error string is safe to surface verbatim AND a stable machine- readable code is meaningful for the client. Delegates to webhttp.WriteError so the envelope and its request-id population match writeError.

func LangNameToISO

func LangNameToISO(name string) string

LangNameToISO converts a language name (as returned by Sonarr/Radarr) to an ISO 639-1 code. Accepts full names ("english") or 2-letter ASCII codes. Returns empty string for unrecognized input.

func ManualOrdinal added in v0.1.148

func ManualOrdinal(path string) int

ManualOrdinal parses the manual sibling number out of a subtitle path produced by ManualSubtitlePath: the all-digit dot segment immediately before the extension (movie.fr.2.srt -> 2, movie.fr.forced.1.srt -> 1). An unnumbered path (the auto file, movie.fr.srt) returns 0. This is the inverse of ManualSubtitlePath's numbering and the ordinal component of the wire FileRef: together with (media_type, media_id, language, variant, source) it uniquely addresses one stored subtitle file, so manual numbered siblings sharing a quad stay distinguishable without a client-supplied path.

func ManualSubtitlePath

func ManualSubtitlePath(videoPath, lang string, n int, hi, forced bool) string

ManualSubtitlePath computes a numbered manual subtitle path. e.g., movie.fr.1.srt, movie.fr.2.srt for regular subs; movie.fr.hi.1.srt or movie.fr.forced.1.srt when the user deliberately downloaded an HI or forced variant. The variant tag appears before the number so parseExternalSubPath continues to recognize it on the next scan.

func MethodNotAllowedC

func MethodNotAllowedC(w http.ResponseWriter, r *http.Request, code ErrorCode)

MethodNotAllowedC writes a 405 with the given code.

func NewSessionHashContext

func NewSessionHashContext(ctx context.Context, sessHash string) context.Context

NewSessionHashContext returns a new context carrying the session token hash for the current request. Only requireAuth populates this; API-key callers have an empty session hash.

func NewUserContext

func NewUserContext(ctx context.Context, u *auth.User) context.Context

NewUserContext returns a new context with the given user stored in it.

func NotFoundC

func NotFoundC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

NotFoundC writes a 404 with the given code and message.

func Ok

func Ok(w http.ResponseWriter)

Ok writes a 200 {"ok": true} response — the standard "action succeeded" reply for endpoints that don't return data. Delegates to webhttp.Ok so the body matches the {"ok":true} every webhttp consumer emits.

func OriginalLangCode added in v0.1.106

func OriginalLangCode(lang *arrapi.Language) string

OriginalLangCode returns the ISO 639-1 code for an arr original-language reference, or "" when the reference is nil or the name is unmapped.

func ParseAudioLangs

func ParseAudioLangs(raw string) []string

ParseAudioLangs splits a comma/slash-separated audio languages string into deduplicated ISO 639-1 codes.

func PayloadTooLargeC

func PayloadTooLargeC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

PayloadTooLargeC writes a 413 with the given code and message.

func SeasonEpisodeFileCount added in v0.1.106

func SeasonEpisodeFileCount(s *arrapi.Series, seasonNum int) int

SeasonEpisodeFileCount returns the number of episode files for a specific season, using Sonarr's per-season statistics. Returns 0 if the season is not found or statistics are unavailable.

func ServiceUnavailableC

func ServiceUnavailableC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

ServiceUnavailableC writes a 503 with the given code and message.

func SessionHashFromContext

func SessionHashFromContext(ctx context.Context) string

SessionHashFromContext returns the session token hash for the current request, or "" if the request was authenticated via API key (no session). Handlers that need to touch the current session (delete on logout, exclude from bulk session invalidation) read it here instead of re-parsing the cookie.

func SubtitlePath

func SubtitlePath(videoPath, lang string, hi, forced bool) string

SubtitlePath computes the subtitle file path for a video.

func TooManyRequestsC

func TooManyRequestsC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

TooManyRequestsC writes a 429 with the given code and message. The caller is responsible for setting the Retry-After header when appropriate.

func UnauthorizedC

func UnauthorizedC(w http.ResponseWriter, r *http.Request, code ErrorCode, msg string)

UnauthorizedC writes a 401 with the given code and message.

func UserFromContext

func UserFromContext(ctx context.Context) *auth.User

UserFromContext extracts the authenticated user from the request context. Returns nil if no user is present.

func ValidateSubtitleData

func ValidateSubtitleData(data []byte) error

ValidateSubtitleData checks whether data looks like subtitle text rather than a binary archive. Returns ErrBinaryData if the data matches a known archive magic signature or has too many non-text bytes.

func WithRequestID

func WithRequestID(ctx context.Context, id string) context.Context

WithRequestID attaches the given id to ctx under webhttp's request-id key. Production requests receive the id transparently from webhttp.Logging; tests use this to inject a known id.

func WriteJSON

func WriteJSON(w http.ResponseWriter, v any)

WriteJSON encodes v as JSON with status 200.

func WriteJSONStatus

func WriteJSONStatus(w http.ResponseWriter, code int, v any)

WriteJSONStatus encodes v as JSON with the given status code. The encode failure is logged at Debug (deliberately quiet: the status line is already on the wire and an app JSON payload that fails to encode is exceptional) rather than delegating to webhttp.WriteJSONStatus, which logs at Warn.

Types

type AdaptiveConfig

type AdaptiveConfig struct {
	InitialDelay      time.Duration
	MaxDelay          time.Duration
	BackoffMultiplier float64
	MaxAttempts       int
	Enabled           bool
}

AdaptiveConfig controls adaptive search backoff.

type AdminUserCreatedResponse

type AdminUserCreatedResponse struct {
	Username string    `json:"username"`
	Email    string    `json:"email"`
	Role     auth.Role `json:"role"`
	ID       int64     `json:"id"`
}

AdminUserCreatedResponse is the JSON response after admin creates a user.

type ArrConfig

type ArrConfig struct {
	URL       string
	APIKey    string
	PublicURL string
}

ArrConfig holds Sonarr or Radarr connection details.

type ArrConfigProvider

type ArrConfigProvider interface {
	SonarrConfig() ArrConfig
	RadarrConfig() ArrConfig
}

ArrConfigProvider provides Sonarr/Radarr connection configuration.

type AudioRuleJSON

type AudioRuleJSON struct {
	Audio     string             `json:"audio"`
	Subtitles []SubtitleTargJSON `json:"subtitles"`
}

AudioRuleJSON is a JSON-friendly audio rule.

type AudioSyncResult

type AudioSyncResult struct {
	Method     string
	Cues       []SubtitleCue
	Offset     int64   // milliseconds
	Confidence float64 // 0.0 to 1.0
	Applied    bool    // true if sync was applied and should be saved
}

AudioSyncResult holds the output of an audio-based sync operation.

type AuthConfigProvider

type AuthConfigProvider interface {
	AuthEnabled() bool
	BasicAuthEnabled() bool
	OIDCEnabled() bool
	OIDCConfig() auth.OIDCConfig
	SessionIdleTimeout() time.Duration
	SessionAbsoluteTimeout() time.Duration
	CheckBreachedPasswords() bool
	WebAuthnRPID() string
}

AuthConfigProvider provides authentication configuration.

type AuthError

type AuthError struct{ Msg string }

AuthError indicates invalid or expired credentials.

func (*AuthError) Error

func (e *AuthError) Error() string

type BackoffEntry

type BackoffEntry struct {
	LastTried time.Time  `json:"last_tried"`
	NextRetry time.Time  `json:"next_retry"`
	MediaType MediaType  `json:"media_type"`
	MediaID   string     `json:"media_id"`
	Language  string     `json:"language"`
	Provider  ProviderID `json:"provider"`
	Failures  int        `json:"failures"`
}

BackoffEntry represents an item in adaptive search backoff.

type BackoffParams

type BackoffParams struct {
	InitialDelay time.Duration
	MaxDelay     time.Duration
	Multiplier   float64
}

BackoffParams groups adaptive backoff configuration for RecordNoResult.

type BackoffStore

type BackoffStore interface {
	RecordNoResult(ctx context.Context, mediaType MediaType, mediaID, language string, providerName ProviderID, bp BackoffParams) error
	BackedOffProviders(ctx context.Context, mediaType MediaType, mediaID, language string, maxAttempts int) ([]ProviderID, error)
}

BackoffStore groups the adaptive-backoff persistence methods.

type CacheClearer

type CacheClearer interface {
	ClearCache()
}

CacheClearer is an optional interface for providers that cache download data (e.g. season pack zips). Called after scan completion to free memory. Providers implementing this get compile-time verification via var _ api.CacheClearer = (*Provider)(nil).

type CleanupResult

type CleanupResult struct {
	// Paths are subtitle file paths whose DB entries were removed.
	// The caller is responsible for validating and deleting files from disk.
	Paths []string
}

CleanupResult holds the outcome of a media cleanup operation. Used by both CleanupForMediaUpgrade and ReconcileState.

type ConfigDrift

type ConfigDrift struct {
	// RemovedLanguages are language codes that were in the old config
	// but not in the new one. Their search_attempts should be cleared.
	RemovedLanguages []string

	// RemovedProviders are provider names that were enabled in the old
	// config but disabled or removed in the new one. Their
	// search_attempts should be cleared.
	RemovedProviders []ProviderID

	// AdaptiveDisabled is true when adaptive search was enabled in the
	// old config but disabled in the new one. All search_attempts
	// should be cleared.
	AdaptiveDisabled bool
}

ConfigDrift describes DB cleanup actions needed after a config change.

func DetectDrift

func DetectDrift(
	oldLangs, newLangs []string,
	oldProviders, newProviders []ProviderID,
	oldAdaptiveEnabled, newAdaptiveEnabled bool,
) ConfigDrift

DetectDrift compares old and new config state to determine what DB cleanup is needed. Parameters are the extracted sets from each config. Duplicate entries in old slices are deduplicated; each removed item appears at most once in the result.

func (*ConfigDrift) Empty

func (d *ConfigDrift) Empty() bool

Empty returns true if no cleanup is needed.

type ConfigLoader

type ConfigLoader func(data []byte) (ConfigProvider, error)

ConfigLoader parses and validates config from raw YAML bytes.

type ConfigProvider

ConfigProvider gives read access to configuration. It composes the focused sub-interfaces defined in config_iface.go. Consumers should accept the narrowest sub-interface that satisfies their needs; ConfigProvider is for composition roots that need everything.

type CoverageStore

type CoverageStore interface {
	RecordSubtitleFiles(ctx context.Context, mediaType MediaType, mediaID string, files []SubtitleFile) (bool, error)
	UpsertSubtitleFile(ctx context.Context, mediaType MediaType, mediaID string, f *SubtitleFile) error
	GetSubtitleFiles(ctx context.Context, mediaType MediaType, mediaIDPrefix string) ([]SubtitleEntry, error)
	DeleteSubtitleFile(ctx context.Context, mediaType MediaType, mediaID, language string, variant Variant, source SubtitleSource, path string) error
	RecordScanState(ctx context.Context, rec *ScanRecord) error
	GetScanStates(ctx context.Context, mediaType MediaType, mediaIDPrefix string) ([]ScanStateRow, error)
	RecentlyScanned(ctx context.Context, cutoff time.Time) (map[string]bool, error)
	TotalSubtitleFiles(ctx context.Context) (int, error)
	LastScanTime(ctx context.Context) (string, error)
	// Scan-cycle mark (duration-aware resume): set when a full scan begins,
	// cleared on normal completion. A dangling mark means the previous cycle
	// was interrupted; ScanCycleStart returns the zero time when absent.
	ScanCycleStart(ctx context.Context) (time.Time, error)
	SetScanCycleStart(ctx context.Context, t time.Time) error
	ClearScanCycleStart(ctx context.Context) error
}

CoverageStore groups subtitle file tracking and scan state methods.

type DownloadMeta

type DownloadMeta struct {
	Title      string
	ImdbID     string
	ReleaseTag string
	VideoPath  string // Path to the video file (for reconciliation and upgrades).
	Season     int
	Episode    int
	Manual     bool // True if user manually selected this subtitle.
}

DownloadMeta holds optional metadata for a subtitle state record.

type DownloadRecord

type DownloadRecord struct {
	Meta         *DownloadMeta
	MediaType    MediaType
	MediaID      string
	Language     string
	Variant      Variant // subtitle variant (standard/hi/forced); empty is normalized to standard
	ProviderName ProviderID
	ReleaseName  string
	Path         string
	Score        int
}

DownloadRecord groups the parameters for Store.SaveDownload. Using a named struct (rather than positional string arguments) makes call sites self-documenting and prevents silent swaps of same-typed fields like Language/ProviderName.

type DownloadStore

type DownloadStore interface {
	SaveDownload(ctx context.Context, rec *DownloadRecord) error
	DownloadedRefs(ctx context.Context, mediaType MediaType, mediaID, language string) ([]DownloadedRef, error)
	CurrentScore(ctx context.Context, mediaType MediaType, mediaID, language string, variant Variant) (score int, mediaImported time.Time, found bool, err error)
}

DownloadStore groups subtitle download record persistence.

subtitle_state rows are keyed by the (media_type, media_id, language, variant) quad, so CurrentScore is answered per variant: an fr/forced download never shadows the fr/standard score. DownloadedRefs deliberately stays language-scoped (all variants): it feeds the manual-search popup's "on disk" markers, and the popup lists every variant of the language.

type DownloadedRef

type DownloadedRef struct {
	ReleaseName string
	Provider    ProviderID
}

DownloadedRef identifies a previously-downloaded subtitle by its release name and provider. Returned by Store.DownloadedRefs to mark matching entries in the manual search popup as already on disk.

type EmbeddedPolicy added in v0.1.148

type EmbeddedPolicy struct {
	// IgnorePGS excludes PGS bitmap subs (Blu-ray) from target satisfaction.
	IgnorePGS bool
	// IgnoreVobSub excludes VobSub bitmap subs (DVD) from target satisfaction.
	IgnoreVobSub bool
	// IgnoreASS excludes ASS/SSA styled subs (anime) from target satisfaction.
	IgnoreASS bool
}

EmbeddedPolicy is the typed search policy for embedded subtitle codecs, resolved from the top-level embedded_subtitles config section. The detector always returns every normalized track; this policy decides which codecs count as "present but not usable" for target satisfaction (they stay visible in coverage but trigger external search).

type EmbeddedTrack

type EmbeddedTrack struct {
	Codec           string
	Lang            string
	Name            string
	Index           int
	Forced          bool
	HearingImpaired bool
}

EmbeddedTrack represents a subtitle track detected inside a video container.

type ErrorCode added in v0.1.148

type ErrorCode string

ErrorCode is the machine-readable code carried in the JSON error envelope. A defined type (not an alias) so the wire generator discovers the catalog as a TS string-union; one-off codes still pass as string literals (untyped constants convert).

const (
	CodeBadRequest         ErrorCode = "bad_request"
	CodeUnauthorized       ErrorCode = "unauthorized"
	CodeForbidden          ErrorCode = "forbidden"
	CodeNotFound           ErrorCode = "not_found"
	CodeMethodNotAllowed   ErrorCode = "method_not_allowed"
	CodeConflict           ErrorCode = "conflict"
	CodePayloadTooLarge    ErrorCode = "payload_too_large"
	CodeRateLimited        ErrorCode = "rate_limited"
	CodeBadGateway         ErrorCode = "bad_gateway"
	CodeServiceUnavailable ErrorCode = "service_unavailable"
	CodeInternalError      ErrorCode = "internal_error"
)

Generic codes (status mapped 1:1).

const (
	CodeAuthInvalidCredentials ErrorCode = "auth_invalid_credentials"
	CodeAuthAccountDisabled    ErrorCode = "auth_account_disabled"
	CodeAuthAccountNotSetup    ErrorCode = "auth_account_not_setup"
	CodeAuthPasswordTooShort   ErrorCode = "auth_password_too_short"
	CodeAuthPasswordBreached   ErrorCode = "auth_password_breached"
	CodeAuthSessionInvalid     ErrorCode = "auth_session_invalid"
	CodeAuthSessionRequired    ErrorCode = "auth_session_required"
	CodeAuthRoleRequired       ErrorCode = "auth_role_required"
	CodeAuthAPIKeyInvalid      ErrorCode = "auth_apikey_invalid"
	CodeAuthAPIKeyDisabled     ErrorCode = "auth_apikey_disabled"
	CodeAuthCSRF               ErrorCode = "auth_csrf"
)

Auth codes.

const (
	CodeWebAuthnSessionInvalid    ErrorCode = "webauthn_session_invalid"
	CodeWebAuthnRegisterFailed    ErrorCode = "webauthn_register_failed"
	CodeWebAuthnAssertionFailed   ErrorCode = "webauthn_assertion_failed"
	CodeWebAuthnUnsupportedOrigin ErrorCode = "webauthn_unsupported_origin"
)

WebAuthn codes.

const (
	CodeOIDCStateInvalid          ErrorCode = "oidc_state_invalid"
	CodeOIDCNonceInvalid          ErrorCode = "oidc_nonce_invalid"
	CodeOIDCExchangeFailed        ErrorCode = "oidc_exchange_failed"
	CodeOIDCUserInfoFailed        ErrorCode = "oidc_userinfo_failed"
	CodeOIDCAccountNotProvisioned ErrorCode = "oidc_account_not_provisioned"
)

OIDC codes.

const (
	CodeSetupAlreadyComplete ErrorCode = "setup_already_complete"
	CodeSetupPasswordInvalid ErrorCode = "setup_password_invalid"
)

Setup codes.

const (
	CodeConfigInvalid        ErrorCode = "config_invalid"
	CodeConfigUnreachableArr ErrorCode = "config_unreachable_arr"
	CodeConfigYAMLParse      ErrorCode = "config_yaml_parse"
	CodeConfigTooLarge       ErrorCode = "config_too_large"
	CodeConfigReloadFailed   ErrorCode = "config_reload_failed"
)

Config codes.

const (
	CodeScanInProgress         ErrorCode = "scan_in_progress"
	CodeScanNoTargets          ErrorCode = "scan_no_targets"
	CodeSearchInProgress       ErrorCode = "search_in_progress"
	CodeSearchProviderDisabled ErrorCode = "search_provider_disabled"
	CodeSearchNoResults        ErrorCode = "search_no_results"
	CodeDownloadFailed         ErrorCode = "download_failed"
	CodeUnlockNotHeld          ErrorCode = "unlock_not_held"
)

Scan / search / manual ops codes.

const (
	CodePathNotAllowed        ErrorCode = "path_not_allowed"
	CodeMediaNotFound         ErrorCode = "media_not_found"
	CodeSubtitleNotFound      ErrorCode = "subtitle_not_found"
	CodePreviewUnavailable    ErrorCode = "preview_unavailable"
	CodeSyncUnsupportedFormat ErrorCode = "sync_unsupported_format"
	CodeSyncNoReference       ErrorCode = "sync_no_reference"
	CodeSyncLowConfidence     ErrorCode = "sync_low_confidence"
	// CodeSubtitleExtensionNotAllowed is the 409 a delete answers when the
	// target's extension lacks the delete capability in the subtitle
	// extension authority (a server-derived stored-state disagreement, not
	// caller authorization — hence 409, not 403).
	CodeSubtitleExtensionNotAllowed ErrorCode = "subtitle_extension_not_allowed"
)

File / preview / sync codes.

const (
	CodeQueryInvalidFilter ErrorCode = "query_invalid_filter"
	CodeQueryLimitExceeded ErrorCode = "query_limit_exceeded"
)

Query codes.

const (
	CodeProviderTimedOut      ErrorCode = "provider_timed_out"
	CodeProviderNotConfigured ErrorCode = "provider_not_configured"
	CodeArrUnreachable        ErrorCode = "arr_unreachable"
)

Provider / arr action codes.

type HistoryStore

type HistoryStore interface {
	HistoryMediaIDs(ctx context.Context, mediaType MediaType, mediaIDPrefix string) ([]string, error)
}

HistoryStore groups download-history lookup methods.

type KeyGenerated added in v0.1.29

type KeyGenerated struct {
	CreatedAt time.Time `json:"created_at"`
	Key       string    `json:"key"`
	KeyPrefix string    `json:"key_prefix"`
	KeySuffix string    `json:"key_suffix"`
	Label     string    `json:"label"`
	ID        int64     `json:"id"`
}

KeyGenerated is the JSON response after generating an API key.

type LangOutcome added in v0.1.148

type LangOutcome struct {
	Lang     string          // ISO 639-1 language code
	Kind     LangOutcomeKind // what happened for this language group
	Paths    []string        // subtitle files downloaded for this language
	Searched int             // variant targets processed against provider results
	Skipped  int             // variant targets skipped within the group
	// Queried counts the provider queries the group's single sweep actually
	// issued (successful or erroring; providers skipped by the health
	// timeout never sent a request and don't count). Zero for skipped and
	// backed-off groups. Distinct from Searched, which counts VARIANT
	// targets processed against results and stays positive even when the
	// sweep issued no query (every eligible provider timed out).
	Queried int
}

LangOutcome is the typed per-language result of a SearchTargets call. It replaces the former parallel FoundLangs/SearchedLangs slices and aggregate counters: each language carries its own classification and evidence, so consumers (season tracker, scan stats, coverage events) read one structure instead of reconstructing sets from slice pairs.

func (*LangOutcome) Found added in v0.1.148

func (o *LangOutcome) Found() bool

Found reports whether at least one subtitle was downloaded for the language.

type LangOutcomeKind added in v0.1.148

type LangOutcomeKind string

LangOutcomeKind classifies what happened for one language group in a SearchTargets call. Exactly one kind applies per language.

const (
	// LangSearched: providers were actually queried for this language.
	LangSearched LangOutcomeKind = "searched"
	// LangSkipped: nothing needed a search (covered on disk, manually
	// locked, or not eligible for upgrade).
	LangSkipped LangOutcomeKind = "skipped"
	// LangBackedOff: the language needed a search but every provider was in
	// adaptive backoff, so no query ran. Distinct from both other kinds; it
	// must never feed no-result evidence into the season tracker.
	LangBackedOff LangOutcomeKind = "backed_off"
)

Language-group outcome kinds.

type LanguageResolver

type LanguageResolver interface {
	ResolveTargetsWithFallback(originalLang string, audioLangs []string) []SubtitleTarget
	LanguageCodes() []string
	ProvidersForTarget(t *SubtitleTarget, allProviders []ProviderID) []ProviderID
	MinScoreForTarget(t *SubtitleTarget, mediaType MediaType) int
}

LanguageResolver resolves subtitle targets from audio language context.

type LanguageRulesJSON

type LanguageRulesJSON struct {
	Rules   []AudioRuleJSON    `json:"rules,omitempty"`
	Default []SubtitleTargJSON `json:"default,omitempty"`
}

LanguageRulesJSON is the language rules in a JSON-serializable format for the settings UI.

type LogFormat

type LogFormat string

LogFormat is a typed string for log output formats.

type LogLevel

type LogLevel string

LogLevel is a typed string for log verbosity levels.

type LoginSuccess added in v0.1.29

type LoginSuccess struct {
	Redirect string     `json:"redirect"`
	User     MeResponse `json:"user"`
}

LoginSuccess is the JSON response after successful login.

type MaintStore

type MaintStore interface {
	DeleteStateByPaths(ctx context.Context, paths []string) (CleanupResult, error)
	CleanupDrift(ctx context.Context, drift ConfigDrift) error
	ReconcileState(ctx context.Context) (ReconcileResult, error)
}

MaintStore groups maintenance and cleanup methods.

type ManualLockEntry

type ManualLockEntry struct {
	MediaType MediaType `json:"media_type"`
	MediaID   string    `json:"media_id"`
	Language  string    `json:"language"`
	Variant   Variant   `json:"variant"`
	Count     int       `json:"count"`
}

ManualLockEntry represents a manually locked media+language+variant quad.

type ManualLockStore

type ManualLockStore interface {
	// IsManuallyLocked reports whether the quad has a manual row. An empty
	// variant asks whether ANY variant of the language is locked.
	IsManuallyLocked(ctx context.Context, mediaType MediaType, mediaID, language string, variant Variant) (bool, error)
	// ClearManualLock clears the quad's lock. An empty variant clears the
	// locks of ALL variants of the language.
	ClearManualLock(ctx context.Context, mediaType MediaType, mediaID, language string, variant Variant) error
	// ManualDownloadCount counts the quad's manual rows (exact variant).
	ManualDownloadCount(ctx context.Context, mediaType MediaType, mediaID, language string, variant Variant) (int, error)
	// ManualSubtitlePaths returns the manual rows' file paths. An empty
	// variant returns the paths of ALL variants of the language.
	ManualSubtitlePaths(ctx context.Context, mediaType MediaType, mediaID, language string, variant Variant) ([]string, error)
	// NextManualNumber returns the next manual ordinal for the quad (exact
	// variant): movie.fr.1.srt and movie.fr.forced.1.srt count independently.
	NextManualNumber(ctx context.Context, mediaType MediaType, mediaID, language string, variant Variant) int
}

ManualLockStore groups manual override lock persistence. Locks live on the (media_type, media_id, language, variant) quad: a manual forced download locks only the forced target, leaving standard/hi automation untouched.

Methods documented as accepting an empty variant treat "" as "any/all variants of the language"; the rest require an exact variant.

type MatchMethod

type MatchMethod string

MatchMethod is a typed string identifying how a subtitle was matched to the requested media. Use the exported constants rather than raw string literals. Follows the same pattern as MediaType and AuthMethod.

const (
	MatchByHash  MatchMethod = "hash"
	MatchByIMDB  MatchMethod = "imdb"
	MatchByTitle MatchMethod = "title"
	MatchByTVDB  MatchMethod = "tvdb"
	MatchByTMDB  MatchMethod = "tmdb"
)

Match method constants. Canonical source for all packages.

func (MatchMethod) String

func (m MatchMethod) String() string

String implements fmt.Stringer.

type MatchSet

type MatchSet struct {
	Hash             bool
	Source           bool
	ReleaseGroup     bool
	StreamingService bool
	VideoCodec       bool
	HDR              bool
	Edition          bool
	SeasonPack       bool
	SeriesIMDB       bool
	IMDB             bool
}

MatchSet tracks which attributes matched between a subtitle and a video. Each field corresponds to a release attribute; struct fields make invalid keys impossible at compile time.

func (MatchSet) Keys

func (m MatchSet) Keys() []string

Keys returns the names of matched attributes (for debug logging and JSON breakdown).

type MeResponse added in v0.1.29

type MeResponse struct {
	Username    string    `json:"username"`
	Role        auth.Role `json:"role"`
	ID          int64     `json:"id"`
	HasPasskeys bool      `json:"has_passkeys"`
	OIDCLinked  bool      `json:"oidc_linked"`
	HasPassword bool      `json:"has_password"`
	CanLinkOIDC bool      `json:"can_link_oidc"`
}

MeResponse is the JSON response for GET /api/auth/me.

type MediaType

type MediaType string

MediaType is a typed string identifying the kind of media being searched. Use the exported constants rather than raw string literals.

const (
	MediaTypeMovie   MediaType = "movie"
	MediaTypeEpisode MediaType = "episode"
)

Media type constants used by SearchRequest.MediaType and related helpers. Canonical source for all packages; import from here instead of declaring local unexported copies.

func (MediaType) String

func (mt MediaType) String() string

String implements fmt.Stringer.

func (MediaType) Valid

func (mt MediaType) Valid() bool

Valid reports whether mt is a recognized media type.

type PasskeyRegistered added in v0.1.29

type PasskeyRegistered struct {
	CreatedAt time.Time `json:"created_at"`
	Name      string    `json:"name"`
	Transport string    `json:"transport"`
	ID        int64     `json:"id"`
}

PasskeyRegistered is the JSON response after successful passkey registration.

type PathValidator

type PathValidator interface {
	MediaRoots() []string
	ValidatePath(ctx context.Context, path string) error
	RemoveUnderRoot(ctx context.Context, path string) error
}

PathValidator provides media path validation.

type PollKey

type PollKey string

PollKey identifies an arr-source poll-timestamp row in the store. Using a typed string instead of bare string prevents typo-induced silent failures (e.g. "Sonarr" capitalization or "sonar" typo would silently insert a new row and force history re-fetch).

const (
	PollKeySonarr PollKey = "sonarr"
	PollKeyRadarr PollKey = "radarr"
)

Canonical poll-key values. New arr sources should add a constant here.

func (PollKey) Valid

func (k PollKey) Valid() bool

Valid returns true if the PollKey is one of the canonical values.

type PollStore

type PollStore interface {
	GetPollTimestamp(ctx context.Context, key PollKey) (time.Time, error)
	SetPollTimestamp(ctx context.Context, key PollKey, t time.Time) error
}

PollStore groups arr webhook/poll timestamp persistence.

type PostProcessConfig

type PostProcessConfig struct {
	StripHI          bool `json:"strip_hi"`
	StripTags        bool `json:"strip_tags"`
	NormalizeUTF8    bool `json:"normalize_utf8"`
	CleanWhitespace  bool `json:"clean_whitespace"`
	NormalizeEndings bool `json:"normalize_endings"`
	RemoveEmpty      bool `json:"remove_empty"`
}

PostProcessConfig controls subtitle post-processing.

type Provider

type Provider interface {
	// Name returns the provider identifier (e.g. "opensubtitles", "yifysubtitles").
	Name() ProviderID

	// Search finds subtitles matching the request.
	Search(ctx context.Context, req *SearchRequest) ([]Subtitle, error)

	// Download fetches the subtitle content for the given search result.
	Download(ctx context.Context, sub *Subtitle) ([]byte, error)
}

Provider is the interface all subtitle providers must implement.

type ProviderCfg

type ProviderCfg struct {
	Settings map[string]any
	Priority int // Lower = higher trust. 0 means unset (defaults to 99).
	Enabled  bool
}

ProviderCfg is a generic provider configuration block.

type ProviderConfigProvider

type ProviderConfigProvider interface {
	ProviderConfigs() map[ProviderID]ProviderCfg
	ProviderPriority(name ProviderID) int
}

ProviderConfigProvider provides provider settings.

type ProviderID

type ProviderID string

ProviderID is a typed string identifying a subtitle provider. Use this instead of raw strings when passing provider names to metrics, backoff, and other subsystems that key on provider identity.

const (
	ProviderNameMock          ProviderID = "mock"
	ProviderNameHDBits        ProviderID = "hdbits"
	ProviderNameOpenSubtitles ProviderID = "opensubtitles"
	ProviderNameSubSource     ProviderID = "subsource"
	ProviderNameAnimeTosho    ProviderID = "animetosho"
	ProviderNameYifySubtitles ProviderID = "yifysubtitles"
	ProviderNameSubDL         ProviderID = "subdl"
	ProviderNameBetaSeries    ProviderID = "betaseries"
	ProviderNameGestdown      ProviderID = "gestdown"
)

Provider name constants (canonical SSOT). All packages MUST reference these instead of declaring local string literals.

func (ProviderID) String

func (p ProviderID) String() string

String implements fmt.Stringer.

type ProviderRegistry

type ProviderRegistry interface {
	// LoadAll instantiates providers from the given config map, skipping
	// unconfigured entries. Returns an error if a configured provider fails.
	LoadAll(ctx context.Context, configs map[ProviderID]ProviderCfg) ([]Provider, error)
	// ProviderNames returns all registered provider names in priority order.
	ProviderNames() []ProviderID
	// Schema returns the UI label and settings fields for a named provider.
	Schema(name ProviderID) (label string, fields []ProviderSchemaField)
}

ProviderRegistry manages provider factories and schema metadata.

type ProviderSchema

type ProviderSchema struct {
	Name     string        `json:"name"`
	Label    string        `json:"label"`
	Settings []SchemaField `json:"settings,omitempty"`
}

ProviderSchema describes a single provider's settings fields.

func BuildProviderSchemas

func BuildProviderSchemas(reg ProviderRegistry, exclude ...string) []ProviderSchema

BuildProviderSchemas converts the registry's provider metadata into ProviderSchema entries for the UI. Names in exclude are omitted.

type ProviderSchemaField

type ProviderSchemaField struct {
	Key     string `json:"key"`
	Label   string `json:"label"`
	Type    string `json:"type"` // text, secret, bool
	Default string `json:"default,omitempty"`
	Help    string `json:"help,omitempty"`
	Secret  bool   `json:"secret,omitempty"`
}

ProviderSchemaField describes a single provider setting for the UI schema.

type ProviderStatus added in v0.1.29

type ProviderStatus struct {
	LastError         string        `json:"last_error,omitempty"`
	CooldownRemaining time.Duration `json:"cooldown_remaining,omitempty"`
	RecentFailures    int           `json:"recent_failures"`
	Threshold         int           `json:"threshold"`
	TimedOut          bool          `json:"timed_out"`
}

ProviderStatus is the state of a single provider's timeout.

type ProviderTimeoutManager

type ProviderTimeoutManager interface {
	ProviderTimeouts() (status map[ProviderID]ProviderStatus, enabled bool)
	ResetTimeouts()
}

ProviderTimeoutManager manages provider timeout state.

type ProvidersResponse

type ProvidersResponse struct {
	Providers map[ProviderID]ProviderStatus `json:"providers"`
	Enabled   bool                          `json:"enabled"`
}

ProvidersResponse is the JSON response for GET /api/providers/timeout.

type QueryStore

type QueryStore interface {
	GetState(ctx context.Context, q *StateQuery) ([]StateEntry, error)
	GetBackoffItems(ctx context.Context) ([]BackoffEntry, error)
	GetBackoffByPrefix(ctx context.Context, mediaType MediaType, mediaIDPrefix string) ([]BackoffEntry, error)
	GetManualLocks(ctx context.Context) ([]ManualLockEntry, error)
	Stats(ctx context.Context) (downloads, attempts int, err error)
}

QueryStore groups read-only state inspection methods.

type RadarrClient added in v0.1.106

type RadarrClient interface {
	Ping(ctx context.Context) error
	GetMovies(ctx context.Context) ([]arrapi.Movie, error)
	GetMovieByID(ctx context.Context, id int) (arrapi.Movie, error)
	GetHistorySince(ctx context.Context, since time.Time, eventTypes ...arrapi.EventType) ([]arrapi.HistoryRecord, error)
	GetWantedMovies(ctx context.Context, excludeTagIDs map[int]struct{}, fn func(arrapi.Movie) error) error
	ResolveExcludeTagIDs(ctx context.Context, tagNames []string, logMissing bool) map[int]struct{}
	RescanMovie(ctx context.Context, movieID int) error
}

RadarrClient is the Radarr-side surface subflux consumes: library reads, per-item lookups, import-history polling, wanted-movie iteration, exclude-tag resolution, and a post-download rescan.

type RateLimitError

type RateLimitError struct {
	Msg        string
	RetryAfter time.Duration
}

RateLimitError indicates the provider's rate limit was exceeded. RetryAfter, when non-zero, is the hint from the upstream's Retry-After header (delta-seconds or HTTP-date resolved to a positive duration). Consumers may use it to schedule the next attempt; a zero value means no hint was provided.

func (*RateLimitError) Error

func (e *RateLimitError) Error() string

type ReconcileResult

type ReconcileResult struct {
	// Deleted contains subtitle paths removed because the video is gone.
	Deleted CleanupResult

	// ResetCount is the number of entries reset for re-search because
	// the subtitle file was missing but the video still exists.
	ResetCount int64
}

ReconcileResult holds the outcome of a state reconciliation pass.

type ScanOutcome

type ScanOutcome string

ScanOutcome is a typed string for scan result classification. Canonical source; scanning/ and server/ should import from here.

const (
	ScanFound    ScanOutcome = "found"
	ScanSkipped  ScanOutcome = "skipped"
	ScanNoResult ScanOutcome = "none"
	// ScanBackedOff means every language target that needed a search had all
	// of its providers in adaptive backoff, so no provider was queried at
	// all. Distinct from ScanNoResult ("we looked, nothing found") and
	// ScanSkipped ("nothing needed a search"): "we already know there is
	// probably nothing, so we didn't look". Never recorded in the season
	// early-termination tracker and counted in its own stats bucket.
	ScanBackedOff ScanOutcome = "backed_off"
)

Scan outcome constants.

type ScanRecord

type ScanRecord struct {
	MediaType MediaType
	MediaID   string
	Title     string
	AudioLang string
	Season    int
	Episode   int
	// Searched records whether provider work actually ran to completion for
	// this stamp. False for inventory-only visits (scan skip paths record
	// coverage without searching); the resume set and staleness displays can
	// distinguish "we looked at the disk" from "we searched providers".
	Searched bool
}

ScanRecord groups the parameters for CoverageStore.RecordScanState.

type ScanStateRow

type ScanStateRow struct {
	ScannedAt string `json:"scanned_at"`
	MediaID   string `json:"media_id"`
	Title     string `json:"title"`
	AudioLang string `json:"audio_lang"`
	Season    int    `json:"season,omitempty"`
	Episode   int    `json:"episode,omitempty"`
	// Searched is false for inventory-only stamps (scan skip paths that
	// recorded coverage without provider work).
	Searched bool `json:"searched"`
}

ScanStateRow records when a media item was last scanned.

type ScanStats

type ScanStats struct {
	// Pre-scan totals (from API).
	TotalSeries       int
	TotalEpisodeFiles int // Sum of episodeFileCount across all series.
	TotalMovies       int
	TotalMovieFiles   int // Movies with hasFile=true.

	// Post-scan outcomes for episodes. EpisodesSearched counts every episode
	// the scan loop processed (it drives progress reporting); the other
	// fields are its per-outcome buckets.
	EpisodesSearched  int // Episodes processed by the scan loop.
	EpisodesSkipped   int // Episodes with existing subs (no search needed).
	EpisodesFound     int // Episodes where a subtitle was downloaded.
	EpisodesNoResult  int // Episodes searched but no subtitle found.
	EpisodesBackedOff int // Episodes where every needed provider was in adaptive backoff (no query ran).
	SeriesSkipped     int // Series skipped by show-level pre-check.

	// Post-scan outcomes for movies.
	MoviesSearched  int
	MoviesSkipped   int
	MoviesFound     int
	MoviesNoResult  int
	MoviesBackedOff int
}

ScanStats tracks scan progress and outcomes for logging.

type SchemaField

type SchemaField struct {
	Key         string         `json:"key"`
	Label       string         `json:"label"`
	Type        string         `json:"type"`
	Default     string         `json:"default,omitempty"`
	Help        string         `json:"help,omitempty"`
	Placeholder string         `json:"placeholder,omitempty"`
	Min         string         `json:"min,omitempty"`
	Max         string         `json:"max,omitempty"`
	ShowWhen    string         `json:"show_when,omitempty"`
	Requires    string         `json:"requires,omitempty"`
	Group       string         `json:"group,omitempty"`
	Fields      []SchemaField  `json:"fields,omitempty"`
	Options     []SchemaOption `json:"options,omitempty"`
	Secret      bool           `json:"secret,omitempty"`
	Required    bool           `json:"required,omitempty"`
}

SchemaField describes a single configuration field for the UI.

type SchemaFunc

type SchemaFunc func(providers []ProviderSchema) []SchemaSection

SchemaFunc returns the full configuration schema for the UI.

type SchemaOption

type SchemaOption struct {
	Value string `json:"value"`
	Label string `json:"label"`
}

SchemaOption is a value+label pair for select fields.

type SchemaSection

type SchemaSection struct {
	Key              string           `json:"key"`
	Title            string           `json:"title"`
	Type             string           `json:"type"`
	Help             string           `json:"help,omitempty"`
	RequiredGroup    string           `json:"required_group,omitempty"`
	EnableKey        string           `json:"enable_key,omitempty"`
	Fields           []SchemaField    `json:"fields,omitempty"`
	ProviderTemplate []SchemaField    `json:"provider_template,omitempty"`
	Providers        []ProviderSchema `json:"providers,omitempty"`
}

SchemaSection describes a top-level config section.

type ScorePreview added in v0.1.29

type ScorePreview struct {
	Tier        ScoreTier `json:"tier"`
	Score       int       `json:"score"`
	ScoreNoHash int       `json:"score_no_hash"`
}

ScorePreview is the JSON response for POST /api/score/preview.

type ScoreResult

type ScoreResult struct {
	Tier        ScoreTier // Quality tier label (excellent/good/acceptable/minimal/none).
	Score       int       // Final score including hash match bonus.
	ScoreNoHash int       // Score from release attributes only, excluding hash match.
}

ScoreResult holds the output of a score simulation.

type ScoreSimulator

type ScoreSimulator interface {
	SimulateScore(mediaType MediaType, videoRelease, subRelease string, matchedBy MatchMethod) ScoreResult
	ScoreSubtitles(req *SearchRequest, results []Subtitle) []ScoredResult
}

ScoreSimulator provides subtitle scoring capabilities.

type ScoreTier

type ScoreTier string

ScoreTier is a typed string for release quality tier labels.

const (
	TierExcellent  ScoreTier = "excellent"  // 80+
	TierGood       ScoreTier = "good"       // 50+
	TierAcceptable ScoreTier = "acceptable" // 20+
	TierMinimal    ScoreTier = "minimal"    // 1+
	TierNone       ScoreTier = "none"       // 0
)

Tier constants for release quality scoring.

type ScoredResult

type ScoredResult struct {
	Matches map[string]int // Per-category score contributions.
	Sub     Subtitle
	Score   int
}

ScoredResult is a scored subtitle for use by the manual search and CLI APIs.

type Scorer

type Scorer interface {
	// Score computes a quality score for a subtitle match set. Returns the
	// full score (including the hash bonus) and the release-attribute-only
	// score. A verifiable hash match short-circuits to the hash weight alone.
	Score(sub SubtitleInfo, matches MatchSet) (score, scoreNoHash int)
	// ScoreToTier maps a numeric score to a human-readable tier label via
	// one global threshold table: excellent >= 80, good >= 50,
	// acceptable >= 20, minimal >= 1, else none. Thresholds do not vary by
	// media type.
	ScoreToTier(score int) ScoreTier
}

Scorer turns a subtitle's release-attribute match set into a quality score.

type Scores

type Scores struct {
	Hash             int `json:"hash" yaml:"hash"`
	Source           int `json:"source" yaml:"source"`
	ReleaseGroup     int `json:"release_group" yaml:"release_group"`
	StreamingService int `json:"streaming_service" yaml:"streaming_service"`
	VideoCodec       int `json:"video_codec" yaml:"video_codec"`
	HDR              int `json:"hdr" yaml:"hdr"`
	Edition          int `json:"edition" yaml:"edition"`         // movies only
	SeasonPack       int `json:"season_pack" yaml:"season_pack"` // episodes only
}

Scores defines the weight for each release attribute match type.

Identity fields (series, season, episode, title, year) are not scored here; they are enforced by the provider query (identity gate). Resolution is excluded; its signal is captured by source + release_group.

Movie-only: Edition. For episodes, edition=0 and season_pack takes its place. Applicable non-hash weights sum to 98 for each media type. Hash match bypasses attribute scoring entirely (score = 100).

type ScoringConfig

type ScoringConfig interface {
	Scores() Scores
}

ScoringConfig provides scoring weight configuration.

type SearchConfig

type SearchConfig struct {
	ExcludeArrTags         []string
	ScanInterval           time.Duration
	ProviderTimeout        time.Duration
	ScanDelay              time.Duration
	MinScore               int
	UpgradeWindowDays      int
	DownloadMaxAttempts    int // Max download attempts per search (0 = default DefaultDownloadMaxAttempts)
	MaxProviderConcurrency int // Max parallel provider searches (0 = default DefaultProviderConcurrency)
	MaxSSEClients          int // Max concurrent SSE connections (0 = default 32)
	UpgradeEnabled         bool
}

SearchConfig controls search behavior.

type SearchConfigProvider

type SearchConfigProvider interface {
	Search() SearchConfig
	Adaptive() AdaptiveConfig
	PostProcessConfig() PostProcessConfig
	SyncConfig() SyncConfig
	// EmbeddedPolicy returns the typed embedded subtitle codec policy
	// from the top-level embedded_subtitles config section.
	EmbeddedPolicy() EmbeddedPolicy
}

SearchConfigProvider provides search behavior configuration.

type SearchEngine

SearchEngine composes all search sub-interfaces for composition roots.

type SearchRequest

type SearchRequest struct {
	MediaType         MediaType // "episode" or "movie"
	ImdbID            string    // IMDB ID; fallback for providers without TMDB/TVDB support
	Title             string
	AlternativeTitles []string // from Sonarr/Radarr alternateTitles
	EpisodeTitle      string   // Episode title from Sonarr (for identity validation across numbering schemes)
	ReleaseName       string
	VideoPath         string   // Absolute path to the video file (for embedded track detection and sync)
	VideoHash         string   // OpenSubtitles hash
	AudioLang         string   // resolved audio language (for coverage tracking)
	Languages         []string // ISO 639-1 codes to search for
	TmdbID            int      // Movie TMDB ID from Radarr; 0 for episodes
	VideoSize         int64    // file size for hash-based search
	Year              int
	Season            int // 0 for movies
	Episode           int // 0 for movies
	TvdbID            int // Series TVDB ID from Sonarr; 0 for movies
	MaxResults        int // 0 = provider default (typically one page)

	// Alternate episode numbering from Sonarr (0 = not available).
	// Scene numbers come from TheXEM; absolute numbers from TVDB.
	SceneSeason     int
	SceneEpisode    int
	AbsoluteEpisode int

	// ForceUpgrade makes the search engine treat all external subtitles
	// as eligible for upgrade, regardless of the upgrade_enabled config.
	// Set by manual scan triggers (series/season search buttons).
	ForceUpgrade bool
}

SearchRequest holds the parameters for a subtitle search across providers.

func (*SearchRequest) MediaLabel

func (r *SearchRequest) MediaLabel() string

MediaLabel returns a human-readable label for log messages. Movies: "Inception (2010)", episodes: "Bleach (2004) - S09E15".

type SearchResult

type SearchResult struct {
	Langs           []LangOutcome // one entry per language group, in target order
	CoverageChanged bool          // true if RecordSubtitleFiles detected changes on disk
}

SearchResult holds the outcome of a SearchTargets call: one typed entry per language group plus the coverage-inventory flag.

func (*SearchResult) Paths

func (r *SearchResult) Paths() []string

Paths returns every subtitle file downloaded across all language groups.

func (*SearchResult) ProviderQueried added in v0.1.148

func (r *SearchResult) ProviderQueried() bool

ProviderQueried reports whether at least one provider query was actually issued during the call, across all language groups. This is the inter-item pacing signal: the scan delay exists to space real provider traffic, so an item that touched no provider (fully covered, locked, in adaptive backoff, or with every eligible provider health-timed-out) must not pay it. Deliberately not keyed on TargetsSearched(): that counts variant targets and stays positive even when the sweep sent zero requests.

func (*SearchResult) TargetsBackedOff added in v0.1.148

func (r *SearchResult) TargetsBackedOff() int

TargetsBackedOff returns the number of language groups that needed a search but had every provider in adaptive backoff.

func (*SearchResult) TargetsSearched added in v0.1.148

func (r *SearchResult) TargetsSearched() int

TargetsSearched returns the number of variant targets that ran against provider results across all language groups.

type SearchTarget

type SearchTarget struct {
	MinScore  *int     `json:"min_score,omitempty"`
	Code      string   `json:"code"`
	Variant   string   `json:"variant"`
	Providers []string `json:"providers,omitempty"`
	Exclude   []string `json:"exclude,omitempty"`
}

SearchTarget describes a single subtitle search target in the API response.

type SearchTargets added in v0.1.29

type SearchTargets struct {
	OrigLang   string         `json:"orig_lang"`
	AudioLangs []string       `json:"audio_langs"`
	Targets    []SearchTarget `json:"targets"`
}

SearchTargets is the JSON response for GET /api/search/targets.

type ServerConfig

type ServerConfig interface {
	ServerPort() int
	PollInterval() time.Duration
	LoggingLevel() LogLevel
	LoggingFormat() LogFormat
}

ServerConfig provides server runtime configuration.

type SetupStatus added in v0.1.29

type SetupStatus struct {
	SetupRequired bool `json:"setup_required"`
	ConfigValid   bool `json:"config_valid"`
}

SetupStatus is the JSON response for GET /api/auth/setup.

type ShowSubtitleCounter

type ShowSubtitleCounter interface {
	// CountShowSubtitles returns the total number of subtitles available for
	// a show in the given language. The request should have ImdbID set and
	// Season/Episode set to 0.
	CountShowSubtitles(ctx context.Context, imdbID, lang string) (int, error)
}

ShowSubtitleCounter can count total subtitles for a show+language without specifying season/episode. Used for show-level pre-checks: if a show has very few subtitles relative to its episode count, skip the entire series. Only providers with show-level query support implement this (OpenSubtitles).

type SignalData added in v0.1.29

type SignalData struct {
	RPID          string   `json:"rp_id"`
	UserID        string   `json:"user_id"`
	Name          string   `json:"name"`
	DisplayName   string   `json:"display_name"`
	CredentialIDs []string `json:"credential_ids"`
}

SignalData is the JSON response for GET /api/auth/webauthn/signal-data.

type SonarrClient added in v0.1.106

type SonarrClient interface {
	Ping(ctx context.Context) error
	GetSeries(ctx context.Context) ([]arrapi.Series, error)
	GetEpisodes(ctx context.Context, seriesID int) ([]arrapi.Episode, error)
	GetSeriesByID(ctx context.Context, id int) (arrapi.Series, error)
	GetEpisodeByID(ctx context.Context, id int) (arrapi.Episode, error)
	GetHistorySince(ctx context.Context, since time.Time, eventTypes ...arrapi.EventType) ([]arrapi.HistoryRecord, error)
	GetWantedEpisodes(ctx context.Context, excludeTagIDs map[int]struct{}, fn func(arrapi.Series, arrapi.Episode) error) error
	ResolveExcludeTagIDs(ctx context.Context, tagNames []string, logMissing bool) map[int]struct{}
	RescanSeries(ctx context.Context, seriesID int) error
}

SonarrClient is the Sonarr-side surface subflux consumes: library reads, per-item lookups, import-history polling, wanted-episode iteration, exclude-tag resolution, and a post-download rescan.

type StateEntry

type StateEntry struct {
	MediaImported time.Time  `json:"media_imported"`
	Title         string     `json:"title"`
	MediaID       string     `json:"media_id"`
	Language      string     `json:"language"`
	Variant       Variant    `json:"variant"`
	Provider      ProviderID `json:"provider"`
	Path          string     `json:"path"`
	ReleaseName   string     `json:"release_name"`
	ImdbID        string     `json:"imdb_id,omitempty"`
	MediaType     MediaType  `json:"media_type"`
	ID            int64      `json:"id"`
	Score         int        `json:"score"`
	Season        int        `json:"season,omitempty"`
	Episode       int        `json:"episode,omitempty"`
	Manual        bool       `json:"manual"`
}

StateEntry represents a subtitle state record for API responses.

type StateQuery

type StateQuery struct {
	MediaType MediaType
	Language  string
	Provider  ProviderID
	Search    string
	Limit     int
	Offset    int
}

StateQuery groups the filter parameters for QueryStore.GetState. Using a named struct (rather than positional string/int arguments) makes call sites self-documenting and prevents silent swaps of same-typed fields like Language/Provider/Search.

type Stats added in v0.1.29

type Stats struct {
	LastScan            string `json:"last_scan"`
	Downloads           int    `json:"downloads"`
	Attempts            int64  `json:"attempts"`
	ScanIntervalSeconds int    `json:"scan_interval_seconds"`
	TotalSubs           int    `json:"total_subs"`
	TotalSeries         int    `json:"total_series"`
	TotalMovies         int    `json:"total_movies"`
	MissingSubs         int    `json:"missing_subs"`
	Partial             bool   `json:"partial"`
}

Stats is the JSON response for GET /api/stats.

type StatusResponse added in v0.1.148

type StatusResponse struct {
	Status string `json:"status"`
}

StatusResponse is the canonical {"status": "..."} operation-result body used by action endpoints (scan triggers, resets, logout).

type Store

Store persists search state and subtitle state. All methods accept context.Context for cancellation and timeout propagation.

Methods are grouped by domain concern into composable sub-interfaces defined in store_iface.go. Consumers should accept the narrowest sub-interface that satisfies their needs.

type Subtitle

type Subtitle struct {
	Provider    ProviderID
	ID          string // provider-specific identifier
	Language    string // ISO 639-1 code
	ReleaseName string
	DownloadURL string
	MatchedBy   MatchMethod // how this subtitle was matched (hash, title, imdb, etc.)
	Title       string      // show/movie title from provider (for identity validation)
	Year        int         // year from provider (0 if unknown)
	Season      int         // season from provider (0 if unknown or movie)
	Episode     int         // episode from provider (0 if unknown or movie)
	HearingImp  bool
	Forced      bool
}

Subtitle is a search result returned by a provider.

type SubtitleCue

type SubtitleCue struct {
	Text  string
	Start time.Duration
	End   time.Duration
}

SubtitleCue represents a single subtitle entry with timing.

type SubtitleEntry added in v0.1.29

type SubtitleEntry struct {
	MediaID   string `json:"media_id"`
	Language  string `json:"language"`
	Variant   string `json:"variant"`
	Source    string `json:"source"`
	Codec     string `json:"codec,omitempty"`
	Path      string `json:"-"`
	VideoPath string `json:"-"`
	Score     int    `json:"score,omitempty"`
	Ordinal   int    `json:"ordinal,omitempty"`
	OffsetMs  int64  `json:"offset_ms,omitempty"`
}

SubtitleEntry is a subtitle-file row from coverage queries. On the wire it carries NO filesystem paths (S7: clients address files by typed reference, never by path); Path and VideoPath stay populated for in-process consumers (file listing size stat, deletion, reconciliation) but are json-omitted. Ordinal is the manual-sibling number parsed from the row's filename (api.ManualOrdinal) — together with (media_type, media_id, language, variant, source) it forms the wire FileRef the client echoes back to address this exact file.

type SubtitleFile

type SubtitleFile struct {
	Language string         // ISO 639-1
	Variant  Variant        // "standard", "hi", "forced"
	Source   SubtitleSource // SourceEmbedded or SourceExternal
	Codec    string         // e.g. "subrip", "ass"; empty for external
	Path     string         // file path for external; empty for embedded
}

SubtitleFile represents a discovered subtitle (embedded or external) for a media file.

type SubtitleInfo

type SubtitleInfo struct {
	HashVerifiable bool
}

SubtitleInfo describes a subtitle result for scoring purposes.

type SubtitlePostProcessor

type SubtitlePostProcessor interface {
	SyncAndPostProcess(ctx context.Context, data []byte, videoPath, lang string, variant Variant) (synced []byte, offsetMs int64)
	HashFile(ctx context.Context, path string) (hash string, size int64, err error)
}

SubtitlePostProcessor handles post-download subtitle processing.

type SubtitleProcessor

type SubtitleProcessor interface {
	// NormalizeEncoding converts subtitle data to UTF-8 from detected encoding.
	NormalizeEncoding(data []byte) []byte
	// ParseSRT parses SRT subtitle data into individual cues.
	ParseSRT(data []byte) ([]SubtitleCue, error)
	// WriteSRT serializes cues back to SRT format.
	WriteSRT(cues []SubtitleCue) ([]byte, error)
	// ShiftCues applies a timing offset to all cues.
	ShiftCues(cues []SubtitleCue, offset time.Duration) []SubtitleCue
	// SyncFromAudio runs audio-based sync on subtitle data against the video.
	SyncFromAudio(ctx context.Context, data []byte, videoPath, subtitlePath string) AudioSyncResult
}

SubtitleProcessor provides low-level SRT manipulation operations. Used by sync handlers to avoid importing the subsync package directly.

type SubtitleSearcher

type SubtitleSearcher interface {
	SearchTargets(ctx context.Context, req *SearchRequest, videoPath string, targets []SubtitleTarget) (SearchResult, error)
	// InventoryCoverage records the on-disk/embedded subtitle inventory for a
	// media item WITHOUT any provider work, stamping its scan state as
	// inventoried-not-searched. Used by scan skip paths (season early stop,
	// show-level skip) so coverage stays truthful for items the scanner
	// deliberately does not search.
	InventoryCoverage(ctx context.Context, req *SearchRequest, videoPath string) (coverageChanged bool)
}

SubtitleSearcher orchestrates subtitle search across providers.

type SubtitleSource

type SubtitleSource string

SubtitleSource identifies where a subtitle was found.

const (
	// SourceEmbedded indicates a subtitle embedded in the video container.
	SourceEmbedded SubtitleSource = "embedded"
	// SourceExternal indicates a subtitle in a separate file on disk.
	SourceExternal SubtitleSource = "external"
)

type SubtitleTargJSON

type SubtitleTargJSON struct {
	MinScore  *int     `json:"min_score,omitempty"`
	Code      string   `json:"code"`
	Variant   string   `json:"variant,omitempty"`
	Variants  []string `json:"variants,omitempty"`
	Providers []string `json:"providers,omitempty"`
	Exclude   []string `json:"exclude,omitempty"`
}

SubtitleTargJSON is a JSON-friendly subtitle target.

type SubtitleTarget

type SubtitleTarget struct {
	MinScore  *int
	Code      string
	Variant   Variant
	Variants  []string
	Providers []ProviderID
	Exclude   []ProviderID
}

SubtitleTarget defines a single subtitle to search for.

func (*SubtitleTarget) EffectiveVariant

func (t *SubtitleTarget) EffectiveVariant() Variant

EffectiveVariant returns the variant, defaulting to DefaultVariant.

type SyncConfig

type SyncConfig struct {
	// SyncSubtitles enables automatic timing sync against embedded
	// reference subtitles when a new subtitle is downloaded.
	SyncSubtitles bool `json:"sync_subtitles"`
	// AudioSyncFallback enables audio-based sync as a fallback when
	// no embedded reference subtitle is available or sync fails.
	AudioSyncFallback bool `json:"audio_sync_fallback"`
	// SyncMinConfidence is the minimum confidence threshold (0.0–1.0) for
	// accepting an automatic sync result. Defaults to DefaultSyncMinConfidence when zero.
	SyncMinConfidence float64 `json:"sync_min_confidence,omitempty"`
}

SyncConfig controls subtitle synchronization on import.

type SyncOffsetStore

type SyncOffsetStore interface {
	SetSyncOffset(ctx context.Context, path string, offsetMs int64) error
	GetSyncOffset(ctx context.Context, path string) (int64, error)
}

SyncOffsetStore groups subtitle timing adjustment persistence.

type UIConfigProvider

type UIConfigProvider interface {
	LanguageRulesForUI() LanguageRulesJSON
}

UIConfigProvider provides UI-specific configuration.

type Variant

type Variant string

Variant is a typed string enum for subtitle variant identifiers. Valid variants: standard (default), hi (hearing impaired), forced.

func VariantFromFlags

func VariantFromFlags(hi, forced bool) Variant

VariantFromFlags derives the variant from HI/forced flags on a picked subtitle. HI wins over forced when both happen to be set.

type VideoInfo

type VideoInfo struct {
	ReleaseGroup string    // Full release name for parsing.
	MediaType    MediaType // "episode" or "movie"
}

VideoInfo describes the video file for scoring purposes. Only MediaType and ReleaseGroup are used by the match builder.

type WebAuthnUnknownCredentialResponse

type WebAuthnUnknownCredentialResponse struct {
	Error  string `json:"error"`
	Signal string `json:"signal"`
}

WebAuthnUnknownCredentialResponse signals an unknown credential to the client.

Jump to

Keyboard shortcuts

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