Documentation
¶
Overview ¶
Package subtitles fetches missing subtitle languages from OpenSubtitles and serves them as external (sidecar) tracks.
Flow: Sidecars is called with the tracks embedded in the chosen release; for each of the user's preferred languages not already embedded it searches OpenSubtitles, ranks candidates (trusted uploaders, download count, release-name match — a moviehash match is impossible before the file exists), downloads the best, normalises it to UTF-8 SRT + WebVTT and caches both on disk. The returned core.StreamInfo carries an ExternalURL of the form "ether-subtitle:<id>"; the Plex front end resolves it with Open / OpenFormat.
Index ¶
- Constants
- Variables
- func AlignAgainst(cues, reference []Cue) (time.Duration, error)
- func MissingLanguages(user *core.User, embedded []core.StreamInfo) []string
- func MovieHash(r io.ReaderAt, size int64) (string, error)
- func Normalize(raw []byte) (srt, vtt string, err error)
- func NormalizeDialogue(s string) string
- func NormalizeLang(raw []byte, lang string) (srt, vtt string, err error)
- func Rank(cands []Candidate, releaseName string)
- func RenderSRT(cues []Cue) string
- func RenderVTT(cues []Cue) string
- func Test(ctx context.Context, cfg config.OpenSubtitles) error
- type AlignOptions
- type AlignSegment
- type Aligner
- type CachedSubtitle
- type Candidate
- type Cue
- type Options
- type Piecewise
- type Service
- func (s *Service) CachedSubtitles() ([]CachedSubtitle, error)
- func (s *Service) CuesForInfo(info core.MediaInfo) ([]Cue, bool)
- func (s *Service) DialogueTimeline(ctx context.Context, title *core.Title, lang, releaseName string, ...) ([]Cue, bool)
- func (s *Service) Enabled() bool
- func (s *Service) Enrich(ctx context.Context, title *core.Title, user *core.User, res *core.Resolution, ...)
- func (s *Service) Open(id string) (io.ReadCloser, string, error)
- func (s *Service) OpenFormat(id, format string) (io.ReadCloser, string, error)
- func (s *Service) Search(ctx context.Context, title *core.Title, lang, releaseName string) ([]Candidate, error)
- func (s *Service) SearchWithHash(ctx context.Context, title *core.Title, lang, releaseName, movieHash string) ([]Candidate, error)
- func (s *Service) Sidecars(ctx context.Context, title *core.Title, user *core.User, ...) ([]core.StreamInfo, error)
- func (s *Service) SidecarsForRelease(ctx context.Context, title *core.Title, user *core.User, ...) ([]core.StreamInfo, error)
- func (s *Service) SidecarsWithHash(ctx context.Context, title *core.Title, user *core.User, ...) ([]core.StreamInfo, error)
- func (s *Service) Sync(id string, opts SyncOptions) (string, error)
- func (s *Service) SyncHandler() http.Handler
- func (s *Service) TimelineAligner() *Aligner
- func (s *Service) UpdateConfig(cfg config.OpenSubtitles)
- type SyncOptions
Constants ¶
const URLPrefix = "ether-subtitle:"
URLPrefix prefixes the ExternalURL of sidecar streams from this package.
Variables ¶
var ErrQuota = errors.New("opensubtitles: download quota exhausted")
ErrQuota is returned when OpenSubtitles refuses a download because the account's (or anonymous IP's) daily download quota is used up.
Functions ¶
func AlignAgainst ¶
AlignAgainst estimates a constant offset using exact normalized text matches against embedded reference subtitles. It requires several independent cues and rejects inconsistent matches rather than guessing from unrelated dialogue.
func MissingLanguages ¶
func MissingLanguages(user *core.User, embedded []core.StreamInfo) []string
MissingLanguages returns the user's preferred languages (in preference order) that no embedded subtitle track provides.
func MovieHash ¶
MovieHash reads exactly the first and last 64 KiB. Call only on cached source ranges so hashing does not fetch a release during first-frame startup. Implemented from the published OpenSubtitles algorithm, without copied code.
func Normalize ¶
Normalize converts raw downloaded subtitle bytes to UTF-8 SRT and WebVTT, guessing a legacy encoding as Windows-1252.
func NormalizeDialogue ¶
NormalizeDialogue reduces cue text to a comparison key: tags and hearing-impaired annotations dropped, punctuation and case folded away, so OCR/punctuation variants of the same line compare near-equal.
func NormalizeLang ¶
NormalizeLang is Normalize with the subtitle's language, which picks the legacy code page for non-UTF files (Windows-1251 for Russian, …).
func Rank ¶
Rank scores candidates in place, best first. There is no file yet, so no moviehash: download count (log-scaled), trusted uploaders, ratings and release-name token overlap carry the signal; AI-translated and hearing-impaired tracks are penalised (HI only slightly — sometimes it is the only option).
Types ¶
type AlignOptions ¶
type AlignOptions struct {
MinMatched int // default 10
MaxResidual time.Duration // median bound, default 500ms
MaxResidualP90 time.Duration // default 1s
}
AlignOptions bounds what counts as a confident alignment.
type AlignSegment ¶
type AlignSegment struct {
FromStart time.Duration `json:"fromStart"`
FromEnd time.Duration `json:"fromEnd"`
Slope float64 `json:"slope"`
Intercept time.Duration `json:"intercept"`
Matched int `json:"matched"`
}
AlignSegment is one affine run: To = Slope*From + Intercept (millisecond space), valid for From times in [FromStart, FromEnd].
type Aligner ¶
type Aligner struct {
// contains filtered or unexported fields
}
Aligner implements core.TimelineAligner: it maps a moment in one release of a title to the same moment in another release using only subtitle dialogue timing, so no video is ever downloaded. Timelines come from subtitles already cached on disk or from OpenSubtitles files matched to each release by name (or, when only a runtime is known, by how close the file's last cue lands to that runtime). Everything is off unless the OpenSubtitles service is enabled.
Failover calls AlignTime with a sub-second deadline: on a cache miss that returns ok=false immediately and warms the cache in the background. Callers with time to spare (marker shifting) get a synchronous build.
func (*Aligner) AlignTime ¶
func (a *Aligner) AlignTime(ctx context.Context, req core.AlignRequest, at time.Duration) (time.Duration, bool)
AlignTime implements core.TimelineAligner.
func (*Aligner) Prewarm ¶
func (a *Aligner) Prewarm(req core.AlignRequest)
Prewarm starts a background alignment build for a release pair so a later failover or marker lookup hits the cache. Duplicate and recently-failed pairs are skipped.
type CachedSubtitle ¶
type Candidate ¶
type Candidate struct {
FileID int64
FileName string
Language string
Release string
DownloadCount int
Ratings float64
Uploader string
UploaderRank string
HearingImp bool
AITranslated bool
HashMatch bool
Score float64 // filled by Rank
}
Candidate is one OpenSubtitles search result.
type Cue ¶
Cue is one subtitle cue.
func ParseASS ¶
ParseASS converts Dialogue events using the declared Format field. Styling, positioning and karaoke effects are deliberately dropped for text fallbacks.
type Options ¶
type Options struct {
Config config.OpenSubtitles
CacheDir string // data dir subfolder for downloaded subtitles
BaseURL string // override for tests
HTTPClient *http.Client
Logger *slog.Logger
UserAgent string // OpenSubtitles requires an app User-Agent
// NegativeTTL is how long "no subtitle found" for a title+language is
// remembered before searching again (default 6h).
NegativeTTL time.Duration
// MaxRetryAfter caps a 429 Retry-After wait (default 3s).
MaxRetryAfter time.Duration
Now func() time.Time
// AlignLanguages are tried in order for dialogue timeline alignment
// (default ["en"]: the most widely subtitled language).
AlignLanguages []string
}
Options construct a Service. Config is a snapshot; call UpdateConfig on config changes if desired.
type Piecewise ¶
type Piecewise struct {
Segments []AlignSegment `json:"segments"`
Matched int `json:"matched"`
// Residual is the median absolute error of matched cues against the fit.
Residual time.Duration `json:"residual"`
}
Piecewise is a confident dialogue-based mapping between two releases.
func AlignPiecewise ¶
func AlignPiecewise(from, to []Cue, opts AlignOptions) (*Piecewise, error)
AlignPiecewise builds the mapping from the "from" cue timeline to the "to" cue timeline, or fails when there is no confident alignment (too few matched lines, or matched lines that do not fit within the residual bound — typically a different language or an unrelated subtitle file).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the OpenSubtitles client + disk cache.
func (*Service) CachedSubtitles ¶
func (s *Service) CachedSubtitles() ([]CachedSubtitle, error)
func (*Service) CuesForInfo ¶
CuesForInfo returns dialogue cues for the release described by info when the system already holds their text: a cached sidecar stream in info.Streams. It never fetches video and never touches the network. Embedded tracks whose text was never extracted are not available here.
func (*Service) DialogueTimeline ¶
func (s *Service) DialogueTimeline(ctx context.Context, title *core.Title, lang, releaseName string, dur time.Duration) ([]Cue, bool)
DialogueTimeline returns the best dialogue timeline for one release (cached sidecar first, then OpenSubtitles), without downloading video.
func (*Service) Enrich ¶
func (s *Service) Enrich(ctx context.Context, title *core.Title, user *core.User, res *core.Resolution, src core.MediaSource, info *core.MediaInfo)
Enrich is playback.Deps.AugmentMedia. The source is exclusively owned by the preparing generation, so local hashing cannot race disposal during failover. Remote media uses release matching until a cache-only edge reader exists.
func (*Service) Open ¶
Open resolves a sidecar id (or a full ExternalURL) to its SRT content. It returns the reader and the format ("srt").
func (*Service) OpenFormat ¶
OpenFormat resolves a sidecar id in the requested format ("srt" or "vtt").
func (*Service) Search ¶
func (s *Service) Search(ctx context.Context, title *core.Title, lang, releaseName string) ([]Candidate, error)
Search returns ranked candidates for a title and language.
func (*Service) SearchWithHash ¶
func (s *Service) SearchWithHash(ctx context.Context, title *core.Title, lang, releaseName, movieHash string) ([]Candidate, error)
SearchWithHash prefers exact cached-source matches; empty hash uses release ranking.
func (*Service) Sidecars ¶
func (s *Service) Sidecars(ctx context.Context, title *core.Title, user *core.User, embedded []core.StreamInfo) ([]core.StreamInfo, error)
Sidecars returns external subtitle streams for the user's preferred languages missing from the embedded tracks. It returns (nil, nil) when the service is disabled or nothing is missing; per-language failures are logged and skipped, never failing playback.
func (*Service) SidecarsForRelease ¶
func (s *Service) SidecarsForRelease(ctx context.Context, title *core.Title, user *core.User, embedded []core.StreamInfo, releaseName string) ([]core.StreamInfo, error)
SidecarsForRelease is Sidecars with the chosen release's name, used to rank candidates by release-name match (subtitle timing follows the release, so the choice is remembered per release).
Per language: a remembered selection with both files on disk is reused without any network call; otherwise OpenSubtitles is searched, the best three candidates are tried in order, and the winner is cached and remembered. "Nothing found" is remembered for NegativeTTL. A quota error stops downloads for the rest of the call.
func (*Service) SidecarsWithHash ¶
func (s *Service) SidecarsWithHash(ctx context.Context, title *core.Title, user *core.User, embedded []core.StreamInfo, releaseName, movieHash string) ([]core.StreamInfo, error)
SidecarsWithHash uses an already-computed cached-source hash for exact release matching.
func (*Service) SyncHandler ¶
SyncHandler serves POST /{cachedSubtitleID}; mount behind owner auth/CSRF.
func (*Service) TimelineAligner ¶
TimelineAligner returns the service's (single) core.TimelineAligner. Wiring is one line each for markers and playback:
markers.New(markers.Options{..., Aligner: subs.TimelineAligner()})
playback.Deps{..., Aligner: subs.TimelineAligner()}
func (*Service) UpdateConfig ¶
func (s *Service) UpdateConfig(cfg config.OpenSubtitles)
UpdateConfig swaps the OpenSubtitles settings (e.g. from config.Store.Subscribe).
type SyncOptions ¶
type SyncOptions struct {
Apply bool `json:"apply"` // apply adjusted copy to the cached release for every viewer
OffsetMS int64 `json:"offsetMs"`
Rate float64 `json:"rate"`
Reference string `json:"reference,omitempty"`
}
SyncOptions adjusts a cached sidecar or estimates the offset from supplied embedded-reference SRT/VTT/ASS. It creates a new cache entry, preserving the original download and every other viewer's selection.