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
- func EpisodeSearchRequest(series *arrapi.Series, ep *arrapi.Episode, langs []string) api.SearchRequest
- func ExtractAltTitles(alts []arrapi.AlternateTitle, primary string) []string
- func FinishScanActivity(unregister func(), tracker ActivityTracker, events EventPublisher, ...)
- func MovieSearchRequest(m *arrapi.Movie, langs []string) api.SearchRequest
- func RunFullScan(ctx context.Context, stop <-chan struct{}, deps *Deps, ls *LiveState, ...) activity.Outcome
- func ScanItemSeasonEp(item ScanItem) (season, episode int)
- func ScanItemTitle(item ScanItem) string
- func SceneOrPath(sceneName, filePath string) string
- func SkipResumed(item ScanItem, recent map[string]bool, stats *api.ScanStats) bool
- type ActivityTracker
- type AlertRecorder
- type BGTracker
- type BackoffPrefixReader
- type Deps
- type EventPublisher
- type Handler
- type HandlerDeps
- type HandlerState
- type LiveState
- type ScanAccepted
- type ScanGuard
- type ScanHandlerRadarr
- type ScanHandlerSonarr
- type ScanItem
- type ScanMetrics
- type ScanOutcome
- type ScanRadarrClient
- type ScanSonarrClient
- type ScanStore
Constants ¶
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 ¶
ScanItemSeasonEp returns season and episode for sorting.
func ScanItemTitle ¶
ScanItemTitle returns the title used for sort ordering.
func SceneOrPath ¶
SceneOrPath returns sceneName if non-empty, otherwise filePath.
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 ¶
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
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
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.
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 ¶
SortByTitle merges episodes and movies into a single slice sorted alphabetically by title (case-insensitive).
type ScanMetrics ¶
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.