UNPKG

@zennomi/mangadex-full-api

Version:

A MangaDex api based around the official API.

1 lines 152 kB
{"version":3,"sources":["../src/internal/IDObject.ts","../src/internal/Relationship.ts","../src/internal/LocalizedString.ts","../src/util/APIResponseError.ts","../src/util/AuthError.ts","../src/util/Network.ts","../src/shared/Author.ts","../src/shared/Cover.ts","../src/shared/Tag.ts","../src/internal/Links.ts","../src/shared/Chapter.ts","../src/shared/Manga.ts","../src/shared/User.ts","../src/shared/Group.ts","../src/shared/UploadSession.ts","../src/shared/PersonalAuthClient.ts","../src/shared/LegacyAuthClient.ts","../src/shared/List.ts","../src/index.ts"],"sourcesContent":["/**\n * This class represents the most abstract version of a MangaDex object, containing\n * only the ID of the object. This is mostly used for instanceOf checks.\n * @internal\n */\nabstract class IDObject {\n abstract id: string;\n}\n\nexport default IDObject;\n","import { RelationshipSchema } from '../types/schema';\nimport IDObject from '../internal/IDObject';\n\n// eslint-disable-next-line @typescript-eslint/ban-types\ntype GettableClass<T> = Function & { get: (id: string) => Promise<T>; getMultiple?: (ids: string[]) => Promise<T[]> };\n\ntype PartialRelationshipSchema = Pick<RelationshipSchema, 'id' | 'type'> &\n Partial<Pick<RelationshipSchema, 'related' | 'attributes'>>;\n\n/**\n * Represents a relationship from one MangaDex object to another such as a manga, author, etc via its id.\n */\nclass Relationship<T> extends IDObject {\n /**\n * The MangaDex UUID of the object this relationship refers to\n */\n id: string;\n /**\n * What type of object is this a relationship to\n */\n type: string;\n /**\n * If the relationship is between two manga, how are they related?\n */\n related?: RelationshipSchema['related'];\n /**\n * Cached relationships are created by using the 'includes' parameter on search requests or\n * other functions that support it. For every type included in the parameter, relationships of\n * that type will be replaced by the actual object. The object will still be represented\n * by a Relationship object, but the {@link resolve} method will return instantly with the cached data.\n * Each resulting object will have no further relationships of its own.\n */\n cached = false;\n\n private cachedData?: object;\n\n private static typeMap: Record<string, GettableClass<unknown>> = {};\n private static typeMapLocked = false;\n\n constructor(data: PartialRelationshipSchema) {\n super();\n this.id = data.id;\n if (!(data.type in Relationship.typeMap)) throw `Unregistered relationship type: ${data.type}`;\n this.type = data.type;\n this.related = data.related;\n\n // Attempt to create cached object for reference expanded relationships\n if (data.attributes) {\n try {\n const classObj = Relationship.typeMap[this.type];\n // Attempt to simulate a common schema object:\n const schemaObj = {\n attributes: data.attributes,\n id: this.id,\n type: this.type,\n relationships: [],\n };\n this.cachedData = Reflect.construct(classObj, [schemaObj]);\n this.cached = true;\n } catch (err) {\n // console.log('Failed to create cache object');\n // console.error(err);\n }\n }\n }\n\n /**\n * This will automatically fetch the associated object that this relationship refers to.\n * In other words, it wil call Manga.get(id), Chapter.get(id), etc with the information\n * stored in this relationship instance. If this relationship is cached, then the resulting\n * object will not have any relationships of its own.\n */\n async resolve(): Promise<T> {\n if (this.cached) return this.cachedData as T;\n return Relationship.typeMap[this.type].get(this.id) as Promise<T>;\n }\n\n /**\n * This will {@link Relationship.resolve} an array of relationships, returning another array\n * in the same order.\n * @param relationshipArray - An array of relationships of the same type\n */\n static async resolveAll<T>(relationshipArray: Relationship<T>[]): Promise<T[]> {\n if (relationshipArray.length === 0) return [];\n const classObj = Relationship.typeMap[relationshipArray[0].type] as GettableClass<T>;\n if (classObj !== undefined && classObj.getMultiple !== undefined) {\n return classObj.getMultiple(relationshipArray.map((i) => i.id));\n } else {\n return await Promise.all(relationshipArray.map((elem) => elem.resolve()));\n }\n }\n\n /**\n * This will search through a relationship response from MangaDex and convert any\n * relationships of a specific type into relationship objects.\n * @internal\n */\n static convertType<T2>(type: string, arr: RelationshipSchema[]): Relationship<T2>[] {\n return arr.filter((elem) => elem.type === type).map((elem) => new Relationship<T2>(elem));\n }\n\n /**\n * This function is used to resolved circular references, and should only be used in base.ts.\n * Specifically, it pairs a relationship type to its associated class.\n * @internal\n */\n static registerTypes(types: string[], classObj: GettableClass<unknown>) {\n if (Relationship.typeMapLocked) {\n throw Error(`Cannot add types ${types} because the Relationship type map has been locked.`);\n }\n types.forEach((type) => (Relationship.typeMap[type] = classObj));\n }\n\n /**\n * Lock the type map so that no more types can be registered.\n * @internal\n */\n static lockTypeMap() {\n Relationship.typeMapLocked = true;\n }\n\n /**\n * Returns an array of all registered Relationship types\n * @internal\n */\n static getRegisteredTypes() {\n return Object.keys(Relationship.typeMap);\n }\n}\n\nexport default Relationship;\n","import type { LocalizedStringSchema } from '../types/schema';\n\n/**\n * This class represents a map of locales and their associated strings.\n * Each string can be accessed by using the locale as the key (e.g. 'en', 'jp').\n * {@link localString} and {@link setGlobalLocale} can be used to\n * automatically access a preferred locale.\n * @example\n * var locStr;\n * locStr['en']; // English String\n * LocalizedString.setGlobalLocale('jp');\n * locStr.localString; // Japanese String\n */\nclass LocalizedString implements LocalizedStringSchema {\n private static globalLocale = 'en';\n\n [x: string]: string;\n\n constructor(strings: LocalizedStringSchema) {\n for (const locale in strings) {\n this[locale] = strings[locale];\n }\n }\n\n /**\n * The string associated with the current global locale (set with setGlobalLocale()).\n * If the global locale is not available for this string, the English string is returned.\n * If that is also unavailable, the next available locale is returned. If no locales are\n * available, an empty string is returned.\n */\n get localString() {\n if (LocalizedString.globalLocale in this) return this[LocalizedString.globalLocale];\n if ('en' in this) return this['en'];\n // localString is not included in Object.keys(this)\n for (const i of Object.keys(this)) if (typeof this[i] === 'string') return this[i];\n return '';\n }\n\n /**\n * This function sets the default locale used by {@link LocalizedString.localString}.\n */\n static setGlobalLocale(locale: string) {\n if (locale.length < 2 || locale.length > 8) throw Error(`Locale \"${locale}\" has an invalid length`);\n LocalizedString.globalLocale = locale;\n }\n}\n\nexport default LocalizedString;\n","import { ErrorResponseSchema, ErrorSchema } from '../types/schema';\n\nexport default class APIResponseError extends Error {\n constructor(info: ErrorResponseSchema | ErrorSchema[] | string) {\n if (typeof info !== 'string') {\n let errors: ErrorSchema[];\n if ('result' in info) errors = info.errors;\n else errors = info;\n\n const parsedErrors = errors.map((err, i, arr) => {\n let str = '';\n if (arr.length > 1) str += `[${i} of ${arr.length}] `;\n str += `${err.title} (${err.status}, ${err.id}): ${err.detail}. `;\n return str;\n });\n info = parsedErrors.join('\\n');\n }\n\n if (info.includes('36 characters')) {\n info +=\n '\\n\\nIt looks like MangaDex expected a UUID, but you provided a non-UUID string. If you are using Tags, please use Tag.getByName() instead of the literal name.';\n }\n\n super(info);\n Object.setPrototypeOf(this, APIResponseError.prototype);\n this.name = 'APIResponseError';\n }\n}\n","export default class AuthError extends Error {\n constructor(info: string) {\n super(info);\n Object.setPrototypeOf(this, AuthError.prototype);\n this.name = 'AuthError';\n }\n}\n","import IDObject from '../internal/IDObject';\nimport APIResponseError from './APIResponseError';\nimport AuthError from './AuthError';\n\nimport type { CheckResponseSchema, ErrorResponseSchema } from '../types/schema';\nimport type { IAuthClient } from '../types/helpers';\n\ntype ParameterObj = {\n [x: string]:\n | string\n | string[]\n | number\n | number[]\n | boolean\n | { [x: string]: string | number }\n | IDObject\n | IDObject[]\n | undefined;\n};\n\ntype ListResponse = { data: { id: string }[]; limit: number; offset: number; total: number };\n\ntype CustomRequestInit = Omit<RequestInit, 'headers'> & { headers?: Record<string, string>; noAuth?: boolean };\n\nclass NetworkStateManager {\n static useDebugServerValue = false;\n static activeClient: IAuthClient | undefined;\n}\n\n/**\n * If true the debug (sandbox) MangaDex domain wil be used instead of the default one.\n * {@link https://sandbox.mangadex.dev}\n */\nexport function useDebugServer(val: boolean) {\n NetworkStateManager.useDebugServerValue = val;\n}\n/**\n * Returns if the debug (sandbox) MangaDex domain is in use\n */\nexport function isDebugServerInUse() {\n return NetworkStateManager.useDebugServerValue;\n}\n\n/**\n * Sets the AuthClient to be used by API calls\n * @param client - The signed-in OAuth or legacy AuthClient\n */\nexport function setActiveAuthClient(client: IAuthClient) {\n NetworkStateManager.activeClient = client;\n}\n\n/**\n * Removes the current active AuthClient so no further API calls are done with user authorization\n */\nexport function clearActiveAuthClient() {\n NetworkStateManager.activeClient = undefined;\n}\n\n/**\n * Returns the current auth client or null if there is none\n */\nexport function getActiveAuthClient() {\n return NetworkStateManager.activeClient ?? null;\n}\n\n/**\n * Performs a fetch request to MangaDex and parses the response as JSON.\n */\nexport async function fetchMD<T extends object>(\n endpoint: string,\n params?: ParameterObj,\n requestInit: CustomRequestInit = {},\n): Promise<T> {\n const domain = NetworkStateManager.useDebugServerValue ? 'https://api.mangadex.dev' : 'https://api.mangadex.org/';\n const url = buildURL(domain, endpoint, params);\n\n if (NetworkStateManager.activeClient && !requestInit.noAuth) {\n const sessionToken = await NetworkStateManager.activeClient.getSessionToken();\n if (requestInit.headers === undefined) requestInit.headers = {};\n requestInit.headers['authorization'] = `Bearer ${sessionToken}`;\n }\n const res = await fetch(url, requestInit);\n\n // Raise error if response isn't JSON\n const contentType = res.headers.get('content-type');\n if (!contentType?.toLowerCase().includes('json')) {\n let errInfo = `${res.statusText} (${res.status}) Response was an unexpected content type: ${\n contentType ?? 'Unspecified Type'\n }.`;\n try {\n let text = await res.text();\n if (text.length > 128) text = text.slice(0, 128);\n errInfo += `\\nStart of Body: ${text}`;\n } catch (_) {}\n throw new APIResponseError(errInfo);\n }\n\n // Raise error if response is a MD error\n const data = (await res.json()) as ErrorResponseSchema | T;\n if ('result' in data && data.result !== 'ok') {\n throw new APIResponseError(data);\n }\n\n // Raise error if error status code was given\n if (res.status >= 400) {\n throw new APIResponseError(`${res.statusText} (${res.status})`);\n }\n\n return data as T;\n}\n\n/**\n * Same as {@link fetchMD}, but returns the 'data' property of the response instead\n */\nexport async function fetchMDData<T extends { data: unknown }>(\n endpoint: string,\n params?: ParameterObj,\n requestInit?: CustomRequestInit,\n): Promise<T['data']> {\n const res = await fetchMD<T>(endpoint, params, requestInit);\n return res.data;\n}\n\n/**\n * Same as {@link fetchMDData} but is designed specifically for search requests. This means that\n * multiple requests will be used for extreme limits.\n */\nexport async function fetchMDSearch<T extends ListResponse>(\n endpoint: string,\n params: ParameterObj & { limit?: number; offset?: number } = {},\n requestInit?: CustomRequestInit,\n maxLimit = 100,\n defaultLimit = 10,\n): Promise<T['data']> {\n // Setup initial limit and offset values:\n const MAX_POSSIBLE_RESULTS = 10000; // Hard limit for any endpoint is 10000 total results\n let targetLimit = Math.min(params.limit ?? defaultLimit, MAX_POSSIBLE_RESULTS);\n const initialOffset = params.offset ?? 0;\n if (initialOffset >= MAX_POSSIBLE_RESULTS || targetLimit <= 0) return [];\n if (initialOffset > MAX_POSSIBLE_RESULTS - Math.min(maxLimit, targetLimit)) {\n // Make limit smaller to avoid bounds error if offset is close to MAX_POSSIBLE_RESULTS\n targetLimit = MAX_POSSIBLE_RESULTS - initialOffset;\n }\n\n // Get one result to find out how many total results there are\n const firstResponse = await fetchMD<T>(\n endpoint,\n { ...params, limit: Math.min(targetLimit, maxLimit) },\n requestInit,\n );\n // Return immediately if multiple requests aren't needed, or if the result contains all possible results\n if (targetLimit <= maxLimit || firstResponse.total <= firstResponse.data.length + initialOffset) {\n return firstResponse.data;\n }\n // Lower the limit if there aren't that many results\n targetLimit = Math.min(targetLimit, firstResponse.total);\n\n // Create an array of requests with each request having the maximum limit until the target limit is reached\n const promises: Promise<T['data']>[] = [];\n for (let offset = initialOffset + maxLimit; offset < targetLimit; offset += maxLimit) {\n const limitForThisRequest = Math.min(targetLimit - offset, maxLimit);\n promises.push(fetchMDData<T>(endpoint, { ...params, limit: limitForThisRequest, offset: offset }, requestInit));\n }\n const newResults = await Promise.all(promises);\n return firstResponse.data.concat(...newResults);\n}\n\n/**\n * Will request a list of objects by an array of their ids (or similar) as a query parameter. This\n * function also accepts extra parameters in the same format as {@link fetchMDSearch}.\n */\nexport async function fetchMDByArrayParam<T extends ListResponse>(\n endpoint: string,\n arr: (string | IDObject)[],\n extraParams: ParameterObj = {},\n arrayParam = 'ids',\n paramLimit = 100,\n requestInit?: CustomRequestInit,\n): Promise<T['data']> {\n const idArray = arr.map((elem) => (elem instanceof IDObject ? elem.id : elem));\n const promises = [];\n for (let i = 0; i < idArray.length; i += paramLimit) {\n promises.push(\n fetchMDData<T>(\n endpoint,\n { ...extraParams, [arrayParam]: idArray.slice(i, i + paramLimit), limit: paramLimit },\n requestInit,\n ),\n );\n }\n const results = await Promise.all(promises);\n // Reorder results so that they're in the same order as the id array\n const sortedResults = results.flat();\n sortedResults.sort((a, b) => idArray.indexOf(a.id) - idArray.indexOf(b.id));\n return sortedResults;\n}\n\n/**\n * Same as {@link fetchMD}, but it instead performs a request with a JSON body\n */\nexport async function fetchMDWithBody<T extends object>(\n endpoint: string,\n body: object,\n params?: ParameterObj,\n method = 'POST',\n requestInit: CustomRequestInit = {},\n): Promise<T> {\n const headers = requestInit.headers !== undefined ? requestInit.headers : {};\n headers['Content-Type'] = 'application/json';\n return fetchMD<T>(endpoint, params, {\n body: JSON.stringify(body),\n method: method,\n headers: headers,\n });\n}\n\n/**\n * Same as {@link fetchMDData}, but it instead performs a request with a JSON body\n */\nexport async function fetchMDDataWithBody<T extends { data: unknown }>(\n endpoint: string,\n body: object,\n params?: ParameterObj,\n method = 'POST',\n requestInit: CustomRequestInit = {},\n): Promise<T['data']> {\n const res = await fetchMDWithBody<T>(endpoint, body, params, method, requestInit);\n return res['data'];\n}\n\n/**\n * Performs a POST fetch request to api.mangadex.network with a JSON body\n */\nexport async function postToMDNetwork(endpoint: string, body: object, params?: ParameterObj): Promise<void> {\n const url = buildURL('https://api.mangadex.network', endpoint, params);\n const res = await fetch(url, {\n body: JSON.stringify(body),\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n if (!res.ok) throw new APIResponseError(`${res.status} ${res.statusText}`);\n}\n\nexport async function fetchMDWithFormData<T extends object>(\n endpoint: string,\n body: Record<\n string,\n | string\n | string[]\n | Blob\n | Blob[]\n | { data: string | Blob; name: string }\n | { data: string | Blob; name: string }[]\n | undefined\n | null\n >,\n params?: ParameterObj,\n method = 'POST',\n requestInit: CustomRequestInit = {},\n): Promise<T> {\n const formdata = new FormData();\n const appendItem = (name: string, item: Blob | string | { data: string | Blob; name: string }) => {\n if (typeof item !== 'string' && 'data' in item && 'name' in item) {\n formdata.append(name, item.data as Blob, item.name);\n } else {\n formdata.append(name, item);\n }\n };\n for (const [name, value] of Object.entries(body)) {\n if (value) {\n // MD accepts array values as name + index, not name[]\n if (Array.isArray(value)) value.forEach((v, i) => appendItem(name + i, v));\n else appendItem(name, value);\n }\n }\n return await fetchMD<T>(endpoint, params, {\n ...requestInit,\n method: method,\n body: formdata,\n });\n}\n\n/**\n * Generate a url from a base domain, path, and parameter object\n */\nexport function buildURL(base: string, path?: string, params?: ParameterObj): URL {\n const url = path ? new URL(path, base) : new URL(base);\n if (!params) return url;\n\n for (const [name, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (let i of value) {\n if (i instanceof IDObject) i = i.id;\n url.searchParams.append(`${name}[]`, i.toString());\n }\n } else if (typeof value === 'object') {\n if (value instanceof IDObject) {\n url.searchParams.append(name, value.id.toString());\n } else {\n const valueEntries = Object.entries(value);\n for (const [k, v] of valueEntries) {\n url.searchParams.append(`${name}[${k}]`, v.toString());\n }\n }\n } else if (value !== undefined) {\n url.searchParams.append(name, value.toString());\n }\n }\n\n return url;\n}\n\n/**\n * Checks if the current user is correctly authorized or if the specified session token is valid.\n */\nexport async function performAuthCheck(sessionToken?: string): Promise<boolean> {\n try {\n let options: CustomRequestInit | undefined = undefined;\n if (sessionToken !== undefined) {\n options = {\n headers: {\n authorization: `Bearer ${sessionToken}`,\n },\n noAuth: true,\n };\n }\n const res = await fetchMD<CheckResponseSchema>('/auth/check', undefined, options);\n return res.isAuthenticated;\n } catch (err) {\n if (err instanceof APIResponseError) return false;\n else throw err;\n }\n}\n\n/**\n * Send a URL-encoded POST request to the MangaDex auth server\n */\nexport async function fetchMDAuth<T extends object>(endpoint: string, body: Record<string, string>): Promise<T> {\n const params = new URLSearchParams();\n for (const [name, value] of Object.entries(body)) params.append(name, value);\n const domain = `https://auth.mangadex.${isDebugServerInUse() ? 'dev' : 'org'}`;\n const url = new URL(endpoint, domain);\n const res = await fetch(url, {\n body: params,\n method: 'POST',\n });\n\n if (res.status >= 400) {\n throw new AuthError(`${res.statusText} (${res.status})`);\n }\n\n const resBody: T = await res.json();\n return resBody;\n}\n","import IDObject from '../internal/IDObject';\nimport LocalizedString from '../internal/LocalizedString';\nimport { fetchMDData, fetchMDSearch, fetchMDByArrayParam, fetchMDDataWithBody, fetchMD } from '../util/Network';\nimport Relationship from '../internal/Relationship';\n\nimport {\n AuthorAttributesSchema,\n AuthorCreateSchema,\n AuthorEditSchema,\n AuthorListSchema,\n AuthorResponseSchema,\n AuthorSchema,\n GetAuthorParamsSchema,\n ResponseSchema,\n} from '../types/schema';\nimport type Manga from './Manga';\nimport type { Merge } from '../types/helpers';\n\ntype AuthorSearchParams = Partial<Merge<GetAuthorParamsSchema, { ids: Author[] }>>;\n\n/**\n * This represents an author or artist of a manga\n */\nexport default class Author extends IDObject implements AuthorAttributesSchema {\n /**\n * The MangaDex UUID for the author/artist\n */\n id: string;\n /**\n * The name of the author/artist\n */\n name: string;\n /**\n * A url to an image of the author/artist\n */\n imageUrl: string | null;\n /**\n * The biography of the author/artist\n */\n biography: LocalizedString;\n /**\n * URL to the author/artist's twitter\n */\n twitter: string | null;\n /**\n * URL to the author/artist's pixiv\n */\n pixiv: string | null;\n /**\n * URL to the author/artist's melon book\n */\n melonBook: string | null;\n /**\n * URL to the author/artist's fanbox\n */\n fanBox: string | null;\n /**\n * URL to the author/artist's booth\n */\n booth: string | null;\n /**\n * URL to the author/artist's nico video\n */\n nicoVideo: string | null;\n /**\n * URL to the author/artist's skeb\n */\n skeb: string | null;\n /**\n * URL to the author/artist's fantia\n */\n fantia: string | null;\n /**\n * URL to the author/artist's tumblr\n */\n tumblr: string | null;\n /**\n * URL to the author/artist's youtube\n */\n youtube: string | null;\n /**\n * URL to the author/artist's weibo\n */\n weibo: string | null;\n /**\n * URL to the author/artist's naver\n */\n naver: string | null;\n /**\n * URL to the author/artist's namicomi\n */\n namicomi: string | null;\n /**\n * URL to the author/artist's website\n */\n website: string | null;\n /**\n * The version of this author/artist's entry (incremented by updating author data)\n */\n version: number;\n /**\n * When the author/artist's entry was added\n */\n createdAt: Date;\n /**\n * The last time the author/artist's entry was updated\n */\n updatedAt: Date;\n /**\n * The manga the author/artist has worked on\n */\n manga: Relationship<Manga>[];\n\n constructor(schem: AuthorSchema) {\n super();\n this.id = schem.id;\n this.name = schem.attributes.name;\n this.imageUrl = schem.attributes.imageUrl;\n this.biography = new LocalizedString(schem.attributes.biography);\n this.twitter = schem.attributes.twitter;\n this.pixiv = schem.attributes.pixiv;\n this.melonBook = schem.attributes.melonBook;\n this.fanBox = schem.attributes.fanBox;\n this.booth = schem.attributes.booth;\n this.nicoVideo = schem.attributes.nicoVideo;\n this.skeb = schem.attributes.skeb;\n this.fantia = schem.attributes.fantia;\n this.tumblr = schem.attributes.tumblr;\n this.youtube = schem.attributes.youtube;\n this.weibo = schem.attributes.weibo;\n this.naver = schem.attributes.naver;\n this.namicomi = schem.attributes.namicomi;\n this.website = schem.attributes.website;\n this.version = schem.attributes.version;\n this.createdAt = new Date(schem.attributes.createdAt);\n this.updatedAt = new Date(schem.attributes.updatedAt);\n this.manga = Relationship.convertType('manga', schem.relationships);\n }\n\n /**\n * Retrieve a chapter object by its id\n */\n static async get(id: string, expandedTypes?: AuthorSearchParams['includes']): Promise<Author> {\n return new Author(await fetchMDData<AuthorResponseSchema>(`/author/${id}`, { includes: expandedTypes }));\n }\n\n /**\n * Retrieves a list of authors/artists according to the specified search parameters\n */\n static async search(query?: AuthorSearchParams) {\n const res = await fetchMDSearch<AuthorListSchema>(`/author`, query);\n return res.map((m) => new Author(m));\n }\n\n /**\n * Performs a search for an author/artist and returns the first one found. If no results are\n * found, null is returned\n */\n static async getByQuery(query?: AuthorSearchParams): Promise<Author | null> {\n const res = await this.search(query);\n return res[0] ?? null;\n }\n\n /**\n * Retrieves an array of authors/artists by an array of ids\n */\n static async getMultiple(ids: string[]): Promise<Author[]> {\n const res = await fetchMDByArrayParam<AuthorListSchema>('/author', ids);\n return res.map((a) => new Author(a));\n }\n\n /**\n * Create a new Author\n */\n static async create(data: AuthorCreateSchema) {\n return new Author(await fetchMDDataWithBody<AuthorResponseSchema>('/author', data));\n }\n\n /**\n * Deletes an author by their id\n */\n static async delete(id: string) {\n await fetchMD<ResponseSchema>(`/author/${id}`, undefined, { method: 'DELETE' });\n }\n\n /**\n * Deletes this author\n */\n async delete() {\n await Author.delete(this.id);\n }\n\n /**\n * Updates an author's information.\n */\n async update(data: Omit<AuthorCreateSchema, 'version'>) {\n return new Author(\n await fetchMDDataWithBody<AuthorResponseSchema>(\n `/author/${this.id}`,\n {\n ...data,\n version: this.version + 1,\n } as AuthorEditSchema,\n undefined,\n 'PUT',\n ),\n );\n }\n}\n","import IDObject from '../internal/IDObject';\nimport {\n fetchMD,\n fetchMDByArrayParam,\n fetchMDData,\n fetchMDDataWithBody,\n fetchMDSearch,\n fetchMDWithFormData,\n} from '../util/Network';\nimport Relationship from '../internal/Relationship';\n\nimport type Manga from './Manga';\nimport type {\n CoverAttributesSchema,\n CoverEditSchema,\n CoverListSchema,\n CoverResponseSchema,\n CoverSchema,\n GetCoverParamsSchema,\n ResponseSchema,\n Cover as CoverNamespace,\n} from '../types/schema';\nimport type { Merge } from '../types/helpers';\nimport type User from './User';\n\ntype CoverSearchParams = Partial<Merge<GetCoverParamsSchema, { ids: Cover[]; manga: Manga[] }>>;\ntype CoverExpandedTypes = CoverSearchParams['includes'];\ntype CoverUploadBody = Omit<CoverNamespace.UploadCover.RequestBody, 'file'> & { file: Blob };\n\nexport default class Cover extends IDObject implements CoverAttributesSchema {\n /**\n * MangaDex UUID for this object\n */\n id: string;\n /**\n * What volume is this cover for, if any\n */\n volume: string | null;\n /**\n * The file name of this cover's image\n */\n fileName: string;\n /**\n * Description of this cover. May be an empty string\n */\n description: string | null;\n /**\n * What language is this cover in\n */\n locale: string | null;\n /**\n * The version of this cover (incremented by updating the cover)\n */\n version: number;\n /**\n * The date this cover was uploaded\n */\n createdAt: Date;\n /**\n * The date this cover was last updated\n */\n updatedAt: Date;\n /**\n * Url to this cover's image\n * Can be null because of undefined manga\n */\n url: string | null;\n /**\n * Relationship to the manga this cover belongs to\n */\n manga: Relationship<Manga>;\n /**\n * Relationship to the user who uploaded this cover\n */\n uploader: Relationship<User>;\n\n constructor(schem: CoverSchema) {\n super();\n this.id = schem.id;\n this.volume = schem.attributes.volume;\n this.fileName = schem.attributes.fileName;\n this.description = schem.attributes.description;\n this.locale = schem.attributes.locale;\n this.version = schem.attributes.version;\n this.createdAt = new Date(schem.attributes.createdAt);\n this.updatedAt = new Date(schem.attributes.updatedAt);\n this.manga = Relationship.convertType<Manga>('manga', schem.relationships).pop()!;\n this.url = this.manga ? `https://mangadex.org/covers/${this.manga.id}/${this.fileName}` : null;\n this.uploader = Relationship.convertType<User>('user', schem.relationships).pop()!;\n }\n\n /**\n * Retrieves a cover by its id\n */\n static async get(id: string | Manga, expandedTypes?: CoverExpandedTypes): Promise<Cover> {\n if (id instanceof IDObject) id = id.id;\n return new Cover(await fetchMDData<CoverResponseSchema>(`/cover/${id}`, { includes: expandedTypes }));\n }\n\n /**\n * Retrieves a list of covers according to the specified search parameters\n */\n static async search(query?: CoverSearchParams) {\n const res = await fetchMDSearch<CoverListSchema>(`/cover`, query);\n return res.map((m) => new Cover(m));\n }\n\n /**\n * Performs a search for a cover and returns the first one found. If no results are\n * found, null is returned\n */\n static async getByQuery(query?: CoverSearchParams): Promise<Cover | null> {\n const res = await this.search(query);\n return res[0] ?? null;\n }\n\n /**\n * Retrieves an array of covers by an array of ids\n */\n static async getMultiple(ids: string[]): Promise<Cover[]> {\n const res = await fetchMDByArrayParam<CoverListSchema>('/cover', ids);\n return res.map((a) => new Cover(a));\n }\n\n /**\n * Returns an array of covers from an array of manga ids or a single manga\n */\n static async getMangaCovers(\n manga: Manga | string | Manga[] | string[],\n expandedTypes?: CoverExpandedTypes,\n ): Promise<Cover[]> {\n if (!Array.isArray(manga)) manga = [typeof manga === 'string' ? manga : manga.id];\n if (manga.length === 0) return [];\n const ids = manga.map((m) => (typeof m === 'string' ? m : m.id));\n return Cover.search({ manga: ids, includes: expandedTypes });\n }\n\n /**\n * Deletes a cover by their id\n */\n static async delete(id: string) {\n await fetchMD<ResponseSchema>(`/cover/${id}`, undefined, { method: 'DELETE' });\n }\n\n /**\n * Deletes this cover\n */\n async delete() {\n await Cover.delete(this.id);\n }\n\n /**\n * Updates a cover's information.\n */\n async update(data: Omit<CoverEditSchema, 'version'>) {\n return new Cover(\n await fetchMDDataWithBody<CoverResponseSchema>(\n `/cover/${this.id}`,\n {\n ...data,\n version: this.version + 1,\n },\n undefined,\n 'PUT',\n ),\n );\n }\n\n /**\n * Uploads a new cover\n */\n static async create(manga: string | Manga, data: CoverUploadBody) {\n if (typeof manga !== 'string') manga = manga.id;\n const res = await fetchMDWithFormData<CoverResponseSchema>(`/cover/${manga}`, data);\n return new Cover(res.data);\n }\n}\n","import { TagAttributesSchema, TagResponseSchema, TagSchema } from '../types/schema';\nimport { fetchMD } from '../util/Network';\nimport IDObject from '../internal/IDObject';\nimport LocalizedString from '../internal/LocalizedString';\n\n/**\n * This class represents a genre tag for a manga\n */\nexport default class Tag extends IDObject implements TagAttributesSchema {\n private static allTagCache: Tag[];\n\n /**\n * MangaDex UUID of this tag\n */\n id: string;\n /**\n * Localized names for this tag\n */\n name: LocalizedString;\n /**\n * Localized descriptions for this tag\n */\n description: LocalizedString;\n /**\n * The tag group this tag belongs to\n */\n group: 'content' | 'format' | 'genre' | 'theme';\n /**\n * The version of this tag (incremented whenever the tag's data is updated)\n */\n version: number;\n\n constructor(data: TagSchema) {\n super();\n this.id = data.id;\n this.name = new LocalizedString(data.attributes.name);\n this.description = new LocalizedString(data.attributes.description);\n this.group = data.attributes.group;\n this.version = data.attributes.version;\n }\n\n /**\n * Get the localString from the name {@link LocalizedString} object\n */\n get localName() {\n return this.name.localString;\n }\n\n /**\n * Get the localString from the description {@link LocalizedString} object\n */\n get localDescription() {\n return this.description.localString;\n }\n\n /**\n * Retrieves every tag used on MangaDex. The result is cached so any promise\n * after the first will resolve instantly.\n */\n static async getAllTags(): Promise<Tag[]> {\n if (!Tag.allTagCache || Tag.allTagCache.length === 0) {\n const res = await fetchMD<TagResponseSchema>('/manga/tag');\n Tag.allTagCache = res.data.map((elem) => new Tag(elem));\n }\n return Tag.allTagCache;\n }\n\n /**\n * Return the first tag that contains the specified name\n */\n static async getByName(name: string): Promise<Tag> {\n const tags = await this.getAllTags();\n const lowerName = name.toLowerCase();\n const foundTag = tags.find((tag) => Object.values(tag.name).some((n) => n.toLowerCase() === lowerName));\n if (!foundTag) throw new Error(`No tag found with name ${name}`);\n return foundTag;\n }\n\n /**\n * Return tags with the associated names\n */\n static async getByNames(names: string[]): Promise<Tag[]> {\n const tags = await this.getAllTags();\n const lowerNames = names.map((n) => n.toLowerCase());\n return tags.filter((tag) => Object.values(tag.name).some((n) => lowerNames.includes(n.toLowerCase())));\n }\n}\n","/**\n * A simple record object representing links to manga on different websites.\n * Websites are the keys, and the values are full urls (when available).\n */\nclass Links {\n /**\n * Anilist (https://anilist.co) link to manga\n */\n anilist?: string;\n /**\n * AnimePlanet (https://anime-planet.com) link to manga\n */\n animePlanet?: string;\n /**\n * Bookwalker (https://bookwalker.jp/) link to manga\n */\n bookWalker?: string;\n /**\n * Mangaupdates (https://mangaupdates.com) link to manga\n */\n mangaUpdates?: string;\n /**\n * Novelupdates (https://novelupdates.com) link to manga\n */\n novelUpdates?: string;\n /**\n * MyAnimeList (https://myanimelist.net) link to manga\n */\n myAnimeList?: string;\n /**\n * Kitsu (https://kitsu.io) link to manga\n */\n kitsu?: string;\n /**\n * Amazon (https://amazon.com) link to manga\n */\n amazon?: string;\n /**\n * EBookJapan (https://ebookjapan.yahoo.co.jp) link to manga\n */\n eBookJapan?: string;\n /**\n * Link to manga raws\n */\n raw?: string;\n /**\n * Link to offical english manga translation\n */\n officialEnglishTranslation?: string;\n /**\n * CDJapan (https://www.cdjapan.co.jp/) link to manga\n */\n cdJapan?: string;\n\n constructor(linksObject?: Record<string, string>) {\n this.anilist = !linksObject?.al ? undefined : `https://anilist.co/manga/${linksObject.al}`;\n\n this.animePlanet = !linksObject?.ap ? undefined : `https://www.anime-planet.com/manga/${linksObject.ap}`;\n\n this.bookWalker = !linksObject?.bw ? undefined : `https://bookwalker.jp/${linksObject.bw}`;\n\n this.mangaUpdates = !linksObject?.mu\n ? undefined\n : `https://www.mangaupdates.com/series.html?id=${linksObject.mu}`;\n\n this.novelUpdates = !linksObject?.nu ? undefined : `https://www.novelupdates.com/series/${linksObject.nu}`;\n\n this.myAnimeList = !linksObject?.mal ? undefined : `https://myanimelist.net/manga/${linksObject.mal}`;\n\n if (linksObject?.kt !== undefined) {\n // Stored as either a number or slug. See official documentation\n if (isNaN(parseInt(linksObject.kt))) {\n this.kitsu = `https://kitsu.io/api/edge/manga?filter[slug]=${linksObject.kt}`;\n } else {\n this.kitsu = `https://kitsu.io/api/edge/manga/${linksObject.kt}`;\n }\n }\n\n this.amazon = linksObject?.amz;\n\n this.eBookJapan = linksObject?.ebj;\n\n this.raw = linksObject?.raw;\n\n this.officialEnglishTranslation = linksObject?.engtl;\n\n this.cdJapan = linksObject?.cdj;\n }\n}\n\nexport default Links;\n","import IDObject from '../internal/IDObject';\nimport {\n fetchMD,\n fetchMDByArrayParam,\n fetchMDData,\n fetchMDDataWithBody,\n fetchMDSearch,\n postToMDNetwork,\n} from '../util/Network';\nimport Relationship from '../internal/Relationship';\n\nimport type {\n AtHome,\n ChapterAttributesSchema,\n ChapterEditSchema,\n ChapterListSchema,\n ChapterResponseSchema,\n ChapterSchema,\n GetChapterParamsSchema,\n ResponseSchema,\n Statistics,\n} from '../types/schema';\nimport type Manga from './Manga';\nimport type User from './User';\nimport type Group from './Group';\nimport type { DeepRequire, Merge } from '../types/helpers';\n\nexport type ChapterSearchParams = Partial<\n Merge<GetChapterParamsSchema, { ids: Chapter[]; groups: Group[]; uploader: User | User[] }>\n>;\ntype AtHomeServerResponse = Required<AtHome.GetAtHomeServerChapterId.ResponseBody>;\ntype OtherChapterAttributes = Omit<ChapterAttributesSchema, 'uploader'>;\ntype ChapterStatsResponse = DeepRequire<Statistics.GetStatisticsChapters.ResponseBody>;\ntype ChapterStats = ChapterStatsResponse['statistics'][string];\n\nexport default class Chapter extends IDObject implements OtherChapterAttributes {\n /**\n * The MangaDex UUID of this chapter\n */\n id: string;\n /**\n * The title of this chapter\n */\n title: string | null;\n /**\n * The manga volume this chapter belongs to\n */\n volume: string | null;\n /**\n * The chapter number for this chapter\n */\n chapter: string | null;\n /**\n * The number of pages in this chapter\n */\n pages: number;\n /**\n * The language of this chapter\n */\n translatedLanguage: string;\n /**\n * Relationship to the user who uploaded this chapter\n */\n uploader: Relationship<User>;\n /**\n * Url to this chapter if it's an external chapter\n */\n externalUrl: string | null;\n /**\n * The version of this chapter (incremented by updating chapter data)\n */\n version: number;\n /**\n * When this chapter was created\n */\n createdAt: Date;\n /**\n * When this chapter was last updated\n */\n updatedAt: Date;\n /**\n * When this chapter was originally published\n */\n publishAt: Date;\n /**\n * When was / when will this chapter be readable?\n */\n readableAt: Date;\n /**\n * Is this chapter an external chapter? If it is, this chapter will have an externalUrl\n */\n isExternal: boolean;\n /**\n * A relationship to the manga this chapter belongs to\n */\n manga: Relationship<Manga>;\n /**\n * Array of relationships to the groups that translated this chapter\n */\n groups: Relationship<Group>[];\n\n constructor(schem: ChapterSchema) {\n super();\n this.id = schem.id;\n this.title = schem.attributes.title;\n this.volume = schem.attributes.volume;\n this.chapter = schem.attributes.chapter;\n this.pages = schem.attributes.pages;\n this.translatedLanguage = schem.attributes.translatedLanguage;\n this.uploader = Relationship.convertType<User>('user', schem.relationships).pop()!;\n this.externalUrl = schem.attributes.externalUrl;\n this.version = schem.attributes.version;\n this.createdAt = new Date(schem.attributes.createdAt);\n this.publishAt = new Date(schem.attributes.publishAt);\n this.updatedAt = new Date(schem.attributes.updatedAt);\n this.readableAt = new Date(schem.attributes.readableAt);\n this.isExternal = schem.attributes.externalUrl !== null;\n this.manga = Relationship.convertType<Manga>('manga', schem.relationships).pop()!;\n this.groups = Relationship.convertType<Group>('scanlation_group', schem.relationships);\n }\n\n /**\n * Retrieves a chapter object by its UUID\n */\n static async get(id: string, expandedTypes?: ChapterSearchParams['includes']): Promise<Chapter> {\n return new Chapter(await fetchMDData<ChapterResponseSchema>(`/chapter/${id}`, { includes: expandedTypes }));\n }\n\n /**\n * Retrieves an array of chapters by an array of their ids\n */\n static async getMultiple(ids: string[]): Promise<Chapter[]> {\n const res = await fetchMDByArrayParam<ChapterListSchema>(`/chapter`, ids);\n return res.map((c) => new Chapter(c));\n }\n\n /**\n * Retrieves a list of chapters according to the specified search parameters\n */\n static async search(query?: ChapterSearchParams): Promise<Chapter[]> {\n const res = await fetchMDSearch<ChapterListSchema>('/chapter', query);\n return res.map((c) => new Chapter(c));\n }\n\n /**\n * Performs a search for a chapter and returns the first one found. If no results are\n * found, null is returned\n */\n static async getByQuery(query?: ChapterSearchParams): Promise<Chapter | null> {\n const res = await this.search(query);\n return res[0] ?? null;\n }\n\n /**\n * Update this chapter's information\n */\n async update(data: Omit<ChapterEditSchema, 'version'>): Promise<Chapter> {\n return new Chapter(\n await fetchMDDataWithBody<ChapterResponseSchema>(\n `/chapter/${this.id}`,\n {\n ...data,\n version: this.version + 1,\n },\n undefined,\n 'PUT',\n ),\n );\n }\n\n /**\n * Delete this chapter\n */\n static async delete(id: string) {\n await fetchMD<ResponseSchema>(`/chapter/${id}`, undefined, { method: 'DELETE' });\n }\n\n /**\n * Delete a chapter by its UUID\n */\n async delete() {\n await Chapter.delete(this.id);\n }\n\n /**\n * Returns an array of image URLs for this chapter's pages. Once an image is requested,\n * if the host is from MangaDex(at)Home, please report if it succeeds or fails by using {@link reportPageURL}.\n * @param saver - If true, the URLs will be for the compressed data-saver images (if available).\n * @param forcePort - If true, the URLs will be forced to be on port 443.\n */\n async getReadablePages(saver = false, forcePort = false): Promise<string[]> {\n if (this.isExternal) throw new Error('Cannot get readable pages for an external chapter.');\n const res = await fetchMD<AtHomeServerResponse>(`/at-home/server/${this.id}`, {\n forcePort443: forcePort,\n });\n // Get the list of image files depending on if data saver images are preferred\n const files = (saver ? res.chapter.dataSaver ?? res.chapter.data : res.chapter.data) ?? [];\n // Build image urls according to https://api.mangadex.org/docs/retrieving-chapter/\n return files.map((file) => `${res.baseUrl}/${saver ? 'data-saver' : 'data'}/${res.chapter.hash}/${file}`);\n }\n\n /**\n * Sends a report to MangaDex about the success/failure of a MangaDex(at)Home server.\n * Read more information: {@link https://api.mangadex.org/docs/04-chapter/retrieving-chapter/#mangadexhome-load-successes-failures-and-retries}\n */\n static async reportPageURL(report: {\n url: string;\n success: boolean;\n bytes: number;\n duration: number;\n cached: boolean;\n }): Promise<void> {\n await postToMDNetwork('/report', report);\n }\n\n /**\n * Gets the statistics about a list of chapters\n */\n static async getStatistics(ids: string[] | Chapter[]): Promise<Record<string, ChapterStats>> {\n const res = await fetchMD<ChapterStatsResponse>(`/statistics/chapter`, { chapter: ids });\n return res.statistics;\n }\n\n /**\n * Gets the statistics about this chapter\n */\n async getStatistics(): Promise<ChapterStats> {\n const res = await Chapter.getStatistics([this.id]);\n return res[this.id];\n }\n}\n","import LocalizedString from '../internal/LocalizedString';\nimport Tag from './Tag';\nimport {\n fetchMD,\n fetchMDByArrayParam,\n fetchMDData,\n fetchMDDataWithBody,\n fetchMDSearch,\n fetchMDWithBody,\n} from '../util/Network';\nimport Relationship from '../internal/Relationship';\nimport Links from '../internal/Links';\nimport IDObject from '../internal/IDObject';\nimport Chapter, { ChapterSearchParams } from './Chapter';\nimport Cover from './Cover';\nimport APIResponseError from '../util/APIResponseError';\n\nimport type Author from './Author';\nimport {\n ChapterListSchema,\n ChapterReadMarkerBatchSchema,\n GetMangaRandomParamsSchema,\n GetSearchMangaParamsSchema,\n MangaAttributesSchema,\n MangaListSchema,\n MangaResponseSchema,\n MangaSchema,\n RelationshipSchema,\n Manga as MangaNamespace,\n Rating,\n ResponseSchema,\n MappingIdBodySchema,\n MappingIdResponseSchema,\n MangaRelationAttributesSchema,\n MangaRelationResponseSchema,\n MangaRelationRequestSchema,\n MangaRelationListSchema,\n Statistics,\n GetMangaDraftsParamsSchema,\n MangaCreateSchema,\n MangaEditSchema,\n User as UserNamespace,\n} from '../types/schema';\nimport type { DeepRequire, Merge } from '../types/helpers';\nimport type Group from './Group';\n\n// This type supplements the schema type so that IDObjects can be used instead\ntype MangaSearchHelpers = {\n group: Group;\n includedTags: Tag[];\n excludedTags: Tag[];\n authors: Author[];\n artists: Author[];\n authorOrArtist: Author;\n ids: IDObject[];\n};\ntype MangaSearchParams = Partial<Merge<GetSearchMangaParamsSchema, MangaSearchHelpers>>;\ntype OtherMangaAttributes = Omit<MangaAttributesSchema, 'tags' | 'links' | 'latestUploadedChapter'>;\ntype RelatedManga = { [x in RelationshipSchema['related']]: Relationship<Manga>[] };\ntype ReadmarkerResponse = Required<MangaNamespace.GetMangaChapterReadmarkers.ResponseBody>;\ntype ReadmarkerResponseGrouped = Required<MangaNamespace.GetMangaChapterReadmarkers2.ResponseBody>;\ntype RatingResponse = Required<Rating.GetRating.ResponseBody>;\ntype MangaReadingStatus = Required<MangaNamespace.GetMangaIdStatus.ResponseBody>['status'];\ntype MangaRelation = MangaRelationAttributesSchema['relation'];\ntype MangaStatsResponse = DeepRequire<Statistics.GetStatisticsManga.ResponseBody>;\ntype MangaStats = MangaStatsResponse['stat