酷狗音乐MP3下载助手 - 下载状态修复版

 // ==UserScript==

// @name         酷狗音乐MP3下载助手 - 下载状态修复版

// @namespace    http://tampermonkey.net/

// @version      4.3.0

// @description  在酷狗音乐网站上监测MP3资源并提供下载功能,文件名格式为"歌曲名-歌手名.mp3",修复下载状态

// @author       YourName

// @match        *://www.kugou.com/*

// @match        *://*.kugou.com/*

// @grant        GM_xmlhttpRequest

// @grant        GM_download

// @grant        GM_notification

// @grant        GM_addStyle

// @connect      *

// @connect      kg*

// @run-at       document-start

// ==/UserScript==


(function() {

    'use strict';


    // 添加自定义样式

    GM_addStyle(`

        .kg-download-btn {

            background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);

            color: white;

            border: none;

            border-radius: 20px;

            padding: 8px 16px;

            margin: 5px;

            font-size: 14px;

            font-weight: bold;

            cursor: pointer;

            display: inline-flex;

            align-items: center;

            justify-content: center;

            transition: all 0.3s ease;

            box-shadow: 0 4px 12px rgba(106, 17, 203, 0.3);

        }


        .kg-download-btn:hover {

            transform: translateY(-2px);

            box-shadow: 0 6px 16px rgba(106, 17, 203, 0.4);

            background: linear-gradient(135deg, #7b1ddb 0%, #3685ff 100%);

        }


        .kg-download-btn:disabled {

            background: #cccccc;

            cursor: not-allowed;

            transform: none;

            box-shadow: none;

        }


        .kg-download-panel {

            position: fixed;

            bottom: 20px;

            right: 20px;

            background: white;

            border-radius: 12px;

            box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);

            padding: 15px;

            z-index: 99999;

            min-width: 320px;

            max-width: 420px;

            border: 1px solid #e0e0e0;

            font-family: 'Microsoft YaHei', Arial, sans-serif;

        }


        .kg-download-title {

            margin: 0 0 12px 0;

            color: #6a11cb;

            font-size: 16px;

            display: flex;

            align-items: center;

        }


        .kg-download-content {

            max-height: 400px;

            overflow-y: auto;

            margin-bottom: 10px;

        }


        .kg-download-item {

            background: #f9f9f9;

            padding: 12px;

            margin-bottom: 10px;

            border-radius: 8px;

            border: 1px solid #eee;

        }


        .kg-song-name {

            font-weight: bold;

            margin-bottom: 6px;

            font-size: 14px;

            color: #333;

        }


        .kg-file-info {

            font-size: 12px;

            color: #666;

            margin-bottom: 8px;

        }


        .kg-notification {

            position: fixed;

            top: 20px;

            right: 20px;

            background: #4CAF50;

            color: white;

            padding: 12px 20px;

            border-radius: 8px;

            box-shadow: 0 4px 12px rgba(0,0,0,0.15);

            z-index: 100000;

            animation: slideIn 0.3s ease-out;

            max-width: 300px;

        }


        .kg-notification.error {

            background: #f44336;

        }


        @keyframes slideIn {

            from { transform: translateX(100%); opacity: 0; }

            to { transform: translateX(0); opacity: 1; }

        }


        .kg-float-btn {

            position: fixed;

            bottom: 80px;

            right: 20px;

            background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);

            color: white;

            border: none;

            border-radius: 50%;

            width: 50px;

            height: 50px;

            font-size: 16px;

            font-weight: bold;

            cursor: pointer;

            z-index: 99998;

            box-shadow: 0 6px 20px rgba(106, 17, 203, 0.4);

            transition: all 0.3s ease;

        }


        .kg-float-btn:hover {

            transform: scale(1.1);

            box-shadow: 0 8px 25px rgba(106, 17, 203, 0.5);

        }


        .kg-filename-preview {

            background: #f0f8ff;

            padding: 10px;

            border-radius: 4px;

            margin: 8px 0;

            font-size: 13px;

            color: #333;

            border: 1px dashed #6a11cb;

            font-family: monospace;

            word-break: break-all;

        }

    `);


    // 存储检测到的音频资源

    const audioResources = new Map();

    // 存储正在下载的资源

    const downloadingResources = new Set();

    let downloadPanel = null;

    let floatBtn = null;


    // 显示通知

    function showNotification(message, isError = false, duration = 3000) {

        const existing = document.querySelector('.kg-notification');

        if (existing) existing.remove();


        const notification = document.createElement('div');

        notification.className = `kg-notification ${isError ? 'error' : ''}`;

        notification.textContent = message;


        document.body.appendChild(notification);


        setTimeout(() => {

            notification.style.opacity = '0';

            notification.style.transition = 'opacity 0.5s';

            setTimeout(() => notification.remove(), 500);

        }, duration);

    }


    // 获取歌曲信息

    function getSongInfo() {

        console.log('开始提取歌曲信息...');


        // 默认值

        let songName = '未知歌曲';

        let artistName = '未知歌手';


        try {

            // 方法1: 直接从.audioName元素提取

            const audioNameElement = document.querySelector('.audioName');

            if (audioNameElement) {

                const audioNameText = audioNameElement.textContent.trim();

                console.log('audioName元素内容:', audioNameText);


                // 处理"周杰伦 - 稻香"格式

                if (audioNameText.includes(' - ')) {

                    const parts = audioNameText.split(' - ');

                    if (parts.length === 2) {

                        // audioName是"周杰伦 - 稻香"格式

                        artistName = parts[0].trim();

                        songName = parts[1].trim();

                        console.log(`从audioName解析: 歌手="${artistName}", 歌曲名="${songName}"`);

                    }

                }

            } else {

                console.log('未找到.audioName元素');

            }


            // 方法2: 如果还没找到,尝试从页面标题提取

            if (songName === '未知歌曲') {

                const title = document.title;

                console.log('页面标题:', title);


                // 标题格式通常是"稻香_周杰伦_高音质在线试听_稻香歌词|歌曲下载_酷狗音乐"

                if (title.includes('_')) {

                    const parts = title.split('_');

                    if (parts.length >= 2) {

                        songName = parts[0].trim();

                        artistName = parts[1].trim();

                        console.log(`从标题解析: 歌曲名="${songName}", 歌手="${artistName}"`);

                    }

                }

            }


            // 清理和格式化

            songName = cleanFileName(songName);

            artistName = cleanFileName(artistName);


        } catch (e) {

            console.error('提取歌曲信息时出错:', e);

        }


        // 最终格式:歌曲名-歌手名(中间只有一个减号,没有空格)

        const fullName = `${songName}-${artistName}`;


        console.log(`最终提取结果: 歌曲名="${songName}", 歌手="${artistName}", 文件名="${fullName}"`);


        return {

            songName: songName,

            artistName: artistName,

            fullName: fullName

        };

    }


    // 清理文件名,移除非法字符和下划线

    function cleanFileName(name) {

        if (!name || name === '未知歌曲' || name === '未知歌手') {

            return name;

        }


        // 移除所有非法文件名字符

        let cleaned = name.replace(/[<>:"/\\|?*]/g, '')

                         .replace(/_/g, ' ')  // 下划线替换为空格

                         .replace(/\s+/g, ' ') // 多个空格合并为一个

                         .trim();


        // 移除首尾空格和点

        cleaned = cleaned.replace(/^[.\s]+|[.\s]+$/g, '');


        // 限制长度

        if (cleaned.length > 50) {

            cleaned = cleaned.substring(0, 50);

        }


        return cleaned;

    }


    // 创建下载面板

    function createDownloadPanel() {

        if (downloadPanel) return downloadPanel;


        downloadPanel = document.createElement('div');

        downloadPanel.className = 'kg-download-panel';

        downloadPanel.innerHTML = `

            <div class="kg-download-title">

                <span style="margin-right: 8px;">🎵</span> 酷狗音乐下载器

                <span style="margin-left: auto; font-size: 12px; cursor: pointer; color: #999;" id="kg-close-btn">×</span>

            </div>

            <div class="kg-download-content" id="kg-download-content">

                <div style="text-align: center; color: #666; padding: 20px;">

                    正在监听音频资源...<br>

                    <small>请播放一首歌曲</small>

                </div>

            </div>

            <div style="display: flex; justify-content: space-between; border-top: 1px solid #eee; padding-top: 10px;">

                <button class="kg-download-btn" id="kg-refresh-btn" style="font-size: 12px;">刷新列表</button>

                <button class="kg-download-btn" id="kg-clear-btn" style="font-size: 12px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">清空列表</button>

            </div>

        `;


        // 添加拖拽功能

        let isDragging = false;

        let offsetX, offsetY;


        downloadPanel.querySelector('.kg-download-title').style.cursor = 'move';

        downloadPanel.querySelector('.kg-download-title').addEventListener('mousedown', startDrag);


        function startDrag(e) {

            if (e.target.id === 'kg-close-btn') return;

            isDragging = true;

            offsetX = e.clientX - downloadPanel.getBoundingClientRect().left;

            offsetY = e.clientY - downloadPanel.getBoundingClientRect().top;

            document.addEventListener('mousemove', drag);

            document.addEventListener('mouseup', stopDrag);

        }


        function drag(e) {

            if (!isDragging) return;

            downloadPanel.style.left = (e.clientX - offsetX) + 'px';

            downloadPanel.style.top = (e.clientY - offsetY) + 'px';

            downloadPanel.style.right = 'auto';

            downloadPanel.style.bottom = 'auto';

        }


        function stopDrag() {

            isDragging = false;

            document.removeEventListener('mousemove', drag);

            document.removeEventListener('mouseup', stopDrag);

        }


        // 事件监听

        downloadPanel.querySelector('#kg-close-btn').addEventListener('click', () => {

            downloadPanel.style.display = 'none';

        });


        downloadPanel.querySelector('#kg-refresh-btn').addEventListener('click', updateDownloadList);


        downloadPanel.querySelector('#kg-clear-btn').addEventListener('click', () => {

            audioResources.clear();

            downloadingResources.clear();

            updateDownloadList();

            showNotification('列表已清空');

        });


        document.body.appendChild(downloadPanel);

        return downloadPanel;

    }


    // 创建浮动按钮

    function createFloatBtn() {

        if (floatBtn) return floatBtn;


        floatBtn = document.createElement('button');

        floatBtn.className = 'kg-float-btn';

        floatBtn.innerHTML = '↓';

        floatBtn.title = '显示下载面板';


        floatBtn.addEventListener('click', () => {

            if (!downloadPanel) createDownloadPanel();

            downloadPanel.style.display = downloadPanel.style.display === 'none' ? 'block' : 'block';

            updateDownloadList();

        });


        document.body.appendChild(floatBtn);

        return floatBtn;

    }


    // 更新下载列表

    function updateDownloadList() {

        if (!downloadPanel) return;


        const content = downloadPanel.querySelector('#kg-download-content');

        if (!content) return;


        if (audioResources.size === 0) {

            content.innerHTML = `

                <div style="text-align: center; color: #666; padding: 20px;">

                    未检测到音频资源<br>

                    <small>请播放一首歌曲</small>

                </div>

            `;

            return;

        }


        let html = '';

        const entries = Array.from(audioResources.entries())

            .sort((a, b) => b[1].timestamp - a[1].timestamp)

            .slice(0, 8);


        entries.forEach(([url, info], index) => {

            const sizeMB = info.size ? ` | ${(info.size / (1024 * 1024)).toFixed(2)} MB` : '';

            const isDownloading = downloadingResources.has(url);


            // 显示文件名预览

            const filenamePreview = `<div class="kg-filename-preview">${info.fullName}.mp3</div>`;


            html += `

                <div class="kg-download-item">

                    <div class="kg-song-name">${index + 1}. ${info.fullName}</div>

                    ${filenamePreview}

                    <div class="kg-file-info">来源: ${info.source}${sizeMB}</div>

                    <button class="kg-download-btn" data-url="${url}" data-filename="${info.fullName}"

                            ${isDownloading ? 'disabled' : ''} style="font-size: 12px; padding: 6px 12px;">

                        ${isDownloading ? '下载中...' : '下载 MP3'}

                    </button>

                </div>

            `;

        });


        content.innerHTML = html;


        // 添加下载按钮事件

        content.querySelectorAll('.kg-download-btn[data-url]').forEach(btn => {

            btn.addEventListener('click', (e) => {

                const url = e.target.dataset.url;

                const filename = e.target.dataset.filename;

                downloadAudio(url, filename);

            });

        });

    }


    // 添加音频资源

    function addAudioResource(url, source = '自动检测') {

        if (!url || audioResources.has(url)) return;


        // 获取页面歌曲信息

        const pageInfo = getSongInfo();


        const resourceId = `audio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;


        const resourceInfo = {

            id: resourceId,

            url: url,

            songName: pageInfo.songName,

            artistName: pageInfo.artistName,

            fullName: pageInfo.fullName,

            timestamp: Date.now(),

            source: source,

            size: null

        };


        audioResources.set(url, resourceInfo);


        console.log(`[${source}] 检测到音频资源:`, resourceInfo);


        // 尝试获取文件大小

        fetchFileSize(url).then(size => {

            if (size) {

                audioResources.get(url).size = size;

                updateDownloadList();

            }

        }).catch(() => {});


        // 更新列表并显示通知

        updateDownloadList();


        if (downloadPanel && downloadPanel.style.display !== 'none') {

            showNotification(`检测到新音频: ${pageInfo.fullName}`, false, 2000);

        }


        return resourceId;

    }


    // 获取文件大小

    async function fetchFileSize(url) {

        try {

            return await new Promise((resolve, reject) => {

                GM_xmlhttpRequest({

                    method: 'HEAD',

                    url: url,

                    onload: function(response) {

                        const size = parseInt(response.responseHeaders.match(/Content-Length:\s*(\d+)/i)?.[1] || 0);

                        resolve(size > 0 ? size : null);

                    },

                    onerror: reject,

                    timeout: 5000

                });

            });

        } catch (e) {

            return null;

        }

    }


    // 下载音频

    function downloadAudio(url, fullName) {

        if (!url || !fullName) {

            showNotification('下载失败: 无效的URL或文件名', true);

            return;

        }


        // 标记为正在下载

        downloadingResources.add(url);

        updateDownloadList();


        // 确保文件名格式为"歌曲名-歌手名.mp3"(中间只有一个减号)

        const cleanedName = fullName.replace(/[_\s]+/g, ' ').trim();

        const filename = `${cleanedName}.mp3`;


        console.log(`开始下载: ${filename} (URL: ${url})`);

        showNotification(`开始下载: ${cleanedName}`, false, 2000);


        // 创建下载ID用于跟踪

        const downloadId = `download_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;

        console.log(`下载ID: ${downloadId}`);


        try {

            GM_download({

                url: url,

                name: filename,

                saveAs: true,

                headers: {

                    'Referer': window.location.origin,

                    'User-Agent': navigator.userAgent

                },

                onerror: function(error) {

                    console.error(`下载失败 (${downloadId}):`, error);

                    showNotification(`下载失败: ${error.error}`, true);


                    // 移除下载标记

                    downloadingResources.delete(url);

                    updateDownloadList();

                },

                onload: function() {

                    console.log(`下载完成 (${downloadId}): ${filename}`);

                    showNotification(`"${cleanedName}" 下载完成`, false, 3000);


                    // 移除下载标记

                    downloadingResources.delete(url);

                    updateDownloadList();

                },

                ontimeout: function() {

                    console.error(`下载超时 (${downloadId})`);

                    showNotification('下载超时,请重试', true);


                    // 移除下载标记

                    downloadingResources.delete(url);

                    updateDownloadList();

                },

                onprogress: function(progress) {

                    // 可以在这里显示下载进度,但目前我们只更新按钮状态

                    console.log(`下载进度 (${downloadId}): ${progress.done}/${progress.total}`);

                }

            });

        } catch (e) {

            console.error(`下载异常 (${downloadId}):`, e);

            showNotification('下载失败', true);


            // 移除下载标记

            downloadingResources.delete(url);

            updateDownloadList();

        }

    }


    // 监听网络请求

    function interceptNetworkRequests() {

        // 监听XHR请求

        const originalXHROpen = XMLHttpRequest.prototype.open;

        const originalXHRSend = XMLHttpRequest.prototype.send;


        XMLHttpRequest.prototype.open = function(method, url) {

            this._requestUrl = url;

            return originalXHROpen.apply(this, arguments);

        };


        XMLHttpRequest.prototype.send = function() {

            const url = this._requestUrl;


            // 监听状态变化

            const originalOnReadyStateChange = this.onreadystatechange;

            this.onreadystatechange = function() {

                if (this.readyState === 4 && this.status === 200) {

                    try {

                        // 检查是否是音频文件

                        if (url.includes('.mp3') || url.includes('.m4a') ||

                            url.includes('.aac') || url.includes('audio/')) {


                            console.log('XHR检测到音频请求:', url);

                            const audioUrl = url.split('?')[0];

                            addAudioResource(audioUrl, 'XHR检测');

                        }

                    } catch (e) {

                        console.warn('解析响应时出错:', e);

                    }

                }


                if (originalOnReadyStateChange) {

                    originalOnReadyStateChange.apply(this, arguments);

                }

            };


            return originalXHRSend.apply(this, arguments);

        };


        // 监听fetch请求

        const originalFetch = window.fetch;

        window.fetch = function(input, init) {

            const url = typeof input === 'string' ? input : input.url;


            return originalFetch.apply(this, arguments).then(response => {

                // 克隆响应以便读取

                const clonedResponse = response.clone();


                // 检查是否是音频相关

                if (url.includes('.mp3') || url.includes('.m4a') ||

                    url.includes('.aac') || url.includes('audio/')) {


                    console.log('Fetch检测到音频请求:', url);

                    const audioUrl = url.split('?')[0];

                    addAudioResource(audioUrl, 'Fetch检测');

                }


                return response;

            });

        };

    }


    // 监听媒体元素

    function monitorMediaElements() {

        // 监听现有的audio/video元素

        document.querySelectorAll('audio, video').forEach(media => {

            if (media.src) {

                checkMediaSource(media);

            }

        });


        // 监听新添加的媒体元素

        const observer = new MutationObserver((mutations) => {

            mutations.forEach((mutation) => {

                mutation.addedNodes.forEach((node) => {

                    if (node.nodeName === 'AUDIO' || node.nodeName === 'VIDEO') {

                        if (node.src) {

                            checkMediaSource(node);

                        }

                    } else if (node.querySelectorAll) {

                        node.querySelectorAll('audio, video').forEach(media => {

                            if (media.src) {

                                checkMediaSource(media);

                            }

                        });

                    }

                });

            });

        });


        observer.observe(document.body, {

            childList: true,

            subtree: true

        });


        // 监听媒体元素属性变化

        const mediaObserver = new MutationObserver((mutations) => {

            mutations.forEach((mutation) => {

                if (mutation.type === 'attributes' &&

                    (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc')) {


                    const media = mutation.target;

                    if (media.src) {

                        checkMediaSource(media);

                    }

                }

            });

        });


        document.querySelectorAll('audio, video').forEach(media => {

            mediaObserver.observe(media, { attributes: true });

        });

    }


    // 检查媒体源

    function checkMediaSource(media) {

        if (!media.src) return;


        // 检查直接src

        if (media.src.includes('.mp3') || media.src.includes('.m4a') ||

            media.src.includes('.aac') || media.src.includes('audio/')) {


            addAudioResource(media.src, '媒体元素');

        }


        // 检查currentSrc(可能更准确)

        if (media.currentSrc && media.currentSrc !== media.src) {

            if (media.currentSrc.includes('.mp3') || media.currentSrc.includes('.m4a') ||

                media.currentSrc.includes('.aac') || media.currentSrc.includes('audio/')) {


                addAudioResource(media.currentSrc, '媒体元素(currentSrc)');

            }

        }

    }


    // 初始化

    function init() {

        console.log('酷狗音乐下载助手已启动 (下载状态修复版)');


        // 创建UI元素

        createFloatBtn();


        // 监听网络请求

        interceptNetworkRequests();


        // 监听媒体元素

        monitorMediaElements();


        // 页面加载完成后,再尝试检测

        setTimeout(() => {

            showNotification('酷狗音乐下载助手已激活', false, 3000);

        }, 2000);


        // 定期检查并重置下载状态(防止状态卡住)

        setInterval(() => {

            const now = Date.now();

            const timeout = 30000; // 30秒超时


            // 检查是否有下载超过30秒还没完成的

            downloadingResources.forEach(url => {

                const resource = audioResources.get(url);

                if (resource && (now - resource.timestamp) > timeout) {

                    console.log(`下载超时重置: ${url}`);

                    downloadingResources.delete(url);

                    updateDownloadList();

                }

            });

        }, 10000); // 每10秒检查一次

    }


    // 启动脚本

    if (document.readyState === 'loading') {

        document.addEventListener('DOMContentLoaded', init);

    } else {

        init();

    }


    // 监听页面变化(单页应用)

    let lastUrl = location.href;

    new MutationObserver(() => {

        const url = location.href;

        if (url !== lastUrl) {

            lastUrl = url;

            setTimeout(() => {

                console.log('页面变化,重新检测音频资源');

                audioResources.clear();

                downloadingResources.clear();

                updateDownloadList();

            }, 1000);

        }

    }).observe(document, { subtree: true, childList: true });


})();

此博客中的热门博文

下载谷歌浏览器

Office 微软官方部署流程