Add SDB Audio Extractor extension with options and popup UI

- Implemented manifest.json for extension configuration
- Created options.html and options.js for user settings management
- Developed popup.html and popup.js for audio extraction functionality
- Added icons for the extension (icon16.png, icon48.png)
- Integrated API token and base URL configuration with validation
- Implemented audio extraction from YouTube videos with error handling
This commit is contained in:
JSC
2025-09-20 20:54:12 +02:00
commit cb607760ce
12 changed files with 1069 additions and 0 deletions

166
README.md Normal file
View File

@@ -0,0 +1,166 @@
# SDB Audio Extractor Chrome Extension
A Chrome extension that allows you to extract audio from YouTube videos directly to your SDB soundboard application.
## Features
- 🎵 Extract audio from YouTube videos with one click
- 🔐 Secure API token authentication
- ⚡ Immediate response - extraction happens in the background
- 📝 Right-click context menu support
- ⚙️ Easy configuration through options page
- 🔄 Real-time status updates
## Installation
### Method 1: Developer Mode (Recommended)
1. Download or clone this extension to your computer
2. Open Chrome and navigate to `chrome://extensions/`
3. Enable "Developer mode" in the top right corner
4. Click "Load unpacked" and select the `chrome-extension` folder
5. The extension should now appear in your extensions list
### Method 2: Pack and Install
1. In Chrome, go to `chrome://extensions/`
2. Enable "Developer mode"
3. Click "Pack extension"
4. Select the `chrome-extension` folder as the root directory
5. Click "Pack Extension" to create a `.crx` file
6. Install the `.crx` file by dragging it to the extensions page
## Configuration
Before using the extension, you need to configure your API credentials:
1. Click the extension icon in Chrome's toolbar
2. Click "Configure API Token" or right-click the extension and select "Options"
3. Follow these steps to get your API token:
- Open your SDB soundboard application (usually at http://192-168-100-199.sslip.io)
- Log into your account
- Navigate to your user settings or profile page
- Generate a new API token
- Copy the token
4. Paste the token in the "API Token" field
5. Verify the "API Base URL" (default: http://192-168-100-199.sslip.io)
6. Click "Test Connection" to verify everything works
7. Click "Save Settings"
## Usage
### Method 1: Extension Popup
1. Navigate to any YouTube video page
2. Click the SDB Audio Extractor extension icon in your toolbar
3. The popup will show the current video title and URL
4. Click "Extract Audio" to start the extraction
5. The extension will send the request to your SDB backend
6. Check your soundboard application for the extracted audio
### Method 2: Right-Click Context Menu
1. Navigate to any YouTube video page
2. Right-click anywhere on the page
3. Select "Extract Audio with SDB" from the context menu
4. You'll receive a notification when the extraction starts
## Requirements
- Chrome browser (version 88+)
- SDB soundboard backend running and accessible
- Valid API token from your SDB user account
- Internet connection for YouTube access
## File Structure
```
chrome-extension/
├── manifest.json # Extension configuration
├── popup.html # Main popup interface
├── popup.js # Popup functionality
├── options.html # Configuration page
├── options.js # Options page functionality
├── content.js # YouTube page integration
├── background.js # Background service worker
├── icon16.png # Extension icon (16x16)
├── icon48.png # Extension icon (48x48)
├── icon128.png # Extension icon (128x128)
└── README.md # This file
```
## Permissions
The extension requires the following permissions:
- `activeTab`: To access the current YouTube tab
- `storage`: To save your API configuration
- `https://www.youtube.com/*`: To work on YouTube pages
- `http://localhost:8000/*`: To communicate with your SDB backend
## Troubleshooting
### "API token not configured" Error
- Make sure you've configured your API token in the extension options
- Verify the token is correctly copied from your SDB account
### "Cannot connect to API server" Error
- Check that your SDB backend is running (usually on port 8000)
- Verify the API Base URL in the extension options
- Make sure there are no firewall issues blocking localhost connections
### "Authentication failed" Error
- Your API token might be expired or invalid
- Generate a new API token in your SDB account
- Update the token in the extension options
### Extension not working on YouTube
- Make sure you're on a YouTube video page (youtube.com/watch?v=...)
- Try refreshing the page and clicking the extension again
- Check the browser console for any error messages
### Right-click menu not appearing
- The context menu only appears on YouTube video pages
- Make sure the extension is enabled and properly loaded
- Try reloading the extension in chrome://extensions/
## Development
To modify or extend this extension:
1. Make your changes to the source files
2. Go to `chrome://extensions/`
3. Click the refresh button on the SDB Audio Extractor extension
4. Test your changes
### Adding New Features
- Modify `manifest.json` for new permissions or files
- Update `popup.html` and `popup.js` for UI changes
- Modify `content.js` for YouTube page interaction
- Update `background.js` for background functionality
## Security Notes
- Your API token is stored locally in Chrome's sync storage
- The extension only communicates with YouTube and your configured SDB backend
- No data is sent to third-party services
- Always use HTTPS in production environments
## Support
If you encounter issues:
1. Check the troubleshooting section above
2. Verify your SDB backend is running and accessible
3. Check the browser console for error messages
4. Ensure your API token is valid and has proper permissions
## License
This extension is part of the SDB soundboard project.

150
background.js Normal file
View File

@@ -0,0 +1,150 @@
// Background service worker
chrome.runtime.onInstalled.addListener(() => {
console.log('SDB Audio Extractor extension installed');
// Create context menu
chrome.contextMenus.create({
id: 'extractAudio',
title: 'Extract Audio with SDB',
contexts: ['page'],
documentUrlPatterns: ['https://www.youtube.com/watch*']
});
});
// Function to clean YouTube URL
function getCleanVideoUrl(url) {
const match = url.match(/[?&]v=([^&]+)/);
if (match) {
return `https://www.youtube.com/watch?v=${match[1]}`;
}
return url; // Return original if we can't extract video ID
}
// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === 'extractAudio') {
// Get API configuration
const result = await chrome.storage.sync.get(['apiToken', 'apiBaseUrl']);
const apiToken = result.apiToken;
const apiBaseUrl = result.apiBaseUrl || 'http://192-168-100-199.sslip.io';
if (!apiToken) {
// Open options page if no token configured
chrome.runtime.openOptionsPage();
return;
}
// Get video information from content script for notification
let videoTitle = null;
try {
const videoInfo = await chrome.tabs.sendMessage(tab.id, { action: 'getVideoInfo' });
if (videoInfo && videoInfo.title) {
videoTitle = videoInfo.title;
}
} catch (error) {
console.log('Could not get video info from content script:', error);
}
// Send extraction request
try {
// Use clean URL for the API request
const cleanUrl = getCleanVideoUrl(tab.url);
const response = await fetch(`${apiBaseUrl}/api/v1/extractions/?url=${encodeURIComponent(cleanUrl)}`, {
method: 'POST',
headers: {
'API-TOKEN': apiToken
}
});
if (response.ok) {
// Show success notification
const notificationMessage = videoTitle
? `Audio extraction started for "${videoTitle}"!`
: 'Audio extraction started successfully!';
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon48.png',
title: 'SDB Audio Extractor',
message: notificationMessage
});
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
console.error('Extraction error:', error);
let errorMessage = 'Unknown error occurred';
if (error.message) {
errorMessage = error.message;
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
errorMessage = 'Cannot connect to API server';
} else if (typeof error === 'string') {
errorMessage = error;
}
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon48.png',
title: 'SDB Audio Extractor',
message: `Failed to extract audio: ${errorMessage}`
});
}
}
});
// Handle messages from content scripts or popup
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
console.log('Background received message:', request);
if (request.action === 'extractAudio') {
// Handle extraction request from content script if needed
handleExtraction(request.url).then(sendResponse);
return true; // Keep message channel open for async response
}
});
async function handleExtraction(url) {
try {
const result = await chrome.storage.sync.get(['apiToken', 'apiBaseUrl']);
const apiToken = result.apiToken;
const apiBaseUrl = result.apiBaseUrl || 'http://192-168-100-199.sslip.io';
if (!apiToken) {
throw new Error('API token not configured');
}
// Use clean URL for the API request
const cleanUrl = getCleanVideoUrl(url);
const response = await fetch(`${apiBaseUrl}/api/v1/extractions/?url=${encodeURIComponent(cleanUrl)}`, {
method: 'POST',
headers: {
'API-TOKEN': apiToken
}
});
if (!response.ok) {
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = await response.json();
if (errorData.detail) {
errorMessage = errorData.detail;
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (typeof errorData === 'string') {
errorMessage = errorData;
}
} catch (e) {
errorMessage = response.statusText || errorMessage;
}
throw new Error(errorMessage);
}
return await response.json();
} catch (error) {
console.error('Extraction error:', error);
throw error;
}
}

