Files
ReadMeABook/src/lib/hooks/useIgnoredAudiobooks.ts
T
kikootwo 09cff5b68d 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.
2026-03-11 11:56:35 -04:00

124 lines
3.1 KiB
TypeScript

/**
* Component: Ignored Audiobooks Hook
* Documentation: documentation/features/ignored-audiobooks.md
*
* Provides hooks for checking and toggling audiobook ignore status.
* - useIsIgnored(asin): check if a specific book is ignored
* - useToggleIgnore(): toggle ignore on/off for a book
* - useIgnoredList(): list all ignored books for the current user
*/
'use client';
import useSWR, { mutate } from 'swr';
import { authenticatedFetcher, fetchWithAuth } from '@/lib/utils/api';
interface IgnoredAudiobook {
id: string;
asin: string;
title: string;
author: string;
coverArtUrl?: string;
createdAt: string;
}
interface IgnoreCheckResult {
ignored: boolean;
ignoredId?: string;
}
/**
* Check if a specific ASIN is ignored by the current user.
* Includes works-system expansion on the server side.
*/
export function useIsIgnored(asin: string | null) {
const endpoint = asin ? `/api/user/ignored-audiobooks/check/${asin}` : null;
const { data, error, isLoading } = useSWR<IgnoreCheckResult>(
endpoint,
authenticatedFetcher,
{
revalidateOnFocus: false,
dedupingInterval: 30000,
}
);
return {
isIgnored: data?.ignored ?? false,
ignoredId: data?.ignoredId ?? null,
isLoading,
error,
};
}
/**
* Toggle ignore status for an audiobook.
* Returns { addIgnore, removeIgnore } functions.
*/
export function useToggleIgnore() {
const addIgnore = async (book: {
asin: string;
title: string;
author: string;
coverArtUrl?: string;
}): Promise<IgnoredAudiobook> => {
const res = await fetchWithAuth('/api/user/ignored-audiobooks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(book),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || 'Failed to ignore audiobook');
}
const result = await res.json();
// Invalidate the check cache for this ASIN
mutate(`/api/user/ignored-audiobooks/check/${book.asin}`);
// Invalidate the full list
mutate('/api/user/ignored-audiobooks');
return result.ignoredAudiobook;
};
const removeIgnore = async (id: string, asin: string): Promise<void> => {
const res = await fetchWithAuth(`/api/user/ignored-audiobooks/${id}`, {
method: 'DELETE',
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || 'Failed to un-ignore audiobook');
}
// Invalidate the check cache for this ASIN
mutate(`/api/user/ignored-audiobooks/check/${asin}`);
// Invalidate the full list
mutate('/api/user/ignored-audiobooks');
};
return { addIgnore, removeIgnore };
}
/**
* List all ignored audiobooks for the current user.
*/
export function useIgnoredList() {
const { data, error, isLoading } = useSWR<{ ignoredAudiobooks: IgnoredAudiobook[] }>(
'/api/user/ignored-audiobooks',
authenticatedFetcher,
{
revalidateOnFocus: false,
dedupingInterval: 60000,
}
);
return {
ignoredAudiobooks: data?.ignoredAudiobooks ?? [],
isLoading,
error,
};
}