// myanimelist.js — bundle standalone. npm deps: cheerio, node:url /** * Scraper: myanimelist.js * Source : kyioapi/myanimelist.js * * Input : Function parameters / CLI args (e.g. query: string, url: string, options: object) * Output : Promise (JSON formatted scraping response / stream URL / media metadata) * * CLI Run: * node kyioapi/myanimelist.js [functionName] [args...] * node cli.js kyioapi/myanimelist.js [functionName] [args...] */ import * as __ns_241 from 'cheerio' const cheerio = __ns_241 import * as __ns_242 from 'node:url' const pathToFileURL = __ns_242.pathToFileURL /** * Base Scraper Class * Centralizes request logic, headers, and common scraping utilities. * Refactored to use native fetch for better performance in Node.js 18+. */ class BaseScraper { constructor(baseURL, options = {}) { this.baseURL = baseURL; this.options = { timeout: options.timeout || 30000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9,id;q=0.8', ...options.headers }, withCookies: options.withCookies || false }; this.cookies = new Map(); } /** * Internal fetch wrapper with timeout and cookie handling */ async _request(url, config = {}) { const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`; const method = config.method || 'GET'; const headers = { ...this.options.headers, ...config.headers }; if (this.options.withCookies && this.cookies.size > 0) { headers['cookie'] = Array.from(this.cookies.entries()) .map(([k, v]) => `${k}=${v}`) .join('; '); } const controller = new AbortController(); const id = setTimeout(() => controller.abort(), config.timeout || this.options.timeout); try { const response = await fetch(fullUrl, { method, headers, body: config.data ? (typeof config.data === 'object' ? JSON.stringify(config.data) : config.data) : undefined, signal: controller.signal, ...config.fetchOptions }); clearTimeout(id); if (this.options.withCookies) { const setCookie = response.headers.get('set-cookie'); if (setCookie) { setCookie.split(',').forEach(cookie => { const [pair] = cookie.split(';'); const [key, value] = pair.split('='); if (key && value) this.cookies.set(key.trim(), value.trim()); }); } } return response; } catch (error) { clearTimeout(id); throw error; } } /** * Fetches raw text */ async getText(url, config = {}) { try { const response = await this._request(url, config); return await response.text(); } catch (error) { console.error(`Scraper Text Error [GET ${url}]:`, error.message); throw error; } } /** * Fetches HTML and returns a Cheerio object */ async getHTML(url, config = {}) { try { const data = await this.getText(url, config); return cheerio.load(data); } catch (error) { throw error; } } /** * Fetches JSON data */ async getJSON(url, config = {}) { try { const response = await this._request(url, config); const text = await response.text(); try { return JSON.parse(text); } catch (e) { if (text.trim().startsWith(' 0 ? $(".js-categories-seasonal table tr").slice(1) : $(".list table tr").slice(1); rows.each((i, el) => { const row = $(el); const titleAnchor = row.find("a.hoverinfo_trigger").first(); const title = titleAnchor.find("strong").text().trim() || row.find(".title strong").text().trim(); const link = titleAnchor.attr("href") || row.find(".title a").attr("href"); if (!title || !link) return; const imgEl = row.find(".picSurround img"); let img = imgEl.attr("data-src") || imgEl.attr("src") || row.find("img").first().attr("data-src") || row.find("img").first().attr("src"); const desc = row.find(".pt4").text().replace('read more.', '').trim(); const type = row.find("td").eq(2).text().trim(); const eps = row.find("td").eq(3).text().trim(); const score = row.find("td").eq(4).text().trim(); results.push({ title, url: link, thumbnail: this._formatImage(img), description: desc, type, episodes: eps, score: score !== 'N/A' ? score : '0.00' }); }); return results; } catch (error) { throw new Error(`MAL Search Error: ${error.message}`); } } /** * Get top anime or upcoming */ async getTop(type = 'all') { try { const validTypes = ['all', 'airing', 'upcoming', 'tv', 'movie', 'ova', 'ona', 'special', 'bypopularity', 'favorite']; const malType = validTypes.includes(type) ? (type === 'all' ? '' : type) : ''; const $ = await this.getHTML(`/topanime.php?type=${malType}`); const results = []; $(".ranking-list").each((i, el) => { const row = $(el); const title = row.find(".title h3 a").text().trim(); const link = row.find(".title h3 a").attr("href"); const score = row.find(".score span").text().trim(); const rank = row.find(".rank span").text().trim(); let img = row.find("img").attr("data-src") || row.find("img").attr("src"); const info = row.find(".information").text().trim().split('\n').map(x => x.trim()).filter(Boolean); if (title) { results.push({ rank: parseInt(rank), title, url: link, score: score !== 'N/A' ? score : '0.00', thumbnail: this._formatImage(img), info: info[0] || '' }); } }); return results; } catch (error) { throw new Error(`MAL Top Error: ${error.message}`); } } /** * Get detailed anime info */ async getDetail(url) { try { const $ = await this.getHTML(url); const title = $("h1.title-name").text().trim(); const alternateTitle = $(".title-english").text().trim(); const thumbnail = this._formatImage($("img[itemprop='image']").attr("data-src") || $("img[itemprop='image']").attr("src")); const score = $(".score-label").first().text().trim(); const rank = $(".ranked strong").text().trim().replace('#', ''); const popularity = $(".popularity strong").text().trim().replace('#', ''); const members = $(".members strong").text().trim(); const synopsis = $("p[itemprop='description']").text().trim(); const info = {}; $(".spaceit_pad").each((i, el) => { const text = $(el).text().trim(); if (text.includes(':')) { const parts = text.split(':'); const key = parts[0].trim().toLowerCase().replace(/\s+/g, '_'); const val = parts.slice(1).join(':').trim().replace(/\s+/g, ' '); if (key && val && val !== 'Unknown' && !key.includes('score')) { info[key] = val; } } }); return { title, alternate_title: alternateTitle, thumbnail, score: score !== 'N/A' ? score : '0.00', rank, popularity, members, synopsis, details: info }; } catch (error) { throw new Error(`MAL Detail Error: ${error.message}`); } } } export { BaseScraper, MyAnimeList }; export default MyAnimeList; // ───────────────────────────────────────────────────────────────────── // Self-test: jalankan langsung dengan `node myanimelist.js [args...]` // ───────────────────────────────────────────────────────────────────── if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const __exports = { MyAnimeList: MyAnimeList } const __fns = Object.fromEntries(Object.entries(__exports).filter(([, v]) => typeof v === 'function')) const __names = Object.keys(__fns) if (__names.length === 0) { console.log('Tidak ada fungsi yang bisa di-demo. Import modul ini dari kode lain.') } else { const __search = __names.find((n) => /search|cari/i.test(n)) const __name = __search || __names[0] const __fn = __fns[__name] const __isClass = /^class\s/.test(Function.prototype.toString.call(__fn)) let __args = process.argv.slice(2).map((a) => { try { return JSON.parse(a) } catch { return a } }) if (__args.length === 0 && __search) __args.push('naruto') if (__args.length === 0 && !__search && !__isClass && __fn.length > 0) { console.log('Fungsi: ' + __names.join(', ')) console.log('Contoh: node ' + "myanimelist.js" + ' ' + __name + ' ') } else { ;(async () => { try { const r = __isClass ? new __fn(...__args) : await __fn(...__args) console.log(JSON.stringify(r, null, 2)) } catch (e) { console.error('Error: ' + (e && e.message ? e.message : e)) process.exitCode = 1 } })() } } }