aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md41
-rw-r--r--src/controllers/animeSearch.controller.ts43
-rw-r--r--src/parsers/animeSearch.ts52
-rw-r--r--src/types/controllers/animeSearch.ts16
-rw-r--r--src/types/controllers/index.ts45
-rw-r--r--src/types/parsers/animeSearch.ts3
-rw-r--r--src/utils/constants.ts115
-rw-r--r--src/utils/index.ts35
-rw-r--r--src/utils/methods.ts85
9 files changed, 339 insertions, 96 deletions
diff --git a/README.md b/README.md
index 4ea343b..d80b96f 100644
--- a/README.md
+++ b/README.md
@@ -401,24 +401,50 @@ console.log(data);
#### Endpoint
```sh
+# basic example
https://api-aniwatch.onrender.com/anime/search?q={query}&page={page}
+
+# advanced example
+https://api-aniwatch.onrender.com/anime/search?q={query}&genres={genres}&type={type}&sort={sort}&season={season}&language={sub_or_dub}&status={status}&rated={rating}&start_date={yyyy-mm-dd}&end_date={yyyy-mm-dd}&score={score}
```
#### Query Parameters
-| Parameter | Type | Description | Required? | Default |
-| :-------: | :----: | :---------------------------------------------------------------: | :-------: | :-----: |
-| `q` | string | The search query, i.e. the title of the item you are looking for. | Yes | -- |
-| `page` | number | The page number of the result. | No | `1` |
+| Parameter | Type | Description | Required? | Default |
+| :----------: | :----: | :---------------------------------------------------------------: | :-------: | :-----: |
+| `q` | string | The search query, i.e. the title of the item you are looking for. | Yes | -- |
+| `page` | number | The page number of the result. | No | `1` |
+| `type` | string | Type of the anime. eg: `movie` | No | -- |
+| `status` | string | Status of the anime. eg: `finished-airing` | No | -- |
+| `rated` | string | Rating of the anime. eg: `r+` or `pg-13` | No | -- |
+| `score` | string | Score of the anime. eg: `good` or `very-good` | No | -- |
+| `season` | string | Season of the aired anime. eg: `spring` | No | -- |
+| `language` | string | Language category of the anime. eg: `sub` or `sub-&-dub` | No | -- |
+| `start_date` | string | Start date of the anime(yyyy-mm-dd). eg: `2014-10-2` | No | -- |
+| `end_date` | string | End date of the anime(yyyy-mm-dd). eg: `2010-12-4` | No | -- |
+| `sort` | string | Order of sorting the anime result. eg: `recently-added` | No | -- |
+| `genres` | string | Genre of the anime, separated by commas. eg: `isekai,shounen` | No | -- |
+
+> [!TIP]
+> For both `start_date` and `end_date`, year must be mentioned. If you wanna omit date or month specify `0` instead.
+> Eg: omitting date -> 2014-10-0, omitting month -> 2014-0-12, omitting both -> 2014-0-0
#### Request sample
```javascript
+// basic example
const resp = await fetch(
"https://api-aniwatch.onrender.com/anime/search?q=titan&page=1"
);
const data = await resp.json();
console.log(data);
+
+// advanced example
+const resp = await fetch(
+ "https://api-aniwatch.onrender.com/anime/search?q=girls&genres=action,adventure&type=movie&sort=score&season=spring&language=dub&status=finished-airing&rated=pg-13&start_date=2014-0-0&score=good"
+);
+const data = await resp.json();
+console.log(data);
```
#### Response Schema
@@ -456,7 +482,12 @@ console.log(data);
],
currentPage: 1,
totalPages: 1,
- hasNextPage: false
+ hasNextPage: false,
+ searchQuery: string,
+ searchFilters: {
+ [filter_name]: [filter_value]
+ ...
+ }
}
```
diff --git a/src/controllers/animeSearch.controller.ts b/src/controllers/animeSearch.controller.ts
index c7ec57a..8937b0a 100644
--- a/src/controllers/animeSearch.controller.ts
+++ b/src/controllers/animeSearch.controller.ts
@@ -1,7 +1,24 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeAnimeSearch } from "../parsers/index.js";
-import { type AnimeSearchQueryParams } from "../types/controllers/index.js";
+import type {
+ SearchFilters,
+ AnimeSearchQueryParams,
+} from "../types/controllers/index.js";
+
+const searchFilters: Record<string, boolean> = {
+ filter: true,
+ type: true,
+ status: true,
+ rated: true,
+ score: true,
+ season: true,
+ language: true,
+ start_date: true,
+ end_date: true,
+ sort: true,
+ genres: true,
+} as const;
// /anime/search?q=${query}&page=${page}
const getAnimeSearch: RequestHandler<
@@ -11,18 +28,24 @@ const getAnimeSearch: RequestHandler<
AnimeSearchQueryParams
> = async (req, res, next) => {
try {
- const query: string | null = req.query.q
- ? decodeURIComponent(req.query.q as string)
- : null;
- const page: number = req.query.page
- ? Number(decodeURIComponent(req.query?.page as string))
- : 1;
-
- if (query === null) {
+ let { q: query, page, ...filters } = req.query;
+
+ query = query ? decodeURIComponent(query) : undefined;
+ const pageNo = page ? Number(decodeURIComponent(page as string)) : 1;
+
+ if (query === undefined) {
throw createHttpError.BadRequest("Search keyword required");
}
- const data = await scrapeAnimeSearch(query, page);
+ const parsedFilters: SearchFilters = {};
+ for (const key in filters) {
+ if (searchFilters[key]) {
+ parsedFilters[key as keyof SearchFilters] =
+ filters[key as keyof SearchFilters];
+ }
+ }
+
+ const data = await scrapeAnimeSearch(query, pageNo, parsedFilters);
res.status(200).json(data);
} catch (err: any) {
diff --git a/src/parsers/animeSearch.ts b/src/parsers/animeSearch.ts
index 43a9fdd..16818c6 100644
--- a/src/parsers/animeSearch.ts
+++ b/src/parsers/animeSearch.ts
@@ -3,18 +3,22 @@ import {
ACCEPT_HEADER,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
- extractMostPopularAnimes,
extractAnimes,
+ getSearchFilterValue,
+ extractMostPopularAnimes,
+ getSearchDateFilterValue,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import type { ScrapedAnimeSearchResult } from "../types/parsers/index.js";
+import type { SearchFilters, FilterKeys } from "../types/controllers/index.js";
// /anime/search?q=${query}&page=${page}
async function scrapeAnimeSearch(
q: string,
- page: number = 1
+ page: number = 1,
+ filters: SearchFilters
): Promise<ScrapedAnimeSearchResult | HttpError> {
const res: ScrapedAnimeSearchResult = {
animes: [],
@@ -22,19 +26,45 @@ async function scrapeAnimeSearch(
currentPage: Number(page),
hasNextPage: false,
totalPages: 1,
+ searchQuery: q,
+ searchFilters: filters,
};
try {
- const mainPage = await axios.get(
- `${SRC_SEARCH_URL}?keyword=${q}&page=${page}`,
- {
- headers: {
- "User-Agent": USER_AGENT_HEADER,
- "Accept-Encoding": ACCEPT_ENCODING_HEADER,
- Accept: ACCEPT_HEADER,
- },
+ const url = new URL(SRC_SEARCH_URL);
+ url.searchParams.set("keyword", q);
+ url.searchParams.set("page", `${page}`);
+ url.searchParams.set("sort", "default");
+
+ for (const key in filters) {
+ if (key.includes("_date")) {
+ const dates = getSearchDateFilterValue(
+ key === "start_date",
+ filters[key as keyof SearchFilters] || ""
+ );
+ if (!dates) continue;
+
+ dates.map((dateParam) => {
+ const [key, val] = dateParam.split("=");
+ url.searchParams.set(key, val);
+ });
+ continue;
}
- );
+
+ const filterVal = getSearchFilterValue(
+ key as FilterKeys,
+ filters[key as keyof SearchFilters] || ""
+ );
+ filterVal && url.searchParams.set(key, filterVal);
+ }
+
+ const mainPage = await axios.get(url.href, {
+ headers: {
+ "User-Agent": USER_AGENT_HEADER,
+ "Accept-Encoding": ACCEPT_ENCODING_HEADER,
+ Accept: ACCEPT_HEADER,
+ },
+ });
const $: CheerioAPI = load(mainPage.data);
diff --git a/src/types/controllers/animeSearch.ts b/src/types/controllers/animeSearch.ts
index 20f4680..fccb243 100644
--- a/src/types/controllers/animeSearch.ts
+++ b/src/types/controllers/animeSearch.ts
@@ -1,4 +1,20 @@
export type AnimeSearchQueryParams = {
q?: string;
page?: string;
+ type?: string;
+ status?: string;
+ rated?: string;
+ score?: string;
+ season?: string;
+ language?: string;
+ start_date?: string;
+ end_date?: string;
+ sort?: string;
+ genres?: string;
};
+
+export type SearchFilters = Omit<AnimeSearchQueryParams, "q" | "page">;
+
+export type FilterKeys = Partial<
+ keyof Omit<SearchFilters, "start_date" | "end_date">
+>;
diff --git a/src/types/controllers/index.ts b/src/types/controllers/index.ts
index fab25b6..14876e2 100644
--- a/src/types/controllers/index.ts
+++ b/src/types/controllers/index.ts
@@ -1,35 +1,10 @@
-import type {
- GenreAnimePathParams,
- GenreAnimeQueryParams,
-} from "./animeGenre.js";
-import type {
- CategoryAnimePathParams,
- CategoryAnimeQueryParams,
-} from "./animeCategory.js";
-import type {
- AnimeProducerPathParams,
- AnimeProducerQueryParams,
-} from "./animeProducer.js";
-import type { AnimeSearchQueryParams } from "./animeSearch.js";
-import type { AnimeEpisodePathParams } from "./animeEpisodes.js";
-import type { EpisodeServersQueryParams } from "./episodeServers.js";
-import type { AnimeAboutInfoQueryParams } from "./animeAboutInfo.js";
-import type { AnimeEpisodeSrcsQueryParams } from "./animeEpisodeSrcs.js";
-import type { EstimatedScheduleQueryParams } from "./estimatedSchedule.js";
-import type { AnimeSearchSuggestQueryParams } from "./animeSearchSuggestion.js";
-
-export type {
- GenreAnimePathParams,
- GenreAnimeQueryParams,
- AnimeSearchQueryParams,
- AnimeEpisodePathParams,
- AnimeProducerPathParams,
- CategoryAnimePathParams,
- AnimeProducerQueryParams,
- CategoryAnimeQueryParams,
- AnimeAboutInfoQueryParams,
- EpisodeServersQueryParams,
- AnimeEpisodeSrcsQueryParams,
- EstimatedScheduleQueryParams,
- AnimeSearchSuggestQueryParams,
-};
+export type * from "./animeGenre.js";
+export type * from "./animeCategory.js";
+export type * from "./animeProducer.js";
+export type * from "./animeSearch.js";
+export type * from "./animeEpisodes.js";
+export type * from "./episodeServers.js";
+export type * from "./animeAboutInfo.js";
+export type * from "./animeEpisodeSrcs.js";
+export type * from "./estimatedSchedule.js";
+export type * from "./animeSearchSuggestion.js";
diff --git a/src/types/parsers/animeSearch.ts b/src/types/parsers/animeSearch.ts
index fdcca4f..7e641ea 100644
--- a/src/types/parsers/animeSearch.ts
+++ b/src/types/parsers/animeSearch.ts
@@ -4,8 +4,11 @@ import type {
} from "./animeCategory.js";
import type { HttpError } from "http-errors";
import type { MostPopularAnime } from "../anime.js";
+import type { SearchFilters } from "../controllers/animeSearch.js";
export interface ScrapedAnimeSearchResult
extends Pick<ScrapedAnimeCategory, CommonAnimeScrapeTypes> {
mostPopularAnimes: Array<MostPopularAnime> | HttpError;
+ searchQuery: string;
+ searchFilters: SearchFilters;
}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 21a6db8..8615050 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1,11 +1,10 @@
import { config } from "dotenv";
+
config();
export const ACCEPT_ENCODING_HEADER = "gzip, deflate, br";
-
export const USER_AGENT_HEADER =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4692.71 Safari/537.36";
-
export const ACCEPT_HEADER =
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9";
@@ -16,3 +15,115 @@ export const SRC_BASE_URL = `https://${DOMAIN}`;
export const SRC_AJAX_URL = `${SRC_BASE_URL}/ajax`;
export const SRC_HOME_URL = `${SRC_BASE_URL}/home`;
export const SRC_SEARCH_URL = `${SRC_BASE_URL}/search`;
+
+// <SearchPageFilters>
+export const genresIdMap: Record<string, number> = {
+ action: 1,
+ adventure: 2,
+ cars: 3,
+ comedy: 4,
+ dementia: 5,
+ demons: 6,
+ drama: 8,
+ ecchi: 9,
+ fantasy: 10,
+ game: 11,
+ harem: 35,
+ historical: 13,
+ horror: 14,
+ isekai: 44,
+ josei: 43,
+ kids: 15,
+ magic: 16,
+ "martial-arts": 17,
+ mecha: 18,
+ military: 38,
+ music: 19,
+ mystery: 7,
+ parody: 20,
+ police: 39,
+ psychological: 40,
+ romance: 22,
+ samurai: 21,
+ school: 23,
+ "sci-fi": 24,
+ seinen: 42,
+ shoujo: 25,
+ "shoujo-ai": 26,
+ shounen: 27,
+ "shounen-ai": 28,
+ "slice-of-life": 36,
+ space: 29,
+ sports: 30,
+ "super-power": 31,
+ supernatural: 37,
+ thriller: 41,
+ vampire: 32,
+} as const;
+
+export const typeIdMap: Record<string, number> = {
+ all: 0,
+ movie: 1,
+ tv: 2,
+ ova: 3,
+ ona: 4,
+ special: 5,
+ music: 6,
+} as const;
+
+export const statusIdMap: Record<string, number> = {
+ all: 0,
+ "finished-airing": 1,
+ "currently-airing": 2,
+ "not-yet-aired": 3,
+} as const;
+
+export const ratedIdMap: Record<string, number> = {
+ all: 0,
+ g: 1,
+ pg: 2,
+ "pg-13": 3,
+ r: 4,
+ "r+": 5,
+ rx: 6,
+} as const;
+
+export const scoreIdMap: Record<string, number> = {
+ all: 0,
+ appalling: 1,
+ horrible: 2,
+ "very-bad": 3,
+ bad: 4,
+ average: 5,
+ fine: 6,
+ good: 7,
+ "very-good": 8,
+ great: 9,
+ masterpiece: 10,
+} as const;
+
+export const seasonIdMap: Record<string, number> = {
+ all: 0,
+ spring: 1,
+ summer: 2,
+ fall: 3,
+ winter: 4,
+} as const;
+
+export const languageIdMap: Record<string, number> = {
+ all: 0,
+ sub: 1,
+ dub: 2,
+ "sub-&-dub": 3,
+} as const;
+
+export const sortIdMap: Record<string, string> = {
+ default: "default",
+ "recently-added": "recently_added",
+ "recently-updated": "recently_updated",
+ score: "score",
+ "name-a-z": "name_az",
+ "released-date": "released_date",
+ "most-watched": "most_watched",
+} as const;
+// </SearchPageFilters>
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 42492ef..b514c85 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,33 +1,2 @@
-import {
- SRC_AJAX_URL,
- SRC_HOME_URL,
- SRC_BASE_URL,
- ACCEPT_HEADER,
- SRC_SEARCH_URL,
- USER_AGENT_HEADER,
- ACCEPT_ENCODING_HEADER,
-} from "./constants.js";
-import {
- extractAnimes,
- substringAfter,
- substringBefore,
- retrieveServerId,
- extractTop10Animes,
- extractMostPopularAnimes,
-} from "./methods.js";
-
-export {
- SRC_AJAX_URL,
- SRC_HOME_URL,
- SRC_BASE_URL,
- ACCEPT_HEADER,
- SRC_SEARCH_URL,
- USER_AGENT_HEADER,
- ACCEPT_ENCODING_HEADER,
- extractAnimes,
- substringAfter,
- substringBefore,
- retrieveServerId,
- extractTop10Animes,
- extractMostPopularAnimes,
-};
+export * from "./methods.js";
+export * from "./constants.js";
diff --git a/src/utils/methods.ts b/src/utils/methods.ts
index 2e062d7..9d89822 100644
--- a/src/utils/methods.ts
+++ b/src/utils/methods.ts
@@ -5,6 +5,17 @@ import type {
Top10AnimeTimePeriod,
} from "../types/anime.js";
import type { CheerioAPI, SelectorType } from "cheerio";
+import {
+ genresIdMap,
+ languageIdMap,
+ ratedIdMap,
+ scoreIdMap,
+ seasonIdMap,
+ sortIdMap,
+ statusIdMap,
+ typeIdMap,
+} from "./constants.js";
+import { type FilterKeys } from "../types/controllers/animeSearch.js";
import createHttpError, { HttpError } from "http-errors";
export const extractAnimes = (
@@ -192,6 +203,80 @@ export function retrieveServerId(
);
}
+function getGenresFilterVal(genreNames: string[]): string | undefined {
+ if (genreNames.length < 1) {
+ return undefined;
+ }
+ return genreNames.map((name) => genresIdMap[name]).join(",");
+}
+
+export function getSearchFilterValue(
+ key: FilterKeys,
+ rawValue: string
+): string | undefined {
+ rawValue = rawValue.trim();
+ if (!rawValue) return undefined;
+
+ switch (key) {
+ case "genres": {
+ return getGenresFilterVal(rawValue.split(","));
+ }
+ case "type": {
+ const val = typeIdMap[rawValue] ?? 0;
+ return val === 0 ? undefined : `${val}`;
+ }
+ case "status": {
+ const val = statusIdMap[rawValue] ?? 0;
+ return val === 0 ? undefined : `${val}`;
+ }
+ case "rated": {
+ const val = ratedIdMap[rawValue] ?? 0;
+ return val === 0 ? undefined : `${val}`;
+ }
+ case "score": {
+ const val = scoreIdMap[rawValue] ?? 0;
+ return val === 0 ? undefined : `${val}`;
+ }
+ case "season": {
+ const val = seasonIdMap[rawValue] ?? 0;
+ return val === 0 ? undefined : `${val}`;
+ }
+ case "language": {
+ const val = languageIdMap[rawValue] ?? 0;
+ return val === 0 ? undefined : `${val}`;
+ }
+ case "sort": {
+ return sortIdMap[rawValue] ?? undefined;
+ }
+ default:
+ return undefined;
+ }
+}
+
+// this fn tackles both start_date and end_date
+export function getSearchDateFilterValue(
+ isStartDate: boolean,
+ rawValue: string
+): string[] | undefined {
+ rawValue = rawValue.trim();
+ if (!rawValue) return undefined;
+
+ const dateRegex = /^\d{4}-([0-9]|1[0-2])-([0-9]|[12][0-9]|3[01])$/;
+ const dateCategory = isStartDate ? "s" : "e";
+ const [year, month, date] = rawValue.split("-");
+
+ if (!dateRegex.test(rawValue)) {
+ return undefined;
+ }
+
+ // sample return -> [sy=2023, sm=10, sd=11]
+ return [
+ Number(year) > 0 ? `${dateCategory}y=${year}` : "",
+ Number(month) > 0 ? `${dateCategory}m=${month}` : "",
+ Number(date) > 0 ? `${dateCategory}d=${date}` : "",
+ ].filter((d) => Boolean(d));
+}
+
export function substringAfter(str: string, toFind: string) {
const index = str.indexOf(toFind);
return index == -1 ? "" : str.substring(index + toFind.length);