mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
feat: Add new endpoints and enhance filter — v2.2.0
New Endpoints (4 new routes): - GET /api/upcoming — upcoming anime releases from homepage - GET /api/top-rankings?sort=top|newest — ranked anime with sort modes - GET /api/recently-updated?tab=all|dub|sub — filtered recent updates - GET /api/completed — finished anime series from homepage Enhanced Filter (/api/filter): - Added source parameter (18 source material types) - Added epMin/epMax for episode range filtering - Added excludeWatchlist parameter for logged-in users - Added season parameter (spring/summer/fall/winter) New Config IDs: - SOURCE_IDS: 18 source material type mappings - SEASON_IDS: 4 broadcast season mappings - EXTENDED_TYPE_IDS: Added tv-short and tv-special types Total endpoints: 34 (was 30) OpenAPI spec updated with new endpoints CHANGELOG.md updated with comprehensive change history
This commit is contained in:
@@ -16,6 +16,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Body Size Limits**: Added `express.json({ limit: '10kb' })` to prevent large payload attacks
|
||||
- **Rate Limiter Cleanup**: Automatic cleanup of stale IP entries every 5 minutes
|
||||
- **Uncaught Exception Handler**: Added handler for uncaught exceptions with graceful shutdown
|
||||
- **Upcoming Anime Endpoint**: `GET /api/upcoming` — upcoming anime releases from homepage
|
||||
- **Top Anime Rankings Endpoint**: `GET /api/top-rankings?sort=top|newest` — ranked anime with sort modes
|
||||
- **Recently Updated Tabs Endpoint**: `GET /api/recently-updated?tab=all|dub|sub` — filtered updates
|
||||
- **Completed Anime Endpoint**: `GET /api/completed` — finished anime series from homepage
|
||||
- **Enhanced Filter**: Added `source`, `epMin`, `epMax`, `excludeWatchlist` parameters to `/api/filter`
|
||||
- **Source IDs Config**: New `SOURCE_IDS` mapping for 18 source material types
|
||||
- **Season IDs Config**: New `SEASON_IDS` mapping for 4 broadcast seasons
|
||||
- **Extended Type IDs**: Added `tv-short` and `tv-special` type mappings
|
||||
|
||||
### Changed
|
||||
- **Mirror Fallback**: All 22 extractors now use `fetchWithMirror()` for consistent mirror fallback
|
||||
|
||||
@@ -212,5 +212,90 @@ const SORT_IDS = {
|
||||
"number_of_episodes": "7"
|
||||
};
|
||||
|
||||
export { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS };
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SOURCE TYPE ID MAPPING
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Source type slug to numeric ID mapping ----
|
||||
/**
|
||||
* Maps anime source type slugs to their corresponding numeric IDs.
|
||||
* Used to filter anime by their original source material.
|
||||
*
|
||||
* @type {Object}
|
||||
* @property {string} manga - ID for manga source
|
||||
* @property {string} light-novel - ID for light novel source
|
||||
* @property {string} original - ID for original work
|
||||
* @property {string} visual-novel - ID for visual novel source
|
||||
* @property {string} game - ID for game source
|
||||
* @property {string} 4-koma-manga - ID for 4-koma manga source
|
||||
*
|
||||
* @example
|
||||
* // Get source ID for filtering
|
||||
* SOURCE_IDS["manga"] // "6"
|
||||
* SOURCE_IDS["original"] // "10"
|
||||
*/
|
||||
const SOURCE_IDS = {
|
||||
"4-koma-manga": "1",
|
||||
"book": "2",
|
||||
"card-game": "3",
|
||||
"game": "4",
|
||||
"light-novel": "5",
|
||||
"manga": "6",
|
||||
"mixed-media": "7",
|
||||
"music": "8",
|
||||
"novel": "9",
|
||||
"original": "10",
|
||||
"other": "11",
|
||||
"picture-book": "12",
|
||||
"radio": "13",
|
||||
"unknown": "14",
|
||||
"video-game": "15",
|
||||
"visual-novel": "16",
|
||||
"web-manga": "17",
|
||||
"web-novel": "18"
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SEASON ID MAPPING
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Season slug to ID mapping ----
|
||||
/**
|
||||
* Maps anime season slugs to their corresponding IDs.
|
||||
* Used to filter anime by their broadcast season.
|
||||
*
|
||||
* @type {Object}
|
||||
* @property {string} spring - ID for spring season
|
||||
* @property {string} summer - ID for summer season
|
||||
* @property {string} fall - ID for fall season
|
||||
* @property {string} winter - ID for winter season
|
||||
*
|
||||
* @example
|
||||
* // Get season ID for filtering
|
||||
* SEASON_IDS["spring"] // "1"
|
||||
*/
|
||||
const SEASON_IDS = {
|
||||
"spring": "1",
|
||||
"summer": "2",
|
||||
"fall": "3",
|
||||
"winter": "4"
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// TV-SPECIFIC TYPE IDS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extended type slug to numeric ID mapping ----
|
||||
/**
|
||||
* Extended type mappings including TV-specific formats.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
const EXTENDED_TYPE_IDS = {
|
||||
...TYPE_IDS,
|
||||
"tv-short": "7",
|
||||
"tv-special": "8"
|
||||
};
|
||||
|
||||
export { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS, SOURCE_IDS, SEASON_IDS, EXTENDED_TYPE_IDS };
|
||||
// ══════════════════════════════════════════════════════════════ END: ids.config.js
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKotoAPI — completedAnime.controller.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
* @description
|
||||
* Controller for completed anime section. Returns list of anime
|
||||
* that have finished airing from the homepage.
|
||||
*
|
||||
* @exports
|
||||
* getCompletedAnime - Express route handler for GET /api/completed
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import { extractCompletedAnime } from "../extractors/completedAnime.extractor.js";
|
||||
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CONTROLLER: COMPLETED ANIME
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Completed anime endpoint ----
|
||||
/**
|
||||
* Handles GET /api/completed requests. Returns list of anime
|
||||
* that have finished airing from the homepage section.
|
||||
*
|
||||
* @param {object} req - Express request object
|
||||
* @param {object} res - Express response object
|
||||
* @param {Function} next - Express next middleware function
|
||||
* @returns {void} Sends JSON response with completed anime list
|
||||
*
|
||||
* @example
|
||||
* GET /api/completed
|
||||
* Response: { success: true, results: [...], ... }
|
||||
*/
|
||||
const getCompletedAnime = async (req, res, next) => {
|
||||
try {
|
||||
const cacheKey = "completed";
|
||||
const cached = getCache(cacheKey);
|
||||
if (cached) {
|
||||
return res.json({ success: true, results: cached });
|
||||
}
|
||||
const data = await extractCompletedAnime();
|
||||
setCache(cacheKey, data, TTL.default);
|
||||
res.json({ success: true, results: data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
export { getCompletedAnime };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: completedAnime.controller.js
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKotoAPI — recentlyUpdatedTabs.controller.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
* @description
|
||||
* Controller for recently updated anime with tab filtering.
|
||||
* Returns recently updated anime filtered by all, dub, or sub.
|
||||
*
|
||||
* @exports
|
||||
* getRecentlyUpdatedTabs - Express route handler for GET /api/recently-updated
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import { extractRecentlyUpdatedTabs } from "../extractors/recentlyUpdatedTabs.extractor.js";
|
||||
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CONTROLLER: RECENTLY UPDATED TABS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Recently updated with tab filtering endpoint ----
|
||||
/**
|
||||
* Handles GET /api/recently-updated requests. Returns recently updated
|
||||
* anime filtered by tab: all, dub, or sub.
|
||||
*
|
||||
* @param {object} req - Express request object
|
||||
* @param {string} [req.query.tab="all"] - Tab filter: "all", "dub", or "sub"
|
||||
* @param {object} res - Express response object
|
||||
* @param {Function} next - Express next middleware function
|
||||
* @returns {void} Sends JSON response with recently updated anime list
|
||||
*
|
||||
* @example
|
||||
* GET /api/recently-updated?tab=dub
|
||||
* Response: { success: true, results: [...], tab: "dub" }
|
||||
*/
|
||||
const getRecentlyUpdatedTabs = async (req, res, next) => {
|
||||
try {
|
||||
const { tab = "all" } = req.query;
|
||||
const cacheKey = `recently_updated_${tab}`;
|
||||
const cached = getCache(cacheKey);
|
||||
if (cached) {
|
||||
return res.json({ success: true, results: cached, tab });
|
||||
}
|
||||
const data = await extractRecentlyUpdatedTabs(tab);
|
||||
setCache(cacheKey, data, TTL.default);
|
||||
res.json({ success: true, results: data, tab });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
export { getRecentlyUpdatedTabs };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: recentlyUpdatedTabs.controller.js
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKotoAPI — topAnimeRankings.controller.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
* @description
|
||||
* Controller for top anime rankings. Returns ranked anime list
|
||||
* with support for top (all-time) and newest sort modes.
|
||||
*
|
||||
* @exports
|
||||
* getTopAnimeRankings - Express route handler for GET /api/top-rankings
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import { extractTopAnimeRankings } from "../extractors/topAnimeRankings.extractor.js";
|
||||
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CONTROLLER: TOP ANIME RANKINGS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Top anime rankings endpoint ----
|
||||
/**
|
||||
* Handles GET /api/top-rankings requests. Returns ranked anime list
|
||||
* with support for top (all-time) and newest sort modes.
|
||||
*
|
||||
* @param {object} req - Express request object
|
||||
* @param {string} [req.query.sort="top"] - Sort mode: "top" or "newest"
|
||||
* @param {object} res - Express response object
|
||||
* @param {Function} next - Express next middleware function
|
||||
* @returns {void} Sends JSON response with ranked anime list
|
||||
*
|
||||
* @example
|
||||
* GET /api/top-rankings?sort=top
|
||||
* Response: { success: true, results: [{ rank: 1, title: "...", ... }] }
|
||||
*/
|
||||
const getTopAnimeRankings = async (req, res, next) => {
|
||||
try {
|
||||
const { sort = "top" } = req.query;
|
||||
const cacheKey = `top_rankings_${sort}`;
|
||||
const cached = getCache(cacheKey);
|
||||
if (cached) {
|
||||
return res.json({ success: true, results: cached });
|
||||
}
|
||||
const data = await extractTopAnimeRankings(sort);
|
||||
setCache(cacheKey, data, TTL.trending);
|
||||
res.json({ success: true, results: data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
export { getTopAnimeRankings };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: topAnimeRankings.controller.js
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKotoAPI — upcomingAnime.controller.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
* @description
|
||||
* Controller for upcoming anime section. Returns list of anime
|
||||
* scheduled for future release from the homepage.
|
||||
*
|
||||
* @exports
|
||||
* getUpcomingAnime - Express route handler for GET /api/upcoming
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import { extractUpcomingAnime } from "../extractors/upcomingAnime.extractor.js";
|
||||
import { getCache, setCache, TTL } from "../helper/cache.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CONTROLLER: UPCOMING ANIME
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Upcoming anime endpoint ----
|
||||
/**
|
||||
* Handles GET /api/upcoming requests. Returns list of anime
|
||||
* scheduled for future release from the homepage section.
|
||||
*
|
||||
* @param {object} req - Express request object
|
||||
* @param {object} res - Express response object
|
||||
* @param {Function} next - Express next middleware function
|
||||
* @returns {void} Sends JSON response with upcoming anime list
|
||||
*
|
||||
* @example
|
||||
* GET /api/upcoming
|
||||
* Response: { success: true, results: [...], ... }
|
||||
*/
|
||||
const getUpcomingAnime = async (req, res, next) => {
|
||||
try {
|
||||
const cacheKey = "upcoming";
|
||||
const cached = getCache(cacheKey);
|
||||
if (cached) {
|
||||
return res.json({ success: true, results: cached });
|
||||
}
|
||||
const data = await extractUpcomingAnime();
|
||||
setCache(cacheKey, data, TTL.default);
|
||||
res.json({ success: true, results: data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
export { getUpcomingAnime };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: upcomingAnime.controller.js
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKotoAPI — completedAnime.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts completed anime from the homepage section.
|
||||
* Scrapes the completed tab/section for finished anime series.
|
||||
*
|
||||
* @exports
|
||||
* extractCompletedAnime
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { fetchWithMirror } from "../helper/mirror.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// COMPLETED ANIME EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract completed anime from homepage section ----
|
||||
/**
|
||||
* Fetches and parses the completed anime section from the homepage.
|
||||
* Returns a list of anime that have finished airing.
|
||||
*
|
||||
* @returns {Promise<Array<Object>>} Array of completed 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
|
||||
*
|
||||
* @example
|
||||
* const completed = await extractCompletedAnime();
|
||||
* console.log(completed.length); // number of completed anime
|
||||
* console.log(completed[0].title); // first completed anime title
|
||||
*/
|
||||
const extractCompletedAnime = async () => {
|
||||
try {
|
||||
const { data } = await fetchWithMirror("/home");
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: completed section contains finished anime series
|
||||
$(".top-table[data-name='completed'] .item, #completed .item, .completed-list .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
if (!slug) return;
|
||||
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
const title = $(el).find(".film-name a, .name.d-title, a.name").text().trim() || "";
|
||||
const japaneseTitle = $(el).find(".name.d-title, a.name").attr("data-jp") || "";
|
||||
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;
|
||||
const type = $(el).find(".meta .inner .right, .type, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
|
||||
results.push({ slug, poster, title, japaneseTitle, sub, dub, total, type });
|
||||
});
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { extractCompletedAnime };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: completedAnime.extractor.js
|
||||
@@ -6,7 +6,7 @@
|
||||
* @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.
|
||||
* source, episode range, and keyword with full pagination support.
|
||||
*
|
||||
* @exports
|
||||
* extractFilter
|
||||
@@ -16,9 +16,8 @@
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import { headers } from "../configs/header.config.js";
|
||||
import { URLS } from "../configs/dataUrl.js";
|
||||
import { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS } from "../configs/ids.config.js";
|
||||
import { GENRE_IDS, TYPE_IDS, STATUS_IDS, RATING_IDS, SORT_IDS, SOURCE_IDS, SEASON_IDS } from "../configs/ids.config.js";
|
||||
import { countPages } from "../helper/countPages.helper.js";
|
||||
import { extractPages } from "../helper/extractPages.helper.js";
|
||||
|
||||
@@ -30,7 +29,7 @@ import { extractPages } from "../helper/extractPages.helper.js";
|
||||
/**
|
||||
* 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.
|
||||
* rating, sort, season, year, source, episode range, and exclude watchlist.
|
||||
*
|
||||
* @param {Object} params - Filter parameters object
|
||||
* @param {string} [params.keyword] - Search keyword to filter by name
|
||||
@@ -39,9 +38,13 @@ import { extractPages } from "../helper/extractPages.helper.js";
|
||||
* @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.sort] - Sort order key (e.g. "score", "name-az")
|
||||
* @param {string} [params.season] - Comma-separated seasons (e.g. "winter,spring")
|
||||
* @param {string} [params.year] - Comma-separated years (e.g. "2024,2025")
|
||||
* @param {string} [params.source] - Comma-separated sources (e.g. "manga,light-novel")
|
||||
* @param {number} [params.epMin] - Minimum episode count filter
|
||||
* @param {number} [params.epMax] - Maximum episode count filter
|
||||
* @param {boolean} [params.excludeWatchlist=false] - Exclude watchlisted anime
|
||||
* @param {number} [params.page=1] - Page number for pagination
|
||||
* @returns {Promise<Object>} Object with totalPages count and data array of results
|
||||
*
|
||||
@@ -49,6 +52,17 @@ import { extractPages } from "../helper/extractPages.helper.js";
|
||||
* const filtered = await extractFilter({ genre: "action", type: "tv", page: 1 });
|
||||
* console.log(filtered.totalPages);
|
||||
* console.log(filtered.data[0].title);
|
||||
*
|
||||
* @example
|
||||
* // Advanced filter with episode range
|
||||
* const filtered = await extractFilter({
|
||||
* genre: "comedy,slice-of-life",
|
||||
* type: "tv",
|
||||
* year: "2024",
|
||||
* epMin: 12,
|
||||
* epMax: 24,
|
||||
* sort: "score"
|
||||
* });
|
||||
*/
|
||||
const extractFilter = async (params) => {
|
||||
try {
|
||||
@@ -102,8 +116,8 @@ const extractFilter = async (params) => {
|
||||
|
||||
// ---- FEATURE: Append season filter ----
|
||||
if (params.season) {
|
||||
const seasons = params.season.split(",");
|
||||
seasons.forEach(s => queryParams.append("season[]", s.trim()));
|
||||
const seasons = params.season.split(",").map(s => SEASON_IDS[s.trim().toLowerCase()] || s.trim());
|
||||
seasons.forEach(s => queryParams.append("season[]", s));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Append year filter ----
|
||||
@@ -112,6 +126,28 @@ const extractFilter = async (params) => {
|
||||
years.forEach(y => queryParams.append("year[]", y.trim()));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Append source filter ----
|
||||
// NOTE: SOURCE_IDS maps source material types to numeric IDs
|
||||
if (params.source) {
|
||||
const sources = params.source.split(",").map(s => SOURCE_IDS[s.trim().toLowerCase()] || s.trim());
|
||||
sources.forEach(s => queryParams.append("source[]", s));
|
||||
}
|
||||
|
||||
// ---- FEATURE: Append episode range filter ----
|
||||
// NOTE: epMin and epMax filter by total episode count range
|
||||
if (params.epMin) {
|
||||
queryParams.set("ep_min", params.epMin);
|
||||
}
|
||||
if (params.epMax) {
|
||||
queryParams.set("ep_max", params.epMax);
|
||||
}
|
||||
|
||||
// ---- FEATURE: Append exclude watchlist filter ----
|
||||
// NOTE: Only works for logged-in users — ignores for guests
|
||||
if (params.excludeWatchlist) {
|
||||
queryParams.set("exclude_watchlist", "1");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKotoAPI — recentlyUpdatedTabs.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts recently updated anime from homepage with tab filtering.
|
||||
* Supports three tabs: all, dub, and sub for different update types.
|
||||
*
|
||||
* @exports
|
||||
* extractRecentlyUpdatedTabs
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { fetchWithMirror } from "../helper/mirror.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// RECENTLY UPDATED TABS EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract recently updated anime with tab filtering ----
|
||||
/**
|
||||
* Fetches and parses the recently updated anime section from the homepage.
|
||||
* Supports filtering by update type: all, dub, or sub.
|
||||
*
|
||||
* @param {string} tab - Tab filter: "all", "dub", or "sub" (default: "all")
|
||||
* @returns {Promise<Array<Object>>} Array of recently updated 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
|
||||
* @returns {string} return[].episodeInfo - Latest episode info
|
||||
*
|
||||
* @example
|
||||
* const all = await extractRecentlyUpdatedTabs("all");
|
||||
* console.log(all.length); // number of updated anime
|
||||
*
|
||||
* const dubOnly = await extractRecentlyUpdatedTabs("dub");
|
||||
* console.log(dubOnly[0].title); // first dubbed updated anime
|
||||
*/
|
||||
const extractRecentlyUpdatedTabs = async (tab = "all") => {
|
||||
try {
|
||||
const { data } = await fetchWithMirror("/home");
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: Homepage has tabbed sections: updated-all, updated-dub, updated-sub
|
||||
// Each section has data-name attribute matching the tab name
|
||||
const selector = tab === "dub"
|
||||
? ".top-table[data-name='updated-dub'] .item, #updated-dub .item"
|
||||
: tab === "sub"
|
||||
? ".top-table[data-name='updated-sub'] .item, #updated-sub .item"
|
||||
: ".top-table[data-name='updated-all'] .item, #recent-update .item, .section-updated .item";
|
||||
|
||||
$(selector).each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
if (!slug) return;
|
||||
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
const title = $(el).find(".film-name a, .name.d-title, a.name").text().trim() || "";
|
||||
const japaneseTitle = $(el).find(".name.d-title, a.name").attr("data-jp") || "";
|
||||
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;
|
||||
const type = $(el).find(".meta .inner .right, .type, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
const episodeInfo = $(el).find(".episode-info, .ep-info").text().trim() || "";
|
||||
|
||||
results.push({ slug, poster, title, japaneseTitle, sub, dub, total, type, episodeInfo });
|
||||
});
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { extractRecentlyUpdatedTabs };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: recentlyUpdatedTabs.extractor.js
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKotoAPI — topAnimeRankings.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts top anime rankings from the homepage with sort options.
|
||||
* Supports both "top" (all-time) and "newest" ranking modes.
|
||||
* Parses ranked items with position numbers 1-9.
|
||||
*
|
||||
* @exports
|
||||
* extractTopAnimeRankings
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { fetchWithMirror } from "../helper/mirror.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// TOP ANIME RANKINGS EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract top anime rankings with sort option ----
|
||||
/**
|
||||
* Fetches and parses the top anime rankings from the homepage.
|
||||
* Supports switching between "top" (all-time) and "newest" rankings.
|
||||
*
|
||||
* @param {string} sort - Ranking sort mode: "top" or "newest" (default: "top")
|
||||
* @returns {Promise<Array<Object>>} Array of ranked anime objects
|
||||
* @returns {number} return[].rank - Ranking position (1-9)
|
||||
* @returns {string} return[].slug - URL slug for the anime
|
||||
* @returns {string} return[].poster - Poster image URL
|
||||
* @returns {string} return[].title - Anime title
|
||||
* @returns {string} return[].japaneseTitle - Japanese title
|
||||
* @returns {number} return[].sub - Subbed episode count
|
||||
* @returns {number} return[].dub - Dubbed episode count
|
||||
* @returns {string} return[].type - Anime type
|
||||
* @returns {string} return[].views - View count
|
||||
*
|
||||
* @example
|
||||
* const topAnime = await extractTopAnimeRankings("top");
|
||||
* console.log(topAnime[0].rank); // 1
|
||||
* console.log(topAnime[0].title); // #1 anime
|
||||
*
|
||||
* const newest = await extractTopAnimeRankings("newest");
|
||||
* console.log(newest[0].title); // #1 newest anime
|
||||
*/
|
||||
const extractTopAnimeRankings = async (sort = "top") => {
|
||||
try {
|
||||
// NOTE: Homepage contains both top and newest rankings in tabbed sections
|
||||
const { data } = await fetchWithMirror("/home");
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: Ranking items have rank1 through rank9 classes for position
|
||||
// The data-sort attribute on tabs controls which ranking is displayed
|
||||
// We parse all ranked items and return them with position
|
||||
$("#top-anime .item, .top-anime .item, .top-table[data-name='top'] .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
if (!slug) return;
|
||||
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
const title = $(el).find(".name, .film-name a").text().trim() || "";
|
||||
const japaneseTitle = $(el).find(".name").attr("data-jp") || "";
|
||||
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 type = $(el).find(".type").text().trim() || "";
|
||||
const views = $(el).find(".views, .view-count").text().trim() || "";
|
||||
|
||||
// NOTE: Rank is determined by position in the list (1-indexed)
|
||||
const rank = i + 1;
|
||||
|
||||
results.push({ rank, slug, poster, title, japaneseTitle, sub, dub, type, views });
|
||||
});
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { extractTopAnimeRankings };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: topAnimeRankings.extractor.js
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
* AniKotoAPI — upcomingAnime.extractor.js
|
||||
* Repository: https://github.com/Shineii86/AniKotoAPI
|
||||
*
|
||||
* @description
|
||||
* Extracts upcoming anime from the homepage section.
|
||||
* Scrapes the #upcoming-anime section for future releases.
|
||||
*
|
||||
* @exports
|
||||
* extractUpcomingAnime
|
||||
*
|
||||
* @author Shinei Nouzen
|
||||
* @license MIT
|
||||
* ======= • ======= • ======= • ======= • =======• =======
|
||||
*/
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import { fetchWithMirror } from "../helper/mirror.helper.js";
|
||||
import { parseListItems } from "../helper/parseListItem.helper.js";
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// UPCOMING ANIME EXTRACTION
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Extract upcoming anime from homepage section ----
|
||||
/**
|
||||
* Fetches and parses the upcoming anime section from the homepage.
|
||||
* Returns a list of anime that are scheduled for future release.
|
||||
*
|
||||
* @returns {Promise<Array<Object>>} Array of upcoming 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[].type - Anime type (TV, Movie, etc.)
|
||||
* @returns {string} return[].releaseDate - Expected release date
|
||||
*
|
||||
* @example
|
||||
* const upcoming = await extractUpcomingAnime();
|
||||
* console.log(upcoming.length); // number of upcoming anime
|
||||
* console.log(upcoming[0].title); // first upcoming anime title
|
||||
*/
|
||||
const extractUpcomingAnime = async () => {
|
||||
try {
|
||||
const { data } = await fetchWithMirror("/home");
|
||||
const $ = cheerio.load(data);
|
||||
|
||||
const results = [];
|
||||
|
||||
// NOTE: upcoming-anime section contains future releases
|
||||
$("#upcoming-anime .item, .upcoming .item").each((i, el) => {
|
||||
const slug = $(el).find("a").attr("href")?.split("/watch/").pop() || "";
|
||||
if (!slug) return;
|
||||
|
||||
const poster = $(el).find("img").attr("src") || "";
|
||||
const title = $(el).find(".film-name a, .name.d-title, a.name").text().trim() || "";
|
||||
const japaneseTitle = $(el).find(".name.d-title, a.name").attr("data-jp") || "";
|
||||
const type = $(el).find(".type, .fdi-item:nth-child(2)").text().trim() || "";
|
||||
const releaseDate = $(el).find(".date, .release-date").text().trim() || "";
|
||||
|
||||
results.push({ slug, poster, title, japaneseTitle, type, releaseDate });
|
||||
});
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { extractUpcomingAnime };
|
||||
|
||||
// ══════════════════════════════════════════════════════════════ END: upcomingAnime.extractor.js
|
||||
+61
-1
@@ -42,6 +42,10 @@ import { getTrendingSidebar } from "../controllers/trendingSidebar.controller.js
|
||||
import { getSeasons } from "../controllers/seasons.controller.js";
|
||||
import { getWatchOrder } from "../controllers/watchOrder.controller.js";
|
||||
import { getDownloadLinks } from "../controllers/download.controller.js";
|
||||
import { getUpcomingAnime } from "../controllers/upcomingAnime.controller.js";
|
||||
import { getTopAnimeRankings } from "../controllers/topAnimeRankings.controller.js";
|
||||
import { getRecentlyUpdatedTabs } from "../controllers/recentlyUpdatedTabs.controller.js";
|
||||
import { getCompletedAnime } from "../controllers/completedAnime.controller.js";
|
||||
import { categoryRoutes } from "./category.route.js";
|
||||
import { getMirrorStatus, resetMirrorCache } from "../helper/mirror.helper.js";
|
||||
|
||||
@@ -328,6 +332,58 @@ app.use((req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// UPCOMING ANIME
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Upcoming anime releases ----
|
||||
app.get("/api/upcoming", async (req, res, next) => {
|
||||
try {
|
||||
await getUpcomingAnime(req, res, next);
|
||||
} catch (error) {
|
||||
jsonError(res, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// TOP ANIME RANKINGS
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Top anime rankings with sort (top/newest) ----
|
||||
app.get("/api/top-rankings", async (req, res, next) => {
|
||||
try {
|
||||
await getTopAnimeRankings(req, res, next);
|
||||
} catch (error) {
|
||||
jsonError(res, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// RECENTLY UPDATED (TAB FILTERED)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Recently updated anime with tab filter (all/dub/sub) ----
|
||||
app.get("/api/recently-updated", async (req, res, next) => {
|
||||
try {
|
||||
await getRecentlyUpdatedTabs(req, res, next);
|
||||
} catch (error) {
|
||||
jsonError(res, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// COMPLETED ANIME
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// ---- FEATURE: Completed anime series ----
|
||||
app.get("/api/completed", async (req, res, next) => {
|
||||
try {
|
||||
await getCompletedAnime(req, res, next);
|
||||
} catch (error) {
|
||||
jsonError(res, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// SEASONS & WATCH ORDER
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
@@ -418,7 +474,7 @@ app.get("/api/stats", (req, res) => {
|
||||
ttl: "5 minutes",
|
||||
description: "Map-based cache with TTL expiration",
|
||||
},
|
||||
endpoints: 30,
|
||||
endpoints: 34,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
@@ -491,6 +547,10 @@ app.get("/api/openapi", (req, res) => {
|
||||
"/watch-order/{id}": { get: { summary: "Watch order", tags: ["Anime"] } },
|
||||
"/az-list/{letter}": { get: { summary: "A-Z listing", tags: ["Discovery"] } },
|
||||
"/suggestions": { get: { summary: "Anime suggestions", tags: ["Search"] } },
|
||||
"/upcoming": { get: { summary: "Upcoming anime", tags: ["Discovery"] } },
|
||||
"/top-rankings": { get: { summary: "Top anime rankings (top/newest)", tags: ["Rankings"] } },
|
||||
"/recently-updated": { get: { summary: "Recently updated (all/dub/sub tabs)", tags: ["Releases"] } },
|
||||
"/completed": { get: { summary: "Completed anime series", tags: ["Rankings"] } },
|
||||
},
|
||||
tags: [
|
||||
{ name: "Home", description: "Homepage data" },
|
||||
|
||||
Reference in New Issue
Block a user