├── .gitignore ├── src ├── utils │ ├── client.ts │ ├── constant.ts │ ├── decrypt.ts │ └── methods.ts ├── server.ts ├── extractors │ └── megacloud.ts └── index.d.ts ├── vercel.json ├── tsconfig.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/utils/client.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { REQ_HEADERS } from "./constant"; 3 | 4 | export const client = axios.create({ 5 | timeout: 20000, 6 | headers: { 7 | "Accept-Encoding": "gzip", 8 | ...REQ_HEADERS 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "/src/server.ts", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "/src/server.ts" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "commonjs", 5 | "rootDir": "src", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "outDir": "./dist", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | }, 14 | "lib": ["esnext"], 15 | "include": ["src"] 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hianime-mapper", 3 | "description": "API that maps anilist Id with hianime & provides video URL", 4 | "main": "dist/server.js", 5 | "scripts": { 6 | "dev": "tsx watch src/server.ts", 7 | "start": "tsx src/server.ts", 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "@hono/node-server": "^1.13.3", 12 | "axios": "^1.12.0", 13 | "cheerio": "^1.0.0", 14 | "hono": "^4.6.8", 15 | "string-similarity-js": "^2.1.4" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^22.8.4", 19 | "tsx": "^4.19.2", 20 | "typescript": "^5.6.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server"; 2 | import { Hono } from "hono"; 3 | import { HTTPException } from "hono/http-exception"; 4 | import { fetchAnilistInfo, getServers, getSources } from "./utils/methods"; 5 | 6 | const app = new Hono(); 7 | 8 | app.get("/", async (c) => { 9 | return c.json({ 10 | about: 11 | "This API maps anilist anime to https://hianime.to and also returns the M3U8 links !", 12 | status: 200, 13 | routes: [ 14 | "/anime/info/:anilistId", 15 | "/anime/servers/:episodeId", 16 | "/anime/sources?serverId={server_id}&episodeId={episode_id}", 17 | ], 18 | }); 19 | }); 20 | 21 | app.get("/anime/info/:id", async (c) => { 22 | const id = c.req.param("id"); 23 | const data = await fetchAnilistInfo(Number(id)); 24 | if (!data) { 25 | throw new HTTPException(500, { message: "Internal server issue !" }); 26 | } 27 | return c.json({ data }); 28 | }); 29 | 30 | app.get("/anime/servers/:id", async (c) => { 31 | const id = c.req.param("id"); 32 | const data = await getServers(id); 33 | if (!data) { 34 | throw new HTTPException(500, { message: "Internal server issue !" }); 35 | } 36 | return c.json({ data }); 37 | }); 38 | 39 | app.get("/anime/sources", async (c) => { 40 | const { serverId, episodeId } = c.req.query(); 41 | if (!serverId || !episodeId) { 42 | throw new HTTPException(400, { 43 | message: "Provide server Id & episode Id !", 44 | }); 45 | } 46 | const data = await getSources(serverId, episodeId); 47 | if (!data) { 48 | throw new HTTPException(500, { message: "Internal server issue !" }); 49 | } 50 | return c.json({ data }); 51 | }); 52 | 53 | serve({ 54 | port: Number(process.env.PORT) || 5000, 55 | fetch: app.fetch, 56 | }); 57 | -------------------------------------------------------------------------------- /src/extractors/megacloud.ts: -------------------------------------------------------------------------------- 1 | import { client } from "../utils/client"; 2 | import { decrypt, extractVariables, getSecret } from "../utils/decrypt"; 3 | 4 | export class Megacloud { 5 | private readonly megacloud = { 6 | script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=", 7 | host: "https://megacloud.tv", 8 | }; 9 | 10 | private videoUrl: string; 11 | constructor(videoUrl: string) { 12 | this.videoUrl = videoUrl; 13 | } 14 | 15 | /** 16 | * scrapeMegaCloud 17 | */ 18 | public async scrapeMegaCloud() { 19 | const res = await client( 20 | `${this.megacloud.host}/embed-2/ajax/e-1/getSources?id=${ 21 | this.videoUrl.split("/").pop()?.split("?")[0] 22 | }`, 23 | { 24 | headers: { 25 | "X-Requested-With": "XMLHttpRequest", 26 | referer: `${this.megacloud.host}/embed-2/e-1/${this.videoUrl 27 | .split("/") 28 | .pop()}&autoPlay=1&oa=0&asi=1`, 29 | }, 30 | } 31 | ); 32 | 33 | let sourceData: Sourcedata = { 34 | intro: res.data.intro, 35 | outro: res.data.outro, 36 | sources: [], 37 | tracks: res.data.tracks, 38 | server: res.data.server, 39 | }; 40 | 41 | const scriptText = await client( 42 | this.megacloud.script.concat(String(Date.now())) 43 | ); 44 | 45 | if (!scriptText) 46 | throw new Error("Unable to fetch script text to get vars !"); 47 | 48 | const vars = extractVariables(scriptText.data, "MEGACLOUD"); 49 | const { secret, encryptedSource } = getSecret(res.data.sources, vars); 50 | const value = decrypt(encryptedSource, secret); 51 | const files: Array<{ file: string; type: string }> = JSON.parse(value); 52 | files.map((s) => { 53 | sourceData.sources.push({ 54 | url: s.file, 55 | type: s.type, 56 | isM3U8: s.file.includes(".m3u8"), 57 | }); 58 | }); 59 | return sourceData; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const USER_AGENT = 2 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"; 3 | 4 | export const REQ_HEADERS = { 5 | Accept: 6 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", 7 | "Accept-Language": "en-US,en;q=0.9", 8 | "User-Agent": USER_AGENT, 9 | }; 10 | 11 | export const ANILIST_BASEURL = "https://graphql.anilist.co"; 12 | 13 | export const HIANIME_BASEURL = "https://hianime.to"; 14 | 15 | 16 | export const ANIME_QUERY = `query ($id: Int) { 17 | Media(id: $id, type: ANIME) { 18 | id 19 | idMal 20 | title { 21 | romaji 22 | english 23 | native 24 | userPreferred 25 | } 26 | coverImage { 27 | extraLarge 28 | large 29 | medium 30 | color 31 | } 32 | format 33 | description 34 | genres 35 | season 36 | episodes 37 | nextAiringEpisode { 38 | id 39 | timeUntilAiring 40 | airingAt 41 | episode 42 | } 43 | status 44 | duration 45 | seasonYear 46 | bannerImage 47 | favourites 48 | popularity 49 | averageScore 50 | trailer { 51 | id 52 | site 53 | thumbnail 54 | } 55 | startDate { 56 | year 57 | month 58 | day 59 | } 60 | countryOfOrigin 61 | recommendations(sort: RATING_DESC) { 62 | edges { 63 | node { 64 | mediaRecommendation { 65 | title { 66 | romaji 67 | english 68 | native 69 | userPreferred 70 | } 71 | format 72 | coverImage { 73 | extraLarge 74 | large 75 | medium 76 | color 77 | } 78 | } 79 | } 80 | } 81 | } 82 | relations { 83 | edges { 84 | id 85 | node { 86 | title { 87 | romaji 88 | english 89 | native 90 | userPreferred 91 | } 92 | coverImage { 93 | extraLarge 94 | large 95 | medium 96 | color 97 | } 98 | } 99 | } 100 | } 101 | characters(sort: FAVOURITES_DESC) { 102 | edges { 103 | role 104 | node { 105 | name { 106 | first 107 | middle 108 | last 109 | full 110 | native 111 | userPreferred 112 | } 113 | image { 114 | large 115 | medium 116 | } 117 | } 118 | voiceActors(sort: FAVOURITES_DESC) { 119 | name { 120 | first 121 | middle 122 | last 123 | full 124 | native 125 | userPreferred 126 | } 127 | image { 128 | large 129 | medium 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | ` 137 | -------------------------------------------------------------------------------- /src/utils/decrypt.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | // extract variables from .js file 4 | export const extractVariables = (text: string, sourceName: string) => { 5 | let allvars; 6 | if (sourceName !== "MEGACLOUD") { 7 | allvars = 8 | text 9 | .match( 10 | /const (?:\w{1,2}=(?:'.{0,50}?'|\w{1,2}\(.{0,20}?\)).{0,20}?,){7}.+?;/gm 11 | ) 12 | ?.at(-1) ?? ""; 13 | } else { 14 | allvars = 15 | text.match(/\w{1,2}=new URLSearchParams.+?;(?=function)/gm)?.at(1) ?? ""; 16 | } 17 | const vars = allvars 18 | .slice(0, -1) 19 | .split("=") 20 | .slice(1) 21 | .map((pair) => Number(pair.split(",").at(0))) 22 | .filter((num) => num === 0 || num); 23 | 24 | return vars; 25 | }; 26 | 27 | // get secret key from extracted variables 28 | export const getSecret = (encryptedString: string, values: number[]) => { 29 | let secret = "", 30 | encryptedSource = encryptedString, 31 | totalInc = 0; 32 | 33 | for (let i = 0; i < values[0]!; i++) { 34 | let start, inc; 35 | switch (i) { 36 | case 0: 37 | (start = values[2]), (inc = values[1]); 38 | break; 39 | case 1: 40 | (start = values[4]), (inc = values[3]); 41 | break; 42 | case 2: 43 | (start = values[6]), (inc = values[5]); 44 | break; 45 | case 3: 46 | (start = values[8]), (inc = values[7]); 47 | break; 48 | case 4: 49 | (start = values[10]), (inc = values[9]); 50 | break; 51 | case 5: 52 | (start = values[12]), (inc = values[11]); 53 | break; 54 | case 6: 55 | (start = values[14]), (inc = values[13]); 56 | break; 57 | case 7: 58 | (start = values[16]), (inc = values[15]); 59 | break; 60 | case 8: 61 | (start = values[18]), (inc = values[17]); 62 | } 63 | const from = start! + totalInc, 64 | to = from + inc!; 65 | (secret += encryptedString.slice(from, to)), 66 | (encryptedSource = encryptedSource.replace( 67 | encryptedString.substring(from, to), 68 | "" 69 | )), 70 | (totalInc += inc!); 71 | } 72 | 73 | return { secret, encryptedSource }; 74 | }; 75 | 76 | // decrypt the encrypted string using secret 77 | export const decrypt = ( 78 | encrypted: string, 79 | keyOrSecret: string, 80 | maybe_iv?: string 81 | ) => { 82 | let key; 83 | let iv; 84 | let contents; 85 | if (maybe_iv) { 86 | key = keyOrSecret; 87 | iv = maybe_iv; 88 | contents = encrypted; 89 | } else { 90 | const cypher = Buffer.from(encrypted, "base64"); 91 | const salt = cypher.subarray(8, 16); 92 | const password = Buffer.concat([Buffer.from(keyOrSecret, "binary"), salt]); 93 | const md5Hashes = []; 94 | let digest = password; 95 | for (let i = 0; i < 3; i++) { 96 | md5Hashes[i] = crypto.createHash("md5").update(digest).digest(); 97 | digest = Buffer.concat([md5Hashes[i], password]); 98 | } 99 | key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); 100 | iv = md5Hashes[2]; 101 | contents = cypher.subarray(16); 102 | } 103 | 104 | const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); 105 | const decrypted = 106 | decipher.update( 107 | contents as any, 108 | typeof contents === "string" ? "base64" : undefined, 109 | "utf8" 110 | ) + decipher.final(); 111 | 112 | return decrypted; 113 | }; 114 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Title { 2 | english: string; 3 | romaji: string; 4 | native: string; 5 | userPreferred: string; 6 | }; 7 | 8 | interface AnilistAnime { 9 | Media: { 10 | id: number; 11 | idMal: number; 12 | title: { 13 | romaji: string; 14 | english: string; 15 | native: string; 16 | userPreferred: string; 17 | }; 18 | coverImage: { 19 | extraLarge: string; 20 | large: string; 21 | medium: string; 22 | color: string; 23 | }; 24 | format: string; 25 | description: string; 26 | genres: string[]; 27 | season: string; 28 | episodes: number; 29 | nextAiringEpisode: { 30 | id: number; 31 | timeUntilAiring: number; 32 | airingAt: number; 33 | episode: number; 34 | }; 35 | status: string; 36 | duration: number; 37 | seasonYear: number; 38 | bannerImage: string; 39 | favourites: number; 40 | popularity: number; 41 | averageScore: number; 42 | trailer: { 43 | id: number; 44 | site: string; 45 | thumbnail: string; 46 | }; 47 | startDate: { 48 | year: number; 49 | month: number; 50 | day: number; 51 | }; 52 | countryOfOrigin: string; 53 | recommendations: { 54 | edges: { 55 | node: { 56 | mediaRecommendation: { 57 | title: { 58 | romaji: string; 59 | english: string; 60 | native: string; 61 | userPreferred: string; 62 | }; 63 | format: string; 64 | coverImage: { 65 | extraLarge: string; 66 | large: string; 67 | medium: string; 68 | color: string; 69 | }; 70 | }; 71 | }; 72 | }[]; 73 | }; 74 | relations: { 75 | edges: { 76 | id: number; 77 | node: { 78 | title: { 79 | romaji: string; 80 | english: string; 81 | native: string; 82 | userPreferred: string; 83 | }; 84 | coverImage: { 85 | extraLarge: string; 86 | large: string; 87 | medium: string; 88 | color: string; 89 | }; 90 | }; 91 | }[]; 92 | }; 93 | characters: { 94 | edges: { 95 | role: string; 96 | node: { 97 | name: { 98 | first: string; 99 | middle: string; 100 | last: string; 101 | full: string; 102 | native: string; 103 | userPreferred: string; 104 | }; 105 | image: { 106 | large: string; 107 | medium: string; 108 | }; 109 | }; 110 | voiceActors: { 111 | name: { 112 | first: string; 113 | middle: string; 114 | last: string; 115 | full: string; 116 | native: string; 117 | userPreferred: string; 118 | }; 119 | image: { 120 | large: string; 121 | medium: string; 122 | }; 123 | }[]; 124 | }[]; 125 | }; 126 | }; 127 | }; 128 | 129 | interface Sourcedata { 130 | intro: { 131 | start: number; 132 | end: number; 133 | }; 134 | outro: { 135 | start: number; 136 | end: number; 137 | }; 138 | sources: { 139 | url: string; 140 | type: string; 141 | isM3U8: boolean; 142 | }[]; 143 | tracks: { 144 | file: string; 145 | kind: string; 146 | label?: string; 147 | default?: boolean; 148 | }[]; 149 | server: number; 150 | }; 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hianime-mapper 2 | 3 | This API is used to fetch episodes and streaming url from https://hianime.to by using anilist Ids. It is built on [Node.js](https://nodejs.org) using the web framework [Hono.js](https://hono.dev) to serve the API. 4 | 5 | ## Run Locally 6 | 7 | Clone the project 8 | 9 | ```bash 10 | git clone https://github.com/IrfanKhan66/hianime-mapper.git 11 | ``` 12 | 13 | Go to the project directory 14 | 15 | ```bash 16 | cd hianime-mapper 17 | ``` 18 | 19 | Install dependencies 20 | 21 | ```bash 22 | npm i 23 | ``` 24 | 25 | Start the server 26 | 27 | ```bash 28 | npm run dev 29 | ``` 30 | 31 | ## Documentation 32 | 33 | **Get routes info & status of API** 34 | 35 | _request url_ 36 | 37 | ```url 38 | https://hianime-mapper.vercel.app/ 39 | 40 | ``` 41 | 42 | _response_ 43 | 44 | ```json 45 | { 46 | "about": "This API maps anilist anime to https://hianime.to and also returns the M3U8 links !", 47 | "status": 200, 48 | "routes": [ 49 | "/anime/info/:anilistId", 50 | "/anime/servers/:episodeId", 51 | "/anime/sources?serverId={server_id}&episodeId={episode_id}" 52 | ] 53 | } 54 | ``` 55 | 56 | **Get info of anime from anilist with hianime episode mappings** 57 | 58 | _request url_ 59 | 60 | ```url 61 | https://hianime-mapper.vercel.app/anime/info/:anilistId 62 | 63 | example : https://hianime-mapper.vercel.app/anime/info/20 64 | ``` 65 | 66 | _response_ 67 | 68 | ```javascript 69 | 70 | { 71 | data: { 72 | id: number; 73 | idMal: number; 74 | title: { 75 | romaji: string; 76 | english: string; 77 | native: string; 78 | userPreferred: string; 79 | }; 80 | coverImage: { 81 | extraLarge: string; 82 | large: string; 83 | medium: string; 84 | color: string; 85 | }; 86 | format: string; 87 | description: string; 88 | genres: string[]; 89 | season: string; 90 | episodes: number; 91 | nextAiringEpisode: { 92 | id: number; 93 | timeUntilAiring: number; 94 | airingAt: number; 95 | episode: number; 96 | }; 97 | status: string; 98 | duration: number; 99 | seasonYear: number; 100 | bannerImage: string; 101 | favourites: number; 102 | popularity: number; 103 | averageScore: number; 104 | trailer: { 105 | id: number; 106 | site: string; 107 | thumbnail: string; 108 | }; 109 | startDate: { 110 | year: number; 111 | month: number; 112 | day: number; 113 | }; 114 | countryOfOrigin: string; 115 | recommendations: { 116 | title: { 117 | romaji: string; 118 | english: string; 119 | native: string; 120 | userPreferred: string; 121 | }; 122 | format: string; 123 | coverImage: { 124 | extraLarge: string; 125 | large: string; 126 | medium: string; 127 | color: string; 128 | }; 129 | }[]; 130 | relations: { 131 | id: number; 132 | title: { 133 | romaji: string; 134 | english: string; 135 | native: string; 136 | userPreferred: string; 137 | }; 138 | coverImage: { 139 | extraLarge: string; 140 | large: string; 141 | medium: string; 142 | color: string; 143 | }; 144 | }[]; 145 | characters: { 146 | role: string; 147 | name: { 148 | first: string; 149 | middle: string; 150 | last: string; 151 | full: string; 152 | native: string; 153 | userPreferred: string; 154 | }; 155 | image: { 156 | large: string; 157 | medium: string; 158 | }; 159 | voiceActors: { 160 | name: { 161 | first: string; 162 | middle: string; 163 | last: string; 164 | full: string; 165 | native: string; 166 | userPreferred: string; 167 | }; 168 | image: { 169 | large: string; 170 | medium: string; 171 | }; 172 | }[]; 173 | }[]; 174 | episodesList: { 175 | id: string; 176 | episodeId: number; 177 | title: string; 178 | number: number; 179 | }[]; 180 | }; 181 | } 182 | 183 | 184 | ``` 185 | 186 | **Get servers** 187 | 188 | _request url_ 189 | 190 | ```url 191 | https://hianime-mapper.vercel.app/anime/servers/:episodeId 192 | 193 | example : https://hianime-mapper.vercel.app/anime/servers/12352 194 | ``` 195 | 196 | _response_ 197 | 198 | ```javascript 199 | { 200 | data: { 201 | sub: { 202 | serverId: string; 203 | serverName: string; 204 | }[], 205 | dub: { 206 | serverId: string; 207 | serverName: string; 208 | }[] 209 | } 210 | } 211 | ``` 212 | 213 | **Get sources** 214 | 215 | _request url_ 216 | 217 | ```url 218 | https://hianime-mapper.vercel.app/anime/sources?serverId={server_id}&episodeId={episode_id} 219 | 220 | example : https://hianime-mapper.vercel.app/anime/sources?serverId=662001&episodeId=12352 221 | 222 | ``` 223 | 224 | _response_ 225 | 226 | ```javascript 227 | { 228 | data:{ 229 | intro: { 230 | start: number; 231 | end: number; 232 | }; 233 | outro: { 234 | start: number; 235 | end: number; 236 | }; 237 | sources: { 238 | url: string; 239 | type: string; 240 | isM3U8: boolean; 241 | }[]; 242 | tracks: { 243 | file: string; 244 | kind: string; 245 | label?: string; 246 | default?: boolean; 247 | }[]; 248 | server: number; 249 | } 250 | } 251 | ``` 252 | 253 | ## Acknowledgements 254 | 255 | - [Consumet](https://github.com/consumet/consumet.ts) 256 | -------------------------------------------------------------------------------- /src/utils/methods.ts: -------------------------------------------------------------------------------- 1 | import { client } from "./client"; 2 | import { ANILIST_BASEURL, ANIME_QUERY, HIANIME_BASEURL } from "./constant"; 3 | import { load } from "cheerio"; 4 | import match from "string-similarity-js"; 5 | import { Megacloud } from "../extractors/megacloud"; 6 | 7 | // fetchAnilistInfo and call hianmie endpoints and return info with eps from hianime 8 | export const fetchAnilistInfo = async (id: number) => { 9 | try { 10 | let infoWithEp; 11 | 12 | const resp = await client.post( 13 | ANILIST_BASEURL, 14 | { 15 | query: ANIME_QUERY, 16 | variables: { 17 | id, 18 | }, 19 | } 20 | ); 21 | const data = resp.data.data.Media; 22 | 23 | const eps = await searchNScrapeEPs(data.title); 24 | infoWithEp = { 25 | ...data, 26 | recommendations: data.recommendations.edges.map( 27 | (el) => el.node.mediaRecommendation 28 | ), 29 | relations: data.relations.edges.map((el) => ({ id: el.id, ...el.node })), 30 | characters: data.characters.edges.map((el) => ({ 31 | role: el.role, 32 | ...el.node, 33 | voiceActors: el.voiceActors, 34 | })), 35 | episodesList: eps, 36 | }; 37 | 38 | return infoWithEp; 39 | } catch (err: any) { 40 | console.error(err); 41 | return null; 42 | } 43 | }; 44 | 45 | // search with title in hianime and call ep scraping func 46 | export const searchNScrapeEPs = async (searchTitle: Title) => { 47 | try { 48 | const resp = await client.get( 49 | `${HIANIME_BASEURL}/search?keyword=${searchTitle.english}` 50 | ); 51 | if (!resp) return console.log("No response from hianime !"); 52 | const $ = load(resp.data); 53 | let similarTitles: { id: string; title: string; similarity: number }[] = []; 54 | $(".film_list-wrap > .flw-item .film-detail .film-name a") 55 | .map((i, el) => { 56 | const title = $(el).text(); 57 | const id = $(el).attr("href")!.split("/").pop()?.split("?")[0] ?? ""; 58 | const similarity = Number( 59 | ( 60 | match( 61 | title.replace(/[\,\:]/g, ""), 62 | searchTitle.english || searchTitle.native 63 | ) * 10 64 | ).toFixed(2) 65 | ); 66 | similarTitles.push({ id, title, similarity }); 67 | }) 68 | .get(); 69 | 70 | similarTitles.sort((a, b) => b.similarity - a.similarity); 71 | 72 | if ( 73 | (searchTitle.english.match(/\Season(.+?)\d/) && 74 | similarTitles[0].title.match(/\Season(.+?)\d/)) || (!searchTitle.english.match(/\Season(.+?)\d/) && !similarTitles[0].title.match(/\Season(.+?)\d/)) 75 | ) 76 | return getEpisodes(similarTitles[0].id); 77 | else return getEpisodes(similarTitles[1].id); 78 | } catch (err) { 79 | console.error(err); 80 | return null; 81 | } 82 | }; 83 | 84 | // calls ep watch endpoint in hianmie and scrapes all eps and returns them in arr 85 | export const getEpisodes = async (animeId: string) => { 86 | try { 87 | const resp = await client.get( 88 | `${HIANIME_BASEURL}/ajax/v2/episode/list/${animeId.split("-").pop()}`, 89 | { 90 | headers: { 91 | referer: `${HIANIME_BASEURL}/watch/${animeId}`, 92 | "X-Requested-With": "XMLHttpRequest", 93 | }, 94 | } 95 | ); 96 | const $ = load(resp.data.html); 97 | let episodesList: { 98 | id: string; 99 | episodeId: number; 100 | title: string; 101 | number: number; 102 | }[] = []; 103 | $("#detail-ss-list div.ss-list a").each((i, el) => { 104 | episodesList.push({ 105 | id: $(el).attr("href")?.split("/").pop() ?? "", 106 | episodeId: Number($(el).attr("href")?.split("?ep=").pop()), 107 | title: $(el).attr("title") ?? "", 108 | number: i + 1, 109 | }); 110 | }); 111 | 112 | return episodesList; 113 | } catch (err) { 114 | console.error(err); 115 | return { episodesList: null }; 116 | } 117 | }; 118 | 119 | // call server to get ep servers 120 | export const getServers = async (epId: string) => { 121 | try { 122 | const resp = await client( 123 | `${HIANIME_BASEURL}/ajax/v2/episode/servers?episodeId=${epId}`, 124 | { 125 | headers: { 126 | "X-Requested-With": "XMLHttpRequest", 127 | referer: `${HIANIME_BASEURL}/watch/${epId}`, 128 | }, 129 | } 130 | ); 131 | 132 | const $ = load(resp.data.html); 133 | 134 | let servers: { 135 | sub: { serverId: string | null; serverName: string }[]; 136 | dub: { serverId: string | null; serverName: string }[]; 137 | } = { 138 | sub: [], 139 | dub: [], 140 | }; 141 | 142 | $(".ps_-block.ps_-block-sub .ps__-list .server-item").each((i, el) => { 143 | const $parent = $(el).closest(".servers-sub, .servers-dub"); 144 | const serverType = $parent.hasClass("servers-sub") ? "sub" : "dub"; 145 | servers[serverType].push({ 146 | serverId: $(el).attr("data-id") ?? null, 147 | serverName: $(el).text().replaceAll("\n", "").trim(), 148 | }); 149 | }); 150 | 151 | return servers; 152 | } catch (err) { 153 | console.error(err); 154 | return { servers: null }; 155 | } 156 | }; 157 | 158 | // get sources of ep 159 | export const getSources = async (serverId: string, epId: string) => { 160 | try { 161 | const res = await client( 162 | `${HIANIME_BASEURL}/ajax/v2/episode/sources?id=${serverId}`, 163 | { 164 | headers: { 165 | "X-Requested-With": "XMLHttpRequest", 166 | referer: `${HIANIME_BASEURL}/watch/${epId}`, 167 | }, 168 | } 169 | ); 170 | 171 | const link = res.data.link; 172 | if (!link) return { sources: null }; 173 | 174 | let sources!: Sourcedata | { sources: null }; 175 | if (String(link).includes("megacloud")) 176 | sources = await new Megacloud(res.data.link).scrapeMegaCloud(); 177 | else if (String(link).includes("watchsb")) sources = { sources: null }; 178 | else if (String(link).includes("streamtape")) sources = { sources: null }; 179 | else { 180 | sources = { sources: null }; 181 | console.log("Unknown link !"); 182 | } 183 | return sources; 184 | } catch (err) { 185 | console.error(err); 186 | return { sources: null }; 187 | } 188 | }; 189 | --------------------------------------------------------------------------------