Nanime Scrape

JavaScriptPublic
by zx-apiSep 5, 2026, 10:31 AMExpires: Never60 views
Nanime Scrape
Raw
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(/&lt;/g, '<')
117        .replace(/&gt;/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