scrape search sound effect [myinstants(.)com]

JavaScriptPublic
by zx-apiAug 14, 2026, 11:47 AMExpires: Never167 views
scrape search sound effect [myinstants(.)com]
Raw
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