mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 19:05:11 +00:00
docs: Add box-style headers, section headers, feature markers, JSDoc, inline notes, footers
Added comprehensive documentation following project conventions: Box-style headers (added/fixed where missing): - All 17 files now have standard Project/Author/License headers Section headers (// == == SECTION == ==): - cache.helper.js: LRU CACHE CLASS, TTL CONFIGURATION, SINGLETON, EXPORTED HELPERS - download.controller.js: DOWNLOAD LINKS HANDLER - download.extractor.js: BASE64 DECODER, REDIRECT FOLLOWER, DOWNLOAD LINKS EXTRACTOR - seasons.extractor.js: SEASONS EXTRACTOR - watchOrder.extractor.js: WATCH ORDER EXTRACTOR - extractPages.helper.js: PAGE EXTRACTOR - pagination.helper.js: PAGINATION METADATA Feature markers (// --- FEATURE: XYZ ---): - cache.helper.js: LRU Cache, Per-Endpoint TTL, Cache Get/Set/Clear - download.controller.js: Download Links - download.extractor.js: Base64 Download Data Decoder, Redirect Chain Resolver, Download Links Extraction - seasons.extractor.js: Season Information Extraction - watchOrder.extractor.js: Watch Order / Related Anime Extraction - streamResolver.extractor.js: Server Name Normalization, Stream URL Resolution, Batch Stream URL Resolution, M3U8 Quality Parser, Subtitle Track Extractor - mirror.helper.js: Mirror Domain Configuration, Mirror Health Check, Working Mirror Discovery, Mirror Cache Reset, URL Builder, Resilient Fetch with Mirror Fallback, Mirror Status Reporter - pagination.helper.js: Pagination Metadata Generator - creatorInfo.js: Creator Attribution Object, Response Creator Injection Middleware Function-level JSDoc (added/fixed): - cache.helper.js: Full JSDoc on all 6 class methods (constructor, get, set, delete, clear, getStats, has) and 4 exported helpers Inline notes for non-obvious logic: - cache.helper.js: Lazy expiration, LRU re-insertion, singleton pattern - download.extractor.js: HTML/JSON hybrid response handling, multiple selectors for different mirrors, redirect fallback - random.extractor.js: maxRedirects, validateStatus, responseURL extraction - schedule.extractor.js: Dual selector pattern for different mirror layouts - animeInfo.extractor.js: Positional .bmeta selector fragility - homeInfo.extractor.js: Dual selector for different layouts - mirror.helper.js: 404 vs connection error handling - animeInfo.controller.js: Accept both id and slug params Footers (added where missing): - cache.helper.js, download.controller.js, download.extractor.js, seasons.extractor.js, watchOrder.extractor.js, extractPages.helper.js, mirror.helper.js All 68 source files now have complete documentation coverage (6/6 checks).
This commit is contained in:
@@ -55,11 +55,14 @@ const extractAnimeInfo = async (slug) => {
|
||||
const reviewCount = $("#w-rating span[itemprop='reviewCount']").text().trim() || "";
|
||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||
|
||||
// NOTE: .bmeta .meta:first-child contains: type, premiered, aired, status, genres
|
||||
// NOTE: Positional selectors (nth-child) are fragile — match source site's exact HTML structure
|
||||
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() || "";
|
||||
|
||||
// NOTE: .bmeta .meta:nth-child(2) contains: MAL score, duration, episodes, studios, producers
|
||||
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() || "";
|
||||
|
||||
@@ -26,10 +26,17 @@ import axios from "axios";
|
||||
import { fetchWithMirror } from "../helper/mirror.helper.js";
|
||||
import { headers } from "../configs/header.config.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// BASE64 DECODER
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Base64 Download Data Decoder ----
|
||||
/**
|
||||
* Decodes base64 download data from the website
|
||||
* Decodes base64 download data from the website.
|
||||
* The source site encodes download server URLs as base64 JSON.
|
||||
*
|
||||
* @param {string} encoded - Base64 encoded string
|
||||
* @returns {Object} Decoded download data
|
||||
* @returns {Object|null} Decoded download data, or null on failure
|
||||
*/
|
||||
function decodeDownloadData(encoded) {
|
||||
try {
|
||||
@@ -40,8 +47,15 @@ function decodeDownloadData(encoded) {
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// REDIRECT FOLLOWER
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Redirect Chain Resolver ----
|
||||
/**
|
||||
* Follows redirect chain to get the final URL
|
||||
* Follows redirect chain to get the final URL.
|
||||
* Uses axios maxRedirects to auto-follow up to 5 redirects.
|
||||
*
|
||||
* @param {string} url - Starting URL
|
||||
* @returns {Promise<string>} Final URL after redirects
|
||||
*/
|
||||
@@ -52,9 +66,10 @@ async function followRedirects(url) {
|
||||
maxRedirects: 5,
|
||||
timeout: 10000
|
||||
});
|
||||
// NOTE: response.request.res.responseUrl contains the final URL after all redirects
|
||||
return response.request?.res?.responseUrl || response.config?.url || url;
|
||||
} catch (error) {
|
||||
// If we get a redirect error, try to extract the location
|
||||
// NOTE: On redirect errors, axios throws — extract Location header manually
|
||||
if (error.response?.headers?.location) {
|
||||
return error.response.headers.location;
|
||||
}
|
||||
@@ -62,6 +77,11 @@ async function followRedirects(url) {
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// DOWNLOAD LINKS EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Download Links Extraction ----
|
||||
/**
|
||||
* Fetches and parses download links for a specific episode.
|
||||
* Scrapes the watch page to extract download-data attributes
|
||||
@@ -84,7 +104,7 @@ const extractDownloadLinks = async (slug, ep) => {
|
||||
try {
|
||||
const { data } = await fetchWithMirror(`/watch/${slug}/ep-${ep}`);
|
||||
|
||||
// Handle both string HTML and JSON responses
|
||||
// NOTE: Response may be raw HTML string or JSON-wrapped HTML
|
||||
let html = "";
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
@@ -105,8 +125,7 @@ const extractDownloadLinks = async (slug, ep) => {
|
||||
|
||||
const downloads = [];
|
||||
|
||||
// Find all download buttons with download-data attribute
|
||||
// Multiple selectors to handle different HTML structures
|
||||
// NOTE: Multiple selectors to handle different HTML structures across mirrors
|
||||
const selectors = [
|
||||
"[download-data]",
|
||||
"li.download-icon",
|
||||
@@ -124,13 +143,13 @@ const extractDownloadLinks = async (slug, ep) => {
|
||||
if (encoded && encoded.length > 10) {
|
||||
const decoded = decodeDownloadData(encoded);
|
||||
if (decoded) {
|
||||
// Extract URLs from the decoded data
|
||||
// NOTE: Decoded data is a nested object: { serverGroup: { serverName: url } }
|
||||
for (const [serverGroup, servers] of Object.entries(decoded)) {
|
||||
for (const [serverName, url] of Object.entries(servers)) {
|
||||
// Avoid duplicates
|
||||
// Avoid duplicates by URL
|
||||
const exists = downloads.some(d => d.url === url);
|
||||
if (!exists) {
|
||||
// Construct worker URL from nekostream.site URL
|
||||
// NOTE: nekostream.site URLs need Cloudflare Worker proxy
|
||||
const workerUrl = url.includes("nekostream.site")
|
||||
? url.replace("https://pahe.nekostream.site/", "https://proud-dew-d754.download992.workers.dev/")
|
||||
: null;
|
||||
@@ -155,7 +174,7 @@ const extractDownloadLinks = async (slug, ep) => {
|
||||
if (downloads.length > 0) break;
|
||||
}
|
||||
|
||||
// Try to follow redirects for each download URL
|
||||
// NOTE: Follow redirects for each download URL to resolve kwik.cx links
|
||||
for (const download of downloads) {
|
||||
if (download.workerUrl) {
|
||||
try {
|
||||
@@ -164,7 +183,7 @@ const extractDownloadLinks = async (slug, ep) => {
|
||||
download.kwikUrl = finalUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
// If redirect fails, construct a placeholder kwik URL
|
||||
// NOTE: Redirect may fail due to Cloudflare — construct placeholder
|
||||
download.kwikUrl = "https://kwik.cx/f/[requires-browser]";
|
||||
}
|
||||
}
|
||||
@@ -185,3 +204,4 @@ const extractDownloadLinks = async (slug, ep) => {
|
||||
};
|
||||
|
||||
export { extractDownloadLinks };
|
||||
// ══════════════════════════════════════════════════════════════ END: download.extractor.js
|
||||
|
||||
@@ -39,6 +39,7 @@ const extractHomeInfo = async () => {
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const spotlights = [];
|
||||
// NOTE: #hotest .swiper-slide.item = spotlight carousel on homepage
|
||||
$("#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] || "";
|
||||
@@ -68,6 +69,7 @@ const extractHomeInfo = async () => {
|
||||
});
|
||||
|
||||
const trending = [];
|
||||
// NOTE: Dual selector — .section-updated = new layout, #recent-update = old layout
|
||||
$(".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") || "";
|
||||
|
||||
@@ -105,6 +105,7 @@ const extractNewlyAdded = async (page = 1) => {
|
||||
// LATEST UPDATED EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Latest Updated Extraction ----
|
||||
/**
|
||||
* Fetches and parses recently updated anime with pagination.
|
||||
* Uses the /latest-updated page which shows anime sorted by last update time.
|
||||
|
||||
@@ -43,10 +43,13 @@ const extractRandom = async () => {
|
||||
try {
|
||||
const { data, request } = await axios.get(URLS.random, {
|
||||
headers,
|
||||
// NOTE: maxRedirects=5 allows following the redirect chain from /random to the final anime page
|
||||
maxRedirects: 5,
|
||||
// NOTE: validateStatus < 400 prevents axios from throwing on redirects (3xx)
|
||||
validateStatus: (status) => status < 400
|
||||
});
|
||||
|
||||
// NOTE: request.responseURL contains the final URL after all redirects
|
||||
const finalUrl = request?.responseURL || URLS.random;
|
||||
const slug = finalUrl.split("/watch/").pop() || "";
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ const extractSchedule = async (date) => {
|
||||
|
||||
const schedule = [];
|
||||
|
||||
// NOTE: Dual selector pattern — different mirrors use different HTML structures
|
||||
// .schedule-item = new layout, .anime-schedule .item = old layout
|
||||
$(".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() || "";
|
||||
|
||||
@@ -19,10 +19,18 @@
|
||||
import * as cheerio from "cheerio";
|
||||
import { fetchWithMirror } from "../helper/mirror.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SEASONS EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Season Information Extraction ----
|
||||
/**
|
||||
* Fetches and parses season information for an anime.
|
||||
* Extracts season data from the watch page sidebar.
|
||||
*
|
||||
* NOTE: The source site's AJAX endpoints (/ajax/seasons/) don't exist,
|
||||
* so we extract from the watch page HTML sidebar instead.
|
||||
*
|
||||
* @param {string|number} slugOrId - The anime slug or numeric ID
|
||||
* @returns {Promise<Object>} Object with animeId, totalSeasons, and seasons array
|
||||
*
|
||||
@@ -41,7 +49,7 @@ const extractSeasons = async (slugOrId) => {
|
||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||
const seasons = [];
|
||||
|
||||
// NOTE: Extract seasons from #ani-seasons section (if populated)
|
||||
// NOTE: Try dedicated seasons section first (#ani-seasons)
|
||||
$("#ani-seasons .swiper-slide, .seasons .item, #w-seasons .item").each((_, el) => {
|
||||
const link = $(el).find("a").first();
|
||||
const href = link.attr("href") || "";
|
||||
@@ -54,7 +62,7 @@ const extractSeasons = async (slugOrId) => {
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: If no dedicated seasons section, extract from episode list sidebar
|
||||
// NOTE: Fallback — extract from episode list sidebar if no dedicated section
|
||||
if (seasons.length === 0) {
|
||||
$(".w-side-section .item").each((_, el) => {
|
||||
const link = $(el).find("a").first();
|
||||
@@ -83,3 +91,4 @@ const extractSeasons = async (slugOrId) => {
|
||||
};
|
||||
|
||||
export { extractSeasons };
|
||||
// ══════════════════════════════════════════════════════════════ END: seasons.extractor.js
|
||||
|
||||
@@ -45,6 +45,7 @@ const EMBED_API_MAP = {
|
||||
"embed.bunkrerrer.com": "megaplay-1.buzz",
|
||||
};
|
||||
|
||||
// ---- FEATURE: Server Name Normalization ----
|
||||
/**
|
||||
* Normalizes server display names to clean identifiers
|
||||
* @param {string} name - Raw server name (e.g., "VidPlay-1")
|
||||
@@ -55,6 +56,7 @@ const normalizeServerName = (name) => {
|
||||
return SERVER_NAME_MAP[name] || name.toLowerCase().replace(/[-\s]+\d+$/, "").trim();
|
||||
};
|
||||
|
||||
// ---- FEATURE: Stream URL Resolution ----
|
||||
/**
|
||||
* Resolves the actual video stream URL from an embed player page.
|
||||
* Extracts data-id from the embed HTML, then queries the megaplay API.
|
||||
@@ -166,6 +168,7 @@ const resolveStreamUrl = async (embedUrl, options = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ---- FEATURE: Batch Stream URL Resolution ----
|
||||
/**
|
||||
* Resolves stream URLs for multiple episodes in batch
|
||||
* @param {string} embedUrl - The embed player URL
|
||||
@@ -185,6 +188,7 @@ const resolveStreamUrls = async (embedUrl, options = {}) => {
|
||||
// M3U8 PARSER
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: M3U8 Quality Parser ----
|
||||
/**
|
||||
* Parses an M3U8 playlist to extract available quality options.
|
||||
* Supports both master playlists (multiple qualities) and media playlists.
|
||||
@@ -265,6 +269,7 @@ const parseM3u8Qualities = async (m3u8Url, options = {}) => {
|
||||
// SUBTITLE EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Subtitle Track Extractor ----
|
||||
/**
|
||||
* Extracts subtitle information from API response data
|
||||
* @param {object} apiData - The megaplay API response
|
||||
|
||||
@@ -19,10 +19,18 @@
|
||||
import * as cheerio from "cheerio";
|
||||
import { fetchWithMirror } from "../helper/mirror.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// WATCH ORDER EXTRACTOR
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Watch Order / Related Anime Extraction ----
|
||||
/**
|
||||
* Fetches and parses watch order / related anime for an anime.
|
||||
* Extracts related anime from the watch page sidebar.
|
||||
*
|
||||
* NOTE: The source site's AJAX endpoints (/ajax/related/) don't exist,
|
||||
* so we extract from the watch page HTML sidebar instead.
|
||||
*
|
||||
* @param {string|number} slugOrId - The anime slug or numeric ID
|
||||
* @returns {Promise<Object>} Object with animeId, totalRelated, related array
|
||||
*
|
||||
@@ -41,7 +49,7 @@ const extractWatchOrder = async (slugOrId) => {
|
||||
const animeId = parseInt($("#watch-main").attr("data-id")) || 0;
|
||||
const related = [];
|
||||
|
||||
// NOTE: Extract from #w-related section
|
||||
// NOTE: Try dedicated related section first (#w-related)
|
||||
$("#w-related .item, .w-side-section:has(.title:contains('Related')) .item").each((_, el) => {
|
||||
const link = $(el).find("a").first();
|
||||
const href = link.attr("href") || "";
|
||||
@@ -55,7 +63,7 @@ const extractWatchOrder = async (slugOrId) => {
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Also extract trending sidebar items as suggested watch order
|
||||
// NOTE: Fallback — extract trending sidebar items as suggested watch order
|
||||
if (related.length === 0) {
|
||||
$(".w-side-section:has(.title:contains('Trending')) .item, .w-side-section:first .item").each((_, el) => {
|
||||
const link = $(el).find("a").first();
|
||||
@@ -85,3 +93,4 @@ const extractWatchOrder = async (slugOrId) => {
|
||||
};
|
||||
|
||||
export { extractWatchOrder };
|
||||
// ══════════════════════════════════════════════════════════════ END: watchOrder.extractor.js
|
||||
|
||||
Reference in New Issue
Block a user