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