mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
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:
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Component: Admin Indexer Options Settings API
|
||||
* Documentation: documentation/settings-pages.md
|
||||
*
|
||||
* Manages indexer-wide behavioral options that are not tied to a specific
|
||||
* indexer connection (e.g., auto-search behavior toggles).
|
||||
*
|
||||
* Read contract (consumed by background auto-search workers):
|
||||
* - Config key: `indexer.skip_unreleased`
|
||||
* - Category: `indexer`
|
||||
* - Value: string `'true'` | `'false'`
|
||||
* - Default: ON when the key is missing OR its value is anything other
|
||||
* than the exact string `'false'`. In other words, skipping
|
||||
* unreleased books is enabled unless the admin explicitly
|
||||
* opted out. Workers MUST match this contract:
|
||||
*
|
||||
* const skip = (await config.get('indexer.skip_unreleased')) !== 'false';
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
|
||||
import { getConfigService } from '@/lib/services/config.service';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
const logger = RMABLogger.create('API.Admin.Settings.IndexerOptions');
|
||||
|
||||
const CONFIG_KEY = 'indexer.skip_unreleased';
|
||||
|
||||
/**
|
||||
* GET /api/admin/settings/indexer-options
|
||||
* Returns the current indexer-wide options.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
return requireAdmin(req, async () => {
|
||||
try {
|
||||
const configService = getConfigService();
|
||||
const value = await configService.get(CONFIG_KEY);
|
||||
|
||||
// Default ON: missing or any value other than 'false' is treated as enabled.
|
||||
const skipUnreleased = value !== 'false';
|
||||
|
||||
return NextResponse.json({ skipUnreleased });
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch indexer options', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch indexer options' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/admin/settings/indexer-options
|
||||
* Persists indexer-wide options. Body: { skipUnreleased: boolean }
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
return requireAuth(request, async (req: AuthenticatedRequest) => {
|
||||
return requireAdmin(req, async () => {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { skipUnreleased } = body ?? {};
|
||||
|
||||
if (typeof skipUnreleased !== 'boolean') {
|
||||
return NextResponse.json(
|
||||
{ error: 'skipUnreleased must be a boolean' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const configService = getConfigService();
|
||||
await configService.setMany([
|
||||
{
|
||||
key: CONFIG_KEY,
|
||||
value: String(skipUnreleased),
|
||||
category: 'indexer',
|
||||
description:
|
||||
'Skip auto-searches for books with future release dates',
|
||||
},
|
||||
]);
|
||||
|
||||
// Explicitly clear cache for the key after write. `setMany` already
|
||||
// does this, but we make it visible here to guarantee fresh reads
|
||||
// by any sibling service that has cached the value.
|
||||
configService.clearCache(CONFIG_KEY);
|
||||
|
||||
logger.info('Indexer options updated', { skipUnreleased });
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Indexer options updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to update indexer options', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to update indexer options',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -81,6 +81,12 @@ export async function GET(request: NextRequest) {
|
||||
url: configMap.get('prowlarr_url') || '',
|
||||
apiKey: maskValue('api_key', configMap.get('prowlarr_api_key')),
|
||||
},
|
||||
indexerOptions: {
|
||||
// Default ON: missing or any value other than 'false' is treated as enabled.
|
||||
// Must stay in lock-step with /api/admin/settings/indexer-options read contract
|
||||
// and any background worker that reads `indexer.skip_unreleased` directly.
|
||||
skipUnreleased: configMap.get('indexer.skip_unreleased') !== 'false',
|
||||
},
|
||||
// downloadClient is populated from multi-client format for backward compatibility
|
||||
// The DownloadTab component now uses DownloadClientManagement which reads from /api/admin/settings/download-clients
|
||||
downloadClient: (() => {
|
||||
|
||||
Reference in New Issue
Block a user