feat: modale Prowlarr avec bouton remplacer + fix parseur volumes

- 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>
This commit is contained in:
2026-03-27 17:27:34 +01:00
parent 8d48b7669f
commit 00f5564f05
6 changed files with 72 additions and 97 deletions

View File

@@ -202,6 +202,9 @@ struct QbTorrentInfo {
#[serde(default)]
progress: f64,
#[serde(default)]
#[allow(dead_code)]
total_size: i64,
#[serde(default)]
dlspeed: i64,
#[serde(default)]
eta: i64,
@@ -320,11 +323,12 @@ async fn poll_qbittorrent_downloads(pool: &PgPool) -> anyhow::Result<bool> {
});
if let Some(row) = row {
let tid: Uuid = row.get("id");
let global_progress = info.progress as f32;
let _ = sqlx::query(
"UPDATE torrent_downloads SET progress = $1, download_speed = $2, eta = $3, updated_at = NOW() \
WHERE id = $4 AND status = 'downloading'",
)
.bind(info.progress as f32)
.bind(global_progress)
.bind(info.dlspeed)
.bind(info.eta)
.bind(tid)
@@ -598,23 +602,22 @@ async fn do_import(
) -> anyhow::Result<Vec<ImportedFile>> {
let physical_content = remap_downloads_path(content_path);
// Find the target directory and reference file from existing book_files.
// Exclude volumes we're about to import so we get a different file as naming reference.
let ref_row = sqlx::query(
// Find the target directory and a naming reference from existing book_files.
// First find ANY existing book to determine the target directory, then pick a
// reference file (preferring one outside expected_volumes for naming consistency).
let any_row = sqlx::query(
"SELECT bf.abs_path, b.volume \
FROM book_files bf \
JOIN books b ON b.id = bf.book_id \
WHERE b.library_id = $1 AND LOWER(b.series) = LOWER($2) AND b.volume IS NOT NULL \
AND b.volume != ALL($3) \
ORDER BY b.volume DESC LIMIT 1",
)
.bind(library_id)
.bind(series_name)
.bind(expected_volumes)
.fetch_optional(pool)
.await?;
let (target_dir, reference) = if let Some(r) = ref_row {
let (target_dir, reference) = if let Some(r) = any_row {
let abs_path: String = r.get("abs_path");
let volume: i32 = r.get("volume");
let physical = remap_libraries_path(&abs_path);