Add BookDate card stack animations and thumbnail caching

Implements pure CSS card stack animations for BookDate recommendations, including smooth exit and advance transitions. Adds local caching of library cover thumbnails during scans, updates database schema and API to serve cached covers, and enhances BookDate to support 'favorites' scope with a book picker modal. Updates admin settings validation logic for Prowlarr, improves indexer state management, and documents new features and backend changes.
This commit is contained in:
kikootwo
2026-01-20 17:28:27 -05:00
parent 2d9ed5c76a
commit ac2ad8aac2
33 changed files with 2371 additions and 707 deletions
+72
View File
@@ -0,0 +1,72 @@
/**
* Component: Library Cover Cache API Route
* Documentation: documentation/features/library-thumbnail-cache.md
*/
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs/promises';
import path from 'path';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.LibraryCovers');
const LIBRARY_CACHE_DIR = '/app/cache/library';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ filename: string }> }
) {
try {
const { filename } = await params;
// Validate filename (prevent directory traversal)
if (!filename || filename.includes('..') || filename.includes('/')) {
return NextResponse.json(
{ error: 'Invalid filename' },
{ status: 400 }
);
}
const filePath = path.join(LIBRARY_CACHE_DIR, filename);
// Check if file exists
try {
await fs.access(filePath);
} catch {
return NextResponse.json(
{ error: 'File not found' },
{ status: 404 }
);
}
// Read the file
const fileBuffer = await fs.readFile(filePath);
// Determine content type based on extension
const ext = path.extname(filename).toLowerCase();
const contentTypeMap: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
};
const contentType = contentTypeMap[ext] || 'application/octet-stream';
// Return the image with appropriate headers
return new NextResponse(fileBuffer, {
status: 200,
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400', // Cache for 24 hours
},
});
} catch (error) {
logger.error('Error serving library cover', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}