mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 21:00:09 +00:00
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:
@@ -6,7 +6,7 @@
|
||||
* under different ASINs (publisher re-listings, rights transfers, etc.).
|
||||
*
|
||||
* Dedup key: normalized title + normalized narrator
|
||||
* Duration tolerance: max(longerDuration * 0.01, 5) minutes
|
||||
* Duration tolerance: max(longerDuration * 0.05, 10) minutes
|
||||
* Missing duration treated as compatible (graceful degradation).
|
||||
*/
|
||||
|
||||
@@ -95,13 +95,13 @@ function normalizeNarrator(narrator?: string): string {
|
||||
|
||||
/**
|
||||
* Check if two durations are compatible (represent the same recording).
|
||||
* Tolerance: max(longerDuration * 0.01, 5) minutes.
|
||||
* Tolerance: max(longerDuration * 0.05, 10) minutes.
|
||||
* Missing duration on either side is treated as compatible.
|
||||
*/
|
||||
export function areDurationsCompatible(a?: number, b?: number): boolean {
|
||||
if (a == null || b == null) return true;
|
||||
const longer = Math.max(a, b);
|
||||
const tolerance = Math.max(longer * 0.01, 5);
|
||||
const tolerance = Math.max(longer * 0.05, 10);
|
||||
return Math.abs(a - b) <= tolerance;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Component: Ignored Audiobooks Utility
|
||||
* Documentation: documentation/features/ignored-audiobooks.md
|
||||
*
|
||||
* Shared utility for annotating audiobook lists with per-user ignore status.
|
||||
* Uses a single bulk query for the user's full ignore list, then annotates in-memory.
|
||||
*/
|
||||
|
||||
import { prisma } from '@/lib/db';
|
||||
|
||||
/**
|
||||
* Annotate an array of audiobook objects with `isIgnored: boolean`.
|
||||
* Fetches the user's full ignore list in one query and matches by ASIN.
|
||||
*
|
||||
* If userId is undefined (unauthenticated), all books get `isIgnored: false`.
|
||||
*/
|
||||
export async function annotateWithIgnoreStatus<T extends { asin: string }>(
|
||||
audiobooks: T[],
|
||||
userId?: string
|
||||
): Promise<(T & { isIgnored: boolean })[]> {
|
||||
if (!userId || audiobooks.length === 0) {
|
||||
return audiobooks.map((book) => ({ ...book, isIgnored: false }));
|
||||
}
|
||||
|
||||
// Single query: get all ASINs this user has ignored
|
||||
const ignoredEntries = await prisma.ignoredAudiobook.findMany({
|
||||
where: { userId },
|
||||
select: { asin: true },
|
||||
});
|
||||
|
||||
const ignoredAsinSet = new Set(ignoredEntries.map((e) => e.asin));
|
||||
|
||||
return audiobooks.map((book) => ({
|
||||
...book,
|
||||
isIgnored: ignoredAsinSet.has(book.asin),
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user