feat: gestion des téléchargements qBittorrent avec import automatique

- 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>
This commit is contained in:
2026-03-26 14:43:10 +01:00
parent a2de2e1601
commit 4bb142d1dd
21 changed files with 1197 additions and 39 deletions

View File

@@ -19,6 +19,7 @@ mod pages;
mod prowlarr;
mod qbittorrent;
mod reading_progress;
mod torrent_import;
mod reading_status_match;
mod reading_status_push;
mod search;
@@ -121,6 +122,7 @@ async fn main() -> anyhow::Result<()> {
.route("/prowlarr/test", get(prowlarr::test_prowlarr))
.route("/qbittorrent/add", axum::routing::post(qbittorrent::add_torrent))
.route("/qbittorrent/test", get(qbittorrent::test_qbittorrent))
.route("/torrent-downloads", get(torrent_import::list_torrent_downloads))
.route("/telegram/test", get(telegram::test_telegram))
.route("/komga/sync", axum::routing::post(komga::sync_komga_read_books))
.route("/komga/reports", get(komga::list_sync_reports))
@@ -190,12 +192,14 @@ async fn main() -> anyhow::Result<()> {
// Clone pool before state is moved into the router
let poller_pool = state.pool.clone();
let torrent_poller_pool = state.pool.clone();
let app = Router::new()
.route("/health", get(handlers::health))
.route("/ready", get(handlers::ready))
.route("/metrics", get(handlers::metrics))
.route("/docs", get(handlers::docs_redirect))
.route("/torrent-downloads/notify", axum::routing::post(torrent_import::notify_torrent_done))
.merge(SwaggerUi::new("/swagger-ui").url("/openapi.json", openapi::ApiDoc::openapi()))
.merge(admin_routes)
.merge(read_routes)
@@ -207,6 +211,11 @@ async fn main() -> anyhow::Result<()> {
job_poller::run_job_poller(poller_pool, 5).await;
});
// Start background poller for qBittorrent torrent completions (every 30s)
tokio::spawn(async move {
torrent_import::run_torrent_poller(torrent_poller_pool, 30).await;
});
let listener = tokio::net::TcpListener::bind(&config.listen_addr).await?;
info!(addr = %config.listen_addr, "api listening");
axum::serve(listener, app).await?;