feat: AniList reading status integration
- Add full AniList integration: OAuth connect, series linking, push/pull sync - Push: PLANNING/CURRENT/COMPLETED based on books read vs total_volumes (never auto-complete from owned books alone) - Pull: update local reading progress from AniList list (per-user) - Detailed sync/pull reports with per-series status and progress - Local user selector in settings to scope sync to a specific user - Rename "AniList" tab/buttons to generic "État de lecture" / "Reading status" - Make Bédéthèque and AniList badges clickable links on series detail page - Fix ON CONFLICT error on series link (provider column in PK) - Migration 0054: fix series_metadata missing columns (authors, publishers, locked_fields, total_volumes, status) - Align button heights on series detail page; move MarkSeriesReadButton to action row Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ export type LibraryDto = {
|
||||
next_metadata_refresh_at: string | null;
|
||||
series_count: number;
|
||||
thumbnail_book_ids: string[];
|
||||
reading_status_provider: string | null;
|
||||
};
|
||||
|
||||
export type IndexJobDto = {
|
||||
@@ -140,6 +141,83 @@ export type SeriesDto = {
|
||||
series_status: string | null;
|
||||
missing_count: number | null;
|
||||
metadata_provider: string | null;
|
||||
anilist_id: number | null;
|
||||
anilist_url: string | null;
|
||||
};
|
||||
|
||||
export type AnilistStatusDto = {
|
||||
connected: boolean;
|
||||
user_id: number;
|
||||
username: string;
|
||||
site_url: string;
|
||||
};
|
||||
|
||||
export type AnilistMediaResultDto = {
|
||||
id: number;
|
||||
title_romaji: string | null;
|
||||
title_english: string | null;
|
||||
title_native: string | null;
|
||||
site_url: string;
|
||||
status: string | null;
|
||||
volumes: number | null;
|
||||
};
|
||||
|
||||
export type AnilistSeriesLinkDto = {
|
||||
library_id: string;
|
||||
series_name: string;
|
||||
anilist_id: number;
|
||||
anilist_title: string | null;
|
||||
anilist_url: string | null;
|
||||
status: string;
|
||||
linked_at: string;
|
||||
synced_at: string | null;
|
||||
};
|
||||
|
||||
export type AnilistUnlinkedSeriesDto = {
|
||||
library_id: string;
|
||||
library_name: string;
|
||||
series_name: string;
|
||||
};
|
||||
|
||||
export type AnilistSyncPreviewItemDto = {
|
||||
series_name: string;
|
||||
anilist_id: number;
|
||||
anilist_title: string | null;
|
||||
anilist_url: string | null;
|
||||
status: "PLANNING" | "CURRENT" | "COMPLETED";
|
||||
progress_volumes: number;
|
||||
books_read: number;
|
||||
book_count: number;
|
||||
};
|
||||
|
||||
export type AnilistSyncItemDto = {
|
||||
series_name: string;
|
||||
anilist_title: string | null;
|
||||
anilist_url: string | null;
|
||||
status: string;
|
||||
progress_volumes: number;
|
||||
};
|
||||
|
||||
export type AnilistSyncReportDto = {
|
||||
synced: number;
|
||||
skipped: number;
|
||||
errors: string[];
|
||||
items: AnilistSyncItemDto[];
|
||||
};
|
||||
|
||||
export type AnilistPullItemDto = {
|
||||
series_name: string;
|
||||
anilist_title: string | null;
|
||||
anilist_url: string | null;
|
||||
anilist_status: string;
|
||||
books_updated: number;
|
||||
};
|
||||
|
||||
export type AnilistPullReportDto = {
|
||||
updated: number;
|
||||
skipped: number;
|
||||
errors: string[];
|
||||
items: AnilistPullItemDto[];
|
||||
};
|
||||
|
||||
export function config() {
|
||||
@@ -919,6 +997,12 @@ export async function getMetadataLink(libraryId: string, seriesName: string) {
|
||||
return apiFetch<ExternalMetadataLinkDto[]>(`/metadata/links?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getReadingStatusLink(libraryId: string, seriesName: string) {
|
||||
return apiFetch<AnilistSeriesLinkDto>(
|
||||
`/anilist/series/${libraryId}/${encodeURIComponent(seriesName)}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getMissingBooks(linkId: string) {
|
||||
return apiFetch<MissingBooksDto>(`/metadata/missing/${linkId}`);
|
||||
}
|
||||
|
||||
@@ -195,6 +195,23 @@ const en: Record<TranslationKey, string> = {
|
||||
"libraryActions.metadataRefreshSchedule": "Auto-refresh",
|
||||
"libraryActions.metadataRefreshDesc": "Periodically re-fetch metadata for existing series",
|
||||
"libraryActions.saving": "Saving...",
|
||||
"libraryActions.sectionReadingStatus": "Reading Status",
|
||||
"libraryActions.readingStatusProvider": "Reading Status Provider",
|
||||
"libraryActions.readingStatusProviderDesc": "Syncs reading states (read / reading / planned) with an external service",
|
||||
|
||||
// Reading status modal
|
||||
"readingStatus.button": "Reading status",
|
||||
"readingStatus.linkTo": "Link to {{provider}}",
|
||||
"readingStatus.search": "Search",
|
||||
"readingStatus.searching": "Searching…",
|
||||
"readingStatus.searchPlaceholder": "Series title…",
|
||||
"readingStatus.noResults": "No results.",
|
||||
"readingStatus.link": "Link",
|
||||
"readingStatus.unlink": "Unlink",
|
||||
"readingStatus.changeLink": "Change",
|
||||
"readingStatus.status.linked": "linked",
|
||||
"readingStatus.status.synced": "synced",
|
||||
"readingStatus.status.error": "error",
|
||||
|
||||
// Library sub-page header
|
||||
"libraryHeader.libraries": "Libraries",
|
||||
@@ -602,6 +619,59 @@ const en: Record<TranslationKey, string> = {
|
||||
"settings.telegramHelpChat": "Send a message to your bot, then open <code>https://api.telegram.org/bot<TOKEN>/getUpdates</code> in your browser. The <b>chat id</b> is in <code>message.chat.id</code>.",
|
||||
"settings.telegramHelpGroup": "For a group: add the bot to the group, send a message, then check the same URL. Group IDs are negative (e.g. <code>-123456789</code>).",
|
||||
|
||||
// Settings - AniList
|
||||
"settings.anilist": "Reading status",
|
||||
"settings.anilistTitle": "AniList Sync",
|
||||
"settings.anilistDesc": "Sync your reading progress with AniList. Get a personal access token at anilist.co/settings/developer.",
|
||||
"settings.anilistToken": "Personal Access Token",
|
||||
"settings.anilistTokenPlaceholder": "AniList token...",
|
||||
"settings.anilistUserId": "AniList User ID",
|
||||
"settings.anilistUserIdPlaceholder": "Numeric (e.g. 123456)",
|
||||
"settings.anilistConnected": "Connected as",
|
||||
"settings.anilistNotConnected": "Not connected",
|
||||
"settings.anilistTestConnection": "Test connection",
|
||||
"settings.anilistLibraries": "Libraries",
|
||||
"settings.anilistLibrariesDesc": "Enable AniList sync per library",
|
||||
"settings.anilistEnabled": "AniList sync enabled",
|
||||
"settings.anilistLocalUserTitle": "Local user",
|
||||
"settings.anilistLocalUserDesc": "Select the local user whose reading progress is synced with this AniList account",
|
||||
"settings.anilistLocalUserNone": "— Select a user —",
|
||||
"settings.anilistSyncTitle": "Sync",
|
||||
"settings.anilistSyncDesc": "Push local reading progress to AniList. Rules: none read → PLANNING · at least 1 read → CURRENT (progress = volumes read) · all published volumes read (total_volumes known) → COMPLETED.",
|
||||
"settings.anilistSyncButton": "Sync to AniList",
|
||||
"settings.anilistPullButton": "Pull from AniList",
|
||||
"settings.anilistPullDesc": "Import your AniList reading list and update local reading progress. Rules: COMPLETED/CURRENT/REPEATING → books marked read up to the progress volume · PLANNING/PAUSED/DROPPED → unread.",
|
||||
"settings.anilistSyncing": "Syncing...",
|
||||
"settings.anilistPulling": "Pulling...",
|
||||
"settings.anilistSynced": "{{count}} series synced",
|
||||
"settings.anilistUpdated": "{{count}} series updated",
|
||||
"settings.anilistSkipped": "{{count}} skipped",
|
||||
"settings.anilistErrors": "{{count}} error(s)",
|
||||
"settings.anilistLinks": "AniList Links",
|
||||
"settings.anilistLinksDesc": "Series linked to AniList",
|
||||
"settings.anilistNoLinks": "No series linked to AniList",
|
||||
"settings.anilistUnlink": "Unlink",
|
||||
"settings.anilistSyncStatus": "synced",
|
||||
"settings.anilistLinkedStatus": "linked",
|
||||
"settings.anilistErrorStatus": "error",
|
||||
"settings.anilistUnlinkedTitle": "{{count}} unlinked series",
|
||||
"settings.anilistUnlinkedDesc": "These series belong to AniList-enabled libraries but have no AniList link yet. Search each one to link it.",
|
||||
"settings.anilistSearchButton": "Search",
|
||||
"settings.anilistSearchNoResults": "No AniList results.",
|
||||
"settings.anilistLinkButton": "Link",
|
||||
"settings.anilistRedirectUrlLabel": "Redirect URL to configure in your AniList app:",
|
||||
"settings.anilistRedirectUrlHint": "Paste this URL in the « Redirect URL » field of your application at anilist.co/settings/developer.",
|
||||
"settings.anilistTokenPresent": "Token present — not verified",
|
||||
"settings.anilistPreviewButton": "Preview",
|
||||
"settings.anilistPreviewing": "Loading...",
|
||||
"settings.anilistPreviewTitle": "{{count}} series to sync",
|
||||
"settings.anilistPreviewEmpty": "No series to sync (link series to AniList first).",
|
||||
"settings.anilistClientId": "AniList Client ID",
|
||||
"settings.anilistClientIdPlaceholder": "E.g. 37777",
|
||||
"settings.anilistConnectButton": "Connect with AniList",
|
||||
"settings.anilistConnectDesc": "Use OAuth to connect automatically. Find your Client ID in your AniList apps (anilist.co/settings/developer).",
|
||||
"settings.anilistManualToken": "Manual token (advanced)",
|
||||
|
||||
// Settings - Language
|
||||
"settings.language": "Language",
|
||||
"settings.languageDesc": "Choose the interface language",
|
||||
|
||||
@@ -193,6 +193,23 @@ const fr = {
|
||||
"libraryActions.metadataRefreshSchedule": "Rafraîchissement auto",
|
||||
"libraryActions.metadataRefreshDesc": "Re-télécharger périodiquement les métadonnées existantes",
|
||||
"libraryActions.saving": "Enregistrement...",
|
||||
"libraryActions.sectionReadingStatus": "État de lecture",
|
||||
"libraryActions.readingStatusProvider": "Provider d'état de lecture",
|
||||
"libraryActions.readingStatusProviderDesc": "Synchronise les états de lecture (lu / en cours / planifié) avec un service externe",
|
||||
|
||||
// Reading status modal
|
||||
"readingStatus.button": "État de lecture",
|
||||
"readingStatus.linkTo": "Lier à {{provider}}",
|
||||
"readingStatus.search": "Rechercher",
|
||||
"readingStatus.searching": "Recherche en cours…",
|
||||
"readingStatus.searchPlaceholder": "Titre de la série…",
|
||||
"readingStatus.noResults": "Aucun résultat.",
|
||||
"readingStatus.link": "Lier",
|
||||
"readingStatus.unlink": "Délier",
|
||||
"readingStatus.changeLink": "Changer",
|
||||
"readingStatus.status.linked": "lié",
|
||||
"readingStatus.status.synced": "synchronisé",
|
||||
"readingStatus.status.error": "erreur",
|
||||
|
||||
// Library sub-page header
|
||||
"libraryHeader.libraries": "Bibliothèques",
|
||||
@@ -600,6 +617,59 @@ const fr = {
|
||||
"settings.telegramHelpChat": "Envoyez un message à votre bot, puis ouvrez <code>https://api.telegram.org/bot<TOKEN>/getUpdates</code> dans votre navigateur. Le <b>chat id</b> apparaît dans <code>message.chat.id</code>.",
|
||||
"settings.telegramHelpGroup": "Pour un groupe : ajoutez le bot au groupe, envoyez un message, puis consultez la même URL. Les IDs de groupe sont négatifs (ex: <code>-123456789</code>).",
|
||||
|
||||
// Settings - AniList
|
||||
"settings.anilist": "État de lecture",
|
||||
"settings.anilistTitle": "Synchronisation AniList",
|
||||
"settings.anilistDesc": "Synchronisez votre progression de lecture avec AniList. Obtenez un token d'accès personnel sur anilist.co/settings/developer.",
|
||||
"settings.anilistToken": "Token d'accès personnel",
|
||||
"settings.anilistTokenPlaceholder": "Token AniList...",
|
||||
"settings.anilistUserId": "ID utilisateur AniList",
|
||||
"settings.anilistUserIdPlaceholder": "Numérique (ex: 123456)",
|
||||
"settings.anilistConnected": "Connecté en tant que",
|
||||
"settings.anilistNotConnected": "Non connecté",
|
||||
"settings.anilistTestConnection": "Tester la connexion",
|
||||
"settings.anilistLibraries": "Bibliothèques",
|
||||
"settings.anilistLibrariesDesc": "Activer la synchronisation AniList pour chaque bibliothèque",
|
||||
"settings.anilistEnabled": "Sync AniList activée",
|
||||
"settings.anilistLocalUserTitle": "Utilisateur local",
|
||||
"settings.anilistLocalUserDesc": "Choisir l'utilisateur local dont la progression est synchronisée avec ce compte AniList",
|
||||
"settings.anilistLocalUserNone": "— Sélectionner un utilisateur —",
|
||||
"settings.anilistSyncTitle": "Synchronisation",
|
||||
"settings.anilistSyncDesc": "Envoyer la progression locale vers AniList. Règles : aucun lu → PLANNING · au moins 1 lu → CURRENT (progression = nbre de tomes lus) · tous les tomes publiés lus (total_volumes connu) → COMPLETED.",
|
||||
"settings.anilistSyncButton": "Synchroniser vers AniList",
|
||||
"settings.anilistPullButton": "Importer depuis AniList",
|
||||
"settings.anilistPullDesc": "Importer votre liste de lecture AniList et mettre à jour la progression locale. Règles : COMPLETED/CURRENT/REPEATING → livres marqués lus jusqu'au volume de progression · PLANNING/PAUSED/DROPPED → non lus.",
|
||||
"settings.anilistSyncing": "Synchronisation...",
|
||||
"settings.anilistPulling": "Import...",
|
||||
"settings.anilistSynced": "{{count}} série(s) synchronisée(s)",
|
||||
"settings.anilistUpdated": "{{count}} série(s) mise(s) à jour",
|
||||
"settings.anilistSkipped": "{{count}} ignorée(s)",
|
||||
"settings.anilistErrors": "{{count}} erreur(s)",
|
||||
"settings.anilistLinks": "Liens AniList",
|
||||
"settings.anilistLinksDesc": "Séries associées à AniList",
|
||||
"settings.anilistNoLinks": "Aucune série liée à AniList",
|
||||
"settings.anilistUnlink": "Délier",
|
||||
"settings.anilistSyncStatus": "synced",
|
||||
"settings.anilistLinkedStatus": "linked",
|
||||
"settings.anilistErrorStatus": "error",
|
||||
"settings.anilistUnlinkedTitle": "{{count}} série(s) non liée(s)",
|
||||
"settings.anilistUnlinkedDesc": "Ces séries appartiennent à des bibliothèques activées mais n'ont pas encore de lien AniList. Recherchez chacune pour la lier.",
|
||||
"settings.anilistSearchButton": "Rechercher",
|
||||
"settings.anilistSearchNoResults": "Aucun résultat AniList.",
|
||||
"settings.anilistLinkButton": "Lier",
|
||||
"settings.anilistRedirectUrlLabel": "URL de redirection à configurer dans votre app AniList :",
|
||||
"settings.anilistRedirectUrlHint": "Collez cette URL dans le champ « Redirect URL » de votre application sur anilist.co/settings/developer.",
|
||||
"settings.anilistTokenPresent": "Token présent — non vérifié",
|
||||
"settings.anilistPreviewButton": "Prévisualiser",
|
||||
"settings.anilistPreviewing": "Chargement...",
|
||||
"settings.anilistPreviewTitle": "{{count}} série(s) à synchroniser",
|
||||
"settings.anilistPreviewEmpty": "Aucune série à synchroniser (liez des séries à AniList d'abord).",
|
||||
"settings.anilistClientId": "Client ID AniList",
|
||||
"settings.anilistClientIdPlaceholder": "Ex: 37777",
|
||||
"settings.anilistConnectButton": "Connecter avec AniList",
|
||||
"settings.anilistConnectDesc": "Utilisez OAuth pour vous connecter automatiquement. Le Client ID se trouve dans vos applications AniList (anilist.co/settings/developer).",
|
||||
"settings.anilistManualToken": "Token manuel (avancé)",
|
||||
|
||||
// Settings - Language
|
||||
"settings.language": "Langue",
|
||||
"settings.languageDesc": "Choisir la langue de l'interface",
|
||||
|
||||
Reference in New Issue
Block a user