Files
AutoJellyProxy/src/server.rs

899 lines
31 KiB
Rust

use crate::{
auth::{parse_authorization_header, verify_password, AuthRequest},
config::Config,
database::{Database, Device},
jellyfin::{power_on_server, BrandingConfig, JellyfinClient, SystemInfo},
proxy::proxy_to_jellyfin_with_retry,
sftp::{calculate_local_file_hash, SftpClient},
websocket::proxy_websocket,
};
use anyhow::{anyhow, Result};
use axum::{
body::Body,
extract::{ws::WebSocketUpgrade, State},
http::{HeaderMap, Method, StatusCode, Uri},
response::{IntoResponse, Response},
routing::any,
Router,
};
use std::{
collections::HashMap,
path::Path as StdPath,
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use tokio::time::sleep;
use tracing::{debug, error, info, warn};
const LOCAL_DB_PATH: &str = "./jellyfin.db";
const SYSTEM_INFO_PATH: &str = "./system_info.json";
// Embedded login HTML content at build time
const LOGIN_HTML: &str = include_str!("../login.html");
pub struct AppState {
config: Config,
jellyfin_client: JellyfinClient,
sftp_client: SftpClient,
cached_system_info: RwLock<Option<SystemInfo>>,
is_jellyfin_online: RwLock<bool>,
last_db_hash: RwLock<Option<String>>,
last_activity: RwLock<Option<Instant>>,
is_powering_on: RwLock<bool>,
power_on_start_time: RwLock<Option<Instant>>,
}
impl AppState {
pub async fn new(config: Config) -> Result<Arc<Self>> {
let jellyfin_client = JellyfinClient::new(config.jellyfin_url.clone(), config.jellyfin_api_key.clone());
let sftp_client = SftpClient::new(
config.sftp_host.clone(),
config.sftp_port,
config.sftp_user.clone(),
config.sftp_password.clone(),
);
// Try to load cached system info
let cached_system_info = if StdPath::new(SYSTEM_INFO_PATH).exists() {
match std::fs::read_to_string(SYSTEM_INFO_PATH) {
Ok(content) => serde_json::from_str(&content).ok(),
Err(_) => None,
}
} else {
None
};
// Initial database download
if let Err(e) = sftp_client.download_file(&config.sftp_path, LOCAL_DB_PATH).await {
warn!("Failed to download initial database: {}", e);
}
let app_state = Self {
config,
jellyfin_client,
sftp_client,
cached_system_info: RwLock::new(cached_system_info),
is_jellyfin_online: RwLock::new(false),
last_db_hash: RwLock::new(None),
last_activity: RwLock::new(None),
is_powering_on: RwLock::new(false),
power_on_start_time: RwLock::new(None),
};
// Initial status check
app_state.update_jellyfin_status().await?;
// Start background database update checker
let state_clone = Arc::new(app_state);
let checker_state = state_clone.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30)); // Check every 30 seconds
loop {
interval.tick().await;
if let Err(e) = checker_state.check_database_updates().await {
warn!("Failed to check database updates: {}", e);
}
}
});
Ok(state_clone)
}
pub async fn update_jellyfin_status(&self) -> Result<()> {
let is_online = self.jellyfin_client.is_online().await;
*self.is_jellyfin_online.write().unwrap() = is_online;
if is_online {
// Update system info cache
if let Ok(system_info) = self.jellyfin_client.get_system_info().await {
*self.cached_system_info.write().unwrap() = Some(system_info.clone());
// Save to file
if let Ok(json_str) = serde_json::to_string_pretty(&system_info) {
let _ = std::fs::write(SYSTEM_INFO_PATH, json_str);
}
}
}
Ok(())
}
pub async fn check_database_updates(&self) -> Result<()> {
match self.sftp_client.get_file_hash(&self.config.sftp_path).await {
Ok(remote_hash) => {
let current_hash = if StdPath::new(LOCAL_DB_PATH).exists() {
calculate_local_file_hash(LOCAL_DB_PATH).ok()
} else {
None
};
let last_hash = self.last_db_hash.read().unwrap().clone();
if Some(&remote_hash) != last_hash.as_ref() || current_hash.as_ref() != Some(&remote_hash) {
info!("Database hash changed, downloading new version");
if let Err(e) = self.sftp_client.download_file(&self.config.sftp_path, LOCAL_DB_PATH).await {
error!("Failed to download updated database: {}", e);
} else {
*self.last_db_hash.write().unwrap() = Some(remote_hash);
info!("Database updated successfully");
}
}
}
Err(e) => {
warn!("Failed to check remote database hash: {}", e);
}
}
Ok(())
}
pub fn is_online(&self) -> bool {
*self.is_jellyfin_online.read().unwrap()
}
pub fn is_powering_on(&self) -> bool {
*self.is_powering_on.read().unwrap()
}
pub fn is_power_on_timeout(&self) -> bool {
if let Some(start_time) = *self.power_on_start_time.read().unwrap() {
start_time.elapsed() > Duration::from_secs(300) // 5 minutes
} else {
false
}
}
pub async fn update_and_check_status(&self) -> bool {
let is_online = self.jellyfin_client.is_online().await;
*self.is_jellyfin_online.write().unwrap() = is_online;
if is_online {
// Reset power-on state if server came online
*self.is_powering_on.write().unwrap() = false;
*self.power_on_start_time.write().unwrap() = None;
}
is_online
}
pub fn update_activity(&self) {
*self.last_activity.write().unwrap() = Some(Instant::now());
}
// Check for database updates on every authentication-related operation
async fn get_database_with_update_check(&self) -> Result<Database> {
// Check for updates first
if let Err(e) = self.check_database_updates().await {
warn!("Failed to check database updates during access: {}", e);
}
if !StdPath::new(LOCAL_DB_PATH).exists() {
return Err(anyhow!("Local database not found"));
}
Database::new(LOCAL_DB_PATH)
}
async fn authenticate_user(&self, username: &str, password: &str) -> Result<Option<crate::database::User>> {
let db = self.get_database_with_update_check().await?;
if let Some(user) = db.get_user_by_username(username)? {
if verify_password(password, &user.password)? {
return Ok(Some(user));
}
}
Ok(None)
}
async fn validate_token(&self, token: &str) -> Result<Option<Device>> {
let db = self.get_database_with_update_check().await?;
db.get_device_by_access_token(token)
}
async fn validate_device_id(&self, device_id: &str) -> Result<Option<Device>> {
let db = self.get_database_with_update_check().await?;
db.get_device_by_device_id(device_id)
}
}
pub fn create_app(state: Arc<AppState>) -> Router {
Router::new()
.route("/", any(handle_root_request))
.route("/web", any(handle_web_request))
.route("/web/", any(handle_web_request))
.route("/web/*path", any(handle_web_request))
.route("/Users/AuthenticateByName", any(handle_auth_request))
.route("/System/Info/Public", any(handle_system_info_request))
.route("/Branding/Configuration", any(handle_branding_request))
.fallback(handle_fallback_request)
.with_state(state)
}
async fn handle_root_request(
State(state): State<Arc<AppState>>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Body,
) -> Result<Response<Body>, StatusCode> {
state.update_activity();
// Check authentication first
let is_authenticated = check_authentication(&state, &headers, &uri.query().map(|q| q.to_string())).await;
// If server is offline but user is authenticated, power it on and wait
if !state.is_online() && is_authenticated {
ensure_server_online_for_authenticated_request(&state).await?;
}
// If server is online (either was online or just came online), proxy the request to Jellyfin
if state.is_online() {
let path = uri.path();
let query = uri.query().map(|q| q.to_string());
let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
error!("Failed to read request body: {}", e);
return Err(StatusCode::BAD_REQUEST);
}
};
return proxy_to_jellyfin_with_retry(
method,
path,
query,
headers,
body_bytes,
&state.jellyfin_client,
{
let state_clone = state.clone();
move || {
let state_clone = state_clone.clone();
Box::pin(async move {
state_clone.update_and_check_status().await
})
}
}
).await;
}
// If we reach here, server is offline and user is not authenticated
// Serve login page only for GET requests
if method == Method::GET {
Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/html")
.body(Body::from(LOGIN_HTML))
.unwrap())
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
async fn handle_web_request(
State(state): State<Arc<AppState>>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Body,
) -> Result<Response<Body>, StatusCode> {
state.update_activity();
// Check authentication first
let is_authenticated = check_authentication(&state, &headers, &uri.query().map(|q| q.to_string())).await;
// If server is offline but user is authenticated, power it on and wait
if !state.is_online() && is_authenticated {
ensure_server_online_for_authenticated_request(&state).await?;
}
// If server is online (either was online or just came online), proxy the request to Jellyfin
if state.is_online() {
let path = uri.path();
let query = uri.query().map(|q| q.to_string());
let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
error!("Failed to read request body: {}", e);
return Err(StatusCode::BAD_REQUEST);
}
};
return proxy_to_jellyfin_with_retry(
method,
path,
query,
headers,
body_bytes,
&state.jellyfin_client,
{
let state_clone = state.clone();
move || {
let state_clone = state_clone.clone();
Box::pin(async move {
state_clone.update_and_check_status().await
})
}
}
).await;
}
// If we reach here, server is offline and user is not authenticated
// Serve login page only for GET requests
if method == Method::GET {
Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/html")
.body(Body::from(LOGIN_HTML))
.unwrap())
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
async fn handle_auth_request(
State(state): State<Arc<AppState>>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Body,
) -> Result<Response<Body>, StatusCode> {
state.update_activity();
// Check authentication first (for existing session tokens)
let is_authenticated = check_authentication(&state, &headers, &uri.query().map(|q| q.to_string())).await;
// If server is offline but user is authenticated, power it on and wait
if !state.is_online() && is_authenticated {
ensure_server_online_for_authenticated_request(&state).await?;
}
// If server is online, proxy the request to Jellyfin
if state.is_online() {
let path = uri.path();
let query = uri.query().map(|q| q.to_string());
let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
error!("Failed to read request body: {}", e);
return Err(StatusCode::BAD_REQUEST);
}
};
return proxy_to_jellyfin_with_retry(
method,
path,
query,
headers,
body_bytes,
&state.jellyfin_client,
{
let state_clone = state.clone();
move || {
let state_clone = state_clone.clone();
Box::pin(async move {
state_clone.update_and_check_status().await
})
}
}
).await;
}
// If server is offline, handle authentication locally
if method == Method::POST {
let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
error!("Failed to read request body: {}", e);
return Err(StatusCode::BAD_REQUEST);
}
};
let auth_request: AuthRequest = match serde_json::from_slice(&body_bytes) {
Ok(req) => req,
Err(e) => {
error!("Failed to parse auth request: {}", e);
return Err(StatusCode::BAD_REQUEST);
}
};
// Validate credentials locally first - don't start server if invalid
match state.authenticate_user(&auth_request.username, &auth_request.pw).await {
Ok(Some(_user)) => {
info!("Credentials validated locally, starting server and proxying to Jellyfin");
// User is valid, power on server and wait for it to come online
ensure_server_online_for_authenticated_request(&state).await?;
// Once server is online, proxy the original request to Jellyfin for real auth response
if state.is_online() {
let path = uri.path();
let query = uri.query().map(|q| q.to_string());
return proxy_to_jellyfin_with_retry(
method,
path,
query,
headers,
body_bytes,
&state.jellyfin_client,
{
let state_clone = state.clone();
move || {
let state_clone = state_clone.clone();
Box::pin(async move {
state_clone.update_and_check_status().await
})
}
}
).await;
} else {
error!("Server failed to come online after authentication");
Err(StatusCode::SERVICE_UNAVAILABLE)
}
}
Ok(None) => {
warn!("Authentication failed for user: {} - not starting server", auth_request.username);
Err(StatusCode::UNAUTHORIZED)
}
Err(e) => {
error!("Database error during authentication: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
} else {
Err(StatusCode::METHOD_NOT_ALLOWED)
}
}
async fn handle_system_info_request(
State(state): State<Arc<AppState>>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Body,
) -> Result<Response<Body>, StatusCode> {
state.update_activity();
// Check authentication first
let is_authenticated = check_authentication(&state, &headers, &uri.query().map(|q| q.to_string())).await;
// If server is offline but user is authenticated, power it on
if !state.is_online() && is_authenticated {
ensure_server_online_for_authenticated_request(&state).await?;
}
// If server is online, proxy the request to Jellyfin
if state.is_online() {
let path = uri.path();
let query = uri.query().map(|q| q.to_string());
let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
error!("Failed to read request body: {}", e);
return Err(StatusCode::BAD_REQUEST);
}
};
return proxy_to_jellyfin_with_retry(
method,
path,
query,
headers,
body_bytes,
&state.jellyfin_client,
{
let state_clone = state.clone();
move || {
let state_clone = state_clone.clone();
Box::pin(async move {
state_clone.update_and_check_status().await
})
}
}
).await;
}
// If server is offline, return cached system info (this endpoint is usually public)
let system_info = get_system_info_impl(state).await;
let json_body = serde_json::to_string(&system_info).unwrap();
Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Body::from(json_body))
.unwrap())
}
async fn handle_branding_request(
State(state): State<Arc<AppState>>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Body,
) -> Result<Response<Body>, StatusCode> {
state.update_activity();
// Check authentication first
let is_authenticated = check_authentication(&state, &headers, &uri.query().map(|q| q.to_string())).await;
// If server is offline but user is authenticated, power it on
if !state.is_online() && is_authenticated {
ensure_server_online_for_authenticated_request(&state).await?;
}
// If server is online, proxy the request to Jellyfin
if state.is_online() {
let path = uri.path();
let query = uri.query().map(|q| q.to_string());
let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
error!("Failed to read request body: {}", e);
return Err(StatusCode::BAD_REQUEST);
}
};
return proxy_to_jellyfin_with_retry(
method,
path,
query,
headers,
body_bytes,
&state.jellyfin_client,
{
let state_clone = state.clone();
move || {
let state_clone = state_clone.clone();
Box::pin(async move {
state_clone.update_and_check_status().await
})
}
}
).await;
}
// If server is offline, return offline branding config (this endpoint is usually public)
let branding_config = get_branding_config_impl(state).await;
let json_body = serde_json::to_string(&branding_config).unwrap();
Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(Body::from(json_body))
.unwrap())
}
async fn get_system_info_impl(state: Arc<AppState>) -> SystemInfo {
if state.is_online() {
// Try to get fresh info from Jellyfin
if let Ok(info) = state.jellyfin_client.get_system_info().await {
return info;
}
}
// Return cached info or default
let cached_info = state.cached_system_info.read().unwrap();
if let Some(mut info) = cached_info.clone() {
if !state.is_online() {
info.server_name = format!("{} (Offline)", info.server_name);
}
info
} else {
SystemInfo {
local_address: "http://localhost:8096".to_string(),
server_name: "Jellyfin Server (Offline)".to_string(),
version: "10.10.6".to_string(),
product_name: "Jellyfin Server".to_string(),
operating_system: "".to_string(),
id: "unknown".to_string(),
startup_wizard_completed: true,
}
}
}
async fn get_branding_config_impl(state: Arc<AppState>) -> BrandingConfig {
if state.is_online() {
if let Ok(config) = state.jellyfin_client.get_branding_config().await {
return config;
}
}
BrandingConfig {
login_disclaimer: "This server is currently offline. Log-in to start the server.".to_string(),
custom_css: "".to_string(),
splashscreen_enabled: true,
}
}
async fn handle_fallback_request(
ws: Option<WebSocketUpgrade>,
State(state): State<Arc<AppState>>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Body,
) -> Result<Response<Body>, StatusCode> {
// Check if this is a WebSocket upgrade request
if let Some(ws_upgrade) = ws {
return handle_websocket_request(ws_upgrade, state, uri, headers).await;
}
// Handle as regular HTTP request
handle_proxy_request(None, State(state), method, uri, headers, body).await
}
async fn handle_websocket_request(
ws_upgrade: WebSocketUpgrade,
state: Arc<AppState>,
uri: Uri,
headers: HeaderMap,
) -> Result<Response<Body>, StatusCode> {
state.update_activity();
let path = uri.path().to_string();
let query = uri.query().map(|q| q.to_string());
// Check authentication for WebSocket connections
let is_authenticated = check_authentication(&state, &headers, &query).await;
// If server is offline but user is authenticated, power it on
if !state.is_online() && is_authenticated {
ensure_server_online_for_authenticated_request(&state).await?;
}
// Check if server is online for WebSocket connections
if !state.is_online() {
// For WebSocket connections when offline, we need authentication
if !is_authenticated {
return Err(StatusCode::UNAUTHORIZED);
}
}
// Handle WebSocket upgrade
let jellyfin_url = state.jellyfin_client.get_base_url().to_string();
let query_str = query.clone();
let headers_clone = headers.clone();
Ok(ws_upgrade.on_upgrade(move |socket| async move {
proxy_websocket(socket, &jellyfin_url, &path, query_str.as_deref(), &headers_clone).await;
}).into_response())
}
async fn handle_proxy_request(
_ws: Option<WebSocketUpgrade>,
State(state): State<Arc<AppState>>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Body,
) -> Result<Response<Body>, StatusCode> {
state.update_activity();
let path = uri.path();
let query = uri.query().map(|q| q.to_string());
// Check authentication for all requests
let is_authenticated = check_authentication(&state, &headers, &query).await;
// If server is offline but user is authenticated, power it on
if !state.is_online() && is_authenticated {
ensure_server_online_for_authenticated_request(&state).await?;
}
// Handle regular HTTP requests
let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
Ok(bytes) => bytes.to_vec(),
Err(e) => {
error!("Failed to read request body: {}", e);
return Err(StatusCode::BAD_REQUEST);
}
};
if !state.is_online() && !is_authenticated {
return Err(StatusCode::UNAUTHORIZED);
}
// Proxy to Jellyfin
proxy_to_jellyfin_with_retry(
method,
path,
query,
headers,
body_bytes,
&state.jellyfin_client,
{
let state_clone = state.clone();
move || {
let state_clone = state_clone.clone();
Box::pin(async move {
state_clone.update_and_check_status().await
})
}
}
).await
}
async fn check_authentication(
state: &Arc<AppState>,
headers: &HeaderMap,
query: &Option<String>,
) -> bool {
// Check for API key in query parameters
if let Some(query_str) = query {
let params: HashMap<_, _> = url::form_urlencoded::parse(query_str.as_bytes()).collect();
if let Some(api_key) = params.get("api_key") {
if let Ok(Some(_)) = state.validate_token(api_key).await {
debug!("Valid API key found in query parameters");
return true;
}
}
if let Some(device_id) = params.get("deviceId").or_else(|| params.get("DeviceId")) {
if let Ok(Some(_)) = state.validate_device_id(device_id).await {
debug!("Valid device ID found in query parameters");
return true;
}
}
}
// Check authorization header
if let Some(auth_header) = headers.get("authorization") {
if let Ok(header_str) = auth_header.to_str() {
debug!("Checking authorization header: {}", header_str);
// First, try to extract token from MediaBrowser header
if header_str.starts_with("MediaBrowser ") && header_str.contains("Token=") {
if let Some(token_start) = header_str.find("Token=\"") {
let token_start = token_start + 7; // Skip 'Token="'
if let Some(token_end) = header_str[token_start..].find('"') {
let token = &header_str[token_start..token_start + token_end];
debug!("Extracted token from header: {}", token);
if let Ok(Some(_)) = state.validate_token(token).await {
debug!("Valid token found in authorization header");
return true;
} else {
debug!("Token validation failed for: {}", token);
}
}
} else if let Some(token_start) = header_str.find("Token=") {
// Handle case without quotes around token value
let token_start = token_start + 6; // Skip 'Token='
let token_end = header_str[token_start..].find(',').or_else(||
header_str[token_start..].find(' ')).unwrap_or(header_str.len() - token_start);
let token = &header_str[token_start..token_start + token_end].trim_matches('"');
debug!("Extracted token from header (no quotes): {}", token);
if let Ok(Some(_)) = state.validate_token(token).await {
debug!("Valid token found in authorization header (no quotes)");
return true;
} else {
debug!("Token validation failed for: {}", token);
}
}
}
// Also try to parse device ID from header and validate it
if let Some((_, _, device_id, _)) = parse_authorization_header(header_str) {
// URL decode the device ID since it might be encoded
if let Ok(decoded_device_id) = urlencoding::decode(&device_id) {
debug!("Checking device ID: {}", decoded_device_id);
if let Ok(Some(_)) = state.validate_device_id(&decoded_device_id).await {
debug!("Valid device ID found in authorization header");
return true;
}
}
// Also try the non-decoded version
if let Ok(Some(_)) = state.validate_device_id(&device_id).await {
debug!("Valid device ID found in authorization header (non-decoded)");
return true;
}
}
}
}
debug!("No valid authentication found");
false
}
async fn ensure_server_online_for_authenticated_request(state: &Arc<AppState>) -> Result<(), StatusCode> {
// If server is already online, nothing to do
if state.is_online() {
return Ok(());
}
// Check if we're already powering on
if state.is_powering_on() {
// Check if power-on has timed out
if state.is_power_on_timeout() {
error!("Server power-on timed out, resetting power-on state");
*state.is_powering_on.write().unwrap() = false;
*state.power_on_start_time.write().unwrap() = None;
return Err(StatusCode::SERVICE_UNAVAILABLE);
}
// Wait for server to come online or timeout
let max_wait = Duration::from_secs(300); // 5 minutes
let start_check = Instant::now();
info!("Server is being powered on, waiting for it to come online...");
while start_check.elapsed() < max_wait {
// Check if server came online
if state.jellyfin_client.is_online().await {
*state.is_jellyfin_online.write().unwrap() = true;
*state.is_powering_on.write().unwrap() = false;
*state.power_on_start_time.write().unwrap() = None;
info!("Server came online successfully");
return Ok(());
}
// Check if power-on process timed out
if state.is_power_on_timeout() {
error!("Server power-on timed out while waiting");
*state.is_powering_on.write().unwrap() = false;
*state.power_on_start_time.write().unwrap() = None;
return Err(StatusCode::SERVICE_UNAVAILABLE);
}
sleep(tokio::time::Duration::from_secs(5)).await;
}
error!("Timed out waiting for server to come online");
return Err(StatusCode::SERVICE_UNAVAILABLE);
}
// Start power-on process
info!("Server is offline but user is authenticated, powering on");
*state.is_powering_on.write().unwrap() = true;
*state.power_on_start_time.write().unwrap() = Some(Instant::now());
if let Err(e) = power_on_server(&state.config.jellyfin_power_on_command).await {
error!("Failed to power on server: {}", e);
*state.is_powering_on.write().unwrap() = false;
*state.power_on_start_time.write().unwrap() = None;
return Err(StatusCode::SERVICE_UNAVAILABLE);
}
// Wait for server to come online
let max_wait = Duration::from_secs(300); // 5 minutes
let start_time = Instant::now();
while start_time.elapsed() < max_wait {
if state.jellyfin_client.is_online().await {
*state.is_jellyfin_online.write().unwrap() = true;
*state.is_powering_on.write().unwrap() = false;
*state.power_on_start_time.write().unwrap() = None;
info!("Server came online successfully");
return Ok(());
}
sleep(tokio::time::Duration::from_secs(5)).await;
}
error!("Server failed to come online within timeout");
*state.is_powering_on.write().unwrap() = false;
*state.power_on_start_time.write().unwrap() = None;
Err(StatusCode::SERVICE_UNAVAILABLE)
}