feat: Identity JSON sync mechanism

- storage.rs: add local_profile field, check disk for profile.jpg in save_identity_file_by_pool
- tmdb_api.rs: trigger JSON sync after TMDb probe
- identity_api.rs: upload_profile_image triggers JSON sync
- identity_binding.rs: bind/unbind/merge trigger JSON sync
- get_identity_json: replace DB fallback with Lazy Sync (generates JSON from DB if missing)
- Fixes missing/obsolete JSON files for all identity mutations
This commit is contained in:
Accusys
2026-05-19 22:20:19 +08:00
parent 7680c202ef
commit 0eb08acaae
4 changed files with 129 additions and 54 deletions

View File

@@ -18,6 +18,9 @@ pub struct IdentityFile {
pub status: Option<String>,
pub tmdb_id: Option<i32>,
pub tmdb_profile: Option<String>,
/// Local profile image filename (e.g., "profile.jpg") if uploaded by user.
/// Overrides tmdb_profile if present.
pub local_profile: Option<String>,
pub metadata: serde_json::Value,
pub file_bindings: Vec<FileBinding>,
pub created_at: String,
@@ -187,7 +190,7 @@ pub async fn save_identity_file_by_pool(pool: &sqlx::PgPool, uuid: &str) -> Resu
let clean = uuid.replace('-', "");
let record = sqlx::query_as::<_, crate::core::db::IdentityDetailRecord>(
&format!(
"SELECT id, uuid::text, name, identity_type, source, status, metadata, reference_data, \
"SELECT id, uuid::text, COALESCE(real_name, actor_name, name) AS name, identity_type, source, status, metadata, reference_data, \
NULL::real[] as voice_embedding, NULL::real[] as identity_embedding, \
face_embedding::real[] as face_embedding, \
tmdb_id, tmdb_profile, created_at::timestamptz as created_at, NULL::timestamptz as updated_at \
@@ -222,6 +225,14 @@ pub async fn save_identity_file_by_pool(pool: &sqlx::PgPool, uuid: &str) -> Resu
})
.collect();
// Check for local profile image
let profile_path = identity_dir(&clean).join("profile.jpg");
let local_profile = if profile_path.exists() {
Some("profile.jpg".to_string())
} else {
None
};
let fmt_time = |dt: Option<chrono::DateTime<chrono::Utc>>| -> String {
dt.map(|d| d.to_rfc3339())
.unwrap_or_else(|| chrono::Utc::now().to_rfc3339())
@@ -236,6 +247,7 @@ pub async fn save_identity_file_by_pool(pool: &sqlx::PgPool, uuid: &str) -> Resu
status: record.status,
tmdb_id: record.tmdb_id,
tmdb_profile: record.tmdb_profile,
local_profile,
metadata: record.metadata,
file_bindings,
created_at: fmt_time(record.created_at),
@@ -349,6 +361,15 @@ pub async fn save_identity_file(db: &PostgresDb, uuid: &str) -> Result<()> {
})
.collect();
// Check for local profile image
let clean = uuid.replace('-', "");
let profile_path = identity_dir(&clean).join("profile.jpg");
let local_profile = if profile_path.exists() {
Some("profile.jpg".to_string())
} else {
None
};
let fmt_time = |dt: Option<chrono::DateTime<chrono::Utc>>| -> String {
dt.map(|d| d.to_rfc3339())
.unwrap_or_else(|| chrono::Utc::now().to_rfc3339())
@@ -363,6 +384,7 @@ pub async fn save_identity_file(db: &PostgresDb, uuid: &str) -> Result<()> {
status: record.status,
tmdb_id: record.tmdb_id,
tmdb_profile: record.tmdb_profile,
local_profile,
metadata: record.metadata,
file_bindings,
created_at: fmt_time(record.created_at),