Add skip-unreleased auto-search feature

Introduce an indexer-wide option to skip automatic searches for books with future release dates (config key: `indexer.skip_unreleased`, default ON). Adds a GET/PUT admin API for indexer options, a UI toggle on the Indexers settings tab (persisted on save), and persistence of a request-level releaseDate in the Prisma schema.

Adds a new request status `awaiting_release` and wires it through constants, UI components (StatusBadge, RequestCard, RecentRequestsTable, Audiobook card/modal, RequestActions), API request flows (bookdate swipe, request creation, manual search, request PATCHs, request listing groups), and services. Implements a pure release-date utility (isUnreleased / shouldSkipAutoSearch) and updates background processors: monitor-rss-feeds (skip matches but do not mutate status), retry-missing-torrents (drives bidirectional transitions between awaiting_search and awaiting_release and queues searches when appropriate), and request-creator/bookdate swipe (gate initial auto-search). Adds tests for the swipe gate and other related test updates. Logs transitions and gate decisions for observability.
This commit is contained in:
kikootwo
2026-05-15 15:35:01 -04:00
parent 5f62ba7146
commit 6f8ac86a43
37 changed files with 1289 additions and 77 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ const getStatusConfig = (audiobook: Audiobook) => {
return { type: 'processing', label: 'Processing', color: 'amber' };
}
const pendingStatuses = ['pending', 'awaiting_search', 'searching', 'awaiting_approval'];
const pendingStatuses = ['pending', 'awaiting_search', 'awaiting_release', 'searching', 'awaiting_approval'];
if (audiobook.requestStatus && pendingStatuses.includes(audiobook.requestStatus)) {
return { type: 'pending', label: 'Requested', color: 'blue' };
}
@@ -53,7 +53,7 @@ const getStatusInfo = (isAvailable: boolean, requestStatus: string | null, reque
return { type: 'processing', label: 'Processing', canRequest: false };
}
const pendingStatuses = ['pending', 'awaiting_search', 'searching', 'awaiting_approval'];
const pendingStatuses = ['pending', 'awaiting_search', 'awaiting_release', 'searching', 'awaiting_approval'];
if (requestStatus && pendingStatuses.includes(requestStatus)) {
const label = requestStatus === 'awaiting_approval'
? requestedByUsername ? `Pending Approval (${requestedByUsername})` : 'Pending Approval'
+18
View File
@@ -27,6 +27,7 @@ interface RequestCardProps {
updatedAt: string;
completedAt?: string;
downloadAvailable?: boolean;
releaseDate?: string | Date | null;
audiobook: {
id: string;
audibleAsin?: string;
@@ -58,6 +59,18 @@ export function RequestCard({ request, showActions = true }: RequestCardProps) {
const isActive = ['searching', 'downloading', 'processing'].includes(request.status);
const isFailed = request.status === 'failed';
const releaseDateLabel = React.useMemo(() => {
if (request.status !== 'awaiting_release' || !request.releaseDate) return null;
const parsed = new Date(request.releaseDate);
if (Number.isNaN(parsed.getTime())) return null;
return parsed.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
}, [request.status, request.releaseDate]);
const handleConfirmCancel = async () => {
try {
await cancelRequest(request.id);
@@ -150,6 +163,11 @@ export function RequestCard({ request, showActions = true }: RequestCardProps) {
{/* Status Badge and Type Badge */}
<div className="flex items-center gap-2 flex-wrap">
<StatusBadge status={request.status} progress={request.progress} />
{releaseDateLabel && (
<span className="text-xs text-gray-500 dark:text-gray-400">
Releases {releaseDateLabel}
</span>
)}
{isEbook && (
<span
className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded-full"
+4
View File
@@ -68,6 +68,10 @@ export function StatusBadge({ status, progress, className }: StatusBadgeProps) {
label: 'Pending Approval',
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
},
awaiting_release: {
label: 'Awaiting Release',
color: 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200',
},
denied: {
label: 'Request Denied',
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',