- metadata.rs: simplifier la query get_missing_books, library_id et
series_name étaient extraits mais non utilisés
- series.rs: supprimer resolve_series_id jamais appelée
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
La table torrent_downloads ne stockait pas series_id. Le DTO l'exposait
comme optional mais était toujours null, causant des liens KO en page
downloads (fallback sur series_name alors que la route attend un UUID).
Ajout d'un LEFT JOIN series sur (library_id, LOWER(name)) dans la query
list_torrent_downloads pour résoudre le series_id dynamiquement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration DB (0070 + 0071):
- Backup automatique de book_reading_progress avant migration
- Crée table series (fusion de series_metadata) avec UUID PK
- Ajoute series_id FK à books, external_metadata_links, anilist_series_links,
available_downloads, download_detection_results
- Supprime les colonnes TEXT legacy et la table series_metadata
Backend API + Indexer:
- Toutes les queries SQL migrées vers series_id FK + JOIN series
- Routes /series/:name → /series/:series_id (UUID)
- Nouvel endpoint GET /series/by-name/:name pour lookup par nom
- match_title_volumes() factorisé entre prowlarr.rs et download_detection.rs
- Fix scheduler.rs: settings → app_settings
- OpenAPI mis à jour avec les nouveaux endpoints
Frontend:
- Routes /libraries/[id]/series/[name] → /series/[seriesId]
- Tous les composants (Edit, Delete, MarkRead, Prowlarr, Metadata,
ReadingStatus) utilisent seriesId
- compressVolumes() pour afficher T1→3 au lieu de T1 T2 T3
- Titre release en entier (plus de truncate) dans available downloads
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
API (15 fichiers):
- series.rs: helpers resolve_series_id/get_or_create_series, toutes les
queries migrent de books.series TEXT vers series_id FK + JOIN series
- Routes /series/:name → /series/:series_id (UUID)
- books.rs: filtres série par series_id, SELECT s.name AS series via JOIN
- metadata.rs: sync écrit dans series au lieu de series_metadata
- metadata_refresh.rs: refresh_link et rematch via series_id
- metadata_batch.rs: sync via series table
- anilist.rs: liens par series_id au lieu de series_name
- download_detection.rs: available_downloads via series_id
- reading_progress.rs: mark_series_read par series_id
- torrent_import.rs: import via series JOIN
- search.rs, stats.rs, libraries.rs: JOINs series pour les noms
- reading_status_match.rs, reading_status_push.rs: séries via JOIN
Indexer (3 fichiers):
- scanner.rs: get_or_create_series_id() avec cache HashMap
- batch.rs: BookInsert/BookUpdate.series_id UUID au lieu de series String
- job.rs: rematch_unlinked_books via series JOIN
4 nouveaux tests (SeriesItem, SeriesMetadata, UpdateSeriesResponse,
BatchStructs avec series_id)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quand les métadonnées externes sont récupérées avant que les livres
n'existent localement, le book_id reste NULL et les livres apparaissent
comme "manquants" alors qu'ils sont présents.
- Ajoute rematch_unlinked_books() qui associe les external_book_metadata
non liées aux livres locaux par correspondance de volume
- Appelé automatiquement à la fin de refresh_link() (API)
- Appelé après chaque scan terminé dans l'indexer (job.rs)
- Testé sur Dragon Ball : 47/85 external books rematched, 43 restants
correspondent aux tomes Dragon Ball Z non présents localement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend:
- Ajoute DELETE /libraries/:id/series/:name dans series.rs
- Supprime tous les fichiers physiques des livres de la série
- Supprime le dossier de la série (remove_dir_all)
- Nettoie en DB : books, series_metadata, external_metadata_links,
anilist_series_links, available_downloads
- Queue un scan job pour cohérence
Frontend:
- Crée DeleteSeriesButton.tsx avec modale de confirmation
- Ajouté dans la toolbar de la page détail série
- i18n fr/en pour les textes de confirmation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ajoute is_integral_release() pour détecter "intégrale", "complet",
"complete", "integral" dans les titres de torrents
- Les releases intégrales matchent tous les volumes manquants d'une série
- 4 tests unitaires (français, anglais, casse, faux positifs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extrait is_not_rar_error() + open_cbr_listing() dans parsers, simplifie 4 fonctions CBR
- Harmonise extract_cbr_page pour vérifier l'erreur comme les 3 autres (était incohérent)
- Améliore From<sqlx::Error>: RowNotFound→404, PoolTimedOut→503, contraintes→400
- Remplace 5 let _ = par if let Err(e) avec warn! dans analyzer.rs (status/progress updates)
- 15 nouveaux tests (parsers: is_not_rar, detect_format, is_image_name; API: sqlx error mapping)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Centralise remap_libraries_path/unmap_libraries_path dans crates/core/paths.rs
(supprime 4 copies dupliquées dans API + indexer)
- Centralise mode_to_interval_minutes/validate_schedule_mode dans crates/core/schedule.rs
(remplace 8 match blocks + 4 validations inline)
- Ajoute helpers env_or<T>/env_string_or dans config.rs, utilise ThumbnailConfig::default()
comme base dans from_env() (élimine la duplication des valeurs par défaut)
- Supprime std::mem::take inutile dans books.rs
- Cible #[allow(dead_code)] sur le champ plutôt que le struct (metadata.rs)
- Remplace eprintln! par tracing::warn! dans parsers
- Fix clippy boolean logic bug dans prowlarr.rs
10 nouveaux tests unitaires (paths + schedule)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ajout Pass 3 dans extract_volumes_from_title pour les patterns "Nom - 07 - Titre.cbz"
et "06. nom.cbz" (nombre nu entre tirets ou en début de nom)
- Gestion des états qBittorrent stalledDL/pausedDL/error/missingFiles → marqués en erreur
en DB et supprimés de qBittorrent
- Ajout de logs de debug pour l'import (fichiers trouvés, volumes extraits, dedup)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ajout DELETE /books/:id : supprime le fichier physique, la thumbnail,
le book en DB et queue un scan de la lib. Bouton avec confirmation
sur la page de détail du livre.
- L'import torrent utilise unaccent() en SQL pour matcher les séries
indépendamment des accents (ex: "les géants" = "les geants").
- Fallback filesystem avec strip_accents pour les séries sans livre en DB.
- Migration 0069: activation de l'extension PostgreSQL unaccent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quand un download contient le même volume en plusieurs formats,
on ne garde que le meilleur (cbz prioritaire, puis cbr, pdf, epub).
Les packs multi-volumes sont toujours conservés.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ajoute DELETE /available-downloads/:id?release=N pour supprimer une
release spécifique du JSON array (supprime l'entrée série si c'est
la dernière). Bouton trash sur chaque release dans la page downloads.
Corrige aussi le parsing des ranges de volumes sans préfixe sur le
second nombre (T17-23 détecte maintenant T17 à T23).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ajoute les événements TorrentImportCompleted et TorrentImportFailed
au système de notifications. Une notif Telegram est envoyée après
l'import des fichiers dans la bibliothèque (succès ou erreur),
avec le nom de la série, la bibliothèque et le nombre de fichiers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
qBittorrent ne suit pas les redirections 301 des URLs proxy Prowlarr.
L'API résout maintenant les redirections elle-même : si l'URL mène à un
magnet on le passe directement, si c'est un .torrent on l'uploade en
multipart. Ajoute aussi des logs sur tous les points d'échec du endpoint
/qbittorrent/add et un User-Agent "Stripstream-Librarian" sur les
appels Prowlarr.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
La recherche utilise désormais le endpoint paginé /books avec un filtre
ILIKE sur title/series/author, ce qui permet la pagination des résultats.
Les series_hits sont toujours récupérés en parallèle via searchBooks.
Corrige aussi le remount du LiveSearchForm lors de la navigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supprime les entrées available_downloads dont le series_name ne
correspond plus à aucune série existante dans books, au début de
chaque job de détection de téléchargements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Le tri par date d'ajout utilisait updated_at qui est écrasé à chaque
rescan de l'indexer, empêchant les livres/séries récemment ajoutés
de remonter en premier. Utilise maintenant created_at qui reflète
la vraie date d'ajout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Modale Prowlarr (page série) : remplacé le bouton qBittorrent brut
par QbittorrentDownloadButton avec suivi managé (libraryId,
seriesName, expectedVolumes) et bouton "télécharger et remplacer".
- Ajout de alwaysShowReplace pour la modale Prowlarr (toujours montrer
le bouton remplacer) vs la page downloads (seulement si allVolumes >
expectedVolumes).
- Fix parseur : les tags de version entre crochets [V2], [V3] ne sont
plus extraits comme volumes (le préfixe "v" est ignoré après "[").
- Progression qBittorrent : utilise directement le champ progress
(completed et amount_left sont non-fiables sur qBittorrent 4.3.2).
- Référence import : ne plus exclure les volumes attendus de la
recherche de référence (corrige le mauvais dossier/nommage quand
tous les volumes sont dans expected_volumes).
- allVolumes ajouté à ProwlarrRelease (backend + frontend).
- flex-wrap sur les pastilles volumes dans la modale Prowlarr.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ajout d'un bouton "télécharger et remplacer" avec popup de
confirmation, qui passe tous les volumes du pack (pas seulement
les manquants) et replace_existing=true à l'API.
- Nouvelle colonne replace_existing dans torrent_downloads.
- Fix critique du parseur de volumes : le pass 2 mélangeait les
indices d'octets (String::find) avec les indices de caractères
(Vec<char>), causant un décalage quand le titre contenait des
caractères multi-octets (é, à...). "Tome #097" extrayait 9
au lieu de 97. Réécrit en indexation char pure.
- Le préfixe "tome" skip désormais "#" (tome #097 → 97).
- Protection intra-batch : si une destination est déjà utilisée,
le fichier garde son nom original au lieu d'écraser.
- Alerte WARN si N fichiers source donnent N/3 volumes uniques.
- Nettoyage du répertoire sl-{id} et de la catégorie qBittorrent
après import.
- Badges volumes en flex-wrap dans la page downloads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
L'ancienne stratégie diff avant/après échouait quand plusieurs
torrents étaient ajoutés en parallèle (le diff voyait N nouveaux
torrents et ne pouvait pas les distinguer). Les tags et savepath
ne sont pas appliqués sur qBittorrent 4.x en url-encoded.
Nouvelle approche : chaque download managé crée une catégorie
`sl-{uuid}` dans qBittorrent, puis résout le hash en filtrant
par catégorie. Le poller retente aussi la résolution par catégorie
pour les torrents avec qb_hash NULL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Nouvelle table available_downloads (library_id, series_name) unique
comme source de vérité pour les téléchargements disponibles
- Les jobs de détection font UPSERT (ajout/mise à jour) et DELETE
(séries complètes ou sans résultat)
- Après import, mise à jour ciblée : retire les volumes importés des
releases, supprime l'entrée si plus de releases
- Migration avec import des données existantes depuis detection_results
- Endpoint latest-found simplifié : une seule query sur la table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Endpoint GET /version (sans auth) retournant la version API
- Bloc About dans l'onglet General : nom du projet, description,
versions API et Backoffice, lien GitHub
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remplace les 5 CTEs + double query (données + count) par une seule
requête avec COUNT(*) OVER() pour le total
- Calcule book_count et series_count directement depuis UNNEST, sans
re-JOIN sur les tables
- Ajoute des index GIN sur books.authors et series_metadata.authors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Après import réussi, supprime le sous-répertoire content_path dans
/downloads/ (ne touche jamais /downloads/ lui-même)
- Supprime le torrent de qBittorrent via DELETE avec deleteFiles=true
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Après import torrent, refresh automatique des métadonnées uniquement
sur la série importée (via refresh_link) au lieu d'un job complet
- Nouvel endpoint POST /metadata/refresh-link/:id pour rafraîchir un
seul lien metadata approuvé
- Bouton "Rafraîchir" dans la modale metadata (état linked) avec
spinner et confirmation visuelle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Endpoint GET /download-detection/latest-found : résultats "found" du
dernier job de détection par bibliothèque
- Section dans la page Téléchargements avec les releases disponibles
groupées par bibliothèque, bouton qBittorrent intégré
- Fix nommage import : exclut les volumes importés de la recherche de
référence (évite le cercle vicieux vol 8 → ref vol 8 → même nom)
- Fix extraction volumes : gère "Tome.007" (point après préfixe) en
plus de "Tome 007" dans extract_volumes_from_title
- Fallback disque pour la référence de nommage quand la DB ne matche pas
- Logging détaillé du processus d'import pour debug
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Nouvelle table `torrent_downloads` pour suivre les téléchargements gérés
- API : endpoint POST /torrent-downloads/notify (webhook optionnel) et GET /torrent-downloads
- Poller background toutes les 30s qui interroge qBittorrent pour détecter
les torrents terminés — aucune config "run external program" nécessaire
- Import automatique : déplacement des fichiers vers la série cible,
renommage selon le pattern existant (détection de la largeur des digits),
support packs multi-volumes, scan job déclenché après import
- Page /downloads dans le backoffice : filtres, auto-refresh, carte par download
- Toggle auto-import intégré dans la card qBittorrent des settings
- Erreurs de détection download affichées dans le détail des jobs
- Volume /downloads monté dans docker-compose
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- metadata_batch, metadata_refresh, reading_status_match, reading_status_push,
download_detection : library_id devient optionnel, la boucle passe côté API
- rebuild (index_jobs.rs), thumbnail_rebuild, thumbnail_regenerate : même logique,
suppression du job unique library_id=NULL au profit d'un job par lib
- Backoffice simplifié : suppression des boucles frontend, les Server Actions
appellent directement l'API sans library_id pour le cas "toutes les librairies"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a user renames a series via the UI, the scanner was using the
filesystem directory name to overwrite the DB series name, effectively
undoing the rename. This adds an original_name column to series_metadata
that tracks the filesystem-derived name, so the scanner can map it back
to the user-chosen name. The migration also back-fills existing renamed
series by comparing book file paths with DB series names.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
T01.T15, [T001.T104], T01-T15 and Tome 01 à Tome 15 are now expanded
to the full range of volumes they contain, so a pack covering volumes
1-15 correctly matches any missing volume within that range.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a configurable schedule (manual/hourly/daily/weekly) for the
download detection job in the library settings modal. The indexer
scheduler triggers the job automatically, and the API job poller
processes it — consistent with the reading_status_push pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For each series with missing volumes and an approved metadata link,
calls Prowlarr to find available matching releases and stores them in
a report (no auto-download). Includes per-series detail page, Telegram
notifications with per-event toggles, and stats display in the jobs table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
On rate limit, wait 10 seconds and retry the same series. If the retry
also returns 429, the job stops. Otherwise it continues normally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Continuing after a 429 is pointless — all subsequent requests will also
fail. The job now returns Err immediately, which sets status='failed' with
a clear message indicating where it stopped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allow switching between number of books and number of pages on the
dashboard reading activity chart. Adds pages_read to the stats API
response and a MetricToggle component alongside the existing PeriodToggle.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GROUP BY sm.total_volumes caused fetch_one to fail when no books matched,
silently skipping all series. COUNT(*) without GROUP BY always returns 1 row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Scope all reading progress (books, series, stats) by user via
Option<Extension<AuthUser>> — admin sees aggregate, read token sees own data
- Fix duplicate book rows when admin views lists (IS NOT NULL guard on JOIN)
- Add X-As-User header support: admin can impersonate any user from backoffice
- UserSwitcher dropdown in nav header (persisted via as_user_id cookie)
- Per-user filter pills on "Currently reading" and "Recently read" dashboard sections
- Inline username editing (UsernameEdit component with optimistic update)
- PATCH /admin/users/:id endpoint to rename a user
- Unassigned read tokens row in users table
- Komga sync now requires a user_id — reading progress attributed to selected user
- Migration 0051: add user_id column to komga_sync_reports
- Nav breakpoints: icons-only from md, labels from xl, hamburger until md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Metadata refresh now skips series with ended/cancelled status
- Add xs size to Button component
- Unify view/cancel button sizes (h-7) with icons (eye & cross)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>