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:
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
2026-06-08 16:14:01 +05:30
parent 87d6d4dbf2
commit 0034617c3b
55 changed files with 2965 additions and 108 deletions
+60 -1
View File
@@ -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
+54 -1
View File
@@ -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
+57 -1
View File
@@ -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
+47 -1
View File
@@ -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
+46 -1
View File
@@ -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
+62 -1
View File
@@ -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
+48 -1
View File
@@ -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
+84 -1
View File
@@ -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
+53 -1
View File
@@ -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
+47 -1
View File
@@ -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
+45 -1
View File
@@ -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
+61 -1
View File
@@ -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
+38 -1
View File
@@ -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
+50 -1
View File
@@ -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
+54 -1
View File
@@ -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
+77 -1
View File
@@ -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
+49 -1
View File
@@ -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
+50 -1
View File
@@ -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
+47 -1
View File
@@ -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
+53 -1
View File
@@ -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
+38 -1
View File
@@ -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
+58 -1
View File
@@ -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