酷狗音乐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 });
})();