commit cb607760ce537d0f61e54ed20b9ec67511f938e7 Author: JSC Date: Sat Sep 20 20:54:12 2025 +0200 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..41382fb --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/background.js b/background.js new file mode 100644 index 0000000..1de692c --- /dev/null +++ b/background.js @@ -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; + } +} \ No newline at end of file diff --git a/content.js b/content.js new file mode 100644 index 0000000..5c3813f --- /dev/null +++ b/content.js @@ -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 }); + +})(); \ No newline at end of file diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c182680 --- /dev/null +++ b/icon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icon128.png b/icon128.png new file mode 100644 index 0000000..4150b87 Binary files /dev/null and b/icon128.png differ diff --git a/icon16.png b/icon16.png new file mode 100644 index 0000000..a173a09 Binary files /dev/null and b/icon16.png differ diff --git a/icon48.png b/icon48.png new file mode 100644 index 0000000..33823bd Binary files /dev/null and b/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..57dbde2 --- /dev/null +++ b/manifest.json @@ -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" + } +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100644 index 0000000..3c8788a --- /dev/null +++ b/options.html @@ -0,0 +1,194 @@ + + + + + SDB Audio Extractor - Options + + + +
+ +
Configuration
+
+ +
+

Setup Instructions

+
    +
  1. Log into your SDB soundboard application
  2. +
  3. Go to your account settings
  4. +
  5. Generate an API token
  6. +
  7. Copy the token and paste it below
  8. +
  9. Make sure your API base URL is correct (usually http://192-168-100-199.sslip.io)
  10. +
  11. Click "Test Connection" to verify everything works
  12. +
+
+ +
+
+ + +
This token is used to authenticate with your SDB backend API
+
+ +
+ + +
The base URL of your SDB backend API (default: http://192-168-100-199.sslip.io)
+
+ + + +
+ + + + + + \ No newline at end of file diff --git a/options.js b/options.js new file mode 100644 index 0000000..2dff7a2 --- /dev/null +++ b/options.js @@ -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); + } + } +}); \ No newline at end of file diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..722af2c --- /dev/null +++ b/popup.html @@ -0,0 +1,168 @@ + + + + + + + +
+ +
Extract audio from YouTube videos
+
+ + + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..3e622f8 --- /dev/null +++ b/popup.js @@ -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'); + } +}); \ No newline at end of file