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:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type AnimeAboutInfoQueryParams = {
|
||||
id?: string;
|
||||
};
|
||||
@@ -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";
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export type AnimeEpisodePathParams = {
|
||||
animeId?: string;
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export type GenreAnimePathParams = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type GenreAnimeQueryParams = {
|
||||
page?: string;
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export type AnimeProducerPathParams = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type AnimeProducerQueryParams = {
|
||||
page?: string;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export type EpisodeServersQueryParams = {
|
||||
episodeId?: string;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export type EstimatedScheduleQueryParams = {
|
||||
date?: string;
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { type AnimeEpisode } from "../anime.js";
|
||||
|
||||
export interface ScrapedAnimeEpisodes {
|
||||
totalEpisodes: number;
|
||||
episodes: Array<AnimeEpisode>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./methods.js";
|
||||
export * from "./constants.js";
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user