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 AcceptsUploadFormat(format string) bool
- func HasMojibake(s string) bool
- 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 RepairMojibake(s string) string
- func SourceKey(raw string) string
- func Test(ctx context.Context, cfg config.OpenSubtitles) error
- type AlignOptions
- type AlignSegment
- type Aligner
- type CachedSubtitle
- type Candidate
- type Cue
- type FitOptions
- type Options
- type Piecewise
- type Preferences
- type ReleaseAccess
- type Service
- func (s *Service) CachedSidecars(title *core.Title, user *core.User, embedded []core.StreamInfo, ...) []core.StreamInfo
- 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) DownloadOnDemand(ctx context.Context, title *core.Title, fileID int64) (core.StreamInfo, error)
- func (s *Service) Enabled() bool
- func (s *Service) Enrich(ctx context.Context, title *core.Title, user *core.User, res *core.Resolution, ...)
- func (s *Service) ManualSidecars(title *core.Title, embedded []core.StreamInfo) []core.StreamInfo
- func (s *Service) Open(id string) (io.ReadCloser, string, error)
- func (s *Service) OpenFormat(id, format string) (io.ReadCloser, string, error)
- func (s *Service) OpenFormatAt(id, format string, offset time.Duration) (io.ReadCloser, string, error)
- func (s *Service) OpenFormatAtFrom(id, format string, offset time.Duration, source string) (io.ReadCloser, string, error)
- func (s *Service) OpenFormatFrom(id, format, source string) (io.ReadCloser, string, error)
- func (s *Service) Search(ctx context.Context, title *core.Title, lang, releaseName string) ([]Candidate, error)
- func (s *Service) SearchOnDemand(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) SetFFmpeg(path string)
- 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) SourceText(key core.RatingKey, except string) (lang, text string, ok bool)
- func (s *Service) StartCorrection(ctx context.Context, title *core.Title, id string, access ReleaseAccess)
- func (s *Service) StoreText(title *core.Title, lang, label, text string, fileID int64) (core.StreamInfo, error)
- func (s *Service) StoreUpload(title *core.Title, filename, format, language string, body []byte) (core.StreamInfo, error)
- func (s *Service) StreamByFileID(title *core.Title, fileID int64) (core.StreamInfo, bool)
- func (s *Service) SyncHandler() http.Handler
- func (s *Service) TimelineAligner() *Aligner
- func (s *Service) UpdateConfig(cfg config.OpenSubtitles)
- type SpeechWindow
- type TimingFit
- func FitSpeechEnergy(cues []Cue, windows []SpeechWindow) (TimingFit, bool)
- func FitSpeechEnergyOptions(cues []Cue, windows []SpeechWindow, opt FitOptions) (TimingFit, bool)
- func FitTiming(candidate, reference []Cue) (TimingFit, bool)
- func FitTimingOptions(candidate, reference []Cue, opt FitOptions) (TimingFit, bool)
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 AcceptsUploadFormat ¶ added in v0.3.0
AcceptsUploadFormat is the Plex Web upload format check: case-sensitive on the wire. PMS accepts srt/ass/ssa/smi/txt/psb; Ether also accepts vtt.
func HasMojibake ¶ added in v0.3.0
HasMojibake reports UTF-8 that still looks like a 1252 mis-decode. Only the two-byte pair patterns count: a single Å/Ä/Ã is a real Nordic or German letter, not mojibake.
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).
func RepairMojibake ¶ added in v0.3.0
RepairMojibake undoes UTF-8 that was read as Windows-1252 or Latin-1 and written back as UTF-8. Unchanged when the reverse is not valid UTF-8 or does not look cleaner.
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 CachedSubtitle struct {
ID string `json:"id"`
Language string `json:"language"`
Release string `json:"release"`
FetchedAt time.Time `json:"fetchedAt"`
Key string `json:"-"`
}
CachedSubtitle identifies a sidecar from its own metadata. Key is internal only; the app resolves it through the catalog. Legacy metadata without a key stays unknown, with no selection-index scan on the list request path.
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
// Forced marks a real forced track: OpenSubtitles'
// foreign_parts_only and NOT hearing_impaired (a HI
// foreign-parts track is SDH, not forced; HearingImp
// carries that signal separately).
Forced 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.
func ReadEmbeddedTextCues ¶ added in v0.3.0
ReadEmbeddedTextCues reads cue times from the first embedded TEXT subtitle track of a Matroska or MP4 source, using only the container index and bounded range reads.
type FitOptions ¶ added in v0.3.0
type FitOptions struct {
MinInliers int
MinConfidence float64
MaxOffset time.Duration
FrameSlack time.Duration
}
FitOptions tunes FitTiming. Zero values use the defaults above.
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
// FFmpeg is the owner-configured ffmpeg used for speech-energy
// measurement. Empty skips that path.
FFmpeg 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 Preferences ¶ added in v0.2.34
type Preferences struct {
Mode string // off, forced-only, foreign-only, always
Style string // normal, sdh, forced
}
Preferences is the viewer's subtitle mode/style (trackprefs vocabulary) for automatic fetching. Empty means no preference: best-ranked wins.
type ReleaseAccess ¶ added in v0.3.0
type ReleaseAccess struct {
Reader io.ReaderAt
Size int64
URL string
Duration time.Duration
Info *core.MediaInfo
Source string
FFmpeg string
Done func()
Hold func() (io.ReaderAt, int64, func(), bool)
}
ReleaseAccess is enough of a playing session to measure a sidecar. Reader and URL must belong to that session's lifetime; Enrich must not pass a prepare-generation handle. Hold, when set, is taken after the speech semaphore so a retired source can close during a failover.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the OpenSubtitles client + disk cache.
func (*Service) CachedSidecars ¶ added in v0.2.21
func (s *Service) CachedSidecars(title *core.Title, user *core.User, embedded []core.StreamInfo, prefs ...Preferences) []core.StreamInfo
CachedSidecars lists, without any network call, the sidecars already on disk for a title: viewer-picked tracks, then for each of the viewer's preferred languages missing from embedded the most recently fetched track (whichever release it was matched to). Streams whose ExternalURL is already in embedded are skipped, so it can top up a session's enriched MediaInfo too. Nil when the service is disabled.
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) DownloadOnDemand ¶ added in v0.2.21
func (s *Service) DownloadOnDemand(ctx context.Context, title *core.Title, fileID int64) (core.StreamInfo, error)
DownloadOnDemand fetches a file a recent SearchOnDemand offered for the title, remembers it as the title's pick for its language (replacing any earlier pick) and returns its sidecar stream. core.ErrNotFound means the file was not offered for this title (or the offer expired).
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, prefs ...Preferences)
Enrich is playback.Deps.AugmentMedia. It attaches sidecars already on disk for this release. It does not search or download: a file is fetched only when the viewer picks one. The source is exclusively owned by the preparing generation, so local hashing cannot race disposal during failover.
func (*Service) ManualSidecars ¶ added in v0.3.0
func (s *Service) ManualSidecars(title *core.Title, embedded []core.StreamInfo) []core.StreamInfo
ManualSidecars lists viewer-picked tracks already on disk. Unlike CachedSidecars it does not require OpenSubtitles to be configured, so a translated or transcribed track still appears when that account is off.
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) OpenFormatAt ¶ added in v0.2.21
func (s *Service) OpenFormatAt(id, format string, offset time.Duration) (io.ReadCloser, string, error)
OpenFormatAt is OpenFormat with the cue times moved back by offset, for a transcode session whose timeline starts offset into the title. Cues that end before the offset are dropped.
func (*Service) OpenFormatAtFrom ¶ added in v0.3.0
func (s *Service) OpenFormatAtFrom(id, format string, offset time.Duration, source string) (io.ReadCloser, string, error)
OpenFormatAtFrom is OpenFormatAt for a known release/source.
func (*Service) OpenFormatFrom ¶ added in v0.3.0
OpenFormatFrom is OpenFormat applying a cached fit only for this release/source. An empty source serves the original sidecar.
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) SearchOnDemand ¶ added in v0.2.21
func (s *Service) SearchOnDemand(ctx context.Context, title *core.Title, lang, releaseName string) ([]Candidate, error)
SearchOnDemand is the viewer-driven search behind Plex's subtitle "Search": ranked OpenSubtitles candidates for one language (releaseName, when a session knows it, improves the ranking). Results are remembered for an hour so DownloadOnDemand only fetches files offered for the title.
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, prefs ...Preferences) ([]core.StreamInfo, error)
Sidecars returns external subtitle streams already cached for the user's preferred languages missing from the embedded tracks. It never searches or downloads; a file is fetched only when the viewer picks one (DownloadOnDemand). It returns (nil, nil) when the service is disabled or nothing cached is missing.
func (*Service) SidecarsForRelease ¶
func (s *Service) SidecarsForRelease(ctx context.Context, title *core.Title, user *core.User, embedded []core.StreamInfo, releaseName string, prefs ...Preferences) ([]core.StreamInfo, error)
SidecarsForRelease is Sidecars with the chosen release's name, so a selection remembered for that release is the one listed. It does not contact OpenSubtitles.
func (*Service) SidecarsWithHash ¶
func (s *Service) SidecarsWithHash(ctx context.Context, title *core.Title, user *core.User, embedded []core.StreamInfo, releaseName, movieHash string, prefs ...Preferences) ([]core.StreamInfo, error)
SidecarsWithHash uses an already-computed cached-source hash for exact release matching.
func (*Service) SourceText ¶ added in v0.3.0
SourceText returns one cached subtitle in a language other than except, preferring English. It reads only files already on disk.
func (*Service) StartCorrection ¶ added in v0.3.0
func (s *Service) StartCorrection(ctx context.Context, title *core.Title, id string, access ReleaseAccess)
StartCorrection measures the cached sidecar against the live session's source. It no-ops without a source handle (no prefetch). The first serve after a pick is uncorrected unless this measurement finishes before OpenFormatFrom's short wait.
func (*Service) StoreText ¶ added in v0.3.0
func (s *Service) StoreText(title *core.Title, lang, label, text string, fileID int64) (core.StreamInfo, error)
StoreText saves subtitle text the server produced (a translation or a transcript) and remembers it as the viewer's pick for that language. A later call with the same title, language and file ID returns the cached track and does not rewrite it. It never contacts a provider.
func (*Service) StoreUpload ¶ added in v0.3.0
func (s *Service) StoreUpload(title *core.Title, filename, format, language string, body []byte) (core.StreamInfo, error)
StoreUpload saves a viewer-uploaded sidecar as UTF-8 SRT+VTT. It is listed as an upload-sig track and does not overwrite another viewer's manual pick.
func (*Service) StreamByFileID ¶ added in v0.3.0
StreamByFileID returns a generated or picked sidecar already on disk. It does not download anything.
func (*Service) SyncHandler ¶
SyncHandler serves GET / of cached sidecars and POST /{id} as a manual timing adjust written into the correction store.
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 SpeechWindow ¶ added in v0.3.0
SpeechWindow is one decoded audio slice used as a timing reference.
func DecodeAudioWindows ¶ added in v0.3.0
func DecodeAudioWindows(ctx context.Context, ffmpegPath, input string, duration time.Duration, windows int, span time.Duration) ([]SpeechWindow, error)
DecodeAudioWindows reads a few short windows of the first audio track with ffmpeg from a local file or HTTP URL. Skipped when ffmpeg is absent.
type TimingFit ¶ added in v0.3.0
type TimingFit struct {
Offset time.Duration
Rate float64
Confidence float64
Inliers int
Residual time.Duration
}
TimingFit is a constant offset plus a frame-rate scale that maps candidate cue times onto a reference clock:
ref ≈ candidate*Rate + Offset
func FitSpeechEnergy ¶ added in v0.3.0
func FitSpeechEnergy(cues []Cue, windows []SpeechWindow) (TimingFit, bool)
FitSpeechEnergy cross-correlates the subtitle on/off mask with a band-passed energy detector over a few short audio windows.
func FitSpeechEnergyOptions ¶ added in v0.3.0
func FitSpeechEnergyOptions(cues []Cue, windows []SpeechWindow, opt FitOptions) (TimingFit, bool)
FitSpeechEnergyOptions is FitSpeechEnergy with explicit thresholds.
func FitTiming ¶ added in v0.3.0
FitTiming aligns candidate cue start times to reference start times. Matching is language-independent: only the timing pattern is used.
func FitTimingOptions ¶ added in v0.3.0
func FitTimingOptions(candidate, reference []Cue, opt FitOptions) (TimingFit, bool)
FitTimingOptions is FitTiming with explicit thresholds (tests use a smaller cue count).