- // ==UserScript==
- // @name VocalRemover Audio Downloader2
- // @namespace http://tampermonkey.net/
- // @version 1.1
- // @description Adds a floating panel to download audio from VocalRemover
- // @author You
- // @match https://vocalremover.media.io/app/*
- // @grant GM_download
- // @license MIT
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // Create the floating panel
- let panel = document.createElement('div');
- panel.id = 'audio-downloader-panel';
- panel.style.cssText = `
- position: fixed;
- bottom: 20px;
- right: 20px;
- width: 400px;
- height: 300px;
- background: #ffffff;
- border-radius: 12px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
- z-index: 9999;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- font-family: Arial, sans-serif;
- border: 1px solid #e0e0e0;
- transition: all 0.3s ease;
- `;
-
- // Create the header
- let header = document.createElement('div');
- header.style.cssText = `
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 16px;
- background: linear-gradient(135deg, #4285f4, #3367d6);
- color: white;
- cursor: move;
- user-select: none;
- border-top-left-radius: 12px;
- border-top-right-radius: 12px;
- `;
- header.innerHTML = `
- <div style="font-weight: bold; font-size: 14px;">音频下载器</div>
- <div style="display: flex; gap: 12px;">
- <button id="minimize-panel" style="background: none; border: none; color: white; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; transition: background 0.2s;">−</button>
- <button id="close-panel" style="background: none; border: none; color: white; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; transition: background 0.2s;">×</button>
- </div>
- `;
- panel.appendChild(header);
-
- // Add hover effects to header buttons
- document.addEventListener('DOMContentLoaded', function() {
- const buttons = header.querySelectorAll('button');
- buttons.forEach(button => {
- button.addEventListener('mouseover', function() {
- this.style.background = 'rgba(255, 255, 255, 0.2)';
- });
- button.addEventListener('mouseout', function() {
- this.style.background = 'none';
- });
- });
- });
-
- // Create the content area
- let contentArea = document.createElement('div');
- contentArea.style.cssText = `
- flex: 1;
- padding: 16px;
- overflow-y: auto;
- scrollbar-width: thin;
- scrollbar-color: #ccc #f5f5f5;
- `;
- contentArea.innerHTML = `
- <div id="audio-list" style="margin-bottom: 15px;">
- <p style="color: #666; text-align: center; margin-top: 65px; font-size: 14px;">还没有检测到音频。点击"检测音频"按钮开始。</p>
- </div>
- `;
- contentArea.addEventListener('scroll', function(e) {
- e.stopPropagation();
- });
- panel.appendChild(contentArea);
-
- // Custom scrollbar styles
- const style = document.createElement('style');
- style.textContent = `
- #audio-downloader-panel ::-webkit-scrollbar {
- width: 6px;
- }
- #audio-downloader-panel ::-webkit-scrollbar-track {
- background: #f5f5f5;
- border-radius: 3px;
- }
- #audio-downloader-panel ::-webkit-scrollbar-thumb {
- background-color: #ccc;
- border-radius: 3px;
- }
- #audio-downloader-panel .download-button {
- transition: background-color 0.2s ease;
- }
- #audio-downloader-panel .download-button:hover {
- background-color: #3c9f40 !important;
- }
- #detect-audio {
- transition: background-color 0.2s ease, transform 0.1s ease;
- }
- #detect-audio:hover {
- background-color: #3367d6 !important;
- }
- #detect-audio:active {
- transform: scale(0.98);
- }
- .audio-item {
- transition: transform 0.1s ease;
- }
- .audio-item:hover {
- transform: translateY(-2px);
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
- }
- `;
- document.head.appendChild(style);
-
- // Create the footer with the detect button
- let footer = document.createElement('div');
- footer.style.cssText = `
- padding: 12px 16px;
- border-top: 1px solid #e0e0e0;
- display: flex;
- justify-content: space-between;
- background: #f8f8f8;
- border-bottom-left-radius: 12px;
- border-bottom-right-radius: 12px;
- `;
- footer.innerHTML = `
- <button id="detect-audio" style="
- padding: 9px 18px;
- background: #4285f4;
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-weight: bold;
- font-size: 13px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- ">检测音频</button>
- <span id="status-message" style="color: #666; font-size: 12px; align-self: center; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></span>
- `;
- panel.appendChild(footer);
-
- // Add the panel to the document
- document.body.appendChild(panel);
-
- // Create the minimized panel
- let minimizedPanel = document.createElement('div');
- minimizedPanel.id = 'minimized-audio-downloader';
- minimizedPanel.style.cssText = `
- position: fixed;
- bottom: 20px;
- right: 20px;
- background: linear-gradient(135deg, #4285f4, #3367d6);
- color: white;
- padding: 10px 18px;
- border-radius: 50px;
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
- z-index: 9999;
- cursor: pointer;
- display: none;
- font-family: Arial, sans-serif;
- font-weight: bold;
- font-size: 13px;
- transition: all 0.3s ease;
- `;
- minimizedPanel.innerHTML = `<span>音频下载器</span>`;
-
- // Add hover effect to minimized panel
- minimizedPanel.addEventListener('mouseover', function() {
- this.style.transform = 'translateY(-2px)';
- this.style.boxShadow = '0 5px 12px rgba(0, 0, 0, 0.25)';
- });
- minimizedPanel.addEventListener('mouseout', function() {
- this.style.transform = 'translateY(0)';
- this.style.boxShadow = '0 3px 10px rgba(0, 0, 0, 0.2)';
- });
-
- document.body.appendChild(minimizedPanel);
-
- // Status update function
- function updateStatus(message, isError = false) {
- const statusElement = document.getElementById('status-message');
- if (statusElement) {
- statusElement.textContent = message;
- statusElement.style.color = isError ? '#f44336' : '#666';
- statusElement.title = message; // Add title for longer messages
- }
- }
-
- // Function to format audio file names
- function formatAudioName(name) {
- if (!name) return 'audio.mp3';
-
- // Clean the name
- let cleanName = name.split('/').pop().split('?')[0];
-
- // Add extension if missing
- if (!cleanName.includes('.')) {
- cleanName += '.mp3';
- }
-
- // Try to make it more readable
- cleanName = cleanName
- .replace(/[_-]+/g, ' ')
- .replace(/(%20)+/g, ' ')
- .replace(/\s+/g, ' ');
-
- // Capitalize first letter of each word
- cleanName = cleanName.split(' ')
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
- .join(' ');
-
- return cleanName;
- }
-
- // Add detected audio to the list
- function addAudioToList(name, url) {
- const audioList = document.getElementById('audio-list');
- if (!audioList) return;
-
- // Check if the URL is already in the list
- const existingItems = audioList.querySelectorAll('.download-button');
- for (let item of existingItems) {
- if (item.getAttribute('data-url') === url) {
- // Already in the list, don't add duplicate
- return;
- }
- }
-
- // Clear the "no audio detected" message if present
- const noAudioMessage = audioList.querySelector('p');
- if (noAudioMessage) {
- audioList.innerHTML = '';
- }
-
- const audioItem = document.createElement('div');
- audioItem.className = 'audio-item';
- audioItem.style.cssText = `
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px;
- background: #f5f5f5;
- border-radius: 8px;
- margin-bottom: 10px;
- border: 1px solid #e8e8e8;
- transition: all 0.2s ease;
- `;
-
- const fileName = formatAudioName(name);
-
- audioItem.innerHTML = `
- <div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 250px; color: #333; font-size: 13px;" title="${fileName}">${fileName}</div>
- <button class="download-button" data-url="${url}" data-filename="${fileName}" style="
- background: #4CAF50;
- color: white;
- border: none;
- border-radius: 6px;
- padding: 6px 12px;
- cursor: pointer;
- font-size: 12px;
- font-weight: bold;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- ">下载</button>
- `;
-
- audioList.appendChild(audioItem);
-
- // Add event listener to the download button
- const downloadButton = audioItem.querySelector('.download-button');
- downloadButton.addEventListener('click', function(e) {
- e.preventDefault();
- const url = this.getAttribute('data-url');
- const filename = this.getAttribute('data-filename');
- triggerDownload(url, filename);
- });
- }
-
- // Make the panel draggable
- let isDragging = false;
- let offsetX, offsetY;
-
- header.addEventListener('mousedown', function(e) {
- isDragging = true;
- offsetX = e.clientX - panel.getBoundingClientRect().left;
- offsetY = e.clientY - panel.getBoundingClientRect().top;
- });
-
- document.addEventListener('mousemove', function(e) {
- if (!isDragging) return;
-
- panel.style.left = (e.clientX - offsetX) + 'px';
- panel.style.top = (e.clientY - offsetY) + 'px';
- panel.style.right = 'auto';
- panel.style.bottom = 'auto';
- });
-
- document.addEventListener('mouseup', function() {
- isDragging = false;
- });
-
- // Panel control functions
- document.getElementById('minimize-panel').addEventListener('click', function(e) {
- e.stopPropagation();
- panel.style.display = 'none';
- minimizedPanel.style.display = 'block';
- });
-
- document.getElementById('close-panel').addEventListener('click', function(e) {
- // e.stopPropagation();
- // panel.style.display = 'none';
- // minimizedPanel.style.display = 'none';
- });
-
- minimizedPanel.addEventListener('click', function() {
- minimizedPanel.style.display = 'none';
- panel.style.display = 'flex';
- });
-
- // ==================== Audio Detection Logic ====================
-
- // Intercept XHR requests to capture audio URLs
- function setupXHRInterceptor() {
- const originalOpen = XMLHttpRequest.prototype.open;
- const originalSend = XMLHttpRequest.prototype.send;
-
- XMLHttpRequest.prototype.open = function(method, url) {
- this._url = url;
- return originalOpen.apply(this, arguments);
- };
-
- XMLHttpRequest.prototype.send = function() {
- this.addEventListener('load', function() {
- // Check if response is audio
- const contentType = this.getResponseHeader('Content-Type');
- if (contentType && (
- contentType.includes('audio') ||
- this._url.includes('.mp3') ||
- this._url.includes('.wav') ||
- this._url.includes('audio')
- )) {
- updateStatus(`检测到音频: ${formatAudioName(this._url)}`);
- handleAudioUrl(this._url);
- }
- });
- return originalSend.apply(this, arguments);
- };
-
- updateStatus('已设置请求拦截器');
- }
-
- // Check network requests for audio files
- function checkNetworkForAudio() {
- if (!window.performance || !window.performance.getEntries) {
- updateStatus('此浏览器不支持Performance API', true);
- return;
- }
-
- const resources = window.performance.getEntries();
- const audioResources = resources.filter(resource => {
- return resource.name.includes('.mp3') ||
- resource.name.includes('.wav') ||
- resource.name.includes('audio') ||
- (resource.initiatorType === 'xmlhttprequest' &&
- resource.name.includes('blob'));
- });
-
- if (audioResources.length > 0) {
- updateStatus(`找到 ${audioResources.length} 个可能的音频资源`);
- audioResources.forEach(resource => {
- if (!resource.name.startsWith('blob:')) {
- handleAudioUrl(resource.name);
- }
- });
- return true;
- } else {
- updateStatus('未找到网络中的音频资源');
- return false;
- }
- }
-
- // Handle audio URL
- function handleAudioUrl(url) {
- if (url) {
- const formattedName = formatAudioName(url);
- addAudioToList(formattedName, url);
- }
- }
-
- // Check audio elements on the page
- function checkAudioElements() {
- const audioElements = document.querySelectorAll('audio');
- if (audioElements.length > 0) {
- updateStatus(`找到 ${audioElements.length} 个音频元素`);
- audioElements.forEach(audio => {
- if (audio.src) {
- if (audio.src.startsWith('blob:')) {
- // Try to get a meaningful name from page context
- let pageName = document.title || '';
- pageName = pageName.replace('VocalRemover', '').trim();
- const audioName = pageName || 'audio.mp3';
- addAudioToList(audioName, audio.src);
- } else {
- handleAudioUrl(audio.src);
- }
- }
- });
- return true;
- }
- return false;
- }
-
- // Check for audio in the application
- function detectAudio() {
- // Add a loading indicator to the button
- const detectButton = document.getElementById('detect-audio');
- const originalText = detectButton.textContent;
- detectButton.textContent = '正在检测...';
- detectButton.style.pointerEvents = 'none';
- detectButton.style.opacity = '0.7';
-
- updateStatus('正在检测音频...');
-
- // Use setTimeout to allow the button state to update first
- setTimeout(() => {
- // Try checking audio elements first
- const foundAudioElements = checkAudioElements();
-
- // Then check network requests
- const foundNetworkAudio = checkNetworkForAudio();
-
- // Setup interceptor for future requests
- setupXHRInterceptor();
-
- // Try to trigger audio playback
- const playButton = document.querySelector('.play-button');
- if (playButton) {
- updateStatus('找到播放按钮,点击以触发音频加载...');
- playButton.click();
- }
-
- // Check for wave elements that might contain audio data
- const waveElements = document.querySelectorAll('wave');
- if (waveElements.length > 0) {
- updateStatus('找到波形图元素,可能包含音频数据');
- }
-
- if (!foundAudioElements && !foundNetworkAudio) {
- // If nothing found, give feedback
- updateStatus('尚未找到音频。尝试播放页面中的音频后再次检测。');
- }
-
- // Reset button state
- detectButton.textContent = originalText;
- detectButton.style.pointerEvents = '';
- detectButton.style.opacity = '';
- }, 100);
- }
-
- // Trigger file download using GM_download if available, or fallback to regular method
- function triggerDownload(url, filename) {
- updateStatus(`准备下载: ${filename}`);
-
- if (typeof GM_download !== 'undefined') {
- // Use GM_download to download file directly
- GM_download({
- url: url,
- name: filename,
- onload: function() {
- updateStatus(`成功下载: ${filename}`);
- },
- onerror: function(error) {
- updateStatus(`下载失败: ${error}`, true);
- // Fall back to traditional method
- traditionalDownload(url, filename);
- }
- });
- } else {
- // Use traditional method
- traditionalDownload(url, filename);
- }
- }
-
- // Traditional download method
- function traditionalDownload(url, filename) {
- // For blob URLs we need to fetch them first
- if (url.startsWith('blob:')) {
- fetch(url)
- .then(response => response.blob())
- .then(blob => {
- const blobUrl = URL.createObjectURL(blob);
- downloadWithLink(blobUrl, filename);
- URL.revokeObjectURL(blobUrl);
- })
- .catch(error => {
- updateStatus(`下载失败: ${error.message}`, true);
- });
- } else {
- downloadWithLink(url, filename);
- }
- }
-
- // Download using a temporary anchor element
- function downloadWithLink(url, filename) {
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- a.style.display = 'none';
- a.target = '_blank'; // Add target blank to avoid opening in the current page
-
- document.body.appendChild(a);
- a.click();
-
- setTimeout(() => {
- document.body.removeChild(a);
- updateStatus(`已启动下载: ${filename}`);
- }, 100);
- }
-
- // Attach event listener to the detect button
- document.getElementById('detect-audio').addEventListener('click', function(e) {
- e.preventDefault(); // Prevent any default action
- e.stopPropagation(); // Stop propagation to prevent jitter
- detectAudio();
- });
- })();