// ==UserScript== // @name Suno 歌曲下载助手 (一键全自动流捕获: WebM / 320k MP3 / WAV / 封面 / 歌词 / ZIP打包) // @namespace https://github.com/meteroa/SpectromancerAPK // @version 3.2.0 // @description 彻底抛弃易受限的 CDN 方式!一键自动唤醒播放、声轨免损录制、320k MP3 自动转码。支持自动打包为 ZIP(音频+封面+歌词一个包搞定,彻底免除浏览器多文件下载弹窗),支持静音录制与提前完成。100% 免拦截无报错。 // @author Spectromancer Team // @match https://suno.com/* // @icon https://suno.com/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_addStyle // @grant GM_notification // @grant GM_setClipboard // @connect cdn2.suno.ai // @connect suno.ai // @connect suno.com // @require https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js // @require https://cdn.jsdelivr.net/npm/lamejs@1.2.1/lame.min.js // @require https://cdn.jsdelivr.net/gh/higuma/ogg-vorbis-encoder-js@master/lib/OggVorbisEncoder.min.js // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const STYLES = ` #suno-dl-widget { position: fixed; bottom: 96px; right: 24px; z-index: 999999; background: rgba(18, 20, 28, 0.95); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border: 1px solid rgba(255, 255, 255, 0.14); box-shadow: 0 20px 48px rgba(0, 0, 0, 0.65), 0 0 0 1px rgba(255, 255, 255, 0.08); border-radius: 18px; padding: 16px; width: 330px; color: #f1f1f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 13px; user-select: none; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); } #suno-dl-widget.collapsed { width: 48px; height: 48px; padding: 0; border-radius: 24px; overflow: hidden; cursor: pointer; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #e52d27, #b31217); box-shadow: 0 8px 24px rgba(0,0,0,0.5); } #suno-dl-widget.collapsed .widget-content { display: none; } #suno-dl-widget.collapsed .widget-icon-btn { display: flex; } .widget-icon-btn { display: none; width: 100%; height: 100%; align-items: center; justify-content: center; font-size: 22px; cursor: pointer; } .widget-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); } .widget-title { font-weight: 700; font-size: 14px; display: flex; align-items: center; gap: 6px; color: #fff; } .widget-title span.badge { background: linear-gradient(135deg, #ff416c, #ff4b2b); color: #fff; font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 6px; } .widget-close-btn { background: none; border: none; color: #8e8e9f; cursor: pointer; font-size: 16px; padding: 2px 6px; border-radius: 6px; line-height: 1; } .widget-close-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.1); } .current-track-card { display: flex; gap: 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 12px; padding: 10px; margin-bottom: 12px; align-items: center; } .track-avatar { width: 44px; height: 44px; border-radius: 8px; background: #2a2a38; object-fit: cover; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 20px; } .track-info { flex: 1; overflow: hidden; } .current-track-title { font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 3px; } .current-track-meta { font-size: 11px; color: #8e8e9f; display: flex; justify-content: space-between; } .options-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; font-size: 11px; color: #a0a0b5; } .options-row label { display: flex; align-items: center; gap: 5px; cursor: pointer; } .options-row input[type="checkbox"] { cursor: pointer; accent-color: #ff4b2b; } .progress-box { margin-bottom: 12px; display: none; } .progress-bar-bg { width: 100%; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin-bottom: 4px; } .progress-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #ff416c, #ff4b2b); transition: width 0.3s; } .progress-text { display: flex; justify-content: space-between; font-size: 11px; color: #ff758c; } .btn-grid-main { display: grid; grid-template-columns: 1.2fr 1fr; gap: 8px; margin-bottom: 8px; } .btn-grid-sub { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; } .btn-action { border: none; padding: 10px 0; border-radius: 10px; font-weight: 600; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 5px; transition: all 0.2s; } .btn-capture-mp3 { background: linear-gradient(135deg, #ff416c, #ff4b2b); color: #fff; } .btn-capture-mp3:hover { filter: brightness(1.15); transform: translateY(-1px); } .btn-capture-webm { background: linear-gradient(135deg, #4776e6, #8e54e9); color: #fff; } .btn-capture-webm:hover { filter: brightness(1.15); transform: translateY(-1px); } .btn-recording { background: #e03131 !important; color: #fff !important; animation: pulse 1s infinite alternate; } @keyframes pulse { from { opacity: 1; } to { opacity: 0.7; } } .btn-sub { background: rgba(255, 255, 255, 0.08); color: #dedee8; font-size: 11px; padding: 7px 0; } .btn-sub:hover { background: rgba(255, 255, 255, 0.15); color: #fff; } .status-bar { margin-top: 10px; font-size: 11px; text-align: center; color: #a0a0b5; min-height: 16px; line-height: 1.4; word-break: break-all; } .status-bar.error { color: #ff6b6b; } .status-bar.success { color: #51cf66; } .status-bar.warn { color: #fcc419; } `; const styleEl = document.createElement('style'); styleEl.textContent = STYLES; document.head.appendChild(styleEl); function sanitizeFilename(name) { if (!name) return 'Suno_Track'; return name .replace(/[\\/:*?"<>|]/g, '_') .replace(/[\r\n\t]/g, ' ') .trim() || 'Suno_Track'; } function triggerDownload(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 1500); } // 跨域安全抓取 Blob (优先 GM_xmlhttpRequest,免浏览器安全拦截) function fetchBlobSafe(url) { return new Promise((resolve) => { if (!url) return resolve(null); if (typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: (res) => { if (res.status >= 200 && res.status < 300) { resolve(res.response); } else { resolve(null); } }, onerror: () => resolve(null) }); } else { fetch(url) .then((r) => r.blob()) .then(resolve) .catch(() => resolve(null)); } }); } // 纯原生 JS 封装 16-bit PCM WAV function audioBufferToWav(buffer) { const numChannels = buffer.numberOfChannels; const sampleRate = buffer.sampleRate; const length = buffer.length * numChannels * 2; const outBuffer = new ArrayBuffer(44 + length); const view = new DataView(outBuffer); function writeString(offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } writeString(0, 'RIFF'); view.setUint32(4, 36 + length, true); writeString(8, 'WAVE'); writeString(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * numChannels * 2, true); view.setUint16(32, numChannels * 2, true); view.setUint16(34, 16, true); writeString(36, 'data'); view.setUint32(40, length, true); let offset = 44; const channels = []; for (let i = 0; i < numChannels; i++) { channels.push(buffer.getChannelData(i)); } for (let i = 0; i < buffer.length; i++) { for (let ch = 0; ch < numChannels; ch++) { let sample = Math.max(-1, Math.min(1, channels[ch][i])); sample = sample < 0 ? sample * 0x8000 : sample * 0x7fff; view.setInt16(offset, sample, true); offset += 2; } } return new Blob([outBuffer], { type: 'audio/wav' }); } // 纯客户端 lamejs 快速编码 MP3 (320kbps) function audioBufferToMp3(audioBuffer, kbps = 320) { if (typeof lamejs === 'undefined') { throw new Error('lamejs 尚未就绪'); } const channels = audioBuffer.numberOfChannels; const sampleRate = audioBuffer.sampleRate; const mp3encoder = new lamejs.Mp3Encoder(channels, sampleRate, kbps); const mp3Data = []; const sampleBlockSize = 1152; if (channels === 1) { const channelData = audioBuffer.getChannelData(0); const samples = new Int16Array(channelData.length); for (let i = 0; i < channelData.length; i++) { let s = Math.max(-1, Math.min(1, channelData[i])); samples[i] = s < 0 ? s * 0x8000 : s * 0x7fff; } for (let i = 0; i < samples.length; i += sampleBlockSize) { const chunk = samples.subarray(i, i + sampleBlockSize); const mp3buf = mp3encoder.encodeBuffer(chunk); if (mp3buf.length > 0) mp3Data.push(mp3buf); } } else { const left = audioBuffer.getChannelData(0); const right = audioBuffer.getChannelData(1); const leftSamples = new Int16Array(left.length); const rightSamples = new Int16Array(right.length); for (let i = 0; i < left.length; i++) { let sL = Math.max(-1, Math.min(1, left[i])); leftSamples[i] = sL < 0 ? sL * 0x8000 : sL * 0x7fff; let sR = Math.max(-1, Math.min(1, right[i])); rightSamples[i] = sR < 0 ? sR * 0x8000 : sR * 0x7fff; } for (let i = 0; i < leftSamples.length; i += sampleBlockSize) { const leftChunk = leftSamples.subarray(i, i + sampleBlockSize); const rightChunk = rightSamples.subarray(i, i + sampleBlockSize); const mp3buf = mp3encoder.encodeBuffer(leftChunk, rightChunk); if (mp3buf.length > 0) mp3Data.push(mp3buf); } } const endBuf = mp3encoder.flush(); if (endBuf.length > 0) mp3Data.push(endBuf); return new Blob(mp3Data, { type: 'audio/mpeg' }); } function getActiveTrackInfo() { const urlMatch = window.location.pathname.match(/\/song\/([a-f0-9\-]+)/i); let clipId = urlMatch ? urlMatch[1] : ''; const audio = document.getElementById('active-audio-play') || document.querySelector('audio'); let title = ''; let artist = ''; let coverUrl = ''; const pageH1 = document.querySelector('h1'); if (pageH1 && pageH1.innerText) { title = pageH1.innerText.trim(); } const playerLink = document.querySelector('a[href*="/song/"]'); if (playerLink) { if (!title) title = playerLink.innerText.trim(); if (!clipId) { const m = playerLink.href.match(/\/song\/([a-f0-9\-]+)/i); if (m) clipId = m[1]; } } if (!clipId && audio && audio.src) { const m = audio.src.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i); if (m) clipId = m[1]; } if (clipId) { coverUrl = `https://cdn2.suno.ai/image_large_${clipId}.jpeg`; } const artistEl = document.querySelector('a[href*="/@"]'); if (artistEl) { artist = artistEl.innerText.replace('@', '').trim(); } if (!title) { title = document.title ? document.title.replace(/\s*by\s*.*\|\s*Suno.*/i, '').trim() : 'Suno_Track'; } return { clipId, title: title || 'Suno_Track', artist: artist || '', coverUrl }; } let activeRecorder = null; let activeChunks = []; let isRecording = false; let progressTimer = null; let previousMutedState = false; // 智能自动唤醒/触发播放当前页面歌曲 async function ensureAudioPlaying() { let audio = document.getElementById('active-audio-play') || document.querySelector('audio'); if (audio && !audio.paused && audio.duration && !isNaN(audio.duration) && audio.duration > 3) { return audio; } // 1. 优先点击页面上的播放按钮 const playSelectors = [ 'button[aria-label="Play"]', 'button[aria-label*="Play" i]', 'button[aria-label*="play" i]', 'button[data-testid*="play" i]' ]; let clicked = false; for (const sel of playSelectors) { try { const btn = document.querySelector(sel); if (btn && btn.offsetParent !== null) { btn.click(); clicked = true; break; } } catch (_) {} } // 2. 寻找带有三角形播放 SVG 的按钮 if (!clicked) { const allBtns = Array.from(document.querySelectorAll('button')); for (const btn of allBtns) { const svg = btn.querySelector('svg path'); if (svg) { const d = svg.getAttribute('d') || ''; if (d.includes('M8 5v14l11-7z') || d.includes('M5 3l14 9-14 9V3z') || d.includes('M5 4.5v15')) { btn.click(); clicked = true; break; } } } } // 3. 直接调用 audio.play() if (audio) { try { await audio.play(); } catch (_) {} } // 轮询等待音频标签就绪 for (let i = 0; i < 16; i++) { await new Promise(r => setTimeout(r, 500)); audio = document.getElementById('active-audio-play') || document.querySelector('audio'); if (audio && audio.duration && !isNaN(audio.duration) && audio.duration > 3) { if (audio.paused) { try { await audio.play(); } catch (_) {} } return audio; } } return audio; } // 一键全自动流录制逻辑 async function triggerOneClickRecording(targetFormat = 'mp3') { const track = getActiveTrackInfo(); if (isRecording) { // 正在录制中被点击:提前结束并立即保存 setStatus('⏳ 正在提前截断并打包导出...', 'warn'); if (activeRecorder && activeRecorder.state !== 'inactive') { activeRecorder.stop(); } return; } setStatus('⏳ [1/3] 正在自动激活音频播放...', 'warn'); // 1. 自动触发播放并等待就绪 const audio = await ensureAudioPlaying(); if (!audio || !audio.duration || isNaN(audio.duration)) { setStatus('⚠️ 未能检测到当前歌曲音频流,请手动点击一次播放后重试!', 'error'); return; } // 2. 获取声轨流 const stream = audio.captureStream ? audio.captureStream() : audio.mozCaptureStream?.(); if (!stream) { setStatus('⚠️ 当前浏览器不支持音频流捕获 (captureStream)', 'error'); return; } const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; activeChunks = []; activeRecorder = new MediaRecorder(stream, { mimeType }); activeRecorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) activeChunks.push(e.data); }; isRecording = true; const dur = audio.duration && !isNaN(audio.duration) ? audio.duration : 210; // 静音选项控制 const muteCheckbox = document.getElementById('suno-opt-mute'); previousMutedState = audio.muted; if (muteCheckbox && muteCheckbox.checked) { audio.muted = true; } // UI 显示录制状态 const mp3Btn = document.getElementById('suno-btn-mp3'); const webmBtn = document.getElementById('suno-btn-webm'); if (targetFormat === 'mp3') { mp3Btn.classList.add('btn-recording'); mp3Btn.innerHTML = '⏹️ 提前完成并保存 MP3'; } else { webmBtn.classList.add('btn-recording'); webmBtn.innerHTML = '⏹️ 提前完成并保存 WebM'; } const pBox = document.getElementById('suno-progress-box'); const pFill = document.getElementById('suno-progress-fill'); const pLabel = document.getElementById('suno-progress-label'); const pDur = document.getElementById('suno-progress-dur'); if (pBox) pBox.style.display = 'block'; // 重置进度至歌曲开头并启动录制 audio.currentTime = 0; activeRecorder.start(200); audio.play().catch(() => {}); setStatus('🔴 [2/3] 正在全自动捕获声轨 (随播随录,随时可点⏹️提前完成)...', 'warn'); progressTimer = setInterval(() => { const cur = audio.currentTime || 0; const pct = Math.min(100, (cur / dur) * 100); if (pFill) pFill.style.width = `${pct}%`; if (pLabel) pLabel.textContent = `🔴 捕获中: ${formatTime(cur)}`; if (pDur) pDur.textContent = formatTime(dur); if (audio.ended || cur >= dur - 0.5) { clearInterval(progressTimer); if (activeRecorder && activeRecorder.state !== 'inactive') { activeRecorder.stop(); } } }, 300); activeRecorder.onstop = async () => { audio.muted = previousMutedState; await finalizeAudio(targetFormat, track); }; } function formatTime(sec) { const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s < 10 ? '0' : ''}${s}`; } // 核心:收尾并导出(支持单文件下载或全套打成 ZIP 一键下载) async function finalizeAudio(targetFormat, track) { isRecording = false; clearInterval(progressTimer); const mp3Btn = document.getElementById('suno-btn-mp3'); const webmBtn = document.getElementById('suno-btn-webm'); mp3Btn.classList.remove('btn-recording'); webmBtn.classList.remove('btn-recording'); mp3Btn.innerHTML = '🚀 一键录制存为 MP3'; webmBtn.innerHTML = '⚡ 一键录制 WebM'; const pBox = document.getElementById('suno-progress-box'); if (pBox) pBox.style.display = 'none'; if (activeChunks.length === 0) { setStatus('⚠️ 未捕获到有效音频数据,请重新点击录制!', 'error'); return; } const webmBlob = new Blob(activeChunks, { type: 'audio/webm' }); const baseName = sanitizeFilename(track.title); const audioFilename = `${baseName}.${targetFormat}`; // 检查是否勾选了“打包存封面/歌词 (ZIP)” const needZip = document.getElementById('suno-opt-zip')?.checked; let finalAudioBlob = null; if (targetFormat === 'webm') { finalAudioBlob = webmBlob; } else { try { setStatus(`⏳ [3/3] 正在转码为 ${targetFormat.toUpperCase()}...`, ''); const arrayBuffer = await webmBlob.arrayBuffer(); const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer); if (targetFormat === 'mp3') { await new Promise(r => setTimeout(r, 20)); finalAudioBlob = audioBufferToMp3(audioBuffer, 320); } else if (targetFormat === 'wav') { finalAudioBlob = audioBufferToWav(audioBuffer); } audioCtx.close().catch(() => {}); } catch (err) { console.warn('Transcode failed, fallback to WebM:', err); finalAudioBlob = webmBlob; } } if (!finalAudioBlob) finalAudioBlob = webmBlob; // 核心改进:打成单个 ZIP 压缩包下载,彻底杜绝浏览器多文件弹窗 if (needZip && typeof JSZip !== 'undefined') { try { setStatus('📦 正在打包为单个 ZIP 压缩包 (音频+封面+歌词)...', 'warn'); const zip = new JSZip(); // 1. 添加音频文件 zip.file(audioFilename, finalAudioBlob); // 2. 添加歌词文件 const lyricContainer = document.querySelector('[class*="lyrics"], [class*="prompt"]'); const lyricsText = lyricContainer ? lyricContainer.innerText : `[Track: ${track.title}]\n[Instrumental]`; zip.file(`${baseName}_lyrics.txt`, lyricsText); // 3. 安全获取封面图并添加 const coverBlob = await fetchBlobSafe(track.coverUrl); if (coverBlob) { zip.file(`${baseName}_cover.jpeg`, coverBlob); } // 生成最终 ZIP 文件 const zipBlob = await zip.generateAsync({ type: 'blob' }); triggerDownload(zipBlob, `${baseName}.zip`); setStatus(`✓ 已打包保存: ${baseName}.zip (含音频+封面+歌词,免弹窗)`, 'success'); return; } catch (zipErr) { console.error('ZIP generation failed:', zipErr); } } // 若未勾选 ZIP 或打包失败,则单下载音频文件(单文件触发不会有浏览器弹窗) triggerDownload(finalAudioBlob, audioFilename); setStatus(`✓ ${targetFormat.toUpperCase()} 已保存: ${audioFilename}`, 'success'); } function downloadCoverSolo() { const track = getActiveTrackInfo(); if (!track.clipId) return; const filename = `${sanitizeFilename(track.title)}_cover.jpeg`; fetchBlobSafe(track.coverUrl).then((blob) => { if (blob) { triggerDownload(blob, filename); setStatus(`✓ 封面已保存: ${filename}`, 'success'); } }); } function downloadLyricsSolo() { const track = getActiveTrackInfo(); const filename = `${sanitizeFilename(track.title)}_lyrics.txt`; const lyricContainer = document.querySelector('[class*="lyrics"], [class*="prompt"]'); let text = lyricContainer ? lyricContainer.innerText : `[Track: ${track.title}]\n[Instrumental]`; triggerDownload(new Blob([text], { type: 'text/plain;charset=utf-8' }), filename); setStatus(`✓ 歌词已保存: ${filename}`, 'success'); } function createWidget() { if (document.getElementById('suno-dl-widget')) return; const widget = document.createElement('div'); widget.id = 'suno-dl-widget'; widget.innerHTML = `
`; document.body.appendChild(widget); const collapseBtn = document.getElementById('suno-btn-collapse'); const iconBtn = widget.querySelector('.widget-icon-btn'); collapseBtn.addEventListener('click', (e) => { e.stopPropagation(); widget.classList.add('collapsed'); }); iconBtn.addEventListener('click', () => { widget.classList.remove('collapsed'); }); document.getElementById('suno-btn-mp3').addEventListener('click', () => triggerOneClickRecording('mp3')); document.getElementById('suno-btn-webm').addEventListener('click', () => triggerOneClickRecording('webm')); document.getElementById('suno-btn-wav').addEventListener('click', () => triggerOneClickRecording('wav')); document.getElementById('suno-btn-cover').addEventListener('click', downloadCoverSolo); document.getElementById('suno-btn-lyrics').addEventListener('click', downloadLyricsSolo); setInterval(updateWidgetTrackInfo, 1500); updateWidgetTrackInfo(); } function setStatus(text, type = '') { const el = document.getElementById('suno-status-text'); if (!el) return; el.textContent = text; el.className = 'status-bar ' + type; } function updateWidgetTrackInfo() { const track = getActiveTrackInfo(); const nameEl = document.getElementById('suno-track-name'); const authorEl = document.getElementById('suno-track-author'); const idEl = document.getElementById('suno-track-id'); const imgEl = document.getElementById('suno-track-img'); if (nameEl && track.title) { nameEl.textContent = track.title; nameEl.title = track.title; } if (authorEl && track.artist) { authorEl.textContent = `@${track.artist}`; } if (idEl) { idEl.textContent = track.clipId ? `ID: ${track.clipId.substring(0, 8)}...` : '未捕获'; } if (imgEl && track.coverUrl) { if (imgEl.src !== track.coverUrl) { imgEl.style.display = 'block'; imgEl.src = track.coverUrl; } } } window.addEventListener('load', () => { setTimeout(createWidget, 1000); }); if (document.readyState === 'complete' || document.readyState === 'interactive') { createWidget(); } })();