
Extract high-quality audio from any video. One click. 1000+ platforms. View repo
The Evolution
Started as a web app for extracting audio from YouTube links. Worked well, but required copy-pasting URLs and leaving your current tab.
The Chrome extension eliminates that friction. One click from any video page.
What I Built
A Chrome extension that:
- Detects video URLs automatically on YouTube, Vimeo, TikTok, etc.
- Extracts MP3 audio via the Cobalt API
- Tracks download history with timestamps
- Provides format/quality options (MP3/WAV/OGG, 128/256/320 kbps)
- Integrates with right-click context menu
- Auto-downloads files directly to your Downloads folder
Stack: TypeScript, React 19, Webpack 5, Chrome Extensions API (Manifest V3)
Key Learnings
1. Manifest V3 is a different beast
Service workers replace background pages. No DOM, no XMLHttpRequest, stricter CSP. The architecture shift from web app to extension required rethinking everything.
2. Message passing is your lifeline
Popup → Service Worker → Content Script. Everything communicates via chrome.runtime.sendMessage. Type-safe message contracts saved hours of debugging.
3. chrome.downloads beats fetch every time
Initially tried downloading files in the popup. Terrible UX. Delegating to chrome.downloads.download() in the service worker is instant and native.
4. Storage APIs have quirks
chrome.storage.local has a 5MB limit. Had to cap history at 100 items and clean up old entries. IndexedDB is overkill for this use case.
5. Cobalt API > yt-dlp for extensions
The web app used yt-dlp locally. Extensions can't run shell commands. Cobalt API handles 1000+ platforms via HTTP. Same result, simpler architecture.
6. Webpack config matters
React in an extension needs careful bundling. Separate entry points for background, popup, options, content scripts. MiniCssExtractPlugin for styles. CopyPlugin for assets.
7. Users expect instant feedback
Loading states, progress indicators, success messages. The extension lives in a 600x600px popup. Every pixel counts for UX.
Architecture
Service Worker (background.js):
- Handles all API calls to Cobalt
- Manages chrome.downloads for file downloads
- Stores history in chrome.storage.local
- Routes messages from popup/content scripts
Popup UI (popup.html):
- React 19 with custom hooks (useExtract, useHistory, useSettings)
- Two-tab interface: Extract | History
- Auto-detects video URLs from active tab
- Dark theme with CSS variables from web app
Content Script:
- Detects MP3 links in page DOM
- Monitors for dynamically loaded content (MutationObserver)
- Sends video URLs to service worker
Options Page:
- Settings UI for format, bitrate, auto-download
- Persists to chrome.storage.local
Build Process
cd chrome-extension
npm install
npm run build # Webpack bundles to dist/
npm run package # Creates extension.zip
Output:
- background.js (5.7 KB)
- popup.js + popup.css (199 KB)
- options.js + options.css (199 KB)
- content-script.js (828 bytes)
Total: ~405 KB minified.
What's Next
Potential improvements:
- Firefox Add-on version (Manifest V2/V3 compatibility)
- Batch download queue
- Playlist support (extract entire YouTube playlists)
- Safari extension (requires different build)
- Format conversion options beyond audio
Reflections
This project taught me more about browser extension architecture than reading docs ever could. The constraints force you to think differently:
- No server. No database. Just service workers and storage APIs.
- No hot reload. Manual reload on chrome://extensions every change.
- Strict permissions model. Every API must be justified.
The result? A tool I actually use daily. From "paste URL in web app" to "right-click → Extract Audio" is a massive UX improvement.
Building for yourself reveals what actually matters.
Links
- Repo: https://github.com/sergiopesch/mp3
- Web App: https://mp3-extractor.vercel.app
- Cobalt API: https://cobalt.tools