Files
ReadMeABook/src/app/api/admin/settings/prowlarr/indexers/route.ts
T
kikootwo 411b5f88a4 Add per-indexer ratio-based seeding policy
Introduce a per-indexer ratioLimit alongside seedingTimeMinutes to control torrent cleanup. Updates include: documentation (scheduler and settings pages), types and API (saved indexer config now includes ratioLimit), setup and management UI (new TorrentSeedingFields component, modal wiring, validation and handlers), and processor logic (cleanup-seeded-torrents now requires AND-semantics between time and ratio; 0 disables a criterion, both 0 = never cleaned, undefined client ratio with ratioLimit>0 = not met). Tests were added/updated to cover ratio-only, time+ratio, missing-ratio, and UI interactions. Default behavior: ratioLimit defaults to 0 (no ratio requirement).
2026-05-18 15:07:50 -04:00

188 lines
6.9 KiB
TypeScript

/**
* Component: Prowlarr Indexers API Route
* Documentation: documentation/backend/api.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { getProwlarrService } from '@/lib/integrations/prowlarr.service';
import { getConfigService } from '@/lib/services/config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.ProwlarrIndexers');
interface SavedIndexerConfig {
id: number;
name: string;
protocol: string;
priority: number;
seedingTimeMinutes?: number; // Torrents only
ratioLimit?: number; // Torrents only (0 = no ratio requirement)
removeAfterProcessing?: boolean; // Usenet only
rssEnabled?: boolean;
audiobookCategories?: number[]; // Array of category IDs for audiobooks (default: [3030])
ebookCategories?: number[]; // Array of category IDs for ebooks (default: [7020])
categories?: number[]; // Legacy field for migration
}
/**
* GET /api/admin/settings/prowlarr/indexers
* Fetch available Prowlarr indexers with configuration
*/
export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const prowlarrService = await getProwlarrService();
const configService = getConfigService();
// Fetch indexers from Prowlarr
const indexers = await prowlarrService.getIndexers();
// Get saved indexer configuration (matches wizard format)
const savedConfigStr = await configService.get('prowlarr_indexers');
const savedIndexers: SavedIndexerConfig[] = savedConfigStr ? JSON.parse(savedConfigStr) : [];
// Get saved flag configuration
const flagConfigStr = await configService.get('indexer_flag_config');
const flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : [];
// Merge with defaults (wizard format: array of {id, name, priority, seedingTimeMinutes})
const savedIndexersMap = new Map<number, SavedIndexerConfig>(
savedIndexers.map((idx) => [idx.id, idx])
);
const indexersWithConfig = indexers.map((indexer: any) => {
const saved = savedIndexersMap.get(indexer.id);
const isAdded = !!saved;
const isTorrent = indexer.protocol?.toLowerCase() === 'torrent';
// Migration: if old 'categories' field exists but new fields don't, migrate
const migratedAudiobookCategories = saved?.audiobookCategories ||
saved?.categories || // Legacy migration
[3030]; // Default to audiobooks category
const migratedEbookCategories = saved?.ebookCategories || [7020]; // Default to ebooks category
const config: any = {
id: indexer.id,
name: indexer.name,
protocol: indexer.protocol,
privacy: indexer.privacy,
enabled: isAdded, // Enabled if in saved list
isAdded, // Explicit flag for UI (new card-based interface)
priority: saved?.priority || 10,
rssEnabled: saved?.rssEnabled ?? false,
audiobookCategories: migratedAudiobookCategories,
ebookCategories: migratedEbookCategories,
supportsRss: indexer.capabilities?.supportsRss !== false, // Default to true if not specified
};
// Add protocol-specific fields
if (isTorrent) {
config.seedingTimeMinutes = saved?.seedingTimeMinutes ?? 0;
config.ratioLimit = saved?.ratioLimit ?? 0;
} else {
config.removeAfterProcessing = saved?.removeAfterProcessing ?? true;
}
return config;
});
return NextResponse.json({
success: true,
indexers: indexersWithConfig,
flagConfigs,
});
} catch (error) {
logger.error('Failed to fetch indexers', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{
success: false,
error: 'Failed to fetch Prowlarr indexers',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
});
});
}
/**
* PUT /api/admin/settings/prowlarr/indexers
* Save indexer configuration and flag configs
*/
export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { indexers, flagConfigs } = await req.json();
// Filter to only enabled indexers and convert to wizard format
const enabledIndexers = indexers
.filter((indexer: any) => indexer.enabled)
.map((indexer: any) => {
const config: any = {
id: indexer.id,
name: indexer.name,
protocol: indexer.protocol,
priority: indexer.priority,
rssEnabled: indexer.rssEnabled || false,
audiobookCategories: indexer.audiobookCategories || [3030], // Default to audiobooks
ebookCategories: indexer.ebookCategories || [7020], // Default to ebooks
};
// Add protocol-specific fields
const isTorrent = indexer.protocol?.toLowerCase() === 'torrent';
if (isTorrent) {
config.seedingTimeMinutes = indexer.seedingTimeMinutes ?? 0;
config.ratioLimit = indexer.ratioLimit ?? 0;
} else {
config.removeAfterProcessing = indexer.removeAfterProcessing ?? true;
}
return config;
});
// Save to configuration (matches wizard format)
const configService = getConfigService();
const configUpdates = [
{
key: 'prowlarr_indexers',
value: JSON.stringify(enabledIndexers),
category: 'indexer',
description: 'Prowlarr indexer settings (enabled, priority, seeding time)',
},
];
// Save flag configs if provided
if (flagConfigs !== undefined) {
configUpdates.push({
key: 'indexer_flag_config',
value: JSON.stringify(flagConfigs),
category: 'indexer',
description: 'Indexer flag bonus/penalty configuration',
});
}
await configService.setMany(configUpdates);
return NextResponse.json({
success: true,
message: 'Indexer configuration saved',
});
} catch (error) {
logger.error('Failed to save indexer config', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{
success: false,
error: 'Failed to save indexer configuration',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
});
});
}