hls: per-library readiness gauges + GET /hls/stats endpoint
The hash-keyed pipeline transcodes lazily, so a freshly mounted (or freshly upgraded) library is "mostly pending" for the first hour while the watcher works through the backlog. The operator wants a live read on remaining work so they can tune `HLS_CONCURRENCY` and know when to stop waiting. Adds: - `src/hls_stats.rs` — pure compute path (`stats_from_rows`) and an Arc<Mutex<dyn ExifDao>> wrapper (`compute_and_publish`). Per library: `total`, `with_playlist`, `pending`, `unsupported`, `hashless_videos`. Dedup is by content_hash so duplicate-bytes-at- N-paths counts once (same domain rule as `faces::stats`). `hashless_videos` is a separate counter so the operator can see the "hash backfill, then transcode" pipeline depth instead of having NULL-hash rows just hide. - Prometheus gauges labeled by library name: `imageserver_hls_videos_total`, `..._with_playlist`, `..._pending`, `..._unsupported`. Updated by the watcher at the end of every full- scan tick *and* on every `/hls/stats` hit, so whichever surface the operator is watching stays fresh. Registered in `main` alongside the existing image/video gauges. - `GET /hls/stats` — Claims-protected JSON snapshot of the same data plus a top-level cross-library aggregate. Runs on a blocking pool so it doesn't pin the actix worker; per-call cost is one `list_paths_and_hashes_for_library` SQL query per library plus a `stat()` per distinct video hash. Bounded — never invoked from middleware, only from the explicit endpoint and the full-scan tick. The watcher's end-of-tick `info!` summary line mirrors the endpoint output for operators tailing the log. - New `ExifDao::list_paths_and_hashes_for_library` method: `SELECT rel_path, content_hash FROM image_exif WHERE library_id = ?`. Single round-trip; callers filter to video extensions client-side because the schema doesn't carry media-type. Mock impl in `files.rs` returns an empty vec. Tests in `hls_stats::tests` exercise stats_from_rows directly (videos- only filter, hash dedup, playlist vs sentinel decision, NULL-hash hashless counting) plus a publish_gauges round-trip that reads the gauge value back. Full suite (347 lib + 360 bin = 707) passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+20
@@ -45,6 +45,7 @@ mod file_types;
|
||||
mod files;
|
||||
mod geo;
|
||||
mod handlers;
|
||||
mod hls_stats;
|
||||
mod libraries;
|
||||
mod library_maintenance;
|
||||
mod perceptual_hash;
|
||||
@@ -126,6 +127,24 @@ fn main() -> std::io::Result<()> {
|
||||
.registry
|
||||
.register(Box::new(thumbnails::VIDEO_GAUGE.clone()))
|
||||
.unwrap();
|
||||
// HLS readiness gauges. Updated by the watcher every full-scan
|
||||
// tick and on every `/hls/stats` request. See `hls_stats`.
|
||||
prometheus
|
||||
.registry
|
||||
.register(Box::new(hls_stats::HLS_VIDEOS_TOTAL.clone()))
|
||||
.unwrap();
|
||||
prometheus
|
||||
.registry
|
||||
.register(Box::new(hls_stats::HLS_VIDEOS_WITH_PLAYLIST.clone()))
|
||||
.unwrap();
|
||||
prometheus
|
||||
.registry
|
||||
.register(Box::new(hls_stats::HLS_VIDEOS_PENDING.clone()))
|
||||
.unwrap();
|
||||
prometheus
|
||||
.registry
|
||||
.register(Box::new(hls_stats::HLS_VIDEOS_UNSUPPORTED.clone()))
|
||||
.unwrap();
|
||||
|
||||
let app_state = app_data.clone();
|
||||
|
||||
@@ -270,6 +289,7 @@ fn main() -> std::io::Result<()> {
|
||||
.service(handlers::video::get_video_preview)
|
||||
.service(handlers::video::get_preview_status)
|
||||
.service(handlers::video::get_video_part)
|
||||
.service(hls_stats::hls_stats_handler)
|
||||
.service(handlers::favorites::favorites)
|
||||
.service(handlers::favorites::put_add_favorite)
|
||||
.service(handlers::favorites::delete_favorite)
|
||||
|
||||
Reference in New Issue
Block a user