feat: add reading_status_push job — differential push to AniList
Push reading statuses (PLANNING/CURRENT/COMPLETED) to AniList for all linked series that changed since last sync, or have new books/no sync yet. - Migration 0057: adds reading_status_push to index_jobs type constraint - Migration 0058: creates reading_status_push_results table (pushed/skipped/no_books/error) - API: new reading_status_push module with start_push, get_push_report, get_push_results - Differential detection: synced_at IS NULL OR reading progress updated OR new books added - Same 429 retry logic as reading_status_match (wait 10s, retry once, abort on 2nd 429) - Notifications: ReadingStatusPushCompleted/Failed events - Backoffice: push button in reading status group, job detail report with per-series list - Replay support, badge label, i18n (FR + EN) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,10 @@ pub struct EventToggles {
|
||||
pub reading_status_match_completed: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub reading_status_match_failed: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub reading_status_push_completed: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub reading_status_push_failed: bool,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
@@ -69,6 +73,8 @@ fn default_events() -> EventToggles {
|
||||
metadata_refresh_failed: true,
|
||||
reading_status_match_completed: true,
|
||||
reading_status_match_failed: true,
|
||||
reading_status_push_completed: true,
|
||||
reading_status_push_failed: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +271,16 @@ pub enum NotificationEvent {
|
||||
library_name: Option<String>,
|
||||
error: String,
|
||||
},
|
||||
// Reading status push (differential push to AniList)
|
||||
ReadingStatusPushCompleted {
|
||||
library_name: Option<String>,
|
||||
total_series: i32,
|
||||
pushed: i32,
|
||||
},
|
||||
ReadingStatusPushFailed {
|
||||
library_name: Option<String>,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Classify an indexer job_type string into the right event constructor category.
|
||||
@@ -511,6 +527,37 @@ fn format_event(event: &NotificationEvent) -> String {
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
NotificationEvent::ReadingStatusPushCompleted {
|
||||
library_name,
|
||||
total_series,
|
||||
pushed,
|
||||
} => {
|
||||
let lib = library_name.as_deref().unwrap_or("All libraries");
|
||||
[
|
||||
format!("✅ <b>Reading status push completed</b>"),
|
||||
format!("━━━━━━━━━━━━━━━━━━━━"),
|
||||
format!("📂 <b>Library:</b> {lib}"),
|
||||
String::new(),
|
||||
format!("📊 <b>Results</b>"),
|
||||
format!(" ⬆️ Pushed: <b>{pushed}</b> / <b>{total_series}</b> series"),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
NotificationEvent::ReadingStatusPushFailed {
|
||||
library_name,
|
||||
error,
|
||||
} => {
|
||||
let lib = library_name.as_deref().unwrap_or("All libraries");
|
||||
let err = truncate(error, 200);
|
||||
[
|
||||
format!("🚨 <b>Reading status push failed</b>"),
|
||||
format!("━━━━━━━━━━━━━━━━━━━━"),
|
||||
format!("📂 <b>Library:</b> {lib}"),
|
||||
String::new(),
|
||||
format!("💬 <code>{err}</code>"),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,6 +600,8 @@ fn is_event_enabled(config: &TelegramConfig, event: &NotificationEvent) -> bool
|
||||
NotificationEvent::MetadataRefreshFailed { .. } => config.events.metadata_refresh_failed,
|
||||
NotificationEvent::ReadingStatusMatchCompleted { .. } => config.events.reading_status_match_completed,
|
||||
NotificationEvent::ReadingStatusMatchFailed { .. } => config.events.reading_status_match_failed,
|
||||
NotificationEvent::ReadingStatusPushCompleted { .. } => config.events.reading_status_push_completed,
|
||||
NotificationEvent::ReadingStatusPushFailed { .. } => config.events.reading_status_push_failed,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user