58
content.js Normal file
View File

@@ -0,0 +1,58 @@
// Content script for YouTube pages
(function() {
'use strict';
// Function to get video information
function getVideoInfo() {
const titleElement = document.querySelector('h1.ytd-watch-metadata yt-formatted-string') ||
document.querySelector('h1.ytd-video-primary-info-renderer .title') ||
document.querySelector('#title h1') ||
document.querySelector('h1');
const title = titleElement ? titleElement.textContent.trim() : document.title;
const currentUrl = window.location.href;
const cleanUrl = getCleanVideoUrl(currentUrl);
return {
title: title,
url: cleanUrl,
videoId: getVideoId(currentUrl)
};
}
// Extract video ID from URL
function getVideoId(url) {
const match = url.match(/[?&]v=([^&]+)/);
return match ? match[1] : null;
}
// Clean YouTube URL to only include the video ID
function getCleanVideoUrl(url) {
const videoId = getVideoId(url);
if (videoId) {
return `https://www.youtube.com/watch?v=${videoId}`;
}
return url; // Return original if we can't extract video ID
}
// Listen for messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getVideoInfo') {
const videoInfo = getVideoInfo();
sendResponse(videoInfo);
}
return true; // Keep message channel open for async response
});
// Optional: Monitor for navigation changes on YouTube (SPA)
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
// URL changed, video info might have changed
console.log('YouTube navigation detected');
}
}).observe(document, { subtree: true, childList: true });
})();

