Documentation
¶
Overview ¶
Package manualops implements the business logic for manual subtitle search and download operations. The HTTP handler glue remains in the parent server package; this package owns validation, query parsing, result building, and the background download pipeline.
Index ¶
- Constants
- Variables
- func IsValidLangCode(lang string) bool
- func LookupEpisodeMediaID(ctx context.Context, ls *LiveState, seriesID, season, episode int) string
- func LookupMediaTitle(ctx context.Context, ls *LiveState, mediaType api.MediaType, arrID int) string
- func LookupMovieMediaID(ctx context.Context, ls *LiveState, arrID int) string
- func NotifyError(deps *SearchDeps, source, alertMsg, uiMsg string)
- func ParseSearchQuery(r *http.Request) (req api.SearchRequest, lang string, mediaType api.MediaType, arrID int)
- func PostDownloadUpdate(ctx context.Context, ls *LiveState, db DownloadStore, req *DownloadRequest, ...)
- func QueryInt(q interface{ ... }, key string) int
- func ResolveMediaIDs(ctx context.Context, ls *LiveState, mediaType api.MediaType, ...) (coverageID, historyID string)
- func RunClearLock(ctx context.Context, deps *SearchDeps, mediaType, mediaID, language string, ...) error
- func RunDownload(ctx context.Context, deps *SearchDeps, ls *LiveState, db DownloadStore, ...) bool
- func TryComputeHash(ctx context.Context, ls *LiveState, req *api.SearchRequest, filePath string)
- func ValidateDownloadRequest(req *DownloadRequest) error
- type ActivityTracker
- type BGTracker
- type DownloadAccepted
- type DownloadRequest
- type DownloadStore
- type EventPublisher
- type Handler
- func (h *Handler) HandleClearLock(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleManualDownload(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleManualSearch(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleSearchResolve(w http.ResponseWriter, r *http.Request)
- type HandlerDeps
- type LiveState
- type ManualRadarrClient
- type ManualSearchResponse
- type ManualSonarrClient
- type ResolveCandidate
- type ResolveQueryParams
- type ResolveRadarrClient
- type ResolveResponse
- type ResolveSearchIDs
- type ResolveSonarrClient
- type ResolvedItem
- type SearchDeps
- type SearchResult
- type SearchStore
Constants ¶
const DownloadTimeout = 5 * time.Minute
DownloadTimeout is the context timeout for manual downloads.
const MaxLangCodeLen = 20
MaxLangCodeLen caps language code length. BCP 47 codes are typically ≤11 chars (e.g. "pt-BR"); 20 provides headroom for unusual subtags.
const MaxResults = 50
MaxResults caps the number of results returned by manual search.
Variables ¶
var ( ErrMissingRequired = errors.New("provider, subtitle_id, media_id, and language are required") ErrInvalidLangCode = errors.New("invalid language code") ErrInvalidMediaType = errors.New("invalid media_type") ErrMissingEpisode = errors.New("season and episode are required for episode downloads") )
Validation errors returned by ValidateDownloadRequest.
Functions ¶
func IsValidLangCode ¶
IsValidLangCode rejects language codes that are too long, contain path separators, traversal sequences, or control characters (including null bytes that cause path truncation).
func LookupEpisodeMediaID ¶
LookupEpisodeMediaID finds the tvdb-based media ID for a Sonarr episode.
func LookupMediaTitle ¶
func LookupMediaTitle(ctx context.Context, ls *LiveState, mediaType api.MediaType, arrID int) string
LookupMediaTitle resolves the title for a media item from the arr client.
func LookupMovieMediaID ¶
LookupMovieMediaID finds the tmdb-based media ID for a Radarr movie.
func NotifyError ¶
func NotifyError(deps *SearchDeps, source, alertMsg, uiMsg string)
NotifyError publishes an error notification and records an alert.
func ParseSearchQuery ¶
func ParseSearchQuery(r *http.Request) (req api.SearchRequest, lang string, mediaType api.MediaType, arrID int)
ParseSearchQuery extracts search parameters from the request URL. The optional media_id parameter is the ARR internal ID (Radarr movie ID / Sonarr series ID): with season/episode it forms the MediaRef the handler resolves server-side for hash computation — the former client-supplied ?file= path parameter is gone (S7).
func PostDownloadUpdate ¶
func PostDownloadUpdate(ctx context.Context, ls *LiveState, db DownloadStore, req *DownloadRequest, mediaType api.MediaType, coverageMediaID, subPath string, variant api.Variant, )
PostDownloadUpdate updates coverage DB and refreshes the arr after a successful manual download.
func QueryInt ¶
QueryInt parses a URL query parameter as a non-negative integer, returning 0 on missing, invalid, or negative values.
func ResolveMediaIDs ¶
func ResolveMediaIDs(ctx context.Context, ls *LiveState, mediaType api.MediaType, arrID, season, episode int, ) (coverageID, historyID string)
ResolveMediaIDs determines the coverage and history media IDs for a manual download.
func RunClearLock ¶
func RunClearLock(ctx context.Context, deps *SearchDeps, mediaType, mediaID, language string, variant api.Variant) error
RunClearLock clears the manual lock for a media+language combination. An empty variant clears the locks of every variant of the language; a specific variant clears only that quad's lock.
func RunDownload ¶
func RunDownload(ctx context.Context, deps *SearchDeps, ls *LiveState, db DownloadStore, prov api.Provider, req *DownloadRequest, actID string, ) bool
RunDownload performs the actual download, post-processing, and save. actID is the download's activity entry: on a successful save the entry's detail is updated with the saved subtitle path, which is how activity consumers (the remote CLI's poll loop) learn where the file landed. Returns true on success.
func TryComputeHash ¶
TryComputeHash attempts to compute the video hash for the search request. Logs warnings on validation or hash failures; updates req in place on success.
func ValidateDownloadRequest ¶
func ValidateDownloadRequest(req *DownloadRequest) error
ValidateDownloadRequest checks that the download request has all required fields and valid values. It normalises MediaType to "movie" when empty. Returns nil on success.
Types ¶
type ActivityTracker ¶
type ActivityTracker interface {
Start(action, detail string, source activity.ActivitySource) string
End(id string)
Fail(id string)
Progress(id string, current, total int, detail string)
}
ActivityTracker manages activity lifecycle. Progress doubles as the detail mutator: download completion writes the saved subtitle path into the entry detail so activity consumers (the remote CLI's poll loop) can report it.
type BGTracker ¶
type BGTracker interface {
Add(delta int)
Done()
}
BGTracker allows the handler to register background goroutines for graceful shutdown tracking.
type DownloadAccepted ¶ added in v0.1.148
type DownloadAccepted struct {
ActivityID string `json:"activity_id"`
Status string `json:"status"`
}
DownloadAccepted is the typed 202 Accepted response for manual downloads.
type DownloadRequest ¶
type DownloadRequest struct {
Provider api.ProviderID `json:"provider"`
SubtitleID string `json:"subtitle_id"`
Language string `json:"language"`
ReleaseName string `json:"release_name,omitempty"`
MediaType api.MediaType `json:"media_type,omitempty"`
Score int `json:"score,omitempty"`
Season int `json:"season,omitempty"`
Episode int `json:"episode,omitempty"`
ArrID int `json:"media_id,omitempty"`
TopPick bool `json:"top_pick,omitempty"`
HearingImp bool `json:"hearing_impaired,omitempty"`
Forced bool `json:"forced,omitempty"`
// contains filtered or unexported fields
}
DownloadRequest holds the parsed fields for a manual download. The video is addressed by MediaRef only (media_type + media_id [arr ID] + season/episode); the server resolves the video file path from the arr — the wire carries no file path (S7). videoPath is the server-resolved value, set by the handler after resolution, never decoded from JSON.
func (*DownloadRequest) SetVideoPath ¶ added in v0.1.148
func (req *DownloadRequest) SetVideoPath(path string)
SetVideoPath records the server-resolved video path on the request. Called by the HTTP handler after MediaRef resolution (and by tests); the download pipeline reads it via VideoPath.
func (*DownloadRequest) VideoPath ¶ added in v0.1.148
func (req *DownloadRequest) VideoPath() string
VideoPath returns the server-resolved video file path.
type DownloadStore ¶
type DownloadStore interface {
SearchStore
NextManualNumber(ctx context.Context, mediaType api.MediaType, mediaID, language string, variant api.Variant) int
UpsertSubtitleFile(ctx context.Context, mediaType api.MediaType, mediaID string, sf *api.SubtitleFile) error
SetSyncOffset(ctx context.Context, path string, offsetMs int64) error
SaveDownload(ctx context.Context, rec *api.DownloadRecord) error
}
DownloadStore is the narrow store interface for manual download operations.
type EventPublisher ¶
type EventPublisher interface {
PublishNotify(level events.NotifyLevel, text string)
PublishCoverageUpdate(mediaType api.MediaType, mediaID, language, source string)
}
EventPublisher publishes events to SSE clients.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler provides HTTP handlers for manual search and download endpoints.
func NewHandler ¶
func NewHandler(deps HandlerDeps) *Handler
NewHandler creates a manual ops Handler with the given dependencies.
func (*Handler) HandleClearLock ¶
func (h *Handler) HandleClearLock(w http.ResponseWriter, r *http.Request)
HandleClearLock handles POST /api/search/clear-lock.
func (*Handler) HandleManualDownload ¶
func (h *Handler) HandleManualDownload(w http.ResponseWriter, r *http.Request)
HandleManualDownload handles POST /api/search/download.
func (*Handler) HandleManualSearch ¶
func (h *Handler) HandleManualSearch(w http.ResponseWriter, r *http.Request)
HandleManualSearch handles GET /api/search?imdb=tt1234567&lang=fr&type=movie
func (*Handler) HandleSearchResolve ¶ added in v0.1.148
func (h *Handler) HandleSearchResolve(w http.ResponseWriter, r *http.Request)
HandleSearchResolve handles GET /api/search/resolve.
type HandlerDeps ¶
type HandlerDeps struct {
DBFunc func() DownloadStore
Activity ActivityTracker
Alerts activity.WarnRecorder
Events EventPublisher
StateFunc func() *LiveState
BGTracker BGTracker
ServerCtx func() context.Context
Resolve *resolve.Resolver
DecodeJSON func(w http.ResponseWriter, r *http.Request, v any, maxSize int64) bool
}
HandlerDeps holds the dependencies for the manual search/download HTTP handlers. Resolve is the S7 typed-reference resolver: the download verb and the manual-search hash computation address the video by MediaRef and the server resolves the file path from the arr — no client-supplied paths.
type LiveState ¶
type LiveState struct {
Cfg api.ConfigProvider
Engine api.SearchEngine
Scorer api.Scorer
Sonarr ManualSonarrClient
Radarr ManualRadarrClient
SonarrLib ResolveSonarrClient
RadarrLib ResolveRadarrClient
Providers []api.Provider
}
LiveState holds the runtime state needed for a manual search pass. Sonarr/Radarr are the narrow by-ID surfaces manual downloads use; SonarrLib/RadarrLib are the library-listing surfaces the resolve endpoint uses (all nil when the corresponding arr is not configured).
type ManualRadarrClient ¶ added in v0.1.106
type ManualRadarrClient interface {
GetMovieByID(ctx context.Context, movieID int) (arrapi.Movie, error)
RescanMovie(ctx context.Context, movieID int) error
}
ManualRadarrClient is the Radarr surface manual downloads use.
type ManualSearchResponse ¶
type ManualSearchResponse struct {
Results []SearchResult `json:"results"`
}
ManualSearchResponse is the typed response from RunSearch. It deliberately carries no lock state: manual locks are invisible infrastructure ("a manual pick is never overwritten"), not a user-facing concept, so the popup has nothing to display about them.
func RunSearch ¶
func RunSearch(ctx context.Context, deps *SearchDeps, ls *LiveState, req *api.SearchRequest, lang string, mediaType api.MediaType, filePath string, ) ManualSearchResponse
RunSearch executes the manual search against all providers and returns the JSON-ready response payload.
type ManualSonarrClient ¶ added in v0.1.106
type ManualSonarrClient interface {
GetSeriesByID(ctx context.Context, seriesID int) (arrapi.Series, error)
RescanSeries(ctx context.Context, seriesID int) error
}
ManualSonarrClient is the Sonarr surface manual downloads use: series lookup (for media-ID and title resolution) and a post-download rescan.
type ResolveCandidate ¶ added in v0.1.148
type ResolveCandidate struct {
MediaType api.MediaType `json:"media_type"`
Title string `json:"title"`
MediaID int `json:"media_id"`
Year int `json:"year,omitempty"`
}
ResolveCandidate is one ambiguity candidate; Year disambiguates equal titles.
type ResolveQueryParams ¶ added in v0.1.148
type ResolveQueryParams struct {
Season *int
Episode *int
Title string
Imdb string
Type string // "", "series", or "movie"
Tmdb int
}
ResolveQueryParams is the validated query surface of the resolve endpoint. Season and Episode carry PRESENCE, not just value: nil means the parameter was absent, while a non-nil zero is an explicit season=0 (specials) or episode=0 — an explicit zero narrows the expansion and marks the query episodic exactly like any other supplied value.
type ResolveRadarrClient ¶ added in v0.1.148
ResolveRadarrClient is the Radarr surface query resolution uses.
type ResolveResponse ¶ added in v0.1.148
type ResolveResponse struct {
Items []ResolvedItem `json:"items,omitempty"`
Candidates []ResolveCandidate `json:"candidates,omitempty"`
Resolved bool `json:"resolved"`
}
ResolveResponse is the typed result of GET /api/search/resolve. Exactly one of the following holds: Resolved with Items (success), Candidates (ambiguous — the client disambiguates), or all empty (no match).
func ResolveQuery ¶ added in v0.1.148
func ResolveQuery(ctx context.Context, ls *LiveState, p *ResolveQueryParams) (ResolveResponse, error)
ResolveQuery maps the validated query onto arr media items. With an explicit type only that arm runs (an unconfigured arr is then an error); with no type the deleted local resolver's fallback is preserved: series first, movies only when the series arm found nothing and no season/episode narrowing was requested — EXCEPT when a TMDB id was supplied, which is a movie-only criterion and therefore resolves the movie identity first (a supplied stable ID always outranks a title match). A conflict short-circuits; an arm failure surfaces only when the healthy arm could not satisfy the query (partial-arr rule).
type ResolveSearchIDs ¶ added in v0.1.148
type ResolveSearchIDs struct {
Imdb string `json:"imdb,omitempty"`
Tvdb int `json:"tvdb,omitempty"`
Tmdb int `json:"tmdb,omitempty"`
}
ResolveSearchIDs carries the stable identifiers of a resolved item for the follow-up search call.
type ResolveSonarrClient ¶ added in v0.1.148
type ResolveSonarrClient interface {
GetSeries(ctx context.Context) ([]arrapi.Series, error)
GetEpisodes(ctx context.Context, seriesID int) ([]arrapi.Episode, error)
}
ResolveSonarrClient is the Sonarr surface query resolution uses: the full series list for matching and per-series episodes for expansion.
type ResolvedItem ¶ added in v0.1.148
type ResolvedItem struct {
MediaType api.MediaType `json:"media_type"`
Title string `json:"title"`
SearchIDs ResolveSearchIDs `json:"search_ids"`
MediaID int `json:"media_id"`
Year int `json:"year,omitempty"`
Season int `json:"season,omitempty"`
Episode int `json:"episode,omitempty"`
}
ResolvedItem is one searchable media item: an episode of the matched series (file-bearing only) or the matched movie. MediaID is the arr ID (Sonarr series ID / Radarr movie ID) — the same identity the search and download endpoints consume.
type SearchDeps ¶
type SearchDeps struct {
DB SearchStore
Activity ActivityTracker
Alerts activity.WarnRecorder
Events EventPublisher
}
SearchDeps holds the narrow dependencies for manual search execution.
type SearchResult ¶
type SearchResult struct {
Matches map[string]int `json:"matches,omitempty"`
Provider api.ProviderID `json:"provider"`
Language string `json:"language"`
ReleaseName string `json:"release_name"`
MatchedBy string `json:"matched_by"`
SubtitleID string `json:"subtitle_id"`
// Tier is the score's quality-tier label, computed server-side by the
// scorer (same table as /api/score): the remote CLI renders it and has
// no scorer of its own.
Tier api.ScoreTier `json:"tier"`
Score int `json:"score"`
HearingImp bool `json:"hearing_impaired"`
Forced bool `json:"forced"`
OnDisk bool `json:"on_disk"`
}
SearchResult is a single result returned by the manual search API.
func BuildSearchResults ¶
func BuildSearchResults(scored []api.ScoredResult, refs []api.DownloadedRef, sc api.Scorer) []SearchResult
BuildSearchResults converts scored results to API response format. sc supplies the server-computed tier label per score (a nil scorer — only possible before the first successful wire — leaves tiers empty).
type SearchStore ¶
type SearchStore interface {
DownloadedRefs(ctx context.Context, mediaType api.MediaType, mediaID, language string) ([]api.DownloadedRef, error)
ClearManualLock(ctx context.Context, mediaType api.MediaType, mediaID, language string, variant api.Variant) error
}
SearchStore is the narrow store interface for manual search operations. ClearManualLock takes a variant; an empty variant means "all variants of the language" (see api.ManualLockStore).