From 32757a8723c2f6263f6d20ad56aff6273fc205a4 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Mon, 16 Mar 2026 21:11:55 +0100 Subject: [PATCH] feat: display series metadata (authors, description) from Stripstream API Fetch metadata from GET /libraries/{id}/series/{name}/metadata and display authors with icon and description in the series header. Co-Authored-By: Claude Opus 4.6 --- src/components/series/SeriesHeader.tsx | 12 ++++- src/lib/providers/komga/komga.adapter.ts | 1 + .../stripstream/stripstream.adapter.ts | 1 + .../stripstream/stripstream.provider.ts | 49 +++++++++++++++++-- src/lib/providers/types.ts | 1 + src/types/stripstream.ts | 10 ++++ 6 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/components/series/SeriesHeader.tsx b/src/components/series/SeriesHeader.tsx index 7fe481a..8aa3636 100644 --- a/src/components/series/SeriesHeader.tsx +++ b/src/components/series/SeriesHeader.tsx @@ -1,6 +1,6 @@ "use client"; -import { Book, BookOpen, BookMarked, Star, StarOff } from "lucide-react"; +import { Book, BookOpen, BookMarked, Star, StarOff, User } from "lucide-react"; import type { NormalizedSeries } from "@/lib/providers/types"; import { useState, useEffect } from "react"; import { useToast } from "@/components/ui/use-toast"; @@ -100,6 +100,9 @@ export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: Serie }; const statusInfo = getReadingStatusInfo(); + const authorsText = series.authors?.length + ? series.authors.map((a) => a.name).join(", ") + : null; return (
@@ -128,6 +131,12 @@ export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: Serie {/* Informations */}

{series.name}

+ {authorsText && ( +

+ + {authorsText} +

+ )} {series.summary && (

{series.summary} @@ -158,6 +167,7 @@ export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: Serie

+ ); }; diff --git a/src/lib/providers/komga/komga.adapter.ts b/src/lib/providers/komga/komga.adapter.ts index 71d1075..46d55c5 100644 --- a/src/lib/providers/komga/komga.adapter.ts +++ b/src/lib/providers/komga/komga.adapter.ts @@ -37,6 +37,7 @@ export class KomgaAdapter { bookCount: series.booksCount, booksReadCount: series.booksReadCount, thumbnailUrl: `/api/komga/images/series/${series.id}/thumbnail`, + libraryId: series.libraryId, summary: series.metadata?.summary ?? null, authors: series.booksMetadata?.authors ?? [], genres: series.metadata?.genres ?? [], diff --git a/src/lib/providers/stripstream/stripstream.adapter.ts b/src/lib/providers/stripstream/stripstream.adapter.ts index 875e718..679c2a1 100644 --- a/src/lib/providers/stripstream/stripstream.adapter.ts +++ b/src/lib/providers/stripstream/stripstream.adapter.ts @@ -73,6 +73,7 @@ export class StripstreamAdapter { bookCount: series.book_count, booksReadCount: series.books_read_count, thumbnailUrl: `/api/stripstream/images/books/${series.first_book_id}/thumbnail`, + libraryId: series.library_id, summary: null, authors: [], genres: [], diff --git a/src/lib/providers/stripstream/stripstream.provider.ts b/src/lib/providers/stripstream/stripstream.provider.ts index 73e6e9f..714da9b 100644 --- a/src/lib/providers/stripstream/stripstream.provider.ts +++ b/src/lib/providers/stripstream/stripstream.provider.ts @@ -12,8 +12,6 @@ import type { import type { HomeData } from "@/types/home"; import { StripstreamClient } from "./stripstream.client"; import { StripstreamAdapter } from "./stripstream.adapter"; -import { ERROR_CODES } from "@/constants/errorCodes"; -import { AppError } from "@/utils/errors"; import type { StripstreamLibraryResponse, StripstreamBooksPage, @@ -23,6 +21,7 @@ import type { StripstreamBookDetails, StripstreamReadingProgressResponse, StripstreamSearchResponse, + StripstreamSeriesMetadata, } from "@/types/stripstream"; import { HOME_CACHE_TAG, LIBRARY_SERIES_CACHE_TAG, SERIES_BOOKS_CACHE_TAG } from "@/constants/cacheConstants"; @@ -91,18 +90,20 @@ export class StripstreamProvider implements IMediaProvider { const seriesInfo = await this.findSeriesByName(book.series, book.library_id); if (seriesInfo) return seriesInfo; - return { + const fallback: NormalizedSeries = { id: seriesId, name: book.series, bookCount: 0, booksReadCount: 0, thumbnailUrl: `/api/stripstream/images/books/${seriesId}/thumbnail`, + libraryId: book.library_id, summary: null, authors: [], genres: [], tags: [], createdAt: null, }; + return this.enrichSeriesWithMetadata(fallback, book.library_id, book.series); } catch { // Fall back: treat seriesId as a series name, find its first book try { @@ -117,18 +118,20 @@ export class StripstreamProvider implements IMediaProvider { const seriesInfo = await this.findSeriesByName(seriesId, firstBook.library_id); if (seriesInfo) return seriesInfo; - return { + const fallback: NormalizedSeries = { id: firstBook.id, name: seriesId, bookCount: 0, booksReadCount: 0, thumbnailUrl: `/api/stripstream/images/books/${firstBook.id}/thumbnail`, + libraryId: firstBook.library_id, summary: null, authors: [], genres: [], tags: [], createdAt: null, }; + return this.enrichSeriesWithMetadata(fallback, firstBook.library_id, seriesId); } catch { return null; } @@ -143,13 +146,49 @@ export class StripstreamProvider implements IMediaProvider { { revalidate: CACHE_TTL_MED } ); const match = seriesPage.items.find((s) => s.name === seriesName); - if (match) return StripstreamAdapter.toNormalizedSeries(match); + if (match) { + const normalized = StripstreamAdapter.toNormalizedSeries(match); + return this.enrichSeriesWithMetadata(normalized, libraryId, seriesName); + } } catch { // ignore } return null; } + private async enrichSeriesWithMetadata( + series: NormalizedSeries, + libraryId: string, + seriesName: string + ): Promise { + try { + const metadata = await this.client.fetch( + `libraries/${libraryId}/series/${encodeURIComponent(seriesName)}/metadata`, + undefined, + { revalidate: CACHE_TTL_MED } + ); + return { + ...series, + summary: metadata.description ?? null, + authors: metadata.authors.map((name) => ({ name, role: "writer" })), + }; + } catch (error) { + return series; + } + } + + private async resolveSeriesInfo(seriesId: string): Promise<{ libraryId: string; seriesName: string } | null> { + try { + const book = await this.client.fetch(`books/${seriesId}`, undefined, { + revalidate: CACHE_TTL_MED, + }); + if (!book.series) return null; + return { libraryId: book.library_id, seriesName: book.series }; + } catch { + return null; + } + } + async getBooks(filter: BookListFilter): Promise { const limit = filter.limit ?? 24; const params: Record = { limit: String(limit) }; diff --git a/src/lib/providers/types.ts b/src/lib/providers/types.ts index ba5aaee..d581315 100644 --- a/src/lib/providers/types.ts +++ b/src/lib/providers/types.ts @@ -18,6 +18,7 @@ export interface NormalizedSeries { bookCount: number; booksReadCount: number; thumbnailUrl: string; + libraryId?: string; // Optional metadata (Komga-rich, Stripstream-sparse) summary?: string | null; authors?: Array<{ name: string; role: string }>; diff --git a/src/types/stripstream.ts b/src/types/stripstream.ts index 5e7a379..03437c5 100644 --- a/src/types/stripstream.ts +++ b/src/types/stripstream.ts @@ -48,6 +48,7 @@ export interface StripstreamSeriesItem { book_count: number; books_read_count: number; first_book_id: string; + library_id: string; } export interface StripstreamSeriesPage { @@ -80,6 +81,15 @@ export interface StripstreamUpdateReadingProgressRequest { current_page?: number | null; } +export interface StripstreamSeriesMetadata { + authors: string[]; + publishers: string[]; + description?: string | null; + start_year?: number | null; + book_author?: string | null; + book_language?: string | null; +} + export interface StripstreamSearchResponse { hits: StripstreamSearchHit[]; series_hits: StripstreamSeriesHit[];