feat(stream): cache extracted subtitles to a hidden .unarr sidecar
On-demand WebVTT extraction re-ran ffmpeg on every /sub request and, for 50GB+ remuxes, couldn't finish a full text track within the 60s HTTP timeout → the web player got a 500 and no subtitles. Extract each text subtitle ONCE — during the library scan (no HTTP deadline, generous per-file timeout) and write-through on the first on-demand request — into a hidden ".unarr/<name>.s<index>.vtt" sidecar next to the media file. The /sub handler serves a fresh sidecar instantly (mtime-invalidated when the media is replaced), so playback subtitles are instant and huge files work. - mediainfo.sidecar: cache paths, mtime freshness, atomic write, ExtractSubtitleVTT, IsTextSubtitleCodec (shared classifier, mirrors engine + web whitelists). - library.PrewarmSidecars: bounded, idempotent, ctx-cancellable background pass run after every scan (manual + daemon auto-scan). - subtitleHandler: cache-read → hit; miss → extract → write-through. - config: library.cache_subtitles (default true), wired via SetCacheSubtitles. Local-only by design: nothing extracted is uploaded — the sidecar is the user's own content, private to their disk.
This commit is contained in:
parent
7417fad45f
commit
178c16f458
6 changed files with 353 additions and 33 deletions
|
|
@ -346,6 +346,10 @@ func runDaemonStart() error {
|
|||
// Wire ffmpeg so /thumbnail can extract single frames for the web's "file
|
||||
// characteristics" panel (frames on demand). Empty = thumbnails 503.
|
||||
streamSrv.SetFFmpegPath(ffmpegResolved)
|
||||
// Write-through cache extracted WebVTT into the hidden ".unarr" sidecar dir so
|
||||
// /sub serves instantly (and giant remuxes that exceed the on-demand timeout
|
||||
// work once the scan prewarm has filled the cache). Default true.
|
||||
streamSrv.SetCacheSubtitles(cfg.Library.CacheSubtitles)
|
||||
streamSrv.SetRequireStreamToken(cfg.Download.RequireStreamToken)
|
||||
// Report the stream-token signing key ONLY when enforcing, so the web's
|
||||
// "secret present → mint HLS token" signal accurately means "this agent
|
||||
|
|
@ -995,6 +999,18 @@ func runAutoScan(ctx context.Context, cfg config.Config, interval time.Duration,
|
|||
Incremental: existing != nil,
|
||||
}
|
||||
|
||||
// Resolve ffmpeg once for the subtitle-sidecar prewarm (extracts text subs
|
||||
// to the hidden ".unarr" cache so /sub is instant + huge remuxes work).
|
||||
// Empty/err = prewarm is skipped silently (on-demand extraction still runs).
|
||||
prewarmFFmpeg := ""
|
||||
if cfg.Library.CacheSubtitles {
|
||||
if ff, err := mediainfo.ResolveFFmpeg(cfg.Library.FFmpegPath); err == nil {
|
||||
prewarmFFmpeg = ff
|
||||
} else {
|
||||
log.Printf("[auto-scan] subtitle prewarm disabled: ffmpeg unavailable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan each path independently and sync per path so the server can
|
||||
// scope stale-item deletion to the correct directory prefix.
|
||||
const batchSize = 100
|
||||
|
|
@ -1009,6 +1025,14 @@ func runAutoScan(ctx context.Context, cfg config.Config, interval time.Duration,
|
|||
}
|
||||
mergedItems = append(mergedItems, cache.Items...)
|
||||
|
||||
if prewarmFFmpeg != "" {
|
||||
library.PrewarmSidecars(ctx, cache, library.PrewarmOptions{
|
||||
FFmpegPath: prewarmFFmpeg,
|
||||
CacheSubtitles: true,
|
||||
Workers: 2,
|
||||
})
|
||||
}
|
||||
|
||||
items := library.BuildSyncItems(cache)
|
||||
if len(items) == 0 {
|
||||
log.Printf("[auto-scan] no items under %s", scanPath)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue