VocalRemover Audio Downloader2

Adds a floating panel to download audio from VocalRemover

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