775 lines
21 KiB
JavaScript
775 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 SPLICE_DOMAIN_RE = /^https?:\/\/([a-z0-9-]+\.)*splice\.com(\/|$)/i;
|
|
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 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 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: 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();
|
|
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();
|