import { Controller, Get, Headers, Param, Query, Res, UseGuards } from "@nestjs/common"; import { FastifyReply } from "fastify"; import { lookup } from "mime-types"; import { BookQueryDto, BookQuerySchema } from "@readabook/shared"; import { AuthGuard } from "../auth/auth.guard.js"; import { ZodValidationPipe } from "../common/zod-validation.pipe.js"; import { BooksService } from "./books.service.js"; @Controller("books") @UseGuards(AuthGuard) export class BooksController { constructor(private readonly books: BooksService) {} @Get() list(@Query(new ZodValidationPipe(BookQuerySchema)) query: BookQueryDto) { return this.books.list(query); } @Get("search") search(@Query(new ZodValidationPipe(BookQuerySchema)) query: BookQueryDto) { return query.q ? this.books.search(query.q, query.limit, query.offset) : []; } @Get(":id") get(@Param("id") id: string) { return this.books.get(Number(id)); } @Get(":id/pages") pages(@Param("id") id: string) { return this.books.listComicPages(Number(id)); } @Get(":id/pages/:page") async page(@Param("id") id: string, @Param("page") page: string, @Res() reply: FastifyReply) { const result = await this.books.readComicPage(Number(id), Number(page)); reply.header("Content-Type", result.contentType); reply.header("Cache-Control", "private, max-age=3600"); return reply.send(result.data); } @Get(":id/file") file(@Param("id") id: string, @Headers("range") range: string | undefined, @Res() reply: FastifyReply) { const { book, stream, contentLength, contentType, end, partial, size, start } = this.books.streamFile(Number(id), range); if (partial) { reply.code(206); reply.header("Content-Range", `bytes ${start}-${end}/${size}`); } reply.header("Accept-Ranges", "bytes"); reply.header("Content-Length", String(contentLength)); reply.header("Content-Type", contentType || lookup(book.filePath) || "application/octet-stream"); reply.header("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(`${book.title}.${book.format}`)}`); reply.header("X-Content-Type-Options", "nosniff"); return reply.send(stream); } @Get(":id/cover") cover(@Param("id") id: string, @Res() reply: FastifyReply) { const { coverPath, stream } = this.books.streamCover(Number(id)); reply.header("Content-Type", lookup(coverPath) || "image/jpeg"); return reply.send(stream); } }