SampleFocus Ripper

The only ripper youll need because its practically the only one that works.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         SampleFocus Ripper
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  The only ripper youll need because its practically the only one that works.
// @author       MassaHex
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=samplefocus.com
// @match        https://samplefocus.com/*
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const Ripper = {
    config: {
      red: "#ee534f",
      green: "#4caf50",
      blue: "#64b5f6",
      yellow: "#ff9800",
    },

    state: {
      mp3Buffer: new Set(),
      owned: new Set(),
    },

    init() {
      this.injectStyles();
      this.loadOwned();
      this.sniffNetwork();
      setInterval(() => this.refreshUI(), 800);
    },

    injectStyles() {
      if (document.getElementById("ripper-style")) return;

      const s = document.createElement("style");
      s.id = "ripper-style";
      s.innerHTML = `

/* kill native tooltips */
.MuiTooltip-popper,[role="tooltip"]{display:none!important}

/* wrapper */
.ripper-wrap{position:relative;display:flex;flex-direction:column;width:100%}

/* tooltip (RESTORED 4.0 STYLE) */
.ripper-tooltip{
position:absolute;
padding:8px 12px;
font-size:11px;
background:#121212;
color:#fff;
border:1px solid #444;
border-radius:6px;
box-shadow:0 8px 24px rgba(0,0,0,0.8);
pointer-events:none;
opacity:0;
visibility:hidden;
transition:.15s;
white-space:nowrap;
z-index:9999;

/* CRITICAL FIX: lock position so it doesn't drift when hidden */
top:100%;
right:0;
transform:none;
}

/* ONLY reposition when visible */
.sample-card:hover .ripper-tooltip{
opacity:1;
visibility:visible;
top:calc(100% + 5px);
right:0;
}

.sample-hero .ripper-tooltip{
opacity:1;
visibility:visible;
top:calc(100% + 12px);
left:50%;
right:auto;
transform:translateX(-50%);
}

.sample-card:hover .ripper-tooltip{opacity:1;visibility:visible;top:calc(100% + 5px);right:0}
.sample-hero .ripper-tooltip{opacity:1;visibility:visible;top:calc(100% + 12px);left:50%;transform:translateX(-50%)}

.ripper-tooltip-header{font-weight:900;color:${this.config.red};font-size:10px;margin-bottom:2px}

/* HERO BUTTON FIX */
.sample-hero button[aria-label="Download"],
.sample-hero span[aria-label="Download"]{
background:#fff!important;
border:1.5px solid #d0d0d0!important;
box-shadow:none!important;
}

/* HERO STATES */
.sample-hero .ripper-owned{border:1.5px solid ${this.config.green}!important}
.sample-hero .ripper-owned i{color:${this.config.green}!important}

.sample-hero .ripper-buy{border:1.5px solid ${this.config.blue}!important}
.sample-hero .ripper-buy i{color:${this.config.blue}!important}

/* CARD ICON COLORS ONLY (NO BORDER CHANGE) */
.sample-card .ripper-owned i{color:${this.config.green}!important}
.sample-card .ripper-buy i{color:${this.config.blue}!important}

/* MP3 BUTTON (RECTANGLE FIXED) */
.ripper-mp3{
margin-top:6px;
width:100%;
height:34px;
display:flex;
align-items:center;
justify-content:center;
border-radius:6px;
cursor:pointer;
color:#fff;
}

.ripper-mp3.ready{background:${this.config.red}}
.ripper-mp3.wait{background:${this.config.yellow};cursor:wait}

`;

      document.head.appendChild(s);
    },

    loadOwned() {
      const raw = localStorage.getItem("downloadedSamples");
      if (!raw) return;

      try {
        const parsed = JSON.parse(raw);
        (parsed instanceof Array ? parsed : [parsed]).forEach((id) =>
          this.state.owned.add(String(id)),
        );
      } catch {
        raw.split(",").forEach((id) => this.state.owned.add(id.trim()));
      }
    },

    sniffNetwork() {
      const orig = window.fetch;
      window.fetch = async (...a) => {
        const url = typeof a[0] === "string" ? a[0] : a[0].url;
        if (url && url.includes(".mp3"))
          this.state.mp3Buffer.add(url.split("?")[0]);
        return orig(...a);
      };
    },

    extract(container) {
      const img = container.querySelector('img[src*="/sample_files/"]');
      if (!img) return {};

      const id = img.src.match(/\/sample_files\/(\d+)\//)?.[1];

      let url = null;
      for (const u of this.state.mp3Buffer) {
        if (u.includes(`/${id}/`)) {
          url = u;
          break;
        }
      }

      let rawName = "";

      // --- HERO PAGE (guaranteed reliable) ---
      if (container.classList.contains("sample-hero")) {
        const parts = window.location.pathname.split("/samples/");
        if (parts[1]) rawName = parts[1].split("/")[0];
      }

      // --- CARD LINK (second best) ---
      if (!rawName) {
        const link = container.querySelector('a[href*="/samples/"]');
        if (link) {
          const parts = link.href.split("/samples/");
          if (parts[1]) rawName = parts[1].split("/")[0];
        }
      }

      // --- LAST RESORT: visible text ---
      if (!rawName) {
        const t = container.querySelector(
          '[role="heading"], .sample-card-link div',
        );
        if (t) rawName = t.textContent.trim();
      }

      // --- FINAL FALLBACK ---
      if (!rawName) rawName = "sample";

      const clean = rawName
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "");

      return {
        id,
        url,
        name: clean,
        fileName: `${clean}.mp3`,
      };
    },

    refreshUI() {
      document.querySelectorAll(".sample-card,.sample-hero").forEach((c) => {
        const btn = c.querySelector('[aria-label="Download"]');
        if (!btn) return;

        const data = this.extract(c);
        const owned = this.state.owned.has(String(data.id));

        /* ICON LOGIC FIXED */
        btn.classList.remove("ripper-owned", "ripper-buy");

        if (owned) {
          btn.classList.add("ripper-owned");
          btn.innerHTML = '<i class="fas fa-download"></i>';
        } else {
          btn.classList.add("ripper-buy");
          btn.innerHTML = '<i class="fas fa-shopping-bag"></i>';
        }

        /* WRAPPER + MP3 BUTTON */
        if (!c.querySelector(".ripper-wrap")) {
          const wrap = document.createElement("div");
          wrap.className = "ripper-wrap";

          const mp3 = document.createElement("div");
          mp3.className = "ripper-mp3 wait";
          mp3.innerHTML = '<i class="fas fa-hourglass-half"></i>';

          const tip = document.createElement("div");
          tip.className = "ripper-tooltip";

          wrap.appendChild(mp3);
          wrap.appendChild(tip);
          btn.parentNode.appendChild(wrap);

          mp3.onclick = (e) => {
            e.stopPropagation();

            const fresh = this.extract(c);
            if (!fresh.url) return;

            mp3.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';

            fetch(fresh.url)
              .then((r) => r.blob())
              .then((b) => {
                const a = document.createElement("a");
                a.href = URL.createObjectURL(b);
                a.download = `${fresh.name}_${fresh.id}.mp3`;
                a.click();

                mp3.innerHTML = '<i class="fas fa-check"></i>';

                setTimeout(() => {
                  mp3.innerHTML = '<i class="fas fa-download"></i>';
                }, 1200);
              });
          };
        }

        const mp3 = c.querySelector(".ripper-mp3");
        const tip = c.querySelector(".ripper-tooltip");

        if (data.url) {
          mp3.classList.remove("wait");
          mp3.classList.add("ready");
          mp3.innerHTML = '<i class="fas fa-download"></i>';
          tip.innerHTML =
            '<div class="ripper-tooltip-header">READY</div><div>Download Preview MP3</div>';
        } else {
          mp3.classList.add("wait");
          mp3.innerHTML = '<i class="fas fa-hourglass-half"></i>';
          tip.innerHTML =
            '<div class="ripper-tooltip-header">SCANNING</div><div>Awaiting Sample Preview</div>';
        }

        /* auto sniff */
        c.addEventListener("mouseenter", () => {
          const w = c.querySelector('[class*="waveform"]');
          if (w)
            w.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
        });
      });
    },
  };

  Ripper.init();
})();