From 46f688ac12a99b8fb145b0745dd4cc6babff1e1e Mon Sep 17 00:00:00 2001 From: Ritesh Ghosh <101876769+ghoshRitesh12@users.noreply.github.com> Date: Sun, 6 Oct 2024 01:13:23 +0530 Subject: Aniwatch API Version 2 (#66) BREAKING CHANGE: * chore: remove files that are not necessary for api v2 * test: update existing tests to use pkg * feat: organized aniwatch api envs and add more info about them * feat: update tsconfig to include strict noUnsed params * feat(api homepage): revamp api home page * feat: update wani kuni image * feat: add dot img * feat: use hono cors * feat: use hono rate limiter * build: remove unnecessary deps, add ones needed and update description * feat: add hianime routes and their handlers * feat: update vercel deployment file * docs: update logo and scraper docs, add envs section * feat: update main server file * feat: update peronal deployments caution section --- src/config/axios.ts | 21 -- src/config/cors.ts | 17 +- src/config/errorHandler.ts | 11 - src/config/notFoundHandler.ts | 8 - src/config/ratelimit.ts | 24 +- src/controllers/animeAboutInfo.controller.ts | 31 --- src/controllers/animeCategory.controller.ts | 39 --- src/controllers/animeEpisodeSrcs.controller.ts | 75 ------ src/controllers/animeEpisodes.controller.ts | 31 --- src/controllers/animeGenre.controller.ts | 37 --- src/controllers/animeProducer.controller.ts | 37 --- src/controllers/animeSearch.controller.ts | 57 ---- .../animeSearchSuggestion.controller.ts | 31 --- src/controllers/episodeServers.controller.ts | 30 --- src/controllers/estimatedSchedule.controller.ts | 36 --- src/controllers/homePage.controller.ts | 18 -- src/controllers/index.ts | 25 -- src/extractors/index.ts | 6 - src/extractors/megacloud.ts | 226 ---------------- src/extractors/rapidcloud.ts | 166 ------------ src/extractors/streamsb.ts | 83 ------ src/extractors/streamtape.ts | 37 --- src/parsers/animeAboutInfo.ts | 229 ---------------- src/parsers/animeCategory.ts | 118 -------- src/parsers/animeEpisodeSrcs.ts | 129 --------- src/parsers/animeEpisodes.ts | 61 ----- src/parsers/animeGenre.ts | 105 -------- src/parsers/animeProducer.ts | 120 --------- src/parsers/animeSearch.ts | 118 -------- src/parsers/animeSearchSuggestion.ts | 77 ------ src/parsers/episodeServers.ts | 85 ------ src/parsers/estimatedSchedule.ts | 74 ----- src/parsers/homePage.ts | 186 ------------- src/parsers/index.ts | 25 -- src/routes/hianime.ts | 125 +++++++++ src/routes/index.ts | 55 ---- src/server.ts | 80 ++++-- src/types/anime.ts | 140 ---------- src/types/controllers/animeAboutInfo.ts | 3 - src/types/controllers/animeCategory.ts | 7 - src/types/controllers/animeEpisodeSrcs.ts | 7 - src/types/controllers/animeEpisodes.ts | 3 - src/types/controllers/animeGenre.ts | 7 - src/types/controllers/animeProducer.ts | 7 - src/types/controllers/animeSearch.ts | 20 -- src/types/controllers/animeSearchSuggestion.ts | 3 - src/types/controllers/episodeServers.ts | 3 - src/types/controllers/estimatedSchedule.ts | 3 - src/types/controllers/index.ts | 10 - src/types/extractor.ts | 18 -- src/types/parsers/animeAboutInfo.ts | 19 -- src/types/parsers/animeCategory.ts | 22 -- src/types/parsers/animeEpisodeSrcs.ts | 12 - src/types/parsers/animeEpisodes.ts | 6 - src/types/parsers/animeGenre.ts | 11 - src/types/parsers/animeProducer.ts | 8 - src/types/parsers/animeSearch.ts | 14 - src/types/parsers/animeSearchSuggestion.ts | 6 - src/types/parsers/episodeServers.ts | 9 - src/types/parsers/estimatedSchedule.ts | 13 - src/types/parsers/homePage.ts | 24 -- src/types/parsers/index.ts | 25 -- src/utils/constants.ts | 129 --------- src/utils/index.ts | 2 - src/utils/methods.ts | 297 --------------------- 65 files changed, 197 insertions(+), 3264 deletions(-) delete mode 100644 src/config/axios.ts delete mode 100644 src/config/errorHandler.ts delete mode 100644 src/config/notFoundHandler.ts delete mode 100644 src/controllers/animeAboutInfo.controller.ts delete mode 100644 src/controllers/animeCategory.controller.ts delete mode 100644 src/controllers/animeEpisodeSrcs.controller.ts delete mode 100644 src/controllers/animeEpisodes.controller.ts delete mode 100644 src/controllers/animeGenre.controller.ts delete mode 100644 src/controllers/animeProducer.controller.ts delete mode 100644 src/controllers/animeSearch.controller.ts delete mode 100644 src/controllers/animeSearchSuggestion.controller.ts delete mode 100644 src/controllers/episodeServers.controller.ts delete mode 100644 src/controllers/estimatedSchedule.controller.ts delete mode 100644 src/controllers/homePage.controller.ts delete mode 100644 src/controllers/index.ts delete mode 100644 src/extractors/index.ts delete mode 100644 src/extractors/megacloud.ts delete mode 100644 src/extractors/rapidcloud.ts delete mode 100644 src/extractors/streamsb.ts delete mode 100644 src/extractors/streamtape.ts delete mode 100644 src/parsers/animeAboutInfo.ts delete mode 100644 src/parsers/animeCategory.ts delete mode 100644 src/parsers/animeEpisodeSrcs.ts delete mode 100644 src/parsers/animeEpisodes.ts delete mode 100644 src/parsers/animeGenre.ts delete mode 100644 src/parsers/animeProducer.ts delete mode 100644 src/parsers/animeSearch.ts delete mode 100644 src/parsers/animeSearchSuggestion.ts delete mode 100644 src/parsers/episodeServers.ts delete mode 100644 src/parsers/estimatedSchedule.ts delete mode 100644 src/parsers/homePage.ts delete mode 100644 src/parsers/index.ts create mode 100644 src/routes/hianime.ts delete mode 100644 src/routes/index.ts delete mode 100644 src/types/anime.ts delete mode 100644 src/types/controllers/animeAboutInfo.ts delete mode 100644 src/types/controllers/animeCategory.ts delete mode 100644 src/types/controllers/animeEpisodeSrcs.ts delete mode 100644 src/types/controllers/animeEpisodes.ts delete mode 100644 src/types/controllers/animeGenre.ts delete mode 100644 src/types/controllers/animeProducer.ts delete mode 100644 src/types/controllers/animeSearch.ts delete mode 100644 src/types/controllers/animeSearchSuggestion.ts delete mode 100644 src/types/controllers/episodeServers.ts delete mode 100644 src/types/controllers/estimatedSchedule.ts delete mode 100644 src/types/controllers/index.ts delete mode 100644 src/types/extractor.ts delete mode 100644 src/types/parsers/animeAboutInfo.ts delete mode 100644 src/types/parsers/animeCategory.ts delete mode 100644 src/types/parsers/animeEpisodeSrcs.ts delete mode 100644 src/types/parsers/animeEpisodes.ts delete mode 100644 src/types/parsers/animeGenre.ts delete mode 100644 src/types/parsers/animeProducer.ts delete mode 100644 src/types/parsers/animeSearch.ts delete mode 100644 src/types/parsers/animeSearchSuggestion.ts delete mode 100644 src/types/parsers/episodeServers.ts delete mode 100644 src/types/parsers/estimatedSchedule.ts delete mode 100644 src/types/parsers/homePage.ts delete mode 100644 src/types/parsers/index.ts delete mode 100644 src/utils/constants.ts delete mode 100644 src/utils/index.ts delete mode 100644 src/utils/methods.ts (limited to 'src') diff --git a/src/config/axios.ts b/src/config/axios.ts deleted file mode 100644 index 6292e9f..0000000 --- a/src/config/axios.ts +++ /dev/null @@ -1,21 +0,0 @@ -import axios, { AxiosError, type AxiosRequestConfig } from "axios"; -import { - SRC_BASE_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} from "../utils/constants.js"; - -const clientConfig: AxiosRequestConfig = { - timeout: 10000, - baseURL: SRC_BASE_URL, - headers: { - Accept: ACCEPT_HEADER, - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - }, -}; - -const client = axios.create(clientConfig); - -export { client, AxiosError }; diff --git a/src/config/cors.ts b/src/config/cors.ts index 8089522..6d6e138 100644 --- a/src/config/cors.ts +++ b/src/config/cors.ts @@ -1,24 +1,17 @@ -import cors from "cors"; import { config } from "dotenv"; +import { cors } from "hono/cors"; config(); -const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS - ? process.env.CORS_ALLOWED_ORIGINS.split(",") +const allowedOrigins = process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS + ? process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS.split(",") : ["http://localhost:4000", "*"]; const corsConfig = cors({ - origin: function (origin, callback) { - if (!origin || allowedOrigins.includes(origin)) { - callback(null, true); - } else { - callback(new Error("Not allowed by CORS")); - } - }, - methods: ["GET"], + allowMethods: ["GET"], maxAge: 600, credentials: true, - optionsSuccessStatus: 200, + origin: allowedOrigins, }); export default corsConfig; diff --git a/src/config/errorHandler.ts b/src/config/errorHandler.ts deleted file mode 100644 index 560f141..0000000 --- a/src/config/errorHandler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ErrorRequestHandler } from "express"; - -const errorHandler: ErrorRequestHandler = (error, req, res, next) => { - const status = error?.status || 500; - res.status(status).json({ - status, - message: error?.message || "Something Went Wrong", - }); -}; - -export default errorHandler; diff --git a/src/config/notFoundHandler.ts b/src/config/notFoundHandler.ts deleted file mode 100644 index a372e8e..0000000 --- a/src/config/notFoundHandler.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { RequestHandler } from "express"; -import createHttpError from "http-errors"; - -const notFoundHandler: RequestHandler = (req, res, next) => { - return next(createHttpError.NotFound()); -}; - -export default notFoundHandler; diff --git a/src/config/ratelimit.ts b/src/config/ratelimit.ts index c5add3f..afd6e54 100644 --- a/src/config/ratelimit.ts +++ b/src/config/ratelimit.ts @@ -1,17 +1,21 @@ import { config } from "dotenv"; -import createHttpError from "http-errors"; -import { rateLimit } from "express-rate-limit"; +import { rateLimiter } from "hono-rate-limiter"; +import { getConnInfo } from "@hono/node-server/conninfo"; config(); -export const ratelimit = rateLimit({ - windowMs: Number(process.env.WINDOWMS) || 30 * 60 * 1000, - limit: Number(process.env.MAX) || 6, - legacyHeaders: true, +export const ratelimit = rateLimiter({ + windowMs: Number(process.env.ANIWATCH_API_WINDOW_MS) || 30 * 60 * 1000, + limit: Number(process.env.ANIWATCH_API_MAX_REQS) || 6, standardHeaders: "draft-7", - handler: function (_, __, next) { - next( - createHttpError.TooManyRequests("Too many API requests, try again later") - ); + keyGenerator(c) { + const { remote } = getConnInfo(c); + const key = + `${String(remote.addressType)}_` + + `${String(remote.address)}:${String(remote.port)}`; + + return key; }, + handler: (c) => + c.json({ status: 429, message: "Too Many Requests 😵" }, { status: 429 }), }); diff --git a/src/controllers/animeAboutInfo.controller.ts b/src/controllers/animeAboutInfo.controller.ts deleted file mode 100644 index 6d50b50..0000000 --- a/src/controllers/animeAboutInfo.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeAnimeAboutInfo } from "../parsers/index.js"; -import { type AnimeAboutInfoQueryParams } from "../types/controllers/index.js"; - -// /anime/info?id=${anime-id} -const getAnimeAboutInfo: RequestHandler< - unknown, - Awaited>, - unknown, - AnimeAboutInfoQueryParams -> = async (req, res, next) => { - try { - const animeId = req.query.id - ? decodeURIComponent(req.query.id as string) - : null; - - if (animeId === null) { - throw createHttpError.BadRequest("Anime unique id required"); - } - - const data = await scrapeAnimeAboutInfo(animeId); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeAboutInfo; diff --git a/src/controllers/animeCategory.controller.ts b/src/controllers/animeCategory.controller.ts deleted file mode 100644 index 373b8e3..0000000 --- a/src/controllers/animeCategory.controller.ts +++ /dev/null @@ -1,39 +0,0 @@ -import createHttpError from "http-errors"; -import type { RequestHandler } from "express"; -import type { AnimeCategories } from "../types/anime.js"; -import { scrapeAnimeCategory } from "../parsers/index.js"; -import type { - CategoryAnimePathParams, - CategoryAnimeQueryParams, -} from "../types/controllers/index.js"; - -// /anime/:category?page=${page} -const getAnimeCategory: RequestHandler< - CategoryAnimePathParams, - Awaited>, - unknown, - CategoryAnimeQueryParams -> = async (req, res, next) => { - try { - const category = req.params.category - ? decodeURIComponent(req.params.category) - : null; - - const page: number = req.query.page - ? Number(decodeURIComponent(req.query?.page as string)) - : 1; - - if (category === null) { - throw createHttpError.BadRequest("category required"); - } - - const data = await scrapeAnimeCategory(category as AnimeCategories, page); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeCategory; diff --git a/src/controllers/animeEpisodeSrcs.controller.ts b/src/controllers/animeEpisodeSrcs.controller.ts deleted file mode 100644 index 6190ce5..0000000 --- a/src/controllers/animeEpisodeSrcs.controller.ts +++ /dev/null @@ -1,75 +0,0 @@ -import axios from "axios"; -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { type CheerioAPI, load } from "cheerio"; -import { scrapeAnimeEpisodeSources } from "../parsers/index.js"; -import { USER_AGENT_HEADER, SRC_BASE_URL } from "../utils/constants.js"; -import { type AnimeServers, Servers } from "../types/anime.js"; -import { type AnimeEpisodeSrcsQueryParams } from "../types/controllers/index.js"; - -type AnilistID = number | null; -type MalID = number | null; - -// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)} -const getAnimeEpisodeSources: RequestHandler< - unknown, - Awaited>, - unknown, - AnimeEpisodeSrcsQueryParams -> = async (req, res, next) => { - try { - const episodeId = req.query.id ? decodeURIComponent(req.query.id) : null; - - const server = ( - req.query.server - ? decodeURIComponent(req.query.server) - : Servers.VidStreaming - ) as AnimeServers; - - const category = ( - req.query.category ? decodeURIComponent(req.query.category) : "sub" - ) as "sub" | "dub"; - - if (episodeId === null) { - throw createHttpError.BadRequest("Anime episode id required"); - } - - let malID: MalID; - let anilistID: AnilistID; - const animeURL = new URL(episodeId?.split("?ep=")[0], SRC_BASE_URL)?.href; - - const [episodeSrcData, animeSrc] = await Promise.all([ - scrapeAnimeEpisodeSources(episodeId, server, category), - axios.get(animeURL, { - headers: { - Referer: SRC_BASE_URL, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - }, - }), - ]); - - const $: CheerioAPI = load(animeSrc?.data); - - try { - anilistID = Number( - JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id - ); - malID = Number(JSON.parse($("body")?.find("#syncData")?.text())?.mal_id); - } catch (err) { - anilistID = null; - malID = null; - } - - res.status(200).json({ - ...episodeSrcData, - anilistID, - malID, - }); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeEpisodeSources; diff --git a/src/controllers/animeEpisodes.controller.ts b/src/controllers/animeEpisodes.controller.ts deleted file mode 100644 index cf815f5..0000000 --- a/src/controllers/animeEpisodes.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeAnimeEpisodes } from "../parsers/index.js"; -import { type AnimeEpisodePathParams } from "../types/controllers/index.js"; - -// /anime/episodes/${anime-id} -const getAnimeEpisodes: RequestHandler< - AnimeEpisodePathParams, - Awaited>, - unknown, - unknown -> = async (req, res, next) => { - try { - const animeId = req.params.animeId - ? decodeURIComponent(req.params.animeId) - : null; - - if (animeId === null) { - throw createHttpError.BadRequest("Anime Id required"); - } - - const data = await scrapeAnimeEpisodes(animeId); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeEpisodes; diff --git a/src/controllers/animeGenre.controller.ts b/src/controllers/animeGenre.controller.ts deleted file mode 100644 index 486b4c7..0000000 --- a/src/controllers/animeGenre.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeGenreAnime } from "../parsers/index.js"; -import type { - GenreAnimePathParams, - GenreAnimeQueryParams, -} from "../types/controllers/index.js"; - -// /anime/genre/${name}?page=${page} -const getGenreAnime: RequestHandler< - GenreAnimePathParams, - Awaited>, - unknown, - GenreAnimeQueryParams -> = async (req, res, next) => { - try { - const name: string | null = req.params.name - ? decodeURIComponent(req.params.name as string) - : null; - - const page: number = req.query.page - ? Number(decodeURIComponent(req.query?.page as string)) - : 1; - - if (name === null) { - throw createHttpError.BadRequest("Anime genre required"); - } - - const data = await scrapeGenreAnime(name, page); - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getGenreAnime; diff --git a/src/controllers/animeProducer.controller.ts b/src/controllers/animeProducer.controller.ts deleted file mode 100644 index 3ebcd8a..0000000 --- a/src/controllers/animeProducer.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeProducerAnimes } from "../parsers/index.js"; -import type { - AnimeProducerPathParams, - AnimeProducerQueryParams, -} from "../types/controllers/index.js"; - -// /anime/producer/${name}?page=${page} -const getProducerAnimes: RequestHandler< - AnimeProducerPathParams, - Awaited>, - unknown, - AnimeProducerQueryParams -> = async (req, res, next) => { - try { - const name: string | null = req.params.name - ? decodeURIComponent(req.params.name as string) - : null; - - const page: number = req.query.page - ? Number(decodeURIComponent(req.query?.page as string)) - : 1; - - if (name === null) { - throw createHttpError.BadRequest("Anime producer name required"); - } - - const data = await scrapeProducerAnimes(name, page); - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getProducerAnimes; diff --git a/src/controllers/animeSearch.controller.ts b/src/controllers/animeSearch.controller.ts deleted file mode 100644 index 8937b0a..0000000 --- a/src/controllers/animeSearch.controller.ts +++ /dev/null @@ -1,57 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeAnimeSearch } from "../parsers/index.js"; -import type { - SearchFilters, - AnimeSearchQueryParams, -} from "../types/controllers/index.js"; - -const searchFilters: Record = { - 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< - unknown, - Awaited>, - unknown, - AnimeSearchQueryParams -> = async (req, res, next) => { - try { - 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 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) { - console.error(err); - next(err); - } -}; - -export default getAnimeSearch; diff --git a/src/controllers/animeSearchSuggestion.controller.ts b/src/controllers/animeSearchSuggestion.controller.ts deleted file mode 100644 index ed8784f..0000000 --- a/src/controllers/animeSearchSuggestion.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeAnimeSearchSuggestion } from "../parsers/index.js"; -import { type AnimeSearchSuggestQueryParams } from "../types/controllers/index.js"; - -// /anime/search/suggest?q=${query} -const getAnimeSearchSuggestion: RequestHandler< - unknown, - Awaited>, - unknown, - AnimeSearchSuggestQueryParams -> = async (req, res, next) => { - try { - const query: string | null = req.query.q - ? decodeURIComponent(req.query.q as string) - : null; - - if (query === null) { - throw createHttpError.BadRequest("Search keyword required"); - } - - const data = await scrapeAnimeSearchSuggestion(query); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeSearchSuggestion; diff --git a/src/controllers/episodeServers.controller.ts b/src/controllers/episodeServers.controller.ts deleted file mode 100644 index 16e1ca5..0000000 --- a/src/controllers/episodeServers.controller.ts +++ /dev/null @@ -1,30 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeEpisodeServers } from "../parsers/index.js"; -import { type EpisodeServersQueryParams } from "../types/controllers/index.js"; - -// /anime/servers?episodeId=${id} -const getEpisodeServers: RequestHandler< - unknown, - Awaited>, - unknown, - EpisodeServersQueryParams -> = async (req, res, next) => { - try { - const episodeId = req.query.episodeId - ? decodeURIComponent(req.query?.episodeId as string) - : null; - - if (episodeId === null) { - throw createHttpError.BadRequest("Episode id required"); - } - - const data = await scrapeEpisodeServers(episodeId); - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getEpisodeServers; diff --git a/src/controllers/estimatedSchedule.controller.ts b/src/controllers/estimatedSchedule.controller.ts deleted file mode 100644 index fef2516..0000000 --- a/src/controllers/estimatedSchedule.controller.ts +++ /dev/null @@ -1,36 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeEstimatedSchedule } from "../parsers/index.js"; -import { type EstimatedScheduleQueryParams } from "../types/controllers/index.js"; - -// /anime/schedule?date=${date} -const getEstimatedSchedule: RequestHandler< - unknown, - Awaited>, - unknown, - EstimatedScheduleQueryParams -> = async (req, res, next) => { - try { - const dateQuery = req.query.date - ? decodeURIComponent(req.query.date as string) - : null; - - if (dateQuery === null) { - throw createHttpError.BadRequest("Date payload required"); - } - if (!/^\d{4}-\d{2}-\d{2}$/.test(dateQuery)) { - throw createHttpError.BadRequest( - "Invalid date payload format. Months and days must have 2 digits" - ); - } - - const data = await scrapeEstimatedSchedule(dateQuery); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getEstimatedSchedule; diff --git a/src/controllers/homePage.controller.ts b/src/controllers/homePage.controller.ts deleted file mode 100644 index 36e1fe9..0000000 --- a/src/controllers/homePage.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type RequestHandler } from "express"; -import { scrapeHomePage } from "../parsers/index.js"; - -// /anime/home -const getHomePageInfo: RequestHandler< - unknown, - Awaited> -> = async (req, res, next) => { - try { - const data = await scrapeHomePage(); - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getHomePageInfo; diff --git a/src/controllers/index.ts b/src/controllers/index.ts deleted file mode 100644 index 6e90440..0000000 --- a/src/controllers/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import getGenreAnime from "./animeGenre.controller.js"; -import getHomePageInfo from "./homePage.controller.js"; -import getAnimeSearch from "./animeSearch.controller.js"; -import getAnimeEpisodes from "./animeEpisodes.controller.js"; -import getAnimeCategory from "./animeCategory.controller.js"; -import getProducerAnimes from "./animeProducer.controller.js"; -import getEpisodeServers from "./episodeServers.controller.js"; -import getAnimeAboutInfo from "./animeAboutInfo.controller.js"; -import getEstimatedSchedule from "./estimatedSchedule.controller.js"; -import getAnimeEpisodeSources from "./animeEpisodeSrcs.controller.js"; -import getAnimeSearchSuggestion from "./animeSearchSuggestion.controller.js"; - -export { - getGenreAnime, - getAnimeSearch, - getHomePageInfo, - getAnimeEpisodes, - getAnimeCategory, - getEpisodeServers, - getProducerAnimes, - getAnimeAboutInfo, - getEstimatedSchedule, - getAnimeEpisodeSources, - getAnimeSearchSuggestion, -}; diff --git a/src/extractors/index.ts b/src/extractors/index.ts deleted file mode 100644 index 788dc8c..0000000 --- a/src/extractors/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import StreamSB from "./streamsb.js"; -import StreamTape from "./streamtape.js"; -import RapidCloud from "./rapidcloud.js"; -import MegaCloud from "./megacloud.js"; - -export { StreamSB, StreamTape, RapidCloud, MegaCloud }; diff --git a/src/extractors/megacloud.ts b/src/extractors/megacloud.ts deleted file mode 100644 index bbed720..0000000 --- a/src/extractors/megacloud.ts +++ /dev/null @@ -1,226 +0,0 @@ -import axios from "axios"; -import crypto from "crypto"; -import createHttpError from "http-errors"; - -// https://megacloud.tv/embed-2/e-1/dBqCr5BcOhnD?k=1 - -const megacloud = { - script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=", - sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=", -} as const; - -type track = { - file: string; - kind: string; - label?: string; - default?: boolean; -}; - -type intro_outro = { - start: number; - end: number; -}; - -type unencryptedSrc = { - file: string; - type: string; -}; - -type extractedSrc = { - sources: string | unencryptedSrc[]; - tracks: track[]; - encrypted: boolean; - intro: intro_outro; - outro: intro_outro; - server: number; -}; - -interface ExtractedData - extends Pick { - sources: { url: string; type: string }[]; -} - -class MegaCloud { - private serverName = "megacloud"; - - async extract(videoUrl: URL) { - try { - const extractedData: ExtractedData = { - tracks: [], - intro: { - start: 0, - end: 0, - }, - outro: { - start: 0, - end: 0, - }, - sources: [], - }; - - const videoId = videoUrl?.href?.split("/")?.pop()?.split("?")[0]; - const { data: srcsData } = await axios.get( - megacloud.sources.concat(videoId || ""), - { - headers: { - Accept: "*/*", - "X-Requested-With": "XMLHttpRequest", - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", - Referer: videoUrl.href, - }, - } - ); - if (!srcsData) { - throw createHttpError.NotFound("Url may have an invalid video id"); - } - - // console.log(JSON.stringify(srcsData, null, 2)); - - const encryptedString = srcsData.sources; - if (!srcsData.encrypted && Array.isArray(encryptedString)) { - extractedData.intro = srcsData.intro; - extractedData.outro = srcsData.outro; - extractedData.tracks = srcsData.tracks; - extractedData.sources = encryptedString.map((s) => ({ - url: s.file, - type: s.type, - })); - - return extractedData; - } - - let text: string; - const { data } = await axios.get( - megacloud.script.concat(Date.now().toString()) - ); - - text = data; - if (!text) { - throw createHttpError.InternalServerError( - "Couldn't fetch script to decrypt resource" - ); - } - - const vars = this.extractVariables(text); - if (!vars.length) { - throw new Error("Can't find variables. Perhaps the extractor is outdated."); - } - - const { secret, encryptedSource } = this.getSecret( - encryptedString as string, - vars - ); - const decrypted = this.decrypt(encryptedSource, secret); - try { - const sources = JSON.parse(decrypted); - extractedData.intro = srcsData.intro; - extractedData.outro = srcsData.outro; - extractedData.tracks = srcsData.tracks; - extractedData.sources = sources.map((s: any) => ({ - url: s.file, - type: s.type, - })); - - return extractedData; - } catch (error) { - throw createHttpError.InternalServerError("Failed to decrypt resource"); - } - } catch (err) { - // console.log(err); - throw err; - } - } - - extractVariables(text: string) { - // copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30' - const regex = - /case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);/g; - const matches = text.matchAll(regex); - const vars = Array.from(matches, (match) => { - const matchKey1 = this.matchingKey(match[1], text); - const matchKey2 = this.matchingKey(match[2], text); - try { - return [parseInt(matchKey1, 16), parseInt(matchKey2, 16)]; - } catch (e) { - return []; - } - }).filter((pair) => pair.length > 0); - - return vars; - } - - getSecret(encryptedString: string, values: number[][]) { - let secret = "", - encryptedSource = "", - encryptedSourceArray = encryptedString.split(""), - currentIndex = 0; - - for (const index of values) { - const start = index[0] + currentIndex; - const end = start + index[1]; - - for (let i = start; i < end; i++) { - secret += encryptedString[i]; - encryptedSourceArray[i] = ""; - } - currentIndex += index[1]; - } - - encryptedSource = encryptedSourceArray.join(""); - - return { secret, encryptedSource }; - } - - decrypt(encrypted: string, keyOrSecret: string, maybe_iv?: string) { - let key; - let iv; - let contents; - if (maybe_iv) { - key = keyOrSecret; - iv = maybe_iv; - contents = encrypted; - } else { - // copied from 'https://github.com/brix/crypto-js/issues/468' - const cypher = Buffer.from(encrypted, "base64"); - const salt = cypher.subarray(8, 16); - const password = Buffer.concat([ - Buffer.from(keyOrSecret, "binary"), - salt, - ]); - const md5Hashes = []; - let digest = password; - for (let i = 0; i < 3; i++) { - md5Hashes[i] = crypto.createHash("md5").update(digest).digest(); - digest = Buffer.concat([md5Hashes[i], password]); - } - key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); - iv = md5Hashes[2]; - contents = cypher.subarray(16); - } - - const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); - const decrypted = - decipher.update( - contents as any, - typeof contents === "string" ? "base64" : undefined, - "utf8" - ) + decipher.final(); - - return decrypted; - } - - // function copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30' - matchingKey(value: string, script: string) { - const regex = new RegExp(`,${value}=((?:0x)?([0-9a-fA-F]+))`); - const match = script.match(regex); - if (match) { - return match[1].replace(/^0x/, ""); - } else { - throw new Error("Failed to match the key"); - } - } - -} - -export default MegaCloud; diff --git a/src/extractors/rapidcloud.ts b/src/extractors/rapidcloud.ts deleted file mode 100644 index 8073c8b..0000000 --- a/src/extractors/rapidcloud.ts +++ /dev/null @@ -1,166 +0,0 @@ -import axios from "axios"; -import CryptoJS from "crypto-js"; -import { substringAfter, substringBefore } from "../utils/index.js"; -import type { Video, Subtitle, Intro } from "../types/extractor.js"; - -type extractReturn = { - sources: Video[]; - subtitles: Subtitle[]; -}; - -// https://megacloud.tv/embed-2/e-1/IxJ7GjGVCyml?k=1 -class RapidCloud { - private serverName = "RapidCloud"; - private sources: Video[] = []; - - // https://rapid-cloud.co/embed-6/eVZPDXwVfrY3?vast=1 - private readonly fallbackKey = "c1d17096f2ca11b7"; - private readonly host = "https://rapid-cloud.co"; - - async extract(videoUrl: URL): Promise { - const result: extractReturn & { intro?: Intro; outro?: Intro } = { - sources: [], - subtitles: [], - }; - - try { - const id = videoUrl.href.split("/").pop()?.split("?")[0]; - const options = { - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }; - - let res = null; - - res = await axios.get( - `https://${videoUrl.hostname}/embed-2/ajax/e-1/getSources?id=${id}`, - options - ); - - let { - data: { sources, tracks, intro, outro, encrypted }, - } = res; - - let decryptKey = await ( - await axios.get( - "https://raw.githubusercontent.com/cinemaxhq/keys/e1/key" - ) - ).data; - - decryptKey = substringBefore( - substringAfter(decryptKey, '"blob-code blob-code-inner js-file-line">'), - "" - ); - - if (!decryptKey) { - decryptKey = await ( - await axios.get( - "https://raw.githubusercontent.com/cinemaxhq/keys/e1/key" - ) - ).data; - } - - if (!decryptKey) decryptKey = this.fallbackKey; - - try { - if (encrypted) { - const sourcesArray = sources.split(""); - let extractedKey = ""; - let currentIndex = 0; - - for (const index of decryptKey) { - const start = index[0] + currentIndex; - const end = start + index[1]; - - for (let i = start; i < end; i++) { - extractedKey += res.data.sources[i]; - sourcesArray[i] = ""; - } - currentIndex += index[1]; - } - - decryptKey = extractedKey; - sources = sourcesArray.join(""); - - const decrypt = CryptoJS.AES.decrypt(sources, decryptKey); - sources = JSON.parse(decrypt.toString(CryptoJS.enc.Utf8)); - } - } catch (err: any) { - console.log(err.message); - throw new Error("Cannot decrypt sources. Perhaps the key is invalid."); - } - - this.sources = sources?.map((s: any) => ({ - url: s.file, - isM3U8: s.file.includes(".m3u8"), - })); - - result.sources.push(...this.sources); - - if (videoUrl.href.includes(new URL(this.host).host)) { - result.sources = []; - this.sources = []; - - for (const source of sources) { - const { data } = await axios.get(source.file, options); - const m3u8data = data - .split("\n") - .filter( - (line: string) => - line.includes(".m3u8") && line.includes("RESOLUTION=") - ); - - const secondHalf = m3u8data.map((line: string) => - line.match(/RESOLUTION=.*,(C)|URI=.*/g)?.map((s) => s.split("=")[1]) - ); - - const TdArray = secondHalf.map((s: string[]) => { - const f1 = s[0].split(",C")[0]; - const f2 = s[1].replace(/"/g, ""); - - return [f1, f2]; - }); - - for (const [f1, f2] of TdArray) { - this.sources.push({ - url: `${source.file?.split("master.m3u8")[0]}${f2.replace( - "iframes", - "index" - )}`, - quality: f1.split("x")[1] + "p", - isM3U8: f2.includes(".m3u8"), - }); - } - result.sources.push(...this.sources); - } - } - - result.intro = - intro?.end > 1 ? { start: intro.start, end: intro.end } : undefined; - result.outro = - outro?.end > 1 ? { start: outro.start, end: outro.end } : undefined; - - result.sources.push({ - url: sources[0].file, - isM3U8: sources[0].file.includes(".m3u8"), - quality: "auto", - }); - - result.subtitles = tracks - .map((s: any) => - s.file - ? { url: s.file, lang: s.label ? s.label : "Thumbnails" } - : null - ) - .filter((s: any) => s); - - return result; - } catch (err: any) { - console.log(err.message); - throw err; - } - } -} - -export default RapidCloud; diff --git a/src/extractors/streamsb.ts b/src/extractors/streamsb.ts deleted file mode 100644 index 3eeaabe..0000000 --- a/src/extractors/streamsb.ts +++ /dev/null @@ -1,83 +0,0 @@ -import axios from "axios"; -import type { Video } from "../types/extractor.js"; -import { USER_AGENT_HEADER } from "../utils/index.js"; - -class StreamSB { - private serverName = "streamSB"; - private sources: Video[] = []; - - private readonly host = "https://watchsb.com/sources50"; - private readonly host2 = "https://streamsss.net/sources16"; - - private PAYLOAD(hex: string): string { - // `5363587530696d33443675687c7c${hex}7c7c433569475830474c497a65767c7c73747265616d7362`; - return `566d337678566f743674494a7c7c${hex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362`; - } - - async extract(videoUrl: URL, isAlt: boolean = false): Promise { - let headers: Record = { - watchsb: "sbstream", - Referer: videoUrl.href, - "User-Agent": USER_AGENT_HEADER, - }; - let id = videoUrl.href.split("/e/").pop(); - if (id?.includes("html")) { - id = id.split(".html")[0]; - } - const bytes = new TextEncoder().encode(id); - - const res = await axios - .get( - `${isAlt ? this.host2 : this.host}/${this.PAYLOAD( - Buffer.from(bytes).toString("hex") - )}`, - { headers } - ) - .catch(() => null); - - if (!res?.data.stream_data) { - throw new Error("No source found. Try a different server"); - } - - headers = { - "User-Agent": USER_AGENT_HEADER, - Referer: videoUrl.href.split("e/")[0], - }; - - const m3u8_urls = await axios.get(res.data.stream_data.file, { - headers, - }); - - const videoList = m3u8_urls?.data?.split("#EXT-X-STREAM-INF:") ?? []; - - for (const video of videoList) { - if (!video.includes("m3u8")) continue; - - const url = video.split("\n")[1]; - const quality = video.split("RESOLUTION=")[1].split(",")[0].split("x")[1]; - - this.sources.push({ - url: url, - quality: `${quality}p`, - isM3U8: true, - }); - } - - this.sources.push({ - url: res.data.stream_data.file, - quality: "auto", - isM3U8: res.data.stream_data.file.includes(".m3u8"), - }); - - return this.sources; - } - - private addSources(source: any): void { - this.sources.push({ - url: source.file, - isM3U8: source.file.includes(".m3u8"), - }); - } -} - -export default StreamSB; diff --git a/src/extractors/streamtape.ts b/src/extractors/streamtape.ts deleted file mode 100644 index 69910ce..0000000 --- a/src/extractors/streamtape.ts +++ /dev/null @@ -1,37 +0,0 @@ -import axios from "axios"; -import { load, type CheerioAPI } from "cheerio"; -import type { Video } from "../types/extractor.js"; - -class StreamTape { - private serverName = "StreamTape"; - private sources: Video[] = []; - - async extract(videoUrl: URL): Promise { - try { - const { data } = await axios.get(videoUrl.href).catch(() => { - throw new Error("Video not found"); - }); - - const $: CheerioAPI = load(data); - - let [fh, sh] = $.html() - ?.match(/robotlink'\).innerHTML = (.*)'/)![1] - .split("+ ('"); - - sh = sh.substring(3); - fh = fh.replace(/\'/g, ""); - - const url = `https:${fh}${sh}`; - - this.sources.push({ - url: url, - isM3U8: url.includes(".m3u8"), - }); - - return this.sources; - } catch (err) { - throw new Error((err as Error).message); - } - } -} -export default StreamTape; diff --git a/src/parsers/animeAboutInfo.ts b/src/parsers/animeAboutInfo.ts deleted file mode 100644 index f92b0fc..0000000 --- a/src/parsers/animeAboutInfo.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { - SRC_BASE_URL, - extractAnimes, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractMostPopularAnimes, -} 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 ScrapedAnimeAboutInfo } from "../types/parsers/index.js"; - -// /anime/info?id=${anime-id} -async function scrapeAnimeAboutInfo( - id: string -): Promise { - const res: ScrapedAnimeAboutInfo = { - anime: { - info: { - id: null, - anilistId: null, - malId: null, - name: null, - poster: null, - description: null, - stats: { - rating: null, - quality: null, - episodes: { - sub: null, - dub: null, - }, - type: null, - duration: null, - }, - promotionalVideos: [], - charactersVoiceActors: [], - }, - moreInfo: {}, - }, - seasons: [], - mostPopularAnimes: [], - relatedAnimes: [], - recommendedAnimes: [], - }; - - try { - const animeUrl: URL = new URL(id, SRC_BASE_URL); - const mainPage = await axios.get(animeUrl.href, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - try { - res.anime.info.anilistId = Number( - JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id - ); - res.anime.info.malId = Number(JSON.parse($("body")?.find("#syncData")?.text())?.mal_id); - } catch (err) { - res.anime.info.anilistId = null; - res.anime.info.malId = null; - } - - const selector: SelectorType = "#ani_detail .container .anis-content"; - - res.anime.info.id = - $(selector) - ?.find(".anisc-detail .film-buttons a.btn-play") - ?.attr("href") - ?.split("/") - ?.pop() || null; - res.anime.info.name = - $(selector) - ?.find(".anisc-detail .film-name.dynamic-name") - ?.text() - ?.trim() || null; - res.anime.info.description = - $(selector) - ?.find(".anisc-detail .film-description .text") - .text() - ?.split("[") - ?.shift() - ?.trim() || null; - res.anime.info.poster = - $(selector)?.find(".film-poster .film-poster-img")?.attr("src")?.trim() || - null; - - // stats - res.anime.info.stats.rating = - $(`${selector} .film-stats .tick .tick-pg`)?.text()?.trim() || null; - res.anime.info.stats.quality = - $(`${selector} .film-stats .tick .tick-quality`)?.text()?.trim() || null; - res.anime.info.stats.episodes = { - sub: - Number($(`${selector} .film-stats .tick .tick-sub`)?.text()?.trim()) || - null, - dub: - Number($(`${selector} .film-stats .tick .tick-dub`)?.text()?.trim()) || - null, - }; - res.anime.info.stats.type = - $(`${selector} .film-stats .tick`) - ?.text() - ?.trim() - ?.replace(/[\s\n]+/g, " ") - ?.split(" ") - ?.at(-2) || null; - res.anime.info.stats.duration = - $(`${selector} .film-stats .tick`) - ?.text() - ?.trim() - ?.replace(/[\s\n]+/g, " ") - ?.split(" ") - ?.pop() || null; - - // get promotional videos - $(".block_area.block_area-promotions .block_area-promotions-list .screen-items .item").each( - (_, el) => { - res.anime.info.promotionalVideos.push({ - title: $(el).attr("data-title"), - source: $(el).attr("data-src"), - thumbnail: $(el).find("img").attr("src"), - }); - } - ); - - // get characters and voice actors - $(".block_area.block_area-actors .block-actors-content .bac-list-wrap .bac-item").each( - (_, el) => { - res.anime.info.charactersVoiceActors.push({ - character: { - id: $(el).find($(".per-info.ltr .pi-avatar")).attr("href")?.split("/")[2] || "", - poster: $(el).find($(".per-info.ltr .pi-avatar img")).attr("data-src") || "", - name: $(el).find($(".per-info.ltr .pi-detail a")).text(), - cast: $(el).find($(".per-info.ltr .pi-detail .pi-cast")).text(), - }, - voiceActor: { - id: $(el).find($(".per-info.rtl .pi-avatar")).attr("href")?.split("/")[2] || "", - poster: $(el).find($(".per-info.rtl .pi-avatar img")).attr("data-src") || "", - name: $(el).find($(".per-info.rtl .pi-detail a")).text(), - cast: $(el).find($(".per-info.rtl .pi-detail .pi-cast")).text(), - }, - }); - } - ); - - // more information - $(`${selector} .anisc-info-wrap .anisc-info .item:not(.w-hide)`).each( - (i, el) => { - let key = $(el) - .find(".item-head") - .text() - .toLowerCase() - .replace(":", "") - .trim(); - key = key.includes(" ") ? key.replace(" ", "") : key; - - const value = [ - ...$(el) - .find("*:not(.item-head)") - .map((i, el) => $(el).text().trim()), - ] - .map((i) => `${i}`) - .toString() - .trim(); - - if (key === "genres") { - res.anime.moreInfo[key] = value.split(",").map((i) => i.trim()); - return; - } - if (key === "producers") { - res.anime.moreInfo[key] = value.split(",").map((i) => i.trim()); - return; - } - res.anime.moreInfo[key] = value; - } - ); - - // more seasons - const seasonsSelector: SelectorType = "#main-content .os-list a.os-item"; - $(seasonsSelector).each((i, el) => { - res.seasons.push({ - id: $(el)?.attr("href")?.slice(1)?.trim() || null, - name: $(el)?.attr("title")?.trim() || null, - title: $(el)?.find(".title")?.text()?.trim(), - poster: - $(el) - ?.find(".season-poster") - ?.attr("style") - ?.split(" ") - ?.pop() - ?.split("(") - ?.pop() - ?.split(")")[0] || null, - isCurrent: $(el).hasClass("active"), - }); - }); - - const relatedAnimeSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(1) .anif-block-ul ul li"; - res.relatedAnimes = extractMostPopularAnimes($, relatedAnimeSelector); - - const mostPopularSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(2) .anif-block-ul ul li"; - res.mostPopularAnimes = extractMostPopularAnimes($, mostPopularSelector); - - const recommendedAnimeSelector: SelectorType = - "#main-content .block_area.block_area_category .tab-content .flw-item"; - res.recommendedAnimes = extractAnimes($, recommendedAnimeSelector); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeAboutInfo; diff --git a/src/parsers/animeCategory.ts b/src/parsers/animeCategory.ts deleted file mode 100644 index c79fcc7..0000000 --- a/src/parsers/animeCategory.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - SRC_BASE_URL, - extractAnimes, - ACCEPT_HEADER, - USER_AGENT_HEADER, - extractTop10Animes, - ACCEPT_ENCODING_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import { type AnimeCategories } from "../types/anime.js"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import { type ScrapedAnimeCategory } from "../types/parsers/index.js"; - -// /anime/:category?page=${page} -async function scrapeAnimeCategory( - category: AnimeCategories, - page: number = 1 -): Promise { - const res: ScrapedAnimeCategory = { - animes: [], - genres: [], - top10Animes: { - today: [], - week: [], - month: [], - }, - category, - currentPage: Number(page), - hasNextPage: false, - totalPages: 1, - }; - - try { - const scrapeUrl: URL = new URL(category, SRC_BASE_URL); - const mainPage = await axios.get(`${scrapeUrl}?page=${page}`, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const selector: SelectorType = - "#main-content .tab-content .film_list-wrap .flw-item"; - - const categoryNameSelector: SelectorType = - "#main-content .block_area .block_area-header .cat-heading"; - res.category = $(categoryNameSelector)?.text()?.trim() ?? category; - - res.hasNextPage = - $(".pagination > li").length > 0 - ? $(".pagination li.active").length > 0 - ? $(".pagination > li").last().hasClass("active") - ? false - : true - : false - : false; - - res.totalPages = - Number( - $('.pagination > .page-item a[title="Last"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $('.pagination > .page-item a[title="Next"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $(".pagination > .page-item.active a")?.text()?.trim() - ) || 1; - - res.animes = extractAnimes($, selector); - - if (res.animes.length === 0 && !res.hasNextPage) { - res.totalPages = 0; - } - - const genreSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; - $(genreSelector).each((i, el) => { - res.genres.push(`${$(el).text().trim()}`); - }); - - const top10AnimeSelector: SelectorType = - '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; - - $(top10AnimeSelector).each((i, el) => { - const period = $(el).attr("id")?.split("-")?.pop()?.trim(); - - if (period === "day") { - res.top10Animes.today = extractTop10Animes($, period); - return; - } - if (period === "week") { - res.top10Animes.week = extractTop10Animes($, period); - return; - } - if (period === "month") { - res.top10Animes.month = extractTop10Animes($, period); - } - }); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeCategory; diff --git a/src/parsers/animeEpisodeSrcs.ts b/src/parsers/animeEpisodeSrcs.ts deleted file mode 100644 index 913c535..0000000 --- a/src/parsers/animeEpisodeSrcs.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - SRC_AJAX_URL, - SRC_BASE_URL, - retrieveServerId, - USER_AGENT_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import { load, type CheerioAPI } from "cheerio"; -import createHttpError, { type HttpError } from "http-errors"; -import { type AnimeServers, Servers } from "../types/anime.js"; -import { - RapidCloud, - StreamSB, - StreamTape, - MegaCloud, -} from "../extractors/index.js"; -import { type ScrapedAnimeEpisodesSources } from "../types/parsers/index.js"; - -// vidtreaming -> 4 -// rapidcloud -> 1 -// streamsb -> 5 -// streamtape -> 3 - -// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)} -async function scrapeAnimeEpisodeSources( - episodeId: string, - server: AnimeServers = Servers.VidStreaming, - category: "sub" | "dub" | "raw" = "sub" -): Promise { - if (episodeId.startsWith("http")) { - const serverUrl = new URL(episodeId); - switch (server) { - case Servers.VidStreaming: - case Servers.VidCloud: - return { - ...(await new MegaCloud().extract(serverUrl)), - }; - case Servers.StreamSB: - return { - headers: { - Referer: serverUrl.href, - watchsb: "streamsb", - "User-Agent": USER_AGENT_HEADER, - }, - sources: await new StreamSB().extract(serverUrl, true), - }; - case Servers.StreamTape: - return { - headers: { Referer: serverUrl.href, "User-Agent": USER_AGENT_HEADER }, - sources: await new StreamTape().extract(serverUrl), - }; - default: // vidcloud - return { - headers: { Referer: serverUrl.href }, - ...(await new RapidCloud().extract(serverUrl)), - }; - } - } - - const epId = new URL(`/watch/${episodeId}`, SRC_BASE_URL).href; - console.log(epId); - - try { - const resp = await axios.get( - `${SRC_AJAX_URL}/v2/episode/servers?episodeId=${epId.split("?ep=")[1]}`, - { - headers: { - Referer: epId, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - }, - } - ); - - const $: CheerioAPI = load(resp.data.html); - - let serverId: string | null = null; - - try { - console.log("THE SERVER: ", server); - - switch (server) { - case Servers.VidCloud: { - serverId = retrieveServerId($, 1, category); - if (!serverId) throw new Error("RapidCloud not found"); - break; - } - case Servers.VidStreaming: { - serverId = retrieveServerId($, 4, category); - console.log("SERVER_ID: ", serverId); - if (!serverId) throw new Error("VidStreaming not found"); - break; - } - case Servers.StreamSB: { - serverId = retrieveServerId($, 5, category); - if (!serverId) throw new Error("StreamSB not found"); - break; - } - case Servers.StreamTape: { - serverId = retrieveServerId($, 3, category); - if (!serverId) throw new Error("StreamTape not found"); - break; - } - } - } catch (err) { - throw createHttpError.NotFound( - "Couldn't find server. Try another server" - ); - } - - const { - data: { link }, - } = await axios.get(`${SRC_AJAX_URL}/v2/episode/sources?id=${serverId}`); - console.log("THE LINK: ", link); - - return await scrapeAnimeEpisodeSources(link, server); - } catch (err: any) { - console.log(err); - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeEpisodeSources; diff --git a/src/parsers/animeEpisodes.ts b/src/parsers/animeEpisodes.ts deleted file mode 100644 index 41ac942..0000000 --- a/src/parsers/animeEpisodes.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - SRC_BASE_URL, - SRC_AJAX_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import { load, type CheerioAPI } from "cheerio"; -import createHttpError, { type HttpError } from "http-errors"; -import { type ScrapedAnimeEpisodes } from "../types/parsers/index.js"; - -// /anime/episodes/${anime-id} -async function scrapeAnimeEpisodes( - animeId: string -): Promise { - const res: ScrapedAnimeEpisodes = { - totalEpisodes: 0, - episodes: [], - }; - - try { - const episodesAjax = await axios.get( - `${SRC_AJAX_URL}/v2/episode/list/${animeId.split("-").pop()}`, - { - headers: { - Accept: ACCEPT_HEADER, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Referer: `${SRC_BASE_URL}/watch/${animeId}`, - }, - } - ); - - const $: CheerioAPI = load(episodesAjax.data.html); - - res.totalEpisodes = Number($(".detail-infor-content .ss-list a").length); - - $(".detail-infor-content .ss-list a").each((i, el) => { - res.episodes.push({ - title: $(el)?.attr("title")?.trim() || null, - episodeId: $(el)?.attr("href")?.split("/")?.pop() || null, - number: Number($(el).attr("data-number")), - isFiller: $(el).hasClass("ssl-item-filler"), - }); - }); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeEpisodes; diff --git a/src/parsers/animeGenre.ts b/src/parsers/animeGenre.ts deleted file mode 100644 index 110398d..0000000 --- a/src/parsers/animeGenre.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - SRC_BASE_URL, - ACCEPT_HEADER, - extractAnimes, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractMostPopularAnimes, -} 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 { ScrapedGenreAnime } from "../types/parsers/index.js"; - -// /anime/genre/${name}?page=${page} -async function scrapeGenreAnime( - genreName: string, - page: number = 1 -): Promise { - const res: ScrapedGenreAnime = { - genreName, - animes: [], - genres: [], - topAiringAnimes: [], - totalPages: 1, - hasNextPage: false, - currentPage: Number(page), - }; - - // there's a typo with zoro where martial arts is marial arts - genreName = genreName === "martial-arts" ? "marial-arts" : genreName; - - try { - const genreUrl: URL = new URL( - `/genre/${genreName}?page=${page}`, - SRC_BASE_URL - ); - - const mainPage = await axios.get(genreUrl.href, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const selector: SelectorType = - "#main-content .tab-content .film_list-wrap .flw-item"; - - const genreNameSelector: SelectorType = - "#main-content .block_area .block_area-header .cat-heading"; - res.genreName = $(genreNameSelector)?.text()?.trim() ?? genreName; - - res.hasNextPage = - $(".pagination > li").length > 0 - ? $(".pagination li.active").length > 0 - ? $(".pagination > li").last().hasClass("active") - ? false - : true - : false - : false; - - res.totalPages = - Number( - $('.pagination > .page-item a[title="Last"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $('.pagination > .page-item a[title="Next"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $(".pagination > .page-item.active a")?.text()?.trim() - ) || 1; - - res.animes = extractAnimes($, selector); - - if (res.animes.length === 0 && !res.hasNextPage) { - res.totalPages = 0; - } - - const genreSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; - $(genreSelector).each((i, el) => { - res.genres.push(`${$(el).text().trim()}`); - }); - - const topAiringSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li"; - res.topAiringAnimes = extractMostPopularAnimes($, topAiringSelector); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeGenreAnime; diff --git a/src/parsers/animeProducer.ts b/src/parsers/animeProducer.ts deleted file mode 100644 index 2862852..0000000 --- a/src/parsers/animeProducer.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - SRC_BASE_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractMostPopularAnimes, - extractAnimes, - extractTop10Animes, -} 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 { ScrapedProducerAnime } from "../types/parsers/index.js"; - -// /anime/producer/${name}?page=${page} -async function scrapeProducerAnimes( - producerName: string, - page: number = 1 -): Promise { - const res: ScrapedProducerAnime = { - producerName, - animes: [], - top10Animes: { - today: [], - week: [], - month: [], - }, - topAiringAnimes: [], - totalPages: 1, - hasNextPage: false, - currentPage: Number(page), - }; - - try { - const producerUrl: URL = new URL( - `/producer/${producerName}?page=${page}`, - SRC_BASE_URL - ); - - const mainPage = await axios.get(producerUrl.href, { - headers: { - Accept: ACCEPT_HEADER, - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const animeSelector: SelectorType = - "#main-content .tab-content .film_list-wrap .flw-item"; - - res.hasNextPage = - $(".pagination > li").length > 0 - ? $(".pagination li.active").length > 0 - ? $(".pagination > li").last().hasClass("active") - ? false - : true - : false - : false; - - res.totalPages = - Number( - $('.pagination > .page-item a[title="Last"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $('.pagination > .page-item a[title="Next"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $(".pagination > .page-item.active a")?.text()?.trim() - ) || 1; - - res.animes = extractAnimes($, animeSelector); - - if (res.animes.length === 0 && !res.hasNextPage) { - res.totalPages = 0; - } - - const producerNameSelector: SelectorType = - "#main-content .block_area .block_area-header .cat-heading"; - res.producerName = $(producerNameSelector)?.text()?.trim() ?? producerName; - - const top10AnimeSelector: SelectorType = - '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; - - $(top10AnimeSelector).each((_, el) => { - const period = $(el).attr("id")?.split("-")?.pop()?.trim(); - - if (period === "day") { - res.top10Animes.today = extractTop10Animes($, period); - return; - } - if (period === "week") { - res.top10Animes.week = extractTop10Animes($, period); - return; - } - if (period === "month") { - res.top10Animes.month = extractTop10Animes($, period); - } - }); - - const topAiringSelector: SelectorType = - "#main-sidebar .block_area_sidebar:nth-child(2) .block_area-content .anif-block-ul ul li"; - res.topAiringAnimes = extractMostPopularAnimes($, topAiringSelector); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeProducerAnimes; diff --git a/src/parsers/animeSearch.ts b/src/parsers/animeSearch.ts deleted file mode 100644 index 16818c6..0000000 --- a/src/parsers/animeSearch.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - SRC_SEARCH_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - 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, - filters: SearchFilters -): Promise { - const res: ScrapedAnimeSearchResult = { - animes: [], - mostPopularAnimes: [], - currentPage: Number(page), - hasNextPage: false, - totalPages: 1, - searchQuery: q, - searchFilters: filters, - }; - - try { - 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); - - const selector: SelectorType = - "#main-content .tab-content .film_list-wrap .flw-item"; - - res.hasNextPage = - $(".pagination > li").length > 0 - ? $(".pagination li.active").length > 0 - ? $(".pagination > li").last().hasClass("active") - ? false - : true - : false - : false; - - res.totalPages = - Number( - $('.pagination > .page-item a[title="Last"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $('.pagination > .page-item a[title="Next"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $(".pagination > .page-item.active a")?.text()?.trim() - ) || 1; - - res.animes = extractAnimes($, selector); - - if (res.animes.length === 0 && !res.hasNextPage) { - res.totalPages = 0; - } - - const mostPopularSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li"; - res.mostPopularAnimes = extractMostPopularAnimes($, mostPopularSelector); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeSearch; diff --git a/src/parsers/animeSearchSuggestion.ts b/src/parsers/animeSearchSuggestion.ts deleted file mode 100644 index acef2a4..0000000 --- a/src/parsers/animeSearchSuggestion.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - SRC_HOME_URL, - SRC_AJAX_URL, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} 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 { ScrapedAnimeSearchSuggestion } from "../types/parsers/index.js"; - -// /anime/search/suggest?q=${query} -async function scrapeAnimeSearchSuggestion( - q: string -): Promise { - const res: ScrapedAnimeSearchSuggestion = { - suggestions: [], - }; - - try { - const { data } = await axios.get( - `${SRC_AJAX_URL}/search/suggest?keyword=${encodeURIComponent(q)}`, - { - headers: { - Accept: "*/*", - Pragma: "no-cache", - Referer: SRC_HOME_URL, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - }, - } - ); - - const $: CheerioAPI = load(data.html); - const selector: SelectorType = ".nav-item:has(.film-poster)"; - - if ($(selector).length < 1) return res; - - $(selector).each((_, el) => { - const id = $(el).attr("href")?.split("?")[0].includes("javascript") - ? null - : $(el).attr("href")?.split("?")[0]?.slice(1); - - res.suggestions.push({ - id, - name: $(el).find(".srp-detail .film-name")?.text()?.trim() || null, - jname: - $(el).find(".srp-detail .film-name")?.attr("data-jname")?.trim() || - $(el).find(".srp-detail .alias-name")?.text()?.trim() || - null, - poster: $(el) - .find(".film-poster .film-poster-img") - ?.attr("data-src") - ?.trim(), - moreInfo: [ - ...$(el) - .find(".film-infor") - .contents() - .map((_, el) => $(el).text().trim()), - ].filter((i) => i), - }); - }); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeSearchSuggestion; diff --git a/src/parsers/episodeServers.ts b/src/parsers/episodeServers.ts deleted file mode 100644 index 8f95229..0000000 --- a/src/parsers/episodeServers.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - SRC_BASE_URL, - SRC_AJAX_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} 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 { ScrapedEpisodeServers } from "../types/parsers/index.js"; - -// /anime/servers?episodeId=${id} -async function scrapeEpisodeServers( - episodeId: string -): Promise { - const res: ScrapedEpisodeServers = { - sub: [], - dub: [], - raw: [], - episodeId, - episodeNo: 0, - }; - - try { - const epId = episodeId.split("?ep=")[1]; - - const { data } = await axios.get( - `${SRC_AJAX_URL}/v2/episode/servers?episodeId=${epId}`, - { - headers: { - Accept: ACCEPT_HEADER, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Referer: new URL(`/watch/${episodeId}`, SRC_BASE_URL).href, - }, - } - ); - - const $: CheerioAPI = load(data.html); - - const epNoSelector: SelectorType = ".server-notice strong"; - res.episodeNo = Number($(epNoSelector).text().split(" ").pop()) || 0; - - $(`.ps_-block.ps_-block-sub.servers-sub .ps__-list .server-item`).each( - (_, el) => { - res.sub.push({ - serverName: $(el).find("a").text().toLowerCase().trim(), - serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, - }); - } - ); - - $(`.ps_-block.ps_-block-sub.servers-dub .ps__-list .server-item`).each( - (_, el) => { - res.dub.push({ - serverName: $(el).find("a").text().toLowerCase().trim(), - serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, - }); - } - ); - - $(`.ps_-block.ps_-block-sub.servers-raw .ps__-list .server-item`).each( - (_, el) => { - res.raw.push({ - serverName: $(el).find("a").text().toLowerCase().trim(), - serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, - }); - } - ); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeEpisodeServers; diff --git a/src/parsers/estimatedSchedule.ts b/src/parsers/estimatedSchedule.ts deleted file mode 100644 index abfce97..0000000 --- a/src/parsers/estimatedSchedule.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - SRC_HOME_URL, - SRC_AJAX_URL, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} 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 ScrapedEstimatedSchedule } from "../types/parsers/index.js"; - -// /anime/schedule?date=${date} -async function scrapeEstimatedSchedule( - date: string -): Promise { - const res: ScrapedEstimatedSchedule = { - scheduledAnimes: [], - }; - - try { - const estScheduleURL = - `${SRC_AJAX_URL}/schedule/list?tzOffset=-330&date=${date}` as const; - - const mainPage = await axios.get(estScheduleURL, { - headers: { - Accept: "*/*", - Referer: SRC_HOME_URL, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage?.data?.html); - - const selector: SelectorType = "li"; - - if ($(selector)?.text()?.trim()?.includes("No data to display")) { - return res; - } - - $(selector).each((_, el) => { - const airingTimestamp = new Date( - `${date}T${$(el)?.find("a .time")?.text()?.trim()}:00` - ).getTime(); - - res.scheduledAnimes.push({ - id: $(el)?.find("a")?.attr("href")?.slice(1)?.trim() || null, - time: $(el)?.find("a .time")?.text()?.trim() || null, - name: $(el)?.find("a .film-name.dynamic-name")?.text()?.trim() || null, - jname: - $(el) - ?.find("a .film-name.dynamic-name") - ?.attr("data-jname") - ?.trim() || null, - airingTimestamp, - secondsUntilAiring: Math.floor((airingTimestamp - Date.now()) / 1000), - episode: Number($(el).find("a .fd-play button").text().trim().split(" ")[1]) - }); - }); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeEstimatedSchedule; diff --git a/src/parsers/homePage.ts b/src/parsers/homePage.ts deleted file mode 100644 index cbdaef6..0000000 --- a/src/parsers/homePage.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { - SRC_HOME_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractTop10Animes, - extractAnimes, - extractMostPopularAnimes, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import type { ScrapedHomePage } from "../types/parsers/index.js"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; - -// /anime/home -async function scrapeHomePage(): Promise { - const res: ScrapedHomePage = { - spotlightAnimes: [], - trendingAnimes: [], - latestEpisodeAnimes: [], - topUpcomingAnimes: [], - top10Animes: { - today: [], - week: [], - month: [], - }, - topAiringAnimes: [], - mostPopularAnimes: [], - mostFavoriteAnimes: [], - latestCompletedAnimes: [], - genres: [], - }; - - try { - const mainPage = await axios.get(SRC_HOME_URL as string, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const spotlightSelector: SelectorType = - "#slider .swiper-wrapper .swiper-slide"; - - $(spotlightSelector).each((i, el) => { - const otherInfo = $(el) - .find(".deslide-item-content .sc-detail .scd-item") - .map((i, el) => $(el).text().trim()) - .get() - .slice(0, -1); - - res.spotlightAnimes.push({ - rank: - Number( - $(el) - .find(".deslide-item-content .desi-sub-text") - ?.text() - .trim() - .split(" ")[0] - .slice(1) - ) || null, - id: $(el) - .find(".deslide-item-content .desi-buttons a") - ?.last() - ?.attr("href") - ?.slice(1) - ?.trim(), - name: $(el) - .find(".deslide-item-content .desi-head-title.dynamic-name") - ?.text() - .trim(), - description: $(el) - .find(".deslide-item-content .desi-description") - ?.text() - ?.split("[") - ?.shift() - ?.trim(), - poster: $(el) - .find(".deslide-cover .deslide-cover-img .film-poster-img") - ?.attr("data-src") - ?.trim(), - jname: $(el) - .find(".deslide-item-content .desi-head-title.dynamic-name") - ?.attr("data-jname") - ?.trim(), - episodes: { - sub: - Number( - $(el) - .find( - ".deslide-item-content .sc-detail .scd-item .tick-item.tick-sub" - ) - ?.text() - ?.trim() - ) || null, - dub: - Number( - $(el) - .find( - ".deslide-item-content .sc-detail .scd-item .tick-item.tick-dub" - ) - ?.text() - ?.trim() - ) || null, - }, - otherInfo, - }); - }); - - const trendingSelector: SelectorType = - "#trending-home .swiper-wrapper .swiper-slide"; - - $(trendingSelector).each((i, el) => { - res.trendingAnimes.push({ - rank: parseInt( - $(el).find(".item .number")?.children()?.first()?.text()?.trim() - ), - id: $(el).find(".item .film-poster")?.attr("href")?.slice(1)?.trim(), - name: $(el) - .find(".item .number .film-title.dynamic-name") - ?.text() - ?.trim(), - jname: $(el) - .find(".item .number .film-title.dynamic-name") - ?.attr("data-jname") - ?.trim(), - poster: $(el) - .find(".item .film-poster .film-poster-img") - ?.attr("data-src") - ?.trim(), - }); - }); - - const latestEpisodeSelector: SelectorType = - "#main-content .block_area_home:nth-of-type(1) .tab-content .film_list-wrap .flw-item"; - res.latestEpisodeAnimes = extractAnimes($, latestEpisodeSelector); - - const topUpcomingSelector: SelectorType = - "#main-content .block_area_home:nth-of-type(3) .tab-content .film_list-wrap .flw-item"; - res.topUpcomingAnimes = extractAnimes($, topUpcomingSelector); - - const genreSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; - $(genreSelector).each((i, el) => { - res.genres.push(`${$(el).text().trim()}`); - }); - - const mostViewedSelector: SelectorType = - '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; - $(mostViewedSelector).each((i, el) => { - const period = $(el).attr("id")?.split("-")?.pop()?.trim(); - - if (period === "day") { - res.top10Animes.today = extractTop10Animes($, period); - return; - } - if (period === "week") { - res.top10Animes.week = extractTop10Animes($, period); - return; - } - if (period === "month") { - res.top10Animes.month = extractTop10Animes($, period); - } - }); - - res.topAiringAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(1) .anif-block-ul ul li"); - res.mostPopularAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(2) .anif-block-ul ul li"); - res.mostFavoriteAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(3) .anif-block-ul ul li"); - res.latestCompletedAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(4) .anif-block-ul ul li"); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeHomePage; diff --git a/src/parsers/index.ts b/src/parsers/index.ts deleted file mode 100644 index 395833a..0000000 --- a/src/parsers/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import scrapeHomePage from "./homePage.js"; -import scrapeGenreAnime from "./animeGenre.js"; -import scrapeAnimeSearch from "./animeSearch.js"; -import scrapeAnimeEpisodes from "./animeEpisodes.js"; -import scrapeAnimeCategory from "./animeCategory.js"; -import scrapeProducerAnimes from "./animeProducer.js"; -import scrapeEpisodeServers from "./episodeServers.js"; -import scrapeAnimeAboutInfo from "./animeAboutInfo.js"; -import scrapeEstimatedSchedule from "./estimatedSchedule.js"; -import scrapeAnimeEpisodeSources from "./animeEpisodeSrcs.js"; -import scrapeAnimeSearchSuggestion from "./animeSearchSuggestion.js"; - -export { - scrapeHomePage, - scrapeGenreAnime, - scrapeAnimeSearch, - scrapeAnimeEpisodes, - scrapeAnimeCategory, - scrapeEpisodeServers, - scrapeProducerAnimes, - scrapeAnimeAboutInfo, - scrapeEstimatedSchedule, - scrapeAnimeEpisodeSources, - scrapeAnimeSearchSuggestion, -}; diff --git a/src/routes/hianime.ts b/src/routes/hianime.ts new file mode 100644 index 0000000..f4e1ff9 --- /dev/null +++ b/src/routes/hianime.ts @@ -0,0 +1,125 @@ +import { Hono } from "hono"; +import { HiAnime } from "aniwatch"; + +const hianime = new HiAnime.Scraper(); +const hianimeRouter = new Hono(); + +// /api/v2/hianime +hianimeRouter.get("/", (c) => c.redirect("/", 301)); + +// /api/v2/hianime/home +hianimeRouter.get("/home", async (c) => { + const data = await hianime.getHomePage(); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/category/{name}?page={page} +hianimeRouter.get("/category/:name", async (c) => { + const categoryName = decodeURIComponent( + c.req.param("name").trim() + ) as HiAnime.AnimeCategories; + + const page: number = + Number(decodeURIComponent(c.req.query("page") || "")) || 1; + + const data = await hianime.getCategoryAnime(categoryName, page); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/genre/{name}?page={page} +hianimeRouter.get("/genre/:name", async (c) => { + const genreName = decodeURIComponent(c.req.param("name").trim()); + const page: number = + Number(decodeURIComponent(c.req.query("page") || "")) || 1; + + const data = await hianime.getGenreAnime(genreName, page); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/producer/{name}?page={page} +hianimeRouter.get("/producer/:name", async (c) => { + const producerName = decodeURIComponent(c.req.param("name").trim()); + const page: number = + Number(decodeURIComponent(c.req.query("page") || "")) || 1; + + const data = await hianime.getProducerAnimes(producerName, page); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/schedule?date={date} +hianimeRouter.get("/schedule", async (c) => { + const date = decodeURIComponent(c.req.query("date") || ""); + + const data = await hianime.getEstimatedSchedule(date); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/search?q={query}&page={page}&filters={...filters} +hianimeRouter.get("/search", async (c) => { + let { q: query, page, ...filters } = c.req.query(); + + query = decodeURIComponent(query || ""); + const pageNo = Number(decodeURIComponent(page || "")) || 1; + + const data = await hianime.search(query, pageNo, filters); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/search/suggestion?q={query} +hianimeRouter.get("/search/suggestion", async (c) => { + const query = decodeURIComponent(c.req.query("q") || ""); + + const data = await hianime.searchSuggestions(query); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/anime/{animeId} +hianimeRouter.get("/anime/:animeId", async (c) => { + const animeId = decodeURIComponent(c.req.param("animeId").trim()); + const data = await hianime.getInfo(animeId); + + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/episode/servers?animeEpisodeId={id} +hianimeRouter.get("/episode/servers", async (c) => { + const animeEpisodeId = decodeURIComponent( + c.req.query("animeEpisodeId") || "" + ); + + const data = await hianime.getEpisodeServers(animeEpisodeId); + return c.json({ success: true, data }, { status: 200 }); +}); + +// episodeId=steinsgate-3?ep=230 +// /api/v2/hianime/episode/sources?animeEpisodeId={episodeId}?server={server}&category={category (dub or sub)} +hianimeRouter.get("/episode/sources", async (c) => { + const animeEpisodeId = decodeURIComponent( + c.req.query("animeEpisodeId") || "" + ); + const server = decodeURIComponent( + c.req.query("server") || HiAnime.Servers.VidStreaming + ) as HiAnime.AnimeServers; + + const category = decodeURIComponent(c.req.query("category") || "sub") as + | "sub" + | "dub" + | "raw"; + + const data = await hianime.getEpisodeSources( + animeEpisodeId, + server, + category + ); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/anime/{anime-id}/episodes +hianimeRouter.get("/anime/:animeId/episodes", async (c) => { + const animeId = decodeURIComponent(c.req.param("animeId").trim()); + const data = await hianime.getEpisodes(animeId); + + return c.json({ success: true, data }, { status: 200 }); +}); + +export { hianimeRouter }; diff --git a/src/routes/index.ts b/src/routes/index.ts deleted file mode 100644 index 7046ae1..0000000 --- a/src/routes/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Router, type IRouter } from "express"; -import { - getGenreAnime, - getAnimeSearch, - getHomePageInfo, - getAnimeCategory, - getAnimeEpisodes, - getEpisodeServers, - getProducerAnimes, - getAnimeAboutInfo, - getEstimatedSchedule, - getAnimeEpisodeSources, - getAnimeSearchSuggestion, -} from "../controllers/index.js"; - -const router: IRouter = Router(); - -// /anime -router.get("/", (_, res) => res.redirect("/")); - -// /anime/home -router.get("/home", getHomePageInfo); - -// /anime/info?id=${anime-id} -router.get("/info", getAnimeAboutInfo); - -// /anime/genre/${name}?page=${page} -router.get("/genre/:name", getGenreAnime); - -// /anime/search?q=${query}&page=${page} -router.get("/search", getAnimeSearch); - -// /anime/search/suggest?q=${query} -router.get("/search/suggest", getAnimeSearchSuggestion); - -// /anime/episodes/${anime-id} -router.get("/episodes/:animeId", getAnimeEpisodes); - -// /anime/servers?episodeId=${id} -router.get("/servers", getEpisodeServers); - -// episodeId=steinsgate-3?ep=230 -// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)} -router.get("/episode-srcs", getAnimeEpisodeSources); - -// /anime/schedule?date=${date} -router.get("/schedule", getEstimatedSchedule); - -// /anime/producer/${name}?page=${page} -router.get("/producer/:name", getProducerAnimes); - -// /anime/:category?page=${page} -router.get("/:category", getAnimeCategory); - -export default router; diff --git a/src/server.ts b/src/server.ts index 3520958..6a9c06e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,59 +1,85 @@ import https from "https"; -import morgan from "morgan"; -import express from "express"; -import { resolve } from "path"; import { config } from "dotenv"; import corsConfig from "./config/cors.js"; import { ratelimit } from "./config/ratelimit.js"; -import errorHandler from "./config/errorHandler.js"; -import notFoundHandler from "./config/notFoundHandler.js"; -import animeRouter from "./routes/index.js"; +import { hianimeRouter } from "./routes/hianime.js"; + +import { serveStatic } from "@hono/node-server/serve-static"; +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { logger } from "hono/logger"; + +import { HiAnimeError } from "aniwatch"; config(); -const app: express.Application = express(); -const PORT: number = Number(process.env.PORT) || 4000; -app.use(morgan("dev")); +const BASE_PATH = "/api/v2" as const; +const PORT: number = Number(process.env.ANIWATCH_API_PORT) || 4000; +const ANIWATCH_API_HOSTNAME = process.env?.ANIWATCH_API_HOSTNAME; + +const app = new Hono(); + +app.use(logger()); app.use(corsConfig); // CAUTION: For personal deployments, "refrain" from having an env // named "ANIWATCH_API_HOSTNAME". You may face rate limitting -// and other issues if you do. -const ISNT_PERSONAL_DEPLOYMENT = Boolean(process?.env?.ANIWATCH_API_HOSTNAME); +// or other issues if you do. +const ISNT_PERSONAL_DEPLOYMENT = Boolean(ANIWATCH_API_HOSTNAME); if (ISNT_PERSONAL_DEPLOYMENT) { app.use(ratelimit); } -app.use(express.static(resolve("public"))); -app.get("/health", (_, res) => res.sendStatus(200)); -app.use("/anime", animeRouter); +app.use("/", serveStatic({ root: "public" })); +app.get("/health", (c) => c.text("OK", { status: 200 })); + +app.basePath(BASE_PATH).route("/hianime", hianimeRouter); +app + .basePath(BASE_PATH) + .get("/anicrush", (c) => c.text("Anicrush could be implemented in future.")); -app.use(notFoundHandler); -app.use(errorHandler); +app.notFound((c) => + c.json({ status: 404, message: "Resource Not Found" }, 404) +); + +app.onError((err, c) => { + console.error(err); + const res = { status: 500, message: "Internal Server Error" }; + + if (err instanceof HiAnimeError) { + res.status = err.status; + res.message = err.message; + } + + return c.json(res, { status: res.status }); +}); // NOTE: this env is "required" for vercel deployments -if (!Boolean(process?.env?.IS_VERCEL_DEPLOYMENT)) { - app.listen(PORT, () => { - console.log(`⚔️ api @ http://localhost:${PORT}`); - }); +if (!Boolean(process?.env?.ANIWATCH_API_VERCEL_DEPLOYMENT)) { + serve({ + port: PORT, + fetch: app.fetch, + }).addListener("listening", () => + console.info( + "\x1b[1;36m" + `aniwatch-api at http://localhost:${PORT}` + "\x1b[0m" + ) + ); // NOTE: remove the `if` block below for personal deployments if (ISNT_PERSONAL_DEPLOYMENT) { + const interval = 9 * 60 * 1000; // 9mins + // don't sleep - const intervalTime = 9 * 60 * 1000; // 9mins setInterval(() => { - console.log("HEALTHCHECK ;)", new Date().toLocaleString()); + console.log("aniwatch-api HEALTH_CHECK at", new Date().toISOString()); https - .get( - new URL("/health", `https://${process.env.ANIWATCH_API_HOSTNAME}`) - .href - ) + .get(`https://${ANIWATCH_API_HOSTNAME}/health`) .on("error", (err) => { console.error(err.message); }); - }, intervalTime); + }, interval); } } diff --git a/src/types/anime.ts b/src/types/anime.ts deleted file mode 100644 index 83a3d1f..0000000 --- a/src/types/anime.ts +++ /dev/null @@ -1,140 +0,0 @@ -export interface Anime { - id: string | null; - name: string | null; - jname: string | null; - poster: string | null; - duration: string | null; - type: string | null; - rating: string | null; - episodes: { - sub: number | null; - dub: number | null; - }; -} - -type CommonAnimeProps = "id" | "name" | "poster"; - -export interface Top10Anime extends Pick { - rank: number | null; - jname: string | null; -} - -export type Top10AnimeTimePeriod = "day" | "week" | "month"; - -export interface MostPopularAnime - extends Pick { - jname: string | null; -} - -export interface SpotlightAnime - extends MostPopularAnime, - Pick { - description: string | null; -} - -export interface TrendingAnime - extends Pick, - Pick {} - -export interface LatestEpisodeAnime extends Anime {} - -export interface TopUpcomingAnime extends Anime {} - -export interface TopAiringAnime extends MostPopularAnime {} -export interface MostFavoriteAnime extends MostPopularAnime {} -export interface LatestCompletedAnime extends MostPopularAnime {} - -export interface AnimeGeneralAboutInfo - extends Pick, - Pick { - anilistId: number | null; - malId: number | null; - stats: { - quality: string | null; - } & Pick; - promotionalVideos: AnimePromotionalVideo[]; - charactersVoiceActors: AnimeCharactersAndVoiceActors[]; -} - -export interface RecommendedAnime extends Anime {} - -export interface RelatedAnime extends MostPopularAnime {} - -export interface Season extends Pick { - isCurrent: boolean; - title: string | null; -} - -export interface AnimePromotionalVideo { - title: string | undefined; - source: string | undefined; - thumbnail: string | undefined; -} - -export interface AnimeCharactersAndVoiceActors { - character: AnimeCharacter; - voiceActor: AnimeCharacter; -} - -export interface AnimeCharacter { - id: string; - poster: string; - name: string; - cast: string; -} - -export interface AnimeSearchSuggestion - extends Omit { - moreInfo: Array; -} - -export interface AnimeEpisode extends Pick { - episodeId: string | null; - number: number; - isFiller: boolean; -} - -export interface SubEpisode { - serverName: string; - serverId: number | null; -} -export interface DubEpisode extends SubEpisode {} -export interface RawEpisode extends SubEpisode {} - -export type AnimeCategories = - | "most-favorite" - | "most-popular" - | "subbed-anime" - | "dubbed-anime" - | "recently-updated" - | "recently-added" - | "top-upcoming" - | "top-airing" - | "movie" - | "special" - | "ova" - | "ona" - | "tv" - | "completed"; - -export type AnimeServers = - | "hd-1" - | "hd-2" - | "megacloud" - | "streamsb" - | "streamtape"; - -export enum Servers { - VidStreaming = "hd-1", - MegaCloud = "megacloud", - StreamSB = "streamsb", - StreamTape = "streamtape", - VidCloud = "hd-2", - AsianLoad = "asianload", - GogoCDN = "gogocdn", - MixDrop = "mixdrop", - UpCloud = "upcloud", - VizCloud = "vizcloud", - MyCloud = "mycloud", - Filemoon = "filemoon", -} diff --git a/src/types/controllers/animeAboutInfo.ts b/src/types/controllers/animeAboutInfo.ts deleted file mode 100644 index aa65efd..0000000 --- a/src/types/controllers/animeAboutInfo.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AnimeAboutInfoQueryParams = { - id?: string; -}; diff --git a/src/types/controllers/animeCategory.ts b/src/types/controllers/animeCategory.ts deleted file mode 100644 index 7de0876..0000000 --- a/src/types/controllers/animeCategory.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type CategoryAnimePathParams = { - category?: string; -}; - -export type CategoryAnimeQueryParams = { - page?: string; -}; diff --git a/src/types/controllers/animeEpisodeSrcs.ts b/src/types/controllers/animeEpisodeSrcs.ts deleted file mode 100644 index 0e82539..0000000 --- a/src/types/controllers/animeEpisodeSrcs.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type AnimeServers } from "../anime.js"; - -export type AnimeEpisodeSrcsQueryParams = { - id?: string; - server?: AnimeServers; - category?: "sub" | "dub"; -}; diff --git a/src/types/controllers/animeEpisodes.ts b/src/types/controllers/animeEpisodes.ts deleted file mode 100644 index 481eb87..0000000 --- a/src/types/controllers/animeEpisodes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AnimeEpisodePathParams = { - animeId?: string; -}; diff --git a/src/types/controllers/animeGenre.ts b/src/types/controllers/animeGenre.ts deleted file mode 100644 index 038d15e..0000000 --- a/src/types/controllers/animeGenre.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type GenreAnimePathParams = { - name?: string; -}; - -export type GenreAnimeQueryParams = { - page?: string; -}; diff --git a/src/types/controllers/animeProducer.ts b/src/types/controllers/animeProducer.ts deleted file mode 100644 index 7f8a9a7..0000000 --- a/src/types/controllers/animeProducer.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type AnimeProducerPathParams = { - name?: string; -}; - -export type AnimeProducerQueryParams = { - page?: string; -}; diff --git a/src/types/controllers/animeSearch.ts b/src/types/controllers/animeSearch.ts deleted file mode 100644 index fccb243..0000000 --- a/src/types/controllers/animeSearch.ts +++ /dev/null @@ -1,20 +0,0 @@ -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; - -export type FilterKeys = Partial< - keyof Omit ->; diff --git a/src/types/controllers/animeSearchSuggestion.ts b/src/types/controllers/animeSearchSuggestion.ts deleted file mode 100644 index 491daa0..0000000 --- a/src/types/controllers/animeSearchSuggestion.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AnimeSearchSuggestQueryParams = { - q?: string; -}; diff --git a/src/types/controllers/episodeServers.ts b/src/types/controllers/episodeServers.ts deleted file mode 100644 index d711a40..0000000 --- a/src/types/controllers/episodeServers.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type EpisodeServersQueryParams = { - episodeId?: string; -}; diff --git a/src/types/controllers/estimatedSchedule.ts b/src/types/controllers/estimatedSchedule.ts deleted file mode 100644 index e732aaa..0000000 --- a/src/types/controllers/estimatedSchedule.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type EstimatedScheduleQueryParams = { - date?: string; -}; diff --git a/src/types/controllers/index.ts b/src/types/controllers/index.ts deleted file mode 100644 index 14876e2..0000000 --- a/src/types/controllers/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -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/extractor.ts b/src/types/extractor.ts deleted file mode 100644 index 71c6be1..0000000 --- a/src/types/extractor.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Video { - url: string; - quality?: string; - isM3U8?: boolean; - size?: number; - [x: string]: unknown; -} - -export interface Subtitle { - id?: string; - url: string; - lang: string; -} - -export interface Intro { - start: number; - end: number; -} diff --git a/src/types/parsers/animeAboutInfo.ts b/src/types/parsers/animeAboutInfo.ts deleted file mode 100644 index 274e7c2..0000000 --- a/src/types/parsers/animeAboutInfo.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { - Season, - RelatedAnime, - RecommendedAnime, - AnimeGeneralAboutInfo, -} from "../anime.js"; -import { type HttpError } from "http-errors"; -import { type ScrapedAnimeSearchResult } from "./animeSearch.js"; - -export interface ScrapedAnimeAboutInfo - extends Pick { - anime: { - info: AnimeGeneralAboutInfo; - moreInfo: Record; - }; - seasons: Array; - relatedAnimes: Array | HttpError; - recommendedAnimes: Array | HttpError; -} diff --git a/src/types/parsers/animeCategory.ts b/src/types/parsers/animeCategory.ts deleted file mode 100644 index 89c56f0..0000000 --- a/src/types/parsers/animeCategory.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { HttpError } from "http-errors"; -import type { Anime, Top10Anime } from "../anime.js"; - -export interface ScrapedAnimeCategory { - animes: Array | HttpError; - genres: Array; - top10Animes: { - today: Array | HttpError; - week: Array | HttpError; - month: Array | HttpError; - }; - category: string; - totalPages: number; - currentPage: number; - hasNextPage: boolean; -} - -export type CommonAnimeScrapeTypes = - | "animes" - | "totalPages" - | "hasNextPage" - | "currentPage"; diff --git a/src/types/parsers/animeEpisodeSrcs.ts b/src/types/parsers/animeEpisodeSrcs.ts deleted file mode 100644 index ef58ed3..0000000 --- a/src/types/parsers/animeEpisodeSrcs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Intro, Subtitle, Video } from "../extractor.js"; - -export interface ScrapedAnimeEpisodesSources { - headers?: { - [k: string]: string; - }; - intro?: Intro; - subtitles?: Subtitle[]; - sources: Video[]; - download?: string; - embedURL?: string; -} diff --git a/src/types/parsers/animeEpisodes.ts b/src/types/parsers/animeEpisodes.ts deleted file mode 100644 index 3587573..0000000 --- a/src/types/parsers/animeEpisodes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type AnimeEpisode } from "../anime.js"; - -export interface ScrapedAnimeEpisodes { - totalEpisodes: number; - episodes: Array; -} diff --git a/src/types/parsers/animeGenre.ts b/src/types/parsers/animeGenre.ts deleted file mode 100644 index 10f8b85..0000000 --- a/src/types/parsers/animeGenre.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { - ScrapedAnimeCategory, - CommonAnimeScrapeTypes, -} from "./animeCategory.js"; -import { type ScrapedHomePage } from "./homePage.js"; - -export interface ScrapedGenreAnime - extends Pick, - Pick { - genreName: string; -} diff --git a/src/types/parsers/animeProducer.ts b/src/types/parsers/animeProducer.ts deleted file mode 100644 index bcd784e..0000000 --- a/src/types/parsers/animeProducer.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ScrapedHomePage } from "./homePage.js"; -import type { ScrapedAnimeCategory } from "./animeCategory.js"; - -export interface ScrapedProducerAnime - extends Omit, - Pick { - producerName: string; -} diff --git a/src/types/parsers/animeSearch.ts b/src/types/parsers/animeSearch.ts deleted file mode 100644 index 7e641ea..0000000 --- a/src/types/parsers/animeSearch.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { - ScrapedAnimeCategory, - CommonAnimeScrapeTypes, -} 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 { - mostPopularAnimes: Array | HttpError; - searchQuery: string; - searchFilters: SearchFilters; -} diff --git a/src/types/parsers/animeSearchSuggestion.ts b/src/types/parsers/animeSearchSuggestion.ts deleted file mode 100644 index 4c262a4..0000000 --- a/src/types/parsers/animeSearchSuggestion.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { HttpError } from "http-errors"; -import type { AnimeSearchSuggestion } from "../anime.js"; - -export interface ScrapedAnimeSearchSuggestion { - suggestions: Array | HttpError; -} diff --git a/src/types/parsers/episodeServers.ts b/src/types/parsers/episodeServers.ts deleted file mode 100644 index a003149..0000000 --- a/src/types/parsers/episodeServers.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { SubEpisode, DubEpisode, RawEpisode } from "../anime.js"; - -export interface ScrapedEpisodeServers { - sub: SubEpisode[]; - dub: DubEpisode[]; - raw: RawEpisode[]; - episodeNo: number; - episodeId: string; -} diff --git a/src/types/parsers/estimatedSchedule.ts b/src/types/parsers/estimatedSchedule.ts deleted file mode 100644 index 881be86..0000000 --- a/src/types/parsers/estimatedSchedule.ts +++ /dev/null @@ -1,13 +0,0 @@ -type EstimatedSchedule = { - id: string | null; - time: string | null; - name: string | null; - jname: string | null; - airingTimestamp: number; - secondsUntilAiring: number; - episode: number; -}; - -export type ScrapedEstimatedSchedule = { - scheduledAnimes: Array; -}; diff --git a/src/types/parsers/homePage.ts b/src/types/parsers/homePage.ts deleted file mode 100644 index 23f6284..0000000 --- a/src/types/parsers/homePage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { - TrendingAnime, - SpotlightAnime, - TopAiringAnime, - TopUpcomingAnime, - LatestEpisodeAnime, - MostFavoriteAnime, - MostPopularAnime, - LatestCompletedAnime, -} from "../anime.js"; -import type { HttpError } from "http-errors"; -import type { ScrapedAnimeCategory } from "./animeCategory.js"; - -export interface ScrapedHomePage - extends Pick { - spotlightAnimes: Array | HttpError; - trendingAnimes: Array | HttpError; - latestEpisodeAnimes: Array | HttpError; - topUpcomingAnimes: Array | HttpError; - topAiringAnimes: Array | HttpError; - mostPopularAnimes: Array | HttpError; - mostFavoriteAnimes: Array | HttpError; - latestCompletedAnimes: Array | HttpError; -} diff --git a/src/types/parsers/index.ts b/src/types/parsers/index.ts deleted file mode 100644 index 057e2fc..0000000 --- a/src/types/parsers/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ScrapedHomePage } from "./homePage.js"; -import type { ScrapedGenreAnime } from "./animeGenre.js"; -import type { ScrapedAnimeEpisodes } from "./animeEpisodes.js"; -import type { ScrapedAnimeCategory } from "./animeCategory.js"; -import type { ScrapedProducerAnime } from "./animeProducer.js"; -import type { ScrapedEpisodeServers } from "./episodeServers.js"; -import type { ScrapedAnimeAboutInfo } from "./animeAboutInfo.js"; -import type { ScrapedAnimeSearchResult } from "./animeSearch.js"; -import type { ScrapedEstimatedSchedule } from "./estimatedSchedule.js"; -import type { ScrapedAnimeEpisodesSources } from "./animeEpisodeSrcs.js"; -import type { ScrapedAnimeSearchSuggestion } from "./animeSearchSuggestion.js"; - -export type { - ScrapedHomePage, - ScrapedGenreAnime, - ScrapedAnimeEpisodes, - ScrapedProducerAnime, - ScrapedAnimeCategory, - ScrapedEpisodeServers, - ScrapedAnimeAboutInfo, - ScrapedAnimeSearchResult, - ScrapedEstimatedSchedule, - ScrapedAnimeEpisodesSources, - ScrapedAnimeSearchSuggestion, -}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts deleted file mode 100644 index de7617f..0000000 --- a/src/utils/constants.ts +++ /dev/null @@ -1,129 +0,0 @@ -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"; - -// previously aniwatch.to || aniwatchtv.to -const DOMAIN = process.env.DOMAIN || "hianime.to"; - -export const SRC_BASE_URL = `https://${DOMAIN}` as const; -export const SRC_AJAX_URL = `${SRC_BASE_URL}/ajax` as const; -export const SRC_HOME_URL = `${SRC_BASE_URL}/home` as const; -export const SRC_SEARCH_URL = `${SRC_BASE_URL}/search` as const; - -// -export const genresIdMap: Record = { - 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 = { - all: 0, - movie: 1, - tv: 2, - ova: 3, - ona: 4, - special: 5, - music: 6, -} as const; - -export const statusIdMap: Record = { - all: 0, - "finished-airing": 1, - "currently-airing": 2, - "not-yet-aired": 3, -} as const; - -export const ratedIdMap: Record = { - all: 0, - g: 1, - pg: 2, - "pg-13": 3, - r: 4, - "r+": 5, - rx: 6, -} as const; - -export const scoreIdMap: Record = { - 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 = { - all: 0, - spring: 1, - summer: 2, - fall: 3, - winter: 4, -} as const; - -export const languageIdMap: Record = { - all: 0, - sub: 1, - dub: 2, - "sub-&-dub": 3, -} as const; - -export const sortIdMap: Record = { - 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; -// diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index b514c85..0000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./methods.js"; -export * from "./constants.js"; diff --git a/src/utils/methods.ts b/src/utils/methods.ts deleted file mode 100644 index 6aed9f7..0000000 --- a/src/utils/methods.ts +++ /dev/null @@ -1,297 +0,0 @@ -import type { - Anime, - Top10Anime, - MostPopularAnime, - 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 = ( - $: CheerioAPI, - selector: SelectorType -): Array | HttpError => { - try { - const animes: Array = []; - - $(selector).each((i, el) => { - const animeId = - $(el) - .find(".film-detail .film-name .dynamic-name") - ?.attr("href") - ?.slice(1) - .split("?ref=search")[0] || null; - - animes.push({ - id: animeId, - name: $(el) - .find(".film-detail .film-name .dynamic-name") - ?.text() - ?.trim(), - jname: - $(el) - .find(".film-detail .film-name .dynamic-name") - ?.attr("data-jname") - ?.trim() || null, - poster: - $(el) - .find(".film-poster .film-poster-img") - ?.attr("data-src") - ?.trim() || null, - duration: $(el) - .find(".film-detail .fd-infor .fdi-item.fdi-duration") - ?.text() - ?.trim(), - type: $(el) - .find(".film-detail .fd-infor .fdi-item:nth-of-type(1)") - ?.text() - ?.trim(), - rating: $(el).find(".film-poster .tick-rate")?.text()?.trim() || null, - episodes: { - sub: - Number( - $(el) - .find(".film-poster .tick-sub") - ?.text() - ?.trim() - .split(" ") - .pop() - ) || null, - dub: - Number( - $(el) - .find(".film-poster .tick-dub") - ?.text() - ?.trim() - .split(" ") - .pop() - ) || null, - }, - }); - }); - - return animes; - } catch (err: any) { - throw createHttpError.InternalServerError( - err?.message || "Something went wrong" - ); - } -}; - -export const extractTop10Animes = ( - $: CheerioAPI, - period: Top10AnimeTimePeriod -): Array | HttpError => { - try { - const animes: Array = []; - const selector = `#top-viewed-${period} ul li`; - - $(selector).each((i, el) => { - animes.push({ - id: - $(el) - .find(".film-detail .dynamic-name") - ?.attr("href") - ?.slice(1) - .trim() || null, - rank: Number($(el).find(".film-number span")?.text()?.trim()) || null, - name: $(el).find(".film-detail .dynamic-name")?.text()?.trim() || null, - jname: - $(el) - .find(".film-detail .dynamic-name") - ?.attr("data-jname") - ?.trim() || null, - poster: - $(el) - .find(".film-poster .film-poster-img") - ?.attr("data-src") - ?.trim() || null, - episodes: { - sub: - Number( - $(el) - .find(".film-detail .fd-infor .tick-item.tick-sub") - ?.text() - ?.trim() - ) || null, - dub: - Number( - $(el) - .find(".film-detail .fd-infor .tick-item.tick-dub") - ?.text() - ?.trim() - ) || null, - }, - }); - }); - - return animes; - } catch (err: any) { - throw createHttpError.InternalServerError( - err?.message || "Something went wrong" - ); - } -}; - -export const extractMostPopularAnimes = ( - $: CheerioAPI, - selector: SelectorType -): Array | HttpError => { - try { - const animes: Array = []; - - $(selector).each((i, el) => { - animes.push({ - id: - $(el) - .find(".film-detail .dynamic-name") - ?.attr("href") - ?.slice(1) - .trim() || null, - name: $(el).find(".film-detail .dynamic-name")?.text()?.trim() || null, - jname: - $(el) - .find(".film-detail .film-name .dynamic-name") - .attr("data-jname") - ?.trim() || null, - poster: - $(el) - .find(".film-poster .film-poster-img") - ?.attr("data-src") - ?.trim() || null, - episodes: { - sub: - Number($(el)?.find(".fd-infor .tick .tick-sub")?.text()?.trim()) || - null, - dub: - Number($(el)?.find(".fd-infor .tick .tick-dub")?.text()?.trim()) || - null, - }, - type: - $(el) - ?.find(".fd-infor .tick") - ?.text() - ?.trim() - ?.replace(/[\s\n]+/g, " ") - ?.split(" ") - ?.pop() || null, - }); - }); - - return animes; - } catch (err: any) { - throw createHttpError.InternalServerError( - err?.message || "Something went wrong" - ); - } -}; - -export function retrieveServerId( - $: CheerioAPI, - index: number, - category: "sub" | "dub" | "raw" -) { - return ( - $(`.ps_-block.ps_-block-sub.servers-${category} > .ps__-list .server-item`) - ?.map((_, el) => - $(el).attr("data-server-id") == `${index}` ? $(el) : null - ) - ?.get()[0] - ?.attr("data-id") || null - ); -} - -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); -} - -export function substringBefore(str: string, toFind: string) { - const index = str.indexOf(toFind); - return index == -1 ? "" : str.substring(0, index); -} -- cgit v1.2.3