/** * Name: Search Sound effect * Sumber: https://whatsapp.com/channel/0029VbDLqe7EquiSF4STU13o * Credit By Zx * Note: Kalau Mau Sher Jangan Hapus credit/Sumber ya cees😹✌️ */ import * as cheerio from "cheerio"; import fs from "fs/promises"; import { createWriteStream } from "fs"; import { Readable } from "stream"; import { pipeline } from "stream/promises"; import path from "path"; export class MyInstants { static DEFAULT_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36"; constructor(options = {}) { this.baseUrl = options.baseUrl || "https://www.myinstants.com"; this.debug = options.debug ?? false; this.delayMs = options.delayMs ?? 1500; this.maxRetries = options.maxRetries ?? 3; this.timeoutMs = options.timeoutMs ?? 30000; this.headers = { "User-Agent": options.userAgent || MyInstants.DEFAULT_USER_AGENT, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": options.acceptLanguage || "en-US,en;q=0.9,id;q=0.8", Referer: options.referer || `${this.baseUrl}/`, ...(options.headers || {}), }; if (options.cookie) { this.headers.Cookie = options.cookie; } } _log(...args) { if (this.debug) console.log("[MyInstants]", ...args); } _sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } _absUrl(url) { if (!url) return null; try { return new URL(url, this.baseUrl).toString(); } catch { return null; } } _cleanText(text) { return String(text || "").replace(/\s+/g, " ").trim(); } _extractAudioPath(onclick = "") { const match = String(onclick).match(/play\(\s*['"]([^'"]+)['"]/); return match?.[1] || null; } _extractSlug(onclick = "") { const match = String(onclick).match( /play\(\s*['"][^'"]+['"]\s*,\s*['"][^'"]*['"]\s*,\s*['"]([^'"]+)['"]\s*\)/ ); return match?.[1] || null; } _extractFavoriteId(onclick = "") { const match = String(onclick).match(/favorite\(\s*['"](\d+)['"]\s*\)/); return match?.[1] || null; } _safeFileName(name) { const base = String(name || "audio.mp3").split("/").pop(); return base.replace(/[^a-zA-Z0-9_.-]+/g, "_").slice(0, 180) || "audio.mp3"; } async _fetch(url, options = {}) { let lastError = null; for (let attempt = 1; attempt <= this.maxRetries; attempt += 1) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.timeoutMs); try { this._log(`FETCH ${url} | attempt=${attempt}`); const response = await fetch(url, { ...options, headers: { ...this.headers, ...(options.headers || {}), }, signal: controller.signal, redirect: "follow", }); clearTimeout(timer); if ([429, 500, 502, 503, 504].includes(response.status)) { throw new Error(`HTTP ${response.status}`); } if (!response.ok) { throw new Error(`HTTP ${response.status} ${response.statusText}`); } return response; } catch (error) { clearTimeout(timer); lastError = error; this._log(`ERROR fetch ${url}: ${error.message}`); if (attempt < this.maxRetries) { await this._sleep(this.delayMs * attempt); } } } throw lastError || new Error("Fetch gagal setelah beberapa percobaan."); } async getHtml(url) { const response = await this._fetch(url, { headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", }, }); return response.text(); } buildUrl(pathname, params = {}) { if (/^https?:\/\//i.test(pathname)) return pathname; const url = new URL( pathname.startsWith("/") ? pathname : `/${pathname}`, this.baseUrl ); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null && value !== "") { url.searchParams.set(key, String(value)); } } return url.toString(); } parseInstantList(html) { const $ = cheerio.load(html); const items = []; $(".instant").each((index, element) => { const $el = $(element); const link = $el.find("a.instant-link"); const button = $el.find("button.small-button"); const onclick = button.attr("onclick") || ""; const href = link.attr("href") || ""; const title = this._cleanText( link.text() || button .attr("title") ?.replace(/^Play\s+/i, "") .replace(/\s+sound$/i, "") || "" ); const audioPath = this._extractAudioPath(onclick); const slug = this._extractSlug(onclick) || href.split("/").filter(Boolean).pop() || null; const favoriteId = this._extractFavoriteId( $el.find('button[onclick*="favorite"]').attr("onclick") || "" ); const color = $el .find(".circle") .attr("style") ?.match(/background-color:\s*([^;]+);?/i)?.[1] ?.trim() || null; items.push({ position: index + 1, title, slug, pageUrl: href ? this._absUrl(href) : null, mp3Url: audioPath ? this._absUrl(audioPath) : null, audioPath, favoriteId, color, }); }); return items; } parseInstantDetail(html) { const $ = cheerio.load(html); const canonical = $('link[rel="canonical"]').attr("href") || $('meta[property="og:url"]').attr("content") || null; const title = this._cleanText( $("#instant-page-title").first().text() || $('meta[property="og:title"]') .attr("content") ?.split(" - ")[0] || $("title").text().split(" - ")[0] || "" ); const preloadMatch = html.match( /var\s+preloadAudioUrl\s*=\s*['"]([^'"]+)['"]/ ); let mp3Url = preloadMatch?.[1] || $('meta[property="og:audio"]').attr("content") || this._extractAudioPath( $("#instant-page-button-element").attr("onclick") || "" ); mp3Url = mp3Url ? this._absUrl(mp3Url) : null; const description = this._cleanText( $("#instant-page-description").text() || $('meta[name="description"]').attr("content") || "" ); const tags = $("#instant-page-tags a") .map((_, el) => ({ tag: this._cleanText($(el).text()), searchUrl: this._absUrl($(el).attr("href")), })) .get(); const favoritesText = $("#instant-page-likes") .text() .replace(/,/g, ""); const favorites = Number( favoritesText.match(/(\d+)\s*users/i)?.[1] || 0 ); const contentText = $("#content").text().replace(/,/g, " "); const views = Number(contentText.match(/(\d+)\s*views/i)?.[1] || 0); const uploaderLink = $('a[href*="/profile/"][href*="/uploaded/"]').first(); const uploader = { username: this._cleanText(uploaderLink.text()), profileUrl: uploaderLink.attr("href") ? this._absUrl(uploaderLink.attr("href")) : null, }; const breadcrumbs = $("#breadcrumbs a") .map((_, el) => this._cleanText($(el).text())) .get(); const category = breadcrumbs.length >= 3 ? breadcrumbs[2] : null; const embedCode = $("#instant-embed").val() || $("#instant-embed").text() || null; const mp3Filename = mp3Url ? this._safeFileName(mp3Url) : null; return { pageUrl: canonical ? this._absUrl(canonical) : null, title, mp3Url, mp3Filename, description, tags, favorites, views, uploader, breadcrumbs, category, embedCode, }; } async scrapeListUrl(url) { const html = await this.getHtml(url); return this.parseInstantList(html); } /** * Trending per negara. */ async getTrending(countryCode = "us", page = 1) { const url = this.buildUrl( `/en/index/${encodeURIComponent(countryCode)}/`, page > 1 ? { page } : {} ); return this.scrapeListUrl(url); } /** * Trending global. */ async getTrendingGlobal(page = 1) { const url = this.buildUrl( "/en/trending/", page > 1 ? { page } : {} ); return this.scrapeListUrl(url); } /** * Sound terbaru. */ async getRecent(page = 1) { const url = this.buildUrl( "/en/recent/", page > 1 ? { page } : {} ); return this.scrapeListUrl(url); } /** * Scraping kategori. */ async getCategory(category, page = 1) { const url = this.buildUrl( `/en/categories/${encodeURIComponent(category)}/`, page > 1 ? { page } : {} ); return this.scrapeListUrl(url); } /** * Pencarian. */ async search(query, page = 1) { const url = this.buildUrl("/en/search/", { name: query, ...(page > 1 ? { page } : {}), }); return this.scrapeListUrl(url); } /** * Ambil banyak halaman pencarian. */ async searchAll( query, { maxPages = 3, limit = 100, dedupe = true } = {} ) { const results = []; const seen = new Set(); for (let page = 1; page <= maxPages; page += 1) { const items = await this.search(query, page); if (!items.length) break; for (const item of items) { const key = item.slug || item.pageUrl || `${item.title}:${item.mp3Url}`; if (dedupe && seen.has(key)) continue; seen.add(key); results.push(item); if (results.length >= limit) return results; } if (page < maxPages) { await this._sleep(this.delayMs); } } return results; } /** * Ambil detail dari slug atau URL. * Contoh slug: vine-boom-sound-70972 */ async getInstantDetail(slugOrUrl) { const url = /^https?:\/\//i.test(slugOrUrl) ? slugOrUrl : this.buildUrl(`/en/instant/${slugOrUrl}/`); const html = await this.getHtml(url); return { requestedUrl: url, ...this.parseInstantDetail(html), }; } /* Function Download MP3 */ async downloadMp3(mp3Url, outputDir = "./downloads", filename = null) { const url = this._absUrl(mp3Url); if (!url) { throw new Error("URL MP3 tidak valid."); } const safeName = this._safeFileName(filename || url); await fs.mkdir(outputDir, { recursive: true }); const filePath = path.join(outputDir, safeName); const response = await this._fetch(url, { headers: { Accept: "audio/mpeg,audio/*;q=0.9,*/*;q=0.8", }, }); if (!response.body) { throw new Error("Response body kosong, gagal download MP3."); } await pipeline( Readable.fromWeb(response.body), createWriteStream(filePath) ); this._log(`MP3 tersimpan: ${filePath}`); return filePath; } /** * Download dari slug atau URL detail. */ async downloadBySlug(slugOrUrl, outputDir = "./downloads") { const detail = await this.getInstantDetail(slugOrUrl); if (!detail.mp3Url) { throw new Error( `MP3 tidak ditemukan untuk: ${slugOrUrl}` ); } const filePath = await this.downloadMp3( detail.mp3Url, outputDir, detail.mp3Filename ); return { ...detail, filePath, }; } /** * Download banyak item. * items bisa berasal dari hasil search/getTrending/getCategory. */ async batchDownload( items, outputDir = "./downloads", { limit = 10, skipExisting = true } = {} ) { const results = []; for (const item of items.slice(0, limit)) { try { const detail = item.mp3Url ? item : await this.getInstantDetail(item.slug || item.pageUrl); if (!detail.mp3Url) { throw new Error("MP3 URL tidak tersedia."); } const fileName = this._safeFileName(detail.mp3Url); const filePath = path.join(outputDir, fileName); if (skipExisting) { try { await fs.access(filePath); results.push({ success: true, skipped: true, title: detail.title || item.title, filePath, }); continue; } catch { } } const savedPath = await this.downloadMp3( detail.mp3Url, outputDir, fileName ); results.push({ success: true, skipped: false, title: detail.title || item.title, filePath: savedPath, }); } catch (error) { results.push({ success: false, title: item?.title || item?.slug || null, error: error.message, }); } await this._sleep(this.delayMs); } return results; } async saveJson(data, filePath = "./output/result.json") { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile( filePath, JSON.stringify(data, null, 2), "utf8" ); this._log(`JSON tersimpan: ${filePath}`); return filePath; } } async function main() { const scraper = new MyInstants({ debug: true, delayMs: 2000, maxRetries: 3, }); // Cari sound berdasarkan keyword const query = "VINE BOOM SOUND"; const searchResults = await scraper.searchAll(query, { maxPages: 2, limit: 20, }); console.log("Hasil pencarian:"); console.log(JSON.stringify(searchResults, null, 2)); await scraper.saveJson( searchResults, "./output/search-vine-boom.json" ); // Ambil detail item pertama if (searchResults[0]?.slug) { const detail = await scraper.getInstantDetail( searchResults[0].slug ); console.log("Detail:"); console.log(detail); // Download MP3 item pertama const downloaded = await scraper.downloadBySlug( searchResults[0].slug, "./downloads" ); console.log("MP3 disimpan ke:", downloaded.filePath); } // :) Contoh ambil trending US const trendingUS = await scraper.getTrending("us", 1); console.log("Trending US:", trendingUS); // :) Contoh download batch 5 sound dari trending const batchResult = await scraper.batchDownload( trendingUS, "./downloads/trending-us", { limit: 5, skipExisting: true, } ); console.log("Batch download result:"); console.log(batchResult); } main().catch(console.error);