25
icon.svg Normal file
View File

@@ -0,0 +1,25 @@
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="24" cy="24" r="22" fill="#ff0000"/>
<!-- Musical note -->
<g fill="#ffffff">
<!-- Note head -->
<ellipse cx="14" cy="33" rx="4" ry="3"/>
<!-- Note stem -->
<rect x="18" y="14" width="2" height="19"/>
<!-- Note flag -->
<path d="M20 14 Q28 10 26 18 L20 16 Z"/>
</g>
<!-- Download arrow -->
<g fill="#ffffff">
<!-- Arrow shaft -->
<rect x="30" y="20" width="2" height="8"/>
<!-- Arrow head -->
<path d="M27 26 L31 32 L35 26 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 622 B

BIN
icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

37
manifest.json Normal file
View File

@@ -0,0 +1,37 @@
{
"manifest_version": 3,
"name": "SDB Audio Extractor",
"version": "1.0",
"description": "Extract audio from YouTube videos to your SDB soundboard",
"permissions": [
"activeTab",
"storage",
"contextMenus",
"notifications"
],
"host_permissions": [
"https://www.youtube.com/*",
"http://localhost:8000/*",
"http://192-168-100-199.sslip.io/*"
],
"action": {
"default_popup": "popup.html",
"default_title": "Extract Audio"
},
"content_scripts": [
{
"matches": ["https://www.youtube.com/*"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"background": {
"service_worker": "background.js"
},
"options_page": "options.html",
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}

194
options.html Normal file
View File

@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SDB Audio Extractor - Options</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
line-height: 1.6;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.logo {
font-size: 32px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 16px;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
input[type="text"], input[type="url"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
}
input[type="text"]:focus, input[type="url"]:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.help-text {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.save-btn {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
margin-right: 10px;
}
.save-btn:hover {
background: #0056b3;
}
.save-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.test-btn {
background: #28a745;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
.test-btn:hover {
background: #1e7e34;
}
.test-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.status {
margin: 20px 0;
padding: 12px;
border-radius: 6px;
font-size: 14px;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.loading {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.instructions {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
border-left: 4px solid #007bff;
}
.instructions h3 {
margin-top: 0;
color: #333;
}
.instructions ol {
margin: 15px 0;
padding-left: 20px;
}
.instructions li {
margin-bottom: 8px;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">🎵 SDB Audio Extractor</div>
<div class="subtitle">Configuration</div>
</div>
<div class="instructions">
<h3>Setup Instructions</h3>
<ol>
<li>Log into your SDB soundboard application</li>
<li>Go to your account settings</li>
<li>Generate an API token</li>
<li>Copy the token and paste it below</li>
<li>Make sure your API base URL is correct (usually http://192-168-100-199.sslip.io)</li>
<li>Click "Test Connection" to verify everything works</li>
</ol>
</div>
<form id="options-form">
<div class="form-group">
<label for="api-token">API Token *</label>
<input type="text" id="api-token" placeholder="Enter your SDB API token" required>
<div class="help-text">This token is used to authenticate with your SDB backend API</div>
</div>
<div class="form-group">
<label for="api-base-url">API Base URL</label>
<input type="url" id="api-base-url" placeholder="http://192-168-100-199.sslip.io">
<div class="help-text">The base URL of your SDB backend API (default: http://192-168-100-199.sslip.io)</div>
</div>
<button type="submit" class="save-btn" id="save-btn">Save Settings</button>
<button type="button" class="test-btn" id="test-btn">Test Connection</button>
</form>
<div id="status" class="status hidden"></div>
<script src="options.js"></script>
</body>
</html>

124
options.js Normal file
View File

@@ -0,0 +1,124 @@
document.addEventListener('DOMContentLoaded', async () => {
const form = document.getElementById('options-form');
const apiTokenInput = document.getElementById('api-token');
const apiBaseUrlInput = document.getElementById('api-base-url');
const saveBtn = document.getElementById('save-btn');
const testBtn = document.getElementById('test-btn');
const status = document.getElementById('status');
// Load saved settings
const result = await chrome.storage.sync.get(['apiToken', 'apiBaseUrl']);
if (result.apiToken) {
apiTokenInput.value = result.apiToken;
}
if (result.apiBaseUrl) {
apiBaseUrlInput.value = result.apiBaseUrl;
} else {
apiBaseUrlInput.value = 'http://192-168-100-199.sslip.io';
}
// Save settings
form.addEventListener('submit', async (e) => {
e.preventDefault();
const apiToken = apiTokenInput.value.trim();
const apiBaseUrl = apiBaseUrlInput.value.trim() || 'http://192-168-100-199.sslip.io';
if (!apiToken) {
showStatus('Please enter an API token', 'error');
return;
}
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
try {
await chrome.storage.sync.set({
apiToken: apiToken,
apiBaseUrl: apiBaseUrl
});
showStatus('Settings saved successfully!', 'success');
// Auto-test connection after saving
setTimeout(testConnection, 1000);
} catch (error) {
console.error('Error saving settings:', error);
showStatus('Failed to save settings', 'error');
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'Save Settings';
}
});
// Test connection
testBtn.addEventListener('click', testConnection);
async function testConnection() {
const apiToken = apiTokenInput.value.trim();
const apiBaseUrl = apiBaseUrlInput.value.trim() || 'http://192-168-100-199.sslip.io';
if (!apiToken) {
showStatus('Please enter an API token first', 'error');
return;
}
testBtn.disabled = true;
testBtn.textContent = 'Testing...';
showStatus('Testing connection...', 'loading');
try {
// Test with health endpoint first
const healthResponse = await fetch(`${apiBaseUrl}/api/v1/health`, {
method: 'GET'
});
if (!healthResponse.ok) {
throw new Error(`Health check failed: HTTP ${healthResponse.status}`);
}
// Test authentication with a simple API call
const authResponse = await fetch(`${apiBaseUrl}/api/v1/extractions/`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiToken}`
}
});
if (authResponse.status === 401) {
throw new Error('Invalid API token - authentication failed');
} else if (authResponse.status === 403) {
throw new Error('API token does not have required permissions');
} else if (!authResponse.ok) {
throw new Error(`API test failed: HTTP ${authResponse.status}`);
}
showStatus('✅ Connection successful! Your settings are working correctly.', 'success');
} catch (error) {
console.error('Connection test error:', error);
if (error.message.includes('fetch')) {
showStatus('❌ Cannot connect to API server. Check if the backend is running and the URL is correct.', 'error');
} else {
showStatus(`❌ Connection failed: ${error.message}`, 'error');
}
} finally {
testBtn.disabled = false;
testBtn.textContent = 'Test Connection';
}
}
function showStatus(message, type) {
status.textContent = message;
status.className = `status ${type}`;
status.classList.remove('hidden');
if (type === 'success') {
setTimeout(() => {
status.classList.add('hidden');
}, 5000);
}
}
});

168
popup.html Normal file
View File

@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
width: 350px;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.subtitle {
color: #666;
font-size: 14px;
}
.video-info {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.video-title {
font-weight: 500;
margin-bottom: 8px;
line-height: 1.4;
}
.video-url {
font-size: 12px;
color: #666;
word-break: break-all;
}
.extract-btn {
width: 100%;
background: #ff0000;
color: white;
border: none;
padding: 12px;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
margin-bottom: 15px;
}
.extract-btn:hover {
background: #cc0000;
}
.extract-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.status {
text-align: center;
margin: 15px 0;
padding: 10px;
border-radius: 6px;
font-size: 14px;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.loading {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.config-section {
border-top: 1px solid #eee;
padding-top: 15px;
margin-top: 15px;
}
.config-btn {
width: 100%;
background: #007bff;
color: white;
border: none;
padding: 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.config-btn:hover {
background: #0056b3;
}
.warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
padding: 10px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 15px;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">🎵 SDB Extractor</div>
<div class="subtitle">Extract audio from YouTube videos</div>
</div>
<div id="warning" class="warning hidden">
Please configure your API token in the extension options before extracting audio.
</div>
<div id="not-youtube" class="warning hidden">
This extension only works on YouTube video pages.
</div>
<div id="video-section" class="hidden">
<div class="video-info">
<div class="video-title" id="video-title">Loading...</div>
<div class="video-url" id="video-url"></div>
</div>
<button id="extract-btn" class="extract-btn">
Extract Audio
</button>
</div>
<div id="status" class="status hidden"></div>
<div class="config-section">
<button id="config-btn" class="config-btn">
Configure API Token
</button>
</div>
<script src="popup.js"></script>
</body>
</html>

147
popup.js Normal file
View File

@@ -0,0 +1,147 @@
document.addEventListener('DOMContentLoaded', async () => {
const extractBtn = document.getElementById('extract-btn');
const configBtn = document.getElementById('config-btn');
const status = document.getElementById('status');
const warning = document.getElementById('warning');
const notYoutube = document.getElementById('not-youtube');
const videoSection = document.getElementById('video-section');
const videoTitle = document.getElementById('video-title');
const videoUrl = document.getElementById('video-url');
// Check if we're on YouTube
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab.url || !tab.url.includes('youtube.com/watch')) {
notYoutube.classList.remove('hidden');
return;
}
// Check API token configuration
const result = await chrome.storage.sync.get(['apiToken', 'apiBaseUrl']);
const apiToken = result.apiToken;
const apiBaseUrl = result.apiBaseUrl || 'http://192-168-100-199.sslip.io';
if (!apiToken) {
warning.classList.remove('hidden');
extractBtn.disabled = true;
} else {
videoSection.classList.remove('hidden');
}
// Get video information from content script
let currentVideoInfo = null;
try {
const response = await chrome.tabs.sendMessage(tab.id, { action: 'getVideoInfo' });
if (response && response.title && response.url) {
currentVideoInfo = response;
videoTitle.textContent = response.title;
videoUrl.textContent = response.url;
} else {
videoTitle.textContent = 'Video information not available';
videoUrl.textContent = tab.url;
}
} catch (error) {
console.error('Error getting video info:', error);
videoTitle.textContent = 'Video information not available';
videoUrl.textContent = tab.url;
}
// Function to clean YouTube URL
function getCleanVideoUrl(url) {
const match = url.match(/[?&]v=([^&]+)/);
if (match) {
return `https://www.youtube.com/watch?v=${match[1]}`;
}
return url; // Return original if we can't extract video ID
}
// Extract button click handler
extractBtn.addEventListener('click', async () => {
if (!apiToken) {
showStatus('Please configure your API token first', 'error');
return;
}
extractBtn.disabled = true;
extractBtn.textContent = 'Extracting...';
showStatus('Starting audio extraction...', 'loading');
try {
// Use clean URL for the API request
const cleanUrl = getCleanVideoUrl(tab.url);
console.log('Making extraction request:', {
apiBaseUrl,
cleanUrl,
title: currentVideoInfo?.title,
hasToken: !!apiToken
});
const response = await fetch(`${apiBaseUrl}/api/v1/extractions/?url=${encodeURIComponent(cleanUrl)}`, {
method: 'POST',
headers: {
'API-TOKEN': apiToken
}
});
console.log('Response status:', response.status, response.statusText);
if (!response.ok) {
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = await response.json();
if (errorData.detail) {
errorMessage = errorData.detail;
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (typeof errorData === 'string') {
errorMessage = errorData;
}
} catch (e) {
// If we can't parse JSON, use the status text
errorMessage = response.statusText || errorMessage;
}
throw new Error(errorMessage);
}
await response.json();
const successMessage = currentVideoInfo?.title
? `Audio extraction started for "${currentVideoInfo.title}"! Check your soundboard for the result.`
: 'Audio extraction started successfully! Check your soundboard for the result.';
showStatus(successMessage, 'success');
// Close popup after successful extraction
setTimeout(() => {
window.close();
}, 2000);
} catch (error) {
console.error('Extraction error:', error);
let errorMessage = 'Unknown error occurred';
if (error.message) {
errorMessage = error.message;
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
errorMessage = 'Cannot connect to API server. Check your API base URL and network connection.';
} else if (typeof error === 'string') {
errorMessage = error;
}
showStatus(`Failed to extract audio: ${errorMessage}`, 'error');
} finally {
extractBtn.disabled = false;
extractBtn.textContent = 'Extract Audio';
}
});
// Config button click handler
configBtn.addEventListener('click', () => {
chrome.runtime.openOptionsPage();
});
function showStatus(message, type) {
status.textContent = message;
status.className = `status ${type}`;
status.classList.remove('hidden');
}
});