Build scripts

This commit is contained in:
2026-08-05 22:59:23 -05:00
parent 4f10991abc
commit cee9f6a02f
18 changed files with 183 additions and 338 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# Build artifacts
dist/
*.xpi
*.zip
# Node / package manager
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Local captures and temp files
*.har
*.log
*.tmp
# OS / editor noise
.DS_Store
Thumbs.db
*~

13
LICENSE.md Normal file
View File

@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2026 Splirate
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View File

@@ -1,65 +1,32 @@
# Firefox Splice Download Helper # Splirate
This Firefox extension captures Splice sample preview context from GraphQL responses, then attempts to download full-file URLs when available. If full retrieval is unavailable, it falls back to decoding preview audio bytes (splicedd-style) and downloads decoded MP3. Allows you to download Splice samples from the browser by decrypting the preview audio.
## Features I didn't read or write the code.
- Captures sample context from Splice GraphQL responses (`preview_mp3`, sample UUID/hash). WTFPL licensed.
- Tracks observed audio sample URLs from network/probe fallback.
- Download action is orchestrated as:
1. Attempt full-file URL retrieval first.
2. If unavailable, decode scrambled preview bytes and download decoded MP3.
- Injects two controls in the target row:
- **Download**: full-first, decoded-preview fallback
- **History**: opens extension history page with past captured URLs
- Popup/history show each captured sample with source and mode hints.
## Targeted Page Element
Inline controls are injected into:
- `.playbar-playback-info-bar.playback-row-section.svelte-11gjrk3`
The inserted controls use sibling-style wrappers based on:
- `bordered-item meta-item svelte-11gjrk3`
## URL Rules
- Page scope: `*://*.splice.com/*`
- Audio sample paths include `/audio_samples/`
- Waveform files (`.wv.json`) are excluded from download candidates
## Load in Firefox (Temporary Add-on) ## Load in Firefox (Temporary Add-on)
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 a matching `*.splice.com` page 4. Visit `https://splice.com/sounds/search/samples`
## Usage ## Build Firefox Package
1. Open a page under `*.splice.com` that loads S3 audio links. 1. From the project root, run either:
2. Wait for inline `Download` and `History` controls to appear in the playback info bar. - `npm run build`
3. Click: - `npm run build:xpi`
- **Download** to run full-first retrieval, then decode fallback if needed - `bash scripts/build-firefox.sh`
- **History** to open full captured history 2. The packaged extension is created in:
4. Optionally click the extension icon to open popup for current-tab entries. - `dist/splirate-firefox-v<version>.xpi`
## Manual Verification Checklist ## Install Locally in Firefox
- [ ] On matching domain, controls appear in target playback row. 1. Build the XPI: `npm run build:xpi`
- [ ] Controls remain after SPA/Svelte rerenders. 2. Open Firefox and go to `about:debugging#/runtime/this-firefox`
- [ ] GraphQL preview context is captured for visible samples. 3. Click **Load Temporary Add-on...**
- [ ] `Download` attempts full retrieval first. 4. Select `dist/splirate-firefox-v<version>.xpi`
- [ ] When full retrieval is unavailable, fallback decoded MP3 is downloaded and playable.
- [ ] `History` opens and allows downloading older entries.
- [ ] Popup updates with captured entries for active tab.
- [ ] Non-`*.splice.com` pages do not capture/store URLs.
## Known Limitations Note: on standard Firefox, unsigned extensions are temporary via `about:debugging` and are removed on restart.
- Full-file URL retrieval depends on backend behavior and session state; it may fail for some assets.
- Fallback decode uses scrambled preview streams, so fallback output follows preview-source quality/availability.
- Signed preview URLs can expire; expired URLs require recapture.
- `saveAs: true` prompts download location each time by design.

View File

