rsshub
Version:
Make RSS Great Again!
286 lines (280 loc) • 11.6 kB
JavaScript
import { t as config } from "./config-C37vj7VH.mjs";
import { t as md5 } from "./md5-C8GRvctM.mjs";
import { t as cache_default } from "./cache-Bo__VnGm.mjs";
import { t as got_default } from "./got-KxxWdaxq.mjs";
import { t as config_not_found_default } from "./config-not-found-Dyp3RlZZ.mjs";
import { FetchError } from "ofetch";
//#region lib/routes/mangadex/_constants.ts
/**
* Define constants for the MangaDex.
*/
var _constants_default = {
API: {
BASE: "https://api.mangadex.org",
MANGA_META: "https://api.mangadex.org/manga/",
MANGA_CHAPTERS: "https://mangadex.org/chapter/",
COVERS: "https://api.mangadex.org/cover/",
COVER_IMAGE: "https://uploads.mangadex.org/covers/",
READING_STATUSES: "https://api.mangadex.org/manga/status",
TOKEN: "https://auth.mangadex.org/realms/mangadex/protocol/openid-connect/token",
SETTING: "https://api.mangadex.org/settings"
},
TOKEN_EXPIRE: 890
};
//#endregion
//#region lib/routes/mangadex/_access.ts
/**
* Retrieves an access token.
*
* @important Ensure the request includes a User-Agent header.
* @throws {ConfigNotFoundError} If the required configuration is missing.
* The following credentials are mandatory:
* - `client ID` and `client secret`
* - One of the following:
* - `username` and `password`
* - `refresh token`
* @throws {FetchError} If the request fails.
* - 400 Bad Request: If the `refresh token` or other credentials are invalid.
* @returns {Promise<string>} A promise that resolves to the access token.
*/
const getToken = () => {
if (!config.mangadex.clientId || !config.mangadex.clientSecret) throw new config_not_found_default("Cannot get access token since MangaDex client ID or secret is not set.");
return cache_default.tryGet("mangadex:access-token", async () => {
if (!config.mangadex.refreshToken) return getAccessTokenByUserCredentials();
try {
return await getAccessTokenByRefreshToken();
} catch (error) {
if (error instanceof FetchError && error.statusCode === 400) return getAccessTokenByUserCredentials();
throw error;
}
}, _constants_default.TOKEN_EXPIRE, false);
};
const getAccessTokenByUserCredentials = async () => {
if (!config.mangadex.clientId || !config.mangadex.clientSecret) throw new config_not_found_default("Cannot get access token since MangaDex client ID or secret is not set.");
if (!config.mangadex.username || !config.mangadex.password) throw new config_not_found_default("Cannot get refresh token since MangaDex username or password is not set");
const response = await got_default.post(_constants_default.API.TOKEN, {
headers: { "User-Agent": config.trueUA },
form: {
grant_type: "password",
username: config.mangadex.username,
password: config.mangadex.password,
client_id: config.mangadex.clientId,
client_secret: config.mangadex.clientSecret
}
});
const refreshToken = response?.data?.refresh_token;
const accessToken = response?.data?.access_token;
if (!refreshToken || !accessToken) throw new Error("Failed to retrieve refresh token from MangaDex API.");
config.mangadex.refreshToken = refreshToken;
return accessToken;
};
const getAccessTokenByRefreshToken = async () => {
if (!config.mangadex.clientId || !config.mangadex.clientSecret) throw new config_not_found_default("Cannot get access token since MangaDex client ID or secret is not set.");
if (!config.mangadex.refreshToken) throw new config_not_found_default("Cannot get access token since MangaDex refresh token is not set.");
const accessToken = (await got_default.post(_constants_default.API.TOKEN, {
headers: { "User-Agent": config.trueUA },
form: {
grant_type: "refresh_token",
refresh_token: config.mangadex.refreshToken,
client_id: config.mangadex.clientId,
client_secret: config.mangadex.clientSecret
}
}))?.data?.access_token;
if (!accessToken) throw new Error("Failed to retrieve access token from MangaDex API.");
return accessToken;
};
var _access_default = getToken;
//#endregion
//#region lib/routes/mangadex/_profile.ts
const getSetting = async () => {
const accessToken = await _access_default();
return cache_default.tryGet("mangadex:settings", async () => {
const setting = (await got_default.get(_constants_default.API.SETTING, { headers: {
Authorization: `Bearer ${accessToken}`,
"User-Agent": config.trueUA
} }))?.data?.settings;
if (!setting) throw new Error("Failed to retrieve user settings from MangaDex API.");
return setting;
}, config.cache.contentExpire, false);
};
const getFilteredLanguages = async (ingoreConfigNotFountError = true) => {
try {
return (await getSetting()).userPreferences.filteredLanguages;
} catch (error) {
if (ingoreConfigNotFountError && error instanceof config_not_found_default) return [];
throw error;
}
};
//#endregion
//#region lib/routes/mangadex/_utils.ts
/**
* Get the first value that matches the keys in the source object
*
* @param source the source object
* @param keys the keys to search
* @returns the first match value, or the first value as fallback
*/
const firstMatch = (source, keys) => {
for (const key of keys) {
const value = source instanceof Map ? source.get(key) : source[key];
if (value) return value;
}
return Object.values(source)[0];
};
/**
* Convert parameters to query string
*
* @param params parameters to be converted to query string
* @returns the query string
* @usage toQueryString({ a: 1, b: '2', c: [3, 4], d: {5: 'five', 6: 'six'} })
* >> '?a=1&b=2&c[]=3&c[]=4&d[5]=five&d[6]=six'
*/
function toQueryString(params) {
const queryParts = [];
for (const [key, value] of Object.entries(params)) if (typeof value === "object" && !Array.isArray(value) && !(value instanceof Set)) {
for (const [subKey, subValue] of Object.entries(value)) if (typeof subValue === "string" || typeof subValue === "number" || typeof subValue === "boolean") queryParts.push(`${encodeURIComponent(key)}[${encodeURIComponent(subKey)}]=${encodeURIComponent(subValue)}`);
} else if (Array.isArray(value) || value instanceof Set) for (const item of value) queryParts.push(`${encodeURIComponent(key)}[]=${encodeURIComponent(item)}`);
else queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
if (queryParts.length === 0) return "";
return "?" + queryParts.join("&");
}
//#endregion
//#region lib/routes/mangadex/_feed.ts
/**
* 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, needCover = true, lang) => {
const includes = needCover ? ["cover_art"] : [];
const rawMangaMeta = await cache_default.tryGet(`mangadex:manga-meta:${id}`, async () => {
const { data } = await got_default.get(`${_constants_default.API.MANGA_META}${id}${toQueryString({ includes })}`);
if (data.result === "error") throw new Error(data.errors[0].detail);
return data.data;
});
const relationships = rawMangaMeta.relationships || [];
const languages = [
...typeof lang === "string" ? [lang] : lang || [],
...await getFilteredLanguages(),
rawMangaMeta.attributes.originalLanguage
].filter(Boolean);
const title = firstMatch({
...rawMangaMeta.attributes.title,
...Object.fromEntries(rawMangaMeta.attributes.altTitles.flatMap((element) => Object.entries(element)))
}, languages);
const description = firstMatch(rawMangaMeta.attributes.description, languages);
if (!needCover) return {
title,
description
};
const coverFilename = relationships.find((relationship) => relationship.type === "cover_art")?.attributes.fileName + ".512.jpg";
return {
title,
description,
cover: `${_constants_default.API.COVER_IMAGE}${id}/${coverFilename}`
};
};
/**
* 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']);
*/
async function getMangaMetaByIds(ids, needCover = true, lang) {
const deDuplidatedIds = [...new Set(ids)].sort();
const includes = needCover ? ["cover_art"] : [];
const rawMangaMetas = await cache_default.tryGet(`mangadex:manga-meta:${md5(deDuplidatedIds.join(""))}`, async () => {
const { data } = await got_default.get(_constants_default.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;
});
const languages = [...typeof lang === "string" ? [lang] : lang || [], ...await getFilteredLanguages()].filter(Boolean);
const map = /* @__PURE__ */ new Map();
for (const rawMangaMeta of rawMangaMetas) {
const id = rawMangaMeta.id;
const title = firstMatch({
...rawMangaMeta.attributes.title,
...Object.fromEntries(rawMangaMeta.attributes.altTitles.flatMap((element) => Object.entries(element)))
}, [...languages, rawMangaMeta.attributes.originalLanguage]);
const description = firstMatch(rawMangaMeta.attributes.description, languages);
let cover;
let manga = {
id,
title,
description,
cover
};
if (needCover) {
const coverFilename = rawMangaMeta.relationships.find((relationship) => relationship.type === "cover_art")?.attributes.fileName;
if (coverFilename) {
cover = `${_constants_default.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, lang, limit) => {
const languages = new Set([...typeof lang === "string" ? [lang] : lang || [], ...await getFilteredLanguages()].filter(Boolean));
const url = `${_constants_default.API.MANGA_META}${id}/feed${toQueryString({
order: { publishAt: "desc" },
limit: limit || 100,
translatedLanguage: languages
})}`;
const chapters = await cache_default.tryGet(`mangadex:manga-chapters:${id}`, async () => {
const { data } = await got_default.get(url);
if (data.result === "error") throw new Error(data.errors[0].detail);
return data.data;
}, config.cache.routeExpire, false);
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_default.API.MANGA_CHAPTERS}${chapter.id}`,
pubDate: new Date(chapter.attributes.publishAt)
}));
};
/**
* 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, needCover = true, lang) => {
const [meta, chapters] = await Promise.all([getMangaMeta(id, needCover, lang), getMangaChapters(id, lang)]);
return {
...meta,
chapters
};
};
//#endregion
export { getFilteredLanguages as a, toQueryString as i, getMangaDetails as n, _access_default as o, getMangaMetaByIds as r, _constants_default as s, getMangaChapters as t };