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:
2026-03-25 10:30:04 +01:00
parent d977b6b27a
commit 10cc69e53f
13 changed files with 939 additions and 7 deletions

View File

@@ -262,6 +262,8 @@ const en: Record<TranslationKey, string> = {
"jobs.groupReadingStatus": "Reading status",
"jobs.matchReadingStatus": "Match series",
"jobs.matchReadingStatusShort": "Auto-link unmatched series to the reading status provider",
"jobs.pushReadingStatus": "Push reading statuses",
"jobs.pushReadingStatusShort": "Push changed reading statuses to AniList (differential push)",
// Jobs list
"jobsList.id": "ID",
@@ -368,6 +370,12 @@ const en: Record<TranslationKey, string> = {
"jobDetail.readingStatusMatchReport": "Match report",
"jobDetail.linked": "Linked",
"jobDetail.ambiguous": "Ambiguous",
"jobDetail.readingStatusPush": "Reading status push",
"jobDetail.readingStatusPushDesc": "Differential push of reading statuses to AniList",
"jobDetail.readingStatusPushReport": "Push report",
"jobDetail.pushed": "Pushed",
"jobDetail.skipped": "Skipped",
"jobDetail.noBooks": "No books",
// Job types
"jobType.rebuild": "Indexing",
@@ -397,6 +405,9 @@ const en: Record<TranslationKey, string> = {
"jobType.reading_status_match": "Reading status match",
"jobType.reading_status_matchLabel": "Series matching (reading status)",
"jobType.reading_status_matchDesc": "Automatically searches each series in the library against the configured reading status provider (e.g. AniList) and creates links for unambiguously identified series.",
"jobType.reading_status_push": "Reading status push",
"jobType.reading_status_pushLabel": "Reading status push",
"jobType.reading_status_pushDesc": "Differentially pushes changed reading statuses (or new series) to AniList.",
// Status badges
"statusBadge.extracting_pages": "Extracting pages",

View File

@@ -260,6 +260,8 @@ const fr = {
"jobs.groupReadingStatus": "Statut de lecture",
"jobs.matchReadingStatus": "Correspondance des séries",
"jobs.matchReadingStatusShort": "Lier automatiquement les séries non associées au provider",
"jobs.pushReadingStatus": "Push des états de lecture",
"jobs.pushReadingStatusShort": "Envoyer les états de lecture modifiés vers AniList (push différentiel)",
// Jobs list
"jobsList.id": "ID",
@@ -366,6 +368,12 @@ const fr = {
"jobDetail.readingStatusMatchReport": "Rapport de correspondance",
"jobDetail.linked": "Liées",
"jobDetail.ambiguous": "Ambiguës",
"jobDetail.readingStatusPush": "Push des états de lecture",
"jobDetail.readingStatusPushDesc": "Envoi différentiel des états de lecture vers AniList",
"jobDetail.readingStatusPushReport": "Rapport de push",
"jobDetail.pushed": "Envoyés",
"jobDetail.skipped": "Ignorés",
"jobDetail.noBooks": "Sans livres",
// Job types
"jobType.rebuild": "Indexation",
@@ -395,6 +403,9 @@ const fr = {
"jobType.reading_status_match": "Correspondance statut lecture",
"jobType.reading_status_matchLabel": "Correspondance des séries (statut lecture)",
"jobType.reading_status_matchDesc": "Recherche automatiquement chaque série de la bibliothèque sur le provider de statut de lecture configuré (ex. AniList) et crée les liens pour les séries identifiées sans ambiguïté.",
"jobType.reading_status_push": "Push statut lecture",
"jobType.reading_status_pushLabel": "Push des états de lecture",
"jobType.reading_status_pushDesc": "Envoie les états de lecture modifiés (ou nouvelles séries) vers AniList de façon différentielle.",
// Status badges
"statusBadge.extracting_pages": "Extraction des pages",