Files
splirate/content-script.js

794 lines
21 KiB
JavaScript

"use strict";
const TARGET_SELECTOR = ".playbar-playback-info-bar.playback-row-section.svelte-11gjrk3";
const INJECTED_CONTAINER_ID = "s3-audio-capture-controls";
const TARGET_HOST = "splice.com";
const TARGET_PATH_PREFIX = "/sounds";
const PROBE_EVENT_SOURCE = "S3_CAPTURE_PROBE";
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;
let currentPlaybackFilename = "";
let currentPlaybackKey = "";
let currentPlaybackBpm = "";
let currentPlaybackTags = [];
let currentPlaybackCoverImageUrl = "";
let lastPlaybackMetaSignature = "";
let lastPlaybackMetaApplied = false;
function isUserCancelledError(result) {
const error = result && typeof result.error === "string" ? result.error.toLowerCase() : "";
return error.includes("canceled by the user") || error.includes("cancelled by the user");
}
function isDomainAllowed(url) {
if (typeof url !== "string") {
return false;
}
try {
const parsed = new URL(url);
const path = parsed.pathname || "";
const inSoundsPath = path === TARGET_PATH_PREFIX || path.startsWith(TARGET_PATH_PREFIX + "/");
return parsed.protocol === "https:" && parsed.hostname === TARGET_HOST && inSoundsPath;
} catch (error) {
return false;
}
}
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 normalizeFileName(raw) {
if (typeof raw !== "string") {
return "";
}
const trimmed = raw.trim();
if (!trimmed) {
return "";
}
const parts = trimmed.split("/");
return parts[parts.length - 1] || trimmed;
}
function getPlaybackRoot() {
const playbar = document.querySelector(TARGET_SELECTOR);
if (playbar) {
return playbar;
}
const filenameNode = document.querySelector('[data-qa="filename"]');
if (filenameNode && typeof filenameNode.closest === "function") {
return filenameNode.closest(".playbar-playback-info-bar");
}
return null;
}
function queryPlaybackNode(selector) {
const root = getPlaybackRoot();
if (!root) {
return null;
}
return root.querySelector(selector);
}
function getPlaybackFilenameFromDom() {
const el = queryPlaybackNode('[data-qa="filename"]');
if (!el) {
return "";
}
return normalizeFileName(el.textContent || "");
}
function getPlaybackKeyFromDom() {
const el = queryPlaybackNode('[data-qa="key"]');
return el ? String(el.textContent || "").trim() : "";
}
function getPlaybackBpmFromDom() {
const el = queryPlaybackNode('[data-qa="bpm"]');
return el ? String(el.textContent || "").trim() : "";
}
function getPlaybackTagsFromDom() {
const root = queryPlaybackNode('[data-qa="tags"]');
if (!root) {
return [];
}
const tags = [];
const spans = root.querySelectorAll("span.tag");
for (const span of spans) {
const value = String(span.textContent || "").trim();
if (value) {
tags.push(value);
}
}
return Array.from(new Set(tags));
}
function getPlaybackCoverImageFromDom() {
const img = queryPlaybackNode('[data-qa="cover-art-image"]');
if (!img) {
return "";
}
const src = img.getAttribute("src") || img.currentSrc || "";
return normalizeUrl(src);
}
function extractCoverImageFromGraphqlItem(item) {
if (!item || typeof item !== "object") {
return "";
}
const ownFiles = Array.isArray(item.files) ? item.files : [];
for (const file of ownFiles) {
if (!file || typeof file !== "object") {
continue;
}
const typeSlug = String(file.asset_file_type_slug || "").toLowerCase();
if (typeSlug !== "cover_image") {
continue;
}
const url = normalizeUrl(file.url || "");
if (url) {
return url;
}
}
if (!item.parents || !Array.isArray(item.parents.items)) {
return "";
}
for (const parent of item.parents.items) {
const files = parent && Array.isArray(parent.files) ? parent.files : [];
for (const file of files) {
if (!file || typeof file !== "object") {
continue;
}
const typeSlug = String(file.asset_file_type_slug || "").toLowerCase();
if (typeSlug !== "cover_image") {
continue;
}
const url = normalizeUrl(file.url || "");
if (url) {
return url;
}
}
}
return "";
}
function extractPackNameFromGraphqlItem(item) {
if (!item || typeof item !== "object" || !item.parents || !Array.isArray(item.parents.items)) {
return "";
}
for (const parent of item.parents.items) {
if (!parent || typeof parent !== "object") {
continue;
}
const name = typeof parent.name === "string" ? parent.name.trim() : "";
if (name) {
return name;
}
}
return "";
}
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 itemName = normalizeFileName(String(item.name || ""));
const itemKey = typeof item.key === "string" ? item.key.trim().toUpperCase() : "";
const itemBpm = item.bpm == null ? "" : String(item.bpm).trim();
const itemTags = Array.isArray(item.tags)
? Array.from(new Set(item.tags.map((tag) => (tag && tag.label ? String(tag.label).trim() : "")).filter(Boolean)))
: [];
const itemCoverImageUrl = extractCoverImageFromGraphqlItem(item);
const itemPackName = extractPackNameFromGraphqlItem(item);
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,
fileName: itemName,
key: itemKey,
bpm: itemBpm,
tags: itemTags,
coverImageUrl: itemCoverImageUrl,
packName: itemPackName
});
}
}
return contexts;
}
function sendPreviewContexts(contexts) {
if (!contexts.length) {
return;
}
browser.runtime.sendMessage({
type: "CAPTURE_PREVIEW_CONTEXT",
contexts
}).catch(() => {
// Ignore transient extension messaging failures.
});
}
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",
fileName: getPlaybackFilenameFromDom()
}).catch(() => {
// Ignore transient extension context failures.
});
}
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);
};
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) {
this.__s3CaptureRequestUrl = typeof url === "string" ? 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: none !important;
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) {
if (isUserCancelledError(result)) {
return;
}
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();
syncPlaybackMeta();
});
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 syncPlaybackMeta() {
const fileName = getPlaybackFilenameFromDom();
const key = getPlaybackKeyFromDom();
const bpm = getPlaybackBpmFromDom();
const tags = getPlaybackTagsFromDom();
const coverImageUrl = getPlaybackCoverImageFromDom();
const sameTags =
tags.length === currentPlaybackTags.length &&
tags.every((tag, idx) => tag === currentPlaybackTags[idx]);
if (
(!fileName || fileName === currentPlaybackFilename) &&
key === currentPlaybackKey &&
bpm === currentPlaybackBpm &&
sameTags &&
coverImageUrl === currentPlaybackCoverImageUrl
) {
const signature = JSON.stringify({
fileName: currentPlaybackFilename,
key: currentPlaybackKey,
bpm: currentPlaybackBpm,
tags: currentPlaybackTags,
coverImageUrl: currentPlaybackCoverImageUrl
});
if (lastPlaybackMetaApplied && signature === lastPlaybackMetaSignature) {
return;
}
}
if (fileName) {
currentPlaybackFilename = fileName;
}
currentPlaybackKey = key;
currentPlaybackBpm = bpm;
currentPlaybackTags = tags;
currentPlaybackCoverImageUrl = coverImageUrl;
const payload = {
type: "UPDATE_ACTIVE_PLAYBACK_META",
fileName: currentPlaybackFilename,
key: currentPlaybackKey,
bpm: currentPlaybackBpm,
tags: currentPlaybackTags,
coverImageUrl: currentPlaybackCoverImageUrl
};
const signature = JSON.stringify(payload);
lastPlaybackMetaSignature = signature;
lastPlaybackMetaApplied = false;
browser.runtime.sendMessage(payload).then((result) => {
// Keep retrying on the next interval until background confirms attachment.
lastPlaybackMetaApplied = Boolean(result && result.ok);
}).catch(() => {
lastPlaybackMetaApplied = false;
// Ignore transient extension messaging failures.
});
}
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 : {};
if (kind === "graphql-contexts") {
const contexts = Array.isArray(payload.contexts) ? payload.contexts : [];
if (contexts.length) {
sendPreviewContexts(contexts);
}
return;
}
const candidateUrls = collectProbeCandidateUrls(payload);
if (candidateUrls.length) {
captureUrls(candidateUrls);
}
});
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();
};
root.appendChild(script);
}
function init() {
if (!isDomainAllowed(window.location.href)) {
return;
}
installPageProbeListener();
injectPageProbeScript();
currentPlaybackFilename = getPlaybackFilenameFromDom();
currentPlaybackKey = getPlaybackKeyFromDom();
currentPlaybackBpm = getPlaybackBpmFromDom();
currentPlaybackTags = getPlaybackTagsFromDom();
currentPlaybackCoverImageUrl = getPlaybackCoverImageFromDom();
lastPlaybackMetaSignature = "";
lastPlaybackMetaApplied = false;
scanNodeForUrls(document);
patchFetch();
patchXhr();
// Avoid buffered resource sweeps; they can re-capture stale previews.
injectControls();
startUiObserver();
startCaptureObserver();
window.setInterval(syncPlaybackMeta, 700);
}
init();