├── src ├── schemas │ ├── samehadaku.schema.ts │ ├── otakudesu.schema.ts │ └── kuramanime.schema.ts ├── interfaces │ ├── samehadaku.interface.ts │ ├── kuramanime.interface.ts │ └── otakudesu.interface.ts ├── parsers │ ├── samehadaku.parser.ts │ ├── extra │ │ ├── samehadaku.extra.parser.ts │ │ ├── otakudesu.extra.parser.ts │ │ └── kuramanime.extra.parser.ts │ ├── main │ │ └── main.parser.ts │ ├── kuramanime.parser.ts │ └── otakudesu.parser.ts ├── helpers │ ├── errorinCuy.ts │ ├── generateSrcFromIframeTag.ts │ ├── setPayload.ts │ └── getHTML.ts ├── configs │ ├── otakudesu.config.ts │ ├── kuramanime.config.ts │ ├── samehadaku.config.ts │ └── app.config.ts ├── routes │ ├── samehadaku.routes.ts │ ├── kuramanime.routes.ts │ └── otakudesu.routes.ts ├── controllers │ ├── samehadaku.controller.ts │ ├── otakudesu.controller.ts │ └── kuramanime.controller.ts ├── scrapers │ ├── samehadaku.scraper.ts │ ├── kuramanime.scraper.ts │ └── otakudesu.scraper.ts ├── index.ts ├── global.d.ts └── middlewares │ ├── cache.ts │ └── errorHandler.ts ├── .prettierrc ├── dist ├── schemas │ ├── samehadaku.schema.js │ ├── otakudesu.schema.js │ └── kuramanime.schema.js ├── interfaces │ ├── otakudesu.interface.js │ ├── kuramanime.interface.js │ └── samehadaku.interface.js ├── parsers │ ├── samehadaku.parser.js │ ├── extra │ │ ├── samehadaku.extra.parser.js │ │ ├── otakudesu.extra.parser.js │ │ └── kuramanime.extra.parser.js │ ├── main │ │ └── main.parser.js │ ├── kuramanime.parser.js │ └── otakudesu.parser.js ├── helpers │ ├── errorinCuy.js │ ├── generateSrcFromIframeTag.js │ ├── setPayload.js │ └── getHTML.js ├── configs │ ├── otakudesu.config.js │ ├── kuramanime.config.js │ ├── samehadaku.config.js │ └── app.config.js ├── routes │ ├── samehadaku.routes.js │ ├── kuramanime.routes.js │ └── otakudesu.routes.js ├── controllers │ ├── samehadaku.controller.js │ ├── otakudesu.controller.js │ └── kuramanime.controller.js ├── scrapers │ ├── samehadaku.scraper.js │ ├── kuramanime.scraper.js │ └── otakudesu.scraper.js ├── index.js └── middlewares │ ├── cache.js │ └── errorHandler.js ├── .gitignore ├── .dockerignore ├── vercel.json ├── docker-compose.yaml ├── Dockerfile ├── package.json ├── LICENSE ├── tsconfig.json └── README.md /src/schemas/samehadaku.schema.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/interfaces/samehadaku.interface.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100 3 | } 4 | -------------------------------------------------------------------------------- /dist/schemas/samehadaku.schema.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/interfaces/otakudesu.interface.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/interfaces/kuramanime.interface.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/interfaces/samehadaku.interface.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/parsers/samehadaku.parser.js: -------------------------------------------------------------------------------- 1 | const samehadakuParser = {}; 2 | export default samehadakuParser; 3 | -------------------------------------------------------------------------------- /src/parsers/samehadaku.parser.ts: -------------------------------------------------------------------------------- 1 | const samehadakuParser = {}; 2 | 3 | export default samehadakuParser; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | .env 4 | .vscode 5 | .idea 6 | .DS_Store 7 | Thumbs.db 8 | Desktop.ini -------------------------------------------------------------------------------- /dist/helpers/errorinCuy.js: -------------------------------------------------------------------------------- 1 | export default function errorinCuy(status, message) { 2 | throw { status, message }; 3 | } 4 | -------------------------------------------------------------------------------- /dist/parsers/extra/samehadaku.extra.parser.js: -------------------------------------------------------------------------------- 1 | const samehadakuExtraParser = {}; 2 | export default samehadakuExtraParser; 3 | -------------------------------------------------------------------------------- /src/parsers/extra/samehadaku.extra.parser.ts: -------------------------------------------------------------------------------- 1 | const samehadakuExtraParser = {}; 2 | 3 | export default samehadakuExtraParser; 4 | -------------------------------------------------------------------------------- /dist/configs/otakudesu.config.js: -------------------------------------------------------------------------------- 1 | const otakudesuConfig = { 2 | baseUrl: "https://otakudesu.best", 3 | }; 4 | export default otakudesuConfig; 5 | -------------------------------------------------------------------------------- /src/helpers/errorinCuy.ts: -------------------------------------------------------------------------------- 1 | export default function errorinCuy(status?: number, message?: string): void { 2 | throw { status, message }; 3 | } 4 | -------------------------------------------------------------------------------- /dist/configs/kuramanime.config.js: -------------------------------------------------------------------------------- 1 | const kuramanimeConfig = { 2 | baseUrl: "https://v8.kuramanime.tel", 3 | }; 4 | export default kuramanimeConfig; 5 | -------------------------------------------------------------------------------- /dist/configs/samehadaku.config.js: -------------------------------------------------------------------------------- 1 | const samehadakuConfig = { 2 | baseUrl: "https://v1.samehadaku.how", 3 | }; 4 | export default samehadakuConfig; 5 | -------------------------------------------------------------------------------- /src/configs/otakudesu.config.ts: -------------------------------------------------------------------------------- 1 | const otakudesuConfig: IAnimeConfig = { 2 | baseUrl: "https://otakudesu.best", 3 | }; 4 | 5 | export default otakudesuConfig; 6 | -------------------------------------------------------------------------------- /src/configs/kuramanime.config.ts: -------------------------------------------------------------------------------- 1 | const kuramanimeConfig: IAnimeConfig = { 2 | baseUrl: "https://v8.kuramanime.tel", 3 | }; 4 | 5 | export default kuramanimeConfig; 6 | -------------------------------------------------------------------------------- /src/configs/samehadaku.config.ts: -------------------------------------------------------------------------------- 1 | const samehadakuConfig: IAnimeConfig = { 2 | baseUrl: "https://v1.samehadaku.how", 3 | }; 4 | 5 | export default samehadakuConfig; 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn-debug.log 4 | *.log 5 | .git 6 | .gitignore 7 | .dockerignore 8 | .vscode 9 | .idea 10 | .DS_Store 11 | *.md 12 | *.test.js 13 | *.spec.js -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "dist/index.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "dist/index.js" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /dist/routes/samehadaku.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import samehadakuController from "../controllers/samehadaku.controller.js"; 3 | const samehadakuRouter = Router(); 4 | samehadakuRouter.get("/", samehadakuController.getRoot); 5 | export default samehadakuRouter; 6 | -------------------------------------------------------------------------------- /dist/helpers/generateSrcFromIframeTag.js: -------------------------------------------------------------------------------- 1 | export default function generateSrcFromIframeTag(html) { 2 | const iframeMatch = html?.match(/]+src="([^"]+)"/i); 3 | const src = iframeMatch ? iframeMatch[1] || "No iframe found" : "No iframe found"; 4 | return src; 5 | } 6 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | wajik-anime-api: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | container_name: wajik-anime-api 7 | restart: unless-stopped 8 | ports: 9 | - "3001:3001" 10 | environment: 11 | - NODE_ENV=production 12 | -------------------------------------------------------------------------------- /src/routes/samehadaku.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import samehadakuController from "@controllers/samehadaku.controller.js"; 3 | 4 | const samehadakuRouter = Router(); 5 | 6 | samehadakuRouter.get("/", samehadakuController.getRoot); 7 | 8 | export default samehadakuRouter; 9 | -------------------------------------------------------------------------------- /src/helpers/generateSrcFromIframeTag.ts: -------------------------------------------------------------------------------- 1 | export default function generateSrcFromIframeTag(html?: string): string { 2 | const iframeMatch = html?.match(/]+src="([^"]+)"/i); 3 | const src = iframeMatch ? iframeMatch[1] || "No iframe found" : "No iframe found"; 4 | 5 | return src; 6 | } 7 | -------------------------------------------------------------------------------- /dist/helpers/setPayload.js: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | export default function setPayload(res, props) { 3 | return { 4 | statusCode: res.statusCode, 5 | statusMessage: http.STATUS_CODES[res.statusCode] || "", 6 | message: props?.message || "", 7 | data: props?.data || null, 8 | pagination: props?.pagination || null, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /dist/controllers/samehadaku.controller.js: -------------------------------------------------------------------------------- 1 | import samehadakuConfig from "../configs/samehadaku.config.js"; 2 | import setPayload from "../helpers/setPayload.js"; 3 | const { baseUrl } = samehadakuConfig; 4 | const samehadakuController = { 5 | async getRoot(req, res, next) { 6 | res.json(setPayload(res, { 7 | message: "Status: OFF", 8 | })); 9 | }, 10 | }; 11 | export default samehadakuController; 12 | -------------------------------------------------------------------------------- /src/configs/app.config.ts: -------------------------------------------------------------------------------- 1 | const appConfig: IAppConfig = { 2 | /** 3 | * server port 4 | */ 5 | PORT: 3001, 6 | 7 | /** 8 | * ngilangin properti sourceUrl di response 9 | * 10 | * jika true: 11 | * { 12 | * {...props} 13 | * sourceUrl: "..." 14 | * } 15 | * 16 | * jika false: 17 | * { 18 | * {...props} 19 | * } 20 | */ 21 | sourceUrl: true, 22 | }; 23 | 24 | export default appConfig; 25 | -------------------------------------------------------------------------------- /dist/configs/app.config.js: -------------------------------------------------------------------------------- 1 | const appConfig = { 2 | /** 3 | * server port 4 | */ 5 | PORT: 3001, 6 | /** 7 | * ngilangin properti sourceUrl di response 8 | * 9 | * jika true: 10 | * { 11 | * {...props} 12 | * sourceUrl: "..." 13 | * } 14 | * 15 | * jika false: 16 | * { 17 | * {...props} 18 | * } 19 | */ 20 | sourceUrl: true, 21 | }; 22 | export default appConfig; 23 | -------------------------------------------------------------------------------- /dist/schemas/otakudesu.schema.js: -------------------------------------------------------------------------------- 1 | import * as v from "valibot"; 2 | const otakudesuSchema = { 3 | query: { 4 | animes: v.optional(v.object({ 5 | page: v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(6), v.regex(/^([1-9]\d*)$/, "invalid page"))), 6 | })), 7 | searchedAnimes: v.object({ 8 | q: v.pipe(v.string(), v.minLength(1), v.maxLength(50)), 9 | }), 10 | }, 11 | }; 12 | export default otakudesuSchema; 13 | -------------------------------------------------------------------------------- /src/controllers/samehadaku.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction } from "express"; 2 | import samehadakuConfig from "@configs/samehadaku.config.js"; 3 | import setPayload from "@helpers/setPayload.js"; 4 | 5 | const { baseUrl } = samehadakuConfig; 6 | 7 | const samehadakuController = { 8 | async getRoot(req: Request, res: Response, next: NextFunction) { 9 | res.json( 10 | setPayload(res, { 11 | message: "Status: OFF", 12 | }) 13 | ); 14 | }, 15 | }; 16 | 17 | export default samehadakuController; 18 | -------------------------------------------------------------------------------- /dist/scrapers/samehadaku.scraper.js: -------------------------------------------------------------------------------- 1 | import samehadakuConfig from "../configs/samehadaku.config.js"; 2 | import getHTML from "../helpers/getHTML.js"; 3 | import { parse } from "node-html-parser"; 4 | const { baseUrl } = samehadakuConfig; 5 | const samehadakuScraper = { 6 | async scrapeDOM(pathname, ref, sanitize = false) { 7 | const html = await getHTML(baseUrl, pathname, ref, sanitize); 8 | const document = parse(html, { 9 | parseNoneClosedTags: true, 10 | }); 11 | return document; 12 | }, 13 | }; 14 | export default samehadakuScraper; 15 | -------------------------------------------------------------------------------- /src/helpers/setPayload.ts: -------------------------------------------------------------------------------- 1 | import type { Response } from "express"; 2 | import http from "http"; 3 | 4 | interface IPayloadProps { 5 | message?: string; 6 | data?: Record; 7 | pagination?: IPagination | undefined; 8 | } 9 | 10 | export default function setPayload(res: Response, props?: IPayloadProps): IPayload { 11 | return { 12 | statusCode: res.statusCode, 13 | statusMessage: http.STATUS_CODES[res.statusCode] || "", 14 | message: props?.message || "", 15 | data: props?.data || null, 16 | pagination: props?.pagination || null, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/schemas/otakudesu.schema.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot"; 2 | 3 | const otakudesuSchema = { 4 | query: { 5 | animes: v.optional( 6 | v.object({ 7 | page: v.optional( 8 | v.pipe( 9 | v.string(), 10 | v.minLength(1), 11 | v.maxLength(6), 12 | v.regex(/^([1-9]\d*)$/, "invalid page") 13 | ) 14 | ), 15 | }) 16 | ), 17 | searchedAnimes: v.object({ 18 | q: v.pipe(v.string(), v.minLength(1), v.maxLength(50)), 19 | }), 20 | }, 21 | }; 22 | 23 | export default otakudesuSchema; 24 | -------------------------------------------------------------------------------- /src/scrapers/samehadaku.scraper.ts: -------------------------------------------------------------------------------- 1 | import samehadakuConfig from "@configs/samehadaku.config.js"; 2 | import getHTML from "@helpers/getHTML.js"; 3 | import { parse, type HTMLElement } from "node-html-parser"; 4 | 5 | const { baseUrl } = samehadakuConfig; 6 | 7 | const samehadakuScraper = { 8 | async scrapeDOM(pathname: string, ref?: string, sanitize: boolean = false): Promise { 9 | const html = await getHTML(baseUrl, pathname, ref, sanitize); 10 | const document = parse(html, { 11 | parseNoneClosedTags: true, 12 | }); 13 | 14 | return document; 15 | }, 16 | }; 17 | 18 | export default samehadakuScraper; 19 | -------------------------------------------------------------------------------- /dist/scrapers/kuramanime.scraper.js: -------------------------------------------------------------------------------- 1 | import kuramanimeConfig from "../configs/kuramanime.config.js"; 2 | import getHTML from "../helpers/getHTML.js"; 3 | import { parse } from "node-html-parser"; 4 | const { baseUrl } = kuramanimeConfig; 5 | const kuramanimeScraper = { 6 | async scrapeDOM(pathname, ref, sanitize = false) { 7 | const html = await getHTML(baseUrl, pathname, ref, sanitize); 8 | const document = parse(html, { 9 | parseNoneClosedTags: true, 10 | }); 11 | return document; 12 | }, 13 | async scrapeSecret(ref) { 14 | const text = await getHTML(baseUrl, "/assets/Ks6sqSgloPTlHMl.txt", ref); 15 | return text; 16 | }, 17 | }; 18 | export default kuramanimeScraper; 19 | -------------------------------------------------------------------------------- /src/scrapers/kuramanime.scraper.ts: -------------------------------------------------------------------------------- 1 | import kuramanimeConfig from "@configs/kuramanime.config.js"; 2 | import getHTML from "@helpers/getHTML.js"; 3 | import { parse, type HTMLElement } from "node-html-parser"; 4 | 5 | const { baseUrl } = kuramanimeConfig; 6 | 7 | const kuramanimeScraper = { 8 | async scrapeDOM(pathname: string, ref?: string, sanitize: boolean = false): Promise { 9 | const html = await getHTML(baseUrl, pathname, ref, sanitize); 10 | const document = parse(html, { 11 | parseNoneClosedTags: true, 12 | }); 13 | 14 | return document; 15 | }, 16 | 17 | async scrapeSecret(ref?: string): Promise { 18 | const text = await getHTML(baseUrl, "/assets/Ks6sqSgloPTlHMl.txt", ref); 19 | 20 | return text; 21 | }, 22 | }; 23 | 24 | export default kuramanimeScraper; 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js 20 Alpine image as a parent image for building 2 | FROM node:20-alpine AS builder 3 | 4 | # Set the working directory in the container 5 | WORKDIR /usr/src/app 6 | 7 | # Copy package.json and package-lock.json to the working directory 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application code 14 | COPY . . 15 | 16 | # Build the TypeScript project 17 | RUN npm run build 18 | 19 | # Use a smaller image for the runtime 20 | FROM node:20-alpine 21 | 22 | # Set the working directory in the container 23 | WORKDIR /usr/src/app 24 | 25 | # Copy the built application from the builder stage 26 | COPY --from=builder /usr/src/app/dist ./dist 27 | COPY --from=builder /usr/src/app/package*.json ./ 28 | 29 | # Install only production dependencies 30 | RUN npm install --only=production 31 | 32 | # Expose the port the app runs on 33 | EXPOSE 3001 34 | 35 | # Command to run the application 36 | CMD ["node", "dist/index.js"] 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wajik-anime-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "tsx watch -r tsconfig-paths/register src/index.ts", 9 | "build": "rimraf dist && tsc && tsc-alias", 10 | "start": "node dist/index.js" 11 | }, 12 | "engines": { 13 | "node": "20 || >=22" 14 | }, 15 | "keywords": [], 16 | "author": "wajik45", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@types/cors": "^2.8.17", 20 | "@types/express": "^4.17.21", 21 | "@types/node": "^22.8.6", 22 | "@types/sanitize-html": "^2.16.0", 23 | "rimraf": "^6.0.1", 24 | "tsc-alias": "^1.8.10", 25 | "tsconfig-paths": "^4.2.0", 26 | "tsx": "^4.20.6", 27 | "typescript": "^5.9.3" 28 | }, 29 | "dependencies": { 30 | "cors": "^2.8.5", 31 | "express": "^4.21.1", 32 | "lru-cache": "^11.0.1", 33 | "node-html-parser": "^7.0.1", 34 | "sanitize-html": "^2.17.0", 35 | "valibot": "^1.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 wajik45 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 | -------------------------------------------------------------------------------- /dist/routes/kuramanime.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { serverCache } from "../middlewares/cache.js"; 3 | import kuramanimeController from "../controllers/kuramanime.controller.js"; 4 | const kuramanimeRouter = Router(); 5 | kuramanimeRouter.get("/", kuramanimeController.getRoot); 6 | kuramanimeRouter.get("/home", serverCache(10), kuramanimeController.getHome); 7 | kuramanimeRouter.get("/anime", serverCache(10), kuramanimeController.getAnimes); 8 | kuramanimeRouter.get("/schedule", serverCache(10), kuramanimeController.getScheduledAnimes); 9 | kuramanimeRouter.get("/properties/:propertyType", serverCache(10), kuramanimeController.getProperties); 10 | kuramanimeRouter.get("/properties/:propertyType/:propertyId", serverCache(10), kuramanimeController.getAnimesByProperty); 11 | kuramanimeRouter.get("/anime/:animeId/:animeSlug", serverCache(10), kuramanimeController.getAnimeDetails); 12 | kuramanimeRouter.get("/batch/:animeId/:animeSlug/:batchId", serverCache(10), kuramanimeController.getBatchDetails); 13 | kuramanimeRouter.get("/episode/:animeId/:animeSlug/:episodeId", serverCache(10), kuramanimeController.getEpisodeDetails); 14 | export default kuramanimeRouter; 15 | -------------------------------------------------------------------------------- /src/routes/kuramanime.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { serverCache } from "@middlewares/cache.js"; 3 | import kuramanimeController from "@controllers/kuramanime.controller.js"; 4 | 5 | const kuramanimeRouter = Router(); 6 | 7 | kuramanimeRouter.get("/", kuramanimeController.getRoot); 8 | kuramanimeRouter.get("/home", serverCache(10), kuramanimeController.getHome); 9 | kuramanimeRouter.get("/anime", serverCache(10), kuramanimeController.getAnimes); 10 | kuramanimeRouter.get("/schedule", serverCache(10), kuramanimeController.getScheduledAnimes); 11 | kuramanimeRouter.get( 12 | "/properties/:propertyType", 13 | serverCache(10), 14 | kuramanimeController.getProperties 15 | ); 16 | kuramanimeRouter.get( 17 | "/properties/:propertyType/:propertyId", 18 | serverCache(10), 19 | kuramanimeController.getAnimesByProperty 20 | ); 21 | kuramanimeRouter.get( 22 | "/anime/:animeId/:animeSlug", 23 | serverCache(10), 24 | kuramanimeController.getAnimeDetails 25 | ); 26 | kuramanimeRouter.get( 27 | "/batch/:animeId/:animeSlug/:batchId", 28 | serverCache(10), 29 | kuramanimeController.getBatchDetails 30 | ); 31 | kuramanimeRouter.get( 32 | "/episode/:animeId/:animeSlug/:episodeId", 33 | serverCache(10), 34 | kuramanimeController.getEpisodeDetails 35 | ); 36 | 37 | export default kuramanimeRouter; 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { clientCache } from "@middlewares/cache.js"; 2 | import appConfig from "@configs/app.config.js"; 3 | import express from "express"; 4 | import errorHandler from "@middlewares/errorHandler.js"; 5 | import otakudesuRouter from "@routes/otakudesu.routes.js"; 6 | import samehadakuRouter from "@routes/samehadaku.routes.js"; 7 | import kuramanimeRouter from "@routes/kuramanime.routes.js"; 8 | import setPayload from "@helpers/setPayload.js"; 9 | 10 | const { PORT } = appConfig; 11 | const app = express(); 12 | 13 | app.use(clientCache(1)); 14 | 15 | app.get("/", (req, res) => { 16 | const routes: IRouteData[] = [ 17 | { 18 | method: "GET", 19 | path: "/otakudesu", 20 | description: "Otakudesu", 21 | pathParams: [], 22 | queryParams: [], 23 | }, 24 | { 25 | method: "GET", 26 | path: "/kuramanime", 27 | description: "Kuramanime", 28 | pathParams: [], 29 | queryParams: [], 30 | }, 31 | ]; 32 | 33 | res.json( 34 | setPayload(res, { 35 | data: { routes }, 36 | }) 37 | ); 38 | }); 39 | 40 | app.use("/otakudesu", otakudesuRouter); 41 | app.use("/kuramanime", kuramanimeRouter); 42 | app.use("/samehadaku", samehadakuRouter); 43 | 44 | app.use(errorHandler); 45 | 46 | app.listen(PORT, () => { 47 | console.log(`server is running on http://localhost:${PORT}`); 48 | }); 49 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | import { clientCache } from "./middlewares/cache.js"; 2 | import appConfig from "./configs/app.config.js"; 3 | import express from "express"; 4 | import errorHandler from "./middlewares/errorHandler.js"; 5 | import otakudesuRouter from "./routes/otakudesu.routes.js"; 6 | import samehadakuRouter from "./routes/samehadaku.routes.js"; 7 | import kuramanimeRouter from "./routes/kuramanime.routes.js"; 8 | import setPayload from "./helpers/setPayload.js"; 9 | const { PORT } = appConfig; 10 | const app = express(); 11 | app.use(clientCache(1)); 12 | app.get("/", (req, res) => { 13 | const routes = [ 14 | { 15 | method: "GET", 16 | path: "/otakudesu", 17 | description: "Otakudesu", 18 | pathParams: [], 19 | queryParams: [], 20 | }, 21 | { 22 | method: "GET", 23 | path: "/kuramanime", 24 | description: "Kuramanime", 25 | pathParams: [], 26 | queryParams: [], 27 | }, 28 | ]; 29 | res.json(setPayload(res, { 30 | data: { routes }, 31 | })); 32 | }); 33 | app.use("/otakudesu", otakudesuRouter); 34 | app.use("/kuramanime", kuramanimeRouter); 35 | app.use("/samehadaku", samehadakuRouter); 36 | app.use(errorHandler); 37 | app.listen(PORT, () => { 38 | console.log(`server is running on http://localhost:${PORT}`); 39 | }); 40 | -------------------------------------------------------------------------------- /dist/middlewares/cache.js: -------------------------------------------------------------------------------- 1 | import { LRUCache } from "lru-cache"; 2 | import path from "path"; 3 | const defaultTTL = 1000 * 60 * 60 * 12; 4 | const lruCache = new LRUCache({ 5 | max: 100, 6 | allowStale: false, 7 | updateAgeOnGet: false, 8 | updateAgeOnHas: false, 9 | ttl: defaultTTL, 10 | }); 11 | /** 12 | * @param ttl minutes, default = 720 13 | */ 14 | export function serverCache(ttl) { 15 | return (req, res, next) => { 16 | const newTTL = ttl ? 1000 * 60 * ttl : defaultTTL; 17 | const key = path.join(req.originalUrl, "/").replace(/\\/g, "/"); 18 | const cachedData = lruCache.get(key); 19 | if (cachedData) { 20 | // console.log("hit"); 21 | res.json(cachedData); 22 | return; 23 | } 24 | // console.log("miss"); 25 | const originalJson = res.json.bind(res); 26 | res.json = (body) => { 27 | if (res.statusCode < 399) { 28 | lruCache.set(key, body, { ttl: newTTL }); 29 | } 30 | return originalJson(body); 31 | }; 32 | next(); 33 | }; 34 | } 35 | /** 36 | * @param maxAge minutes, default = 1 37 | */ 38 | export function clientCache(maxAge) { 39 | return (req, res, next) => { 40 | res.setHeader("Cache-Control", `public, max-age=${maxAge ? maxAge * 60 : 60}`); 41 | next(); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface IAppConfig { 2 | PORT: number; 3 | sourceUrl: boolean; 4 | } 5 | 6 | interface IAnimeConfig { 7 | baseUrl: string; 8 | } 9 | 10 | interface IPagination { 11 | currentPage: number | null; 12 | prevPage: number | null; 13 | hasPrevPage: boolean; 14 | nextPage: number | null; 15 | hasNextPage: boolean; 16 | totalPages: number | null; 17 | } 18 | 19 | interface IPayload { 20 | statusCode: number; 21 | statusMessage: string; 22 | message: string; 23 | data: Record | null; 24 | pagination: TPagination | null; 25 | } 26 | 27 | interface ISynopsis { 28 | paragraphList: string[]; 29 | } 30 | 31 | interface IUrl { 32 | title: string; 33 | url: string; 34 | } 35 | 36 | interface IServer { 37 | title: string; 38 | serverId: string; 39 | } 40 | 41 | interface IQuality { 42 | title: string; 43 | size?: string; 44 | urlList?: IUrl[]; 45 | serverList?: IServer[]; 46 | } 47 | 48 | interface IFormat { 49 | title: string; 50 | qualityList: IQuality[]; 51 | } 52 | 53 | interface IRouteData { 54 | path: string; 55 | method: string; 56 | description: string; 57 | pathParams: { 58 | key: string; 59 | value: string; 60 | defaultValue: string | null; 61 | required: boolean; 62 | }[]; 63 | queryParams: { 64 | key: string; 65 | value: string; 66 | defaultValue: string | null; 67 | required: boolean; 68 | }[]; 69 | } 70 | -------------------------------------------------------------------------------- /dist/parsers/main/main.parser.js: -------------------------------------------------------------------------------- 1 | import appConfig from "../../configs/app.config.js"; 2 | const { sourceUrl } = appConfig; 3 | const mainParser = { 4 | Text(el, regexp) { 5 | const text = el?.text; 6 | if (regexp && text) { 7 | const match = text.match(regexp); 8 | if (match) { 9 | return match[1]?.trim() || ""; 10 | } 11 | } 12 | return text?.trim() || ""; 13 | }, 14 | Id(el) { 15 | const url = el?.getAttribute("href"); 16 | if (url) { 17 | const arr = url.split("/").filter((str) => str); 18 | return arr[arr.length - 1] || ""; 19 | } 20 | return ""; 21 | }, 22 | Src(el) { 23 | return el?.getAttribute("data-src") || el?.getAttribute("src") || ""; 24 | }, 25 | Num(el, regexp) { 26 | let text = el?.text; 27 | if (regexp && text) { 28 | const match = text.match(regexp); 29 | if (match) { 30 | return Number(match[1]?.trim()) || null; 31 | } 32 | } 33 | return Number(text?.replace(/\,/g, "").trim()) || null; 34 | }, 35 | Attr(el, attribute) { 36 | return el?.getAttribute(attribute) || ""; 37 | }, 38 | AnimeSrc(el, baseUrl) { 39 | const text = el?.getAttribute("href"); 40 | return sourceUrl ? (baseUrl ? new URL(text || "", baseUrl).toString() : text || "") : undefined; 41 | }, 42 | }; 43 | export default mainParser; 44 | -------------------------------------------------------------------------------- /src/middlewares/cache.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction } from "express"; 2 | import { LRUCache } from "lru-cache"; 3 | import path from "path"; 4 | 5 | const defaultTTL = 1000 * 60 * 60 * 12; 6 | const lruCache = new LRUCache({ 7 | max: 100, 8 | allowStale: false, 9 | updateAgeOnGet: false, 10 | updateAgeOnHas: false, 11 | ttl: defaultTTL, 12 | }); 13 | 14 | /** 15 | * @param ttl minutes, default = 720 16 | */ 17 | export function serverCache(ttl?: number) { 18 | return (req: Request, res: Response, next: NextFunction) => { 19 | const newTTL = ttl ? 1000 * 60 * ttl : defaultTTL; 20 | const key = path.join(req.originalUrl, "/").replace(/\\/g, "/"); 21 | const cachedData = lruCache.get(key); 22 | 23 | if (cachedData) { 24 | // console.log("hit"); 25 | 26 | res.json(cachedData); 27 | 28 | return; 29 | } 30 | 31 | // console.log("miss"); 32 | 33 | const originalJson = res.json.bind(res); 34 | 35 | res.json = (body: IPayload) => { 36 | if (res.statusCode < 399) { 37 | lruCache.set(key, body, { ttl: newTTL }); 38 | } 39 | 40 | return originalJson(body); 41 | }; 42 | 43 | next(); 44 | }; 45 | } 46 | 47 | /** 48 | * @param maxAge minutes, default = 1 49 | */ 50 | export function clientCache(maxAge?: number) { 51 | return (req: Request, res: Response, next: NextFunction) => { 52 | res.setHeader("Cache-Control", `public, max-age=${maxAge ? maxAge * 60 : 60}`); 53 | 54 | next(); 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /dist/routes/otakudesu.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { serverCache } from "../middlewares/cache.js"; 3 | import otakudesuController from "../controllers/otakudesu.controller.js"; 4 | const otakudesuRouter = Router(); 5 | otakudesuRouter.get("/", otakudesuController.getRoot); 6 | otakudesuRouter.get("/home", serverCache(10), otakudesuController.getHome); 7 | otakudesuRouter.get("/schedule", serverCache(10), otakudesuController.getSchedule); 8 | otakudesuRouter.get("/anime", serverCache(10), otakudesuController.getAllAnimes); 9 | otakudesuRouter.get("/genre", serverCache(10), otakudesuController.getAllGenres); 10 | otakudesuRouter.get("/ongoing", serverCache(10), otakudesuController.getOngoingAnimes); 11 | otakudesuRouter.get("/completed", serverCache(10), otakudesuController.getCompletedAnimes); 12 | otakudesuRouter.get("/search", serverCache(10), otakudesuController.getSearchedAnimes); 13 | otakudesuRouter.get("/genre/:genreId", serverCache(10), otakudesuController.getAnimesByGenre); 14 | otakudesuRouter.get("/batch/:batchId", serverCache(10), otakudesuController.getBatchDetails); 15 | otakudesuRouter.get("/anime/:animeId", serverCache(10), otakudesuController.getAnimeDetails); 16 | otakudesuRouter.get("/episode/:episodeId", serverCache(10), otakudesuController.getEpisodeDetails); 17 | otakudesuRouter.get("/server/:serverId", serverCache(10), otakudesuController.getServerDetails); 18 | otakudesuRouter.post("/server/:serverId", serverCache(10), otakudesuController.getServerDetails); 19 | export default otakudesuRouter; 20 | -------------------------------------------------------------------------------- /src/routes/otakudesu.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { serverCache } from "@middlewares/cache.js"; 3 | import otakudesuController from "@controllers/otakudesu.controller.js"; 4 | 5 | const otakudesuRouter = Router(); 6 | 7 | otakudesuRouter.get("/", otakudesuController.getRoot); 8 | otakudesuRouter.get("/home", serverCache(10), otakudesuController.getHome); 9 | otakudesuRouter.get("/schedule", serverCache(10), otakudesuController.getSchedule); 10 | otakudesuRouter.get("/anime", serverCache(10), otakudesuController.getAllAnimes); 11 | otakudesuRouter.get("/genre", serverCache(10), otakudesuController.getAllGenres); 12 | otakudesuRouter.get("/ongoing", serverCache(10), otakudesuController.getOngoingAnimes); 13 | otakudesuRouter.get("/completed", serverCache(10), otakudesuController.getCompletedAnimes); 14 | otakudesuRouter.get("/search", serverCache(10), otakudesuController.getSearchedAnimes); 15 | otakudesuRouter.get("/genre/:genreId", serverCache(10), otakudesuController.getAnimesByGenre); 16 | otakudesuRouter.get("/batch/:batchId", serverCache(10), otakudesuController.getBatchDetails); 17 | otakudesuRouter.get("/anime/:animeId", serverCache(10), otakudesuController.getAnimeDetails); 18 | otakudesuRouter.get("/episode/:episodeId", serverCache(10), otakudesuController.getEpisodeDetails); 19 | otakudesuRouter.get("/server/:serverId", serverCache(10), otakudesuController.getServerDetails); 20 | otakudesuRouter.post("/server/:serverId", serverCache(10), otakudesuController.getServerDetails); 21 | 22 | export default otakudesuRouter; 23 | -------------------------------------------------------------------------------- /dist/scrapers/otakudesu.scraper.js: -------------------------------------------------------------------------------- 1 | import otakudesuConfig from "../configs/otakudesu.config.js"; 2 | import getHTML from "../helpers/getHTML.js"; 3 | import { parse } from "node-html-parser"; 4 | const { baseUrl } = otakudesuConfig; 5 | const otakudesuScraper = { 6 | async scrapeDOM(pathname, ref, sanitize = false) { 7 | const html = await getHTML(baseUrl, pathname, ref, sanitize); 8 | const document = parse(html, { 9 | parseNoneClosedTags: true, 10 | }); 11 | return document; 12 | }, 13 | async scrapeNonce(body, referer) { 14 | const nonceResponse = await fetch(new URL("/wp-admin/admin-ajax.php", baseUrl), { 15 | method: "POST", 16 | body, 17 | headers: { 18 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 19 | Referer: referer, 20 | Origin: baseUrl, 21 | }, 22 | }); 23 | const nonce = (await nonceResponse.json()); 24 | return nonce; 25 | }, 26 | async scrapeServer(body, referer) { 27 | const serverResponse = await fetch(new URL("/wp-admin/admin-ajax.php", baseUrl), { 28 | method: "POST", 29 | body, 30 | headers: { 31 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 32 | Origin: baseUrl, 33 | Referer: referer, 34 | }, 35 | }); 36 | const server = (await serverResponse.json()); 37 | return server; 38 | }, 39 | }; 40 | export default otakudesuScraper; 41 | -------------------------------------------------------------------------------- /src/scrapers/otakudesu.scraper.ts: -------------------------------------------------------------------------------- 1 | import otakudesuConfig from "@configs/otakudesu.config.js"; 2 | import getHTML from "@helpers/getHTML.js"; 3 | import { parse, type HTMLElement } from "node-html-parser"; 4 | 5 | const { baseUrl } = otakudesuConfig; 6 | 7 | const otakudesuScraper = { 8 | async scrapeDOM(pathname: string, ref?: string, sanitize: boolean = false): Promise { 9 | const html = await getHTML(baseUrl, pathname, ref, sanitize); 10 | const document = parse(html, { 11 | parseNoneClosedTags: true, 12 | }); 13 | 14 | return document; 15 | }, 16 | 17 | async scrapeNonce(body: string, referer: string): Promise<{ data?: string }> { 18 | const nonceResponse = await fetch(new URL("/wp-admin/admin-ajax.php", baseUrl), { 19 | method: "POST", 20 | body, 21 | headers: { 22 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 23 | Referer: referer, 24 | Origin: baseUrl, 25 | }, 26 | }); 27 | 28 | const nonce = (await nonceResponse.json()) as { data: string }; 29 | 30 | return nonce; 31 | }, 32 | 33 | async scrapeServer(body: string, referer: string): Promise<{ data?: string }> { 34 | const serverResponse = await fetch(new URL("/wp-admin/admin-ajax.php", baseUrl), { 35 | method: "POST", 36 | body, 37 | headers: { 38 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 39 | Origin: baseUrl, 40 | Referer: referer, 41 | }, 42 | }); 43 | 44 | const server = (await serverResponse.json()) as { data: string }; 45 | 46 | return server; 47 | }, 48 | }; 49 | 50 | export default otakudesuScraper; 51 | -------------------------------------------------------------------------------- /src/parsers/main/main.parser.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLElement } from "node-html-parser"; 2 | import appConfig from "@configs/app.config.js"; 3 | 4 | const { sourceUrl } = appConfig; 5 | 6 | const mainParser = { 7 | Text(el: HTMLElement | null | undefined | undefined, regexp?: RegExp): string { 8 | const text = el?.text; 9 | 10 | if (regexp && text) { 11 | const match = text.match(regexp); 12 | 13 | if (match) { 14 | return match[1]?.trim() || ""; 15 | } 16 | } 17 | 18 | return text?.trim() || ""; 19 | }, 20 | 21 | Id(el: HTMLElement | null | undefined): string { 22 | const url = el?.getAttribute("href"); 23 | 24 | if (url) { 25 | const arr = url.split("/").filter((str) => str); 26 | 27 | return arr[arr.length - 1] || ""; 28 | } 29 | 30 | return ""; 31 | }, 32 | 33 | Src(el: HTMLElement | null | undefined): string { 34 | return el?.getAttribute("data-src") || el?.getAttribute("src") || ""; 35 | }, 36 | 37 | Num(el: HTMLElement | null | undefined, regexp?: RegExp): number | null { 38 | let text = el?.text; 39 | 40 | if (regexp && text) { 41 | const match = text.match(regexp); 42 | 43 | if (match) { 44 | return Number(match[1]?.trim()) || null; 45 | } 46 | } 47 | 48 | return Number(text?.replace(/\,/g, "").trim()) || null; 49 | }, 50 | 51 | Attr(el: HTMLElement | null | undefined, attribute: string): string { 52 | return el?.getAttribute(attribute) || ""; 53 | }, 54 | 55 | AnimeSrc(el: HTMLElement | null | undefined, baseUrl?: string | undefined): string | undefined { 56 | const text = el?.getAttribute("href"); 57 | 58 | return sourceUrl ? (baseUrl ? new URL(text || "", baseUrl).toString() : text || "") : undefined; 59 | }, 60 | }; 61 | 62 | export default mainParser; 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Visit https://aka.ms/tsconfig to read more about this file 3 | "compilerOptions": { 4 | // File Layout 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | 8 | // Environment Settings 9 | // See also https://aka.ms/tsconfig/module 10 | "module": "nodenext", 11 | "target": "esnext", 12 | 13 | // For nodejs: 14 | "lib": ["esnext"], 15 | "types": ["node"], 16 | // and npm install -D @types/node 17 | 18 | // Other Outputs 19 | // "sourceMap": true, 20 | // "declaration": true, 21 | // "declarationMap": true, 22 | 23 | // Stricter Typechecking Options 24 | "noUncheckedIndexedAccess": true, 25 | "exactOptionalPropertyTypes": true, 26 | 27 | // Style Options 28 | // "noImplicitReturns": true, 29 | // "noImplicitOverride": true, 30 | // "noUnusedLocals": true, 31 | // "noUnusedParameters": true, 32 | // "noFallthroughCasesInSwitch": true, 33 | // "noPropertyAccessFromIndexSignature": true, 34 | 35 | // Recommended Options 36 | "strict": true, 37 | "jsx": "react-jsx", 38 | "verbatimModuleSyntax": true, 39 | "isolatedModules": true, 40 | "noUncheckedSideEffectImports": true, 41 | "moduleDetection": "force", 42 | "skipLibCheck": true, 43 | 44 | "paths": { 45 | "@configs/*": ["./src/configs/*"], 46 | "@controllers/*": ["./src/controllers/*"], 47 | "@helpers/*": ["./src/helpers/*"], 48 | "@interfaces/*": ["./src/interfaces/*"], 49 | "@middlewares/*": ["./src/middlewares/*"], 50 | "@parsers/*": ["./src/parsers/*"], 51 | "@routes/*": ["./src/routes/*"], 52 | "@schemas/*": ["./src/schemas/*"], 53 | "@scrapers/*": ["./src/scrapers/*"] 54 | } 55 | }, 56 | "include": ["src/**/*"], 57 | "exclude": ["node_modules", "dist"] 58 | } 59 | -------------------------------------------------------------------------------- /dist/middlewares/errorHandler.js: -------------------------------------------------------------------------------- 1 | import { ValiError } from "valibot"; 2 | import setPayload from "../helpers/setPayload.js"; 3 | // import { v4 as uuidv4 } from "uuid"; 4 | // import path from "path"; 5 | // import fs from "fs"; 6 | // function logErrorToFile(err: any, req: Request, requestId: string): void { 7 | // const dir = path.join(__dirname, "..", "..", "logs"); 8 | // if (!fs.existsSync(dir)) { 9 | // fs.mkdirSync(dir); 10 | // } 11 | // function convertTimestamp(timestamp: Date) { 12 | // const date = new Date(timestamp); 13 | // const hours = String(date.getHours()).padStart(2, "0"); 14 | // const minutes = String(date.getMinutes()).padStart(2, "0"); 15 | // const day = String(date.getDate()).padStart(2, "0"); 16 | // const month = String(date.getMonth() + 1).padStart(2, "0"); 17 | // const year = date.getFullYear().toString().slice(2); 18 | // return `${hours}:${minutes} ${day}-${month}-${year}`; 19 | // } 20 | // const logFilePath = path.join(dir, "error.log"); 21 | // const logMessage = `\n[${convertTimestamp(new Date())}] Request ID: ${requestId}\n${req.method} ${ 22 | // req.url 23 | // } ('Internal Server Error')\nError: ${err.message || ""}\nStack: ${err.stack || ""}\n`; 24 | // fs.appendFileSync(logFilePath, logMessage); 25 | // } 26 | export default function errorHandler(err, req, res, next) { 27 | if (err instanceof ValiError) { 28 | res.status(400).json(setPayload(res, { message: err.issues[0]?.message })); 29 | return; 30 | } 31 | if (typeof err.status === "number" && err.status < 500) { 32 | res.status(err.status).json(setPayload(res, { message: err.message })); 33 | return; 34 | } 35 | // const uuid = uuidv4(); 36 | // logErrorToFile(err, req, uuid); 37 | res.status(500).json(setPayload(res, { 38 | // message: `Terjadi kesalahan tak terduga. Request ID: ${uuid}`, 39 | message: `Terjadi kesalahan tak terduga`, 40 | })); 41 | } 42 | -------------------------------------------------------------------------------- /src/helpers/getHTML.ts: -------------------------------------------------------------------------------- 1 | import errorinCuy from "./errorinCuy.js"; 2 | import sanitizeHtml from "sanitize-html"; 3 | 4 | export const userAgent = 5 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"; 6 | 7 | export default async function getHTML( 8 | baseUrl: string, 9 | pathname: string, 10 | ref?: string, 11 | sanitize = false 12 | ): Promise { 13 | const url = new URL(pathname, baseUrl); 14 | const headers: Record = { 15 | "User-Agent": userAgent, 16 | }; 17 | 18 | if (ref) { 19 | headers.Refferer = ref.startsWith("http") ? ref : new URL(ref, baseUrl).toString(); 20 | } 21 | 22 | const response = await fetch(url, { headers, redirect: "manual" }); 23 | 24 | if (!response.ok) { 25 | response.status > 399 ? errorinCuy(response.status) : errorinCuy(404); 26 | } 27 | 28 | const html = await response.text(); 29 | 30 | if (!html.trim()) errorinCuy(404); 31 | 32 | if (sanitize) { 33 | return sanitizeHtml(html, { 34 | allowedTags: [ 35 | "address", 36 | "article", 37 | "aside", 38 | "footer", 39 | "header", 40 | "h1", 41 | "h2", 42 | "h3", 43 | "h4", 44 | "h5", 45 | "h6", 46 | "main", 47 | "nav", 48 | "section", 49 | "blockquote", 50 | "div", 51 | "dl", 52 | "figcaption", 53 | "figure", 54 | "hr", 55 | "li", 56 | "main", 57 | "ol", 58 | "p", 59 | "pre", 60 | "ul", 61 | "a", 62 | "abbr", 63 | "b", 64 | "br", 65 | "code", 66 | "data", 67 | "em", 68 | "i", 69 | "mark", 70 | "span", 71 | "strong", 72 | "sub", 73 | "sup", 74 | "time", 75 | "u", 76 | "img", 77 | ], 78 | allowedAttributes: { 79 | a: ["href", "name", "target"], 80 | img: ["src"], 81 | "*": ["class", "id"], 82 | }, 83 | }); 84 | } 85 | 86 | return html as string; 87 | } 88 | -------------------------------------------------------------------------------- /src/middlewares/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction } from "express"; 2 | import { ValiError } from "valibot"; 3 | import setPayload from "@helpers/setPayload.js"; 4 | // import { v4 as uuidv4 } from "uuid"; 5 | // import path from "path"; 6 | // import fs from "fs"; 7 | 8 | // function logErrorToFile(err: any, req: Request, requestId: string): void { 9 | // const dir = path.join(__dirname, "..", "..", "logs"); 10 | 11 | // if (!fs.existsSync(dir)) { 12 | // fs.mkdirSync(dir); 13 | // } 14 | 15 | // function convertTimestamp(timestamp: Date) { 16 | // const date = new Date(timestamp); 17 | // const hours = String(date.getHours()).padStart(2, "0"); 18 | // const minutes = String(date.getMinutes()).padStart(2, "0"); 19 | // const day = String(date.getDate()).padStart(2, "0"); 20 | // const month = String(date.getMonth() + 1).padStart(2, "0"); 21 | // const year = date.getFullYear().toString().slice(2); 22 | 23 | // return `${hours}:${minutes} ${day}-${month}-${year}`; 24 | // } 25 | 26 | // const logFilePath = path.join(dir, "error.log"); 27 | // const logMessage = `\n[${convertTimestamp(new Date())}] Request ID: ${requestId}\n${req.method} ${ 28 | // req.url 29 | // } ('Internal Server Error')\nError: ${err.message || ""}\nStack: ${err.stack || ""}\n`; 30 | 31 | // fs.appendFileSync(logFilePath, logMessage); 32 | // } 33 | 34 | export default function errorHandler( 35 | err: any, 36 | req: Request, 37 | res: Response, 38 | next: NextFunction 39 | ): void { 40 | if (err instanceof ValiError) { 41 | res.status(400).json(setPayload(res, { message: err.issues[0]?.message })); 42 | 43 | return; 44 | } 45 | 46 | if (typeof err.status === "number" && err.status < 500) { 47 | res.status(err.status).json(setPayload(res, { message: err.message })); 48 | 49 | return; 50 | } 51 | 52 | // const uuid = uuidv4(); 53 | 54 | // logErrorToFile(err, req, uuid); 55 | 56 | res.status(500).json( 57 | setPayload(res, { 58 | // message: `Terjadi kesalahan tak terduga. Request ID: ${uuid}`, 59 | message: `Terjadi kesalahan tak terduga`, 60 | }) 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/schemas/kuramanime.schema.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot"; 2 | 3 | const propertyType = v.union([ 4 | v.literal("genre"), 5 | v.literal("season"), 6 | v.literal("studio"), 7 | v.literal("type"), 8 | v.literal("quality"), 9 | v.literal("source"), 10 | v.literal("country"), 11 | ]); 12 | 13 | const sort = v.optional( 14 | v.union([ 15 | v.literal("a-z"), 16 | v.literal("z-a"), 17 | v.literal("oldest"), 18 | v.literal("latest"), 19 | v.literal("popular"), 20 | v.literal("most_viewed"), 21 | v.literal("updated"), 22 | ]) 23 | ); 24 | 25 | const page = v.optional( 26 | v.pipe(v.string(), v.minLength(1), v.maxLength(6), v.regex(/^([1-9]\d*)$/, "invalid page")) 27 | ); 28 | 29 | const kuramanimeSchema = { 30 | query: { 31 | animes: v.object({ 32 | search: v.optional(v.string()), 33 | status: v.optional( 34 | v.union([ 35 | v.literal("ongoing"), 36 | v.literal("completed"), 37 | v.literal("upcoming"), 38 | v.literal("movie"), 39 | ]) 40 | ), 41 | sort, 42 | page, 43 | }), 44 | animesByPropertyId: v.object({ 45 | sort, 46 | page, 47 | }), 48 | scheduledAnimes: v.optional( 49 | v.object({ 50 | day: v.optional( 51 | v.union([ 52 | v.literal("all"), 53 | v.literal("random"), 54 | v.literal("monday"), 55 | v.literal("tuesday"), 56 | v.literal("wednesday"), 57 | v.literal("thursday"), 58 | v.literal("friday"), 59 | v.literal("saturday"), 60 | v.literal("sunday"), 61 | ]) 62 | ), 63 | page, 64 | }) 65 | ), 66 | }, 67 | param: { 68 | properties: v.object({ 69 | propertyType, 70 | }), 71 | animesByPropertyId: v.object({ 72 | propertyType, 73 | propertyId: v.string(), 74 | }), 75 | animeDetails: v.object({ 76 | animeId: v.string(), 77 | animeSlug: v.string(), 78 | }), 79 | batchDetails: v.object({ 80 | batchId: v.string(), 81 | animeId: v.string(), 82 | animeSlug: v.string(), 83 | }), 84 | episodeDetails: v.object({ 85 | episodeId: v.string(), 86 | animeId: v.string(), 87 | animeSlug: v.string(), 88 | }), 89 | }, 90 | }; 91 | 92 | export default kuramanimeSchema; 93 | -------------------------------------------------------------------------------- /dist/helpers/getHTML.js: -------------------------------------------------------------------------------- 1 | import errorinCuy from "./errorinCuy.js"; 2 | import sanitizeHtml from "sanitize-html"; 3 | export const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"; 4 | export default async function getHTML(baseUrl, pathname, ref, sanitize = false) { 5 | const url = new URL(pathname, baseUrl); 6 | const headers = { 7 | "User-Agent": userAgent, 8 | }; 9 | if (ref) { 10 | headers.Refferer = ref.startsWith("http") ? ref : new URL(ref, baseUrl).toString(); 11 | } 12 | const response = await fetch(url, { headers, redirect: "manual" }); 13 | if (!response.ok) { 14 | response.status > 399 ? errorinCuy(response.status) : errorinCuy(404); 15 | } 16 | const html = await response.text(); 17 | if (!html.trim()) 18 | errorinCuy(404); 19 | if (sanitize) { 20 | return sanitizeHtml(html, { 21 | allowedTags: [ 22 | "address", 23 | "article", 24 | "aside", 25 | "footer", 26 | "header", 27 | "h1", 28 | "h2", 29 | "h3", 30 | "h4", 31 | "h5", 32 | "h6", 33 | "main", 34 | "nav", 35 | "section", 36 | "blockquote", 37 | "div", 38 | "dl", 39 | "figcaption", 40 | "figure", 41 | "hr", 42 | "li", 43 | "main", 44 | "ol", 45 | "p", 46 | "pre", 47 | "ul", 48 | "a", 49 | "abbr", 50 | "b", 51 | "br", 52 | "code", 53 | "data", 54 | "em", 55 | "i", 56 | "mark", 57 | "span", 58 | "strong", 59 | "sub", 60 | "sup", 61 | "time", 62 | "u", 63 | "img", 64 | ], 65 | allowedAttributes: { 66 | a: ["href", "name", "target"], 67 | img: ["src"], 68 | "*": ["class", "id"], 69 | }, 70 | }); 71 | } 72 | return html; 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wajik-anime-api 2 | 3 | REST API streaming dan download Anime subtitle Indonesia dari berbagai sumber 4 | 5 | # Sumber: 6 | 7 | API ini unofficial jadi ga ada kaitan dengan sumber yang tersedia... 8 | 9 | 1. otakudesu: https://otakudesu.best 10 | 2. kuramanime: https://v8.kuramanime.tel 11 | 12 | - domain sering berubah jangan lupa pantau terus untuk edit url ada di di "src/configs/{source}.config.ts" 13 | 14 | # Installasi App 15 | 16 | - install NodeJS 20 || >=22 17 | - Jalankan perintah di terminal 18 | 19 | ```sh 20 | # clone repo 21 | git clone https://github.com/wajik45/wajik-anime-api.git 22 | 23 | # masuk repo 24 | cd wajik-anime-api 25 | 26 | # install dependensi 27 | npm install 28 | 29 | # menjalankan server mode development 30 | npm run dev 31 | ``` 32 | 33 | # Build App 34 | 35 | ```sh 36 | # build 37 | npm run build 38 | 39 | # menjalankan server 40 | npm start 41 | ``` 42 | 43 | - Server akan berjalan di http://localhost:3001 44 | 45 | # Routes 46 | 47 | | Endpoint | Description | 48 | | --------- | ------------------------------------------------------------------------------------------------ | 49 | | /{sumber} | Deskripsi ada di response sesuai dengan sumber, gunakan ext JSON Parser jika menggunakan browser | 50 | 51 | ### Contoh request 52 | 53 | ```js 54 | (async () => { 55 | const response = await fetch("http://localhost:3001/otakudesu/ongoing?page=1"); 56 | const result = await response.json(); 57 | 58 | console.log(result); 59 | })(); 60 | ``` 61 | 62 | ### Contoh response 63 | 64 | ```json 65 | { 66 | "statusCode": 200, 67 | "statusMessage": "OK", 68 | "message": "", 69 | "data": { 70 | "animeList": [ 71 | { 72 | "title": "Dr. Stone Season 3 Part 2", 73 | "poster": "https://otakudesu.cloud/wp-content/uploads/2024/01/Dr.-Stone-Season-3-Part-2-Sub-Indo.jpg", 74 | "episodes": "11", 75 | "animeId": "drstn-s3-p2-sub-indo", 76 | "latestReleaseDate": "05 Jan", 77 | "releaseDay": "Jum'at", 78 | "otakudesuUrl": "https://otakudesu.cloud/anime/drstn-s3-p2-sub-indo/" 79 | }, 80 | {"..."} 81 | ] 82 | }, 83 | "pagination": { 84 | "currentPage": 1, 85 | "prevPage": null, 86 | "hasPrevPage": false, 87 | "nextPage": 2, 88 | "hasNextPage": true, 89 | "totalPages": 4 90 | }, 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /dist/schemas/kuramanime.schema.js: -------------------------------------------------------------------------------- 1 | import * as v from "valibot"; 2 | const propertyType = v.union([ 3 | v.literal("genre"), 4 | v.literal("season"), 5 | v.literal("studio"), 6 | v.literal("type"), 7 | v.literal("quality"), 8 | v.literal("source"), 9 | v.literal("country"), 10 | ]); 11 | const sort = v.optional(v.union([ 12 | v.literal("a-z"), 13 | v.literal("z-a"), 14 | v.literal("oldest"), 15 | v.literal("latest"), 16 | v.literal("popular"), 17 | v.literal("most_viewed"), 18 | v.literal("updated"), 19 | ])); 20 | const page = v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(6), v.regex(/^([1-9]\d*)$/, "invalid page"))); 21 | const kuramanimeSchema = { 22 | query: { 23 | animes: v.object({ 24 | search: v.optional(v.string()), 25 | status: v.optional(v.union([ 26 | v.literal("ongoing"), 27 | v.literal("completed"), 28 | v.literal("upcoming"), 29 | v.literal("movie"), 30 | ])), 31 | sort, 32 | page, 33 | }), 34 | animesByPropertyId: v.object({ 35 | sort, 36 | page, 37 | }), 38 | scheduledAnimes: v.optional(v.object({ 39 | day: v.optional(v.union([ 40 | v.literal("all"), 41 | v.literal("random"), 42 | v.literal("monday"), 43 | v.literal("tuesday"), 44 | v.literal("wednesday"), 45 | v.literal("thursday"), 46 | v.literal("friday"), 47 | v.literal("saturday"), 48 | v.literal("sunday"), 49 | ])), 50 | page, 51 | })), 52 | }, 53 | param: { 54 | properties: v.object({ 55 | propertyType, 56 | }), 57 | animesByPropertyId: v.object({ 58 | propertyType, 59 | propertyId: v.string(), 60 | }), 61 | animeDetails: v.object({ 62 | animeId: v.string(), 63 | animeSlug: v.string(), 64 | }), 65 | batchDetails: v.object({ 66 | batchId: v.string(), 67 | animeId: v.string(), 68 | animeSlug: v.string(), 69 | }), 70 | episodeDetails: v.object({ 71 | episodeId: v.string(), 72 | animeId: v.string(), 73 | animeSlug: v.string(), 74 | }), 75 | }, 76 | }; 77 | export default kuramanimeSchema; 78 | -------------------------------------------------------------------------------- /src/interfaces/kuramanime.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IMainCard { 2 | title: string; 3 | kuramanimeUrl?: string | undefined; 4 | } 5 | 6 | // ----------------------------------------- 7 | 8 | export interface ITextAnimeCard extends IMainCard { 9 | animeId: string; 10 | animeSlug: string; 11 | } 12 | 13 | export interface ITextPropertyCard extends IMainCard { 14 | propertyId: string; 15 | } 16 | 17 | export interface ITextPropertyTypeCard extends ITextPropertyCard { 18 | propertyType: string; 19 | } 20 | 21 | export interface ITextBatchCard extends IMainCard { 22 | batchId: string; 23 | animeId: string; 24 | animeSlug: string; 25 | } 26 | 27 | export interface ITextEpisodeCard extends IMainCard { 28 | episodeId: string; 29 | animeId: string; 30 | animeSlug: string; 31 | } 32 | 33 | // ------------------------------------------- 34 | 35 | export interface IHome { 36 | ongoing: { 37 | kuramanimeUrl?: string | undefined; 38 | episodeList: IEpisodeCard[]; 39 | }; 40 | completed: { 41 | kuramanimeUrl?: string | undefined; 42 | animeList: IAnimeCard[]; 43 | }; 44 | movie: { 45 | kuramanimeUrl?: string | undefined; 46 | animeList: IAnimeCard[]; 47 | }; 48 | } 49 | 50 | export interface IAnimeCard extends ITextAnimeCard { 51 | poster: string; 52 | type: string; 53 | quality: string; 54 | highlight: string; 55 | } 56 | 57 | export interface IScheduledAnimeCard extends ITextAnimeCard { 58 | poster: string; 59 | type: string; 60 | quality: string; 61 | day: string; 62 | releaseTime: string; 63 | } 64 | 65 | export interface IEpisodeCard extends ITextEpisodeCard { 66 | poster: string; 67 | type: string; 68 | quality: string; 69 | episodes: string; 70 | totalEpisodes: string; 71 | } 72 | 73 | export interface IAnimeDetails { 74 | title: string; 75 | animeId: string; 76 | animeSlug: string; 77 | alternativeTitle: string; 78 | poster: string; 79 | episodes: string; 80 | aired: string; 81 | duration: string; 82 | explicit: string; 83 | score: string; 84 | fans: string; 85 | rating: string; 86 | credit: string; 87 | synopsis: ISynopsis; 88 | episode: { 89 | first: number | null; 90 | last: number | null; 91 | }; 92 | type: ITextPropertyTypeCard; 93 | status: ITextPropertyTypeCard; 94 | season: ITextPropertyTypeCard; 95 | quality: ITextPropertyTypeCard; 96 | country: ITextPropertyTypeCard; 97 | source: ITextPropertyTypeCard; 98 | genreList: ITextPropertyTypeCard[]; 99 | themeList: ITextPropertyTypeCard[]; 100 | demographicList: ITextPropertyTypeCard[]; 101 | studioList: ITextPropertyTypeCard[]; 102 | batchList: ITextBatchCard[]; 103 | similarAnimeList: ITextAnimeCard[]; 104 | } 105 | 106 | export interface IEpisodeDetails { 107 | title: string; 108 | episodeTitle: string; 109 | animeId: string; 110 | animeSlug: string; 111 | lastUpdated: string; 112 | hasPrevEpisode: boolean; 113 | prevEpisode: ITextEpisodeCard | null; 114 | hasNextEpisode: boolean; 115 | nextEpisode: ITextEpisodeCard | null; 116 | episode: { 117 | first: number; 118 | last: number; 119 | }; 120 | server: { qualityList: IQuality[] }; 121 | download: { qualityList: IQuality[] }; 122 | } 123 | 124 | export interface IBatchDetails { 125 | title: string; 126 | batchTitle: string; 127 | animeId: string; 128 | animeSlug: string; 129 | download: { qualityList: IQuality[] }; 130 | } 131 | -------------------------------------------------------------------------------- /dist/parsers/extra/otakudesu.extra.parser.js: -------------------------------------------------------------------------------- 1 | import * as T from "../../interfaces/otakudesu.interface.js"; 2 | import mainParser from "../main/main.parser.js"; 3 | import otakudesuConfig from "../../configs/otakudesu.config.js"; 4 | const { Text, Attr, Id, Num, Src, AnimeSrc } = mainParser; 5 | const { baseUrl } = otakudesuConfig; 6 | const otakudesuExtraParser = { 7 | parseOngoingCard(el) { 8 | const title = Text(el.querySelector("h2.jdlflm")); 9 | const poster = Src(el.querySelector(".thumbz img")); 10 | const episodes = Text(el.querySelector(".epz"), /Episode (\S+)/); 11 | const otakudesuUrl = AnimeSrc(el.querySelector(".thumb a")); 12 | const animeId = Id(el.querySelector(".thumb a")); 13 | const latestReleaseDate = Text(el.querySelector(".newnime")); 14 | const releaseDay = Text(el.querySelector(".epztipe")); 15 | return { 16 | title, 17 | poster, 18 | episodes, 19 | animeId, 20 | latestReleaseDate, 21 | releaseDay, 22 | otakudesuUrl, 23 | }; 24 | }, 25 | parseCompletedCard(el) { 26 | const title = Text(el.querySelector("h2.jdlflm")); 27 | const poster = Src(el.querySelector(".thumbz img")); 28 | const episodes = Text(el.querySelector(".epz"), /(\S+) Episode/); 29 | const otakudesuUrl = AnimeSrc(el.querySelector(".thumb a")); 30 | const animeId = Id(el.querySelector(".thumb a")); 31 | const score = Text(el.querySelector(".epztipe")); 32 | const lastReleaseDate = Text(el.querySelector(".newnime")); 33 | return { 34 | title, 35 | poster, 36 | episodes, 37 | animeId, 38 | score, 39 | lastReleaseDate, 40 | otakudesuUrl, 41 | }; 42 | }, 43 | parseTextCard(el) { 44 | const title = Text(el); 45 | const id = Id(el); 46 | const otakudesuUrl = AnimeSrc(el, baseUrl); 47 | return { 48 | title, 49 | id, 50 | otakudesuUrl, 51 | }; 52 | }, 53 | parseInfo(elems) { 54 | function getInfo(index) { 55 | const info = Text(elems[index]?.nextSibling, /:\s*(.+)/); 56 | return info; 57 | } 58 | return getInfo; 59 | }, 60 | parseTextGenreList(elems) { 61 | const genreList = elems.map((el) => { 62 | const { id, title, otakudesuUrl } = this.parseTextCard(el); 63 | return { 64 | title, 65 | genreId: id, 66 | otakudesuUrl, 67 | }; 68 | }); 69 | return genreList; 70 | }, 71 | parseTextEpisodeList(elems) { 72 | const episodeList = elems.map((el) => { 73 | const { id, title, otakudesuUrl } = otakudesuExtraParser.parseTextCard(el); 74 | const match = title.match(/Episode\s+(\d+)/); 75 | return { 76 | title: match ? match[1] || "0" : title, 77 | episodeId: id, 78 | otakudesuUrl, 79 | }; 80 | }); 81 | return episodeList; 82 | }, 83 | parseSynopsis(elems) { 84 | return { 85 | paragraphList: elems 86 | .map((el) => { 87 | const paragraph = el.text; 88 | return paragraph; 89 | }) 90 | .filter((paragraph) => { 91 | return paragraph; 92 | }), 93 | }; 94 | }, 95 | }; 96 | export default otakudesuExtraParser; 97 | -------------------------------------------------------------------------------- /src/interfaces/otakudesu.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IMainCard { 2 | title: string; 3 | otakudesuUrl?: string | undefined; 4 | } 5 | 6 | // ----------------------------------------- 7 | 8 | export interface ITextAnimeCard extends IMainCard { 9 | animeId: string; 10 | } 11 | 12 | export interface ITextBatchCard extends IMainCard { 13 | batchId: string; 14 | } 15 | 16 | export interface ITextEpisodeCard extends IMainCard { 17 | episodeId: string; 18 | } 19 | 20 | export interface ITextGenreCard extends IMainCard { 21 | genreId: string; 22 | } 23 | 24 | // ------------------------------------------- 25 | 26 | export interface IHome { 27 | ongoing: { 28 | otakudesuUrl?: string | undefined; 29 | animeList: IOngoingAnimeCard[]; 30 | }; 31 | completed: { 32 | otakudesuUrl?: string | undefined; 33 | animeList: ICompletedAnimeCard[]; 34 | }; 35 | } 36 | 37 | export interface IOngoingAnimeCard extends ITextAnimeCard { 38 | poster: string; 39 | episodes: string; 40 | releaseDay: string; 41 | latestReleaseDate: string; 42 | } 43 | 44 | export interface ICompletedAnimeCard extends ITextAnimeCard { 45 | poster: string; 46 | episodes: string; 47 | score: string; 48 | lastReleaseDate: string; 49 | } 50 | 51 | export interface IRecommendedAnimeCard extends ITextAnimeCard { 52 | poster: string; 53 | } 54 | 55 | export interface ISearchedAnimeCard extends ITextAnimeCard { 56 | poster: string; 57 | status: string; 58 | score: string; 59 | genreList: ITextGenreCard[]; 60 | } 61 | 62 | export interface IGenreFilteredAnimeCard extends ITextAnimeCard { 63 | poster: string; 64 | studios: string; 65 | score: string; 66 | episodes: string; 67 | season: string; 68 | synopsis: { 69 | paragraphList: string[]; 70 | }; 71 | genreList: ITextGenreCard[]; 72 | } 73 | 74 | export interface IAnimeCollection { 75 | startWith: string; 76 | animeList: ITextAnimeCard[]; 77 | } 78 | 79 | export interface IScheduleCollection { 80 | title: string; 81 | animeList: ITextAnimeCard[]; 82 | } 83 | 84 | export interface IAnimeDetails { 85 | title: string; 86 | japanese: string; 87 | score: string; 88 | producers: string; 89 | type: string; 90 | status: string; 91 | episodes: string; 92 | duration: string; 93 | aired: string; 94 | studios: string; 95 | poster: string; 96 | synopsis: ISynopsis; 97 | batch: ITextBatchCard | null; 98 | genreList: ITextGenreCard[]; 99 | episodeList: ITextEpisodeCard[]; 100 | recommendedAnimeList: IRecommendedAnimeCard[]; 101 | } 102 | 103 | export interface IEpisodeDetails { 104 | title: string; 105 | animeId: string; 106 | releaseTime: string; 107 | defaultStreamingUrl: string; 108 | hasPrevEpisode: boolean; 109 | prevEpisode: ITextEpisodeCard | null; 110 | hasNextEpisode: boolean; 111 | nextEpisode: ITextEpisodeCard | null; 112 | server: { qualityList: IQuality[] }; 113 | download: { qualityList: IQuality[] }; 114 | info: { 115 | credit: string; 116 | encoder: string; 117 | duration: string; 118 | type: string; 119 | genreList: ITextGenreCard[]; 120 | episodeList: ITextEpisodeCard[]; 121 | }; 122 | } 123 | 124 | export interface IBatchDetails { 125 | title: string; 126 | animeId: string; 127 | poster: string; 128 | japanese: string; 129 | type: string; 130 | score: string; 131 | episodes: string; 132 | duration: string; 133 | studios: string; 134 | producers: string; 135 | aired: string; 136 | credit: string; 137 | download: { formatList: IFormat[] }; 138 | genreList: ITextGenreCard[]; 139 | } 140 | 141 | export interface IServerDetails { 142 | url: string; 143 | } 144 | -------------------------------------------------------------------------------- /src/parsers/extra/otakudesu.extra.parser.ts: -------------------------------------------------------------------------------- 1 | import * as T from "@interfaces/otakudesu.interface.js"; 2 | import type { HTMLElement } from "node-html-parser"; 3 | import mainParser from "@parsers/main/main.parser.js"; 4 | import otakudesuConfig from "@configs/otakudesu.config.js"; 5 | 6 | const { Text, Attr, Id, Num, Src, AnimeSrc } = mainParser; 7 | const { baseUrl } = otakudesuConfig; 8 | 9 | const otakudesuExtraParser = { 10 | parseOngoingCard(el: HTMLElement): T.IOngoingAnimeCard { 11 | const title = Text(el.querySelector("h2.jdlflm")); 12 | const poster = Src(el.querySelector(".thumbz img")); 13 | const episodes = Text(el.querySelector(".epz"), /Episode (\S+)/); 14 | const otakudesuUrl = AnimeSrc(el.querySelector(".thumb a")); 15 | const animeId = Id(el.querySelector(".thumb a")); 16 | const latestReleaseDate = Text(el.querySelector(".newnime")); 17 | const releaseDay = Text(el.querySelector(".epztipe")); 18 | 19 | return { 20 | title, 21 | poster, 22 | episodes, 23 | animeId, 24 | latestReleaseDate, 25 | releaseDay, 26 | otakudesuUrl, 27 | }; 28 | }, 29 | 30 | parseCompletedCard(el: HTMLElement): T.ICompletedAnimeCard { 31 | const title = Text(el.querySelector("h2.jdlflm")); 32 | const poster = Src(el.querySelector(".thumbz img")); 33 | const episodes = Text(el.querySelector(".epz"), /(\S+) Episode/); 34 | const otakudesuUrl = AnimeSrc(el.querySelector(".thumb a")); 35 | const animeId = Id(el.querySelector(".thumb a")); 36 | const score = Text(el.querySelector(".epztipe")); 37 | const lastReleaseDate = Text(el.querySelector(".newnime")); 38 | 39 | return { 40 | title, 41 | poster, 42 | episodes, 43 | animeId, 44 | score, 45 | lastReleaseDate, 46 | otakudesuUrl, 47 | }; 48 | }, 49 | 50 | parseTextCard(el: HTMLElement): T.IMainCard & { id: string } { 51 | const title = Text(el); 52 | const id = Id(el); 53 | const otakudesuUrl = AnimeSrc(el, baseUrl); 54 | 55 | return { 56 | title, 57 | id, 58 | otakudesuUrl, 59 | }; 60 | }, 61 | 62 | parseInfo(elems: HTMLElement[]): (index: number) => string { 63 | function getInfo(index: number) { 64 | const info = Text(elems[index]?.nextSibling as HTMLElement, /:\s*(.+)/); 65 | 66 | return info; 67 | } 68 | 69 | return getInfo; 70 | }, 71 | 72 | parseTextGenreList(elems: HTMLElement[]): T.ITextGenreCard[] { 73 | const genreList: T.ITextGenreCard[] = elems.map((el) => { 74 | const { id, title, otakudesuUrl } = this.parseTextCard(el); 75 | 76 | return { 77 | title, 78 | genreId: id, 79 | otakudesuUrl, 80 | }; 81 | }); 82 | 83 | return genreList; 84 | }, 85 | 86 | parseTextEpisodeList(elems: HTMLElement[]): T.ITextEpisodeCard[] { 87 | const episodeList = elems.map((el) => { 88 | const { id, title, otakudesuUrl } = otakudesuExtraParser.parseTextCard(el); 89 | const match = title.match(/Episode\s+(\d+)/); 90 | 91 | return { 92 | title: match ? match[1] || "0" : title, 93 | episodeId: id, 94 | otakudesuUrl, 95 | }; 96 | }); 97 | 98 | return episodeList; 99 | }, 100 | 101 | parseSynopsis(elems: HTMLElement[]): ISynopsis { 102 | return { 103 | paragraphList: elems 104 | .map((el) => { 105 | const paragraph = el.text; 106 | 107 | return paragraph; 108 | }) 109 | .filter((paragraph) => { 110 | return paragraph; 111 | }), 112 | }; 113 | }, 114 | }; 115 | 116 | export default otakudesuExtraParser; 117 | -------------------------------------------------------------------------------- /dist/parsers/extra/kuramanime.extra.parser.js: -------------------------------------------------------------------------------- 1 | import * as T from "../../interfaces/kuramanime.interface.js"; 2 | import mainParser from "../main/main.parser.js"; 3 | const { Text, Attr, Id, Num, Src, AnimeSrc } = mainParser; 4 | const kuramanimeExtraParser = { 5 | parseAnimeId(el) { 6 | return (AnimeSrc(el) 7 | ?.match(/\/anime\/([^/]+)/)?.[0] 8 | .replace("/anime/", "") || ""); 9 | }, 10 | parseEpisodeAnimeId(el, animeId) { 11 | return (AnimeSrc(el) 12 | ?.match(/\/anime\/[^/]+\/([^/]+)/)?.[0] 13 | .replace(`/anime/${animeId}/`, "") || ""); 14 | }, 15 | parsePropertyId(el) { 16 | return AnimeSrc(el)?.match(/([^\/\?#]+)(?=\?|$)/)?.[0] || ""; 17 | }, 18 | parsePropertyType(el) { 19 | return AnimeSrc(el)?.match(/\/properties\/([^\/?]+)\/([^\/?]+)/i)?.[1] || ""; 20 | }, 21 | parseAnimeCard(el) { 22 | const title = Text(el.querySelector(".product__item__text h5")); 23 | const animeSlug = Id(el.querySelector(".product__item__text h5 a")); 24 | const poster = Attr(el.querySelector(".product__item__pic"), "data-setbg"); 25 | const type = Text(el.querySelector(".product__item__text ul a:nth-child(1)")); 26 | const quality = Text(el.querySelector(".product__item__text ul a:nth-child(2)")); 27 | const highlight = Text(el.querySelector(".product__item__pic .ep")); 28 | const kuramanimeUrl = AnimeSrc(el.querySelector(".product__item__text h5 a")); 29 | const animeId = this.parseAnimeId(el.querySelector(".product__item__text h5 a")); 30 | return { 31 | title, 32 | animeId, 33 | animeSlug, 34 | poster, 35 | quality, 36 | type, 37 | highlight, 38 | kuramanimeUrl, 39 | }; 40 | }, 41 | parseScheduledAnimeCard(el) { 42 | const title = Text(el.querySelector(".product__item__text h5")); 43 | const animeSlug = Id(el.querySelector(".product__item__text h5 a")); 44 | const poster = Attr(el.querySelector(".product__item__pic"), "data-setbg"); 45 | const type = Text(el.querySelector(".product__item__text ul a:nth-child(1)")); 46 | const quality = Text(el.querySelector(".product__item__text ul a:nth-child(2)")); 47 | const day = Text(el.querySelector(".view-end ul li:first-child")); 48 | const releaseTime = Text(el.querySelector(".view-end ul li:last-child")); 49 | const kuramanimeUrl = AnimeSrc(el.querySelector(".product__item__text h5 a")); 50 | const animeId = this.parseAnimeId(el.querySelector(".product__item__text h5 a")); 51 | return { 52 | title, 53 | animeId, 54 | animeSlug, 55 | poster, 56 | quality, 57 | type, 58 | day, 59 | releaseTime, 60 | kuramanimeUrl, 61 | }; 62 | }, 63 | parseEpisodeCard(el) { 64 | const title = Text(el.querySelector(".product__item__text h5")); 65 | const episodeId = Id(el.querySelector(".product__item__text h5 a")); 66 | const poster = Attr(el.querySelector(".product__item__pic"), "data-setbg"); 67 | const type = Text(el.querySelector(".product__item__text ul a:nth-child(1)")); 68 | const quality = Text(el.querySelector(".product__item__text ul a:nth-child(2)")); 69 | const episodes = Text(el.querySelector(".product__item__pic .ep"), /Ep\s*([^\s/]+)/); 70 | const totalEpisodes = Text(el.querySelector(".product__item__pic .ep"), /\/\s*([^\s]+)/); 71 | const kuramanimeUrl = AnimeSrc(el.querySelector(".product__item__text h5 a")); 72 | const animeId = this.parseAnimeId(el.querySelector(".product__item__text h5 a")); 73 | const animeSlug = this.parseEpisodeAnimeId(el.querySelector(".product__item__text h5 a"), animeId); 74 | return { 75 | title, 76 | animeId, 77 | animeSlug, 78 | episodeId, 79 | poster, 80 | quality, 81 | type, 82 | episodes, 83 | totalEpisodes, 84 | kuramanimeUrl, 85 | }; 86 | }, 87 | parseTextCard(el) { 88 | const title = Text(el); 89 | const id = this.parsePropertyId(el); 90 | const kuramanimeUrl = AnimeSrc(el)?.trim(); 91 | return { 92 | title, 93 | id, 94 | kuramanimeUrl, 95 | }; 96 | }, 97 | parseInfo(elems) { 98 | function getInfo(index) { 99 | const info = Text(elems[index]); 100 | return info; 101 | } 102 | return getInfo; 103 | }, 104 | parseInfoProperty(elems) { 105 | const getPropertyInfo = (index) => { 106 | const aEl = elems[index]?.querySelector("a"); 107 | const propertyInfo = { 108 | title: Text(aEl), 109 | propertyType: this.parsePropertyType(aEl), 110 | propertyId: this.parsePropertyId(aEl), 111 | kuramanimeUrl: AnimeSrc(aEl), 112 | }; 113 | return propertyInfo; 114 | }; 115 | return getPropertyInfo; 116 | }, 117 | parseInfoProperties(elems) { 118 | const getPropertyInfo = (index) => { 119 | const aElems = elems[index]?.querySelectorAll("a"); 120 | const properties = aElems?.map((aEl) => { 121 | return { 122 | title: Text(aEl), 123 | propertyType: this.parsePropertyType(aEl), 124 | propertyId: this.parsePropertyId(aEl), 125 | kuramanimeUrl: AnimeSrc(aEl), 126 | }; 127 | }) || []; 128 | return properties; 129 | }; 130 | return getPropertyInfo; 131 | }, 132 | }; 133 | export default kuramanimeExtraParser; 134 | -------------------------------------------------------------------------------- /src/parsers/extra/kuramanime.extra.parser.ts: -------------------------------------------------------------------------------- 1 | import * as T from "@interfaces/kuramanime.interface.js"; 2 | import type { HTMLElement } from "node-html-parser"; 3 | import mainParser from "@parsers/main/main.parser.js"; 4 | 5 | const { Text, Attr, Id, Num, Src, AnimeSrc } = mainParser; 6 | 7 | const kuramanimeExtraParser = { 8 | parseAnimeId(el: HTMLElement | null | undefined): string { 9 | return ( 10 | AnimeSrc(el) 11 | ?.match(/\/anime\/([^/]+)/)?.[0] 12 | .replace("/anime/", "") || "" 13 | ); 14 | }, 15 | 16 | parseEpisodeAnimeId(el: HTMLElement | null | undefined, animeId: string): string { 17 | return ( 18 | AnimeSrc(el) 19 | ?.match(/\/anime\/[^/]+\/([^/]+)/)?.[0] 20 | .replace(`/anime/${animeId}/`, "") || "" 21 | ); 22 | }, 23 | 24 | parsePropertyId(el: HTMLElement | null | undefined): string { 25 | return AnimeSrc(el)?.match(/([^\/\?#]+)(?=\?|$)/)?.[0] || ""; 26 | }, 27 | 28 | parsePropertyType(el: HTMLElement | null | undefined): string { 29 | return AnimeSrc(el)?.match(/\/properties\/([^\/?]+)\/([^\/?]+)/i)?.[1] || ""; 30 | }, 31 | 32 | parseAnimeCard(el: HTMLElement): T.IAnimeCard { 33 | const title = Text(el.querySelector(".product__item__text h5")); 34 | const animeSlug = Id(el.querySelector(".product__item__text h5 a")); 35 | const poster = Attr(el.querySelector(".product__item__pic"), "data-setbg"); 36 | const type = Text(el.querySelector(".product__item__text ul a:nth-child(1)")); 37 | const quality = Text(el.querySelector(".product__item__text ul a:nth-child(2)")); 38 | const highlight = Text(el.querySelector(".product__item__pic .ep")); 39 | const kuramanimeUrl = AnimeSrc(el.querySelector(".product__item__text h5 a")); 40 | const animeId = this.parseAnimeId(el.querySelector(".product__item__text h5 a")); 41 | 42 | return { 43 | title, 44 | animeId, 45 | animeSlug, 46 | poster, 47 | quality, 48 | type, 49 | highlight, 50 | kuramanimeUrl, 51 | }; 52 | }, 53 | 54 | parseScheduledAnimeCard(el: HTMLElement): T.IScheduledAnimeCard { 55 | const title = Text(el.querySelector(".product__item__text h5")); 56 | const animeSlug = Id(el.querySelector(".product__item__text h5 a")); 57 | const poster = Attr(el.querySelector(".product__item__pic"), "data-setbg"); 58 | const type = Text(el.querySelector(".product__item__text ul a:nth-child(1)")); 59 | const quality = Text(el.querySelector(".product__item__text ul a:nth-child(2)")); 60 | const day = Text(el.querySelector(".view-end ul li:first-child")); 61 | const releaseTime = Text(el.querySelector(".view-end ul li:last-child")); 62 | const kuramanimeUrl = AnimeSrc(el.querySelector(".product__item__text h5 a")); 63 | const animeId = this.parseAnimeId(el.querySelector(".product__item__text h5 a")); 64 | 65 | return { 66 | title, 67 | animeId, 68 | animeSlug, 69 | poster, 70 | quality, 71 | type, 72 | day, 73 | releaseTime, 74 | kuramanimeUrl, 75 | }; 76 | }, 77 | 78 | parseEpisodeCard(el: HTMLElement): T.IEpisodeCard { 79 | const title = Text(el.querySelector(".product__item__text h5")); 80 | const episodeId = Id(el.querySelector(".product__item__text h5 a")); 81 | const poster = Attr(el.querySelector(".product__item__pic"), "data-setbg"); 82 | const type = Text(el.querySelector(".product__item__text ul a:nth-child(1)")); 83 | const quality = Text(el.querySelector(".product__item__text ul a:nth-child(2)")); 84 | const episodes = Text(el.querySelector(".product__item__pic .ep"), /Ep\s*([^\s/]+)/); 85 | const totalEpisodes = Text(el.querySelector(".product__item__pic .ep"), /\/\s*([^\s]+)/); 86 | const kuramanimeUrl = AnimeSrc(el.querySelector(".product__item__text h5 a")); 87 | const animeId = this.parseAnimeId(el.querySelector(".product__item__text h5 a")); 88 | const animeSlug = this.parseEpisodeAnimeId( 89 | el.querySelector(".product__item__text h5 a"), 90 | animeId 91 | ); 92 | 93 | return { 94 | title, 95 | animeId, 96 | animeSlug, 97 | episodeId, 98 | poster, 99 | quality, 100 | type, 101 | episodes, 102 | totalEpisodes, 103 | kuramanimeUrl, 104 | }; 105 | }, 106 | 107 | parseTextCard(el: HTMLElement): T.IMainCard & { id: string } { 108 | const title = Text(el); 109 | const id = this.parsePropertyId(el); 110 | const kuramanimeUrl = AnimeSrc(el)?.trim(); 111 | 112 | return { 113 | title, 114 | id, 115 | kuramanimeUrl, 116 | }; 117 | }, 118 | 119 | parseInfo(elems: HTMLElement[]): (index: number) => string { 120 | function getInfo(index: number) { 121 | const info = Text(elems[index]); 122 | 123 | return info; 124 | } 125 | 126 | return getInfo; 127 | }, 128 | 129 | parseInfoProperty(elems: HTMLElement[]): (index: number) => T.ITextPropertyTypeCard { 130 | const getPropertyInfo = (index: number) => { 131 | const aEl = elems[index]?.querySelector("a"); 132 | const propertyInfo: T.ITextPropertyTypeCard = { 133 | title: Text(aEl), 134 | propertyType: this.parsePropertyType(aEl), 135 | propertyId: this.parsePropertyId(aEl), 136 | kuramanimeUrl: AnimeSrc(aEl), 137 | }; 138 | 139 | return propertyInfo; 140 | }; 141 | 142 | return getPropertyInfo; 143 | }, 144 | 145 | parseInfoProperties(elems: HTMLElement[]): (index: number) => T.ITextPropertyTypeCard[] { 146 | const getPropertyInfo = (index: number) => { 147 | const aElems = elems[index]?.querySelectorAll("a"); 148 | const properties: T.ITextPropertyTypeCard[] = 149 | aElems?.map((aEl) => { 150 | return { 151 | title: Text(aEl), 152 | propertyType: this.parsePropertyType(aEl), 153 | propertyId: this.parsePropertyId(aEl), 154 | kuramanimeUrl: AnimeSrc(aEl), 155 | }; 156 | }) || []; 157 | 158 | return properties; 159 | }; 160 | 161 | return getPropertyInfo; 162 | }, 163 | }; 164 | 165 | export default kuramanimeExtraParser; 166 | -------------------------------------------------------------------------------- /src/controllers/otakudesu.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction } from "express"; 2 | import otakudesuScraper from "@scrapers/otakudesu.scraper.js"; 3 | import otakudesuParser from "@parsers/otakudesu.parser.js"; 4 | import otakudesuConfig from "@configs/otakudesu.config.js"; 5 | import otakudesuSchema from "@schemas/otakudesu.schema.js"; 6 | import setPayload from "@helpers/setPayload.js"; 7 | import * as v from "valibot"; 8 | 9 | const { baseUrl } = otakudesuConfig; 10 | 11 | const otakudesuController = { 12 | async getRoot(req: Request, res: Response, next: NextFunction) { 13 | const routes: IRouteData[] = [ 14 | { 15 | method: "GET", 16 | path: "/otakudesu/home", 17 | description: "Halaman utama", 18 | pathParams: [], 19 | queryParams: [], 20 | }, 21 | { 22 | method: "GET", 23 | path: "/otakudesu/schedule", 24 | description: "Jadwal rilis", 25 | pathParams: [], 26 | queryParams: [], 27 | }, 28 | { 29 | method: "GET", 30 | path: "/otakudesu/anime", 31 | description: "Daftar semua anime", 32 | pathParams: [], 33 | queryParams: [], 34 | }, 35 | { 36 | method: "GET", 37 | path: "/otakudesu/genre", 38 | description: "Daftar semua genre", 39 | pathParams: [], 40 | queryParams: [], 41 | }, 42 | { 43 | method: "GET", 44 | path: "/otakudesu/ongoing", 45 | description: "Daftar anime sedang tayang", 46 | pathParams: [], 47 | queryParams: [ 48 | { 49 | key: "page", 50 | value: "string", 51 | defaultValue: "1", 52 | required: false, 53 | }, 54 | ], 55 | }, 56 | { 57 | method: "GET", 58 | path: "/otakudesu/completed", 59 | description: "Daftar anime selesai", 60 | pathParams: [], 61 | queryParams: [ 62 | { 63 | key: "page", 64 | value: "string", 65 | defaultValue: "1", 66 | required: false, 67 | }, 68 | ], 69 | }, 70 | { 71 | method: "GET", 72 | path: "/otakudesu/search", 73 | description: "Daftar anime berdasarkan pencarian", 74 | pathParams: [], 75 | queryParams: [ 76 | { 77 | key: "q", 78 | value: "string", 79 | defaultValue: null, 80 | required: true, 81 | }, 82 | ], 83 | }, 84 | { 85 | method: "GET", 86 | path: "/otakudesu/genre/{genreId}", 87 | description: "Daftar anime berdasarkan genre", 88 | pathParams: [ 89 | { 90 | key: "genreId", 91 | value: "string", 92 | defaultValue: null, 93 | required: true, 94 | }, 95 | ], 96 | queryParams: [ 97 | { 98 | key: "page", 99 | value: "string", 100 | defaultValue: "1", 101 | required: false, 102 | }, 103 | ], 104 | }, 105 | { 106 | method: "GET", 107 | path: "/otakudesu/batch/{batchId}", 108 | description: "Batch anime berdasarkan id batch", 109 | pathParams: [ 110 | { 111 | key: "batchId", 112 | value: "string", 113 | defaultValue: null, 114 | required: true, 115 | }, 116 | ], 117 | queryParams: [], 118 | }, 119 | { 120 | method: "GET", 121 | path: "/otakudesu/anime/{animeId}", 122 | description: "Detail anime berdasarkan id anime", 123 | pathParams: [ 124 | { 125 | key: "animeId", 126 | value: "string", 127 | defaultValue: null, 128 | required: true, 129 | }, 130 | ], 131 | queryParams: [], 132 | }, 133 | { 134 | method: "GET", 135 | path: "/otakudesu/episode/{episodeId}", 136 | description: "Detail episode berdasarkan id episode", 137 | pathParams: [ 138 | { 139 | key: "episodeId", 140 | value: "string", 141 | defaultValue: null, 142 | required: true, 143 | }, 144 | ], 145 | queryParams: [], 146 | }, 147 | { 148 | method: "GET | POST", 149 | path: "/otakudesu/server/{serverId}", 150 | description: "Link video berdasarkan id server", 151 | pathParams: [ 152 | { 153 | key: "serverId", 154 | value: "string", 155 | defaultValue: null, 156 | required: true, 157 | }, 158 | ], 159 | queryParams: [], 160 | }, 161 | ]; 162 | 163 | res.json( 164 | setPayload(res, { 165 | message: "Status: OK 🚀", 166 | data: { routes }, 167 | }) 168 | ); 169 | }, 170 | 171 | async getHome(req: Request, res: Response, next: NextFunction) { 172 | try { 173 | const ref = "https://google.com/"; 174 | const document = await otakudesuScraper.scrapeDOM("/", ref); 175 | const home = otakudesuParser.parseHome(document); 176 | const payload = setPayload(res, { 177 | data: home, 178 | }); 179 | 180 | res.json(payload); 181 | } catch (error) { 182 | next(error); 183 | } 184 | }, 185 | 186 | async getSchedule(req: Request, res: Response, next: NextFunction) { 187 | try { 188 | const pathname = "/jadwal-rilis/"; 189 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 190 | const scheduleList = otakudesuParser.parseSchedules(document); 191 | const payload = setPayload(res, { 192 | data: { scheduleList }, 193 | }); 194 | 195 | res.json(payload); 196 | } catch (error) { 197 | next(error); 198 | } 199 | }, 200 | 201 | async getAllAnimes(req: Request, res: Response, next: NextFunction) { 202 | try { 203 | const pathname = "/anime-list/"; 204 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl, true); 205 | const list = otakudesuParser.parseAllAnimes(document); 206 | const payload = setPayload(res, { 207 | data: { list }, 208 | }); 209 | 210 | res.json(payload); 211 | } catch (error) { 212 | next(error); 213 | } 214 | }, 215 | 216 | async getAllGenres(req: Request, res: Response, next: NextFunction) { 217 | try { 218 | const pathname = "/genre-list/"; 219 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 220 | const genreList = otakudesuParser.parseAllGenres(document); 221 | const payload = setPayload(res, { 222 | data: { genreList }, 223 | }); 224 | 225 | res.json(payload); 226 | } catch (error) { 227 | next(error); 228 | } 229 | }, 230 | 231 | async getOngoingAnimes(req: Request, res: Response, next: NextFunction) { 232 | try { 233 | const page = Number(v.parse(otakudesuSchema.query.animes, req.query)?.page); 234 | const pathname = page > 1 ? `/ongoing-anime/page/${page}/` : "/ongoing-anime/"; 235 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 236 | const animeList = otakudesuParser.parseOngoingAnimes(document); 237 | const pagination = otakudesuParser.parsePagination(document); 238 | const payload = setPayload(res, { 239 | data: { animeList }, 240 | pagination, 241 | }); 242 | 243 | res.json(payload); 244 | } catch (error) { 245 | next(error); 246 | } 247 | }, 248 | 249 | async getCompletedAnimes(req: Request, res: Response, next: NextFunction) { 250 | try { 251 | const page = Number(v.parse(otakudesuSchema.query.animes, req.query)?.page); 252 | const pathname = page > 1 ? `/complete-anime/page/${page}/` : "/complete-anime/"; 253 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 254 | const animeList = otakudesuParser.parseCompletedAnimes(document); 255 | const pagination = otakudesuParser.parsePagination(document); 256 | const payload = setPayload(res, { 257 | data: { animeList }, 258 | pagination, 259 | }); 260 | 261 | res.json(payload); 262 | } catch (error) { 263 | next(error); 264 | } 265 | }, 266 | 267 | async getSearchedAnimes(req: Request, res: Response, next: NextFunction) { 268 | try { 269 | const { q } = v.parse(otakudesuSchema.query.searchedAnimes, req.query); 270 | const pathname = `/?s=${q}&post_type=anime`; 271 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 272 | const animeList = otakudesuParser.parseSearchedAnimes(document); 273 | const payload = setPayload(res, { 274 | data: { animeList }, 275 | }); 276 | 277 | res.json(payload); 278 | } catch (error) { 279 | next(error); 280 | } 281 | }, 282 | 283 | async getAnimesByGenre(req: Request, res: Response, next: NextFunction) { 284 | try { 285 | const genreId = req.params.genreId; 286 | const page = Number(v.parse(otakudesuSchema.query.animes, req.query)?.page); 287 | const pathname = page > 1 ? `/genres/${genreId}/page/${page}/` : `/genres/${genreId}/`; 288 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 289 | const animeList = otakudesuParser.parseAnimesByGenre(document); 290 | const pagination = otakudesuParser.parsePagination(document); 291 | const payload = setPayload(res, { 292 | data: { animeList }, 293 | pagination, 294 | }); 295 | 296 | res.json(payload); 297 | } catch (error) { 298 | next(error); 299 | } 300 | }, 301 | 302 | async getBatchDetails(req: Request, res: Response, next: NextFunction) { 303 | try { 304 | const batchId = req.params.batchId; 305 | const pathname = `/batch/${batchId}/`; 306 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 307 | const details = otakudesuParser.parseBatchDetails(document); 308 | const payload = setPayload(res, { 309 | data: { details }, 310 | }); 311 | 312 | res.json(payload); 313 | } catch (error) { 314 | next(error); 315 | } 316 | }, 317 | 318 | async getAnimeDetails(req: Request, res: Response, next: NextFunction) { 319 | try { 320 | const animeId = req.params.animeId; 321 | const pathname = `/anime/${animeId}/`; 322 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 323 | const details = otakudesuParser.parseAnimeDetails(document); 324 | const payload = setPayload(res, { 325 | data: { details }, 326 | }); 327 | 328 | res.json(payload); 329 | } catch (error) { 330 | next(error); 331 | } 332 | }, 333 | 334 | async getEpisodeDetails(req: Request, res: Response, next: NextFunction) { 335 | try { 336 | const episodeId = req.params.episodeId; 337 | const pathname = `/episode/${episodeId}/`; 338 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 339 | const details = await otakudesuParser.parseEpisodeDetails( 340 | document, 341 | new URL(pathname, baseUrl).toString() 342 | ); 343 | const payload = setPayload(res, { 344 | data: { details }, 345 | }); 346 | 347 | res.json(payload); 348 | } catch (error) { 349 | next(error); 350 | } 351 | }, 352 | 353 | async getServerDetails(req: Request, res: Response, next: NextFunction) { 354 | try { 355 | const serverId = req.params.serverId || ""; 356 | const details = await otakudesuParser.parseServerDetails(serverId); 357 | const payload = setPayload(res, { 358 | data: { details }, 359 | }); 360 | 361 | res.json(payload); 362 | } catch (error: any) { 363 | if (error.message.includes("is not valid JSON")) { 364 | res.status(400).json(setPayload(res)); 365 | 366 | return; 367 | } 368 | 369 | next(error); 370 | } 371 | }, 372 | }; 373 | 374 | export default otakudesuController; 375 | -------------------------------------------------------------------------------- /src/controllers/kuramanime.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction } from "express"; 2 | import setPayload from "@helpers/setPayload.js"; 3 | import kuramanimeConfig from "@configs/kuramanime.config.js"; 4 | import kuramanimeScraper from "@scrapers/kuramanime.scraper.js"; 5 | import kuramanimeParser from "@parsers/kuramanime.parser.js"; 6 | import kuramanimeSchema from "@schemas/kuramanime.schema.js"; 7 | import * as v from "valibot"; 8 | 9 | const { baseUrl } = kuramanimeConfig; 10 | 11 | const kuramanimeController = { 12 | async getRoot(req: Request, res: Response, next: NextFunction) { 13 | const routes: IRouteData[] = [ 14 | { 15 | method: "GET", 16 | path: "/kuramanime/home", 17 | description: "Halaman utama", 18 | pathParams: [], 19 | queryParams: [], 20 | }, 21 | { 22 | method: "GET", 23 | path: "/kuramanime/anime", 24 | description: "Daftar anime", 25 | pathParams: [], 26 | queryParams: [ 27 | { 28 | key: "search", 29 | value: "string", 30 | defaultValue: null, 31 | required: false, 32 | }, 33 | { 34 | key: "status", 35 | value: '"ongoing" | "completed" | "upcoming" | "movie"', 36 | defaultValue: null, 37 | required: false, 38 | }, 39 | { 40 | key: "sort", 41 | value: '"a-z" | "z-a" | "oldest" | "latest" | "popular" | "most_viewed" | "updated"', 42 | defaultValue: '"latest" | "updated"', 43 | required: false, 44 | }, 45 | { 46 | key: "page", 47 | value: "string", 48 | defaultValue: "1", 49 | required: false, 50 | }, 51 | ], 52 | }, 53 | { 54 | method: "GET", 55 | path: "/kuramanime/schedule", 56 | description: "Jadwal rilis", 57 | pathParams: [], 58 | queryParams: [ 59 | { 60 | key: "day", 61 | value: 62 | '"all" | "random" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"', 63 | defaultValue: "all", 64 | required: false, 65 | }, 66 | { 67 | key: "page", 68 | value: "string", 69 | defaultValue: "1", 70 | required: false, 71 | }, 72 | ], 73 | }, 74 | { 75 | method: "GET", 76 | path: "/kuramanime/properties/{propertyType}", 77 | description: "Daftar properti berdasarkan tipe properti", 78 | pathParams: [ 79 | { 80 | key: "propertyType", 81 | value: '"genre" | "season" | "studio" | "type" | "quality" | "source" | "country"', 82 | defaultValue: null, 83 | required: true, 84 | }, 85 | ], 86 | queryParams: [], 87 | }, 88 | { 89 | method: "GET", 90 | path: "/kuramanime/properties/{propertyType}/{propertyId}", 91 | description: "Daftar anime berdasarkan id properti", 92 | pathParams: [ 93 | { 94 | key: "propertyType", 95 | value: '"genre" | "season" | "studio" | "type" | "quality" | "source" | "country"', 96 | defaultValue: null, 97 | required: true, 98 | }, 99 | { 100 | key: "propertyId", 101 | value: "string", 102 | defaultValue: null, 103 | required: true, 104 | }, 105 | ], 106 | queryParams: [], 107 | }, 108 | { 109 | method: "GET", 110 | path: "/kuramanime/anime/{animeId}/{animeSlug}", 111 | description: "Detail anime berdasarkan id anime", 112 | pathParams: [ 113 | { 114 | key: "animeId", 115 | value: "string", 116 | defaultValue: null, 117 | required: true, 118 | }, 119 | { 120 | key: "animeSlug", 121 | value: "string", 122 | defaultValue: null, 123 | required: true, 124 | }, 125 | ], 126 | queryParams: [], 127 | }, 128 | { 129 | method: "GET", 130 | path: "/kuramanime/batch/{animeId}/{animeSlug}/{batchId}", 131 | description: "Batch anime berdasarkan id batch", 132 | pathParams: [ 133 | { 134 | key: "animeId", 135 | value: "string", 136 | defaultValue: null, 137 | required: true, 138 | }, 139 | { 140 | key: "animeSlug", 141 | value: "string", 142 | defaultValue: null, 143 | required: true, 144 | }, 145 | { 146 | key: "batchId", 147 | value: "string", 148 | defaultValue: null, 149 | required: true, 150 | }, 151 | ], 152 | queryParams: [], 153 | }, 154 | { 155 | method: "GET", 156 | path: "/kuramanime/episode/{animeId}/{animeSlug}/{episodeId}", 157 | description: "Detail episode berdasarkan id episode", 158 | pathParams: [ 159 | { 160 | key: "animeId", 161 | value: "string", 162 | defaultValue: null, 163 | required: true, 164 | }, 165 | { 166 | key: "animeSlug", 167 | value: "string", 168 | defaultValue: null, 169 | required: true, 170 | }, 171 | { 172 | key: "episodeId", 173 | value: "string", 174 | defaultValue: null, 175 | required: true, 176 | }, 177 | ], 178 | queryParams: [], 179 | }, 180 | ]; 181 | 182 | res.json( 183 | setPayload(res, { 184 | message: "Status: OK 🚀", 185 | data: { routes }, 186 | }) 187 | ); 188 | }, 189 | 190 | async getHome(req: Request, res: Response, next: NextFunction) { 191 | try { 192 | const document = await kuramanimeScraper.scrapeDOM("/", "https://google.com"); 193 | const home = kuramanimeParser.parseHome(document); 194 | const payload = setPayload(res, { 195 | data: home, 196 | }); 197 | 198 | res.json(payload); 199 | } catch (error) { 200 | next(error); 201 | } 202 | }, 203 | 204 | async getAnimes(req: Request, res: Response, next: NextFunction) { 205 | try { 206 | const query = v.parse(kuramanimeSchema.query.animes, req.query); 207 | const status = query?.status; 208 | const search = query?.search || ""; 209 | const page = Number(query?.page) || 1; 210 | const sort = 211 | (query?.sort === "a-z" 212 | ? "ascending" 213 | : query?.sort === "z-a" 214 | ? "descending" 215 | : query?.sort) || (status === "ongoing" ? "updated" : "latest"); 216 | 217 | function getPathname() { 218 | if (status) { 219 | return `/quick/${ 220 | status === "completed" ? "finished" : status 221 | }?order_by=${sort}&page=${page}`; 222 | } 223 | 224 | if (search) { 225 | return `/anime?order_by=${sort}&page=${page}&search=${search}`; 226 | } 227 | 228 | return `/anime?order_by=${sort}&page=${page}`; 229 | } 230 | 231 | const pathname = getPathname(); 232 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 233 | const pagination = kuramanimeParser.parsePagination(document); 234 | const payload = setPayload(res, { 235 | data: { 236 | animeList: status !== "ongoing" ? kuramanimeParser.parseAnimes(document) : undefined, 237 | episodeList: status === "ongoing" ? kuramanimeParser.parseEpisodes(document) : undefined, 238 | }, 239 | pagination, 240 | }); 241 | 242 | res.json(payload); 243 | } catch (error) { 244 | next(error); 245 | } 246 | }, 247 | 248 | async getProperties(req: Request, res: Response, next: NextFunction) { 249 | try { 250 | const { propertyType } = v.parse(kuramanimeSchema.param.properties, req.params); 251 | const pathname = `/properties/${propertyType}`; 252 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 253 | const propertyList = kuramanimeParser.parseProperties(document); 254 | const payload = setPayload(res, { 255 | data: { 256 | propertyType, 257 | propertyList, 258 | }, 259 | }); 260 | 261 | res.json(payload); 262 | } catch (error) { 263 | next(error); 264 | } 265 | }, 266 | 267 | async getAnimesByProperty(req: Request, res: Response, next: NextFunction) { 268 | try { 269 | const { propertyType, propertyId } = v.parse( 270 | kuramanimeSchema.param.animesByPropertyId, 271 | req.params 272 | ); 273 | const query = v.parse(kuramanimeSchema.query.animesByPropertyId, req.query); 274 | const page = Number(query?.page) || 1; 275 | const sort = 276 | (query?.sort === "a-z" 277 | ? "ascending" 278 | : query?.sort === "z-a" 279 | ? "descending" 280 | : query?.sort) || "latest"; 281 | const pathname = `/properties/${propertyType}/${propertyId}?order_by=${sort}&page=${page}`; 282 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 283 | const animeList = kuramanimeParser.parseAnimes(document); 284 | const pagination = kuramanimeParser.parsePagination(document); 285 | const payload = setPayload(res, { 286 | data: { animeList }, 287 | pagination, 288 | }); 289 | 290 | res.json(payload); 291 | } catch (error) { 292 | next(error); 293 | } 294 | }, 295 | 296 | async getScheduledAnimes(req: Request, res: Response, next: NextFunction) { 297 | try { 298 | const query = v.parse(kuramanimeSchema.query.scheduledAnimes, req.query); 299 | const page = Number(query?.page) || 1; 300 | const day = query?.day || "all"; 301 | const pathname = `/schedule?scheduled_day=${day}&page=${page}`; 302 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 303 | const animeList = kuramanimeParser.parseScheduledAnimes(document); 304 | const pagination = kuramanimeParser.parsePagination(document); 305 | const payload = setPayload(res, { 306 | data: { animeList }, 307 | pagination, 308 | }); 309 | 310 | res.json(payload); 311 | } catch (error) { 312 | next(error); 313 | } 314 | }, 315 | 316 | async getAnimeDetails(req: Request, res: Response, next: NextFunction) { 317 | try { 318 | const params = v.parse(kuramanimeSchema.param.animeDetails, req.params); 319 | const pathname = `/anime/${params.animeId}/${params.animeSlug}`; 320 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 321 | const details = kuramanimeParser.parseAnimeDetails(document, params); 322 | const payload = setPayload(res, { 323 | data: { details }, 324 | }); 325 | 326 | res.json(payload); 327 | } catch (error) { 328 | next(error); 329 | } 330 | }, 331 | 332 | async getBatchDetails(req: Request, res: Response, next: NextFunction) { 333 | try { 334 | const params = v.parse(kuramanimeSchema.param.batchDetails, req.params); 335 | const mainPathname = `/anime/${params.animeId}/${params.animeSlug}/batch/${params.batchId}`; 336 | const secret = await kuramanimeScraper.scrapeSecret(`${baseUrl}/${mainPathname}`); 337 | const pathname = `${mainPathname}?Ub3BzhijicHXZdv=${secret}&C2XAPerzX1BM7V9=kuramadrive&page=1`; 338 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 339 | const details = kuramanimeParser.parseBatchDetails(document, params); 340 | const payload = setPayload(res, { 341 | data: { details }, 342 | }); 343 | 344 | res.json(payload); 345 | } catch (error) { 346 | next(error); 347 | } 348 | }, 349 | 350 | async getEpisodeDetails(req: Request, res: Response, next: NextFunction) { 351 | try { 352 | const params = v.parse(kuramanimeSchema.param.episodeDetails, req.params); 353 | const mainPathname = `anime/${params.animeId}/${params.animeSlug}/episode/${params.episodeId}`; 354 | const secret = await kuramanimeScraper.scrapeSecret(`${baseUrl}/${mainPathname}`); 355 | const pathname = `${mainPathname}?Ub3BzhijicHXZdv=${secret}&C2XAPerzX1BM7V9=kuramadrive&page=1`; 356 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 357 | const details = kuramanimeParser.parseEpisodeDetails(document, params); 358 | const payload = setPayload(res, { 359 | data: { details }, 360 | }); 361 | 362 | res.json(payload); 363 | } catch (error) { 364 | next(error); 365 | } 366 | }, 367 | }; 368 | 369 | export default kuramanimeController; 370 | -------------------------------------------------------------------------------- /dist/controllers/otakudesu.controller.js: -------------------------------------------------------------------------------- 1 | import otakudesuScraper from "../scrapers/otakudesu.scraper.js"; 2 | import otakudesuParser from "../parsers/otakudesu.parser.js"; 3 | import otakudesuConfig from "../configs/otakudesu.config.js"; 4 | import otakudesuSchema from "../schemas/otakudesu.schema.js"; 5 | import setPayload from "../helpers/setPayload.js"; 6 | import * as v from "valibot"; 7 | const { baseUrl } = otakudesuConfig; 8 | const otakudesuController = { 9 | async getRoot(req, res, next) { 10 | const routes = [ 11 | { 12 | method: "GET", 13 | path: "/otakudesu/home", 14 | description: "Halaman utama", 15 | pathParams: [], 16 | queryParams: [], 17 | }, 18 | { 19 | method: "GET", 20 | path: "/otakudesu/schedule", 21 | description: "Jadwal rilis", 22 | pathParams: [], 23 | queryParams: [], 24 | }, 25 | { 26 | method: "GET", 27 | path: "/otakudesu/anime", 28 | description: "Daftar semua anime", 29 | pathParams: [], 30 | queryParams: [], 31 | }, 32 | { 33 | method: "GET", 34 | path: "/otakudesu/genre", 35 | description: "Daftar semua genre", 36 | pathParams: [], 37 | queryParams: [], 38 | }, 39 | { 40 | method: "GET", 41 | path: "/otakudesu/ongoing", 42 | description: "Daftar anime sedang tayang", 43 | pathParams: [], 44 | queryParams: [ 45 | { 46 | key: "page", 47 | value: "string", 48 | defaultValue: "1", 49 | required: false, 50 | }, 51 | ], 52 | }, 53 | { 54 | method: "GET", 55 | path: "/otakudesu/completed", 56 | description: "Daftar anime selesai", 57 | pathParams: [], 58 | queryParams: [ 59 | { 60 | key: "page", 61 | value: "string", 62 | defaultValue: "1", 63 | required: false, 64 | }, 65 | ], 66 | }, 67 | { 68 | method: "GET", 69 | path: "/otakudesu/search", 70 | description: "Daftar anime berdasarkan pencarian", 71 | pathParams: [], 72 | queryParams: [ 73 | { 74 | key: "q", 75 | value: "string", 76 | defaultValue: null, 77 | required: true, 78 | }, 79 | ], 80 | }, 81 | { 82 | method: "GET", 83 | path: "/otakudesu/genre/{genreId}", 84 | description: "Daftar anime berdasarkan genre", 85 | pathParams: [ 86 | { 87 | key: "genreId", 88 | value: "string", 89 | defaultValue: null, 90 | required: true, 91 | }, 92 | ], 93 | queryParams: [ 94 | { 95 | key: "page", 96 | value: "string", 97 | defaultValue: "1", 98 | required: false, 99 | }, 100 | ], 101 | }, 102 | { 103 | method: "GET", 104 | path: "/otakudesu/batch/{batchId}", 105 | description: "Batch anime berdasarkan id batch", 106 | pathParams: [ 107 | { 108 | key: "batchId", 109 | value: "string", 110 | defaultValue: null, 111 | required: true, 112 | }, 113 | ], 114 | queryParams: [], 115 | }, 116 | { 117 | method: "GET", 118 | path: "/otakudesu/anime/{animeId}", 119 | description: "Detail anime berdasarkan id anime", 120 | pathParams: [ 121 | { 122 | key: "animeId", 123 | value: "string", 124 | defaultValue: null, 125 | required: true, 126 | }, 127 | ], 128 | queryParams: [], 129 | }, 130 | { 131 | method: "GET", 132 | path: "/otakudesu/episode/{episodeId}", 133 | description: "Detail episode berdasarkan id episode", 134 | pathParams: [ 135 | { 136 | key: "episodeId", 137 | value: "string", 138 | defaultValue: null, 139 | required: true, 140 | }, 141 | ], 142 | queryParams: [], 143 | }, 144 | { 145 | method: "GET | POST", 146 | path: "/otakudesu/server/{serverId}", 147 | description: "Link video berdasarkan id server", 148 | pathParams: [ 149 | { 150 | key: "serverId", 151 | value: "string", 152 | defaultValue: null, 153 | required: true, 154 | }, 155 | ], 156 | queryParams: [], 157 | }, 158 | ]; 159 | res.json(setPayload(res, { 160 | message: "Status: OK 🚀", 161 | data: { routes }, 162 | })); 163 | }, 164 | async getHome(req, res, next) { 165 | try { 166 | const ref = "https://google.com/"; 167 | const document = await otakudesuScraper.scrapeDOM("/", ref); 168 | const home = otakudesuParser.parseHome(document); 169 | const payload = setPayload(res, { 170 | data: home, 171 | }); 172 | res.json(payload); 173 | } 174 | catch (error) { 175 | next(error); 176 | } 177 | }, 178 | async getSchedule(req, res, next) { 179 | try { 180 | const pathname = "/jadwal-rilis/"; 181 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 182 | const scheduleList = otakudesuParser.parseSchedules(document); 183 | const payload = setPayload(res, { 184 | data: { scheduleList }, 185 | }); 186 | res.json(payload); 187 | } 188 | catch (error) { 189 | next(error); 190 | } 191 | }, 192 | async getAllAnimes(req, res, next) { 193 | try { 194 | const pathname = "/anime-list/"; 195 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl, true); 196 | const list = otakudesuParser.parseAllAnimes(document); 197 | const payload = setPayload(res, { 198 | data: { list }, 199 | }); 200 | res.json(payload); 201 | } 202 | catch (error) { 203 | next(error); 204 | } 205 | }, 206 | async getAllGenres(req, res, next) { 207 | try { 208 | const pathname = "/genre-list/"; 209 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 210 | const genreList = otakudesuParser.parseAllGenres(document); 211 | const payload = setPayload(res, { 212 | data: { genreList }, 213 | }); 214 | res.json(payload); 215 | } 216 | catch (error) { 217 | next(error); 218 | } 219 | }, 220 | async getOngoingAnimes(req, res, next) { 221 | try { 222 | const page = Number(v.parse(otakudesuSchema.query.animes, req.query)?.page); 223 | const pathname = page > 1 ? `/ongoing-anime/page/${page}/` : "/ongoing-anime/"; 224 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 225 | const animeList = otakudesuParser.parseOngoingAnimes(document); 226 | const pagination = otakudesuParser.parsePagination(document); 227 | const payload = setPayload(res, { 228 | data: { animeList }, 229 | pagination, 230 | }); 231 | res.json(payload); 232 | } 233 | catch (error) { 234 | next(error); 235 | } 236 | }, 237 | async getCompletedAnimes(req, res, next) { 238 | try { 239 | const page = Number(v.parse(otakudesuSchema.query.animes, req.query)?.page); 240 | const pathname = page > 1 ? `/complete-anime/page/${page}/` : "/complete-anime/"; 241 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 242 | const animeList = otakudesuParser.parseCompletedAnimes(document); 243 | const pagination = otakudesuParser.parsePagination(document); 244 | const payload = setPayload(res, { 245 | data: { animeList }, 246 | pagination, 247 | }); 248 | res.json(payload); 249 | } 250 | catch (error) { 251 | next(error); 252 | } 253 | }, 254 | async getSearchedAnimes(req, res, next) { 255 | try { 256 | const { q } = v.parse(otakudesuSchema.query.searchedAnimes, req.query); 257 | const pathname = `/?s=${q}&post_type=anime`; 258 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 259 | const animeList = otakudesuParser.parseSearchedAnimes(document); 260 | const payload = setPayload(res, { 261 | data: { animeList }, 262 | }); 263 | res.json(payload); 264 | } 265 | catch (error) { 266 | next(error); 267 | } 268 | }, 269 | async getAnimesByGenre(req, res, next) { 270 | try { 271 | const genreId = req.params.genreId; 272 | const page = Number(v.parse(otakudesuSchema.query.animes, req.query)?.page); 273 | const pathname = page > 1 ? `/genres/${genreId}/page/${page}/` : `/genres/${genreId}/`; 274 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 275 | const animeList = otakudesuParser.parseAnimesByGenre(document); 276 | const pagination = otakudesuParser.parsePagination(document); 277 | const payload = setPayload(res, { 278 | data: { animeList }, 279 | pagination, 280 | }); 281 | res.json(payload); 282 | } 283 | catch (error) { 284 | next(error); 285 | } 286 | }, 287 | async getBatchDetails(req, res, next) { 288 | try { 289 | const batchId = req.params.batchId; 290 | const pathname = `/batch/${batchId}/`; 291 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 292 | const details = otakudesuParser.parseBatchDetails(document); 293 | const payload = setPayload(res, { 294 | data: { details }, 295 | }); 296 | res.json(payload); 297 | } 298 | catch (error) { 299 | next(error); 300 | } 301 | }, 302 | async getAnimeDetails(req, res, next) { 303 | try { 304 | const animeId = req.params.animeId; 305 | const pathname = `/anime/${animeId}/`; 306 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 307 | const details = otakudesuParser.parseAnimeDetails(document); 308 | const payload = setPayload(res, { 309 | data: { details }, 310 | }); 311 | res.json(payload); 312 | } 313 | catch (error) { 314 | next(error); 315 | } 316 | }, 317 | async getEpisodeDetails(req, res, next) { 318 | try { 319 | const episodeId = req.params.episodeId; 320 | const pathname = `/episode/${episodeId}/`; 321 | const document = await otakudesuScraper.scrapeDOM(pathname, baseUrl); 322 | const details = await otakudesuParser.parseEpisodeDetails(document, new URL(pathname, baseUrl).toString()); 323 | const payload = setPayload(res, { 324 | data: { details }, 325 | }); 326 | res.json(payload); 327 | } 328 | catch (error) { 329 | next(error); 330 | } 331 | }, 332 | async getServerDetails(req, res, next) { 333 | try { 334 | const serverId = req.params.serverId || ""; 335 | const details = await otakudesuParser.parseServerDetails(serverId); 336 | const payload = setPayload(res, { 337 | data: { details }, 338 | }); 339 | res.json(payload); 340 | } 341 | catch (error) { 342 | if (error.message.includes("is not valid JSON")) { 343 | res.status(400).json(setPayload(res)); 344 | return; 345 | } 346 | next(error); 347 | } 348 | }, 349 | }; 350 | export default otakudesuController; 351 | -------------------------------------------------------------------------------- /dist/controllers/kuramanime.controller.js: -------------------------------------------------------------------------------- 1 | import setPayload from "../helpers/setPayload.js"; 2 | import kuramanimeConfig from "../configs/kuramanime.config.js"; 3 | import kuramanimeScraper from "../scrapers/kuramanime.scraper.js"; 4 | import kuramanimeParser from "../parsers/kuramanime.parser.js"; 5 | import kuramanimeSchema from "../schemas/kuramanime.schema.js"; 6 | import * as v from "valibot"; 7 | const { baseUrl } = kuramanimeConfig; 8 | const kuramanimeController = { 9 | async getRoot(req, res, next) { 10 | const routes = [ 11 | { 12 | method: "GET", 13 | path: "/kuramanime/home", 14 | description: "Halaman utama", 15 | pathParams: [], 16 | queryParams: [], 17 | }, 18 | { 19 | method: "GET", 20 | path: "/kuramanime/anime", 21 | description: "Daftar anime", 22 | pathParams: [], 23 | queryParams: [ 24 | { 25 | key: "search", 26 | value: "string", 27 | defaultValue: null, 28 | required: false, 29 | }, 30 | { 31 | key: "status", 32 | value: '"ongoing" | "completed" | "upcoming" | "movie"', 33 | defaultValue: null, 34 | required: false, 35 | }, 36 | { 37 | key: "sort", 38 | value: '"a-z" | "z-a" | "oldest" | "latest" | "popular" | "most_viewed" | "updated"', 39 | defaultValue: '"latest" | "updated"', 40 | required: false, 41 | }, 42 | { 43 | key: "page", 44 | value: "string", 45 | defaultValue: "1", 46 | required: false, 47 | }, 48 | ], 49 | }, 50 | { 51 | method: "GET", 52 | path: "/kuramanime/schedule", 53 | description: "Jadwal rilis", 54 | pathParams: [], 55 | queryParams: [ 56 | { 57 | key: "day", 58 | value: '"all" | "random" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"', 59 | defaultValue: "all", 60 | required: false, 61 | }, 62 | { 63 | key: "page", 64 | value: "string", 65 | defaultValue: "1", 66 | required: false, 67 | }, 68 | ], 69 | }, 70 | { 71 | method: "GET", 72 | path: "/kuramanime/properties/{propertyType}", 73 | description: "Daftar properti berdasarkan tipe properti", 74 | pathParams: [ 75 | { 76 | key: "propertyType", 77 | value: '"genre" | "season" | "studio" | "type" | "quality" | "source" | "country"', 78 | defaultValue: null, 79 | required: true, 80 | }, 81 | ], 82 | queryParams: [], 83 | }, 84 | { 85 | method: "GET", 86 | path: "/kuramanime/properties/{propertyType}/{propertyId}", 87 | description: "Daftar anime berdasarkan id properti", 88 | pathParams: [ 89 | { 90 | key: "propertyType", 91 | value: '"genre" | "season" | "studio" | "type" | "quality" | "source" | "country"', 92 | defaultValue: null, 93 | required: true, 94 | }, 95 | { 96 | key: "propertyId", 97 | value: "string", 98 | defaultValue: null, 99 | required: true, 100 | }, 101 | ], 102 | queryParams: [], 103 | }, 104 | { 105 | method: "GET", 106 | path: "/kuramanime/anime/{animeId}/{animeSlug}", 107 | description: "Detail anime berdasarkan id anime", 108 | pathParams: [ 109 | { 110 | key: "animeId", 111 | value: "string", 112 | defaultValue: null, 113 | required: true, 114 | }, 115 | { 116 | key: "animeSlug", 117 | value: "string", 118 | defaultValue: null, 119 | required: true, 120 | }, 121 | ], 122 | queryParams: [], 123 | }, 124 | { 125 | method: "GET", 126 | path: "/kuramanime/batch/{animeId}/{animeSlug}/{batchId}", 127 | description: "Batch anime berdasarkan id batch", 128 | pathParams: [ 129 | { 130 | key: "animeId", 131 | value: "string", 132 | defaultValue: null, 133 | required: true, 134 | }, 135 | { 136 | key: "animeSlug", 137 | value: "string", 138 | defaultValue: null, 139 | required: true, 140 | }, 141 | { 142 | key: "batchId", 143 | value: "string", 144 | defaultValue: null, 145 | required: true, 146 | }, 147 | ], 148 | queryParams: [], 149 | }, 150 | { 151 | method: "GET", 152 | path: "/kuramanime/episode/{animeId}/{animeSlug}/{episodeId}", 153 | description: "Detail episode berdasarkan id episode", 154 | pathParams: [ 155 | { 156 | key: "animeId", 157 | value: "string", 158 | defaultValue: null, 159 | required: true, 160 | }, 161 | { 162 | key: "animeSlug", 163 | value: "string", 164 | defaultValue: null, 165 | required: true, 166 | }, 167 | { 168 | key: "episodeId", 169 | value: "string", 170 | defaultValue: null, 171 | required: true, 172 | }, 173 | ], 174 | queryParams: [], 175 | }, 176 | ]; 177 | res.json(setPayload(res, { 178 | message: "Status: OK 🚀", 179 | data: { routes }, 180 | })); 181 | }, 182 | async getHome(req, res, next) { 183 | try { 184 | const document = await kuramanimeScraper.scrapeDOM("/", "https://google.com"); 185 | const home = kuramanimeParser.parseHome(document); 186 | const payload = setPayload(res, { 187 | data: home, 188 | }); 189 | res.json(payload); 190 | } 191 | catch (error) { 192 | next(error); 193 | } 194 | }, 195 | async getAnimes(req, res, next) { 196 | try { 197 | const query = v.parse(kuramanimeSchema.query.animes, req.query); 198 | const status = query?.status; 199 | const search = query?.search || ""; 200 | const page = Number(query?.page) || 1; 201 | const sort = (query?.sort === "a-z" 202 | ? "ascending" 203 | : query?.sort === "z-a" 204 | ? "descending" 205 | : query?.sort) || (status === "ongoing" ? "updated" : "latest"); 206 | function getPathname() { 207 | if (status) { 208 | return `/quick/${status === "completed" ? "finished" : status}?order_by=${sort}&page=${page}`; 209 | } 210 | if (search) { 211 | return `/anime?order_by=${sort}&page=${page}&search=${search}`; 212 | } 213 | return `/anime?order_by=${sort}&page=${page}`; 214 | } 215 | const pathname = getPathname(); 216 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 217 | const pagination = kuramanimeParser.parsePagination(document); 218 | const payload = setPayload(res, { 219 | data: { 220 | animeList: status !== "ongoing" ? kuramanimeParser.parseAnimes(document) : undefined, 221 | episodeList: status === "ongoing" ? kuramanimeParser.parseEpisodes(document) : undefined, 222 | }, 223 | pagination, 224 | }); 225 | res.json(payload); 226 | } 227 | catch (error) { 228 | next(error); 229 | } 230 | }, 231 | async getProperties(req, res, next) { 232 | try { 233 | const { propertyType } = v.parse(kuramanimeSchema.param.properties, req.params); 234 | const pathname = `/properties/${propertyType}`; 235 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 236 | const propertyList = kuramanimeParser.parseProperties(document); 237 | const payload = setPayload(res, { 238 | data: { 239 | propertyType, 240 | propertyList, 241 | }, 242 | }); 243 | res.json(payload); 244 | } 245 | catch (error) { 246 | next(error); 247 | } 248 | }, 249 | async getAnimesByProperty(req, res, next) { 250 | try { 251 | const { propertyType, propertyId } = v.parse(kuramanimeSchema.param.animesByPropertyId, req.params); 252 | const query = v.parse(kuramanimeSchema.query.animesByPropertyId, req.query); 253 | const page = Number(query?.page) || 1; 254 | const sort = (query?.sort === "a-z" 255 | ? "ascending" 256 | : query?.sort === "z-a" 257 | ? "descending" 258 | : query?.sort) || "latest"; 259 | const pathname = `/properties/${propertyType}/${propertyId}?order_by=${sort}&page=${page}`; 260 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 261 | const animeList = kuramanimeParser.parseAnimes(document); 262 | const pagination = kuramanimeParser.parsePagination(document); 263 | const payload = setPayload(res, { 264 | data: { animeList }, 265 | pagination, 266 | }); 267 | res.json(payload); 268 | } 269 | catch (error) { 270 | next(error); 271 | } 272 | }, 273 | async getScheduledAnimes(req, res, next) { 274 | try { 275 | const query = v.parse(kuramanimeSchema.query.scheduledAnimes, req.query); 276 | const page = Number(query?.page) || 1; 277 | const day = query?.day || "all"; 278 | const pathname = `/schedule?scheduled_day=${day}&page=${page}`; 279 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 280 | const animeList = kuramanimeParser.parseScheduledAnimes(document); 281 | const pagination = kuramanimeParser.parsePagination(document); 282 | const payload = setPayload(res, { 283 | data: { animeList }, 284 | pagination, 285 | }); 286 | res.json(payload); 287 | } 288 | catch (error) { 289 | next(error); 290 | } 291 | }, 292 | async getAnimeDetails(req, res, next) { 293 | try { 294 | const params = v.parse(kuramanimeSchema.param.animeDetails, req.params); 295 | const pathname = `/anime/${params.animeId}/${params.animeSlug}`; 296 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 297 | const details = kuramanimeParser.parseAnimeDetails(document, params); 298 | const payload = setPayload(res, { 299 | data: { details }, 300 | }); 301 | res.json(payload); 302 | } 303 | catch (error) { 304 | next(error); 305 | } 306 | }, 307 | async getBatchDetails(req, res, next) { 308 | try { 309 | const params = v.parse(kuramanimeSchema.param.batchDetails, req.params); 310 | const mainPathname = `/anime/${params.animeId}/${params.animeSlug}/batch/${params.batchId}`; 311 | const secret = await kuramanimeScraper.scrapeSecret(`${baseUrl}/${mainPathname}`); 312 | const pathname = `${mainPathname}?Ub3BzhijicHXZdv=${secret}&C2XAPerzX1BM7V9=kuramadrive&page=1`; 313 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 314 | const details = kuramanimeParser.parseBatchDetails(document, params); 315 | const payload = setPayload(res, { 316 | data: { details }, 317 | }); 318 | res.json(payload); 319 | } 320 | catch (error) { 321 | next(error); 322 | } 323 | }, 324 | async getEpisodeDetails(req, res, next) { 325 | try { 326 | const params = v.parse(kuramanimeSchema.param.episodeDetails, req.params); 327 | const mainPathname = `anime/${params.animeId}/${params.animeSlug}/episode/${params.episodeId}`; 328 | const secret = await kuramanimeScraper.scrapeSecret(`${baseUrl}/${mainPathname}`); 329 | const pathname = `${mainPathname}?Ub3BzhijicHXZdv=${secret}&C2XAPerzX1BM7V9=kuramadrive&page=1`; 330 | const document = await kuramanimeScraper.scrapeDOM(pathname, baseUrl); 331 | const details = kuramanimeParser.parseEpisodeDetails(document, params); 332 | const payload = setPayload(res, { 333 | data: { details }, 334 | }); 335 | res.json(payload); 336 | } 337 | catch (error) { 338 | next(error); 339 | } 340 | }, 341 | }; 342 | export default kuramanimeController; 343 | -------------------------------------------------------------------------------- /src/parsers/kuramanime.parser.ts: -------------------------------------------------------------------------------- 1 | import * as T from "@interfaces/kuramanime.interface.js"; 2 | import * as v from "valibot"; 3 | import type { HTMLElement } from "node-html-parser"; 4 | import { parse } from "node-html-parser"; 5 | import mainParser from "./main/main.parser.js"; 6 | import kuramanimeExtraParser from "./extra/kuramanime.extra.parser.js"; 7 | import errorinCuy from "@helpers/errorinCuy.js"; 8 | import kuramanimeSchema from "@schemas/kuramanime.schema.js"; 9 | import kuramanimeConfig from "@configs/kuramanime.config.js"; 10 | 11 | const { Text, Attr, Id, Num, Src, AnimeSrc } = mainParser; 12 | const { baseUrl } = kuramanimeConfig; 13 | 14 | const kuramanimeParser = { 15 | parseHome(document: HTMLElement): T.IHome { 16 | const home: T.IHome = { 17 | ongoing: { 18 | kuramanimeUrl: "", 19 | episodeList: [], 20 | }, 21 | completed: { 22 | kuramanimeUrl: "", 23 | animeList: [], 24 | }, 25 | movie: { 26 | kuramanimeUrl: "", 27 | animeList: [], 28 | }, 29 | }; 30 | 31 | const homeElems = document.querySelectorAll(".product.spad .trending__product"); 32 | 33 | homeElems.forEach((homeEl, index) => { 34 | const kuramanimeUrl = AnimeSrc(homeEl.querySelector(".btn__all a")); 35 | const animeElems = homeEl.querySelectorAll(".row .product__item"); 36 | 37 | const animeList: T.IAnimeCard[] = animeElems.map((animeEl) => { 38 | const animeCard = kuramanimeExtraParser.parseAnimeCard(animeEl); 39 | 40 | return animeCard; 41 | }); 42 | 43 | const episodeList: T.IEpisodeCard[] = animeElems.map((animeEl) => { 44 | const episodeCard = kuramanimeExtraParser.parseEpisodeCard(animeEl); 45 | 46 | return episodeCard; 47 | }); 48 | 49 | const key = index === 0 ? "ongoing" : index === 1 ? "completed" : "movie"; 50 | 51 | home[key].kuramanimeUrl = kuramanimeUrl; 52 | home[index === 1 ? "completed" : "movie"].animeList = animeList; 53 | 54 | if (index === 0) { 55 | home.ongoing.episodeList = episodeList; 56 | } 57 | }); 58 | 59 | return home; 60 | }, 61 | 62 | parseAnimes(document: HTMLElement): T.IAnimeCard[] { 63 | const animeElems = document.querySelectorAll("#animeList .product__item"); 64 | 65 | const animeList: T.IAnimeCard[] = animeElems.map((animeEl) => { 66 | const animeCard = kuramanimeExtraParser.parseAnimeCard(animeEl); 67 | 68 | return animeCard; 69 | }); 70 | 71 | if (animeList.length === 0) { 72 | throw errorinCuy(404); 73 | } 74 | 75 | return animeList; 76 | }, 77 | 78 | parseScheduledAnimes(document: HTMLElement): T.IScheduledAnimeCard[] { 79 | const animeElems = document.querySelectorAll("#animeList .product__item"); 80 | 81 | const animeList: T.IScheduledAnimeCard[] = animeElems.map((animeEl) => { 82 | const animeCard = kuramanimeExtraParser.parseScheduledAnimeCard(animeEl); 83 | 84 | return animeCard; 85 | }); 86 | 87 | if (animeList.length === 0) { 88 | throw errorinCuy(404); 89 | } 90 | 91 | return animeList; 92 | }, 93 | 94 | parseEpisodes(document: HTMLElement): T.IEpisodeCard[] { 95 | const episodeElems = document.querySelectorAll("#animeList .product__item"); 96 | 97 | const episodeList: T.IEpisodeCard[] = episodeElems.map((animeEl) => { 98 | const episodeCard = kuramanimeExtraParser.parseEpisodeCard(animeEl); 99 | 100 | return episodeCard; 101 | }); 102 | 103 | if (episodeList.length === 0) { 104 | throw errorinCuy(404); 105 | } 106 | 107 | return episodeList; 108 | }, 109 | 110 | parseProperties(document: HTMLElement): T.ITextPropertyCard[] { 111 | const propertyElems = document.querySelectorAll("#animeList ul li a"); 112 | const propertyList: T.ITextPropertyCard[] = propertyElems.map((propertyEl) => { 113 | const { id, title, kuramanimeUrl } = kuramanimeExtraParser.parseTextCard(propertyEl); 114 | 115 | return { title, propertyId: id, kuramanimeUrl }; 116 | }); 117 | 118 | if (propertyList.length === 0) { 119 | throw errorinCuy(404); 120 | } 121 | 122 | return propertyList; 123 | }, 124 | 125 | parseAnimeDetails( 126 | document: HTMLElement, 127 | { animeId, animeSlug }: v.InferOutput 128 | ): T.IAnimeDetails { 129 | const title = Text(document.querySelector(".anime__details__title h3")); 130 | const alternativeTitle = Text( 131 | document.querySelector(".anime__details__title h3")?.nextElementSibling 132 | ); 133 | const poster = Attr(document.querySelector(".anime__details__pic"), "data-setbg"); 134 | const synopsis: ISynopsis = { 135 | paragraphList: document 136 | .querySelectorAll("#synopsisField br") 137 | .map((pEl) => { 138 | const paragraph = pEl.previousSibling?.text.trim(); 139 | 140 | if (paragraph && paragraph !== "\n") { 141 | return paragraph; 142 | } 143 | 144 | return ""; 145 | }) 146 | .filter((p) => p !== ""), 147 | }; 148 | 149 | synopsis.paragraphList.push(Text(document.querySelector("#synopsisField i"))); 150 | 151 | const episodeListEl = parse( 152 | Attr(document.querySelector("#episodeLists"), "data-content").trim() 153 | ); 154 | let firstEpisode: number | null = null; 155 | let lastEpisode: number | null = null; 156 | let firstEpisodeByIndex: number | null = null; 157 | let lastEpisodeByIndex: number | null = null; 158 | 159 | const episodeElems = episodeListEl.querySelectorAll("a"); 160 | 161 | episodeElems.forEach((episodeEl, index) => { 162 | const text = Text(episodeEl); 163 | const match = text.match(/\b(\d+)\b/); 164 | const episode = match ? Number(match[1]) : null; 165 | 166 | if (text.includes("Terlama")) { 167 | firstEpisode = episode; 168 | } else if (text.includes("Terbaru")) { 169 | lastEpisode = episode; 170 | } else { 171 | if (index === 0) { 172 | firstEpisodeByIndex = episode; 173 | } else if (index === episodeElems.length - 1) { 174 | lastEpisodeByIndex = episode; 175 | } 176 | } 177 | }); 178 | 179 | const infoElems = document.querySelectorAll(".anime__details__widget ul li .col-9"); 180 | const getInfo = kuramanimeExtraParser.parseInfo(infoElems); 181 | const getInfoProperty = kuramanimeExtraParser.parseInfoProperty(infoElems); 182 | const getInfoProperties = kuramanimeExtraParser.parseInfoProperties(infoElems); 183 | const batchElems = parse( 184 | Attr(document.querySelector("#episodeBatchLists"), "data-content").trim() 185 | ).querySelectorAll("a"); 186 | const batchList: T.ITextBatchCard[] = batchElems.map((batchEl) => { 187 | return { 188 | title: Text(batchEl), 189 | batchId: Id(batchEl), 190 | animeId, 191 | animeSlug, 192 | kuramanimeUrl: AnimeSrc(batchEl), 193 | }; 194 | }); 195 | 196 | const similarAnimeElems = document.querySelectorAll(".breadcrumb__links__v2 a"); 197 | const similarAnimeList: T.ITextAnimeCard[] = similarAnimeElems.map((animeEl) => { 198 | return { 199 | title: Text(animeEl).replace("- ", ""), 200 | animeId: kuramanimeExtraParser.parseAnimeId(animeEl), 201 | animeSlug: Id(animeEl), 202 | kuramanimeUrl: AnimeSrc(animeEl), 203 | }; 204 | }); 205 | 206 | return { 207 | title, 208 | alternativeTitle, 209 | animeId, 210 | animeSlug, 211 | poster, 212 | synopsis, 213 | episode: { 214 | first: firstEpisode || firstEpisodeByIndex, 215 | last: lastEpisode || lastEpisodeByIndex || firstEpisodeByIndex, 216 | }, 217 | episodes: getInfo(1), 218 | aired: getInfo(3).replace(/\s+/g, " ").trim(), 219 | duration: getInfo(5), 220 | explicit: getInfo(10), 221 | score: getInfo(14), 222 | fans: getInfo(15), 223 | rating: getInfo(16), 224 | credit: getInfo(17), 225 | type: getInfoProperty(0), 226 | status: getInfoProperty(2), 227 | season: getInfoProperty(4), 228 | quality: getInfoProperty(6), 229 | country: getInfoProperty(7), 230 | source: getInfoProperty(8), 231 | genreList: getInfoProperties(9), 232 | themeList: getInfoProperties(12), 233 | demographicList: getInfoProperties(11), 234 | studioList: getInfoProperties(13), 235 | batchList, 236 | similarAnimeList, 237 | }; 238 | }, 239 | 240 | parseBatchDetails( 241 | document: HTMLElement, 242 | { animeId, animeSlug }: v.InferOutput 243 | ): T.IBatchDetails { 244 | const batchTitleEl = document.querySelector(".breadcrumb__links #episodeTitle"); 245 | const downloadQualityElems = document.querySelectorAll("#animeDownloadLink h6"); 246 | const download: T.IBatchDetails["download"] = { 247 | qualityList: downloadQualityElems.map((downloadQualityEl) => { 248 | const title = Text(downloadQualityEl); 249 | const urlList: IUrl[] = []; 250 | 251 | let urlEl: HTMLElement | null | undefined = downloadQualityEl; 252 | 253 | while (urlEl) { 254 | if (urlEl.tagName === "A") { 255 | urlList.push({ 256 | title: Text(urlEl), 257 | url: Attr(urlEl, "href"), 258 | }); 259 | } else if (urlEl.tagName === "BR") { 260 | break; 261 | } 262 | 263 | urlEl = urlEl?.nextElementSibling; 264 | } 265 | 266 | return { 267 | title: title.split("—")[0]?.trim() || "", 268 | size: title.split("—")[1]?.trim().replace(/\(|\)/g, "") || "", 269 | urlList, 270 | }; 271 | }), 272 | }; 273 | 274 | return { 275 | title: Text(batchTitleEl?.previousElementSibling), 276 | batchTitle: Text(batchTitleEl), 277 | animeId, 278 | animeSlug, 279 | download, 280 | }; 281 | }, 282 | 283 | parseEpisodeDetails( 284 | document: HTMLElement, 285 | { animeId, animeSlug }: v.InferOutput 286 | ): T.IEpisodeDetails { 287 | const episodeTitleEl = document.querySelector(".breadcrumb__links #episodeTitle"); 288 | const prevEpisodeEl = document.querySelector(".episode__navigations a:first-child"); 289 | const nextEpisodeEl = document.querySelector(".episode__navigations a:last-child"); 290 | 291 | let prevEpisode: T.ITextEpisodeCard | null = null; 292 | let nextEpisode: T.ITextEpisodeCard | null = null; 293 | 294 | if (!prevEpisodeEl?.classList.contains("nav__disabled")) { 295 | prevEpisode = { 296 | title: "Prev episode", 297 | episodeId: Id(prevEpisodeEl), 298 | animeId, 299 | animeSlug, 300 | kuramanimeUrl: AnimeSrc(prevEpisodeEl, baseUrl), 301 | }; 302 | } 303 | 304 | if (!nextEpisodeEl?.classList.contains("nav__disabled")) { 305 | nextEpisode = { 306 | title: "Next episode", 307 | episodeId: Id(nextEpisodeEl), 308 | animeId, 309 | animeSlug, 310 | kuramanimeUrl: AnimeSrc(nextEpisodeEl, baseUrl), 311 | }; 312 | } 313 | 314 | const serverQualityElems = document.querySelectorAll("#player source"); 315 | const server: T.IEpisodeDetails["server"] = { 316 | qualityList: serverQualityElems.map((serverQualityEl) => { 317 | const title = Attr(serverQualityEl, "size"); 318 | const url = Attr(serverQualityEl, "src"); 319 | 320 | return { 321 | title, 322 | urlList: [ 323 | { 324 | title: "kuramadrive", 325 | url, 326 | }, 327 | ], 328 | }; 329 | }), 330 | }; 331 | 332 | const downloadQualityElems = document.querySelectorAll("#animeDownloadLink h6"); 333 | const download: T.IBatchDetails["download"] = { 334 | qualityList: downloadQualityElems.map((downloadQualityEl) => { 335 | const title = Text(downloadQualityEl); 336 | const urlList: IUrl[] = []; 337 | 338 | let urlEl: HTMLElement | null | undefined = downloadQualityEl; 339 | 340 | while (urlEl) { 341 | if (urlEl.tagName === "A") { 342 | urlList.push({ 343 | title: Text(urlEl), 344 | url: Attr(urlEl, "href"), 345 | }); 346 | } else if (urlEl.tagName === "BR") { 347 | break; 348 | } 349 | 350 | urlEl = urlEl?.nextElementSibling; 351 | } 352 | 353 | return { 354 | title: title.split("—")[0]?.trim() || "", 355 | size: title.split("—")[1]?.trim().replace(/\(|\)/g, "") || "", 356 | urlList, 357 | }; 358 | }), 359 | }; 360 | 361 | return { 362 | title: Text(episodeTitleEl?.previousElementSibling), 363 | episodeTitle: Text(episodeTitleEl), 364 | animeId, 365 | animeSlug, 366 | lastUpdated: Text(document.querySelector(".breadcrumb__links__v2 span:last-child")) 367 | .replace(/\s+/g, " ") 368 | .trim(), 369 | prevEpisode, 370 | hasPrevEpisode: prevEpisode ? true : false, 371 | nextEpisode, 372 | hasNextEpisode: nextEpisode ? true : false, 373 | episode: { 374 | first: 1, 375 | last: 1, 376 | }, 377 | server, 378 | download, 379 | }; 380 | }, 381 | 382 | parsePagination(document: HTMLElement): IPagination | undefined { 383 | const paginationEl = document.querySelector(".product__pagination"); 384 | 385 | if (paginationEl) { 386 | const pagination: IPagination = { 387 | currentPage: null, 388 | prevPage: null, 389 | hasPrevPage: false, 390 | nextPage: null, 391 | hasNextPage: false, 392 | totalPages: null, 393 | }; 394 | 395 | const currentPageEl = paginationEl.querySelector(".current-page"); 396 | const prevPageEl = paginationEl.querySelector(".page__link:first-child"); 397 | const prevPageVal = prevPageEl?.getAttribute("href")?.match(/(?:\?|&)page=(\d+)/)?.[1]; 398 | const nextPageEl = paginationEl.querySelector(".page__link:last-child"); 399 | const nextPageVal = nextPageEl?.getAttribute("href")?.match(/(?:\?|&)page=(\d+)/)?.[1]; 400 | const totalPagesEl = paginationEl.querySelector("a:last-child")?.previousElementSibling; 401 | 402 | pagination.currentPage = Number(Text(currentPageEl)) || null; 403 | pagination.prevPage = 404 | pagination.currentPage && Number(prevPageVal) < pagination.currentPage 405 | ? Number(prevPageVal) || null 406 | : null; 407 | pagination.hasPrevPage = pagination.prevPage ? true : false; 408 | pagination.nextPage = 409 | pagination.currentPage && Number(nextPageVal) > pagination.currentPage 410 | ? Number(nextPageVal) || null 411 | : null; 412 | pagination.hasNextPage = pagination.nextPage ? true : false; 413 | pagination.totalPages = Number(Text(totalPagesEl)) || null; 414 | 415 | return pagination; 416 | } 417 | 418 | return undefined; 419 | }, 420 | }; 421 | 422 | export default kuramanimeParser; 423 | -------------------------------------------------------------------------------- /dist/parsers/kuramanime.parser.js: -------------------------------------------------------------------------------- 1 | import * as T from "../interfaces/kuramanime.interface.js"; 2 | import * as v from "valibot"; 3 | import { parse } from "node-html-parser"; 4 | import mainParser from "./main/main.parser.js"; 5 | import kuramanimeExtraParser from "./extra/kuramanime.extra.parser.js"; 6 | import errorinCuy from "../helpers/errorinCuy.js"; 7 | import kuramanimeSchema from "../schemas/kuramanime.schema.js"; 8 | import kuramanimeConfig from "../configs/kuramanime.config.js"; 9 | const { Text, Attr, Id, Num, Src, AnimeSrc } = mainParser; 10 | const { baseUrl } = kuramanimeConfig; 11 | const kuramanimeParser = { 12 | parseHome(document) { 13 | const home = { 14 | ongoing: { 15 | kuramanimeUrl: "", 16 | episodeList: [], 17 | }, 18 | completed: { 19 | kuramanimeUrl: "", 20 | animeList: [], 21 | }, 22 | movie: { 23 | kuramanimeUrl: "", 24 | animeList: [], 25 | }, 26 | }; 27 | const homeElems = document.querySelectorAll(".product.spad .trending__product"); 28 | homeElems.forEach((homeEl, index) => { 29 | const kuramanimeUrl = AnimeSrc(homeEl.querySelector(".btn__all a")); 30 | const animeElems = homeEl.querySelectorAll(".row .product__item"); 31 | const animeList = animeElems.map((animeEl) => { 32 | const animeCard = kuramanimeExtraParser.parseAnimeCard(animeEl); 33 | return animeCard; 34 | }); 35 | const episodeList = animeElems.map((animeEl) => { 36 | const episodeCard = kuramanimeExtraParser.parseEpisodeCard(animeEl); 37 | return episodeCard; 38 | }); 39 | const key = index === 0 ? "ongoing" : index === 1 ? "completed" : "movie"; 40 | home[key].kuramanimeUrl = kuramanimeUrl; 41 | home[index === 1 ? "completed" : "movie"].animeList = animeList; 42 | if (index === 0) { 43 | home.ongoing.episodeList = episodeList; 44 | } 45 | }); 46 | return home; 47 | }, 48 | parseAnimes(document) { 49 | const animeElems = document.querySelectorAll("#animeList .product__item"); 50 | const animeList = animeElems.map((animeEl) => { 51 | const animeCard = kuramanimeExtraParser.parseAnimeCard(animeEl); 52 | return animeCard; 53 | }); 54 | if (animeList.length === 0) { 55 | throw errorinCuy(404); 56 | } 57 | return animeList; 58 | }, 59 | parseScheduledAnimes(document) { 60 | const animeElems = document.querySelectorAll("#animeList .product__item"); 61 | const animeList = animeElems.map((animeEl) => { 62 | const animeCard = kuramanimeExtraParser.parseScheduledAnimeCard(animeEl); 63 | return animeCard; 64 | }); 65 | if (animeList.length === 0) { 66 | throw errorinCuy(404); 67 | } 68 | return animeList; 69 | }, 70 | parseEpisodes(document) { 71 | const episodeElems = document.querySelectorAll("#animeList .product__item"); 72 | const episodeList = episodeElems.map((animeEl) => { 73 | const episodeCard = kuramanimeExtraParser.parseEpisodeCard(animeEl); 74 | return episodeCard; 75 | }); 76 | if (episodeList.length === 0) { 77 | throw errorinCuy(404); 78 | } 79 | return episodeList; 80 | }, 81 | parseProperties(document) { 82 | const propertyElems = document.querySelectorAll("#animeList ul li a"); 83 | const propertyList = propertyElems.map((propertyEl) => { 84 | const { id, title, kuramanimeUrl } = kuramanimeExtraParser.parseTextCard(propertyEl); 85 | return { title, propertyId: id, kuramanimeUrl }; 86 | }); 87 | if (propertyList.length === 0) { 88 | throw errorinCuy(404); 89 | } 90 | return propertyList; 91 | }, 92 | parseAnimeDetails(document, { animeId, animeSlug }) { 93 | const title = Text(document.querySelector(".anime__details__title h3")); 94 | const alternativeTitle = Text(document.querySelector(".anime__details__title h3")?.nextElementSibling); 95 | const poster = Attr(document.querySelector(".anime__details__pic"), "data-setbg"); 96 | const synopsis = { 97 | paragraphList: document 98 | .querySelectorAll("#synopsisField br") 99 | .map((pEl) => { 100 | const paragraph = pEl.previousSibling?.text.trim(); 101 | if (paragraph && paragraph !== "\n") { 102 | return paragraph; 103 | } 104 | return ""; 105 | }) 106 | .filter((p) => p !== ""), 107 | }; 108 | synopsis.paragraphList.push(Text(document.querySelector("#synopsisField i"))); 109 | const episodeListEl = parse(Attr(document.querySelector("#episodeLists"), "data-content").trim()); 110 | let firstEpisode = null; 111 | let lastEpisode = null; 112 | let firstEpisodeByIndex = null; 113 | let lastEpisodeByIndex = null; 114 | const episodeElems = episodeListEl.querySelectorAll("a"); 115 | episodeElems.forEach((episodeEl, index) => { 116 | const text = Text(episodeEl); 117 | const match = text.match(/\b(\d+)\b/); 118 | const episode = match ? Number(match[1]) : null; 119 | if (text.includes("Terlama")) { 120 | firstEpisode = episode; 121 | } 122 | else if (text.includes("Terbaru")) { 123 | lastEpisode = episode; 124 | } 125 | else { 126 | if (index === 0) { 127 | firstEpisodeByIndex = episode; 128 | } 129 | else if (index === episodeElems.length - 1) { 130 | lastEpisodeByIndex = episode; 131 | } 132 | } 133 | }); 134 | const infoElems = document.querySelectorAll(".anime__details__widget ul li .col-9"); 135 | const getInfo = kuramanimeExtraParser.parseInfo(infoElems); 136 | const getInfoProperty = kuramanimeExtraParser.parseInfoProperty(infoElems); 137 | const getInfoProperties = kuramanimeExtraParser.parseInfoProperties(infoElems); 138 | const batchElems = parse(Attr(document.querySelector("#episodeBatchLists"), "data-content").trim()).querySelectorAll("a"); 139 | const batchList = batchElems.map((batchEl) => { 140 | return { 141 | title: Text(batchEl), 142 | batchId: Id(batchEl), 143 | animeId, 144 | animeSlug, 145 | kuramanimeUrl: AnimeSrc(batchEl), 146 | }; 147 | }); 148 | const similarAnimeElems = document.querySelectorAll(".breadcrumb__links__v2 a"); 149 | const similarAnimeList = similarAnimeElems.map((animeEl) => { 150 | return { 151 | title: Text(animeEl).replace("- ", ""), 152 | animeId: kuramanimeExtraParser.parseAnimeId(animeEl), 153 | animeSlug: Id(animeEl), 154 | kuramanimeUrl: AnimeSrc(animeEl), 155 | }; 156 | }); 157 | return { 158 | title, 159 | alternativeTitle, 160 | animeId, 161 | animeSlug, 162 | poster, 163 | synopsis, 164 | episode: { 165 | first: firstEpisode || firstEpisodeByIndex, 166 | last: lastEpisode || lastEpisodeByIndex || firstEpisodeByIndex, 167 | }, 168 | episodes: getInfo(1), 169 | aired: getInfo(3).replace(/\s+/g, " ").trim(), 170 | duration: getInfo(5), 171 | explicit: getInfo(10), 172 | score: getInfo(14), 173 | fans: getInfo(15), 174 | rating: getInfo(16), 175 | credit: getInfo(17), 176 | type: getInfoProperty(0), 177 | status: getInfoProperty(2), 178 | season: getInfoProperty(4), 179 | quality: getInfoProperty(6), 180 | country: getInfoProperty(7), 181 | source: getInfoProperty(8), 182 | genreList: getInfoProperties(9), 183 | themeList: getInfoProperties(12), 184 | demographicList: getInfoProperties(11), 185 | studioList: getInfoProperties(13), 186 | batchList, 187 | similarAnimeList, 188 | }; 189 | }, 190 | parseBatchDetails(document, { animeId, animeSlug }) { 191 | const batchTitleEl = document.querySelector(".breadcrumb__links #episodeTitle"); 192 | const downloadQualityElems = document.querySelectorAll("#animeDownloadLink h6"); 193 | const download = { 194 | qualityList: downloadQualityElems.map((downloadQualityEl) => { 195 | const title = Text(downloadQualityEl); 196 | const urlList = []; 197 | let urlEl = downloadQualityEl; 198 | while (urlEl) { 199 | if (urlEl.tagName === "A") { 200 | urlList.push({ 201 | title: Text(urlEl), 202 | url: Attr(urlEl, "href"), 203 | }); 204 | } 205 | else if (urlEl.tagName === "BR") { 206 | break; 207 | } 208 | urlEl = urlEl?.nextElementSibling; 209 | } 210 | return { 211 | title: title.split("—")[0]?.trim() || "", 212 | size: title.split("—")[1]?.trim().replace(/\(|\)/g, "") || "", 213 | urlList, 214 | }; 215 | }), 216 | }; 217 | return { 218 | title: Text(batchTitleEl?.previousElementSibling), 219 | batchTitle: Text(batchTitleEl), 220 | animeId, 221 | animeSlug, 222 | download, 223 | }; 224 | }, 225 | parseEpisodeDetails(document, { animeId, animeSlug }) { 226 | const episodeTitleEl = document.querySelector(".breadcrumb__links #episodeTitle"); 227 | const prevEpisodeEl = document.querySelector(".episode__navigations a:first-child"); 228 | const nextEpisodeEl = document.querySelector(".episode__navigations a:last-child"); 229 | let prevEpisode = null; 230 | let nextEpisode = null; 231 | if (!prevEpisodeEl?.classList.contains("nav__disabled")) { 232 | prevEpisode = { 233 | title: "Prev episode", 234 | episodeId: Id(prevEpisodeEl), 235 | animeId, 236 | animeSlug, 237 | kuramanimeUrl: AnimeSrc(prevEpisodeEl, baseUrl), 238 | }; 239 | } 240 | if (!nextEpisodeEl?.classList.contains("nav__disabled")) { 241 | nextEpisode = { 242 | title: "Next episode", 243 | episodeId: Id(nextEpisodeEl), 244 | animeId, 245 | animeSlug, 246 | kuramanimeUrl: AnimeSrc(nextEpisodeEl, baseUrl), 247 | }; 248 | } 249 | const serverQualityElems = document.querySelectorAll("#player source"); 250 | const server = { 251 | qualityList: serverQualityElems.map((serverQualityEl) => { 252 | const title = Attr(serverQualityEl, "size"); 253 | const url = Attr(serverQualityEl, "src"); 254 | return { 255 | title, 256 | urlList: [ 257 | { 258 | title: "kuramadrive", 259 | url, 260 | }, 261 | ], 262 | }; 263 | }), 264 | }; 265 | const downloadQualityElems = document.querySelectorAll("#animeDownloadLink h6"); 266 | const download = { 267 | qualityList: downloadQualityElems.map((downloadQualityEl) => { 268 | const title = Text(downloadQualityEl); 269 | const urlList = []; 270 | let urlEl = downloadQualityEl; 271 | while (urlEl) { 272 | if (urlEl.tagName === "A") { 273 | urlList.push({ 274 | title: Text(urlEl), 275 | url: Attr(urlEl, "href"), 276 | }); 277 | } 278 | else if (urlEl.tagName === "BR") { 279 | break; 280 | } 281 | urlEl = urlEl?.nextElementSibling; 282 | } 283 | return { 284 | title: title.split("—")[0]?.trim() || "", 285 | size: title.split("—")[1]?.trim().replace(/\(|\)/g, "") || "", 286 | urlList, 287 | }; 288 | }), 289 | }; 290 | return { 291 | title: Text(episodeTitleEl?.previousElementSibling), 292 | episodeTitle: Text(episodeTitleEl), 293 | animeId, 294 | animeSlug, 295 | lastUpdated: Text(document.querySelector(".breadcrumb__links__v2 span:last-child")) 296 | .replace(/\s+/g, " ") 297 | .trim(), 298 | prevEpisode, 299 | hasPrevEpisode: prevEpisode ? true : false, 300 | nextEpisode, 301 | hasNextEpisode: nextEpisode ? true : false, 302 | episode: { 303 | first: 1, 304 | last: 1, 305 | }, 306 | server, 307 | download, 308 | }; 309 | }, 310 | parsePagination(document) { 311 | const paginationEl = document.querySelector(".product__pagination"); 312 | if (paginationEl) { 313 | const pagination = { 314 | currentPage: null, 315 | prevPage: null, 316 | hasPrevPage: false, 317 | nextPage: null, 318 | hasNextPage: false, 319 | totalPages: null, 320 | }; 321 | const currentPageEl = paginationEl.querySelector(".current-page"); 322 | const prevPageEl = paginationEl.querySelector(".page__link:first-child"); 323 | const prevPageVal = prevPageEl?.getAttribute("href")?.match(/(?:\?|&)page=(\d+)/)?.[1]; 324 | const nextPageEl = paginationEl.querySelector(".page__link:last-child"); 325 | const nextPageVal = nextPageEl?.getAttribute("href")?.match(/(?:\?|&)page=(\d+)/)?.[1]; 326 | const totalPagesEl = paginationEl.querySelector("a:last-child")?.previousElementSibling; 327 | pagination.currentPage = Number(Text(currentPageEl)) || null; 328 | pagination.prevPage = 329 | pagination.currentPage && Number(prevPageVal) < pagination.currentPage 330 | ? Number(prevPageVal) || null 331 | : null; 332 | pagination.hasPrevPage = pagination.prevPage ? true : false; 333 | pagination.nextPage = 334 | pagination.currentPage && Number(nextPageVal) > pagination.currentPage 335 | ? Number(nextPageVal) || null 336 | : null; 337 | pagination.hasNextPage = pagination.nextPage ? true : false; 338 | pagination.totalPages = Number(Text(totalPagesEl)) || null; 339 | return pagination; 340 | } 341 | return undefined; 342 | }, 343 | }; 344 | export default kuramanimeParser; 345 | -------------------------------------------------------------------------------- /src/parsers/otakudesu.parser.ts: -------------------------------------------------------------------------------- 1 | import * as T from "@interfaces/otakudesu.interface.js"; 2 | import type { HTMLElement } from "node-html-parser"; 3 | import mainParser from "@parsers/main/main.parser.js"; 4 | import errorinCuy from "@helpers/errorinCuy.js"; 5 | import otakudesuExtraParser from "@parsers/extra/otakudesu.extra.parser.js"; 6 | import otakudesuConfig from "@configs/otakudesu.config.js"; 7 | import generateSrcFromIframeTag from "@helpers/generateSrcFromIframeTag.js"; 8 | import otakudesuScraper from "@scrapers/otakudesu.scraper.js"; 9 | 10 | const { baseUrl } = otakudesuConfig; 11 | const { Text, Attr, Id, Num, Src, AnimeSrc } = mainParser; 12 | 13 | const otakudesuParser = { 14 | parseHome(document: HTMLElement): T.IHome { 15 | const parentElems = document.querySelectorAll(".venz"); 16 | const ongoingAnimeElems = parentElems[0]?.querySelectorAll("ul li"); 17 | const completedAnimeElems = parentElems[1]?.querySelectorAll("ul li"); 18 | 19 | function getSource(index: number) { 20 | return AnimeSrc(parentElems[index]?.previousElementSibling || null); 21 | } 22 | 23 | const ongoingUrl = getSource(0); 24 | const completedUrl = getSource(1); 25 | 26 | const ongoingAnimeList: T.IOngoingAnimeCard[] = 27 | ongoingAnimeElems?.map((animeEl) => { 28 | const animeCard = otakudesuExtraParser.parseOngoingCard(animeEl); 29 | 30 | return animeCard; 31 | }) || []; 32 | 33 | const completedAnimeList: T.ICompletedAnimeCard[] = 34 | completedAnimeElems?.map((animeEl) => { 35 | const animeCard = otakudesuExtraParser.parseCompletedCard(animeEl); 36 | 37 | return animeCard; 38 | }) || []; 39 | 40 | return { 41 | ongoing: { 42 | otakudesuUrl: ongoingUrl, 43 | animeList: ongoingAnimeList, 44 | }, 45 | completed: { 46 | otakudesuUrl: completedUrl, 47 | animeList: completedAnimeList, 48 | }, 49 | }; 50 | }, 51 | 52 | parseSchedules(document: HTMLElement): T.IScheduleCollection[] { 53 | const scheduleElems = document.querySelectorAll(".kglist321"); 54 | const scheduleList: T.IScheduleCollection[] = scheduleElems.map((scheduleEl) => { 55 | const title = Text(scheduleEl.querySelector("h2")); 56 | const animeElems = scheduleEl.querySelectorAll("ul li a"); 57 | const animeList: T.ITextAnimeCard[] = animeElems.map((animeEl) => { 58 | const { id, title, otakudesuUrl } = otakudesuExtraParser.parseTextCard(animeEl); 59 | 60 | return { 61 | title, 62 | animeId: id, 63 | otakudesuUrl, 64 | }; 65 | }); 66 | 67 | return { 68 | title, 69 | animeList, 70 | }; 71 | }); 72 | 73 | if (scheduleList.length === 0) { 74 | throw errorinCuy(404); 75 | } 76 | 77 | return scheduleList; 78 | }, 79 | 80 | parseAllAnimes(document: HTMLElement): T.IAnimeCollection[] { 81 | const parentElems = document.querySelectorAll(".bariskelom"); 82 | const list: T.IAnimeCollection[] = parentElems.map((letterEl) => { 83 | const startWith = Text(letterEl.querySelector(".barispenz")); 84 | const animeElems = letterEl.querySelectorAll(".jdlbar"); 85 | const animeList: T.ITextAnimeCard[] = animeElems.map((animeEl) => { 86 | const { id, title, otakudesuUrl } = otakudesuExtraParser.parseTextCard( 87 | animeEl.querySelector("a")! 88 | ); 89 | 90 | return { 91 | title, 92 | animeId: id, 93 | otakudesuUrl, 94 | }; 95 | }); 96 | 97 | return { 98 | startWith, 99 | animeList, 100 | }; 101 | }); 102 | 103 | if (list.length === 0) { 104 | throw errorinCuy(404); 105 | } 106 | 107 | return list; 108 | }, 109 | 110 | parseAllGenres(document: HTMLElement): T.ITextGenreCard[] { 111 | const genreElems = document.querySelectorAll("ul.genres li a"); 112 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 113 | 114 | if (genreList.length === 0) { 115 | throw errorinCuy(404); 116 | } 117 | 118 | return genreList; 119 | }, 120 | 121 | parseOngoingAnimes(document: HTMLElement): T.IOngoingAnimeCard[] { 122 | const animeElems = document.querySelectorAll(".venz ul li"); 123 | const animeList: T.IOngoingAnimeCard[] = animeElems.map((animeEl) => { 124 | const animeCard = otakudesuExtraParser.parseOngoingCard(animeEl); 125 | 126 | return animeCard; 127 | }); 128 | 129 | if (animeList.length === 0) { 130 | throw errorinCuy(404); 131 | } 132 | 133 | return animeList; 134 | }, 135 | 136 | parseCompletedAnimes(document: HTMLElement): T.ICompletedAnimeCard[] { 137 | const animeElems = document.querySelectorAll(".venz ul li"); 138 | const animeList: T.ICompletedAnimeCard[] = animeElems.map((animeEl) => { 139 | const animeCard = otakudesuExtraParser.parseCompletedCard(animeEl); 140 | 141 | return animeCard; 142 | }); 143 | 144 | if (animeList.length === 0) { 145 | throw errorinCuy(404); 146 | } 147 | 148 | return animeList; 149 | }, 150 | 151 | parseSearchedAnimes(document: HTMLElement): T.ISearchedAnimeCard[] { 152 | const animeElems = document.querySelectorAll("ul.chivsrc li"); 153 | const animeList: T.ISearchedAnimeCard[] = animeElems.map((animeEl) => { 154 | const genreElems = 155 | animeEl.lastElementChild?.previousElementSibling?.previousElementSibling?.querySelectorAll( 156 | "a" 157 | )!; 158 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 159 | 160 | return { 161 | title: Text(animeEl.querySelector("h2")), 162 | animeId: Id(animeEl.querySelector("a")), 163 | poster: Src(animeEl.querySelector("img")), 164 | score: Text(animeEl.lastElementChild!), 165 | status: Text(animeEl.lastElementChild?.previousElementSibling!), 166 | otakudesuUrl: AnimeSrc(animeEl.querySelector("a")), 167 | genreList, 168 | }; 169 | }); 170 | 171 | if (animeList.length === 0) { 172 | throw errorinCuy(404); 173 | } 174 | 175 | return animeList; 176 | }, 177 | 178 | parseAnimesByGenre(document: HTMLElement): T.IGenreFilteredAnimeCard[] { 179 | const animeElems = document.querySelectorAll(".page .col-anime"); 180 | const animeList: T.IGenreFilteredAnimeCard[] = animeElems.map((animeEl) => { 181 | const paragraphElems = animeEl.querySelectorAll(".col-synopsis p"); 182 | const synopsis: ISynopsis = { 183 | paragraphList: paragraphElems 184 | .map((paragraphEl) => { 185 | const paragraph = paragraphEl.text; 186 | 187 | return paragraph; 188 | }) 189 | .filter((paragraph) => { 190 | return paragraph; 191 | }), 192 | }; 193 | 194 | const genreElems = animeEl.querySelectorAll(".col-anime-genre a"); 195 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 196 | 197 | return { 198 | title: Text(animeEl.querySelector(".col-anime-title")), 199 | animeId: Id(animeEl.querySelector(".col-anime-title a")), 200 | poster: Src(animeEl.querySelector(".col-anime-cover img")), 201 | score: Text(animeEl.querySelector(".col-anime-rating")), 202 | episodes: Text(animeEl.querySelector(".col-anime-eps"), /(\S+) Eps/), 203 | season: Text(animeEl.querySelector(".col-anime-date")), 204 | studios: Text(animeEl.querySelector(".col-anime-studio")), 205 | otakudesuUrl: AnimeSrc(animeEl.querySelector(".col-anime-title a")), 206 | synopsis, 207 | genreList, 208 | }; 209 | }); 210 | 211 | if (animeList.length === 0) { 212 | throw errorinCuy(404); 213 | } 214 | 215 | return animeList; 216 | }, 217 | 218 | parseBatchDetails(document: HTMLElement): T.IBatchDetails { 219 | const downloadElems = document.querySelectorAll(".batchlink ul"); 220 | const formatList: IFormat[] = downloadElems.map((downloadEl) => { 221 | const qualityElems = downloadEl.querySelectorAll("li"); 222 | const qualityList: IQuality[] = qualityElems.map((qualityEl) => { 223 | const urlElems = qualityEl.querySelectorAll("a"); 224 | const urlList: IUrl[] = urlElems.map((urlEl) => { 225 | return { 226 | title: Text(urlEl), 227 | url: Attr(urlEl, "href"), 228 | }; 229 | }); 230 | 231 | return { 232 | title: Text(qualityEl.querySelector("strong")), 233 | size: Text(qualityEl.querySelector("i")), 234 | urlList, 235 | }; 236 | }); 237 | 238 | return { 239 | title: Text(downloadEl.previousElementSibling), 240 | qualityList, 241 | }; 242 | }); 243 | 244 | const genreElems = document.querySelectorAll(".infos a"); 245 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 246 | 247 | const getInfo = otakudesuExtraParser.parseInfo(document.querySelectorAll(".infos b")); 248 | 249 | return { 250 | title: getInfo(0), 251 | animeId: Id(document.querySelector(".totalepisode a")), 252 | poster: Src(document.querySelector(".imganime img")), 253 | japanese: getInfo(1), 254 | type: getInfo(2), 255 | episodes: getInfo(3), 256 | score: getInfo(4), 257 | duration: getInfo(6), 258 | studios: getInfo(7), 259 | producers: getInfo(8), 260 | aired: getInfo(9), 261 | credit: getInfo(10), 262 | download: { formatList }, 263 | genreList, 264 | }; 265 | }, 266 | 267 | parseAnimeDetails(document: HTMLElement): T.IAnimeDetails { 268 | const paragraphElems = document.querySelectorAll(".sinopc p"); 269 | const synopsis = otakudesuExtraParser.parseSynopsis(paragraphElems); 270 | const headerTitleElems = document.querySelectorAll(".smokelister"); 271 | 272 | let batch: T.ITextBatchCard | null = null; 273 | let episodeList: T.ITextEpisodeCard[] = []; 274 | 275 | for (let i = 0; i < headerTitleElems.length; i++) { 276 | const headerTitleEl = headerTitleElems[i]; 277 | 278 | if (headerTitleEl?.text.toLowerCase().includes("batch")) { 279 | const batchEl = headerTitleEl.nextElementSibling?.querySelector("a"); 280 | 281 | if (batchEl) { 282 | batch = { 283 | title: Text(batchEl), 284 | batchId: Id(batchEl), 285 | otakudesuUrl: AnimeSrc(batchEl), 286 | }; 287 | 288 | break; 289 | } 290 | } 291 | } 292 | 293 | for (let i = 0; i < headerTitleElems.length; i++) { 294 | const headerTitleEl = headerTitleElems[i]; 295 | 296 | if ( 297 | !headerTitleEl?.text.toLowerCase().includes("batch") && 298 | headerTitleEl?.text.toLowerCase().includes("episode") 299 | ) { 300 | const episodeElems = headerTitleEl.nextElementSibling?.querySelectorAll("li a"); 301 | 302 | if (episodeElems) { 303 | episodeList = otakudesuExtraParser.parseTextEpisodeList(episodeElems); 304 | 305 | break; 306 | } 307 | } 308 | } 309 | 310 | const genreParEl = document.querySelector(".infozingle")?.lastElementChild!; 311 | const genreElems = genreParEl.querySelectorAll("a"); 312 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 313 | 314 | const animeElems = document.querySelectorAll(".isi-recommend-anime-series .isi-konten"); 315 | const recommendedAnimeList: T.IRecommendedAnimeCard[] = animeElems.map((animeEl) => { 316 | return { 317 | title: Text(animeEl.querySelector(".judul-anime")), 318 | animeId: Id(animeEl.querySelector(".isi-anime a")), 319 | poster: Src(animeEl.querySelector("img")), 320 | otakudesuUrl: AnimeSrc(animeEl.querySelector(".isi-anime a")), 321 | }; 322 | }); 323 | 324 | const getInfo = otakudesuExtraParser.parseInfo(document.querySelectorAll(".infozingle b")); 325 | 326 | return { 327 | title: getInfo(0), 328 | japanese: getInfo(1), 329 | score: getInfo(2), 330 | producers: getInfo(3), 331 | type: getInfo(4), 332 | status: getInfo(5), 333 | episodes: getInfo(6), 334 | duration: getInfo(7), 335 | aired: getInfo(8), 336 | studios: getInfo(9), 337 | poster: Src(document.querySelector(".fotoanime img")), 338 | synopsis: synopsis, 339 | batch, 340 | genreList, 341 | episodeList, 342 | recommendedAnimeList, 343 | }; 344 | }, 345 | 346 | async parseEpisodeDetails(document: HTMLElement, url: string): Promise { 347 | const navigationElems = document.querySelectorAll(".flir a"); 348 | let prevEpisode: T.ITextEpisodeCard | null = null; 349 | let nextEpisode: T.ITextEpisodeCard | null = null; 350 | 351 | navigationElems.forEach((navigationEl) => { 352 | const navigationTitle = navigationEl.text; 353 | const navigationObj: T.ITextEpisodeCard = { 354 | title: navigationTitle, 355 | episodeId: Id(navigationEl), 356 | otakudesuUrl: AnimeSrc(navigationEl), 357 | }; 358 | 359 | if (navigationTitle.toLowerCase().includes("prev")) { 360 | prevEpisode = { ...navigationObj, title: "Prev" }; 361 | } else if (navigationTitle.toLowerCase().includes("next")) { 362 | nextEpisode = { ...navigationObj, title: "Next" }; 363 | } 364 | }); 365 | 366 | const downloadElems = document.querySelectorAll(".download ul li"); 367 | const download: IFormat = { 368 | title: "Download", 369 | qualityList: downloadElems.map((downloadEl) => { 370 | const title = Text(downloadEl.querySelector("strong")); 371 | const size = Text(downloadEl.querySelector("i")); 372 | const urlElems = downloadEl.querySelectorAll("a"); 373 | const urlList: IUrl[] = urlElems.map((urlEl) => { 374 | const title = Text(urlEl); 375 | const url = Attr(urlEl, "href"); 376 | 377 | return { title, url }; 378 | }); 379 | 380 | return { 381 | title, 382 | size, 383 | urlList, 384 | }; 385 | }), 386 | }; 387 | 388 | const credentials = [ 389 | ...new Set([...document.innerText.matchAll(/action:"([^"]+)"/g)].map((m) => m[1])), 390 | ]; 391 | 392 | const nonceBody = new URLSearchParams({ 393 | action: credentials[1] || "", 394 | }); 395 | 396 | const nonce = await otakudesuScraper.scrapeNonce(nonceBody.toString(), url); 397 | const serverElems = document.querySelectorAll(".mirrorstream > ul"); 398 | const server: IFormat = { 399 | title: "Server", 400 | qualityList: serverElems.map((serverEl) => { 401 | const title = serverEl.querySelector("li")?.previousSibling?.text || ""; 402 | const serverElems = serverEl.querySelectorAll("li a[data-content]"); 403 | const serverList: IServer[] = serverElems.map((serverEl) => { 404 | const title = Text(serverEl); 405 | const serverId = Attr(serverEl, "data-content"); 406 | const decodedServerId = { 407 | ...JSON.parse(Buffer.from(serverId, "base64").toString()), 408 | nonce: nonce.data || "", 409 | action: credentials[0], 410 | referer: url, 411 | }; 412 | 413 | const encodedServerId = Buffer.from(JSON.stringify(decodedServerId), "utf-8").toString( 414 | "base64url" 415 | ); 416 | 417 | return { 418 | title, 419 | serverId: encodedServerId, 420 | }; 421 | }); 422 | 423 | return { 424 | title, 425 | serverList, 426 | }; 427 | }), 428 | }; 429 | 430 | const title = Text(document.querySelector(".venutama h1.posttl")); 431 | const animeId = Id( 432 | document.querySelector(".alert-info")?.lastElementChild?.querySelector("a")! 433 | ); 434 | const releaseTime = Text(document.querySelector(".kategoz .fa-clock-o")?.nextElementSibling!) 435 | .replace(/Release on /g, "") 436 | .toUpperCase(); 437 | const defaultStreamingUrl = Src(document.querySelector(".player-embed iframe")); 438 | const hasPrevEpisode = prevEpisode ? true : false; 439 | const hasNextEpisode = nextEpisode ? true : false; 440 | 441 | const genreElems = document.querySelectorAll(".infozingle p")[2]?.querySelectorAll("a"); 442 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems || []); 443 | 444 | const episodeElems = document.querySelectorAll(".keyingpost li a"); 445 | const episodeList = otakudesuExtraParser.parseTextEpisodeList(episodeElems); 446 | 447 | const infoElems = document.querySelectorAll(".infozingle b"); 448 | const getInfo = otakudesuExtraParser.parseInfo(infoElems); 449 | 450 | return { 451 | title, 452 | animeId, 453 | releaseTime, 454 | defaultStreamingUrl, 455 | hasPrevEpisode, 456 | prevEpisode, 457 | hasNextEpisode, 458 | nextEpisode, 459 | server, 460 | download, 461 | info: { 462 | credit: getInfo(0), 463 | encoder: getInfo(1), 464 | duration: getInfo(3), 465 | type: getInfo(4), 466 | genreList, 467 | episodeList, 468 | }, 469 | }; 470 | }, 471 | 472 | async parseServerDetails(serverId: string): Promise { 473 | const serverIdObj = JSON.parse(Buffer.from(serverId, "base64").toString()); 474 | const referer = serverIdObj?.referer; 475 | delete serverIdObj["referer"]; 476 | const serverBody = new URLSearchParams(serverIdObj); 477 | const server = await otakudesuScraper.scrapeServer(serverBody.toString(), referer); 478 | const url = generateSrcFromIframeTag(Buffer.from(server.data || "", "base64").toString()); 479 | 480 | return { url }; 481 | }, 482 | 483 | parsePagination(document: HTMLElement): IPagination | undefined { 484 | function generatePage(el: HTMLElement | null): number | null { 485 | const url = el?.getAttribute("href"); 486 | const match = url?.match(/page\/(\d+)\//); 487 | 488 | if (match) { 489 | const page = Number(match[1]) || null; 490 | 491 | return page; 492 | } 493 | 494 | return Number(el?.text) || null; 495 | } 496 | 497 | const paginationEl = document.querySelector(".pagination .pagenavix"); 498 | 499 | if (paginationEl) { 500 | const pagination: IPagination = { 501 | currentPage: null, 502 | prevPage: null, 503 | hasPrevPage: false, 504 | nextPage: null, 505 | hasNextPage: false, 506 | totalPages: null, 507 | }; 508 | 509 | const currentPageEl = paginationEl.querySelector(".page-numbers.current"); 510 | const prevPageEl = paginationEl.querySelector(".page-numbers.prev"); 511 | const nextPageEl = paginationEl.querySelector(".page-numbers.next"); 512 | const lastPageEl = paginationEl.lastElementChild; 513 | 514 | pagination.currentPage = Num(currentPageEl); 515 | pagination.prevPage = pagination.currentPage === 2 ? 1 : generatePage(prevPageEl); 516 | pagination.nextPage = generatePage(nextPageEl); 517 | pagination.hasPrevPage = pagination.prevPage ? true : false; 518 | pagination.hasNextPage = pagination.nextPage ? true : false; 519 | 520 | if (lastPageEl) { 521 | if (lastPageEl === nextPageEl) { 522 | pagination.totalPages = generatePage(lastPageEl.previousElementSibling); 523 | } else { 524 | pagination.totalPages = Num(lastPageEl); 525 | } 526 | } 527 | 528 | return pagination; 529 | } 530 | 531 | return undefined; 532 | }, 533 | }; 534 | 535 | export default otakudesuParser; 536 | -------------------------------------------------------------------------------- /dist/parsers/otakudesu.parser.js: -------------------------------------------------------------------------------- 1 | import * as T from "../interfaces/otakudesu.interface.js"; 2 | import mainParser from "./main/main.parser.js"; 3 | import errorinCuy from "../helpers/errorinCuy.js"; 4 | import otakudesuExtraParser from "./extra/otakudesu.extra.parser.js"; 5 | import otakudesuConfig from "../configs/otakudesu.config.js"; 6 | import generateSrcFromIframeTag from "../helpers/generateSrcFromIframeTag.js"; 7 | import otakudesuScraper from "../scrapers/otakudesu.scraper.js"; 8 | const { baseUrl } = otakudesuConfig; 9 | const { Text, Attr, Id, Num, Src, AnimeSrc } = mainParser; 10 | const otakudesuParser = { 11 | parseHome(document) { 12 | const parentElems = document.querySelectorAll(".venz"); 13 | const ongoingAnimeElems = parentElems[0]?.querySelectorAll("ul li"); 14 | const completedAnimeElems = parentElems[1]?.querySelectorAll("ul li"); 15 | function getSource(index) { 16 | return AnimeSrc(parentElems[index]?.previousElementSibling || null); 17 | } 18 | const ongoingUrl = getSource(0); 19 | const completedUrl = getSource(1); 20 | const ongoingAnimeList = ongoingAnimeElems?.map((animeEl) => { 21 | const animeCard = otakudesuExtraParser.parseOngoingCard(animeEl); 22 | return animeCard; 23 | }) || []; 24 | const completedAnimeList = completedAnimeElems?.map((animeEl) => { 25 | const animeCard = otakudesuExtraParser.parseCompletedCard(animeEl); 26 | return animeCard; 27 | }) || []; 28 | return { 29 | ongoing: { 30 | otakudesuUrl: ongoingUrl, 31 | animeList: ongoingAnimeList, 32 | }, 33 | completed: { 34 | otakudesuUrl: completedUrl, 35 | animeList: completedAnimeList, 36 | }, 37 | }; 38 | }, 39 | parseSchedules(document) { 40 | const scheduleElems = document.querySelectorAll(".kglist321"); 41 | const scheduleList = scheduleElems.map((scheduleEl) => { 42 | const title = Text(scheduleEl.querySelector("h2")); 43 | const animeElems = scheduleEl.querySelectorAll("ul li a"); 44 | const animeList = animeElems.map((animeEl) => { 45 | const { id, title, otakudesuUrl } = otakudesuExtraParser.parseTextCard(animeEl); 46 | return { 47 | title, 48 | animeId: id, 49 | otakudesuUrl, 50 | }; 51 | }); 52 | return { 53 | title, 54 | animeList, 55 | }; 56 | }); 57 | if (scheduleList.length === 0) { 58 | throw errorinCuy(404); 59 | } 60 | return scheduleList; 61 | }, 62 | parseAllAnimes(document) { 63 | const parentElems = document.querySelectorAll(".bariskelom"); 64 | const list = parentElems.map((letterEl) => { 65 | const startWith = Text(letterEl.querySelector(".barispenz")); 66 | const animeElems = letterEl.querySelectorAll(".jdlbar"); 67 | const animeList = animeElems.map((animeEl) => { 68 | const { id, title, otakudesuUrl } = otakudesuExtraParser.parseTextCard(animeEl.querySelector("a")); 69 | return { 70 | title, 71 | animeId: id, 72 | otakudesuUrl, 73 | }; 74 | }); 75 | return { 76 | startWith, 77 | animeList, 78 | }; 79 | }); 80 | if (list.length === 0) { 81 | throw errorinCuy(404); 82 | } 83 | return list; 84 | }, 85 | parseAllGenres(document) { 86 | const genreElems = document.querySelectorAll("ul.genres li a"); 87 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 88 | if (genreList.length === 0) { 89 | throw errorinCuy(404); 90 | } 91 | return genreList; 92 | }, 93 | parseOngoingAnimes(document) { 94 | const animeElems = document.querySelectorAll(".venz ul li"); 95 | const animeList = animeElems.map((animeEl) => { 96 | const animeCard = otakudesuExtraParser.parseOngoingCard(animeEl); 97 | return animeCard; 98 | }); 99 | if (animeList.length === 0) { 100 | throw errorinCuy(404); 101 | } 102 | return animeList; 103 | }, 104 | parseCompletedAnimes(document) { 105 | const animeElems = document.querySelectorAll(".venz ul li"); 106 | const animeList = animeElems.map((animeEl) => { 107 | const animeCard = otakudesuExtraParser.parseCompletedCard(animeEl); 108 | return animeCard; 109 | }); 110 | if (animeList.length === 0) { 111 | throw errorinCuy(404); 112 | } 113 | return animeList; 114 | }, 115 | parseSearchedAnimes(document) { 116 | const animeElems = document.querySelectorAll("ul.chivsrc li"); 117 | const animeList = animeElems.map((animeEl) => { 118 | const genreElems = animeEl.lastElementChild?.previousElementSibling?.previousElementSibling?.querySelectorAll("a"); 119 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 120 | return { 121 | title: Text(animeEl.querySelector("h2")), 122 | animeId: Id(animeEl.querySelector("a")), 123 | poster: Src(animeEl.querySelector("img")), 124 | score: Text(animeEl.lastElementChild), 125 | status: Text(animeEl.lastElementChild?.previousElementSibling), 126 | otakudesuUrl: AnimeSrc(animeEl.querySelector("a")), 127 | genreList, 128 | }; 129 | }); 130 | if (animeList.length === 0) { 131 | throw errorinCuy(404); 132 | } 133 | return animeList; 134 | }, 135 | parseAnimesByGenre(document) { 136 | const animeElems = document.querySelectorAll(".page .col-anime"); 137 | const animeList = animeElems.map((animeEl) => { 138 | const paragraphElems = animeEl.querySelectorAll(".col-synopsis p"); 139 | const synopsis = { 140 | paragraphList: paragraphElems 141 | .map((paragraphEl) => { 142 | const paragraph = paragraphEl.text; 143 | return paragraph; 144 | }) 145 | .filter((paragraph) => { 146 | return paragraph; 147 | }), 148 | }; 149 | const genreElems = animeEl.querySelectorAll(".col-anime-genre a"); 150 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 151 | return { 152 | title: Text(animeEl.querySelector(".col-anime-title")), 153 | animeId: Id(animeEl.querySelector(".col-anime-title a")), 154 | poster: Src(animeEl.querySelector(".col-anime-cover img")), 155 | score: Text(animeEl.querySelector(".col-anime-rating")), 156 | episodes: Text(animeEl.querySelector(".col-anime-eps"), /(\S+) Eps/), 157 | season: Text(animeEl.querySelector(".col-anime-date")), 158 | studios: Text(animeEl.querySelector(".col-anime-studio")), 159 | otakudesuUrl: AnimeSrc(animeEl.querySelector(".col-anime-title a")), 160 | synopsis, 161 | genreList, 162 | }; 163 | }); 164 | if (animeList.length === 0) { 165 | throw errorinCuy(404); 166 | } 167 | return animeList; 168 | }, 169 | parseBatchDetails(document) { 170 | const downloadElems = document.querySelectorAll(".batchlink ul"); 171 | const formatList = downloadElems.map((downloadEl) => { 172 | const qualityElems = downloadEl.querySelectorAll("li"); 173 | const qualityList = qualityElems.map((qualityEl) => { 174 | const urlElems = qualityEl.querySelectorAll("a"); 175 | const urlList = urlElems.map((urlEl) => { 176 | return { 177 | title: Text(urlEl), 178 | url: Attr(urlEl, "href"), 179 | }; 180 | }); 181 | return { 182 | title: Text(qualityEl.querySelector("strong")), 183 | size: Text(qualityEl.querySelector("i")), 184 | urlList, 185 | }; 186 | }); 187 | return { 188 | title: Text(downloadEl.previousElementSibling), 189 | qualityList, 190 | }; 191 | }); 192 | const genreElems = document.querySelectorAll(".infos a"); 193 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 194 | const getInfo = otakudesuExtraParser.parseInfo(document.querySelectorAll(".infos b")); 195 | return { 196 | title: getInfo(0), 197 | animeId: Id(document.querySelector(".totalepisode a")), 198 | poster: Src(document.querySelector(".imganime img")), 199 | japanese: getInfo(1), 200 | type: getInfo(2), 201 | episodes: getInfo(3), 202 | score: getInfo(4), 203 | duration: getInfo(6), 204 | studios: getInfo(7), 205 | producers: getInfo(8), 206 | aired: getInfo(9), 207 | credit: getInfo(10), 208 | download: { formatList }, 209 | genreList, 210 | }; 211 | }, 212 | parseAnimeDetails(document) { 213 | const paragraphElems = document.querySelectorAll(".sinopc p"); 214 | const synopsis = otakudesuExtraParser.parseSynopsis(paragraphElems); 215 | const headerTitleElems = document.querySelectorAll(".smokelister"); 216 | let batch = null; 217 | let episodeList = []; 218 | for (let i = 0; i < headerTitleElems.length; i++) { 219 | const headerTitleEl = headerTitleElems[i]; 220 | if (headerTitleEl?.text.toLowerCase().includes("batch")) { 221 | const batchEl = headerTitleEl.nextElementSibling?.querySelector("a"); 222 | if (batchEl) { 223 | batch = { 224 | title: Text(batchEl), 225 | batchId: Id(batchEl), 226 | otakudesuUrl: AnimeSrc(batchEl), 227 | }; 228 | break; 229 | } 230 | } 231 | } 232 | for (let i = 0; i < headerTitleElems.length; i++) { 233 | const headerTitleEl = headerTitleElems[i]; 234 | if (!headerTitleEl?.text.toLowerCase().includes("batch") && 235 | headerTitleEl?.text.toLowerCase().includes("episode")) { 236 | const episodeElems = headerTitleEl.nextElementSibling?.querySelectorAll("li a"); 237 | if (episodeElems) { 238 | episodeList = otakudesuExtraParser.parseTextEpisodeList(episodeElems); 239 | break; 240 | } 241 | } 242 | } 243 | const genreParEl = document.querySelector(".infozingle")?.lastElementChild; 244 | const genreElems = genreParEl.querySelectorAll("a"); 245 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems); 246 | const animeElems = document.querySelectorAll(".isi-recommend-anime-series .isi-konten"); 247 | const recommendedAnimeList = animeElems.map((animeEl) => { 248 | return { 249 | title: Text(animeEl.querySelector(".judul-anime")), 250 | animeId: Id(animeEl.querySelector(".isi-anime a")), 251 | poster: Src(animeEl.querySelector("img")), 252 | otakudesuUrl: AnimeSrc(animeEl.querySelector(".isi-anime a")), 253 | }; 254 | }); 255 | const getInfo = otakudesuExtraParser.parseInfo(document.querySelectorAll(".infozingle b")); 256 | return { 257 | title: getInfo(0), 258 | japanese: getInfo(1), 259 | score: getInfo(2), 260 | producers: getInfo(3), 261 | type: getInfo(4), 262 | status: getInfo(5), 263 | episodes: getInfo(6), 264 | duration: getInfo(7), 265 | aired: getInfo(8), 266 | studios: getInfo(9), 267 | poster: Src(document.querySelector(".fotoanime img")), 268 | synopsis: synopsis, 269 | batch, 270 | genreList, 271 | episodeList, 272 | recommendedAnimeList, 273 | }; 274 | }, 275 | async parseEpisodeDetails(document, url) { 276 | const navigationElems = document.querySelectorAll(".flir a"); 277 | let prevEpisode = null; 278 | let nextEpisode = null; 279 | navigationElems.forEach((navigationEl) => { 280 | const navigationTitle = navigationEl.text; 281 | const navigationObj = { 282 | title: navigationTitle, 283 | episodeId: Id(navigationEl), 284 | otakudesuUrl: AnimeSrc(navigationEl), 285 | }; 286 | if (navigationTitle.toLowerCase().includes("prev")) { 287 | prevEpisode = { ...navigationObj, title: "Prev" }; 288 | } 289 | else if (navigationTitle.toLowerCase().includes("next")) { 290 | nextEpisode = { ...navigationObj, title: "Next" }; 291 | } 292 | }); 293 | const downloadElems = document.querySelectorAll(".download ul li"); 294 | const download = { 295 | title: "Download", 296 | qualityList: downloadElems.map((downloadEl) => { 297 | const title = Text(downloadEl.querySelector("strong")); 298 | const size = Text(downloadEl.querySelector("i")); 299 | const urlElems = downloadEl.querySelectorAll("a"); 300 | const urlList = urlElems.map((urlEl) => { 301 | const title = Text(urlEl); 302 | const url = Attr(urlEl, "href"); 303 | return { title, url }; 304 | }); 305 | return { 306 | title, 307 | size, 308 | urlList, 309 | }; 310 | }), 311 | }; 312 | const credentials = [ 313 | ...new Set([...document.innerText.matchAll(/action:"([^"]+)"/g)].map((m) => m[1])), 314 | ]; 315 | const nonceBody = new URLSearchParams({ 316 | action: credentials[1] || "", 317 | }); 318 | const nonce = await otakudesuScraper.scrapeNonce(nonceBody.toString(), url); 319 | const serverElems = document.querySelectorAll(".mirrorstream > ul"); 320 | const server = { 321 | title: "Server", 322 | qualityList: serverElems.map((serverEl) => { 323 | const title = serverEl.querySelector("li")?.previousSibling?.text || ""; 324 | const serverElems = serverEl.querySelectorAll("li a[data-content]"); 325 | const serverList = serverElems.map((serverEl) => { 326 | const title = Text(serverEl); 327 | const serverId = Attr(serverEl, "data-content"); 328 | const decodedServerId = { 329 | ...JSON.parse(Buffer.from(serverId, "base64").toString()), 330 | nonce: nonce.data || "", 331 | action: credentials[0], 332 | referer: url, 333 | }; 334 | const encodedServerId = Buffer.from(JSON.stringify(decodedServerId), "utf-8").toString("base64url"); 335 | return { 336 | title, 337 | serverId: encodedServerId, 338 | }; 339 | }); 340 | return { 341 | title, 342 | serverList, 343 | }; 344 | }), 345 | }; 346 | const title = Text(document.querySelector(".venutama h1.posttl")); 347 | const animeId = Id(document.querySelector(".alert-info")?.lastElementChild?.querySelector("a")); 348 | const releaseTime = Text(document.querySelector(".kategoz .fa-clock-o")?.nextElementSibling) 349 | .replace(/Release on /g, "") 350 | .toUpperCase(); 351 | const defaultStreamingUrl = Src(document.querySelector(".player-embed iframe")); 352 | const hasPrevEpisode = prevEpisode ? true : false; 353 | const hasNextEpisode = nextEpisode ? true : false; 354 | const genreElems = document.querySelectorAll(".infozingle p")[2]?.querySelectorAll("a"); 355 | const genreList = otakudesuExtraParser.parseTextGenreList(genreElems || []); 356 | const episodeElems = document.querySelectorAll(".keyingpost li a"); 357 | const episodeList = otakudesuExtraParser.parseTextEpisodeList(episodeElems); 358 | const infoElems = document.querySelectorAll(".infozingle b"); 359 | const getInfo = otakudesuExtraParser.parseInfo(infoElems); 360 | return { 361 | title, 362 | animeId, 363 | releaseTime, 364 | defaultStreamingUrl, 365 | hasPrevEpisode, 366 | prevEpisode, 367 | hasNextEpisode, 368 | nextEpisode, 369 | server, 370 | download, 371 | info: { 372 | credit: getInfo(0), 373 | encoder: getInfo(1), 374 | duration: getInfo(3), 375 | type: getInfo(4), 376 | genreList, 377 | episodeList, 378 | }, 379 | }; 380 | }, 381 | async parseServerDetails(serverId) { 382 | const serverIdObj = JSON.parse(Buffer.from(serverId, "base64").toString()); 383 | const referer = serverIdObj?.referer; 384 | delete serverIdObj["referer"]; 385 | const serverBody = new URLSearchParams(serverIdObj); 386 | const server = await otakudesuScraper.scrapeServer(serverBody.toString(), referer); 387 | const url = generateSrcFromIframeTag(Buffer.from(server.data || "", "base64").toString()); 388 | return { url }; 389 | }, 390 | parsePagination(document) { 391 | function generatePage(el) { 392 | const url = el?.getAttribute("href"); 393 | const match = url?.match(/page\/(\d+)\//); 394 | if (match) { 395 | const page = Number(match[1]) || null; 396 | return page; 397 | } 398 | return Number(el?.text) || null; 399 | } 400 | const paginationEl = document.querySelector(".pagination .pagenavix"); 401 | if (paginationEl) { 402 | const pagination = { 403 | currentPage: null, 404 | prevPage: null, 405 | hasPrevPage: false, 406 | nextPage: null, 407 | hasNextPage: false, 408 | totalPages: null, 409 | }; 410 | const currentPageEl = paginationEl.querySelector(".page-numbers.current"); 411 | const prevPageEl = paginationEl.querySelector(".page-numbers.prev"); 412 | const nextPageEl = paginationEl.querySelector(".page-numbers.next"); 413 | const lastPageEl = paginationEl.lastElementChild; 414 | pagination.currentPage = Num(currentPageEl); 415 | pagination.prevPage = pagination.currentPage === 2 ? 1 : generatePage(prevPageEl); 416 | pagination.nextPage = generatePage(nextPageEl); 417 | pagination.hasPrevPage = pagination.prevPage ? true : false; 418 | pagination.hasNextPage = pagination.nextPage ? true : false; 419 | if (lastPageEl) { 420 | if (lastPageEl === nextPageEl) { 421 | pagination.totalPages = generatePage(lastPageEl.previousElementSibling); 422 | } 423 | else { 424 | pagination.totalPages = Num(lastPageEl); 425 | } 426 | } 427 | return pagination; 428 | } 429 | return undefined; 430 | }, 431 | }; 432 | export default otakudesuParser; 433 | --------------------------------------------------------------------------------