The Problem
Streaming platforms have changed how we consume music — but not always for the better. Artists get fractions of a cent per stream. Listeners don't truly own anything. And the platforms themselves? They sit on poorly secured APIs while charging premium prices for basic features like offline listening and high-quality audio.
I built Sleepy MP3 Downloader as a browser plugin to prove a point: the way we access digital music is fundamentally broken, and the technical barriers these platforms put up are paper-thin.
Architecture Overview
The plugin is a Manifest V3 Chrome extension with three main layers:
- Content Script — Injected directly into SoundCloud and Bandcamp pages, manipulating the DOM and intercepting data
- Service Worker — Background process that intercepts network requests, bypasses CORS, and manages state
- Companion API — A local server that handles audio conversion to 320kbps MP3
The manifest declares broad permissions across both platforms:
{
"manifest_version": 3,
"name": "Sleepy MP3 Downloader",
"permissions": [
"activeTab", "scripting", "downloads",
"storage", "declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
"webRequest"
],
"host_permissions": [
"https://soundcloud.com/*",
"https://*.bandcamp.com/*",
"*://api-v2.soundcloud.com/*",
"https://t4.bcbits.com/*"
]
}
Method 1: Intercepting SoundCloud's Client ID
SoundCloud's API requires a client_id for authentication. The problem? Every single API request the site makes broadcasts this key in plain URL parameters. The service worker passively intercepts it:
// service_worker.js — intercept SoundCloud client_id
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
const url = new URL(details.url);
const clientId = url.searchParams.get("client_id");
if (clientId) {
chrome.storage.local.set({ soundcloudClientId: clientId });
}
},
{ urls: ["*://api-v2.soundcloud.com/*"] }
);
The content script then picks this up and stores it locally for all future API calls:
// content.js — retrieve intercepted client_id
chrome.storage.local.get("soundcloudClientId", (result) => {
const clientId = result.soundcloudClientId;
if (clientId) {
localStorage.setItem("soundcloudClientId", clientId);
}
});
const out = localStorage.getItem("soundcloudClientId");
const SOUNDCLOUD_CLIENT_ID = "client_id=" + out;
With this single key, we have full access to SoundCloud's resolve API — which can look up any track from a URL and return its raw audio streams.
Method 2: SoundCloud's Resolve API
SoundCloud has a remarkably useful (and poorly protected) URL resolver. Given any track or playlist URL, it returns the full metadata and transcoding streams:
const SOUNDCLOUD_API_URL = "https://api-v2.soundcloud.com/resolve?url=";
// Resolve any track URL to get metadata + audio streams
const endUrl = SOUNDCLOUD_API_URL + trackUrl + "&" + SOUNDCLOUD_CLIENT_ID;
const response = await fetch(endUrl);
const data = await response.json();
// Find the progressive (direct download) audio stream
function GetStreamURL(data, clientId) {
const progressiveTranscoding = data.media.transcodings.find(
transcoding => transcoding.format.protocol === "progressive"
);
return progressiveTranscoding.url + "?" + clientId;
}
The response contains everything: title, artist, album, genre, artwork URLs, and most importantly — direct links to the audio transcoding streams. No authentication beyond the leaked client ID.
Method 3: DOM Injection on Bandcamp
Bandcamp takes a different approach. The audio data is embedded directly in the page's HTML inside a script tag. The content script parses it straight from the DOM:
// Find the script tag containing track data
const scriptTag = Array.from(document.querySelectorAll('script'))
.find(s => s.src.startsWith(
'https://s4.bcbits.com/client-bundle/1/trackpipe/tralbum_head-'
));
const tralbumData = scriptTag.getAttribute('data-tralbum');
const parsedData = JSON.parse(
tralbumData
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/'/g, "'")
);
// Each track has a direct MP3 link
tracks = parsedData.trackinfo;
const mp3Link = tracks[index].file["mp3-128"];
The track data — including direct MP3-128 stream URLs — is just sitting there in the HTML, entity-encoded in a data attribute. No API call needed.
Method 4: CORS Bypass via Service Worker
Browsers enforce CORS to prevent cross-origin requests. But service workers in Chrome extensions operate outside this restriction. Audio fetches that would fail in the content script get routed through the background:
// service_worker.js — bypass CORS for audio fetching
if (request.action === "fetchAudio") {
fetch(request.url)
.then(response => response.blob())
.then(blob => {
const reader = new FileReader();
reader.onloadend = () => {
sendResponse({ success: true, dataUrl: reader.result });
};
reader.readAsDataURL(blob);
});
return true; // Keep message channel open for async response
}
For Bandcamp specifically, the plugin also uses declarative net request rules to strip CORS headers entirely:
// rules.json — modify CORS headers on Bandcamp CDN
[{
"id": 1,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{ "header": "Access-Control-Allow-Origin",
"operation": "set", "value": "*" }
]
},
"condition": {
"urlFilter": "https://t4.bcbits.com/*",
"resourceTypes": ["xmlhttprequest"]
}
}]
Method 5: Dynamic UI Injection
The plugin uses MutationObservers to watch for new DOM elements and inject download buttons in real-time — even as SoundCloud lazy-loads content:
const observeTrackItems = () => {
const observer = new MutationObserver(() => {
const soundcloudTargets = document.querySelectorAll(
'.trackItem, .systemPlaylistBannerItem'
);
const bandcampTargets = document.querySelectorAll(
'td.download-col, div.digitaldescription'
);
soundcloudTargets.forEach(target => {
if (!target.querySelector('.soundcloud-button')) {
const btn = createSoundCloudDownloadButton(target);
target.appendChild(btn);
}
});
bandcampTargets.forEach(target => {
if (!target.querySelector('.bandcamp-button')) {
const btn = createBandCampDownloadButton();
target.appendChild(btn);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
};
On Bandcamp, buttons even dynamically match the artist's page theme by parsing the custom CSS design rules:
// Pull colors from Bandcamp's custom theme
const styleElement = document.querySelector(
'style#custom-design-rules-style'
);
const designData = JSON.parse(
styleElement.getAttribute('data-design')
);
backgroundColor = "#" + designData.link_color;
textColor = "#" + designData.bg_color;
Method 6: Metadata Tagging & Bulk Downloads
Every downloaded track gets properly tagged with ID3 metadata — title, artist, album, genre, and cover art — using the browser-id3-writer library:
function tagAudio({ audioBuffer, title, album, artist, genre, coverImage }) {
const writer = new ID3Writer(audioBuffer);
writer
.setFrame('TIT2', title)
.setFrame('TALB', album)
.setFrame('TPE1', [artist])
.setFrame('TCON', [genre])
.setFrame('APIC', {
type: 3,
data: coverImage,
description: 'Cover',
});
writer.addTag();
return writer.getBlob();
}
For playlists, the plugin auto-scrolls the page to force SoundCloud to lazy-load all tracks, downloads each one sequentially, tags them, and bundles everything into a ZIP file using JSZip:
const zip = new JSZip();
for (const track of tracks) {
// ... fetch, convert, tag each track
zip.file(trackTitle + ".mp3", taggedBlob);
}
zip.generateAsync({ type: "blob" }).then(content => {
const zipUrl = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = zipUrl;
a.setAttribute('download', '[SLEEPY_DOWNLOADER] - Tracks.zip');
a.click();
});
What This Proves
Building Sleepy MP3 Downloader wasn't about piracy — it was about exposing how fragile these platforms' security really is:
- Client IDs broadcast in every request — SoundCloud leaks its own API keys in plain sight
- Audio streams accessible without authentication — once you have a client ID, every track is one API call away
- Track data embedded in page HTML — Bandcamp puts direct MP3 links in the DOM
- CORS as the only barrier — easily bypassed by any browser extension
These platforms charge users for "premium" access to content that is technically accessible to anyone who opens DevTools. The real question isn't whether this can be done — it's why these companies haven't built better systems for both artists and listeners.
Music deserves better infrastructure. Artists deserve fair compensation. And listeners deserve to actually own the music they love.
Download the source on GitHub.