Back to Home
Gistify Snippet
kyiov
3d ago·1 file(s)
Public

Anichin Donghua Scraper & Stream Resolver

Mengimplementasikan web scraper untuk portal streaming Donghua Anichin, dilengkapi throttling cerdas, resolusi IPv4, dan resolver host pemutar video embed multi-server.

#scraper#donghua#anichin#axios#cheerio#nodejs
5
Raw
anichin.jsjavascript
252 lines
1
2/**
3 * @fileoverview Modul Scraper & Ekstraktor Donghua Anichin.
4 *
5 * Mengimplementasikan web scraper untuk portal streaming Donghua Anichin (anichin.moe).
6 * Menggunakan mekanisme throttling / rate limiting cerdas (jeda minimum 1200ms per request)
7 * untuk menjaga stabilitas koneksi, didukung resolver host pemutar video embed multi-server,
8 * serta ekstraksi tautan download berbagai pilihan resolusi video.
9 *
10 * @module scrapers/anichin
11 * @version 1.5.0
12 * @author kyiov
13 * @license MIT
14 */
15
16import axios from 'axios'
17import * as cheerio from 'cheerio'
18import dns from 'node:dns'
19
20// Prioritaskan resolusi IPv4 untuk mencegah latensi rute
21dns.setDefaultResultOrder('ipv4first')
22
23/** @constant {string} Domain basis resmi Anichin */
24const BASE_URL = 'https://anichin.moe'
25
26/** @constant {string} Header User-Agent browser modern */
27const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
28
29let lastReq = 0
30
31async function fetchPage(url) {
32 const now = Date.now()
33 if (now - lastReq < 1200) {
34 await new Promise((r) => setTimeout(r, 1200 - (now - lastReq)))
35 }
36 try {
37 const fullUrl = url.startsWith('http') ? url : BASE_URL + (url.startsWith('/') ? '' : '/') + url
38 const res = await axios.get(fullUrl, {
39 headers: {
40 'User-Agent': UA,
41 'Referer': BASE_URL + '/',
42 'Accept': 'text/html,*/*',
43 },
44 timeout: 30000,
45 })
46 lastReq = Date.now()
47 return cheerio.load(res.data)
48 } catch (e) {
49 console.error('[Anichin Fetch Error]:', url, e.message)
50 return null
51 }
52}
53
54function parseList($) {
55 const hasil = []
56 $('.bs .bsx, .listupd .bsx').each((_, el) => {
57 const $el = $(el)
58 const a = $el.find('a').first()
59 const href = a.attr('href') || ''
60 const img = $el.find('img').first()
61 hasil.push({
62 judul: $el.find('.tt h2, .tt h3, h2, h3').first().text().trim(),
63 url: href.startsWith('http') ? href : BASE_URL + (href.startsWith('/') ? '' : '/') + href,
64 gambar: img.attr('src') || img.attr('data-src') || '',
65 episode: $el.find('.epx, .ep, .newnime').first().text().trim(),
66 tipe: $el.find('.typez, .type').first().text().trim(),
67 })
68 })
69 return hasil
70}
71
72export async function searchDonghua(query, pages = 1) {
73 const semua = []
74 const q = encodeURIComponent(query)
75 for (let i = 1; i <= pages; i++) {
76 const url = i > 1 ? `${BASE_URL}/page/${i}/?s=${q}` : `${BASE_URL}/?s=${q}`
77 const $ = await fetchPage(url)
78 if (!$) break
79 const list = parseList($)
80 if (!list.length) break
81 semua.push(...list)
82 }
83 return semua
84}
85
86export async function getOngoingDonghua(pages = 1) {
87 const semua = []
88 for (let i = 1; i <= pages; i++) {
89 const url = i > 1 ? `${BASE_URL}/ongoing/page/${i}/` : `${BASE_URL}/ongoing/`
90 const $ = await fetchPage(url)
91 if (!$) break
92 const list = parseList($)
93 if (!list.length) break
94 list.forEach((a) => (a.status = 'Ongoing'))
95 semua.push(...list)
96 }
97 return semua
98}
99
100export async function getDetailDonghua(url) {
101 const $ = await fetchPage(url)
102 if (!$) throw new Error('Gagal mengambil detail donghua')
103
104 const info = {}
105 $('.info-content .spe span, .single-info .spe span').each((_, el) => {
106 const teks = $(el).text().trim()
107 if (teks.includes(':')) {
108 const [k, ...v] = teks.split(':')
109 info[k.trim()] = v.join(':').trim()
110 }
111 })
112
113 const genre = []
114 $('.info-content .genxed a, .genre-info a').each((_, el) => {
115 const g = $(el).text().trim()
116 if (g) genre.push(g)
117 })
118 if (genre.length) info.genre = genre
119
120 const episode = []
121 $('.eplister ul li').each((_, el) => {
122 const link = $(el).find('a')
123 const href = link.attr('href') || ''
124 const epNum = $(el).find('.epl-num').text().trim()
125 const epTitle = $(el).find('.epl-title, .epl-sub').text().trim()
126 const epDate = $(el).find('.epl-date, .newnime').text().trim()
127 episode.push({
128 episode: epNum || `Ep ${episode.length + 1}`,
129 judul: epTitle,
130 tanggal: epDate,
131 url: href.startsWith('http') ? href : BASE_URL + (href.startsWith('/') ? '' : '/') + href,
132 })
133 })
134
135 const img = $('.thumb img, .poster img, .ts-post-image').first()
136 return {
137 url,
138 judul: $('h1.entry-title').first().text().trim(),
139 gambar: img.attr('src') || img.attr('data-src') || '',
140 sinopsis: $('.entry-content .spe p, .description .sinopsis, .sinopsis').first().text().trim(),
141 info,
142 episode,
143 totalEpisode: episode.length,
144 }
145}
146
147export async function resolveStreamHost(streamGatewayUrl) {
148 try {
149 const target = streamGatewayUrl.startsWith('http') ? streamGatewayUrl : BASE_URL + (streamGatewayUrl.startsWith('/') ? '' : '/') + streamGatewayUrl
150 const res = await axios.get(target, {
151 headers: {
152 'User-Agent': UA,
153 'Referer': BASE_URL + '/',
154 },
155 timeout: 10000,
156 })
157 const $ = cheerio.load(res.data)
158
159 let finalUrl = $('iframe').attr('src') || ''
160 let serverType = 'Player'
161
162 if (finalUrl.includes('rumble.com')) serverType = 'Rumble'
163 else if (finalUrl.includes('ok.ru')) serverType = 'OK.ru'
164 else if (finalUrl.includes('dailymotion')) serverType = 'Dailymotion'
165 else if (finalUrl.includes('odysee')) serverType = 'Odysee'
166 else if (finalUrl.includes('streamtape')) serverType = 'Streamtape'
167
168 return {
169 server: serverType,
170 streamUrl: finalUrl || target,
171 rawGateway: target
172 }
173 } catch (e) {
174 return {
175 server: 'Stream Gateway',
176 streamUrl: streamGatewayUrl,
177 error: e.message
178 }
179 }
180}
181
182export async function getEpisodeStreams(url) {
183 const $ = await fetchPage(url)
184 if (!$) throw new Error('Gagal mengambil data episode donghua')
185
186 const servers = []
187
188 $('.player-embed iframe, .player iframe').each((_, el) => {
189 const src = $(el).attr('src') || ''
190 if (src) servers.push(src)
191 })
192
193 $('select.mirror option').each((_, el) => {
194 let val = $(el).attr('value') || ''
195 if (val) {
196 if (/^[A-Za-z0-9+/=]{30,}$/.test(val)) {
197 try {
198 const decoded = Buffer.from(val, 'base64').toString('utf8')
199 const match = decoded.match(/src=["']([^"']+)["']/)
200 if (match) val = match[1]
201 } catch {}
202 }
203 if (val && !servers.includes(val)) servers.push(val)
204 }
205 })
206
207 const resolvedStreams = []
208 for (const s of servers.slice(0, 6)) {
209 const res = await resolveStreamHost(s)
210 if (res.streamUrl) resolvedStreams.push(res)
211 }
212
213 const downloads = []
214 $('.dlbox ul li, .download ul li, .mctnx a').each((_, el) => {
215 const a = $(el).is('a') ? $(el) : $(el).find('a')
216 const href = a.attr('href') || ''
217 const text = $(el).text().trim().replace(/\s+/g, ' ')
218 if (href && !downloads.some(d => d.url === href)) {
219 downloads.push({
220 quality: text || 'Download Link',
221 url: href
222 })
223 }
224 })
225
226 return {
227 url,
228 judul: $('h1.entry-title').first().text().trim(),
229 streams: resolvedStreams,
230 downloads
231 }
232}
233
234export default {
235 searchDonghua,
236 getOngoingDonghua,
237 getDetailDonghua,
238 getEpisodeStreams
239}
240
241import { pathToFileURL } from 'node:url'
242
243// Contoh pemanggilan langsung jika file dieksekusi secara mandiri
244if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
245 try {
246 const res = await searchDonghua('renegade')
247 console.log(JSON.stringify(res, null, 2))
248 } catch (err) {
249 console.error('Galat Anichin:', err.message || err)
250 }
251}
252
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.