@@ -4,7 +4,8 @@ const TAB_CAPTURED = new Map();
const FULL_URL_BY_SAMPLE = new Map(); 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 SPLICE_DOMAIN_RE = /^https?:\/\/([a-z0-9-]+\.)*splice\.com(\/|$)/i; const TARGET_HOST = "splice.com";
const TARGET_PATH = "/sounds/search/samples";
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;
@@ -12,7 +13,15 @@ const HASH_RE = /^[a-f0-9]{64}$/i;
const GRAPHQL_URL = "https://surfaces-graphql.splice.com/graphql"; const GRAPHQL_URL = "https://surfaces-graphql.splice.com/graphql";
function isDomainAllowed(pageUrl) { function isDomainAllowed(pageUrl) {
return typeof pageUrl === "string" && SPLICE_DOMAIN_RE.test(pageUrl); if (typeof pageUrl !== "string") {
return false;
}
try {
const parsed = new URL(pageUrl);
return parsed.protocol === "https:" && parsed.hostname === TARGET_HOST && parsed.pathname === TARGET_PATH;
} catch (error) {
return false;
}
} }
function isS3Url(url) { function isS3Url(url) {
@@ -825,8 +834,19 @@ function handleWebRequestCapture(details) {
} }
const tabId = typeof details.tabId === "number" ? details.tabId : -1; const tabId = typeof details.tabId === "number" ? details.tabId : -1;
const merged = upsertUrl(tabId, url, "webrequest"); if (tabId < 0) {
void merged; return;
}
browser.tabs.get(tabId).then((tab) => {
const tabUrl = tab && typeof tab.url === "string" ? tab.url : "";
if (!isDomainAllowed(tabUrl)) {
return;
}
const merged = upsertUrl(tabId, url, "webrequest");
void merged;
}).catch(() => {
// Ignore tab lookup failures.
});
} }
browser.webRequest.onBeforeRequest.addListener( browser.webRequest.onBeforeRequest.addListener(

View File

@@ -2,7 +2,8 @@
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 SPLICE_DOMAIN_RE = /^https?:\/\/([a-z0-9-]+\.)*splice\.com(\/|$)/i; const TARGET_HOST = "splice.com";
const TARGET_PATH = "/sounds/search/samples";
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,7 +22,15 @@ let lastPlaybackMetaSignature = "";
let lastPlaybackMetaApplied = false; let lastPlaybackMetaApplied = false;
function isDomainAllowed(url) { function isDomainAllowed(url) {
return typeof url === "string" && SPLICE_DOMAIN_RE.test(url); if (typeof url !== "string") {
return false;
}
try {
const parsed = new URL(url);
return parsed.protocol === "https:" && parsed.hostname === TARGET_HOST && parsed.pathname === TARGET_PATH;
} catch (error) {
return false;
}
} }
function isS3Url(url) { function isS3Url(url) {
@@ -548,7 +557,7 @@ function ensureInlineStyles() {
.s3-capture-button { .s3-capture-button {
border: 1px solid rgba(255, 255, 255, 0.25); border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 4px; border-radius: 4px;
background: rgba(255, 255, 255, 0.08); background: none !important;
color: inherit; color: inherit;
cursor: pointer; cursor: pointer;
padding: 4px 8px; padding: 4px 8px;

View File

@@ -12,7 +12,15 @@ body {
} }
h1 { h1 {
margin: 0 0 8px; margin: 0;
font-size: 0;
line-height: 1;
}
.brand-logo {
height: 34px;
width: auto;
display: block;
} }
p { p {

View File

@@ -4,14 +4,15 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>S3 Audio Capture History</title> <title>Splirate History</title>
<link rel="icon" type="image/png" href="logo-single.png">
<link rel="stylesheet" href="history.css"> <link rel="stylesheet" href="history.css">
</head> </head>
<body> <body>
<main class="history-page"> <main class="history-page">
<header> <header>
<h1>S3 Audio Capture History</h1> <h1><img class="brand-logo" src="logo.png" alt="Splirate"></h1>
</header> </header>
<p id="status">Loading history...</p> <p id="status">Loading history...</p>
<ul id="historyList"></ul> <ul id="historyList"></ul>

View File

@@ -56,7 +56,7 @@ function buildItem(entry) {
const cover = document.createElement("img"); const cover = document.createElement("img");
cover.className = "history-cover-image"; cover.className = "history-cover-image";
cover.src = entry.coverImageUrl || "icons/icon-48.png"; cover.src = entry.coverImageUrl || "logo-single.png";
cover.alt = "Cover art"; cover.alt = "Cover art";
cover.loading = "lazy"; cover.loading = "lazy";
@@ -126,7 +126,7 @@ async function loadHistory() {
const rows = Array.isArray(response.history) ? response.history : []; const rows = Array.isArray(response.history) ? response.history : [];
if (!rows.length) { if (!rows.length) {
statusEl.textContent = "No captured S3 URLs yet."; statusEl.textContent = "No captured samples yet.";
return; return;
} }

BIN
logo-single.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -1,14 +1,19 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Splice Downloader", "name": "Splirate",
"version": "1.0.0", "version": "1.0.0",
"description": "Capture Splice S3 audio links on matching pages and download them.", "description": "Splirate captures Splice sample links and downloads playable audio.",
"icons": {
"48": "logo-single.png",
"96": "logo-single.png",
"128": "logo-single.png"
},
"permissions": [ "permissions": [
"tabs", "tabs",
"storage", "storage",
"downloads", "downloads",
"webRequest", "webRequest",
"*://*.splice.com/*", "https://splice.com/sounds/search/samples*",
"*://*.amazonaws.com/*", "*://*.amazonaws.com/*",
"*://*.amazonaws.com.cn/*", "*://*.amazonaws.com.cn/*",
"webNavigation" "webNavigation"
@@ -21,7 +26,7 @@
"content_scripts": [ "content_scripts": [
{ {
"matches": [ "matches": [
"*://*.splice.com/*" "https://splice.com/sounds/search/samples*"
], ],
"js": [ "js": [
"content-script.js" "content-script.js"
@@ -30,7 +35,11 @@
} }
], ],
"browser_action": { "browser_action": {
"default_title": "S3 Audio Capture", "default_title": "Splirate",
"default_icon": {
"48": "logo-single.png",
"96": "logo-single.png"
},
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"web_accessible_resources": [ "web_accessible_resources": [
@@ -41,7 +50,7 @@
], ],
"applications": { "applications": {
"gecko": { "gecko": {
"id": "s3-audio-capture@splice.local", "id": "splirate@splice.local",
"strict_min_version": "109.0" "strict_min_version": "109.0"
} }
} }

11
package.json Normal file
View File

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

View File

@@ -22,9 +22,14 @@ body {
.popup-header h1 { .popup-header h1 {
margin: 0; margin: 0;
font-size: 15px; font-size: 0;
font-weight: 600; line-height: 1;
letter-spacing: 0.01em; }
.brand-logo {
height: 30px;
width: auto;
display: block;
} }
#openHistoryBtn { #openHistoryBtn {

View File

@@ -3,13 +3,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>S3 Audio Capture</title> <title>Splirate</title>
<link rel="icon" type="image/png" href="logo-single.png">
<link rel="stylesheet" href="popup.css"> <link rel="stylesheet" href="popup.css">
</head> </head>
<body> <body>
<main class="popup"> <main class="popup">
<header class="popup-header"> <header class="popup-header">
<h1>S3 Audio Capture</h1> <h1><img class="brand-logo" src="logo.png" alt="Splirate"></h1>
<button id="openHistoryBtn" type="button">Open Full History</button> <button id="openHistoryBtn" type="button">Open Full History</button>
</header> </header>
<section> <section>

View File

@@ -68,7 +68,7 @@ function buildRow(entry) {
const cover = document.createElement("img"); const cover = document.createElement("img");
cover.className = "cover-image"; cover.className = "cover-image";
cover.src = entry.coverImageUrl || "icons/icon-48.png"; cover.src = entry.coverImageUrl || "logo-single.png";
cover.alt = "Cover art"; cover.alt = "Cover art";
cover.loading = "lazy"; cover.loading = "lazy";
@@ -149,7 +149,7 @@ async function loadCapturedUrls() {
listEl.innerHTML = ""; listEl.innerHTML = "";
const entries = Array.isArray(response.entries) ? response.entries : []; const entries = Array.isArray(response.entries) ? response.entries : [];
if (!entries.length) { if (!entries.length) {
setStatus("No captured S3 URLs for this tab yet."); setStatus("No captured samples for this tab yet.");
return; return;
} }

File diff suppressed because one or more lines are too long

42
scripts/build-firefox.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="${ROOT_DIR}/dist"
MANIFEST_PATH="${ROOT_DIR}/manifest.json"
if [[ ! -f "${MANIFEST_PATH}" ]]; then
echo "manifest.json not found at ${MANIFEST_PATH}" >&2
exit 1
fi
if ! command -v zip >/dev/null 2>&1; then
echo "zip command is required but not installed." >&2
exit 1
fi
VERSION="$(sed -n 's/^[[:space:]]*"version":[[:space:]]*"\([^"]*\)".*/\1/p' "${MANIFEST_PATH}" | head -n 1)"
if [[ -z "${VERSION}" ]]; then
echo "Unable to read version from manifest.json" >&2
exit 1
fi
ARTIFACT_NAME="splirate-firefox-v${VERSION}.xpi"
ARTIFACT_PATH="${DIST_DIR}/${ARTIFACT_NAME}"
mkdir -p "${DIST_DIR}"
rm -f "${ARTIFACT_PATH}"
cd "${ROOT_DIR}"
zip -r "${ARTIFACT_PATH}" . \
-x ".git/*" \
-x ".DS_Store" \
-x "dist/*" \
-x "scripts/*" \
-x "*.har" \
-x "*.tmp" \
-x "*.log"
echo "Built Firefox extension package:"
echo "${ARTIFACT_PATH}"

BIN
test.mp3

Binary file not shown.

BIN
test2.mp3

Binary file not shown.