Populating metadata

This commit is contained in:
2026-08-05 22:29:38 -05:00
parent cd1bb5460d
commit 235f76a124
7 changed files with 508 additions and 35 deletions

View File

@@ -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);