Add authors pages and requestType notifications

Introduce full authors browsing/detail feature and enhance notifications to support type-specific titles.

- Add server APIs: authors search, author detail, and author books routes (audnexus integration) that require auth and enrich results with library matches.
- Add frontend pages/components: /authors listing and /authors/[asin] detail pages; AuthorCard, AuthorGrid, AuthorDetailCard, SimilarAuthorsRow, and related skeletons.
- Add hook and integration stubs: new useAuthors hook and audnexus-authors integration; update audible service to expose audibleBaseUrl.
- Update AudiobookDetailsModal to use audibleBaseUrl and link author names to author detail pages.
- Add header navigation link to Authors.
- Notifications: extend docs and code to include requestType (audiobook|ebook), add getEventTitle/getEventMeta helpers, update queue signature and providers/processors/tests to pass/handle requestType so titles can be resolved per request type.
- Misc: job queue, processors, provider tests and notification tests updated to reflect new behavior.

This change enables browsing authors and provides type-aware notification titles without per-provider changes.
This commit is contained in:
kikootwo
2026-02-12 15:21:42 -05:00
parent e40e77c8fe
commit 89422fc77a
33 changed files with 1629 additions and 40 deletions
@@ -10,6 +10,7 @@
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { createPortal } from 'react-dom';
import { useAudiobookDetails } from '@/lib/hooks/useAudiobooks';
import { useCreateRequest, useEbookStatus, useFetchEbookByAsin } from '@/lib/hooks/useRequests';
@@ -71,7 +72,7 @@ export function AudiobookDetailsModal({
}: AudiobookDetailsModalProps) {
const { user } = useAuth();
const { squareCovers } = usePreferences();
const { audiobook, isLoading, error } = useAudiobookDetails(isOpen ? asin : null);
const { audiobook, audibleBaseUrl, isLoading, error } = useAudiobookDetails(isOpen ? asin : null);
const { createRequest, isLoading: isRequesting } = useCreateRequest();
const { ebookStatus, revalidate: revalidateEbookStatus } = useEbookStatus(isOpen && isAvailable ? asin : null);
const { fetchEbook, isLoading: isFetchingEbook } = useFetchEbookByAsin();
@@ -286,7 +287,20 @@ export function AudiobookDetailsModal({
{audiobook.title}
</h2>
<p className="mt-2 text-base sm:text-lg text-gray-600 dark:text-gray-300">
{audiobook.author}
{audiobook.authorAsin ? (
<Link
href={`/authors/${audiobook.authorAsin}`}
onClick={(e) => {
e.stopPropagation();
onClose();
}}
className="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
{audiobook.author}
</Link>
) : (
audiobook.author
)}
</p>
{audiobook.narrator && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
@@ -418,7 +432,7 @@ export function AudiobookDetailsModal({
<div>
<p className="text-gray-500 dark:text-gray-400">Source</p>
<a
href={`https://www.audible.com/pd/${asin}`}
href={`${audibleBaseUrl}/pd/${asin}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-orange-600 dark:text-orange-400 hover:underline"