Add per-user ignored audiobooks feature

Introduce a per-user "ignored audiobooks" feature to suppress auto-requests. Changes include:

- Database: add Prisma model IgnoredAudiobook and SQL migration to create ignored_audiobooks table with indexes and FK to users.
- Backend: new API routes to list, add, delete, and check ignored audiobooks (/api/user/ignored-audiobooks, /check/:asin, /:id). Add annotateWithIgnoreStatus utility and integrate it into multiple audiobook list endpoints (popular, new-releases, category, search, authors, series).
- Request creator: add ignore-list check (with sibling-ASIN expansion) and a bypassIgnore option for manual requests; return an 'ignored' reason when blocked.
- Frontend: hooks (useIsIgnored, useToggleIgnore, useIgnoredList) and UI updates — AudiobookCard shows an "Ignored" indicator and AudiobookDetailsModal adds an ignore toggle and propagates local state changes.
- Misc: adjust deduplication duration tolerance (to 5% / min 10 minutes), tweak SWR refresh intervals for shelves/syncing, and small logging/info updates.
- Tests: add unit tests for request-creator ignore logic and update existing tests/mocks to account for ignore annotation; extend prisma test helper with ignoredAudiobook mock.

This commit implements the ignore-list end-to-end (DB, server, client, and tests) so users can ignore specific ASINs and have auto-request flows respect that preference.
This commit is contained in:
kikootwo
2026-03-11 11:56:35 -04:00
parent da7ad7cac1
commit 09cff5b68d
25 changed files with 873 additions and 35 deletions
+2 -1
View File
@@ -53,7 +53,7 @@ export async function POST(request: NextRequest) {
narrator: audiobook.narrator,
description: audiobook.description,
coverArtUrl: audiobook.coverArtUrl,
}, { skipAutoSearch });
}, { skipAutoSearch, bypassIgnore: true });
if (!result.success) {
const statusMap: Record<string, { error: string; status: number }> = {
@@ -61,6 +61,7 @@ export async function POST(request: NextRequest) {
being_processed: { error: 'BeingProcessed', status: 409 },
duplicate: { error: 'DuplicateRequest', status: 409 },
user_not_found: { error: 'UserNotFound', status: 404 },
ignored: { error: 'Ignored', status: 409 },
};
const mapped = statusMap[result.reason] || { error: 'RequestError', status: 500 };
return NextResponse.json(