├── .gitignore ├── LICENSE ├── README.md ├── dist ├── index.js └── source │ ├── kuramanime │ ├── kuramanime.js │ └── parser.js │ ├── kuronime │ ├── decryptor.js │ ├── kuronime.js │ └── parser.js │ ├── nanime │ ├── nanime.js │ └── parser.js │ ├── otakudesu │ ├── otakudesu.js │ └── parser.js │ └── utils │ └── types.js ├── package.json ├── src ├── index.ts └── source │ ├── kuramanime │ ├── kuramanime.ts │ └── parser.ts │ ├── kuronime │ ├── decryptor.js │ ├── kuronime.ts │ └── parser.ts │ ├── nanime │ ├── nanime.ts │ └── parser.ts │ ├── otakudesu │ ├── otakudesu.ts │ └── parser.ts │ └── utils │ └── types.ts ├── tsconfig.json └── vercel.json /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | yarn.lock 4 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Miukyo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 | 6 |

Aniyoi API

7 | 8 |

9 | RESTFUL API Streaming Anime Subtitle Indonesia
10 | API ini masih dalam pembuatan jadi mungkin akan rawan error, Jika kalian tertarik untuk membantu bisa dm saya di discord miukyo 11 |
12 |

13 | 14 | # !!HIATUS!! GA TAU BAKAL LANJUT KAPAN, INFO LEBIH LANJUT BISA DM 15 | 16 | 17 |

Table of Contents

