Files
PinePods-nix/PinePods-0.8.2/rust-api/src/handlers/tasks.rs
2026-03-03 10:57:43 -05:00

397 lines
15 KiB
Rust

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<AppState>,
Json(request): Json<InitRequest>,
) -> Result<Json<serde_json::Value>, 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<AppState>,
) -> Result<Json<serde_json::Value>, 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<AppState>,
) -> Result<Json<serde_json::Value>, 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<AppState>,
) -> Result<Json<serde_json::Value>, 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<AppState>,
) -> Result<Json<serde_json::Value>, 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<i32> {
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(())
}