libraries: PATCH /libraries/{id} with live-apply

Adds an HTTP mutation surface for `libraries.enabled` and
`libraries.excluded_dirs`, replacing the SQL-only workflow noted in
CLAUDE.md. Apollo's Settings panel calls this from the LIBRARIES
section so the operator no longer has to ssh + sqlite3 to flip a
library off or edit its excludes.

Live-apply (no restart) via a new `live_libraries: Arc<RwLock<Vec<
Library>>>` field on AppState. The existing immutable `libraries`
Vec stays for hot-path handlers that only need stable id → root_path
lookups, avoiding a 19-call-site refactor. The watcher and
cleanup_orphaned_playlists now take the lock instead of a Vec
snapshot and re-read at the top of each tick, so `enabled` /
`excluded_dirs` changes are picked up within one
WATCH_QUICK_INTERVAL_SECONDS. The GET /libraries handler also reads
through the live view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-05-13 08:47:35 -04:00
parent 74bf693878
commit b3124437ec
4 changed files with 187 additions and 32 deletions

View File

@@ -18,15 +18,25 @@ use crate::video::actors::{
};
use actix::{Actor, Addr};
use std::env;
use std::sync::{Arc, Mutex};
use std::sync::{Arc, Mutex, RwLock};
pub struct AppState {
pub stream_manager: Arc<Addr<StreamActor>>,
pub playlist_manager: Arc<Addr<VideoPlaylistManager>>,
pub preview_clip_generator: Arc<Addr<PreviewClipGenerator>>,
/// All configured media libraries. Ordered by `id` ascending; the first
/// entry is the primary library.
/// entry is the primary library. Frozen at startup — handlers that
/// only need stable lookup (id → name / root_path) read this. Mutable
/// flags (`enabled`, `excluded_dirs`) reflect their startup values;
/// for live state see [`AppState::live_libraries`].
pub libraries: Vec<Library>,
/// Live view of the libraries table, shared mutably between the
/// watcher (which reads it at the top of each tick to honour the
/// latest `enabled` / `excluded_dirs`) and the PATCH /libraries/{id}
/// handler (which writes it on a successful mutation). The split
/// from [`AppState::libraries`] is deliberate: handlers that only
/// look up by id don't need to take a lock per request.
pub live_libraries: Arc<RwLock<Vec<Library>>>,
/// Per-library availability snapshot. Updated by the file watcher at
/// the top of each tick via `libraries::refresh_health`. HTTP handlers
/// read it (e.g. `/libraries` surfacing). See "Library availability
@@ -112,11 +122,13 @@ impl AppState {
);
let library_health = libraries::new_health_map(&libraries_vec);
let live_libraries = Arc::new(RwLock::new(libraries_vec.clone()));
Self {
stream_manager,
playlist_manager: Arc::new(video_playlist_manager.start()),
preview_clip_generator: Arc::new(preview_clip_generator.start()),
libraries: libraries_vec,
live_libraries,
library_health,
base_path,
thumbnail_path,