18 | 19 | - [Installation](#installation) 20 | - [Local](#local) 21 | - [List Source](#list-source) 22 | - [Routes](#routes) 23 | - [Get Recent Episodes](#get-recent-episodes) 24 | - [Get Popular Anime](#get-popular-anime) 25 | - [Get Anime Search](#get-anime-search) 26 | - [Get Genre List](#get-genre-list) 27 | - [Get Genre](#get-genre) 28 | - [Get Season List](#get-season-list) 29 | - [Get Season](#get-season) 30 | - [Get Anime Details](#get-anime-details) 31 | - [Get Streaming URLs](#get-streaming-urls) 32 | 33 | ## Installation 34 | 35 | ### Local 36 | 37 | Run command berikut untuk mengclone repo ini, dan menginstall dependencies: 38 | 39 | ```sh 40 | git clone https://github.com/miukyo/aniyoi-api.git 41 | cd aniyoi-api 42 | npm install #atau yarn install 43 | ``` 44 | 45 | Start server dengan command berikut: 46 | 47 | ```sh 48 | npm start #atau yarn start 49 | ``` 50 | 51 | Server akan berjalan di http://localhost:3001 52 | 53 | ## List Source 54 | 55 | Beberapa source mungkin tidak support dengan beberapa fitur. Dikarenakan kurangnya informasi pada website asli. 56 | 57 | | Source | Fitur yang bermasalah | 58 | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 59 | | Kuronime | | 60 | | Kuramanime | | 61 | | Nanime | | 62 | | Otaku Desu Cancelled | | 63 | 64 | 65 | Jika kalian tau website anime subtitle anime yang bagus dan detail mohon kontak saya. Akan saya include di sini, Terima kasih! 66 | 67 | ## Routes 68 | 69 | Contoh dibawah menggunakan [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), kamu juga bisa menggunakan library http lainnya. 70 | 71 | ### Get Recent Episodes 72 | 73 | | Parameter | Description | 74 | | ------------ | ----------------------------------------- | 75 | | `page` (int) | pilih page dari maximum page. Default : 1 | 76 | 77 | ```js 78 | fetch("localhost:3001/{source}/recent") 79 | .then((response) => response.json()) 80 | .then((animelist) => console.log(animelist)); 81 | ``` 82 | 83 | Output >> 84 | 85 | ```ts 86 | { 87 | "list": [ 88 | { 89 | "slug": string, 90 | "title": string, 91 | "episode?": number, //(optional) 92 | "cover": string, 93 | "url": string, 94 | }, 95 | {...} 96 | ], 97 | "maxPage": number, 98 | "page": number, 99 | } 100 | ``` 101 | 102 | ### Get Popular Anime 103 | 104 | | Parameter | Description | 105 | | ------------ | ----------------------------------------- | 106 | | `page` (int) | pilih page dari maximum page. Default : 1 | 107 | 108 | ```js 109 | fetch("localhost:3001/{source}/popular") 110 | .then((response) => response.json()) 111 | .then((animelist) => console.log(animelist)); 112 | ``` 113 | 114 | Output >> 115 | 116 | ```ts 117 | { 118 | "list": [ 119 | { 120 | "slug": string, 121 | "title": string, 122 | "cover": string, 123 | "url": string, 124 | }, 125 | {...} 126 | ], 127 | "maxPage": number, 128 | "page": number, 129 | } 130 | ``` 131 | 132 | ### Get Anime Search 133 | 134 | | Parameter | Description | 135 | | ---------------- | ----------------------------------------- | 136 | | `query` (string) | nama anime yang ingin dicari | 137 | | `page` (int) | pilih page dari maximum page. Default : 1 | 138 | 139 | ```js 140 | fetch("localhost:3001/{source}/search") 141 | .then((response) => response.json()) 142 | .then((animelist) => console.log(animelist)); 143 | ``` 144 | 145 | Output >> 146 | 147 | ```ts 148 | { 149 | "list": [ 150 | { 151 | "slug": string, 152 | "title": string, 153 | "cover": string, 154 | "url": string, 155 | }, 156 | {...} 157 | ], 158 | "maxPage": number, 159 | "page": number, 160 | } 161 | ``` 162 | 163 | ### Get Genre List 164 | 165 | | Parameter | Description | 166 | | --------- | ----------- | 167 | | - | - | 168 | 169 | ```js 170 | fetch("localhost:3001/{source}/genre") 171 | .then((response) => response.json()) 172 | .then((animelist) => console.log(animelist)); 173 | ``` 174 | 175 | Output >> 176 | 177 | 178 | ```ts 179 | [ 180 | { 181 | "slug": string, 182 | "title": string, 183 | "url": string, 184 | }, 185 | {...}, 186 | ] 187 | ``` 188 | 189 | ### Get Genre 190 | 191 | | Parameter | Description | 192 | | ---------------------- | ------------------------------------------------------- | 193 | | `page` (int) | pilih page dari maximum page. Default : 1 | 194 | | `:genre-slug` (string) | genre slug dapat didapatkan dalam respon **Genre List** | 195 | 196 | 197 | ```js 198 | fetch("localhost:3001/{source}/genre/{genre-slug}") 199 | .then((response) => response.json()) 200 | .then((animelist) => console.log(animelist)); 201 | ``` 202 | 203 | Output >> 204 | 205 | ```ts 206 | { 207 | "list": [ 208 | { 209 | "slug": string, 210 | "title": string, 211 | "cover": string, 212 | "url": string, 213 | }, 214 | {...} 215 | ], 216 | "maxPage": number, 217 | "page": number, 218 | } 219 | ``` 220 | 221 | ### Get Season List 222 | 223 | | Parameter | Description | 224 | | --------- | ----------- | 225 | | - | - | 226 | 227 | ```js 228 | fetch("localhost:3001/{source}/season") 229 | .then((response) => response.json()) 230 | .then((animelist) => console.log(animelist)); 231 | ``` 232 | 233 | Output >> 234 | 235 | 236 | ```ts 237 | [ 238 | { 239 | "slug": string, 240 | "title": string, 241 | "url": string, 242 | }, 243 | {...}, 244 | ] 245 | ``` 246 | 247 | ### Get Season 248 | 249 | | Parameter | Description | 250 | | ----------------------- | --------------------------------------------------------- | 251 | | `page` (int) | pilih page dari maximum page. Default : 1 | 252 | | `:season-slug` (string) | season slug dapat didapatkan dalam respon **Season List** | 253 | 254 | ```js 255 | fetch("localhost:3001/{source}/season/{season-slug}") 256 | .then((response) => response.json()) 257 | .then((animelist) => console.log(animelist)); 258 | ``` 259 | 260 | Output >> 261 | 262 | ```ts 263 | { 264 | "list": [ 265 | { 266 | "slug": string, 267 | "title": string, 268 | "cover": string, 269 | "url": string, 270 | }, 271 | {...} 272 | ], 273 | "maxPage": number, 274 | "page": number, 275 | } 276 | ``` 277 | 278 | ### Get Anime Details 279 | 280 | | Parameter | Description | 281 | | ---------------------- | ------------------------------------------------------------- | 282 | | `:anime-slug` (string) | anime slug dapat didapatkan dalam respon list seperti diatas. | 283 | 284 | ```js 285 | fetch("localhost:3001/{source}/anime/{anime-slug}") 286 | .then((response) => response.json()) 287 | .then((animelist) => console.log(animelist)); 288 | ``` 289 | 290 | Output >> 291 | 292 | ```ts 293 | { 294 | "slug": string, 295 | "title": string, 296 | "titleAlt?": string, //(optional) 297 | "synopsis?": string, //(optional | beberapa ada yang tidak memiliki sinopsis) 298 | "episodeTotal": number, //(anime ongoing di kuronime akan merespon "0") 299 | "episode": number, 300 | "season?": string, //(optional) 301 | "genre?": string[], //(optional) 302 | "cover": string, 303 | "url": string, 304 | } 305 | ``` 306 | 307 | ### Get Streaming URLs 308 | 309 | 310 | | Parameter | Description | 311 | | ---------------------- | ---------------------------------------------------------------- | 312 | | `:anime-slug` (string) | anime slug dapat didapatkan dalam respon list seperti diatas. | 313 | | `:anime-episode` (int) | pilih episode anime yang tersedia dalam respon **Anime Details** | 314 | 315 | 316 | ```js 317 | fetch("localhost:3001/{source}/anime/{anime-slug}/{anime-episode}") 318 | .then((response) => response.json()) 319 | .then((animelist) => console.log(animelist)); 320 | ``` 321 | 322 | Output >> 323 | 324 | ```ts 325 | { 326 | "episode": number, 327 | "video": [ 328 | { 329 | "quality": string, 330 | "url": string, 331 | }, 332 | {...}, 333 | ] 334 | } 335 | ``` 336 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.app = void 0; 7 | const express_1 = __importDefault(require("express")); 8 | const cors_1 = __importDefault(require("cors")); 9 | const kuramanime_1 = __importDefault(require("./source/kuramanime/kuramanime")); 10 | const nanime_1 = __importDefault(require("./source/nanime/nanime")); 11 | const kuronime_1 = __importDefault(require("./source/kuronime/kuronime")); 12 | const axios_1 = __importDefault(require("axios")); 13 | exports.app = (0, express_1.default)(); 14 | axios_1.default.defaults.validateStatus = () => true; 15 | axios_1.default.defaults.headers.common["User-Agent"] = 16 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54"; 17 | exports.app.use((0, cors_1.default)()); 18 | exports.app.get("/", async (req, res) => { 19 | res.send("ANIYOI API IS UP 🚀"); 20 | }); 21 | exports.app.use("/kuramanime", kuramanime_1.default); 22 | exports.app.use("/nanime", nanime_1.default); 23 | exports.app.use("/kuronime", kuronime_1.default); 24 | // app.use("/otakudesu", otakudesu); //Url page streaming memakai slug yang berbeda 25 | exports.app.listen(process.env.PORT || 3001, () => { 26 | console.warn("\nReady 🚀"); 27 | }); 28 | -------------------------------------------------------------------------------- /dist/source/kuramanime/kuramanime.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const parser_1 = require("./parser"); 8 | const app = express_1.default.Router(); 9 | app.get("/", async (req, res) => { 10 | return res.status(200).send("Kuramanime Server is ready 🚀"); 11 | }); 12 | app.get("/recent", async (req, res) => { 13 | try { 14 | const { page } = req.query; 15 | const data = await (0, parser_1.recentRelease)(page); 16 | return res.status(200).json(data); 17 | } 18 | catch (err) { 19 | return res.status(500).json({ 20 | error: "Internal Error", 21 | message: err.toString(), 22 | }); 23 | } 24 | }); 25 | app.get("/search", async (req, res) => { 26 | try { 27 | const { page, query } = req.query; 28 | const data = await (0, parser_1.search)(query, page); 29 | return res.status(200).json(data); 30 | } 31 | catch (err) { 32 | return res.status(500).json({ 33 | error: "Internal Error", 34 | message: err.toString(), 35 | }); 36 | } 37 | }); 38 | app.get("/popular", async (req, res) => { 39 | try { 40 | const { page } = req.query; 41 | const data = await (0, parser_1.popular)(page); 42 | return res.status(200).json(data); 43 | } 44 | catch (err) { 45 | return res.status(500).json({ 46 | error: "Internal Error", 47 | message: err.toString(), 48 | }); 49 | } 50 | }); 51 | app.get("/genre", async (req, res) => { 52 | try { 53 | const { page } = req.query; 54 | const data = await (0, parser_1.genreList)(page); 55 | return res.status(200).json(data); 56 | } 57 | catch (err) { 58 | return res.status(500).json({ 59 | error: "Internal Error", 60 | message: err.toString(), 61 | }); 62 | } 63 | }); 64 | app.get("/genre/:genre", async (req, res) => { 65 | try { 66 | const genreType = req.params.genre; 67 | const { page } = req.query; 68 | const data = await (0, parser_1.genre)(genreType, page); 69 | return res.status(200).json(data); 70 | } 71 | catch (err) { 72 | return res.status(500).json({ 73 | error: "Internal Error", 74 | message: err.toString(), 75 | }); 76 | } 77 | }); 78 | app.get("/season", async (req, res) => { 79 | try { 80 | const { page } = req.query; 81 | const data = await (0, parser_1.seasonList)(page); 82 | return res.status(200).json(data); 83 | } 84 | catch (err) { 85 | return res.status(500).json({ 86 | error: "Internal Error", 87 | message: err.toString(), 88 | }); 89 | } 90 | }); 91 | app.get("/season/:season", async (req, res) => { 92 | try { 93 | const seasonYear = req.params.season; 94 | const { page } = req.query; 95 | const data = await (0, parser_1.season)(seasonYear, page); 96 | return res.status(200).json(data); 97 | } 98 | catch (err) { 99 | return res.status(500).json({ 100 | error: "Internal Error", 101 | message: err.toString(), 102 | }); 103 | } 104 | }); 105 | app.get("/anime/:animeSlug", async (req, res) => { 106 | try { 107 | const slug = req.params.animeSlug; 108 | const data = await (0, parser_1.anime)(slug); 109 | return res.status(200).json(data); 110 | } 111 | catch (err) { 112 | return res.status(500).json({ 113 | error: "Internal Error", 114 | message: err.toString(), 115 | }); 116 | } 117 | }); 118 | app.get("/anime/:animeId/:episodeId", async (req, res) => { 119 | try { 120 | const id = req.params.animeId; 121 | const ep = req.params.episodeId; 122 | const data = await (0, parser_1.animeVideoSource)(id, ep); 123 | return res.status(200).json(data); 124 | } 125 | catch (err) { 126 | return res.status(500).json({ 127 | error: "Internal Error", 128 | message: err.toString(), 129 | }); 130 | } 131 | }); 132 | exports.default = app; 133 | -------------------------------------------------------------------------------- /dist/source/kuramanime/parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.animeVideoSource = exports.anime = exports.season = exports.seasonList = exports.genre = exports.genreList = exports.popular = exports.search = exports.recentRelease = void 0; 7 | const axios_1 = __importDefault(require("axios")); 8 | const cheerio_1 = __importDefault(require("cheerio")); 9 | const BASEURL = "https://kuramanime.net"; 10 | const recentRelease = async (page = 1) => { 11 | let list = []; 12 | try { 13 | const base = await axios_1.default.get(`${BASEURL}/anime/ongoing?order_by=latest&page=${page}`); 14 | const $ = cheerio_1.default.load(base.data); 15 | if (!$("#animeList .product__item").html()) { 16 | throw new Error("Page not found, you may request more than the maximum page"); 17 | } 18 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 19 | .last() 20 | .html(); 21 | $("#animeList .product__item").each((i, el) => { 22 | return list.push({ 23 | slug: $(el).find("a").attr("href")?.split("/")[5], 24 | title: $(el).find(".product__item__text>h5>a").text(), 25 | episode: ~~$(el) 26 | .find(".ep span") 27 | .text() 28 | .replace("Ep", "") 29 | .split("/")[0] 30 | .trim(), 31 | cover: $(el).attr("data-setbg"), 32 | url: $(el).find("a").attr("href"), 33 | }); 34 | }); 35 | return { 36 | page: ~~page, 37 | maxPage: maxPage, 38 | list: list, 39 | }; 40 | } 41 | catch (err) { 42 | throw err; 43 | } 44 | }; 45 | exports.recentRelease = recentRelease; 46 | const search = async (query, page = 1) => { 47 | let list = []; 48 | try { 49 | const base = await axios_1.default.get(`${BASEURL}/anime?search=${query}&order_by=latest&page=${page}`); 50 | const $ = cheerio_1.default.load(base.data); 51 | if (!$("#animeList .product__item").html()) { 52 | throw new Error("Anime not found,or you may request more than the maximum page"); 53 | } 54 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 55 | .last() 56 | .html(); 57 | $("#animeList .product__item").each((i, el) => { 58 | list.push({ 59 | slug: $(el).find("a").attr("href")?.split("/")[5], 60 | title: $(el).find(".product__item__text>h5>a").text(), 61 | cover: $(el).attr("data-setbg"), 62 | url: $(el).find("a").attr("href"), 63 | }); 64 | }); 65 | if (list.length == 0) { 66 | throw new Error("Anime not found"); 67 | } 68 | return { 69 | page: ~~page, 70 | maxPage: maxPage, 71 | list: list, 72 | }; 73 | } 74 | catch (err) { 75 | throw err; 76 | } 77 | }; 78 | exports.search = search; 79 | const popular = async (page = 1) => { 80 | let list = []; 81 | try { 82 | const base = await axios_1.default.get(`${BASEURL}/anime/ongoing?order_by=popular&page=${page}`); 83 | const $ = cheerio_1.default.load(base.data); 84 | if (!$("#animeList .product__item").html()) { 85 | throw new Error("Page not found, you may request more than the maximum page"); 86 | } 87 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 88 | .last() 89 | .html(); 90 | $("#animeList .product__item").each((i, el) => { 91 | list.push({ 92 | slug: $(el).find("a").attr("href")?.split("/")[5], 93 | title: $(el).find(".product__item__text>h5>a").text(), 94 | episode: ~~$(el) 95 | .find(".ep span") 96 | .text() 97 | .replace("Ep", "") 98 | .split("/")[0] 99 | .trim(), 100 | cover: $(el).attr("data-setbg"), 101 | url: $(el).find("a").attr("href"), 102 | }); 103 | }); 104 | return { 105 | page: ~~page, 106 | maxPage: maxPage, 107 | list: list, 108 | }; 109 | } 110 | catch (err) { 111 | throw err; 112 | } 113 | }; 114 | exports.popular = popular; 115 | const genreList = async (page = 1) => { 116 | let list = []; 117 | try { 118 | const base = await axios_1.default.get(`${BASEURL}/properties/genre?genre_type=all&page=${page}`); 119 | const $ = cheerio_1.default.load(base.data); 120 | if (!$("#animeList .kuramanime__genres li").html()) { 121 | throw new Error("Page not found"); 122 | } 123 | $("#animeList .kuramanime__genres li").each((i, el) => { 124 | list.push({ 125 | slug: $(el).find("a").attr("href")?.split("/")[5]?.split("?")[0], 126 | title: $(el).find("a").text(), 127 | url: $(el).find("a").attr("href")?.trim(), 128 | }); 129 | }); 130 | return list; 131 | } 132 | catch (err) { 133 | throw err; 134 | } 135 | }; 136 | exports.genreList = genreList; 137 | const genre = async (genre, page = 1) => { 138 | let list = []; 139 | try { 140 | const base = await axios_1.default.get(`${BASEURL}/properties/genre/${genre}?order_by=latest&page=${page}`); 141 | const $ = cheerio_1.default.load(base.data); 142 | if (!$("#animeList .product__item").html()) { 143 | throw new Error("Genre not found, or you may request more than the maximum page"); 144 | } 145 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 146 | .last() 147 | .html(); 148 | $("#animeList .product__item").each((i, el) => { 149 | list.push({ 150 | slug: $(el).find("a").attr("href")?.split("/")[5], 151 | title: $(el).find(".product__item__text>h5>a").text(), 152 | cover: $(el).attr("data-setbg"), 153 | url: $(el).find("a").attr("href"), 154 | }); 155 | }); 156 | return { 157 | page: ~~page, 158 | maxPage: maxPage, 159 | list: list, 160 | }; 161 | } 162 | catch (err) { 163 | throw err; 164 | } 165 | }; 166 | exports.genre = genre; 167 | const seasonList = async (page = 1) => { 168 | let list = []; 169 | try { 170 | const base = await axios_1.default.get(`${BASEURL}/properties/season?page=${page}`); 171 | const $ = cheerio_1.default.load(base.data); 172 | if (!$("#animeList .kuramanime__genres li").html()) { 173 | throw new Error("Page not found"); 174 | } 175 | $("#animeList .kuramanime__genres li").each((i, el) => { 176 | list.push({ 177 | slug: $(el).find("a").attr("href")?.split("/")[5].split("?")[0], 178 | title: $(el).find("a").text(), 179 | url: $(el).find("a").attr("href")?.trim(), 180 | }); 181 | }); 182 | return list; 183 | } 184 | catch (err) { 185 | throw err; 186 | } 187 | }; 188 | exports.seasonList = seasonList; 189 | const season = async (season, page = 1) => { 190 | let list = []; 191 | try { 192 | const base = await axios_1.default.get(`${BASEURL}/properties/season/${season}?order_by=latest&page=${page}`); 193 | const $ = cheerio_1.default.load(base.data); 194 | if (!$("#animeList .product__item").html()) { 195 | throw new Error("Season not found, or you may request more than the maximum page"); 196 | } 197 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 198 | .last() 199 | .html(); 200 | $("#animeList .product__item").each((i, el) => { 201 | list.push({ 202 | slug: $(el).find("a").attr("href")?.split("/")[5], 203 | title: $(el).find(".product__item__text>h5>a").text(), 204 | cover: $(el).attr("data-setbg"), 205 | url: $(el).find("a").attr("href"), 206 | }); 207 | }); 208 | return { 209 | page: ~~page, 210 | maxPage: maxPage, 211 | list: list, 212 | }; 213 | } 214 | catch (err) { 215 | throw err; 216 | } 217 | }; 218 | exports.season = season; 219 | const anime = async (slug) => { 220 | try { 221 | const base = await axios_1.default.get(`${BASEURL}/anime/${slug}`); 222 | const $ = cheerio_1.default.load(base.data); 223 | if (!$(".anime__details__widget ul").html()) { 224 | throw new Error("Anime not found"); 225 | } 226 | let genre = []; 227 | $(".anime__details__widget ul") 228 | .find('span:contains("Genre:"),span:contains("Tema:")') 229 | .parent() 230 | .find("a") 231 | .each((i, el) => { 232 | genre.push($(el).text().replace(",", "").trim()); 233 | }); 234 | let episode = []; 235 | let $$ = cheerio_1.default.load($("#episodeListsSection > #episodeLists").attr("data-content")); 236 | $$("a").each((i, el) => { 237 | episode.push($(el).text().trim()); 238 | }); 239 | return { 240 | slug: $('meta[property="og:url"]').attr("content")?.split("/")[5], 241 | title: $(".anime__details__title > h3").text(), 242 | titleAlt: $(".anime__details__title > span").text(), 243 | synopsis: $("#synopsisField").text(), 244 | episodeTotal: ~~$(".anime__details__widget ul") 245 | .find('span:contains("Total Eps:")') 246 | .next() 247 | .text(), 248 | episode: episode.length, 249 | season: $(".anime__details__widget ul") 250 | .find('span:contains("Musim:")') 251 | .next() 252 | .text(), 253 | genre: genre, 254 | cover: $(".set-bg").attr("data-setbg"), 255 | url: $('meta[property="og:url"]').attr("content"), 256 | }; 257 | } 258 | catch (err) { 259 | throw err; 260 | } 261 | }; 262 | exports.anime = anime; 263 | const animeVideoSource = async (slug, ep) => { 264 | try { 265 | const url = await axios_1.default.head(`${BASEURL}/anime/${slug}`); 266 | const actualUrl = await url.request.res.responseUrl; 267 | const base = await axios_1.default.get(actualUrl + `/episode/${ep}?activate_stream=1`); 268 | const $ = cheerio_1.default.load(base.data); 269 | let videoSource = []; 270 | if (!$("#animeVideoPlayer video").html()) { 271 | throw new Error("Episode not found"); 272 | } 273 | $("#animeVideoPlayer video") 274 | .find("source") 275 | .each((i, el) => { 276 | videoSource.push({ 277 | quality: $(el).attr("size") + "p", 278 | url: $(el).attr("src"), 279 | }); 280 | }); 281 | return { 282 | episode: ~~ep, 283 | video: videoSource, 284 | }; 285 | } 286 | catch (err) { 287 | throw err; 288 | } 289 | }; 290 | exports.animeVideoSource = animeVideoSource; 291 | -------------------------------------------------------------------------------- /dist/source/kuronime/decryptor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var Chara = [ 4 | "", 5 | "split", 6 | "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/", 7 | "slice", 8 | "indexOf", 9 | "", 10 | "", 11 | ".", 12 | "pow", 13 | "reduce", 14 | "reverse", 15 | "0", 16 | ]; 17 | function charDec(d, e, f) { 18 | var g = Chara[2][Chara[1]](Chara[0]); 19 | var h = g[Chara[3]](0, e); 20 | var i = g[Chara[3]](0, f); 21 | var j = d[Chara[1]](Chara[0])[Chara[10]]()[Chara[9]](function (a, b, c) { 22 | if (h[Chara[4]](b) !== -1) 23 | return (a += h[Chara[4]](b) * Math[Chara[8]](e, c)); 24 | }, 0); 25 | var k = Chara[0]; 26 | while (j > 0) { 27 | k = i[j % f] + k; 28 | j = (j - (j % f)) / f; 29 | } 30 | return k || Chara[11]; 31 | } 32 | function decy(h, u, n, t, e, r) { 33 | r = ""; 34 | for (var i = 0, len = h.length; i < len; i++) { 35 | var s = ""; 36 | while (h[i] !== n[e]) { 37 | s += h[i]; 38 | i++; 39 | } 40 | for (var j = 0; j < n.length; j++) 41 | s = s.replace(new RegExp(n[j], "g"), j); 42 | r += String.fromCharCode(charDec(s, e, 10) - t); 43 | } 44 | return decodeURIComponent(escape(r)); 45 | } 46 | exports.default = decy; 47 | -------------------------------------------------------------------------------- /dist/source/kuronime/kuronime.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const parser_1 = require("./parser"); 8 | const app = express_1.default.Router(); 9 | app.get("/", async (req, res) => { 10 | return res.status(200).send("Kuronime Server is ready 🚀"); 11 | }); 12 | app.get("/recent", async (req, res) => { 13 | try { 14 | const { page } = req.query; 15 | const data = await (0, parser_1.recentRelease)(page); 16 | return res.status(200).json(data); 17 | } 18 | catch (err) { 19 | return res.status(500).json({ 20 | error: "Internal Error", 21 | message: err.toString(), 22 | }); 23 | } 24 | }); 25 | app.get("/search", async (req, res) => { 26 | try { 27 | const { page, query } = req.query; 28 | const data = await (0, parser_1.search)(query, page); 29 | return res.status(200).json(data); 30 | } 31 | catch (err) { 32 | return res.status(500).json({ 33 | error: "Internal Error", 34 | message: err.toString(), 35 | }); 36 | } 37 | }); 38 | app.get("/popular", async (req, res) => { 39 | try { 40 | const { page } = req.query; 41 | const data = await (0, parser_1.popular)(page); 42 | return res.status(200).json(data); 43 | } 44 | catch (err) { 45 | return res.status(500).json({ 46 | error: "Internal Error", 47 | message: err.toString(), 48 | }); 49 | } 50 | }); 51 | app.get("/genre", async (req, res) => { 52 | try { 53 | const { page } = req.query; 54 | const data = await (0, parser_1.genreList)(page); 55 | return res.status(200).json(data); 56 | } 57 | catch (err) { 58 | return res.status(500).json({ 59 | error: "Internal Error", 60 | message: err.toString(), 61 | }); 62 | } 63 | }); 64 | app.get("/genre/:genre", async (req, res) => { 65 | try { 66 | const genreType = req.params.genre; 67 | const { page } = req.query; 68 | const data = await (0, parser_1.genre)(genreType, page); 69 | return res.status(200).json(data); 70 | } 71 | catch (err) { 72 | return res.status(500).json({ 73 | error: "Internal Error", 74 | message: err.toString(), 75 | }); 76 | } 77 | }); 78 | app.get("/season", async (req, res) => { 79 | try { 80 | const { page } = req.query; 81 | const data = await (0, parser_1.seasonList)(page); 82 | return res.status(200).json(data); 83 | } 84 | catch (err) { 85 | return res.status(500).json({ 86 | error: "Internal Error", 87 | message: err.toString(), 88 | }); 89 | } 90 | }); 91 | app.get("/season/:season", async (req, res) => { 92 | try { 93 | const seasonYear = req.params.season; 94 | const { page } = req.query; 95 | const data = await (0, parser_1.season)(seasonYear, page); 96 | return res.status(200).json(data); 97 | } 98 | catch (err) { 99 | return res.status(500).json({ 100 | error: "Internal Error", 101 | message: err.toString(), 102 | }); 103 | } 104 | }); 105 | app.get("/anime/:animeSlug", async (req, res) => { 106 | try { 107 | const slug = req.params.animeSlug; 108 | const data = await (0, parser_1.anime)(slug); 109 | return res.status(200).json(data); 110 | } 111 | catch (err) { 112 | return res.status(500).json({ 113 | error: "Internal Error", 114 | message: err.toString(), 115 | }); 116 | } 117 | }); 118 | app.get("/anime/:animeId/:episodeId", async (req, res) => { 119 | try { 120 | const id = req.params.animeId; 121 | const ep = req.params.episodeId; 122 | const data = await (0, parser_1.animeVideoSource)(id, ep); 123 | return res.status(200).json(data); 124 | } 125 | catch (err) { 126 | return res.status(500).json({ 127 | error: "Internal Error", 128 | message: err.toString(), 129 | }); 130 | } 131 | }); 132 | exports.default = app; 133 | -------------------------------------------------------------------------------- /dist/source/kuronime/parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.animeVideoSource = exports.anime = exports.season = exports.seasonList = exports.genre = exports.genreList = exports.popular = exports.search = exports.recentRelease = void 0; 7 | const axios_1 = __importDefault(require("axios")); 8 | const cheerio_1 = __importDefault(require("cheerio")); 9 | const decryptor_js_1 = __importDefault(require("./decryptor.js")); 10 | const BASEURL = "https://45.12.2.2"; 11 | const recentRelease = async (page = 1) => { 12 | let list = []; 13 | try { 14 | const base = await axios_1.default.get(`${BASEURL}/page/${page}`); 15 | const $ = cheerio_1.default.load(base.data); 16 | if (!$(".postbody").html()) { 17 | throw new Error("Page not found"); 18 | } 19 | let maxPage = ~~$(".postbody>.bixbox") 20 | .first() 21 | .find(".pagination a:not(.prev,.next)") 22 | .last() 23 | .text() 24 | .replace(",", ""); 25 | $(".postbody>.bixbox") 26 | .first() 27 | .find(".excstf article") 28 | .each((i, el) => { 29 | return list.push({ 30 | slug: $(el) 31 | .find("a") 32 | .attr("href") 33 | ?.split("/")[3] 34 | .replace(/\b(?:nonton-|-episode-[a-zA-Z0-9_]*)\b/gi, ""), 35 | title: $(el).find(".bsuxtt").text(), 36 | episode: ~~$(el).find(".bt .ep").text().replace("Episode", "").trim(), 37 | cover: $(el).find(".limit img[itemprop]").attr("data-src")?.split("?")[0] + "?resize=141,200", 38 | url: $(el) 39 | .find("a") 40 | .attr("href") 41 | ?.replace(/\b(?:nonton-|-episode-[a-zA-Z0-9_]*)\b/gi, ""), 42 | }); 43 | }); 44 | return { 45 | page: ~~page, 46 | maxPage: maxPage, 47 | list: list, 48 | }; 49 | } 50 | catch (err) { 51 | throw err; 52 | } 53 | }; 54 | exports.recentRelease = recentRelease; 55 | const search = async (query, page = 1) => { 56 | let list = []; 57 | try { 58 | const base = await axios_1.default.get(`${BASEURL}/anime/page/${page}/?title=${query}&order=update`); 59 | const $ = cheerio_1.default.load(base.data); 60 | if (!$(".postbody").html()) { 61 | throw new Error("Page not found"); 62 | } 63 | let maxPage = ~~$(".postbody>.bixbox") 64 | .first() 65 | .find(".pagination a:not(.prev,.next)") 66 | .last() 67 | .text() 68 | .replace(",", ""); 69 | $(".postbody>.bixbox") 70 | .first() 71 | .find(".listupd article") 72 | .each((i, el) => { 73 | return list.push({ 74 | slug: $(el).find("a").attr("href")?.split("/")[4], 75 | title: $(el).find("a").attr("title"), 76 | cover: $(el).find(".limit img[itemprop]").attr("data-src")?.split("?")[0] + "?resize=141,200", 77 | url: $(el).find("a").attr("href"), 78 | }); 79 | }); 80 | if (list.length == 0) { 81 | throw new Error("Anime not found"); 82 | } 83 | return { 84 | page: ~~page, 85 | maxPage: maxPage, 86 | list: list, 87 | }; 88 | } 89 | catch (err) { 90 | throw err; 91 | } 92 | }; 93 | exports.search = search; 94 | const popular = async (page = 1) => { 95 | let list = []; 96 | try { 97 | const base = await axios_1.default.get(`${BASEURL}/popular-anime/page/${page}`); 98 | const $ = cheerio_1.default.load(base.data); 99 | if (!$(".postbody").html()) { 100 | throw new Error("Page not found"); 101 | } 102 | let maxPage = ~~$(".postbody>.bixbox") 103 | .first() 104 | .find(".pagination a:not(.prev,.next)") 105 | .last() 106 | .text() 107 | .replace(",", ""); 108 | $(".postbody>.bixbox") 109 | .first() 110 | .find(".listupd article") 111 | .each((i, el) => { 112 | return list.push({ 113 | slug: $(el).find("a").attr("href")?.split("/")[4], 114 | title: $(el).find("a").attr("title"), 115 | cover: $(el).find(".limit img[itemprop]").attr("data-src")?.split("?")[0] + "?resize=141,200", 116 | url: $(el).find("a").attr("href"), 117 | }); 118 | }); 119 | return { 120 | page: ~~page, 121 | maxPage: maxPage, 122 | list: list, 123 | }; 124 | } 125 | catch (err) { 126 | throw err; 127 | } 128 | }; 129 | exports.popular = popular; 130 | const genreList = async (page = 1) => { 131 | let list = []; 132 | try { 133 | const base = await axios_1.default.get(`${BASEURL}/genres`); 134 | const $ = cheerio_1.default.load(base.data); 135 | if (!$(".postbody").html()) { 136 | throw new Error("Page not found"); 137 | } 138 | $(".postbody>.bixbox") 139 | .first() 140 | .find(".genre li") 141 | .each((i, el) => { 142 | list.push({ 143 | slug: $(el).find("a").attr("href")?.split("/")[4], 144 | title: $(el).find("a").text(), 145 | url: $(el).find("a").attr("href"), 146 | }); 147 | }); 148 | return list; 149 | } 150 | catch (err) { 151 | throw err; 152 | } 153 | }; 154 | exports.genreList = genreList; 155 | const genre = async (genre, page = 1) => { 156 | let list = []; 157 | try { 158 | const base = await axios_1.default.get(`${BASEURL}/genres/${genre}/page/${page}`); 159 | const $ = cheerio_1.default.load(base.data); 160 | if (!$(".postbody").html()) { 161 | throw new Error("Page not found"); 162 | } 163 | let maxPage = ~~$(".postbody>.bixbox") 164 | .first() 165 | .find(".pagination a:not(.prev,.next)") 166 | .last() 167 | .text() 168 | .replace(",", ""); 169 | $(".postbody>.bixbox") 170 | .first() 171 | .find(".listupd article") 172 | .each((i, el) => { 173 | return list.push({ 174 | slug: $(el).find("a").attr("href")?.split("/")[4], 175 | title: $(el).find("a").attr("title"), 176 | cover: $(el).find(".limit img[itemprop]").attr("data-src")?.split("?")[0] + "?resize=141,200", 177 | url: $(el).find("a").attr("href"), 178 | }); 179 | }); 180 | return { 181 | page: ~~page, 182 | maxPage: maxPage, 183 | list: list, 184 | }; 185 | } 186 | catch (err) { 187 | throw err; 188 | } 189 | }; 190 | exports.genre = genre; 191 | const seasonList = async (page = 1) => { 192 | let list = []; 193 | try { 194 | const base = await axios_1.default.get(`${BASEURL}/season/winter-2023`); 195 | const $ = cheerio_1.default.load(base.data); 196 | if (!$(".postbody").html()) { 197 | throw new Error("Page not found"); 198 | } 199 | $(".postbody>.bixbox") 200 | .first() 201 | .find("#season option:not([selected])") 202 | .each((i, el) => { 203 | list.push({ 204 | slug: $(el).attr("value"), 205 | title: $(el).text(), 206 | url: `${BASEURL}/season/` + $(el).attr("value"), 207 | }); 208 | }); 209 | return list; 210 | } 211 | catch (err) { 212 | throw err; 213 | } 214 | }; 215 | exports.seasonList = seasonList; 216 | const season = async (season, page = 1) => { 217 | let list = []; 218 | try { 219 | const base = await axios_1.default.get(`${BASEURL}/season/${season}`); 220 | const $ = cheerio_1.default.load(base.data); 221 | if (!$(".postbody").html()) { 222 | throw new Error("Page not found"); 223 | } 224 | let maxPage = 0; 225 | $(".postbody>.bixbox") 226 | .first() 227 | .find(".listupd .sebox") 228 | .each((i, el) => { 229 | return list.push({ 230 | slug: $(el).find(".tisebox a").attr("href")?.split("/")[4], 231 | title: $(el).find(".tisebox a").text(), 232 | cover: $(el).find(".bigsebox img").attr("data-src")?.split("?")[0] + "?resize=141,200", 233 | url: $(el).find(".tisebox a").attr("href"), 234 | }); 235 | }); 236 | return { 237 | page: ~~page, 238 | maxPage: maxPage, 239 | list: list, 240 | }; 241 | } 242 | catch (err) { 243 | throw err; 244 | } 245 | }; 246 | exports.season = season; 247 | const anime = async (slug) => { 248 | try { 249 | const base = await axios_1.default.get(`${BASEURL}/anime/${slug}`); 250 | const $ = cheerio_1.default.load(base.data); 251 | if (!$(".postbody").html()) { 252 | throw new Error("Anime not found"); 253 | } 254 | let genre = []; 255 | $(".postbody article .entry-content") 256 | .find(".infodetail ul li:nth-child(2) a") 257 | .each((i, el) => { 258 | genre.push($(el).text()); 259 | }); 260 | let episode = []; 261 | $(".postbody article .entry-content") 262 | .find(".bixbox.bxcl ul li") 263 | .each((i, el) => { 264 | if (!$(el).find(".lchx a").text().match(/OVA/)) { 265 | episode.push($(el).find(".lchx a").text()); 266 | } 267 | }); 268 | return { 269 | slug: $('meta[property="og:url"]').attr("content")?.split("/")[4], 270 | title: $(".postbody .info-header h1.entry-title").text(), 271 | titleAlt: $(".postbody .infodetail ul li:first-child").text().replace("Judul:", "").trim(), 272 | synopsis: $(".postbody .main-info .r .conx p").text(), 273 | episodeTotal: ~~$(".postbody .infodetail ul li:nth-child(9)").text().split(":")[1].trim() | 0, 274 | episode: episode.length, 275 | season: $(".postbody .infodetail ul li:nth-child(6)").find("a").text(), 276 | genre: genre, 277 | cover: $(".postbody .main-info .l img").attr("data-src")?.split("?")[0] + "?resize=141,200", 278 | url: $('meta[property="og:url"]').attr("content"), 279 | }; 280 | } 281 | catch (err) { 282 | throw err; 283 | } 284 | }; 285 | exports.anime = anime; 286 | const animeVideoSource = async (slug, ep) => { 287 | try { 288 | const base = await axios_1.default.get(`${BASEURL}/nonton-${slug}-episode-${ep}`); 289 | const $ = cheerio_1.default.load(base.data); 290 | if (!$(".postbody").html()) { 291 | throw new Error("Episode not found"); 292 | } 293 | let embedUrl = $("#tonton.video-content .player-embed") 294 | .find("iframe") 295 | .attr("data-src"); 296 | const videoSourceBase = await axios_1.default.get(embedUrl); 297 | const $$ = cheerio_1.default.load(videoSourceBase.data); 298 | if (!!$(".postbody .megavid .video-nav .iconx a").html()) { 299 | let enc = $$("script:not([src])") 300 | .first() 301 | .html() 302 | .match(/\(r\)\)\}(.*)/); 303 | let penc = enc[1].replace(/[\(\)"']/g, "").split(","); 304 | let decrypt = (0, decryptor_js_1.default)(penc[0], parseInt(penc[1]), penc[2], parseInt(penc[3]), parseInt(penc[4]), parseInt(penc[5])); 305 | let getSrcs = JSON.parse(decrypt.match(/srcs\s*=\s*(\[.*\])/)[1]); 306 | let videoSource = []; 307 | const waitSrc = getSrcs.map(async (el, i) => { 308 | const url = await axios_1.default.get(el.file, { maxRedirects: 0 }); 309 | let $$$ = cheerio_1.default.load(url.data); 310 | let surl = $$$("a").attr("href"); 311 | return { 312 | quality: el.label === "HD" ? "720p" : el.label === "SD" ? "480p" : "Unknown", 313 | url: surl, 314 | }; 315 | }); 316 | videoSource = await Promise.all(waitSrc); 317 | return { 318 | episode: ~~ep, 319 | video: videoSource, 320 | }; 321 | } 322 | else { 323 | throw new Error("Video source are not supported."); 324 | } 325 | } 326 | catch (err) { 327 | throw err; 328 | } 329 | }; 330 | exports.animeVideoSource = animeVideoSource; 331 | -------------------------------------------------------------------------------- /dist/source/nanime/nanime.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const parser_1 = require("./parser"); 8 | const app = express_1.default.Router(); 9 | app.get("/", async (req, res) => { 10 | return res.status(200).send("Nanime Server is ready 🚀"); 11 | }); 12 | app.get("/recent", async (req, res) => { 13 | try { 14 | const { page } = req.query; 15 | const data = await (0, parser_1.recentRelease)(page); 16 | return res.status(200).json(data); 17 | } 18 | catch (err) { 19 | return res.status(500).json({ 20 | error: "Internal Error", 21 | message: err.toString(), 22 | }); 23 | } 24 | }); 25 | app.get("/search", async (req, res) => { 26 | try { 27 | const { page, query } = req.query; 28 | const data = await (0, parser_1.search)(query, page); 29 | return res.status(200).json(data); 30 | } 31 | catch (err) { 32 | return res.status(500).json({ 33 | error: "Internal Error", 34 | message: err.toString(), 35 | }); 36 | } 37 | }); 38 | app.get("/popular", async (req, res) => { 39 | return res.status(500).json({ 40 | error: "Internal Error", 41 | message: "This request is not available for this server", 42 | }); 43 | }); 44 | app.get("/genre", async (req, res) => { 45 | try { 46 | const { page } = req.query; 47 | const data = await (0, parser_1.genreList)(page); 48 | return res.status(200).json(data); 49 | } 50 | catch (err) { 51 | return res.status(500).json({ 52 | error: "Internal Error", 53 | message: err.toString(), 54 | }); 55 | } 56 | }); 57 | app.get("/genre/:genre", async (req, res) => { 58 | try { 59 | const genreType = req.params.genre; 60 | const { page } = req.query; 61 | const data = await (0, parser_1.genre)(genreType, page); 62 | return res.status(200).json(data); 63 | } 64 | catch (err) { 65 | return res.status(500).json({ 66 | error: "Internal Error", 67 | message: err.toString(), 68 | }); 69 | } 70 | }); 71 | app.get("/season", async (req, res) => { 72 | try { 73 | const { page } = req.query; 74 | const data = await (0, parser_1.seasonList)(page); 75 | return res.status(200).json(data); 76 | } 77 | catch (err) { 78 | return res.status(500).json({ 79 | error: "Internal Error", 80 | message: err.toString(), 81 | }); 82 | } 83 | }); 84 | app.get("/season/:season", async (req, res) => { 85 | try { 86 | const seasonYear = req.params.season; 87 | const { page } = req.query; 88 | const data = await (0, parser_1.season)(seasonYear, page); 89 | return res.status(200).json(data); 90 | } 91 | catch (err) { 92 | return res.status(500).json({ 93 | error: "Internal Error", 94 | message: err.toString(), 95 | }); 96 | } 97 | }); 98 | app.get("/anime/:animeSlug", async (req, res) => { 99 | try { 100 | const slug = req.params.animeSlug; 101 | const data = await (0, parser_1.anime)(slug); 102 | return res.status(200).json(data); 103 | } 104 | catch (err) { 105 | return res.status(500).json({ 106 | error: "Internal Error", 107 | message: err.toString(), 108 | }); 109 | } 110 | }); 111 | app.get("/anime/:animeId/:episodeId", async (req, res) => { 112 | try { 113 | const id = req.params.animeId; 114 | const ep = req.params.episodeId; 115 | const data = await (0, parser_1.animeVideoSource)(id, ep); 116 | return res.status(200).json(data); 117 | } 118 | catch (err) { 119 | return res.status(500).json({ 120 | error: "Internal Error", 121 | message: err.toString(), 122 | }); 123 | } 124 | }); 125 | exports.default = app; 126 | -------------------------------------------------------------------------------- /dist/source/nanime/parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.animeVideoSource = exports.anime = exports.season = exports.seasonList = exports.genre = exports.genreList = exports.search = exports.recentRelease = void 0; 7 | const axios_1 = __importDefault(require("axios")); 8 | const cheerio_1 = __importDefault(require("cheerio")); 9 | const BASEURL = "https://nanimex1.com"; 10 | const recentRelease = async (page = 1) => { 11 | let list = []; 12 | try { 13 | const base = await axios_1.default.get(`${BASEURL}/page/${page}`); 14 | const $ = cheerio_1.default.load(base.data); 15 | let maxPage = ~~$(".box-poster .btn-group .page-numbers:not(.prev,.next)").last().html(); 16 | if (!maxPage) { 17 | throw new Error("Page not found, you may request more than the maximum page"); 18 | } 19 | $(".box-poster .content-item").each((i, el) => { 20 | return list.push({ 21 | slug: $(el).find("a").attr("href")?.split("/")[4], 22 | title: $(el).find("h3").attr("title"), 23 | episode: ~~$(el).find(".episode .btn-danger").text().replace("Episode", "").trim(), 24 | cover: $(el).find(".poster img").attr("data-lazy-src"), 25 | url: $(el).find("a").attr("href"), 26 | }); 27 | }); 28 | return { 29 | page: ~~page, 30 | maxPage: maxPage, 31 | list: list, 32 | }; 33 | } 34 | catch (err) { 35 | throw err; 36 | } 37 | }; 38 | exports.recentRelease = recentRelease; 39 | const search = async (query, page = 1) => { 40 | let list = []; 41 | try { 42 | const base = await axios_1.default.get(`${BASEURL}/page/${page}/?s=${query}`); 43 | const $ = cheerio_1.default.load(base.data); 44 | let maxPage = ~~$(".box-poster .btn-group .page-numbers:not(.prev,.next)").last().html(); 45 | if (!maxPage) { 46 | throw new Error("Anime not found, or you may request more than the maximum page"); 47 | } 48 | $(".box-poster .content-item").each((i, el) => { 49 | if (!!$(el).find('.episode .btn-danger:contains("Episode")').html()) { 50 | return list.push({ 51 | slug: $(el).find("a").attr("href")?.split("/")[4], 52 | title: $(el).find("h3").attr("title"), 53 | episode: ~~$(el).find(".episode .btn-danger").text().replace("Episode", "").trim(), 54 | cover: $(el).find(".poster img").attr("src"), 55 | url: $(el).find("a").attr("href"), 56 | }); 57 | } 58 | }); 59 | if (list.length == 0) { 60 | throw new Error("Anime not found"); 61 | } 62 | return { 63 | page: ~~page, 64 | maxPage: maxPage, 65 | list: list, 66 | }; 67 | } 68 | catch (err) { 69 | throw err; 70 | } 71 | }; 72 | exports.search = search; 73 | const genreList = async (page = 1) => { 74 | let list = []; 75 | try { 76 | const base = await axios_1.default.get(`${BASEURL}`); 77 | const $ = cheerio_1.default.load(base.data); 78 | if (!$(".box-content:last-child .box-primary:nth-child(2) .box-body a").html()) { 79 | throw new Error("Page not found, you may request more than the maximum page"); 80 | } 81 | $(".box-content:last-child .box-primary:nth-child(2) .box-body a").each((i, el) => { 82 | list.push({ 83 | slug: $(el).attr("href")?.split("/")[4], 84 | title: $(el).text(), 85 | url: $(el).attr("href"), 86 | }); 87 | }); 88 | return list; 89 | } 90 | catch (err) { 91 | throw err; 92 | } 93 | }; 94 | exports.genreList = genreList; 95 | const genre = async (genre, page = 1) => { 96 | let list = []; 97 | try { 98 | const base = await axios_1.default.get(`${BASEURL}/genre/${genre}/page/${page}`); 99 | const $ = cheerio_1.default.load(base.data); 100 | let maxPage = ~~$(".box-poster .btn-group .page-numbers:not(.prev,.next)").last().html(); 101 | if (!maxPage) { 102 | throw new Error("Genre not found, you may request more than the maximum page"); 103 | } 104 | $(".box-poster .content-item").each((i, el) => { 105 | if (!!$(el).find('.episode .btn-danger:contains("Episode")').html()) { 106 | return list.push({ 107 | slug: $(el).find("a").attr("href")?.split("/")[4], 108 | title: $(el).find("h3").attr("title"), 109 | episode: ~~$(el).find(".episode .btn-danger").text().replace("Episode", "").trim(), 110 | cover: $(el).find(".poster img").attr("data-lazy-src"), 111 | url: $(el).find("a").attr("href"), 112 | }); 113 | } 114 | }); 115 | if (list.length == 0) { 116 | throw new Error("Anime not found"); 117 | } 118 | return { 119 | page: ~~page, 120 | maxPage: maxPage, 121 | list: list, 122 | }; 123 | } 124 | catch (err) { 125 | throw err; 126 | } 127 | }; 128 | exports.genre = genre; 129 | const seasonList = async (page = 1) => { 130 | let list = []; 131 | try { 132 | const base = await axios_1.default.get(`${BASEURL}/properties/season?page=${page}`); 133 | const $ = cheerio_1.default.load(base.data); 134 | if (!$(".box-content:last-child .box-primary:nth-child(2) .box-body a").html()) { 135 | throw new Error("Page not found, or you may request more than the maximum page"); 136 | } 137 | $(".box-content:last-child .box-primary:nth-child(3) .box-body a").each((i, el) => { 138 | list.push({ 139 | slug: $(el).attr("href")?.split("/")[4], 140 | title: $(el).text(), 141 | url: $(el).attr("href"), 142 | }); 143 | }); 144 | return list; 145 | } 146 | catch (err) { 147 | throw err; 148 | } 149 | }; 150 | exports.seasonList = seasonList; 151 | const season = async (season, page = 1) => { 152 | let list = []; 153 | try { 154 | const base = await axios_1.default.get(`${BASEURL}/year_/${season}/page/${page}`); 155 | const $ = cheerio_1.default.load(base.data); 156 | let maxPage = ~~$(".box-poster .btn-group .page-numbers:not(.prev,.next)").last().html(); 157 | if (!maxPage) { 158 | throw new Error("Season not found, you may request more than the maximum page"); 159 | } 160 | $(".box-poster .content-item").each((i, el) => { 161 | if (!!$(el).find('.episode .btn-danger:contains("Episode")').html()) { 162 | return list.push({ 163 | slug: $(el).find("a").attr("href")?.split("/")[4], 164 | title: $(el).find("h3").attr("title"), 165 | episode: ~~$(el).find(".episode .btn-danger").text().replace("Episode", "").trim(), 166 | cover: $(el).find(".poster img").attr("data-lazy-src"), 167 | url: $(el).find("a").attr("href"), 168 | }); 169 | } 170 | }); 171 | if (list.length == 0) { 172 | throw new Error("Anime not found"); 173 | } 174 | return { 175 | page: ~~page, 176 | maxPage: maxPage, 177 | list: list, 178 | }; 179 | } 180 | catch (err) { 181 | throw err; 182 | } 183 | }; 184 | exports.season = season; 185 | const anime = async (slug) => { 186 | try { 187 | const base = await axios_1.default.get(`${BASEURL}/anime/${slug}`); 188 | const $ = cheerio_1.default.load(base.data); 189 | if (!$(".box-default .box-body > div:nth-child(3) .episode_list").html()) { 190 | throw new Error("Anime not found"); 191 | } 192 | let genre = []; 193 | $(".box-default .box-primary table") 194 | .find('td:contains("Genre")') 195 | .parent() 196 | .find("a") 197 | .each((i, el) => { 198 | genre.push($(el).text()); 199 | }); 200 | let episode = []; 201 | $(".box-default .box-body > div:nth-child(3) .episode_list table") 202 | .find("tr") 203 | .each((i, el) => { 204 | episode.push($(el).text()); 205 | }); 206 | return { 207 | slug: $('meta[property="og:url"]').attr("content")?.split("/")[4], 208 | title: $(".box-content .box-body > div:nth-child(2) table tr:nth-child(1) > td:nth-child(2) > a:nth-child(1)").text(), 209 | titleAlt: $(".box-content .box-body > div:nth-child(2) table tr:nth-child(2) > td:nth-child(2)").text(), 210 | synopsis: $(".box-content .box-body .attachment-block.clearfix .attachment-text").text(), 211 | episodeTotal: ~~$(".box-default>.box-body>.box-primary table") 212 | .find('td:contains("Total Episode")') 213 | .parent() 214 | .find("td:last-child") 215 | .text(), 216 | episode: episode.length, 217 | season: $(".box-content .box-body > div:nth-child(2) table tr:nth-child(1) > td:nth-child(2) > a:nth-child(2)").text(), 218 | genre: genre, 219 | cover: $(".box-content .box-body .attachment-block.clearfix .attachment-img").attr("data-lazy-src"), 220 | url: $('meta[property="og:url"]').attr("content"), 221 | }; 222 | } 223 | catch (err) { 224 | throw err; 225 | } 226 | }; 227 | exports.anime = anime; 228 | const animeVideoSource = async (slug, ep) => { 229 | try { 230 | const formattedEp = ("00" + ep).slice(-3); 231 | const base = await axios_1.default.get(`${BASEURL}/episode/${slug}-episode-${formattedEp}`); 232 | const $ = cheerio_1.default.load(base.data); 233 | if (!$("#change-server > option").html()) { 234 | throw new Error("Episode not found"); 235 | } 236 | let videoSource = []; 237 | $("#change-server > option").each((i, el) => { 238 | videoSource.push({ 239 | quality: $(el).text().split(" ")[0], 240 | url: $(el).attr("value"), 241 | }); 242 | }); 243 | return { 244 | episode: ~~ep, 245 | video: videoSource, 246 | }; 247 | } 248 | catch (err) { 249 | throw err; 250 | } 251 | }; 252 | exports.animeVideoSource = animeVideoSource; 253 | -------------------------------------------------------------------------------- /dist/source/otakudesu/otakudesu.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const express_1 = __importDefault(require("express")); 7 | const parser_1 = require("./parser"); 8 | const app = express_1.default.Router(); 9 | app.get("/", async (req, res) => { 10 | return res.status(200).send("Otakudesu Server is ready 🚀"); 11 | }); 12 | app.get("/recent", async (req, res) => { 13 | try { 14 | const { page } = req.query; 15 | const data = await (0, parser_1.recentRelease)(page); 16 | return res.status(200).json(data); 17 | } 18 | catch (err) { 19 | return res.status(500).json({ 20 | error: "Internal Error", 21 | message: err.toString(), 22 | }); 23 | } 24 | }); 25 | app.get("/search", async (req, res) => { 26 | try { 27 | const { page, query } = req.query; 28 | const data = await (0, parser_1.search)(query, page); 29 | return res.status(200).json(data); 30 | } 31 | catch (err) { 32 | return res.status(500).json({ 33 | error: "Internal Error", 34 | message: err.toString(), 35 | }); 36 | } 37 | }); 38 | app.get("/popular", async (req, res) => { 39 | return res.status(500).json({ 40 | error: "Internal Error", 41 | message: "This request is not available for this server", 42 | }); 43 | }); 44 | app.get("/genre", async (req, res) => { 45 | try { 46 | const { page } = req.query; 47 | const data = await (0, parser_1.genreList)(page); 48 | return res.status(200).json(data); 49 | } 50 | catch (err) { 51 | return res.status(500).json({ 52 | error: "Internal Error", 53 | message: err.toString(), 54 | }); 55 | } 56 | }); 57 | app.get("/genre/:genre", async (req, res) => { 58 | try { 59 | const genreType = req.params.genre; 60 | const { page } = req.query; 61 | const data = await (0, parser_1.genre)(genreType, page); 62 | return res.status(200).json(data); 63 | } 64 | catch (err) { 65 | return res.status(500).json({ 66 | error: "Internal Error", 67 | message: err.toString(), 68 | }); 69 | } 70 | }); 71 | app.get("/season", async (req, res) => { 72 | return res.status(500).json({ 73 | error: "Internal Error", 74 | message: "This request is not available for this server", 75 | }); 76 | }); 77 | app.get("/season/:season", async (req, res) => { 78 | return res.status(500).json({ 79 | error: "Internal Error", 80 | message: "This request is not available for this server", 81 | }); 82 | }); 83 | app.get("/anime/:animeSlug", async (req, res) => { 84 | try { 85 | const slug = req.params.animeSlug; 86 | const data = await (0, parser_1.anime)(slug); 87 | return res.status(200).json(data); 88 | } 89 | catch (err) { 90 | return res.status(500).json({ 91 | error: "Internal Error", 92 | message: err.toString(), 93 | }); 94 | } 95 | }); 96 | app.get("/anime/:animeId/:episodeId", async (req, res) => { 97 | try { 98 | const id = req.params.animeId; 99 | const ep = req.params.episodeId; 100 | const data = await (0, parser_1.animeVideoSource)(id, ep); 101 | return res.status(200).json(data); 102 | } 103 | catch (err) { 104 | return res.status(500).json({ 105 | error: "Internal Error", 106 | message: err.toString(), 107 | }); 108 | } 109 | }); 110 | exports.default = app; 111 | -------------------------------------------------------------------------------- /dist/source/otakudesu/parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.animeVideoSource = exports.anime = exports.genre = exports.genreList = exports.search = exports.recentRelease = void 0; 7 | const axios_1 = __importDefault(require("axios")); 8 | const cheerio_1 = __importDefault(require("cheerio")); 9 | const BASEURL = "https://otakudesu.ltd"; 10 | const recentRelease = async (page = 1) => { 11 | let list = []; 12 | try { 13 | const base = await axios_1.default.get(`${BASEURL}/ongoing-anime/page/${page}`); 14 | const $ = cheerio_1.default.load(base.data); 15 | let maxPage = ~~$(".venutama .pagination .page-numbers:not(.prev,.next)") 16 | .last() 17 | .html(); 18 | if (!maxPage) { 19 | throw new Error("Page not found, you may request more than the maximum page"); 20 | } 21 | $(".venutama .venz ul>li").each((i, el) => { 22 | return list.push({ 23 | slug: $(el).find(".thumb a").attr("href")?.split("/")[4], 24 | title: $(el).find(".thumb h2").text(), 25 | episode: ~~$(el).find(".epz").text().replace("Episode", "").trim(), 26 | cover: $(el).find(".thumb img").attr("src"), 27 | url: $(el).find(".thumb a").attr("href"), 28 | }); 29 | }); 30 | return { 31 | page: ~~page, 32 | maxPage: maxPage, 33 | list: list, 34 | }; 35 | } 36 | catch (err) { 37 | throw err; 38 | } 39 | }; 40 | exports.recentRelease = recentRelease; 41 | const search = async (query, page = 1) => { 42 | let list = []; 43 | try { 44 | const base = await axios_1.default.get(`${BASEURL}/?s=${query}&post_type=anime`); 45 | const $ = cheerio_1.default.load(base.data); 46 | let maxPage = 1; 47 | if ($(".venutama .chivsrc li").length < 1) { 48 | throw new Error("Anime not found, or you may request more than the maximum page"); 49 | } 50 | $(".venutama .chivsrc li").each((i, el) => { 51 | return list.push({ 52 | slug: $(el).find("h2>a").attr("href")?.split("/")[4], 53 | title: $(el) 54 | .find("h2>a") 55 | .text() 56 | .replace(/\s*\(Episode[^\)]*\)|\s*Subtitle Indonesia*|\s*Sub Indo*/g, ""), 57 | cover: $(el).find("img").attr("src"), 58 | url: $(el).find("h2>a").attr("href"), 59 | }); 60 | }); 61 | if (list.length == 0) { 62 | throw new Error("Anime not found"); 63 | } 64 | return { 65 | page: ~~page, 66 | maxPage: maxPage, 67 | list: list, 68 | }; 69 | } 70 | catch (err) { 71 | throw err; 72 | } 73 | }; 74 | exports.search = search; 75 | const genreList = async (page = 1) => { 76 | let list = []; 77 | try { 78 | const base = await axios_1.default.get(`${BASEURL}/genre-list`); 79 | const $ = cheerio_1.default.load(base.data); 80 | $(".genres li a").each((i, el) => { 81 | list.push({ 82 | slug: $(el).attr("href")?.split("/")[2], 83 | title: $(el).text(), 84 | url: $(el).attr("href"), 85 | }); 86 | }); 87 | return list; 88 | } 89 | catch (err) { 90 | throw err; 91 | } 92 | }; 93 | exports.genreList = genreList; 94 | const genre = async (genre, page = 1) => { 95 | let list = []; 96 | try { 97 | const base = await axios_1.default.get(`${BASEURL}/genres/${genre}/page/${page}`); 98 | const $ = cheerio_1.default.load(base.data); 99 | let maxPage = ~~$(".venser .pagination .page-numbers:not(.prev,.next)") 100 | .last() 101 | .html(); 102 | if (!maxPage) { 103 | throw new Error("Genre not found, you may request more than the maximum page"); 104 | } 105 | $(".venser .col-anime").each((i, el) => { 106 | return list.push({ 107 | slug: $(el).find(".col-anime-title a").attr("href")?.split("/")[4], 108 | title: $(el).find(".col-anime-title a").text(), 109 | episode: ~~$(el) 110 | .find(".col-anime-eps") 111 | .text() 112 | .replace(/Eps|Unknown/g, "") 113 | .trim(), 114 | cover: $(el).find(".col-anime-cover img").attr("src"), 115 | url: $(el).find(".col-anime-title a").attr("href"), 116 | }); 117 | }); 118 | if (list.length == 0) { 119 | throw new Error("Anime not found"); 120 | } 121 | return { 122 | page: ~~page, 123 | maxPage: maxPage, 124 | list: list, 125 | }; 126 | } 127 | catch (err) { 128 | throw err; 129 | } 130 | }; 131 | exports.genre = genre; 132 | const anime = async (slug) => { 133 | try { 134 | const base = await axios_1.default.get(`${BASEURL}/anime/${slug}`, { 135 | maxRedirects: 0, 136 | }); 137 | const $ = cheerio_1.default.load(base.data); 138 | if (!base.data) { 139 | throw new Error("Anime not found"); 140 | } 141 | let genre = []; 142 | $(".infozingle p:last-child") 143 | .find("a") 144 | .each((i, el) => { 145 | genre.push($(el).text()); 146 | }); 147 | let episode = []; 148 | $(".episodelist ul li").each((i, el) => { 149 | episode.push($(el).find("span:first-child").text()); 150 | }); 151 | return { 152 | slug: $('meta[property="og:url"]').attr("content")?.split("/")[4], 153 | title: $(".infozingle p:first-child").text().replace("Judul:", "").trim(), 154 | titleAlt: $(".infozingle p:nth-child(2)") 155 | .text() 156 | .replace("Japanese:", "") 157 | .trim(), 158 | synopsis: "Synopsis not available", 159 | episodeTotal: ~~$(".infozingle p:nth-child(7)") 160 | .text() 161 | .replace(/Total Episode:|Unknown/g, ""), 162 | episode: episode.length, 163 | genre: genre, 164 | cover: $(".venser .fotoanime img").attr("src"), 165 | url: $('meta[property="og:url"]').attr("content"), 166 | }; 167 | } 168 | catch (err) { 169 | throw err; 170 | } 171 | }; 172 | exports.anime = anime; 173 | const animeVideoSource = async (slug, ep) => { 174 | try { 175 | const formattedEp = ("00" + ep).slice(-3); 176 | const base = await axios_1.default.get(`${BASEURL}/episode/${slug}-episode-${formattedEp}`); 177 | const $ = cheerio_1.default.load(base.data); 178 | if (!$("#change-server > option").html()) { 179 | throw new Error("Episode not found"); 180 | } 181 | let videoSource = []; 182 | $("#change-server > option").each((i, el) => { 183 | videoSource.push({ 184 | quality: $(el).text().split(" ")[0], 185 | url: $(el).attr("value"), 186 | }); 187 | }); 188 | return { 189 | episode: ~~ep, 190 | video: videoSource, 191 | }; 192 | } 193 | catch (err) { 194 | throw err; 195 | } 196 | }; 197 | exports.animeVideoSource = animeVideoSource; 198 | -------------------------------------------------------------------------------- /dist/source/utils/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aniyoi-api", 3 | "version": "1.0.0", 4 | "description": "Anime Scrapper", 5 | "author": "miukyo", 6 | "main": "src/index.ts", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "node ./dist/index.js", 10 | "build": "rimraf dist && tsc", 11 | "dev": "nodemon ./src/index.ts" 12 | }, 13 | "pre-commit": [ 14 | "build" 15 | ], 16 | "devDependencies": { 17 | "@types/cheerio": "^0.22.31", 18 | "@types/cors": "^2.8.13", 19 | "@types/express": "^4.17.15", 20 | "@types/node": "^18.11.18", 21 | "nodemon": "^2.0.20", 22 | "pre-commit": "^1.2.2", 23 | "prettier": "^2.8.4", 24 | "rimraf": "^4.1.2", 25 | "ts-node": "^10.9.1" 26 | }, 27 | "dependencies": { 28 | "axios": "^1.2.2", 29 | "cheerio": "^1.0.0-rc.12", 30 | "cors": "^2.8.5", 31 | "dotenv": "^16.0.3", 32 | "express": "^4.18.2", 33 | "typescript": "^4.9.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import kuramanime from "./source/kuramanime/kuramanime"; 4 | import nanime from "./source/nanime/nanime"; 5 | import kuronime from "./source/kuronime/kuronime"; 6 | import otakudesu from "./source/otakudesu/otakudesu"; 7 | 8 | import axios from "axios"; 9 | 10 | export const app = express(); 11 | axios.defaults.validateStatus = () => true; 12 | axios.defaults.headers.common["User-Agent"] = 13 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54"; 14 | 15 | app.use(cors()); 16 | app.get("/", async (req, res) => { 17 | res.send("ANIYOI API IS UP 🚀"); 18 | }); 19 | app.use("/kuramanime", kuramanime); 20 | app.use("/nanime", nanime); 21 | app.use("/kuronime", kuronime); 22 | // app.use("/otakudesu", otakudesu); //Url page streaming memakai slug yang berbeda 23 | 24 | app.listen(process.env.PORT || 3001, () => { 25 | console.warn("\nReady 🚀"); 26 | }); 27 | -------------------------------------------------------------------------------- /src/source/kuramanime/kuramanime.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | anime, 4 | animeVideoSource, 5 | genre, 6 | genreList, 7 | popular, 8 | recentRelease, 9 | search, 10 | season, 11 | seasonList, 12 | } from "./parser"; 13 | 14 | const app = express.Router(); 15 | 16 | app.get("/", async (req: any, res: any) => { 17 | return res.status(200).send("Kuramanime Server is ready 🚀"); 18 | }); 19 | 20 | app.get("/recent", async (req: any, res: any) => { 21 | try { 22 | const { page } = req.query; 23 | const data = await recentRelease(page); 24 | return res.status(200).json(data); 25 | } catch (err: any) { 26 | return res.status(500).json({ 27 | error: "Internal Error", 28 | message: err.toString(), 29 | }); 30 | } 31 | }); 32 | 33 | app.get("/search", async (req: any, res: any) => { 34 | try { 35 | const { page, query } = req.query; 36 | const data = await search(query, page); 37 | return res.status(200).json(data); 38 | } catch (err: any) { 39 | return res.status(500).json({ 40 | error: "Internal Error", 41 | message: err.toString(), 42 | }); 43 | } 44 | }); 45 | 46 | app.get("/popular", async (req: any, res: any) => { 47 | try { 48 | const { page } = req.query; 49 | const data = await popular(page); 50 | return res.status(200).json(data); 51 | } catch (err: any) { 52 | return res.status(500).json({ 53 | error: "Internal Error", 54 | message: err.toString(), 55 | }); 56 | } 57 | }); 58 | 59 | app.get("/genre", async (req: any, res: any) => { 60 | try { 61 | const { page } = req.query; 62 | const data = await genreList(page); 63 | return res.status(200).json(data); 64 | } catch (err: any) { 65 | return res.status(500).json({ 66 | error: "Internal Error", 67 | message: err.toString(), 68 | }); 69 | } 70 | }); 71 | 72 | app.get("/genre/:genre", async (req: any, res: any) => { 73 | try { 74 | const genreType = req.params.genre; 75 | const { page } = req.query; 76 | const data = await genre(genreType, page); 77 | return res.status(200).json(data); 78 | } catch (err: any) { 79 | return res.status(500).json({ 80 | error: "Internal Error", 81 | message: err.toString(), 82 | }); 83 | } 84 | }); 85 | 86 | app.get("/season", async (req: any, res: any) => { 87 | try { 88 | const { page } = req.query; 89 | const data = await seasonList(page); 90 | return res.status(200).json(data); 91 | } catch (err: any) { 92 | return res.status(500).json({ 93 | error: "Internal Error", 94 | message: err.toString(), 95 | }); 96 | } 97 | }); 98 | 99 | app.get("/season/:season", async (req: any, res: any) => { 100 | try { 101 | const seasonYear = req.params.season; 102 | const { page } = req.query; 103 | const data = await season(seasonYear, page); 104 | return res.status(200).json(data); 105 | } catch (err: any) { 106 | return res.status(500).json({ 107 | error: "Internal Error", 108 | message: err.toString(), 109 | }); 110 | } 111 | }); 112 | 113 | app.get("/anime/:animeSlug", async (req: any, res: any) => { 114 | try { 115 | const slug = req.params.animeSlug; 116 | const data = await anime(slug); 117 | return res.status(200).json(data); 118 | } catch (err: any) { 119 | return res.status(500).json({ 120 | error: "Internal Error", 121 | message: err.toString(), 122 | }); 123 | } 124 | }); 125 | 126 | app.get("/anime/:animeId/:episodeId", async (req: any, res: any) => { 127 | try { 128 | const id = req.params.animeId; 129 | const ep = req.params.episodeId; 130 | const data = await animeVideoSource(id, ep); 131 | return res.status(200).json(data); 132 | } catch (err: any) { 133 | return res.status(500).json({ 134 | error: "Internal Error", 135 | message: err.toString(), 136 | }); 137 | } 138 | }); 139 | 140 | export default app; 141 | -------------------------------------------------------------------------------- /src/source/kuramanime/parser.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import cheerio from "cheerio"; 3 | import type { 4 | AnimeDetails, 5 | Anime, 6 | Genre, 7 | AnimeVideo, 8 | ListAnime, 9 | } from "../utils/types"; 10 | 11 | const BASEURL = "https://kuramanime.net"; 12 | 13 | export const recentRelease = async (page: number = 1): Promise => { 14 | let list: Anime[] = []; 15 | try { 16 | const base = await axios.get( 17 | `${BASEURL}/anime/ongoing?order_by=latest&page=${page}` 18 | ); 19 | const $ = cheerio.load(base.data); 20 | if (!$("#animeList .product__item").html()) { 21 | throw new Error("Page not found, you may request more than the maximum page"); 22 | } 23 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 24 | .last() 25 | .html()!; 26 | $("#animeList .product__item").each((i, el) => { 27 | return list.push({ 28 | slug: $(el).find("a").attr("href")?.split("/")[5]!, 29 | title: $(el).find(".product__item__text>h5>a").text()!, 30 | episode: ~~$(el) 31 | .find(".ep span") 32 | .text() 33 | .replace("Ep", "") 34 | .split("/")[0] 35 | .trim(), 36 | cover: $(el).attr("data-setbg")!, 37 | url: $(el).find("a").attr("href")!, 38 | }); 39 | }); 40 | return { 41 | page: ~~page, 42 | maxPage: maxPage, 43 | list: list, 44 | }; 45 | } catch (err: any) { 46 | throw err; 47 | } 48 | }; 49 | 50 | export const search = async ( 51 | query: string, 52 | page: number = 1 53 | ): Promise => { 54 | let list: Anime[] = []; 55 | try { 56 | const base = await axios.get( 57 | `${BASEURL}/anime?search=${query}&order_by=latest&page=${page}` 58 | ); 59 | const $ = cheerio.load(base.data); 60 | if (!$("#animeList .product__item").html()) { 61 | throw new Error("Anime not found,or you may request more than the maximum page"); 62 | } 63 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 64 | .last() 65 | .html()!; 66 | $("#animeList .product__item").each((i, el) => { 67 | list.push({ 68 | slug: $(el).find("a").attr("href")?.split("/")[5]!, 69 | title: $(el).find(".product__item__text>h5>a").text(), 70 | cover: $(el).attr("data-setbg")!, 71 | url: $(el).find("a").attr("href")!, 72 | }); 73 | }); 74 | if (list.length == 0) { 75 | throw new Error("Anime not found"); 76 | } 77 | return { 78 | page: ~~page, 79 | maxPage: maxPage, 80 | list: list, 81 | }; 82 | } catch (err: any) { 83 | throw err; 84 | } 85 | }; 86 | 87 | export const popular = async (page: number = 1): Promise => { 88 | let list: Anime[] = []; 89 | try { 90 | const base = await axios.get( 91 | `${BASEURL}/anime/ongoing?order_by=popular&page=${page}` 92 | ); 93 | const $ = cheerio.load(base.data); 94 | if (!$("#animeList .product__item").html()) { 95 | throw new Error("Page not found, you may request more than the maximum page"); 96 | } 97 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 98 | .last() 99 | .html()!; 100 | $("#animeList .product__item").each((i, el) => { 101 | list.push({ 102 | slug: $(el).find("a").attr("href")?.split("/")[5]!, 103 | title: $(el).find(".product__item__text>h5>a").text(), 104 | episode: ~~$(el) 105 | .find(".ep span") 106 | .text() 107 | .replace("Ep", "") 108 | .split("/")[0] 109 | .trim(), 110 | cover: $(el).attr("data-setbg")!, 111 | url: $(el).find("a").attr("href")!, 112 | }); 113 | }); 114 | return { 115 | page: ~~page, 116 | maxPage: maxPage, 117 | list: list, 118 | }; 119 | } catch (err: any) { 120 | throw err; 121 | } 122 | }; 123 | 124 | export const genreList = async (page: number = 1): Promise => { 125 | let list: Genre[] = []; 126 | try { 127 | const base = await axios.get( 128 | `${BASEURL}/properties/genre?genre_type=all&page=${page}` 129 | ); 130 | const $ = cheerio.load(base.data); 131 | if (!$("#animeList .kuramanime__genres li").html()) { 132 | throw new Error("Page not found"); 133 | } 134 | $("#animeList .kuramanime__genres li").each((i, el) => { 135 | list.push({ 136 | slug: $(el).find("a").attr("href")?.split("/")[5]?.split("?")[0]!, 137 | title: $(el).find("a").text(), 138 | url: $(el).find("a").attr("href")?.trim()!, 139 | }); 140 | }); 141 | return list; 142 | } catch (err: any) { 143 | throw err; 144 | } 145 | }; 146 | 147 | export const genre = async ( 148 | genre: string, 149 | page: number = 1 150 | ): Promise => { 151 | let list: Anime[] = []; 152 | try { 153 | const base = await axios.get( 154 | `${BASEURL}/properties/genre/${genre}?order_by=latest&page=${page}` 155 | ); 156 | const $ = cheerio.load(base.data); 157 | if (!$("#animeList .product__item").html()) { 158 | throw new Error("Genre not found, or you may request more than the maximum page"); 159 | } 160 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 161 | .last() 162 | .html()!; 163 | $("#animeList .product__item").each((i, el) => { 164 | list.push({ 165 | slug: $(el).find("a").attr("href")?.split("/")[5]!, 166 | title: $(el).find(".product__item__text>h5>a").text(), 167 | cover: $(el).attr("data-setbg")!, 168 | url: $(el).find("a").attr("href")!, 169 | }); 170 | }); 171 | return { 172 | page: ~~page, 173 | maxPage: maxPage, 174 | list: list, 175 | }; 176 | } catch (err: any) { 177 | throw err; 178 | } 179 | }; 180 | 181 | export const seasonList = async (page: number = 1): Promise => { 182 | let list: Genre[] = []; 183 | try { 184 | const base = await axios.get(`${BASEURL}/properties/season?page=${page}`); 185 | const $ = cheerio.load(base.data); 186 | if (!$("#animeList .kuramanime__genres li").html()) { 187 | throw new Error("Page not found"); 188 | } 189 | $("#animeList .kuramanime__genres li").each((i, el) => { 190 | list.push({ 191 | slug: $(el).find("a").attr("href")?.split("/")[5]!.split("?")[0]!, 192 | title: $(el).find("a").text(), 193 | url: $(el).find("a").attr("href")?.trim()!, 194 | }); 195 | }); 196 | return list; 197 | } catch (err: any) { 198 | throw err; 199 | } 200 | }; 201 | 202 | export const season = async ( 203 | season: string, 204 | page: number = 1 205 | ): Promise => { 206 | let list: Anime[] = []; 207 | try { 208 | const base = await axios.get( 209 | `${BASEURL}/properties/season/${season}?order_by=latest&page=${page}` 210 | ); 211 | const $ = cheerio.load(base.data); 212 | if (!$("#animeList .product__item").html()) { 213 | throw new Error("Season not found, or you may request more than the maximum page"); 214 | } 215 | let maxPage = ~~$("#animeList .product__pagination a:not(:has(i))") 216 | .last() 217 | .html()!; 218 | $("#animeList .product__item").each((i, el) => { 219 | list.push({ 220 | slug: $(el).find("a").attr("href")?.split("/")[5]!, 221 | title: $(el).find(".product__item__text>h5>a").text(), 222 | cover: $(el).attr("data-setbg")!, 223 | url: $(el).find("a").attr("href")!, 224 | }); 225 | }); 226 | return { 227 | page: ~~page, 228 | maxPage: maxPage, 229 | list: list, 230 | }; 231 | } catch (err: any) { 232 | throw err; 233 | } 234 | }; 235 | 236 | export const anime = async (slug: string): Promise => { 237 | try { 238 | const base = await axios.get(`${BASEURL}/anime/${slug}`); 239 | const $ = cheerio.load(base.data); 240 | if (!$(".anime__details__widget ul").html()) { 241 | throw new Error("Anime not found"); 242 | } 243 | let genre: string[] = []; 244 | $(".anime__details__widget ul") 245 | .find('span:contains("Genre:"),span:contains("Tema:")') 246 | .parent() 247 | .find("a") 248 | .each((i, el) => { 249 | genre.push($(el).text().replace(",", "").trim()); 250 | }); 251 | let episode = []; 252 | let $$ = cheerio.load( 253 | $("#episodeListsSection > #episodeLists").attr("data-content")! 254 | ); 255 | $$("a").each((i, el) => { 256 | episode.push($(el).text().trim()); 257 | }); 258 | return { 259 | slug: $('meta[property="og:url"]').attr("content")?.split("/")[5]!, 260 | title: $(".anime__details__title > h3").text(), 261 | titleAlt: $(".anime__details__title > span").text(), 262 | synopsis: $("#synopsisField").text(), 263 | episodeTotal: ~~$(".anime__details__widget ul") 264 | .find('span:contains("Total Eps:")') 265 | .next() 266 | .text(), 267 | episode: episode.length, 268 | season: $(".anime__details__widget ul") 269 | .find('span:contains("Musim:")') 270 | .next() 271 | .text(), 272 | genre: genre, 273 | cover: $(".set-bg").attr("data-setbg")!, 274 | url: $('meta[property="og:url"]').attr("content")!, 275 | }; 276 | } catch (err: any) { 277 | throw err; 278 | } 279 | }; 280 | 281 | export const animeVideoSource = async ( 282 | slug: string, 283 | ep: number 284 | ): Promise => { 285 | try { 286 | const url = await axios.head(`${BASEURL}/anime/${slug}`); 287 | const actualUrl = await url.request.res.responseUrl; 288 | const base = await axios.get( 289 | actualUrl + `/episode/${ep}?activate_stream=1` 290 | ); 291 | const $ = cheerio.load(base.data); 292 | let videoSource: { quality: string; url: string }[] = []; 293 | if (!$("#animeVideoPlayer video").html()) { 294 | throw new Error("Episode not found"); 295 | } 296 | $("#animeVideoPlayer video") 297 | .find("source") 298 | .each((i, el) => { 299 | videoSource.push({ 300 | quality: $(el).attr("size")! + "p", 301 | url: $(el).attr("src")!, 302 | }); 303 | }); 304 | return { 305 | episode: ~~ep, 306 | video: videoSource, 307 | }; 308 | } catch (err: any) { 309 | throw err; 310 | } 311 | }; 312 | -------------------------------------------------------------------------------- /src/source/kuronime/decryptor.js: -------------------------------------------------------------------------------- 1 | var Chara = [ 2 | "", 3 | "split", 4 | "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/", 5 | "slice", 6 | "indexOf", 7 | "", 8 | "", 9 | ".", 10 | "pow", 11 | "reduce", 12 | "reverse", 13 | "0", 14 | ]; 15 | function charDec(d,e,f) { 16 | var g = Chara[2][Chara[1]](Chara[0]); 17 | var h = g[Chara[3]](0, e); 18 | var i = g[Chara[3]](0, f); 19 | var j = d[Chara[1]](Chara[0]) 20 | [Chara[10]]() 21 | [Chara[9]](function (a, b, c) { 22 | if (h[Chara[4]](b) !== -1) 23 | return (a += h[Chara[4]](b) * Math[Chara[8]](e, c)); 24 | }, 0); 25 | var k = Chara[0]; 26 | while (j > 0) { 27 | k = i[j % f] + k; 28 | j = (j - (j % f)) / f; 29 | } 30 | return k || Chara[11]; 31 | } 32 | 33 | export default function decy(h, u, n, t, e, r) { 34 | r = ""; 35 | for (var i = 0, len = h.length; i < len; i++) { 36 | var s = ""; 37 | while (h[i] !== n[e]) { 38 | s += h[i]; 39 | i++; 40 | } 41 | for (var j = 0; j < n.length; j++) s = s.replace(new RegExp(n[j], "g"), j); 42 | r += String.fromCharCode(charDec(s, e, 10) - t); 43 | } 44 | return decodeURIComponent(escape(r)); 45 | } 46 | -------------------------------------------------------------------------------- /src/source/kuronime/kuronime.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | anime, 4 | animeVideoSource, 5 | genre, 6 | genreList, 7 | popular, 8 | recentRelease, 9 | search, 10 | season, 11 | seasonList, 12 | } from "./parser"; 13 | 14 | const app = express.Router(); 15 | 16 | app.get("/", async (req: any, res: any) => { 17 | return res.status(200).send("Kuronime Server is ready 🚀"); 18 | }); 19 | 20 | app.get("/recent", async (req: any, res: any) => { 21 | try { 22 | const { page } = req.query; 23 | const data = await recentRelease(page); 24 | return res.status(200).json(data); 25 | } catch (err: any) { 26 | return res.status(500).json({ 27 | error: "Internal Error", 28 | message: err.toString(), 29 | }); 30 | } 31 | }); 32 | 33 | app.get("/search", async (req: any, res: any) => { 34 | try { 35 | const { page, query } = req.query; 36 | const data = await search(query, page); 37 | return res.status(200).json(data); 38 | } catch (err: any) { 39 | return res.status(500).json({ 40 | error: "Internal Error", 41 | message: err.toString(), 42 | }); 43 | } 44 | }); 45 | 46 | app.get("/popular", async (req: any, res: any) => { 47 | try { 48 | const { page } = req.query; 49 | const data = await popular(page); 50 | return res.status(200).json(data); 51 | } catch (err: any) { 52 | return res.status(500).json({ 53 | error: "Internal Error", 54 | message: err.toString(), 55 | }); 56 | } 57 | }); 58 | 59 | app.get("/genre", async (req: any, res: any) => { 60 | try { 61 | const { page } = req.query; 62 | const data = await genreList(page); 63 | return res.status(200).json(data); 64 | } catch (err: any) { 65 | return res.status(500).json({ 66 | error: "Internal Error", 67 | message: err.toString(), 68 | }); 69 | } 70 | }); 71 | 72 | app.get("/genre/:genre", async (req: any, res: any) => { 73 | try { 74 | const genreType = req.params.genre; 75 | const { page } = req.query; 76 | const data = await genre(genreType, page); 77 | return res.status(200).json(data); 78 | } catch (err: any) { 79 | return res.status(500).json({ 80 | error: "Internal Error", 81 | message: err.toString(), 82 | }); 83 | } 84 | }); 85 | 86 | app.get("/season", async (req: any, res: any) => { 87 | try { 88 | const { page } = req.query; 89 | const data = await seasonList(page); 90 | return res.status(200).json(data); 91 | } catch (err: any) { 92 | return res.status(500).json({ 93 | error: "Internal Error", 94 | message: err.toString(), 95 | }); 96 | } 97 | }); 98 | 99 | app.get("/season/:season", async (req: any, res: any) => { 100 | try { 101 | const seasonYear = req.params.season; 102 | const { page } = req.query; 103 | const data = await season(seasonYear, page); 104 | return res.status(200).json(data); 105 | } catch (err: any) { 106 | return res.status(500).json({ 107 | error: "Internal Error", 108 | message: err.toString(), 109 | }); 110 | } 111 | }); 112 | 113 | app.get("/anime/:animeSlug", async (req: any, res: any) => { 114 | try { 115 | const slug = req.params.animeSlug; 116 | const data = await anime(slug); 117 | return res.status(200).json(data); 118 | } catch (err: any) { 119 | return res.status(500).json({ 120 | error: "Internal Error", 121 | message: err.toString(), 122 | }); 123 | } 124 | }); 125 | 126 | app.get("/anime/:animeId/:episodeId", async (req: any, res: any) => { 127 | try { 128 | const id = req.params.animeId; 129 | const ep = req.params.episodeId; 130 | const data = await animeVideoSource(id, ep); 131 | return res.status(200).json(data); 132 | } catch (err: any) { 133 | return res.status(500).json({ 134 | error: "Internal Error", 135 | message: err.toString(), 136 | }); 137 | } 138 | }); 139 | 140 | export default app; 141 | -------------------------------------------------------------------------------- /src/source/kuronime/parser.ts: -------------------------------------------------------------------------------- 1 | import axios, { Axios } from "axios"; 2 | import cheerio from "cheerio"; 3 | import type { AnimeDetails, Anime, Genre, AnimeVideo, ListAnime } from "../utils/types"; 4 | import decryptor from "./decryptor.js"; 5 | 6 | const BASEURL = "https://45.12.2.2"; 7 | 8 | export const recentRelease = async (page: number = 1): Promise => { 9 | let list: Anime[] = []; 10 | try { 11 | const base = await axios.get(`${BASEURL}/page/${page}`); 12 | const $ = cheerio.load(base.data); 13 | if (!$(".postbody").html()) { 14 | throw new Error("Page not found"); 15 | } 16 | let maxPage = ~~$(".postbody>.bixbox") 17 | .first() 18 | .find(".pagination a:not(.prev,.next)") 19 | .last() 20 | .text() 21 | .replace(",", ""); 22 | $(".postbody>.bixbox") 23 | .first() 24 | .find(".excstf article") 25 | .each((i, el) => { 26 | return list.push({ 27 | slug: $(el) 28 | .find("a") 29 | .attr("href") 30 | ?.split("/")[3] 31 | .replace(/\b(?:nonton-|-episode-[a-zA-Z0-9_]*)\b/gi, "")!, 32 | title: $(el).find(".bsuxtt").text()!, 33 | episode: ~~$(el).find(".bt .ep").text().replace("Episode", "").trim(), 34 | cover: 35 | $(el).find(".limit img[itemprop]").attr("data-src")?.split("?")[0]! + "?resize=141,200", 36 | url: $(el) 37 | .find("a") 38 | .attr("href") 39 | ?.replace(/\b(?:nonton-|-episode-[a-zA-Z0-9_]*)\b/gi, "")!, 40 | }); 41 | }); 42 | return { 43 | page: ~~page, 44 | maxPage: maxPage, 45 | list: list, 46 | }; 47 | } catch (err: any) { 48 | throw err; 49 | } 50 | }; 51 | 52 | export const search = async (query: string, page: number = 1): Promise => { 53 | let list: Anime[] = []; 54 | try { 55 | const base = await axios.get(`${BASEURL}/anime/page/${page}/?title=${query}&order=update`); 56 | const $ = cheerio.load(base.data); 57 | if (!$(".postbody").html()) { 58 | throw new Error("Page not found"); 59 | } 60 | let maxPage = ~~$(".postbody>.bixbox") 61 | .first() 62 | .find(".pagination a:not(.prev,.next)") 63 | .last() 64 | .text() 65 | .replace(",", ""); 66 | $(".postbody>.bixbox") 67 | .first() 68 | .find(".listupd article") 69 | .each((i, el) => { 70 | return list.push({ 71 | slug: $(el).find("a").attr("href")?.split("/")[4]!, 72 | title: $(el).find("a").attr("title")!, 73 | cover: 74 | $(el).find(".limit img[itemprop]").attr("data-src")?.split("?")[0]! + "?resize=141,200", 75 | url: $(el).find("a").attr("href")!, 76 | }); 77 | }); 78 | if (list.length == 0) { 79 | throw new Error("Anime not found"); 80 | } 81 | return { 82 | page: ~~page, 83 | maxPage: maxPage, 84 | list: list, 85 | }; 86 | } catch (err: any) { 87 | throw err; 88 | } 89 | }; 90 | 91 | export const popular = async (page: number = 1): Promise => { 92 | let list: Anime[] = []; 93 | try { 94 | const base = await axios.get(`${BASEURL}/popular-anime/page/${page}`); 95 | const $ = cheerio.load(base.data); 96 | if (!$(".postbody").html()) { 97 | throw new Error("Page not found"); 98 | } 99 | let maxPage = ~~$(".postbody>.bixbox") 100 | .first() 101 | .find(".pagination a:not(.prev,.next)") 102 | .last() 103 | .text() 104 | .replace(",", ""); 105 | $(".postbody>.bixbox") 106 | .first() 107 | .find(".listupd article") 108 | .each((i, el) => { 109 | return list.push({ 110 | slug: $(el).find("a").attr("href")?.split("/")[4]!, 111 | title: $(el).find("a").attr("title")!, 112 | cover: 113 | $(el).find(".limit img[itemprop]").attr("data-src")?.split("?")[0]! + "?resize=141,200", 114 | url: $(el).find("a").attr("href")!, 115 | }); 116 | }); 117 | return { 118 | page: ~~page, 119 | maxPage: maxPage, 120 | list: list, 121 | }; 122 | } catch (err: any) { 123 | throw err; 124 | } 125 | }; 126 | 127 | export const genreList = async (page: number = 1): Promise => { 128 | let list: Genre[] = []; 129 | try { 130 | const base = await axios.get(`${BASEURL}/genres`); 131 | const $ = cheerio.load(base.data); 132 | if (!$(".postbody").html()) { 133 | throw new Error("Page not found"); 134 | } 135 | $(".postbody>.bixbox") 136 | .first() 137 | .find(".genre li") 138 | .each((i, el) => { 139 | list.push({ 140 | slug: $(el).find("a").attr("href")?.split("/")[4]!, 141 | title: $(el).find("a").text(), 142 | url: $(el).find("a").attr("href")!, 143 | }); 144 | }); 145 | return list; 146 | } catch (err: any) { 147 | throw err; 148 | } 149 | }; 150 | 151 | export const genre = async (genre: string, page: number = 1): Promise => { 152 | let list: Anime[] = []; 153 | try { 154 | const base = await axios.get(`${BASEURL}/genres/${genre}/page/${page}`); 155 | const $ = cheerio.load(base.data); 156 | if (!$(".postbody").html()) { 157 | throw new Error("Page not found"); 158 | } 159 | let maxPage = ~~$(".postbody>.bixbox") 160 | .first() 161 | .find(".pagination a:not(.prev,.next)") 162 | .last() 163 | .text() 164 | .replace(",", ""); 165 | $(".postbody>.bixbox") 166 | .first() 167 | .find(".listupd article") 168 | .each((i, el) => { 169 | return list.push({ 170 | slug: $(el).find("a").attr("href")?.split("/")[4]!, 171 | title: $(el).find("a").attr("title")!, 172 | cover: 173 | $(el).find(".limit img[itemprop]").attr("data-src")?.split("?")[0]! + "?resize=141,200", 174 | url: $(el).find("a").attr("href")!, 175 | }); 176 | }); 177 | return { 178 | page: ~~page, 179 | maxPage: maxPage, 180 | list: list, 181 | }; 182 | } catch (err: any) { 183 | throw err; 184 | } 185 | }; 186 | 187 | export const seasonList = async (page: number = 1): Promise => { 188 | let list: Genre[] = []; 189 | try { 190 | const base = await axios.get(`${BASEURL}/season/winter-2023`); 191 | const $ = cheerio.load(base.data); 192 | if (!$(".postbody").html()) { 193 | throw new Error("Page not found"); 194 | } 195 | $(".postbody>.bixbox") 196 | .first() 197 | .find("#season option:not([selected])") 198 | .each((i, el) => { 199 | list.push({ 200 | slug: $(el).attr("value")!, 201 | title: $(el).text(), 202 | url: `${BASEURL}/season/` + $(el).attr("value")!, 203 | }); 204 | }); 205 | return list; 206 | } catch (err: any) { 207 | throw err; 208 | } 209 | }; 210 | 211 | export const season = async (season: string, page: number = 1): Promise => { 212 | let list: Anime[] = []; 213 | try { 214 | const base = await axios.get(`${BASEURL}/season/${season}`); 215 | const $ = cheerio.load(base.data); 216 | if (!$(".postbody").html()) { 217 | throw new Error("Page not found"); 218 | } 219 | let maxPage = 0; 220 | $(".postbody>.bixbox") 221 | .first() 222 | .find(".listupd .sebox") 223 | .each((i, el) => { 224 | return list.push({ 225 | slug: $(el).find(".tisebox a").attr("href")?.split("/")[4]!, 226 | title: $(el).find(".tisebox a").text()!, 227 | cover: $(el).find(".bigsebox img").attr("data-src")?.split("?")[0]! + "?resize=141,200", 228 | url: $(el).find(".tisebox a").attr("href")!, 229 | }); 230 | }); 231 | return { 232 | page: ~~page, 233 | maxPage: maxPage, 234 | list: list, 235 | }; 236 | } catch (err: any) { 237 | throw err; 238 | } 239 | }; 240 | 241 | export const anime = async (slug: string): Promise => { 242 | try { 243 | const base = await axios.get(`${BASEURL}/anime/${slug}`); 244 | const $ = cheerio.load(base.data); 245 | if (!$(".postbody").html()) { 246 | throw new Error("Anime not found"); 247 | } 248 | let genre: string[] = []; 249 | $(".postbody article .entry-content") 250 | .find(".infodetail ul li:nth-child(2) a") 251 | .each((i, el) => { 252 | genre.push($(el).text()); 253 | }); 254 | let episode: string[] = []; 255 | $(".postbody article .entry-content") 256 | .find(".bixbox.bxcl ul li") 257 | .each((i, el) => { 258 | if (!$(el).find(".lchx a").text().match(/OVA/)) { 259 | episode.push($(el).find(".lchx a").text()); 260 | } 261 | }); 262 | return { 263 | slug: $('meta[property="og:url"]').attr("content")?.split("/")[4]!, 264 | title: $(".postbody .info-header h1.entry-title").text(), 265 | titleAlt: $(".postbody .infodetail ul li:first-child").text().replace("Judul:", "").trim(), 266 | synopsis: $(".postbody .main-info .r .conx p").text(), 267 | episodeTotal: ~~$(".postbody .infodetail ul li:nth-child(9)").text().split(":")[1].trim() | 0, 268 | episode: episode.length, 269 | season: $(".postbody .infodetail ul li:nth-child(6)").find("a").text(), 270 | genre: genre, 271 | cover: $(".postbody .main-info .l img").attr("data-src")?.split("?")[0]! + "?resize=141,200"!, 272 | url: $('meta[property="og:url"]').attr("content")!, 273 | }; 274 | } catch (err: any) { 275 | throw err; 276 | } 277 | }; 278 | 279 | export const animeVideoSource = async (slug: string, ep: number): Promise => { 280 | try { 281 | const base = await axios.get(`${BASEURL}/nonton-${slug}-episode-${ep}`); 282 | const $ = cheerio.load(base.data); 283 | if (!$(".postbody").html()) { 284 | throw new Error("Episode not found"); 285 | } 286 | let embedUrl: string = $("#tonton.video-content .player-embed") 287 | .find("iframe") 288 | .attr("data-src")!; 289 | const videoSourceBase = await axios.get(embedUrl); 290 | const $$ = cheerio.load(videoSourceBase.data); 291 | if (!!$(".postbody .megavid .video-nav .iconx a").html()) { 292 | let enc = $$("script:not([src])") 293 | .first() 294 | .html()! 295 | .match(/\(r\)\)\}(.*)/)!; 296 | let penc = enc[1].replace(/[\(\)"']/g, "").split(","); 297 | let decrypt = decryptor( 298 | penc[0], 299 | parseInt(penc[1]), 300 | penc[2], 301 | parseInt(penc[3]), 302 | parseInt(penc[4]), 303 | parseInt(penc[5]) 304 | ); 305 | let getSrcs: { file: string; label: string; type: string }[] = JSON.parse( 306 | decrypt.match(/srcs\s*=\s*(\[.*\])/)![1] 307 | ); 308 | 309 | let videoSource: { quality: string; url: string }[] = []; 310 | 311 | const waitSrc: Promise<{ quality: string; url: string }>[] = getSrcs.map(async (el, i) => { 312 | const url = await axios.get(el.file, { maxRedirects: 0 }); 313 | let $$$ = cheerio.load(url.data); 314 | let surl = $$$("a").attr("href"); 315 | return { 316 | quality: el.label === "HD" ? "720p" : el.label === "SD" ? "480p" : "Unknown", 317 | url: surl!, 318 | }; 319 | }); 320 | videoSource = await Promise.all(waitSrc); 321 | return { 322 | episode: ~~ep, 323 | video: videoSource, 324 | }; 325 | } else { 326 | throw new Error("Video source are not supported."); 327 | } 328 | } catch (err: any) { 329 | throw err; 330 | } 331 | }; 332 | -------------------------------------------------------------------------------- /src/source/nanime/nanime.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | anime, 4 | animeVideoSource, 5 | genre, 6 | genreList, 7 | recentRelease, 8 | search, 9 | season, 10 | seasonList, 11 | } from "./parser"; 12 | 13 | const app = express.Router(); 14 | 15 | app.get("/", async (req: any, res: any) => { 16 | return res.status(200).send("Nanime Server is ready 🚀"); 17 | }); 18 | 19 | app.get("/recent", async (req: any, res: any) => { 20 | try { 21 | const { page } = req.query; 22 | const data = await recentRelease(page); 23 | return res.status(200).json(data); 24 | } catch (err: any) { 25 | return res.status(500).json({ 26 | error: "Internal Error", 27 | message: err.toString(), 28 | }); 29 | } 30 | }); 31 | 32 | app.get("/search", async (req: any, res: any) => { 33 | try { 34 | const { page, query } = req.query; 35 | const data = await search(query, page); 36 | return res.status(200).json(data); 37 | } catch (err: any) { 38 | return res.status(500).json({ 39 | error: "Internal Error", 40 | message: err.toString(), 41 | }); 42 | } 43 | }); 44 | 45 | app.get("/popular", async (req: any, res: any) => { 46 | return res.status(500).json({ 47 | error: "Internal Error", 48 | message: "This request is not available for this server", 49 | }); 50 | }); 51 | 52 | app.get("/genre", async (req: any, res: any) => { 53 | try { 54 | const { page } = req.query; 55 | const data = await genreList(page); 56 | return res.status(200).json(data); 57 | } catch (err: any) { 58 | return res.status(500).json({ 59 | error: "Internal Error", 60 | message: err.toString(), 61 | }); 62 | } 63 | }); 64 | 65 | app.get("/genre/:genre", async (req: any, res: any) => { 66 | try { 67 | const genreType = req.params.genre; 68 | const { page } = req.query; 69 | const data = await genre(genreType, page); 70 | return res.status(200).json(data); 71 | } catch (err: any) { 72 | return res.status(500).json({ 73 | error: "Internal Error", 74 | message: err.toString(), 75 | }); 76 | } 77 | }); 78 | 79 | app.get("/season", async (req: any, res: any) => { 80 | try { 81 | const { page } = req.query; 82 | const data = await seasonList(page); 83 | return res.status(200).json(data); 84 | } catch (err: any) { 85 | return res.status(500).json({ 86 | error: "Internal Error", 87 | message: err.toString(), 88 | }); 89 | } 90 | }); 91 | 92 | app.get("/season/:season", async (req: any, res: any) => { 93 | try { 94 | const seasonYear = req.params.season; 95 | const { page } = req.query; 96 | const data = await season(seasonYear, page); 97 | return res.status(200).json(data); 98 | } catch (err: any) { 99 | return res.status(500).json({ 100 | error: "Internal Error", 101 | message: err.toString(), 102 | }); 103 | } 104 | }); 105 | 106 | app.get("/anime/:animeSlug", async (req: any, res: any) => { 107 | try { 108 | const slug = req.params.animeSlug; 109 | const data = await anime(slug); 110 | return res.status(200).json(data); 111 | } catch (err: any) { 112 | return res.status(500).json({ 113 | error: "Internal Error", 114 | message: err.toString(), 115 | }); 116 | } 117 | }); 118 | 119 | app.get("/anime/:animeId/:episodeId", async (req: any, res: any) => { 120 | try { 121 | const id = req.params.animeId; 122 | const ep = req.params.episodeId; 123 | const data = await animeVideoSource(id, ep); 124 | return res.status(200).json(data); 125 | } catch (err: any) { 126 | return res.status(500).json({ 127 | error: "Internal Error", 128 | message: err.toString(), 129 | }); 130 | } 131 | }); 132 | 133 | export default app; 134 | -------------------------------------------------------------------------------- /src/source/nanime/parser.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import cheerio from "cheerio"; 3 | import type { AnimeDetails, Anime, Genre, AnimeVideo, ListAnime } from "../utils/types"; 4 | 5 | const BASEURL = "https://nanimex1.com"; 6 | 7 | export const recentRelease = async (page: number = 1): Promise => { 8 | let list: Anime[] = []; 9 | try { 10 | const base = await axios.get(`${BASEURL}/page/${page}`); 11 | const $ = cheerio.load(base.data); 12 | let maxPage = ~~$(".box-poster .btn-group .page-numbers:not(.prev,.next)").last().html()!; 13 | if (!maxPage) { 14 | throw new Error("Page not found, you may request more than the maximum page"); 15 | } 16 | $(".box-poster .content-item").each((i, el) => { 17 | return list.push({ 18 | slug: $(el).find("a").attr("href")?.split("/")[4]!, 19 | title: $(el).find("h3").attr("title")!, 20 | episode: ~~$(el).find(".episode .btn-danger").text().replace("Episode", "").trim(), 21 | cover: $(el).find(".poster img").attr("data-lazy-src")!, 22 | url: $(el).find("a").attr("href")!, 23 | }); 24 | }); 25 | 26 | return { 27 | page: ~~page, 28 | maxPage: maxPage, 29 | list: list, 30 | }; 31 | } catch (err: any) { 32 | throw err; 33 | } 34 | }; 35 | 36 | export const search = async (query: string, page: number = 1): Promise => { 37 | let list: Anime[] = []; 38 | try { 39 | const base = await axios.get(`${BASEURL}/page/${page}/?s=${query}`); 40 | const $ = cheerio.load(base.data); 41 | let maxPage = ~~$(".box-poster .btn-group .page-numbers:not(.prev,.next)").last().html()!; 42 | if (!maxPage) { 43 | throw new Error("Anime not found, or you may request more than the maximum page"); 44 | } 45 | $(".box-poster .content-item").each((i, el) => { 46 | if (!!$(el).find('.episode .btn-danger:contains("Episode")').html()) { 47 | return list.push({ 48 | slug: $(el).find("a").attr("href")?.split("/")[4]!, 49 | title: $(el).find("h3").attr("title")!, 50 | episode: ~~$(el).find(".episode .btn-danger").text().replace("Episode", "").trim(), 51 | cover: $(el).find(".poster img").attr("src")!, 52 | url: $(el).find("a").attr("href")!, 53 | }); 54 | } 55 | }); 56 | if (list.length == 0) { 57 | throw new Error("Anime not found"); 58 | } 59 | return { 60 | page: ~~page, 61 | maxPage: maxPage, 62 | list: list, 63 | }; 64 | } catch (err: any) { 65 | throw err; 66 | } 67 | }; 68 | 69 | export const genreList = async (page: number = 1): Promise => { 70 | let list: Genre[] = []; 71 | try { 72 | const base = await axios.get(`${BASEURL}`); 73 | const $ = cheerio.load(base.data); 74 | if (!$(".box-content:last-child .box-primary:nth-child(2) .box-body a").html()) { 75 | throw new Error("Page not found, you may request more than the maximum page"); 76 | } 77 | $(".box-content:last-child .box-primary:nth-child(2) .box-body a").each((i, el) => { 78 | list.push({ 79 | slug: $(el).attr("href")?.split("/")[4]!, 80 | title: $(el).text(), 81 | url: $(el).attr("href")!, 82 | }); 83 | }); 84 | return list; 85 | } catch (err: any) { 86 | throw err; 87 | } 88 | }; 89 | 90 | export const genre = async (genre: string, page: number = 1): Promise => { 91 | let list: Anime[] = []; 92 | try { 93 | const base = await axios.get(`${BASEURL}/genre/${genre}/page/${page}`); 94 | const $ = cheerio.load(base.data); 95 | let maxPage = ~~$(".box-poster .btn-group .page-numbers:not(.prev,.next)").last().html()!; 96 | if (!maxPage) { 97 | throw new Error("Genre not found, you may request more than the maximum page"); 98 | } 99 | $(".box-poster .content-item").each((i, el) => { 100 | if (!!$(el).find('.episode .btn-danger:contains("Episode")').html()) { 101 | return list.push({ 102 | slug: $(el).find("a").attr("href")?.split("/")[4]!, 103 | title: $(el).find("h3").attr("title")!, 104 | episode: ~~$(el).find(".episode .btn-danger").text().replace("Episode", "").trim(), 105 | cover: $(el).find(".poster img").attr("data-lazy-src")!, 106 | url: $(el).find("a").attr("href")!, 107 | }); 108 | } 109 | }); 110 | if (list.length == 0) { 111 | throw new Error("Anime not found"); 112 | } 113 | return { 114 | page: ~~page, 115 | maxPage: maxPage, 116 | list: list, 117 | }; 118 | } catch (err: any) { 119 | throw err; 120 | } 121 | }; 122 | 123 | export const seasonList = async (page: number = 1): Promise => { 124 | let list: Genre[] = []; 125 | try { 126 | const base = await axios.get(`${BASEURL}/properties/season?page=${page}`); 127 | const $ = cheerio.load(base.data); 128 | if (!$(".box-content:last-child .box-primary:nth-child(2) .box-body a").html()) { 129 | throw new Error("Page not found, or you may request more than the maximum page"); 130 | } 131 | $(".box-content:last-child .box-primary:nth-child(3) .box-body a").each((i, el) => { 132 | list.push({ 133 | slug: $(el).attr("href")?.split("/")[4]!, 134 | title: $(el).text(), 135 | url: $(el).attr("href")!, 136 | }); 137 | }); 138 | return list; 139 | } catch (err: any) { 140 | throw err; 141 | } 142 | }; 143 | 144 | export const season = async (season: string, page: number = 1): Promise => { 145 | let list: Anime[] = []; 146 | try { 147 | const base = await axios.get(`${BASEURL}/year_/${season}/page/${page}`); 148 | const $ = cheerio.load(base.data); 149 | let maxPage = ~~$(".box-poster .btn-group .page-numbers:not(.prev,.next)").last().html()!; 150 | if (!maxPage) { 151 | throw new Error("Season not found, you may request more than the maximum page"); 152 | } 153 | $(".box-poster .content-item").each((i, el) => { 154 | if (!!$(el).find('.episode .btn-danger:contains("Episode")').html()) { 155 | return list.push({ 156 | slug: $(el).find("a").attr("href")?.split("/")[4]!, 157 | title: $(el).find("h3").attr("title")!, 158 | episode: ~~$(el).find(".episode .btn-danger").text().replace("Episode", "").trim(), 159 | cover: $(el).find(".poster img").attr("data-lazy-src")!, 160 | url: $(el).find("a").attr("href")!, 161 | }); 162 | } 163 | }); 164 | if (list.length == 0) { 165 | throw new Error("Anime not found"); 166 | } 167 | return { 168 | page: ~~page, 169 | maxPage: maxPage, 170 | list: list, 171 | }; 172 | } catch (err: any) { 173 | throw err; 174 | } 175 | }; 176 | 177 | export const anime = async (slug: string): Promise => { 178 | try { 179 | const base = await axios.get(`${BASEURL}/anime/${slug}`); 180 | const $ = cheerio.load(base.data); 181 | if (!$(".box-default .box-body > div:nth-child(3) .episode_list").html()) { 182 | throw new Error("Anime not found"); 183 | } 184 | let genre: string[] = []; 185 | $(".box-default .box-primary table") 186 | .find('td:contains("Genre")') 187 | .parent() 188 | .find("a") 189 | .each((i, el) => { 190 | genre.push($(el).text()); 191 | }); 192 | let episode: string[] = []; 193 | $(".box-default .box-body > div:nth-child(3) .episode_list table") 194 | .find("tr") 195 | .each((i, el) => { 196 | episode.push($(el).text()); 197 | }); 198 | return { 199 | slug: $('meta[property="og:url"]').attr("content")?.split("/")[4]!, 200 | title: $( 201 | ".box-content .box-body > div:nth-child(2) table tr:nth-child(1) > td:nth-child(2) > a:nth-child(1)" 202 | ).text(), 203 | titleAlt: $( 204 | ".box-content .box-body > div:nth-child(2) table tr:nth-child(2) > td:nth-child(2)" 205 | ).text(), 206 | synopsis: $(".box-content .box-body .attachment-block.clearfix .attachment-text").text(), 207 | episodeTotal: ~~$(".box-default>.box-body>.box-primary table") 208 | .find('td:contains("Total Episode")') 209 | .parent() 210 | .find("td:last-child") 211 | .text(), 212 | episode: episode.length, 213 | season: $( 214 | ".box-content .box-body > div:nth-child(2) table tr:nth-child(1) > td:nth-child(2) > a:nth-child(2)" 215 | ).text(), 216 | genre: genre, 217 | cover: $(".box-content .box-body .attachment-block.clearfix .attachment-img").attr( 218 | "data-lazy-src" 219 | )!, 220 | url: $('meta[property="og:url"]').attr("content")!, 221 | }; 222 | } catch (err: any) { 223 | throw err; 224 | } 225 | }; 226 | 227 | export const animeVideoSource = async (slug: string, ep: number): Promise => { 228 | try { 229 | const formattedEp = ("00" + ep).slice(-3); 230 | const base = await axios.get(`${BASEURL}/episode/${slug}-episode-${formattedEp}`); 231 | const $ = cheerio.load(base.data); 232 | if (!$("#change-server > option").html()) { 233 | throw new Error("Episode not found"); 234 | } 235 | let videoSource: { quality: string; url: string }[] = []; 236 | $("#change-server > option").each((i, el) => { 237 | videoSource.push({ 238 | quality: $(el).text().split(" ")[0]!, 239 | url: $(el).attr("value")!, 240 | }); 241 | }); 242 | 243 | return { 244 | episode: ~~ep, 245 | video: videoSource, 246 | }; 247 | } catch (err: any) { 248 | throw err; 249 | } 250 | }; 251 | -------------------------------------------------------------------------------- /src/source/otakudesu/otakudesu.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | anime, 4 | animeVideoSource, 5 | genre, 6 | genreList, 7 | recentRelease, 8 | search, 9 | } from "./parser"; 10 | 11 | const app = express.Router(); 12 | 13 | app.get("/", async (req: any, res: any) => { 14 | return res.status(200).send("Otakudesu Server is ready 🚀"); 15 | }); 16 | 17 | app.get("/recent", async (req: any, res: any) => { 18 | try { 19 | const { page } = req.query; 20 | const data = await recentRelease(page); 21 | return res.status(200).json(data); 22 | } catch (err: any) { 23 | return res.status(500).json({ 24 | error: "Internal Error", 25 | message: err.toString(), 26 | }); 27 | } 28 | }); 29 | 30 | app.get("/search", async (req: any, res: any) => { 31 | try { 32 | const { page, query } = req.query; 33 | const data = await search(query, page); 34 | return res.status(200).json(data); 35 | } catch (err: any) { 36 | return res.status(500).json({ 37 | error: "Internal Error", 38 | message: err.toString(), 39 | }); 40 | } 41 | }); 42 | 43 | app.get("/popular", async (req: any, res: any) => { 44 | return res.status(500).json({ 45 | error: "Internal Error", 46 | message: "This request is not available for this server", 47 | }); 48 | }); 49 | 50 | app.get("/genre", async (req: any, res: any) => { 51 | try { 52 | const { page } = req.query; 53 | const data = await genreList(page); 54 | return res.status(200).json(data); 55 | } catch (err: any) { 56 | return res.status(500).json({ 57 | error: "Internal Error", 58 | message: err.toString(), 59 | }); 60 | } 61 | }); 62 | 63 | app.get("/genre/:genre", async (req: any, res: any) => { 64 | try { 65 | const genreType = req.params.genre; 66 | const { page } = req.query; 67 | const data = await genre(genreType, page); 68 | return res.status(200).json(data); 69 | } catch (err: any) { 70 | return res.status(500).json({ 71 | error: "Internal Error", 72 | message: err.toString(), 73 | }); 74 | } 75 | }); 76 | 77 | app.get("/season", async (req: any, res: any) => { 78 | return res.status(500).json({ 79 | error: "Internal Error", 80 | message: "This request is not available for this server", 81 | }); 82 | }); 83 | 84 | app.get("/season/:season", async (req: any, res: any) => { 85 | return res.status(500).json({ 86 | error: "Internal Error", 87 | message: "This request is not available for this server", 88 | }); 89 | }); 90 | 91 | app.get("/anime/:animeSlug", async (req: any, res: any) => { 92 | try { 93 | const slug = req.params.animeSlug; 94 | const data = await anime(slug); 95 | return res.status(200).json(data); 96 | } catch (err: any) { 97 | return res.status(500).json({ 98 | error: "Internal Error", 99 | message: err.toString(), 100 | }); 101 | } 102 | }); 103 | 104 | app.get("/anime/:animeId/:episodeId", async (req: any, res: any) => { 105 | try { 106 | const id = req.params.animeId; 107 | const ep = req.params.episodeId; 108 | const data = await animeVideoSource(id, ep); 109 | return res.status(200).json(data); 110 | } catch (err: any) { 111 | return res.status(500).json({ 112 | error: "Internal Error", 113 | message: err.toString(), 114 | }); 115 | } 116 | }); 117 | 118 | export default app; 119 | -------------------------------------------------------------------------------- /src/source/otakudesu/parser.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import cheerio from "cheerio"; 3 | import type { 4 | AnimeDetails, 5 | Anime, 6 | Genre, 7 | AnimeVideo, 8 | ListAnime, 9 | } from "../utils/types"; 10 | 11 | const BASEURL = "https://otakudesu.ltd"; 12 | 13 | export const recentRelease = async (page: number = 1): Promise => { 14 | let list: Anime[] = []; 15 | try { 16 | const base = await axios.get(`${BASEURL}/ongoing-anime/page/${page}`); 17 | const $ = cheerio.load(base.data); 18 | let maxPage = ~~$(".venutama .pagination .page-numbers:not(.prev,.next)") 19 | .last() 20 | .html()!; 21 | if (!maxPage) { 22 | throw new Error( 23 | "Page not found, you may request more than the maximum page" 24 | ); 25 | } 26 | $(".venutama .venz ul>li").each((i, el) => { 27 | return list.push({ 28 | slug: $(el).find(".thumb a").attr("href")?.split("/")[4]!, 29 | title: $(el).find(".thumb h2").text()!, 30 | episode: ~~$(el).find(".epz").text().replace("Episode", "").trim(), 31 | cover: $(el).find(".thumb img").attr("src")!, 32 | url: $(el).find(".thumb a").attr("href")!, 33 | }); 34 | }); 35 | 36 | return { 37 | page: ~~page, 38 | maxPage: maxPage, 39 | list: list, 40 | }; 41 | } catch (err: any) { 42 | throw err; 43 | } 44 | }; 45 | 46 | export const search = async ( 47 | query: string, 48 | page: number = 1 49 | ): Promise => { 50 | let list: Anime[] = []; 51 | try { 52 | const base = await axios.get(`${BASEURL}/?s=${query}&post_type=anime`); 53 | const $ = cheerio.load(base.data); 54 | let maxPage = 1; 55 | if ($(".venutama .chivsrc li").length < 1) { 56 | throw new Error( 57 | "Anime not found, or you may request more than the maximum page" 58 | ); 59 | } 60 | $(".venutama .chivsrc li").each((i, el) => { 61 | return list.push({ 62 | slug: $(el).find("h2>a").attr("href")?.split("/")[4]!, 63 | title: $(el) 64 | .find("h2>a") 65 | .text()! 66 | .replace( 67 | /\s*\(Episode[^\)]*\)|\s*Subtitle Indonesia*|\s*Sub Indo*/g, 68 | "" 69 | ), 70 | cover: $(el).find("img").attr("src")!, 71 | url: $(el).find("h2>a").attr("href")!, 72 | }); 73 | }); 74 | if (list.length == 0) { 75 | throw new Error("Anime not found"); 76 | } 77 | return { 78 | page: ~~page, 79 | maxPage: maxPage, 80 | list: list, 81 | }; 82 | } catch (err: any) { 83 | throw err; 84 | } 85 | }; 86 | 87 | export const genreList = async (page: number = 1): Promise => { 88 | let list: Genre[] = []; 89 | try { 90 | const base = await axios.get(`${BASEURL}/genre-list`); 91 | const $ = cheerio.load(base.data); 92 | $(".genres li a").each((i, el) => { 93 | list.push({ 94 | slug: $(el).attr("href")?.split("/")[2]!, 95 | title: $(el).text(), 96 | url: $(el).attr("href")!, 97 | }); 98 | }); 99 | return list; 100 | } catch (err: any) { 101 | throw err; 102 | } 103 | }; 104 | 105 | export const genre = async ( 106 | genre: string, 107 | page: number = 1 108 | ): Promise => { 109 | let list: Anime[] = []; 110 | try { 111 | const base = await axios.get(`${BASEURL}/genres/${genre}/page/${page}`); 112 | const $ = cheerio.load(base.data); 113 | let maxPage = ~~$(".venser .pagination .page-numbers:not(.prev,.next)") 114 | .last() 115 | .html()!; 116 | if (!maxPage) { 117 | throw new Error( 118 | "Genre not found, you may request more than the maximum page" 119 | ); 120 | } 121 | $(".venser .col-anime").each((i, el) => { 122 | return list.push({ 123 | slug: $(el).find(".col-anime-title a").attr("href")?.split("/")[4]!, 124 | title: $(el).find(".col-anime-title a").text()!, 125 | episode: ~~$(el) 126 | .find(".col-anime-eps") 127 | .text() 128 | .replace(/Eps|Unknown/g, "") 129 | .trim(), 130 | cover: $(el).find(".col-anime-cover img").attr("src")!, 131 | url: $(el).find(".col-anime-title a").attr("href")!, 132 | }); 133 | }); 134 | if (list.length == 0) { 135 | throw new Error("Anime not found"); 136 | } 137 | return { 138 | page: ~~page, 139 | maxPage: maxPage, 140 | list: list, 141 | }; 142 | } catch (err: any) { 143 | throw err; 144 | } 145 | }; 146 | 147 | export const anime = async (slug: string): Promise => { 148 | try { 149 | const base = await axios.get(`${BASEURL}/anime/${slug}`, { 150 | maxRedirects: 0, 151 | }); 152 | const $ = cheerio.load(base.data); 153 | if (!base.data) { 154 | throw new Error("Anime not found"); 155 | } 156 | let genre: string[] = []; 157 | $(".infozingle p:last-child") 158 | .find("a") 159 | .each((i, el) => { 160 | genre.push($(el).text()); 161 | }); 162 | let episode: string[] = []; 163 | $(".episodelist ul li").each((i, el) => { 164 | episode.push($(el).find("span:first-child").text()); 165 | }); 166 | return { 167 | slug: $('meta[property="og:url"]').attr("content")?.split("/")[4]!, 168 | title: $(".infozingle p:first-child").text().replace("Judul:", "").trim(), 169 | titleAlt: $(".infozingle p:nth-child(2)") 170 | .text() 171 | .replace("Japanese:", "") 172 | .trim(), 173 | synopsis: "Synopsis not available", 174 | episodeTotal: ~~$(".infozingle p:nth-child(7)") 175 | .text() 176 | .replace(/Total Episode:|Unknown/g, ""), 177 | episode: episode.length, 178 | genre: genre, 179 | cover: $(".venser .fotoanime img").attr("src")!, 180 | url: $('meta[property="og:url"]').attr("content")!, 181 | }; 182 | } catch (err: any) { 183 | throw err; 184 | } 185 | }; 186 | 187 | export const animeVideoSource = async ( 188 | slug: string, 189 | ep: number 190 | ): Promise => { 191 | try { 192 | const formattedEp = ("00" + ep).slice(-3); 193 | const base = await axios.get( 194 | `${BASEURL}/episode/${slug}-episode-${formattedEp}` 195 | ); 196 | const $ = cheerio.load(base.data); 197 | if (!$("#change-server > option").html()) { 198 | throw new Error("Episode not found"); 199 | } 200 | let videoSource: { quality: string; url: string }[] = []; 201 | $("#change-server > option").each((i, el) => { 202 | videoSource.push({ 203 | quality: $(el).text().split(" ")[0]!, 204 | url: $(el).attr("value")!, 205 | }); 206 | }); 207 | 208 | return { 209 | episode: ~~ep, 210 | video: videoSource, 211 | }; 212 | } catch (err: any) { 213 | throw err; 214 | } 215 | }; 216 | -------------------------------------------------------------------------------- /src/source/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type ListAnime = { 2 | list: Anime[]; 3 | maxPage: number; 4 | page: number; 5 | }; 6 | export type Anime = { 7 | slug: string; 8 | title: string; 9 | episode?: number; 10 | cover: string; 11 | url: string; 12 | }; 13 | 14 | export type AnimeDetails = { 15 | slug: string; 16 | title: string; 17 | titleAlt?: string; 18 | synopsis?: string; 19 | episodeTotal: number; 20 | episode: number; 21 | season?: string; 22 | genre?: string[]; 23 | cover: string; 24 | url: string; 25 | }; 26 | 27 | export type AnimeVideo = { 28 | episode: number; 29 | video: VideoSource[]; 30 | }; 31 | 32 | export type VideoSource = { 33 | quality: string; 34 | url: string; 35 | }; 36 | 37 | export type Genre = { 38 | slug: string; 39 | title: string; 40 | url: string; 41 | }; 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "baseUrl": "./", 7 | "allowJs": true, 8 | "outDir": "./dist", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | } 14 | } -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "dist/index.js", 6 | "use": "@vercel/node", 7 | "config": { "includeFiles": ["dist/**"] } 8 | } 9 | ], 10 | "routes": [ 11 | { 12 | "src": "/(.*)", 13 | "dest": "dist/index.js" 14 | } 15 | ] 16 | } 17 | --------------------------------------------------------------------------------