import { request, type Page } from './client'; export type Chapter = { id: string; manga_id: string; number: number; title: string | null; page_count: number; created_at: string; }; export type ChaptersPage = { items: Chapter[]; page: Page; }; export type ListOptions = { limit?: number; offset?: number; }; export async function listChapters( mangaId: string, opts: ListOptions = {} ): Promise { const params = new URLSearchParams(); if (opts.limit != null) params.set('limit', String(opts.limit)); if (opts.offset != null) params.set('offset', String(opts.offset)); const qs = params.toString(); return request( `/v1/mangas/${encodeURIComponent(mangaId)}/chapters${qs ? `?${qs}` : ''}` ); } export async function getChapter(mangaId: string, chapterId: string): Promise { return request( `/v1/mangas/${encodeURIComponent(mangaId)}/chapters/${encodeURIComponent(chapterId)}` ); } export type ChapterPage = { id: string; chapter_id: string; page_number: number; storage_key: string; content_type: string; }; export async function getChapterPages( mangaId: string, chapterId: string ): Promise { const r = await request<{ pages: ChapterPage[] }>( `/v1/mangas/${encodeURIComponent(mangaId)}/chapters/${encodeURIComponent(chapterId)}/pages` ); return r.pages; } export type NewChapter = { number: number; title?: string | null; }; /** * `POST /api/v1/mangas/:id/chapters` is multipart: a `metadata` part * (JSON) plus one or more ordered `page` parts. Each page file is * renamed to `page-NNN.` before submission so the user's * original filenames (often personally-identifying or just messy: * `IMG_2837.HEIC`, `~/scans/full chapter pack/`) don't end up in * request bodies or server logs. The bytes are unchanged — the * backend still sniffs the MIME from magic bytes and stores under * its own `{nnnn}.{ext}` scheme. */ export async function createChapter( mangaId: string, metadata: NewChapter, pages: File[] ): Promise { const form = new FormData(); form.append( 'metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }) ); pages.forEach((file, i) => { const ext = extensionFor(file); const renamed = new File( [file], `page-${String(i + 1).padStart(3, '0')}${ext}`, { type: file.type } ); form.append('page', renamed); }); return request( `/v1/mangas/${encodeURIComponent(mangaId)}/chapters`, { method: 'POST', body: form } ); } /** * Pick a sensible extension for the renamed multipart part. Prefer * the original filename's extension when present (jpg/jpeg/png/webp/ * gif/avif), otherwise derive from the MIME type. Falls back to an * empty string so the renamed file is just `page-001` — the * server sniffs bytes anyway. */ function extensionFor(file: File): string { const dot = file.name.lastIndexOf('.'); if (dot > 0) { const ext = file.name.slice(dot).toLowerCase(); if (/^\.(jpe?g|png|webp|gif|avif)$/.test(ext)) return ext; } const fromMime: Record = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp', 'image/gif': '.gif', 'image/avif': '.avif' }; return fromMime[file.type] ?? ''; }