Aniwatch API Version 2 (#66)

BREAKING CHANGE:

* chore: remove files that are not necessary for api v2

* test: update existing tests to use  pkg

* feat: organized aniwatch api envs and add more info about them

* feat: update tsconfig to include strict noUnsed params

* feat(api homepage): revamp api home page

* feat: update wani kuni image

* feat: add dot img

* feat: use hono cors

* feat: use hono rate limiter

* build: remove unnecessary deps, add ones needed and update description

* feat: add hianime routes and their handlers

* feat: update vercel deployment file

* docs: update logo and scraper docs, add envs section

* feat: update main server file

* feat: update peronal deployments caution section
This commit is contained in:
Ritesh Ghosh
2024-10-06 01:13:23 +05:30
committed by GitHub
parent 55810ccf23
commit 46f688ac12
84 changed files with 1028 additions and 3893 deletions
-21
View File
@@ -1,21 +0,0 @@
import axios, { AxiosError, type AxiosRequestConfig } from "axios";
import {
SRC_BASE_URL,
ACCEPT_HEADER,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
} from "../utils/constants.js";
const clientConfig: AxiosRequestConfig = {
timeout: 10000,
baseURL: SRC_BASE_URL,
headers: {
Accept: ACCEPT_HEADER,
"User-Agent": USER_AGENT_HEADER,
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
},
};
const client = axios.create(clientConfig);
export { client, AxiosError };
+5 -12
View File
@@ -1,24 +1,17 @@
import cors from "cors";
import { config } from "dotenv";
import { cors } from "hono/cors";
config();
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
? process.env.CORS_ALLOWED_ORIGINS.split(",")
const allowedOrigins = process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS
? process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS.split(",")
: ["http://localhost:4000", "*"];
const corsConfig = cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
methods: ["GET"],
allowMethods: ["GET"],
maxAge: 600,
credentials: true,
optionsSuccessStatus: 200,
origin: allowedOrigins,
});
export default corsConfig;
-11
View File
@@ -1,11 +0,0 @@
import type { ErrorRequestHandler } from "express";
const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
const status = error?.status || 500;
res.status(status).json({
status,
message: error?.message || "Something Went Wrong",
});
};
export default errorHandler;
-8
View File
@@ -1,8 +0,0 @@
import type { RequestHandler } from "express";
import createHttpError from "http-errors";
const notFoundHandler: RequestHandler = (req, res, next) => {
return next(createHttpError.NotFound());
};
export default notFoundHandler;
+14 -10
View File
@@ -1,17 +1,21 @@
import { config } from "dotenv";
import createHttpError from "http-errors";
import { rateLimit } from "express-rate-limit";
import { rateLimiter } from "hono-rate-limiter";
import { getConnInfo } from "@hono/node-server/conninfo";
config();
export const ratelimit = rateLimit({
windowMs: Number(process.env.WINDOWMS) || 30 * 60 * 1000,
limit: Number(process.env.MAX) || 6,
legacyHeaders: true,
export const ratelimit = rateLimiter({
windowMs: Number(process.env.ANIWATCH_API_WINDOW_MS) || 30 * 60 * 1000,
limit: Number(process.env.ANIWATCH_API_MAX_REQS) || 6,
standardHeaders: "draft-7",
handler: function (_, __, next) {
next(
createHttpError.TooManyRequests("Too many API requests, try again later")
);
keyGenerator(c) {
const { remote } = getConnInfo(c);
const key =
`${String(remote.addressType)}_` +
`${String(remote.address)}:${String(remote.port)}`;
return key;
},
handler: (c) =>
c.json({ status: 429, message: "Too Many Requests 😵" }, { status: 429 }),
});
@@ -1,31 +0,0 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeAnimeAboutInfo } from "../parsers/index.js";
import { type AnimeAboutInfoQueryParams } from "../types/controllers/index.js";
// /anime/info?id=${anime-id}
const getAnimeAboutInfo: RequestHandler<
unknown,
Awaited<ReturnType<typeof scrapeAnimeAboutInfo>>,
unknown,
AnimeAboutInfoQueryParams
> = async (req, res, next) => {
try {
const animeId = req.query.id
? decodeURIComponent(req.query.id as string)
: null;
if (animeId === null) {
throw createHttpError.BadRequest("Anime unique id required");
}
const data = await scrapeAnimeAboutInfo(animeId);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getAnimeAboutInfo;
@@ -1,39 +0,0 @@
import createHttpError from "http-errors";
import type { RequestHandler } from "express";
import type { AnimeCategories } from "../types/anime.js";
import { scrapeAnimeCategory } from "../parsers/index.js";
import type {
CategoryAnimePathParams,
CategoryAnimeQueryParams,
} from "../types/controllers/index.js";
// /anime/:category?page=${page}
const getAnimeCategory: RequestHandler<
CategoryAnimePathParams,
Awaited<ReturnType<typeof scrapeAnimeCategory>>,
unknown,
CategoryAnimeQueryParams
> = async (req, res, next) => {
try {
const category = req.params.category
? decodeURIComponent(req.params.category)
: null;
const page: number = req.query.page
? Number(decodeURIComponent(req.query?.page as string))
: 1;
if (category === null) {
throw createHttpError.BadRequest("category required");
}
const data = await scrapeAnimeCategory(category as AnimeCategories, page);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getAnimeCategory;
@@ -1,75 +0,0 @@
import axios from "axios";
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { type CheerioAPI, load } from "cheerio";
import { scrapeAnimeEpisodeSources } from "../parsers/index.js";
import { USER_AGENT_HEADER, SRC_BASE_URL } from "../utils/constants.js";
import { type AnimeServers, Servers } from "../types/anime.js";
import { type AnimeEpisodeSrcsQueryParams } from "../types/controllers/index.js";
type AnilistID = number | null;
type MalID = number | null;
// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)}
const getAnimeEpisodeSources: RequestHandler<
unknown,
Awaited<ReturnType<typeof scrapeAnimeEpisodeSources & AnilistID>>,
unknown,
AnimeEpisodeSrcsQueryParams
> = async (req, res, next) => {
try {
const episodeId = req.query.id ? decodeURIComponent(req.query.id) : null;
const server = (
req.query.server
? decodeURIComponent(req.query.server)
: Servers.VidStreaming
) as AnimeServers;
const category = (
req.query.category ? decodeURIComponent(req.query.category) : "sub"
) as "sub" | "dub";
if (episodeId === null) {
throw createHttpError.BadRequest("Anime episode id required");
}
let malID: MalID;
let anilistID: AnilistID;
const animeURL = new URL(episodeId?.split("?ep=")[0], SRC_BASE_URL)?.href;
const [episodeSrcData, animeSrc] = await Promise.all([
scrapeAnimeEpisodeSources(episodeId, server, category),
axios.get(animeURL, {
headers: {
Referer: SRC_BASE_URL,
"User-Agent": USER_AGENT_HEADER,
"X-Requested-With": "XMLHttpRequest",
},
}),
]);
const $: CheerioAPI = load(animeSrc?.data);
try {
anilistID = Number(
JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id
);
malID = Number(JSON.parse($("body")?.find("#syncData")?.text())?.mal_id);
} catch (err) {
anilistID = null;
malID = null;
}
res.status(200).json({
...episodeSrcData,
anilistID,
malID,
});
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getAnimeEpisodeSources;
@@ -1,31 +0,0 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeAnimeEpisodes } from "../parsers/index.js";
import { type AnimeEpisodePathParams } from "../types/controllers/index.js";
// /anime/episodes/${anime-id}
const getAnimeEpisodes: RequestHandler<
AnimeEpisodePathParams,
Awaited<ReturnType<typeof scrapeAnimeEpisodes>>,
unknown,
unknown
> = async (req, res, next) => {
try {
const animeId = req.params.animeId
? decodeURIComponent(req.params.animeId)
: null;
if (animeId === null) {
throw createHttpError.BadRequest("Anime Id required");
}
const data = await scrapeAnimeEpisodes(animeId);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getAnimeEpisodes;
-37
View File
@@ -1,37 +0,0 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeGenreAnime } from "../parsers/index.js";
import type {
GenreAnimePathParams,
GenreAnimeQueryParams,
} from "../types/controllers/index.js";
// /anime/genre/${name}?page=${page}
const getGenreAnime: RequestHandler<
GenreAnimePathParams,
Awaited<ReturnType<typeof scrapeGenreAnime>>,
unknown,
GenreAnimeQueryParams
> = async (req, res, next) => {
try {
const name: string | null = req.params.name
? decodeURIComponent(req.params.name as string)
: null;
const page: number = req.query.page
? Number(decodeURIComponent(req.query?.page as string))
: 1;
if (name === null) {
throw createHttpError.BadRequest("Anime genre required");
}
const data = await scrapeGenreAnime(name, page);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getGenreAnime;
@@ -1,37 +0,0 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeProducerAnimes } from "../parsers/index.js";
import type {
AnimeProducerPathParams,
AnimeProducerQueryParams,
} from "../types/controllers/index.js";
// /anime/producer/${name}?page=${page}
const getProducerAnimes: RequestHandler<
AnimeProducerPathParams,
Awaited<ReturnType<typeof scrapeProducerAnimes>>,
unknown,
AnimeProducerQueryParams
> = async (req, res, next) => {
try {
const name: string | null = req.params.name
? decodeURIComponent(req.params.name as string)
: null;
const page: number = req.query.page
? Number(decodeURIComponent(req.query?.page as string))
: 1;
if (name === null) {
throw createHttpError.BadRequest("Anime producer name required");
}
const data = await scrapeProducerAnimes(name, page);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getProducerAnimes;
-57
View File
@@ -1,57 +0,0 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeAnimeSearch } from "../parsers/index.js";
import type {
SearchFilters,
AnimeSearchQueryParams,
} from "../types/controllers/index.js";
const searchFilters: Record<string, boolean> = {
filter: true,
type: true,
status: true,
rated: true,
score: true,
season: true,
language: true,
start_date: true,
end_date: true,
sort: true,
genres: true,
} as const;
// /anime/search?q=${query}&page=${page}
const getAnimeSearch: RequestHandler<
unknown,
Awaited<ReturnType<typeof scrapeAnimeSearch>>,
unknown,
AnimeSearchQueryParams
> = async (req, res, next) => {
try {
let { q: query, page, ...filters } = req.query;
query = query ? decodeURIComponent(query) : undefined;
const pageNo = page ? Number(decodeURIComponent(page as string)) : 1;
if (query === undefined) {
throw createHttpError.BadRequest("Search keyword required");
}
const parsedFilters: SearchFilters = {};
for (const key in filters) {
if (searchFilters[key]) {
parsedFilters[key as keyof SearchFilters] =
filters[key as keyof SearchFilters];
}
}
const data = await scrapeAnimeSearch(query, pageNo, parsedFilters);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getAnimeSearch;
@@ -1,31 +0,0 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeAnimeSearchSuggestion } from "../parsers/index.js";
import { type AnimeSearchSuggestQueryParams } from "../types/controllers/index.js";
// /anime/search/suggest?q=${query}
const getAnimeSearchSuggestion: RequestHandler<
unknown,
Awaited<ReturnType<typeof scrapeAnimeSearchSuggestion>>,
unknown,
AnimeSearchSuggestQueryParams
> = async (req, res, next) => {
try {
const query: string | null = req.query.q
? decodeURIComponent(req.query.q as string)
: null;
if (query === null) {
throw createHttpError.BadRequest("Search keyword required");
}
const data = await scrapeAnimeSearchSuggestion(query);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getAnimeSearchSuggestion;
@@ -1,30 +0,0 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeEpisodeServers } from "../parsers/index.js";
import { type EpisodeServersQueryParams } from "../types/controllers/index.js";
// /anime/servers?episodeId=${id}
const getEpisodeServers: RequestHandler<
unknown,
Awaited<ReturnType<typeof scrapeEpisodeServers>>,
unknown,
EpisodeServersQueryParams
> = async (req, res, next) => {
try {
const episodeId = req.query.episodeId
? decodeURIComponent(req.query?.episodeId as string)
: null;
if (episodeId === null) {
throw createHttpError.BadRequest("Episode id required");
}
const data = await scrapeEpisodeServers(episodeId);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getEpisodeServers;
@@ -1,36 +0,0 @@
import createHttpError from "http-errors";
import { type RequestHandler } from "express";
import { scrapeEstimatedSchedule } from "../parsers/index.js";
import { type EstimatedScheduleQueryParams } from "../types/controllers/index.js";
// /anime/schedule?date=${date}
const getEstimatedSchedule: RequestHandler<
unknown,
Awaited<ReturnType<typeof scrapeEstimatedSchedule>>,
unknown,
EstimatedScheduleQueryParams
> = async (req, res, next) => {
try {
const dateQuery = req.query.date
? decodeURIComponent(req.query.date as string)
: null;
if (dateQuery === null) {
throw createHttpError.BadRequest("Date payload required");
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateQuery)) {
throw createHttpError.BadRequest(
"Invalid date payload format. Months and days must have 2 digits"
);
}
const data = await scrapeEstimatedSchedule(dateQuery);
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getEstimatedSchedule;
-18
View File
@@ -1,18 +0,0 @@
import { type RequestHandler } from "express";
import { scrapeHomePage } from "../parsers/index.js";
// /anime/home
const getHomePageInfo: RequestHandler<
unknown,
Awaited<ReturnType<typeof scrapeHomePage>>
> = async (req, res, next) => {
try {
const data = await scrapeHomePage();
res.status(200).json(data);
} catch (err: any) {
console.error(err);
next(err);
}
};
export default getHomePageInfo;
-25
View File
@@ -1,25 +0,0 @@
import getGenreAnime from "./animeGenre.controller.js";
import getHomePageInfo from "./homePage.controller.js";
import getAnimeSearch from "./animeSearch.controller.js";
import getAnimeEpisodes from "./animeEpisodes.controller.js";
import getAnimeCategory from "./animeCategory.controller.js";
import getProducerAnimes from "./animeProducer.controller.js";
import getEpisodeServers from "./episodeServers.controller.js";
import getAnimeAboutInfo from "./animeAboutInfo.controller.js";
import getEstimatedSchedule from "./estimatedSchedule.controller.js";
import getAnimeEpisodeSources from "./animeEpisodeSrcs.controller.js";
import getAnimeSearchSuggestion from "./animeSearchSuggestion.controller.js";
export {
getGenreAnime,
getAnimeSearch,
getHomePageInfo,
getAnimeEpisodes,
getAnimeCategory,
getEpisodeServers,
getProducerAnimes,
getAnimeAboutInfo,
getEstimatedSchedule,
getAnimeEpisodeSources,
getAnimeSearchSuggestion,
};
-6
View File
@@ -1,6 +0,0 @@
import StreamSB from "./streamsb.js";
import StreamTape from "./streamtape.js";
import RapidCloud from "./rapidcloud.js";
import MegaCloud from "./megacloud.js";
export { StreamSB, StreamTape, RapidCloud, MegaCloud };
-226
View File
@@ -1,226 +0,0 @@
import axios from "axios";
import crypto from "crypto";
import createHttpError from "http-errors";
// https://megacloud.tv/embed-2/e-1/dBqCr5BcOhnD?k=1
const megacloud = {
script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=",
sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=",
} as const;
type track = {
file: string;
kind: string;
label?: string;
default?: boolean;
};
type intro_outro = {
start: number;
end: number;
};
type unencryptedSrc = {
file: string;
type: string;
};
type extractedSrc = {
sources: string | unencryptedSrc[];
tracks: track[];
encrypted: boolean;
intro: intro_outro;
outro: intro_outro;
server: number;
};
interface ExtractedData
extends Pick<extractedSrc, "intro" | "outro" | "tracks"> {
sources: { url: string; type: string }[];
}
class MegaCloud {
private serverName = "megacloud";
async extract(videoUrl: URL) {
try {
const extractedData: ExtractedData = {
tracks: [],
intro: {
start: 0,
end: 0,
},
outro: {
start: 0,
end: 0,
},
sources: [],
};
const videoId = videoUrl?.href?.split("/")?.pop()?.split("?")[0];
const { data: srcsData } = await axios.get<extractedSrc>(
megacloud.sources.concat(videoId || ""),
{
headers: {
Accept: "*/*",
"X-Requested-With": "XMLHttpRequest",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
Referer: videoUrl.href,
},
}
);
if (!srcsData) {
throw createHttpError.NotFound("Url may have an invalid video id");
}
// console.log(JSON.stringify(srcsData, null, 2));
const encryptedString = srcsData.sources;
if (!srcsData.encrypted && Array.isArray(encryptedString)) {
extractedData.intro = srcsData.intro;
extractedData.outro = srcsData.outro;
extractedData.tracks = srcsData.tracks;
extractedData.sources = encryptedString.map((s) => ({
url: s.file,
type: s.type,
}));
return extractedData;
}
let text: string;
const { data } = await axios.get(
megacloud.script.concat(Date.now().toString())
);
text = data;
if (!text) {
throw createHttpError.InternalServerError(
"Couldn't fetch script to decrypt resource"
);
}
const vars = this.extractVariables(text);
if (!vars.length) {
throw new Error("Can't find variables. Perhaps the extractor is outdated.");
}
const { secret, encryptedSource } = this.getSecret(
encryptedString as string,
vars
);
const decrypted = this.decrypt(encryptedSource, secret);
try {
const sources = JSON.parse(decrypted);
extractedData.intro = srcsData.intro;
extractedData.outro = srcsData.outro;
extractedData.tracks = srcsData.tracks;
extractedData.sources = sources.map((s: any) => ({
url: s.file,
type: s.type,
}));
return extractedData;
} catch (error) {
throw createHttpError.InternalServerError("Failed to decrypt resource");
}
} catch (err) {
// console.log(err);
throw err;
}
}
extractVariables(text: string) {
// copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30'
const regex =
/case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);/g;
const matches = text.matchAll(regex);
const vars = Array.from(matches, (match) => {
const matchKey1 = this.matchingKey(match[1], text);
const matchKey2 = this.matchingKey(match[2], text);
try {
return [parseInt(matchKey1, 16), parseInt(matchKey2, 16)];
} catch (e) {
return [];
}
}).filter((pair) => pair.length > 0);
return vars;
}
getSecret(encryptedString: string, values: number[][]) {
let secret = "",
encryptedSource = "",
encryptedSourceArray = encryptedString.split(""),
currentIndex = 0;
for (const index of values) {
const start = index[0] + currentIndex;
const end = start + index[1];
for (let i = start; i < end; i++) {
secret += encryptedString[i];
encryptedSourceArray[i] = "";
}
currentIndex += index[1];
}
encryptedSource = encryptedSourceArray.join("");
return { secret, encryptedSource };
}
decrypt(encrypted: string, keyOrSecret: string, maybe_iv?: string) {
let key;
let iv;
let contents;
if (maybe_iv) {
key = keyOrSecret;
iv = maybe_iv;
contents = encrypted;
} else {
// copied from 'https://github.com/brix/crypto-js/issues/468'
const cypher = Buffer.from(encrypted, "base64");
const salt = cypher.subarray(8, 16);
const password = Buffer.concat([
Buffer.from(keyOrSecret, "binary"),
salt,
]);
const md5Hashes = [];
let digest = password;
for (let i = 0; i < 3; i++) {
md5Hashes[i] = crypto.createHash("md5").update(digest).digest();
digest = Buffer.concat([md5Hashes[i], password]);
}
key = Buffer.concat([md5Hashes[0], md5Hashes[1]]);
iv = md5Hashes[2];
contents = cypher.subarray(16);
}
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
const decrypted =
decipher.update(
contents as any,
typeof contents === "string" ? "base64" : undefined,
"utf8"
) + decipher.final();
return decrypted;
}
// function copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30'
matchingKey(value: string, script: string) {
const regex = new RegExp(`,${value}=((?:0x)?([0-9a-fA-F]+))`);
const match = script.match(regex);
if (match) {
return match[1].replace(/^0x/, "");
} else {
throw new Error("Failed to match the key");
}
}
}
export default MegaCloud;
-166
View File
@@ -1,166 +0,0 @@
import axios from "axios";
import CryptoJS from "crypto-js";
import { substringAfter, substringBefore } from "../utils/index.js";
import type { Video, Subtitle, Intro } from "../types/extractor.js";
type extractReturn = {
sources: Video[];
subtitles: Subtitle[];
};
// https://megacloud.tv/embed-2/e-1/IxJ7GjGVCyml?k=1
class RapidCloud {
private serverName = "RapidCloud";
private sources: Video[] = [];
// https://rapid-cloud.co/embed-6/eVZPDXwVfrY3?vast=1
private readonly fallbackKey = "c1d17096f2ca11b7";
private readonly host = "https://rapid-cloud.co";
async extract(videoUrl: URL): Promise<extractReturn> {
const result: extractReturn & { intro?: Intro; outro?: Intro } = {
sources: [],
subtitles: [],
};
try {
const id = videoUrl.href.split("/").pop()?.split("?")[0];
const options = {
headers: {
"X-Requested-With": "XMLHttpRequest",
},
};
let res = null;
res = await axios.get(
`https://${videoUrl.hostname}/embed-2/ajax/e-1/getSources?id=${id}`,
options
);
let {
data: { sources, tracks, intro, outro, encrypted },
} = res;
let decryptKey = await (
await axios.get(
"https://raw.githubusercontent.com/cinemaxhq/keys/e1/key"
)
).data;
decryptKey = substringBefore(
substringAfter(decryptKey, '"blob-code blob-code-inner js-file-line">'),
"</td>"
);
if (!decryptKey) {
decryptKey = await (
await axios.get(
"https://raw.githubusercontent.com/cinemaxhq/keys/e1/key"
)
).data;
}
if (!decryptKey) decryptKey = this.fallbackKey;
try {
if (encrypted) {
const sourcesArray = sources.split("");
let extractedKey = "";
let currentIndex = 0;
for (const index of decryptKey) {
const start = index[0] + currentIndex;
const end = start + index[1];
for (let i = start; i < end; i++) {
extractedKey += res.data.sources[i];
sourcesArray[i] = "";
}
currentIndex += index[1];
}
decryptKey = extractedKey;
sources = sourcesArray.join("");
const decrypt = CryptoJS.AES.decrypt(sources, decryptKey);
sources = JSON.parse(decrypt.toString(CryptoJS.enc.Utf8));
}
} catch (err: any) {
console.log(err.message);
throw new Error("Cannot decrypt sources. Perhaps the key is invalid.");
}
this.sources = sources?.map((s: any) => ({
url: s.file,
isM3U8: s.file.includes(".m3u8"),
}));
result.sources.push(...this.sources);
if (videoUrl.href.includes(new URL(this.host).host)) {
result.sources = [];
this.sources = [];
for (const source of sources) {
const { data } = await axios.get(source.file, options);
const m3u8data = data
.split("\n")
.filter(
(line: string) =>
line.includes(".m3u8") && line.includes("RESOLUTION=")
);
const secondHalf = m3u8data.map((line: string) =>
line.match(/RESOLUTION=.*,(C)|URI=.*/g)?.map((s) => s.split("=")[1])
);
const TdArray = secondHalf.map((s: string[]) => {
const f1 = s[0].split(",C")[0];
const f2 = s[1].replace(/"/g, "");
return [f1, f2];
});
for (const [f1, f2] of TdArray) {
this.sources.push({
url: `${source.file?.split("master.m3u8")[0]}${f2.replace(
"iframes",
"index"
)}`,
quality: f1.split("x")[1] + "p",
isM3U8: f2.includes(".m3u8"),
});
}
result.sources.push(...this.sources);
}
}
result.intro =
intro?.end > 1 ? { start: intro.start, end: intro.end } : undefined;
result.outro =
outro?.end > 1 ? { start: outro.start, end: outro.end } : undefined;
result.sources.push({
url: sources[0].file,
isM3U8: sources[0].file.includes(".m3u8"),
quality: "auto",
});
result.subtitles = tracks
.map((s: any) =>
s.file
? { url: s.file, lang: s.label ? s.label : "Thumbnails" }
: null
)
.filter((s: any) => s);
return result;
} catch (err: any) {
console.log(err.message);
throw err;
}
}
}
export default RapidCloud;
-83
View File
@@ -1,83 +0,0 @@
import axios from "axios";
import type { Video } from "../types/extractor.js";
import { USER_AGENT_HEADER } from "../utils/index.js";
class StreamSB {
private serverName = "streamSB";
private sources: Video[] = [];
private readonly host = "https://watchsb.com/sources50";
private readonly host2 = "https://streamsss.net/sources16";
private PAYLOAD(hex: string): string {
// `5363587530696d33443675687c7c${hex}7c7c433569475830474c497a65767c7c73747265616d7362`;
return `566d337678566f743674494a7c7c${hex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362`;
}
async extract(videoUrl: URL, isAlt: boolean = false): Promise<Video[]> {
let headers: Record<string, string> = {
watchsb: "sbstream",
Referer: videoUrl.href,
"User-Agent": USER_AGENT_HEADER,
};
let id = videoUrl.href.split("/e/").pop();
if (id?.includes("html")) {
id = id.split(".html")[0];
}
const bytes = new TextEncoder().encode(id);
const res = await axios
.get(
`${isAlt ? this.host2 : this.host}/${this.PAYLOAD(
Buffer.from(bytes).toString("hex")
)}`,
{ headers }
)
.catch(() => null);
if (!res?.data.stream_data) {
throw new Error("No source found. Try a different server");
}
headers = {
"User-Agent": USER_AGENT_HEADER,
Referer: videoUrl.href.split("e/")[0],
};
const m3u8_urls = await axios.get(res.data.stream_data.file, {
headers,
});
const videoList = m3u8_urls?.data?.split("#EXT-X-STREAM-INF:") ?? [];
for (const video of videoList) {
if (!video.includes("m3u8")) continue;
const url = video.split("\n")[1];
const quality = video.split("RESOLUTION=")[1].split(",")[0].split("x")[1];
this.sources.push({
url: url,
quality: `${quality}p`,
isM3U8: true,
});
}
this.sources.push({
url: res.data.stream_data.file,
quality: "auto",
isM3U8: res.data.stream_data.file.includes(".m3u8"),
});
return this.sources;
}
private addSources(source: any): void {
this.sources.push({
url: source.file,
isM3U8: source.file.includes(".m3u8"),
});
}
}
export default StreamSB;
-37
View File
@@ -1,37 +0,0 @@
import axios from "axios";
import { load, type CheerioAPI } from "cheerio";
import type { Video } from "../types/extractor.js";
class StreamTape {
private serverName = "StreamTape";
private sources: Video[] = [];
async extract(videoUrl: URL): Promise<Video[]> {
try {
const { data } = await axios.get(videoUrl.href).catch(() => {
throw new Error("Video not found");
});
const $: CheerioAPI = load(data);
let [fh, sh] = $.html()
?.match(/robotlink'\).innerHTML = (.*)'/)![1]
.split("+ ('");
sh = sh.substring(3);
fh = fh.replace(/\'/g, "");
const url = `https:${fh}${sh}`;
this.sources.push({
url: url,
isM3U8: url.includes(".m3u8"),
});
return this.sources;
} catch (err) {
throw new Error((err as Error).message);
}
}
}
export default StreamTape;
-229
View File
@@ -1,229 +0,0 @@
import {
SRC_BASE_URL,
extractAnimes,
ACCEPT_HEADER,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
extractMostPopularAnimes,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import { type ScrapedAnimeAboutInfo } from "../types/parsers/index.js";
// /anime/info?id=${anime-id}
async function scrapeAnimeAboutInfo(
id: string
): Promise<ScrapedAnimeAboutInfo | HttpError> {
const res: ScrapedAnimeAboutInfo = {
anime: {
info: {
id: null,
anilistId: null,
malId: null,
name: null,
poster: null,
description: null,
stats: {
rating: null,
quality: null,
episodes: {
sub: null,
dub: null,
},
type: null,
duration: null,
},
promotionalVideos: [],
charactersVoiceActors: [],
},
moreInfo: {},
},
seasons: [],
mostPopularAnimes: [],
relatedAnimes: [],
recommendedAnimes: [],
};
try {
const animeUrl: URL = new URL(id, SRC_BASE_URL);
const mainPage = await axios.get(animeUrl.href, {
headers: {
"User-Agent": USER_AGENT_HEADER,
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
Accept: ACCEPT_HEADER,
},
});
const $: CheerioAPI = load(mainPage.data);
try {
res.anime.info.anilistId = Number(
JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id
);
res.anime.info.malId = Number(JSON.parse($("body")?.find("#syncData")?.text())?.mal_id);
} catch (err) {
res.anime.info.anilistId = null;
res.anime.info.malId = null;
}
const selector: SelectorType = "#ani_detail .container .anis-content";
res.anime.info.id =
$(selector)
?.find(".anisc-detail .film-buttons a.btn-play")
?.attr("href")
?.split("/")
?.pop() || null;
res.anime.info.name =
$(selector)
?.find(".anisc-detail .film-name.dynamic-name")
?.text()
?.trim() || null;
res.anime.info.description =
$(selector)
?.find(".anisc-detail .film-description .text")
.text()
?.split("[")
?.shift()
?.trim() || null;
res.anime.info.poster =
$(selector)?.find(".film-poster .film-poster-img")?.attr("src")?.trim() ||
null;
// stats
res.anime.info.stats.rating =
$(`${selector} .film-stats .tick .tick-pg`)?.text()?.trim() || null;
res.anime.info.stats.quality =
$(`${selector} .film-stats .tick .tick-quality`)?.text()?.trim() || null;
res.anime.info.stats.episodes = {
sub:
Number($(`${selector} .film-stats .tick .tick-sub`)?.text()?.trim()) ||
null,
dub:
Number($(`${selector} .film-stats .tick .tick-dub`)?.text()?.trim()) ||
null,
};
res.anime.info.stats.type =
$(`${selector} .film-stats .tick`)
?.text()
?.trim()
?.replace(/[\s\n]+/g, " ")
?.split(" ")
?.at(-2) || null;
res.anime.info.stats.duration =
$(`${selector} .film-stats .tick`)
?.text()
?.trim()
?.replace(/[\s\n]+/g, " ")
?.split(" ")
?.pop() || null;
// get promotional videos
$(".block_area.block_area-promotions .block_area-promotions-list .screen-items .item").each(
(_, el) => {
res.anime.info.promotionalVideos.push({
title: $(el).attr("data-title"),
source: $(el).attr("data-src"),
thumbnail: $(el).find("img").attr("src"),
});
}
);
// get characters and voice actors
$(".block_area.block_area-actors .block-actors-content .bac-list-wrap .bac-item").each(
(_, el) => {
res.anime.info.charactersVoiceActors.push({
character: {
id: $(el).find($(".per-info.ltr .pi-avatar")).attr("href")?.split("/")[2] || "",
poster: $(el).find($(".per-info.ltr .pi-avatar img")).attr("data-src") || "",
name: $(el).find($(".per-info.ltr .pi-detail a")).text(),
cast: $(el).find($(".per-info.ltr .pi-detail .pi-cast")).text(),
},
voiceActor: {
id: $(el).find($(".per-info.rtl .pi-avatar")).attr("href")?.split("/")[2] || "",
poster: $(el).find($(".per-info.rtl .pi-avatar img")).attr("data-src") || "",
name: $(el).find($(".per-info.rtl .pi-detail a")).text(),
cast: $(el).find($(".per-info.rtl .pi-detail .pi-cast")).text(),
},
});
}
);
// more information
$(`${selector} .anisc-info-wrap .anisc-info .item:not(.w-hide)`).each(
(i, el) => {
let key = $(el)
.find(".item-head")
.text()
.toLowerCase()
.replace(":", "")
.trim();
key = key.includes(" ") ? key.replace(" ", "") : key;
const value = [
...$(el)
.find("*:not(.item-head)")
.map((i, el) => $(el).text().trim()),
]
.map((i) => `${i}`)
.toString()
.trim();
if (key === "genres") {
res.anime.moreInfo[key] = value.split(",").map((i) => i.trim());
return;
}
if (key === "producers") {
res.anime.moreInfo[key] = value.split(",").map((i) => i.trim());
return;
}
res.anime.moreInfo[key] = value;
}
);
// more seasons
const seasonsSelector: SelectorType = "#main-content .os-list a.os-item";
$(seasonsSelector).each((i, el) => {
res.seasons.push({
id: $(el)?.attr("href")?.slice(1)?.trim() || null,
name: $(el)?.attr("title")?.trim() || null,
title: $(el)?.find(".title")?.text()?.trim(),
poster:
$(el)
?.find(".season-poster")
?.attr("style")
?.split(" ")
?.pop()
?.split("(")
?.pop()
?.split(")")[0] || null,
isCurrent: $(el).hasClass("active"),
});
});
const relatedAnimeSelector: SelectorType =
"#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(1) .anif-block-ul ul li";
res.relatedAnimes = extractMostPopularAnimes($, relatedAnimeSelector);
const mostPopularSelector: SelectorType =
"#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(2) .anif-block-ul ul li";
res.mostPopularAnimes = extractMostPopularAnimes($, mostPopularSelector);
const recommendedAnimeSelector: SelectorType =
"#main-content .block_area.block_area_category .tab-content .flw-item";
res.recommendedAnimes = extractAnimes($, recommendedAnimeSelector);
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeAnimeAboutInfo;
-118
View File
@@ -1,118 +0,0 @@
import {
SRC_BASE_URL,
extractAnimes,
ACCEPT_HEADER,
USER_AGENT_HEADER,
extractTop10Animes,
ACCEPT_ENCODING_HEADER,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import { type AnimeCategories } from "../types/anime.js";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import { type ScrapedAnimeCategory } from "../types/parsers/index.js";
// /anime/:category?page=${page}
async function scrapeAnimeCategory(
category: AnimeCategories,
page: number = 1
): Promise<ScrapedAnimeCategory | HttpError> {
const res: ScrapedAnimeCategory = {
animes: [],
genres: [],
top10Animes: {
today: [],
week: [],
month: [],
},
category,
currentPage: Number(page),
hasNextPage: false,
totalPages: 1,
};
try {
const scrapeUrl: URL = new URL(category, SRC_BASE_URL);
const mainPage = await axios.get(`${scrapeUrl}?page=${page}`, {
headers: {
"User-Agent": USER_AGENT_HEADER,
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
Accept: ACCEPT_HEADER,
},
});
const $: CheerioAPI = load(mainPage.data);
const selector: SelectorType =
"#main-content .tab-content .film_list-wrap .flw-item";
const categoryNameSelector: SelectorType =
"#main-content .block_area .block_area-header .cat-heading";
res.category = $(categoryNameSelector)?.text()?.trim() ?? category;
res.hasNextPage =
$(".pagination > li").length > 0
? $(".pagination li.active").length > 0
? $(".pagination > li").last().hasClass("active")
? false
: true
: false
: false;
res.totalPages =
Number(
$('.pagination > .page-item a[title="Last"]')
?.attr("href")
?.split("=")
.pop() ??
$('.pagination > .page-item a[title="Next"]')
?.attr("href")
?.split("=")
.pop() ??
$(".pagination > .page-item.active a")?.text()?.trim()
) || 1;
res.animes = extractAnimes($, selector);
if (res.animes.length === 0 && !res.hasNextPage) {
res.totalPages = 0;
}
const genreSelector: SelectorType =
"#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li";
$(genreSelector).each((i, el) => {
res.genres.push(`${$(el).text().trim()}`);
});
const top10AnimeSelector: SelectorType =
'#main-sidebar .block_area-realtime [id^="top-viewed-"]';
$(top10AnimeSelector).each((i, el) => {
const period = $(el).attr("id")?.split("-")?.pop()?.trim();
if (period === "day") {
res.top10Animes.today = extractTop10Animes($, period);
return;
}
if (period === "week") {
res.top10Animes.week = extractTop10Animes($, period);
return;
}
if (period === "month") {
res.top10Animes.month = extractTop10Animes($, period);
}
});
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeAnimeCategory;
-129
View File
@@ -1,129 +0,0 @@
import {
SRC_AJAX_URL,
SRC_BASE_URL,
retrieveServerId,
USER_AGENT_HEADER,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import { load, type CheerioAPI } from "cheerio";
import createHttpError, { type HttpError } from "http-errors";
import { type AnimeServers, Servers } from "../types/anime.js";
import {
RapidCloud,
StreamSB,
StreamTape,
MegaCloud,
} from "../extractors/index.js";
import { type ScrapedAnimeEpisodesSources } from "../types/parsers/index.js";
// vidtreaming -> 4
// rapidcloud -> 1
// streamsb -> 5
// streamtape -> 3
// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)}
async function scrapeAnimeEpisodeSources(
episodeId: string,
server: AnimeServers = Servers.VidStreaming,
category: "sub" | "dub" | "raw" = "sub"
): Promise<ScrapedAnimeEpisodesSources | HttpError> {
if (episodeId.startsWith("http")) {
const serverUrl = new URL(episodeId);
switch (server) {
case Servers.VidStreaming:
case Servers.VidCloud:
return {
...(await new MegaCloud().extract(serverUrl)),
};
case Servers.StreamSB:
return {
headers: {
Referer: serverUrl.href,
watchsb: "streamsb",
"User-Agent": USER_AGENT_HEADER,
},
sources: await new StreamSB().extract(serverUrl, true),
};
case Servers.StreamTape:
return {
headers: { Referer: serverUrl.href, "User-Agent": USER_AGENT_HEADER },
sources: await new StreamTape().extract(serverUrl),
};
default: // vidcloud
return {
headers: { Referer: serverUrl.href },
...(await new RapidCloud().extract(serverUrl)),
};
}
}
const epId = new URL(`/watch/${episodeId}`, SRC_BASE_URL).href;
console.log(epId);
try {
const resp = await axios.get(
`${SRC_AJAX_URL}/v2/episode/servers?episodeId=${epId.split("?ep=")[1]}`,
{
headers: {
Referer: epId,
"User-Agent": USER_AGENT_HEADER,
"X-Requested-With": "XMLHttpRequest",
},
}
);
const $: CheerioAPI = load(resp.data.html);
let serverId: string | null = null;
try {
console.log("THE SERVER: ", server);
switch (server) {
case Servers.VidCloud: {
serverId = retrieveServerId($, 1, category);
if (!serverId) throw new Error("RapidCloud not found");
break;
}
case Servers.VidStreaming: {
serverId = retrieveServerId($, 4, category);
console.log("SERVER_ID: ", serverId);
if (!serverId) throw new Error("VidStreaming not found");
break;
}
case Servers.StreamSB: {
serverId = retrieveServerId($, 5, category);
if (!serverId) throw new Error("StreamSB not found");
break;
}
case Servers.StreamTape: {
serverId = retrieveServerId($, 3, category);
if (!serverId) throw new Error("StreamTape not found");
break;
}
}
} catch (err) {
throw createHttpError.NotFound(
"Couldn't find server. Try another server"
);
}
const {
data: { link },
} = await axios.get(`${SRC_AJAX_URL}/v2/episode/sources?id=${serverId}`);
console.log("THE LINK: ", link);
return await scrapeAnimeEpisodeSources(link, server);
} catch (err: any) {
console.log(err);
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeAnimeEpisodeSources;
-61
View File
@@ -1,61 +0,0 @@
import {
SRC_BASE_URL,
SRC_AJAX_URL,
ACCEPT_HEADER,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import { load, type CheerioAPI } from "cheerio";
import createHttpError, { type HttpError } from "http-errors";
import { type ScrapedAnimeEpisodes } from "../types/parsers/index.js";
// /anime/episodes/${anime-id}
async function scrapeAnimeEpisodes(
animeId: string
): Promise<ScrapedAnimeEpisodes | HttpError> {
const res: ScrapedAnimeEpisodes = {
totalEpisodes: 0,
episodes: [],
};
try {
const episodesAjax = await axios.get(
`${SRC_AJAX_URL}/v2/episode/list/${animeId.split("-").pop()}`,
{
headers: {
Accept: ACCEPT_HEADER,
"User-Agent": USER_AGENT_HEADER,
"X-Requested-With": "XMLHttpRequest",
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
Referer: `${SRC_BASE_URL}/watch/${animeId}`,
},
}
);
const $: CheerioAPI = load(episodesAjax.data.html);
res.totalEpisodes = Number($(".detail-infor-content .ss-list a").length);
$(".detail-infor-content .ss-list a").each((i, el) => {
res.episodes.push({
title: $(el)?.attr("title")?.trim() || null,
episodeId: $(el)?.attr("href")?.split("/")?.pop() || null,
number: Number($(el).attr("data-number")),
isFiller: $(el).hasClass("ssl-item-filler"),
});
});
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeAnimeEpisodes;
-105
View File
@@ -1,105 +0,0 @@
import {
SRC_BASE_URL,
ACCEPT_HEADER,
extractAnimes,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
extractMostPopularAnimes,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import type { ScrapedGenreAnime } from "../types/parsers/index.js";
// /anime/genre/${name}?page=${page}
async function scrapeGenreAnime(
genreName: string,
page: number = 1
): Promise<ScrapedGenreAnime | HttpError> {
const res: ScrapedGenreAnime = {
genreName,
animes: [],
genres: [],
topAiringAnimes: [],
totalPages: 1,
hasNextPage: false,
currentPage: Number(page),
};
// there's a typo with zoro where martial arts is marial arts
genreName = genreName === "martial-arts" ? "marial-arts" : genreName;
try {
const genreUrl: URL = new URL(
`/genre/${genreName}?page=${page}`,
SRC_BASE_URL
);
const mainPage = await axios.get(genreUrl.href, {
headers: {
"User-Agent": USER_AGENT_HEADER,
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
Accept: ACCEPT_HEADER,
},
});
const $: CheerioAPI = load(mainPage.data);
const selector: SelectorType =
"#main-content .tab-content .film_list-wrap .flw-item";
const genreNameSelector: SelectorType =
"#main-content .block_area .block_area-header .cat-heading";
res.genreName = $(genreNameSelector)?.text()?.trim() ?? genreName;
res.hasNextPage =
$(".pagination > li").length > 0
? $(".pagination li.active").length > 0
? $(".pagination > li").last().hasClass("active")
? false
: true
: false
: false;
res.totalPages =
Number(
$('.pagination > .page-item a[title="Last"]')
?.attr("href")
?.split("=")
.pop() ??
$('.pagination > .page-item a[title="Next"]')
?.attr("href")
?.split("=")
.pop() ??
$(".pagination > .page-item.active a")?.text()?.trim()
) || 1;
res.animes = extractAnimes($, selector);
if (res.animes.length === 0 && !res.hasNextPage) {
res.totalPages = 0;
}
const genreSelector: SelectorType =
"#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li";
$(genreSelector).each((i, el) => {
res.genres.push(`${$(el).text().trim()}`);
});
const topAiringSelector: SelectorType =
"#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li";
res.topAiringAnimes = extractMostPopularAnimes($, topAiringSelector);
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeGenreAnime;
-120
View File
@@ -1,120 +0,0 @@
import {
SRC_BASE_URL,
ACCEPT_HEADER,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
extractMostPopularAnimes,
extractAnimes,
extractTop10Animes,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import type { ScrapedProducerAnime } from "../types/parsers/index.js";
// /anime/producer/${name}?page=${page}
async function scrapeProducerAnimes(
producerName: string,
page: number = 1
): Promise<ScrapedProducerAnime | HttpError> {
const res: ScrapedProducerAnime = {
producerName,
animes: [],
top10Animes: {
today: [],
week: [],
month: [],
},
topAiringAnimes: [],
totalPages: 1,
hasNextPage: false,
currentPage: Number(page),
};
try {
const producerUrl: URL = new URL(
`/producer/${producerName}?page=${page}`,
SRC_BASE_URL
);
const mainPage = await axios.get(producerUrl.href, {
headers: {
Accept: ACCEPT_HEADER,
"User-Agent": USER_AGENT_HEADER,
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
},
});
const $: CheerioAPI = load(mainPage.data);
const animeSelector: SelectorType =
"#main-content .tab-content .film_list-wrap .flw-item";
res.hasNextPage =
$(".pagination > li").length > 0
? $(".pagination li.active").length > 0
? $(".pagination > li").last().hasClass("active")
? false
: true
: false
: false;
res.totalPages =
Number(
$('.pagination > .page-item a[title="Last"]')
?.attr("href")
?.split("=")
.pop() ??
$('.pagination > .page-item a[title="Next"]')
?.attr("href")
?.split("=")
.pop() ??
$(".pagination > .page-item.active a")?.text()?.trim()
) || 1;
res.animes = extractAnimes($, animeSelector);
if (res.animes.length === 0 && !res.hasNextPage) {
res.totalPages = 0;
}
const producerNameSelector: SelectorType =
"#main-content .block_area .block_area-header .cat-heading";
res.producerName = $(producerNameSelector)?.text()?.trim() ?? producerName;
const top10AnimeSelector: SelectorType =
'#main-sidebar .block_area-realtime [id^="top-viewed-"]';
$(top10AnimeSelector).each((_, el) => {
const period = $(el).attr("id")?.split("-")?.pop()?.trim();
if (period === "day") {
res.top10Animes.today = extractTop10Animes($, period);
return;
}
if (period === "week") {
res.top10Animes.week = extractTop10Animes($, period);
return;
}
if (period === "month") {
res.top10Animes.month = extractTop10Animes($, period);
}
});
const topAiringSelector: SelectorType =
"#main-sidebar .block_area_sidebar:nth-child(2) .block_area-content .anif-block-ul ul li";
res.topAiringAnimes = extractMostPopularAnimes($, topAiringSelector);
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeProducerAnimes;
-118
View File
@@ -1,118 +0,0 @@
import {
SRC_SEARCH_URL,
ACCEPT_HEADER,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
extractAnimes,
getSearchFilterValue,
extractMostPopularAnimes,
getSearchDateFilterValue,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import type { ScrapedAnimeSearchResult } from "../types/parsers/index.js";
import type { SearchFilters, FilterKeys } from "../types/controllers/index.js";
// /anime/search?q=${query}&page=${page}
async function scrapeAnimeSearch(
q: string,
page: number = 1,
filters: SearchFilters
): Promise<ScrapedAnimeSearchResult | HttpError> {
const res: ScrapedAnimeSearchResult = {
animes: [],
mostPopularAnimes: [],
currentPage: Number(page),
hasNextPage: false,
totalPages: 1,
searchQuery: q,
searchFilters: filters,
};
try {
const url = new URL(SRC_SEARCH_URL);
url.searchParams.set("keyword", q);
url.searchParams.set("page", `${page}`);
url.searchParams.set("sort", "default");
for (const key in filters) {
if (key.includes("_date")) {
const dates = getSearchDateFilterValue(
key === "start_date",
filters[key as keyof SearchFilters] || ""
);
if (!dates) continue;
dates.map((dateParam) => {
const [key, val] = dateParam.split("=");
url.searchParams.set(key, val);
});
continue;
}
const filterVal = getSearchFilterValue(
key as FilterKeys,
filters[key as keyof SearchFilters] || ""
);
filterVal && url.searchParams.set(key, filterVal);
}
const mainPage = await axios.get(url.href, {
headers: {
"User-Agent": USER_AGENT_HEADER,
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
Accept: ACCEPT_HEADER,
},
});
const $: CheerioAPI = load(mainPage.data);
const selector: SelectorType =
"#main-content .tab-content .film_list-wrap .flw-item";
res.hasNextPage =
$(".pagination > li").length > 0
? $(".pagination li.active").length > 0
? $(".pagination > li").last().hasClass("active")
? false
: true
: false
: false;
res.totalPages =
Number(
$('.pagination > .page-item a[title="Last"]')
?.attr("href")
?.split("=")
.pop() ??
$('.pagination > .page-item a[title="Next"]')
?.attr("href")
?.split("=")
.pop() ??
$(".pagination > .page-item.active a")?.text()?.trim()
) || 1;
res.animes = extractAnimes($, selector);
if (res.animes.length === 0 && !res.hasNextPage) {
res.totalPages = 0;
}
const mostPopularSelector: SelectorType =
"#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li";
res.mostPopularAnimes = extractMostPopularAnimes($, mostPopularSelector);
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeAnimeSearch;
-77
View File
@@ -1,77 +0,0 @@
import {
SRC_HOME_URL,
SRC_AJAX_URL,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import type { ScrapedAnimeSearchSuggestion } from "../types/parsers/index.js";
// /anime/search/suggest?q=${query}
async function scrapeAnimeSearchSuggestion(
q: string
): Promise<ScrapedAnimeSearchSuggestion | HttpError> {
const res: ScrapedAnimeSearchSuggestion = {
suggestions: [],
};
try {
const { data } = await axios.get(
`${SRC_AJAX_URL}/search/suggest?keyword=${encodeURIComponent(q)}`,
{
headers: {
Accept: "*/*",
Pragma: "no-cache",
Referer: SRC_HOME_URL,
"User-Agent": USER_AGENT_HEADER,
"X-Requested-With": "XMLHttpRequest",
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
},
}
);
const $: CheerioAPI = load(data.html);
const selector: SelectorType = ".nav-item:has(.film-poster)";
if ($(selector).length < 1) return res;
$(selector).each((_, el) => {
const id = $(el).attr("href")?.split("?")[0].includes("javascript")
? null
: $(el).attr("href")?.split("?")[0]?.slice(1);
res.suggestions.push({
id,
name: $(el).find(".srp-detail .film-name")?.text()?.trim() || null,
jname:
$(el).find(".srp-detail .film-name")?.attr("data-jname")?.trim() ||
$(el).find(".srp-detail .alias-name")?.text()?.trim() ||
null,
poster: $(el)
.find(".film-poster .film-poster-img")
?.attr("data-src")
?.trim(),
moreInfo: [
...$(el)
.find(".film-infor")
.contents()
.map((_, el) => $(el).text().trim()),
].filter((i) => i),
});
});
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeAnimeSearchSuggestion;
-85
View File
@@ -1,85 +0,0 @@
import {
SRC_BASE_URL,
SRC_AJAX_URL,
ACCEPT_HEADER,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import type { ScrapedEpisodeServers } from "../types/parsers/index.js";
// /anime/servers?episodeId=${id}
async function scrapeEpisodeServers(
episodeId: string
): Promise<ScrapedEpisodeServers | HttpError> {
const res: ScrapedEpisodeServers = {
sub: [],
dub: [],
raw: [],
episodeId,
episodeNo: 0,
};
try {
const epId = episodeId.split("?ep=")[1];
const { data } = await axios.get(
`${SRC_AJAX_URL}/v2/episode/servers?episodeId=${epId}`,
{
headers: {
Accept: ACCEPT_HEADER,
"User-Agent": USER_AGENT_HEADER,
"X-Requested-With": "XMLHttpRequest",
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
Referer: new URL(`/watch/${episodeId}`, SRC_BASE_URL).href,
},
}
);
const $: CheerioAPI = load(data.html);
const epNoSelector: SelectorType = ".server-notice strong";
res.episodeNo = Number($(epNoSelector).text().split(" ").pop()) || 0;
$(`.ps_-block.ps_-block-sub.servers-sub .ps__-list .server-item`).each(
(_, el) => {
res.sub.push({
serverName: $(el).find("a").text().toLowerCase().trim(),
serverId: Number($(el)?.attr("data-server-id")?.trim()) || null,
});
}
);
$(`.ps_-block.ps_-block-sub.servers-dub .ps__-list .server-item`).each(
(_, el) => {
res.dub.push({
serverName: $(el).find("a").text().toLowerCase().trim(),
serverId: Number($(el)?.attr("data-server-id")?.trim()) || null,
});
}
);
$(`.ps_-block.ps_-block-sub.servers-raw .ps__-list .server-item`).each(
(_, el) => {
res.raw.push({
serverName: $(el).find("a").text().toLowerCase().trim(),
serverId: Number($(el)?.attr("data-server-id")?.trim()) || null,
});
}
);
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeEpisodeServers;
-74
View File
@@ -1,74 +0,0 @@
import {
SRC_HOME_URL,
SRC_AJAX_URL,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
import { type ScrapedEstimatedSchedule } from "../types/parsers/index.js";
// /anime/schedule?date=${date}
async function scrapeEstimatedSchedule(
date: string
): Promise<ScrapedEstimatedSchedule | HttpError> {
const res: ScrapedEstimatedSchedule = {
scheduledAnimes: [],
};
try {
const estScheduleURL =
`${SRC_AJAX_URL}/schedule/list?tzOffset=-330&date=${date}` as const;
const mainPage = await axios.get(estScheduleURL, {
headers: {
Accept: "*/*",
Referer: SRC_HOME_URL,
"User-Agent": USER_AGENT_HEADER,
"X-Requested-With": "XMLHttpRequest",
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
},
});
const $: CheerioAPI = load(mainPage?.data?.html);
const selector: SelectorType = "li";
if ($(selector)?.text()?.trim()?.includes("No data to display")) {
return res;
}
$(selector).each((_, el) => {
const airingTimestamp = new Date(
`${date}T${$(el)?.find("a .time")?.text()?.trim()}:00`
).getTime();
res.scheduledAnimes.push({
id: $(el)?.find("a")?.attr("href")?.slice(1)?.trim() || null,
time: $(el)?.find("a .time")?.text()?.trim() || null,
name: $(el)?.find("a .film-name.dynamic-name")?.text()?.trim() || null,
jname:
$(el)
?.find("a .film-name.dynamic-name")
?.attr("data-jname")
?.trim() || null,
airingTimestamp,
secondsUntilAiring: Math.floor((airingTimestamp - Date.now()) / 1000),
episode: Number($(el).find("a .fd-play button").text().trim().split(" ")[1])
});
});
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeEstimatedSchedule;
-186
View File
@@ -1,186 +0,0 @@
import {
SRC_HOME_URL,
ACCEPT_HEADER,
USER_AGENT_HEADER,
ACCEPT_ENCODING_HEADER,
extractTop10Animes,
extractAnimes,
extractMostPopularAnimes,
} from "../utils/index.js";
import axios, { AxiosError } from "axios";
import createHttpError, { type HttpError } from "http-errors";
import type { ScrapedHomePage } from "../types/parsers/index.js";
import { load, type CheerioAPI, type SelectorType } from "cheerio";
// /anime/home
async function scrapeHomePage(): Promise<ScrapedHomePage | HttpError> {
const res: ScrapedHomePage = {
spotlightAnimes: [],
trendingAnimes: [],
latestEpisodeAnimes: [],
topUpcomingAnimes: [],
top10Animes: {
today: [],
week: [],
month: [],
},
topAiringAnimes: [],
mostPopularAnimes: [],
mostFavoriteAnimes: [],
latestCompletedAnimes: [],
genres: [],
};
try {
const mainPage = await axios.get(SRC_HOME_URL as string, {
headers: {
"User-Agent": USER_AGENT_HEADER,
"Accept-Encoding": ACCEPT_ENCODING_HEADER,
Accept: ACCEPT_HEADER,
},
});
const $: CheerioAPI = load(mainPage.data);
const spotlightSelector: SelectorType =
"#slider .swiper-wrapper .swiper-slide";
$(spotlightSelector).each((i, el) => {
const otherInfo = $(el)
.find(".deslide-item-content .sc-detail .scd-item")
.map((i, el) => $(el).text().trim())
.get()
.slice(0, -1);
res.spotlightAnimes.push({
rank:
Number(
$(el)
.find(".deslide-item-content .desi-sub-text")
?.text()
.trim()
.split(" ")[0]
.slice(1)
) || null,
id: $(el)
.find(".deslide-item-content .desi-buttons a")
?.last()
?.attr("href")
?.slice(1)
?.trim(),
name: $(el)
.find(".deslide-item-content .desi-head-title.dynamic-name")
?.text()
.trim(),
description: $(el)
.find(".deslide-item-content .desi-description")
?.text()
?.split("[")
?.shift()
?.trim(),
poster: $(el)
.find(".deslide-cover .deslide-cover-img .film-poster-img")
?.attr("data-src")
?.trim(),
jname: $(el)
.find(".deslide-item-content .desi-head-title.dynamic-name")
?.attr("data-jname")
?.trim(),
episodes: {
sub:
Number(
$(el)
.find(
".deslide-item-content .sc-detail .scd-item .tick-item.tick-sub"
)
?.text()
?.trim()
) || null,
dub:
Number(
$(el)
.find(
".deslide-item-content .sc-detail .scd-item .tick-item.tick-dub"
)
?.text()
?.trim()
) || null,
},
otherInfo,
});
});
const trendingSelector: SelectorType =
"#trending-home .swiper-wrapper .swiper-slide";
$(trendingSelector).each((i, el) => {
res.trendingAnimes.push({
rank: parseInt(
$(el).find(".item .number")?.children()?.first()?.text()?.trim()
),
id: $(el).find(".item .film-poster")?.attr("href")?.slice(1)?.trim(),
name: $(el)
.find(".item .number .film-title.dynamic-name")
?.text()
?.trim(),
jname: $(el)
.find(".item .number .film-title.dynamic-name")
?.attr("data-jname")
?.trim(),
poster: $(el)
.find(".item .film-poster .film-poster-img")
?.attr("data-src")
?.trim(),
});
});
const latestEpisodeSelector: SelectorType =
"#main-content .block_area_home:nth-of-type(1) .tab-content .film_list-wrap .flw-item";
res.latestEpisodeAnimes = extractAnimes($, latestEpisodeSelector);
const topUpcomingSelector: SelectorType =
"#main-content .block_area_home:nth-of-type(3) .tab-content .film_list-wrap .flw-item";
res.topUpcomingAnimes = extractAnimes($, topUpcomingSelector);
const genreSelector: SelectorType =
"#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li";
$(genreSelector).each((i, el) => {
res.genres.push(`${$(el).text().trim()}`);
});
const mostViewedSelector: SelectorType =
'#main-sidebar .block_area-realtime [id^="top-viewed-"]';
$(mostViewedSelector).each((i, el) => {
const period = $(el).attr("id")?.split("-")?.pop()?.trim();
if (period === "day") {
res.top10Animes.today = extractTop10Animes($, period);
return;
}
if (period === "week") {
res.top10Animes.week = extractTop10Animes($, period);
return;
}
if (period === "month") {
res.top10Animes.month = extractTop10Animes($, period);
}
});
res.topAiringAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(1) .anif-block-ul ul li");
res.mostPopularAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(2) .anif-block-ul ul li");
res.mostFavoriteAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(3) .anif-block-ul ul li");
res.latestCompletedAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(4) .anif-block-ul ul li");
return res;
} catch (err: any) {
if (err instanceof AxiosError) {
throw createHttpError(
err?.response?.status || 500,
err?.response?.statusText || "Something went wrong"
);
}
throw createHttpError.InternalServerError(err?.message);
}
}
export default scrapeHomePage;
-25
View File
@@ -1,25 +0,0 @@
import scrapeHomePage from "./homePage.js";
import scrapeGenreAnime from "./animeGenre.js";
import scrapeAnimeSearch from "./animeSearch.js";
import scrapeAnimeEpisodes from "./animeEpisodes.js";
import scrapeAnimeCategory from "./animeCategory.js";
import scrapeProducerAnimes from "./animeProducer.js";
import scrapeEpisodeServers from "./episodeServers.js";
import scrapeAnimeAboutInfo from "./animeAboutInfo.js";
import scrapeEstimatedSchedule from "./estimatedSchedule.js";
import scrapeAnimeEpisodeSources from "./animeEpisodeSrcs.js";
import scrapeAnimeSearchSuggestion from "./animeSearchSuggestion.js";
export {
scrapeHomePage,
scrapeGenreAnime,
scrapeAnimeSearch,
scrapeAnimeEpisodes,
scrapeAnimeCategory,
scrapeEpisodeServers,
scrapeProducerAnimes,
scrapeAnimeAboutInfo,
scrapeEstimatedSchedule,
scrapeAnimeEpisodeSources,
scrapeAnimeSearchSuggestion,
};
+125
View File
@@ -0,0 +1,125 @@
import { Hono } from "hono";
import { HiAnime } from "aniwatch";
const hianime = new HiAnime.Scraper();
const hianimeRouter = new Hono();
// /api/v2/hianime
hianimeRouter.get("/", (c) => c.redirect("/", 301));
// /api/v2/hianime/home
hianimeRouter.get("/home", async (c) => {
const data = await hianime.getHomePage();
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/category/{name}?page={page}
hianimeRouter.get("/category/:name", async (c) => {
const categoryName = decodeURIComponent(
c.req.param("name").trim()
) as HiAnime.AnimeCategories;
const page: number =
Number(decodeURIComponent(c.req.query("page") || "")) || 1;
const data = await hianime.getCategoryAnime(categoryName, page);
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/genre/{name}?page={page}
hianimeRouter.get("/genre/:name", async (c) => {
const genreName = decodeURIComponent(c.req.param("name").trim());
const page: number =
Number(decodeURIComponent(c.req.query("page") || "")) || 1;
const data = await hianime.getGenreAnime(genreName, page);
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/producer/{name}?page={page}
hianimeRouter.get("/producer/:name", async (c) => {
const producerName = decodeURIComponent(c.req.param("name").trim());
const page: number =
Number(decodeURIComponent(c.req.query("page") || "")) || 1;
const data = await hianime.getProducerAnimes(producerName, page);
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/schedule?date={date}
hianimeRouter.get("/schedule", async (c) => {
const date = decodeURIComponent(c.req.query("date") || "");
const data = await hianime.getEstimatedSchedule(date);
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/search?q={query}&page={page}&filters={...filters}
hianimeRouter.get("/search", async (c) => {
let { q: query, page, ...filters } = c.req.query();
query = decodeURIComponent(query || "");
const pageNo = Number(decodeURIComponent(page || "")) || 1;
const data = await hianime.search(query, pageNo, filters);
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/search/suggestion?q={query}
hianimeRouter.get("/search/suggestion", async (c) => {
const query = decodeURIComponent(c.req.query("q") || "");
const data = await hianime.searchSuggestions(query);
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/anime/{animeId}
hianimeRouter.get("/anime/:animeId", async (c) => {
const animeId = decodeURIComponent(c.req.param("animeId").trim());
const data = await hianime.getInfo(animeId);
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/episode/servers?animeEpisodeId={id}
hianimeRouter.get("/episode/servers", async (c) => {
const animeEpisodeId = decodeURIComponent(
c.req.query("animeEpisodeId") || ""
);
const data = await hianime.getEpisodeServers(animeEpisodeId);
return c.json({ success: true, data }, { status: 200 });
});
// episodeId=steinsgate-3?ep=230
// /api/v2/hianime/episode/sources?animeEpisodeId={episodeId}?server={server}&category={category (dub or sub)}
hianimeRouter.get("/episode/sources", async (c) => {
const animeEpisodeId = decodeURIComponent(
c.req.query("animeEpisodeId") || ""
);
const server = decodeURIComponent(
c.req.query("server") || HiAnime.Servers.VidStreaming
) as HiAnime.AnimeServers;
const category = decodeURIComponent(c.req.query("category") || "sub") as
| "sub"
| "dub"
| "raw";
const data = await hianime.getEpisodeSources(
animeEpisodeId,
server,
category
);
return c.json({ success: true, data }, { status: 200 });
});
// /api/v2/hianime/anime/{anime-id}/episodes
hianimeRouter.get("/anime/:animeId/episodes", async (c) => {
const animeId = decodeURIComponent(c.req.param("animeId").trim());
const data = await hianime.getEpisodes(animeId);
return c.json({ success: true, data }, { status: 200 });
});
export { hianimeRouter };
-55
View File
@@ -1,55 +0,0 @@
import { Router, type IRouter } from "express";
import {
getGenreAnime,
getAnimeSearch,
getHomePageInfo,
getAnimeCategory,
getAnimeEpisodes,
getEpisodeServers,
getProducerAnimes,
getAnimeAboutInfo,
getEstimatedSchedule,
getAnimeEpisodeSources,
getAnimeSearchSuggestion,
} from "../controllers/index.js";
const router: IRouter = Router();
// /anime
router.get("/", (_, res) => res.redirect("/"));
// /anime/home
router.get("/home", getHomePageInfo);
// /anime/info?id=${anime-id}
router.get("/info", getAnimeAboutInfo);
// /anime/genre/${name}?page=${page}
router.get("/genre/:name", getGenreAnime);
// /anime/search?q=${query}&page=${page}
router.get("/search", getAnimeSearch);
// /anime/search/suggest?q=${query}
router.get("/search/suggest", getAnimeSearchSuggestion);
// /anime/episodes/${anime-id}
router.get("/episodes/:animeId", getAnimeEpisodes);
// /anime/servers?episodeId=${id}
router.get("/servers", getEpisodeServers);
// episodeId=steinsgate-3?ep=230
// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)}
router.get("/episode-srcs", getAnimeEpisodeSources);
// /anime/schedule?date=${date}
router.get("/schedule", getEstimatedSchedule);
// /anime/producer/${name}?page=${page}
router.get("/producer/:name", getProducerAnimes);
// /anime/:category?page=${page}
router.get("/:category", getAnimeCategory);
export default router;
+53 -27
View File
@@ -1,59 +1,85 @@
import https from "https";
import morgan from "morgan";
import express from "express";
import { resolve } from "path";
import { config } from "dotenv";
import corsConfig from "./config/cors.js";
import { ratelimit } from "./config/ratelimit.js";
import errorHandler from "./config/errorHandler.js";
import notFoundHandler from "./config/notFoundHandler.js";
import animeRouter from "./routes/index.js";
import { hianimeRouter } from "./routes/hianime.js";
import { serveStatic } from "@hono/node-server/serve-static";
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { logger } from "hono/logger";
import { HiAnimeError } from "aniwatch";
config();
const app: express.Application = express();
const PORT: number = Number(process.env.PORT) || 4000;
app.use(morgan("dev"));
const BASE_PATH = "/api/v2" as const;
const PORT: number = Number(process.env.ANIWATCH_API_PORT) || 4000;
const ANIWATCH_API_HOSTNAME = process.env?.ANIWATCH_API_HOSTNAME;
const app = new Hono();
app.use(logger());
app.use(corsConfig);
// CAUTION: For personal deployments, "refrain" from having an env
// named "ANIWATCH_API_HOSTNAME". You may face rate limitting
// and other issues if you do.
const ISNT_PERSONAL_DEPLOYMENT = Boolean(process?.env?.ANIWATCH_API_HOSTNAME);
// or other issues if you do.
const ISNT_PERSONAL_DEPLOYMENT = Boolean(ANIWATCH_API_HOSTNAME);
if (ISNT_PERSONAL_DEPLOYMENT) {
app.use(ratelimit);
}
app.use(express.static(resolve("public")));
app.get("/health", (_, res) => res.sendStatus(200));
app.use("/anime", animeRouter);
app.use("/", serveStatic({ root: "public" }));
app.get("/health", (c) => c.text("OK", { status: 200 }));
app.use(notFoundHandler);
app.use(errorHandler);
app.basePath(BASE_PATH).route("/hianime", hianimeRouter);
app
.basePath(BASE_PATH)
.get("/anicrush", (c) => c.text("Anicrush could be implemented in future."));
app.notFound((c) =>
c.json({ status: 404, message: "Resource Not Found" }, 404)
);
app.onError((err, c) => {
console.error(err);
const res = { status: 500, message: "Internal Server Error" };
if (err instanceof HiAnimeError) {
res.status = err.status;
res.message = err.message;
}
return c.json(res, { status: res.status });
});
// NOTE: this env is "required" for vercel deployments
if (!Boolean(process?.env?.IS_VERCEL_DEPLOYMENT)) {
app.listen(PORT, () => {
console.log(`⚔️ api @ http://localhost:${PORT}`);
});
if (!Boolean(process?.env?.ANIWATCH_API_VERCEL_DEPLOYMENT)) {
serve({
port: PORT,
fetch: app.fetch,
}).addListener("listening", () =>
console.info(
"\x1b[1;36m" + `aniwatch-api at http://localhost:${PORT}` + "\x1b[0m"
)
);
// NOTE: remove the `if` block below for personal deployments
if (ISNT_PERSONAL_DEPLOYMENT) {
const interval = 9 * 60 * 1000; // 9mins
// don't sleep
const intervalTime = 9 * 60 * 1000; // 9mins
setInterval(() => {
console.log("HEALTHCHECK ;)", new Date().toLocaleString());
console.log("aniwatch-api HEALTH_CHECK at", new Date().toISOString());
https
.get(
new URL("/health", `https://${process.env.ANIWATCH_API_HOSTNAME}`)
.href
)
.get(`https://${ANIWATCH_API_HOSTNAME}/health`)
.on("error", (err) => {
console.error(err.message);
});
}, intervalTime);
}, interval);
}
}
-140
View File
@@ -1,140 +0,0 @@
export interface Anime {
id: string | null;
name: string | null;
jname: string | null;
poster: string | null;
duration: string | null;
type: string | null;
rating: string | null;
episodes: {
sub: number | null;
dub: number | null;
};
}
type CommonAnimeProps = "id" | "name" | "poster";
export interface Top10Anime extends Pick<Anime, CommonAnimeProps | "episodes"> {
rank: number | null;
jname: string | null;
}
export type Top10AnimeTimePeriod = "day" | "week" | "month";
export interface MostPopularAnime
extends Pick<Anime, CommonAnimeProps | "episodes" | "type"> {
jname: string | null;
}
export interface SpotlightAnime
extends MostPopularAnime,
Pick<Top10Anime, "rank"> {
description: string | null;
}
export interface TrendingAnime
extends Pick<Anime, CommonAnimeProps>,
Pick<Top10Anime, "rank"> {}
export interface LatestEpisodeAnime extends Anime {}
export interface TopUpcomingAnime extends Anime {}
export interface TopAiringAnime extends MostPopularAnime {}
export interface MostFavoriteAnime extends MostPopularAnime {}
export interface LatestCompletedAnime extends MostPopularAnime {}
export interface AnimeGeneralAboutInfo
extends Pick<Anime, CommonAnimeProps>,
Pick<SpotlightAnime, "description"> {
anilistId: number | null;
malId: number | null;
stats: {
quality: string | null;
} & Pick<Anime, "duration" | "episodes" | "rating" | "type">;
promotionalVideos: AnimePromotionalVideo[];
charactersVoiceActors: AnimeCharactersAndVoiceActors[];
}
export interface RecommendedAnime extends Anime {}
export interface RelatedAnime extends MostPopularAnime {}
export interface Season extends Pick<Anime, CommonAnimeProps> {
isCurrent: boolean;
title: string | null;
}
export interface AnimePromotionalVideo {
title: string | undefined;
source: string | undefined;
thumbnail: string | undefined;
}
export interface AnimeCharactersAndVoiceActors {
character: AnimeCharacter;
voiceActor: AnimeCharacter;
}
export interface AnimeCharacter {
id: string;
poster: string;
name: string;
cast: string;
}
export interface AnimeSearchSuggestion
extends Omit<MostPopularAnime, "episodes" | "type"> {
moreInfo: Array<string>;
}
export interface AnimeEpisode extends Pick<Season, "title"> {
episodeId: string | null;
number: number;
isFiller: boolean;
}
export interface SubEpisode {
serverName: string;
serverId: number | null;
}
export interface DubEpisode extends SubEpisode {}
export interface RawEpisode extends SubEpisode {}
export type AnimeCategories =
| "most-favorite"
| "most-popular"
| "subbed-anime"
| "dubbed-anime"
| "recently-updated"
| "recently-added"
| "top-upcoming"
| "top-airing"
| "movie"
| "special"
| "ova"
| "ona"
| "tv"
| "completed";
export type AnimeServers =
| "hd-1"
| "hd-2"
| "megacloud"
| "streamsb"
| "streamtape";
export enum Servers {
VidStreaming = "hd-1",
MegaCloud = "megacloud",
StreamSB = "streamsb",
StreamTape = "streamtape",
VidCloud = "hd-2",
AsianLoad = "asianload",
GogoCDN = "gogocdn",
MixDrop = "mixdrop",
UpCloud = "upcloud",
VizCloud = "vizcloud",
MyCloud = "mycloud",
Filemoon = "filemoon",
}
-3
View File
@@ -1,3 +0,0 @@
export type AnimeAboutInfoQueryParams = {
id?: string;
};
-7
View File
@@ -1,7 +0,0 @@
export type CategoryAnimePathParams = {
category?: string;
};
export type CategoryAnimeQueryParams = {
page?: string;
};
@@ -1,7 +0,0 @@
import { type AnimeServers } from "../anime.js";
export type AnimeEpisodeSrcsQueryParams = {
id?: string;
server?: AnimeServers;
category?: "sub" | "dub";
};
-3
View File
@@ -1,3 +0,0 @@
export type AnimeEpisodePathParams = {
animeId?: string;
};
-7
View File
@@ -1,7 +0,0 @@
export type GenreAnimePathParams = {
name?: string;
};
export type GenreAnimeQueryParams = {
page?: string;
};
-7
View File
@@ -1,7 +0,0 @@
export type AnimeProducerPathParams = {
name?: string;
};
export type AnimeProducerQueryParams = {
page?: string;
};
-20
View File
@@ -1,20 +0,0 @@
export type AnimeSearchQueryParams = {
q?: string;
page?: string;
type?: string;
status?: string;
rated?: string;
score?: string;
season?: string;
language?: string;
start_date?: string;
end_date?: string;
sort?: string;
genres?: string;
};
export type SearchFilters = Omit<AnimeSearchQueryParams, "q" | "page">;
export type FilterKeys = Partial<
keyof Omit<SearchFilters, "start_date" | "end_date">
>;
@@ -1,3 +0,0 @@
export type AnimeSearchSuggestQueryParams = {
q?: string;
};
-3
View File
@@ -1,3 +0,0 @@
export type EpisodeServersQueryParams = {
episodeId?: string;
};
@@ -1,3 +0,0 @@
export type EstimatedScheduleQueryParams = {
date?: string;
};
-10
View File
@@ -1,10 +0,0 @@
export type * from "./animeGenre.js";
export type * from "./animeCategory.js";
export type * from "./animeProducer.js";
export type * from "./animeSearch.js";
export type * from "./animeEpisodes.js";
export type * from "./episodeServers.js";
export type * from "./animeAboutInfo.js";
export type * from "./animeEpisodeSrcs.js";
export type * from "./estimatedSchedule.js";
export type * from "./animeSearchSuggestion.js";
-18
View File
@@ -1,18 +0,0 @@
export interface Video {
url: string;
quality?: string;
isM3U8?: boolean;
size?: number;
[x: string]: unknown;
}
export interface Subtitle {
id?: string;
url: string;
lang: string;
}
export interface Intro {
start: number;
end: number;
}
-19
View File
@@ -1,19 +0,0 @@
import type {
Season,
RelatedAnime,
RecommendedAnime,
AnimeGeneralAboutInfo,
} from "../anime.js";
import { type HttpError } from "http-errors";
import { type ScrapedAnimeSearchResult } from "./animeSearch.js";
export interface ScrapedAnimeAboutInfo
extends Pick<ScrapedAnimeSearchResult, "mostPopularAnimes"> {
anime: {
info: AnimeGeneralAboutInfo;
moreInfo: Record<string, string | string[]>;
};
seasons: Array<Season>;
relatedAnimes: Array<RelatedAnime> | HttpError;
recommendedAnimes: Array<RecommendedAnime> | HttpError;
}
-22
View File
@@ -1,22 +0,0 @@
import type { HttpError } from "http-errors";
import type { Anime, Top10Anime } from "../anime.js";
export interface ScrapedAnimeCategory {
animes: Array<Anime> | HttpError;
genres: Array<string>;
top10Animes: {
today: Array<Top10Anime> | HttpError;
week: Array<Top10Anime> | HttpError;
month: Array<Top10Anime> | HttpError;
};
category: string;
totalPages: number;
currentPage: number;
hasNextPage: boolean;
}
export type CommonAnimeScrapeTypes =
| "animes"
| "totalPages"
| "hasNextPage"
| "currentPage";
-12
View File
@@ -1,12 +0,0 @@
import type { Intro, Subtitle, Video } from "../extractor.js";
export interface ScrapedAnimeEpisodesSources {
headers?: {
[k: string]: string;
};
intro?: Intro;
subtitles?: Subtitle[];
sources: Video[];
download?: string;
embedURL?: string;
}
-6
View File
@@ -1,6 +0,0 @@
import { type AnimeEpisode } from "../anime.js";
export interface ScrapedAnimeEpisodes {
totalEpisodes: number;
episodes: Array<AnimeEpisode>;
}
-11
View File
@@ -1,11 +0,0 @@
import type {
ScrapedAnimeCategory,
CommonAnimeScrapeTypes,
} from "./animeCategory.js";
import { type ScrapedHomePage } from "./homePage.js";
export interface ScrapedGenreAnime
extends Pick<ScrapedAnimeCategory, CommonAnimeScrapeTypes | "genres">,
Pick<ScrapedHomePage, "topAiringAnimes"> {
genreName: string;
}
-8
View File
@@ -1,8 +0,0 @@
import type { ScrapedHomePage } from "./homePage.js";
import type { ScrapedAnimeCategory } from "./animeCategory.js";
export interface ScrapedProducerAnime
extends Omit<ScrapedAnimeCategory, "genres" | "category">,
Pick<ScrapedHomePage, "topAiringAnimes"> {
producerName: string;
}
-14
View File
@@ -1,14 +0,0 @@
import type {
ScrapedAnimeCategory,
CommonAnimeScrapeTypes,
} from "./animeCategory.js";
import type { HttpError } from "http-errors";
import type { MostPopularAnime } from "../anime.js";
import type { SearchFilters } from "../controllers/animeSearch.js";
export interface ScrapedAnimeSearchResult
extends Pick<ScrapedAnimeCategory, CommonAnimeScrapeTypes> {
mostPopularAnimes: Array<MostPopularAnime> | HttpError;
searchQuery: string;
searchFilters: SearchFilters;
}
@@ -1,6 +0,0 @@
import type { HttpError } from "http-errors";
import type { AnimeSearchSuggestion } from "../anime.js";
export interface ScrapedAnimeSearchSuggestion {
suggestions: Array<AnimeSearchSuggestion> | HttpError;
}
-9
View File
@@ -1,9 +0,0 @@
import type { SubEpisode, DubEpisode, RawEpisode } from "../anime.js";
export interface ScrapedEpisodeServers {
sub: SubEpisode[];
dub: DubEpisode[];
raw: RawEpisode[];
episodeNo: number;
episodeId: string;
}
-13
View File
@@ -1,13 +0,0 @@
type EstimatedSchedule = {
id: string | null;
time: string | null;
name: string | null;
jname: string | null;
airingTimestamp: number;
secondsUntilAiring: number;
episode: number;
};
export type ScrapedEstimatedSchedule = {
scheduledAnimes: Array<EstimatedSchedule>;
};
-24
View File
@@ -1,24 +0,0 @@
import type {
TrendingAnime,
SpotlightAnime,
TopAiringAnime,
TopUpcomingAnime,
LatestEpisodeAnime,
MostFavoriteAnime,
MostPopularAnime,
LatestCompletedAnime,
} from "../anime.js";
import type { HttpError } from "http-errors";
import type { ScrapedAnimeCategory } from "./animeCategory.js";
export interface ScrapedHomePage
extends Pick<ScrapedAnimeCategory, "genres" | "top10Animes"> {
spotlightAnimes: Array<SpotlightAnime> | HttpError;
trendingAnimes: Array<TrendingAnime> | HttpError;
latestEpisodeAnimes: Array<LatestEpisodeAnime> | HttpError;
topUpcomingAnimes: Array<TopUpcomingAnime> | HttpError;
topAiringAnimes: Array<TopAiringAnime> | HttpError;
mostPopularAnimes: Array<MostPopularAnime> | HttpError;
mostFavoriteAnimes: Array<MostFavoriteAnime> | HttpError;
latestCompletedAnimes: Array<LatestCompletedAnime> | HttpError;
}
-25
View File
@@ -1,25 +0,0 @@
import type { ScrapedHomePage } from "./homePage.js";
import type { ScrapedGenreAnime } from "./animeGenre.js";
import type { ScrapedAnimeEpisodes } from "./animeEpisodes.js";
import type { ScrapedAnimeCategory } from "./animeCategory.js";
import type { ScrapedProducerAnime } from "./animeProducer.js";
import type { ScrapedEpisodeServers } from "./episodeServers.js";
import type { ScrapedAnimeAboutInfo } from "./animeAboutInfo.js";
import type { ScrapedAnimeSearchResult } from "./animeSearch.js";
import type { ScrapedEstimatedSchedule } from "./estimatedSchedule.js";
import type { ScrapedAnimeEpisodesSources } from "./animeEpisodeSrcs.js";
import type { ScrapedAnimeSearchSuggestion } from "./animeSearchSuggestion.js";
export type {
ScrapedHomePage,
ScrapedGenreAnime,
ScrapedAnimeEpisodes,
ScrapedProducerAnime,
ScrapedAnimeCategory,
ScrapedEpisodeServers,
ScrapedAnimeAboutInfo,
ScrapedAnimeSearchResult,
ScrapedEstimatedSchedule,
ScrapedAnimeEpisodesSources,
ScrapedAnimeSearchSuggestion,
};
-129
View File
@@ -1,129 +0,0 @@
import { config } from "dotenv";
config();
export const ACCEPT_ENCODING_HEADER = "gzip, deflate, br";
export const USER_AGENT_HEADER =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4692.71 Safari/537.36";
export const ACCEPT_HEADER =
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9";
// previously aniwatch.to || aniwatchtv.to
const DOMAIN = process.env.DOMAIN || "hianime.to";
export const SRC_BASE_URL = `https://${DOMAIN}` as const;
export const SRC_AJAX_URL = `${SRC_BASE_URL}/ajax` as const;
export const SRC_HOME_URL = `${SRC_BASE_URL}/home` as const;
export const SRC_SEARCH_URL = `${SRC_BASE_URL}/search` as const;
// <SearchPageFilters>
export const genresIdMap: Record<string, number> = {
action: 1,
adventure: 2,
cars: 3,
comedy: 4,
dementia: 5,
demons: 6,
drama: 8,
ecchi: 9,
fantasy: 10,
game: 11,
harem: 35,
historical: 13,
horror: 14,
isekai: 44,
josei: 43,
kids: 15,
magic: 16,
"martial-arts": 17,
mecha: 18,
military: 38,
music: 19,
mystery: 7,
parody: 20,
police: 39,
psychological: 40,
romance: 22,
samurai: 21,
school: 23,
"sci-fi": 24,
seinen: 42,
shoujo: 25,
"shoujo-ai": 26,
shounen: 27,
"shounen-ai": 28,
"slice-of-life": 36,
space: 29,
sports: 30,
"super-power": 31,
supernatural: 37,
thriller: 41,
vampire: 32,
} as const;
export const typeIdMap: Record<string, number> = {
all: 0,
movie: 1,
tv: 2,
ova: 3,
ona: 4,
special: 5,
music: 6,
} as const;
export const statusIdMap: Record<string, number> = {
all: 0,
"finished-airing": 1,
"currently-airing": 2,
"not-yet-aired": 3,
} as const;
export const ratedIdMap: Record<string, number> = {
all: 0,
g: 1,
pg: 2,
"pg-13": 3,
r: 4,
"r+": 5,
rx: 6,
} as const;
export const scoreIdMap: Record<string, number> = {
all: 0,
appalling: 1,
horrible: 2,
"very-bad": 3,
bad: 4,
average: 5,
fine: 6,
good: 7,
"very-good": 8,
great: 9,
masterpiece: 10,
} as const;
export const seasonIdMap: Record<string, number> = {
all: 0,
spring: 1,
summer: 2,
fall: 3,
winter: 4,
} as const;
export const languageIdMap: Record<string, number> = {
all: 0,
sub: 1,
dub: 2,
"sub-&-dub": 3,
} as const;
export const sortIdMap: Record<string, string> = {
default: "default",
"recently-added": "recently_added",
"recently-updated": "recently_updated",
score: "score",
"name-a-z": "name_az",
"released-date": "released_date",
"most-watched": "most_watched",
} as const;
// </SearchPageFilters>
-2
View File
@@ -1,2 +0,0 @@
export * from "./methods.js";
export * from "./constants.js";
-297
View File
@@ -1,297 +0,0 @@
import type {
Anime,
Top10Anime,
MostPopularAnime,
Top10AnimeTimePeriod,
} from "../types/anime.js";
import type { CheerioAPI, SelectorType } from "cheerio";
import {
genresIdMap,
languageIdMap,
ratedIdMap,
scoreIdMap,
seasonIdMap,
sortIdMap,
statusIdMap,
typeIdMap,
} from "./constants.js";
import { type FilterKeys } from "../types/controllers/animeSearch.js";
import createHttpError, { HttpError } from "http-errors";
export const extractAnimes = (
$: CheerioAPI,
selector: SelectorType
): Array<Anime> | HttpError => {
try {
const animes: Array<Anime> = [];
$(selector).each((i, el) => {
const animeId =
$(el)
.find(".film-detail .film-name .dynamic-name")
?.attr("href")
?.slice(1)
.split("?ref=search")[0] || null;
animes.push({
id: animeId,
name: $(el)
.find(".film-detail .film-name .dynamic-name")
?.text()
?.trim(),
jname:
$(el)
.find(".film-detail .film-name .dynamic-name")
?.attr("data-jname")
?.trim() || null,
poster:
$(el)
.find(".film-poster .film-poster-img")
?.attr("data-src")
?.trim() || null,
duration: $(el)
.find(".film-detail .fd-infor .fdi-item.fdi-duration")
?.text()
?.trim(),
type: $(el)
.find(".film-detail .fd-infor .fdi-item:nth-of-type(1)")
?.text()
?.trim(),
rating: $(el).find(".film-poster .tick-rate")?.text()?.trim() || null,
episodes: {
sub:
Number(
$(el)
.find(".film-poster .tick-sub")
?.text()
?.trim()
.split(" ")
.pop()
) || null,
dub:
Number(
$(el)
.find(".film-poster .tick-dub")
?.text()
?.trim()
.split(" ")
.pop()
) || null,
},
});
});
return animes;
} catch (err: any) {
throw createHttpError.InternalServerError(
err?.message || "Something went wrong"
);
}
};
export const extractTop10Animes = (
$: CheerioAPI,
period: Top10AnimeTimePeriod
): Array<Top10Anime> | HttpError => {
try {
const animes: Array<Top10Anime> = [];
const selector = `#top-viewed-${period} ul li`;
$(selector).each((i, el) => {
animes.push({
id:
$(el)
.find(".film-detail .dynamic-name")
?.attr("href")
?.slice(1)
.trim() || null,
rank: Number($(el).find(".film-number span")?.text()?.trim()) || null,
name: $(el).find(".film-detail .dynamic-name")?.text()?.trim() || null,
jname:
$(el)
.find(".film-detail .dynamic-name")
?.attr("data-jname")
?.trim() || null,
poster:
$(el)
.find(".film-poster .film-poster-img")
?.attr("data-src")
?.trim() || null,
episodes: {
sub:
Number(
$(el)
.find(".film-detail .fd-infor .tick-item.tick-sub")
?.text()
?.trim()
) || null,
dub:
Number(
$(el)
.find(".film-detail .fd-infor .tick-item.tick-dub")
?.text()
?.trim()
) || null,
},
});
});
return animes;
} catch (err: any) {
throw createHttpError.InternalServerError(
err?.message || "Something went wrong"
);
}
};
export const extractMostPopularAnimes = (
$: CheerioAPI,
selector: SelectorType
): Array<MostPopularAnime> | HttpError => {
try {
const animes: Array<MostPopularAnime> = [];
$(selector).each((i, el) => {
animes.push({
id:
$(el)
.find(".film-detail .dynamic-name")
?.attr("href")
?.slice(1)
.trim() || null,
name: $(el).find(".film-detail .dynamic-name")?.text()?.trim() || null,
jname:
$(el)
.find(".film-detail .film-name .dynamic-name")
.attr("data-jname")
?.trim() || null,
poster:
$(el)
.find(".film-poster .film-poster-img")
?.attr("data-src")
?.trim() || null,
episodes: {
sub:
Number($(el)?.find(".fd-infor .tick .tick-sub")?.text()?.trim()) ||
null,
dub:
Number($(el)?.find(".fd-infor .tick .tick-dub")?.text()?.trim()) ||
null,
},
type:
$(el)
?.find(".fd-infor .tick")
?.text()
?.trim()
?.replace(/[\s\n]+/g, " ")
?.split(" ")
?.pop() || null,
});
});
return animes;
} catch (err: any) {
throw createHttpError.InternalServerError(
err?.message || "Something went wrong"
);
}
};
export function retrieveServerId(
$: CheerioAPI,
index: number,
category: "sub" | "dub" | "raw"
) {
return (
$(`.ps_-block.ps_-block-sub.servers-${category} > .ps__-list .server-item`)
?.map((_, el) =>
$(el).attr("data-server-id") == `${index}` ? $(el) : null
)
?.get()[0]
?.attr("data-id") || null
);
}
function getGenresFilterVal(genreNames: string[]): string | undefined {
if (genreNames.length < 1) {
return undefined;
}
return genreNames.map((name) => genresIdMap[name]).join(",");
}
export function getSearchFilterValue(
key: FilterKeys,
rawValue: string
): string | undefined {
rawValue = rawValue.trim();
if (!rawValue) return undefined;
switch (key) {
case "genres": {
return getGenresFilterVal(rawValue.split(","));
}
case "type": {
const val = typeIdMap[rawValue] ?? 0;
return val === 0 ? undefined : `${val}`;
}
case "status": {
const val = statusIdMap[rawValue] ?? 0;
return val === 0 ? undefined : `${val}`;
}
case "rated": {
const val = ratedIdMap[rawValue] ?? 0;
return val === 0 ? undefined : `${val}`;
}
case "score": {
const val = scoreIdMap[rawValue] ?? 0;
return val === 0 ? undefined : `${val}`;
}
case "season": {
const val = seasonIdMap[rawValue] ?? 0;
return val === 0 ? undefined : `${val}`;
}
case "language": {
const val = languageIdMap[rawValue] ?? 0;
return val === 0 ? undefined : `${val}`;
}
case "sort": {
return sortIdMap[rawValue] ?? undefined;
}
default:
return undefined;
}
}
// this fn tackles both start_date and end_date
export function getSearchDateFilterValue(
isStartDate: boolean,
rawValue: string
): string[] | undefined {
rawValue = rawValue.trim();
if (!rawValue) return undefined;
const dateRegex = /^\d{4}-([0-9]|1[0-2])-([0-9]|[12][0-9]|3[01])$/;
const dateCategory = isStartDate ? "s" : "e";
const [year, month, date] = rawValue.split("-");
if (!dateRegex.test(rawValue)) {
return undefined;
}
// sample return -> [sy=2023, sm=10, sd=11]
return [
Number(year) > 0 ? `${dateCategory}y=${year}` : "",
Number(month) > 0 ? `${dateCategory}m=${month}` : "",
Number(date) > 0 ? `${dateCategory}d=${date}` : "",
].filter((d) => Boolean(d));
}
export function substringAfter(str: string, toFind: string) {
const index = str.indexOf(toFind);
return index == -1 ? "" : str.substring(index + toFind.length);
}
export function substringBefore(str: string, toFind: string) {
const index = str.indexOf(toFind);
return index == -1 ? "" : str.substring(0, index);
}