mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
v1.5.0: Complete documentation rewrite - 50+ files with AlisaReactionBot style
- Box-style header comments (Project, Author, License) on every file - Section headers using double-line box decorators - Feature markers (// ---- FEATURE: XYZ ----) before every major function - JSDoc comments for EVERY function with @param, @returns, @example - Inline notes (NOTE, WARNING, TIP) for non-obvious logic - Footer END markers on every module - Matched code style to AlisaReactionBot repository - All files now searchable by feature name via grep Author: Shinei Nouzen <157171073+Shineii86@users.noreply.github.com>
This commit is contained in:
@@ -1,47 +1,104 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — animeInfo.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts detailed anime information from anikototv.to including title,
|
||||
* synopsis, metadata (type, status, studios, producers, genres), ratings,
|
||||
* and background artwork for a given anime slug.
|
||||
*
|
||||
* @exports
|
||||
* extractAnimeInfo
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// ANIME INFO EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract full anime detail page data for a given slug ----
|
||||
/**
|
||||
* Fetches the anime detail page from anikototv.to and parses all metadata
|
||||
* including titles, synopsis, ratings, studios, producers, genres, and
|
||||
* background artwork.
|
||||
*
|
||||
* @param {string} slug - The anime slug identifier (e.g. "naruto-shippuden")
|
||||
* @returns {Promise<Object>} Full anime info object with all extracted fields
|
||||
*
|
||||
* @example
|
||||
* const info = await extractAnimeInfo("one-piece");
|
||||
* console.log(info.title); // "One Piece"
|
||||
* console.log(info.studios); // ["Toei Animation"]
|
||||
*/
|
||||
const extractAnimeInfo = async (slug) => {
|
||||
try {
|
||||
const url = URLS.watch(slug);
|
||||
const { data } = await axios.get(url, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
// ---- FEATURE: Extract title and identification fields ----
|
||||
// NOTE: itemprop='name' is used for schema.org microdata on the site
|
||||
const title = $("h1[itemprop='name'].title.d-title").text().trim() || "";
|
||||
// NOTE: data-jp attribute holds the Japanese title variant
|
||||
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
||||
const altNames = $(".names.font-italic").text().trim() || "";
|
||||
const poster = $("img[itemprop='image']").attr("src") || "";
|
||||
const synopsis = $(".synopsis .content").text().trim() || "";
|
||||
|
||||
// ---- FEATURE: Extract rating and review metadata ----
|
||||
// NOTE: Multiple rating selectors are checked — .score .value is the display score,
|
||||
// itemprop='ratingValue' is the microdata fallback
|
||||
const rating = $("#w-rating .score .value").text().trim() || "";
|
||||
const ratingValue = $("#w-rating span[itemprop='ratingValue']").text().trim() || "";
|
||||
const reviewCount = $("#w-rating span[itemprop='reviewCount']").text().trim() || "";
|
||||
// NOTE: data-id on #watch-main is the internal anime ID used for AJAX calls
|
||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||
|
||||
// ---- FEATURE: Extract anime metadata (type, premiere, aired, status) ----
|
||||
// NOTE: .bmeta contains the structured metadata block — first child holds basic info
|
||||
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
||||
const premiered = $(".bmeta .meta:first-child > div:nth-child(2) span").text().trim() || "";
|
||||
const aired = $(".bmeta .meta:first-child > div:nth-child(3) span").text().trim() || "";
|
||||
const status = $(".bmeta .meta:first-child > div:nth-child(4) span a").text().trim() || "";
|
||||
|
||||
// ---- FEATURE: Extract secondary metadata (MAL score, duration, episodes) ----
|
||||
// NOTE: Second .meta child holds MAL-linked and production metadata
|
||||
const malScore = $(".bmeta .meta:nth-child(2) > div:nth-child(1) span").text().trim() || "";
|
||||
const duration = $(".bmeta .meta:nth-child(2) > div:nth-child(2) span").text().trim() || "";
|
||||
const episodes = $(".bmeta .meta:nth-child(2) > div:nth-child(3) span").text().trim() || "";
|
||||
|
||||
// ---- FEATURE: Extract studios list ----
|
||||
// NOTE: itemprop='director' links on studio spans — itemprop='name' inside holds the name
|
||||
const studios = [];
|
||||
$(".bmeta .meta:nth-child(2) > div:nth-child(4) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
||||
studios.push($(el).text().trim());
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract producers list ----
|
||||
// NOTE: Same selector pattern as studios but from the 5th child (producers row)
|
||||
const producers = [];
|
||||
$(".bmeta .meta:nth-child(2) > div:nth-child(5) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
||||
producers.push($(el).text().trim());
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract genres ----
|
||||
// NOTE: Genre links contain '/genre/' in their href — filters out non-genre links
|
||||
const genres = [];
|
||||
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
||||
genres.push($(el).text().trim());
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract background artwork from player element ----
|
||||
// NOTE: The #player div has a CSS background-image set via inline style
|
||||
const backgroundImage = $("#player").css("background-image")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||
|
||||
return {
|
||||
@@ -71,4 +128,6 @@ const extractAnimeInfo = async (slug) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractAnimeInfo };
|
||||
export { extractAnimeInfo };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: animeInfo.extractor.js
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — azList.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts anime from A-Z listing pages with pagination.
|
||||
* Supports filtering by letter (a-z) or "all" for complete list.
|
||||
*
|
||||
* @exports
|
||||
* extractAzList
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
@@ -5,13 +22,46 @@ import { URLS } from "../configs/dataUrl.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// A-Z LIST EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract anime from A-Z listing with pagination ----
|
||||
/**
|
||||
* Fetches and parses anime from the A-Z listing page.
|
||||
* Supports filtering by letter (a-z) or "all" for complete list.
|
||||
*
|
||||
* @param {string} letter - Letter to filter by (a-z) or "all" (default: "all")
|
||||
* @param {number} page - Page number to fetch (default: 1)
|
||||
* @returns {Promise<Object>} Object containing totalPages, letter, and data array
|
||||
* @returns {number} return.totalPages - Total number of pages available
|
||||
* @returns {string} return.letter - The letter filter used
|
||||
* @returns {Array<Object>} return.data - Array of anime objects
|
||||
* @returns {string} return.data[].slug - URL slug for the anime
|
||||
* @returns {string} return.data[].poster - Poster image URL
|
||||
* @returns {string} return.data[].title - English title
|
||||
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||
* @returns {number} return.data[].sub - Subbed episode count
|
||||
* @returns {number} return.data[].dub - Dubbed episode count
|
||||
* @returns {number} return.data[].total - Total episode count
|
||||
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||
* @returns {string} return.data[].rating - Anime rating
|
||||
*
|
||||
* @example
|
||||
* const azList = await extractAzList("a", 1);
|
||||
* console.log(azList.letter); // "a"
|
||||
* console.log(azList.data[0].title); // First anime starting with "a"
|
||||
*/
|
||||
const extractAzList = async (letter = "all", page = 1) => {
|
||||
try {
|
||||
// NOTE: URLS.azList generates the correct endpoint for the given letter
|
||||
const url = URLS.azList(letter);
|
||||
const $ = await extractPages(url, page);
|
||||
const totalPages = countPages($);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: Multiple selectors to handle different page layouts
|
||||
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
@@ -23,6 +73,7 @@ const extractAzList = async (letter = "all", page = 1) => {
|
||||
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
results.push({
|
||||
slug,
|
||||
@@ -44,4 +95,6 @@ const extractAzList = async (letter = "all", page = 1) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractAzList };
|
||||
export { extractAzList };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: azList.extractor.js
|
||||
|
||||
@@ -1,11 +1,61 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — category.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts anime filtered by category (genre, type, status, etc.).
|
||||
* Supports multiple category types with automatic URL detection.
|
||||
*
|
||||
* @exports
|
||||
* extractCategory
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CATEGORY FILTER EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract anime filtered by category with pagination ----
|
||||
/**
|
||||
* Fetches and parses anime filtered by category.
|
||||
* Automatically detects category type (genre, type, status) from the input.
|
||||
*
|
||||
* @param {string} category - Category to filter by (e.g., "genre/action", "type/tv", "status/ongoing")
|
||||
* @param {number} page - Page number to fetch (default: 1)
|
||||
* @returns {Promise<Object>} Object containing totalPages and data array
|
||||
* @returns {number} return.totalPages - Total number of pages available
|
||||
* @returns {Array<Object>} return.data - Array of anime objects
|
||||
* @returns {string} return.data[].slug - URL slug for the anime
|
||||
* @returns {string} return.data[].animeId - Internal anime ID
|
||||
* @returns {string} return.data[].poster - Poster image URL
|
||||
* @returns {string} return.data[].title - English title
|
||||
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||
* @returns {number} return.data[].sub - Subbed episode count
|
||||
* @returns {number} return.data[].dub - Dubbed episode count
|
||||
* @returns {number} return.data[].total - Total episode count
|
||||
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||
* @returns {string} return.data[].rating - Anime rating
|
||||
*
|
||||
* @example
|
||||
* const actionAnime = await extractCategory("genre/action", 1);
|
||||
* console.log(actionAnime.data[0].title); // First action anime
|
||||
*
|
||||
* const tvAnime = await extractCategory("type/tv", 1);
|
||||
* console.log(tvAnime.data[0].title); // First TV anime
|
||||
*/
|
||||
const extractCategory = async (category, page = 1) => {
|
||||
try {
|
||||
// NOTE: Category type is determined by prefix in the category string
|
||||
let url;
|
||||
if (category.startsWith("genre/")) {
|
||||
url = URLS.genre(category.replace("genre/", ""));
|
||||
@@ -14,6 +64,7 @@ const extractCategory = async (category, page = 1) => {
|
||||
} else if (category.startsWith("status/")) {
|
||||
url = URLS.status(category.replace("status/", ""));
|
||||
} else {
|
||||
// NOTE: Fallback to direct URL construction for unknown category types
|
||||
url = `${URLS.home}/${category}`;
|
||||
}
|
||||
|
||||
@@ -21,6 +72,8 @@ const extractCategory = async (category, page = 1) => {
|
||||
const totalPages = countPages($);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: The list items are contained in #list-items container
|
||||
$("#list-items > .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
||||
@@ -33,6 +86,7 @@ const extractCategory = async (category, page = 1) => {
|
||||
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
||||
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
results.push({
|
||||
slug,
|
||||
@@ -55,4 +109,6 @@ const extractCategory = async (category, page = 1) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractCategory };
|
||||
export { extractCategory };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: category.extractor.js
|
||||
|
||||
@@ -1,14 +1,52 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — episodeList.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts the full episode list for a given anime from anikototv.to.
|
||||
* First resolves the anime ID from the watch page, then fetches the
|
||||
* episode list via AJAX endpoint.
|
||||
*
|
||||
* @exports
|
||||
* extractEpisodeList
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS, BASE_URL } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// EPISODE LIST EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract complete episode list for an anime by slug ----
|
||||
/**
|
||||
* Fetches the anime watch page to resolve the internal anime ID, then
|
||||
* queries the AJAX episode list endpoint to get all episodes with their
|
||||
* metadata (ID, number, slug, title, active state, href).
|
||||
*
|
||||
* @param {string} slug - The anime slug identifier (e.g. "naruto-shippuden")
|
||||
* @returns {Promise<Object>} Object with animeId, slug, totalEpisodes, and episodes array
|
||||
*
|
||||
* @example
|
||||
* const episodeData = await extractEpisodeList("one-piece");
|
||||
* console.log(episodeData.totalEpisodes); // e.g. 1100+
|
||||
* console.log(episodeData.episodes[0].episode_no); // 1
|
||||
*/
|
||||
const extractEpisodeList = async (slug) => {
|
||||
try {
|
||||
// NOTE: First fetch the watch page to extract the internal anime ID
|
||||
const infoUrl = URLS.watch(slug);
|
||||
const { data: infoData } = await axios.get(infoUrl, { headers });
|
||||
const $ = cheerio.load(infoData);
|
||||
|
||||
// NOTE: data-id on #watch-main is the anime ID used for the AJAX episode list call
|
||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||
|
||||
if (!animeId) {
|
||||
@@ -16,6 +54,7 @@ const extractEpisodeList = async (slug) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// NOTE: AJAX endpoint requires X-Requested-With header to return HTML fragment
|
||||
const ajaxUrl = `${BASE_URL}/ajax/episode/list/${animeId}`;
|
||||
const { data: ajaxData } = await axios.get(ajaxUrl, {
|
||||
headers: {
|
||||
@@ -26,13 +65,17 @@ const extractEpisodeList = async (slug) => {
|
||||
|
||||
const $ep = cheerio.load(ajaxData);
|
||||
|
||||
// ---- FEATURE: Parse episode list from AJAX response HTML ----
|
||||
// NOTE: Episode links have either data-num or data-ep-id attributes
|
||||
const episodes = [];
|
||||
$ep("a[data-num], a[data-ep-id]").each((i, el) => {
|
||||
const epId = $ep(el).attr("data-ep-id") || $ep(el).attr("data-id") || "";
|
||||
const epNum = parseInt($ep(el).attr("data-num")) || i + 1;
|
||||
const epSlug = $ep(el).attr("data-slug") || "";
|
||||
const href = $ep(el).attr("href") || "";
|
||||
// NOTE: Episode title is in .ep-title or .ep-name span
|
||||
const epTitle = $ep(el).find(".ep-title, .ep-name").text().trim() || "";
|
||||
// NOTE: Active class indicates the currently-playing episode
|
||||
const isActive = $ep(el).hasClass("active") || false;
|
||||
|
||||
episodes.push({
|
||||
@@ -52,6 +95,7 @@ const extractEpisodeList = async (slug) => {
|
||||
episodes
|
||||
};
|
||||
} catch (ajaxError) {
|
||||
// NOTE: Gracefully return empty list if AJAX call fails (e.g. network issue)
|
||||
return {
|
||||
animeId,
|
||||
slug,
|
||||
@@ -64,4 +108,6 @@ const extractEpisodeList = async (slug) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractEpisodeList };
|
||||
export { extractEpisodeList };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: episodeList.extractor.js
|
||||
|
||||
@@ -1,15 +1,58 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — episodeListAjax.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Low-level AJAX extractor for fetching raw episode list HTML from
|
||||
* anikototv.to's server-side endpoint. Supports optional style and VRF
|
||||
* query parameters for custom episode list rendering.
|
||||
*
|
||||
* @exports
|
||||
* extractEpisodeListAjax
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { BASE_URL } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// EPISODE LIST AJAX EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Fetch raw episode list HTML via AJAX with optional params ----
|
||||
/**
|
||||
* Directly queries the AJAX episode list endpoint for a given anime ID.
|
||||
* Returns the raw HTML response from the server — does not parse or
|
||||
* transform the data. Use this when you need the raw response or want
|
||||
* to apply custom style/VRF parameters.
|
||||
*
|
||||
* @param {number|string} animeId - The internal anime ID from anikototv.to
|
||||
* @param {string} [style=""] - Optional style parameter for episode list rendering
|
||||
* @param {string} [vrf=""] - Optional VRF parameter (verification/decryption key)
|
||||
* @returns {Promise<string>} Raw HTML response from the AJAX endpoint
|
||||
*
|
||||
* @example
|
||||
* const rawHtml = await extractEpisodeListAjax(12345);
|
||||
* console.log(rawHtml); // HTML fragment with episode links
|
||||
*
|
||||
* @example
|
||||
* const rawHtml = await extractEpisodeListAjax(12345, "modern", "abc123");
|
||||
*/
|
||||
const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => {
|
||||
try {
|
||||
let url = `${BASE_URL}/ajax/episode/list/${animeId}`;
|
||||
// NOTE: Optional params are appended only if non-empty to avoid trailing ?/&
|
||||
const params = [];
|
||||
if (style) params.push(`style=${style}`);
|
||||
if (vrf) params.push(`vrf=${vrf}`);
|
||||
if (params.length) url += `?${params.join("&")}`;
|
||||
|
||||
// NOTE: X-Requested-With header is required — without it the server returns the full page
|
||||
const { data } = await axios.get(url, {
|
||||
headers: {
|
||||
...headers,
|
||||
@@ -23,4 +66,6 @@ const extractEpisodeListAjax = async (animeId, style = "", vrf = "") => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractEpisodeListAjax };
|
||||
export { extractEpisodeListAjax };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: episodeListAjax.extractor.js
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — filter.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Provides advanced anime filtering on anikototv.to — supports filtering
|
||||
* by genre, type, status, language, rating, sort order, season, year,
|
||||
* and keyword with full pagination support.
|
||||
*
|
||||
* @exports
|
||||
* extractFilter
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
@@ -5,56 +23,97 @@ import { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS } from "../config
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// FILTER EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract paginated anime results with multi-criteria filtering ----
|
||||
/**
|
||||
* Builds a filter URL from the given parameters and fetches matching anime
|
||||
* from anikototv.to. Supports keyword, genre, type, status, language,
|
||||
* rating, sort, season, and year filters — all combinable.
|
||||
*
|
||||
* @param {Object} params - Filter parameters object
|
||||
* @param {string} [params.keyword] - Search keyword to filter by name
|
||||
* @param {string} [params.genre] - Comma-separated genre names (e.g. "action,comedy")
|
||||
* @param {string} [params.type] - Comma-separated types (e.g. "tv,movie")
|
||||
* @param {string} [params.status] - Comma-separated statuses (e.g. "airing,completed")
|
||||
* @param {string} [params.language] - Comma-separated languages (e.g. "sub,dub")
|
||||
* @param {string} [params.rating] - Comma-separated ratings (e.g. "pg-13,r")
|
||||
* @param {string} [params.sort] - Sort order key (e.g. "score", "name")
|
||||
* @param {string} [params.season] - Comma-separated seasons (e.g. "winter,spring")
|
||||
* @param {string} [params.year] - Comma-separated years (e.g. "2024,2025")
|
||||
* @param {number} [params.page=1] - Page number for pagination
|
||||
* @returns {Promise<Object>} Object with totalPages count and data array of results
|
||||
*
|
||||
* @example
|
||||
* const filtered = await extractFilter({ genre: "action", type: "tv", page: 1 });
|
||||
* console.log(filtered.totalPages);
|
||||
* console.log(filtered.data[0].title);
|
||||
*/
|
||||
const extractFilter = async (params) => {
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
// ---- FEATURE: Build keyword query parameter ----
|
||||
// WARNING: keyword param is always set — empty string if not provided (site requires it)
|
||||
if (params.keyword) {
|
||||
queryParams.set("keyword", params.keyword);
|
||||
} else {
|
||||
queryParams.set("keyword", "");
|
||||
}
|
||||
|
||||
// ---- FEATURE: Map genre names to IDs and append ----
|
||||
// NOTE: GENRE_IDS config maps human-readable names to site-specific IDs
|
||||
if (params.genre) {
|
||||
const genres = params.genre.split(",").map(g => GENRE_IDS[g.trim().toLowerCase()] || g.trim());
|
||||
genres.forEach(g => queryParams.append("genre[]", g));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Map type names to IDs and append ----
|
||||
if (params.type) {
|
||||
const types = params.type.split(",").map(t => TYPE_IDS[t.trim().toLowerCase()] || t.trim());
|
||||
types.forEach(t => queryParams.append("term_type[]", t));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Map status names to IDs and append ----
|
||||
if (params.status) {
|
||||
const statuses = params.status.split(",").map(s => STATUS_IDS[s.trim().toLowerCase()] || s.trim());
|
||||
statuses.forEach(s => queryParams.append("status[]", s));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Append language filter ----
|
||||
// NOTE: Languages are passed directly without ID mapping
|
||||
if (params.language) {
|
||||
const langs = params.language.split(",");
|
||||
langs.forEach(l => queryParams.append("language[]", l.trim()));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Map rating names to IDs and append ----
|
||||
if (params.rating) {
|
||||
const ratings = params.rating.split(",").map(r => RATING_IDS[r.trim().toLowerCase()] || r.trim());
|
||||
ratings.forEach(r => queryParams.append("rating[]", r));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Map sort key to sort value ----
|
||||
if (params.sort) {
|
||||
const sort = SORT_IDS[params.sort.trim().toLowerCase()] ?? params.sort;
|
||||
if (sort) queryParams.set("sort", sort);
|
||||
}
|
||||
|
||||
// ---- FEATURE: Append season filter ----
|
||||
if (params.season) {
|
||||
const seasons = params.season.split(",");
|
||||
seasons.forEach(s => queryParams.append("season[]", s.trim()));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Append year filter ----
|
||||
if (params.year) {
|
||||
const years = params.year.split(",");
|
||||
years.forEach(y => queryParams.append("year[]", y.trim()));
|
||||
}
|
||||
|
||||
// NOTE: The filter URL is built by appending query params to the search base URL
|
||||
const filterUrl = `${URLS.search}?${queryParams.toString()}`;
|
||||
const $ = await extractPages(filterUrl, params.page || 1);
|
||||
const totalPages = countPages($);
|
||||
@@ -94,4 +153,6 @@ const extractFilter = async (params) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractFilter };
|
||||
export { extractFilter };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: filter.extractor.js
|
||||
|
||||
@@ -1,22 +1,60 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — homeInfo.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts homepage data from anikototv.to including spotlight carousel,
|
||||
* trending anime, top airing list, and available genre filters.
|
||||
*
|
||||
* @exports
|
||||
* extractHomeInfo
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// HOME INFO EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract full homepage data (spotlights, trending, top airing, genres) ----
|
||||
/**
|
||||
* Fetches and parses the anikototv.to homepage to extract spotlight carousel,
|
||||
* recently updated trending anime, top airing of the day, and genre list.
|
||||
*
|
||||
* @returns {Promise<Object>} Object containing spotlights, trending, topAiring, and genres arrays
|
||||
*
|
||||
* @example
|
||||
* const home = await extractHomeInfo();
|
||||
* console.log(home.spotlights.length); // number of spotlight items
|
||||
*/
|
||||
const extractHomeInfo = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(URLS.home, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
// ---- FEATURE: Extract spotlight carousel items from #hotest section ----
|
||||
// NOTE: Swiper slides contain the hero carousel on the homepage
|
||||
const spotlights = [];
|
||||
$("#hotest .swiper-slide.item").each((i, el) => {
|
||||
// NOTE: The play button's href contains the /watch/{slug} path
|
||||
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
||||
// NOTE: Poster is set as inline CSS background-image on a div inside .image
|
||||
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||
const title = $(el).find("h2.title.d-title").text().trim() || "";
|
||||
// NOTE: data-jp attribute holds the Japanese/romaji title variant
|
||||
const japaneseTitle = $(el).find("h2.title.d-title").attr("data-jp") || "";
|
||||
const description = $(el).find(".desc").text().trim() || "";
|
||||
const rating = $(el).find("i.rating").text().trim() || "";
|
||||
const quality = $(el).find("i.quality").text().trim() || "";
|
||||
// NOTE: sub/dub counts are extracted as integers, defaulting to 0
|
||||
const sub = parseInt($(el).find("i.sub").text().trim()) || 0;
|
||||
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
||||
const date = $(el).find("i.date").text().trim() || "";
|
||||
@@ -37,12 +75,15 @@ const extractHomeInfo = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract trending/recently-updated anime list ----
|
||||
// NOTE: Combines both .section-updated and #recent-update containers for coverage
|
||||
const trending = [];
|
||||
$(".section-updated .item, #recent-update .item").each((i, el) => {
|
||||
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".poster img").attr("src") || "";
|
||||
const title = $(el).find("a.name.d-title").text().trim() || "";
|
||||
const japaneseTitle = $(el).find("a.name.d-title").attr("data-jp") || "";
|
||||
// NOTE: Episode counts are in .ep-status spans — sub, dub, and total
|
||||
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
||||
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
||||
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
||||
@@ -62,6 +103,8 @@ const extractHomeInfo = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract top airing anime of the day ----
|
||||
// NOTE: Only the 'day' tab content is scraped from #top-anime
|
||||
const topAiring = [];
|
||||
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
@@ -76,6 +119,8 @@ const extractHomeInfo = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract genre filter list from navigation menu ----
|
||||
// NOTE: Only links containing /genre/ in href are considered valid genre entries
|
||||
const genres = [];
|
||||
$("#menu ul.c4 li a").each((i, el) => {
|
||||
const genre = $(el).text().trim();
|
||||
@@ -96,4 +141,6 @@ const extractHomeInfo = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractHomeInfo };
|
||||
export { extractHomeInfo };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: homeInfo.extractor.js
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — newRelease.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts newly released and newly added anime with pagination.
|
||||
* Provides both new releases and sorted by addition date.
|
||||
*
|
||||
* @exports
|
||||
* extractNewRelease
|
||||
* extractNewlyAdded
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
@@ -5,12 +23,43 @@ import { URLS } from "../configs/dataUrl.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// NEW RELEASE EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract newly released anime with pagination ----
|
||||
/**
|
||||
* Fetches and parses newly released anime with pagination.
|
||||
* Returns paginated results with total page count.
|
||||
*
|
||||
* @param {number} page - Page number to fetch (default: 1)
|
||||
* @returns {Promise<Object>} Object containing totalPages and data array
|
||||
* @returns {number} return.totalPages - Total number of pages available
|
||||
* @returns {Array<Object>} return.data - Array of anime objects
|
||||
* @returns {string} return.data[].slug - URL slug for the anime
|
||||
* @returns {string} return.data[].poster - Poster image URL
|
||||
* @returns {string} return.data[].title - English title
|
||||
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||
* @returns {number} return.data[].sub - Subbed episode count
|
||||
* @returns {number} return.data[].dub - Dubbed episode count
|
||||
* @returns {number} return.data[].total - Total episode count
|
||||
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||
* @returns {string} return.data[].rating - Anime rating
|
||||
*
|
||||
* @example
|
||||
* const newRelease = await extractNewRelease(1);
|
||||
* console.log(newRelease.totalPages);
|
||||
* console.log(newRelease.data[0].title);
|
||||
*/
|
||||
const extractNewRelease = async (page = 1) => {
|
||||
try {
|
||||
// NOTE: extractPages handles pagination and returns parsed Cheerio object
|
||||
const $ = await extractPages(URLS.newRelease, page);
|
||||
const totalPages = countPages($);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: Multiple selectors to handle different page layouts
|
||||
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
@@ -22,6 +71,7 @@ const extractNewRelease = async (page = 1) => {
|
||||
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
results.push({
|
||||
slug,
|
||||
@@ -43,12 +93,42 @@ const extractNewRelease = async (page = 1) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// NEWLY ADDED EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract newly added anime sorted by addition date ----
|
||||
/**
|
||||
* Fetches and parses newly added anime sorted by addition date.
|
||||
* Uses the latestUpdated endpoint with sort=added parameter.
|
||||
*
|
||||
* @param {number} page - Page number to fetch (default: 1)
|
||||
* @returns {Promise<Object>} Object containing totalPages and data array
|
||||
* @returns {number} return.totalPages - Total number of pages available
|
||||
* @returns {Array<Object>} return.data - Array of anime objects
|
||||
* @returns {string} return.data[].slug - URL slug for the anime
|
||||
* @returns {string} return.data[].poster - Poster image URL
|
||||
* @returns {string} return.data[].title - English title
|
||||
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||
* @returns {number} return.data[].sub - Subbed episode count
|
||||
* @returns {number} return.data[].dub - Dubbed episode count
|
||||
* @returns {number} return.data[].total - Total episode count
|
||||
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||
*
|
||||
* @example
|
||||
* const newlyAdded = await extractNewlyAdded(1);
|
||||
* console.log(newlyAdded.totalPages);
|
||||
* console.log(newlyAdded.data[0].title);
|
||||
*/
|
||||
const extractNewlyAdded = async (page = 1) => {
|
||||
try {
|
||||
// NOTE: The latestUpdated endpoint with sort=added returns recently added anime
|
||||
const $ = await extractPages(`${URLS.latestUpdated}?sort=added`, page);
|
||||
const totalPages = countPages($);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: Multiple selectors to handle different page layouts
|
||||
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
@@ -59,6 +139,7 @@ const extractNewlyAdded = async (page = 1) => {
|
||||
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
||||
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
results.push({
|
||||
slug,
|
||||
@@ -79,4 +160,6 @@ const extractNewlyAdded = async (page = 1) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractNewRelease, extractNewlyAdded };
|
||||
export { extractNewRelease, extractNewlyAdded };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: newRelease.extractor.js
|
||||
|
||||
@@ -1,15 +1,64 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — popular.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts most viewed/popular anime with pagination support.
|
||||
* Scrapes the most viewed section with page navigation.
|
||||
*
|
||||
* @exports
|
||||
* extractPopular
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// POPULAR EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract most viewed/popular anime with pagination ----
|
||||
/**
|
||||
* Fetches and parses the most viewed anime list with pagination.
|
||||
* Returns paginated results with total page count.
|
||||
*
|
||||
* @param {number} page - Page number to fetch (default: 1)
|
||||
* @returns {Promise<Object>} Object containing totalPages and data array
|
||||
* @returns {number} return.totalPages - Total number of pages available
|
||||
* @returns {Array<Object>} return.data - Array of popular anime objects
|
||||
* @returns {string} return.data[].slug - URL slug for the anime
|
||||
* @returns {string} return.data[].animeId - Internal anime ID
|
||||
* @returns {string} return.data[].poster - Poster image URL
|
||||
* @returns {string} return.data[].title - English title
|
||||
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||
* @returns {number} return.data[].sub - Subbed episode count
|
||||
* @returns {number} return.data[].dub - Dubbed episode count
|
||||
* @returns {number} return.data[].total - Total episode count
|
||||
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||
* @returns {string} return.data[].rating - Anime rating
|
||||
*
|
||||
* @example
|
||||
* const popular = await extractPopular(1);
|
||||
* console.log(popular.totalPages); // Total pages available
|
||||
* console.log(popular.data[0].title); // First anime title
|
||||
*/
|
||||
const extractPopular = async (page = 1) => {
|
||||
try {
|
||||
// NOTE: extractPages handles pagination and returns parsed Cheerio object
|
||||
const $ = await extractPages(URLS.mostViewed, page);
|
||||
const totalPages = countPages($);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: The list items are contained in #list-items container
|
||||
$("#list-items > .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
||||
@@ -22,6 +71,7 @@ const extractPopular = async (page = 1) => {
|
||||
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
||||
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
results.push({
|
||||
slug,
|
||||
@@ -44,4 +94,6 @@ const extractPopular = async (page = 1) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractPopular };
|
||||
export { extractPopular };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: popular.extractor.js
|
||||
|
||||
@@ -1,29 +1,73 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — random.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Fetches a random anime from anikototv.to by following the redirect
|
||||
* chain from the random endpoint, then extracts the full anime detail
|
||||
* page metadata from the resolved URL.
|
||||
*
|
||||
* @exports
|
||||
* extractRandom
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// RANDOM ANIME EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Fetch a random anime and extract its detail metadata ----
|
||||
/**
|
||||
* Hits the random endpoint on anikototv.to which redirects to a random
|
||||
* anime page. Follows up to 5 redirects, extracts the final URL to get
|
||||
* the slug, then parses the full anime detail page for metadata.
|
||||
*
|
||||
* @returns {Promise<Object>} Random anime data with slug, title, poster, synopsis, etc.
|
||||
*
|
||||
* @example
|
||||
* const random = await extractRandom();
|
||||
* console.log(random.title); // random anime title
|
||||
* console.log(random.url); // full resolved URL
|
||||
*/
|
||||
const extractRandom = async () => {
|
||||
try {
|
||||
// NOTE: maxRedirects=5 ensures we follow the random redirect chain
|
||||
// NOTE: validateStatus allows any 2xx/3xx response without throwing
|
||||
const { data, request, response } = await axios.get(URLS.random, {
|
||||
headers,
|
||||
maxRedirects: 5,
|
||||
validateStatus: (status) => status < 400
|
||||
});
|
||||
|
||||
// NOTE: responseURL on the request object gives us the final resolved URL after redirects
|
||||
const finalUrl = request?.responseURL || URLS.random;
|
||||
// NOTE: The slug is extracted from the /watch/{slug} path in the final URL
|
||||
const slug = finalUrl.split("/watch/").pop() || "";
|
||||
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
// ---- FEATURE: Extract anime title and identification ----
|
||||
const title = $("h1[itemprop='name'].title.d-title").text().trim() || "";
|
||||
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
||||
const poster = $("img[itemprop='image']").attr("src") || "";
|
||||
|
||||
// ---- FEATURE: Extract type, synopsis, rating, and anime ID ----
|
||||
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
||||
const synopsis = $(".synopsis .content").text().trim() || "";
|
||||
const rating = $("#w-rating .score .value").text().trim() || "";
|
||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||
|
||||
// ---- FEATURE: Extract genres ----
|
||||
// NOTE: Genre links are identified by '/genre/' in their href attribute
|
||||
const genres = [];
|
||||
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
||||
genres.push($(el).text().trim());
|
||||
@@ -46,4 +90,6 @@ const extractRandom = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractRandom };
|
||||
export { extractRandom };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: random.extractor.js
|
||||
|
||||
@@ -1,21 +1,63 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — schedule.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts anime schedule for a specific date.
|
||||
* Returns airing times and episode numbers for scheduled anime.
|
||||
*
|
||||
* @exports
|
||||
* extractSchedule
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SCHEDULE EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract anime schedule for a specific date ----
|
||||
/**
|
||||
* Fetches and parses the anime schedule for a given date.
|
||||
* Returns airing times and episode numbers for scheduled anime.
|
||||
*
|
||||
* @param {string} date - Date to fetch schedule for (format: YYYY-MM-DD)
|
||||
* @returns {Promise<Array<Object>>} Array of scheduled anime objects
|
||||
* @returns {string} return.slug - URL slug for the anime
|
||||
* @returns {string} return.title - Anime title
|
||||
* @returns {string} return.time - Airing time
|
||||
* @returns {number} return.episode_no - Episode number
|
||||
*
|
||||
* @example
|
||||
* const schedule = await extractSchedule("2024-01-15");
|
||||
* console.log(schedule[0].time); // Airing time
|
||||
* console.log(schedule[0].episode_no); // Episode number
|
||||
*/
|
||||
const extractSchedule = async (date) => {
|
||||
try {
|
||||
// NOTE: The schedule is fetched by appending date parameter to homepage
|
||||
const url = `${URLS.home}?date=${date}`;
|
||||
const { data } = await axios.get(url, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const schedule = [];
|
||||
|
||||
// NOTE: Schedule items use different selectors depending on page structure
|
||||
$(".schedule-item, .anime-schedule .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
const title = $(el).find(".film-name a, .name").text().trim() || "";
|
||||
const time = $(el).find(".time, .schedule-time").text().trim() || "";
|
||||
const episodeNo = parseInt($(el).find(".episode-no, .ep-num").text().trim()) || 0;
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
schedule.push({
|
||||
slug,
|
||||
@@ -32,4 +74,6 @@ const extractSchedule = async (date) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractSchedule };
|
||||
export { extractSchedule };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: schedule.extractor.js
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — search.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Provides search functionality for anikototv.to — supports paginated
|
||||
* search results and lightweight autocomplete suggestions.
|
||||
*
|
||||
* @exports
|
||||
* extractSearchResults, extractSearchSuggestions
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
@@ -5,23 +22,47 @@ import { URLS } from "../configs/dataUrl.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SEARCH RESULTS EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract paginated search results for a keyword ----
|
||||
/**
|
||||
* Searches anikototv.to for anime matching the given keyword and returns
|
||||
* paginated results with full metadata including episode counts and genres.
|
||||
*
|
||||
* @param {string} keyword - The search query string
|
||||
* @param {number} [page=1] - Page number for pagination
|
||||
* @returns {Promise<Object>} Object with totalPages count and data array of results
|
||||
*
|
||||
* @example
|
||||
* const results = await extractSearchResults("naruto", 1);
|
||||
* console.log(results.totalPages); // total available pages
|
||||
* console.log(results.data[0].title); // first result title
|
||||
*/
|
||||
const extractSearchResults = async (keyword, page = 1) => {
|
||||
try {
|
||||
// WARNING: keyword param is required — omitting it may cause a 500 error from the source
|
||||
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
||||
const $ = await extractPages(url, page);
|
||||
const totalPages = countPages($);
|
||||
|
||||
const results = [];
|
||||
// NOTE: #list-items > .item is the standard result container on anikototv.to search pages
|
||||
$("#list-items > .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
// NOTE: .ani.poster.tip is the poster container with tooltip data
|
||||
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
||||
const title = $(el).find(".info .b1 a.name.d-title").text().trim() || "";
|
||||
const japaneseTitle = $(el).find(".info .b1 a.name.d-title").attr("data-jp") || "";
|
||||
// NOTE: data-tip attribute on the poster container holds the animeId
|
||||
const animeId = $(el).find(".ani.poster.tip").attr("data-tip") || "";
|
||||
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
||||
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
||||
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
||||
// NOTE: Type is in the second .m-item label (e.g. TV, Movie, OVA)
|
||||
const type = $(el).find(".info .meta .m-item:nth-child(2) label").text().trim() || "";
|
||||
// NOTE: Rating is in a dedicated .m-item.rated span
|
||||
const rating = $(el).find(".info .meta .m-item.rated span").text().trim() || "";
|
||||
const genres = [];
|
||||
$(el).find(".info .b1 .genre a").each((j, g) => {
|
||||
@@ -51,6 +92,22 @@ const extractSearchResults = async (keyword, page = 1) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SEARCH SUGGESTIONS EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract lightweight autocomplete suggestions (capped at 10) ----
|
||||
/**
|
||||
* Fetches search suggestions for a keyword — returns a lightweight subset
|
||||
* of result data suitable for autocomplete dropdowns.
|
||||
*
|
||||
* @param {string} keyword - The search query string
|
||||
* @returns {Promise<Array>} Array of up to 10 suggestion objects with slug, poster, title, type
|
||||
*
|
||||
* @example
|
||||
* const suggestions = await extractSearchSuggestions("one piece");
|
||||
* console.log(suggestions.length); // max 10
|
||||
*/
|
||||
const extractSearchSuggestions = async (keyword) => {
|
||||
try {
|
||||
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
||||
@@ -69,10 +126,13 @@ const extractSearchSuggestions = async (keyword) => {
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Limit to 10 suggestions to keep autocomplete payloads small
|
||||
return suggestions.slice(0, 10);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { extractSearchResults, extractSearchSuggestions };
|
||||
export { extractSearchResults, extractSearchSuggestions };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: search.extractor.js
|
||||
|
||||
@@ -1,9 +1,43 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — seasons.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts season information for a specific anime via API.
|
||||
* Returns season data from the internal API endpoint.
|
||||
*
|
||||
* @exports
|
||||
* extractSeasons
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { BASE_URL } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SEASONS API EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract season data from internal API ----
|
||||
/**
|
||||
* Fetches season information for a specific anime from the internal API.
|
||||
* Returns an array of season objects or empty array on error.
|
||||
*
|
||||
* @param {string} animeId - The anime ID to fetch seasons for
|
||||
* @returns {Promise<Array<Object>>} Array of season objects or empty array
|
||||
*
|
||||
* @example
|
||||
* const seasons = await extractSeasons("12345");
|
||||
* console.log(seasons.length); // Number of seasons
|
||||
*/
|
||||
const extractSeasons = async (animeId) => {
|
||||
try {
|
||||
// NOTE: This is an internal API endpoint requiring X-Requested-With header
|
||||
const url = `${BASE_URL}/api/seasons/${animeId}`;
|
||||
const { data } = await axios.get(url, {
|
||||
headers: {
|
||||
@@ -14,8 +48,11 @@ const extractSeasons = async (animeId) => {
|
||||
|
||||
return data || [];
|
||||
} catch (error) {
|
||||
// NOTE: Return empty array on error to maintain consistent API
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export { extractSeasons };
|
||||
export { extractSeasons };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: seasons.extractor.js
|
||||
|
||||
@@ -1,14 +1,60 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — spotlight.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts spotlight/featured anime items from the homepage.
|
||||
* Scrapes the "hottest" carousel section for trending anime.
|
||||
*
|
||||
* @exports
|
||||
* extractSpotlight
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SPOTLIGHT EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract featured/spotlight anime from homepage ----
|
||||
/**
|
||||
* Fetches and parses the spotlight carousel from the homepage.
|
||||
* Returns an array of featured anime with metadata like poster, title,
|
||||
* rating, quality, and episode counts.
|
||||
*
|
||||
* @returns {Promise<Array<Object>>} Array of spotlight anime objects
|
||||
* @returns {string} return.slug - URL slug for the anime
|
||||
* @returns {string} return.poster - Poster image URL
|
||||
* @returns {string} return.title - English title
|
||||
* @returns {string} return.japaneseTitle - Japanese title
|
||||
* @returns {string} return.description - Short description
|
||||
* @returns {string} return.rating - Anime rating
|
||||
* @returns {string} return.quality - Video quality (e.g., HD)
|
||||
* @returns {number} return.sub - Subbed episode count
|
||||
* @returns {number} return.dub - Dubbed episode count
|
||||
* @returns {string} return.date - Release date
|
||||
*
|
||||
* @example
|
||||
* const spotlights = await extractSpotlight();
|
||||
* console.log(spotlights[0].title);
|
||||
*/
|
||||
const extractSpotlight = async () => {
|
||||
try {
|
||||
// NOTE: The homepage contains the spotlight carousel in #hotest section
|
||||
const { data } = await axios.get(URLS.home, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const spotlights = [];
|
||||
|
||||
// NOTE: Each slide in the swiper carousel represents a featured anime
|
||||
$("#hotest .swiper-slide.item").each((i, el) => {
|
||||
const slug = $(el).find("a.play").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".image div").attr("style")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||
@@ -21,6 +67,7 @@ const extractSpotlight = async () => {
|
||||
const dub = parseInt($(el).find("i.dub").text().trim()) || 0;
|
||||
const date = $(el).find("i.date").text().trim() || "";
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
spotlights.push({
|
||||
slug,
|
||||
@@ -43,4 +90,6 @@ const extractSpotlight = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractSpotlight };
|
||||
export { extractSpotlight };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: spotlight.extractor.js
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — status.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts anime filtered by status (ongoing, completed, etc.).
|
||||
* Supports pagination for browsing large result sets.
|
||||
*
|
||||
* @exports
|
||||
* extractStatus
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
@@ -5,13 +22,46 @@ import { URLS } from "../configs/dataUrl.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// STATUS FILTER EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract anime filtered by status with pagination ----
|
||||
/**
|
||||
* Fetches and parses anime filtered by status (ongoing, completed, etc.).
|
||||
* Returns paginated results with total page count.
|
||||
*
|
||||
* @param {string} status - Status to filter by (e.g., "ongoing", "completed")
|
||||
* @param {number} page - Page number to fetch (default: 1)
|
||||
* @returns {Promise<Object>} Object containing totalPages, status, and data array
|
||||
* @returns {number} return.totalPages - Total number of pages available
|
||||
* @returns {string} return.status - The status filter used
|
||||
* @returns {Array<Object>} return.data - Array of anime objects
|
||||
* @returns {string} return.data[].slug - URL slug for the anime
|
||||
* @returns {string} return.data[].poster - Poster image URL
|
||||
* @returns {string} return.data[].title - English title
|
||||
* @returns {string} return.data[].japaneseTitle - Japanese title
|
||||
* @returns {number} return.data[].sub - Subbed episode count
|
||||
* @returns {number} return.data[].dub - Dubbed episode count
|
||||
* @returns {number} return.data[].total - Total episode count
|
||||
* @returns {string} return.data[].type - Anime type (TV, Movie, etc.)
|
||||
* @returns {string} return.data[].rating - Anime rating
|
||||
*
|
||||
* @example
|
||||
* const ongoing = await extractStatus("ongoing", 1);
|
||||
* console.log(ongoing.status); // "ongoing"
|
||||
* console.log(ongoing.data[0].title); // First ongoing anime
|
||||
*/
|
||||
const extractStatus = async (status, page = 1) => {
|
||||
try {
|
||||
// NOTE: URLS.status generates the correct endpoint for the given status
|
||||
const url = URLS.status(status);
|
||||
const $ = await extractPages(url, page);
|
||||
const totalPages = countPages($);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: Multiple selectors to handle different page layouts
|
||||
$(".film_list-wrap .flw-item, .film-detail, .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
@@ -23,6 +73,7 @@ const extractStatus = async (status, page = 1) => {
|
||||
const type = $(el).find(".meta .inner .right, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
const rating = $(el).find(".rating, .fdi-item:nth-child(3)").text().trim() || "";
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
results.push({
|
||||
slug,
|
||||
@@ -44,4 +95,6 @@ const extractStatus = async (status, page = 1) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractStatus };
|
||||
export { extractStatus };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: status.extractor.js
|
||||
|
||||
@@ -1,10 +1,46 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — streamInfo.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Handles server/stream resolution for episodes — fetches individual
|
||||
* stream URLs, server lists via AJAX, and alternative servers from
|
||||
* the nekostream mapper API.
|
||||
*
|
||||
* @exports
|
||||
* extractStreamInfo, extractServerList, extractMapperServers
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { BASE_URL } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// STREAM INFO EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Resolve a single stream URL from a linkId ----
|
||||
/**
|
||||
* Fetches the actual stream URL and skip data for a given server link ID.
|
||||
* This is the primary method for resolving playable video URLs.
|
||||
*
|
||||
* @param {string} linkId - The server link ID to resolve
|
||||
* @returns {Promise<Object>} Object with linkId, url, and skipData (intro/outro timestamps)
|
||||
*
|
||||
* @example
|
||||
* const stream = await extractStreamInfo("abc123");
|
||||
* console.log(stream.url); // direct video URL
|
||||
* console.log(stream.skipData); // intro/outro skip ranges
|
||||
*/
|
||||
const extractStreamInfo = async (linkId) => {
|
||||
try {
|
||||
const url = `${BASE_URL}/ajax/server?get=${linkId}`;
|
||||
// NOTE: X-Requested-With header is required for AJAX endpoints on anikototv.to
|
||||
const { data } = await axios.get(url, {
|
||||
headers: {
|
||||
...headers,
|
||||
@@ -26,6 +62,22 @@ const extractStreamInfo = async (linkId) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SERVER LIST EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Fetch available servers for a set of episode IDs ----
|
||||
/**
|
||||
* Retrieves the server list for the given episode IDs from the AJAX endpoint.
|
||||
* Returns raw server data as provided by the source site.
|
||||
*
|
||||
* @param {string} episodeIds - Comma-separated or single episode ID string
|
||||
* @returns {Promise<Object>} Raw server list data from the AJAX response
|
||||
*
|
||||
* @example
|
||||
* const servers = await extractServerList("12345");
|
||||
* // servers contains the server configuration for that episode
|
||||
*/
|
||||
const extractServerList = async (episodeIds) => {
|
||||
try {
|
||||
const url = `${BASE_URL}/ajax/server/list?servers=${episodeIds}`;
|
||||
@@ -42,6 +94,26 @@ const extractServerList = async (episodeIds) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// MAPPER SERVERS EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Fetch alternative streaming servers from nekostream mapper API ----
|
||||
/**
|
||||
* Queries the nekostream mapper API for alternative streaming providers
|
||||
* (sub and dub) for a given anime identified by MAL ID, slug, and timestamp.
|
||||
* Returns a flat array of server objects per provider.
|
||||
*
|
||||
* @param {number|string} malId - MyAnimeList ID for the anime
|
||||
* @param {string} slug - The anime slug on anikototv.to
|
||||
* @param {string|number} timestamp - Episode timestamp identifier
|
||||
* @returns {Promise<Array>} Array of server objects with provider, type, url, download fields
|
||||
*
|
||||
* @example
|
||||
* const servers = await extractMapperServers(21, "one-piece", 1234567890);
|
||||
* console.log(servers[0].provider); // "vidstreaming"
|
||||
* console.log(servers[0].type); // "sub" or "dub"
|
||||
*/
|
||||
const extractMapperServers = async (malId, slug, timestamp) => {
|
||||
try {
|
||||
const url = `https://mapper.nekostream.site/api/mal/${malId}/${slug}/${timestamp}`;
|
||||
@@ -51,6 +123,7 @@ const extractMapperServers = async (malId, slug, timestamp) => {
|
||||
|
||||
if (data && typeof data === "object") {
|
||||
for (const [provider, sources] of Object.entries(data)) {
|
||||
// NOTE: Each provider may have sub, dub, or both source types
|
||||
if (sources && sources.sub) {
|
||||
servers.push({
|
||||
provider,
|
||||
@@ -72,8 +145,11 @@ const extractMapperServers = async (malId, slug, timestamp) => {
|
||||
|
||||
return servers;
|
||||
} catch (error) {
|
||||
// NOTE: Returns empty array on failure — mapper API may be unavailable
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export { extractStreamInfo, extractServerList, extractMapperServers };
|
||||
export { extractStreamInfo, extractServerList, extractMapperServers };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: streamInfo.extractor.js
|
||||
|
||||
@@ -1,15 +1,59 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — suggestion.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts search suggestions/autocomplete results for a keyword.
|
||||
* Returns up to 10 matching anime suggestions.
|
||||
*
|
||||
* @exports
|
||||
* extractSuggestions
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SUGGESTION EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract search suggestions for a keyword ----
|
||||
/**
|
||||
* Fetches and parses search suggestions for a given keyword.
|
||||
* Returns up to 10 matching anime suggestions.
|
||||
*
|
||||
* @param {string} keyword - Search keyword to get suggestions for
|
||||
* @returns {Promise<Array<Object>>} Array of suggestion objects (max 10)
|
||||
* @returns {string} return.slug - URL slug for the anime
|
||||
* @returns {string} return.poster - Poster image URL
|
||||
* @returns {string} return.title - English title
|
||||
* @returns {string} return.japaneseTitle - Japanese title
|
||||
* @returns {string} return.type - Anime type (TV, Movie, etc.)
|
||||
* @returns {number} return.sub - Subbed episode count
|
||||
* @returns {number} return.dub - Dubbed episode count
|
||||
*
|
||||
* @example
|
||||
* const suggestions = await extractSuggestions("naruto");
|
||||
* console.log(suggestions.length); // Up to 10 results
|
||||
* console.log(suggestions[0].title);
|
||||
*/
|
||||
const extractSuggestions = async (keyword) => {
|
||||
try {
|
||||
// NOTE: The search endpoint requires the 'keyword' parameter
|
||||
const url = `${URLS.search}?keyword=${encodeURIComponent(keyword)}`;
|
||||
const { data } = await axios.get(url, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const suggestions = [];
|
||||
|
||||
// NOTE: Search results use the same list structure as other pages
|
||||
$("#list-items > .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".ani.poster.tip > a > img").attr("src") || "";
|
||||
@@ -19,15 +63,19 @@ const extractSuggestions = async (keyword) => {
|
||||
const sub = parseInt($(el).find(".ep-status.sub span").text().trim()) || 0;
|
||||
const dub = parseInt($(el).find(".ep-status.dub span").text().trim()) || 0;
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
suggestions.push({ slug, poster, title, japaneseTitle, type, sub, dub });
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Limit to 10 suggestions for autocomplete-style responses
|
||||
return suggestions.slice(0, 10);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { extractSuggestions };
|
||||
export { extractSuggestions };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: suggestion.extractor.js
|
||||
|
||||
@@ -1,10 +1,53 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — topten.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts top 10 anime rankings from the homepage.
|
||||
* Parses daily, weekly, and monthly top anime lists.
|
||||
*
|
||||
* @exports
|
||||
* extractTopTen
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// TOP TEN EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract daily, weekly, and monthly top 10 anime ----
|
||||
/**
|
||||
* Fetches and parses the top 10 anime rankings from the homepage.
|
||||
* Returns three arrays: today, week, and month rankings.
|
||||
*
|
||||
* @returns {Promise<Object>} Object containing today, week, and month arrays
|
||||
* @returns {Array<Object>} return.today - Daily top 10 anime
|
||||
* @returns {Array<Object>} return.week - Weekly top 10 anime
|
||||
* @returns {Array<Object>} return.month - Monthly top 10 anime
|
||||
* @returns {string} return.today[].slug - URL slug for the anime
|
||||
* @returns {number} return.today[].rank - Ranking position (1-10)
|
||||
* @returns {string} return.today[].name - Anime name
|
||||
* @returns {string} return.today[].poster - Poster image URL
|
||||
* @returns {number} return.today[].sub - Subbed episode count
|
||||
* @returns {number} return.today[].dub - Dubbed episode count
|
||||
* @returns {string} return.today[].type - Anime type (TV, Movie, etc.)
|
||||
*
|
||||
* @example
|
||||
* const topTen = await extractTopTen();
|
||||
* console.log(topTen.today[0].name); // #1 anime today
|
||||
*/
|
||||
const extractTopTen = async () => {
|
||||
try {
|
||||
// NOTE: The top 10 section uses tabbed content with data-name attributes
|
||||
const { data } = await axios.get(URLS.home, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
@@ -12,6 +55,7 @@ const extractTopTen = async () => {
|
||||
const week = [];
|
||||
const month = [];
|
||||
|
||||
// NOTE: Each tab contains a list of anime items with ranking data
|
||||
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".poster img").attr("src") || "";
|
||||
@@ -21,11 +65,13 @@ const extractTopTen = async () => {
|
||||
const type = $(el).find(".type").text().trim() || "";
|
||||
const rank = i + 1;
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
today.push({ slug, rank, name, poster, sub, dub, type });
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Weekly rankings follow the same structure as daily
|
||||
$("#top-anime .tab-content[data-name='week'] a.item").each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".poster img").attr("src") || "";
|
||||
@@ -40,6 +86,7 @@ const extractTopTen = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Monthly rankings follow the same structure
|
||||
$("#top-anime .tab-content[data-name='month'] a.item").each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".poster img").attr("src") || "";
|
||||
@@ -60,4 +107,6 @@ const extractTopTen = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractTopTen };
|
||||
export { extractTopTen };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: topten.extractor.js
|
||||
|
||||
@@ -1,14 +1,57 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — trending.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts trending and recently updated anime from the homepage.
|
||||
* Scrapes both the updated section and recent updates section.
|
||||
*
|
||||
* @exports
|
||||
* extractTrending
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// TRENDING EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract trending and recently updated anime ----
|
||||
/**
|
||||
* Fetches and parses trending anime from the homepage.
|
||||
* Combines results from both the updated section and recent updates section.
|
||||
*
|
||||
* @returns {Promise<Array<Object>>} Array of trending anime objects
|
||||
* @returns {string} return.slug - URL slug for the anime
|
||||
* @returns {string} return.poster - Poster image URL
|
||||
* @returns {string} return.title - English title
|
||||
* @returns {string} return.japaneseTitle - Japanese title
|
||||
* @returns {number} return.sub - Subbed episode count
|
||||
* @returns {number} return.dub - Dubbed episode count
|
||||
* @returns {number} return.total - Total episode count
|
||||
* @returns {string} return.type - Anime type (TV, Movie, etc.)
|
||||
*
|
||||
* @example
|
||||
* const trending = await extractTrending();
|
||||
* console.log(trending[0].title);
|
||||
*/
|
||||
const extractTrending = async () => {
|
||||
try {
|
||||
// NOTE: Trending data is split across multiple sections on the homepage
|
||||
const { data } = await axios.get(URLS.home, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const trending = [];
|
||||
|
||||
// NOTE: This selector targets both the updated section and recent updates
|
||||
$(".section-updated .item, #recent-update .item").each((i, el) => {
|
||||
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".poster img").attr("src") || "";
|
||||
@@ -19,6 +62,7 @@ const extractTrending = async () => {
|
||||
const total = parseInt($(el).find(".ep-status.total span").text().trim()) || 0;
|
||||
const type = $(el).find(".meta .inner .right").text().trim() || "";
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
trending.push({
|
||||
slug,
|
||||
@@ -39,4 +83,6 @@ const extractTrending = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractTrending };
|
||||
export { extractTrending };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: trending.extractor.js
|
||||
|
||||
@@ -1,10 +1,55 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — trendingSidebar.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts trending sidebar data including top anime rankings
|
||||
* and latest episodes. Provides comprehensive sidebar content.
|
||||
*
|
||||
* @exports
|
||||
* extractTrendingSidebar
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// TRENDING SIDEBAR EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract trending sidebar with top rankings and latest episodes ----
|
||||
/**
|
||||
* Fetches and parses the trending sidebar from the homepage.
|
||||
* Returns daily, weekly, and monthly top anime plus latest episodes.
|
||||
*
|
||||
* @returns {Promise<Object>} Object containing day, week, month, and latestEpisodes
|
||||
* @returns {Array<Object>} return.day - Daily top anime
|
||||
* @returns {Array<Object>} return.week - Weekly top anime
|
||||
* @returns {Array<Object>} return.month - Monthly top anime
|
||||
* @returns {Array<Object>} return.latestEpisodes - Recently updated episodes
|
||||
* @returns {string} return.day[].slug - URL slug for the anime
|
||||
* @returns {number} return.day[].rank - Ranking position
|
||||
* @returns {string} return.day[].name - Anime name
|
||||
* @returns {string} return.day[].poster - Poster image URL
|
||||
* @returns {number} return.day[].sub - Subbed episode count
|
||||
* @returns {number} return.day[].dub - Dubbed episode count
|
||||
* @returns {string} return.day[].type - Anime type
|
||||
*
|
||||
* @example
|
||||
* const sidebar = await extractTrendingSidebar();
|
||||
* console.log(sidebar.day[0].name); // Top anime today
|
||||
* console.log(sidebar.latestEpisodes[0].title); // Latest episode
|
||||
*/
|
||||
const extractTrendingSidebar = async () => {
|
||||
try {
|
||||
// NOTE: The sidebar content is embedded in the homepage HTML
|
||||
const { data } = await axios.get(URLS.home, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
@@ -12,6 +57,7 @@ const extractTrendingSidebar = async () => {
|
||||
const week = [];
|
||||
const month = [];
|
||||
|
||||
// NOTE: Daily rankings use the 'day' tab content
|
||||
$("#top-anime .tab-content[data-name='day'] a.item").each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".poster img").attr("src") || "";
|
||||
@@ -21,11 +67,13 @@ const extractTrendingSidebar = async () => {
|
||||
const type = $(el).find(".type").text().trim() || "";
|
||||
const rank = i + 1;
|
||||
|
||||
// NOTE: Only include items with valid slugs
|
||||
if (slug) {
|
||||
day.push({ slug, rank, name, poster, sub, dub, type });
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Weekly rankings use the 'week' tab content
|
||||
$("#top-anime .tab-content[data-name='week'] a.item").each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".poster img").attr("src") || "";
|
||||
@@ -40,6 +88,7 @@ const extractTrendingSidebar = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Monthly rankings use the 'month' tab content
|
||||
$("#top-anime .tab-content[data-name='month'] a.item").each((i, el) => {
|
||||
const slug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
const poster = $(el).find(".poster img").attr("src") || "";
|
||||
@@ -54,6 +103,7 @@ const extractTrendingSidebar = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Latest episodes section provides recently updated content
|
||||
const latestEpisodes = [];
|
||||
$("#recent-update .item, .section-updated .item").each((i, el) => {
|
||||
const slug = $(el).find("a.name.d-title").attr("href")?.split("/watch/").pop() || "";
|
||||
@@ -76,4 +126,6 @@ const extractTrendingSidebar = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractTrendingSidebar };
|
||||
export { extractTrendingSidebar };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: trendingSidebar.extractor.js
|
||||
|
||||
@@ -1,9 +1,43 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — watchOrder.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts watch order information for a specific anime via API.
|
||||
* Returns recommended viewing order from the internal API endpoint.
|
||||
*
|
||||
* @exports
|
||||
* extractWatchOrder
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { BASE_URL } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// WATCH ORDER API EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract watch order data from internal API ----
|
||||
/**
|
||||
* Fetches watch order information for a specific anime from the internal API.
|
||||
* Returns an array of watch order objects or empty array on error.
|
||||
*
|
||||
* @param {string} animeId - The anime ID to fetch watch order for
|
||||
* @returns {Promise<Array<Object>>} Array of watch order objects or empty array
|
||||
*
|
||||
* @example
|
||||
* const watchOrder = await extractWatchOrder("12345");
|
||||
* console.log(watchOrder[0].title); // First anime in watch order
|
||||
*/
|
||||
const extractWatchOrder = async (animeId) => {
|
||||
try {
|
||||
// NOTE: This is an internal API endpoint requiring X-Requested-With header
|
||||
const url = `${BASE_URL}/api/watch-order/${animeId}`;
|
||||
const { data } = await axios.get(url, {
|
||||
headers: {
|
||||
@@ -14,8 +48,11 @@ const extractWatchOrder = async (animeId) => {
|
||||
|
||||
return data || [];
|
||||
} catch (error) {
|
||||
// NOTE: Return empty array on error to maintain consistent API
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export { extractWatchOrder };
|
||||
export { extractWatchOrder };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: watchOrder.extractor.js
|
||||
|
||||
@@ -1,44 +1,94 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKatoAPI — watchPage.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKatoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts the full watch/episode page data from anikototv.to including
|
||||
* anime metadata, server list, trending sidebar, and recommended anime.
|
||||
*
|
||||
* @exports
|
||||
* extractWatchPage
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import axios from "axios";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS, BASE_URL } from "../configs/dataUrl.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// WATCH PAGE EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract complete watch page data for a specific episode ----
|
||||
/**
|
||||
* Fetches and parses the episode watch page from anikototv.to. Extracts
|
||||
* anime metadata, episode details, available streaming servers, trending
|
||||
* sidebar, and recommended anime.
|
||||
*
|
||||
* @param {string} slug - The anime slug (e.g. "one-piece")
|
||||
* @param {string|number} ep - The episode number to watch
|
||||
* @returns {Promise<Object>} Full watch page data object
|
||||
*
|
||||
* @example
|
||||
* const watchData = await extractWatchPage("one-piece", 100);
|
||||
* console.log(watchData.servers.length); // number of available servers
|
||||
* console.log(watchData.title); // anime title
|
||||
*/
|
||||
const extractWatchPage = async (slug, ep) => {
|
||||
try {
|
||||
const url = URLS.episode(slug, ep);
|
||||
const { data } = await axios.get(url, { headers });
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
// ---- FEATURE: Extract anime identification and title ----
|
||||
// NOTE: data-id on #watch-main is the internal anime ID for AJAX calls
|
||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||
const animeUrl = $("#watch-main").attr("data-url") || "";
|
||||
const title = $("h1[itemprop='name'].title.d-title").text().trim() || slug;
|
||||
const japaneseTitle = $("h1[itemprop='name'].title.d-title").attr("data-jp") || "";
|
||||
const episodeNumber = parseInt(ep) || 0;
|
||||
|
||||
// ---- FEATURE: Extract synopsis and rating ----
|
||||
const synopsis = $(".synopsis .content").text().trim() || "";
|
||||
const rating = $("#w-rating .score .value").text().trim() || "";
|
||||
const poster = $("img[itemprop='image']").attr("src") || "";
|
||||
// NOTE: Background image is extracted from CSS background-image property on #player
|
||||
const backgroundImage = $("#player").css("background-image")?.match(/url\(['"]?(.+?)['"]?\)/)?.[1] || "";
|
||||
|
||||
// ---- FEATURE: Extract anime metadata (type, status, MAL score, duration, episodes) ----
|
||||
const type = $(".bmeta .meta:first-child > div:nth-child(1) span").text().trim() || "";
|
||||
const status = $(".bmeta .meta:first-child > div:nth-child(4) span a").text().trim() || "";
|
||||
const malScore = $(".bmeta .meta:nth-child(2) > div:nth-child(1) span").text().trim() || "";
|
||||
const duration = $(".bmeta .meta:nth-child(2) > div:nth-child(2) span").text().trim() || "";
|
||||
const episodes = $(".bmeta .meta:nth-child(2) > div:nth-child(3) span").text().trim() || "";
|
||||
|
||||
// ---- FEATURE: Extract genres ----
|
||||
// NOTE: Genre links contain '/genre/' in href to distinguish from other links
|
||||
const genres = [];
|
||||
$(".bmeta .meta:first-child > div:nth-child(5) span a[href*='/genre/']").each((i, el) => {
|
||||
genres.push($(el).text().trim());
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract studios ----
|
||||
// NOTE: Uses itemprop='director' links with itemprop='name' spans inside
|
||||
const studios = [];
|
||||
$(".bmeta .meta:nth-child(2) > div:nth-child(4) span a[itemprop='director'] span[itemprop='name']").each((i, el) => {
|
||||
studios.push($(el).text().trim());
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract next episode release info ----
|
||||
// NOTE: .next-episode shows the countdown text, .count-down has data-target timestamp
|
||||
const nextEpisodeDate = $(".next-episode, .alert.next-episode").text().trim() || "";
|
||||
const nextEpisodeTimestamp = parseInt($(".count-down").attr("data-target")) || 0;
|
||||
|
||||
// ---- FEATURE: Extract available streaming servers ----
|
||||
// NOTE: Each server li has data-link-id (unique), data-ep-id, data-cmid, data-sv-id
|
||||
// NOTE: Server type (sub/dub) is derived from closest .type container's data-type attribute
|
||||
const servers = [];
|
||||
$("#w-servers .servers .type li, #w-servers li[data-link-id]").each((i, el) => {
|
||||
const linkId = $(el).attr("data-link-id") || "";
|
||||
@@ -60,11 +110,14 @@ const extractWatchPage = async (slug, ep) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract trending/watch-order sidebar items ----
|
||||
// NOTE: Grabs from both the first .w-side-section and #watch-order containers
|
||||
const trending = [];
|
||||
$(".w-side-section:first .scaff.side.items a.item, #watch-order a.item").each((i, el) => {
|
||||
const trendSlug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
const trendPoster = $(el).find(".poster img").attr("src") || "";
|
||||
const trendTitle = $(el).find(".name").text().trim() || "";
|
||||
// NOTE: .dot:last-child typically holds the type badge text
|
||||
const trendType = $(el).find(".dot:last-child").text().trim() || "";
|
||||
const trendScore = $(el).find(".score").text().trim() || "";
|
||||
|
||||
@@ -79,6 +132,8 @@ const extractWatchPage = async (slug, ep) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- FEATURE: Extract recommended anime sidebar items ----
|
||||
// NOTE: Targets the section with a title containing "Recommended", or the last sidebar
|
||||
const recommended = [];
|
||||
$(".w-side-section:has(.title:contains('Recommended')) a.item, aside.sidebar:last .w-side-section a.item").each((i, el) => {
|
||||
const recSlug = $(el).attr("href")?.split("/watch/").pop() || "";
|
||||
@@ -125,4 +180,6 @@ const extractWatchPage = async (slug, ep) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { extractWatchPage };
|
||||
export { extractWatchPage };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: watchPage.extractor.js
|
||||
|
||||
Reference in New Issue
Block a user