feat: add per-library download detection auto-schedule

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>
This commit is contained in:
2026-03-25 13:57:59 +01:00
parent 19de3ceebb
commit e0d94758af
12 changed files with 212 additions and 35 deletions

View File

@@ -148,6 +148,7 @@ export default async function LibrariesPage() {
metadataRefreshMode={lib.metadata_refresh_mode}
readingStatusProvider={lib.reading_status_provider}
readingStatusPushMode={lib.reading_status_push_mode}
downloadDetectionMode={lib.download_detection_mode ?? "manual"}
/>
<form>
<input type="hidden" name="id" value={lib.id} />

View File

@@ -8,8 +8,8 @@ export async function PATCH(
) {
const { id } = await params;
try {
const { monitor_enabled, scan_mode, watcher_enabled, metadata_refresh_mode } = await request.json();
const data = await updateLibraryMonitoring(id, monitor_enabled, scan_mode, watcher_enabled, metadata_refresh_mode);
const { monitor_enabled, scan_mode, watcher_enabled, metadata_refresh_mode, download_detection_mode } = await request.json();
const data = await updateLibraryMonitoring(id, monitor_enabled, scan_mode, watcher_enabled, metadata_refresh_mode, download_detection_mode);
revalidatePath("/libraries");
return NextResponse.json(data);
} catch (error) {

View File

@@ -16,6 +16,7 @@ interface LibraryActionsProps {
metadataRefreshMode: string;
readingStatusProvider: string | null;
readingStatusPushMode: string;
downloadDetectionMode: string;
onUpdate?: () => void;
}
@@ -29,6 +30,7 @@ export function LibraryActions({
metadataRefreshMode,
readingStatusProvider,
readingStatusPushMode,
downloadDetectionMode,
}: LibraryActionsProps) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
@@ -46,6 +48,7 @@ export function LibraryActions({
const newMetadataRefreshMode = formData.get("metadata_refresh_mode") as string;
const newReadingStatusProvider = (formData.get("reading_status_provider") as string) || null;
const newReadingStatusPushMode = (formData.get("reading_status_push_mode") as string) || "manual";
const newDownloadDetectionMode = (formData.get("download_detection_mode") as string) || "manual";
try {
const [response] = await Promise.all([
@@ -57,6 +60,7 @@ export function LibraryActions({
scan_mode: scanMode,
watcher_enabled: watcherEnabled,
metadata_refresh_mode: newMetadataRefreshMode,
download_detection_mode: newDownloadDetectionMode,
}),
}),
fetch(`/api/libraries/${libraryId}/metadata-provider`, {
@@ -313,6 +317,34 @@ export function LibraryActions({
</div>
</div>
<hr className="border-border/40" />
{/* Section: Prowlarr */}
<div className="space-y-5">
<h3 className="flex items-center gap-2 text-sm font-semibold text-foreground uppercase tracking-wide">
<svg className="w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
{t("libraryActions.sectionProwlarr")}
</h3>
<div>
<div className="flex items-center justify-between gap-4">
<label className="text-sm font-medium text-foreground">{t("libraryActions.downloadDetectionSchedule")}</label>
<select
name="download_detection_mode"
defaultValue={downloadDetectionMode}
className="text-sm border border-border rounded-lg px-3 py-1.5 bg-background min-w-[160px] shrink-0"
>
<option value="manual">{t("monitoring.manual")}</option>
<option value="hourly">{t("monitoring.hourly")}</option>
<option value="daily">{t("monitoring.daily")}</option>
<option value="weekly">{t("monitoring.weekly")}</option>
</select>
</div>
<p className="text-xs text-muted-foreground mt-1.5">{t("libraryActions.downloadDetectionScheduleDesc")}</p>
</div>
</div>
{saveError && (
<p className="text-sm text-destructive bg-destructive/10 px-3 py-2 rounded-lg break-all">
{saveError}

View File

@@ -17,6 +17,8 @@ export type LibraryDto = {
reading_status_provider: string | null;
reading_status_push_mode: string;
next_reading_status_push_at: string | null;
download_detection_mode: string;
next_download_detection_at: string | null;
};
export type IndexJobDto = {
@@ -301,12 +303,14 @@ export async function updateLibraryMonitoring(
scanMode: string,
watcherEnabled?: boolean,
metadataRefreshMode?: string,
downloadDetectionMode?: string,
) {
const body: {
monitor_enabled: boolean;
scan_mode: string;
watcher_enabled?: boolean;
metadata_refresh_mode?: string;
download_detection_mode?: string;
} = {
monitor_enabled: monitorEnabled,
scan_mode: scanMode,
@@ -317,6 +321,9 @@ export async function updateLibraryMonitoring(
if (metadataRefreshMode !== undefined) {
body.metadata_refresh_mode = metadataRefreshMode;
}
if (downloadDetectionMode !== undefined) {
body.download_detection_mode = downloadDetectionMode;
}
return apiFetch<LibraryDto>(`/libraries/${libraryId}/monitoring`, {
method: "PATCH",
body: JSON.stringify(body),

View File

@@ -202,6 +202,9 @@ const en: Record<TranslationKey, string> = {
"libraryActions.readingStatusProviderDesc": "Syncs reading states (read / reading / planned) with an external service",
"libraryActions.readingStatusPushSchedule": "Auto-push schedule",
"libraryActions.readingStatusPushScheduleDesc": "Automatically push reading progress to the provider on a schedule",
"libraryActions.sectionProwlarr": "Download detection",
"libraryActions.downloadDetectionSchedule": "Auto-detection schedule",
"libraryActions.downloadDetectionScheduleDesc": "Automatically run missing volume detection via Prowlarr on a schedule",
// Reading status modal
"readingStatus.button": "Reading status",

View File

@@ -200,6 +200,9 @@ const fr = {
"libraryActions.readingStatusProviderDesc": "Synchronise les états de lecture (lu / en cours / planifié) avec un service externe",
"libraryActions.readingStatusPushSchedule": "Synchronisation automatique",
"libraryActions.readingStatusPushScheduleDesc": "Pousse automatiquement la progression de lecture vers le provider selon un calendrier",
"libraryActions.sectionProwlarr": "Détection de téléchargements",
"libraryActions.downloadDetectionSchedule": "Détection automatique",
"libraryActions.downloadDetectionScheduleDesc": "Lance automatiquement la détection de volumes manquants via Prowlarr selon un calendrier",
// Reading status modal
"readingStatus.button": "État de lecture",