// ==UserScript== // @name Suno 歌曲下载助手 (一键全自动流捕获: WebM / 320k MP3 / WAV / 封面 / 歌词) // @version 3.1.0 // @description 彻底抛弃易受限的 CDN 方式!一键自动唤醒播放、声轨免损录制、320k MP3 自动转码与保存,支持静音录制与提前完成。100% 免拦截无报错。 // @author Spectromancer Team // @match https://suno.com/* // @icon https://suno.com/favicon.ico // @grant GM_download // @grant GM_addStyle // @grant GM_notification // @grant GM_setClipboard // @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); } // 纯原生 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. 如果没找到带 aria-label 的,寻找带有三角形播放 svg path 的按钮 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 (_) {} } // 轮询等待音频标签就绪(最多等待 8 秒) 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(); const filename = `${sanitizeFilename(track.title)}.${targetFormat}`; 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, filename); }; } function formatTime(sec) { const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s < 10 ? '0' : ''}${s}`; } async function finalizeAudio(targetFormat, filename) { 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 autoMeta = document.getElementById('suno-opt-meta')?.checked; if (autoMeta) { downloadCoverAuto(); downloadLyricsAuto(); } if (targetFormat === 'webm') { triggerDownload(webmBlob, filename); setStatus(`✓ WebM 原音轨已保存 (${(webmBlob.size/1024/1024).toFixed(2)}MB)`, 'success'); return; } try { setStatus('⏳ [3/3] 正在转码为 320kbps 高保真 MP3...', ''); 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)); const mp3Blob = audioBufferToMp3(audioBuffer, 320); audioCtx.close().catch(() => {}); triggerDownload(mp3Blob, filename); setStatus(`✓ MP3 已保存: ${filename} (${(mp3Blob.size/1024/1024).toFixed(2)}MB)`, 'success'); } else if (targetFormat === 'wav') { const wavBlob = audioBufferToWav(audioBuffer); audioCtx.close().catch(() => {}); triggerDownload(wavBlob, filename); setStatus(`✓ WAV 已保存: ${filename}`, 'success'); } } catch (err) { console.error(err); // 转码失败时直接下载原始 WebM 作为保底 const fallbackName = filename.replace(/\.(mp3|wav)$/i, '.webm'); triggerDownload(webmBlob, fallbackName); setStatus(`⚠️ 转码失败,已自动保存高质量 WebM 原声: ${fallbackName}`, 'warn'); } } function downloadCoverAuto() { const track = getActiveTrackInfo(); if (!track.clipId) return; const filename = `${sanitizeFilename(track.title)}_cover.jpeg`; const a = document.createElement('a'); a.href = track.coverUrl; a.download = filename; a.target = '_blank'; a.click(); } function downloadLyricsAuto() { 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); } 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', downloadCoverAuto); document.getElementById('suno-btn-lyrics').addEventListener('click', downloadLyricsAuto); 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(); } })();