// ==UserScript== // @name [红狐播放器]音视频下载工具(B站专享版) // @namespace http://tampermonkey.net/ // @version 0.1.1 // @description 适用于从B站跳转的内容,抓取页面中音视频链接并分别下载(推荐配合 ffmpeg 合并) // @match https://rdfplayer.mrgaocloud.com/player/* // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_notification // @grant GM_registerMenuCommand // @icon data:image/svg+xml,🦊 // @license GPL License // @connect * // @downloadURL none // ==/UserScript== (function () { 'use strict'; const CHECK_INTERVAL = 1000; const MAX_RETRY = 3; let audioURL = null; let videoURL = null; let downloadBtnVisible = localStorage.getItem('downloadBtnVisible') !== 'false'; // 默认 true GM_registerMenuCommand(downloadBtnVisible ? '🔕 隐藏下载按钮' : '🔔 显示下载按钮', toggleDownloadButton); const init = () => { if (downloadBtnVisible) { addDownloadButton(); } startElementObserver(); }; function toggleDownloadButton() { downloadBtnVisible = !downloadBtnVisible; localStorage.setItem('downloadBtnVisible', downloadBtnVisible); const existing = document.getElementById('directDownloadBtn'); if (existing) { existing.remove(); } if (downloadBtnVisible) { addDownloadButton(); GM_notification('下载按钮已显示 ✅'); } else { GM_notification('下载按钮已隐藏 ❌'); } // 更新菜单项显示 location.reload(); // 简单做法:刷新页面,更新菜单项标题 } const addDownloadButton = () => { const btn = document.createElement('button'); btn.id = 'directDownloadBtn'; btn.textContent = '⏬ 下载音视频'; Object.assign(btn.style, { position: 'fixed', top: '20px', right: '20px', zIndex: 9999, padding: '10px 15px', background: '#9E9E9E', color: 'white', border: 'none', borderRadius: '4px', cursor: 'not-allowed', boxShadow: '0 2px 6px rgba(0,0,0,0.3)' }); btn.disabled = true; btn.onclick = handleDownload; document.body.appendChild(btn); }; const startElementObserver = () => { const interval = setInterval(() => { const elements = Array.from(document.querySelectorAll('video[src*=".m4s"]')); if (elements.length >= 2) { const url1 = elements[0].src; const url2 = elements[1].src; // 简单通过 URL 字符串中是否包含 "video"、"audio" 或者带宽判断 if (url1.includes('bw=') && url2.includes('bw=')) { const bw1 = getBandwidth(url1); const bw2 = getBandwidth(url2); if (bw1 > bw2) { videoURL = url1; audioURL = url2; } else { videoURL = url2; audioURL = url1; } } else { // 若不含带宽信息,保底用顺序判断 videoURL = url1; audioURL = url2; } console.log('[🎞️ 视频 URL]', videoURL); console.log('[🔊 音频 URL]', audioURL); const btn = document.getElementById('directDownloadBtn'); btn.disabled = false; btn.style.background = '#4CAF50'; btn.style.cursor = 'pointer'; } }, CHECK_INTERVAL); }; const getBandwidth = (url) => { const match = url.match(/bw=(\d+)/); return match ? parseInt(match[1], 10) : 0; }; // 新增:获取视频标题 const getVideoTitle = () => { let titleElem = document.querySelector('.v-title'); let title = titleElem ? titleElem.textContent.trim() : ''; if (!title) { title = document.title || 'unknown_title'; } // 仅保留 ffmpeg/文件系统安全字符 title = title .normalize('NFKC') // Unicode 规范化(防止奇怪合成字符) .replace(/[^\w\u4e00-\u9fa5\s\-()\[\]【】()]/g, '') // 只保留中英文、数字、空格、安全符号 .replace(/\s+/g, ' ') // 合并空格 .trim(); // 去头尾空格 // 限制长度 if (title.length > 50) { title = title.substring(0, 50); } return title || 'unknown_title'; }; const handleDownload = async () => { if (!audioURL || !videoURL) return; const btn = document.getElementById('directDownloadBtn'); try { btn.textContent = '🔄 下载中...'; const [audioData, videoData] = await Promise.all([ fetchWithRetry(audioURL), fetchWithRetry(videoURL) ]); // 获取标题(你前面提到的 v-title 元素) let title = document.querySelector('.v-title')?.textContent.trim(); if (!title) { title = `文件_${Date.now()}`; } // 替换非法文件名字符 const safeTitle = title.replace(/[\/\\:*?"<>|]/g, '_'); const videoFilename = `video_${safeTitle}.mp4`; const audioFilename = `audio_${safeTitle}.wav`; const outputFilename = `${safeTitle}.mp4`; triggerDownload(new Blob([videoData], { type: 'video/mp4' }), videoFilename); triggerDownload(new Blob([audioData], { type: 'audio/wav' }), audioFilename); GM_notification('✅ 下载完成,准备合并 ffmpeg 命令'); // ⏬ 弹出提示框:ffmpeg 命令 const command = `ffmpeg -i "${videoFilename}" -i "${audioFilename}" -c:v copy -c:a aac "${outputFilename}"`; setTimeout(() => { prompt("✅ 请复制以下 ffmpeg 命令去终端执行合并:", command); }, 500); // 等待下载完成后提示 } catch (err) { GM_notification(`❌ 下载失败: ${err}`); console.error(err); } finally { btn.textContent = '⏬ 下载音视频(分离)'; } }; const fetchWithRetry = async (url, retry = MAX_RETRY) => { for (let i = 0; i < retry; i++) { try { const data = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Referer': window.location.href, 'User-Agent': navigator.userAgent }, responseType: 'arraybuffer', onload: (res) => res.status === 200 ? resolve(res.response) : reject(), onerror: reject }); }); return new Uint8Array(data); } catch (err) { if (i === retry - 1) throw new Error(`下载失败: ${url}`); await new Promise(r => setTimeout(r, 1000 * (i + 1))); } } }; const triggerDownload = (blob, filename) => { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); setTimeout(() => URL.revokeObjectURL(link.href), 1000); }; window.addEventListener('load', init); })();