/* * Name: Nanime Scrape * Description: Ngambil Data Lengkap * Credit by zx * Note: Kalau Mau di sher lagi kasih sumber/credit ya 😡😡 * * Sumber: https://whatsapp.com/channel/0029VbDLqe7EquiSF4STU13o * * URL WEBSITE UPLOADER GRATIS: https://zfile.web.id/ * Website Uploader File Gratis yang bisa memberikan url public untuk mendownload File tersebut * */ const axios = require('axios'); const cheerio = require('cheerio'); const { CookieJar } = require('tough-cookie'); const { wrapper } = require('axios-cookiejar-support'); const { inspect } = require('util'); const fs = require('fs'); class ScraperResult { constructor(success, data = null, meta = {}, error = null) { this.success = success; this.data = data; this.meta = meta; this.error = error; this.timestamp = new Date().toISOString(); } [inspect.custom](depth, options) { return JSON.stringify(this.toJSON(), null, 2); } toString() { return JSON.stringify(this.toJSON(), null, 2); } toJSON() { return { success: this.success, error: this.error, meta: this.meta, data: this.data, timestamp: this.timestamp, }; } toJson() { return JSON.stringify(this.toJSON(), null, 2); } save(filename) { fs.writeFileSync(filename, this.toJson(), 'utf-8'); console.log(`[ScraperResult] Saved to ${filename}`); return this; } print() { console.log(this.toJson()); return this; } } class NanimeScraper { constructor(options = {}) { this.baseURL = options.baseURL || 'https://nanimeid.net'; this.timeout = options.timeout || 30000; this.userAgent = options.userAgent || 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Mobile Safari/537.36'; this.inertiaVersion = ''; this.xsrfToken = ''; this.jar = new CookieJar(); this.client = wrapper( axios.create({ baseURL: this.baseURL, timeout: this.timeout, jar: this.jar, withCredentials: true, maxRedirects: 5, headers: { 'User-Agent': this.userAgent }, }) ); } async _getCookiesString() { try { return await this.jar.getCookieString(this.baseURL); } catch { return ''; } } async _extractXsrfToken() { try { const cookieStr = await this._getCookiesString(); const match = cookieStr.match(/XSRF-TOKEN=([^;]+)/); if (match) this.xsrfToken = decodeURIComponent(match[1]); } catch (e) { console.error('[NanimeScraper] XSRF extract error:', e.message); } } _extractInertiaData(html) { const $ = cheerio.load(html); const dataPage = $('#app').attr('data-page'); if (!dataPage) return null; try { const decoded = dataPage .replace(/"/g, '"') .replace(/&/g, '&') .replace(/'/g, "'") .replace(/</g, '<') .replace(/>/g, '>'); return JSON.parse(decoded); } catch (e) { return null; } } async _inertiaHeaders(referer) { const cookies = await this._getCookiesString(); const headers = { Accept: 'text/html, application/xhtml+xml', 'X-Requested-With': 'XMLHttpRequest', 'X-Inertia': 'true', Referer: referer || `${this.baseURL}/`, }; if (this.inertiaVersion) headers['X-Inertia-Version'] = this.inertiaVersion; if (this.xsrfToken) headers['X-XSRF-TOKEN'] = this.xsrfToken; if (cookies) headers['Cookie'] = cookies; return headers; } _parseInertiaResponse(response) { const data = response.data; if (data && typeof data === 'object' && data.props) return data; if (typeof data === 'string') return this._extractInertiaData(data); return null; } _parseGenre(g) { if (!g) return null; return { id: g.id, name: g.name, slug: g.slug, icon: g.icon || null }; } _parseAnime(raw) { if (!raw) return null; return { id: raw.id, anilistId: raw.anilist_id || null, title: raw.title, slug: raw.slug, synopsis: raw.synopsis || '', status: raw.status, poster: raw.poster || '', type: raw.type, episodesCount: raw.episodes_count || 0, episodesCountDisplay: raw.episodes_count_display || raw.episodes_count || 0, trailerUrl: raw.trailer_url || null, isFeatured: !!raw.is_featured, rating: parseFloat(raw.rating) || 0, releaseYear: raw.release_year || null, studio: raw.studio || '', viewsCount: raw.views_count || 0, latestEpisodeAt: raw.latest_episode_at || null, genres: (raw.genres || []).map((g) => this._parseGenre(g)), createdAt: raw.created_at, updatedAt: raw.updated_at, }; } _parseManga(raw) { if (!raw) return null; return { id: raw.id, title: raw.title, slug: raw.slug, synopsis: raw.synopsis || '', status: raw.status, poster: raw.poster || '', type: raw.type || 'Manga', author: raw.author || '-', artist: raw.artist || '-', sourceUrl: raw.source_url || null, isFeatured: !!raw.is_featured, rating: parseFloat(raw.rating) || 0, releaseYear: raw.release_year || null, viewsCount: raw.views_count || 0, genres: (raw.genres || []).map((g) => this._parseGenre(g)), lastChapter: raw.last_chapter || null, createdAt: raw.created_at, updatedAt: raw.updated_at, }; } _parsePagination(props, dataKey) { const paginated = props[dataKey]; if (!paginated) return { items: [], meta: {} }; if (paginated.data) { return { items: paginated.data, meta: { currentPage: paginated.current_page, lastPage: paginated.last_page, perPage: paginated.per_page, total: paginated.total, from: paginated.from, to: paginated.to, }, }; } if (Array.isArray(paginated)) return { items: paginated, meta: { total: paginated.length } }; return { items: [], meta: {} }; } async init() { try { const response = await this.client.get('/', { headers: { Accept: 'text/html, application/xhtml+xml', 'User-Agent': this.userAgent }, }); await this._extractXsrfToken(); const inertiaData = this._extractInertiaData(response.data); if (inertiaData && inertiaData.version) this.inertiaVersion = inertiaData.version; console.log('[NanimeScraper] Initialized successfully'); console.log(`[NanimeScraper] XSRF Token: ${this.xsrfToken ? 'OK' : 'MISSING'}`); console.log(`[NanimeScraper] Inertia Version: ${this.inertiaVersion || 'N/A'}`); return new ScraperResult(true, { inertiaVersion: this.inertiaVersion, xsrfToken: this.xsrfToken }); } catch (error) { console.error('[NanimeScraper] Init failed:', error.message); return new ScraperResult(false, null, {}, error.message); } } async getAnimeList(options = {}) { const { sort = 'latest', letter = '', search = '', page = 1 } = options; const params = new URLSearchParams(); params.set('sort', sort); if (letter) params.set('letter', letter); if (search) params.set('search', search); if (page > 1) params.set('page', page); const url = `/explore?${params.toString()}`; try { const headers = await this._inertiaHeaders(`${this.baseURL}/explore`); const response = await this.client.get(url, { headers }); const inertia = this._parseInertiaResponse(response); if (!inertia || !inertia.props) { return new ScraperResult(false, [], {}, 'No props in response'); } const { items, meta } = this._parsePagination(inertia.props, 'animes'); return new ScraperResult( true, items.map((a) => this._parseAnime(a)), { sort, letter, search, page, ...meta } ); } catch (error) { return new ScraperResult(false, [], {}, error.message); } } async getAnimeDetail(slug) { if (!slug) return new ScraperResult(false, null, {}, 'Slug is required'); const url = `/anime/${slug}`; try { const headers = await this._inertiaHeaders(`${this.baseURL}/library`); const response = await this.client.get(url, { headers }); const inertia = this._parseInertiaResponse(response); if (!inertia || !inertia.props) { return new ScraperResult(false, null, {}, 'Anime not found'); } const animeRaw = inertia.props.anime || inertia.props.data; if (!animeRaw) return new ScraperResult(false, null, {}, 'Anime data missing'); const anime = this._parseAnime(animeRaw); if (animeRaw.episodes && Array.isArray(animeRaw.episodes)) { anime.episodes = animeRaw.episodes.map((ep) => ({ id: ep.id, animeId: ep.anime_id, title: ep.title || '', number: ep.number || ep.episode_number, videoUrl: ep.video_url || null, mirrorStreams: ep.mirror_streams || [], duration: ep.duration || null, releaseDate: ep.release_date || null, createdAt: ep.created_at, updatedAt: ep.updated_at, })); } return new ScraperResult(true, anime, { slug }); } catch (error) { return new ScraperResult(false, null, {}, error.message); } } async getEpisode(animeSlug, episodeNumber) { if (!animeSlug || !episodeNumber) { return new ScraperResult(false, null, {}, 'animeSlug and episodeNumber required'); } const url = `/anime/${animeSlug}/episode/${episodeNumber}`; try { const headers = await this._inertiaHeaders(`${this.baseURL}/anime/${animeSlug}`); const response = await this.client.get(url, { headers }); const inertia = this._parseInertiaResponse(response); if (!inertia || !inertia.props) { return new ScraperResult(false, null, {}, 'Episode not found'); } return new ScraperResult( true, { component: inertia.component, props: inertia.props, animeSlug, episodeNumber, } ); } catch (error) { return new ScraperResult(false, null, {}, error.message); } } async getMangaList(options = {}) { const { page = 1, search = '', type = '', genre = '' } = options; const params = new URLSearchParams(); if (page > 1) params.set('page', page); if (search) params.set('search', search); if (type) params.set('type', type); if (genre) params.set('genre', genre); const url = `/manga${params.toString() ? '?' + params.toString() : ''}`; try { const headers = await this._inertiaHeaders(`${this.baseURL}/manga`); const response = await this.client.get(url, { headers }); const inertia = this._parseInertiaResponse(response); if (!inertia || !inertia.props) { return new ScraperResult(false, [], {}, 'No props in response'); } const { items, meta } = this._parsePagination(inertia.props, 'mangas'); return new ScraperResult( true, items.map((m) => this._parseManga(m)), { page, genres: (inertia.props.genres || []).map((g) => this._parseGenre(g)), ...meta, } ); } catch (error) { return new ScraperResult(false, [], {}, error.message); } } async getMangaDetail(slug) { if (!slug) return new ScraperResult(false, null, {}, 'Slug is required'); const url = `/manga/${slug}`; try { const headers = await this._inertiaHeaders(`${this.baseURL}/manga`); const response = await this.client.get(url, { headers }); const inertia = this._parseInertiaResponse(response); if (!inertia || !inertia.props) { return new ScraperResult(false, null, {}, 'Manga not found'); } const mangaRaw = inertia.props.manga || inertia.props.data; if (!mangaRaw) return new ScraperResult(false, null, {}, 'Manga data missing'); return new ScraperResult(true, this._parseManga(mangaRaw), { slug }); } catch (error) { return new ScraperResult(false, null, {}, error.message); } } async getUserProfile(username) { if (!username) return new ScraperResult(false, null, {}, 'Username is required'); const url = `/u/${username}`; try { const headers = await this._inertiaHeaders(`${this.baseURL}/`); const response = await this.client.get(url, { headers }); const inertia = this._parseInertiaResponse(response); if (!inertia || !inertia.props) { return new ScraperResult(false, null, {}, 'User not found'); } const props = inertia.props; const profileUser = props.profileUser || props.user || null; const series = props.series || []; const stats = props.stats || {}; return new ScraperResult( true, { user: profileUser ? { id: profileUser.id, name: profileUser.name, username: profileUser.username, avatarUrl: profileUser.avatar_url || null, coverUrl: profileUser.cover_url || null, bio: profileUser.bio || null, badge: profileUser.badge || null, profilePublic: profileUser.profile_public, } : null, series: series.map((s) => { if (s.item_type === 'anime' || s.anilist_id !== undefined) { return { itemType: 'anime', ...this._parseAnime(s) }; } return { itemType: 'manga', ...this._parseManga(s) }; }), stats: { watched: stats.watched || 0, bookmarks: stats.bookmarks || 0, episodes: stats.episodes || 0, followers: stats.followers || 0, following: stats.following || 0, comments: stats.comments || 0, }, }, { username } ); } catch (error) { return new ScraperResult(false, null, {}, error.message); } } async search(query) { if (!query) return new ScraperResult(false, [], {}, 'Query is required'); return this.getAnimeList({ search: query }); } async getGenres() { try { const headers = await this._inertiaHeaders(`${this.baseURL}/manga`); const response = await this.client.get('/manga', { headers }); const inertia = this._parseInertiaResponse(response); if (!inertia || !inertia.props) { return new ScraperResult(false, [], {}, 'No data'); } const genres = inertia.props.genres || []; return new ScraperResult( true, genres.map((g) => this._parseGenre(g)) ); } catch (error) { return new ScraperResult(false, [], {}, error.message); } } async healthCheck() { try { const start = Date.now(); const response = await this.client.get('/', { headers: { Accept: 'text/html', 'User-Agent': this.userAgent }, }); const latency = Date.now() - start; return new ScraperResult(true, { status: response.status, latency: `${latency}ms`, xsrfToken: !!this.xsrfToken, inertiaVersion: this.inertiaVersion || 'N/A', }); } catch (error) { return new ScraperResult(false, null, {}, error.message); } } } module.exports = { NanimeScraper }; // contoh penggunaan async function main() { const scraper = new NanimeScraper(); // 1. Init — langsung auto-format di console await scraper.init(); // 2. Health check const health = await scraper.healthCheck(); console.log(health); // 3. Anime list const animeList = await scraper.getAnimeList({ sort: 'latest', letter: 'A' }); console.log(animeList); console.log('Total:', animeList.meta.total); console.log('First title:', animeList.data[0]?.title); // 4. Simpan ke file animeList.save('anime_list.json'); // 5. Detail anime if (animeList.data.length > 0) { const detail = await scraper.getAnimeDetail(animeList.data[0].slug); console.log(detail); } // 6. Manga const manga = await scraper.getMangaList({ page: 1 }); console.log(manga); // 7. User const user = await scraper.getUserProfile('global_scrape'); console.log(user); } main().catch(console.error);