UNPKG

@mohtasimalam/hentai.js

Version:

A library that fetches hentai data from different sources.

894 lines (883 loc) 31.2 kB
// src/sources/video/hanime.ts import { load } from "cheerio"; var HANIME_BASE_URL = "https://hanime.tv"; var HANIME_SEARCH_URL = "https://search.htv-services.com"; var HANIME_SIGNATURE_GENERATOR = () => Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join(""); var HAnime = class { BASE_URL = HANIME_BASE_URL; SEARCH_URL = HANIME_BASE_URL; generateSignature = HANIME_SIGNATURE_GENERATOR; /** * Creates a new instance of the HAnime client. * * @param {HAnimeOptions} [options] - Configuration options for the HAnime client. * @param {string} [options.baseUrl] - Custom base URL for the HAnime website. * @param {string} [options.searchUrl] - Custom search API URL. * @param {function(): string} [options.signatureGenerator] - Custom function to generate request signatures. */ constructor(options) { this.BASE_URL = options?.baseUrl || HANIME_BASE_URL; this.SEARCH_URL = options?.searchUrl || HANIME_SEARCH_URL; this.generateSignature = options?.signatureGenerator || HANIME_SIGNATURE_GENERATOR; } /** * Searches for videos on Hanime.tv based on the provided query. * * @param {string} query - The search query string. * @param {number} [page=1] - The page number to retrieve (default is 1). * @param {number} [perPage=10] - The number of results per page (default is 10). * @returns {Promise<PaginatedResult<HAnimeSearchResult>>} A promise that resolves to a paginated result of search results. */ search = async (query, page = 1, perPage = 10) => { if (!query) { throw new Error("Search query cannot be empty"); } let validPage = page; if (validPage < 1) { validPage = 1; } let validPerPage = perPage; if (validPerPage < 1) { validPerPage = 10; } try { const response = await fetch(this.SEARCH_URL, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ blacklist: [], brands: [], order_by: "created_at_unix", page: validPage - 1, tags: [], search_text: query.trim(), tags_mode: "AND" }) }); if (!response.ok) { throw new Error(`Search request failed with status: ${response.status}`); } const data = await response.json(); if (!data.hits) { return { results: [], total: 0, page, pages: 0, hasNextPage: false }; } let allResults = []; try { allResults = data.hits.map((result) => this.mapToSearchResult(result)); } catch (error) { console.error("Failed to parse search results:", error); throw new Error("Failed to parse search results"); } const totalResults = data.nbHits || 0; const totalPages = Math.max(1, Math.ceil(totalResults / validPerPage)); const finalPage = Math.min(Math.max(1, validPage), totalPages); const startIndex = (finalPage - 1) * validPerPage; const endIndex = Math.min(startIndex + validPerPage, allResults.length); const results = allResults.slice(startIndex, endIndex); return { results, total: totalResults, page: finalPage, pages: totalPages, previous: finalPage > 1 ? finalPage - 1 : void 0, next: finalPage < totalPages ? finalPage + 1 : void 0, hasNextPage: finalPage < totalPages }; } catch (error) { console.error("Search error:", error); throw new Error( `Failed to search HAnime: ${error instanceof Error ? error.message : String(error)}` ); } }; /** * Retrieves detailed information about a specific video by its slug. * * @param {string} slug - The unique slug identifier for the video. * @returns {Promise<HAnimeVideoInfo>} A promise that resolves to detailed information about the video. */ getInfo = async (slug) => { const path = `/videos/hentai/${slug}`; const url = `${this.BASE_URL}${path}`; const response = await fetch(url); const html = await response.text(); const $ = load(html); const script = $('script:contains("window.__NUXT__")'); const scriptHtml = script.html(); const json = JSON.parse( scriptHtml?.replace("window.__NUXT__=", "").replaceAll(";", "") || "{}" ); const videoData = json.state.data.video; return { title: json.state.data.video.hentai_franchise.name, slug: json.state.data.video.hentai_franchise.slug, id: videoData.hentai_video.id, description: videoData.hentai_video.description, views: videoData.hentai_video.views, interests: videoData.hentai_video.interests, posterUrl: videoData.hentai_video.poster_url, coverUrl: videoData.hentai_video.cover_url, brand: { name: videoData.hentai_video.brand, id: videoData.hentai_video.brand_id }, durationMs: videoData.hentai_video.duration_in_ms, isCensored: videoData.hentai_video.is_censored, likes: videoData.hentai_video.likes, rating: videoData.hentai_video.rating, dislikes: videoData.hentai_video.dislikes, downloads: videoData.hentai_video.downloads, rankMonthly: videoData.hentai_video.monthly_rank, tags: videoData.hentai_tags, createdAt: videoData.hentai_video.created_at, releasedAt: videoData.hentai_video.released_at, episodes: { next: this.mapToEpisode(videoData.next_hentai_video), all: json.state.data.video.hentai_franchise_hentai_videos.map(this.mapToEpisode), random: this.mapToEpisode(videoData.next_random_hentai_video) } }; }; /** * Retrieves stream information for a specific video episode by its slug. * * @param {string} slug - The unique slug identifier for the video episode. * @returns {Promise<HAnimeStream[]>} A promise that resolves to an array of available video streams. */ getEpisode = async (slug) => { const apiUrl = `${this.BASE_URL}/rapi/v7/videos_manifests/${slug}`; const signature = this.generateSignature(); const response = await fetch(apiUrl, { headers: { "x-signature": signature, "x-time": Math.floor(Date.now() / 1e3).toString(), "x-signature-version": "web2" } }); const json = await response.json(); const data = json.videos_manifest; const videos = data.servers.flatMap((server) => server.streams); const streams = videos.map((video) => ({ id: video.id, serverId: video.server_id, kind: video.kind, extension: video.extension, mimeType: video.mime_type, width: video.width, height: video.height, durationInMs: video.duration_in_ms, filesizeMbs: video.filesize_mbs, filename: video.filename, url: video.url })).filter((video) => video.url && video.url !== "" && video.kind !== "premium_alert"); return streams; }; mapToSearchResult = (raw) => { return { id: raw.id, name: raw.name, titles: raw.titles, slug: raw.slug, description: raw.description, views: raw.views, interests: raw.interests, bannerImage: raw.poster_url, coverImage: raw.cover_url, brand: { name: raw.brand, id: raw.brand_id }, durationMs: raw.duration_in_ms, isCensored: raw.is_censored, likes: raw.likes, rating: raw.rating, dislikes: raw.dislikes, downloads: raw.downloads, rankMonthly: raw.monthly_rank, tags: typeof raw.tags === "object" && Array.isArray(raw.tags) ? raw.tags : JSON.parse(raw.tags), createdAt: raw.created_at, releasedAt: raw.released_at }; }; mapToEpisode = (raw) => { return { id: raw.id, name: raw.name, slug: raw.slug, views: raw.views, interests: raw.interests, thumbnailUrl: raw.poster_url, coverUrl: raw.cover_url, isHardSubtitled: raw.is_hard_subtitled, brand: { name: raw.brand, id: raw.brand_id }, durationMs: raw.duration_in_ms, isCensored: raw.is_censored, likes: raw.likes, rating: raw.rating, dislikes: raw.dislikes, downloads: raw.downloads, rankMonthly: raw.monthly_rank, brandId: raw.brand_id, isBannedIn: raw.is_banned_in, previewUrl: raw.preview_url, color: raw.primary_color, createdAt: raw.created_at_unix, releasedAt: raw.released_at_unix }; }; }; // src/sources/video/hentai-haven.ts import { load as load2 } from "cheerio"; import { parse } from "date-fns"; // src/utils/get-number-from-string.ts function getNumberFromString(str) { if (typeof str !== "string") { throw new TypeError("Input must be a string"); } if (str.trim() === "") { return null; } const numbers = str.match(/\d+/g); return numbers ? Number(numbers[0]) : null; } // src/utils/rot13.ts var rot13Cipher = (str) => { if (typeof str !== "string") { throw new TypeError("Input must be a string"); } if (str.length === 0) { return ""; } return str.replace(/[a-zA-Z]/g, (c) => { const charCode = c.charCodeAt(0); const isUpperCase = charCode >= 65 && charCode <= 90; const shiftedCharCode = isUpperCase ? (charCode - 65 + 13) % 26 + 65 : (charCode - 97 + 13) % 26 + 97; return String.fromCharCode(shiftedCharCode); }); }; // src/sources/video/hentai-haven.ts var HENTAI_HAVEN_URL = "http://hentaihaven.xxx"; var HentaiHaven = class { /** * Base URL for API requests. */ BASE_URL = HENTAI_HAVEN_URL; /** * Creates a new instance of the HentaiHaven client. * * @param {HentaiHavenOptions} options - Configuration options for the HentaiHaven client. * @param {string} [options.baseUrl] - Custom base URL for the HentaiHaven website. */ constructor(options) { this.BASE_URL = options?.baseUrl || HENTAI_HAVEN_URL; } /** * Searches for hentai videos on Hentai Haven based on the provided query. * * @param {string} query - The search query string. * @returns {Promise<HHSearchResult[]>} A promise that resolves to an array of search results. * @throws {TypeError} If the query is empty or not a string. */ search = async (query) => { if (!query || typeof query !== "string") { throw new TypeError("Invalid query in search."); } const url = `${this.BASE_URL}/?s=${query}&post_type=wp-manga`; const response = await fetch(url); const data = await response.text(); const $ = load2(data); const results = []; $(".c-tabs-item__content").each((_i, el) => { const cover = $(el).find(".c-image-hover img").attr("src") || ""; const id = $(el).find(".c-image-hover a").attr("href")?.split("/")[4] || ""; const title = $(el).find(".post-title h3").text().trim(); const alternative = $(el).find(".tab-summary .mg_alternative .summary-content").text().trim(); const author = $(el).find(".tab-summary .mg_author .summary-content").text().trim(); const released = Number( $(el).find(".tab-summary .mg_release .summary-content").text().trim() ); const totalEpisodes = getNumberFromString($(el).find(".tab-meta .latest-chap .chapter").text().trim()) || 0; const dateString = $(el).find(".tab-meta .post-on").text().trim(); const parsedDate = parse(dateString, "MMM dd, yyyy", /* @__PURE__ */ new Date()); const rating = Number($(el).find(".tab-meta .rating .total_votes").text().trim()); const genres = []; $(".tab-summary .mg_genres .summary-content a").each((_, element) => { genres.push({ id: $(element).attr("href")?.split("/")[4] || "", url: $(element).attr("href") || "", name: $(element).text().trim().replaceAll(",", "") }); }); results.push({ id, title, cover: cover.replaceAll(" ", "%20"), rating, released, genres, totalEpisodes, date: { unparsed: dateString, parsed: parsedDate }, alternative, author }); }); return results; }; /** * Retrieves detailed information about a hentai series by its ID. * * @param {string} id - The unique identifier of the hentai series. * @param {HHEpisodesSort} [episodeSort="ASC"] - The sort order for episodes (ascending or descending). * @returns {Promise<HHHentaiInfo>} A promise that resolves to detailed information about the hentai series. * @throws {Error} If the ID is not provided or if there's an error fetching the data. */ getInfo = async (id, episodeSort = "ASC") => { if (!id) { throw new Error("Id is required"); } const url = `${this.BASE_URL}/watch/${id}`; const response = await fetch(url); const data = await response.text(); if (data === "" || !data) { throw new Error("Error fetching data"); } const $ = load2(data); if ($("body").text().includes("webpage has been blocked")) { throw new Error(`The webpage is blocked. Consider using a CORS proxy. GET ${url}`); } const title = $(".post-title h1").text().trim(); const cover = $(".summary_image img").attr("src") || ""; const ratingCount = Number($('span[property="ratingCount"]').text().trim()); const views = getNumberFromString( $(".post-content_item:nth-child(4) .summary-content").text() ); const released = Number($(".post-status .summary-content a").text().trim()); const summary = $(".description-summary p").text().trim(); const genres = []; const episodes = []; $(".genres-content a").each((_i, el) => { genres.push({ id: $(el).attr("href")?.split("/")[4] || "", url: $(el).attr("href") || "", name: $(el).text().trim() }); }); const episodesLength = $("li.wp-manga-chapter").length; $("li.wp-manga-chapter").each((i, el) => { const thumbnail = $(el).find("img").attr("src"); const id2 = `${$(el).find("a").attr("href")?.split("/")[4]}/${$(el).find("a").attr("href")?.split("/")[5]}`; const title2 = $(el).find("a").text().trim(); const number = episodesLength - i; const released2 = $(el).find(".chapter-release-date").text().trim(); const releasedUTC = parse(released2, "MMMM dd, yyyy", /* @__PURE__ */ new Date()); episodes.push({ // Episode id spoofing cause the API doesn't return the episode id, it returns a path. id: btoa(id2), title: title2, thumbnail, number, releasedUTC, releasedRelative: released2 }); }); this.sortEpisodes(episodes, episodeSort); return { id, title, cover: cover ? cover.replaceAll(" ", "%20") : "", summary, views, ratingCount, released, genres, totalEpisodes: episodesLength, episodes }; }; /** * Retrieves streaming sources for a specific episode by its ID. * * @param {string} id - The encoded episode ID. * @returns {Promise<HHHentaiSources>} A promise that resolves to the streaming sources for the episode. * @throws {TypeError} If the ID is invalid or not provided. * @throws {Error} If the episode ID is not properly encoded. */ getEpisode = async (id) => { if (!id || typeof id !== "string") { throw new TypeError("Invalid identifier"); } if (id?.includes("episode-")) { throw new Error("The Episode ID must be encoded."); } const pageUrl = `${this.BASE_URL}/watch/${atob(id)}`; const pageResponse = await fetch(pageUrl); const pageHtml = await pageResponse.text(); const $page = load2(pageHtml); const iframeSrc = $page(".player_logic_item > iframe").attr("src"); const iframeResponse = await fetch(iframeSrc || ""); const iframeHtml = await iframeResponse.text(); const $iframe = load2(iframeHtml); const secureToken = $iframe('meta[name="x-secure-token"]').attr("content")?.replace("sha512-", ""); const rotatedSha = rot13Cipher(secureToken || ""); const firstDecode = atob(rotatedSha); const secondRotate = rot13Cipher(firstDecode); const secondDecode = atob(secondRotate); const thirdRotate = rot13Cipher(secondDecode); const decryptedData = JSON.parse(atob(thirdRotate)); const formData = new FormData(); formData.append("action", "zarat_get_data_player_ajax"); formData.append("a", decryptedData.en); formData.append("b", decryptedData.iv); const apiUrl = `${decryptedData.uri || "https://hentaihaven.xxx/wp-content/plugins/player-logic/"}api.php`; const apiResponse = await (await fetch(apiUrl, { method: "POST", body: formData, mode: "cors", cache: "default" })).json(); const sources = apiResponse.data.sources; const thumbnail = apiResponse.data.image; return { sources, thumbnail }; }; /** * Sorts an array of episodes based on the specified sort order. * * @param {HHHentaiEpisode[]} episodes - The array of episodes to sort. * @param {HHEpisodesSort} sortOrder - The sort order to apply ("ASC" for ascending, "DESC" for descending). */ sortEpisodes(episodes, sortOrder) { episodes.sort((a, b) => { if (sortOrder === "ASC") { return a.number - b.number; } return b.number - a.number; }); } }; // src/sources/video/hentai-stream.ts import { load as load3 } from "cheerio"; import { parseDate } from "chrono-node"; // src/utils/normalize.ts function normalize(str) { if (typeof str !== "string") { throw new TypeError("Input must be a string"); } if (str.trim() === "") { return str; } const normalized = str.replace(/[^a-zA-Z\s]/g, " "); return normalized.toLowerCase().replace(/\s{2,}/g, " ").replaceAll("episode", ""); } // src/utils/remove-number-from-string.ts function removeNumberFromString(str) { if (typeof str !== "string") { throw new TypeError("Input must be a string"); } if (str.trim() === "") { return str; } return str.replace(/\d+/g, ""); } // src/sources/video/hentai-stream.ts var HENTAI_STREAM_BASE_URL = "https://tube.hentaistream.com"; var HentaiStream = class { BASE_URL = HENTAI_STREAM_BASE_URL; /** * Creates a new HentaiStream instance * @param options - Configuration options */ constructor(options) { this.BASE_URL = options?.baseUrl || HENTAI_STREAM_BASE_URL; } /** * Search for anime on HentaiStream * @param query - The search query * @returns Promise resolving to an array of search results * @throws {TypeError} If query is invalid */ search = async (query) => { if (!query || typeof query !== "string") { throw new TypeError("Invalid Query"); } const url = `${this.BASE_URL}/?s=${encodeURIComponent(query)}`; const response = await fetch(url); const data = await response.text(); const $ = load3(data); const results = []; $(".content .post").each((_i, e) => { const $e = $(e); const id = ($e.find("div.postimg a").attr("href") || "").split("/").pop() || ""; const title = $e.find("p.posttitle ins").text().trim() || ""; const image = $e.find("div.postimg img").attr("src"); const views = Number.parseInt($e.find(".view").text().trim().split(" ")[0].replaceAll(",", "")) || 0; const releaseDate = parseDate($e.find(".dtcreated").text().trim().split("Added: ")[1].trim()); results.push({ id, image, title, views, releaseDate }); }); return results; }; /** * Get information about a specific episode * @param id - The episode ID * @returns Promise resolving to episode information * @throws {TypeError} If ID is invalid */ getInfoEpisode = async (id) => { if (!id || typeof id !== "string") { throw new TypeError("Invalid ID."); } const url = `${this.BASE_URL}/${id}`; const response = await fetch(url); const data = await response.text(); const $ = load3(data); const title = $(".videotitle").text().trim().replaceAll("\xA4", "").trim(); const releasedDate = parseDate( $(".threebox p:nth-child(1)").text().trim().split("Added: ")[1].split(" @")[0].trim() ); const views = Number.parseInt( $(".threebox p:nth-child(2)").text().trim().split("Views: ")[1].replaceAll(",", "") ); const genres = $('div.videotags:contains("Genre(s)") a').map((_i, e) => $(e).text().trim()).get(); return { title, releasedDate, views, genres }; }; /** * Get detailed information about an anime series * @param id - The anime ID or title * @returns Promise resolving to anime information or null if not found * @throws {TypeError} If ID is invalid */ getInfo = async (id) => { if (!id || typeof id !== "string") { throw new TypeError("Invalid ID."); } const normalizedId = normalize(id); const searchData = await this.search(normalizedId); const matchingResults = searchData.filter((result) => { const normalizedTitle = normalize(result.title || ""); return normalizedTitle.includes(normalizedId); }); if (matchingResults.length === 0) { return null; } const firstResult = matchingResults[0]; const episodes = await Promise.all( matchingResults.map(async (result) => { const episodeInfo = await this.getInfoEpisode(result.id); const episodeNumber = getNumberFromString(result.title || ""); return { ...result, ...episodeInfo, episodeNumber }; }) ); episodes.sort((a, b) => { const aNum = a.episodeNumber || 0; const bNum = b.episodeNumber || 0; return aNum - bNum; }); const averageViews = Math.ceil( episodes.reduce((sum, episode) => sum + (episode.views || 0), 0) / episodes.length ); const genres = new Set(episodes.flatMap((episode) => episode.genres || [])); return { title: (normalizedId.charAt(0).toUpperCase() + normalizedId.slice(1)).trim(), image: firstResult.image, genres: [...genres.values()], views: averageViews, episodes: episodes.map((ep) => ({ id: btoa(ep.id), number: ep.episodeNumber, views: ep.views, releasedDate: ep.releaseDate, title: ep.title, image: ep.image })), releasedDate: episodes[0].releaseDate }; }; /** * Get streaming information for a specific episode * @param id - The encoded episode ID * @returns Promise resolving to episode streaming information * @throws {TypeError} If ID is invalid * @throws {Error} If streams cannot be fetched */ getEpisode = async (id) => { if (!id || typeof id !== "string") { throw new TypeError("Invalid ID."); } const url = `${this.BASE_URL}/${atob(id)}`; const response = await fetch(url); const data = await response.text(); const $ = load3(data); const frameUrl = $("iframe").attr("src"); const title = $(".videotitle").text().trim().replaceAll("\xA4", "").trim(); const releasedDate = parseDate( $(".threebox p:nth-child(1)").text().trim().split("Added: ")[1].split(" @")[0].trim() ); const views = Number.parseInt( $(".threebox p:nth-child(2)").text().trim().split("Views: ")[1].replaceAll(",", "") ); if (!frameUrl) { throw new Error("Failed to fetch streams"); } const res = await fetch(frameUrl); const frameData = await res.text(); const $$ = load3(frameData); const $video = $$("video"); const source = $video.find("source").attr("src"); return { title, releasedDate, views, source }; }; }; // src/sources/gallery/r34.ts import { load as load4 } from "cheerio"; // src/utils/Dimension.ts var Dimension = class _Dimension { width; height; constructor(width, height) { this.width = width; this.height = height; } getAspectRatio() { const gcd = this.gcd(this.width, this.height); return `${this.width / gcd}:${this.height / gcd}`; } getWidthInPx() { return this.width; } getHeightInPx() { return this.height; } getWidthInRem(baseFontSize = 16) { return this.width / baseFontSize; } getHeightInRem(baseFontSize = 16) { return this.height / baseFontSize; } gcd(a, b) { if (b === 0) { return a; } return this.gcd(b, a % b); } static fromString(dimensionString) { const [width, height] = dimensionString.split("x").map(Number); if (Number.isNaN(width) || Number.isNaN(height) || width <= 0 || height <= 0) { return null; } return new _Dimension(width, height); } }; // src/sources/gallery/r34.ts var RULE34_BASE_URL = "https://rule34.xxx"; var RULE34_API_URL = "https://ac.rule34.xxx"; var Rule34 = class { /** * Base URL for the Rule34 website. */ BASE_URL = RULE34_BASE_URL; /** * API URL for Rule34 autocomplete functionality. */ API_URL = RULE34_API_URL; /** * Creates a new instance of the Rule34 client. * * @param {R34Options} [options] - Configuration options for the Rule34 client. * @param {string} [options.baseUrl] - Custom base URL for the Rule34 website. * @param {string} [options.apiUrl] - Custom API URL for Rule34 autocomplete functionality. */ constructor(options) { this.BASE_URL = options?.baseUrl || RULE34_BASE_URL; this.API_URL = options?.apiUrl || RULE34_API_URL; } /** * Searches for autocomplete suggestions based on the provided query. * * @param {string} query - The search query string. * @returns {Promise<Array<{completedQuery: string, label: string, type: string}>>} A promise that resolves to an array of autocomplete suggestions. * @throws {TypeError} If the query is empty or not a string. */ searchAutocomplete = async (query) => { if (!query || typeof query !== "string") { throw new TypeError("Query invalid"); } const url = `${this.API_URL}/autocomplete.php?q=${query}`; const response = await fetch(url); const data = await response.json(); return data.map((item) => ({ completedQuery: item.value, label: item.label, type: item.type })); }; /** * Searches for images on Rule34 based on the provided query. * * @param {string} query - The search query string. * @param {number} [page=1] - The page number to retrieve (default is 1). * @param {number} [perPage=10] - The number of results per page (default is 10). * @returns {Promise<R34SearchResult>} A promise that resolves to a paginated result of search results. * @throws {TypeError} If the query is empty or not a string. */ search = async (query, page = 1, perPage = 10) => { if (!query || typeof query !== "string") { throw new TypeError("Query invalid"); } const url = `${this.BASE_URL}/index.php?page=post&s=list&tags=${query}&pid=${(page - 1) * perPage}`; const response = await fetch(url); const data = await response.text(); const $ = load4(data); const results = []; $(".image-list span").each((_i, e) => { const $e = $(e); const id = $e.attr("id")?.replace("s", "") || ""; const image = $e.find("img").attr("src") || ""; const tags = $e.find("img").attr("alt")?.trim()?.split(" ").filter((tag) => tag !== "") || []; results.push({ id, image, tags, type: "preview" }); }); const pagination = $("#paginator .pagination"); const totalPages = Number.parseInt(pagination.find("a:last").attr("href")?.split("pid=")[1] || "1", 10) / perPage + 1; const currentPage = page; const nextPage = currentPage < totalPages ? currentPage + 1 : null; const previousPage = currentPage > 1 ? currentPage - 1 : null; const hasNextPage = nextPage !== null; const next = nextPage !== null ? nextPage * perPage : 0; const previous = previousPage !== null ? previousPage * perPage : 0; return { total: totalPages * perPage, next, previous, pages: totalPages, page: currentPage, hasNextPage, results }; }; /** * Gets detailed information about a specific image by its ID. * * @param {string} id - The ID of the image to retrieve information for. * @returns {Promise<R34ImageInfo>} A promise that resolves to an object containing detailed information about the image. */ getInfo = async (id) => { const url = `${this.BASE_URL}/index.php?page=post&s=view&id=${id}`; const resizeCookies = { "resize-notification": 1, "resize-original": 1 }; const [resizedResponse, nonResizedResponse] = await Promise.all([ fetch(url), fetch(url, { headers: { cookie: Object.entries(resizeCookies).map(([key, value]) => `${key}=${value}`).join("; ") } }) ]); const [resized, original] = await Promise.all([ resizedResponse.text(), nonResizedResponse.text() ]); const $resized = load4(resized); const resizedImageUrl = $resized("#image").attr("src"); const $ = load4(original); const fullImage = $("#image").attr("src"); const tags = $("#image").attr("alt")?.trim()?.split(" ").filter((tag) => tag !== ""); const stats = $("#stats ul"); const postedData = stats.find("li:nth-child(2)").text().trim(); const createdAt = new Date(postedData.split("Posted: ")[1].split("by")[0]).getTime(); const publishedBy = postedData.split("by")[1].trim(); const size = stats.find("li:nth-child(3)").text().trim().split("Size: ")[1]; const rating = stats.find("li:contains('Rating:')").text().trim().split("Rating: ")[1]; const dimension = Dimension.fromString(size); const comments = $("#comment-list div").map((_i, el) => { const $el = $(el); const id2 = $el.attr("id")?.replace("c", ""); const user = $el.find(".col1").text().trim().split("\n")[0]; const comment = $el.find(".col2").text().trim(); return { id: id2, user, comment }; }).get().filter(Boolean).filter((comment) => comment.comment !== ""); return { id, fullImage, resizedImageUrl, tags, createdAt, publishedBy, rating, sizes: { aspect: dimension?.getAspectRatio(), width: dimension?.getWidthInPx(), height: dimension?.getHeightInPx(), widthRem: dimension?.getWidthInRem(), heightRem: dimension?.getHeightInRem(), fullSize: dimension ? dimension.getWidthInPx() * dimension.getHeightInPx() : void 0, formatted: `${dimension?.getWidthInPx()}x${dimension?.getHeightInPx()}` }, comments }; }; }; export { Dimension, HANIME_BASE_URL, HANIME_SEARCH_URL, HANIME_SIGNATURE_GENERATOR, HAnime, HENTAI_HAVEN_URL, HENTAI_STREAM_BASE_URL, HentaiHaven, HentaiStream, RULE34_API_URL, RULE34_BASE_URL, Rule34, getNumberFromString, normalize, removeNumberFromString, rot13Cipher }; //# sourceMappingURL=index.js.map