scanning

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: 16 Imported by: 0

Documentation

Overview

Package scanning implements the full-scan orchestration engine.

It iterates all wanted episodes and movies from arr APIs, sorts them alphabetically, searches for missing subtitles, and records results. The scheduling infrastructure (timers, goroutine lifecycle) remains in the parent server package.

Index

Constants

View Source
const (
	ScanFound     = api.ScanFound
	ScanSkipped   = api.ScanSkipped
	ScanNoResult  = api.ScanNoResult
	ScanBackedOff = api.ScanBackedOff
)

Scan outcome constants re-exported from api for local use.

Variables

This section is empty.

Functions

func EpisodeSearchRequest

func EpisodeSearchRequest(series *arrapi.Series, ep *arrapi.Episode, langs []string) api.SearchRequest

EpisodeSearchRequest builds a SearchRequest from arr Series+Episode data. This is the single source of truth for the episode→SearchRequest mapping, used by both scanning and polling.

func ExtractAltTitles

func ExtractAltTitles(alts []arrapi.AlternateTitle, primary string) []string

ExtractAltTitles returns unique alternative titles, excluding the primary.

func FinishScanActivity added in v0.1.148

func FinishScanActivity(unregister func(), tracker ActivityTracker, events EventPublisher,
	actID, action, detail string, source activity.ActivitySource, outcome activity.Outcome,
)

FinishScanActivity applies a runner's terminal outcome to its activity entry and publishes the scan:done event. This is the ONLY outcome→terminal mapping: completed→End, failed→Fail, cancelled→FinishCancelled (the terminal Done+Cancelled+EndedAt state). Shutdown performs no user-facing marking and publishes nothing — the process is exiting and the in-memory ring dies with it; collapsing shutdown into failed or cancelled would lie on both.

unregister — the stop-registration release — runs FIRST, before the terminal transition: the instant Done becomes observable the entry must no longer report cancellable, so a cancel arriving after completion answers 409, never a 204 for work that is already done. Callers keep a deferred unregister as the panic fallback; the release is idempotent.

func MovieSearchRequest

func MovieSearchRequest(m *arrapi.Movie, langs []string) api.SearchRequest

MovieSearchRequest builds a SearchRequest from arr Movie data. This is the single source of truth for the movie→SearchRequest mapping, used by both scanning and polling.

func RunFullScan

func RunFullScan(ctx context.Context, stop <-chan struct{}, deps *Deps, ls *LiveState, actID string) activity.Outcome

RunFullScan iterates all wanted episodes and movies, searching for missing subtitles. Episodes and movies are sorted alphabetically by title.

The activity entry is started by the CALLER (HTTP handler or scheduler) at the accept boundary — actID identifies it; the returned outcome is applied by the caller via FinishScanActivity. The stop channel is the graceful cancel signal, checked between items only: the context stays server-derived and reaches the current item's provider calls, so cancelling it is a hard kill reserved for process shutdown.

func ScanItemSeasonEp

func ScanItemSeasonEp(item ScanItem) (season, episode int)

ScanItemSeasonEp returns season and episode for sorting.

func ScanItemTitle

func ScanItemTitle(item ScanItem) string

ScanItemTitle returns the title used for sort ordering.

func SceneOrPath

func SceneOrPath(sceneName, filePath string) string

SceneOrPath returns sceneName if non-empty, otherwise filePath.

func SkipResumed

func SkipResumed(item ScanItem, recent map[string]bool, stats *api.ScanStats) bool

SkipResumed checks if a scan item was already processed recently.

Types

type ActivityTracker

type ActivityTracker interface {
	Start(action, detail string, source activity.ActivitySource) string
	StartScan(action, detail string, source activity.ActivitySource,
		scope activity.ScanScope, role auth.Role) (id string, existing bool)
	End(id string)
	Fail(id string)
	FinishCancelled(id string)
	Progress(id string, current, total int, msg string)
	SetQueued(id string, queued bool)
	IsCancelled(id string) bool
}

ActivityTracker manages scan activity lifecycle.

type AlertRecorder

type AlertRecorder interface {
	Record(category, msg string)
	RecordInfo(msg string)
}

AlertRecorder records alerts visible in the UI.

type BGTracker

type BGTracker interface {
	Add(delta int)
	Done()
}

BGTracker allows the scanning handler to register background goroutines with the server's WaitGroup for graceful shutdown.

type BackoffPrefixReader added in v0.1.148

type BackoffPrefixReader interface {
	GetBackoffByPrefix(ctx context.Context, mediaType api.MediaType, mediaIDPrefix string) ([]api.BackoffEntry, error)
}

BackoffPrefixReader is the narrow store surface earlyStop seeding needs: the season's existing adaptive-backoff rows by media-id prefix.

