6 Commits
signed ... main

Author SHA1 Message Date
74af18f8aa Update readme URL 2026-08-06 16:42:14 -05:00
f07e9f9c3a Remove suffix, don't alert on download cancel 2026-08-06 16:30:01 -05:00
fecc87cd66 Update readme 2026-08-05 23:42:59 -05:00
964be309cf Update readme 2026-08-05 23:42:34 -05:00
1fb42334ae Clear cache on relaod 2026-08-05 23:36:34 -05:00
49bc022909 Update readme and icons 2026-08-05 23:31:00 -05:00
15 changed files with 201 additions and 21 deletions

View File

@@ -33,7 +33,7 @@ WTFPL licensed.
1. Open `about:debugging#/runtime/this-firefox` 1. Open `about:debugging#/runtime/this-firefox`
2. Click **Load Temporary Add-on...** 2. Click **Load Temporary Add-on...**
3. Select this extension's `manifest.json` 3. Select this extension's `manifest.json`
4. Visit `https://splice.com/sounds/search/samples` 4. Visit any URL under `https://splice.com/sounds` (for example `https://splice.com/sounds/packs/`)
## Build Firefox Package ## Build Firefox Package
@@ -51,4 +51,24 @@ WTFPL licensed.
3. Click **Load Temporary Add-on...** 3. Click **Load Temporary Add-on...**
4. Select `dist/splirate-firefox-v<version>.xpi` 4. Select `dist/splirate-firefox-v<version>.xpi`
Note: on standard Firefox, unsigned extensions are temporary via `about:debugging` and are removed on restart. Note: on standard Firefox, unsigned extensions are temporary via `about:debugging` and are removed on restart.
## Signed Build Download
- Direct signed XPI: [splirate-1.0.2.xpi](https://git.zetaphor.com/zetaphor/splirate/releases/download/signed/splirate-1.0.2.xpi)
- Releases page: [Splirate Releases](https://git.zetaphor.com/zetaphor/splirate/releases)
## Persistent Install (Private / Unlisted Signing)
Use Mozilla's unlisted signing to get a persistent signed XPI without public listing.
1. Create AMO API credentials:
- [Firefox Add-on Developer Hub](https://addons.mozilla.org/developers/)
2. Export credentials in your shell:
- `export AMO_JWT_ISSUER="your-issuer"`
- `export AMO_JWT_SECRET="your-secret"`
3. Run signing:
- `npm run sign`
4. Install the signed `.xpi` from:
- `dist/signed/`
- Firefox: `about:addons` → gear icon → **Install Add-on From File...**

View File

@@ -5,7 +5,7 @@ const FULL_URL_BY_SAMPLE = new Map();
const HISTORY_KEY = "capturedS3History"; const HISTORY_KEY = "capturedS3History";
const MAX_HISTORY_ITEMS = 500; const MAX_HISTORY_ITEMS = 500;
const TARGET_HOST = "splice.com"; const TARGET_HOST = "splice.com";
const TARGET_PATH = "/sounds/search/samples"; const TARGET_PATH_PREFIX = "/sounds";
const AUDIO_SAMPLE_PATH_RE = /\/audio_samples\//i; const AUDIO_SAMPLE_PATH_RE = /\/audio_samples\//i;
const WAVEFORM_PATH_RE = /\.wv\.json$/i; const WAVEFORM_PATH_RE = /\.wv\.json$/i;
const SCRAMBLED_PATH_RE = /-scrambled\//i; const SCRAMBLED_PATH_RE = /-scrambled\//i;
@@ -18,7 +18,9 @@ function isDomainAllowed(pageUrl) {
} }
try { try {
const parsed = new URL(pageUrl); const parsed = new URL(pageUrl);
return parsed.protocol === "https:" && parsed.hostname === TARGET_HOST && parsed.pathname === TARGET_PATH; 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) { } catch (error) {
return false; return false;
} }
@@ -409,7 +411,7 @@ function deriveFilename(entry, mode) {
} }
return `${safeStem}.wav`; return `${safeStem}.wav`;
} }
return `${safeStem}_decoded.mp3`; return `${safeStem}.mp3`;
} }
async function downloadDirectUrl(url, entry) { async function downloadDirectUrl(url, entry) {
@@ -647,6 +649,12 @@ function serializeTabEntries(state) {
.map(serializeEntry); .map(serializeEntry);
} }
async function clearAllCapturedState() {
TAB_CAPTURED.clear();
FULL_URL_BY_SAMPLE.clear();
await browser.storage.local.remove(HISTORY_KEY);
}
browser.runtime.onMessage.addListener((message, sender) => { browser.runtime.onMessage.addListener((message, sender) => {
if (!message || typeof message.type !== "string") { if (!message || typeof message.type !== "string") {
return undefined; return undefined;
@@ -724,6 +732,10 @@ browser.runtime.onMessage.addListener((message, sender) => {
}); });
} }
if (message.type === "CLEAR_CAPTURED_STATE") {
return clearAllCapturedState().then(() => ({ ok: true }));
}
if (message.type === "DOWNLOAD_ENTRY") { if (message.type === "DOWNLOAD_ENTRY") {
return resolveAndDownloadEntry(message.entry) return resolveAndDownloadEntry(message.entry)
.then(async (result) => { .then(async (result) => {
@@ -864,3 +876,7 @@ browser.webNavigation.onCommitted.addListener((details) => {
} }
maybeClearTabState(details.tabId); maybeClearTabState(details.tabId);
}); });
browser.runtime.onStartup.addListener(() => {
void clearAllCapturedState();
});

View File

@@ -3,7 +3,7 @@
const TARGET_SELECTOR = ".playbar-playback-info-bar.playback-row-section.svelte-11gjrk3"; const TARGET_SELECTOR = ".playbar-playback-info-bar.playback-row-section.svelte-11gjrk3";
const INJECTED_CONTAINER_ID = "s3-audio-capture-controls"; const INJECTED_CONTAINER_ID = "s3-audio-capture-controls";
const TARGET_HOST = "splice.com"; const TARGET_HOST = "splice.com";
const TARGET_PATH = "/sounds/search/samples"; const TARGET_PATH_PREFIX = "/sounds";
const PROBE_EVENT_SOURCE = "S3_CAPTURE_PROBE"; const PROBE_EVENT_SOURCE = "S3_CAPTURE_PROBE";
let knownUrls = new Set(); let knownUrls = new Set();
let knownPerformanceUrls = new Set(); let knownPerformanceUrls = new Set();
@@ -21,13 +21,20 @@ let currentPlaybackCoverImageUrl = "";
let lastPlaybackMetaSignature = ""; let lastPlaybackMetaSignature = "";
let lastPlaybackMetaApplied = false; 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) { function isDomainAllowed(url) {
if (typeof url !== "string") { if (typeof url !== "string") {
return false; return false;
} }
try { try {
const parsed = new URL(url); const parsed = new URL(url);
return parsed.protocol === "https:" && parsed.hostname === TARGET_HOST && parsed.pathname === TARGET_PATH; 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) { } catch (error) {
return false; return false;
} }
@@ -595,6 +602,9 @@ function injectControls() {
const currentButton = createButtonMetaItem("download", "Download current audio", () => { const currentButton = createButtonMetaItem("download", "Download current audio", () => {
browser.runtime.sendMessage({ type: "DOWNLOAD_LATEST" }).then((result) => { browser.runtime.sendMessage({ type: "DOWNLOAD_LATEST" }).then((result) => {
if (!result || !result.ok) { if (!result || !result.ok) {
if (isUserCancelledError(result)) {
return;
}
window.alert((result && result.error) || "No URL available for download."); window.alert((result && result.error) || "No URL available for download.");
} }
}).catch(() => { }).catch(() => {

View File

@@ -11,6 +11,14 @@ body {
padding: 24px; padding: 24px;
} }
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
h1 { h1 {
margin: 0; margin: 0;
font-size: 0; font-size: 0;
@@ -27,6 +35,23 @@ p {
color: #8ea292; color: #8ea292;
} }
#clearHistoryBtn {
border: 1px solid #3f2a2a;
background: #1c1111;
color: #dcb9b9;
border-radius: 999px;
cursor: pointer;
padding: 6px 10px;
font-size: 12px;
transition: all 120ms ease;
}
#clearHistoryBtn:hover {
border-color: #6b4444;
color: #f3d7d7;
background: #241515;
}
#historyList { #historyList {
list-style: none; list-style: none;
margin: 0; margin: 0;

View File

@@ -13,6 +13,7 @@
<main class="history-page"> <main class="history-page">
<header> <header>
<h1><img class="brand-logo" src="logo.png" alt="Splirate"></h1> <h1><img class="brand-logo" src="logo.png" alt="Splirate"></h1>
<button id="clearHistoryBtn" type="button">Clear</button>
</header> </header>
<p id="status">Loading history...</p> <p id="status">Loading history...</p>
<ul id="historyList"></ul> <ul id="historyList"></ul>

View File

@@ -2,6 +2,12 @@
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const listEl = document.getElementById("historyList"); const listEl = document.getElementById("historyList");
const clearHistoryBtn = document.getElementById("clearHistoryBtn");
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 formatMeta(entry) { function formatMeta(entry) {
const packName = entry && entry.packName ? entry.packName : "n/a"; const packName = entry && entry.packName ? entry.packName : "n/a";
@@ -104,6 +110,9 @@ function buildItem(entry) {
entry entry
}); });
if (!result || !result.ok) { if (!result || !result.ok) {
if (isUserCancelledError(result)) {
return;
}
window.alert((result && result.error) || "Download failed."); window.alert((result && result.error) || "Download failed.");
return; return;
} }
@@ -140,3 +149,19 @@ async function loadHistory() {
loadHistory().catch(() => { loadHistory().catch(() => {
statusEl.textContent = "Unexpected error while loading history."; statusEl.textContent = "Unexpected error while loading history.";
}); });
if (clearHistoryBtn) {
clearHistoryBtn.addEventListener("click", async () => {
const confirmed = window.confirm("Clear all captured samples and history?");
if (!confirmed) {
return;
}
const result = await browser.runtime.sendMessage({ type: "CLEAR_CAPTURED_STATE" });
if (!result || !result.ok) {
statusEl.textContent = "Unable to clear captured samples.";
return;
}
await loadHistory();
statusEl.textContent = "Captured samples cleared.";
});
}

BIN
icons/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
icons/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
icons/icon-96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,19 +1,19 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Splirate", "name": "Splirate",
"version": "1.0.0", "version": "1.0.2",
"description": "Splirate captures Splice sample links and downloads playable audio.", "description": "Splirate captures Splice sample links and downloads playable audio.",
"icons": { "icons": {
"48": "logo-single.png", "48": "icons/icon-48.png",
"96": "logo-single.png", "96": "icons/icon-96.png",
"128": "logo-single.png" "128": "icons/icon-128.png"
}, },
"permissions": [ "permissions": [
"tabs", "tabs",
"storage", "storage",
"downloads", "downloads",
"webRequest", "webRequest",
"https://splice.com/sounds/search/samples*", "https://splice.com/sounds*",
"*://*.amazonaws.com/*", "*://*.amazonaws.com/*",
"*://*.amazonaws.com.cn/*", "*://*.amazonaws.com.cn/*",
"webNavigation" "webNavigation"
@@ -26,7 +26,7 @@
"content_scripts": [ "content_scripts": [
{ {
"matches": [ "matches": [
"https://splice.com/sounds/search/samples*" "https://splice.com/sounds*"
], ],
"js": [ "js": [
"content-script.js" "content-script.js"
@@ -37,8 +37,8 @@
"browser_action": { "browser_action": {
"default_title": "Splirate", "default_title": "Splirate",
"default_icon": { "default_icon": {
"48": "logo-single.png", "48": "icons/icon-48.png",
"96": "logo-single.png" "96": "icons/icon-96.png"
}, },
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
@@ -48,10 +48,15 @@
"history.css", "history.css",
"page-probe.js" "page-probe.js"
], ],
"applications": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "splirate@splice.local", "id": "splirate@splice.local",
"strict_min_version": "109.0" "strict_min_version": "109.0",
"data_collection_permissions": {
"required": [
"none"
]
}
} }
} }
} }

View File

@@ -1,11 +1,13 @@
{ {
"name": "splirate", "name": "splirate",
"version": "1.0.0", "version": "1.0.2",
"private": true, "private": true,
"description": "Splirate Firefox extension", "description": "Splirate Firefox extension",
"scripts": { "scripts": {
"build": "bash scripts/build-firefox.sh", "build": "bash scripts/build-firefox.sh",
"build:firefox": "bash scripts/build-firefox.sh", "build:firefox": "bash scripts/build-firefox.sh",
"build:xpi": "bash scripts/build-firefox.sh" "build:xpi": "bash scripts/build-firefox.sh",
"sign": "bash scripts/sign-firefox.sh",
"sign:firefox": "bash scripts/sign-firefox.sh"
} }
} }

View File

@@ -20,6 +20,12 @@ body {
margin-bottom: 10px; margin-bottom: 10px;
} }
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.popup-header h1 { .popup-header h1 {
margin: 0; margin: 0;
font-size: 0; font-size: 0;
@@ -43,12 +49,29 @@ body {
transition: all 120ms ease; transition: all 120ms ease;
} }
#clearBtn {
border: 1px solid #3f2a2a;
background: #1c1111;
color: #dcb9b9;
border-radius: 999px;
cursor: pointer;
padding: 6px 10px;
font-size: 12px;
transition: all 120ms ease;
}
#openHistoryBtn:hover { #openHistoryBtn:hover {
border-color: #3c5342; border-color: #3c5342;
color: #e3f3e7; color: #e3f3e7;
background: #151d18; background: #151d18;
} }
#clearBtn:hover {
border-color: #6b4444;
color: #f3d7d7;
background: #241515;
}
.status { .status {
margin: 8px 0 12px; margin: 8px 0 12px;
color: #829589; color: #829589;

View File

@@ -11,7 +11,10 @@
<main class="popup"> <main class="popup">
<header class="popup-header"> <header class="popup-header">
<h1><img class="brand-logo" src="logo.png" alt="Splirate"></h1> <h1><img class="brand-logo" src="logo.png" alt="Splirate"></h1>
<button id="openHistoryBtn" type="button">Open Full History</button> <div class="header-actions">
<button id="clearBtn" type="button">Clear</button>
<button id="openHistoryBtn" type="button">History</button>
</div>
</header> </header>
<section> <section>
<p id="status" class="status">Loading captured URLs...</p> <p id="status" class="status">Loading captured URLs...</p>

View File

@@ -3,11 +3,17 @@
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const listEl = document.getElementById("urlList"); const listEl = document.getElementById("urlList");
const openHistoryBtn = document.getElementById("openHistoryBtn"); const openHistoryBtn = document.getElementById("openHistoryBtn");
const clearBtn = document.getElementById("clearBtn");
function setStatus(message) { function setStatus(message) {
statusEl.textContent = message; statusEl.textContent = message;
} }
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 entryLabel(entry) { function entryLabel(entry) {
return entry && entry.fullUrl ? "full candidate" : "preview fallback"; return entry && entry.fullUrl ? "full candidate" : "preview fallback";
} }
@@ -114,6 +120,9 @@ function buildRow(entry) {
download.addEventListener("click", async () => { download.addEventListener("click", async () => {
const result = await browser.runtime.sendMessage({ type: "DOWNLOAD_ENTRY", entry }); const result = await browser.runtime.sendMessage({ type: "DOWNLOAD_ENTRY", entry });
if (!result || !result.ok) { if (!result || !result.ok) {
if (isUserCancelledError(result)) {
return;
}
window.alert((result && result.error) || "Download failed."); window.alert((result && result.error) || "Download failed.");
return; return;
} }
@@ -153,6 +162,22 @@ openHistoryBtn.addEventListener("click", async () => {
window.close(); window.close();
}); });
if (clearBtn) {
clearBtn.addEventListener("click", async () => {
const confirmed = window.confirm("Clear all captured samples and history?");
if (!confirmed) {
return;
}
const result = await browser.runtime.sendMessage({ type: "CLEAR_CAPTURED_STATE" });
if (!result || !result.ok) {
setStatus("Unable to clear captured samples.");
return;
}
await loadCapturedUrls();
setStatus("Captured samples cleared.");
});
}
loadCapturedUrls().catch(() => { loadCapturedUrls().catch(() => {
setStatus("Unexpected error while loading URLs."); setStatus("Unexpected error while loading URLs.");
}); });

25
scripts/sign-firefox.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="${ROOT_DIR}/dist/signed"
if [[ -z "${AMO_JWT_ISSUER:-}" || -z "${AMO_JWT_SECRET:-}" ]]; then
echo "Missing AMO credentials." >&2
echo "Set AMO_JWT_ISSUER and AMO_JWT_SECRET before running this script." >&2
exit 1
fi
mkdir -p "${DIST_DIR}"
cd "${ROOT_DIR}"
# Signs as an unlisted add-on (private distribution).
npx web-ext sign \
--source-dir "${ROOT_DIR}" \
--artifacts-dir "${DIST_DIR}" \
--channel unlisted \
--ignore-files "dist/*" "scripts/*" ".git/*" "*.har" "*.log" "*.tmp"
echo "Signed XPI artifacts are in:"
echo "${DIST_DIR}"