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:
@@ -1102,6 +1102,43 @@ export async function getReadingStatusMatchResults(jobId: string) {
|
||||
return apiFetch<ReadingStatusMatchResultDto[]>(`/reading-status/match/${jobId}/results`);
|
||||
}
|
||||
|
||||
export async function startReadingStatusPush(libraryId: string) {
|
||||
return apiFetch<{ id: string; status: string }>("/reading-status/push", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ library_id: libraryId }),
|
||||
});
|
||||
}
|
||||
|
||||
export type ReadingStatusPushReportDto = {
|
||||
job_id: string;
|
||||
status: string;
|
||||
total_series: number;
|
||||
pushed: number;
|
||||
skipped: number;
|
||||
no_books: number;
|
||||
errors: number;
|
||||
};
|
||||
|
||||
export type ReadingStatusPushResultDto = {
|
||||
id: string;
|
||||
series_name: string;
|
||||
status: "pushed" | "skipped" | "no_books" | "error";
|
||||
anilist_id: number | null;
|
||||
anilist_title: string | null;
|
||||
anilist_url: string | null;
|
||||
anilist_status: string | null;
|
||||
progress_volumes: number | null;
|
||||
error_message: string | null;
|
||||
};
|
||||
|
||||
export async function getReadingStatusPushReport(jobId: string) {
|
||||
return apiFetch<ReadingStatusPushReportDto>(`/reading-status/push/${jobId}/report`);
|
||||
}
|
||||
|
||||
export async function getReadingStatusPushResults(jobId: string) {
|
||||
return apiFetch<ReadingStatusPushResultDto[]>(`/reading-status/push/${jobId}/results`);
|
||||
}
|
||||
|
||||
export type RefreshFieldDiff = {
|
||||
field: string;
|
||||
old?: unknown;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user