fix(analysis): persist failed tracks and reconcile progress counts (#867)

* fix(analysis): persist failed-track suppression and reduce aggressive polling

Persist unsupported/broken analysis tracks as failed entries and expose them in Settings with track metadata, export, and targeted rescan actions. Also make aggressive-mode completion checks cheap by gating re-entry on live track-count changes with a startup seed and 5-minute recheck cadence.

* fix(analysis): mark unsupported decode tracks as failed in cpu-seed

When full-seed falls back to waveform-only (no EBU loudness) or enrichment decode fails, persist analysis_track status as failed so aggressive backfill does not requeue the same unsupported tracks indefinitely.

* fix(analysis): reconcile legacy ready tracks without loudness

Auto-mark legacy ready tracks that only miss loudness as failed during needs-work checks so analysis progress converges instead of staying permanently pending.

* docs(changelog): add failed-analysis recovery notes for PR 867

Document persistent failed-track handling, analytics strategy controls, and low-cost aggressive-mode recheck behavior in the 1.47.0 changelog.

* fix(i18n): align settings locale coverage across all languages

Sync missing Settings keys for all shipped locales and replace recent English fallbacks with localized strings so analytics and backup UI text stays consistent outside en/ru.
This commit is contained in:
cucadmuh
2026-05-25 01:37:15 +03:00
committed by GitHub
parent 820f71c421
commit bc85065316
21 changed files with 1263 additions and 17 deletions
@@ -59,6 +59,14 @@ pub struct AnalysisDeleteServerReportDto {
pub loudness: u64,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalysisFailedTrackDto {
pub track_id: String,
pub md5_16kb: String,
pub updated_at: i64,
}
impl From<analysis_cache::AnalysisDeleteServerReport> for AnalysisDeleteServerReportDto {
fn from(value: analysis_cache::AnalysisDeleteServerReport) -> Self {
Self {
@@ -69,6 +77,16 @@ impl From<analysis_cache::AnalysisDeleteServerReport> for AnalysisDeleteServerRe
}
}
impl From<analysis_cache::FailedTrackEntry> for AnalysisFailedTrackDto {
fn from(value: analysis_cache::FailedTrackEntry) -> Self {
Self {
track_id: value.track_id,
md5_16kb: value.md5_16kb,
updated_at: value.updated_at,
}
}
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalysisServerKeyMigrationDto {
@@ -227,6 +245,54 @@ pub fn analysis_delete_all_for_server(
Ok(report.into())
}
#[tauri::command]
pub fn analysis_get_failed_track_count(
server_id: String,
cache: tauri::State<'_, analysis_cache::AnalysisCache>,
) -> Result<i64, String> {
let server_id = server_id.trim().to_string();
if server_id.is_empty() {
return Ok(0);
}
cache.count_failed_tracks(&server_id)
}
#[tauri::command]
pub fn analysis_list_failed_tracks(
server_id: String,
limit: Option<u32>,
cache: tauri::State<'_, analysis_cache::AnalysisCache>,
) -> Result<Vec<AnalysisFailedTrackDto>, String> {
let server_id = server_id.trim().to_string();
if server_id.is_empty() {
return Ok(Vec::new());
}
let limit = limit
.map(|v| usize::try_from(v).unwrap_or(usize::MAX))
.map(|v| v.clamp(1, 5_000));
let rows = cache.list_failed_tracks(&server_id, limit)?;
Ok(rows.into_iter().map(AnalysisFailedTrackDto::from).collect())
}
#[tauri::command]
pub fn analysis_clear_failed_tracks(
server_id: String,
track_ids: Option<Vec<String>>,
cache: tauri::State<'_, analysis_cache::AnalysisCache>,
) -> Result<u64, String> {
let server_id = server_id.trim().to_string();
if server_id.is_empty() {
return Err("server_id required".to_string());
}
let track_ids = track_ids
.unwrap_or_default()
.into_iter()
.map(|id| id.trim().to_string())
.filter(|id| !id.is_empty())
.collect::<Vec<_>>();
cache.clear_failed_tracks(&server_id, &track_ids)
}
#[tauri::command]
pub fn analysis_migrate_server_index_keys(
mappings: Vec<AnalysisServerKeyMigrationDto>,