update: pipeline, search, clip, embedding fixes
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user