Files
stripstream/src/lib/services/series.service.ts

160 lines
4.8 KiB
TypeScript

import { BaseApiService } from "./base-api.service";
import type { LibraryResponse } from "@/types/library";
import type { KomgaBook, KomgaSeries } from "@/types/komga";
import { BookService } from "./book.service";
import { ImageService } from "./image.service";
import { PreferencesService } from "./preferences.service";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { UserPreferences } from "@/types/preferences";
import logger from "@/lib/logger";
type KomgaCondition = Record<string, unknown>;
export class SeriesService extends BaseApiService {
private static readonly CACHE_TTL = 120; // 2 minutes
static async getSeries(seriesId: string): Promise<KomgaSeries> {
try {
return this.fetchFromApi<KomgaSeries>(
{ path: `series/${seriesId}` },
{},
{ revalidate: this.CACHE_TTL }
);
} catch (error) {
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
static async getSeriesBooks(
seriesId: string,
page: number = 0,
size: number = 24,
unreadOnly: boolean = false
): Promise<LibraryResponse<KomgaBook>> {
try {
const headers = { "Content-Type": "application/json" };
// Construction du body de recherche pour Komga
let condition: KomgaCondition;
if (unreadOnly) {
// Utiliser allOf pour combiner seriesId avec anyOf pour UNREAD ou IN_PROGRESS
condition = {
allOf: [
{
seriesId: {
operator: "is",
value: seriesId,
},
},
{
anyOf: [
{
readStatus: {
operator: "is",
value: "UNREAD",
},
},
{
readStatus: {
operator: "is",
value: "IN_PROGRESS",
},
},
],
},
],
};
} else {
condition = {
seriesId: {
operator: "is",
value: seriesId,
},
};
}
const searchBody = { condition };
const params: Record<string, string | string[]> = {
page: String(page),
size: String(size),
sort: "number,asc",
};
const response = await this.fetchFromApi<LibraryResponse<KomgaBook>>(
{ path: "books/list", params },
headers,
{
method: "POST",
body: JSON.stringify(searchBody),
revalidate: this.CACHE_TTL,
}
);
// Filtrer uniquement les livres supprimés côté client (léger)
const filteredContent = response.content.filter((book: KomgaBook) => !book.deleted);
return {
...response,
content: filteredContent,
numberOfElements: filteredContent.length,
};
} catch (error) {
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
static async getFirstBook(seriesId: string): Promise<string> {
try {
const data: LibraryResponse<KomgaBook> = await this.fetchFromApi<LibraryResponse<KomgaBook>>({
path: `series/${seriesId}/books`,
params: { page: "0", size: "1" },
});
if (!data.content || data.content.length === 0) {
throw new AppError(ERROR_CODES.SERIES.NO_BOOKS_FOUND);
}
return data.content[0].id;
} catch (error) {
logger.error({ err: error }, "Erreur lors de la récupération du premier livre");
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
static async getCover(seriesId: string): Promise<Response> {
try {
// Récupérer les préférences de l'utilisateur
const preferences: UserPreferences = await PreferencesService.getPreferences();
// Si l'utilisateur préfère les vignettes, utiliser la miniature (streaming)
if (preferences.showThumbnails) {
return ImageService.streamImage(`series/${seriesId}/thumbnail`);
}
// Sinon, récupérer la première page (streaming)
const firstBookId = await this.getFirstBook(seriesId);
return BookService.getPage(firstBookId, 1);
} catch (error) {
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
static getCoverUrl(seriesId: string): string {
return `/api/komga/images/series/${seriesId}/thumbnail`;
}
static async getMultipleSeries(seriesIds: string[]): Promise<KomgaSeries[]> {
try {
const seriesPromises: Promise<KomgaSeries>[] = seriesIds.map((id: string) =>
this.getSeries(id)
);
const series: KomgaSeries[] = await Promise.all(seriesPromises);
return series.filter(Boolean);
} catch (error) {
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
}
}