diff --git a/background.js b/background.js index 53000d2..d31a53b 100644 --- a/background.js +++ b/background.js @@ -10,6 +10,8 @@ const WAVEFORM_PATH_RE = /\.wv\.json$/i; const SCRAMBLED_PATH_RE = /-scrambled\//i; const HASH_RE = /^[a-f0-9]{64}$/i; const GRAPHQL_URL = "https://surfaces-graphql.splice.com/graphql"; +const DEBUG_TRACE = true; +const DEBUG_TRACE_MARKER = "S3DBG_MAPTRACE_V1"; function isDomainAllowed(pageUrl) { return typeof pageUrl === "string" && SPLICE_DOMAIN_RE.test(pageUrl); @@ -75,6 +77,28 @@ function normalizeUrl(url) { } } +function normalizeName(value) { + return typeof value === "string" ? value.trim().toLowerCase() : ""; +} + +function debugMapTrace(stage, entry) { + if (!DEBUG_TRACE) { + return; + } + const safe = entry && typeof entry === "object" ? entry : {}; + console.debug(DEBUG_TRACE_MARKER, stage, { + sampleId: safe.sampleId || "", + fileName: safe.fileName || "", + key: safe.key || "", + bpm: safe.bpm || "", + tagsCount: Array.isArray(safe.tags) ? safe.tags.length : 0, + coverImageUrl: safe.coverImageUrl || "", + previewUrl: safe.previewUrl || "", + fullUrl: safe.fullUrl || "", + source: safe.source || "" + }); +} + function extractSampleIdFromUrl(url) { try { const parsed = new URL(url); @@ -128,7 +152,8 @@ function maybeClearTabState(tabId) { } } -function mergeEntry(tabState, inputEntry, source) { +function mergeEntry(tabState, inputEntry, source, options) { + const setLatest = !options || options.setLatest !== false; const sampleId = inputEntry.sampleId || extractSampleIdFromUrl(inputEntry.previewUrl || inputEntry.fullUrl || ""); if (!sampleId) { return null; @@ -138,6 +163,11 @@ function mergeEntry(tabState, inputEntry, source) { sampleId, previewUrl: "", fullUrl: "", + fileName: "", + key: "", + bpm: "", + tags: [], + coverImageUrl: "", source: source || "unknown", capturedAt: Date.now() }; @@ -158,9 +188,26 @@ function mergeEntry(tabState, inputEntry, source) { } else if (!merged.fullUrl && FULL_URL_BY_SAMPLE.has(sampleId)) { merged.fullUrl = FULL_URL_BY_SAMPLE.get(sampleId); } + if (inputEntry.fileName) { + merged.fileName = String(inputEntry.fileName).trim(); + } + if (typeof inputEntry.key === "string" && inputEntry.key.trim()) { + merged.key = inputEntry.key.trim(); + } + if (typeof inputEntry.bpm === "string" && inputEntry.bpm.trim()) { + merged.bpm = inputEntry.bpm.trim(); + } + if (Array.isArray(inputEntry.tags) && inputEntry.tags.length) { + merged.tags = Array.from(new Set(inputEntry.tags.map((x) => String(x).trim()).filter(Boolean))); + } + if (typeof inputEntry.coverImageUrl === "string" && inputEntry.coverImageUrl.trim()) { + merged.coverImageUrl = inputEntry.coverImageUrl.trim(); + } tabState.entriesBySample.set(sampleId, merged); - tabState.latestSampleId = sampleId; + if (setLatest) { + tabState.latestSampleId = sampleId; + } return merged; } @@ -176,12 +223,28 @@ function upsertPreviewContext(tabId, context, source) { } return mergeEntry( tabState, - { sampleId, previewUrl, fullUrl: context.fullUrl ? normalizeUrl(context.fullUrl) : "" }, - source || "graphql_preview" + { + sampleId, + previewUrl, + fullUrl: context.fullUrl ? normalizeUrl(context.fullUrl) : "", + fileName: context.fileName || "", + key: context.key || "", + bpm: context.bpm || "", + tags: Array.isArray(context.tags) ? context.tags : [], + coverImageUrl: context.coverImageUrl || "" + }, + source || "graphql_preview", + { setLatest: false } ); } function upsertUrl(tabId, url, source) { + const sourceTag = source && typeof source === "object" ? source.tag || "" : source || ""; + const sourceFileName = source && typeof source === "object" ? source.fileName || "" : ""; + const sourceKey = source && typeof source === "object" ? source.key || "" : ""; + const sourceBpm = source && typeof source === "object" ? source.bpm || "" : ""; + const sourceTags = source && typeof source === "object" && Array.isArray(source.tags) ? source.tags : []; + const sourceCoverImageUrl = source && typeof source === "object" ? source.coverImageUrl || "" : ""; const normalized = normalizeUrl(url); if (!normalized || !isAudioSampleUrl(normalized) || isWaveformUrl(normalized)) { return null; @@ -195,11 +258,16 @@ function upsertUrl(tabId, url, source) { const entry = { sampleId, previewUrl: isPreviewUrl(normalized) ? normalized : "", - fullUrl: isFullUrl(normalized) ? normalized : "" + fullUrl: isFullUrl(normalized) ? normalized : "", + fileName: sourceFileName, + key: sourceKey, + bpm: sourceBpm, + tags: sourceTags, + coverImageUrl: sourceCoverImageUrl }; if (typeof tabId === "number" && tabId >= 0) { - return mergeEntry(ensureTabState(tabId), entry, source); + return mergeEntry(ensureTabState(tabId), entry, sourceTag); } if (entry.fullUrl) { @@ -216,14 +284,32 @@ async function appendHistoryRows(rows) { const data = await browser.storage.local.get(HISTORY_KEY); const current = Array.isArray(data[HISTORY_KEY]) ? data[HISTORY_KEY] : []; const combined = [...current]; - const seen = new Set(current.map((item) => `${item.sampleId}|${item.url}`)); + const seenMap = new Map(); + current.forEach((item, idx) => { + seenMap.set(`${item.sampleId}|${item.url}`, idx); + }); for (const row of rows) { const key = `${row.sampleId}|${row.url}`; - if (!row.url || seen.has(key)) { + if (!row.url) { continue; } - seen.add(key); + if (seenMap.has(key)) { + const existingIdx = seenMap.get(key); + const existing = combined[existingIdx] || {}; + const hasCoverImageUrl = Object.prototype.hasOwnProperty.call(row, "coverImageUrl"); + combined[existingIdx] = { + ...existing, + ...row, + fileName: row.fileName || existing.fileName || "", + key: row.key || existing.key || "", + bpm: row.bpm || existing.bpm || "", + tags: Array.isArray(row.tags) && row.tags.length ? row.tags : (existing.tags || []), + coverImageUrl: hasCoverImageUrl ? String(row.coverImageUrl || "") : (existing.coverImageUrl || "") + }; + continue; + } + seenMap.set(key, combined.length); combined.push(row); } @@ -236,6 +322,11 @@ async function appendDownloadHistory(entry, result) { sampleId: resolvedEntry.sampleId || "", previewUrl: resolvedEntry.previewUrl || "", fullUrl: resolvedEntry.fullUrl || "", + fileName: resolvedEntry.fileName || "", + key: resolvedEntry.key || "", + bpm: resolvedEntry.bpm || "", + tags: Array.isArray(resolvedEntry.tags) ? resolvedEntry.tags : [], + coverImageUrl: resolvedEntry.coverImageUrl || "", url: result.historyUrl || resolvedEntry.fullUrl || "", source: result.mode === "full" ? "full_download" : "decoded_preview", capturedAt: Date.now(), @@ -253,6 +344,11 @@ function makeDecodedPlaceholderRow(entry, source) { sampleId, previewUrl: entry.previewUrl || "", fullUrl: entry.fullUrl || "", + fileName: entry.fileName || "", + key: entry.key || "", + bpm: entry.bpm || "", + tags: Array.isArray(entry.tags) ? entry.tags : [], + coverImageUrl: entry.coverImageUrl || "", url: `decoded://${sampleId}`, source: source || entry.source || "captured_preview", capturedAt: entry.capturedAt || Date.now(), @@ -293,14 +389,28 @@ function decodeSpliceAudio(data) { } function deriveFilename(entry, mode) { - const base = entry.sampleId || `splice-${Date.now()}`; + const sampleBase = entry.sampleId || `splice-${Date.now()}`; + const rawName = typeof entry.fileName === "string" ? entry.fileName.trim() : ""; + const cleanName = rawName ? rawName.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_") : ""; + const hasExt = cleanName.lastIndexOf(".") > 0; + const stem = hasExt ? cleanName.slice(0, cleanName.lastIndexOf(".")) : cleanName; + const ext = hasExt ? cleanName.slice(cleanName.lastIndexOf(".")).toLowerCase() : ""; + + const safeStem = stem || sampleBase; if (mode === "full") { - const full = entry.fullUrl || ""; - if (full.endsWith(".wav") || full.includes(".wav?")) { - return `${base}.wav`; + if (ext === ".wav" || ext === ".mp3" || ext === ".aif" || ext === ".aiff" || ext === ".flac") { + return `${safeStem}${ext}`; } + const full = (entry.fullUrl || "").toLowerCase(); + if (full.endsWith(".wav") || full.includes(".wav?")) { + return `${safeStem}.wav`; + } + if (full.endsWith(".mp3") || full.includes(".mp3?")) { + return `${safeStem}.mp3`; + } + return `${safeStem}.wav`; } - return `${base}-decoded.mp3`; + return `${safeStem}_decoded.mp3`; } async function downloadDirectUrl(url, entry) { @@ -421,7 +531,12 @@ function findEntryInTabStates(sampleId) { return { sampleId: entry.sampleId || sampleId, previewUrl: normalizeUrl(entry.previewUrl || ""), - fullUrl: normalizeUrl(entry.fullUrl || "") + fullUrl: normalizeUrl(entry.fullUrl || ""), + fileName: entry.fileName || "", + key: entry.key || "", + bpm: entry.bpm || "", + tags: Array.isArray(entry.tags) ? entry.tags : [], + coverImageUrl: entry.coverImageUrl || "" }; } } @@ -439,7 +554,16 @@ async function hydrateEntryFromHistory(sampleId) { const previewUrl = normalizeUrl(row.previewUrl || ""); const fullUrl = normalizeUrl(row.fullUrl || ""); if (previewUrl || fullUrl) { - return { sampleId, previewUrl, fullUrl }; + return { + sampleId, + previewUrl, + fullUrl, + fileName: row.fileName || "", + key: row.key || "", + bpm: row.bpm || "", + tags: Array.isArray(row.tags) ? row.tags : [], + coverImageUrl: row.coverImageUrl || "" + }; } } return null; @@ -451,7 +575,12 @@ async function resolveAndDownloadEntry(entryInput) { let normalizedEntry = { sampleId, previewUrl: normalizeUrl(entry.previewUrl || ""), - fullUrl: normalizeUrl(entry.fullUrl || "") + fullUrl: normalizeUrl(entry.fullUrl || ""), + fileName: entry.fileName || "", + key: entry.key || "", + bpm: entry.bpm || "", + tags: Array.isArray(entry.tags) ? entry.tags : [], + coverImageUrl: entry.coverImageUrl || "" }; if (normalizedEntry.sampleId && !normalizedEntry.previewUrl && !normalizedEntry.fullUrl) { @@ -499,6 +628,11 @@ function serializeEntry(entry) { sampleId: entry.sampleId || "", previewUrl: entry.previewUrl || "", fullUrl: entry.fullUrl || "", + fileName: entry.fileName || "", + key: entry.key || "", + bpm: entry.bpm || "", + tags: Array.isArray(entry.tags) ? entry.tags : [], + coverImageUrl: entry.coverImageUrl || "", source: entry.source || "", capturedAt: entry.capturedAt || Date.now() }; @@ -526,26 +660,33 @@ browser.runtime.onMessage.addListener((message, sender) => { } const contexts = Array.isArray(message.contexts) ? message.contexts : []; - const rows = []; for (const context of contexts) { const merged = upsertPreviewContext(tabId, context, "graphql_preview"); if (merged) { - const row = makeDecodedPlaceholderRow(merged, "graphql_preview"); - if (row) { - rows.push(row); - } + debugMapTrace("CAPTURE_PREVIEW_CONTEXT_MERGED", merged); } } - return appendHistoryRows(rows).then(() => ({ ok: true, added: contexts.length })); + return Promise.resolve({ ok: true, added: contexts.length }); } if (message.type === "CAPTURE_URLS") { const tabId = tabIdFromSender(sender); const urls = Array.isArray(message.urls) ? message.urls : []; + const canAttachPlaybackMeta = urls.length === 1; const rows = []; for (const raw of urls) { - const merged = upsertUrl(tabId, raw, message.source || "network_preview"); - if (merged && merged.previewUrl) { + const merged = upsertUrl(tabId, raw, { + tag: message.source || "network_preview", + fileName: canAttachPlaybackMeta ? (message.fileName || "") : "", + key: "", + bpm: "", + tags: [], + coverImageUrl: "" + }); + if (merged) { + debugMapTrace("CAPTURE_URLS_MERGED", merged); + } + if (merged && merged.previewUrl && canAttachPlaybackMeta) { const row = makeDecodedPlaceholderRow(merged, message.source || "network_preview"); if (row) { rows.push(row); @@ -597,6 +738,70 @@ browser.runtime.onMessage.addListener((message, sender) => { .catch((error) => ({ ok: false, error: error.message || "Download failed." })); } + if (message.type === "UPDATE_ACTIVE_PLAYBACK_META" || message.type === "UPDATE_ACTIVE_FILENAME") { + const tabId = tabIdFromSender(sender); + const state = TAB_CAPTURED.get(tabId); + if (!state || !state.latestSampleId) { + return Promise.resolve({ ok: false }); + } + const latest = state.entriesBySample.get(state.latestSampleId); + if (!latest) { + return Promise.resolve({ ok: false }); + } + const fileName = typeof message.fileName === "string" ? message.fileName.trim() : ""; + const key = typeof message.key === "string" ? message.key.trim() : ""; + const bpm = typeof message.bpm === "string" ? message.bpm.trim() : ""; + const tags = Array.isArray(message.tags) ? message.tags.map((x) => String(x).trim()).filter(Boolean) : []; + const coverImageUrl = typeof message.coverImageUrl === "string" ? message.coverImageUrl.trim() : ""; + const entries = Array.from(state.entriesBySample.values()); + let targetEntry = null; + if (fileName) { + const wantedName = normalizeName(fileName); + const matches = entries + .filter((entry) => normalizeName(entry.fileName) === wantedName) + .sort((a, b) => (b.capturedAt || 0) - (a.capturedAt || 0)); + if (matches.length) { + targetEntry = matches[0]; + } + } + if (!targetEntry && state.latestSampleId) { + const latestEntry = state.entriesBySample.get(state.latestSampleId); + const freshEnough = latestEntry && (Date.now() - (latestEntry.capturedAt || 0) < 15000); + const canHydrate = latestEntry && !latestEntry.fileName; + if (freshEnough && canHydrate) { + targetEntry = latestEntry; + } + } + if (!targetEntry) { + return Promise.resolve({ ok: false }); + } + + if (fileName) { + targetEntry.fileName = fileName; + } + if (key) { + targetEntry.key = key; + } + if (bpm) { + targetEntry.bpm = bpm; + } + if (tags.length) { + targetEntry.tags = Array.from(new Set(tags)); + } + if (coverImageUrl) { + targetEntry.coverImageUrl = coverImageUrl; + } + targetEntry.capturedAt = Date.now(); + state.entriesBySample.set(targetEntry.sampleId, targetEntry); + state.latestSampleId = targetEntry.sampleId; + debugMapTrace("UPDATE_ACTIVE_PLAYBACK_META_TARGET", targetEntry); + const row = makeDecodedPlaceholderRow(targetEntry, targetEntry.source || "playback_meta"); + if (!row) { + return Promise.resolve({ ok: true }); + } + return appendHistoryRows([row]).then(() => ({ ok: true })); + } + if (message.type === "DOWNLOAD_LATEST") { const tabId = tabIdFromSender(sender); const latest = getLatestEntryForTab(tabId); diff --git a/content-script.js b/content-script.js index 50379c7..0b32efd 100644 --- a/content-script.js +++ b/content-script.js @@ -6,6 +6,7 @@ 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; +const DEBUG_PLAYBACK_META_MARKER = "S3DBG_PLAYBAR_META_V2"; let knownUrls = new Set(); let knownPerformanceUrls = new Set(); let observer = null; @@ -14,6 +15,13 @@ 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); @@ -55,6 +63,121 @@ function normalizeUrl(rawUrl) { } } +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 logDebug(message, extra) { if (!DEBUG_CAPTURE) { return; @@ -142,6 +265,13 @@ function extractPreviewContextsFromGraphql(json) { 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 files = Array.isArray(item.files) ? item.files : []; for (const file of files) { @@ -155,7 +285,12 @@ function extractPreviewContextsFromGraphql(json) { } contexts.push({ sampleId: sampleId || extractSampleIdFromUrl(fileUrl), - previewUrl: fileUrl + previewUrl: fileUrl, + fileName: itemName, + key: itemKey, + bpm: itemBpm, + tags: itemTags, + coverImageUrl: itemCoverImageUrl }); } } @@ -221,7 +356,8 @@ function captureUrls(inputUrls) { browser.runtime.sendMessage({ type: "CAPTURE_URLS", urls: newUrls, - source: "content_capture" + source: "content_capture", + fileName: getPlaybackFilenameFromDom() }).catch(() => { // Ignore transient extension context failures. }); @@ -499,6 +635,7 @@ function startUiObserver() { } observer = new MutationObserver(() => { injectControls(); + syncPlaybackMeta(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); } @@ -527,6 +664,71 @@ function startCaptureObserver() { }); } +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 + }; + if (DEBUG_CAPTURE) { + console.debug(DEBUG_PLAYBACK_META_MARKER, { + fileName: payload.fileName, + key: payload.key, + bpm: payload.bpm, + tagsCount: Array.isArray(payload.tags) ? payload.tags.length : 0, + coverImageUrl: payload.coverImageUrl + }); + } + 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; @@ -584,15 +786,21 @@ function init() { installPageProbeListener(); injectPageProbeScript(); + currentPlaybackFilename = getPlaybackFilenameFromDom(); + currentPlaybackKey = getPlaybackKeyFromDom(); + currentPlaybackBpm = getPlaybackBpmFromDom(); + currentPlaybackTags = getPlaybackTagsFromDom(); + currentPlaybackCoverImageUrl = getPlaybackCoverImageFromDom(); + lastPlaybackMetaSignature = ""; + lastPlaybackMetaApplied = false; scanNodeForUrls(document); patchFetch(); patchXhr(); - capturePerformanceResourceUrls(); - startPerformanceObserver(); - window.setInterval(capturePerformanceResourceUrls, 2000); + // Avoid buffered resource sweeps; they can re-capture stale previews. injectControls(); startUiObserver(); startCaptureObserver(); + window.setInterval(syncPlaybackMeta, 700); } init(); diff --git a/history.css b/history.css index e11983b..b2f69fb 100644 --- a/history.css +++ b/history.css @@ -35,6 +35,15 @@ p { background: #151a26; } +.history-cover-image { + width: 48px; + height: 48px; + border-radius: 6px; + object-fit: cover; + display: block; + margin-bottom: 10px; +} + .history-url { margin: 0 0 8px; word-break: break-all; diff --git a/history.js b/history.js index 4cce27a..c9df467 100644 --- a/history.js +++ b/history.js @@ -11,18 +11,38 @@ function formatDate(ms) { } } +function formatMeta(entry) { + const key = entry && entry.key ? entry.key : "n/a"; + const bpm = entry && entry.bpm ? entry.bpm : "n/a"; + const tags = entry && Array.isArray(entry.tags) && entry.tags.length ? entry.tags.join(", ") : "n/a"; + return `Key: ${key} | BPM: ${bpm} | Tags: ${tags}`; +} + function buildItem(entry) { const li = document.createElement("li"); li.className = "history-item"; + if (entry.coverImageUrl) { + const cover = document.createElement("img"); + cover.className = "history-cover-image"; + cover.src = entry.coverImageUrl; + cover.alt = "Cover art"; + cover.loading = "lazy"; + li.appendChild(cover); + } + const url = document.createElement("p"); url.className = "history-url"; - url.textContent = entry.fullUrl || entry.url || ""; + url.textContent = entry.fileName || entry.sampleId || "(unknown filename)"; const meta = document.createElement("p"); meta.className = "history-meta"; - const mode = entry.fullUrl ? "full candidate" : "preview fallback"; - meta.textContent = `Captured: ${formatDate(entry.capturedAt)} | Mode: ${mode} | Source: ${entry.source || "unknown"}`; + const mode = entry.mode === "full" || entry.fullUrl ? "full candidate" : "preview fallback"; + meta.textContent = `Captured: ${formatDate(entry.capturedAt)} | Mode: ${mode} | Source: ${entry.source || "unknown"} | ID: ${entry.sampleId || "n/a"}`; + + const details = document.createElement("p"); + details.className = "history-meta"; + details.textContent = formatMeta(entry); const download = document.createElement("button"); download.type = "button"; @@ -41,6 +61,7 @@ function buildItem(entry) { li.appendChild(url); li.appendChild(meta); + li.appendChild(details); li.appendChild(download); return li; } diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..946d8a4 Binary files /dev/null and b/logo.png differ diff --git a/popup.css b/popup.css index fbfde40..54f5605 100644 --- a/popup.css +++ b/popup.css @@ -61,6 +61,15 @@ button.secondary { padding: 8px; } +.cover-image { + width: 36px; + height: 36px; + border-radius: 4px; + object-fit: cover; + display: block; + margin-bottom: 8px; +} + .url-text { margin: 0 0 8px; font-size: 11px; diff --git a/popup.js b/popup.js index 703034a..02ed569 100644 --- a/popup.js +++ b/popup.js @@ -12,17 +12,37 @@ function entryLabel(entry) { return entry && entry.fullUrl ? "full candidate" : "preview fallback"; } +function formatMeta(entry) { + const key = entry && entry.key ? entry.key : "n/a"; + const bpm = entry && entry.bpm ? entry.bpm : "n/a"; + const tags = entry && Array.isArray(entry.tags) && entry.tags.length ? entry.tags.join(", ") : "n/a"; + return `Key: ${key} | BPM: ${bpm} | Tags: ${tags}`; +} + function buildRow(entry) { const li = document.createElement("li"); li.className = "url-item"; + if (entry.coverImageUrl) { + const cover = document.createElement("img"); + cover.className = "cover-image"; + cover.src = entry.coverImageUrl; + cover.alt = "Cover art"; + cover.loading = "lazy"; + li.appendChild(cover); + } + const p = document.createElement("p"); p.className = "url-text"; - p.textContent = entry.previewUrl || entry.fullUrl || ""; + p.textContent = entry.fileName || entry.sampleId || "(unknown filename)"; const status = document.createElement("p"); status.className = "status"; - status.textContent = `Source: ${entry.source || "unknown"} | Mode: ${entryLabel(entry)}`; + status.textContent = `Source: ${entry.source || "unknown"} | Mode: ${entryLabel(entry)} | ID: ${entry.sampleId || "n/a"}`; + + const meta = document.createElement("p"); + meta.className = "status"; + meta.textContent = formatMeta(entry); const actions = document.createElement("div"); actions.className = "actions"; @@ -52,6 +72,7 @@ function buildRow(entry) { actions.appendChild(copy); li.appendChild(p); li.appendChild(status); + li.appendChild(meta); li.appendChild(actions); return li; }