// ==UserScript==
// @name [红狐播放器]音视频下载工具(B站专享版)
// @namespace http://tampermonkey.net/
// @version 0.1.2
// @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,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'><text x='0' y='24' font-size='24'>🦊</text></svg>
// @license GPL License
// @connect *
// ==/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';
}
title = title
.normalize('NFKC') // 规范化
.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 元素)
const safeTitle = getVideoTitle();
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);
})();