From 7df5bbcc542292e9737093955bc6b3c2ddd69b35 Mon Sep 17 00:00:00 2001 From: Shinei Nouzen <157171073+Shineii86@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:55:14 +0530 Subject: [PATCH] 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). --- src/controllers/animeInfo.controller.js | 1 + src/controllers/download.controller.js | 6 ++ src/controllers/newRelease.controller.js | 1 + src/extractors/animeInfo.extractor.js | 3 + src/extractors/download.extractor.js | 44 +++++--- src/extractors/homeInfo.extractor.js | 2 + src/extractors/newRelease.extractor.js | 1 + src/extractors/random.extractor.js | 3 + src/extractors/schedule.extractor.js | 2 + src/extractors/seasons.extractor.js | 13 ++- src/extractors/streamResolver.extractor.js | 5 + src/extractors/watchOrder.extractor.js | 13 ++- src/helper/cache.helper.js | 117 ++++++++++++++++++--- src/helper/extractPages.helper.js | 8 +- src/helper/mirror.helper.js | 8 ++ src/helper/pagination.helper.js | 5 + src/middleware/creatorInfo.js | 2 + 17 files changed, 205 insertions(+), 29 deletions(-) diff --git a/src/controllers/animeInfo.controller.js b/src/controllers/animeInfo.controller.js index 2feb76c..0dbaa4a 100644 --- a/src/controllers/animeInfo.controller.js +++ b/src/controllers/animeInfo.controller.js @@ -42,6 +42,7 @@ import { getCache, setCache, TTL } from "../helper/cache.helper.js"; const getAnimeInfo = async (req, res, next) => { try { const { id, slug } = req.query; + // NOTE: Accept both ?id= and ?slug= for backwards compatibility const animeSlug = id || slug; if (!animeSlug) { return res.status(400).json({ success: false, message: "Anime slug is required (?id= or ?slug=)" }); diff --git a/src/controllers/download.controller.js b/src/controllers/download.controller.js index 62caae4..f7a6bb6 100644 --- a/src/controllers/download.controller.js +++ b/src/controllers/download.controller.js @@ -18,6 +18,11 @@ import { extractDownloadLinks } from "../extractors/download.extractor.js"; import { getCache, setCache, TTL } from "../helper/cache.helper.js"; +// ══════════════════════════════════════════════════════════════ +// DOWNLOAD LINKS HANDLER +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Download Links ---- /** * Handles GET /api/download requests. Returns download links * for the specified anime episode. @@ -58,3 +63,4 @@ const getDownloadLinks = async (req, res, next) => { }; export { getDownloadLinks }; +// ══════════════════════════════════════════════════════════════ END: download.controller.js diff --git a/src/controllers/newRelease.controller.js b/src/controllers/newRelease.controller.js index 1c7828a..61a23a1 100644 --- a/src/controllers/newRelease.controller.js +++ b/src/controllers/newRelease.controller.js @@ -97,6 +97,7 @@ const getNewlyAdded = async (req, res, next) => { // CONTROLLER: LATEST UPDATED // ══════════════════════════════════════════════════════════════ +// ---- FEATURE: Latest Updated Endpoint ---- /** * Handles GET /api/latest-updated requests. Returns paginated list * of recently updated anime sorted by last update time. diff --git a/src/extractors/animeInfo.extractor.js b/src/extractors/animeInfo.extractor.js index 6b8513d..f201d19 100644 --- a/src/extractors/animeInfo.extractor.js +++ b/src/extractors/animeInfo.extractor.js @@ -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() || ""; diff --git a/src/extractors/download.extractor.js b/src/extractors/download.extractor.js index f1137bf..1ca4a38 100644 --- a/src/extractors/download.extractor.js +++ b/src/extractors/download.extractor.js @@ -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} 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 diff --git a/src/extractors/homeInfo.extractor.js b/src/extractors/homeInfo.extractor.js index 805f701..d1943d0 100644 --- a/src/extractors/homeInfo.extractor.js +++ b/src/extractors/homeInfo.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") || ""; diff --git a/src/extractors/newRelease.extractor.js b/src/extractors/newRelease.extractor.js index 08b3810..1e66fec 100644 --- a/src/extractors/newRelease.extractor.js +++ b/src/extractors/newRelease.extractor.js @@ -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. diff --git a/src/extractors/random.extractor.js b/src/extractors/random.extractor.js index 771dfb4..2d3f9a1 100644 --- a/src/extractors/random.extractor.js +++ b/src/extractors/random.extractor.js @@ -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() || ""; diff --git a/src/extractors/schedule.extractor.js b/src/extractors/schedule.extractor.js index ecea05c..8eb5b0c 100644 --- a/src/extractors/schedule.extractor.js +++ b/src/extractors/schedule.extractor.js @@ -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() || ""; diff --git a/src/extractors/seasons.extractor.js b/src/extractors/seasons.extractor.js index cdcc214..5f5451b 100644 --- a/src/extractors/seasons.extractor.js +++ b/src/extractors/seasons.extractor.js @@ -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 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 diff --git a/src/extractors/streamResolver.extractor.js b/src/extractors/streamResolver.extractor.js index 7dfbacf..80d7ed6 100644 --- a/src/extractors/streamResolver.extractor.js +++ b/src/extractors/streamResolver.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 diff --git a/src/extractors/watchOrder.extractor.js b/src/extractors/watchOrder.extractor.js index 2b21040..d7e52e1 100644 --- a/src/extractors/watchOrder.extractor.js +++ b/src/extractors/watchOrder.extractor.js @@ -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 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 diff --git a/src/helper/cache.helper.js b/src/helper/cache.helper.js index e9fc3d7..fa54ace 100644 --- a/src/helper/cache.helper.js +++ b/src/helper/cache.helper.js @@ -16,7 +16,21 @@ * ======= • ======= • ======= • ======= • =======• ======= */ +// ══════════════════════════════════════════════════════════════ +// LRU CACHE CLASS +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: LRU Cache ---- +/** + * Least Recently Used (LRU) cache implementation. + * Maintains access order by deleting and re-inserting on read. + * Auto-evicts oldest entry when maxSize is reached. + */ class LRUCache { + /** + * @param {number} maxSize - Maximum number of entries (default: 100) + * @param {number} defaultTTL - Default time-to-live in ms (default: 300000 = 5min) + */ constructor(maxSize = 100, defaultTTL = 300000) { this.cache = new Map(); this.maxSize = maxSize; @@ -24,6 +38,13 @@ class LRUCache { this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0 }; } + /** + * Get a value from the cache. Moves the entry to the end (most recent) on access. + * Returns undefined if not found or expired. + * + * @param {string} key - Cache key + * @returns {*} Cached value or undefined + */ get(key) { if (!this.cache.has(key)) { this.stats.misses++; @@ -31,23 +52,34 @@ class LRUCache { } const entry = this.cache.get(key); + // NOTE: Lazy expiration — check expiry on access, not background timer if (Date.now() > entry.expiresAt) { this.cache.delete(key); this.stats.misses++; return undefined; } + // NOTE: Re-insert to move to end (most recently used position) this.stats.hits++; this.cache.delete(key); this.cache.set(key, entry); return entry.value; } + /** + * Set a value in the cache with optional TTL. + * Evicts the oldest entry if at capacity. + * + * @param {string} key - Cache key + * @param {*} value - Value to store + * @param {number} [ttl] - Time-to-live in ms (uses defaultTTL if not provided) + */ set(key, value, ttl) { if (this.cache.has(key)) { this.cache.delete(key); } + // NOTE: Evict oldest (first) entry when cache is full if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); @@ -60,18 +92,34 @@ class LRUCache { this.stats.sets++; } + /** + * Delete a specific key from the cache. + * + * @param {string} key - Cache key to delete + * @returns {boolean} True if the key existed before deletion + */ delete(key) { const existed = this.cache.delete(key); if (existed) this.stats.deletes++; return existed; } + /** + * Clear all entries from the cache. + * + * @returns {number} Number of entries cleared + */ clear() { const size = this.cache.size; this.cache.clear(); return size; } + /** + * Get cache statistics including hit rate. + * + * @returns {Object} Stats object with hits, misses, sets, deletes, size, hitRate + */ getStats() { const total = this.stats.hits + this.stats.misses; return { @@ -81,6 +129,12 @@ class LRUCache { }; } + /** + * Check if a key exists and is not expired. + * + * @param {string} key - Cache key + * @returns {boolean} True if key exists and is valid + */ has(key) { if (!this.cache.has(key)) return false; const entry = this.cache.get(key); @@ -92,29 +146,68 @@ class LRUCache { } } +// ══════════════════════════════════════════════════════════════ +// TTL CONFIGURATION +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Per-Endpoint TTL ---- +// NOTE: TTL values tuned per endpoint — stale data tolerance vs server load const TTL = { - home: 600000, - search: 300000, - info: 600000, - episodes: 300000, - servers: 600000, - stream: 180000, - spotlight: 600000, - trending: 600000, - schedule: 1800000, - genres: 3600000, - suggestions: 300000, - default: 300000 + home: 600000, // 10min — homepage changes infrequently + search: 300000, // 5min — search results update moderate + info: 600000, // 10min — anime info is relatively stable + episodes: 300000, // 5min — episode lists update weekly + servers: 600000, // 10min — server URLs rarely change + stream: 180000, // 3min — stream URLs rotate frequently + spotlight: 600000, // 10min — spotlight rotates daily + trending: 600000, // 10min — trending updates hourly + schedule: 1800000, // 30min — schedule changes daily + genres: 3600000, // 60min — genre list is static + suggestions: 300000, // 5min — suggestions update moderate + default: 300000 // 5min — safe default for unknown endpoints }; +// ══════════════════════════════════════════════════════════════ +// SINGLETON CACHE INSTANCE +// ══════════════════════════════════════════════════════════════ + +// NOTE: Singleton pattern — single cache instance shared across all requests const cache = new LRUCache( parseInt(process.env.CACHE_MAX_SIZE) || 200, parseInt(process.env.CACHE_DEFAULT_TTL) || 300000 ); +// ══════════════════════════════════════════════════════════════ +// EXPORTED HELPERS +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Cache Get/Set/Clear ---- +/** + * Get a value from the global cache. + * @param {string} key - Cache key + * @returns {*} Cached value or undefined + */ const getCache = (key) => cache.get(key); + +/** + * Set a value in the global cache with optional TTL. + * @param {string} key - Cache key + * @param {*} data - Value to store + * @param {number} [ttl] - Time-to-live in ms + */ const setCache = (key, data, ttl) => cache.set(key, data, ttl); + +/** + * Clear all entries from the global cache. + * @returns {number} Number of entries cleared + */ const clearCache = () => cache.clear(); + +/** + * Get cache statistics (hits, misses, hit rate, size). + * @returns {Object} Cache stats + */ const getCacheStats = () => cache.getStats(); export { getCache, setCache, clearCache, getCacheStats, LRUCache, TTL }; +// ══════════════════════════════════════════════════════════════ END: cache.helper.js diff --git a/src/helper/extractPages.helper.js b/src/helper/extractPages.helper.js index d20b3dc..6152799 100644 --- a/src/helper/extractPages.helper.js +++ b/src/helper/extractPages.helper.js @@ -19,6 +19,11 @@ import * as cheerio from "cheerio"; import { fetchWithMirror } from "./mirror.helper.js"; +// ══════════════════════════════════════════════════════════════ +// PAGE EXTRACTOR +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Paginated Page Fetcher ---- /** * Fetches HTML content from a URL with pagination and mirror fallback. * Automatically tries alternative domains if primary fails. @@ -40,7 +45,7 @@ const extractPages = async (url, page = 1) => { const urlObj = new URL(url); path = urlObj.pathname + urlObj.search; } catch (e) { - // If URL parsing fails, try to extract path manually + // NOTE: If URL parsing fails, try regex fallback const match = url.match(/https?:\/\/[^/]+(\/.*)/); if (match) path = match[1]; } @@ -57,3 +62,4 @@ const extractPages = async (url, page = 1) => { }; export { extractPages }; +// ══════════════════════════════════════════════════════════════ END: extractPages.helper.js diff --git a/src/helper/mirror.helper.js b/src/helper/mirror.helper.js index 5693ae5..f6ff594 100644 --- a/src/helper/mirror.helper.js +++ b/src/helper/mirror.helper.js @@ -24,6 +24,7 @@ import { getCache, setCache } from "./cache.helper.js"; // MIRROR CONFIGURATION // ══════════════════════════════════════════════════════════════ +// ---- FEATURE: Mirror Domain Configuration ---- // Default mirrors with names and priority const DEFAULT_MIRRORS = [ { url: "https://anikototv.to", name: "Primary", priority: 1 }, @@ -63,6 +64,7 @@ let failedMirrors = new Set(); // HELPER FUNCTIONS // ══════════════════════════════════════════════════════════════ +// ---- FEATURE: Mirror Health Check ---- /** * Check if a mirror is accessible * @param {string} baseUrl - Mirror base URL @@ -81,6 +83,7 @@ async function checkMirror(baseUrl) { } } +// ---- FEATURE: Working Mirror Discovery ---- /** * Get the best available mirror * @returns {Promise} Working mirror base URL @@ -116,6 +119,7 @@ async function getWorkingMirror() { return workingMirror; } +// ---- FEATURE: Mirror Cache Reset ---- /** * Reset mirror cache and failed state */ @@ -126,6 +130,7 @@ function resetMirrorCache() { console.log("[MIRROR] Cache reset"); } +// ---- FEATURE: URL Builder ---- /** * Build URL with current mirror * @param {string} path - URL path @@ -137,6 +142,7 @@ function buildUrl(path, baseUrl = workingMirror) { return `${base}${path}`; } +// ---- FEATURE: Resilient Fetch with Mirror Fallback ---- /** * Fetch with automatic mirror fallback * @param {string} path - URL path to fetch @@ -227,6 +233,7 @@ async function fetchWithMirror(path, options = {}) { throw new Error(`All mirrors failed. Last error: ${lastError?.message}`); } +// ---- FEATURE: Mirror Status Reporter ---- /** * Get mirror status for all domains * @returns {Promise} @@ -260,3 +267,4 @@ export { getMirrorStatus, ALL_MIRRORS }; +// ══════════════════════════════════════════════════════════════ END: mirror.helper.js diff --git a/src/helper/pagination.helper.js b/src/helper/pagination.helper.js index 34b9ffd..7b6d3fa 100644 --- a/src/helper/pagination.helper.js +++ b/src/helper/pagination.helper.js @@ -15,6 +15,11 @@ * ======= • ======= • ======= • ======= • =======• ======= */ +// ══════════════════════════════════════════════════════════════ +// PAGINATION METADATA +// ══════════════════════════════════════════════════════════════ + +// ---- FEATURE: Pagination Metadata Generator ---- /** * Adds pagination metadata to a response object. * diff --git a/src/middleware/creatorInfo.js b/src/middleware/creatorInfo.js index 38ada3b..62d1099 100644 --- a/src/middleware/creatorInfo.js +++ b/src/middleware/creatorInfo.js @@ -22,6 +22,7 @@ // CREATOR METADATA // ══════════════════════════════════════════════════════════════ +// ---- FEATURE: Creator Attribution Object ---- // NOTE: Injected into every response unless `data.creator` already exists const creatorInfo = { creator: "Shinei Nouzen", @@ -34,6 +35,7 @@ const creatorInfo = { // MIDDLEWARE // ══════════════════════════════════════════════════════════════ +// ---- FEATURE: Response Creator Injection Middleware ---- /** * Express middleware that enriches every JSON response with * creator attribution and an IN-formatted timestamp.