Build scripts
This commit is contained in:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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
13
LICENSE.md
Normal 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.
|
||||||
69
README.md
69
README.md
@@ -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.
|
|
||||||
@@ -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;
|
||||||
|
if (tabId < 0) {
|
||||||
|
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");
|
const merged = upsertUrl(tabId, url, "webrequest");
|
||||||
void merged;
|
void merged;
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore tab lookup failures.
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
browser.webRequest.onBeforeRequest.addListener(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
10
history.css
10
history.css
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
BIN
logo-single.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -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
11
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
popup.css
11
popup.css
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
4
popup.js
4
popup.js
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
262
sample.har
262
sample.har
File diff suppressed because one or more lines are too long
42
scripts/build-firefox.sh
Executable file
42
scripts/build-firefox.sh
Executable 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}"
|
||||||
Reference in New Issue
Block a user