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:
@@ -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} />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user