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:
166
README.md
Normal file
166
README.md
Normal 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
150
background.js
Normal 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
58
content.js
Normal 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
25
icon.svg
Normal 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
BIN
icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
icon16.png
Normal file
BIN
icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
icon48.png
Normal file
BIN
icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
37
manifest.json
Normal file
37
manifest.json
Normal 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
194
options.html
Normal 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
124
options.js
Normal 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
168
popup.html
Normal 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
147
popup.js
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user