fix: update stripstream provider to use series_id UUIDs
All checks were successful
Build, Push & Deploy / deploy (push) Successful in 6m12s
All checks were successful
Build, Push & Deploy / deploy (push) Successful in 6m12s
The stripstreamlib API now identifies series by UUID (series_id) instead of name. Updates the provider to use series_id for series lookups, metadata fetching, and book filtering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,7 @@ export class StripstreamAdapter {
|
|||||||
|
|
||||||
static toNormalizedSeries(series: StripstreamSeriesItem): NormalizedSeries {
|
static toNormalizedSeries(series: StripstreamSeriesItem): NormalizedSeries {
|
||||||
return {
|
return {
|
||||||
id: series.first_book_id,
|
id: series.series_id,
|
||||||
name: series.name,
|
name: series.name,
|
||||||
bookCount: series.book_count,
|
bookCount: series.book_count,
|
||||||
booksReadCount: series.books_read_count,
|
booksReadCount: series.books_read_count,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
StripstreamReadingProgressResponse,
|
StripstreamReadingProgressResponse,
|
||||||
StripstreamSearchResponse,
|
StripstreamSearchResponse,
|
||||||
StripstreamSeriesMetadata,
|
StripstreamSeriesMetadata,
|
||||||
|
StripstreamSeriesLookup,
|
||||||
} from "@/types/stripstream";
|
} from "@/types/stripstream";
|
||||||
import { HOME_CACHE_TAG, LIBRARY_SERIES_CACHE_TAG, SERIES_BOOKS_CACHE_TAG } from "@/constants/cacheConstants";
|
import { HOME_CACHE_TAG, LIBRARY_SERIES_CACHE_TAG, SERIES_BOOKS_CACHE_TAG } from "@/constants/cacheConstants";
|
||||||
|
|
||||||
@@ -78,92 +79,90 @@ export class StripstreamProvider implements IMediaProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSeriesById(seriesId: string): Promise<NormalizedSeries | null> {
|
async getSeriesById(seriesId: string): Promise<NormalizedSeries | null> {
|
||||||
// seriesId can be either a first_book_id (from series cards) or a series name (from book.seriesId).
|
// seriesId can be a real series UUID (from series listing) or a first_book_id (from search results).
|
||||||
// Try first_book_id first; fall back to series name search.
|
// Try as series_id first via the global series list.
|
||||||
|
const series = await this.findSeriesInList(seriesId);
|
||||||
|
if (series) {
|
||||||
|
const normalized = StripstreamAdapter.toNormalizedSeries(series);
|
||||||
|
return this.enrichSeriesWithMetadata(normalized, series.library_id, series.series_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try as first_book_id (search results still use first_book_id)
|
||||||
try {
|
try {
|
||||||
const book = await this.client.fetch<StripstreamBookDetails>(`books/${seriesId}`, undefined, {
|
const book = await this.client.fetch<StripstreamBookDetails>(`books/${seriesId}`, undefined, {
|
||||||
revalidate: CACHE_TTL_MED,
|
revalidate: CACHE_TTL_MED,
|
||||||
});
|
});
|
||||||
if (!book.series) return null;
|
if (!book.series) return null;
|
||||||
|
return this.resolveSeriesByName(book.series, book.library_id);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try to find series in library to get real book counts
|
private async findSeriesInList(seriesId: string): Promise<StripstreamSeriesItem | null> {
|
||||||
const seriesInfo = await this.findSeriesByName(book.series, book.library_id);
|
try {
|
||||||
if (seriesInfo) return seriesInfo;
|
let page = 1;
|
||||||
|
const limit = 200;
|
||||||
|
while (true) {
|
||||||
|
const response = await this.client.fetch<StripstreamSeriesPage>(
|
||||||
|
"series",
|
||||||
|
{ limit: String(limit), page: String(page) },
|
||||||
|
{ revalidate: CACHE_TTL_MED }
|
||||||
|
);
|
||||||
|
const match = response.items.find((s) => s.series_id === seriesId);
|
||||||
|
if (match) return match;
|
||||||
|
if (response.items.length < limit) return null;
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveSeriesByName(name: string, libraryId: string): Promise<NormalizedSeries | null> {
|
||||||
|
try {
|
||||||
|
// Use by-name endpoint to get the series UUID
|
||||||
|
const lookup = await this.client.fetch<StripstreamSeriesLookup>(
|
||||||
|
`libraries/${libraryId}/series/by-name/${encodeURIComponent(name)}`,
|
||||||
|
undefined,
|
||||||
|
{ revalidate: CACHE_TTL_MED }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the full series item for book counts etc.
|
||||||
|
const series = await this.findSeriesInList(lookup.id);
|
||||||
|
if (series) {
|
||||||
|
const normalized = StripstreamAdapter.toNormalizedSeries(series);
|
||||||
|
return this.enrichSeriesWithMetadata(normalized, series.library_id, series.series_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: construct from lookup + metadata
|
||||||
const fallback: NormalizedSeries = {
|
const fallback: NormalizedSeries = {
|
||||||
id: seriesId,
|
id: lookup.id,
|
||||||
name: book.series,
|
name: lookup.name,
|
||||||
bookCount: 0,
|
bookCount: 0,
|
||||||
booksReadCount: 0,
|
booksReadCount: 0,
|
||||||
thumbnailUrl: `/api/stripstream/images/books/${seriesId}/thumbnail`,
|
thumbnailUrl: "",
|
||||||
libraryId: book.library_id,
|
libraryId: lookup.library_id,
|
||||||
summary: null,
|
summary: null,
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
};
|
};
|
||||||
return this.enrichSeriesWithMetadata(fallback, book.library_id, book.series);
|
return this.enrichSeriesWithMetadata(fallback, lookup.library_id, lookup.id);
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back: treat seriesId as a series name, find its first book
|
return null;
|
||||||
try {
|
|
||||||
const page = await this.client.fetch<StripstreamBooksPage>(
|
|
||||||
"books",
|
|
||||||
{ series: seriesId, limit: "1" },
|
|
||||||
{ revalidate: CACHE_TTL_MED }
|
|
||||||
);
|
|
||||||
if (!page.items.length) return null;
|
|
||||||
const firstBook = page.items[0];
|
|
||||||
|
|
||||||
const seriesInfo = await this.findSeriesByName(seriesId, firstBook.library_id);
|
|
||||||
if (seriesInfo) return seriesInfo;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findSeriesByName(seriesName: string, libraryId: string): Promise<NormalizedSeries | null> {
|
|
||||||
try {
|
|
||||||
const seriesPage = await this.client.fetch<StripstreamSeriesPage>(
|
|
||||||
`libraries/${libraryId}/series`,
|
|
||||||
{ q: seriesName, limit: "10" },
|
|
||||||
{ revalidate: CACHE_TTL_MED }
|
|
||||||
);
|
|
||||||
const match = seriesPage.items.find((s) => s.name === seriesName);
|
|
||||||
if (match) {
|
|
||||||
const normalized = StripstreamAdapter.toNormalizedSeries(match);
|
|
||||||
return this.enrichSeriesWithMetadata(normalized, libraryId, seriesName);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async enrichSeriesWithMetadata(
|
private async enrichSeriesWithMetadata(
|
||||||
series: NormalizedSeries,
|
series: NormalizedSeries,
|
||||||
libraryId: string,
|
libraryId: string,
|
||||||
seriesName: string
|
seriesId: string
|
||||||
): Promise<NormalizedSeries> {
|
): Promise<NormalizedSeries> {
|
||||||
try {
|
try {
|
||||||
const metadata = await this.client.fetch<StripstreamSeriesMetadata>(
|
const metadata = await this.client.fetch<StripstreamSeriesMetadata>(
|
||||||
`libraries/${libraryId}/series/${encodeURIComponent(seriesName)}/metadata`,
|
`libraries/${libraryId}/series/${seriesId}/metadata`,
|
||||||
undefined,
|
undefined,
|
||||||
{ revalidate: CACHE_TTL_MED }
|
{ revalidate: CACHE_TTL_MED }
|
||||||
);
|
);
|
||||||
@@ -172,18 +171,28 @@ export class StripstreamProvider implements IMediaProvider {
|
|||||||
summary: metadata.description ?? null,
|
summary: metadata.description ?? null,
|
||||||
authors: metadata.authors.map((name) => ({ name, role: "writer" })),
|
authors: metadata.authors.map((name) => ({ name, role: "writer" })),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch {
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveSeriesInfo(seriesId: string): Promise<{ libraryId: string; seriesName: string } | null> {
|
private async resolveSeriesId(seriesIdOrBookId: string): Promise<string | null> {
|
||||||
|
// If it's already a series_id, verify it exists
|
||||||
|
const series = await this.findSeriesInList(seriesIdOrBookId);
|
||||||
|
if (series) return series.series_id;
|
||||||
|
|
||||||
|
// Fallback: try as first_book_id — resolve to series_id via by-name
|
||||||
try {
|
try {
|
||||||
const book = await this.client.fetch<StripstreamBookDetails>(`books/${seriesId}`, undefined, {
|
const book = await this.client.fetch<StripstreamBookDetails>(`books/${seriesIdOrBookId}`, undefined, {
|
||||||
revalidate: CACHE_TTL_MED,
|
revalidate: CACHE_TTL_MED,
|
||||||
});
|
});
|
||||||
if (!book.series) return null;
|
if (!book.series) return null;
|
||||||
return { libraryId: book.library_id, seriesName: book.series };
|
const lookup = await this.client.fetch<StripstreamSeriesLookup>(
|
||||||
|
`libraries/${book.library_id}/series/by-name/${encodeURIComponent(book.series)}`,
|
||||||
|
undefined,
|
||||||
|
{ revalidate: CACHE_TTL_MED }
|
||||||
|
);
|
||||||
|
return lookup.id;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -194,17 +203,9 @@ export class StripstreamProvider implements IMediaProvider {
|
|||||||
const params: Record<string, string | undefined> = { limit: String(limit) };
|
const params: Record<string, string | undefined> = { limit: String(limit) };
|
||||||
|
|
||||||
if (filter.seriesName) {
|
if (filter.seriesName) {
|
||||||
// seriesName is first_book_id for Stripstream — resolve to actual series name
|
// seriesName is a series_id UUID — the books API now expects a series_id
|
||||||
try {
|
const seriesId = await this.resolveSeriesId(filter.seriesName);
|
||||||
const book = await this.client.fetch<StripstreamBookDetails>(
|
params.series = seriesId ?? filter.seriesName;
|
||||||
`books/${filter.seriesName}`,
|
|
||||||
undefined,
|
|
||||||
{ revalidate: CACHE_TTL_MED }
|
|
||||||
);
|
|
||||||
params.series = book.series ?? filter.seriesName;
|
|
||||||
} catch {
|
|
||||||
params.series = filter.seriesName;
|
|
||||||
}
|
|
||||||
} else if (filter.libraryId) {
|
} else if (filter.libraryId) {
|
||||||
params.library_id = filter.libraryId;
|
params.library_id = filter.libraryId;
|
||||||
}
|
}
|
||||||
@@ -242,8 +243,15 @@ export class StripstreamProvider implements IMediaProvider {
|
|||||||
});
|
});
|
||||||
if (!book.series || book.volume == null) return null;
|
if (!book.series || book.volume == null) return null;
|
||||||
|
|
||||||
|
// Resolve series name to series_id for the books API
|
||||||
|
const lookup = await this.client.fetch<StripstreamSeriesLookup>(
|
||||||
|
`libraries/${book.library_id}/series/by-name/${encodeURIComponent(book.series)}`,
|
||||||
|
undefined,
|
||||||
|
{ revalidate: CACHE_TTL_MED }
|
||||||
|
);
|
||||||
|
|
||||||
const response = await this.client.fetch<StripstreamBooksPage>("books", {
|
const response = await this.client.fetch<StripstreamBooksPage>("books", {
|
||||||
series: book.series,
|
series: lookup.id,
|
||||||
limit: "200",
|
limit: "200",
|
||||||
}, { revalidate: CACHE_TTL_SHORT });
|
}, { revalidate: CACHE_TTL_SHORT });
|
||||||
|
|
||||||
@@ -347,14 +355,30 @@ export class StripstreamProvider implements IMediaProvider {
|
|||||||
limit: String(limit),
|
limit: String(limit),
|
||||||
}, { revalidate: CACHE_TTL_SHORT });
|
}, { revalidate: CACHE_TTL_SHORT });
|
||||||
|
|
||||||
const seriesResults: NormalizedSearchResult[] = response.series_hits.map((s) => ({
|
// Resolve series_id for each hit via by-name lookup
|
||||||
id: s.first_book_id,
|
const seriesResults: NormalizedSearchResult[] = await Promise.all(
|
||||||
title: s.name,
|
response.series_hits.map(async (s) => {
|
||||||
href: `/series/${s.first_book_id}`,
|
let id = s.first_book_id;
|
||||||
coverUrl: `/api/stripstream/images/books/${s.first_book_id}/thumbnail`,
|
try {
|
||||||
type: "series" as const,
|
const lookup = await this.client.fetch<StripstreamSeriesLookup>(
|
||||||
bookCount: s.book_count,
|
`libraries/${s.library_id}/series/by-name/${encodeURIComponent(s.name)}`,
|
||||||
}));
|
undefined,
|
||||||
|
{ revalidate: CACHE_TTL_MED }
|
||||||
|
);
|
||||||
|
id = lookup.id;
|
||||||
|
} catch {
|
||||||
|
// fallback to first_book_id
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title: s.name,
|
||||||
|
href: `/series/${id}`,
|
||||||
|
coverUrl: `/api/stripstream/images/books/${s.first_book_id}/thumbnail`,
|
||||||
|
type: "series" as const,
|
||||||
|
bookCount: s.book_count,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const bookResults: NormalizedSearchResult[] = response.hits.map((hit) => ({
|
const bookResults: NormalizedSearchResult[] = response.hits.map((hit) => ({
|
||||||
id: hit.id,
|
id: hit.id,
|
||||||
|
|||||||
@@ -44,12 +44,23 @@ export interface StripstreamBooksPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StripstreamSeriesItem {
|
export interface StripstreamSeriesItem {
|
||||||
|
series_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
book_count: number;
|
book_count: number;
|
||||||
books_read_count: number;
|
books_read_count: number;
|
||||||
first_book_id: string;
|
first_book_id: string;
|
||||||
library_id: string;
|
library_id: string;
|
||||||
missing_count?: number | null;
|
missing_count?: number | null;
|
||||||
|
anilist_id?: number | null;
|
||||||
|
anilist_url?: string | null;
|
||||||
|
metadata_provider?: string | null;
|
||||||
|
series_status?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StripstreamSeriesLookup {
|
||||||
|
id: string;
|
||||||
|
library_id: string;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StripstreamSeriesPage {
|
export interface StripstreamSeriesPage {
|
||||||
|
|||||||
Reference in New Issue
Block a user