//! HttpPlugin — Web UI + REST API //! //! 基于 warp 的 HTTP 服务,提供播放控制、配置管理、视频管理等 API。 mod routes; use crate::core::config::AppConfig; use crate::core::message::{Envelope, Message}; use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform}; use anyhow::{Context, Result}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, Mutex}; struct PendingWifiResponse { version: u64, payload: Option, } pub(crate) struct HttpState { wifi_response: Mutex, wifi_response_cv: Condvar, config: Mutex>, player_status: Mutex, ble_ready: AtomicBool, } impl HttpState { fn new(config: Arc) -> Self { let player_status = crate::core::message::PlayerStatusData { running: false, paused: !config.playback.auto_start, in_transition: false, current_index: 0, playlist_length: config.playlist.len(), current_video: config.playlist.first().map(|item| item.id.clone()), }; Self { wifi_response: Mutex::new(PendingWifiResponse { version: 0, payload: None, }), wifi_response_cv: Condvar::new(), config: Mutex::new(config), player_status: Mutex::new(player_status), ble_ready: AtomicBool::new(false), } } fn publish_wifi_result(&self, payload: String) { if let Ok(mut state) = self.wifi_response.lock() { state.version += 1; state.payload = Some(payload); self.wifi_response_cv.notify_all(); } } pub(crate) fn config(&self) -> Arc { self.config .lock() .map(|config| Arc::clone(&config)) .expect("http config state poisoned") } fn replace_config(&self, config: Arc) { if let Ok(mut current) = self.config.lock() { *current = Arc::clone(&config); } if let Ok(mut player_status) = self.player_status.lock() { player_status.playlist_length = config.playlist.len(); if player_status.current_video.is_none() { player_status.current_video = config.playlist.first().map(|item| item.id.clone()); } } } pub(crate) fn player_status(&self) -> crate::core::message::PlayerStatusData { self.player_status .lock() .map(|status| status.clone()) .expect("http player status state poisoned") } fn update_player_status(&self, status: crate::core::message::PlayerStatusData) { if let Ok(mut current) = self.player_status.lock() { *current = status; } } pub(crate) fn ble_ready(&self) -> bool { self.ble_ready.load(Ordering::SeqCst) } fn set_ble_ready(&self, ready: bool) { self.ble_ready.store(ready, Ordering::SeqCst); } } pub struct HttpPlugin { ctx: Option, state: Option>, } impl HttpPlugin { pub fn new() -> Self { Self { ctx: None, state: None, } } } impl Plugin for HttpPlugin { fn id(&self) -> &'static str { "http" } fn info(&self) -> PluginInfo { PluginInfo { name: "HTTP API", version: "0.2.0", description: "Web UI + REST API (warp)", platform: Platform::Any, } } fn init(&mut self, ctx: PluginContext) -> Result<()> { self.state = Some(Arc::new(HttpState::new(Arc::clone(&ctx.config)))); self.ctx = Some(ctx); Ok(()) } fn start(&mut self) -> Result<()> { let ctx = self .ctx .as_ref() .context("http plugin context is not initialized")?; if !ctx.config.remote_control.enabled { println!("[HttpPlugin] Remote control disabled, skip HTTP server startup"); return Ok(()); } let host = ctx.config.remote_control.host.clone(); let port = ctx.config.remote_control.port; let tx = ctx.tx.clone(); let state = Arc::clone( self.state .as_ref() .context("http plugin state is not initialized")?, ); std::thread::spawn(move || { let runtime = match tokio::runtime::Builder::new_multi_thread() .enable_all() .build() { Ok(runtime) => runtime, Err(error) => { eprintln!("[HttpPlugin] failed to create tokio runtime: {error}"); return; } }; runtime.block_on(async move { let routes = routes::build_routes(tx.clone(), state); let addr: std::net::SocketAddr = match format!("{host}:{port}").parse() { Ok(addr) => addr, Err(error) => { eprintln!("[HttpPlugin] invalid listen address {host}:{port}: {error}"); return; } }; if let Err(error) = tx.send(Envelope { from: "http", to: crate::core::message::Destination::Manager, message: Message::PluginReady("http"), }) { eprintln!("[HttpPlugin] failed to report ready state: {error}"); } println!("[HttpPlugin] listening on http://{addr}"); warp::serve(routes).run(addr).await; }); }); Ok(()) } fn handle_message(&mut self, msg: Message) -> Result<()> { let state = match self.state.as_ref() { Some(state) => state, None => return Ok(()), }; match msg { Message::WifiResult(payload) => state.publish_wifi_result(payload), Message::PlayerStatus(status) => state.update_player_status(status), Message::ConfigReloaded(config) => state.replace_config(config), Message::PluginReady("ble") => state.set_ble_ready(true), Message::Shutdown => state.set_ble_ready(false), _ => {} } Ok(()) } fn stop(&mut self) -> Result<()> { Ok(()) } }