VocalRemover Audio Downloader2

Adds a floating panel to download audio from VocalRemover

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
    });
})();