From 958db9911cf1aa1b276b223f8e0fbff331cff092 Mon Sep 17 00:00:00 2001 From: RXWatcher <14085001+RXWatcher@users.noreply.github.com> Date: Mon, 25 May 2026 01:43:55 +0200 Subject: [PATCH] feat(audiobooks): add embedding-based Similar audiobooks rail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third feature-detected rail on the detail page. When the backend returns similar_audiobooks (server-side embedding similarity, ranked top-N), the rail mounts above Also-by-author and In-series, with the subtitle 'Based on listening patterns' to give the recommendation source a little provenance. Frontend has no embedding logic — purely a display path that lights up the moment the API exposes the field. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/lib/audiobooks/types.ts | 1 + web/src/pages/audiobooks/AudiobookDetail.tsx | 13 +++++++++++++ web/src/pages/audiobooks/components/RelatedRail.tsx | 8 ++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/web/src/lib/audiobooks/types.ts b/web/src/lib/audiobooks/types.ts index 1b2a37c1..b4a03a59 100644 --- a/web/src/lib/audiobooks/types.ts +++ b/web/src/lib/audiobooks/types.ts @@ -63,6 +63,7 @@ export interface AudiobookDetailResponse { narrator?: string; files: AudiobookFile[]; progress?: AudiobookProgress; + similar_audiobooks?: AudiobookRelatedItem[]; also_by_author?: AudiobookRelatedItem[]; in_series?: { name?: string; diff --git a/web/src/pages/audiobooks/AudiobookDetail.tsx b/web/src/pages/audiobooks/AudiobookDetail.tsx index 19cbbf96..c3973051 100644 --- a/web/src/pages/audiobooks/AudiobookDetail.tsx +++ b/web/src/pages/audiobooks/AudiobookDetail.tsx @@ -233,6 +233,19 @@ export default function AudiobookDetail() { /> {narrator && } + {data.similar_audiobooks && data.similar_audiobooks.length > 0 && ( + ({ + content_id: it.content_id, + title: it.title, + poster_url: it.poster_url, + subtitle: it.year ? String(it.year) : undefined, + }))} + /> + )} + {data.also_by_author && data.also_by_author.length > 0 && ( -

{heading}

+
+

{heading}

+ {subtitle &&

{subtitle}

} +
{items.map((item) => (