use axum::{ extract::State, http::HeaderMap, response::Json, }; use serde::Deserialize; use serde_json; use crate::{ error::{AppError, AppResult}, handlers::{extract_api_key, validate_api_key}, AppState, }; #[derive(Deserialize)] pub struct InitRequest { pub api_key: String, } // Startup tasks endpoint - matches Python startup_tasks function exactly pub async fn startup_tasks( State(state): State, Json(request): Json, ) -> Result, AppError> { // Verify if the API key is valid let is_valid = validate_api_key(&state, &request.api_key).await?; if !is_valid { return Err(AppError::forbidden("Invalid or unauthorized API key")); } // Check if the provided API key is from the background_tasks user (UserID 1) let api_user_id = state.db_pool.get_user_id_from_api_key(&request.api_key).await?; if api_user_id != 1 { return Err(AppError::forbidden("Invalid or unauthorized API key")); } // Execute the startup tasks state.db_pool.add_news_feed_if_not_added().await?; // Create default playlists for any users that might be missing them state.db_pool.create_missing_default_playlists().await?; Ok(Json(serde_json::json!({"status": "Startup tasks completed successfully."}))) } // Cleanup tasks endpoint - matches Python cleanup_tasks function exactly pub async fn cleanup_tasks( headers: HeaderMap, State(state): State, ) -> Result, AppError> { let api_key = extract_api_key(&headers)?; // Verify if the API key is valid and is web key (admin only) let is_valid = validate_api_key(&state, &api_key).await?; if !is_valid { return Err(AppError::forbidden("Invalid API key")); } let api_user_id = state.db_pool.get_user_id_from_api_key(&api_key).await?; if api_user_id != 1 { return Err(AppError::forbidden("Admin access required")); } // Run cleanup tasks in background let db_pool = state.db_pool.clone(); let task_id = state.task_spawner.spawn_progress_task( "cleanup_tasks".to_string(), 0, // System user move |reporter| async move { reporter.update_progress(50.0, Some("Running cleanup tasks...".to_string())).await?; db_pool.cleanup_old_episodes().await .map_err(|e| AppError::internal(&format!("Cleanup failed: {}", e)))?; reporter.update_progress(100.0, Some("Cleanup completed successfully".to_string())).await?; Ok(serde_json::json!({"status": "Cleanup tasks completed successfully"})) }, ).await?; Ok(Json(serde_json::json!({ "detail": "Cleanup tasks initiated.", "task_id": task_id }))) } // Update playlists endpoint - matches Python update_playlists function exactly pub async fn update_playlists( headers: HeaderMap, State(state): State, ) -> Result, AppError> { let api_key = extract_api_key(&headers)?; // Verify if the API key is valid and is web key (admin only) let is_valid = validate_api_key(&state, &api_key).await?; if !is_valid { return Err(AppError::forbidden("Invalid API key")); } let api_user_id = state.db_pool.get_user_id_from_api_key(&api_key).await?; if api_user_id != 1 { return Err(AppError::forbidden("Admin access required")); } // Run playlist update in background let db_pool = state.db_pool.clone(); let task_id = state.task_spawner.spawn_progress_task( "update_playlists".to_string(), 0, // System user move |reporter| async move { reporter.update_progress(50.0, Some("Updating all playlists...".to_string())).await?; db_pool.update_all_playlists().await .map_err(|e| AppError::internal(&format!("Playlist update failed: {}", e)))?; reporter.update_progress(100.0, Some("Playlist update completed successfully".to_string())).await?; Ok(serde_json::json!({"status": "Playlist update completed successfully"})) }, ).await?; Ok(Json(serde_json::json!({ "detail": "Playlist update initiated.", "task_id": task_id }))) } // Refresh hosts endpoint - matches Python refresh_all_hosts function exactly pub async fn refresh_hosts( headers: HeaderMap, State(state): State, ) -> Result, AppError> { let api_key = extract_api_key(&headers)?; // Verify it's the system API key (background_tasks user with UserID 1) let is_valid = validate_api_key(&state, &api_key).await?; if !is_valid { return Err(AppError::forbidden("Invalid API key")); } let api_user_id = state.db_pool.get_user_id_from_api_key(&api_key).await?; if api_user_id != 1 { return Err(AppError::forbidden("This endpoint requires system API key")); } // Run host refresh in background let db_pool = state.db_pool.clone(); let task_id = state.task_spawner.spawn_progress_task( "refresh_hosts".to_string(), 0, // System user move |reporter| async move { reporter.update_progress(10.0, Some("Getting all people/hosts...".to_string())).await?; let all_people = db_pool.get_all_people_for_refresh().await .map_err(|e| AppError::internal(&format!("Failed to get people: {}", e)))?; tracing::info!("Found {} people/hosts to refresh", all_people.len()); let mut successful_refreshes = 0; let mut failed_refreshes = 0; for (index, (person_id, person_name, user_id)) in all_people.iter().enumerate() { let progress = 10.0 + (80.0 * (index as f64) / (all_people.len() as f64)); reporter.update_progress(progress, Some(format!("Refreshing host: {} ({}/{})", person_name, index + 1, all_people.len()))).await?; tracing::info!("Starting refresh for host: {} (ID: {}, User: {})", person_name, person_id, user_id); match process_person_refresh(&db_pool, *person_id, person_name, *user_id).await { Ok(_) => { successful_refreshes += 1; tracing::info!("Successfully refreshed host: {}", person_name); } Err(e) => { failed_refreshes += 1; tracing::error!("Failed to refresh host {}: {}", person_name, e); } } } // After processing all people, trigger the regular podcast refresh tracing::info!("Person subscription processed, initiating server refresh..."); match trigger_podcast_refresh(&db_pool).await { Ok(_) => { tracing::info!("Server refresh completed successfully"); } Err(e) => { tracing::error!("Error during server refresh: {}", e); } } tracing::info!("Host refresh completed: {}/{} successful, {} failed", successful_refreshes, all_people.len(), failed_refreshes); reporter.update_progress(100.0, Some(format!( "Host refresh completed: {}/{} successful", successful_refreshes, all_people.len() ))).await?; Ok(serde_json::json!({ "success": true, "hosts_refreshed": successful_refreshes, "hosts_failed": failed_refreshes, "total_hosts": all_people.len() })) }, ).await?; Ok(Json(serde_json::json!({ "detail": "Host refresh initiated.", "task_id": task_id }))) } // Helper function to process individual person refresh - matches Python process_person_subscription async fn process_person_refresh( db_pool: &crate::database::DatabasePool, person_id: i32, person_name: &str, user_id: i32, ) -> AppResult<()> { tracing::info!("Processing person subscription for: {} (ID: {}, User: {})", person_name, person_id, user_id); // Get person details and refresh their content match db_pool.process_person_subscription(user_id, person_id, person_name.to_string()).await { Ok(_) => { tracing::info!("Successfully processed person subscription for {}", person_name); Ok(()) } Err(e) => { tracing::error!("Error processing person subscription for {}: {}", person_name, e); Err(e) } } } // Helper function to trigger podcast refresh after person processing - matches Python refresh_pods_task async fn trigger_podcast_refresh(db_pool: &crate::database::DatabasePool) -> AppResult<()> { // Get all users with podcasts and refresh them let all_users = db_pool.get_all_users_with_podcasts().await?; for user_id in all_users { match refresh_user_podcasts(db_pool, user_id).await { Ok((podcast_count, episode_count)) => { tracing::info!("Successfully refreshed user {}: {} podcasts, {} new episodes", user_id, podcast_count, episode_count); } Err(e) => { tracing::error!("Failed to refresh user {}: {}", user_id, e); } } } Ok(()) } // Helper function to refresh podcasts for a single user async fn refresh_user_podcasts(db_pool: &crate::database::DatabasePool, user_id: i32) -> AppResult<(i32, i32)> { let podcasts = db_pool.get_user_podcasts_for_refresh(user_id).await?; let mut successful_podcasts = 0; let mut total_new_episodes = 0; for podcast in podcasts { match refresh_single_podcast(db_pool, &podcast).await { Ok(new_episode_count) => { successful_podcasts += 1; total_new_episodes += new_episode_count; tracing::info!("Refreshed podcast '{}': {} new episodes", podcast.name, new_episode_count); } Err(e) => { tracing::error!("Failed to refresh podcast '{}': {}", podcast.name, e); } } } Ok((successful_podcasts, total_new_episodes)) } // Auto-complete episodes based on user settings - nightly task pub async fn auto_complete_episodes( headers: HeaderMap, State(state): State, ) -> Result, AppError> { let api_key = extract_api_key(&headers)?; // Verify if the API key is valid let is_valid = validate_api_key(&state, &api_key).await?; if !is_valid { return Err(AppError::forbidden("Invalid or unauthorized API key")); } // Check if the provided API key is from the background_tasks user (UserID 1) let api_user_id = state.db_pool.get_user_id_from_api_key(&api_key).await?; if api_user_id != 1 { return Err(AppError::forbidden("Invalid or unauthorized API key")); } // Get all users who have auto_complete_seconds > 0 let users_with_auto_complete = state.db_pool.get_users_with_auto_complete_enabled().await?; let mut total_completed = 0; for user in users_with_auto_complete { let completed_count = state.db_pool.auto_complete_user_episodes(user.user_id, user.auto_complete_seconds).await.unwrap_or(0); total_completed += completed_count; } Ok(Json(serde_json::json!({ "status": "Auto-complete task completed successfully", "episodes_completed": total_completed }))) } // Helper function to refresh a single podcast async fn refresh_single_podcast( _db_pool: &crate::database::DatabasePool, podcast: &crate::handlers::refresh::PodcastForRefresh, ) -> AppResult { tracing::info!("Refreshing podcast: {} (ID: {})", podcast.name, podcast.id); // This would normally refresh the podcast feed and return new episode count // For now return 0 as placeholder since we need the podcast refresh system to be implemented Ok(0) } // Internal functions for scheduler (no HTTP context needed) pub async fn cleanup_tasks_internal(state: &AppState) -> AppResult<()> { tracing::info!("Starting internal cleanup tasks (scheduler)"); state.db_pool.cleanup_old_episodes().await?; tracing::info!("Cleanup tasks completed successfully"); Ok(()) } pub async fn update_playlists_internal(state: &AppState) -> AppResult<()> { tracing::info!("Starting internal playlist update (scheduler)"); state.db_pool.update_all_playlists().await?; tracing::info!("Playlist update completed successfully"); Ok(()) } pub async fn refresh_hosts_internal(state: &AppState) -> AppResult<()> { tracing::info!("Starting internal host refresh (scheduler)"); let all_people = state.db_pool.get_all_people_for_refresh().await?; tracing::info!("Found {} people/hosts to refresh", all_people.len()); let mut successful_refreshes = 0; let mut failed_refreshes = 0; for (person_id, person_name, user_id) in all_people.iter() { tracing::info!("Starting refresh for host: {} (ID: {}, User: {})", person_name, person_id, user_id); match process_person_refresh(&state.db_pool, *person_id, person_name, *user_id).await { Ok(_) => { successful_refreshes += 1; tracing::info!("Successfully refreshed host: {}", person_name); } Err(e) => { failed_refreshes += 1; tracing::error!("Failed to refresh host {}: {}", person_name, e); } } } // After processing all people, trigger the regular podcast refresh tracing::info!("Person subscription processed, initiating server refresh..."); match trigger_podcast_refresh(&state.db_pool).await { Ok(_) => { tracing::info!("Server refresh completed successfully"); } Err(e) => { tracing::error!("Error during server refresh: {}", e); } } tracing::info!("Host refresh completed: {}/{} successful, {} failed", successful_refreshes, all_people.len(), failed_refreshes); Ok(()) } pub async fn auto_complete_episodes_internal(state: &AppState) -> AppResult<()> { tracing::info!("Starting internal auto-complete episodes (scheduler)"); // Get all users who have auto_complete_seconds > 0 let users_with_auto_complete = state.db_pool.get_users_with_auto_complete_enabled().await?; let mut total_completed = 0; for user in users_with_auto_complete { let completed_count = state.db_pool.auto_complete_user_episodes(user.user_id, user.auto_complete_seconds).await.unwrap_or(0); total_completed += completed_count; } tracing::info!("Auto-complete task completed: {} episodes completed", total_completed); Ok(()) }