type Deps

type Deps struct {
	DB ScanStore
	// Backoff feeds season-tracker earlyStop seeding from existing
	// adaptive-backoff rows. Optional: nil disables seeding.
	Backoff       BackoffPrefixReader
	Metrics       ScanMetrics
	Events        EventPublisher
	Activity      ActivityTracker
	Alerts        AlertRecorder
	ShowSkipCache *showskip.Cache
	// ClearCaches clears provider download caches after scan completion.
	ClearCaches func(providers []api.Provider)
}

Deps holds the narrow dependencies the scan orchestration needs from the server. This avoids importing the full Server struct.

type EventPublisher

type EventPublisher interface {
	PublishCoverageUpdate(mediaType api.MediaType, mediaID string)
	PublishScanStart(action, detail string, source activity.ActivitySource, actID string)
	PublishScanDone(action, detail string, source activity.ActivitySource, actID string, outcome activity.Outcome)
}

EventPublisher publishes events to SSE clients. Scan events carry the activity id (both) and the terminal outcome (scan:done).

type Handler

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

Handler provides HTTP handlers for the /api/scan/* endpoints.

func NewHandler

func NewHandler(deps HandlerDeps) *Handler

NewHandler creates a scan Handler with the given dependencies.

func (*Handler) HandleScanItem

func (h *Handler) HandleScanItem(w http.ResponseWriter, r *http.Request)

HandleScanItem scans a single episode or movie. POST /api/scan/item with JSON body — 202 + activity_id.

func (*Handler) HandleScanMovie

func (h *Handler) HandleScanMovie(w http.ResponseWriter, r *http.Request)

HandleScanMovie scans all missing subtitles for a specific movie. POST /api/scan/movie/{radarrId} — 202 + activity_id.

func (*Handler) HandleScanSeason

func (h *Handler) HandleScanSeason(w http.ResponseWriter, r *http.Request)

HandleScanSeason scans all missing subtitles for a specific season. POST /api/scan/season/{sonarrId}/{seasonNum} — 202 + activity_id.

func (*Handler) HandleScanSeries

func (h *Handler) HandleScanSeries(w http.ResponseWriter, r *http.Request)

HandleScanSeries scans all missing subtitles for a specific series. POST /api/scan/series/{sonarrId} — 202 + activity_id at accept time.

type HandlerDeps

type HandlerDeps struct {
	// StateFunc returns the current live state snapshot as BOTH views one
	// operation needs — the handler view and the scanning-engine view —
	// derived from ONE generation read, so the two views can never straddle
	// a hot reload.
	StateFunc func() (*HandlerState, *LiveState)
	// CtxFunc returns the server-level context (outlives individual requests).
	CtxFunc func() context.Context
	// ScanDeps provides the scanning engine dependencies (stable server
	// singletons, not generation state).
	ScanDeps func() *Deps
	// Activity tracks scan activity lifecycle.
	Activity ActivityTracker
	// Stops registers the graceful stop callbacks of running scans.
	Stops *activity.StopRegistry
	// ScanGuard serializes manual scan requests.
	ScanGuard *ScanGuard
	// Alerts records alerts visible in the UI.
	Alerts AlertRecorder
	// Events publishes SSE events.
	Events EventPublisher
	// InvalidateStats clears the stats cache after scan completion.
	InvalidateStats func()
	// BGTracker tracks background goroutine lifecycle for graceful shutdown.
	BGTracker BGTracker
}

HandlerDeps holds the dependencies for the scan HTTP handler family.

type HandlerState

type HandlerState struct {
	Cfg    api.ConfigProvider
	Engine api.SearchEngine
	Sonarr ScanHandlerSonarr // nil when sonarr not configured
	Radarr ScanHandlerRadarr // nil when radarr not configured
}

HandlerState holds the runtime state needed by scan HTTP handlers. Provided by the server on each request via the StateFunc callback.

type LiveState

type LiveState struct {
	Cfg         api.ConfigProvider
	Engine      api.SearchEngine
	Sonarr      ScanSonarrClient
	Radarr      ScanRadarrClient
	ShowCounter api.ShowSubtitleCounter
	Providers   []api.Provider
}

LiveState holds the runtime state needed for a scan pass.

type ScanAccepted added in v0.1.148

type ScanAccepted struct {
	ActivityID string `json:"activity_id"`
	Status     string `json:"status"`
}

ScanAccepted is the typed 202 Accepted response for scan starts. The scan executes in a server-owned background goroutine; GET /api/activity is the status monitor for the returned activity id (RFC 9110 202 guidance). Deliberately a separate type from manualops.DownloadAccepted: the two docs diverge.

type ScanGuard

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

ScanGuard serializes manual scan requests. Extracted from activity.Log to separate coordination concerns from data concerns. The slot is a capacity-1 channel semaphore rather than a mutex so acquisition can be SELECTED against the stop signal and the server context: a queued scan whose stop arrives reaches its terminal cancelled state immediately, and shutdown never blocks behind a running scan — a bare mutex Lock could observe neither. The zero value is ready to use (the Server embeds one).

func (*ScanGuard) Acquire added in v0.1.148

func (g *ScanGuard) Acquire(ctx context.Context, stop <-chan struct{}) bool

Acquire takes the scan slot, blocking until it is free — or gives up when the stop signal fires or ctx (the server context) is cancelled first, in which case the slot is NOT held and the caller must not Release. It reports whether the slot was acquired. When the slot frees at the same instant a signal fires, either case may win; callers re-check the signals after a successful acquisition.

func (*ScanGuard) Release added in v0.1.148

func (g *ScanGuard) Release()

Release frees the scan slot. Only a successful Acquire may Release; a release without a held slot is a programming error and panics (matching sync.Mutex.Unlock semantics).

type ScanHandlerRadarr added in v0.1.106

type ScanHandlerRadarr interface {
	GetMovieByID(ctx context.Context, id int) (arrapi.Movie, error)
}

ScanHandlerRadarr is the Radarr surface the manual scan HTTP handlers call.

type ScanHandlerSonarr added in v0.1.106

type ScanHandlerSonarr interface {
	GetSeriesByID(ctx context.Context, id int) (arrapi.Series, error)
	GetEpisodes(ctx context.Context, seriesID int) ([]arrapi.Episode, error)
}

ScanHandlerSonarr is the Sonarr surface the manual scan HTTP handlers call.

type ScanItem

type ScanItem struct {
	Series *arrapi.Series  // non-nil for episodes
	Ep     *arrapi.Episode // non-nil for episodes
	Movie  *arrapi.Movie   // non-nil for movies
}

ScanItem holds either an episode or a movie for alphabetical scanning.

func SortByTitle

func SortByTitle(episodes, movies []ScanItem) []ScanItem

SortByTitle merges episodes and movies into a single slice sorted alphabetically by title (case-insensitive).

type ScanMetrics

type ScanMetrics interface {
	RecordScan(items, found int, dur time.Duration)
	AdaptiveSkip()
}

ScanMetrics records scan-level metrics.

type ScanOutcome

type ScanOutcome = api.ScanOutcome

ScanOutcome is a type alias for api.ScanOutcome.

func ScanEpisode

func ScanEpisode(ctx context.Context, deps *Deps, ls *LiveState, series *arrapi.Series, ep *arrapi.Episode, forceUpgrade ...bool) (ScanOutcome, []api.LangOutcome, bool)

ScanEpisode searches for subtitles for a single episode. Returns the scan outcome, the typed per-language outcomes from the engine — the season tracker records evidence only for entries whose Kind is api.LangSearched (a skipped or backed-off language must never accrue a false no-result streak) — and whether the search actually queried any provider (the inter-item pacing signal: callers skip the scan delay for items that generated no provider traffic).

func ScanMovie

func ScanMovie(ctx context.Context, deps *Deps, ls *LiveState, m *arrapi.Movie, forceUpgrade ...bool) (ScanOutcome, bool)

ScanMovie searches for subtitles for a single movie. The second return reports whether any provider was actually queried (the inter-item pacing signal; see ScanEpisode).

type ScanRadarrClient added in v0.1.106

type ScanRadarrClient interface {
	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
}

ScanRadarrClient is the Radarr surface the full-scan engine needs.

type ScanSonarrClient added in v0.1.106

type ScanSonarrClient interface {
	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
}

ScanSonarrClient is the Sonarr surface the full-scan engine needs: wanted-episode iteration, exclude-tag resolution, and a post-download rescan.

type ScanStore

type ScanStore interface {
	RecentlyScanned(ctx context.Context, cutoff time.Time) (map[string]bool, error)
	RecordScanState(ctx context.Context, rec *api.ScanRecord) error
	// Scan-cycle mark (duration-aware resume): set when a full scan begins,
	// cleared on normal completion. A dangling mark at the next scan start
	// means the previous cycle was interrupted; the resume cutoff extends
	// back to that cycle's start so a pass longer than scan_interval keeps
	// its early segment in the resume set. ScanCycleStart returns the zero
	// time when no mark is stored.
	ScanCycleStart(ctx context.Context) (time.Time, error)
	SetScanCycleStart(ctx context.Context, t time.Time) error
	ClearScanCycleStart(ctx context.Context) error
}

ScanStore is the narrow store interface for scan state tracking.

Jump to

Keyboard shortcuts

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