feat: display series metadata (authors, description) from Stripstream API
All checks were successful
Build, Push & Deploy / deploy (push) Successful in 4m14s

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 21:11:55 +01:00
parent 11da2335cd
commit 32757a8723
6 changed files with 68 additions and 6 deletions

View File

@@ -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 (
<div className="relative min-h-[300px] md:h-[300px] w-screen -ml-[calc((100vw-100%)/2)] overflow-hidden">
@@ -128,6 +131,12 @@ export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: Serie
{/* Informations */}
<div className="flex-1 text-white space-y-2 text-center md:text-left">
<h1 className="text-2xl md:text-3xl font-bold">{series.name}</h1>
{authorsText && (
<p className="text-white/70 text-sm flex items-center gap-1 justify-center md:justify-start">
<User className="h-3.5 w-3.5 flex-shrink-0" />
{authorsText}
</p>
)}
{series.summary && (
<p className="text-white/80 line-clamp-3 text-sm md:text-base">
{series.summary}
@@ -158,6 +167,7 @@ export const SeriesHeader = ({ series, refreshSeries, initialIsFavorite }: Serie
</div>
</div>
</div>
</div>
);
};

View File

@@ -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 ?? [],

View File

@@ -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: [],

View File

@@ -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<NormalizedSeries> {
try {
const metadata = await this.client.fetch<StripstreamSeriesMetadata>(
`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<StripstreamBookDetails>(`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<NormalizedBooksPage> {
const limit = filter.limit ?? 24;
const params: Record<string, string | undefined> = { limit: String(limit) };

View File

@@ -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 }>;

View File

@@ -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[];