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:
+21
-5
@@ -1,7 +1,23 @@
|
|||||||
DOMAIN="aniwatchtv.to"
|
# port number of the aniwatch api
|
||||||
PORT=4000
|
ANIWATCH_API_PORT=4000
|
||||||
CORS_ALLOWED_ORIGINS=https://your-production-domain.com,https://another-trusted-domain.com
|
|
||||||
|
# env to control allowed origins
|
||||||
|
ANIWATCH_API_CORS_ALLOWED_ORIGINS=https://your-production-domain.com,https://another-trusted-domain.com
|
||||||
|
|
||||||
# RATE LIMIT
|
# RATE LIMIT
|
||||||
WINDOW_MS=1800000 # duration to track requests (in milliseconds) for rate limiting. here, 30*60*1000 = 1800000 = 30 minutes
|
# duration to track requests (in milliseconds) for rate limiting. here, 30*60*1000 = 1800000 = 30 minutes
|
||||||
MAX=70 # maximum number of requests in this timeperiod
|
ANIWATCH_API_WINDOW_MS=1800000
|
||||||
|
# maximum number of requests in this timeperiod
|
||||||
|
ANIWATCH_API_MAX_REQS=70
|
||||||
|
|
||||||
|
# CAUTION:
|
||||||
|
# For personal deployments, if you wanna have rate limitting
|
||||||
|
# in your application, then set this env to your deployed
|
||||||
|
# instance's hostname, otherwise don't set or have this env at all.
|
||||||
|
# If you set this env to an incorrect value, you may face other issues.
|
||||||
|
|
||||||
|
# ANIWATCH_API_HOSTNAME="api-aniwatch.onrender.com"
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: this env is "required" for vercel deployments
|
||||||
|
# ANIWATCH_VERCEL_DEPLOYMENT=<true or any non zero value>
|
||||||
|
|||||||
+8
-1
@@ -1,3 +1,10 @@
|
|||||||
import app from "../src/server.js";
|
import app from "../src/server.js";
|
||||||
|
import { handle } from "hono/vercel";
|
||||||
|
|
||||||
export default app;
|
const handler = handle(app);
|
||||||
|
|
||||||
|
export const GET = handler;
|
||||||
|
export const POST = handler;
|
||||||
|
export const PATCH = handler;
|
||||||
|
export const PUT = handler;
|
||||||
|
export const OPTIONS = handler;
|
||||||
|
|||||||
+9
-18
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aniwatch-api",
|
"name": "aniwatch-api",
|
||||||
"version": "1.40.0",
|
"version": "1.40.0",
|
||||||
"description": "Node.js API for obtaining anime information from hianime.to (formerly aniwatch.to)",
|
"description": "Node.js API for obtaining anime information from hianime.to",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -31,26 +31,17 @@
|
|||||||
"author": "https://github.com/ghoshRitesh12",
|
"author": "https://github.com/ghoshRitesh12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"@hono/node-server": "^1.13.1",
|
||||||
"cheerio": "1.0.0",
|
"aniwatch": "^2.4.1",
|
||||||
"cors": "^2.8.5",
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"hono": "^4.6.3",
|
||||||
"express-rate-limit": "^7.4.0",
|
"hono-rate-limiter": "^0.4.0"
|
||||||
"http-errors": "^2.0.0",
|
|
||||||
"morgan": "^1.10.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/node": "^22.7.4",
|
||||||
"@types/crypto-js": "^4.2.2",
|
|
||||||
"@types/express": "^4.17.21",
|
|
||||||
"@types/http-errors": "^2.0.4",
|
|
||||||
"@types/morgan": "^1.9.9",
|
|
||||||
"@types/node": "^22.5.4",
|
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.1",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^2.0.5"
|
"vitest": "^2.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 939 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 356 KiB |
+184
-72
@@ -5,67 +5,151 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Aniwatch API</title>
|
<title>Aniwatch API</title>
|
||||||
<meta name="content-language" content="en">
|
<meta name="content-language" content="en">
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<!-- <meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' https://api-aniwatch.onrender.com; style-src 'unsafe-inline'; img-src 'self' https://raw.githubusercontent.com https://img.shields.io https://contrib.rocks">
|
content="default-src 'self' https://api-aniwatch.onrender.com; style-src 'unsafe-inline'; img-src 'self' https://raw.githubusercontent.com https://img.shields.io https://contrib.rocks"> -->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="title" content="Aniwatch API">
|
<meta name="title" content="Aniwatch API">
|
||||||
|
<meta name="theme-color" content="#d5b3ff">
|
||||||
|
<meta name="color-scheme" content="dark">
|
||||||
<link rel="canonical" href="https://api-aniwatch.onrender.com">
|
<link rel="canonical" href="https://api-aniwatch.onrender.com">
|
||||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
||||||
<meta property="og:image"
|
<meta property="og:image"
|
||||||
content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/main/public/img/img1.gif">
|
content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
||||||
<meta property="og:url" content="https://api-aniwatch.onrender.com">
|
<meta property="og:url" content="https://api-aniwatch.onrender.com">
|
||||||
<meta property="og:image:width" content="457">
|
<meta property="og:image:width" content="500">
|
||||||
<meta property="og:image:height" content="480">
|
<meta property="og:image:height" content="500">
|
||||||
<meta property="og:site_name" content="aniwatch">
|
<meta property="og:site_name" content="aniwatch">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_image">
|
||||||
<meta name="twitter:site" content="@aniwatch-api">
|
<meta name="twitter:site" content="@aniwatch-api">
|
||||||
<meta name="twitter:title" content="Aniwatch API">
|
<meta name="twitter:title" content="Aniwatch API">
|
||||||
<meta name="twitter:description"
|
<meta name="twitter:description" content="Node.js API for obtaining anime information from hianime.to">
|
||||||
content="Node Express API for obtaining anime information from hianime.to (formerly aniwatch.to)">
|
|
||||||
<meta name="twitter:image:src"
|
<meta name="twitter:image:src"
|
||||||
content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/main/public/img/img1.gif">
|
content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
||||||
<meta name="keywords" content="hianime api scraper anime aniwatch node express typescript">
|
<meta name="keywords" content="hianime api scraper anime aniwatch node express typescript">
|
||||||
<meta property="og:title" content="Aniwatch API">
|
<meta property="og:title" content="Aniwatch API">
|
||||||
<meta name="description"
|
<meta name="description" content="Node.js API for obtaining anime information from hianime.to">
|
||||||
content="Node Express API for obtaining anime information from hianime.to (formerly aniwatch.to)">
|
<meta property="og:description" content="Node.js API for obtaining anime information from hianime.to">
|
||||||
<meta property="og:description"
|
|
||||||
content="Node Express API for obtaining anime information from hianime.to (formerly aniwatch.to)">
|
|
||||||
<link rel="shortcut icon"
|
<link rel="shortcut icon"
|
||||||
href="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/main/public/img/img1.gif">
|
href="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
|
--accent: #d5b3ff;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
transition:
|
||||||
|
0.15s ease scale,
|
||||||
|
0.15s ease width,
|
||||||
|
0.15s ease height,
|
||||||
|
0.15s ease gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: start;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
background: #18161A;
|
background-color: #151518;
|
||||||
padding: 10rem 1rem 0 1rem;
|
padding: 0rem 1rem 0 1rem;
|
||||||
|
min-height: 98vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-block: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
section::before {
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
inset: -2rem 0 0 0;
|
||||||
|
isolation: isolate;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
background-color: #7300ff;
|
||||||
|
opacity: .35;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(12rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
section::before {
|
||||||
|
background-color: #7300ffbb;
|
||||||
|
opacity: .3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
isolation: isolate;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay .child {
|
||||||
|
background-position: center center;
|
||||||
|
background-size: cover;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
|
||||||
|
opacity: .4;
|
||||||
|
filter: blur(5rem);
|
||||||
|
background-image: url(https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/wano_kuni.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.overlay .child {
|
||||||
|
opacity: .35;
|
||||||
|
filter: blur(8rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
background: url("https://hianime.to/images/live-thumb.png") repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
background-clip: text;
|
color: var(--accent);
|
||||||
background: linear-gradient(to left, #ff5a7e 40%, #00aeff);
|
margin-top: 0;
|
||||||
text-align: center;
|
font-size: 1.65rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: transparent;
|
text-align: center;
|
||||||
-webkit-background-clip: text;
|
text-wrap: balance;
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #00AEDD;
|
line-height: 1.3;
|
||||||
|
text-wrap: balance;
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@@ -74,13 +158,24 @@
|
|||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#anime_girl {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
#anime_girl {
|
||||||
|
max-width: 12rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.img-wrapper {
|
.img-wrapper {
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
max-width: 12rem;
|
max-width: 12rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: 1rem auto 1rem auto;
|
margin: 1rem auto 1.5rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.api-stats-container {
|
.api-stats-container {
|
||||||
@@ -104,62 +199,79 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
1. https://api-aniwatch.onrender.com is only meant to demo the API and has rate-limiting enabled to minimise bandwidth
|
1. The hosted version of this API is only meant to demo the API and would have rate-limiting enabled to minimise bandwidth
|
||||||
consumption. It is recommended to deploy your own instance for personal use by customizing the api as you need it to be.
|
consumption. It is recommended to deploy your own instance for personal use by customizing the API as you need it to be.
|
||||||
2. This API is just an unofficial api for hianime.to and is in no other way officially related to the same.
|
2. This API is just an unofficial API for hianime.to and is in no other way officially related to the same.
|
||||||
3. The content that this api provides is not mine, nor is it hosted by me. These belong to their respective owners. This
|
3. The content that this API provides is not mine, nor is it hosted by me. These belong to their respective owners. This
|
||||||
api just demonstrates how to build an api that scrapes websites and uses their content.
|
API just demonstrates how to build an API that scrapes websites and uses their content.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h2>
|
<span class="overlay">
|
||||||
Welcome to the unofficial
|
<span class="child"></span>
|
||||||
<a href="https://hianime.to" style="text-decoration: underline;">hianime.to (formerly aniwatch.to)</a>
|
</span>
|
||||||
api
|
|
||||||
<span style="-webkit-text-fill-color: white">⚔️</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="api-stats-container">
|
<section>
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/stargazers">
|
<h2>
|
||||||
<img src="https://img.shields.io/github/stars/ghoshRitesh12/aniwatch-api?style=social" alt="Stars"
|
Welcome to the unofficial
|
||||||
decoding="async">
|
<a href="https://hianime.to/home" style="text-underline-offset: 3px;">hianime.to</a>
|
||||||
</a>
|
API
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/network/members">
|
<span style="-webkit-text-fill-color: var(--accent)">⚔️</span>
|
||||||
<img src="https://img.shields.io/github/forks/ghoshRitesh12/aniwatch-api?style=social" alt="Forks"
|
</h2>
|
||||||
decoding="async">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/issues?q=is%3Aissue+is%3Aopen+">
|
|
||||||
<img src="https://img.shields.io/github/issues/ghoshRitesh12/aniwatch-api?style=social&logo=github" alt="Issues"
|
|
||||||
decoding="async">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest">
|
|
||||||
<img
|
|
||||||
src="https://img.shields.io/github/v/release/ghoshRitesh12/aniwatch-api?display_name=release&style=social&logo=github"
|
|
||||||
alt="Version" decoding="async">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="font-size: 1.2rem; text-align: center;">
|
<div class="api-stats-container">
|
||||||
<div class="img-wrapper">
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/stargazers">
|
||||||
<img draggable="false" src="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/main/public/img/img1.gif"
|
<img src="https://img.shields.io/github/stars/ghoshRitesh12/aniwatch-api?style=social" alt="Stars"
|
||||||
alt="kawai anime girl" decoding="async" fetchpriority="high" style="user-select: none;" />
|
decoding="async">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/network/members">
|
||||||
|
<img src="https://img.shields.io/github/forks/ghoshRitesh12/aniwatch-api?style=social" alt="Forks"
|
||||||
|
decoding="async">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/issues?q=is%3Aissue+is%3Aopen+">
|
||||||
|
<img src="https://img.shields.io/github/issues/ghoshRitesh12/aniwatch-api?style=social&logo=github" alt="Issues"
|
||||||
|
decoding="async">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest">
|
||||||
|
<img
|
||||||
|
src="https://img.shields.io/github/v/release/ghoshRitesh12/aniwatch-api?display_name=release&style=social&logo=github"
|
||||||
|
alt="Version" decoding="async">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api#documentation" rel="noopener noreferer">
|
<div style="text-align: center;">
|
||||||
Visit docs for more information
|
<div class="img-wrapper">
|
||||||
</a>
|
<img draggable="false" id="anime_girl"
|
||||||
</div>
|
src="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png"
|
||||||
|
alt="kawai_anime_girl" decoding="async" fetchpriority="high" style="user-select: none;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="contributors">
|
<div style="display: flex; align-items: center; gap: .5rem; text-underline-offset: 3px;">
|
||||||
<p>Thanks to the following people for keeping this project alive and relevant.</p>
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest" rel="noopener noreferer">
|
||||||
|
Checkout latest changelogs
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/graphs/contributors"
|
<span style="font-size: .9rem;">•</span>
|
||||||
style="display: block; margin-bottom: 1rem; user-select: none;">
|
|
||||||
<img src="https://contrib.rocks/image?repo=ghoshRitesh12/aniwatch-api" alt="Contributors" decoding="async">
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api#documentation" rel="noopener noreferer">
|
||||||
</a>
|
Visit docs for more information
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contributors">
|
||||||
|
<p style="text-wrap: balance; line-height: 1.2;">Thanks to the following people for keeping this
|
||||||
|
project alive and
|
||||||
|
relevant.</p>
|
||||||
|
|
||||||
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/graphs/contributors"
|
||||||
|
style="display: block; margin-bottom: 1rem; user-select: none;">
|
||||||
|
<img src="https://contrib.rocks/image?repo=ghoshRitesh12/aniwatch-api" alt="Contributors" decoding="async">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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 { config } from "dotenv";
|
||||||
|
import { cors } from "hono/cors";
|
||||||
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
|
const allowedOrigins = process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS
|
||||||
? process.env.CORS_ALLOWED_ORIGINS.split(",")
|
? process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS.split(",")
|
||||||
: ["http://localhost:4000", "*"];
|
: ["http://localhost:4000", "*"];
|
||||||
|
|
||||||
const corsConfig = cors({
|
const corsConfig = cors({
|
||||||
origin: function (origin, callback) {
|
allowMethods: ["GET"],
|
||||||
if (!origin || allowedOrigins.includes(origin)) {
|
|
||||||
callback(null, true);
|
|
||||||
} else {
|
|
||||||
callback(new Error("Not allowed by CORS"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: ["GET"],
|
|
||||||
maxAge: 600,
|
maxAge: 600,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
optionsSuccessStatus: 200,
|
origin: allowedOrigins,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default corsConfig;
|
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 { config } from "dotenv";
|
||||||
import createHttpError from "http-errors";
|
import { rateLimiter } from "hono-rate-limiter";
|
||||||
import { rateLimit } from "express-rate-limit";
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
||||||
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
export const ratelimit = rateLimit({
|
export const ratelimit = rateLimiter({
|
||||||
windowMs: Number(process.env.WINDOWMS) || 30 * 60 * 1000,
|
windowMs: Number(process.env.ANIWATCH_API_WINDOW_MS) || 30 * 60 * 1000,
|
||||||
limit: Number(process.env.MAX) || 6,
|
limit: Number(process.env.ANIWATCH_API_MAX_REQS) || 6,
|
||||||
legacyHeaders: true,
|
|
||||||
standardHeaders: "draft-7",
|
standardHeaders: "draft-7",
|
||||||
handler: function (_, __, next) {
|
keyGenerator(c) {
|
||||||
next(
|
const { remote } = getConnInfo(c);
|
||||||
createHttpError.TooManyRequests("Too many API requests, try again later")
|
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 https from "https";
|
||||||
import morgan from "morgan";
|
|
||||||
import express from "express";
|
|
||||||
import { resolve } from "path";
|
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
|
|
||||||
import corsConfig from "./config/cors.js";
|
import corsConfig from "./config/cors.js";
|
||||||
import { ratelimit } from "./config/ratelimit.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();
|
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);
|
app.use(corsConfig);
|
||||||
|
|
||||||
// CAUTION: For personal deployments, "refrain" from having an env
|
// CAUTION: For personal deployments, "refrain" from having an env
|
||||||
// named "ANIWATCH_API_HOSTNAME". You may face rate limitting
|
// named "ANIWATCH_API_HOSTNAME". You may face rate limitting
|
||||||
// and other issues if you do.
|
// or other issues if you do.
|
||||||
const ISNT_PERSONAL_DEPLOYMENT = Boolean(process?.env?.ANIWATCH_API_HOSTNAME);
|
const ISNT_PERSONAL_DEPLOYMENT = Boolean(ANIWATCH_API_HOSTNAME);
|
||||||
if (ISNT_PERSONAL_DEPLOYMENT) {
|
if (ISNT_PERSONAL_DEPLOYMENT) {
|
||||||
app.use(ratelimit);
|
app.use(ratelimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.static(resolve("public")));
|
app.use("/", serveStatic({ root: "public" }));
|
||||||
app.get("/health", (_, res) => res.sendStatus(200));
|
app.get("/health", (c) => c.text("OK", { status: 200 }));
|
||||||
app.use("/anime", animeRouter);
|
|
||||||
|
|
||||||
app.use(notFoundHandler);
|
app.basePath(BASE_PATH).route("/hianime", hianimeRouter);
|
||||||
app.use(errorHandler);
|
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
|
// NOTE: this env is "required" for vercel deployments
|
||||||
if (!Boolean(process?.env?.IS_VERCEL_DEPLOYMENT)) {
|
if (!Boolean(process?.env?.ANIWATCH_API_VERCEL_DEPLOYMENT)) {
|
||||||
app.listen(PORT, () => {
|
serve({
|
||||||
console.log(`⚔️ api @ http://localhost:${PORT}`);
|
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
|
// NOTE: remove the `if` block below for personal deployments
|
||||||
if (ISNT_PERSONAL_DEPLOYMENT) {
|
if (ISNT_PERSONAL_DEPLOYMENT) {
|
||||||
|
const interval = 9 * 60 * 1000; // 9mins
|
||||||
|
|
||||||
// don't sleep
|
// don't sleep
|
||||||
const intervalTime = 9 * 60 * 1000; // 9mins
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
console.log("HEALTHCHECK ;)", new Date().toLocaleString());
|
console.log("aniwatch-api HEALTH_CHECK at", new Date().toISOString());
|
||||||
https
|
https
|
||||||
.get(
|
.get(`https://${ANIWATCH_API_HOSTNAME}/health`)
|
||||||
new URL("/health", `https://${process.env.ANIWATCH_API_HOSTNAME}`)
|
|
||||||
.href
|
|
||||||
)
|
|
||||||
.on("error", (err) => {
|
.on("error", (err) => {
|
||||||
console.error(err.message);
|
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);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { HiAnime } from "aniwatch";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeAnimeAboutInfo } from "../src/parsers/index.js";
|
|
||||||
|
|
||||||
test("returns information about an anime", async () => {
|
const animeId = "steinsgate-3";
|
||||||
const data = await scrapeAnimeAboutInfo("steinsgate-3");
|
|
||||||
|
// npx vitest run animeAboutInfo.test.ts
|
||||||
|
test(`GET /api/v2/hianime/anime/${animeId}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getInfo(animeId);
|
||||||
|
|
||||||
expect(data.anime.info.name).not.toEqual(null);
|
expect(data.anime.info.name).not.toEqual(null);
|
||||||
expect(data.recommendedAnimes).not.toEqual([]);
|
expect(data.recommendedAnimes).not.toEqual([]);
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeAnimeCategory } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
test("returns animes belonging to a category", async () => {
|
const category = "subbed-anime";
|
||||||
const data = await scrapeAnimeCategory("subbed-anime");
|
|
||||||
|
// npx vitest run animeCategory.test.ts
|
||||||
|
test(`GET /api/v2/hianime/category/${category}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getCategoryAnime(category);
|
||||||
|
|
||||||
expect(data.animes).not.toEqual([]);
|
expect(data.animes).not.toEqual([]);
|
||||||
expect(data.genres).not.toEqual([]);
|
expect(data.genres).not.toEqual([]);
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeAnimeEpisodeSources } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
test("returns anime episode streaming link(s)", async () => {
|
const animeEpisodeId = "steinsgate-3?ep=230";
|
||||||
const data = await scrapeAnimeEpisodeSources(
|
const server = "hd-1";
|
||||||
"steinsgate-3?ep=230",
|
const category = "sub";
|
||||||
"hd-1",
|
|
||||||
"sub"
|
// npx vitest run animeEpisodeSrcs.test.ts
|
||||||
|
test(`GET /api/v2/hianime/episode/sources?animeEpisodeId=${animeEpisodeId}&server=${server}&category=${category}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getEpisodeSources(
|
||||||
|
animeEpisodeId,
|
||||||
|
server,
|
||||||
|
category
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(data.sources).not.toEqual([]);
|
expect(data.sources).not.toEqual([]);
|
||||||
// expect(data)
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { HiAnime } from "aniwatch";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeAnimeEpisodes } from "../src/parsers/index.js";
|
|
||||||
|
|
||||||
test("returns episodes info of an anime", async () => {
|
const animeId = "steinsgate-3";
|
||||||
const data = await scrapeAnimeEpisodes("steinsgate-3");
|
|
||||||
|
// npx vitest run animeEpisodes.test.ts
|
||||||
|
test(`GET /api/v2/hianime/anime/${animeId}/episodes`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getEpisodes(animeId);
|
||||||
|
|
||||||
expect(data.totalEpisodes).not.toEqual(0);
|
expect(data.totalEpisodes).not.toEqual(0);
|
||||||
expect(data.episodes).not.toEqual([]);
|
expect(data.episodes).not.toEqual([]);
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeGenreAnime } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
test("returns animes belonging to a genre", async () => {
|
const genreName = "shounen";
|
||||||
const data = await scrapeGenreAnime("shounen", 2);
|
const page = 2;
|
||||||
|
|
||||||
|
// npx vitest run animeGenre.test.ts
|
||||||
|
test(`GET /api/v2/hianime/genre/${genreName}?page=${page}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
|
||||||
|
const data = await hianime.getGenreAnime(genreName, page);
|
||||||
|
|
||||||
expect(data.animes).not.toEqual([]);
|
expect(data.animes).not.toEqual([]);
|
||||||
expect(data.genres).not.toEqual([]);
|
expect(data.genres).not.toEqual([]);
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeProducerAnimes } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
test("returns animes produced by a producer", async () => {
|
const producerName = "toei-animation";
|
||||||
const data = await scrapeProducerAnimes("toei-animation", 2);
|
const page = 2;
|
||||||
|
|
||||||
|
// npx vitest run animeProducer.test.ts
|
||||||
|
test(`GET /api/v2/hianime/producer/${producerName}?page=${page}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getProducerAnimes(producerName, page);
|
||||||
|
|
||||||
expect(data.animes).not.toEqual([]);
|
expect(data.animes).not.toEqual([]);
|
||||||
expect(data.topAiringAnimes).not.toEqual([]);
|
expect(data.topAiringAnimes).not.toEqual([]);
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeAnimeSearch } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
test("returns animes related to search query", async () => {
|
const query = "monster";
|
||||||
const data = await scrapeAnimeSearch("monster", 2);
|
const page = 1;
|
||||||
|
const filter: HiAnime.SearchFilters = {
|
||||||
|
genres: "seinen,psychological",
|
||||||
|
};
|
||||||
|
|
||||||
|
// npx vitest run animeSearch.test.ts
|
||||||
|
test(`GET /api/v2/hianime/search?q=${query}&page=${page}&genres=${filter.genres}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.search(query, page, filter);
|
||||||
|
|
||||||
expect(data.animes).not.toEqual([]);
|
expect(data.animes).not.toEqual([]);
|
||||||
expect(data.mostPopularAnimes).not.toEqual([]);
|
expect(data.mostPopularAnimes).not.toEqual([]);
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeAnimeSearchSuggestion } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
test("returns animes search suggestions related to search query", async () => {
|
const query = "one piece";
|
||||||
const data = await scrapeAnimeSearchSuggestion("one piece");
|
|
||||||
|
// npx vitest run animeSearchSuggestion.test.ts
|
||||||
|
test(`GET /api/v2/hianime/search/suggestion?q=${query}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.searchSuggestions(query);
|
||||||
|
|
||||||
expect(data.suggestions).not.toEqual([]);
|
expect(data.suggestions).not.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeEpisodeServers } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
test("returns episode source servers", async () => {
|
const animeEpisodeId = "steinsgate-0-92?ep=2055";
|
||||||
const data = await scrapeEpisodeServers("steinsgate-0-92?ep=2055");
|
|
||||||
|
// npx vitest run episodeServers.test.ts
|
||||||
|
test(`GET /api/v2/hianime/episode/servers?animeEpisodeId=${animeEpisodeId}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getEpisodeServers(animeEpisodeId);
|
||||||
|
|
||||||
expect(data.episodeId).not.toEqual(null);
|
expect(data.episodeId).not.toEqual(null);
|
||||||
expect(data.episodeNo).not.toEqual(0);
|
expect(data.episodeNo).not.toEqual(0);
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeEstimatedSchedule } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
function padZero(num: number) {
|
const padZero = (num: number) => (num < 10 ? `0${num}` : num.toString());
|
||||||
return num < 10 ? `0${num}` : num.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
test("returns estimated schedule anime release", async () => {
|
const d = new Date();
|
||||||
const d = new Date();
|
const date = `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(
|
||||||
const data = await scrapeEstimatedSchedule(
|
d.getDate()
|
||||||
`${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(d.getDate())}`
|
)}`;
|
||||||
);
|
|
||||||
|
// npx vitest run estimatedSchedule.test.ts
|
||||||
|
test(`GET /api/v2/hianime/schedule?date=${date}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getEstimatedSchedule(date);
|
||||||
|
|
||||||
expect(data.scheduledAnimes).not.toEqual([]);
|
expect(data.scheduledAnimes).not.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { scrapeHomePage } from "../src/parsers/index.js";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
test("returns anime information present in homepage", async () => {
|
test("GET /api/v2/hianime/home", async () => {
|
||||||
const data = await scrapeHomePage();
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getHomePage();
|
||||||
|
|
||||||
expect(data.spotlightAnimes).not.toEqual([]);
|
expect(data.spotlightAnimes).not.toEqual([]);
|
||||||
expect(data.trendingAnimes).not.toEqual([]);
|
expect(data.trendingAnimes).not.toEqual([]);
|
||||||
|
|||||||
+7
-15
@@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"target": "ES2022",
|
"target": "ES2023",
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
//
|
//
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./",
|
"rootDir": "./",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
@@ -21,18 +23,8 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strictFunctionTypes": true,
|
"strictFunctionTypes": true,
|
||||||
"lib": [
|
"lib": ["ES2023"]
|
||||||
"ES2022"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["./src", "./api"],
|
||||||
"./src",
|
"exclude": ["node_modules"]
|
||||||
"./api"
|
}
|
||||||
],
|
|
||||||
"ts-node": {
|
|
||||||
"esm": true
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user