import IDObject from '../internal/IDObject'; import { fetchMD, fetchMDByArrayParam, fetchMDData, fetchMDDataWithBody, fetchMDSearch, postToMDNetwork, } from '../util/Network'; import Relationship from '../internal/Relationship'; import type { AtHome, ChapterAttributesSchema, ChapterEditSchema, ChapterListSchema, ChapterResponseSchema, ChapterSchema, GetChapterParamsSchema, ResponseSchema, Statistics, } from '../types/schema'; import type Manga from './Manga'; import type User from './User'; import type Group from './Group'; import type { DeepRequire, Merge } from '../types/helpers'; export type ChapterSearchParams = Partial< Merge >; type AtHomeServerResponse = Required; type OtherChapterAttributes = Omit; type ChapterStatsResponse = DeepRequire; type ChapterStats = ChapterStatsResponse['statistics'][string]; export default class Chapter extends IDObject implements OtherChapterAttributes { /** * The MangaDex UUID of this chapter */ id: string; /** * The title of this chapter */ title: string | null; /** * The manga volume this chapter belongs to */ volume: string | null; /** * The chapter number for this chapter */ chapter: string | null; /** * The number of pages in this chapter */ pages: number; /** * The language of this chapter */ translatedLanguage: string; /** * Relationship to the user who uploaded this chapter */ uploader: Relationship; /** * Url to this chapter if it's an external chapter */ externalUrl: string | null; /** * The version of this chapter (incremented by updating chapter data) */ version: number; /** * When this chapter was created */ createdAt: Date; /** * When this chapter was last updated */ updatedAt: Date; /** * When this chapter was originally published */ publishAt: Date; /** * When was / when will this chapter be readable? */ readableAt: Date; /** * Is this chapter an external chapter? If it is, this chapter will have an externalUrl */ isExternal: boolean; /** * A relationship to the manga this chapter belongs to */ manga: Relationship; /** * Array of relationships to the groups that translated this chapter */ groups: Relationship[]; /** * Is this chapter unavailable? */ isUnavailable: boolean; constructor(schem: ChapterSchema) { super(); this.id = schem.id; this.title = schem.attributes.title; this.volume = schem.attributes.volume; this.chapter = schem.attributes.chapter; this.pages = schem.attributes.pages; this.translatedLanguage = schem.attributes.translatedLanguage; this.uploader = Relationship.convertType('user', schem.relationships).pop()!; this.externalUrl = schem.attributes.externalUrl; this.version = schem.attributes.version; this.createdAt = new Date(schem.attributes.createdAt); this.publishAt = new Date(schem.attributes.publishAt); this.updatedAt = new Date(schem.attributes.updatedAt); this.readableAt = new Date(schem.attributes.readableAt); this.isExternal = schem.attributes.externalUrl !== null; this.manga = Relationship.convertType('manga', schem.relationships).pop()!; this.groups = Relationship.convertType('scanlation_group', schem.relationships); this.isUnavailable = schem.attributes.isUnavailable; } /** * Retrieves a chapter object by its UUID */ static async get(id: string, expandedTypes?: ChapterSearchParams['includes']): Promise { return new Chapter(await fetchMDData(`/chapter/${id}`, { includes: expandedTypes })); } /** * Retrieves an array of chapters by an array of their ids */ static async getMultiple(ids: string[]): Promise { const res = await fetchMDByArrayParam(`/chapter`, ids); return res.map((c) => new Chapter(c)); } /** * Retrieves a list of chapters according to the specified search parameters */ static async search(query?: ChapterSearchParams): Promise { const res = await fetchMDSearch('/chapter', query); return res.map((c) => new Chapter(c)); } /** * Performs a search for a chapter and returns the first one found. If no results are * found, null is returned */ static async getByQuery(query?: ChapterSearchParams): Promise { const res = await this.search(query); return res[0] ?? null; } /** * Update this chapter's information */ async update(data: Omit): Promise { return new Chapter( await fetchMDDataWithBody( `/chapter/${this.id}`, { ...data, version: this.version + 1, }, undefined, 'PUT', ), ); } /** * Delete this chapter */ static async delete(id: string) { await fetchMD(`/chapter/${id}`, undefined, { method: 'DELETE' }); } /** * Delete a chapter by its UUID */ async delete() { await Chapter.delete(this.id); } /** * Returns an array of image URLs for this chapter's pages. Once an image is requested, * if the host is from MangaDex(at)Home, please report if it succeeds or fails by using {@link reportPageURL}. * @param saver - If true, the URLs will be for the compressed data-saver images (if available). * @param forcePort - If true, the URLs will be forced to be on port 443. */ async getReadablePages(saver = false, forcePort = false): Promise { if (this.isExternal) throw new Error('Cannot get readable pages for an external chapter.'); const res = await fetchMD(`/at-home/server/${this.id}`, { forcePort443: forcePort, }); // Get the list of image files depending on if data saver images are preferred const files = (saver ? res.chapter.dataSaver ?? res.chapter.data : res.chapter.data) ?? []; // Build image urls according to https://api.mangadex.org/docs/retrieving-chapter/ return files.map((file) => `${res.baseUrl}/${saver ? 'data-saver' : 'data'}/${res.chapter.hash}/${file}`); } /** * Sends a report to MangaDex about the success/failure of a MangaDex(at)Home server. * Read more information: {@link https://api.mangadex.org/docs/04-chapter/retrieving-chapter/#mangadexhome-load-successes-failures-and-retries} */ static async reportPageURL(report: { url: string; success: boolean; bytes: number; duration: number; cached: boolean; }): Promise { await postToMDNetwork('/report', report); } /** * Gets the statistics about a list of chapters */ static async getStatistics(ids: string[] | Chapter[]): Promise> { const res = await fetchMD(`/statistics/chapter`, { chapter: ids }); return res.statistics; } /** * Gets the statistics about this chapter */ async getStatistics(): Promise { const res = await Chapter.getStatistics([this.id]); return res[this.id]; } }