Back to Home
Gistify Snippet
Public
Donghub Donghua Anime Stream & Search Scraper
Scraper for Donghub.vip to search Chinese anime (Donghua), extract stream video sources, m3u8 playlist URLs, and episode lists.
#scraper#donghua#anime#streaming#javascript
0
Rawdonghub.jsjavascript
164 lines| 1 | // donghub.js — bundle standalone. npm deps: axios, cheerio, crypto, node:url |
| 2 | |
| 3 | /** |
| 4 | * Scraper: donghub.js |
| 5 | * Source : kyioapi/donghub.js |
| 6 | * |
| 7 | * Input : Function parameters / CLI args (e.g. query: string, url: string, options: object) |
| 8 | * Output : Promise<Object | Array | Buffer> (JSON formatted scraping response / stream URL / media metadata) |
| 9 | * |
| 10 | * CLI Run: |
| 11 | * node kyioapi/donghub.js [functionName] [args...] |
| 12 | * node cli.js kyioapi/donghub.js [functionName] [args...] |
| 13 | */ |
| 14 | |
| 15 | import * as __ns_86 from 'axios' |
| 16 | const axios = __ns_86.default |
| 17 | import * as __ns_87 from 'cheerio' |
| 18 | const cheerio = __ns_87 |
| 19 | import * as __ns_88 from 'crypto' |
| 20 | const crypto = __ns_88.default |
| 21 | import * as __ns_89 from 'node:url' |
| 22 | const pathToFileURL = __ns_89.pathToFileURL |
| 23 | |
| 24 | |
| 25 | |
| 26 | |
| 27 | |
| 28 | /** |
| 29 | * Donghub Scraper |
| 30 | * @baseurl https://donghub.vip |
| 31 | */ |
| 32 | class Donghub { |
| 33 | constructor() { |
| 34 | this.baseUrl = 'https://donghub.vip'; |
| 35 | this.headers = { |
| 36 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', |
| 37 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', |
| 38 | 'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7', |
| 39 | 'Sec-Ch-Ua-Platform': '"Linux"', |
| 40 | 'Upgrade-Insecure-Requests': '1' |
| 41 | }; |
| 42 | this.cookies = this.generateCookies(); |
| 43 | } |
| 44 | |
| 45 | generateCookies() { |
| 46 | const ga1 = crypto.randomBytes(4).toString('hex') + '.' + crypto.randomBytes(5).toString('hex'); |
| 47 | const ts = Date.now(); |
| 48 | return `_ga=GA1.1.${ga1}; HstCfa5009307=${ts}; HstCla5009307=${ts}; HstCmu5009307=${ts}; HstPn5009307=1; _ga_BC9Q6DVLH9=GS2.1.s${ts}$o1$g1$t${ts + 1000}$j35$l0$h0`; |
| 49 | } |
| 50 | |
| 51 | async getEpisodeVideoUrl(episodeUrl) { |
| 52 | const response = await axios.get(episodeUrl, { headers: { ...this.headers, 'Cookie': this.cookies } }); |
| 53 | const $ = cheerio.load(response.data); |
| 54 | const videoSources = []; |
| 55 | |
| 56 | $('iframe').each((i, el) => { |
| 57 | const src = $(el).attr('src'); |
| 58 | if (src) videoSources.push({ type: 'iframe', url: src, provider: src.includes('youtube') ? 'youtube' : src.includes('drive') ? 'gdrive' : 'embed' }); |
| 59 | }); |
| 60 | |
| 61 | $('video source').each((i, el) => { |
| 62 | if ($(el).attr('src')) videoSources.push({ type: 'video', url: $(el).attr('src'), format: $(el).attr('type') || 'video/mp4' }); |
| 63 | }); |
| 64 | |
| 65 | const playerScript = $('script:contains("player.src")').text(); |
| 66 | if (playerScript) { |
| 67 | const matches = playerScript.match(/https?:\/\/[^\s"']+\.(?:mp4|mkv|m3u8)[^\s"']*/g); |
| 68 | if (matches) matches.forEach(url => videoSources.push({ type: 'script', url: url, quality: 'unknown' })); |
| 69 | } |
| 70 | return videoSources; |
| 71 | } |
| 72 | |
| 73 | async search(query) { |
| 74 | const response = await axios.get(`${this.baseUrl}/?s=${encodeURIComponent(query)}`, { headers: { ...this.headers, 'Cookie': this.cookies, 'Referer': this.baseUrl + '/' } }); |
| 75 | const $ = cheerio.load(response.data); |
| 76 | const results = []; |
| 77 | $('.listupd article.bs').each((i, el) => { |
| 78 | const article = $(el); |
| 79 | results.push({ |
| 80 | title: article.find('a').attr('title') || article.find('h2').text(), |
| 81 | url: article.find('a').attr('href'), |
| 82 | image: article.find('img').attr('src'), |
| 83 | type: article.find('.typez').text(), |
| 84 | status: article.find('.epx').text(), |
| 85 | subtitle: article.find('.sb').text(), |
| 86 | hot: article.find('.hotbadge').length > 0 |
| 87 | }); |
| 88 | }); |
| 89 | return { query, results }; |
| 90 | } |
| 91 | |
| 92 | async getDetail(url) { |
| 93 | const response = await axios.get(url, { headers: { ...this.headers, 'Cookie': this.cookies, 'Referer': this.baseUrl + '/' } }); |
| 94 | const $ = cheerio.load(response.data); |
| 95 | const detail = { |
| 96 | title: $('.entry-title').first().text(), |
| 97 | url: url, |
| 98 | image: $('.thumb img').attr('src') || $('.bigcover img').attr('src'), |
| 99 | alternative: $('.alter').text(), |
| 100 | status: $('.spe span:contains("Status:")').text().replace('Status:', '').trim(), |
| 101 | network: $('.spe a[href*="network"]').text(), |
| 102 | released: $('.spe span:contains("Released:")').text().replace('Released:', '').trim(), |
| 103 | duration: $('.spe span:contains("Duration:")').text().replace('Duration:', '').trim(), |
| 104 | type: $('.spe span:contains("Type:")').text().replace('Type:', '').trim(), |
| 105 | episodes: $('.spe span:contains("Episodes:")').text().replace('Episodes:', '').trim(), |
| 106 | genres: $('.genxed a').map((i, el) => $(el).text()).get(), |
| 107 | synopsis: $('.entry-content p').first().text().replace(/\n/g, ' ').trim(), |
| 108 | episodeList: [] |
| 109 | }; |
| 110 | |
| 111 | const epItems = $('.eplister ul li').map((i, el) => ({ |
| 112 | episode: $(el).find('.epl-num').text(), |
| 113 | title: $(el).find('.epl-title').text(), |
| 114 | url: $(el).find('a').attr('href'), |
| 115 | releaseDate: $(el).find('.epl-date').text(), |
| 116 | subtitle: $(el).find('.epl-sub .status').text() |
| 117 | })).get(); |
| 118 | |
| 119 | detail.episodeList = await Promise.all(epItems.map(async (ep) => ({ |
| 120 | ...ep, |
| 121 | videoSources: await this.getEpisodeVideoUrl(ep.url) |
| 122 | }))); |
| 123 | |
| 124 | return detail; |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | const donghub = new Donghub(); |
| 129 | export { Donghub, donghub }; |
| 130 | export default Donghub; |
| 131 | |
| 132 | // ───────────────────────────────────────────────────────────────────── |
| 133 | // Self-test: jalankan langsung dengan `node donghub.js [args...]` |
| 134 | // ───────────────────────────────────────────────────────────────────── |
| 135 | if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { |
| 136 | const __exports = { Donghub: Donghub, donghub: donghub } |
| 137 | const __fns = Object.fromEntries(Object.entries(__exports).filter(([, v]) => typeof v === 'function')) |
| 138 | const __names = Object.keys(__fns) |
| 139 | if (__names.length === 0) { |
| 140 | console.log('Tidak ada fungsi yang bisa di-demo. Import modul ini dari kode lain.') |
| 141 | } else { |
| 142 | const __search = __names.find((n) => /search|cari/i.test(n)) |
| 143 | const __name = __search || __names[0] |
| 144 | const __fn = __fns[__name] |
| 145 | const __isClass = /^class\s/.test(Function.prototype.toString.call(__fn)) |
| 146 | let __args = process.argv.slice(2).map((a) => { try { return JSON.parse(a) } catch { return a } }) |
| 147 | if (__args.length === 0 && __search) __args.push('naruto') |
| 148 | if (__args.length === 0 && !__search && !__isClass && __fn.length > 0) { |
| 149 | console.log('Fungsi: ' + __names.join(', ')) |
| 150 | console.log('Contoh: node ' + "donghub.js" + ' ' + __name + ' <arg>') |
| 151 | } else { |
| 152 | ;(async () => { |
| 153 | try { |
| 154 | const r = __isClass ? new __fn(...__args) : await __fn(...__args) |
| 155 | console.log(JSON.stringify(r, null, 2)) |
| 156 | } catch (e) { |
| 157 | console.error('Error: ' + (e && e.message ? e.message : e)) |
| 158 | process.exitCode = 1 |
| 159 | } |
| 160 | })() |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | |
Discussion & Comments0
No comments yet — be the first to leave feedback or start a code discussion.
Sign in with your GitHub account to write comments and join the discussion.