added cargo files
This commit is contained in:
397
PinePods-0.8.2/rust-api/src/handlers/tasks.rs
Normal file
397
PinePods-0.8.2/rust-api/src/handlers/tasks.rs
Normal file
@@ -0,0 +1,397 @@
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user