Merge pull request #34 from ghoshRitesh12/advanced-search
Add advanced search feature
This commit is contained in:
@@ -401,24 +401,50 @@ console.log(data);
|
|||||||
#### Endpoint
|
#### Endpoint
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
# basic example
|
||||||
https://api-aniwatch.onrender.com/anime/search?q={query}&page={page}
|
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
|
#### Query Parameters
|
||||||
|
|
||||||
| Parameter | Type | Description | Required? | Default |
|
| Parameter | Type | Description | Required? | Default |
|
||||||
| :-------: | :----: | :---------------------------------------------------------------: | :-------: | :-----: |
|
| :----------: | :----: | :---------------------------------------------------------------: | :-------: | :-----: |
|
||||||
| `q` | string | The search query, i.e. the title of the item you are looking for. | Yes | -- |
|
| `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` |
|
| `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
|
#### Request sample
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
// basic example
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
"https://api-aniwatch.onrender.com/anime/search?q=titan&page=1"
|
"https://api-aniwatch.onrender.com/anime/search?q=titan&page=1"
|
||||||
);
|
);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
console.log(data);
|
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
|
#### Response Schema
|
||||||
@@ -456,7 +482,12 @@ console.log(data);
|
|||||||
],
|
],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
hasNextPage: false
|
hasNextPage: false,
|
||||||
|
searchQuery: string,
|
||||||
|
searchFilters: {
|
||||||
|
[filter_name]: [filter_value]
|
||||||
|
...
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { type RequestHandler } from "express";
|
import { type RequestHandler } from "express";
|
||||||
import { scrapeAnimeSearch } from "../parsers/index.js";
|
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}
|
// /anime/search?q=${query}&page=${page}
|
||||||
const getAnimeSearch: RequestHandler<
|
const getAnimeSearch: RequestHandler<
|
||||||
@@ -11,18 +28,24 @@ const getAnimeSearch: RequestHandler<
|
|||||||
AnimeSearchQueryParams
|
AnimeSearchQueryParams
|
||||||
> = async (req, res, next) => {
|
> = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const query: string | null = req.query.q
|
let { q: query, page, ...filters } = req.query;
|
||||||
? decodeURIComponent(req.query.q as string)
|
|
||||||
: null;
|
|
||||||
const page: number = req.query.page
|
|
||||||
? Number(decodeURIComponent(req.query?.page as string))
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
if (query === null) {
|
query = query ? decodeURIComponent(query) : undefined;
|
||||||
|
const pageNo = page ? Number(decodeURIComponent(page as string)) : 1;
|
||||||
|
|
||||||
|
if (query === undefined) {
|
||||||
throw createHttpError.BadRequest("Search keyword required");
|
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);
|
res.status(200).json(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
+41
-11
@@ -3,18 +3,22 @@ import {
|
|||||||
ACCEPT_HEADER,
|
ACCEPT_HEADER,
|
||||||
USER_AGENT_HEADER,
|
USER_AGENT_HEADER,
|
||||||
ACCEPT_ENCODING_HEADER,
|
ACCEPT_ENCODING_HEADER,
|
||||||
extractMostPopularAnimes,
|
|
||||||
extractAnimes,
|
extractAnimes,
|
||||||
|
getSearchFilterValue,
|
||||||
|
extractMostPopularAnimes,
|
||||||
|
getSearchDateFilterValue,
|
||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import createHttpError, { type HttpError } from "http-errors";
|
import createHttpError, { type HttpError } from "http-errors";
|
||||||
import { load, type CheerioAPI, type SelectorType } from "cheerio";
|
import { load, type CheerioAPI, type SelectorType } from "cheerio";
|
||||||
import type { ScrapedAnimeSearchResult } from "../types/parsers/index.js";
|
import type { ScrapedAnimeSearchResult } from "../types/parsers/index.js";
|
||||||
|
import type { SearchFilters, FilterKeys } from "../types/controllers/index.js";
|
||||||
|
|
||||||
// /anime/search?q=${query}&page=${page}
|
// /anime/search?q=${query}&page=${page}
|
||||||
async function scrapeAnimeSearch(
|
async function scrapeAnimeSearch(
|
||||||
q: string,
|
q: string,
|
||||||
page: number = 1
|
page: number = 1,
|
||||||
|
filters: SearchFilters
|
||||||
): Promise<ScrapedAnimeSearchResult | HttpError> {
|
): Promise<ScrapedAnimeSearchResult | HttpError> {
|
||||||
const res: ScrapedAnimeSearchResult = {
|
const res: ScrapedAnimeSearchResult = {
|
||||||
animes: [],
|
animes: [],
|
||||||
@@ -22,19 +26,45 @@ async function scrapeAnimeSearch(
|
|||||||
currentPage: Number(page),
|
currentPage: Number(page),
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
|
searchQuery: q,
|
||||||
|
searchFilters: filters,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mainPage = await axios.get(
|
const url = new URL(SRC_SEARCH_URL);
|
||||||
`${SRC_SEARCH_URL}?keyword=${q}&page=${page}`,
|
url.searchParams.set("keyword", q);
|
||||||
{
|
url.searchParams.set("page", `${page}`);
|
||||||
headers: {
|
url.searchParams.set("sort", "default");
|
||||||
"User-Agent": USER_AGENT_HEADER,
|
|
||||||
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
|
for (const key in filters) {
|
||||||
Accept: ACCEPT_HEADER,
|
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);
|
const $: CheerioAPI = load(mainPage.data);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
export type AnimeSearchQueryParams = {
|
export type AnimeSearchQueryParams = {
|
||||||
q?: string;
|
q?: string;
|
||||||
page?: 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">
|
||||||
|
>;
|
||||||
|
|||||||
@@ -1,35 +1,10 @@
|
|||||||
import type {
|
export type * from "./animeGenre.js";
|
||||||
GenreAnimePathParams,
|
export type * from "./animeCategory.js";
|
||||||
GenreAnimeQueryParams,
|
export type * from "./animeProducer.js";
|
||||||
} from "./animeGenre.js";
|
export type * from "./animeSearch.js";
|
||||||
import type {
|
export type * from "./animeEpisodes.js";
|
||||||
CategoryAnimePathParams,
|
export type * from "./episodeServers.js";
|
||||||
CategoryAnimeQueryParams,
|
export type * from "./animeAboutInfo.js";
|
||||||
} from "./animeCategory.js";
|
export type * from "./animeEpisodeSrcs.js";
|
||||||
import type {
|
export type * from "./estimatedSchedule.js";
|
||||||
AnimeProducerPathParams,
|
export type * from "./animeSearchSuggestion.js";
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import type {
|
|||||||
} from "./animeCategory.js";
|
} from "./animeCategory.js";
|
||||||
import type { HttpError } from "http-errors";
|
import type { HttpError } from "http-errors";
|
||||||
import type { MostPopularAnime } from "../anime.js";
|
import type { MostPopularAnime } from "../anime.js";
|
||||||
|
import type { SearchFilters } from "../controllers/animeSearch.js";
|
||||||
|
|
||||||
export interface ScrapedAnimeSearchResult
|
export interface ScrapedAnimeSearchResult
|
||||||
extends Pick<ScrapedAnimeCategory, CommonAnimeScrapeTypes> {
|
extends Pick<ScrapedAnimeCategory, CommonAnimeScrapeTypes> {
|
||||||
mostPopularAnimes: Array<MostPopularAnime> | HttpError;
|
mostPopularAnimes: Array<MostPopularAnime> | HttpError;
|
||||||
|
searchQuery: string;
|
||||||
|
searchFilters: SearchFilters;
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-2
@@ -1,11 +1,10 @@
|
|||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
export const ACCEPT_ENCODING_HEADER = "gzip, deflate, br";
|
export const ACCEPT_ENCODING_HEADER = "gzip, deflate, br";
|
||||||
|
|
||||||
export const USER_AGENT_HEADER =
|
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";
|
"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 =
|
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";
|
"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_AJAX_URL = `${SRC_BASE_URL}/ajax`;
|
||||||
export const SRC_HOME_URL = `${SRC_BASE_URL}/home`;
|
export const SRC_HOME_URL = `${SRC_BASE_URL}/home`;
|
||||||
export const SRC_SEARCH_URL = `${SRC_BASE_URL}/search`;
|
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>
|
||||||
|
|||||||
+2
-33
@@ -1,33 +1,2 @@
|
|||||||
import {
|
export * from "./methods.js";
|
||||||
SRC_AJAX_URL,
|
export * from "./constants.js";
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ import type {
|
|||||||
Top10AnimeTimePeriod,
|
Top10AnimeTimePeriod,
|
||||||
} from "../types/anime.js";
|
} from "../types/anime.js";
|
||||||
import type { CheerioAPI, SelectorType } from "cheerio";
|
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";
|
import createHttpError, { HttpError } from "http-errors";
|
||||||
|
|
||||||
export const extractAnimes = (
|
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) {
|
export function substringAfter(str: string, toFind: string) {
|
||||||
const index = str.indexOf(toFind);
|
const index = str.indexOf(toFind);
|
||||||
return index == -1 ? "" : str.substring(index + toFind.length);
|
return index == -1 ? "" : str.substring(index + toFind.length);
|
||||||
|
|||||||
Reference in New Issue
Block a user