Merge branch 'toggleable-shelves'

This commit is contained in:
kikootwo
2026-03-11 10:02:57 -04:00
15 changed files with 167 additions and 40 deletions
+8 -6
View File
@@ -527,9 +527,10 @@ model GoodreadsShelf {
rssUrl String @map("rss_url") @db.Text rssUrl String @map("rss_url") @db.Text
lastSyncAt DateTime? @map("last_sync_at") lastSyncAt DateTime? @map("last_sync_at")
bookCount Int? @map("book_count") bookCount Int? @map("book_count")
coverUrls String? @map("cover_urls") @db.Text // JSON array of cover image URLs coverUrls String? @map("cover_urls") @db.Text // JSON array of cover image URLs
createdAt DateTime @default(now()) @map("created_at") autoRequest Boolean @default(true) @map("auto_request") // Whether to auto-create requests for books on this shelf
updatedAt DateTime @updatedAt @map("updated_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations // Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -577,9 +578,10 @@ model HardcoverShelf {
apiToken String @map("api_token") @db.Text // User's personal access token for hardcover api apiToken String @map("api_token") @db.Text // User's personal access token for hardcover api
lastSyncAt DateTime? @map("last_sync_at") lastSyncAt DateTime? @map("last_sync_at")
bookCount Int? @map("book_count") bookCount Int? @map("book_count")
coverUrls String? @map("cover_urls") @db.Text // JSON array of cover image URLs coverUrls String? @map("cover_urls") @db.Text // JSON array of cover image URLs
createdAt DateTime @default(now()) @map("created_at") autoRequest Boolean @default(true) @map("auto_request") // Whether to auto-create requests for books on this shelf
updatedAt DateTime @updatedAt @map("updated_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations // Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -13,7 +13,8 @@ import { z } from 'zod';
const logger = RMABLogger.create('API.GoodreadsShelves'); const logger = RMABLogger.create('API.GoodreadsShelves');
const UpdateGoodreadsSchema = z.object({ const UpdateGoodreadsSchema = z.object({
rssUrl: z.string().url('Must be a valid URL'), rssUrl: z.string().url('Must be a valid URL').optional(),
autoRequest: z.boolean().optional(),
}); });
/** /**
@@ -81,21 +82,37 @@ export async function PATCH(
} }
const body = await request.json(); const body = await request.json();
const { rssUrl } = UpdateGoodreadsSchema.parse(body); const { rssUrl, autoRequest } = UpdateGoodreadsSchema.parse(body);
const updateData: Record<string, unknown> = {};
let needsResync = false;
if (rssUrl !== undefined) {
updateData.rssUrl = rssUrl;
updateData.lastSyncAt = null;
updateData.bookCount = null;
updateData.coverUrls = null;
needsResync = true;
}
if (autoRequest !== undefined) {
updateData.autoRequest = autoRequest;
}
// Force re-fetch by clearing metadata
const updated = await prisma.goodreadsShelf.update({ const updated = await prisma.goodreadsShelf.update({
where: { id }, where: { id },
data: { rssUrl, lastSyncAt: null, bookCount: null, coverUrls: null }, data: updateData,
}); });
try { if (needsResync) {
const jobQueue = getJobQueueService(); try {
await jobQueue.addSyncShelvesJob(undefined, updated.id, 'goodreads', 0, req.user.id); const jobQueue = getJobQueueService();
} catch (error) { await jobQueue.addSyncShelvesJob(undefined, updated.id, 'goodreads', 0, req.user.id);
logger.error('Failed to trigger immediate list sync', { } catch (error) {
error: error instanceof Error ? error.message : String(error), logger.error('Failed to trigger immediate list sync', {
}); error: error instanceof Error ? error.message : String(error),
});
}
} }
return NextResponse.json({ success: true, shelf: updated }); return NextResponse.json({ success: true, shelf: updated });
+5 -1
View File
@@ -20,6 +20,7 @@ const AddShelfSchema = z.object({
(url) => GOODREADS_RSS_PATTERN.test(url), (url) => GOODREADS_RSS_PATTERN.test(url),
{ message: 'URL must be a Goodreads shelf RSS URL (goodreads.com/review/list_rss/...)' } { message: 'URL must be a Goodreads shelf RSS URL (goodreads.com/review/list_rss/...)' }
), ),
autoRequest: z.boolean().optional().default(true),
}); });
/** /**
@@ -66,6 +67,7 @@ export async function GET(request: NextRequest) {
lastSyncAt: shelf.lastSyncAt, lastSyncAt: shelf.lastSyncAt,
createdAt: shelf.createdAt, createdAt: shelf.createdAt,
bookCount: shelf.bookCount ?? null, bookCount: shelf.bookCount ?? null,
autoRequest: shelf.autoRequest,
books, books,
}; };
}); });
@@ -90,7 +92,7 @@ export async function POST(request: NextRequest) {
} }
const body = await req.json(); const body = await req.json();
const { rssUrl } = AddShelfSchema.parse(body); const { rssUrl, autoRequest } = AddShelfSchema.parse(body);
// Check for duplicate // Check for duplicate
const existing = await prisma.goodreadsShelf.findUnique({ const existing = await prisma.goodreadsShelf.findUnique({
@@ -132,6 +134,7 @@ export async function POST(request: NextRequest) {
name: shelfName, name: shelfName,
rssUrl, rssUrl,
bookCount, bookCount,
autoRequest,
coverUrls: initialBooks.length > 0 ? JSON.stringify(initialBooks) : null, coverUrls: initialBooks.length > 0 ? JSON.stringify(initialBooks) : null,
}, },
}); });
@@ -154,6 +157,7 @@ export async function POST(request: NextRequest) {
lastSyncAt: shelf.lastSyncAt, lastSyncAt: shelf.lastSyncAt,
createdAt: shelf.createdAt, createdAt: shelf.createdAt,
bookCount: shelf.bookCount, bookCount: shelf.bookCount,
autoRequest: shelf.autoRequest,
books: initialBooks, books: initialBooks,
}, },
bookCount, bookCount,
@@ -18,6 +18,7 @@ const UpdateHardcoverSchema = z.object({
listId: z.string().min(1, 'List ID is required').optional(), listId: z.string().min(1, 'List ID is required').optional(),
apiToken: z.string().optional(), apiToken: z.string().optional(),
forceSync: z.boolean().optional(), forceSync: z.boolean().optional(),
autoRequest: z.boolean().optional(),
}); });
/** /**
@@ -90,9 +91,13 @@ export async function PATCH(
} }
const body = await request.json(); const body = await request.json();
const { listId, apiToken, forceSync } = UpdateHardcoverSchema.parse(body); const { listId, apiToken, forceSync, autoRequest } = UpdateHardcoverSchema.parse(body);
const updateData: { listId?: string; apiToken?: string; lastSyncAt?: null; bookCount?: null; coverUrls?: null } = {}; const updateData: { listId?: string; apiToken?: string; autoRequest?: boolean; lastSyncAt?: null; bookCount?: null; coverUrls?: null } = {};
if (autoRequest !== undefined) {
updateData.autoRequest = autoRequest;
}
let needsResync = !!forceSync; let needsResync = !!forceSync;
let cleanedToken: string | undefined; let cleanedToken: string | undefined;
+7 -1
View File
@@ -18,6 +18,7 @@ const logger = RMABLogger.create('API.HardcoverShelves');
const AddShelfSchema = z.object({ const AddShelfSchema = z.object({
listId: z.string().min(1, { message: 'List ID is required' }), listId: z.string().min(1, { message: 'List ID is required' }),
apiToken: z.string().min(1, { message: 'API Token is required' }), apiToken: z.string().min(1, { message: 'API Token is required' }),
autoRequest: z.boolean().optional().default(true),
}); });
/** /**
@@ -46,6 +47,7 @@ export async function GET(request: NextRequest) {
lastSyncAt: shelf.lastSyncAt, lastSyncAt: shelf.lastSyncAt,
createdAt: shelf.createdAt, createdAt: shelf.createdAt,
bookCount: shelf.bookCount ?? null, bookCount: shelf.bookCount ?? null,
autoRequest: shelf.autoRequest,
books, books,
}; };
}); });
@@ -75,7 +77,9 @@ export async function POST(request: NextRequest) {
} }
const body = await req.json(); const body = await req.json();
let { listId, apiToken } = AddShelfSchema.parse(body); const parsed = AddShelfSchema.parse(body);
let { listId, apiToken } = parsed;
const { autoRequest } = parsed;
// Clean up token in case user pasted "Bearer " prefix // Clean up token in case user pasted "Bearer " prefix
apiToken = apiToken.trim(); apiToken = apiToken.trim();
@@ -139,6 +143,7 @@ export async function POST(request: NextRequest) {
name: listName, name: listName,
listId, listId,
apiToken: encryptedToken, apiToken: encryptedToken,
autoRequest,
bookCount, bookCount,
coverUrls: coverUrls:
initialBooks.length > 0 ? JSON.stringify(initialBooks) : null, initialBooks.length > 0 ? JSON.stringify(initialBooks) : null,
@@ -168,6 +173,7 @@ export async function POST(request: NextRequest) {
lastSyncAt: shelf.lastSyncAt, lastSyncAt: shelf.lastSyncAt,
createdAt: shelf.createdAt, createdAt: shelf.createdAt,
bookCount: shelf.bookCount, bookCount: shelf.bookCount,
autoRequest: shelf.autoRequest,
books: initialBooks, books: initialBooks,
}, },
bookCount, bookCount,
+2
View File
@@ -42,6 +42,7 @@ export async function GET(request: NextRequest) {
lastSyncAt: s.lastSyncAt, lastSyncAt: s.lastSyncAt,
createdAt: s.createdAt, createdAt: s.createdAt,
bookCount: s.bookCount ?? null, bookCount: s.bookCount ?? null,
autoRequest: s.autoRequest,
books: processBooks(s.coverUrls), books: processBooks(s.coverUrls),
})), })),
...hardcover.map((s) => ({ ...hardcover.map((s) => ({
@@ -52,6 +53,7 @@ export async function GET(request: NextRequest) {
lastSyncAt: s.lastSyncAt, lastSyncAt: s.lastSyncAt,
createdAt: s.createdAt, createdAt: s.createdAt,
bookCount: s.bookCount ?? null, bookCount: s.bookCount ?? null,
autoRequest: s.autoRequest,
books: processBooks(s.coverUrls), books: processBooks(s.coverUrls),
})), })),
].sort( ].sort(
+62 -4
View File
@@ -11,8 +11,8 @@ import {
GenericShelf, GenericShelf,
useSyncShelves, useSyncShelves,
} from '@/lib/hooks/useShelves'; } from '@/lib/hooks/useShelves';
import { useDeleteGoodreadsShelf } from '@/lib/hooks/useGoodreadsShelves'; import { useDeleteGoodreadsShelf, useUpdateGoodreadsShelf } from '@/lib/hooks/useGoodreadsShelves';
import { useDeleteHardcoverShelf } from '@/lib/hooks/useHardcoverShelves'; import { useDeleteHardcoverShelf, useUpdateHardcoverShelf } from '@/lib/hooks/useHardcoverShelves';
import { AddShelfModal } from '@/components/ui/AddShelfModal'; import { AddShelfModal } from '@/components/ui/AddShelfModal';
import { AudiobookDetailsModal } from '@/components/audiobooks/AudiobookDetailsModal'; import { AudiobookDetailsModal } from '@/components/audiobooks/AudiobookDetailsModal';
import { usePreferences } from '@/contexts/PreferencesContext'; import { usePreferences } from '@/contexts/PreferencesContext';
@@ -42,6 +42,8 @@ export function ShelvesSection() {
const { deleteShelf: deleteHardcover, isLoading: isDeletingHardcover } = const { deleteShelf: deleteHardcover, isLoading: isDeletingHardcover } =
useDeleteHardcoverShelf(); useDeleteHardcoverShelf();
const { syncShelves, isSyncing: isSyncingAll } = useSyncShelves(); const { syncShelves, isSyncing: isSyncingAll } = useSyncShelves();
const { updateShelf: updateGoodreads } = useUpdateGoodreadsShelf();
const { updateShelf: updateHardcover } = useUpdateHardcoverShelf();
const { squareCovers } = usePreferences(); const { squareCovers } = usePreferences();
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null); const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
@@ -62,6 +64,18 @@ export function ShelvesSection() {
} }
}; };
const handleToggleAutoRequest = async (shelf: GenericShelf) => {
try {
if (shelf.type === 'goodreads') {
await updateGoodreads(shelf.id, { autoRequest: !shelf.autoRequest });
} else {
await updateHardcover(shelf.id, { autoRequest: !shelf.autoRequest });
}
} catch {
// Error handled by hook
}
};
const isDeleting = isDeletingGoodreads || isDeletingHardcover; const isDeleting = isDeletingGoodreads || isDeletingHardcover;
return ( return (
@@ -159,6 +173,7 @@ export function ShelvesSection() {
onConfirmDelete={() => setConfirmDeleteId(shelf.id)} onConfirmDelete={() => setConfirmDeleteId(shelf.id)}
onCancelDelete={() => setConfirmDeleteId(null)} onCancelDelete={() => setConfirmDeleteId(null)}
onManage={() => setManageShelf(shelf)} onManage={() => setManageShelf(shelf)}
onToggleAutoRequest={() => handleToggleAutoRequest(shelf)}
onBookClick={(asin) => setSelectedAsin(asin)} onBookClick={(asin) => setSelectedAsin(asin)}
/> />
))} ))}
@@ -282,6 +297,7 @@ interface ShelfCardProps {
onConfirmDelete: () => void; onConfirmDelete: () => void;
onCancelDelete: () => void; onCancelDelete: () => void;
onManage: () => void; onManage: () => void;
onToggleAutoRequest: () => void;
onBookClick: (asin: string) => void; onBookClick: (asin: string) => void;
} }
@@ -294,6 +310,7 @@ function ShelfCard({
onConfirmDelete, onConfirmDelete,
onCancelDelete, onCancelDelete,
onManage, onManage,
onToggleAutoRequest,
onBookClick, onBookClick,
}: ShelfCardProps) { }: ShelfCardProps) {
const { syncShelves, isSyncing: isManualSyncing } = useSyncShelves(); const { syncShelves, isSyncing: isManualSyncing } = useSyncShelves();
@@ -321,7 +338,12 @@ function ShelfCard({
); );
return ( return (
<div className="group rounded-2xl bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700/30 p-6 sm:p-7 transition-all duration-300 hover:shadow-lg hover:shadow-black/[0.04] dark:hover:shadow-black/20 hover:border-gray-200 dark:hover:border-gray-600/40"> <div className={cn(
'group rounded-2xl bg-white dark:bg-gray-800 border p-6 sm:p-7 transition-all duration-300',
shelf.autoRequest
? 'border-gray-100 dark:border-gray-700/30 hover:shadow-lg hover:shadow-black/[0.04] dark:hover:shadow-black/20 hover:border-gray-200 dark:hover:border-gray-600/40'
: 'border-gray-200/60 dark:border-gray-700/20 bg-gray-50/50 dark:bg-gray-800/60',
)}>
{/* Top: Shelf info + actions */} {/* Top: Shelf info + actions */}
<div <div
className={cn( className={cn(
@@ -330,7 +352,12 @@ function ShelfCard({
)} )}
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="font-semibold text-[15px] text-gray-900 dark:text-white truncate leading-snug flex items-center"> <h3 className={cn(
'font-semibold text-[15px] truncate leading-snug flex items-center',
shelf.autoRequest
? 'text-gray-900 dark:text-white'
: 'text-gray-400 dark:text-gray-500',
)}>
{shelf.name} {providerIcon} {shelf.name} {providerIcon}
</h3> </h3>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
@@ -339,6 +366,14 @@ function ShelfCard({
{shelf.bookCount} {shelf.bookCount === 1 ? 'book' : 'books'} {shelf.bookCount} {shelf.bookCount === 1 ? 'book' : 'books'}
</span> </span>
)} )}
{!shelf.autoRequest && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-1 ring-amber-200/50 dark:ring-amber-500/20">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
Paused
</span>
)}
<span className="inline-flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500"> <span className="inline-flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500">
{isSyncing ? ( {isSyncing ? (
<> <>
@@ -381,6 +416,27 @@ function ShelfCard({
</div> </div>
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button
onClick={onToggleAutoRequest}
className={cn(
'p-2 transition-all duration-200 rounded-xl outline-none',
shelf.autoRequest
? 'text-gray-400 hover:text-amber-500 dark:text-gray-500 dark:hover:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10 opacity-40 hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-amber-500/40'
: 'text-amber-500 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 opacity-100',
)}
title={shelf.autoRequest ? 'Pause auto-requesting' : 'Resume auto-requesting'}
aria-label={shelf.autoRequest ? 'Pause auto-requesting' : 'Resume auto-requesting'}
>
{shelf.autoRequest ? (
<svg className="w-[18px] h-[18px]" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
) : (
<svg className="w-[18px] h-[18px]" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
</svg>
)}
</button>
<button <button
onClick={onManage} onClick={onManage}
className="p-2 text-gray-400 hover:text-blue-500 dark:text-gray-500 dark:hover:text-blue-400 transition-all duration-200 rounded-xl hover:bg-blue-50 dark:hover:bg-blue-500/10 opacity-40 hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-blue-500/40 outline-none" className="p-2 text-gray-400 hover:text-blue-500 dark:text-gray-500 dark:hover:text-blue-400 transition-all duration-200 rounded-xl hover:bg-blue-50 dark:hover:bg-blue-500/10 opacity-40 hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-blue-500/40 outline-none"
@@ -451,6 +507,7 @@ function ShelfCard({
</div> </div>
{/* Bottom: Stacked book covers */} {/* Bottom: Stacked book covers */}
<div className={cn(!shelf.autoRequest && 'opacity-50 grayscale-[30%]')}>
{hasCovers ? ( {hasCovers ? (
<CoverStack <CoverStack
books={displayBooks} books={displayBooks}
@@ -472,6 +529,7 @@ function ShelfCard({
))} ))}
</div> </div>
) : null} ) : null}
</div>
</div> </div>
); );
} }
+31 -2
View File
@@ -32,6 +32,8 @@ export function AddShelfModal({ isOpen, onClose }: AddShelfModalProps) {
const [statusId, setStatusId] = useState('1'); const [statusId, setStatusId] = useState('1');
const [customListId, setCustomListId] = useState(''); const [customListId, setCustomListId] = useState('');
// Shared State
const [autoRequest, setAutoRequest] = useState(true);
const [validationError, setValidationError] = useState(''); const [validationError, setValidationError] = useState('');
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
const [successMessage, setSuccessMessage] = useState(''); const [successMessage, setSuccessMessage] = useState('');
@@ -72,12 +74,12 @@ export function AddShelfModal({ isOpen, onClose }: AddShelfModalProps) {
try { try {
if (provider === 'goodreads') { if (provider === 'goodreads') {
const shelf = await addGoodreads(rssUrl); const shelf = await addGoodreads(rssUrl, autoRequest);
setSuccessMessage(`Added shelf "${shelf.name}" successfully!`); setSuccessMessage(`Added shelf "${shelf.name}" successfully!`);
setRssUrl(''); setRssUrl('');
} else { } else {
const finalId = listType === 'status' ? `status-${statusId}` : customListId.trim(); const finalId = listType === 'status' ? `status-${statusId}` : customListId.trim();
const shelf = await addHardcover(apiToken.trim(), finalId); const shelf = await addHardcover(apiToken.trim(), finalId, autoRequest);
setSuccessMessage(`Added list "${shelf.name}" successfully!`); setSuccessMessage(`Added list "${shelf.name}" successfully!`);
setApiToken(''); setApiToken('');
setCustomListId(''); setCustomListId('');
@@ -98,6 +100,7 @@ export function AddShelfModal({ isOpen, onClose }: AddShelfModalProps) {
setRssUrl(''); setRssUrl('');
setApiToken(''); setApiToken('');
setCustomListId(''); setCustomListId('');
setAutoRequest(true);
setValidationError(''); setValidationError('');
setSuccess(false); setSuccess(false);
setSuccessMessage(''); setSuccessMessage('');
@@ -215,6 +218,32 @@ export function AddShelfModal({ isOpen, onClose }: AddShelfModalProps) {
/> />
)} )}
{/* Auto-Request Toggle */}
<label className="flex items-center justify-between gap-3 p-3 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700/30 cursor-pointer select-none">
<div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Auto-request books</span>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Automatically request audiobooks from this shelf
</p>
</div>
<button
type="button"
role="switch"
aria-checked={autoRequest}
onClick={() => setAutoRequest(!autoRequest)}
disabled={isLoading || success}
className={`relative inline-flex h-5 w-9 flex-shrink-0 rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40 ${
autoRequest ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'
} ${(isLoading || success) ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<span
className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out ${
autoRequest ? 'translate-x-4' : 'translate-x-0.5'
} mt-0.5`}
/>
</button>
</label>
<div className="flex justify-end gap-3 pt-2"> <div className="flex justify-end gap-3 pt-2">
<Button type="button" variant="ghost" size="sm" onClick={handleClose} disabled={isLoading || success}> <Button type="button" variant="ghost" size="sm" onClick={handleClose} disabled={isLoading || success}>
Cancel Cancel
+1 -1
View File
@@ -45,7 +45,7 @@ export function ManageShelfModal({ shelf, isOpen, onClose }: ManageShelfModalPro
try { try {
if (shelf.type === 'goodreads') { if (shelf.type === 'goodreads') {
if (!rssUrl.trim()) return; if (!rssUrl.trim()) return;
await updateGoodreads(shelf.id, rssUrl.trim()); await updateGoodreads(shelf.id, { rssUrl: rssUrl.trim() });
} else { } else {
if (!listId.trim()) return; if (!listId.trim()) return;
await updateHardcover(shelf.id, { await updateHardcover(shelf.id, {
+5 -4
View File
@@ -16,6 +16,7 @@ export interface GoodreadsShelf {
lastSyncAt: string | null; lastSyncAt: string | null;
createdAt: string; createdAt: string;
bookCount: number | null; bookCount: number | null;
autoRequest: boolean;
books: ShelfBook[]; books: ShelfBook[];
} }
@@ -27,8 +28,8 @@ export const useGoodreadsShelves = useList;
export function useAddGoodreadsShelf() { export function useAddGoodreadsShelf() {
const { addShelf: addGeneric, isLoading, error } = useAdd(); const { addShelf: addGeneric, isLoading, error } = useAdd();
const addShelf = async (rssUrl: string) => { const addShelf = async (rssUrl: string, autoRequest: boolean = true) => {
return addGeneric({ rssUrl }); return addGeneric({ rssUrl, autoRequest });
}; };
return { addShelf, isLoading, error }; return { addShelf, isLoading, error };
@@ -39,8 +40,8 @@ export const useDeleteGoodreadsShelf = useDelete;
export function useUpdateGoodreadsShelf() { export function useUpdateGoodreadsShelf() {
const { updateShelf: updateGeneric, isLoading, error } = useUpdate(); const { updateShelf: updateGeneric, isLoading, error } = useUpdate();
const updateShelf = async (shelfId: string, rssUrl: string) => { const updateShelf = async (shelfId: string, updates: { rssUrl?: string; autoRequest?: boolean }) => {
return updateGeneric(shelfId, { rssUrl }); return updateGeneric(shelfId, updates);
}; };
return { updateShelf, isLoading, error }; return { updateShelf, isLoading, error };
+4 -3
View File
@@ -16,6 +16,7 @@ export interface HardcoverShelf {
lastSyncAt: string | null; lastSyncAt: string | null;
createdAt: string; createdAt: string;
bookCount: number | null; bookCount: number | null;
autoRequest: boolean;
books: ShelfBook[]; books: ShelfBook[];
} }
@@ -27,8 +28,8 @@ export const useHardcoverShelves = useList;
export function useAddHardcoverShelf() { export function useAddHardcoverShelf() {
const { addShelf: addGeneric, isLoading, error } = useAdd(); const { addShelf: addGeneric, isLoading, error } = useAdd();
const addShelf = async (apiToken: string, listId: string) => { const addShelf = async (apiToken: string, listId: string, autoRequest: boolean = true) => {
return addGeneric({ apiToken, listId }); return addGeneric({ apiToken, listId, autoRequest });
}; };
return { addShelf, isLoading, error }; return { addShelf, isLoading, error };
@@ -41,7 +42,7 @@ export function useUpdateHardcoverShelf() {
const updateShelf = async ( const updateShelf = async (
shelfId: string, shelfId: string,
updates: { listId?: string; apiToken?: string; forceSync?: boolean }, updates: { listId?: string; apiToken?: string; forceSync?: boolean; autoRequest?: boolean },
) => { ) => {
return updateGeneric(shelfId, updates); return updateGeneric(shelfId, updates);
}; };
+1
View File
@@ -18,6 +18,7 @@ export interface GenericShelf {
lastSyncAt: string | null; lastSyncAt: string | null;
createdAt: string; createdAt: string;
bookCount: number | null; bookCount: number | null;
autoRequest: boolean;
books: ShelfBook[]; books: ShelfBook[];
} }
+2 -2
View File
@@ -148,10 +148,10 @@ export async function processGoodreadsShelves(
continue; continue;
} }
log.info(`Found ${rssData.books.length} books in shelf "${shelf.name}"`); log.info(`Found ${rssData.books.length} books in shelf "${shelf.name}"${!shelf.autoRequest ? ' (auto-request disabled)' : ''}`);
const bookData = await processShelfBooks( const bookData = await processShelfBooks(
'goodreads', rssData.books, shelf.user.id, shelf.id, stats, log, maxLookups, 'goodreads', rssData.books, shelf.user.id, shelf.id, stats, log, maxLookups, shelf.autoRequest,
); );
await prisma.goodreadsShelf.update({ await prisma.goodreadsShelf.update({
+2 -2
View File
@@ -88,10 +88,10 @@ export async function processHardcoverShelves(
continue; continue;
} }
log.info(`Found ${fetchedData.books.length} books in list "${shelf.name}" (Hardcover API)`); log.info(`Found ${fetchedData.books.length} books in list "${shelf.name}" (Hardcover API)${!shelf.autoRequest ? ' (auto-request disabled)' : ''}`);
const bookData = await processShelfBooks( const bookData = await processShelfBooks(
'hardcover', fetchedData.books, shelf.user.id, shelf.id, stats, log, maxLookups, 'hardcover', fetchedData.books, shelf.user.id, shelf.id, stats, log, maxLookups, shelf.autoRequest,
); );
const finalListName = const finalListName =
+2 -1
View File
@@ -73,6 +73,7 @@ export async function processShelfBooks(
stats: ShelfSyncStats, stats: ShelfSyncStats,
log: LoggerType, log: LoggerType,
maxLookups: number, maxLookups: number,
autoRequest: boolean = true,
): Promise<{ coverUrl: string; asin: string | null; title: string; author: string }[]> { ): Promise<{ coverUrl: string; asin: string | null; title: string; author: string }[]> {
stats.booksFound += books.length; stats.booksFound += books.length;
@@ -112,7 +113,7 @@ export async function processShelfBooks(
} }
} }
if (mapping.audibleAsin) { if (mapping.audibleAsin && autoRequest) {
try { try {
const result = await createRequestForUser(userId, { const result = await createRequestForUser(userId, {
asin: mapping.audibleAsin, asin: mapping.audibleAsin,