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).
|
||||
- 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
|
||||
WTFPL licensed.
|
||||
|
||||
## Load in Firefox (Temporary Add-on)
|
||||
|
||||
1. Open `about:debugging#/runtime/this-firefox`
|
||||
2. Click **Load Temporary Add-on...**
|
||||
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.
|
||||
2. Wait for inline `Download` and `History` controls to appear in the playback info bar.
|
||||
3. Click:
|
||||
- **Download** to run full-first retrieval, then decode fallback if needed
|
||||
- **History** to open full captured history
|
||||
4. Optionally click the extension icon to open popup for current-tab entries.
|
||||
1. From the project root, run either:
|
||||
- `npm run build`
|
||||
- `npm run build:xpi`
|
||||
- `bash scripts/build-firefox.sh`
|
||||
2. The packaged extension is created in:
|
||||
- `dist/splirate-firefox-v<version>.xpi`
|
||||
|
||||
## Manual Verification Checklist
|
||||
## Install Locally in Firefox
|
||||
|
||||
- [ ] On matching domain, controls appear in target playback row.
|
||||
- [ ] Controls remain after SPA/Svelte rerenders.
|
||||
- [ ] GraphQL preview context is captured for visible samples.
|
||||
- [ ] `Download` attempts full retrieval first.
|
||||
- [ ] 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.
|
||||
1. Build the XPI: `npm run build:xpi`
|
||||
2. Open Firefox and go to `about:debugging#/runtime/this-firefox`
|
||||
3. Click **Load Temporary Add-on...**
|
||||
4. Select `dist/splirate-firefox-v<version>.xpi`
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- 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.
|
||||
Note: on standard Firefox, unsigned extensions are temporary via `about:debugging` and are removed on restart.
|
||||
@@ -4,7 +4,8 @@ const TAB_CAPTURED = new Map();
|
||||
const FULL_URL_BY_SAMPLE = new Map();
|
||||
const HISTORY_KEY = "capturedS3History";
|
||||
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 WAVEFORM_PATH_RE = /\.wv\.json$/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";
|
||||
|
||||
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) {
|
||||
@@ -825,8 +834,19 @@ function handleWebRequestCapture(details) {
|
||||
}
|
||||
|
||||
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");
|
||||
void merged;
|
||||
}).catch(() => {
|
||||
// Ignore tab lookup failures.
|
||||
});
|
||||
}
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
const TARGET_SELECTOR = ".playbar-playback-info-bar.playback-row-section.svelte-11gjrk3";
|
||||
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";
|
||||
let knownUrls = new Set();
|
||||
let knownPerformanceUrls = new Set();
|
||||
@@ -21,7 +22,15 @@ let lastPlaybackMetaSignature = "";
|
||||
let lastPlaybackMetaApplied = false;
|
||||
|
||||
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) {
|
||||
@@ -548,7 +557,7 @@ function ensureInlineStyles() {
|
||||
.s3-capture-button {
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: none !important;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
|
||||
10
history.css
10
history.css
@@ -12,7 +12,15 @@ body {
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
margin: 0;
|
||||
font-size: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
height: 34px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="history-page">
|
||||
<header>
|
||||
<h1>S3 Audio Capture History</h1>
|
||||
<h1><img class="brand-logo" src="logo.png" alt="Splirate"></h1>
|
||||
</header>
|
||||
<p id="status">Loading history...</p>
|
||||
<ul id="historyList"></ul>
|
||||
|
||||
@@ -56,7 +56,7 @@ function buildItem(entry) {
|
||||
|
||||
const cover = document.createElement("img");
|
||||
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.loading = "lazy";
|
||||
|
||||
@@ -126,7 +126,7 @@ async function loadHistory() {
|
||||
|
||||
const rows = Array.isArray(response.history) ? response.history : [];
|
||||
if (!rows.length) {
|
||||
statusEl.textContent = "No captured S3 URLs yet.";
|
||||
statusEl.textContent = "No captured samples yet.";
|
||||
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,
|
||||
"name": "Splice Downloader",
|
||||
"name": "Splirate",
|
||||
"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": [
|
||||
"tabs",
|
||||
"storage",
|
||||
"downloads",
|
||||
"webRequest",
|
||||
"*://*.splice.com/*",
|
||||
"https://splice.com/sounds/search/samples*",
|
||||
"*://*.amazonaws.com/*",
|
||||
"*://*.amazonaws.com.cn/*",
|
||||
"webNavigation"
|
||||
@@ -21,7 +26,7 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"*://*.splice.com/*"
|
||||
"https://splice.com/sounds/search/samples*"
|
||||
],
|
||||
"js": [
|
||||
"content-script.js"
|
||||
@@ -30,7 +35,11 @@
|
||||
}
|
||||
],
|
||||
"browser_action": {
|
||||
"default_title": "S3 Audio Capture",
|
||||
"default_title": "Splirate",
|
||||
"default_icon": {
|
||||
"48": "logo-single.png",
|
||||
"96": "logo-single.png"
|
||||
},
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
@@ -41,7 +50,7 @@
|
||||
],
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "s3-audio-capture@splice.local",
|
||||
"id": "splirate@splice.local",
|
||||
"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 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
font-size: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
height: 30px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#openHistoryBtn {
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<main class="popup">
|
||||
<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>
|
||||
</header>
|
||||
<section>
|
||||
|
||||
4
popup.js
4
popup.js
@@ -68,7 +68,7 @@ function buildRow(entry) {
|
||||
|
||||
const cover = document.createElement("img");
|
||||
cover.className = "cover-image";
|
||||
cover.src = entry.coverImageUrl || "icons/icon-48.png";
|
||||
cover.src = entry.coverImageUrl || "logo-single.png";
|
||||
cover.alt = "Cover art";
|
||||
cover.loading = "lazy";
|
||||
|
||||
@@ -149,7 +149,7 @@ async function loadCapturedUrls() {
|
||||
listEl.innerHTML = "";
|
||||
const entries = Array.isArray(response.entries) ? response.entries : [];
|
||||
if (!entries.length) {
|
||||
setStatus("No captured S3 URLs for this tab yet.");
|
||||
setStatus("No captured samples for this tab yet.");
|
||||
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