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:
Shinei Nouzen
2026-07-28 19:45:25 +05:30
parent a8f1917ce7
commit fd29b02158
12 changed files with 751 additions and 9 deletions
@@ -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
+43 -7
View File
@@ -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
+73
View File
@@ -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