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:
@@ -54,6 +54,9 @@ pub struct ProwlarrRelease {
|
|||||||
pub categories: Option<Vec<ProwlarrCategory>>,
|
pub categories: Option<Vec<ProwlarrCategory>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub matched_missing_volumes: Option<Vec<i32>>,
|
pub matched_missing_volumes: Option<Vec<i32>>,
|
||||||
|
/// All volumes extracted from the release title (not just missing ones).
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub all_volumes: Vec<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ToSchema)]
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
@@ -202,6 +205,12 @@ fn extract_volumes_from_title(title: &str) -> Vec<i32> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip "v" inside brackets like [V2] — that's a version, not a volume
|
||||||
|
if needs_boundary && ci > 0 && chars[ci - 1] == '[' {
|
||||||
|
ci += plen;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip optional spaces, dots, or '#' after prefix
|
// Skip optional spaces, dots, or '#' after prefix
|
||||||
let mut i = ci + plen;
|
let mut i = ci + plen;
|
||||||
while i < len && (chars[i] == ' ' || chars[i] == '.' || chars[i] == '#') {
|
while i < len && (chars[i] == ' ' || chars[i] == '.' || chars[i] == '#') {
|
||||||
@@ -301,12 +310,13 @@ fn match_missing_volumes(
|
|||||||
releases
|
releases
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
|
let title_volumes = extract_volumes_from_title(&r.title);
|
||||||
let matched = if missing_numbers.is_empty() {
|
let matched = if missing_numbers.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let title_volumes = extract_volumes_from_title(&r.title);
|
|
||||||
let matched: Vec<i32> = title_volumes
|
let matched: Vec<i32> = title_volumes
|
||||||
.into_iter()
|
.iter()
|
||||||
|
.copied()
|
||||||
.filter(|v| missing_numbers.contains(v))
|
.filter(|v| missing_numbers.contains(v))
|
||||||
.collect();
|
.collect();
|
||||||
if matched.is_empty() {
|
if matched.is_empty() {
|
||||||
@@ -329,6 +339,7 @@ fn match_missing_volumes(
|
|||||||
info_url: r.info_url,
|
info_url: r.info_url,
|
||||||
categories: r.categories,
|
categories: r.categories,
|
||||||
matched_missing_volumes: matched,
|
matched_missing_volumes: matched,
|
||||||
|
all_volumes: title_volumes,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -412,19 +423,23 @@ pub async fn search_prowlarr(
|
|||||||
} else {
|
} else {
|
||||||
raw_releases
|
raw_releases
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| ProwlarrRelease {
|
.map(|r| {
|
||||||
guid: r.guid,
|
let all_volumes = extract_volumes_from_title(&r.title);
|
||||||
title: r.title,
|
ProwlarrRelease {
|
||||||
size: r.size,
|
guid: r.guid,
|
||||||
download_url: r.download_url,
|
title: r.title,
|
||||||
indexer: r.indexer,
|
size: r.size,
|
||||||
seeders: r.seeders,
|
download_url: r.download_url,
|
||||||
leechers: r.leechers,
|
indexer: r.indexer,
|
||||||
publish_date: r.publish_date,
|
seeders: r.seeders,
|
||||||
protocol: r.protocol,
|
leechers: r.leechers,
|
||||||
info_url: r.info_url,
|
publish_date: r.publish_date,
|
||||||
categories: r.categories,
|
protocol: r.protocol,
|
||||||
matched_missing_volumes: None,
|
info_url: r.info_url,
|
||||||
|
categories: r.categories,
|
||||||
|
matched_missing_volumes: None,
|
||||||
|
all_volumes,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
@@ -551,10 +566,19 @@ mod tests {
|
|||||||
fn tome_hash_with_accented_chars() {
|
fn tome_hash_with_accented_chars() {
|
||||||
// Tome #097 with accented characters earlier in the string — the é in
|
// Tome #097 with accented characters earlier in the string — the é in
|
||||||
// "Compressé" shifts byte offsets vs char offsets; this must not break parsing.
|
// "Compressé" shifts byte offsets vs char offsets; this must not break parsing.
|
||||||
|
// [V2] is a version tag, not a volume — must NOT extract 2.
|
||||||
let v = sorted(extract_volumes_from_title(
|
let v = sorted(extract_volumes_from_title(
|
||||||
"[Compressé] One Piece [Team Chromatique] - Tome #097 - [V2].cbz",
|
"[Compressé] One Piece [Team Chromatique] - Tome #097 - [V2].cbz",
|
||||||
));
|
));
|
||||||
assert!(v.contains(&97), "expected 97 in {:?}", v);
|
assert!(v.contains(&97), "expected 97 in {:?}", v);
|
||||||
|
assert!(!v.contains(&2), "[V2] should not be extracted as volume 2: {:?}", v);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_in_brackets_ignored() {
|
||||||
|
// [V1], [V2], [V3] are version tags, not volumes
|
||||||
|
let v = extract_volumes_from_title("Naruto T05 [V2].cbz");
|
||||||
|
assert_eq!(v, vec![5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -202,6 +202,9 @@ struct QbTorrentInfo {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
progress: f64,
|
progress: f64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
total_size: i64,
|
||||||
|
#[serde(default)]
|
||||||
dlspeed: i64,
|
dlspeed: i64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
eta: i64,
|
eta: i64,
|
||||||
@@ -320,11 +323,12 @@ async fn poll_qbittorrent_downloads(pool: &PgPool) -> anyhow::Result<bool> {
|
|||||||
});
|
});
|
||||||
if let Some(row) = row {
|
if let Some(row) = row {
|
||||||
let tid: Uuid = row.get("id");
|
let tid: Uuid = row.get("id");
|
||||||
|
let global_progress = info.progress as f32;
|
||||||
let _ = sqlx::query(
|
let _ = sqlx::query(
|
||||||
"UPDATE torrent_downloads SET progress = $1, download_speed = $2, eta = $3, updated_at = NOW() \
|
"UPDATE torrent_downloads SET progress = $1, download_speed = $2, eta = $3, updated_at = NOW() \
|
||||||
WHERE id = $4 AND status = 'downloading'",
|
WHERE id = $4 AND status = 'downloading'",
|
||||||
)
|
)
|
||||||
.bind(info.progress as f32)
|
.bind(global_progress)
|
||||||
.bind(info.dlspeed)
|
.bind(info.dlspeed)
|
||||||
.bind(info.eta)
|
.bind(info.eta)
|
||||||
.bind(tid)
|
.bind(tid)
|
||||||
@@ -598,23 +602,22 @@ async fn do_import(
|
|||||||
) -> anyhow::Result<Vec<ImportedFile>> {
|
) -> anyhow::Result<Vec<ImportedFile>> {
|
||||||
let physical_content = remap_downloads_path(content_path);
|
let physical_content = remap_downloads_path(content_path);
|
||||||
|
|
||||||
// Find the target directory and reference file from existing book_files.
|
// Find the target directory and a naming reference from existing book_files.
|
||||||
// Exclude volumes we're about to import so we get a different file as naming reference.
|
// First find ANY existing book to determine the target directory, then pick a
|
||||||
let ref_row = sqlx::query(
|
// reference file (preferring one outside expected_volumes for naming consistency).
|
||||||
|
let any_row = sqlx::query(
|
||||||
"SELECT bf.abs_path, b.volume \
|
"SELECT bf.abs_path, b.volume \
|
||||||
FROM book_files bf \
|
FROM book_files bf \
|
||||||
JOIN books b ON b.id = bf.book_id \
|
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 \
|
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",
|
ORDER BY b.volume DESC LIMIT 1",
|
||||||
)
|
)
|
||||||
.bind(library_id)
|
.bind(library_id)
|
||||||
.bind(series_name)
|
.bind(series_name)
|
||||||
.bind(expected_volumes)
|
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?;
|
.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 abs_path: String = r.get("abs_path");
|
||||||
let volume: i32 = r.get("volume");
|
let volume: i32 = r.get("volume");
|
||||||
let physical = remap_libraries_path(&abs_path);
|
let physical = remap_libraries_path(&abs_path);
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ export default async function SeriesDetailPage({
|
|||||||
/>
|
/>
|
||||||
<ProwlarrSearchModal
|
<ProwlarrSearchModal
|
||||||
seriesName={seriesName}
|
seriesName={seriesName}
|
||||||
|
libraryId={id}
|
||||||
missingBooks={missingData?.missing_books ?? null}
|
missingBooks={missingData?.missing_books ?? null}
|
||||||
initialProwlarrConfigured={prowlarrConfigured}
|
initialProwlarrConfigured={prowlarrConfigured}
|
||||||
initialQbConfigured={qbConfigured}
|
initialQbConfigured={qbConfigured}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
|
|||||||
import { Icon } from "./ui";
|
import { Icon } from "./ui";
|
||||||
import type { ProwlarrRelease, ProwlarrSearchResponse } from "../../lib/api";
|
import type { ProwlarrRelease, ProwlarrSearchResponse } from "../../lib/api";
|
||||||
import { useTranslation } from "../../lib/i18n/context";
|
import { useTranslation } from "../../lib/i18n/context";
|
||||||
|
import { QbittorrentProvider, QbittorrentDownloadButton } from "./QbittorrentDownloadButton";
|
||||||
|
|
||||||
interface MissingBookItem {
|
interface MissingBookItem {
|
||||||
title: string | null;
|
title: string | null;
|
||||||
@@ -14,6 +15,7 @@ interface MissingBookItem {
|
|||||||
|
|
||||||
interface ProwlarrSearchModalProps {
|
interface ProwlarrSearchModalProps {
|
||||||
seriesName: string;
|
seriesName: string;
|
||||||
|
libraryId?: string;
|
||||||
missingBooks: MissingBookItem[] | null;
|
missingBooks: MissingBookItem[] | null;
|
||||||
initialProwlarrConfigured?: boolean;
|
initialProwlarrConfigured?: boolean;
|
||||||
initialQbConfigured?: boolean;
|
initialQbConfigured?: boolean;
|
||||||
@@ -26,7 +28,7 @@ function formatSize(bytes: number): string {
|
|||||||
return bytes + " B";
|
return bytes + " B";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProwlarrSearchModal({ seriesName, missingBooks, initialProwlarrConfigured, initialQbConfigured }: ProwlarrSearchModalProps) {
|
export function ProwlarrSearchModal({ seriesName, libraryId, missingBooks, initialProwlarrConfigured, initialQbConfigured }: ProwlarrSearchModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isConfigured, setIsConfigured] = useState<boolean | null>(initialProwlarrConfigured ?? null);
|
const [isConfigured, setIsConfigured] = useState<boolean | null>(initialProwlarrConfigured ?? null);
|
||||||
@@ -37,9 +39,6 @@ export function ProwlarrSearchModal({ seriesName, missingBooks, initialProwlarrC
|
|||||||
|
|
||||||
// qBittorrent state
|
// qBittorrent state
|
||||||
const [isQbConfigured, setIsQbConfigured] = useState(initialQbConfigured ?? false);
|
const [isQbConfigured, setIsQbConfigured] = useState(initialQbConfigured ?? false);
|
||||||
const [sendingGuid, setSendingGuid] = useState<string | null>(null);
|
|
||||||
const [sentGuids, setSentGuids] = useState<Set<string>>(new Set());
|
|
||||||
const [sendError, setSendError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Check if Prowlarr and qBittorrent are configured on mount (skip if server provided)
|
// Check if Prowlarr and qBittorrent are configured on mount (skip if server provided)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -112,30 +111,6 @@ export function ProwlarrSearchModal({ seriesName, missingBooks, initialProwlarrC
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSendToQbittorrent(downloadUrl: string, guid: string) {
|
|
||||||
setSendingGuid(guid);
|
|
||||||
setSendError(null);
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/api/qbittorrent/add", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ url: downloadUrl }),
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
|
||||||
if (data.error) {
|
|
||||||
setSendError(data.error);
|
|
||||||
} else if (data.success) {
|
|
||||||
setSentGuids((prev) => new Set(prev).add(guid));
|
|
||||||
} else {
|
|
||||||
setSendError(data.message || t("prowlarr.sentError"));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setSendError(t("prowlarr.sentError"));
|
|
||||||
} finally {
|
|
||||||
setSendingGuid(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't render button if not configured
|
// Don't render button if not configured
|
||||||
if (isConfigured === false) return null;
|
if (isConfigured === false) return null;
|
||||||
if (isConfigured === null) return null;
|
if (isConfigured === null) return null;
|
||||||
@@ -229,6 +204,7 @@ export function ProwlarrSearchModal({ seriesName, missingBooks, initialProwlarrC
|
|||||||
|
|
||||||
{/* Results */}
|
{/* Results */}
|
||||||
{!isSearching && results.length > 0 && (
|
{!isSearching && results.length > 0 && (
|
||||||
|
<QbittorrentProvider initialConfigured={isQbConfigured}>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
{t("prowlarr.resultCount", { count: results.length, plural: results.length !== 1 ? "s" : "" })}
|
{t("prowlarr.resultCount", { count: results.length, plural: results.length !== 1 ? "s" : "" })}
|
||||||
@@ -257,7 +233,7 @@ export function ProwlarrSearchModal({ seriesName, missingBooks, initialProwlarrC
|
|||||||
{release.title}
|
{release.title}
|
||||||
</span>
|
</span>
|
||||||
{hasMissing && (
|
{hasMissing && (
|
||||||
<div className="flex items-center gap-1 mt-1">
|
<div className="flex flex-wrap items-center gap-1 mt-1">
|
||||||
{release.matchedMissingVolumes!.map((vol) => (
|
{release.matchedMissingVolumes!.map((vol) => (
|
||||||
<span key={vol} className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-500/20 text-green-600">
|
<span key={vol} className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-500/20 text-green-600">
|
||||||
{t("prowlarr.missingVol", { vol })}
|
{t("prowlarr.missingVol", { vol })}
|
||||||
@@ -295,43 +271,16 @@ export function ProwlarrSearchModal({ seriesName, missingBooks, initialProwlarrC
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<div className="flex items-center justify-end gap-1.5">
|
<div className="flex items-center justify-end gap-1.5">
|
||||||
{isQbConfigured && release.downloadUrl && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSendToQbittorrent(release.downloadUrl!, release.guid)}
|
|
||||||
disabled={sendingGuid === release.guid || sentGuids.has(release.guid)}
|
|
||||||
className={`inline-flex items-center justify-center w-7 h-7 rounded-md transition-colors disabled:opacity-50 ${
|
|
||||||
sentGuids.has(release.guid)
|
|
||||||
? "text-green-500"
|
|
||||||
: "text-primary hover:bg-primary/10"
|
|
||||||
}`}
|
|
||||||
title={sentGuids.has(release.guid) ? t("prowlarr.sentSuccess") : t("prowlarr.sendToQbittorrent")}
|
|
||||||
>
|
|
||||||
{sendingGuid === release.guid ? (
|
|
||||||
<Icon name="spinner" size="sm" className="animate-spin" />
|
|
||||||
) : sentGuids.has(release.guid) ? (
|
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M3 8l4 4 6-7" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M14 8V14H2V2H8M10 2H14V6M14 2L7 9" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{release.downloadUrl && (
|
{release.downloadUrl && (
|
||||||
<a
|
<QbittorrentDownloadButton
|
||||||
href={release.downloadUrl}
|
downloadUrl={release.downloadUrl}
|
||||||
target="_blank"
|
releaseId={release.guid}
|
||||||
rel="noopener noreferrer"
|
libraryId={libraryId}
|
||||||
className="inline-flex items-center justify-center w-7 h-7 rounded-md text-primary hover:bg-primary/10 transition-colors"
|
seriesName={seriesName}
|
||||||
title={t("prowlarr.download")}
|
expectedVolumes={release.matchedMissingVolumes ?? release.allVolumes}
|
||||||
>
|
allVolumes={release.allVolumes}
|
||||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
alwaysShowReplace
|
||||||
<path d="M8 2v8M4 7l4 4 4-4M2 13h12" />
|
/>
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
{release.infoUrl && (
|
{release.infoUrl && (
|
||||||
<a
|
<a
|
||||||
@@ -353,13 +302,7 @@ export function ProwlarrSearchModal({ seriesName, missingBooks, initialProwlarrC
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</QbittorrentProvider>
|
||||||
|
|
||||||
{/* qBittorrent send error */}
|
|
||||||
{sendError && (
|
|
||||||
<div className="p-3 rounded-lg bg-destructive/10 text-destructive text-sm">
|
|
||||||
{sendError}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* No results */}
|
{/* No results */}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function QbittorrentDownloadButton({
|
|||||||
seriesName,
|
seriesName,
|
||||||
expectedVolumes,
|
expectedVolumes,
|
||||||
allVolumes,
|
allVolumes,
|
||||||
|
alwaysShowReplace,
|
||||||
}: {
|
}: {
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
releaseId: string;
|
releaseId: string;
|
||||||
@@ -43,6 +44,8 @@ export function QbittorrentDownloadButton({
|
|||||||
seriesName?: string;
|
seriesName?: string;
|
||||||
expectedVolumes?: number[];
|
expectedVolumes?: number[];
|
||||||
allVolumes?: number[];
|
allVolumes?: number[];
|
||||||
|
/** Show replace button even when allVolumes == expectedVolumes (e.g. in Prowlarr search modal) */
|
||||||
|
alwaysShowReplace?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { configured, onDownloadStarted } = useContext(QbConfigContext);
|
const { configured, onDownloadStarted } = useContext(QbConfigContext);
|
||||||
@@ -53,8 +56,8 @@ export function QbittorrentDownloadButton({
|
|||||||
|
|
||||||
if (!configured) return null;
|
if (!configured) return null;
|
||||||
|
|
||||||
const hasExistingVolumes = allVolumes && expectedVolumes
|
const showReplaceButton = allVolumes && allVolumes.length > 0
|
||||||
&& allVolumes.length > expectedVolumes.length;
|
&& (alwaysShowReplace || (expectedVolumes && allVolumes.length > expectedVolumes.length));
|
||||||
|
|
||||||
async function handleSend(volumes?: number[], replaceExisting = false) {
|
async function handleSend(volumes?: number[], replaceExisting = false) {
|
||||||
setSending(true);
|
setSending(true);
|
||||||
@@ -115,7 +118,7 @@ export function QbittorrentDownloadButton({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{hasExistingVolumes && (
|
{showReplaceButton && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowConfirm(true)}
|
onClick={() => setShowConfirm(true)}
|
||||||
|
|||||||
@@ -1277,6 +1277,7 @@ export type ProwlarrRelease = {
|
|||||||
infoUrl: string | null;
|
infoUrl: string | null;
|
||||||
categories: ProwlarrCategory[] | null;
|
categories: ProwlarrCategory[] | null;
|
||||||
matchedMissingVolumes: number[] | null;
|
matchedMissingVolumes: number[] | null;
|
||||||
|
allVolumes?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProwlarrSearchResponse = {
|
export type ProwlarrSearchResponse = {
|
||||||
|
|||||||
Reference in New Issue
Block a user