feat(stream): cache scan-time thumbnail frames to the .unarr sidecar
Pre-extract the file panel's sample frames (10/30/50/70/90% of runtime, w=320) during the library scan and write-through any on-demand /thumbnail request into the hidden ".unarr/<name>.t<sec>w<width>.jpg" sidecar. The /thumbnail handler serves a fresh sidecar instantly, so the characteristics panel and seekbar previews stop re-running ffmpeg per request. - mediainfo.sidecar: ThumbnailCachePath, ReadCachedThumbnail, WriteCachedThumbnail, ExtractThumbnailJPEG (mirrors engine.buildThumbnailArgs). - library.PrewarmSidecars: also enqueues the panel frame positions (kept in lockstep with the web's THUMB_FRACTIONS / THUMB_WIDTH) per item with a duration. - thumbnailHandler: cache-read → hit; miss → extract → write-through. - config: library.cache_thumbnails (default true) + both cache toggles exposed in the interactive 'unarr config' library menu. Local only by design — frames are the user's own content, never uploaded.
This commit is contained in:
parent
178c16f458
commit
1e5de874cf
6 changed files with 237 additions and 52 deletions
|
|
@ -322,6 +322,14 @@ func configLibrary(cfg *config.Config) error {
|
|||
Title("Allow file deletion from web UI?").
|
||||
Description("When enabled, the web library's Delete button can permanently remove files from disk.\nOnly activate this if you understand that deleted files cannot be recovered.").
|
||||
Value(&cfg.Library.AllowDelete),
|
||||
huh.NewConfirm().
|
||||
Title("Cache subtitles during scan?").
|
||||
Description("Extract embedded text subtitles to WebVTT once during the scan and store them\nbeside the media (hidden .unarr dir) so playback subtitles are instant — and huge\nremuxes don't time out extracting on demand. Local only; nothing is uploaded.").
|
||||
Value(&cfg.Library.CacheSubtitles),
|
||||
huh.NewConfirm().
|
||||
Title("Cache thumbnails during scan?").
|
||||
Description("Pre-extract a few preview frames per file (hidden .unarr dir) so the file panel\nand seekbar previews load instantly. Small optimized JPEGs; local only.").
|
||||
Value(&cfg.Library.CacheThumbnails),
|
||||
),
|
||||
).Run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ func runDaemonStart() error {
|
|||
// /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.SetCacheThumbnails(cfg.Library.CacheThumbnails)
|
||||
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
|
||||
|
|
@ -999,15 +1000,16 @@ 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).
|
||||
// Resolve ffmpeg once for the sidecar prewarm (extracts text subs → WebVTT
|
||||
// and panel thumbnail frames → JPEG into the hidden ".unarr" cache so /sub
|
||||
// and /thumbnail are instant + huge remuxes work). Empty/err = prewarm is
|
||||
// skipped silently (on-demand extraction still runs).
|
||||
prewarmFFmpeg := ""
|
||||
if cfg.Library.CacheSubtitles {
|
||||
if cfg.Library.CacheSubtitles || cfg.Library.CacheThumbnails {
|
||||
if ff, err := mediainfo.ResolveFFmpeg(cfg.Library.FFmpegPath); err == nil {
|
||||
prewarmFFmpeg = ff
|
||||
} else {
|
||||
log.Printf("[auto-scan] subtitle prewarm disabled: ffmpeg unavailable: %v", err)
|
||||
log.Printf("[auto-scan] sidecar prewarm disabled: ffmpeg unavailable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1027,9 +1029,10 @@ func runAutoScan(ctx context.Context, cfg config.Config, interval time.Duration,
|
|||
|
||||
if prewarmFFmpeg != "" {
|
||||
library.PrewarmSidecars(ctx, cache, library.PrewarmOptions{
|
||||
FFmpegPath: prewarmFFmpeg,
|
||||
CacheSubtitles: true,
|
||||
Workers: 2,
|
||||
FFmpegPath: prewarmFFmpeg,
|
||||
CacheSubtitles: cfg.Library.CacheSubtitles,
|
||||
CacheThumbnails: cfg.Library.CacheThumbnails,
|
||||
Workers: 2,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,16 +140,18 @@ func runScan(dirPath string, workers int, ffprobePath string, noSync bool) error
|
|||
return enc.Encode(cache)
|
||||
}
|
||||
|
||||
// Pre-extract subtitle sidecars (text subs → WebVTT in a hidden ".unarr" dir)
|
||||
// so playback gets instant subtitles and huge remuxes never hit the on-demand
|
||||
// timeout. Best-effort + Ctrl-C interruptible (the scan itself is already saved).
|
||||
if cfg.Library.CacheSubtitles {
|
||||
// Pre-extract sidecars (text subs → WebVTT, panel frames → JPEG) into a hidden
|
||||
// ".unarr" dir so playback gets instant subtitles/thumbnails and huge remuxes
|
||||
// never hit the on-demand timeout. Best-effort + Ctrl-C interruptible (the scan
|
||||
// itself is already saved).
|
||||
if cfg.Library.CacheSubtitles || cfg.Library.CacheThumbnails {
|
||||
if ff, err := mediainfo.ResolveFFmpeg(cfg.Library.FFmpegPath); err == nil {
|
||||
fmt.Fprintf(os.Stderr, " Pre-extracting subtitles to cache… (Ctrl-C to skip)\n")
|
||||
fmt.Fprintf(os.Stderr, " Pre-extracting subtitles + thumbnails to cache… (Ctrl-C to skip)\n")
|
||||
library.PrewarmSidecars(ctx, cache, library.PrewarmOptions{
|
||||
FFmpegPath: ff,
|
||||
CacheSubtitles: true,
|
||||
Workers: 2,
|
||||
FFmpegPath: ff,
|
||||
CacheSubtitles: cfg.Library.CacheSubtitles,
|
||||
CacheThumbnails: cfg.Library.CacheThumbnails,
|
||||
Workers: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue