diff --git a/apps/backoffice/app/(app)/downloads/DownloadsPage.tsx b/apps/backoffice/app/(app)/downloads/DownloadsPage.tsx index ba69590..70e0bf3 100644 --- a/apps/backoffice/app/(app)/downloads/DownloadsPage.tsx +++ b/apps/backoffice/app/(app)/downloads/DownloadsPage.tsx @@ -13,6 +13,21 @@ type TFunction = (key: TranslationKey, vars?: Record) = const STATUS_ACTIVE = new Set(["downloading", "completed", "importing"]); +/** Group releases by identical title, preserving order of first occurrence. */ +function groupReleasesByTitle(releases: T[]): { title: string; items: T[]; originalIndices: number[] }[] { + const groups: Map = new Map(); + releases.forEach((r, idx) => { + const existing = groups.get(r.title); + if (existing) { + existing.items.push(r); + existing.originalIndices.push(idx); + } else { + groups.set(r.title, { items: [r], originalIndices: [idx] }); + } + }); + return Array.from(groups.entries()).map(([title, { items, originalIndices }]) => ({ title, items, originalIndices })); +} + function statusLabel(status: string, t: TFunction): string { const map: Record = { downloading: "downloads.status.downloading", @@ -385,46 +400,55 @@ function AvailableLibraryCard({ lib, onDeleted }: { lib: LatestFoundPerLibraryDt {r.available_releases && r.available_releases.length > 0 && (
- {r.available_releases.map((release, idx) => ( -
-
-

{release.title}

-
- {release.indexer && {release.indexer}} - {release.seeders != null && ( - {release.seeders}S - )} - {(release.size / 1024 / 1024).toFixed(0)} MB -
- {release.matched_missing_volumes.map(vol => ( - T{vol} - ))} -
+ {groupReleasesByTitle(r.available_releases).map((group) => ( +
+ {/* Title + matched volumes (shown once) */} +
+

{group.title}

+
+ {group.items[0].matched_missing_volumes.map(vol => ( + T{vol} + ))}
-
- {release.download_url && ( - - )} - -
+ {/* Sources */} + {group.items.map((release, si) => { + const idx = group.originalIndices[si]; + return ( +
0 ? "border-t border-border/20" : ""}`}> +
+ {release.indexer && {release.indexer}} + {release.seeders != null && ( + {release.seeders}S + )} + {(release.size / 1024 / 1024).toFixed(0)} MB +
+
+ {release.download_url && ( + + )} + +
+
+ ); + })}
))}
diff --git a/apps/backoffice/app/components/ProwlarrSearchModal.tsx b/apps/backoffice/app/components/ProwlarrSearchModal.tsx index be10ff1..4428878 100644 --- a/apps/backoffice/app/components/ProwlarrSearchModal.tsx +++ b/apps/backoffice/app/components/ProwlarrSearchModal.tsx @@ -28,6 +28,21 @@ function formatSize(bytes: number): string { return bytes + " B"; } +/** Group releases by identical title, preserving order of first occurrence. */ +function groupReleasesByTitle(releases: T[]): { title: string; items: T[] }[] { + const groups: Map = new Map(); + for (const r of releases) { + const key = r.title; + const existing = groups.get(key); + if (existing) { + existing.push(r); + } else { + groups.set(key, [r]); + } + } + return Array.from(groups.entries()).map(([title, items]) => ({ title, items })); +} + export function ProwlarrSearchModal({ seriesName, libraryId, missingBooks, initialProwlarrConfigured, initialQbConfigured }: ProwlarrSearchModalProps) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); @@ -224,24 +239,27 @@ export function ProwlarrSearchModal({ seriesName, libraryId, missingBooks, initi - {results.map((release, i) => { - const hasMissing = release.matchedMissingVolumes && release.matchedMissingVolumes.length > 0; - return ( - - - - {release.title} - - {hasMissing && ( -
- {release.matchedMissingVolumes!.map((vol) => ( - - {t("prowlarr.missingVol", { vol })} - - ))} -
- )} - + {groupReleasesByTitle(results).map((group) => { + const first = group.items[0]; + const hasMissing = first.matchedMissingVolumes && first.matchedMissingVolumes.length > 0; + return group.items.map((release, si) => ( + + {si === 0 ? ( + + + {release.title} + + {hasMissing && ( +
+ {first.matchedMissingVolumes!.map((vol) => ( + + {t("prowlarr.missingVol", { vol })} + + ))} +
+ )} + + ) : null} {release.indexer || "—"} @@ -296,7 +314,7 @@ export function ProwlarrSearchModal({ seriesName, libraryId, missingBooks, initi
- ); + )); })}