"use strict"; const TARGET_SELECTOR = ".playbar-playback-info-bar.playback-row-section.svelte-11gjrk3"; const INJECTED_CONTAINER_ID = "s3-audio-capture-controls"; const SPLICE_DOMAIN_RE = /^https?:\/\/([a-z0-9-]+\.)*splice\.com(\/|$)/i; const GRAPHQL_URL_FRAGMENT = "surfaces-graphql.splice.com/graphql"; const PROBE_EVENT_SOURCE = "S3_CAPTURE_PROBE"; const DEBUG_CAPTURE = true; let knownUrls = new Set(); let knownPerformanceUrls = new Set(); let observer = null; let captureObserver = null; let performanceObserver = null; let fetchPatched = false; let xhrPatched = false; let probeListenerInstalled = false; function isDomainAllowed(url) { return typeof url === "string" && SPLICE_DOMAIN_RE.test(url); } function isS3Url(url) { if (typeof url !== "string") { return false; } try { const parsed = new URL(url); if (parsed.protocol !== "https:") { return false; } const host = parsed.hostname.toLowerCase(); const isAwsHost = host.endsWith(".amazonaws.com") || host.endsWith(".amazonaws.com.cn"); const isS3Host = host.includes(".s3.") || host.startsWith("s3."); if (!isAwsHost || !isS3Host) { return false; } const path = parsed.pathname || ""; if (!path.includes("/audio_samples/")) { return false; } if (path.endsWith(".wv.json")) { return false; } return true; } catch (error) { return false; } } function normalizeUrl(rawUrl) { try { return new URL(rawUrl, window.location.href).toString(); } catch (error) { return ""; } } function logDebug(message, extra) { if (!DEBUG_CAPTURE) { return; } if (typeof extra === "undefined") { console.debug("[S3 capture]", message); return; } console.debug("[S3 capture]", message, extra); } function collectProbeCandidateUrls(payload) { if (!payload || typeof payload !== "object") { return []; } const candidates = []; const keys = ["requestUrl", "responseUrl", "linkedUrl", "url", "blobUrl"]; for (const key of keys) { if (typeof payload[key] === "string" && payload[key]) { candidates.push(payload[key]); } } return candidates; } function extractSampleIdFromUrl(url) { try { const parsed = new URL(url); const marker = "/audio_samples/"; const idx = parsed.pathname.indexOf(marker); if (idx < 0) { return ""; } const rest = parsed.pathname.slice(idx + marker.length); const parts = rest.split("/").filter(Boolean); const first = (parts[0] || "").replace(/-scrambled$/i, ""); const second = (parts[1] || "").split(".")[0]; const hashRe = /^[a-f0-9]{64}$/i; if (hashRe.test(first)) { return first.toLowerCase(); } if (hashRe.test(second)) { return second.toLowerCase(); } return ""; } catch (error) { return ""; } } function recursivelyCollectAudioUrls(value, output) { if (typeof value === "string") { const normalized = normalizeUrl(value); if (normalized && isS3Url(normalized)) { output.add(normalized); } return; } if (Array.isArray(value)) { for (const item of value) { recursivelyCollectAudioUrls(item, output); } return; } if (value && typeof value === "object") { for (const nested of Object.values(value)) { recursivelyCollectAudioUrls(nested, output); } } } function extractPreviewContextsFromGraphql(json) { const contexts = []; const items = json && json.data && json.data.assetsSearch && Array.isArray(json.data.assetsSearch.items) ? json.data.assetsSearch.items : []; for (const item of items) { if (!item || typeof item !== "object") { continue; } const sampleId = typeof item.uuid === "string" ? item.uuid.toLowerCase() : ""; const files = Array.isArray(item.files) ? item.files : []; for (const file of files) { if (!file || typeof file !== "object") { continue; } const typeSlug = String(file.asset_file_type_slug || "").toLowerCase(); const fileUrl = normalizeUrl(file.url || ""); if (typeSlug !== "preview_mp3" || !fileUrl) { continue; } contexts.push({ sampleId: sampleId || extractSampleIdFromUrl(fileUrl), previewUrl: fileUrl }); } } return contexts; } function sendPreviewContexts(contexts) { if (!contexts.length) { return; } browser.runtime.sendMessage({ type: "CAPTURE_PREVIEW_CONTEXT", contexts }).catch(() => { // Ignore transient extension messaging failures. }); logDebug("Captured preview contexts", contexts); } function inspectGraphqlResponse(candidateUrl, response) { if (typeof candidateUrl !== "string" || !candidateUrl.includes(GRAPHQL_URL_FRAGMENT)) { return; } if (!response || typeof response.clone !== "function") { return; } response.clone().json().then((json) => { const contexts = extractPreviewContextsFromGraphql(json); if (contexts.length) { sendPreviewContexts(contexts); } const urls = new Set(); recursivelyCollectAudioUrls(json, urls); if (urls.size) { captureUrls(Array.from(urls)); } }).catch(() => { // Some GraphQL responses may not be parseable JSON. }); } function captureUrls(inputUrls) { if (!isDomainAllowed(window.location.href)) { return; } const newUrls = []; for (const rawUrl of inputUrls) { const normalized = normalizeUrl(rawUrl); if (!normalized || !isS3Url(normalized) || knownUrls.has(normalized)) { continue; } knownUrls.add(normalized); newUrls.push(normalized); } if (!newUrls.length) { return; } browser.runtime.sendMessage({ type: "CAPTURE_URLS", urls: newUrls, source: "content_capture" }).catch(() => { // Ignore transient extension context failures. }); logDebug("Captured S3 URLs", newUrls); } function scanNodeForUrls(root) { if (!(root instanceof Element || root instanceof Document)) { return; } const urlCandidates = []; const selectors = [ "a[href]", "audio[src]", "source[src]", "[data-url]", "[data-src]" ]; for (const selector of selectors) { const nodes = root.querySelectorAll(selector); for (const node of nodes) { const attrs = ["href", "src", "data-url", "data-src"]; for (const attr of attrs) { const value = node.getAttribute(attr); if (value) { urlCandidates.push(value); } } } } captureUrls(urlCandidates); } function patchFetch() { if (fetchPatched || typeof window.fetch !== "function") { return; } try { const originalFetch = window.fetch.bind(window); window.fetch = function patchedFetch(input, init) { const candidate = typeof input === "string" ? input : input && input.url; if (candidate) { captureUrls([candidate]); } if (init && typeof init === "object" && typeof init.url === "string") { captureUrls([init.url]); } return originalFetch(input, init).then((response) => { if (candidate) { inspectGraphqlResponse(candidate, response); } return response; }); }; fetchPatched = true; } catch (error) { // Firefox may expose fetch as read-only in content script worlds. fetchPatched = false; } } function patchXhr() { if (xhrPatched || !window.XMLHttpRequest) { return; } try { const originalOpen = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function patchedOpen(method, url) { if (typeof url === "string") { captureUrls([url]); } return originalOpen.apply(this, arguments); }; xhrPatched = true; } catch (error) { xhrPatched = false; } } function capturePerformanceResourceUrls() { if (typeof performance === "undefined" || typeof performance.getEntriesByType !== "function") { return; } const resources = performance.getEntriesByType("resource"); const candidates = []; for (const entry of resources) { if (!entry || typeof entry.name !== "string") { continue; } if (knownPerformanceUrls.has(entry.name)) { continue; } knownPerformanceUrls.add(entry.name); candidates.push(entry.name); } if (candidates.length) { captureUrls(candidates); } } function startPerformanceObserver() { if (performanceObserver || typeof PerformanceObserver !== "function") { return; } try { performanceObserver = new PerformanceObserver((list) => { const candidates = []; for (const entry of list.getEntries()) { if (!entry || typeof entry.name !== "string") { continue; } if (knownPerformanceUrls.has(entry.name)) { continue; } knownPerformanceUrls.add(entry.name); candidates.push(entry.name); } if (candidates.length) { captureUrls(candidates); } }); performanceObserver.observe({ type: "resource", buffered: true }); } catch (error) { performanceObserver = null; } } function createIcon(kind) { const ns = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(ns, "svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("width", "14"); svg.setAttribute("height", "14"); svg.setAttribute("aria-hidden", "true"); if (kind === "download") { const arrow = document.createElementNS(ns, "path"); arrow.setAttribute("d", "M12 3v10m0 0l4-4m-4 4l-4-4"); arrow.setAttribute("fill", "none"); arrow.setAttribute("stroke", "currentColor"); arrow.setAttribute("stroke-width", "2"); arrow.setAttribute("stroke-linecap", "round"); arrow.setAttribute("stroke-linejoin", "round"); const tray = document.createElementNS(ns, "path"); tray.setAttribute("d", "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"); tray.setAttribute("fill", "none"); tray.setAttribute("stroke", "currentColor"); tray.setAttribute("stroke-width", "2"); tray.setAttribute("stroke-linecap", "round"); tray.setAttribute("stroke-linejoin", "round"); svg.appendChild(arrow); svg.appendChild(tray); return svg; } const circle = document.createElementNS(ns, "circle"); circle.setAttribute("cx", "12"); circle.setAttribute("cy", "12"); circle.setAttribute("r", "9"); circle.setAttribute("fill", "none"); circle.setAttribute("stroke", "currentColor"); circle.setAttribute("stroke-width", "2"); const hand = document.createElementNS(ns, "path"); hand.setAttribute("d", "M12 7v5l3 2"); hand.setAttribute("fill", "none"); hand.setAttribute("stroke", "currentColor"); hand.setAttribute("stroke-width", "2"); hand.setAttribute("stroke-linecap", "round"); hand.setAttribute("stroke-linejoin", "round"); svg.appendChild(circle); svg.appendChild(hand); return svg; } function createButtonMetaItem(iconType, ariaLabel, onClick) { const wrapper = document.createElement("div"); wrapper.className = "bordered-item meta-item svelte-11gjrk3"; const inner = document.createElement("div"); inner.className = "meta-wrap svelte-11gjrk3"; const button = document.createElement("button"); button.type = "button"; button.className = "meta-value svelte-11gjrk3 s3-capture-button"; button.setAttribute("aria-label", ariaLabel); button.setAttribute("title", ariaLabel); button.appendChild(createIcon(iconType)); button.addEventListener("click", onClick); inner.appendChild(button); wrapper.appendChild(inner); return wrapper; } function ensureInlineStyles() { if (document.getElementById("s3-audio-capture-style")) { return; } const style = document.createElement("style"); style.id = "s3-audio-capture-style"; style.textContent = ` .s3-capture-button { border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 4px; background: rgba(255, 255, 255, 0.08); color: inherit; cursor: pointer; padding: 4px 8px; font: inherit; display: inline-flex; align-items: center; justify-content: center; } .s3-capture-button:hover { background: rgba(255, 255, 255, 0.16); } .s3-capture-button svg { display: block; } `; document.head.appendChild(style); } function injectControls() { const target = document.querySelector(TARGET_SELECTOR); if (!target) { return; } if (target.querySelector("#" + INJECTED_CONTAINER_ID)) { return; } ensureInlineStyles(); const controlsRoot = document.createElement("div"); controlsRoot.id = INJECTED_CONTAINER_ID; controlsRoot.style.display = "contents"; const currentButton = createButtonMetaItem("download", "Download current audio", () => { browser.runtime.sendMessage({ type: "DOWNLOAD_LATEST" }).then((result) => { if (!result || !result.ok) { window.alert((result && result.error) || "No URL available for download."); } }).catch(() => { window.alert("Extension message failed."); }); }); const historyButton = createButtonMetaItem("history", "Open captured history", () => { browser.runtime.sendMessage({ type: "OPEN_HISTORY" }).catch(() => { window.alert("Unable to open history."); }); }); controlsRoot.appendChild(currentButton); controlsRoot.appendChild(historyButton); target.appendChild(controlsRoot); } function startUiObserver() { if (observer) { observer.disconnect(); } observer = new MutationObserver(() => { injectControls(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); } function startCaptureObserver() { if (captureObserver) { captureObserver.disconnect(); } captureObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { scanNodeForUrls(addedNode); } if (mutation.type === "attributes" && mutation.target instanceof Element) { scanNodeForUrls(mutation.target); } } }); captureObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["href", "src", "data-url", "data-src"] }); } function installPageProbeListener() { if (probeListenerInstalled) { return; } window.addEventListener("message", (event) => { if (event.source !== window) { return; } const data = event.data; if (!data || data.source !== PROBE_EVENT_SOURCE) { return; } const kind = typeof data.kind === "string" ? data.kind : "unknown"; const payload = data.payload && typeof data.payload === "object" ? data.payload : {}; const candidateUrls = collectProbeCandidateUrls(payload); if (candidateUrls.length) { captureUrls(candidateUrls); } logDebug("Probe event: " + kind, payload); }); probeListenerInstalled = true; } function injectPageProbeScript() { if (document.getElementById("s3-capture-page-probe")) { return; } const root = document.head || document.documentElement; if (!root) { return; } const script = document.createElement("script"); script.id = "s3-capture-page-probe"; script.src = browser.runtime.getURL("page-probe.js"); script.async = false; script.onload = () => { script.remove(); logDebug("Injected page probe."); }; script.onerror = () => { logDebug("Failed to inject page probe."); }; root.appendChild(script); } function init() { if (!isDomainAllowed(window.location.href)) { return; } installPageProbeListener(); injectPageProbeScript(); scanNodeForUrls(document); patchFetch(); patchXhr(); capturePerformanceResourceUrls(); startPerformanceObserver(); window.setInterval(capturePerformanceResourceUrls, 2000); injectControls(); startUiObserver(); startCaptureObserver(); } init();