diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/controllers/animeSearch.controller.ts | 43 | ||||
| -rw-r--r-- | src/parsers/animeSearch.ts | 52 | ||||
| -rw-r--r-- | src/types/controllers/animeSearch.ts | 16 | ||||
| -rw-r--r-- | src/types/controllers/index.ts | 45 | ||||
| -rw-r--r-- | src/types/parsers/animeSearch.ts | 3 | ||||
| -rw-r--r-- | src/utils/constants.ts | 115 | ||||
| -rw-r--r-- | src/utils/index.ts | 35 | ||||
| -rw-r--r-- | src/utils/methods.ts | 85 |
8 files changed, 303 insertions, 91 deletions
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); |
