├── public └── .gitkeep ├── api └── index.ts ├── bun.lockb ├── vercel.json ├── src ├── types │ ├── aniwatch │ │ ├── root.ts │ │ ├── episodes.ts │ │ ├── servers.ts │ │ ├── search.ts │ │ ├── episode_server_source.ts │ │ ├── category.ts │ │ ├── about.ts │ │ ├── home.ts │ │ └── anime.ts │ └── gogoanime │ │ ├── episodes.ts │ │ ├── about.ts │ │ ├── search.ts │ │ ├── home.ts │ │ └── anime.ts ├── lib │ ├── isSiteReachable.ts │ └── getRoot.ts ├── utils │ ├── aniwatch │ │ ├── serverSubString.ts │ │ ├── video-extractor.ts │ │ ├── streamtape.ts │ │ ├── proxy.ts │ │ ├── constants.ts │ │ ├── streamsb.ts │ │ ├── rapidcloud.ts │ │ └── megacloud.ts │ └── gogoanime │ │ └── constants.ts ├── middlewares │ ├── rateLimit.ts │ └── cache.ts ├── config │ ├── headers.ts │ └── websites.ts ├── server.ts ├── controllers │ ├── aniwatch │ │ ├── homeController.ts │ │ ├── aboutController.ts │ │ ├── atozController.ts │ │ ├── categoryController.ts │ │ ├── controllers.ts │ │ ├── episodesController.ts │ │ ├── episodeServersController.ts │ │ ├── searchController.ts │ │ └── episodeServerSourcesController.ts │ └── gogoanime │ │ ├── homeController.ts │ │ ├── aboutController.ts │ │ ├── anime.episodes.ts │ │ ├── topAiring.controller.ts │ │ ├── newSeasons.controller.ts │ │ ├── animeMovies.controller.ts │ │ ├── popularAnimes.controller.ts │ │ ├── completedAnimes.controller.ts │ │ ├── recentReleases.controller.ts │ │ ├── controllers.ts │ │ └── searchController.ts ├── extracters │ ├── aniwatch │ │ ├── server_id.ts │ │ ├── atoz_animes.ts │ │ ├── category_animes.ts │ │ ├── searched_animes.ts │ │ ├── genre_list.ts │ │ ├── recommended_animes.ts │ │ ├── latest_anime_episodes.ts │ │ ├── episodes.ts │ │ ├── trending_animes.ts │ │ ├── extracters.ts │ │ ├── featured_animes.ts │ │ ├── anime_seasons_info.ts │ │ ├── about_extra_anime.ts │ │ ├── top_upcoming_animes.ts │ │ ├── top10_animes.ts │ │ ├── mostpopular_animes.ts │ │ ├── related_animes.ts │ │ ├── spotlight_animes.ts │ │ └── about_anime.ts │ └── gogoanime │ │ ├── extracters.ts │ │ ├── anime_movies.ts │ │ ├── popular_animes.ts │ │ ├── completed_animes.ts │ │ ├── episodes.ts │ │ ├── extract_recently_added_series_home.ts │ │ ├── new_seasons.ts │ │ ├── searched_animes.ts │ │ ├── top_airing.ts │ │ ├── extract_recent_released_home.ts │ │ ├── recent_released_episodes.ts │ │ └── about_anime.ts ├── scrapers │ ├── aniwatch │ │ ├── scrapers.ts │ │ ├── atozAnimes.ts │ │ ├── episodes.ts │ │ ├── servers.ts │ │ ├── search.ts │ │ ├── about.ts │ │ ├── category.ts │ │ ├── episodeServerSource.ts │ │ └── home.ts │ └── gogoanime │ │ ├── scrappers.ts │ │ ├── popular-animes.ts │ │ ├── completed-animes.ts │ │ ├── episodes.ts │ │ ├── new-seasons.ts │ │ ├── anime-movies.ts │ │ ├── top-airing.ts │ │ ├── recent-releases.ts │ │ ├── about.ts │ │ ├── search.ts │ │ └── home.ts └── routes │ ├── routes.ts │ ├── aniwatch │ └── routes.ts │ └── gogoanime │ └── routes.ts ├── nodemon.json ├── Dockerfile ├── package.json ├── tsconfig.json ├── .gitignore └── README.md /public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import app from "../src/server"; 2 | 3 | export default app; 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhaythakur71181/Anime-API/HEAD/bun.lockb -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/api" }] 3 | } 4 | -------------------------------------------------------------------------------- /src/types/aniwatch/root.ts: -------------------------------------------------------------------------------- 1 | export interface GetRoot { 2 | docs: string; 3 | sites: Record; 4 | } 5 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": ".ts,.js", 4 | "ignore": [], 5 | "exec": "npx ts-node ./src/server.ts" 6 | } 7 | -------------------------------------------------------------------------------- /src/types/gogoanime/episodes.ts: -------------------------------------------------------------------------------- 1 | import type { Episode } from "../goganime/anime"; 2 | 3 | export interface ScrapedEpisodes { 4 | episodes: Episode[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/aniwatch/episodes.ts: -------------------------------------------------------------------------------- 1 | import { Episode } from "./anime"; 2 | 3 | export interface ScrapedEpisodesPage { 4 | totalEpisodes: number; 5 | episodes: Episode[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/gogoanime/about.ts: -------------------------------------------------------------------------------- 1 | import { AboutAnimeInfo } from "./anime"; 2 | 3 | export interface ScrapedAboutPage { 4 | id: string; 5 | anime_id: String; 6 | info: AboutAnimeInfo; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/gogoanime/search.ts: -------------------------------------------------------------------------------- 1 | import { SearchedAnime } from "./anime"; 2 | 3 | export interface ScrapedSearchPage { 4 | animes: SearchedAnime[]; 5 | currentPage: number; 6 | hasNextPage: boolean; 7 | totalPages: number; 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:21-slim 2 | 3 | WORKDIR /ani 4 | 5 | COPY src /ani/src/ 6 | COPY package.json /ani/ 7 | COPY tsconfig.json /ani/ 8 | 9 | RUN npm install 10 | RUN npm run build 11 | 12 | CMD ["npm", "run", "start"] 13 | EXPOSE 3001 14 | -------------------------------------------------------------------------------- /src/types/gogoanime/home.ts: -------------------------------------------------------------------------------- 1 | import type { RecentRelease, Anime } from "./anime"; 2 | 3 | export interface ScrapedHomePage { 4 | genres: string[]; 5 | recentReleases: RecentRelease[]; 6 | recentlyAddedSeries: Anime[]; 7 | onGoingSeries: Anime[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/isSiteReachable.ts: -------------------------------------------------------------------------------- 1 | export const isSiteReachable = async (url: string): Promise => { 2 | try { 3 | const response = await fetch(url, { method: 'HEAD' }); 4 | return response.ok; 5 | } catch (error) { 6 | return false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/types/aniwatch/servers.ts: -------------------------------------------------------------------------------- 1 | import { SubEpisode, DubEpisode, RawEpisode } from "./anime"; 2 | 3 | export interface ScrapedEpisodeServer { 4 | episodeId: string; 5 | episodeNo: number; 6 | sub: SubEpisode[]; 7 | dub: DubEpisode[]; 8 | raw: RawEpisode[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/aniwatch/search.ts: -------------------------------------------------------------------------------- 1 | import { SearchedAnime, MostPopularAnime } from "./anime"; 2 | 3 | export interface ScrapedSearchPage { 4 | animes: SearchedAnime[]; 5 | mostPopularAnimes: MostPopularAnime[]; 6 | currentPage: number; 7 | hasNextPage: boolean; 8 | totalPages: number; 9 | genres: string[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/aniwatch/episode_server_source.ts: -------------------------------------------------------------------------------- 1 | import type { Intro, Subtitle, Video } from "./anime"; 2 | 3 | export interface ScrapedAnimeEpisodesSources { 4 | headers?: { 5 | [k: string]: string; 6 | }; 7 | intro?: Intro; 8 | subtitles?: Subtitle[]; 9 | sources: Video[]; 10 | download?: string; 11 | embedURL?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/aniwatch/serverSubString.ts: -------------------------------------------------------------------------------- 1 | export function substringAfter(str: string, toFind: string) { 2 | const index = str.indexOf(toFind); 3 | return index == -1 ? "" : str.substring(index + toFind.length); 4 | } 5 | 6 | export function substringBefore(str: string, toFind: string) { 7 | const index = str.indexOf(toFind); 8 | return index == -1 ? "" : str.substring(0, index); 9 | } 10 | -------------------------------------------------------------------------------- /src/types/aniwatch/category.ts: -------------------------------------------------------------------------------- 1 | import { Top10Anime, CategoryAnime } from "./anime"; 2 | 3 | export interface ScrapedCategoryPage { 4 | animes: CategoryAnime[]; 5 | top10Animes: { 6 | day: Top10Anime[]; 7 | week: Top10Anime[]; 8 | month: Top10Anime[]; 9 | }; 10 | category: string; 11 | genres: string[]; 12 | currentPage: number; 13 | hasNextPage: boolean; 14 | totalPages: number; 15 | } 16 | -------------------------------------------------------------------------------- /src/types/aniwatch/about.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AboutAnimeInfo, 3 | ExtraAboutAnimeInfo, 4 | AnimeSeasonsInfo, 5 | RelatedAnime, 6 | RecommendedAnime, 7 | MostPopularAnime, 8 | } from "./anime"; 9 | 10 | export interface ScrapedAboutPage { 11 | info: AboutAnimeInfo; 12 | moreInfo: ExtraAboutAnimeInfo; 13 | seasons: AnimeSeasonsInfo[]; 14 | relatedAnimes: RelatedAnime[]; 15 | recommendedAnimes: RecommendedAnime[]; 16 | mostPopularAnimes: MostPopularAnime[]; 17 | } 18 | -------------------------------------------------------------------------------- /src/middlewares/rateLimit.ts: -------------------------------------------------------------------------------- 1 | import rateLimit from "express-rate-limit"; 2 | 3 | export const limiter = rateLimit({ 4 | windowMs: 60 * 60 * 1000, // 60 minutes 5 | max: 50, // Limit each IP to 100 requests per `window` (here, per 15 minutes) 6 | standardHeaders: "draft-7", // Set `RateLimit` and `RateLimit-Policy`` headers 7 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 8 | message: 9 | "Too many accounts created from this IP, please try again after 1 hour", 10 | }); 11 | -------------------------------------------------------------------------------- /src/config/headers.ts: -------------------------------------------------------------------------------- 1 | type HeaderConfig = { 2 | "USER_AGENT_HEADER": string, 3 | "ACCEPT_ENCODEING_HEADER": string, 4 | "ACCEPT_HEADER": string 5 | } 6 | 7 | const headers: HeaderConfig = { 8 | USER_AGENT_HEADER: "Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0", 9 | ACCEPT_ENCODEING_HEADER: "gzip, deflate, br", 10 | ACCEPT_HEADER: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" 11 | } 12 | 13 | export { headers, HeaderConfig }; 14 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { config } from "dotenv"; 3 | import { limiter } from "./middlewares/rateLimit"; 4 | import { router } from "./routes/routes"; 5 | 6 | config(); // dotenv 7 | 8 | const app = express(); 9 | const PORT = process.env.PORT ?? 3001; 10 | 11 | //middlewares 12 | app.use(limiter); 13 | 14 | // router 15 | app.use("/", router); 16 | 17 | app.listen(PORT, () => { 18 | console.log(`⚔️ API started ON PORT : ${PORT} @ STARTED ⚔️`); 19 | }); 20 | 21 | export default app; 22 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/homeController.ts: -------------------------------------------------------------------------------- 1 | import { scrapeHomePage } from "../../scrapers/aniwatch/scrapers"; 2 | import type { RequestHandler } from "express"; 3 | 4 | const getHomePageInfo: RequestHandler = async (_req, res) => { 5 | try { 6 | const data = await scrapeHomePage(); 7 | res.status(200).json(data); 8 | } catch (err) { 9 | //////////////////////////////////// 10 | console.log(err); // for TESTING// 11 | //////////////////////////////////// 12 | } 13 | }; 14 | 15 | export { getHomePageInfo }; 16 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/homeController.ts: -------------------------------------------------------------------------------- 1 | import { scrapeHomePage } from "../../scrapers/gogoanime/scrappers"; 2 | import type { RequestHandler } from "express"; 3 | 4 | const getHomePageInfo: RequestHandler = async (_req, res) => { 5 | try { 6 | const data = await scrapeHomePage(); 7 | res.status(200).json(data); 8 | } catch (err) { 9 | //////////////////////////////////// 10 | console.log(err); // for TESTING// 11 | //////////////////////////////////// 12 | } 13 | }; 14 | 15 | export { getHomePageInfo }; 16 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/aboutController.ts: -------------------------------------------------------------------------------- 1 | import { scrapeAboutPage } from "../../scrapers/aniwatch/about"; 2 | import type { RequestHandler } from "express"; 3 | 4 | const getAboutPageInfo: RequestHandler = async (req, res) => { 5 | try { 6 | const data = await scrapeAboutPage(req.params.id); 7 | res.status(200).json(data); 8 | } catch (err) { 9 | //////////////////////////////////// 10 | console.log(err); // for TESTING// 11 | //////////////////////////////////// 12 | } 13 | }; 14 | 15 | export { getAboutPageInfo }; 16 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/aboutController.ts: -------------------------------------------------------------------------------- 1 | import { scrapeAboutPage } from "../../scrapers/gogoanime/about"; 2 | import type { RequestHandler } from "express"; 3 | 4 | const getAboutPageInfo: RequestHandler = async (req, res) => { 5 | try { 6 | const id: string = req.params.id; 7 | const data = await scrapeAboutPage(id); 8 | res.status(200).json(data); 9 | } catch (err) { 10 | //////////////////////////////////// 11 | console.log(err); // for TESTING// 12 | //////////////////////////////////// 13 | } 14 | }; 15 | 16 | export { getAboutPageInfo }; 17 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/anime.episodes.ts: -------------------------------------------------------------------------------- 1 | import { scrapeEpisodePage } from "../../scrapers/gogoanime/scrappers"; 2 | import type { RequestHandler } from "express"; 3 | 4 | const getAnimeEpisodes: RequestHandler = async (req, res) => { 5 | try { 6 | const id: string = req.params.id; 7 | const data = await scrapeEpisodePage(id); 8 | res.status(200).json(data); 9 | } catch (err) { 10 | //////////////////////////////////// 11 | console.log(err); // for TESTING// 12 | //////////////////////////////////// 13 | } 14 | }; 15 | 16 | export { getAnimeEpisodes }; 17 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/server_id.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI } from "cheerio"; 2 | 3 | export const extract_server_id = ( 4 | $: CheerioAPI, 5 | index: number, 6 | category: "sub" | "dub" | "raw", 7 | ) => { 8 | console.warn("DEBUGPRINT[2]: server_id.ts:5: index=", index); 9 | console.warn("DEBUGPRINT[3]: server_id.ts:6: category=", category); 10 | return ( 11 | $(`.ps_-block.ps_-block-sub.servers-${category} > .ps__-list .server-item`) 12 | ?.map((_, el) => 13 | $(el).attr("data-server-id") == `${index}` ? $(el) : null, 14 | ) 15 | ?.get()[0] 16 | ?.attr("data-id") || null 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/atozController.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { scrapeatozAnimes } from "../../scrapers/aniwatch/scrapers"; 3 | 4 | const getatozPage: RequestHandler = async (req, res) => { 5 | try { 6 | const page = req.query.page 7 | ? Number(decodeURIComponent(req.query?.page as string)) 8 | : 1; 9 | const data = await scrapeatozAnimes(page); 10 | res.status(200).json(data); 11 | } catch (err) { 12 | //////////////////////////////////// 13 | console.log(err); // for TESTING// 14 | //////////////////////////////////// 15 | } 16 | }; 17 | 18 | export { getatozPage }; 19 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/topAiring.controller.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { scrapeTopAiring } from "../../scrapers/gogoanime/scrappers"; 3 | 4 | const getTopAiring: RequestHandler = async (req, res) => { 5 | try { 6 | const page = req.query.page 7 | ? Number(decodeURIComponent(req.query?.page as string)) 8 | : 1; 9 | const data = await scrapeTopAiring(page); 10 | res.status(200).json(data); 11 | } catch (err) { 12 | //////////////////////////////////// 13 | console.log(err); // for TESTING// 14 | //////////////////////////////////// 15 | } 16 | }; 17 | 18 | export { getTopAiring }; 19 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/newSeasons.controller.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { scrapeNewSeasons } from "../../scrapers/gogoanime/scrappers"; 3 | 4 | const getNewSeasons: RequestHandler = async (req, res) => { 5 | try { 6 | const page = req.query.page 7 | ? Number(decodeURIComponent(req.query?.page as string)) 8 | : 1; 9 | const data = await scrapeNewSeasons(page); 10 | res.status(200).json(data); 11 | } catch (err) { 12 | //////////////////////////////////// 13 | console.log(err); // for TESTING// 14 | //////////////////////////////////// 15 | } 16 | }; 17 | 18 | export { getNewSeasons }; 19 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/animeMovies.controller.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { scrapeAnimeMovies } from "../../scrapers/gogoanime/scrappers"; 3 | 4 | const getAnimeMovies: RequestHandler = async (req, res) => { 5 | try { 6 | const page = req.query.page 7 | ? Number(decodeURIComponent(req.query?.page as string)) 8 | : 1; 9 | const data = await scrapeAnimeMovies(page); 10 | res.status(200).json(data); 11 | } catch (err) { 12 | //////////////////////////////////// 13 | console.log(err); // for TESTING// 14 | //////////////////////////////////// 15 | } 16 | }; 17 | 18 | export { getAnimeMovies }; 19 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/popularAnimes.controller.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { scrapePopularAnime } from "../../scrapers/gogoanime/scrappers"; 3 | 4 | const getPopularAnimes: RequestHandler = async (req, res) => { 5 | try { 6 | const page = req.query.page 7 | ? Number(decodeURIComponent(req.query?.page as string)) 8 | : 1; 9 | const data = await scrapePopularAnime(page); 10 | res.status(200).json(data); 11 | } catch (err) { 12 | //////////////////////////////////// 13 | console.log(err); // for TESTING// 14 | //////////////////////////////////// 15 | } 16 | }; 17 | 18 | export { getPopularAnimes }; 19 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/completedAnimes.controller.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { scrapeCompletedAnime } from "../../scrapers/gogoanime/scrappers"; 3 | 4 | const getCompletedAnimes: RequestHandler = async (req, res) => { 5 | try { 6 | const page = req.query.page 7 | ? Number(decodeURIComponent(req.query?.page as string)) 8 | : 1; 9 | const data = await scrapeCompletedAnime(page); 10 | res.status(200).json(data); 11 | } catch (err) { 12 | //////////////////////////////////// 13 | console.log(err); // for TESTING// 14 | //////////////////////////////////// 15 | } 16 | }; 17 | 18 | export { getCompletedAnimes }; 19 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/recentReleases.controller.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { scrapeRecentReleases } from "../../scrapers/gogoanime/scrappers"; 3 | 4 | const getRecentReleases: RequestHandler = async (req, res) => { 5 | try { 6 | const page = req.query.page 7 | ? Number(decodeURIComponent(req.query?.page as string)) 8 | : 1; 9 | const data = await scrapeRecentReleases(page); 10 | res.status(200).json(data); 11 | } catch (err) { 12 | //////////////////////////////////// 13 | console.log(err); // for TESTING// 14 | //////////////////////////////////// 15 | } 16 | }; 17 | 18 | export { getRecentReleases }; 19 | -------------------------------------------------------------------------------- /src/utils/aniwatch/video-extractor.ts: -------------------------------------------------------------------------------- 1 | import { IVideo, ISource } from "../../types/aniwatch/anime"; 2 | import Proxy from "./proxy"; 3 | 4 | abstract class VideoExtractor extends Proxy { 5 | /** 6 | * The server name of the video provider 7 | */ 8 | protected abstract serverName: string; 9 | 10 | /** 11 | * list of videos available 12 | */ 13 | protected abstract sources: IVideo[]; 14 | 15 | /** 16 | * takes video link 17 | * 18 | * returns video sources (video links) available 19 | */ 20 | protected abstract extract( 21 | videoUrl: URL, 22 | ...args: any 23 | ): Promise; 24 | } 25 | 26 | export default VideoExtractor; 27 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/scrapers.ts: -------------------------------------------------------------------------------- 1 | import { scrapeHomePage } from "./home"; 2 | import { scrapeAboutPage } from "./about"; 3 | import { scrapeSearchPage } from "./search"; 4 | import { scrapeCategoryPage } from "./category"; 5 | import { scrapeEpisodesPage } from "./episodes"; 6 | import { scrapeEpisodeServersPage } from "./servers"; 7 | import { scrapeAnimeEpisodeSources } from "./episodeServerSource"; 8 | import { scrapeatozAnimes } from "./atozAnimes"; 9 | 10 | export { 11 | scrapeHomePage, 12 | scrapeAboutPage, 13 | scrapeSearchPage, 14 | scrapeCategoryPage, 15 | scrapeEpisodesPage, 16 | scrapeEpisodeServersPage, 17 | scrapeAnimeEpisodeSources, 18 | scrapeatozAnimes, 19 | }; 20 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/categoryController.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { scrapeCategoryPage } from "../../scrapers/aniwatch/scrapers"; 3 | 4 | const getCategoryPage: RequestHandler = async (req, res) => { 5 | try { 6 | const category = req.params.category; 7 | const page = req.query.page 8 | ? Number(decodeURIComponent(req.query?.page as string)) 9 | : 1; 10 | const data = await scrapeCategoryPage(category, page); 11 | res.status(200).json(data); 12 | } catch (err) { 13 | //////////////////////////////////// 14 | console.log(err); // for TESTING// 15 | //////////////////////////////////// 16 | } 17 | }; 18 | 19 | export { getCategoryPage }; 20 | -------------------------------------------------------------------------------- /src/config/websites.ts: -------------------------------------------------------------------------------- 1 | type WebsiteConfig = { 2 | BASE: string; 3 | }; 4 | 5 | export type AnimeWebsiteConfig = WebsiteConfig & { 6 | CLONES?: Record; 7 | }; 8 | 9 | type Websites = Record; 10 | 11 | // anime websites and their clones 12 | export const websites_collection: Websites = { 13 | AniWatch: { 14 | BASE: "https://aniwatchtv.to", 15 | CLONES: { 16 | HiAnime: [ 17 | "https://hianimez.is", 18 | "https://hianimez.to", 19 | "https://hianime.nz", 20 | "https://hianime.bz", 21 | "https://hianime.pe", 22 | ], 23 | }, 24 | }, 25 | GogoAnime: { 26 | BASE: "https://ww24.gogoanimes.fi", 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/types/aniwatch/home.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MinimalAnime, 3 | Top10Anime, 4 | SpotLightAnime, 5 | TopUpcomingAnime, 6 | LatestAnimeEpisode, 7 | } from "./anime"; 8 | 9 | export interface ScrapedHomePage { 10 | spotLightAnimes: SpotLightAnime[]; 11 | trendingAnimes: MinimalAnime[]; 12 | top10Animes: { 13 | day: Top10Anime[]; 14 | week: Top10Anime[]; 15 | month: Top10Anime[]; 16 | }; 17 | latestEpisodes: LatestAnimeEpisode[]; 18 | featuredAnimes: { 19 | topAiringAnimes: MinimalAnime[]; 20 | mostPopularAnimes: MinimalAnime[]; 21 | mostFavoriteAnimes: MinimalAnime[]; 22 | latestCompletedAnimes: MinimalAnime[]; 23 | } 24 | topUpcomingAnimes: TopUpcomingAnime[]; 25 | genres: string[]; 26 | } 27 | -------------------------------------------------------------------------------- /src/routes/routes.ts: -------------------------------------------------------------------------------- 1 | import aniwatch_router from "./aniwatch/routes"; 2 | import gogoanime_router from "./gogoanime/routes"; 3 | import { getRoot } from "../lib/getRoot"; 4 | import { Router, type IRouter } from "express"; 5 | 6 | const router: IRouter = Router(); 7 | 8 | // / 9 | router.get("/", getRoot); 10 | 11 | // health check API 12 | router.get("/health", (_req, res) => { 13 | res.sendStatus(200); 14 | }); 15 | 16 | // aniwatch, hianime, zoro 17 | router.use("/aniwatch", aniwatch_router); 18 | router.use("/hianime", aniwatch_router); 19 | router.use("/zoro", aniwatch_router); 20 | 21 | // gogoanime, anitaku 22 | router.use("/gogoanime", gogoanime_router); 23 | router.use("/anitaku", gogoanime_router); 24 | 25 | export { router }; 26 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/controllers.ts: -------------------------------------------------------------------------------- 1 | import { getHomePageInfo } from "./homeController"; 2 | import { getAboutPageInfo } from "./aboutController"; 3 | import { getSearchPageInfo } from "./searchController"; 4 | import { getCategoryPage } from "./categoryController"; 5 | import { getEpisodesInfo } from "./episodesController"; 6 | import { getEpisodeServersInfo } from "./episodeServersController"; 7 | import { getAnimeEpisodeSourcesInfo } from "./episodeServerSourcesController"; 8 | import { getatozPage } from "./atozController"; 9 | 10 | export { 11 | getHomePageInfo, 12 | getAboutPageInfo, 13 | getSearchPageInfo, 14 | getCategoryPage, 15 | getEpisodesInfo, 16 | getEpisodeServersInfo, 17 | getAnimeEpisodeSourcesInfo, 18 | getatozPage, 19 | }; 20 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/scrappers.ts: -------------------------------------------------------------------------------- 1 | import { scrapeRecentReleases } from "./recent-releases"; 2 | import { scrapeNewSeasons } from "./new-seasons"; 3 | import { scrapePopularAnime } from "./popular-animes"; 4 | import { scrapeCompletedAnime } from "./completed-animes"; 5 | import { scrapeAnimeMovies } from "./anime-movies"; 6 | import { scrapeTopAiring } from "./top-airing"; 7 | import { scrapeAboutPage } from "./about"; 8 | import { scrapeHomePage } from "./home"; 9 | import { scrapeEpisodePage } from "./episodes"; 10 | 11 | export { 12 | scrapeRecentReleases, 13 | scrapeNewSeasons, 14 | scrapePopularAnime, 15 | scrapeCompletedAnime, 16 | scrapeAnimeMovies, 17 | scrapeTopAiring, 18 | scrapeAboutPage, 19 | scrapeHomePage, 20 | scrapeEpisodePage 21 | }; 22 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/episodesController.ts: -------------------------------------------------------------------------------- 1 | import createHttpError from "http-errors"; 2 | import type { RequestHandler } from "express"; 3 | import { scrapeEpisodesPage } from "../../scrapers/aniwatch/scrapers"; 4 | 5 | const getEpisodesInfo: RequestHandler = async (req, res) => { 6 | try { 7 | const anime_id = req.params.id ? decodeURIComponent(req.params.id) : null; 8 | 9 | if (anime_id === null) { 10 | throw createHttpError.BadRequest("Anime Id Required"); 11 | } 12 | 13 | const data = await scrapeEpisodesPage(anime_id); 14 | res.status(200).json(data); 15 | } catch (err) { 16 | //////////////////////////////////// 17 | console.log(err); // for TESTING// 18 | //////////////////////////////////// 19 | } 20 | }; 21 | 22 | export { getEpisodesInfo }; 23 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/episodeServersController.ts: -------------------------------------------------------------------------------- 1 | import createHttpError from "http-errors"; 2 | import { type RequestHandler } from "express"; 3 | import { scrapeEpisodeServersPage } from "../../scrapers/aniwatch/scrapers"; 4 | 5 | const getEpisodeServersInfo: RequestHandler = async (req, res) => { 6 | try { 7 | const episodeId = req.query.id 8 | ? decodeURIComponent(req.query?.id as string) 9 | : null; 10 | 11 | if (episodeId === null) { 12 | throw createHttpError.BadRequest("Episode Id required"); 13 | } 14 | 15 | const data = await scrapeEpisodeServersPage(episodeId); 16 | res.status(200).json(data); 17 | } catch (err) { 18 | //////////////////////////////////// 19 | console.log(err); // for TESTING// 20 | //////////////////////////////////// 21 | } 22 | }; 23 | 24 | export { getEpisodeServersInfo }; 25 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/controllers.ts: -------------------------------------------------------------------------------- 1 | import { getRecentReleases } from "./recentReleases.controller"; 2 | import { getNewSeasons } from "./newSeasons.controller"; 3 | import { getPopularAnimes } from "./popularAnimes.controller"; 4 | import { getCompletedAnimes } from "./completedAnimes.controller"; 5 | import { getAnimeMovies } from "./animeMovies.controller"; 6 | import { getTopAiring } from "./topAiring.controller"; 7 | import { getHomePageInfo } from "./homeController"; 8 | import { getAboutPageInfo } from "./aboutController"; 9 | import { getSearchPageInfo } from "./searchController"; 10 | import { getAnimeEpisodes } from "./anime.episodes"; 11 | 12 | export { 13 | getRecentReleases, 14 | getNewSeasons, 15 | getPopularAnimes, 16 | getCompletedAnimes, 17 | getAnimeMovies, 18 | getTopAiring, 19 | getHomePageInfo, 20 | getSearchPageInfo, 21 | getAboutPageInfo, 22 | getAnimeEpisodes 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aniwatch-api", 3 | "version": "1.1.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "dev": "npx nodemon", 8 | "start": "node ./build/server.js", 9 | "build": "rimraf ./build && tsc", 10 | "vercel-build": "echo building" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/crypto-js": "^4.2.2", 17 | "@types/express": "^4.17.21", 18 | "@types/node": "^20.17.10", 19 | "husky": "^9.1.4", 20 | "rimraf": "^5.0.5", 21 | "tsup": "^8.2.4", 22 | "typescript": "^5.5.4", 23 | "vitest": "^2.0.5" 24 | }, 25 | "dependencies": { 26 | "axios": "^1.6.7", 27 | "cheerio": "^1.0.0-rc.12", 28 | "crypto-js": "^4.2.0", 29 | "dotenv": "^16.4.1", 30 | "express": "^4.18.2", 31 | "express-rate-limit": "^7.1.5", 32 | "http-errors": "^2.0.0", 33 | "node-cache": "^5.1.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/getRoot.ts: -------------------------------------------------------------------------------- 1 | import type { GetRoot } from "../types/aniwatch/root"; 2 | import type { RequestHandler } from "express"; 3 | import { isSiteReachable } from "./isSiteReachable"; 4 | 5 | // TODO: make config json files , make it better in future 6 | export const getRoot: RequestHandler = async (_req, res) => { 7 | try { 8 | const data: GetRoot = { 9 | docs: "", 10 | sites: {}, 11 | }; 12 | 13 | data.docs = "https://github.com/falcon71181/Anime-API/blob/main/README.md"; 14 | 15 | const aniwatchStatus = await isSiteReachable("https://aniwatch.to"); 16 | const aniwatchtvStatus = await isSiteReachable("https://aniwatchtv.to"); 17 | 18 | const gogoanimeStatus = await isSiteReachable("https://gogoanime3.co"); 19 | 20 | data.sites["aniwatch"] = aniwatchStatus || aniwatchtvStatus; 21 | data.sites["gogoanime"] = gogoanimeStatus; 22 | 23 | res.status(200).json(data); 24 | } catch (error) { 25 | console.log(error); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/searchController.ts: -------------------------------------------------------------------------------- 1 | import { scrapeSearchPage } from "../../scrapers/aniwatch/search"; 2 | import createHttpError from "http-errors"; 3 | import type { RequestHandler } from "express"; 4 | 5 | const getSearchPageInfo: RequestHandler = async (req, res) => { 6 | try { 7 | const page: number = req.query.page 8 | ? Number(decodeURIComponent(req.query?.page as string)) 9 | : 1; 10 | const keyword: string | null = req.query.keyword 11 | ? decodeURIComponent(req.query.keyword as string) 12 | : null; 13 | 14 | if (keyword === null) { 15 | throw createHttpError.BadRequest("Search keyword required"); 16 | } 17 | 18 | const data = await scrapeSearchPage(keyword, page); 19 | res.status(200).json(data); 20 | } catch (err) { 21 | //////////////////////////////////// 22 | console.log(err); // for TESTING// 23 | //////////////////////////////////// 24 | } 25 | }; 26 | 27 | export { getSearchPageInfo }; 28 | -------------------------------------------------------------------------------- /src/controllers/gogoanime/searchController.ts: -------------------------------------------------------------------------------- 1 | import { scrapeSearchPage } from "../../scrapers/gogoanime/search"; 2 | import createHttpError from "http-errors"; 3 | import type { RequestHandler } from "express"; 4 | 5 | const getSearchPageInfo: RequestHandler = async (req, res) => { 6 | try { 7 | const page: number = req.query.page 8 | ? Number(decodeURIComponent(req.query?.page as string)) 9 | : 1; 10 | const keyword: string | null = req.query.keyword 11 | ? decodeURIComponent(req.query.keyword as string) 12 | : null; 13 | 14 | if (keyword === null) { 15 | throw createHttpError.BadRequest("Search keyword required"); 16 | } 17 | 18 | const data = await scrapeSearchPage(keyword, page); 19 | res.status(200).json(data); 20 | } catch (err) { 21 | //////////////////////////////////// 22 | console.log(err); // for TESTING// 23 | //////////////////////////////////// 24 | } 25 | }; 26 | 27 | export { getSearchPageInfo }; 28 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/atoz_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { extract_top_upcoming_animes } from "./extracters"; 5 | 6 | export const extract_atoz_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ) => { 10 | try { 11 | const animes = extract_top_upcoming_animes($, selectors); 12 | 13 | return animes; 14 | } catch (err) { 15 | //////////////////////////////////////////////////////////////// 16 | console.error("Error in extract_atoz_animes :", err); // for TESTING// 17 | //////////////////////////////////////////////////////////////// 18 | 19 | if (err instanceof AxiosError) { 20 | throw createHttpError( 21 | err?.response?.status || 500, 22 | err?.response?.statusText || "Something went wrong", 23 | ); 24 | } else { 25 | throw createHttpError.InternalServerError("Internal server error"); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/extracters.ts: -------------------------------------------------------------------------------- 1 | import { extract_latest_episodes } from "./recent_released_episodes"; 2 | import { extract_new_seasons } from "./new_seasons"; 3 | import { extract_popular_animes } from "./popular_animes"; 4 | import { extract_completed_animes } from "./completed_animes"; 5 | import { extract_anime_movies } from "./anime_movies"; 6 | import { extract_top_airing } from "./top_airing"; 7 | import { extract_searched_animes } from "./searched_animes"; 8 | import { extract_recent_released_home } from "./extract_recent_released_home"; 9 | import { extract_recently_added_series_home } from "./extract_recently_added_series_home"; 10 | import { extract_episodes } from "./episodes"; 11 | 12 | export { 13 | extract_latest_episodes, 14 | extract_new_seasons, 15 | extract_popular_animes, 16 | extract_completed_animes, 17 | extract_anime_movies, 18 | extract_top_airing, 19 | extract_searched_animes, 20 | extract_recent_released_home, 21 | extract_recently_added_series_home, 22 | extract_episodes 23 | }; 24 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/category_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { extract_top_upcoming_animes } from "./extracters"; 5 | 6 | export const extract_category_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ) => { 10 | try { 11 | const animes = extract_top_upcoming_animes($, selectors); 12 | 13 | return animes; 14 | } catch (err) { 15 | //////////////////////////////////////////////////////////////// 16 | console.error("Error in extract_category_animes :", err); // for TESTING// 17 | //////////////////////////////////////////////////////////////// 18 | 19 | if (err instanceof AxiosError) { 20 | throw createHttpError( 21 | err?.response?.status || 500, 22 | err?.response?.statusText || "Something went wrong", 23 | ); 24 | } else { 25 | throw createHttpError.InternalServerError("Internal server error"); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/searched_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { extract_top_upcoming_animes } from "./extracters"; 5 | 6 | export const extract_searched_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ) => { 10 | try { 11 | const animes = extract_top_upcoming_animes($, selectors); 12 | 13 | return animes; 14 | } catch (err) { 15 | //////////////////////////////////////////////////////////////// 16 | console.error("Error in extract_searched_animes :", err); // for TESTING// 17 | //////////////////////////////////////////////////////////////// 18 | 19 | if (err instanceof AxiosError) { 20 | throw createHttpError( 21 | err?.response?.status || 500, 22 | err?.response?.statusText || "Something went wrong", 23 | ); 24 | } else { 25 | throw createHttpError.InternalServerError("Internal server error"); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/genre_list.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | 5 | export const extract_genre_list = ( 6 | $: CheerioAPI, 7 | selectors: SelectorType, 8 | ): string[] => { 9 | try { 10 | const genres: string[] = []; 11 | 12 | $(selectors).each((_index, element) => { 13 | genres.push(`${$(element)?.text()?.trim() || null}`); 14 | }); 15 | return genres; 16 | } catch (err) { 17 | /////////////////////////////////////////////////////////////////// 18 | console.error("Error in extract_genre_list :", err); // for TESTING// 19 | /////////////////////////////////////////////////////////////////// 20 | 21 | if (err instanceof AxiosError) { 22 | throw createHttpError( 23 | err?.response?.status || 500, 24 | err?.response?.statusText || "Something went wrong", 25 | ); 26 | } else { 27 | throw createHttpError.InternalServerError("Internal server error"); 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/aniwatch/streamtape.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { load, type CheerioAPI } from "cheerio"; 3 | import type { Video } from "../../types/aniwatch/anime"; 4 | 5 | class StreamTape { 6 | private serverName = "StreamTape"; 7 | private sources: Video[] = []; 8 | 9 | async extract(videoUrl: URL): Promise { 10 | try { 11 | const { data } = await axios.get(videoUrl.href).catch(() => { 12 | throw new Error("Video not found"); 13 | }); 14 | 15 | const $: CheerioAPI = load(data); 16 | 17 | let [fh, sh] = $.html() 18 | ?.match(/robotlink'\).innerHTML = (.*)'/)![1] 19 | .split("+ ('"); 20 | 21 | sh = sh.substring(3); 22 | fh = fh.replace(/\'/g, ""); 23 | 24 | const url = `https:${fh}${sh}`; 25 | 26 | this.sources.push({ 27 | url: url, 28 | isM3U8: url.includes(".m3u8"), 29 | }); 30 | 31 | return this.sources; 32 | } catch (err) { 33 | throw new Error((err as Error).message); 34 | } 35 | } 36 | } 37 | export default StreamTape; 38 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/anime_movies.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { extract_new_seasons } from "./extracters"; 5 | import { AnimeMovie } from "../../types/gogoanime/anime"; 6 | 7 | export const extract_anime_movies = ( 8 | $: CheerioAPI, 9 | selectors: SelectorType, 10 | url_base: string 11 | ): AnimeMovie[] => { 12 | try { 13 | const animes: AnimeMovie[] = extract_new_seasons($, selectors, url_base); 14 | return animes; 15 | } catch (err) { 16 | //////////////////////////////////////////////////////////////// 17 | console.error("Error in extract_anime_movies :", err); // for TESTING// 18 | //////////////////////////////////////////////////////////////// 19 | 20 | if (err instanceof AxiosError) { 21 | throw createHttpError( 22 | err?.response?.status || 500, 23 | err?.response?.statusText || "Something went wrong", 24 | ); 25 | } else { 26 | throw createHttpError.InternalServerError("Internal server error"); 27 | } 28 | } 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/popular_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { extract_new_seasons } from "./extracters"; 5 | import { PopularAnime } from "../../types/gogoanime/anime"; 6 | 7 | export const extract_popular_animes = ( 8 | $: CheerioAPI, 9 | selectors: SelectorType, 10 | url_base: string 11 | ): PopularAnime[] => { 12 | try { 13 | const animes: PopularAnime[] = extract_new_seasons($, selectors, url_base); 14 | return animes; 15 | } catch (err) { 16 | //////////////////////////////////////////////////////////////// 17 | console.error("Error in extract_new_seasons :", err); // for TESTING// 18 | //////////////////////////////////////////////////////////////// 19 | 20 | if (err instanceof AxiosError) { 21 | throw createHttpError( 22 | err?.response?.status || 500, 23 | err?.response?.statusText || "Something went wrong", 24 | ); 25 | } else { 26 | throw createHttpError.InternalServerError("Internal server error"); 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/recommended_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { extract_top_upcoming_animes } from "./extracters"; 5 | import { RecommendedAnime } from "../../types/aniwatch/anime"; 6 | 7 | export const extract_recommended_animes = ( 8 | $: CheerioAPI, 9 | selectors: SelectorType, 10 | ): RecommendedAnime[] => { 11 | try { 12 | const animes: RecommendedAnime[] = extract_top_upcoming_animes( 13 | $, 14 | selectors, 15 | ); 16 | 17 | return animes; 18 | } catch (err) { 19 | //////////////////////////////////////////////////////////////// 20 | console.error("Error in extract_recommended_animes :", err); // for TESTING// 21 | //////////////////////////////////////////////////////////////// 22 | 23 | if (err instanceof AxiosError) { 24 | throw createHttpError( 25 | err?.response?.status || 500, 26 | err?.response?.statusText || "Something went wrong", 27 | ); 28 | } else { 29 | throw createHttpError.InternalServerError("Internal server error"); 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/latest_anime_episodes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import { extract_top_upcoming_animes } from "./extracters"; 3 | import createHttpError from "http-errors"; 4 | import { AxiosError } from "axios"; 5 | import { LatestAnimeEpisode } from "../../types/aniwatch/anime"; 6 | 7 | export const extract_latest_episodes = ( 8 | $: CheerioAPI, 9 | selectors: SelectorType, 10 | ): LatestAnimeEpisode[] => { 11 | try { 12 | const animes: LatestAnimeEpisode[] = extract_top_upcoming_animes( 13 | $, 14 | selectors, 15 | ); 16 | 17 | return animes; 18 | } catch (err) { 19 | //////////////////////////////////////////////////////////////// 20 | console.error("Error in extract_latest_episodes :", err); // for TESTING// 21 | //////////////////////////////////////////////////////////////// 22 | 23 | if (err instanceof AxiosError) { 24 | throw createHttpError( 25 | err?.response?.status || 500, 26 | err?.response?.statusText || "Something went wrong", 27 | ); 28 | } else { 29 | throw createHttpError.InternalServerError("Internal server error"); 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/completed_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { extract_new_seasons } from "./extracters"; 5 | import { CompletedAnime } from "../../types/gogoanime/anime"; 6 | 7 | export const extract_completed_animes = ( 8 | $: CheerioAPI, 9 | selectors: SelectorType, 10 | url_base: string, 11 | ): CompletedAnime[] => { 12 | try { 13 | const animes: CompletedAnime[] = extract_new_seasons( 14 | $, 15 | selectors, 16 | url_base, 17 | ); 18 | return animes; 19 | } catch (err) { 20 | //////////////////////////////////////////////////////////////// 21 | console.error("Error in extract_completed_animes :", err); // for TESTING// 22 | //////////////////////////////////////////////////////////////// 23 | 24 | if (err instanceof AxiosError) { 25 | throw createHttpError( 26 | err?.response?.status || 500, 27 | err?.response?.statusText || "Something went wrong", 28 | ); 29 | } else { 30 | throw createHttpError.InternalServerError("Internal server error"); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/episodes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { Episode } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_episodes_info = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ) => { 10 | try { 11 | const episodes: Episode[] = []; 12 | 13 | $(`${selectors}`).each((_index, element) => { 14 | episodes.push({ 15 | name: $(element)?.attr("title")?.trim() || null, 16 | episodeNo: Number($(element).attr("data-number")), 17 | episodeId: $(element)?.attr("href")?.split("/")?.pop() || null, 18 | filler: $(element).hasClass("ssl-item-filler"), 19 | }); 20 | }); 21 | 22 | return episodes; 23 | } catch (err) { 24 | /////////////////////////////////////////////////////////////////// 25 | console.error("Error in extract_episodes_info :", err); // for TESTING// 26 | /////////////////////////////////////////////////////////////////// 27 | 28 | if (err instanceof AxiosError) { 29 | throw createHttpError( 30 | err?.response?.status || 500, 31 | err?.response?.statusText || "Something went wrong", 32 | ); 33 | } else { 34 | throw createHttpError.InternalServerError("Internal server error"); 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/episodes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { Episode } from "../../types/gogoanime/anime"; 5 | 6 | export const extract_episodes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | url_base: string 10 | ): Episode[] => { 11 | try { 12 | let episodes: Episode[] = []; 13 | $(selectors).each((_, ele) => { 14 | const a = $(ele).children('a'); 15 | 16 | const title = a.children('.name').text().trim(); 17 | 18 | const href = a.attr('href') ?? ''; 19 | 20 | const id = href.split('/').pop() ?? ''; 21 | const link = new URL(href, url_base).toString(); 22 | 23 | episodes.push({ id, title, link}); 24 | }); 25 | 26 | return episodes; 27 | } catch (err) { 28 | //////////////////////////////////////////////////////////////// 29 | console.error("Error in extract_episodes :", err); // for TESTING// 30 | //////////////////////////////////////////////////////////////// 31 | 32 | if (err instanceof AxiosError) { 33 | throw createHttpError( 34 | err?.response?.status || 500, 35 | err?.response?.statusText || "Something went wrong", 36 | ); 37 | } else { 38 | throw createHttpError.InternalServerError("Internal server error"); 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/types/gogoanime/anime.ts: -------------------------------------------------------------------------------- 1 | import type { ScrapedHomePage } from "./home"; 2 | import type { ScrapedEpisodes } from "./episodes"; 3 | 4 | interface Anime { 5 | id: string | null; 6 | name: string | null; 7 | img: string | null; 8 | } 9 | 10 | interface Episode { 11 | id: string; 12 | title: string; 13 | link: string; 14 | } 15 | 16 | interface AboutAnimeInfo { 17 | name: string | null; 18 | img: string | null; 19 | type: string | null; 20 | genre: string[] | null; 21 | status: string | null; 22 | aired_in: number | null; 23 | other_name: string | null; 24 | episodes: number | null; 25 | } 26 | 27 | interface RecentRelease extends Anime { 28 | episodeId: string; 29 | episodeNo: number; 30 | subOrDub: string; 31 | episodeUrl: string; 32 | } 33 | 34 | interface NewSeason extends Anime { 35 | releasedYear: string; 36 | animeUrl: string; 37 | } 38 | 39 | interface PopularAnime extends NewSeason {} 40 | interface CompletedAnime extends NewSeason {} 41 | interface AnimeMovie extends NewSeason {} 42 | interface TopAiring extends Anime { 43 | latestEp: string; 44 | animeUrl: string; 45 | genres: string[]; 46 | } 47 | 48 | interface SearchedAnime extends Anime { 49 | releasedYear: string | null; 50 | } 51 | 52 | export type { 53 | ScrapedHomePage, 54 | ScrapedEpisodes, 55 | RecentRelease, 56 | Anime, 57 | NewSeason, 58 | PopularAnime, 59 | CompletedAnime, 60 | AnimeMovie, 61 | TopAiring, 62 | SearchedAnime, 63 | AboutAnimeInfo, 64 | Episode, 65 | }; 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "lib": [ 5 | "es6", 6 | "dom" 7 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 8 | "module": "commonjs" /* Specify what module code is generated. */, 9 | "rootDir": "src" /* Specify the root folder within your source files. */, 10 | "resolveJsonModule": true /* Enable importing .json files. */, 11 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 12 | "outDir": "build" /* Specify an output folder for all emitted files. */, 13 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 14 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 15 | "strict": true /* Enable all strict type-checking options. */, 16 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 17 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 18 | "noEmit": true, 19 | "allowJs": true, 20 | "declaration": true, 21 | "isolatedModules": false, 22 | "moduleDetection": "force", 23 | "verbatimModuleSyntax": false, 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/extract_recently_added_series_home.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { CheerioAPI, SelectorType } from "cheerio"; 3 | import createHttpError from "http-errors"; 4 | import { Anime } from "../../types/gogoanime/anime"; 5 | 6 | export const extract_recently_added_series_home = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): Anime[] => { 10 | try { 11 | let recentlyAddedSeries: Anime[] = []; 12 | 13 | $(selectors).each((_index, element) => { 14 | const animeNAME = 15 | $(element).find("a[title]")?.last().text()?.trim() ?? "UNKNOWN ANIME"; 16 | 17 | const id = 18 | $(element).find("a")?.attr("href")?.trim().split("/").pop() ?? ""; 19 | 20 | const animeIMG = `https://gogocdn.net/cover/${id}.png`; 21 | 22 | let anime: Anime = { 23 | id: id, 24 | name: animeNAME, 25 | img: animeIMG, 26 | }; 27 | 28 | recentlyAddedSeries.push(anime); 29 | }); 30 | 31 | return recentlyAddedSeries; 32 | } catch (err) { 33 | /////////////////////////////////////////////////////////////////// 34 | console.error("Error in extract_recently_added_series_home :", err); // for TESTING// 35 | /////////////////////////////////////////////////////////////////// 36 | 37 | if (err instanceof AxiosError) { 38 | throw createHttpError( 39 | err?.response?.status || 500, 40 | err?.response?.statusText || "Something went wrong", 41 | ); 42 | } else { 43 | throw createHttpError.InternalServerError("Internal server error"); 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/trending_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { MinimalAnime } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_trending_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): MinimalAnime[] => { 10 | try { 11 | const animes: MinimalAnime[] = []; 12 | 13 | $(selectors).each((_index, element) => { 14 | const animeID = 15 | $(element).find(".item .film-poster")?.attr("href")?.slice(1) || null; 16 | const animeNAME = 17 | $(element) 18 | .find(".item .number .film-title.dynamic-name") 19 | ?.text() 20 | ?.trim() ?? "UNKNOWN ANIME"; 21 | const animeIMG = $(element) 22 | .find(".item .film-poster .film-poster-img") 23 | ?.attr("data-src") 24 | ?.trim() || null; 25 | 26 | animes.push({ 27 | id: animeID, 28 | name: animeNAME, 29 | img: animeIMG, 30 | }); 31 | }); 32 | return animes; 33 | } catch (err) { 34 | /////////////////////////////////////////////////////////////////////// 35 | console.error("Error in extract_trending_animes :", err); // for TESTING// 36 | /////////////////////////////////////////////////////////////////////// 37 | 38 | if (err instanceof AxiosError) { 39 | throw createHttpError( 40 | err?.response?.status || 500, 41 | err?.response?.statusText || "Something went wrong", 42 | ); 43 | } else { 44 | throw createHttpError.InternalServerError("Internal server error"); 45 | } 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/extracters.ts: -------------------------------------------------------------------------------- 1 | import { extract_spotlight_animes } from "./spotlight_animes"; 2 | import { extract_top10_animes } from "./top10_animes"; 3 | import { extract_trending_animes } from "./trending_animes"; 4 | import { extract_featured_animes } from "./featured_animes"; 5 | import { extract_top_upcoming_animes } from "./top_upcoming_animes"; 6 | import { extract_latest_episodes } from "./latest_anime_episodes"; 7 | import { extract_genre_list } from "./genre_list"; 8 | import { extract_about_info } from "./about_anime"; 9 | import { extract_extra_about_info } from "./about_extra_anime"; 10 | import { extract_anime_seasons_info } from "./anime_seasons_info"; 11 | import { extract_related_animes } from "./related_animes"; 12 | import { extract_recommended_animes } from "./recommended_animes"; 13 | import { extract_mostpopular_animes } from "./mostpopular_animes"; 14 | import { extract_searched_animes } from "./searched_animes"; 15 | import { extract_category_animes } from "./category_animes"; 16 | import { extract_episodes_info } from "./episodes"; 17 | import { extract_server_id } from "./server_id"; 18 | import { extract_atoz_animes } from "./atoz_animes"; 19 | 20 | export { 21 | extract_spotlight_animes, 22 | extract_top10_animes, 23 | extract_trending_animes, 24 | extract_featured_animes, 25 | extract_latest_episodes, 26 | extract_top_upcoming_animes, 27 | extract_genre_list, 28 | extract_about_info, 29 | extract_extra_about_info, 30 | extract_anime_seasons_info, 31 | extract_related_animes, 32 | extract_recommended_animes, 33 | extract_mostpopular_animes, 34 | extract_searched_animes, 35 | extract_category_animes, 36 | extract_episodes_info, 37 | extract_server_id, 38 | extract_atoz_animes, 39 | }; 40 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/new_seasons.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { NewSeason } from "../../types/gogoanime/anime"; 5 | 6 | export const extract_new_seasons = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | url_base: string, 10 | ): NewSeason[] => { 11 | try { 12 | const animes: NewSeason[] = []; 13 | $(selectors).each((_index, element: any) => { 14 | const animeID = 15 | $(element).find('p.name > a')?.attr('href')?.split('/')[2] ?? "UNKNOWN"; 16 | const animeNAME = 17 | $(element).find("p.name > a")?.attr("title") ?? "UNKNOWN"; 18 | const animeIMG = $(element).find('div > a > img').attr('src') ?? "UNKNOWN"; 19 | const releasedDate = $(element).find('p.released').text().replace('Released: ', '').trim(); 20 | const animeUrl = url_base + '/' + $(element).find('p.name > a').attr('href'); 21 | 22 | animes.push({ 23 | id: animeID, 24 | name: animeNAME, 25 | img: animeIMG, 26 | releasedYear: releasedDate, 27 | animeUrl: animeUrl, 28 | }); 29 | }); 30 | return animes; 31 | } catch (err) { 32 | //////////////////////////////////////////////////////////////// 33 | console.error("Error in extract_new_seasons :", err); // for TESTING// 34 | //////////////////////////////////////////////////////////////// 35 | 36 | if (err instanceof AxiosError) { 37 | throw createHttpError( 38 | err?.response?.status || 500, 39 | err?.response?.statusText || "Something went wrong", 40 | ); 41 | } else { 42 | throw createHttpError.InternalServerError("Internal server error"); 43 | } 44 | } 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/featured_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { MinimalAnime } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_featured_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): MinimalAnime[] => { 10 | try { 11 | const animes: MinimalAnime[] = []; 12 | 13 | $(selectors).each((_index, element) => { 14 | const animeID = 15 | $(element) 16 | .find(".film-detail .film-name .dynamic-name") 17 | ?.attr("href") 18 | ?.slice(1) 19 | ?.trim() || null; 20 | const animeNAME = 21 | $(element) 22 | .find(".film-detail .film-name .dynamic-name") 23 | ?.text() 24 | ?.trim() ?? "UNKNOWN ANIME"; 25 | const animeIMG = 26 | $(element) 27 | .find(".film-poster a .film-poster-img") 28 | ?.attr("data-src") 29 | ?.trim() || null; 30 | 31 | animes.push({ 32 | id: animeID, 33 | name: animeNAME, 34 | img: animeIMG, 35 | }); 36 | }); 37 | return animes.slice(0, 5); 38 | } catch (err) { 39 | ///////////////////////////////////////////////////////////////////////// 40 | console.error("Error in extract_featured_animes :", err); // for TESTING// 41 | ///////////////////////////////////////////////////////////////////////// 42 | 43 | if (err instanceof AxiosError) { 44 | throw createHttpError( 45 | err?.response?.status || 500, 46 | err?.response?.statusText || "Something went wrong", 47 | ); 48 | } else { 49 | throw createHttpError.InternalServerError("Internal server error"); 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/popular-animes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/gogoanime/constants"; 4 | import { headers } from "../../config/headers"; 5 | import axios, { AxiosError } from "axios"; 6 | import { load } from "cheerio"; 7 | import type { CheerioAPI, SelectorType } from "cheerio"; 8 | import createHttpError, { HttpError } from "http-errors"; 9 | import { extract_popular_animes } from "../../extracters/gogoanime/extracters"; 10 | import { PopularAnime } from "../../types/gogoanime/anime"; 11 | 12 | export const scrapePopularAnime = async (page: number): Promise => { 13 | const URLs = await URL_fn(); 14 | try { 15 | let res: PopularAnime[] = []; 16 | const mainPage = await axios.get(`${URLs.POPULAR}?page=${page}`, { 17 | headers: { 18 | "User-Agent": headers.USER_AGENT_HEADER, 19 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 20 | Accept: headers.ACCEPT_HEADER, 21 | }, 22 | }); 23 | 24 | const $: CheerioAPI = load(mainPage.data); 25 | 26 | const popularAnimesSelectors: SelectorType = 27 | "div.last_episodes > ul > li"; 28 | 29 | res = extract_popular_animes($, popularAnimesSelectors, URLs.BASE); 30 | 31 | return res; 32 | } catch (err) { 33 | //////////////////////////////////////////////////////////////// 34 | console.error("Error in scrapePopularAnime :", err); // for TESTING// 35 | //////////////////////////////////////////////////////////////// 36 | 37 | if (err instanceof AxiosError) { 38 | throw createHttpError( 39 | err?.response?.status || 500, 40 | err?.response?.statusText || "Something went wrong", 41 | ); 42 | } else { 43 | throw createHttpError.InternalServerError("Internal server error"); 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/completed-animes.ts: -------------------------------------------------------------------------------- 1 | import { URL_fn } from "../../utils/gogoanime/constants"; 2 | import { headers } from "../../config/headers"; 3 | import axios, { AxiosError } from "axios"; 4 | import { load } from "cheerio"; 5 | import type { CheerioAPI, SelectorType } from "cheerio"; 6 | import createHttpError, { HttpError } from "http-errors"; 7 | import { extract_completed_animes } from "../../extracters/gogoanime/extracters"; 8 | import { CompletedAnime } from "../../types/gogoanime/anime"; 9 | 10 | export const scrapeCompletedAnime = async ( 11 | page: number, 12 | ): Promise => { 13 | const URLs = await URL_fn(); 14 | try { 15 | let res: CompletedAnime[] = []; 16 | const mainPage = await axios.get(`${URLs.POPULAR}?page=${page}`, { 17 | headers: { 18 | "User-Agent": headers.USER_AGENT_HEADER, 19 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 20 | Accept: headers.ACCEPT_HEADER, 21 | }, 22 | }); 23 | 24 | const $: CheerioAPI = load(mainPage.data); 25 | 26 | const popularAnimesSelectors: SelectorType = "div.last_episodes > ul > li"; 27 | 28 | res = extract_completed_animes($, popularAnimesSelectors, URLs.BASE); 29 | 30 | return res; 31 | } catch (err) { 32 | //////////////////////////////////////////////////////////////// 33 | console.error("Error in scrapeCompletedAnime :", err); // for TESTING// 34 | //////////////////////////////////////////////////////////////// 35 | 36 | if (err instanceof AxiosError) { 37 | throw createHttpError( 38 | err?.response?.status || 500, 39 | err?.response?.statusText || "Something went wrong", 40 | ); 41 | } else { 42 | throw createHttpError.InternalServerError("Internal server error"); 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/searched_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { SearchedAnime } from "../../types/gogoanime/anime"; 5 | 6 | export const extract_searched_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType 9 | ): SearchedAnime[] => { 10 | try { 11 | const animes: SearchedAnime[] = []; 12 | 13 | $(selectors).each((_index, element) => { 14 | const animeID = 15 | $(element) 16 | .find(".name a") 17 | ?.attr("href") 18 | ?.replace(/\/category\//g, "") 19 | ?.trim() ?? "UNKNOWN ANIME"; 20 | const animeNAME = $(element).find(".name a")?.text()?.trim() ?? "null"; 21 | const animePoster = 22 | $(element).find(".img img")?.attr("src")?.trim() ?? null; 23 | const releasedIn = 24 | $(element) 25 | .find(".released") 26 | ?.text() 27 | ?.replace(/Released:\s*/g, "") 28 | ?.trim() || null; 29 | 30 | animes.push({ 31 | id: animeID, 32 | name: animeNAME, 33 | img: animePoster, 34 | releasedYear: releasedIn, 35 | }); 36 | }); 37 | 38 | return animes; 39 | } catch (err) { 40 | //////////////////////////////////////////////////////////////// 41 | console.error("Error in extract_searched_animes :", err); // for TESTING// 42 | //////////////////////////////////////////////////////////////// 43 | 44 | if (err instanceof AxiosError) { 45 | throw createHttpError( 46 | err?.response?.status || 500, 47 | err?.response?.statusText || "Something went wrong" 48 | ); 49 | } else { 50 | throw createHttpError.InternalServerError("Internal server error"); 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/anime_seasons_info.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { AnimeSeasonsInfo } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_anime_seasons_info = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): AnimeSeasonsInfo[] => { 10 | try { 11 | const seasons: AnimeSeasonsInfo[] = []; 12 | 13 | $(selectors).each((_index, element) => { 14 | const animeID = $(element)?.attr("href")?.slice(1)?.trim() || null; 15 | const animeNAME = $(element)?.attr("title")?.trim() ?? "UNKNOWN ANIME"; 16 | const animeTITLE = $(element)?.find(".title")?.text()?.trim() || null; 17 | const animeIMG = 18 | $(element) 19 | ?.find(".season-poster") 20 | ?.attr("style") 21 | ?.split(" ") 22 | ?.pop() 23 | ?.split("(") 24 | ?.pop() 25 | ?.split(")")[0] || null; 26 | 27 | seasons.push({ 28 | id: animeID, 29 | name: animeNAME, 30 | seasonTitle: animeTITLE, 31 | img: animeIMG, 32 | isCurrent: $(element)?.hasClass("active"), 33 | }); 34 | }); 35 | return seasons; 36 | } catch (err) { 37 | /////////////////////////////////////////////////////////////////// 38 | console.error("Error in extract_anime_seasons_info :", err); // for TESTING// 39 | /////////////////////////////////////////////////////////////////// 40 | 41 | if (err instanceof AxiosError) { 42 | throw createHttpError( 43 | err?.response?.status || 500, 44 | err?.response?.statusText || "Something went wrong", 45 | ); 46 | } else { 47 | throw createHttpError.InternalServerError("Internal server error"); 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/episodes.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from "axios"; 2 | import { URL_fn } from "../../utils/gogoanime/constants"; 3 | import { headers } from "../../config/headers"; 4 | import { CheerioAPI, load, SelectorType } from "cheerio"; 5 | import { 6 | extract_episodes, 7 | } from "../../extracters/gogoanime/extracters"; 8 | import createHttpError, { HttpError } from "http-errors"; 9 | import { ScrapedEpisodes } from "../../types/gogoanime/anime"; 10 | 11 | export const scrapeEpisodePage = async (id: String): Promise< 12 | ScrapedEpisodes | HttpError 13 | > => { 14 | const URLs = await URL_fn(); 15 | 16 | const mainPage = await axios.get(`${URLs.AJAX}/load-list-episode?ep_start=0&ep_end=9999&id=${id}&default_ep=0`, { 17 | headers: { 18 | "User-Agent": headers.USER_AGENT_HEADER, 19 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 20 | Accept: headers.ACCEPT_HEADER, 21 | }, 22 | }); 23 | 24 | let res: ScrapedEpisodes = { 25 | episodes: [] 26 | }; 27 | 28 | const $: CheerioAPI = load(mainPage.data); 29 | 30 | const episode_selectors: SelectorType = "ul#episode_related li"; 31 | 32 | try { 33 | let episodes = extract_episodes($, episode_selectors, URLs.BASE); 34 | 35 | res.episodes = episodes; 36 | 37 | return res; 38 | } catch (err) { 39 | //////////////////////////////////////////////////////////////// 40 | console.error("Error in scrapeEpisodePage :", err); // for TESTING// 41 | //////////////////////////////////////////////////////////////// 42 | 43 | if (err instanceof AxiosError) { 44 | throw createHttpError( 45 | err?.response?.status || 500, 46 | err?.response?.statusText || "Something went wrong", 47 | ); 48 | } else { 49 | throw createHttpError.InternalServerError("Internal server error"); 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/new-seasons.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/gogoanime/constants"; 4 | import { headers } from "../../config/headers"; 5 | import axios, { AxiosError } from "axios"; 6 | import { load } from "cheerio"; 7 | import type { CheerioAPI, SelectorType } from "cheerio"; 8 | import createHttpError, { HttpError } from "http-errors"; 9 | import { extract_new_seasons } from "../../extracters/gogoanime/extracters"; 10 | import { NewSeason } from "../../types/gogoanime/anime"; 11 | 12 | export const scrapeNewSeasons = async ( 13 | page: number, 14 | ): Promise => { 15 | const URLs = await URL_fn(); 16 | try { 17 | let res: NewSeason[] = []; 18 | const mainPage = await axios.get( 19 | `${URLs.NEW_SEASON}?page=${page}`, 20 | { 21 | headers: { 22 | "User-Agent": headers.USER_AGENT_HEADER, 23 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 24 | Accept: headers.ACCEPT_HEADER, 25 | }, 26 | }, 27 | ); 28 | 29 | const $: CheerioAPI = load(mainPage.data); 30 | 31 | const recentReleasesSelectors: SelectorType = 32 | "div.last_episodes > ul > li"; 33 | 34 | res = extract_new_seasons( 35 | $, 36 | recentReleasesSelectors, 37 | URLs.BASE, 38 | ); 39 | 40 | return res; 41 | } catch (err) { 42 | //////////////////////////////////////////////////////////////// 43 | console.error("Error in scrapeNewSeasons :", err); // for TESTING// 44 | //////////////////////////////////////////////////////////////// 45 | 46 | if (err instanceof AxiosError) { 47 | throw createHttpError( 48 | err?.response?.status || 500, 49 | err?.response?.statusText || "Something went wrong", 50 | ); 51 | } else { 52 | throw createHttpError.InternalServerError("Internal server error"); 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/anime-movies.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/gogoanime/constants"; 4 | import { headers } from "../../config/headers"; 5 | import axios, { AxiosError } from "axios"; 6 | import { load } from "cheerio"; 7 | import type { CheerioAPI, SelectorType } from "cheerio"; 8 | import createHttpError, { HttpError } from "http-errors"; 9 | import { extract_anime_movies } from "../../extracters/gogoanime/extracters"; 10 | import { AnimeMovie } from "../../types/gogoanime/anime"; 11 | 12 | export const scrapeAnimeMovies = async ( 13 | page: number, 14 | ): Promise => { 15 | const URLs = await URL_fn(); 16 | try { 17 | let res: AnimeMovie[] = []; 18 | const mainPage = await axios.get( 19 | `${URLs.MOVIES}?page=${page}`, 20 | { 21 | headers: { 22 | "User-Agent": headers.USER_AGENT_HEADER, 23 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 24 | Accept: headers.ACCEPT_HEADER, 25 | }, 26 | }, 27 | ); 28 | 29 | const $: CheerioAPI = load(mainPage.data); 30 | 31 | const animeMoviesSelectors: SelectorType = 32 | "div.last_episodes > ul > li"; 33 | 34 | res = extract_anime_movies( 35 | $, 36 | animeMoviesSelectors, 37 | URLs.BASE, 38 | ); 39 | 40 | return res; 41 | } catch (err) { 42 | //////////////////////////////////////////////////////////////// 43 | console.error("Error in scrapeAnimeMovies :", err); // for TESTING// 44 | //////////////////////////////////////////////////////////////// 45 | 46 | if (err instanceof AxiosError) { 47 | throw createHttpError( 48 | err?.response?.status || 500, 49 | err?.response?.statusText || "Something went wrong", 50 | ); 51 | } else { 52 | throw createHttpError.InternalServerError("Internal server error"); 53 | } 54 | } 55 | }; 56 | 57 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/atozAnimes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError, { HttpError } from "http-errors"; 3 | import { 4 | URL_fn, 5 | } from "../../utils/aniwatch/constants"; 6 | import { headers } from "../../config/headers"; 7 | import axios, { AxiosError } from "axios"; 8 | import { load } from "cheerio"; 9 | import { 10 | extract_atoz_animes 11 | } from "../../extracters/aniwatch/extracters"; 12 | import { Anime } from "../../types/aniwatch/anime"; 13 | 14 | export const scrapeatozAnimes = async ( 15 | page: number, 16 | ): Promise => { 17 | let res: Anime[] = []; 18 | 19 | try { 20 | const URLs = await URL_fn(); 21 | const scrapeUrl = new URL("az-list", URLs.BASE); 22 | 23 | const mainPage = await axios.get(`${scrapeUrl}/?page=${page}`, { 24 | headers: { 25 | "User-Agent": headers.USER_AGENT_HEADER, 26 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 27 | Accept: headers.ACCEPT_HEADER, 28 | }, 29 | }); 30 | 31 | const $: CheerioAPI = load(mainPage.data); 32 | const selectors: SelectorType = 33 | "#main-wrapper div div.page-az-wrap section div.tab-content div div.film_list-wrap .flw-item"; 34 | res = extract_atoz_animes($, selectors); 35 | 36 | return res; 37 | } catch (err) { 38 | //////////////////////////////////////////////////////////////// 39 | console.error("Error in scrapeatozAnimes :", err); // for TESTING// 40 | //////////////////////////////////////////////////////////////// 41 | 42 | if (err instanceof AxiosError) { 43 | throw createHttpError( 44 | err?.response?.status || 500, 45 | err?.response?.statusText || "Something went wrong", 46 | ); 47 | } else { 48 | throw createHttpError.InternalServerError("Internal server error"); 49 | } 50 | } 51 | }; 52 | 53 | export default scrapeatozAnimes; 54 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/top-airing.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/gogoanime/constants"; 4 | import { headers } from "../../config/headers"; 5 | import axios, { AxiosError } from "axios"; 6 | import { load } from "cheerio"; 7 | import type { CheerioAPI, SelectorType } from "cheerio"; 8 | import createHttpError, { HttpError } from "http-errors"; 9 | import { extract_top_airing } from "../../extracters/gogoanime/extracters"; 10 | import { TopAiring } from "../../types/gogoanime/anime"; 11 | 12 | export const scrapeTopAiring = async ( 13 | page: number, 14 | ): Promise => { 15 | const URLs = await URL_fn(); 16 | try { 17 | let res: TopAiring[] = []; 18 | const mainPage = await axios.get( 19 | `${URLs.AJAX}/page-recent-release-ongoing.html 20 | ?page=${page}`, 21 | { 22 | headers: { 23 | "User-Agent": headers.USER_AGENT_HEADER, 24 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 25 | Accept: headers.ACCEPT_HEADER, 26 | }, 27 | }, 28 | ); 29 | 30 | const $: CheerioAPI = load(mainPage.data); 31 | 32 | const topAiringSelectors: SelectorType = 33 | "div.added_series_body.popular > ul > li"; 34 | 35 | res = extract_top_airing( 36 | $, 37 | topAiringSelectors, 38 | URLs.BASE, 39 | ); 40 | 41 | return res; 42 | } catch (err) { 43 | //////////////////////////////////////////////////////////////// 44 | console.error("Error in scrapeTopAiring :", err); // for TESTING// 45 | //////////////////////////////////////////////////////////////// 46 | 47 | if (err instanceof AxiosError) { 48 | throw createHttpError( 49 | err?.response?.status || 500, 50 | err?.response?.statusText || "Something went wrong", 51 | ); 52 | } else { 53 | throw createHttpError.InternalServerError("Internal server error"); 54 | } 55 | } 56 | }; 57 | 58 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/recent-releases.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/gogoanime/constants"; 4 | import { headers } from "../../config/headers"; 5 | import axios, { AxiosError } from "axios"; 6 | import { load } from "cheerio"; 7 | import type { CheerioAPI, SelectorType } from "cheerio"; 8 | import createHttpError, { HttpError } from "http-errors"; 9 | import { extract_latest_episodes } from "../../extracters/gogoanime/extracters"; 10 | import { RecentRelease } from "../../types/gogoanime/anime"; 11 | 12 | export const scrapeRecentReleases = async ( 13 | page: number, 14 | ): Promise => { 15 | const URLs = await URL_fn(); 16 | try { 17 | let res: RecentRelease[] = []; 18 | const mainPage = await axios.get( 19 | `${URLs.AJAX}/page-recent-release.html?page=${page}`, 20 | { 21 | headers: { 22 | "User-Agent": headers.USER_AGENT_HEADER, 23 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 24 | Accept: headers.ACCEPT_HEADER, 25 | }, 26 | }, 27 | ); 28 | 29 | const $: CheerioAPI = load(mainPage.data); 30 | 31 | const recentReleasesSelectors: SelectorType = 32 | "div.last_episodes.loaddub > ul > li"; 33 | 34 | res = extract_latest_episodes( 35 | $, 36 | recentReleasesSelectors, 37 | URLs.BASE, 38 | ); 39 | 40 | return res; 41 | } catch (err) { 42 | //////////////////////////////////////////////////////////////// 43 | console.error("Error in scrapeRecentReleases :", err); // for TESTING// 44 | //////////////////////////////////////////////////////////////// 45 | 46 | if (err instanceof AxiosError) { 47 | throw createHttpError( 48 | err?.response?.status || 500, 49 | err?.response?.statusText || "Something went wrong", 50 | ); 51 | } else { 52 | throw createHttpError.InternalServerError("Internal server error"); 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/utils/aniwatch/proxy.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosAdapter, AxiosInstance } from "axios"; 2 | 3 | import { ProxyConfig } from "../../types/aniwatch/anime"; 4 | 5 | export class Proxy { 6 | /** 7 | * 8 | * @param proxyConfig The proxy config (optional) 9 | * @param adapter The axios adapter (optional) 10 | */ 11 | constructor( 12 | protected proxyConfig?: ProxyConfig, 13 | protected adapter?: AxiosAdapter, 14 | ) { 15 | this.client = axios.create(); 16 | 17 | if (proxyConfig) this.setProxy(proxyConfig); 18 | if (adapter) this.setAxiosAdapter(adapter); 19 | } 20 | private validUrl = /^https?:\/\/.+/; 21 | /** 22 | * Set or Change the proxy config 23 | */ 24 | setProxy(proxyConfig: ProxyConfig) { 25 | if (!proxyConfig?.url) return; 26 | 27 | if (typeof proxyConfig?.url === "string") 28 | if (!this.validUrl.test(proxyConfig.url)) 29 | throw new Error("Proxy URL is invalid!"); 30 | 31 | if (Array.isArray(proxyConfig?.url)) { 32 | for (const [i, url] of this.toMap(proxyConfig.url)) 33 | if (!this.validUrl.test(url)) 34 | throw new Error(`Proxy URL at index ${i} is invalid!`); 35 | 36 | this.rotateProxy({ ...proxyConfig, urls: proxyConfig.url }); 37 | } 38 | } 39 | 40 | /** 41 | * Set or Change the axios adapter 42 | */ 43 | setAxiosAdapter(adapter: AxiosAdapter) { 44 | this.client.defaults.adapter = adapter; 45 | } 46 | private rotateProxy = ( 47 | proxy: Omit & { urls: string[] }, 48 | ) => { 49 | setInterval(() => { 50 | const url = proxy.urls.shift(); 51 | if (url) proxy.urls.push(url); 52 | 53 | this.setProxy({ url: proxy.urls[0], key: proxy.key }); 54 | }, proxy?.rotateInterval ?? 5000); 55 | }; 56 | 57 | private toMap = (arr: T[]): [number, T][] => arr.map((v, i) => [i, v]); 58 | 59 | protected client: AxiosInstance; 60 | } 61 | 62 | export default Proxy; 63 | -------------------------------------------------------------------------------- /src/utils/aniwatch/constants.ts: -------------------------------------------------------------------------------- 1 | import { isSiteReachable } from "../../lib/isSiteReachable"; 2 | import { websites_collection, AnimeWebsiteConfig } from "../../config/websites"; 3 | 4 | type AniWatchConfig = { 5 | BASE: string; 6 | HOME: string; 7 | SEARCH: string; 8 | GENRE: string; 9 | AJAX: string; 10 | }; 11 | 12 | const aniwatch: AnimeWebsiteConfig = websites_collection["AniWatch"]; 13 | // storing initial base link 14 | let aniwatch_base = aniwatch.BASE; 15 | // array of clones 16 | let clones_array: string[] = []; 17 | clones_array.push(aniwatch_base); 18 | 19 | if (aniwatch.CLONES) { 20 | const aniwatch_clones: Record = aniwatch.CLONES; 21 | 22 | for (const key in aniwatch_clones) { 23 | if (Object.prototype.hasOwnProperty.call(aniwatch_clones, key)) { 24 | const values: string[] = aniwatch_clones[key]; 25 | clones_array.push(...values); 26 | } 27 | } 28 | } 29 | 30 | // Testing 31 | // console.log(clones_array); 32 | 33 | // make new aniwatchobj using new aniwatch_base 34 | const makeAniWatchObj = (aniwatch_base: string): AniWatchConfig => { 35 | // Testing 36 | // console.log(aniwatch_base); 37 | return { 38 | BASE: aniwatch_base, 39 | HOME: `${aniwatch_base}/home`, 40 | SEARCH: `${aniwatch_base}/search`, 41 | GENRE: `${aniwatch_base}/genre`, 42 | AJAX: `${aniwatch_base}/ajax`, 43 | }; 44 | }; 45 | 46 | export const DEFAULT_HIANIME_URL = "https://hianime.to"; 47 | 48 | // return fn 49 | const URL_fn = async (): Promise => { 50 | try { 51 | for (const url of clones_array) { 52 | if (await isSiteReachable(url as string)) { 53 | aniwatch_base = url; 54 | break; 55 | } 56 | } 57 | return makeAniWatchObj(aniwatch_base as string); 58 | } catch (error) { 59 | console.error("Error occurred in both sites:", error); 60 | throw error; // Rethrow the error to handle it outside 61 | } 62 | }; 63 | 64 | export { URL_fn }; 65 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/about_extra_anime.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { ExtraAboutAnimeInfo } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_extra_about_info = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): ExtraAboutAnimeInfo => { 10 | try { 11 | const moreInfo: ExtraAboutAnimeInfo = {}; 12 | const genres: string[] = []; 13 | const producers: string[] = []; 14 | 15 | $(selectors + " .item-title").each((_index, element) => { 16 | const animeKEY: string = 17 | $(element).find(".item-head")?.text()?.trim() ?? "UNKNOWN"; 18 | const animeVALUE = $(element).find(".name")?.text()?.trim() ?? "UNKNOWN"; 19 | 20 | if (animeKEY !== "Producers:" && animeKEY !== "Overview:") { 21 | moreInfo[animeKEY] = animeVALUE; 22 | } else if (animeKEY === "Producers:") { 23 | $(selectors + " .item-title a").each((_index, element) => { 24 | const animeProducers = $(element)?.text()?.trim() ?? "UNKNOWN"; 25 | producers.push(animeProducers); 26 | }); 27 | } 28 | }); 29 | 30 | $(selectors + " .item-list a").each((_index, element) => { 31 | const animeGENRES = $(element)?.text()?.trim() ?? "UNKNOWN"; 32 | genres.push(animeGENRES); 33 | }); 34 | 35 | moreInfo["Genres"] = genres; 36 | moreInfo["Producers"] = producers; 37 | 38 | return moreInfo; 39 | } catch (err) { 40 | /////////////////////////////////////////////////////////////////// 41 | console.error("Error in extract_extra_about_info :", err); // for TESTING// 42 | /////////////////////////////////////////////////////////////////// 43 | 44 | if (err instanceof AxiosError) { 45 | throw createHttpError( 46 | err?.response?.status || 500, 47 | err?.response?.statusText || "Something went wrong", 48 | ); 49 | } else { 50 | throw createHttpError.InternalServerError("Internal server error"); 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/top_airing.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { TopAiring } from "../../types/gogoanime/anime"; 5 | 6 | export const extract_top_airing = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | url_base: string, 10 | ): TopAiring[] => { 11 | try { 12 | const animes: TopAiring[] = []; 13 | $(selectors).each((_index, element: any) => { 14 | let genres: string[] = []; 15 | $(element) 16 | .find('p.genres > a') 17 | .each((_index, element) => { 18 | genres.push($(element)?.attr('title') ?? ""); 19 | }); 20 | const animeID = 21 | $(element).find('a:nth-child(1)')?.attr('href')?.split('/')[2] ?? "UNKNOWN"; 22 | const animeNAME = 23 | $(element).find('a:nth-child(1)')?.attr('title') ?? "UNKNOWN"; 24 | const animeIMG = $(element) 25 | .find('a:nth-child(1) > div') 26 | ?.attr('style') 27 | ?.match('(https?://.*.(?:png|jpg))')?.[0] || "UNKNOWN"; 28 | const latestEpesiode = $(element).find('p:nth-child(4) > a').text().trim(); 29 | const animeUrl = url_base + '/' + $(element).find('a:nth-child(1)').attr('href'); 30 | 31 | animes.push({ 32 | id: animeID, 33 | name: animeNAME, 34 | img: animeIMG, 35 | latestEp: latestEpesiode, 36 | animeUrl: animeUrl, 37 | genres: genres 38 | }); 39 | }); 40 | return animes; 41 | } catch (err) { 42 | //////////////////////////////////////////////////////////////// 43 | console.error("Error in extract_top_airing :", err); // for TESTING// 44 | //////////////////////////////////////////////////////////////// 45 | 46 | if (err instanceof AxiosError) { 47 | throw createHttpError( 48 | err?.response?.status || 500, 49 | err?.response?.statusText || "Something went wrong", 50 | ); 51 | } else { 52 | throw createHttpError.InternalServerError("Internal server error"); 53 | } 54 | } 55 | }; 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/episodes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/aniwatch/constants"; 4 | import { headers } from "../../config/headers"; 5 | import axios, { AxiosError } from "axios"; 6 | import createHttpError, { HttpError } from "http-errors"; 7 | import { load } from "cheerio"; 8 | import type { CheerioAPI, SelectorType } from "cheerio"; 9 | import { ScrapedEpisodesPage } from "../../types/aniwatch/anime"; 10 | import { extract_episodes_info } from "../../extracters/aniwatch/extracters"; 11 | 12 | export const scrapeEpisodesPage = async ( 13 | animeId: string, 14 | ): Promise => { 15 | const res: ScrapedEpisodesPage = { 16 | totalEpisodes: 0, 17 | episodes: [], 18 | }; 19 | 20 | try { 21 | const URLs = await URL_fn(); 22 | const episodes = await axios.get( 23 | `${URLs.AJAX}/v2/episode/list/${animeId.split("-").pop()}`, 24 | { 25 | headers: { 26 | "User-Agent": headers.USER_AGENT_HEADER, 27 | "X-Requested-With": "XMLHttpRequest", 28 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 29 | Accept: headers.ACCEPT_HEADER, 30 | Referer: `${URLs.BASE}/watch/${animeId}`, 31 | }, 32 | }, 33 | ); 34 | 35 | const $: CheerioAPI = load(episodes.data.html); 36 | const selectors: SelectorType = ".detail-infor-content .ss-list a"; 37 | 38 | res.totalEpisodes = Number($(`${selectors}`).length); 39 | res.episodes = extract_episodes_info($, selectors); 40 | 41 | return res; 42 | } catch (err) { 43 | //////////////////////////////////////////////////////////////// 44 | console.error("Error in scrapeEpisodesPage :", err); // for TESTING// 45 | //////////////////////////////////////////////////////////////// 46 | 47 | if (err instanceof AxiosError) { 48 | throw createHttpError( 49 | err?.response?.status || 500, 50 | err?.response?.statusText || "Something went wrong", 51 | ); 52 | } else { 53 | throw createHttpError.InternalServerError("Internal server error"); 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/extract_recent_released_home.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { CheerioAPI, SelectorType } from "cheerio"; 3 | import createHttpError from "http-errors"; 4 | import type { RecentRelease } from "../../types/gogoanime/anime"; 5 | 6 | export const extract_recent_released_home = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): RecentRelease[] => { 10 | try { 11 | let recentReleases: RecentRelease[] = []; 12 | 13 | $(selectors).each((_index, _element) => { 14 | const animeNAME = 15 | $(_element).find(".name a")?.text()?.trim() ?? "UNKNOWN ANIME"; 16 | 17 | const animeIMG = 18 | $(_element).find(".img img")?.attr("src")?.trim() ?? null; 19 | 20 | const id = animeIMG?.split("/").pop()?.split(".")[0] ?? ""; 21 | 22 | const episodeNo = 23 | Number( 24 | $(_element).find("p.episode")?.text()?.trim().split(" ").pop(), 25 | ) ?? 0; 26 | 27 | const episodeUrl = 28 | $(_element).find("p.name a")?.attr("href")?.trim() ?? ""; 29 | 30 | const episodeId = episodeUrl?.split("/")?.pop() ?? ""; 31 | 32 | const subOrDub = $(_element).find(".type")?.hasClass("ic-SUB") 33 | ? "SUB" 34 | : "DUB"; 35 | 36 | let anime: RecentRelease = { 37 | id: id, 38 | name: animeNAME, 39 | img: animeIMG, 40 | episodeId: episodeId, 41 | episodeNo: episodeNo, 42 | subOrDub: subOrDub, 43 | episodeUrl: episodeUrl, 44 | }; 45 | recentReleases.push(anime); 46 | }); 47 | 48 | return recentReleases; 49 | } catch (err) { 50 | /////////////////////////////////////////////////////////////////// 51 | console.error("Error in extract_recent_released_home :", err); // for TESTING// 52 | /////////////////////////////////////////////////////////////////// 53 | 54 | if (err instanceof AxiosError) { 55 | throw createHttpError( 56 | err?.response?.status || 500, 57 | err?.response?.statusText || "Something went wrong", 58 | ); 59 | } else { 60 | throw createHttpError.InternalServerError("Internal server error"); 61 | } 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/routes/aniwatch/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router, type IRouter } from "express"; 2 | import { 3 | getHomePageInfo, 4 | getAboutPageInfo, 5 | getSearchPageInfo, 6 | getCategoryPage, 7 | getEpisodesInfo, 8 | getEpisodeServersInfo, 9 | getAnimeEpisodeSourcesInfo, 10 | getatozPage, 11 | } from "../../controllers/aniwatch/controllers"; 12 | import { cacheManager } from "../../middlewares/cache"; 13 | 14 | const aniwatch_router: IRouter = Router(); 15 | 16 | // /aniwatch/ 17 | aniwatch_router.get("/", cacheManager.middleware(), getHomePageInfo); 18 | 19 | // aniwatch/az-list?page=${page} 20 | aniwatch_router.get( 21 | "/az-list", 22 | cacheManager.middleware({ 23 | duration: 3600 * 24, // 1 day cache 24 | keyParams: ["page"], 25 | }), 26 | getatozPage, 27 | ); 28 | 29 | // /aniwatch/search?keyword=$(query)&page=${page} 30 | aniwatch_router.get( 31 | "/search", 32 | cacheManager.middleware({ 33 | duration: 3600, // 1 hour cache 34 | keyParams: ["keyword", "page"], 35 | }), 36 | getSearchPageInfo, 37 | ); 38 | 39 | // /aniwatch/anime/:id 40 | aniwatch_router.get( 41 | "/anime/:id", 42 | cacheManager.middleware({ 43 | duration: 3600 * 24 * 31, // 1 month cache 44 | }), 45 | getAboutPageInfo, 46 | ); 47 | 48 | // /aniwatch/episodes/:id 49 | aniwatch_router.get( 50 | "/episodes/:id", 51 | cacheManager.middleware({ 52 | duration: 3600 * 24, // 1 day cache 53 | }), 54 | getEpisodesInfo, 55 | ); 56 | 57 | // /aniwatch/servers?id=${id} 58 | aniwatch_router.get( 59 | "/servers", 60 | cacheManager.middleware(), 61 | getEpisodeServersInfo, 62 | ); 63 | 64 | // /aniwatch/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)} 65 | aniwatch_router.get( 66 | "/episode-srcs", 67 | cacheManager.middleware({ 68 | duration: 1800, // 30 minutes cache 69 | keyParams: ["id", "category", "server"], 70 | }), 71 | getAnimeEpisodeSourcesInfo, 72 | ); 73 | 74 | // aniwatch/:category?page=${page} 75 | aniwatch_router.get( 76 | "/:category", 77 | cacheManager.middleware({ 78 | duration: 3600 * 24, // 1 day cache 79 | keyParams: ["page"], 80 | }), 81 | getCategoryPage, 82 | ); 83 | 84 | export default aniwatch_router; 85 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/about.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from "axios"; 2 | import { URL_fn } from "../../utils/gogoanime/constants"; 3 | import { headers } from "../../config/headers"; 4 | import { CheerioAPI, load, SelectorType } from "cheerio"; 5 | import { extract_about_info } from "../../extracters/gogoanime/about_anime"; 6 | import createHttpError, { HttpError } from "http-errors"; 7 | import { AboutAnimeInfo } from "../../types/gogoanime/anime"; 8 | import { ScrapedAboutPage } from "../../types/gogoanime/about"; 9 | 10 | export const scrapeAboutPage = async ( 11 | id: string 12 | ): Promise => { 13 | const defaultInfo: AboutAnimeInfo = { 14 | name: null, 15 | img: null, 16 | type: null, 17 | genre: null, 18 | status: null, 19 | aired_in: null, 20 | other_name: null, 21 | episodes: null, 22 | }; 23 | 24 | const res: ScrapedAboutPage = { 25 | id: id, 26 | anime_id: "", 27 | info: defaultInfo, 28 | }; 29 | 30 | const URLs = await URL_fn(); 31 | const aboutURL: string = new URL(id, URLs.CATEGORY).toString(); 32 | 33 | const mainPage = await axios.get(aboutURL, { 34 | headers: { 35 | "User-Agent": headers.USER_AGENT_HEADER, 36 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 37 | Accept: headers.ACCEPT_HEADER, 38 | }, 39 | }); 40 | 41 | const $: CheerioAPI = load(mainPage.data); 42 | const selectors: SelectorType = ".main_body"; 43 | 44 | try { 45 | const animeId = 46 | $('input#movie_id.movie_id[type="hidden"]').attr('value') ?? ''; 47 | res.anime_id = animeId; 48 | res.info = extract_about_info($, selectors); 49 | return res; 50 | } catch (err) { 51 | //////////////////////////////////////////////////////////////// 52 | console.error("Error in scrapeAboutPage :", err); // for TESTING// 53 | //////////////////////////////////////////////////////////////// 54 | 55 | if (err instanceof AxiosError) { 56 | throw createHttpError( 57 | err?.response?.status || 500, 58 | err?.response?.statusText || "Something went wrong" 59 | ); 60 | } else { 61 | throw createHttpError.InternalServerError("Internal server error"); 62 | } 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/recent_released_episodes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { RecentRelease } from "../../types/gogoanime/anime"; 5 | 6 | export const extract_latest_episodes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | url_base: string, 10 | ): RecentRelease[] => { 11 | try { 12 | const animes: RecentRelease[] = []; 13 | $(selectors).each((_index, element: any) => { 14 | const animeID = 15 | $(element) 16 | .find("p.name > a") 17 | ?.attr("href") 18 | ?.split("/")[1] 19 | ?.split("-episode-")[0] ?? "UNKNOWN"; 20 | const episodeId = 21 | $(element).find("p.name > a")?.attr("href")?.split("/")[1] ?? "UNKNOWN"; 22 | const animeNAME = 23 | $(element).find("p.name > a")?.attr("title") ?? "UNKNOWN"; 24 | const episodeNo = Number( 25 | $(element).find("p.episode").text().replace("Episode ", "").trim(), 26 | ); 27 | const subOrDub = 28 | $(element) 29 | .find("div > a > div") 30 | ?.attr("class") 31 | ?.replace("type ic-", "") || "UNKNOWN"; 32 | const animeIMG = $(element).find("div > a > img")?.attr("src") ?? "SUB"; 33 | const episodeUrl = 34 | url_base + "/" + $(element).find("p.name > a").attr("href"); 35 | 36 | animes.push({ 37 | id: animeID, 38 | name: animeNAME, 39 | img: animeIMG, 40 | episodeId: episodeId, 41 | episodeNo: episodeNo, 42 | episodeUrl: episodeUrl, 43 | subOrDub: subOrDub, 44 | }); 45 | }); 46 | return animes; 47 | } catch (err) { 48 | //////////////////////////////////////////////////////////////// 49 | console.error("Error in extract_latest_episodes :", err); // for TESTING// 50 | //////////////////////////////////////////////////////////////// 51 | 52 | if (err instanceof AxiosError) { 53 | throw createHttpError( 54 | err?.response?.status || 500, 55 | err?.response?.statusText || "Something went wrong", 56 | ); 57 | } else { 58 | throw createHttpError.InternalServerError("Internal server error"); 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/utils/gogoanime/constants.ts: -------------------------------------------------------------------------------- 1 | import { isSiteReachable } from "../../lib/isSiteReachable"; 2 | import { websites_collection, AnimeWebsiteConfig } from "../../config/websites"; 3 | 4 | type GogoAnimeConfig = { 5 | BASE: string; 6 | HOME: string; 7 | SEARCH: string; 8 | CATEGORY: string; 9 | MOVIES: string; 10 | POPULAR: string; 11 | NEW_SEASON: string; 12 | SEASONS: string; 13 | COMPLETED: string; 14 | AJAX: string; 15 | }; 16 | 17 | const gogoanime: AnimeWebsiteConfig = websites_collection["GogoAnime"]; 18 | // storing initial base link 19 | let gogoanime_base = gogoanime.BASE; 20 | // array of clones 21 | let clones_array: string[] = []; 22 | clones_array.push(gogoanime_base); 23 | 24 | if (gogoanime.CLONES) { 25 | const gogoanime_clones: Record = gogoanime.CLONES; 26 | 27 | for (const key in gogoanime_clones) { 28 | if (Object.prototype.hasOwnProperty.call(gogoanime_clones, key)) { 29 | const values: string[] = gogoanime_clones[key]; 30 | clones_array.push(...values); 31 | } 32 | } 33 | } 34 | 35 | // Testing 36 | // console.log(clones_array); 37 | 38 | // make new gogoanimeobj using new gogoanime_base 39 | const makeGogoAnimeObj = (gogoanime_base: string): GogoAnimeConfig => { 40 | // Testing 41 | // console.log(gogoanime_base); 42 | return { 43 | BASE: gogoanime.BASE, 44 | HOME: `${gogoanime_base}/home.html`, 45 | SEARCH: `${gogoanime_base}/search.html`, 46 | CATEGORY: `${gogoanime_base}/category/`, 47 | MOVIES: `${gogoanime_base}/anime-movies.html`, 48 | POPULAR: `${gogoanime_base}/popular.html`, 49 | NEW_SEASON: `${gogoanime_base}/new-season.html`, 50 | SEASONS: `${gogoanime_base}/sub-category/`, 51 | COMPLETED: `${gogoanime_base}/completed-anime.html`, 52 | AJAX: "https://ajax.gogocdn.net/ajax", 53 | }; 54 | }; 55 | 56 | // return fn 57 | const URL_fn = async (): Promise => { 58 | try { 59 | for (const url of clones_array) { 60 | if (await isSiteReachable(url as string)) { 61 | gogoanime_base = url; 62 | break; 63 | } 64 | } 65 | return makeGogoAnimeObj(gogoanime_base as string); 66 | } catch (error) { 67 | console.error("Error occurred in both sites:", error); 68 | throw error; // Rethrow the error to handle it outside 69 | } 70 | }; 71 | 72 | export { URL_fn }; 73 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/search.ts: -------------------------------------------------------------------------------- 1 | import createHttpError, { HttpError } from "http-errors"; 2 | import { ScrapedSearchPage } from "../../types/gogoanime/search"; 3 | import { URL_fn } from "../../utils/gogoanime/constants"; 4 | import axios, { AxiosError } from "axios"; 5 | import { headers } from "../../config/headers"; 6 | import { CheerioAPI, load, SelectorType } from "cheerio"; 7 | import { extract_searched_animes } from "../../extracters/gogoanime/searched_animes"; 8 | 9 | export const scrapeSearchPage = async ( 10 | query: string, 11 | page: number 12 | ): Promise => { 13 | const res: ScrapedSearchPage = { 14 | animes: [], 15 | currentPage: Number(page), 16 | hasNextPage: false, 17 | totalPages: 1, 18 | }; 19 | 20 | try { 21 | const URLs = await URL_fn(); 22 | const mainPage = await axios.get( 23 | `${URLs.SEARCH}?keyword=${query}&page=${page}`, 24 | { 25 | headers: { 26 | "User-Agent": headers.USER_AGENT_HEADER, 27 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 28 | Accept: headers.ACCEPT_HEADER, 29 | }, 30 | } 31 | ); 32 | 33 | const $: CheerioAPI = load(mainPage.data); 34 | 35 | const selectors: SelectorType = ".main_body .last_episodes .items li"; 36 | 37 | res.animes = extract_searched_animes($, selectors); 38 | res.hasNextPage = 39 | $("li.selected").length > 0 40 | ? $("li.selected").length > 0 41 | ? $("li.selected").next().length > 0 42 | : false 43 | : false; 44 | 45 | res.totalPages = 46 | Number( 47 | $('li a[href*="page"]:last')?.attr("href")?.split("=").pop() ?? 48 | $('li a[href*="page"]:last')?.attr("href")?.split("=").pop() ?? 49 | $("li.selected a")?.text()?.trim() 50 | ) || 1; 51 | 52 | return res; 53 | } catch (err) { 54 | //////////////////////////////////////////////////////////////// 55 | console.error("Error in scrapeSearchPage :", err); // for TESTING// 56 | //////////////////////////////////////////////////////////////// 57 | 58 | if (err instanceof AxiosError) { 59 | throw createHttpError( 60 | err?.response?.status || 500, 61 | err?.response?.statusText || "Something went wrong" 62 | ); 63 | } else { 64 | throw createHttpError.InternalServerError("Internal server error"); 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/controllers/aniwatch/episodeServerSourcesController.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import createHttpError from "http-errors"; 3 | import { type RequestHandler } from "express"; 4 | import { type CheerioAPI, load } from "cheerio"; 5 | import { scrapeAnimeEpisodeSources } from "../../scrapers/aniwatch/scrapers"; 6 | import { URL_fn } from "../../utils/aniwatch/constants"; 7 | import { headers } from "../../config/headers"; 8 | import { type AnimeServers, Servers } from "../../types/aniwatch/anime"; 9 | 10 | type AnilistID = number | null; 11 | type MalID = number | null; 12 | 13 | const getAnimeEpisodeSourcesInfo: RequestHandler = async (req, res) => { 14 | const URLs = await URL_fn(); 15 | try { 16 | const episodeId = req.query.id 17 | ? decodeURIComponent(req.query.id as string) 18 | : null; 19 | 20 | const server = ( 21 | req.query.server 22 | ? decodeURIComponent(req.query.server as string) 23 | : Servers.VidStreaming 24 | ) as AnimeServers; 25 | 26 | const category = ( 27 | req.query.category 28 | ? decodeURIComponent(req.query.category as string) 29 | : "sub" 30 | ) as "sub" | "dub"; 31 | 32 | if (episodeId === null) { 33 | throw createHttpError.BadRequest("Anime episode id required"); 34 | } 35 | 36 | let malID: MalID; 37 | let anilistID: AnilistID; 38 | const animeURL = new URL(episodeId?.split("?ep=")[0], URLs.BASE)?.href; 39 | 40 | const [episodeSrcData, animeSrc] = await Promise.all([ 41 | scrapeAnimeEpisodeSources(episodeId, server, category), 42 | axios.get(animeURL, { 43 | headers: { 44 | Referer: URLs.BASE, 45 | "User-Agent": headers.USER_AGENT_HEADER, 46 | "X-Requested-With": "XMLHttpRequest", 47 | }, 48 | }), 49 | ]); 50 | 51 | const $: CheerioAPI = load(animeSrc?.data); 52 | 53 | try { 54 | anilistID = Number( 55 | JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id, 56 | ); 57 | malID = Number(JSON.parse($("body")?.find("#syncData")?.text())?.mal_id); 58 | } catch (err) { 59 | anilistID = null; 60 | malID = null; 61 | } 62 | 63 | res.status(200).json({ 64 | ...episodeSrcData, 65 | anilistID, 66 | malID, 67 | }); 68 | } catch (err) { 69 | //////////////////////////////////// 70 | console.log(err); // for TESTING// 71 | //////////////////////////////////// 72 | } 73 | }; 74 | 75 | export { getAnimeEpisodeSourcesInfo }; 76 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/top_upcoming_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { TopUpcomingAnime } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_top_upcoming_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): TopUpcomingAnime[] => { 10 | try { 11 | const animes: TopUpcomingAnime[] = []; 12 | $(selectors).each((_index, element) => { 13 | const animeID = 14 | $(element) 15 | .find(".film-detail .film-name .dynamic-name") 16 | ?.attr("href") 17 | ?.slice(1) || null; 18 | const animeNAME = 19 | $(element) 20 | .find(".film-detail .film-name .dynamic-name") 21 | ?.text() 22 | ?.trim() ?? "UNKNOWN ANIME"; 23 | const noOfSubEps = 24 | Number($(element).find(".film-poster .tick .tick-sub")?.text()) || null; 25 | const noOfDubEps = 26 | Number($(element).find(".film-poster .tick .tick-dub")?.text()) || null; 27 | const totalNoOfEps = 28 | Number($(element).find(".film-poster .tick .tick-eps")?.text()) || null; 29 | const epLengthTime = 30 | $(element) 31 | .find(".film-detail .fd-infor .fdi-duration") 32 | ?.text() 33 | ?.trim() ?? "UNKNOWN"; 34 | const adultRated = 35 | $(element).find(".film-poster .tick-rate")?.text()?.trim() || null; 36 | const animeIMG = 37 | $(element).find(".film-poster .film-poster-img").attr("data-src") || 38 | null; 39 | 40 | animes.push({ 41 | id: animeID, 42 | name: animeNAME, 43 | img: animeIMG, 44 | episodes: { 45 | eps: totalNoOfEps, 46 | sub: noOfSubEps, 47 | dub: noOfDubEps, 48 | }, 49 | duration: epLengthTime, 50 | rated: adultRated === "18+", 51 | }); 52 | }); 53 | return animes; 54 | } catch (err) { 55 | //////////////////////////////////////////////////////////////// 56 | console.error("Error in extract_top_upcoming_animes :", err); // for TESTING// 57 | //////////////////////////////////////////////////////////////// 58 | 59 | if (err instanceof AxiosError) { 60 | throw createHttpError( 61 | err?.response?.status || 500, 62 | err?.response?.statusText || "Something went wrong", 63 | ); 64 | } else { 65 | throw createHttpError.InternalServerError("Internal server error"); 66 | } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/top10_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { Top10Anime } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_top10_animes = ( 7 | $: CheerioAPI, 8 | periodType: SelectorType, 9 | ): Top10Anime[] => { 10 | try { 11 | const animes: Top10Anime[] = []; 12 | const selectors = `#top-viewed-${periodType} ul li`; 13 | 14 | $(selectors).each((_index, element) => { 15 | const animeID = 16 | $(element) 17 | .find(".film-detail .film-name .dynamic-name") 18 | ?.attr("href") 19 | ?.slice(1) || null; 20 | 21 | const animeNAME = 22 | $(element) 23 | .find(".film-detail .film-name .dynamic-name") 24 | ?.text() 25 | ?.trim() ?? "UNKNOWN ANIME"; 26 | 27 | const animeRANK = 28 | Number($(element).find(".film-number span")?.text()?.trim()) || null; 29 | 30 | const noOfSubEps = 31 | Number( 32 | $(element).find(".film-detail .fd-infor .tick-item.tick-sub")?.text(), 33 | ) || null; 34 | 35 | const noOfDubEps = 36 | Number( 37 | $(element).find(".film-detail .fd-infor .tick-item.tick-dub")?.text(), 38 | ) || null; 39 | 40 | const totalNoOfEps = 41 | Number( 42 | $(element).find(".film-detail .fd-infor .tick-item.tick-eps")?.text(), 43 | ) || null; 44 | 45 | const animeIMG = 46 | $(element) 47 | .find(".film-poster .film-poster-img") 48 | ?.attr("data-src") 49 | ?.trim() || null; 50 | 51 | animes.push({ 52 | id: animeID, 53 | name: animeNAME, 54 | rank: animeRANK, 55 | img: animeIMG, 56 | episodes: { 57 | eps: totalNoOfEps, 58 | sub: noOfSubEps, 59 | dub: noOfDubEps, 60 | }, 61 | }); 62 | }); 63 | return animes; 64 | } catch (err) { 65 | ///////////////////////////////////////////////////////////////////// 66 | console.error("Error in extract_top10_animes :", err); // for TESTING// 67 | ///////////////////////////////////////////////////////////////////// 68 | 69 | if (err instanceof AxiosError) { 70 | throw createHttpError( 71 | err?.response?.status || 500, 72 | err?.response?.statusText || "Something went wrong", 73 | ); 74 | } else { 75 | throw createHttpError.InternalServerError("Internal server error"); 76 | } 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/mostpopular_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { MostPopularAnime } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_mostpopular_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): MostPopularAnime[] => { 10 | try { 11 | const animes: MostPopularAnime[] = []; 12 | 13 | $(selectors).each((_index, element) => { 14 | const animeID = 15 | $(element) 16 | .find(".film-detail .dynamic-name") 17 | ?.attr("href") 18 | ?.slice(1) 19 | .trim() || null; 20 | const animeNAME = 21 | $(element).find(".film-detail .dynamic-name")?.text()?.trim() ?? 22 | "UNKNOWN ANIME"; 23 | const animeIMG = 24 | $(element) 25 | .find(".film-poster .film-poster-img") 26 | ?.attr("data-src") 27 | ?.trim() || null; 28 | const epSUB = 29 | Number( 30 | $(element) 31 | .find(".fd-infor .tick .tick-item.tick-sub") 32 | ?.text() 33 | ?.trim(), 34 | ) || null; 35 | const epDUB = 36 | Number( 37 | $(element) 38 | .find(".fd-infor .tick .tick-item.tick-dub") 39 | ?.text() 40 | ?.trim(), 41 | ) || null; 42 | const total_eps = 43 | Number( 44 | $(element) 45 | .find(".fd-infor .tick .tick-item.tick-eps") 46 | ?.text() 47 | ?.trim(), 48 | ) || null; 49 | const animeTYPE = 50 | $(selectors) 51 | ?.find(".fd-infor .tick") 52 | ?.text() 53 | ?.trim() 54 | ?.replace(/[\s\n]+/g, " ") 55 | ?.split(" ") 56 | ?.pop() || null; 57 | 58 | animes.push({ 59 | id: animeID, 60 | name: animeNAME, 61 | category: animeTYPE, 62 | img: animeIMG, 63 | episodes: { 64 | eps: total_eps, 65 | sub: epSUB, 66 | dub: epDUB, 67 | }, 68 | }); 69 | }); 70 | 71 | return animes; 72 | } catch (err) { 73 | //////////////////////////////////////////////////////////////// 74 | console.error("Error in extract_mostpopular_animes :", err); // for TESTING// 75 | //////////////////////////////////////////////////////////////// 76 | 77 | if (err instanceof AxiosError) { 78 | throw createHttpError( 79 | err?.response?.status || 500, 80 | err?.response?.statusText || "Something went wrong", 81 | ); 82 | } else { 83 | throw createHttpError.InternalServerError("Internal server error"); 84 | } 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/related_animes.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { RelatedAnime } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_related_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): RelatedAnime[] => { 10 | try { 11 | const animes: RelatedAnime[] = []; 12 | 13 | $(selectors).each((_index, element) => { 14 | const animeID = 15 | $(element) 16 | .find(".film-detail .dynamic-name") 17 | ?.attr("href") 18 | ?.slice(1) 19 | .trim() || null; 20 | const animeNAME = 21 | $(element).find(".film-detail .dynamic-name")?.text()?.trim() ?? 22 | "UNKNOWN ANIME"; 23 | const animeIMG = 24 | $(element) 25 | .find(".film-poster .film-poster-img") 26 | ?.attr("data-src") 27 | ?.trim() || null; 28 | const epSUB = 29 | Number( 30 | $(selectors) 31 | .find(".fd-infor .tick .tick-item.tick-sub") 32 | ?.text() 33 | ?.trim(), 34 | ) || null; 35 | const epDUB = 36 | Number( 37 | $(selectors) 38 | .find(".fd-infor .tick .tick-item.tick-dub") 39 | ?.text() 40 | ?.trim(), 41 | ) || null; 42 | const total_eps = 43 | Number( 44 | $(selectors) 45 | .find(".fd-infor .tick .tick-item.tick-eps") 46 | ?.text() 47 | ?.trim(), 48 | ) || null; 49 | const animeTYPE = 50 | $(selectors) 51 | ?.find(".fd-infor .tick") 52 | ?.text() 53 | ?.trim() 54 | ?.replace(/[\s\n]+/g, " ") 55 | ?.split(" ") 56 | ?.pop() || null; 57 | 58 | animes.push({ 59 | id: animeID, 60 | name: animeNAME, 61 | category: animeTYPE, 62 | img: animeIMG, 63 | episodes: { 64 | eps: total_eps, 65 | sub: epSUB, 66 | dub: epDUB, 67 | }, 68 | }); 69 | }); 70 | return animes; 71 | } catch (err) { 72 | /////////////////////////////////////////////////////////////////////////// 73 | console.error("Error in extract_related_animes :", err); // for TESTING// 74 | /////////////////////////////////////////////////////////////////////////// 75 | 76 | if (err instanceof AxiosError) { 77 | throw createHttpError( 78 | err?.response?.status || 500, 79 | err?.response?.statusText || "Something went wrong", 80 | ); 81 | } else { 82 | throw createHttpError.InternalServerError("Internal server error"); 83 | } 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/servers.ts: -------------------------------------------------------------------------------- 1 | import { URL_fn } from "../../utils/aniwatch/constants"; 2 | import { headers } from "../../config/headers"; 3 | import axios, { AxiosError } from "axios"; 4 | import createHttpError, { type HttpError } from "http-errors"; 5 | import { load, type CheerioAPI, type SelectorType } from "cheerio"; 6 | import type { ScrapedEpisodeServer } from "../../types/aniwatch/anime"; 7 | 8 | export const scrapeEpisodeServersPage = async ( 9 | episodeId: string, 10 | ): Promise => { 11 | const res: ScrapedEpisodeServer = { 12 | episodeId, 13 | episodeNo: 0, 14 | sub: [], 15 | dub: [], 16 | raw: [], 17 | }; 18 | 19 | try { 20 | const epId = episodeId.split("?ep=")[1]; 21 | const URLs = await URL_fn(); 22 | 23 | const { data } = await axios.get( 24 | `${URLs.AJAX}/v2/episode/servers?episodeId=${epId}`, 25 | { 26 | headers: { 27 | "User-Agent": headers.USER_AGENT_HEADER, 28 | "X-Requested-With": "XMLHttpRequest", 29 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 30 | Accept: headers.ACCEPT_HEADER, 31 | Referer: new URL(`/watch/${episodeId}`, URLs.BASE).href, 32 | }, 33 | }, 34 | ); 35 | 36 | const $: CheerioAPI = load(data.html); 37 | 38 | const epNoSelector: SelectorType = ".server-notice strong"; 39 | res.episodeNo = Number($(epNoSelector).text().split(" ").pop()) || 0; 40 | 41 | $(`.ps_-block.ps_-block-sub.servers-sub .ps__-list .server-item`).each( 42 | (_, el) => { 43 | res.sub.push({ 44 | serverName: $(el).find("a").text().toLowerCase().trim(), 45 | serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, 46 | }); 47 | }, 48 | ); 49 | 50 | $(`.ps_-block.ps_-block-sub.servers-dub .ps__-list .server-item`).each( 51 | (_, el) => { 52 | res.dub.push({ 53 | serverName: $(el).find("a").text().toLowerCase().trim(), 54 | serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, 55 | }); 56 | }, 57 | ); 58 | 59 | $(`.ps_-block.ps_-block-sub.servers-raw .ps__-list .server-item`).each( 60 | (_, el) => { 61 | res.raw.push({ 62 | serverName: $(el).find("a").text().toLowerCase().trim(), 63 | serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, 64 | }); 65 | }, 66 | ); 67 | 68 | return res; 69 | } catch (err: any) { 70 | if (err instanceof AxiosError) { 71 | throw createHttpError( 72 | err?.response?.status || 500, 73 | err?.response?.statusText || "Something went wrong", 74 | ); 75 | } else { 76 | throw createHttpError.InternalServerError("Internal server error"); 77 | } 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # node modules 133 | node_modules/ 134 | 135 | package-lock.json 136 | -------------------------------------------------------------------------------- /src/routes/gogoanime/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router, type IRouter } from "express"; 2 | import { 3 | getRecentReleases, 4 | getNewSeasons, 5 | getPopularAnimes, 6 | getCompletedAnimes, 7 | getAnimeMovies, 8 | getTopAiring, 9 | getHomePageInfo, 10 | getAboutPageInfo, 11 | getSearchPageInfo, 12 | getAnimeEpisodes, 13 | } from "../../controllers/gogoanime/controllers"; 14 | import { cacheManager } from "../../middlewares/cache"; 15 | 16 | const gogoanime_router: IRouter = Router(); 17 | 18 | // /gogoanime. 19 | gogoanime_router.get("/", (_req, res) => { 20 | res.redirect("/"); 21 | }); // TODO: make custom gogoanime api docs API 22 | 23 | // /gogoanime/home 24 | gogoanime_router.get("/home", cacheManager.middleware(), getHomePageInfo); 25 | 26 | // /gogoanime/search?keyword=${query}&page=${page} 27 | gogoanime_router.get( 28 | "/search", 29 | cacheManager.middleware({ 30 | duration: 3600, // 1 hour cache 31 | keyParams: ["keyword", "page"], 32 | }), 33 | getSearchPageInfo, 34 | ); 35 | 36 | // /gogoanime/anime/:id 37 | gogoanime_router.get("/anime/:id", cacheManager.middleware(), getAboutPageInfo); 38 | 39 | // /gogoanime/recent-releases?page=${pageNo} 40 | gogoanime_router.get( 41 | "/recent-releases", 42 | cacheManager.middleware({ 43 | duration: 3600 * 24, // 1 day cache 44 | keyParams: ["page"], 45 | }), 46 | getRecentReleases, 47 | ); 48 | 49 | // /gogoanime/new-seasons?page=${pageNo} 50 | gogoanime_router.get( 51 | "/new-seasons", 52 | cacheManager.middleware({ 53 | duration: 3600 * 24, // 1 day cache 54 | keyParams: ["page"], 55 | }), 56 | getNewSeasons, 57 | ); 58 | 59 | // /gogoanime/popular?page=${pageNo} 60 | gogoanime_router.get( 61 | "/popular", 62 | cacheManager.middleware({ 63 | duration: 3600 * 24, // 1 day cache 64 | keyParams: ["page"], 65 | }), 66 | getPopularAnimes, 67 | ); 68 | 69 | // /gogoanime/complete?page=${pageNo} d 70 | gogoanime_router.get( 71 | "/completed", 72 | cacheManager.middleware({ 73 | duration: 3600 * 24, // 1 day cache 74 | keyParams: ["page"], 75 | }), 76 | getCompletedAnimes, 77 | ); 78 | 79 | // /gogoanime/anime-movies?page=${pageNo} 80 | gogoanime_router.get( 81 | "/anime-movies", 82 | cacheManager.middleware({ 83 | duration: 3600 * 24, // 1 day cache 84 | keyParams: ["page"], 85 | }), 86 | getAnimeMovies, 87 | ); 88 | 89 | // /gogoanime/top-airing?page=${pageNo} 90 | gogoanime_router.get( 91 | "/top-airing", 92 | cacheManager.middleware({ 93 | duration: 3600 * 24, // 1 day cache 94 | keyParams: ["page"], 95 | }), 96 | getTopAiring, 97 | ); 98 | 99 | // /gogoanime/episodes/:anime-id 100 | gogoanime_router.get("/episodes/:id", cacheManager.middleware(), getAnimeEpisodes); 101 | 102 | export default gogoanime_router; 103 | -------------------------------------------------------------------------------- /src/scrapers/gogoanime/home.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from "axios"; 2 | import { URL_fn } from "../../utils/gogoanime/constants"; 3 | import { headers } from "../../config/headers"; 4 | import { CheerioAPI, load, SelectorType } from "cheerio"; 5 | import { 6 | extract_recent_released_home, 7 | extract_recently_added_series_home, 8 | } from "../../extracters/gogoanime/extracters"; 9 | import createHttpError, { HttpError } from "http-errors"; 10 | import { ScrapedHomePage } from "../../types/gogoanime/anime"; 11 | 12 | export const scrapeHomePage = async (): Promise< 13 | ScrapedHomePage | HttpError 14 | > => { 15 | const URLs = await URL_fn(); 16 | 17 | const mainPage = await axios.get(URLs.HOME, { 18 | headers: { 19 | "User-Agent": headers.USER_AGENT_HEADER, 20 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 21 | Accept: headers.ACCEPT_HEADER, 22 | }, 23 | }); 24 | 25 | let res: ScrapedHomePage = { 26 | genres: [], 27 | recentReleases: [], 28 | recentlyAddedSeries: [], 29 | onGoingSeries: [], 30 | }; 31 | 32 | const $: CheerioAPI = load(mainPage.data); 33 | 34 | const recent_releases_selectors: SelectorType = 35 | "#load_recent_release > div.last_episodes.loaddub > ul > li"; 36 | const recently_added_series_selectors: SelectorType = 37 | "#wrapper_bg > section > section.content_left > div.main_body.none > div.added_series_body.final > ul > li"; 38 | const ongoing_series_selectors: SelectorType = 39 | "#scrollbar2 > div.viewport > div > nav > ul > li"; 40 | 41 | try { 42 | let recentReleases = extract_recent_released_home( 43 | $, 44 | recent_releases_selectors, 45 | ); 46 | let recentlyAddedSeries = extract_recently_added_series_home( 47 | $, 48 | recently_added_series_selectors, 49 | ); 50 | let onGoingSeries = extract_recently_added_series_home( 51 | $, 52 | ongoing_series_selectors, 53 | ); 54 | 55 | res.recentReleases = recentReleases; 56 | res.recentlyAddedSeries = recentlyAddedSeries; 57 | res.onGoingSeries = onGoingSeries; 58 | 59 | $("nav.menu_series.genre.right > ul > li").each((_index, element) => { 60 | const genre = $(element).find("a"); 61 | const href = genre.attr("href"); 62 | if (href) { 63 | res.genres.push(href.replace("/genre/", "")); 64 | } 65 | }); 66 | 67 | return res; 68 | } catch (err) { 69 | //////////////////////////////////////////////////////////////// 70 | console.error("Error in scrapeHomePage :", err); // for TESTING// 71 | //////////////////////////////////////////////////////////////// 72 | 73 | if (err instanceof AxiosError) { 74 | throw createHttpError( 75 | err?.response?.status || 500, 76 | err?.response?.statusText || "Something went wrong", 77 | ); 78 | } else { 79 | throw createHttpError.InternalServerError("Internal server error"); 80 | } 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/utils/aniwatch/streamsb.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import type { Video } from "../../types/aniwatch/anime"; 3 | import { headers as HEADERS } from "../../config/headers"; 4 | 5 | const USER_AGENT_HEADER = HEADERS.USER_AGENT_HEADER; 6 | 7 | class StreamSB { 8 | private serverName = "streamSB"; 9 | private sources: Video[] = []; 10 | 11 | private readonly host = "https://watchsb.com/sources50"; 12 | private readonly host2 = "https://streamsss.net/sources16"; 13 | 14 | private PAYLOAD(hex: string): string { 15 | // `5363587530696d33443675687c7c${hex}7c7c433569475830474c497a65767c7c73747265616d7362`; 16 | return `566d337678566f743674494a7c7c${hex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362`; 17 | } 18 | 19 | async extract(videoUrl: URL, isAlt: boolean = false): Promise { 20 | let headers: Record = { 21 | watchsb: "sbstream", 22 | Referer: videoUrl.href, 23 | "User-Agent": USER_AGENT_HEADER, 24 | }; 25 | let id = videoUrl.href.split("/e/").pop(); 26 | if (id?.includes("html")) { 27 | id = id.split(".html")[0]; 28 | } 29 | const bytes = new TextEncoder().encode(id); 30 | 31 | const res = await axios 32 | .get( 33 | `${isAlt ? this.host2 : this.host}/${this.PAYLOAD( 34 | Buffer.from(bytes).toString("hex") 35 | )}`, 36 | { headers } 37 | ) 38 | .catch(() => null); 39 | 40 | if (!res?.data.stream_data) { 41 | throw new Error("No source found. Try a different server"); 42 | } 43 | 44 | headers = { 45 | "User-Agent": USER_AGENT_HEADER, 46 | Referer: videoUrl.href.split("e/")[0], 47 | }; 48 | 49 | const m3u8_urls = await axios.get(res.data.stream_data.file, { 50 | headers, 51 | }); 52 | 53 | const videoList = m3u8_urls?.data?.split("#EXT-X-STREAM-INF:") ?? []; 54 | 55 | for (const video of videoList) { 56 | if (!video.includes("m3u8")) continue; 57 | 58 | const url = video.split("\n")[1]; 59 | const quality = video.split("RESOLUTION=")[1].split(",")[0].split("x")[1]; 60 | 61 | this.sources.push({ 62 | url: url, 63 | quality: `${quality}p`, 64 | isM3U8: true, 65 | }); 66 | } 67 | 68 | this.sources.push({ 69 | url: res.data.stream_data.file, 70 | quality: "auto", 71 | isM3U8: res.data.stream_data.file.includes(".m3u8"), 72 | }); 73 | 74 | return this.sources; 75 | } 76 | 77 | private addSources(source: any): void { 78 | this.sources.push({ 79 | url: source.file, 80 | isM3U8: source.file.includes(".m3u8"), 81 | }); 82 | } 83 | } 84 | 85 | export default StreamSB; 86 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/spotlight_animes.ts: -------------------------------------------------------------------------------- 1 | import createHttpError from "http-errors"; 2 | import type { CheerioAPI, SelectorType } from "cheerio"; 3 | import { AxiosError } from "axios"; 4 | import { SpotLightAnime } from "../../types/aniwatch/anime"; 5 | 6 | export const extract_spotlight_animes = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType, 9 | ): SpotLightAnime[] => { 10 | try { 11 | const animes: SpotLightAnime[] = []; 12 | 13 | $(selectors).each((_index, element) => { 14 | const animeID = 15 | $(element) 16 | .find(".deslide-item-content .desi-buttons a") 17 | ?.last() 18 | ?.attr("href") 19 | ?.slice(1) 20 | ?.trim() || null; 21 | const animeNAME = 22 | $(element) 23 | .find(".deslide-item-content .desi-head-title.dynamic-name") 24 | ?.text() 25 | ?.trim() ?? "UNKNOWN ANIME"; 26 | const animeRANK = 27 | Number( 28 | $(element) 29 | .find(".deslide-item-content .desi-sub-text") 30 | ?.text() 31 | ?.trim() 32 | ?.split(" ")[0] 33 | ?.slice(1), 34 | ) || null; 35 | const animeIMG = 36 | $(element) 37 | .find(".deslide-cover .deslide-cover-img .film-poster-img") 38 | ?.attr("data-src") 39 | ?.trim() || null; 40 | const animeDESCRIPTION = 41 | $(element) 42 | .find(".deslide-item-content .desi-description") 43 | ?.text() 44 | ?.split("[") 45 | ?.shift() 46 | ?.trim() ?? "UNKNOW ANIME DESCRIPTION"; 47 | const animeEXTRA = $(element) 48 | .find(".deslide-item-content .sc-detail .scd-item") 49 | .map((_i, el) => $(el).text().trim()) 50 | .get(); 51 | 52 | const episodeDetails = animeEXTRA[4].split(/\s+/).map(Number) || null; 53 | 54 | animes.push({ 55 | id: animeID, 56 | name: animeNAME, 57 | rank: animeRANK, 58 | img: animeIMG, 59 | episodes: { 60 | eps: episodeDetails[2], 61 | sub: episodeDetails[0], 62 | dub: episodeDetails[1], 63 | }, 64 | duration: animeEXTRA[1], 65 | quality: animeEXTRA[3], 66 | category: animeEXTRA[0], 67 | releasedDay: animeEXTRA[2], 68 | description: animeDESCRIPTION, 69 | }); 70 | }); 71 | 72 | return animes; 73 | } catch (err) { 74 | //////////////////////////////////////////////////////////////////////// 75 | console.error("Error in extract_spotlight_animes :", err); // for TESTING// 76 | //////////////////////////////////////////////////////////////////////// 77 | 78 | if (err instanceof AxiosError) { 79 | throw createHttpError( 80 | err?.response?.status || 500, 81 | err?.response?.statusText || "Something went wrong", 82 | ); 83 | } else { 84 | throw createHttpError.InternalServerError("Internal server error"); 85 | } 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/search.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/aniwatch/constants"; 4 | import { headers } from "../../config/headers"; 5 | import createHttpError, { HttpError } from "http-errors"; 6 | import axios, { AxiosError } from "axios"; 7 | import { load } from "cheerio"; 8 | import type { CheerioAPI, SelectorType } from "cheerio"; 9 | import { 10 | extract_searched_animes, 11 | extract_mostpopular_animes, 12 | extract_genre_list, 13 | } from "../../extracters/aniwatch/extracters"; 14 | import { ScrapedSearchPage } from "../../types/aniwatch/anime"; 15 | 16 | export const scrapeSearchPage = async ( 17 | query: string, 18 | page: number, 19 | ): Promise => { 20 | const res: ScrapedSearchPage = { 21 | animes: [], 22 | mostPopularAnimes: [], 23 | currentPage: Number(page), 24 | hasNextPage: false, 25 | totalPages: 1, 26 | genres: [], 27 | }; 28 | 29 | try { 30 | const URLs = await URL_fn(); 31 | const mainPage = await axios.get( 32 | `${URLs.SEARCH}?keyword=${query}&page=${page}`, 33 | { 34 | headers: { 35 | "User-Agent": headers.USER_AGENT_HEADER, 36 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 37 | Accept: headers.ACCEPT_HEADER, 38 | }, 39 | }, 40 | ); 41 | const $: CheerioAPI = load(mainPage.data); 42 | 43 | const selectors: SelectorType = 44 | "#main-content .tab-content .film_list-wrap .flw-item"; 45 | const mostPopularAnimesSelectors: SelectorType = 46 | "#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li"; 47 | const genresSelectors: SelectorType = 48 | "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; 49 | 50 | res.animes = extract_searched_animes($, selectors); 51 | res.mostPopularAnimes = extract_mostpopular_animes( 52 | $, 53 | mostPopularAnimesSelectors, 54 | ); 55 | res.genres = extract_genre_list($, genresSelectors); 56 | 57 | res.hasNextPage = 58 | $(".pagination > li").length > 0 59 | ? $(".pagination li.active").length > 0 60 | ? $(".pagination > li").last().hasClass("active") 61 | ? false 62 | : true 63 | : false 64 | : false; 65 | 66 | res.totalPages = 67 | Number( 68 | $('.pagination > .page-item a[title="Last"]') 69 | ?.attr("href") 70 | ?.split("=") 71 | .pop() ?? 72 | $('.pagination > .page-item a[title="Next"]') 73 | ?.attr("href") 74 | ?.split("=") 75 | .pop() ?? 76 | $(".pagination > .page-item.active a")?.text()?.trim(), 77 | ) || 1; 78 | 79 | if (!res.hasNextPage && res.animes.length === 0) { 80 | res.totalPages = 0; 81 | } 82 | 83 | return res; 84 | } catch (err) { 85 | //////////////////////////////////////////////////////////////// 86 | console.error("Error in scrapeSearchPage :", err); // for TESTING// 87 | //////////////////////////////////////////////////////////////// 88 | 89 | if (err instanceof AxiosError) { 90 | throw createHttpError( 91 | err?.response?.status || 500, 92 | err?.response?.statusText || "Something went wrong", 93 | ); 94 | } else { 95 | throw createHttpError.InternalServerError("Internal server error"); 96 | } 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /src/extracters/gogoanime/about_anime.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { CheerioAPI, SelectorType } from "cheerio"; 3 | import createHttpError from "http-errors"; 4 | import { AboutAnimeInfo } from "../../types/gogoanime/anime"; 5 | 6 | export const extract_about_info = ( 7 | $: CheerioAPI, 8 | selectors: SelectorType 9 | ): AboutAnimeInfo => { 10 | try { 11 | let info: AboutAnimeInfo | undefined; 12 | 13 | $(selectors).each((_index, _element) => { 14 | const animeNAME = 15 | $(selectors) 16 | .find(".anime_info_episodes h2") 17 | ?.text() 18 | ?.split("/") 19 | ?.pop() ?? "UNKNOWN ANIME"; 20 | 21 | const animeIMG = 22 | $(selectors).find(".anime_info_body_bg img")?.attr("src")?.trim() ?? 23 | null; 24 | 25 | const animeTYPE = 26 | $(selectors) 27 | .find("p.type:contains('Type:') a") 28 | ?.text() 29 | .replace("Type:", "") 30 | .trim() || null; 31 | 32 | const animeGENRES: string[] = []; 33 | $(selectors) 34 | .find("p.type:contains('Genre:') a") 35 | .each((_, element) => { 36 | animeGENRES.push($(element).text().trim()); 37 | }); 38 | 39 | const animeSTATUS = 40 | $(selectors) 41 | .find("p.type:contains('Status:') a") 42 | ?.text() 43 | .replace("Status:", "") 44 | .trim() || null; 45 | 46 | const animeAIRED = 47 | parseInt( 48 | $(selectors) 49 | .find("p.type:contains('Released:')") 50 | ?.text() 51 | .replace("Released: ", "") 52 | .trim() 53 | ) || null; 54 | 55 | const animeOTHERNAME = 56 | $(selectors) 57 | .find("p.type:contains('Other name:') a") 58 | ?.text() 59 | .replace("Other name:", "") 60 | .trim() || null; 61 | 62 | const totalEPISODES = 63 | parseInt( 64 | $(selectors) 65 | .find("#episode_page li:last-child a") 66 | .text() 67 | .split("-")[1] 68 | ) || 0; 69 | 70 | info = { 71 | name: animeNAME, 72 | img: animeIMG, 73 | type: animeTYPE, 74 | genre: animeGENRES, 75 | status: animeSTATUS, 76 | aired_in: animeAIRED, 77 | other_name: animeOTHERNAME, 78 | episodes: totalEPISODES, 79 | }; 80 | }); 81 | 82 | if (info === undefined) { 83 | info = { 84 | name: null, 85 | img: null, 86 | type: null, 87 | genre: null, 88 | status: null, 89 | aired_in: null, 90 | other_name: null, 91 | episodes: null, 92 | }; 93 | } 94 | 95 | return info; 96 | } catch (err) { 97 | /////////////////////////////////////////////////////////////////// 98 | console.error("Error in extract_about_info :", err); // for TESTING// 99 | /////////////////////////////////////////////////////////////////// 100 | 101 | if (err instanceof AxiosError) { 102 | throw createHttpError( 103 | err?.response?.status || 500, 104 | err?.response?.statusText || "Something went wrong" 105 | ); 106 | } else { 107 | throw createHttpError.InternalServerError("Internal server error"); 108 | } 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/about.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/aniwatch/constants"; 4 | import { headers } from "../../config/headers"; 5 | import axios, { AxiosError } from "axios"; 6 | import { load } from "cheerio"; 7 | import type { CheerioAPI, SelectorType } from "cheerio"; 8 | import createHttpError, { HttpError } from "http-errors"; 9 | import { 10 | extract_about_info, 11 | extract_extra_about_info, 12 | extract_anime_seasons_info, 13 | extract_related_animes, 14 | extract_recommended_animes, 15 | extract_mostpopular_animes, 16 | } from "../../extracters/aniwatch/extracters"; 17 | import { ScrapedAboutPage, AboutAnimeInfo } from "../../types/aniwatch/anime"; 18 | 19 | export const scrapeAboutPage = async ( 20 | id: string, 21 | ): Promise => { 22 | const defaultInfo: AboutAnimeInfo = { 23 | id: null, 24 | mal_id: null, 25 | al_id: null, 26 | anime_id: null, 27 | name: "UNKNOWN ANIME", 28 | img: null, 29 | rating: null, 30 | episodes: { 31 | eps: null, 32 | sub: null, 33 | dub: null, 34 | }, 35 | category: null, 36 | quality: null, 37 | duration: null, 38 | description: "UNKNOW ANIME DESCRIPTION", 39 | }; 40 | 41 | const res: ScrapedAboutPage = { 42 | info: defaultInfo, // TODO: need to improve it in future 43 | moreInfo: {}, 44 | seasons: [], 45 | relatedAnimes: [], 46 | recommendedAnimes: [], 47 | mostPopularAnimes: [], 48 | }; 49 | const URLs = await URL_fn(); 50 | const aboutURL: string = new URL(id, URLs.BASE).toString(); 51 | const mainPage = await axios.get(aboutURL, { 52 | headers: { 53 | "User-Agent": headers.USER_AGENT_HEADER, 54 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 55 | Accept: headers.ACCEPT_HEADER, 56 | }, 57 | }); 58 | 59 | const $: CheerioAPI = load(mainPage.data); 60 | const selectors: SelectorType = "#ani_detail .container .anis-content"; 61 | const extraInfoSelector: SelectorType = `${selectors} .anisc-info`; 62 | const seasonsSelectors: SelectorType = ".os-list a.os-item"; 63 | const relatedAnimesSelectors: SelectorType = 64 | "#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(1) .anif-block-ul ul li"; 65 | const recommendedAnimesSelectors: SelectorType = 66 | "#main-content .block_area.block_area_category .tab-content .flw-item"; 67 | const mostPopularAnimesSelectors: SelectorType = 68 | "#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(2) .anif-block-ul ul li"; 69 | 70 | try { 71 | res.info = extract_about_info($, selectors); 72 | res.moreInfo = extract_extra_about_info($, extraInfoSelector); 73 | res.seasons = extract_anime_seasons_info($, seasonsSelectors); 74 | res.relatedAnimes = extract_related_animes($, relatedAnimesSelectors); 75 | res.recommendedAnimes = extract_recommended_animes( 76 | $, 77 | recommendedAnimesSelectors, 78 | ); 79 | res.mostPopularAnimes = extract_mostpopular_animes( 80 | $, 81 | mostPopularAnimesSelectors, 82 | ); 83 | 84 | return res; 85 | } catch (err) { 86 | //////////////////////////////////////////////////////////////// 87 | console.error("Error in scrapeAboutPage :", err); // for TESTING// 88 | //////////////////////////////////////////////////////////////// 89 | 90 | if (err instanceof AxiosError) { 91 | throw createHttpError( 92 | err?.response?.status || 500, 93 | err?.response?.statusText || "Something went wrong", 94 | ); 95 | } else { 96 | throw createHttpError.InternalServerError("Internal server error"); 97 | } 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/category.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError, { HttpError } from "http-errors"; 3 | import { 4 | URL_fn, 5 | } from "../../utils/aniwatch/constants"; 6 | import { headers } from "../../config/headers"; 7 | import axios, { AxiosError } from "axios"; 8 | import { load } from "cheerio"; 9 | import { 10 | extract_genre_list, 11 | extract_top10_animes, 12 | extract_category_animes, 13 | } from "../../extracters/aniwatch/extracters"; 14 | import { ScrapedCategoryPage } from "../../types/aniwatch/anime"; 15 | 16 | export const scrapeCategoryPage = async ( 17 | category: string, 18 | page: number, 19 | ): Promise => { 20 | const res: ScrapedCategoryPage = { 21 | animes: [], 22 | top10Animes: { 23 | day: [], 24 | week: [], 25 | month: [], 26 | }, 27 | category, 28 | genres: [], 29 | currentPage: Number(page), 30 | hasNextPage: false, 31 | totalPages: 1, 32 | }; 33 | 34 | try { 35 | const URLs = await URL_fn(); 36 | const scrapeUrl = new URL(category, URLs.BASE); 37 | 38 | const mainPage = await axios.get(`${scrapeUrl}?page=${page}`, { 39 | headers: { 40 | "User-Agent": headers.USER_AGENT_HEADER, 41 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 42 | Accept: headers.ACCEPT_HEADER, 43 | }, 44 | }); 45 | 46 | const $: CheerioAPI = load(mainPage.data); 47 | const selectors: SelectorType = 48 | "#main-content .tab-content .film_list-wrap .flw-item"; 49 | const categorySelectors: SelectorType = 50 | "#main-content .block_area .block_area-header .cat-heading"; 51 | const top10Selectors: SelectorType = 52 | '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; 53 | const genresSelectors: SelectorType = 54 | "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; 55 | 56 | res.category = $(categorySelectors)?.text()?.trim() ?? category; 57 | res.animes = extract_category_animes($, selectors); 58 | res.genres = extract_genre_list($, genresSelectors); 59 | 60 | res.hasNextPage = 61 | $(".pagination > li").length > 0 62 | ? $(".pagination li.active").length > 0 63 | ? $(".pagination > li").last().hasClass("active") 64 | ? false 65 | : true 66 | : false 67 | : false; 68 | 69 | res.totalPages = 70 | Number( 71 | $('.pagination > .page-item a[title="Last"]') 72 | ?.attr("href") 73 | ?.split("=") 74 | .pop() ?? 75 | $('.pagination > .page-item a[title="Next"]') 76 | ?.attr("href") 77 | ?.split("=") 78 | .pop() ?? 79 | $(".pagination > .page-item.active a")?.text()?.trim(), 80 | ) || 1; 81 | 82 | if (res.animes.length === 0 && !res.hasNextPage) { 83 | res.totalPages = 0; 84 | } 85 | 86 | $(top10Selectors).each((_index, element) => { 87 | const periodType = $(element).attr("id")?.split("-")?.pop()?.trim(); 88 | if (periodType === "day") { 89 | res.top10Animes.day = extract_top10_animes($, periodType); 90 | return; 91 | } 92 | if (periodType === "week") { 93 | res.top10Animes.week = extract_top10_animes($, periodType); 94 | return; 95 | } 96 | if (periodType === "month") { 97 | res.top10Animes.month = extract_top10_animes($, periodType); 98 | } 99 | }); 100 | 101 | return res; 102 | } catch (err) { 103 | //////////////////////////////////////////////////////////////// 104 | console.error("Error in scrapeCategoryPage :", err); // for TESTING// 105 | //////////////////////////////////////////////////////////////// 106 | 107 | if (err instanceof AxiosError) { 108 | throw createHttpError( 109 | err?.response?.status || 500, 110 | err?.response?.statusText || "Something went wrong", 111 | ); 112 | } else { 113 | throw createHttpError.InternalServerError("Internal server error"); 114 | } 115 | } 116 | }; 117 | 118 | export default scrapeCategoryPage; 119 | -------------------------------------------------------------------------------- /src/extracters/aniwatch/about_anime.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioAPI, SelectorType } from "cheerio"; 2 | import createHttpError from "http-errors"; 3 | import { AxiosError } from "axios"; 4 | import { AboutAnimeInfo } from "../../types/aniwatch/anime"; 5 | 6 | //NEED TO IMPROVE IT IN FUTURE 7 | export const extract_about_info = ( 8 | $: CheerioAPI, 9 | selectors: SelectorType, 10 | ): AboutAnimeInfo => { 11 | try { 12 | let info: AboutAnimeInfo | undefined; 13 | 14 | $(selectors).each((_index, _element) => { 15 | let { anime_id, mal_id, anilist_id } = JSON.parse($('#syncData').text()); 16 | anime_id = parseIntSafe(anime_id); 17 | mal_id = parseIntSafe(mal_id); 18 | anilist_id = parseIntSafe(anilist_id); 19 | 20 | const animeID = 21 | $(selectors) 22 | .find(".anisc-detail .film-buttons a.btn-play") 23 | .attr("href") 24 | ?.split("/") 25 | ?.pop() || null; 26 | const animeNAME = 27 | $(selectors) 28 | .find(".anisc-detail .film-name.dynamic-name") 29 | .text() 30 | .trim() ?? "UNKNOWN ANIME"; 31 | const animeIMG = 32 | $(selectors) 33 | .find(".film-poster .film-poster-img") 34 | ?.attr("src") 35 | ?.trim() ?? "UNKNOWN"; 36 | const animeRATING = 37 | $(`${selectors} .film-stats .tick .tick-pg`)?.text()?.trim() || null; 38 | const animeQUALITY = 39 | $(`${selectors} .film-stats .tick .tick-quality`)?.text()?.trim() || 40 | null; 41 | const epSUB = 42 | Number($(`${selectors} .film-stats .tick .tick-sub`)?.text()?.trim()) || 43 | null; 44 | const epDUB = 45 | Number($(`${selectors} .film-stats .tick .tick-dub`)?.text()?.trim()) || 46 | null; 47 | const total_eps = 48 | Number($(`${selectors} .film-stats .tick .tick-eps`)?.text()?.trim()) || 49 | null; 50 | const animeCategory = 51 | $(`${selectors} .film-stats .tick`) 52 | ?.text() 53 | ?.trim() 54 | ?.replace(/[\s\n]+/g, " ") 55 | ?.split(" ") 56 | ?.at(-2) || null; 57 | const duration = 58 | $(`${selectors} .film-stats .tick`) 59 | ?.text() 60 | ?.trim() 61 | ?.replace(/[\s\n]+/g, " ") 62 | ?.split(" ") 63 | ?.pop() || null; 64 | const animeDESCRIPTION = 65 | $(selectors) 66 | .find(".anisc-detail .film-description .text") 67 | ?.text() 68 | ?.split("[") 69 | ?.shift() 70 | ?.trim() ?? "UNKNOW ANIME DESCRIPTION"; 71 | 72 | info = { 73 | id: animeID, 74 | mal_id, 75 | anime_id, 76 | al_id: anilist_id, 77 | name: animeNAME, 78 | img: animeIMG, 79 | rating: animeRATING, 80 | episodes: { 81 | eps: total_eps, 82 | sub: epSUB, 83 | dub: epDUB, 84 | }, 85 | category: animeCategory, 86 | quality: animeQUALITY, 87 | duration: duration, 88 | description: animeDESCRIPTION, 89 | }; 90 | }); 91 | 92 | if (info === undefined) { 93 | info = { 94 | id: null, 95 | mal_id: null, 96 | al_id: null, 97 | anime_id: null, 98 | name: null, 99 | img: null, 100 | rating: null, 101 | episodes: { 102 | eps: null, 103 | sub: null, 104 | dub: null, 105 | }, 106 | category: null, 107 | quality: null, 108 | duration: null, 109 | description: null, 110 | }; 111 | } 112 | 113 | return info; 114 | } catch (err) { 115 | /////////////////////////////////////////////////////////////////// 116 | console.error("Error in extract_about_info :", err); // for TESTING// 117 | /////////////////////////////////////////////////////////////////// 118 | 119 | if (err instanceof AxiosError) { 120 | throw createHttpError( 121 | err?.response?.status || 500, 122 | err?.response?.statusText || "Something went wrong", 123 | ); 124 | } else { 125 | throw createHttpError.InternalServerError("Internal server error"); 126 | } 127 | } 128 | }; 129 | 130 | function parseIntSafe(str: string): number | null { 131 | const parsedId = parseInt(str, 10); 132 | 133 | if (!isNaN(parsedId)) { 134 | return parsedId; 135 | } else { 136 | return null; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/middlewares/cache.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import NodeCache from "node-cache"; 3 | 4 | interface CacheConfig { 5 | duration: number; 6 | keyParams?: string[]; // Specific query/body params to include in cache key 7 | ignoreParams?: string[]; // Params to exclude from cache key 8 | varyByHeaders?: string[]; // Headers to include in cache key 9 | customKeyGenerator?: (req: Request) => string; // Custom key generation function 10 | } 11 | 12 | interface CacheOptions { 13 | defaultTTL: number; 14 | checkPeriod?: number; 15 | maxKeys?: number; 16 | } 17 | 18 | class AdvancedCache { 19 | private cache: NodeCache; 20 | private defaultConfig: CacheConfig; 21 | 22 | constructor(options: CacheOptions) { 23 | this.cache = new NodeCache({ 24 | stdTTL: options.defaultTTL, 25 | checkperiod: options.checkPeriod || 600, 26 | maxKeys: options.maxKeys || -1, 27 | }); 28 | 29 | this.defaultConfig = { 30 | duration: options.defaultTTL, 31 | keyParams: [], 32 | ignoreParams: [], 33 | varyByHeaders: [], 34 | }; 35 | } 36 | 37 | /** 38 | * Generate a cache key based on request and configuration 39 | */ 40 | private generateCacheKey(req: Request, config: CacheConfig): string { 41 | if (config.customKeyGenerator) { 42 | return config.customKeyGenerator(req); 43 | } 44 | 45 | const components: string[] = [req.method, req.path]; 46 | 47 | // Add specified query parameters 48 | const queryParams: Record = {}; 49 | if (req.query) { 50 | Object.keys(req.query).forEach((key) => { 51 | if ( 52 | (!config.keyParams?.length || config.keyParams.includes(key)) && 53 | !config.ignoreParams?.includes(key) 54 | ) { 55 | queryParams[key] = req.query[key]; 56 | } 57 | }); 58 | } 59 | 60 | // Add specified body parameters for POST/PUT requests 61 | const bodyParams: Record = {}; 62 | if (req.body && (req.method === "POST" || req.method === "PUT")) { 63 | Object.keys(req.body).forEach((key) => { 64 | if ( 65 | (!config.keyParams?.length || config.keyParams.includes(key)) && 66 | !config.ignoreParams?.includes(key) 67 | ) { 68 | bodyParams[key] = req.body[key]; 69 | } 70 | }); 71 | } 72 | 73 | // Add specified headers 74 | const headers: Record = {}; 75 | if (config.varyByHeaders?.length) { 76 | config.varyByHeaders.forEach((header) => { 77 | const headerValue = req.get(header); 78 | if (headerValue) { 79 | headers[header] = headerValue; 80 | } 81 | }); 82 | } 83 | 84 | // Combine all components into a single key 85 | components.push( 86 | JSON.stringify(queryParams), 87 | JSON.stringify(bodyParams), 88 | JSON.stringify(headers), 89 | ); 90 | 91 | return components.join("|"); 92 | } 93 | 94 | /** 95 | * Create middleware with specific cache configuration 96 | */ 97 | middleware(config?: Partial) { 98 | const finalConfig: CacheConfig = { ...this.defaultConfig, ...config }; 99 | 100 | return (req: Request, res: Response, next: NextFunction) => { 101 | // Skip caching for non-GET methods unless explicitly configured 102 | if (req.method !== "GET" && !config?.customKeyGenerator) { 103 | return next(); 104 | } 105 | 106 | const cacheKey = this.generateCacheKey(req, finalConfig); 107 | const cachedResponse = this.cache.get(cacheKey); 108 | 109 | if (cachedResponse) { 110 | return res.json(cachedResponse); 111 | } 112 | 113 | // Override res.json to cache the response 114 | const originalJson = res.json.bind(res); 115 | res.json = (body: any) => { 116 | this.cache.set(cacheKey, body, finalConfig.duration); 117 | return originalJson(body); 118 | }; 119 | 120 | next(); 121 | }; 122 | } 123 | 124 | /** 125 | * Clear cache entries matching a pattern 126 | */ 127 | clearCache(pattern?: RegExp): void { 128 | if (!pattern) { 129 | this.cache.flushAll(); 130 | return; 131 | } 132 | 133 | const keys = this.cache.keys(); 134 | keys.forEach((key) => { 135 | if (pattern.test(key)) { 136 | this.cache.del(key); 137 | } 138 | }); 139 | } 140 | } 141 | 142 | const cacheManager = new AdvancedCache({ 143 | defaultTTL: 3600 * 24, // 1 day default TTL 144 | checkPeriod: 600, // Check for expired keys every 10 minutes 145 | maxKeys: 1000, // Maximum number of cache entries 146 | }); 147 | 148 | export { cacheManager, AdvancedCache, CacheConfig, CacheOptions }; 149 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/episodeServerSource.ts: -------------------------------------------------------------------------------- 1 | import { URL_fn } from "../../utils/aniwatch/constants"; 2 | import { headers } from "../../config/headers"; 3 | import axios, { AxiosError } from "axios"; 4 | import { load, type CheerioAPI } from "cheerio"; 5 | import createHttpError, { type HttpError } from "http-errors"; 6 | import { type AnimeServers, Servers } from "../../types/aniwatch/anime"; 7 | import { extract_server_id } from "../../extracters/aniwatch/extracters"; 8 | import RapidCloud from "../../utils/aniwatch/rapidcloud"; 9 | import StreamSB from "../../utils/aniwatch/streamsb"; 10 | import StreamTape from "../../utils/aniwatch/streamtape"; 11 | import MegaCloud from "../../utils/aniwatch/megacloud"; 12 | import { type ScrapedAnimeEpisodesSources } from "../../types/aniwatch/anime"; 13 | 14 | export const scrapeAnimeEpisodeSources = async ( 15 | episodeId: string, 16 | server: AnimeServers = Servers.VidStreaming, 17 | category: "sub" | "dub" | "raw" = "sub", 18 | ): Promise => { 19 | const URLs = await URL_fn(); 20 | 21 | if (episodeId.startsWith("http")) { 22 | const serverUrl = new URL(episodeId); 23 | switch (server) { 24 | case Servers.MegaCloud: 25 | case Servers.VidStreaming: 26 | case Servers.VidCloud: 27 | return { 28 | ...(await new MegaCloud().extract2(serverUrl)), 29 | }; 30 | case Servers.StreamSB: 31 | return { 32 | headers: { 33 | Referer: serverUrl.href, 34 | watchsb: "streamsb", 35 | "User-Agent": headers.USER_AGENT_HEADER, 36 | }, 37 | sources: await new StreamSB().extract(serverUrl, true), 38 | }; 39 | case Servers.StreamTape: 40 | return { 41 | headers: { 42 | Referer: serverUrl.href, 43 | "User-Agent": headers.USER_AGENT_HEADER, 44 | }, 45 | sources: await new StreamTape().extract(serverUrl), 46 | }; 47 | default: 48 | return { 49 | headers: { Referer: serverUrl.href }, 50 | ...(await new RapidCloud().extract(serverUrl)), 51 | }; 52 | } 53 | } 54 | 55 | const epId = new URL(`/watch/${episodeId}`, URLs.BASE).href; 56 | console.log(epId); 57 | 58 | try { 59 | const resp = await axios.get( 60 | `${URLs.AJAX}/v2/episode/servers?episodeId=${epId.split("?ep=")[1]}`, 61 | { 62 | headers: { 63 | Referer: epId, 64 | "User-Agent": headers.USER_AGENT_HEADER, 65 | "X-Requested-With": "XMLHttpRequest", 66 | }, 67 | }, 68 | ); 69 | 70 | const $: CheerioAPI = load(resp.data.html); 71 | 72 | /** 73 | * vidStreaming -> 4 74 | * rapidcloud, vidcloud, megacloud -> 1 75 | * streamsb -> 5 76 | * streamtape -> 3 77 | */ 78 | let serverId: string | null = null; 79 | try { 80 | console.log("THE SERVER: ", server); 81 | switch (server) { 82 | case Servers.MegaCloud: 83 | case Servers.VidCloud: 84 | case Servers.HD2: 85 | serverId = extract_server_id($, 1, category); 86 | console.log("SERVER_ID: ", serverId); 87 | 88 | // zoro's vidcloud server is rapidcloud 89 | if (!serverId) throw new Error("RapidCloud not found"); 90 | break; 91 | case Servers.VidStreaming: 92 | case Servers.HD1: 93 | serverId = extract_server_id($, 4, category); 94 | console.log("SERVER_ID: ", serverId); 95 | 96 | // zoro's vidcloud server is rapidcloud 97 | if (!serverId) throw new Error("vidtreaming not found"); 98 | break; 99 | case Servers.StreamSB: 100 | serverId = extract_server_id($, 5, category); 101 | console.log("SERVER_ID: ", serverId); 102 | 103 | if (!serverId) throw new Error("StreamSB not found"); 104 | break; 105 | case Servers.StreamTape: 106 | serverId = extract_server_id($, 3, category); 107 | console.log("SERVER_ID: ", serverId); 108 | 109 | if (!serverId) throw new Error("StreamTape not found"); 110 | break; 111 | } 112 | } catch (err) { 113 | throw createHttpError.NotFound( 114 | "Couldn't find server. Try another server", 115 | ); 116 | } 117 | 118 | const { 119 | data: { link }, 120 | } = await axios.get(`${URLs.AJAX}/v2/episode/sources?id=${serverId}`); 121 | console.log("THE LINK: ", link); 122 | 123 | return await scrapeAnimeEpisodeSources(link, server); 124 | } catch (err: any) { 125 | console.log(err); 126 | if (err instanceof AxiosError) { 127 | throw createHttpError( 128 | err?.response?.status || 500, 129 | err?.response?.statusText || "Something went wrong", 130 | ); 131 | } else { 132 | throw createHttpError.InternalServerError("Internal server error"); 133 | } 134 | } 135 | }; 136 | -------------------------------------------------------------------------------- /src/scrapers/aniwatch/home.ts: -------------------------------------------------------------------------------- 1 | import { 2 | URL_fn, 3 | } from "../../utils/aniwatch/constants"; 4 | import { headers } from "../../config/headers"; 5 | import axios, { AxiosError } from "axios"; 6 | import { load } from "cheerio"; 7 | import type { CheerioAPI, SelectorType } from "cheerio"; 8 | import createHttpError, { HttpError } from "http-errors"; 9 | import { 10 | extract_top10_animes, 11 | extract_spotlight_animes, 12 | extract_trending_animes, 13 | extract_latest_episodes, 14 | extract_featured_animes, 15 | extract_top_upcoming_animes, 16 | extract_genre_list, 17 | } from "../../extracters/aniwatch/extracters"; 18 | import { 19 | Top10AnimeTimePeriod, 20 | ScrapedHomePage, 21 | } from "../../types/aniwatch/anime"; 22 | 23 | export const scrapeHomePage = async (): Promise< 24 | ScrapedHomePage | HttpError 25 | > => { 26 | const res: ScrapedHomePage = { 27 | spotLightAnimes: [], 28 | trendingAnimes: [], 29 | latestEpisodes: [], 30 | top10Animes: { 31 | day: [], 32 | week: [], 33 | month: [], 34 | }, 35 | featuredAnimes: { 36 | topAiringAnimes: [], 37 | mostPopularAnimes: [], 38 | mostFavoriteAnimes: [], 39 | latestCompletedAnimes: [], 40 | }, 41 | topUpcomingAnimes: [], 42 | genres: [], 43 | }; 44 | const URLs = await URL_fn(); 45 | try { 46 | const mainPage = await axios.get(URLs.HOME, { 47 | headers: { 48 | "User-Agent": headers.USER_AGENT_HEADER, 49 | "Accept-Encoding": headers.ACCEPT_ENCODEING_HEADER, 50 | Accept: headers.ACCEPT_HEADER, 51 | }, 52 | }); 53 | const $: CheerioAPI = load(mainPage.data); 54 | const trendingAnimeSelectors: SelectorType = 55 | "#anime-trending #trending-home .swiper-wrapper .swiper-slide"; 56 | const top10Selectors: SelectorType = 57 | '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; 58 | const latestEpisodesSelectors: SelectorType = 59 | "#main-content .block_area_home:nth-of-type(1) .tab-content .film_list-wrap .flw-item"; 60 | const topAiringSelectors: SelectorType = 61 | "#anime-featured .row div:nth-of-type(1) .anif-block-ul ul li"; 62 | const mostPopularSelectors: SelectorType = 63 | "#anime-featured .row div:nth-of-type(2) .anif-block-ul ul li"; 64 | const mostFavoriteSelectors: SelectorType = 65 | "#anime-featured .row div:nth-of-type(3) .anif-block-ul ul li"; 66 | const latestCompletedSelectors: SelectorType = 67 | "#anime-featured .row div:nth-of-type(4) .anif-block-ul ul li"; 68 | const topUpcomingSelectors: SelectorType = 69 | "#main-content .block_area_home:nth-of-type(3) .tab-content .film_list-wrap .flw-item"; 70 | const spotLightSelectors: SelectorType = 71 | "#slider .swiper-wrapper .swiper-slide"; 72 | const genresSelectors: SelectorType = 73 | "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; 74 | 75 | res.trendingAnimes = extract_trending_animes($, trendingAnimeSelectors); 76 | res.latestEpisodes = extract_latest_episodes($, latestEpisodesSelectors); 77 | 78 | res.featuredAnimes.topAiringAnimes = extract_featured_animes($, topAiringSelectors); 79 | res.featuredAnimes.mostPopularAnimes = extract_featured_animes($, mostPopularSelectors); 80 | res.featuredAnimes.mostFavoriteAnimes = extract_featured_animes($, mostFavoriteSelectors); 81 | res.featuredAnimes.latestCompletedAnimes = extract_featured_animes($, latestCompletedSelectors); 82 | 83 | res.topUpcomingAnimes = extract_top_upcoming_animes( 84 | $, 85 | topUpcomingSelectors, 86 | ); 87 | res.spotLightAnimes = extract_spotlight_animes($, spotLightSelectors); 88 | res.genres = extract_genre_list($, genresSelectors); 89 | 90 | $(top10Selectors).each((_index, element) => { 91 | const periodType = $(element) 92 | .attr("id") 93 | ?.split("-") 94 | ?.pop() 95 | ?.trim() as Top10AnimeTimePeriod; 96 | if (periodType === "day") { 97 | res.top10Animes.day = extract_top10_animes($, periodType); 98 | return; 99 | } 100 | if (periodType === "week") { 101 | res.top10Animes.week = extract_top10_animes($, periodType); 102 | return; 103 | } 104 | if (periodType === "month") { 105 | res.top10Animes.month = extract_top10_animes($, periodType); 106 | } 107 | }); 108 | 109 | return res; 110 | } catch (err) { 111 | //////////////////////////////////////////////////////////////// 112 | console.error("Error in scrapeHomePage:", err); // for TESTING// 113 | //////////////////////////////////////////////////////////////// 114 | 115 | if (err instanceof AxiosError) { 116 | throw createHttpError( 117 | err?.response?.status || 500, 118 | err?.response?.statusText || "Something went wrong", 119 | ); 120 | } else { 121 | throw createHttpError.InternalServerError("Internal server error"); 122 | } 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /src/types/aniwatch/anime.ts: -------------------------------------------------------------------------------- 1 | import { ScrapedHomePage } from "./home"; 2 | import { ScrapedAboutPage } from "./about"; 3 | import { ScrapedSearchPage } from "./search"; 4 | import { ScrapedCategoryPage } from "./category"; 5 | import { ScrapedEpisodesPage } from "./episodes"; 6 | import { ScrapedEpisodeServer } from "./servers"; 7 | import { ScrapedAnimeEpisodesSources } from "./episode_server_source"; 8 | import { GetRoot } from "./root"; 9 | 10 | interface MinimalAnime { 11 | id: string | null; 12 | name: string | null; 13 | img: string | null; 14 | } 15 | 16 | interface Anime extends MinimalAnime { 17 | episodes: { 18 | eps: number | null; 19 | sub: number | null; 20 | dub: number | null; 21 | }; 22 | } 23 | 24 | interface Top10Anime extends Anime { 25 | rank: number | null; 26 | } 27 | 28 | type Top10AnimeTimePeriod = "day" | "week" | "month"; 29 | 30 | interface SpotLightAnime extends Anime { 31 | rank: number | null; 32 | duration: string | null; 33 | category: string | null; 34 | releasedDay: string | null; 35 | quality: string | null; 36 | description: string | null; 37 | } 38 | 39 | interface TopUpcomingAnime extends Anime { 40 | duration: string | null; 41 | rated: boolean | false; 42 | } 43 | 44 | type LatestAnimeEpisode = TopUpcomingAnime; 45 | 46 | interface AboutAnimeInfo extends Anime { 47 | anime_id: number | null; 48 | mal_id: number | null; 49 | al_id: number | null; 50 | rating: string | null; 51 | category: string | null; 52 | duration: string | null; 53 | quality: string | null; 54 | description: string | null; 55 | } 56 | 57 | type ExtraAboutAnimeInfo = Record; 58 | 59 | interface AnimeSeasonsInfo extends MinimalAnime { 60 | seasonTitle: string | null; 61 | isCurrent: boolean | false; 62 | } 63 | 64 | interface RelatedAnime extends Anime { 65 | category: string | null; 66 | } 67 | 68 | type RecommendedAnime = TopUpcomingAnime; 69 | type MostPopularAnime = RelatedAnime; 70 | type SearchedAnime = TopUpcomingAnime; 71 | type CategoryAnime = TopUpcomingAnime; 72 | 73 | interface Episode { 74 | name: string | null; 75 | episodeNo: number | null; 76 | episodeId: string | null; 77 | filler: boolean | false; 78 | } 79 | 80 | interface SubEpisode { 81 | serverName: string; 82 | serverId: number | null; 83 | } 84 | 85 | type DubEpisode = SubEpisode; 86 | type RawEpisode = SubEpisode; 87 | 88 | interface Video { 89 | url: string; 90 | quality?: string; 91 | isM3U8?: boolean; 92 | size?: number; 93 | [x: string]: unknown; 94 | } 95 | 96 | interface Subtitle { 97 | id?: string; 98 | url: string; 99 | lang: string; 100 | } 101 | 102 | interface Intro { 103 | start: number; 104 | end: number; 105 | } 106 | 107 | interface ProxyConfig { 108 | /** 109 | * The proxy URL 110 | * @example https://proxy.com 111 | **/ 112 | url: string | string[]; 113 | /** 114 | * X-API-Key header value (if any) 115 | **/ 116 | key?: string; 117 | /** 118 | * The proxy rotation interval in milliseconds. (default: 5000) 119 | */ 120 | rotateInterval?: number; 121 | } 122 | 123 | interface IVideo { 124 | /** 125 | * The **MAIN URL** of the video provider that should take you to the video 126 | */ 127 | url: string; 128 | /** 129 | * The Quality of the video should include the `p` suffix 130 | */ 131 | quality?: string; 132 | /** 133 | * make sure to set this to `true` if the video is hls 134 | */ 135 | isM3U8?: boolean; 136 | /** 137 | * set this to `true` if the video is dash (mpd) 138 | */ 139 | isDASH?: boolean; 140 | /** 141 | * size of the video in **bytes** 142 | */ 143 | size?: number; 144 | [x: string]: unknown; // other fields 145 | } 146 | 147 | interface ISubtitle { 148 | /** 149 | * The id of the subtitle. **not** required 150 | */ 151 | id?: string; 152 | /** 153 | * The **url** that should take you to the subtitle **directly**. 154 | */ 155 | url: string; 156 | /** 157 | * The language of the subtitle 158 | */ 159 | lang: string; 160 | } 161 | 162 | interface ISource { 163 | headers?: { [k: string]: string }; 164 | intro?: Intro; 165 | outro?: Intro; 166 | subtitles?: ISubtitle[]; 167 | sources: IVideo[]; 168 | download?: string; 169 | embedURL?: string; 170 | } 171 | 172 | type AnimeServers = 173 | | "hd-1" 174 | | "hd-2" 175 | | "vidstreaming" 176 | | "megacloud" 177 | | "streamsb" 178 | | "streamtape" 179 | | "vidcloud"; 180 | 181 | enum Servers { 182 | VidStreaming = "vidstreaming", 183 | MegaCloud = "megacloud", 184 | StreamSB = "streamsb", 185 | StreamTape = "streamtape", 186 | VidCloud = "vidcloud", 187 | HD1 = "hd-1", 188 | HD2 = "hd-2", 189 | AsianLoad = "asianload", 190 | GogoCDN = "gogocdn", 191 | MixDrop = "mixdrop", 192 | UpCloud = "upcloud", 193 | VizCloud = "vizcloud", 194 | MyCloud = "mycloud", 195 | Filemoon = "filemoon", 196 | } 197 | 198 | type Category = 199 | | "subbed-anime" 200 | | "dubbed-anime" 201 | | "tv" 202 | | "movie" 203 | | "most-popular" 204 | | "top-airing" 205 | | "ova" 206 | | "ona" 207 | | "special" 208 | | "events"; 209 | 210 | type Genre = 211 | | "Action" 212 | | "Adventure" 213 | | "Cars" 214 | | "Comedy" 215 | | "Dementia" 216 | | "Demons" 217 | | "Drama" 218 | | "Ecchi" 219 | | "Fantasy" 220 | | "Game" 221 | | "Harem" 222 | | "Historical" 223 | | "Horror" 224 | | "Isekai" 225 | | "Josei" 226 | | "Kids" 227 | | "Magic" 228 | | "Martial Arts" 229 | | "Mecha" 230 | | "Military" 231 | | "Music" 232 | | "Mystery" 233 | | "Parody" 234 | | "Police" 235 | | "Psychological" 236 | | "Romance" 237 | | "Samurai" 238 | | "School" 239 | | "Sci-Fi" 240 | | "Seinen" 241 | | "Shoujo" 242 | | "Shoujo Ai" 243 | | "Shounen" 244 | | "Shounen Ai" 245 | | "Slice of Life" 246 | | "Space" 247 | | "Sports" 248 | | "Super Power" 249 | | "Supernatural" 250 | | "Thriller" 251 | | "Vampire"; 252 | 253 | export { 254 | GetRoot, 255 | ScrapedHomePage, 256 | ScrapedAboutPage, 257 | ScrapedSearchPage, 258 | ScrapedCategoryPage, 259 | ScrapedEpisodesPage, 260 | ScrapedEpisodeServer, 261 | ScrapedAnimeEpisodesSources, 262 | MinimalAnime, 263 | Anime, 264 | Top10Anime, 265 | Top10AnimeTimePeriod, 266 | SpotLightAnime, 267 | TopUpcomingAnime, 268 | LatestAnimeEpisode, 269 | AboutAnimeInfo, 270 | ExtraAboutAnimeInfo, 271 | AnimeSeasonsInfo, 272 | RelatedAnime, 273 | RecommendedAnime, 274 | MostPopularAnime, 275 | SearchedAnime, 276 | CategoryAnime, 277 | Genre, 278 | Category, 279 | Episode, 280 | SubEpisode, 281 | DubEpisode, 282 | RawEpisode, 283 | AnimeServers, 284 | Servers, 285 | Video, 286 | Subtitle, 287 | Intro, 288 | ProxyConfig, 289 | IVideo, 290 | ISource, 291 | ISubtitle, 292 | }; 293 | -------------------------------------------------------------------------------- /src/utils/aniwatch/rapidcloud.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js"; 2 | import { 3 | substringAfter, 4 | substringBefore, 5 | } from "../../utils/aniwatch/serverSubString"; 6 | import { IVideo, ISubtitle, Intro } from "../../types/aniwatch/anime"; 7 | import VideoExtractor from "./video-extractor"; 8 | import { load } from "cheerio"; 9 | 10 | class RapidCloud extends VideoExtractor { 11 | protected override serverName = "RapidCloud"; 12 | protected override sources: IVideo[] = []; 13 | 14 | private readonly fallbackKey = "c1d17096f2ca11b7"; 15 | private readonly host = "https://rapid-cloud.co"; 16 | 17 | override extract = async ( 18 | videoUrl: URL, 19 | ): Promise<{ sources: IVideo[] } & { subtitles: ISubtitle[] }> => { 20 | const result: { 21 | sources: IVideo[]; 22 | subtitles: ISubtitle[]; 23 | intro?: Intro; 24 | outro?: Intro; 25 | } = { 26 | sources: [], 27 | subtitles: [], 28 | }; 29 | try { 30 | const id = videoUrl.href.split("/").pop()?.split("?")[0]; 31 | const options = { 32 | headers: { 33 | "X-Requested-With": "XMLHttpRequest", 34 | }, 35 | }; 36 | 37 | let res = null; 38 | 39 | res = await this.client.get( 40 | `https://${videoUrl.hostname}/embed-2/ajax/e-1/getSources?id=${id}`, 41 | options, 42 | ); 43 | 44 | let { 45 | data: { sources, tracks, intro, outro, encrypted }, 46 | } = res; 47 | 48 | let decryptKey = await ( 49 | await this.client.get( 50 | "https://raw.githubusercontent.com/cinemaxhq/keys/e1/key", 51 | ) 52 | ).data; 53 | 54 | decryptKey = substringBefore( 55 | substringAfter(decryptKey, '"blob-code blob-code-inner js-file-line">'), 56 | "", 57 | ); 58 | 59 | if (!decryptKey) { 60 | decryptKey = await ( 61 | await this.client.get( 62 | "https://raw.githubusercontent.com/cinemaxhq/keys/e1/key", 63 | ) 64 | ).data; 65 | } 66 | 67 | if (!decryptKey) decryptKey = this.fallbackKey; 68 | 69 | try { 70 | if (encrypted) { 71 | const sourcesArray = sources.split(""); 72 | 73 | let extractedKey = ""; 74 | let currentIndex = 0; 75 | for (const index of decryptKey) { 76 | const start = index[0] + currentIndex; 77 | const end = start + index[1]; 78 | for (let i = start; i < end; i++) { 79 | extractedKey += res.data.sources[i]; 80 | sourcesArray[i] = ""; 81 | } 82 | currentIndex += index[1]; 83 | } 84 | 85 | decryptKey = extractedKey; 86 | sources = sourcesArray.join(""); 87 | 88 | const decrypt = CryptoJS.AES.decrypt(sources, decryptKey); 89 | sources = JSON.parse(decrypt.toString(CryptoJS.enc.Utf8)); 90 | } 91 | } catch (err) { 92 | throw new Error("Cannot decrypt sources. Perhaps the key is invalid."); 93 | } 94 | this.sources = sources?.map((s: any) => ({ 95 | url: s.file, 96 | isM3U8: s.file.includes(".m3u8"), 97 | })); 98 | 99 | result.sources.push(...this.sources); 100 | 101 | if (videoUrl.href.includes(new URL(this.host).host)) { 102 | result.sources = []; 103 | this.sources = []; 104 | for (const source of sources) { 105 | const { data } = await this.client.get(source.file, options); 106 | const m3u8data = data 107 | .split("\n") 108 | .filter( 109 | (line: string) => 110 | line.includes(".m3u8") && line.includes("RESOLUTION="), 111 | ); 112 | 113 | const secondHalf = m3u8data.map((line: string) => 114 | line 115 | .match(/RESOLUTION=.*,(C)|URI=.*/g) 116 | ?.map((s: string) => s.split("=")[1]), 117 | ); 118 | 119 | const TdArray = secondHalf.map((s: string[]) => { 120 | const f1 = s[0].split(",C")[0]; 121 | const f2 = s[1].replace(/"/g, ""); 122 | 123 | return [f1, f2]; 124 | }); 125 | for (const [f1, f2] of TdArray) { 126 | this.sources.push({ 127 | url: `${source.file?.split("master.m3u8")[0]}${f2.replace("iframes", "index")}`, 128 | quality: f1.split("x")[1] + "p", 129 | isM3U8: f2.includes(".m3u8"), 130 | }); 131 | } 132 | result.sources.push(...this.sources); 133 | } 134 | } 135 | 136 | result.intro = 137 | intro?.end > 1 ? { start: intro.start, end: intro.end } : undefined; 138 | result.outro = 139 | outro?.end > 1 ? { start: outro.start, end: outro.end } : undefined; 140 | 141 | result.sources.push({ 142 | url: sources[0].file, 143 | isM3U8: sources[0].file.includes(".m3u8"), 144 | quality: "auto", 145 | }); 146 | 147 | result.subtitles = tracks 148 | .map((s: any) => 149 | s.file 150 | ? { 151 | url: s.file, 152 | lang: s.label ? s.label : "Thumbnails", 153 | } 154 | : null, 155 | ) 156 | .filter((s: any) => s); 157 | 158 | return result; 159 | } catch (err) { 160 | throw err; 161 | } 162 | }; 163 | 164 | private captcha = async (url: string, key: string): Promise => { 165 | const uri = new URL(url); 166 | const domain = uri.protocol + "//" + uri.host; 167 | 168 | const { data } = await this.client.get( 169 | `https://www.google.com/recaptcha/api.js?render=${key}`, 170 | { 171 | headers: { 172 | Referer: domain, 173 | }, 174 | }, 175 | ); 176 | 177 | const v = data 178 | ?.substring(data.indexOf("/releases/"), data.lastIndexOf("/recaptcha")) 179 | .split("/releases/")[1]; 180 | 181 | //TODO: NEED to fix the co (domain) parameter to work with every domain 182 | const anchor = `https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=kr42069kr&k=${key}&co=aHR0cHM6Ly9yYXBpZC1jbG91ZC5ydTo0NDM.&v=${v}`; 183 | const c = load((await this.client.get(anchor)).data)( 184 | "#recaptcha-token", 185 | ).attr("value"); 186 | 187 | // currently its not returning proper response. not sure why 188 | const res = await this.client.post( 189 | `https://www.google.com/recaptcha/api2/reload?k=${key}`, 190 | { 191 | v: v, 192 | k: key, 193 | c: c, 194 | co: "aHR0cHM6Ly9yYXBpZC1jbG91ZC5ydTo0NDM.", 195 | sa: "", 196 | reason: "q", 197 | }, 198 | { 199 | headers: { 200 | Referer: anchor, 201 | }, 202 | }, 203 | ); 204 | 205 | return res.data.substring( 206 | res.data.indexOf('rresp","'), 207 | res.data.lastIndexOf('",null'), 208 | ); 209 | }; 210 | 211 | // private wss = async (): Promise => { 212 | // let sId = ''; 213 | 214 | // const ws = new WebSocket('wss://ws1.rapid-cloud.ru/socket.io/?EIO=4&transport=websocket'); 215 | 216 | // ws.on('open', () => { 217 | // ws.send('40'); 218 | // }); 219 | 220 | // return await new Promise((resolve, reject) => { 221 | // ws.on('message', (data: string) => { 222 | // data = data.toString(); 223 | // if (data?.startsWith('40')) { 224 | // sId = JSON.parse(data.split('40')[1]).sid; 225 | // ws.close(4969, "I'm a teapot"); 226 | // resolve(sId); 227 | // } 228 | // }); 229 | // }); 230 | // }; 231 | } 232 | 233 | export default RapidCloud; 234 | -------------------------------------------------------------------------------- /src/utils/aniwatch/megacloud.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { IVideo, ISubtitle, Intro } from "../../types/aniwatch/anime"; 3 | import VideoExtractor from "./video-extractor"; 4 | import { getSources } from "./megacloud.getsrcs"; 5 | 6 | const megacloud = { 7 | script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=", 8 | sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=", 9 | } as const; 10 | 11 | type tracks = { 12 | file: string; 13 | kind: string; 14 | label?: string; 15 | default?: boolean; 16 | }; 17 | 18 | export type unencrypSources = { 19 | file: string; 20 | type: string; 21 | }; 22 | 23 | export type apiFormat = { 24 | sources: string | unencrypSources[]; 25 | tracks: tracks[]; 26 | encrypted: boolean; 27 | intro: Intro; 28 | outro: Intro; 29 | server: number; 30 | }; 31 | 32 | type ExtractedData = Pick & { 33 | sources: { url: string; type: string }[]; 34 | }; 35 | 36 | class MegaCloud extends VideoExtractor { 37 | protected override serverName = "MegaCloud"; 38 | protected override sources: IVideo[] = []; 39 | 40 | async extract(videoUrl: URL) { 41 | try { 42 | const result: { 43 | sources: IVideo[]; 44 | subtitles: ISubtitle[]; 45 | intro?: Intro; 46 | outro?: Intro; 47 | } = { 48 | sources: [], 49 | subtitles: [], 50 | }; 51 | 52 | const videoId = videoUrl?.href?.split("/")?.pop()?.split("?")[0]; 53 | const { data: srcsData } = await this.client.get( 54 | megacloud.sources.concat(videoId || ""), 55 | { 56 | headers: { 57 | Accept: "*/*", 58 | "X-Requested-With": "XMLHttpRequest", 59 | "User-Agent": 60 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", 61 | Referer: videoUrl.href, 62 | }, 63 | }, 64 | ); 65 | if (!srcsData) { 66 | throw new Error("Url may have an invalid video id"); 67 | } 68 | 69 | const encryptedString = srcsData.sources; 70 | if (!srcsData.encrypted && Array.isArray(encryptedString)) { 71 | result.intro = srcsData.intro; 72 | result.outro = srcsData.outro; 73 | result.subtitles = srcsData.tracks.map((s: any) => ({ 74 | url: s.file, 75 | lang: s.label ? s.label : "Thumbnails", 76 | })); 77 | result.sources = encryptedString.map((s) => ({ 78 | url: s.file, 79 | type: s.type, 80 | isM3U8: s.file.includes(".m3u8"), 81 | })); 82 | return result; 83 | } 84 | 85 | const { data } = await this.client.get( 86 | megacloud.script.concat(Date.now().toString()), 87 | ); 88 | 89 | const text = data; 90 | if (!text) throw new Error("Couldn't fetch script to decrypt resource"); 91 | 92 | const vars = this.extractVariables(text); 93 | const { secret, encryptedSource } = this.getSecret( 94 | encryptedString as string, 95 | vars, 96 | ); 97 | const decrypted = this.decrypt(encryptedSource, secret); 98 | try { 99 | const sources = JSON.parse(decrypted); 100 | result.intro = srcsData.intro; 101 | result.outro = srcsData.outro; 102 | result.subtitles = srcsData.tracks.map((s: any) => ({ 103 | url: s.file, 104 | lang: s.label ? s.label : "Thumbnails", 105 | })); 106 | result.sources = sources.map((s: any) => ({ 107 | url: s.file, 108 | type: s.type, 109 | isM3U8: s.file.includes(".m3u8"), 110 | })); 111 | 112 | return result; 113 | } catch (error) { 114 | throw new Error("Failed to decrypt resource"); 115 | } 116 | } catch (err) { 117 | throw err; 118 | } 119 | } 120 | 121 | extractVariables(text: string) { 122 | // copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30' 123 | const regex = 124 | /case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);/g; 125 | const matches = text.matchAll(regex); 126 | const vars = Array.from(matches, (match) => { 127 | const matchKey1 = this.matchingKey(match[1], text); 128 | const matchKey2 = this.matchingKey(match[2], text); 129 | try { 130 | return [parseInt(matchKey1, 16), parseInt(matchKey2, 16)]; 131 | } catch (e) { 132 | return []; 133 | } 134 | }).filter((pair) => pair.length > 0); 135 | 136 | return vars; 137 | } 138 | 139 | getSecret(encryptedString: string, values: number[][]) { 140 | let secret = "", 141 | encryptedSource = "", 142 | encryptedSourceArray = encryptedString.split(""), 143 | currentIndex = 0; 144 | 145 | for (const index of values) { 146 | const start = index[0] + currentIndex; 147 | const end = start + index[1]; 148 | 149 | for (let i = start; i < end; i++) { 150 | secret += encryptedString[i]; 151 | encryptedSourceArray[i] = ""; 152 | } 153 | currentIndex += index[1]; 154 | } 155 | 156 | encryptedSource = encryptedSourceArray.join(""); 157 | 158 | return { secret, encryptedSource }; 159 | } 160 | 161 | decrypt(encrypted: string, keyOrSecret: string, maybe_iv?: string) { 162 | let key; 163 | let iv; 164 | let contents; 165 | if (maybe_iv) { 166 | key = keyOrSecret; 167 | iv = maybe_iv; 168 | contents = encrypted; 169 | } else { 170 | // copied from 'https://github.com/brix/crypto-js/issues/468' 171 | const cypher = Buffer.from(encrypted, "base64"); 172 | const salt = cypher.subarray(8, 16); 173 | const password = Buffer.concat([ 174 | Buffer.from(keyOrSecret, "binary"), 175 | salt, 176 | ]); 177 | const md5Hashes = []; 178 | let digest = password; 179 | for (let i = 0; i < 3; i++) { 180 | md5Hashes[i] = crypto.createHash("md5").update(digest).digest(); 181 | digest = Buffer.concat([md5Hashes[i], password]); 182 | } 183 | key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); 184 | iv = md5Hashes[2]; 185 | contents = cypher.subarray(16); 186 | } 187 | 188 | const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); 189 | const decrypted = 190 | decipher.update( 191 | contents as any, 192 | typeof contents === "string" ? "base64" : undefined, 193 | "utf8", 194 | ) + decipher.final(); 195 | 196 | return decrypted; 197 | } 198 | 199 | // function copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30' 200 | matchingKey(value: string, script: string) { 201 | const regex = new RegExp(`,${value}=((?:0x)?([0-9a-fA-F]+))`); 202 | const match = script.match(regex); 203 | if (match) { 204 | return match[1].replace(/^0x/, ""); 205 | } else { 206 | throw new Error("Failed to match the key"); 207 | } 208 | } 209 | 210 | // https://megacloud.tv/embed-2/e-1/1hnXq7VzX0Ex?k=1 211 | async extract2(embedIframeURL: URL): Promise { 212 | try { 213 | const extractedData: ExtractedData = { 214 | tracks: [], 215 | intro: { 216 | start: 0, 217 | end: 0, 218 | }, 219 | outro: { 220 | start: 0, 221 | end: 0, 222 | }, 223 | sources: [], 224 | }; 225 | 226 | const xrax = embedIframeURL.pathname.split("/").pop() || ""; 227 | 228 | const resp = await getSources(xrax); 229 | if (!resp) return extractedData; 230 | 231 | if (Array.isArray(resp.sources)) { 232 | extractedData.sources = resp.sources.map((s) => ({ 233 | url: s.file, 234 | type: s.type, 235 | })); 236 | } 237 | extractedData.intro = resp.intro ? resp.intro : extractedData.intro; 238 | extractedData.outro = resp.outro ? resp.outro : extractedData.outro; 239 | extractedData.tracks = resp.tracks; 240 | 241 | return extractedData; 242 | } catch (err) { 243 | throw err; 244 | } 245 | } 246 | } 247 | 248 | export default MegaCloud; 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡Anime-API⚡ 2 |

3 | 4 |
5 | api-anime-rouge.vercel.app 6 |

7 |

8 | 9 | Check it out at api-anime-rouge.vercel.app. 10 | 11 | 12 | 13 | >[!IMPORTANT] 14 | >Local Caching is implemented 15 | 16 | | Routes | Caching Duration | 17 | |---------------------------------------------------------|-----------------------| 18 | | `/aniwatch/` | 1 day (3600 * 24) | 19 | | `/aniwatch/az-list?page=${page}` | 1 day (3600 * 24) | 20 | | `/aniwatch/search?keyword=$(query)&page=${page}` | 1 hour (3600) | 21 | | `/aniwatch/anime/:id` | 1 month (3600 * 24 * 31) | 22 | | `/aniwatch/episodes/:id` | 1 day (3600 * 24) | 23 | | `/aniwatch/servers?id=${id}` | 1 day (3600 * 24) | 24 | | `/aniwatch/episode-srcs?id=${episodeId}?server=${server}&category=${category}` | 30 minutes (1800) | 25 | | `/aniwatch/:category?page=${page}` | 1 day (3600 * 24) | 26 | | `/gogoanime/home` | 1 day (3600 * 24) | 27 | | `/gogoanime/search?keyword=${query}&page=${page}` | 1 hour (3600) | 28 | | `/gogoanime/anime/:id` | 1 day (3600 * 24) | 29 | | `/gogoanime/recent-releases?page=${pageNo}` | 1 day (3600 * 24) | 30 | | `/gogoanime/new-seasons?page=${pageNo}` | 1 day (3600 * 24) | 31 | | `/gogoanime/popular?page=${pageNo}` | 1 day (3600 * 24) | 32 | | `/gogoanime/completed?page=${pageNo}` | 1 day (3600 * 24) | 33 | | `/gogoanime/anime-movies?page=${pageNo}` | 1 day (3600 * 24) | 34 | | `/gogoanime/top-airing?page=${pageNo}` | 1 day (3600 * 24) | 35 | 36 | 37 | ### Deploy this project to Vercel 38 | 39 | Click the button below to deploy this project to your Vercel account: 40 | 41 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/falcon71181/Anime-API) 42 | 43 | 44 | ## ⚡ Web Scraping Status 45 | 46 | Anime Websites | STATUS 47 | -------------- | ------------- 48 | aniwatch | DONE 49 | gogoanime | WORKING ON IT 50 | kickassanime | IN FUTURE 51 | 52 | >[!NOTE] 53 | >More Websites Will be Added in Future 54 | 55 | ## Index 56 | 57 | - [AniWatch](#aniwatch) 58 | - [GogoAnime](#gogoanime) 59 | 60 | ## AniWatch 61 | 62 | ### `GET` AniWatch Home Page 63 | 64 | #### Endpoint 65 | 66 | ```url 67 | https://api-anime-rouge.vercel.app/aniwatch/ 68 | ``` 69 | 70 | #### Request sample 71 | 72 | ```javascript 73 | const res = await fetch("https://api-anime-rouge.vercel.app/aniwatch/"); 74 | const data = await res.json(); 75 | console.log(data); 76 | ``` 77 | 78 | #### Response Schema 79 | 80 | ```typescript 81 | { 82 | spotlightAnimes: [ 83 | { 84 | id: string, 85 | name: string, 86 | rank: number, 87 | img: string, 88 | episodes: { 89 | eps: number, 90 | sub: number, 91 | dub: number, 92 | }, 93 | duration: string, 94 | quality: string, 95 | category: string, 96 | releasedDay: string, 97 | descriptions: string, 98 | }, 99 | {...}, 100 | ], 101 | trendingAnimes: [ 102 | { 103 | id: string, 104 | name: string, 105 | img: string, 106 | }, 107 | {...}, 108 | ], 109 | latestEpisodes: [ 110 | { 111 | id: string, 112 | name: string, 113 | img: string, 114 | episodes: { 115 | eps: number, 116 | sub: number, 117 | dub: number, 118 | }, 119 | duration: string, 120 | rated: boolean, 121 | }, 122 | {...}, 123 | ], 124 | top10Animes: { 125 | day: [ 126 | { 127 | id: string, 128 | name: string, 129 | rank: number, 130 | img: string, 131 | episodes: { 132 | eps: number, 133 | sub: number, 134 | dub: number, 135 | }, 136 | }, 137 | {...}, 138 | ], 139 | week: [...], 140 | month: [...] 141 | }, 142 | featuredAnimes: { 143 | topAiringAnimes: [ 144 | { 145 | id: string, 146 | name: string, 147 | img: string, 148 | }, 149 | {...}, 150 | ], 151 | mostPopularAnimes: [ 152 | { 153 | id: string, 154 | name: string, 155 | img: string, 156 | }, 157 | {...}, 158 | ], 159 | mostFavoriteAnimes: [ 160 | { 161 | id: string, 162 | name: string, 163 | img: string, 164 | }, 165 | {...}, 166 | ], 167 | latestCompletedAnimes: [ 168 | { 169 | id: string, 170 | name: string, 171 | img: string, 172 | }, 173 | {...}, 174 | ], 175 | }, 176 | topUpcomingAnimes: [ 177 | { 178 | id: string, 179 | name: string, 180 | img: string, 181 | episodes: { 182 | eps: number, 183 | sub: number, 184 | dub: number, 185 | }, 186 | duration: string, 187 | rated: boolean, 188 | }, 189 | {...}, 190 | ], 191 | genres: string[] 192 | } 193 | ``` 194 | 195 | ### `GET` AniWatch A to Z List Page 196 | 197 | #### Endpoint 198 | 199 | ```url 200 | https://api-anime-rouge.vercel.app/aniwatch/az-list?page=${page} 201 | ``` 202 | 203 | #### Query Parameters 204 | 205 | | Parameter | Type | Description | Required? | Default | 206 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 207 | | `page` | number | Page No. of Search Page | YES | 1 | 208 | 209 | 210 | #### Request sample 211 | 212 | ```typescript 213 | const resp = await fetch("https://api-anime-rouge.vercel.app/aniwatch/az-list?page=69"); 214 | const data = await resp.json(); 215 | console.log(data); 216 | ``` 217 | 218 | #### Response Schema 219 | 220 | ```typescript 221 | [ 222 | { 223 | "id": string, 224 | "name": string, 225 | "category": string, 226 | "img": string, 227 | "episodes": { 228 | "eps": number, 229 | "sub": number, 230 | "dub": number 231 | } 232 | }, 233 | {...}, 234 | ], 235 | ``` 236 | 237 | ### `GET` Anime About Info 238 | 239 | #### Endpoint 240 | 241 | ```sh 242 | https://api-anime-rouge.vercel.app/aniwatch/anime/:id 243 | ``` 244 | 245 | #### Query Parameters 246 | 247 | | Parameter | Type | Description | Required? | Default | 248 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 249 | | `id` | string | The unique Anime ID | YES | ----- | 250 | 251 | > [!NOTE] 252 | > Anime ID should be In Kebab Case 253 | 254 | #### Request sample 255 | 256 | ```javascript 257 | const res = await fetch( 258 | "https://api-anime-rouge.vercel.app/aniwatch/anime/jujutsu-kaisen-2nd-season-18413" 259 | ); 260 | const data = await res.json(); 261 | console.log(data); 262 | ``` 263 | 264 | #### Response Schema 265 | 266 | ``` typescript 267 | { 268 | "info": { 269 | "id": string, 270 | "anime_id": number, 271 | "mal_id": number, 272 | "al_id": number, 273 | "name": string, 274 | "img": string, 275 | "rating": string, 276 | "episodes": { 277 | "eps": number, 278 | "sub": number, 279 | "dub": number 280 | }, 281 | "category": string, 282 | "quality": string, 283 | "duration": string, 284 | "description": string 285 | }, 286 | "moreInfo": { 287 | "Japanese:": string, 288 | "Synonyms:": string, 289 | "Aired:": string, 290 | "Premiered:": string, 291 | "Duration:": string, 292 | "Status:": string, 293 | "MAL Score:": string, 294 | "Studios:": string[], 295 | "Genres": string[], 296 | "Producers": string[] 297 | }, 298 | "seasons": [ 299 | { 300 | "id": string, 301 | "name": string, 302 | "seasonTitle": string, 303 | "img": string, 304 | "isCurrent": boolean 305 | }, 306 | {...}, 307 | }, 308 | "relatedAnimes": [ 309 | { 310 | "id": string, 311 | "name": string, 312 | "category": string, 313 | "img": string, 314 | "episodes": { 315 | "eps": number, 316 | "sub": number, 317 | "dub": number 318 | } 319 | }, 320 | {...}, 321 | ], 322 | "recommendedAnimes": [ 323 | { 324 | "id": string, 325 | "name": string, 326 | "img": string, 327 | "episodes": { 328 | "eps": number, 329 | "sub": number, 330 | "dub": number 331 | }, 332 | "duration": string, 333 | "rated": boolean 334 | }, 335 | {...}, 336 | ], 337 | "mostPopularAnimes": [ 338 | { 339 | "id": string, 340 | "name": string, 341 | "category": string, 342 | "img": string, 343 | "episodes": { 344 | "eps": number, 345 | "sub": number, 346 | "dub": number 347 | } 348 | }, 349 | {...}, 350 | ], 351 | } 352 | ``` 353 | 354 | ### `GET` Search Anime 355 | 356 | #### Endpoint 357 | 358 | ```sh 359 | https://api-anime-rouge.vercel.app/aniwatch/search?keyword=$(query)&page=$(page) 360 | ``` 361 | 362 | #### Query Parameters 363 | 364 | | Parameter | Type | Description | Required? | Default | 365 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 366 | | `query` | string | Search Query for Anime | YES | ----- | 367 | | `page` | number | Page No. of Search Page | YES | 1 | 368 | > [!NOTE] 369 | >
Search Query should be In Kebab Case
370 | >
Page No should be a Number
371 | #### Request sample 372 | 373 | ```javascript 374 | const res = await fetch( 375 | "https://api-anime-rouge.vercel.app/aniwatch/search?keyword=one+piece&page=1" 376 | ); 377 | const data = await res.json(); 378 | console.log(data); 379 | ``` 380 | 381 | #### Response Schema 382 | 383 | ```typescript 384 | { 385 | "animes": [ 386 | { 387 | "id": string, 388 | "name": string, 389 | "img": string, 390 | "episodes": { 391 | "eps": number, 392 | "sub": number, 393 | "dub": number 394 | }, 395 | "duration": string, 396 | "rated": boolean 397 | }, 398 | {...}, 399 | ], 400 | "mostPopularAnimes": [ 401 | { 402 | "id": string, 403 | "name": string, 404 | "category": string, 405 | "img": string, 406 | "episodes": { 407 | "eps": number, 408 | "sub": number, 409 | "dub": number 410 | } 411 | }, 412 | {...}, 413 | ], 414 | "currentPage": number, 415 | "hasNextPage": boolean, 416 | "totalPages": number, 417 | "genres": string[] 418 | } 419 | ``` 420 | 421 | ### `GET` Category Anime 422 | 423 | #### Endpoint 424 | 425 | ```sh 426 | https://api-anime-rouge.vercel.app/aniwatch/:category?page=$(page) 427 | ``` 428 | 429 | #### Query Parameters 430 | 431 | | Parameter | Type | Description | Required? | Default | 432 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 433 | | `category`| string | Search Query for Anime | YES | ----- | 434 | | `page` | number | Page No. of Search Page | YES | 1 | 435 | 436 | 437 | 438 | > [!NOTE] 439 | >
category should be In Kebab Case
440 | >
Page No should be a Number
441 | 442 | 443 | 444 | > [!TIP] 445 | > Add type to Category - "subbed-anime" | "dubbed-anime" | "tv" | "movie" | "most-popular" | "top-airing" | "ova" | "ona" | "special" | "events"; 446 | 447 | 448 | 449 | #### Request sample 450 | 451 | ```javascript 452 | const res = await fetch( 453 | "https://api-anime-rouge.vercel.app/aniwatch/ona?page=1" 454 | ); 455 | const data = await res.json(); 456 | console.log(data); 457 | ``` 458 | 459 | #### Response Schema 460 | 461 | ```typescript 462 | { 463 | "animes": [ 464 | { 465 | "id": string, 466 | "name": string, 467 | "img": string, 468 | "episodes": { 469 | "eps": number, 470 | "sub": number, 471 | "dub": number 472 | }, 473 | "duration": string, 474 | "rated": boolean 475 | }, 476 | {...}, 477 | ], 478 | "top10Animes": { 479 | "day": [ 480 | { 481 | "id": string, 482 | "name": string, 483 | "rank": number, 484 | "img": string, 485 | "episodes": { 486 | "eps": number, 487 | "sub": number, 488 | "dub": number 489 | } 490 | }, 491 | {..}, 492 | ], 493 | "week": [ 494 | { 495 | "id": string, 496 | "name": string, 497 | "rank": number, 498 | "img": string, 499 | "episodes": { 500 | "eps": number, 501 | "sub": number, 502 | "dub": number 503 | } 504 | }, 505 | {...}, 506 | ], 507 | "month": [ 508 | { 509 | "id": string, 510 | "name": string, 511 | "rank": number, 512 | "img": string, 513 | "episodes": { 514 | "eps": number, 515 | "sub": number, 516 | "dub": number 517 | } 518 | }, 519 | {...}, 520 | ], 521 | "category": string, 522 | "genres": string[], 523 | "currentPage": number, 524 | "hasNextPage": boolean, 525 | "totalPages": number 526 | } 527 | ``` 528 | 529 | ### `GET` Anime Episodes 530 | 531 | #### Endpoint 532 | 533 | ```sh 534 | https://api-anime-rouge.vercel.app/aniwatch/episodes/:id 535 | ``` 536 | 537 | #### Query Parameters 538 | 539 | | Parameter | Type | Description | Required? | Default | 540 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 541 | | `id` | string | Anime ID | YES | ----- | 542 | 543 | 544 | 545 | > [!NOTE] 546 | >
Anime ID should be In Kebab Case
547 | 548 | 549 | 550 | #### Request sample 551 | 552 | ```javascript 553 | const res = await fetch( 554 | "https://api-anime-rouge.vercel.app/aniwatch/episodes/one-piece-100" 555 | ); 556 | const data = await res.json(); 557 | console.log(data); 558 | ``` 559 | 560 | #### Response Schema 561 | 562 | ```typescript 563 | { 564 | "totalEpisodes": number, 565 | "episodes": [ 566 | { 567 | "name": string, 568 | "episodeNo": number, 569 | "episodeId": string, 570 | "filler": boolean 571 | }, 572 | {...}, 573 | ] 574 | } 575 | ``` 576 | 577 | ### `GET` Anime Episodes Servers 578 | 579 | #### Endpoint 580 | 581 | ```sh 582 | https://api-anime-rouge.vercel.app/aniwatch/servers?id=${id} 583 | ``` 584 | 585 | #### Query Parameters 586 | 587 | | Parameter | Type | Description | Required? | Default | 588 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 589 | | `id` | string | Episode ID | YES | ----- | 590 | 591 | 592 | 593 | > [!NOTE] 594 | >
Episode ID should be In Kebab Case
595 | 596 | important 597 | 598 | > [!NOTE] 599 | >
id is a combination of AnimeId and EpisodeId
600 | 601 | eg. 602 | ```bash 603 | one-piece-100?ep=84802 604 | ``` 605 | 606 | 607 | 608 | #### Request sample 609 | 610 | ```javascript 611 | const res = await fetch( 612 | "https://api-anime-rouge.vercel.app/aniwatch/servers?id=one-piece-100?ep=84802" 613 | ); 614 | const data = await res.json(); 615 | console.log(data); 616 | ``` 617 | 618 | #### Response Schema 619 | 620 | ```typescript 621 | { 622 | "episodeId": string, 623 | "episodeNo": number, 624 | "sub": [ 625 | { 626 | "serverName": string, 627 | "serverId": number 628 | }, 629 | {...}, 630 | ], 631 | "dub": [ 632 | { 633 | "serverName": string, 634 | "serverId": number 635 | }, 636 | {...}, 637 | ], 638 | } 639 | ``` 640 | 641 | 642 | ### `GET` Anime Episode Streaming Source Links 643 | 644 | #### Endpoint 645 | 646 | ```sh 647 | https://api-anime-rouge.vercel.app/anime/episode-srcs?id={episodeId}&server={server}&category={category} 648 | ``` 649 | 650 | #### Query Parameters 651 | 652 | | Parameter | Type | Description | Required? | Default | 653 | | :--------: | :----: | :-------------------------------------------: | :-------: | :--------------: | 654 | | `id` | string | episode Id | Yes | -- | 655 | | `server` | string | server name. | No | `"vidstreaming"` | 656 | | `category` | string | The category of the episode ('sub' or 'dub'). | No | `"sub"` | 657 | 658 | #### Request sample 659 | 660 | ```javascript 661 | const res = await fetch( 662 | "https://api-anime-rouge.vercel.app/aniwatch/episode-srcs?id=solo-leveling-18718?ep=120094&server=vidstreaming&category=sub" 663 | ); 664 | const data = await res.json(); 665 | console.log(data); 666 | ``` 667 | > [!CAUTION] 668 | > decryption key changes frequently ..., it sometime may not work 669 | 670 | 671 | 672 | #### Response Schema 673 | 674 | ```typescript 675 | { 676 | headers: { 677 | Referer: string, 678 | "User-Agent": string, 679 | ... 680 | }, 681 | sources: [ 682 | { 683 | url: string, 684 | isM3U8: boolean, 685 | quality?: string, 686 | }, 687 | {...} 688 | ], 689 | subtitles: [ 690 | { 691 | lang: "English", 692 | url: string, 693 | }, 694 | {...} 695 | ], 696 | anilistID: number | null, 697 | malID: number | null, 698 | } 699 | ``` 700 | 701 | 702 | 703 | ## GoGoAnime 704 | 705 | ### `GET` GoGoAnime Recent Releases 706 | 707 | 708 | ### `GET` GoGoAnime Home 709 | 710 | #### Endpoint 711 | 712 | ```sh 713 | https://api-anime-rouge.vercel.app/gogoanime/home 714 | ``` 715 | 716 | #### Request sample 717 | 718 | ```javascript 719 | const res = await fetch("https://api-anime-rouge.vercel.app/gogoanime/home"); 720 | const data = await res.json(); 721 | console.log(data); 722 | ``` 723 | 724 | #### Response Schema 725 | 726 | ```typescript 727 | [ 728 | "genres": string[], 729 | "recentReleases": [ 730 | { 731 | "id": string, 732 | "name": string, 733 | "img": string, 734 | "episodeId": string, 735 | "episodeNo": number, 736 | "subOrDub": "SUB" | "DUB", 737 | "episodeUrl": string, 738 | }, 739 | {...}, 740 | ], 741 | "recentlyAddedSeries": [ 742 | { 743 | "id": string, 744 | "name": string, 745 | "img": string, 746 | }, 747 | {...}, 748 | ], 749 | "onGoingSeries": [ 750 | { 751 | "id": string, 752 | "name": string, 753 | "img": string, 754 | }, 755 | {...}, 756 | ], 757 | ] 758 | ``` 759 | 760 | #### Endpoint 761 | 762 | ```sh 763 | https://api-anime-rouge.vercel.app/gogoanime/recent-releases?page=${page} 764 | ``` 765 | 766 | 767 | #### Query Parameters 768 | 769 | | Parameter | Type | Description | Required? | Default | 770 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 771 | | `page` | number | Page No. of Search Page | YES | 1 | 772 | 773 | 774 | 775 | #### Request sample 776 | 777 | ```javascript 778 | const res = await fetch( 779 | "https://api-anime-rouge.vercel.app/gogoanime/recent-releases" 780 | ); 781 | const data = await res.json(); 782 | console.log(data); 783 | ``` 784 | 785 | #### Response Schema 786 | 787 | ```typescript 788 | [ 789 | { 790 | "id": string, 791 | "name": string, 792 | "img": string, 793 | "episodeId": string, 794 | "episodeNo": number, 795 | "episodeUrl": string, 796 | "subOrDub": string // "SUB" | "DUB" 797 | }, 798 | {...}, 799 | ] 800 | ``` 801 | 802 | 803 | 804 | ### `GET` GoGoAnime New Seasons 805 | 806 | #### Endpoint 807 | 808 | ```sh 809 | https://api-anime-rouge.vercel.app/gogoanime/new-seasons?page=${page} 810 | ``` 811 | 812 | 813 | 814 | #### Query Parameters 815 | 816 | | Parameter | Type | Description | Required? | Default | 817 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 818 | | `page` | number | Page No. of Search Page | YES | 1 | 819 | 820 | 821 | 822 | #### Request sample 823 | 824 | ```javascript 825 | const res = await fetch( 826 | "https://api-anime-rouge.vercel.app/gogoanime/new-seasons" 827 | ); 828 | const data = await res.json(); 829 | console.log(data); 830 | ``` 831 | 832 | #### Response Schema 833 | 834 | ```typescript 835 | [ 836 | { 837 | "id": string, 838 | "name": string, 839 | "img": string, 840 | "releasedYear": string, 841 | "animeUrl": string 842 | }, 843 | {...}, 844 | ] 845 | ``` 846 | 847 | 848 | 849 | ### `GET` GoGoAnime Popular 850 | 851 | #### Endpoint 852 | 853 | ```sh 854 | https://api-anime-rouge.vercel.app/gogoanime/popular?page=${page} 855 | ``` 856 | 857 | 858 | 859 | #### Query Parameters 860 | 861 | | Parameter | Type | Description | Required? | Default | 862 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 863 | | `page` | number | Page No. of Search Page | YES | 1 | 864 | 865 | 866 | 867 | #### Request sample 868 | 869 | ```javascript 870 | const res = await fetch( 871 | "https://api-anime-rouge.vercel.app/gogoanime/popular" 872 | ); 873 | const data = await res.json(); 874 | console.log(data); 875 | ``` 876 | 877 | #### Response Schema 878 | 879 | ```typescript 880 | [ 881 | { 882 | "id": string, 883 | "name": string, 884 | "img": string, 885 | "releasedYear": string, 886 | "animeUrl": string 887 | }, 888 | {...}, 889 | ] 890 | ``` 891 | 892 | 893 | 894 | 895 | 896 | ### `GET` GoGoAnime Anime Movies 897 | 898 | #### Endpoint 899 | 900 | ```sh 901 | https://api-anime-rouge.vercel.app/gogoanime/anime-movies?page=${page} 902 | ``` 903 | 904 | 905 | 906 | #### Query Parameters 907 | 908 | | Parameter | Type | Description | Required? | Default | 909 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 910 | | `page` | number | Page No. of Search Page | YES | 1 | 911 | 912 | 913 | 914 | #### Request sample 915 | 916 | ```javascript 917 | const res = await fetch( 918 | "https://api-anime-rouge.vercel.app/gogoanime/anime-movies" 919 | ); 920 | const data = await res.json(); 921 | console.log(data); 922 | ``` 923 | 924 | #### Response Schema 925 | 926 | ```typescript 927 | [ 928 | { 929 | "id": string, 930 | "name": string, 931 | "img": string, 932 | "releasedYear": string, 933 | "animeUrl": string 934 | }, 935 | {...}, 936 | ] 937 | ``` 938 | 939 | 940 | 941 | ### `GET` GoGoAnime Top Airing 942 | 943 | #### Endpoint 944 | 945 | ```sh 946 | https://api-anime-rouge.vercel.app/gogoanime/top-airing?page=${page} 947 | ``` 948 | 949 | 950 | 951 | #### Query Parameters 952 | 953 | | Parameter | Type | Description | Required? | Default | 954 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 955 | | `page` | number | Page No. of Search Page | YES | 1 | 956 | 957 | 958 | 959 | #### Request sample 960 | 961 | ```javascript 962 | const res = await fetch( 963 | "https://api-anime-rouge.vercel.app/gogoanime/top-airing" 964 | ); 965 | const data = await res.json(); 966 | console.log(data); 967 | ``` 968 | 969 | #### Response Schema 970 | 971 | ```typescript 972 | [ 973 | { 974 | "id": string, 975 | "name": string, 976 | "img": string, 977 | "latestEp": string, 978 | "animeUrl": string, 979 | "genres": string[] 980 | }, 981 | {...}, 982 | ] 983 | ``` 984 | 985 | ### `GET` GoGoAnime Completed Animes 986 | 987 | #### Endpoint 988 | 989 | ```sh 990 | https://api-anime-rouge.vercel.app/gogoanime/completed?page=${page} 991 | ``` 992 | 993 | 994 | 995 | #### Query Parameters 996 | 997 | | Parameter | Type | Description | Required? | Default | 998 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 999 | | `page` | number | Page No. of Search Page | YES | 1 | 1000 | 1001 | 1002 | 1003 | #### Request sample 1004 | 1005 | ```javascript 1006 | const res = await fetch( 1007 | "https://api-anime-rouge.vercel.app/gogoanime/completed" 1008 | ); 1009 | const data = await res.json(); 1010 | console.log(data); 1011 | ``` 1012 | 1013 | #### Response Schema 1014 | 1015 | ```typescript 1016 | [ 1017 | { 1018 | "id": string, 1019 | "name": string, 1020 | "img": string, 1021 | "latestEp": string, 1022 | "animeUrl": string 1023 | }, 1024 | {...}, 1025 | ] 1026 | ``` 1027 | 1028 | ### `GET` Search Anime 1029 | 1030 | #### Endpoint 1031 | 1032 | ```sh 1033 | https://api-anime-rouge.vercel.app/gogoanime/search?keyword=$(query)&page=$(page) 1034 | ``` 1035 | 1036 | #### Query Parameters 1037 | 1038 | | Parameter | Type | Description | Required? | Default | 1039 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 1040 | | `query` | string | Search Query for Anime | YES | ----- | 1041 | | `page` | number | Page No. of Search Page | YES | 1 | 1042 | > [!NOTE] 1043 | >
Search Query should be In Kebab Case
1044 | >
Page No should be a Number
1045 | #### Request sample 1046 | 1047 | ```javascript 1048 | const res = await fetch( 1049 | "https://api-anime-rouge.vercel.app/gogoanime/search?keyword=one+piece&page=1" 1050 | ); 1051 | const data = await res.json(); 1052 | console.log(data); 1053 | ``` 1054 | 1055 | #### Response Schema 1056 | 1057 | ```typescript 1058 | { 1059 | "animes": [ 1060 | { 1061 | "id": string, 1062 | "name": string, 1063 | "img": string, 1064 | "releasedYear": string 1065 | }, 1066 | {...}, 1067 | ], 1068 | "mostPopularAnimes": [ 1069 | { 1070 | "id": string, 1071 | "name": string, 1072 | "category": string, 1073 | "img": string, 1074 | "episodes": { 1075 | "eps": number, 1076 | "sub": number, 1077 | "dub": number 1078 | } 1079 | }, 1080 | {...}, 1081 | ], 1082 | "currentPage": number, 1083 | "hasNextPage": boolean, 1084 | "totalPages": number 1085 | } 1086 | ``` 1087 | 1088 | 1089 | ### `GET` Anime About Info 1090 | 1091 | #### Endpoint 1092 | 1093 | ```sh 1094 | https://api-anime-rouge.vercel.app/gogoanime/anime/:id 1095 | ``` 1096 | 1097 | #### Query Parameters 1098 | 1099 | | Parameter | Type | Description | Required? | Default | 1100 | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | 1101 | | `id` | string | The unique Anime ID | YES | ----- | 1102 | 1103 | > [!NOTE] 1104 | > Anime ID should be In Kebab Case 1105 | 1106 | #### Request sample 1107 | 1108 | ```javascript 1109 | const res = await fetch( 1110 | "https://api-anime-rouge.vercel.app/gogoanime/anime/one-piece" 1111 | ); 1112 | const data = await res.json(); 1113 | console.log(data); 1114 | ``` 1115 | 1116 | #### Response Schema 1117 | 1118 | ``` typescript 1119 | { 1120 | "id": string, 1121 | "anime_id": string, 1122 | "info": { 1123 | "name": string, 1124 | "img": string, 1125 | "type": string, 1126 | "genre": string[], 1127 | "status": string, 1128 | "aired_in": number, 1129 | "other_name": string, 1130 | "episodes": number 1131 | } 1132 | } 1133 | ``` 1134 | 1135 | 1136 | ############################################################################# 1137 | 1138 | ## 🖱️ For Front End 1139 | 1140 | > [!TIP] 1141 | > Kindly use this repo to make Front End 1142 | 1143 | - [Eltik / Anify](https://github.com/Eltik/Anify) 1144 | 1145 | ############################################################################# 1146 | 1147 | ## 🤝 Thanks ❤️ 1148 | 1149 | - [consumet.ts](https://github.com/consumet/consumet.ts) 1150 | - [ghoshRitesh12](https://github.com/ghoshRitesh12) 1151 | --------------------------------------------------------------------------------