scrape search sound effect [myinstants(.)com]
JavaScriptPublicby zx-apiAug 14, 2026, 11:47 AMExpires: Never167 views
1/**
2 * Name: Search Sound effect
3 * Sumber: https://whatsapp.com/channel/0029VbDLqe7EquiSF4STU13o
4 * Credit By Zx
5 * Note: Kalau Mau Sher Jangan Hapus credit/Sumber ya cees😹✌️
6*/
7
8import * as cheerio from "cheerio";
9import fs from "fs/promises";
10import { createWriteStream } from "fs";
11import { Readable } from "stream";
12import { pipeline } from "stream/promises";
13import path from "path";
14
15export class MyInstants {
16 static DEFAULT_USER_AGENT =
17 "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36";
18
19 constructor(options = {}) {
20 this.baseUrl = options.baseUrl || "https://www.myinstants.com";
21 this.debug = options.debug ?? false;
22 this.delayMs = options.delayMs ?? 1500;
23 this.maxRetries = options.maxRetries ?? 3;
24 this.timeoutMs = options.timeoutMs ?? 30000;
25
26 this.headers = {
27 "User-Agent":
28 options.userAgent || MyInstants.DEFAULT_USER_AGENT,
29 Accept:
30 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
31 "Accept-Language":
32 options.acceptLanguage || "en-US,en;q=0.9,id;q=0.8",
33 Referer: options.referer || `${this.baseUrl}/`,
34 ...(options.headers || {}),
35 };
36
37 if (options.cookie) {
38 this.headers.Cookie = options.cookie;
39 }
40 }
41
42 _log(...args) {
43 if (this.debug) console.log("[MyInstants]", ...args);
44 }
45
46 _sleep(ms) {
47 return new Promise((resolve) => setTimeout(resolve, ms));
48 }
49
50 _absUrl(url) {
51 if (!url) return null;
52 try {
53 return new URL(url, this.baseUrl).toString();
54 } catch {
55 return null;
56 }
57 }
58
59 _cleanText(text) {
60 return String(text || "").replace(/\s+/g, " ").trim();
61 }
62
63 _extractAudioPath(onclick = "") {
64 const match = String(onclick).match(/play\(\s*['"]([^'"]+)['"]/);
65 return match?.[1] || null;
66 }
67
68 _extractSlug(onclick = "") {
69 const match = String(onclick).match(
70 /play\(\s*['"][^'"]+['"]\s*,\s*['"][^'"]*['"]\s*,\s*['"]([^'"]+)['"]\s*\)/
71 );
72 return match?.[1] || null;
73 }
74
75 _extractFavoriteId(onclick = "") {
76 const match = String(onclick).match(/favorite\(\s*['"](\d+)['"]\s*\)/);
77 return match?.[1] || null;
78 }
79
80 _safeFileName(name) {
81 const base = String(name || "audio.mp3").split("/").pop();
82 return base.replace(/[^a-zA-Z0-9_.-]+/g, "_").slice(0, 180) || "audio.mp3";
83 }
84
85 async _fetch(url, options = {}) {
86 let lastError = null;
87
88 for (let attempt = 1; attempt <= this.maxRetries; attempt += 1) {
89 const controller = new AbortController();
90 const timer = setTimeout(() => controller.abort(), this.timeoutMs);
91
92 try {
93 this._log(`FETCH ${url} | attempt=${attempt}`);
94
95 const response = await fetch(url, {
96 ...options,
97 headers: {
98 ...this.headers,
99 ...(options.headers || {}),
100 },
101 signal: controller.signal,
102 redirect: "follow",
103 });
104
105 clearTimeout(timer);
106
107 if ([429, 500, 502, 503, 504].includes(response.status)) {
108 throw new Error(`HTTP ${response.status}`);
109 }
110
111 if (!response.ok) {
112 throw new Error(`HTTP ${response.status} ${response.statusText}`);
113 }
114
115 return response;
116 } catch (error) {
117 clearTimeout(timer);
118 lastError = error;
119 this._log(`ERROR fetch ${url}: ${error.message}`);
120
121 if (attempt < this.maxRetries) {
122 await this._sleep(this.delayMs * attempt);
123 }
124 }
125 }
126
127 throw lastError || new Error("Fetch gagal setelah beberapa percobaan.");
128 }
129
130 async getHtml(url) {
131 const response = await this._fetch(url, {
132 headers: {
133 Accept:
134 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
135 },
136 });
137
138 return response.text();
139 }
140
141 buildUrl(pathname, params = {}) {
142 if (/^https?:\/\//i.test(pathname)) return pathname;
143
144 const url = new URL(
145 pathname.startsWith("/") ? pathname : `/${pathname}`,
146 this.baseUrl
147 );
148
149 for (const [key, value] of Object.entries(params)) {
150 if (value !== undefined && value !== null && value !== "") {
151 url.searchParams.set(key, String(value));
152 }
153 }
154
155 return url.toString();
156 }
157
158 parseInstantList(html) {
159 const $ = cheerio.load(html);
160 const items = [];
161
162 $(".instant").each((index, element) => {
163 const $el = $(element);
164
165 const link = $el.find("a.instant-link");
166 const button = $el.find("button.small-button");
167
168 const onclick = button.attr("onclick") || "";
169 const href = link.attr("href") || "";
170
171 const title = this._cleanText(
172 link.text() ||
173 button
174 .attr("title")
175 ?.replace(/^Play\s+/i, "")
176 .replace(/\s+sound$/i, "") ||
177 ""
178 );
179
180 const audioPath = this._extractAudioPath(onclick);
181 const slug =
182 this._extractSlug(onclick) ||
183 href.split("/").filter(Boolean).pop() ||
184 null;
185
186 const favoriteId = this._extractFavoriteId(
187 $el.find('button[onclick*="favorite"]').attr("onclick") || ""
188 );
189
190 const color =
191 $el
192 .find(".circle")
193 .attr("style")
194 ?.match(/background-color:\s*([^;]+);?/i)?.[1]
195 ?.trim() || null;
196
197 items.push({
198 position: index + 1,
199 title,
200 slug,
201 pageUrl: href ? this._absUrl(href) : null,
202 mp3Url: audioPath ? this._absUrl(audioPath) : null,
203 audioPath,
204 favoriteId,
205 color,
206 });
207 });
208
209 return items;
210 }
211
212 parseInstantDetail(html) {
213 const $ = cheerio.load(html);
214
215 const canonical =
216 $('link[rel="canonical"]').attr("href") ||
217 $('meta[property="og:url"]').attr("content") ||
218 null;
219
220 const title = this._cleanText(
221 $("#instant-page-title").first().text() ||
222 $('meta[property="og:title"]')
223 .attr("content")
224 ?.split(" - ")[0] ||
225 $("title").text().split(" - ")[0] ||
226 ""
227 );
228
229 const preloadMatch = html.match(
230 /var\s+preloadAudioUrl\s*=\s*['"]([^'"]+)['"]/
231 );
232
233 let mp3Url =
234 preloadMatch?.[1] ||
235 $('meta[property="og:audio"]').attr("content") ||
236 this._extractAudioPath(
237 $("#instant-page-button-element").attr("onclick") || ""
238 );
239
240 mp3Url = mp3Url ? this._absUrl(mp3Url) : null;
241
242 const description = this._cleanText(
243 $("#instant-page-description").text() ||
244 $('meta[name="description"]').attr("content") ||
245 ""
246 );
247
248 const tags = $("#instant-page-tags a")
249 .map((_, el) => ({
250 tag: this._cleanText($(el).text()),
251 searchUrl: this._absUrl($(el).attr("href")),
252 }))
253 .get();
254
255 const favoritesText = $("#instant-page-likes")
256 .text()
257 .replace(/,/g, "");
258
259 const favorites = Number(
260 favoritesText.match(/(\d+)\s*users/i)?.[1] || 0
261 );
262
263 const contentText = $("#content").text().replace(/,/g, " ");
264
265 const views = Number(contentText.match(/(\d+)\s*views/i)?.[1] || 0);
266
267 const uploaderLink = $('a[href*="/profile/"][href*="/uploaded/"]').first();
268
269 const uploader = {
270 username: this._cleanText(uploaderLink.text()),
271 profileUrl: uploaderLink.attr("href")
272 ? this._absUrl(uploaderLink.attr("href"))
273 : null,
274 };
275
276 const breadcrumbs = $("#breadcrumbs a")
277 .map((_, el) => this._cleanText($(el).text()))
278 .get();
279
280 const category = breadcrumbs.length >= 3 ? breadcrumbs[2] : null;
281
282 const embedCode =
283 $("#instant-embed").val() || $("#instant-embed").text() || null;
284
285 const mp3Filename = mp3Url ? this._safeFileName(mp3Url) : null;
286
287 return {
288 pageUrl: canonical ? this._absUrl(canonical) : null,
289 title,
290 mp3Url,
291 mp3Filename,
292 description,
293 tags,
294 favorites,
295 views,
296 uploader,
297 breadcrumbs,
298 category,
299 embedCode,
300 };
301 }
302
303 async scrapeListUrl(url) {
304 const html = await this.getHtml(url);
305 return this.parseInstantList(html);
306 }
307
308 /**
309 * Trending per negara.
310 */
311 async getTrending(countryCode = "us", page = 1) {
312 const url = this.buildUrl(
313 `/en/index/${encodeURIComponent(countryCode)}/`,
314 page > 1 ? { page } : {}
315 );
316
317 return this.scrapeListUrl(url);
318 }
319
320 /**
321 * Trending global.
322 */
323 async getTrendingGlobal(page = 1) {
324 const url = this.buildUrl(
325 "/en/trending/",
326 page > 1 ? { page } : {}
327 );
328
329 return this.scrapeListUrl(url);
330 }
331
332 /**
333 * Sound terbaru.
334 */
335 async getRecent(page = 1) {
336 const url = this.buildUrl(
337 "/en/recent/",
338 page > 1 ? { page } : {}
339 );
340
341 return this.scrapeListUrl(url);
342 }
343
344 /**
345 * Scraping kategori.
346 */
347 async getCategory(category, page = 1) {
348 const url = this.buildUrl(
349 `/en/categories/${encodeURIComponent(category)}/`,
350 page > 1 ? { page } : {}
351 );
352
353 return this.scrapeListUrl(url);
354 }
355
356 /**
357 * Pencarian.
358 */
359 async search(query, page = 1) {
360 const url = this.buildUrl("/en/search/", {
361 name: query,
362 ...(page > 1 ? { page } : {}),
363 });
364
365 return this.scrapeListUrl(url);
366 }
367
368 /**
369 * Ambil banyak halaman pencarian.
370 */
371 async searchAll(
372 query,
373 { maxPages = 3, limit = 100, dedupe = true } = {}
374 ) {
375 const results = [];
376 const seen = new Set();
377
378 for (let page = 1; page <= maxPages; page += 1) {
379 const items = await this.search(query, page);
380
381 if (!items.length) break;
382
383 for (const item of items) {
384 const key =
385 item.slug ||
386 item.pageUrl ||
387 `${item.title}:${item.mp3Url}`;
388
389 if (dedupe && seen.has(key)) continue;
390
391 seen.add(key);
392 results.push(item);
393
394 if (results.length >= limit) return results;
395 }
396
397 if (page < maxPages) {
398 await this._sleep(this.delayMs);
399 }
400 }
401
402 return results;
403 }
404
405 /**
406 * Ambil detail dari slug atau URL.
407 * Contoh slug: vine-boom-sound-70972
408 */
409 async getInstantDetail(slugOrUrl) {
410 const url = /^https?:\/\//i.test(slugOrUrl)
411 ? slugOrUrl
412 : this.buildUrl(`/en/instant/${slugOrUrl}/`);
413
414 const html = await this.getHtml(url);
415
416 return {
417 requestedUrl: url,
418 ...this.parseInstantDetail(html),
419 };
420 }
421
422 /* Function Download MP3 */
423
424 async downloadMp3(mp3Url, outputDir = "./downloads", filename = null) {
425 const url = this._absUrl(mp3Url);
426
427 if (!url) {
428 throw new Error("URL MP3 tidak valid.");
429 }
430
431 const safeName = this._safeFileName(filename || url);
432
433 await fs.mkdir(outputDir, { recursive: true });
434
435 const filePath = path.join(outputDir, safeName);
436
437 const response = await this._fetch(url, {
438 headers: {
439 Accept: "audio/mpeg,audio/*;q=0.9,*/*;q=0.8",
440 },
441 });
442
443 if (!response.body) {
444 throw new Error("Response body kosong, gagal download MP3.");
445 }
446
447 await pipeline(
448 Readable.fromWeb(response.body),
449 createWriteStream(filePath)
450 );
451
452 this._log(`MP3 tersimpan: ${filePath}`);
453
454 return filePath;
455 }
456
457 /**
458 * Download dari slug atau URL detail.
459 */
460 async downloadBySlug(slugOrUrl, outputDir = "./downloads") {
461 const detail = await this.getInstantDetail(slugOrUrl);
462
463 if (!detail.mp3Url) {
464 throw new Error(
465 `MP3 tidak ditemukan untuk: ${slugOrUrl}`
466 );
467 }
468
469 const filePath = await this.downloadMp3(
470 detail.mp3Url,
471 outputDir,
472 detail.mp3Filename
473 );
474
475 return {
476 ...detail,
477 filePath,
478 };
479 }
480
481 /**
482 * Download banyak item.
483 * items bisa berasal dari hasil search/getTrending/getCategory.
484 */
485 async batchDownload(
486 items,
487 outputDir = "./downloads",
488 { limit = 10, skipExisting = true } = {}
489 ) {
490 const results = [];
491
492 for (const item of items.slice(0, limit)) {
493 try {
494 const detail = item.mp3Url
495 ? item
496 : await this.getInstantDetail(item.slug || item.pageUrl);
497
498 if (!detail.mp3Url) {
499 throw new Error("MP3 URL tidak tersedia.");
500 }
501
502 const fileName = this._safeFileName(detail.mp3Url);
503 const filePath = path.join(outputDir, fileName);
504
505 if (skipExisting) {
506 try {
507 await fs.access(filePath);
508
509 results.push({
510 success: true,
511 skipped: true,
512 title: detail.title || item.title,
513 filePath,
514 });
515
516 continue;
517 } catch {
518 }
519 }
520
521 const savedPath = await this.downloadMp3(
522 detail.mp3Url,
523 outputDir,
524 fileName
525 );
526
527 results.push({
528 success: true,
529 skipped: false,
530 title: detail.title || item.title,
531 filePath: savedPath,
532 });
533 } catch (error) {
534 results.push({
535 success: false,
536 title: item?.title || item?.slug || null,
537 error: error.message,
538 });
539 }
540
541 await this._sleep(this.delayMs);
542 }
543
544 return results;
545 }
546
547 async saveJson(data, filePath = "./output/result.json") {
548 await fs.mkdir(path.dirname(filePath), { recursive: true });
549
550 await fs.writeFile(
551 filePath,
552 JSON.stringify(data, null, 2),
553 "utf8"
554 );
555
556 this._log(`JSON tersimpan: ${filePath}`);
557
558 return filePath;
559 }
560}
561
562async function main() {
563 const scraper = new MyInstants({
564 debug: true,
565 delayMs: 2000,
566 maxRetries: 3,
567 });
568
569 // Cari sound berdasarkan keyword
570 const query = "VINE BOOM SOUND";
571
572 const searchResults = await scraper.searchAll(query, {
573 maxPages: 2,
574 limit: 20,
575 });
576
577 console.log("Hasil pencarian:");
578 console.log(JSON.stringify(searchResults, null, 2));
579
580 await scraper.saveJson(
581 searchResults,
582 "./output/search-vine-boom.json"
583 );
584
585 // Ambil detail item pertama
586 if (searchResults[0]?.slug) {
587 const detail = await scraper.getInstantDetail(
588 searchResults[0].slug
589 );
590
591 console.log("Detail:");
592 console.log(detail);
593
594 // Download MP3 item pertama
595 const downloaded = await scraper.downloadBySlug(
596 searchResults[0].slug,
597 "./downloads"
598 );
599
600 console.log("MP3 disimpan ke:", downloaded.filePath);
601 }
602
603 // :) Contoh ambil trending US
604 const trendingUS = await scraper.getTrending("us", 1);
605 console.log("Trending US:", trendingUS);
606
607 // :) Contoh download batch 5 sound dari trending
608 const batchResult = await scraper.batchDownload(
609 trendingUS,
610 "./downloads/trending-us",
611 {
612 limit: 5,
613 skipExisting: true,
614 }
615 );
616
617 console.log("Batch download result:");
618 console.log(batchResult);
619}
620
621main().catch(console.error);621 lines·15,307 chars·15.0 KB
Wwrap·Ffullscreen