Nanime Scrape
JavaScriptPublicby zx-apiSep 5, 2026, 10:31 AMExpires: Never60 views
1
2/*
3 * Name: Nanime Scrape
4 * Description: Ngambil Data Lengkap
5 * Credit by zx
6 * Note: Kalau Mau di sher lagi kasih sumber/credit ya ๐ก๐ก
7 *
8 * Sumber: https://whatsapp.com/channel/0029VbDLqe7EquiSF4STU13o
9 *
10 * URL WEBSITE UPLOADER GRATIS: https://zfile.web.id/
11 * Website Uploader File Gratis yang bisa memberikan url public untuk mendownload File tersebut
12 *
13*/
14
15const axios = require('axios');
16const cheerio = require('cheerio');
17const { CookieJar } = require('tough-cookie');
18const { wrapper } = require('axios-cookiejar-support');
19const { inspect } = require('util');
20const fs = require('fs');
21
22class ScraperResult {
23 constructor(success, data = null, meta = {}, error = null) {
24 this.success = success;
25 this.data = data;
26 this.meta = meta;
27 this.error = error;
28 this.timestamp = new Date().toISOString();
29 }
30
31 [inspect.custom](depth, options) {
32 return JSON.stringify(this.toJSON(), null, 2);
33 }
34
35 toString() {
36 return JSON.stringify(this.toJSON(), null, 2);
37 }
38
39 toJSON() {
40 return {
41 success: this.success,
42 error: this.error,
43 meta: this.meta,
44 data: this.data,
45 timestamp: this.timestamp,
46 };
47 }
48
49 toJson() {
50 return JSON.stringify(this.toJSON(), null, 2);
51 }
52
53 save(filename) {
54 fs.writeFileSync(filename, this.toJson(), 'utf-8');
55 console.log(`[ScraperResult] Saved to ${filename}`);
56 return this;
57 }
58
59 print() {
60 console.log(this.toJson());
61 return this;
62 }
63}
64
65class NanimeScraper {
66 constructor(options = {}) {
67 this.baseURL = options.baseURL || 'https://nanimeid.net';
68 this.timeout = options.timeout || 30000;
69 this.userAgent =
70 options.userAgent ||
71 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Mobile Safari/537.36';
72
73 this.inertiaVersion = '';
74 this.xsrfToken = '';
75
76 this.jar = new CookieJar();
77 this.client = wrapper(
78 axios.create({
79 baseURL: this.baseURL,
80 timeout: this.timeout,
81 jar: this.jar,
82 withCredentials: true,
83 maxRedirects: 5,
84 headers: { 'User-Agent': this.userAgent },
85 })
86 );
87 }
88
89 async _getCookiesString() {
90 try {
91 return await this.jar.getCookieString(this.baseURL);
92 } catch {
93 return '';
94 }
95 }
96
97 async _extractXsrfToken() {
98 try {
99 const cookieStr = await this._getCookiesString();
100 const match = cookieStr.match(/XSRF-TOKEN=([^;]+)/);
101 if (match) this.xsrfToken = decodeURIComponent(match[1]);
102 } catch (e) {
103 console.error('[NanimeScraper] XSRF extract error:', e.message);
104 }
105 }
106
107 _extractInertiaData(html) {
108 const $ = cheerio.load(html);
109 const dataPage = $('#app').attr('data-page');
110 if (!dataPage) return null;
111 try {
112 const decoded = dataPage
113 .replace(/"/g, '"')
114 .replace(/&/g, '&')
115 .replace(/'/g, "'")
116 .replace(/</g, '<')
117 .replace(/>/g, '>');
118 return JSON.parse(decoded);
119 } catch (e) {
120 return null;
121 }
122 }
123
124 async _inertiaHeaders(referer) {
125 const cookies = await this._getCookiesString();
126 const headers = {
127 Accept: 'text/html, application/xhtml+xml',
128 'X-Requested-With': 'XMLHttpRequest',
129 'X-Inertia': 'true',
130 Referer: referer || `${this.baseURL}/`,
131 };
132 if (this.inertiaVersion) headers['X-Inertia-Version'] = this.inertiaVersion;
133 if (this.xsrfToken) headers['X-XSRF-TOKEN'] = this.xsrfToken;
134 if (cookies) headers['Cookie'] = cookies;
135 return headers;
136 }
137
138 _parseInertiaResponse(response) {
139 const data = response.data;
140 if (data && typeof data === 'object' && data.props) return data;
141 if (typeof data === 'string') return this._extractInertiaData(data);
142 return null;
143 }
144
145 _parseGenre(g) {
146 if (!g) return null;
147 return { id: g.id, name: g.name, slug: g.slug, icon: g.icon || null };
148 }
149
150 _parseAnime(raw) {
151 if (!raw) return null;
152 return {
153 id: raw.id,
154 anilistId: raw.anilist_id || null,
155 title: raw.title,
156 slug: raw.slug,
157 synopsis: raw.synopsis || '',
158 status: raw.status,
159 poster: raw.poster || '',
160 type: raw.type,
161 episodesCount: raw.episodes_count || 0,
162 episodesCountDisplay: raw.episodes_count_display || raw.episodes_count || 0,
163 trailerUrl: raw.trailer_url || null,
164 isFeatured: !!raw.is_featured,
165 rating: parseFloat(raw.rating) || 0,
166 releaseYear: raw.release_year || null,
167 studio: raw.studio || '',
168 viewsCount: raw.views_count || 0,
169 latestEpisodeAt: raw.latest_episode_at || null,
170 genres: (raw.genres || []).map((g) => this._parseGenre(g)),
171 createdAt: raw.created_at,
172 updatedAt: raw.updated_at,
173 };
174 }
175
176 _parseManga(raw) {
177 if (!raw) return null;
178 return {
179 id: raw.id,
180 title: raw.title,
181 slug: raw.slug,
182 synopsis: raw.synopsis || '',
183 status: raw.status,
184 poster: raw.poster || '',
185 type: raw.type || 'Manga',
186 author: raw.author || '-',
187 artist: raw.artist || '-',
188 sourceUrl: raw.source_url || null,
189 isFeatured: !!raw.is_featured,
190 rating: parseFloat(raw.rating) || 0,
191 releaseYear: raw.release_year || null,
192 viewsCount: raw.views_count || 0,
193 genres: (raw.genres || []).map((g) => this._parseGenre(g)),
194 lastChapter: raw.last_chapter || null,
195 createdAt: raw.created_at,
196 updatedAt: raw.updated_at,
197 };
198 }
199
200 _parsePagination(props, dataKey) {
201 const paginated = props[dataKey];
202 if (!paginated) return { items: [], meta: {} };
203 if (paginated.data) {
204 return {
205 items: paginated.data,
206 meta: {
207 currentPage: paginated.current_page,
208 lastPage: paginated.last_page,
209 perPage: paginated.per_page,
210 total: paginated.total,
211 from: paginated.from,
212 to: paginated.to,
213 },
214 };
215 }
216 if (Array.isArray(paginated)) return { items: paginated, meta: { total: paginated.length } };
217 return { items: [], meta: {} };
218 }
219
220 async init() {
221 try {
222 const response = await this.client.get('/', {
223 headers: { Accept: 'text/html, application/xhtml+xml', 'User-Agent': this.userAgent },
224 });
225 await this._extractXsrfToken();
226 const inertiaData = this._extractInertiaData(response.data);
227 if (inertiaData && inertiaData.version) this.inertiaVersion = inertiaData.version;
228
229 console.log('[NanimeScraper] Initialized successfully');
230 console.log(`[NanimeScraper] XSRF Token: ${this.xsrfToken ? 'OK' : 'MISSING'}`);
231 console.log(`[NanimeScraper] Inertia Version: ${this.inertiaVersion || 'N/A'}`);
232 return new ScraperResult(true, { inertiaVersion: this.inertiaVersion, xsrfToken: this.xsrfToken });
233 } catch (error) {
234 console.error('[NanimeScraper] Init failed:', error.message);
235 return new ScraperResult(false, null, {}, error.message);
236 }
237 }
238
239 async getAnimeList(options = {}) {
240 const { sort = 'latest', letter = '', search = '', page = 1 } = options;
241 const params = new URLSearchParams();
242 params.set('sort', sort);
243 if (letter) params.set('letter', letter);
244 if (search) params.set('search', search);
245 if (page > 1) params.set('page', page);
246
247 const url = `/explore?${params.toString()}`;
248 try {
249 const headers = await this._inertiaHeaders(`${this.baseURL}/explore`);
250 const response = await this.client.get(url, { headers });
251 const inertia = this._parseInertiaResponse(response);
252 if (!inertia || !inertia.props) {
253 return new ScraperResult(false, [], {}, 'No props in response');
254 }
255 const { items, meta } = this._parsePagination(inertia.props, 'animes');
256 return new ScraperResult(
257 true,
258 items.map((a) => this._parseAnime(a)),
259 { sort, letter, search, page, ...meta }
260 );
261 } catch (error) {
262 return new ScraperResult(false, [], {}, error.message);
263 }
264 }
265
266 async getAnimeDetail(slug) {
267 if (!slug) return new ScraperResult(false, null, {}, 'Slug is required');
268 const url = `/anime/${slug}`;
269 try {
270 const headers = await this._inertiaHeaders(`${this.baseURL}/library`);
271 const response = await this.client.get(url, { headers });
272 const inertia = this._parseInertiaResponse(response);
273 if (!inertia || !inertia.props) {
274 return new ScraperResult(false, null, {}, 'Anime not found');
275 }
276 const animeRaw = inertia.props.anime || inertia.props.data;
277 if (!animeRaw) return new ScraperResult(false, null, {}, 'Anime data missing');
278 const anime = this._parseAnime(animeRaw);
279 if (animeRaw.episodes && Array.isArray(animeRaw.episodes)) {
280 anime.episodes = animeRaw.episodes.map((ep) => ({
281 id: ep.id,
282 animeId: ep.anime_id,
283 title: ep.title || '',
284 number: ep.number || ep.episode_number,
285 videoUrl: ep.video_url || null,
286 mirrorStreams: ep.mirror_streams || [],
287 duration: ep.duration || null,
288 releaseDate: ep.release_date || null,
289 createdAt: ep.created_at,
290 updatedAt: ep.updated_at,
291 }));
292 }
293 return new ScraperResult(true, anime, { slug });
294 } catch (error) {
295 return new ScraperResult(false, null, {}, error.message);
296 }
297 }
298
299 async getEpisode(animeSlug, episodeNumber) {
300 if (!animeSlug || !episodeNumber) {
301 return new ScraperResult(false, null, {}, 'animeSlug and episodeNumber required');
302 }
303 const url = `/anime/${animeSlug}/episode/${episodeNumber}`;
304 try {
305 const headers = await this._inertiaHeaders(`${this.baseURL}/anime/${animeSlug}`);
306 const response = await this.client.get(url, { headers });
307 const inertia = this._parseInertiaResponse(response);
308 if (!inertia || !inertia.props) {
309 return new ScraperResult(false, null, {}, 'Episode not found');
310 }
311 return new ScraperResult(
312 true,
313 {
314 component: inertia.component,
315 props: inertia.props,
316 animeSlug,
317 episodeNumber,
318 }
319 );
320 } catch (error) {
321 return new ScraperResult(false, null, {}, error.message);
322 }
323 }
324
325 async getMangaList(options = {}) {
326 const { page = 1, search = '', type = '', genre = '' } = options;
327 const params = new URLSearchParams();
328 if (page > 1) params.set('page', page);
329 if (search) params.set('search', search);
330 if (type) params.set('type', type);
331 if (genre) params.set('genre', genre);
332 const url = `/manga${params.toString() ? '?' + params.toString() : ''}`;
333 try {
334 const headers = await this._inertiaHeaders(`${this.baseURL}/manga`);
335 const response = await this.client.get(url, { headers });
336 const inertia = this._parseInertiaResponse(response);
337 if (!inertia || !inertia.props) {
338 return new ScraperResult(false, [], {}, 'No props in response');
339 }
340 const { items, meta } = this._parsePagination(inertia.props, 'mangas');
341 return new ScraperResult(
342 true,
343 items.map((m) => this._parseManga(m)),
344 {
345 page,
346 genres: (inertia.props.genres || []).map((g) => this._parseGenre(g)),
347 ...meta,
348 }
349 );
350 } catch (error) {
351 return new ScraperResult(false, [], {}, error.message);
352 }
353 }
354
355 async getMangaDetail(slug) {
356 if (!slug) return new ScraperResult(false, null, {}, 'Slug is required');
357 const url = `/manga/${slug}`;
358 try {
359 const headers = await this._inertiaHeaders(`${this.baseURL}/manga`);
360 const response = await this.client.get(url, { headers });
361 const inertia = this._parseInertiaResponse(response);
362 if (!inertia || !inertia.props) {
363 return new ScraperResult(false, null, {}, 'Manga not found');
364 }
365 const mangaRaw = inertia.props.manga || inertia.props.data;
366 if (!mangaRaw) return new ScraperResult(false, null, {}, 'Manga data missing');
367 return new ScraperResult(true, this._parseManga(mangaRaw), { slug });
368 } catch (error) {
369 return new ScraperResult(false, null, {}, error.message);
370 }
371 }
372
373 async getUserProfile(username) {
374 if (!username) return new ScraperResult(false, null, {}, 'Username is required');
375 const url = `/u/${username}`;
376 try {
377 const headers = await this._inertiaHeaders(`${this.baseURL}/`);
378 const response = await this.client.get(url, { headers });
379 const inertia = this._parseInertiaResponse(response);
380 if (!inertia || !inertia.props) {
381 return new ScraperResult(false, null, {}, 'User not found');
382 }
383 const props = inertia.props;
384 const profileUser = props.profileUser || props.user || null;
385 const series = props.series || [];
386 const stats = props.stats || {};
387 return new ScraperResult(
388 true,
389 {
390 user: profileUser
391 ? {
392 id: profileUser.id,
393 name: profileUser.name,
394 username: profileUser.username,
395 avatarUrl: profileUser.avatar_url || null,
396 coverUrl: profileUser.cover_url || null,
397 bio: profileUser.bio || null,
398 badge: profileUser.badge || null,
399 profilePublic: profileUser.profile_public,
400 }
401 : null,
402 series: series.map((s) => {
403 if (s.item_type === 'anime' || s.anilist_id !== undefined) {
404 return { itemType: 'anime', ...this._parseAnime(s) };
405 }
406 return { itemType: 'manga', ...this._parseManga(s) };
407 }),
408 stats: {
409 watched: stats.watched || 0,
410 bookmarks: stats.bookmarks || 0,
411 episodes: stats.episodes || 0,
412 followers: stats.followers || 0,
413 following: stats.following || 0,
414 comments: stats.comments || 0,
415 },
416 },
417 { username }
418 );
419 } catch (error) {
420 return new ScraperResult(false, null, {}, error.message);
421 }
422 }
423
424 async search(query) {
425 if (!query) return new ScraperResult(false, [], {}, 'Query is required');
426 return this.getAnimeList({ search: query });
427 }
428
429 async getGenres() {
430 try {
431 const headers = await this._inertiaHeaders(`${this.baseURL}/manga`);
432 const response = await this.client.get('/manga', { headers });
433 const inertia = this._parseInertiaResponse(response);
434 if (!inertia || !inertia.props) {
435 return new ScraperResult(false, [], {}, 'No data');
436 }
437 const genres = inertia.props.genres || [];
438 return new ScraperResult(
439 true,
440 genres.map((g) => this._parseGenre(g))
441 );
442 } catch (error) {
443 return new ScraperResult(false, [], {}, error.message);
444 }
445 }
446
447 async healthCheck() {
448 try {
449 const start = Date.now();
450 const response = await this.client.get('/', {
451 headers: { Accept: 'text/html', 'User-Agent': this.userAgent },
452 });
453 const latency = Date.now() - start;
454 return new ScraperResult(true, {
455 status: response.status,
456 latency: `${latency}ms`,
457 xsrfToken: !!this.xsrfToken,
458 inertiaVersion: this.inertiaVersion || 'N/A',
459 });
460 } catch (error) {
461 return new ScraperResult(false, null, {}, error.message);
462 }
463 }
464}
465
466module.exports = { NanimeScraper };
467
468
469// contoh penggunaan
470async function main() {
471 const scraper = new NanimeScraper();
472
473 // 1. Init โ langsung auto-format di console
474 await scraper.init();
475
476 // 2. Health check
477 const health = await scraper.healthCheck();
478 console.log(health);
479
480 // 3. Anime list
481 const animeList = await scraper.getAnimeList({ sort: 'latest', letter: 'A' });
482 console.log(animeList);
483 console.log('Total:', animeList.meta.total);
484 console.log('First title:', animeList.data[0]?.title);
485
486 // 4. Simpan ke file
487 animeList.save('anime_list.json');
488
489 // 5. Detail anime
490 if (animeList.data.length > 0) {
491 const detail = await scraper.getAnimeDetail(animeList.data[0].slug);
492 console.log(detail);
493 }
494
495 // 6. Manga
496 const manga = await scraper.getMangaList({ page: 1 });
497 console.log(manga);
498
499 // 7. User
500 const user = await scraper.getUserProfile('global_scrape');
501 console.log(user);
502}
503
504main().catch(console.error);504 linesยท16,286 charsยท15.9 KB
WwrapยทFfullscreen