手把手教你写一个QQ音乐免费下载的油猴脚本(附完整源码与常见问题排查)
从零实现QQ音乐免费下载的油猴脚本开发实战指南
每次在QQ音乐网页版听到喜欢的免费歌曲,总想保存到本地却找不到下载入口?作为前端开发者,我们可以用Tampermonkey油猴脚本轻松解决这个问题。本文将带你从零开始,开发一个能自动识别歌曲信息并添加下载按钮的实用脚本,不仅提供完整代码,还会深入讲解DOM操作时机、跨域处理等核心知识点。
1. 开发环境准备与基础概念
在开始编写脚本前,我们需要先了解几个关键工具和概念。Tampermonkey是一款强大的浏览器扩展,允许用户自定义JavaScript脚本来自动化网页操作或增强功能。它支持Chrome、Firefox、Edge等主流浏览器,安装过程非常简单:
- 打开浏览器扩展商店(如Chrome Web Store)
- 搜索"Tampermonkey"并点击安装
- 安装完成后,浏览器右上角会出现Tampermonkey图标
油猴脚本本质上是一个特殊的用户脚本,由以下几部分组成:
// ==UserScript== // @name 脚本名称 // @namespace 命名空间 // @version 版本号 // @description 脚本描述 // @author 作者 // @match 脚本生效的URL // @grant 需要的特殊权限 // ==/UserScript== (function() { 'use strict'; // 脚本主体代码 })();对于我们的音乐下载脚本,需要特别注意@grant GM_download这一声明,它是Tampermonkey提供的下载API,比浏览器原生下载功能更强大。
2. 网页元素分析与数据提取
要实现音乐下载功能,首先需要分析QQ音乐网页版的结构。打开开发者工具(F12),我们可以观察到几个关键元素:
- 歌曲名称位于
.song_info__name类的<a>标签内 - 音频文件URL可以从
<audio>标签的src属性获取 - 播放器控制区域有固定的DOM结构
通过以下代码可以提取这些关键信息:
// 获取歌曲名称元素 const songNameElement = document.querySelector('.song_info__name a'); // 获取音频元素 const audioElement = document.querySelector('audio'); if (songNameElement && audioElement) { const songName = songNameElement.innerText; const audioUrl = audioElement.src; console.log(`歌曲名称: ${songName}`); console.log(`音频地址: ${audioUrl}`); }常见问题1:脚本运行时控制台报错"无法读取null的属性"
这是因为DOM还未完全加载,我们的查询返回了null。解决方法是在脚本中使用
setTimeout延迟执行,或者监听DOMContentLoaded事件。
3. 实现下载功能与UI集成
获取到音频URL后,我们需要使用Tampermonkey的GM_download API实现下载功能。这个API比浏览器原生下载更灵活,支持自定义文件名和保存对话框:
function downloadMusic(url, filename) { GM_download({ url: url, name: filename + '.mp3', saveAs: true, onerror: function(error) { console.error('下载失败:', error); alert('下载失败: ' + error.error); }, onload: function() { console.log('下载完成'); } }); }为了让用户操作更直观,我们可以在播放器界面添加一个下载按钮:
function createDownloadButton() { const button = document.createElement('button'); button.textContent = '下载歌曲'; button.style.cssText = ` padding: 6px 12px; background: #31c27c; color: white; border: none; border-radius: 4px; margin-left: 10px; cursor: pointer; font-size: 14px; `; button.addEventListener('click', () => { const songName = document.querySelector('.song_info__name a').innerText; const audioUrl = document.querySelector('audio').src; downloadMusic(audioUrl, songName); }); return button; }UI集成技巧:
- 使用CSS in JS方式设置按钮样式,避免污染全局样式
- 将按钮插入到合适的位置,保持界面协调
- 添加hover效果提升用户体验
4. 完整脚本实现与优化
结合前面的知识点,下面是完整的脚本代码,包含错误处理和性能优化:
// ==UserScript== // @name QQ音乐免费下载助手 // @namespace https://github.com/yourname // @version 1.0 // @description 为QQ音乐网页版添加免费歌曲下载功能 // @author YourName // @match https://y.qq.com/n/ryqq/player* // @icon https://y.qq.com/favicon.ico // @grant GM_download // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 最大重试次数 const MAX_RETRY = 3; let retryCount = 0; function downloadMusic(url, filename) { GM_download({ url: url, name: sanitizeFilename(filename) + '.mp3', saveAs: true, onerror: function(error) { console.error('下载失败:', error); if (error.error === 'not_whitelisted') { alert('请检查下载地址是否合法'); } } }); } // 清理文件名中的非法字符 function sanitizeFilename(name) { return name.replace(/[\\/:*?"<>|]/g, ''); } function createDownloadButton() { const button = document.createElement('button'); button.textContent = '下载歌曲'; button.className = 'tm-download-btn'; button.addEventListener('click', handleDownload); return button; } function handleDownload() { try { const songInfo = getSongInfo(); if (!songInfo.url) { throw new Error('无法获取音频地址'); } downloadMusic(songInfo.url, songInfo.name); } catch (error) { console.error('下载出错:', error); alert(error.message); } } function getSongInfo() { const nameElement = document.querySelector('.song_info__name a'); const audioElement = document.querySelector('audio'); if (!nameElement || !audioElement) { throw new Error('页面元素未找到'); } return { name: nameElement.innerText, url: audioElement.src }; } function init() { const container = document.querySelector('.song_info__name'); if (!container) { if (retryCount < MAX_RETRY) { retryCount++; setTimeout(init, 1000 * retryCount); } return; } // 避免重复添加按钮 if (document.querySelector('.tm-download-btn')) { return; } const button = createDownloadButton(); container.appendChild(button); } // 延迟初始化,确保DOM加载完成 setTimeout(init, 1000); })();优化点说明:
- 添加了重试机制,应对DOM加载延迟
- 文件名清理功能,避免非法字符导致保存失败
- 按钮防重复添加检查
- 更完善的错误处理和用户反馈
5. 高级功能扩展与调试技巧
基础功能实现后,我们可以考虑添加一些增强功能:
多格式支持:检测音频格式并自动调整
function getAudioFormat(url) { const ext = url.split('.').pop().toLowerCase(); return ['mp3', 'm4a', 'flac'].includes(ext) ? ext : 'mp3'; }下载队列:批量下载播放列表中的歌曲
function processPlaylist() { const items = document.querySelectorAll('.playlist__item'); items.forEach((item, index) => { setTimeout(() => { item.click(); // 模拟点击切换歌曲 setTimeout(() => { const info = getSongInfo(); downloadMusic(info.url, `${index + 1}. ${info.name}`); }, 1000); }, index * 3000); }); }调试技巧:
- 使用
@run-at document-idle让脚本在页面完全加载后执行 - 在Tampermonkey管理界面启用调试模式
- 使用
GM_log替代console.log查看完整日志 - 通过
// @require引入外部库增强功能
跨域问题解决方案:
如果遇到跨域限制,可以尝试通过服务器代理或使用
GM_xmlhttpRequest替代直接下载。
6. 常见问题排查与解决方案
在实际使用中可能会遇到各种问题,下面是一些典型场景的解决方法:
问题1:按钮没有显示
- 检查
@matchURL是否匹配当前页面 - 增加延迟时间或使用MutationObserver监听DOM变化
- 确认元素选择器是否正确(QQ音乐可能更新界面)
问题2:下载失败或文件损坏
- 检查音频URL是否有效
- 尝试去掉
saveAs参数让浏览器自动处理 - 确认有足够的磁盘空间和写入权限
问题3:脚本在特定页面不工作
- 使用
// @include替代// @match扩大匹配范围 - 检查页面是否使用了iframe,需要特殊处理
- 查看控制台错误信息针对性解决
性能优化建议:
- 避免频繁的DOM查询,缓存元素引用
- 使用事件委托减少事件监听器数量
- 对于复杂操作,考虑使用Web Worker
开发这类实用脚本最令人兴奋的部分是看到自己的代码真正解决了实际问题。记得定期更新脚本以适应网站改版,同时尊重音乐平台的服务条款,仅用于个人学习和合法用途。
