Files
momentry_core/src/api/tmdb_api.rs
2026-05-17 19:46:35 +08:00

283 lines
8.6 KiB
Rust

use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use crate::api::server::AppState;
use crate::core::config;
use crate::core::db::PostgresDb;
use crate::core::tmdb;
#[derive(Debug, Serialize)]
struct TmdbPrefetchResponse {
success: bool,
file_uuid: String,
message: String,
cache_path: Option<String>,
}
#[derive(Debug, Serialize)]
struct TmdbProbeResponse {
success: bool,
file_uuid: String,
tmdb_id: Option<u64>,
movie_title: Option<String>,
cast_count: Option<usize>,
identities_created: Option<usize>,
message: String,
}
#[derive(Debug, Serialize)]
struct TmdbResourceResponse {
success: bool,
status: tmdb::status::TmdbResourceStatus,
identities_seeded: i64,
identities_with_embedding: i64,
cache_files: usize,
operations: Vec<TmdbOperation>,
}
#[derive(Debug, Serialize)]
struct TmdbOperation {
method: String,
path: String,
description: String,
}
#[derive(Debug, Serialize)]
struct TmdbCheckResponse {
success: bool,
status: tmdb::status::TmdbResourceStatus,
}
#[derive(Debug, Deserialize)]
struct PrefetchRequest {
file_uuid: String,
}
#[derive(Debug, Deserialize)]
struct FileUuidParam {
file_uuid: String,
}
pub fn tmdb_routes() -> Router<AppState> {
Router::new()
.route("/api/v1/agents/tmdb/prefetch", post(tmdb_prefetch))
.route("/api/v1/file/:file_uuid/tmdb-probe", post(tmdb_probe_handler))
.route("/api/v1/resource/tmdb", get(tmdb_resource_status))
.route("/api/v1/resource/tmdb/check", post(tmdb_resource_check))
}
async fn tmdb_prefetch(
State(state): State<AppState>,
Json(req): Json<PrefetchRequest>,
) -> Json<TmdbPrefetchResponse> {
let file_uuid = req.file_uuid;
// Verify file exists in DB
let file_exists: bool = sqlx::query_scalar(
&format!("SELECT COUNT(*) > 0 FROM {} WHERE file_uuid = $1", crate::core::db::schema::table_name("videos"))
)
.bind(&file_uuid)
.fetch_one(state.db.pool())
.await
.unwrap_or(false);
if !file_exists {
return Json(TmdbPrefetchResponse {
success: false,
file_uuid: file_uuid.clone(),
message: format!("File not found: {}", file_uuid),
cache_path: None,
});
}
// Offline-first: check if identity files already exist on disk (pre-prepared)
let identities_dir = std::path::Path::new(&*config::OUTPUT_DIR).join("identities");
let index_path = identities_dir.join("_index.json");
let cache_path = format!("{}/{}.tmdb.json", *config::OUTPUT_DIR, file_uuid);
let cache_file = std::path::Path::new(&cache_path);
if index_path.exists() && cache_file.exists() {
return Json(TmdbPrefetchResponse {
success: true,
file_uuid,
message: format!(
"Offline: using local identity files from {}.",
identities_dir.display()
),
cache_path: Some(cache_path),
});
}
if config::tmdb::API_KEY.is_none() {
return Json(TmdbPrefetchResponse {
success: false,
file_uuid: file_uuid.clone(),
message: "TMDB_API_KEY not configured and no local cache found.".to_string(),
cache_path: None,
});
}
let scripts_dir = config::SCRIPTS_DIR.clone();
let python_path = config::PYTHON_PATH.clone();
let agent_script = std::path::Path::new(&scripts_dir).join("tmdb_agent.py");
if !agent_script.exists() {
return Json(TmdbPrefetchResponse {
success: false,
file_uuid,
message: format!("tmdb_agent.py not found at {}", agent_script.display()),
cache_path: None,
});
}
let db_url = config::DATABASE_URL.clone();
let output = tokio::process::Command::new(&*python_path)
.arg(&agent_script)
.arg("--file-uuid")
.arg(&file_uuid)
.env("DATABASE_URL", &db_url)
.env("DATABASE_SCHEMA", &*config::DATABASE_SCHEMA)
.output()
.await;
match output {
Ok(o) => {
if o.status.success() {
let out = String::from_utf8_lossy(&o.stdout);
Json(TmdbPrefetchResponse {
success: true,
file_uuid,
message: out.lines().last().unwrap_or("OK").to_string(),
cache_path: Some(cache_path),
})
} else {
let stderr = String::from_utf8_lossy(&o.stderr);
Json(TmdbPrefetchResponse {
success: false,
file_uuid,
message: stderr.to_string(),
cache_path: None,
})
}
}
Err(e) => Json(TmdbPrefetchResponse {
success: false,
file_uuid,
message: format!("Failed to run tmdb_agent.py: {}", e),
cache_path: None,
}),
}
}
async fn tmdb_probe_handler(
Path(params): Path<FileUuidParam>,
State(state): State<AppState>,
) -> Result<Json<TmdbProbeResponse>, (StatusCode, Json<serde_json::Value>)> {
let file_uuid = params.file_uuid;
// Verify file exists
let file_exists: bool = sqlx::query_scalar(
&format!("SELECT COUNT(*) > 0 FROM {} WHERE file_uuid = $1", crate::core::db::schema::table_name("videos"))
)
.bind(&file_uuid)
.fetch_one(state.db.pool())
.await
.unwrap_or(false);
if !file_exists {
return Err((StatusCode::NOT_FOUND, Json(serde_json::json!({
"error": "Video not found", "file_uuid": file_uuid
}))));
}
match tmdb::probe::probe_from_cache(&state.db, &file_uuid).await {
Ok(result) => Ok(Json(TmdbProbeResponse {
success: true,
file_uuid,
tmdb_id: Some(result.tmdb_id),
movie_title: Some(result.title),
cast_count: Some(result.cast_count),
identities_created: Some(result.identities_created),
message: format!(
"Created/updated {} identities for movie ID {}",
result.identities_created, result.tmdb_id
),
})),
Err(e) => {
let msg = e.to_string();
if msg.contains("not found") {
Ok(Json(TmdbProbeResponse {
success: false,
file_uuid,
tmdb_id: None,
movie_title: None,
cast_count: None,
identities_created: None,
message: "No TMDb cache found. Run tmdb-prefetch first.".to_string(),
}))
} else {
Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
"error": msg, "file_uuid": file_uuid
}))))
}
}
}
}
async fn tmdb_resource_status(
State(state): State<AppState>,
) -> Json<TmdbResourceResponse> {
let status = tmdb::status::quick_status();
let identities_seeded = tmdb::status::count_tmdb_identities(state.db.pool())
.await
.unwrap_or(0);
let identities_with_embedding = tmdb::status::count_tmdb_identities_with_embedding(state.db.pool())
.await
.unwrap_or(0);
let cache_files = tmdb::status::count_cache_files();
Json(TmdbResourceResponse {
success: true,
status,
identities_seeded,
identities_with_embedding,
cache_files,
operations: vec![
TmdbOperation {
method: "GET".to_string(),
path: "/api/v1/resource/tmdb".to_string(),
description: "TMDb resource status".to_string(),
},
TmdbOperation {
method: "POST".to_string(),
path: "/api/v1/resource/tmdb/check".to_string(),
description: "Ping TMDb API health".to_string(),
},
TmdbOperation {
method: "POST".to_string(),
path: "/api/v1/agents/tmdb/prefetch".to_string(),
description: "Fetch TMDb data and cache locally".to_string(),
},
TmdbOperation {
method: "POST".to_string(),
path: "/api/v1/file/:file_uuid/tmdb-probe".to_string(),
description: "Read cache and create identities".to_string(),
},
],
})
}
async fn tmdb_resource_check() -> Json<TmdbCheckResponse> {
let status = tmdb::status::check_tmdb_api().await;
Json(TmdbCheckResponse {
success: status.api_reachable.unwrap_or(false) && status.api_key_configured,
status,
})
}