rsshub
Version:
Make RSS Great Again!
185 lines (153 loc) • 6.83 kB
text/typescript
import got from '@/utils/got';
import { config } from '@/config';
import cache from '@/utils/cache';
import md5 from '@/utils/md5';
import { getFilteredLanguages } from './_profile';
import { toQueryString, firstMatch } from './_utils';
import constants from './_constants';
/**
* Retrieves the title, description, and cover of a manga.
*
* @author chrisis58, vzz64
* @param id manga id
* @param lang language(s), absent for default
* @param needCover whether to fetch cover
* @returns title, description, and cover of the manga
*/
const getMangaMeta = async (id: string, needCover: boolean = true, lang?: string | string[]) => {
const includes = needCover ? ['cover_art'] : [];
const rawMangaMeta = (await cache.tryGet(`mangadex:manga-meta:${id}`, async () => {
const { data } = await got.get(
`${constants.API.MANGA_META}${id}${toQueryString({
includes,
})}`
);
if (data.result === 'error') {
throw new Error(data.errors[0].detail);
}
return data.data;
})) as any;
const relationships = (rawMangaMeta.relationships || []) as Array<{ type: string; id: string; attributes: any }>;
const languages = [
...(typeof lang === 'string' ? [lang] : lang || []),
...(await getFilteredLanguages()),
rawMangaMeta.attributes.originalLanguage, // fallback to original language
].filter(Boolean);
// combine title and altTitles
const titles = {
...rawMangaMeta.attributes.title,
...Object.fromEntries(rawMangaMeta.attributes.altTitles.flatMap((element) => Object.entries(element))),
};
const title = firstMatch(titles, languages) as string;
const description = firstMatch(rawMangaMeta.attributes.description, languages) as string;
if (!needCover) {
return { title, description };
}
const coverFilename = relationships.find((relationship) => relationship.type === 'cover_art')?.attributes.fileName + '.512.jpg';
const cover = `${constants.API.COVER_IMAGE}${id}/${coverFilename}`;
return { title, description, cover };
};
/**
* Retrieves the title, description, and cover of multiple manga.
* TODO: Retrieve page by page to avoid meeting the length limit of URL.
*
* @param ids manga ids
* @param needCover whether to fetch cover
* @param lang language(s), absent for default
* @returns a map of manga id to title, description, and cover
* @usage const mangaMetaMap = await getMangaMetaByIds(['f98660a1-d2e2-461c-960d-7bd13df8b76d']);
*/
export async function getMangaMetaByIds(ids: string[], needCover: boolean = true, lang?: string | string[]): Promise<Map<string, { id: string; title: string; description: string; cover?: string }>> {
const deDuplidatedIds = [...new Set(ids)].sort();
const includes = needCover ? ['cover_art'] : [];
const rawMangaMetas = (await cache.tryGet(
`mangadex:manga-meta:${md5(deDuplidatedIds.join(''))}`, // shorten the key
async () => {
const { data } = await got.get(
constants.API.MANGA_META.slice(0, -1) +
toQueryString({
ids: deDuplidatedIds,
includes,
limit: deDuplidatedIds.length,
})
);
if (data.result === 'error') {
throw new Error('Failed to retrieve manga meta from MangaDex API.');
}
return data.data;
}
)) as Array<any>;
const languages = [...(typeof lang === 'string' ? [lang] : lang || []), ...(await getFilteredLanguages())].filter(Boolean);
const map = new Map<string, { id: string; title: string; description: string; cover?: string }>();
for (const rawMangaMeta of rawMangaMetas) {
const id = rawMangaMeta.id;
const titles = {
...rawMangaMeta.attributes.title,
...Object.fromEntries(rawMangaMeta.attributes.altTitles.flatMap((element) => Object.entries(element))),
};
const title = firstMatch(titles, [...languages, rawMangaMeta.attributes.originalLanguage]) as string;
const description = firstMatch(rawMangaMeta.attributes.description, languages) as string;
let cover: string | undefined;
let manga = { id, title, description, cover };
if (needCover) {
const coverFilename = rawMangaMeta.relationships.find((relationship) => relationship.type === 'cover_art')?.attributes.fileName;
if (coverFilename) {
cover = `${constants.API.COVER_IMAGE}${rawMangaMeta.id}/${coverFilename}.512.jpg`;
manga = { ...manga, cover };
}
}
map.set(id, manga);
}
return map;
}
/**
* Retrieves the chapters of a manga.
*
* @author chrisis58, vzz64
* @param id manga id
* @param lang language(s), absent for default
* @returns chapters of the manga
*/
const getMangaChapters = async (id: string, lang?: string | string[], limit?: number) => {
const languages = new Set([...(typeof lang === 'string' ? [lang] : lang || []), ...(await getFilteredLanguages())].filter(Boolean));
const url = `${constants.API.MANGA_META}${id}/feed${toQueryString({
order: {
publishAt: 'desc',
},
limit: limit || 100,
translatedLanguage: languages,
})}`;
const chapters = (await cache.tryGet(
`mangadex:manga-chapters:${id}`,
async () => {
const { data } = await got.get(url);
if (data.result === 'error') {
throw new Error(data.errors[0].detail);
}
return data.data;
},
config.cache.routeExpire,
false
)) as any;
if (!chapters) {
return [];
}
return chapters.map((chapter) => ({
title: [chapter.attributes.volume ? `Vol. ${chapter.attributes.volume}` : null, chapter.attributes.chapter ? `Ch. ${chapter.attributes.chapter}` : null, chapter.attributes.title].filter(Boolean).join(' '),
link: `${constants.API.MANGA_CHAPTERS}${chapter.id}`,
pubDate: new Date(chapter.attributes.publishAt),
})) as Array<{ title: string; link: string; pubDate: Date }>;
};
/**
* Retrieves the title, description, cover, and chapters of a manga.
* Cominbation of getMangaMeta and getMangaChapters.
*
* @param id manga id
* @param lang language, absent for default
* @returns title, description, cover, and chapters of the manga
*/
const getMangaDetails = async (id: string, needCover: boolean = true, lang?: string | string[]) => {
const [meta, chapters] = await Promise.all([getMangaMeta(id, needCover, lang), getMangaChapters(id, lang)]);
return { ...meta, chapters };
};
export { getMangaMeta, getMangaChapters, getMangaDetails };