VocalRemover Audio Downloader2

Adds a floating panel to download audio from VocalRemover

  1. // ==UserScript==
  2. // @name VocalRemover Audio Downloader2
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description Adds a floating panel to download audio from VocalRemover
  6. // @author You
  7. // @match https://vocalremover.media.io/app/*
  8. // @grant GM_download
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Create the floating panel
  16. let panel = document.createElement('div');
  17. panel.id = 'audio-downloader-panel';
  18. panel.style.cssText = `
  19. position: fixed;
  20. bottom: 20px;
  21. right: 20px;
  22. width: 400px;
  23. height: 300px;
  24. background: #ffffff;
  25. border-radius: 12px;
  26. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  27. z-index: 9999;
  28. display: flex;
  29. flex-direction: column;
  30. overflow: hidden;
  31. font-family: Arial, sans-serif;
  32. border: 1px solid #e0e0e0;
  33. transition: all 0.3s ease;
  34. `;
  35.  
  36. // Create the header
  37. let header = document.createElement('div');
  38. header.style.cssText = `
  39. display: flex;
  40. justify-content: space-between;
  41. align-items: center;
  42. padding: 12px 16px;
  43. background: linear-gradient(135deg, #4285f4, #3367d6);
  44. color: white;
  45. cursor: move;
  46. user-select: none;
  47. border-top-left-radius: 12px;
  48. border-top-right-radius: 12px;
  49. `;
  50. header.innerHTML = `
  51. <div style="font-weight: bold; font-size: 14px;">音频下载器</div>
  52. <div style="display: flex; gap: 12px;">
  53. <button id="minimize-panel" style="background: none; border: none; color: white; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; transition: background 0.2s;">−</button>
  54. <button id="close-panel" style="background: none; border: none; color: white; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; transition: background 0.2s;">×</button>
  55. </div>
  56. `;
  57. panel.appendChild(header);
  58.  
  59. // Add hover effects to header buttons
  60. document.addEventListener('DOMContentLoaded', function() {
  61. const buttons = header.querySelectorAll('button');
  62. buttons.forEach(button => {
  63. button.addEventListener('mouseover', function() {
  64. this.style.background = 'rgba(255, 255, 255, 0.2)';
  65. });
  66. button.addEventListener('mouseout', function() {
  67. this.style.background = 'none';
  68. });
  69. });
  70. });
  71.  
  72. // Create the content area
  73. let contentArea = document.createElement('div');
  74. contentArea.style.cssText = `
  75. flex: 1;
  76. padding: 16px;
  77. overflow-y: auto;
  78. scrollbar-width: thin;
  79. scrollbar-color: #ccc #f5f5f5;
  80. `;
  81. contentArea.innerHTML = `
  82. <div id="audio-list" style="margin-bottom: 15px;">
  83. <p style="color: #666; text-align: center; margin-top: 65px; font-size: 14px;">还没有检测到音频。点击"检测音频"按钮开始。</p>
  84. </div>
  85. `;
  86. contentArea.addEventListener('scroll', function(e) {
  87. e.stopPropagation();
  88. });
  89. panel.appendChild(contentArea);
  90.  
  91. // Custom scrollbar styles
  92. const style = document.createElement('style');
  93. style.textContent = `
  94. #audio-downloader-panel ::-webkit-scrollbar {
  95. width: 6px;
  96. }
  97. #audio-downloader-panel ::-webkit-scrollbar-track {
  98. background: #f5f5f5;
  99. border-radius: 3px;
  100. }
  101. #audio-downloader-panel ::-webkit-scrollbar-thumb {
  102. background-color: #ccc;
  103. border-radius: 3px;
  104. }
  105. #audio-downloader-panel .download-button {
  106. transition: background-color 0.2s ease;
  107. }
  108. #audio-downloader-panel .download-button:hover {
  109. background-color: #3c9f40 !important;
  110. }
  111. #detect-audio {
  112. transition: background-color 0.2s ease, transform 0.1s ease;
  113. }
  114. #detect-audio:hover {
  115. background-color: #3367d6 !important;
  116. }
  117. #detect-audio:active {
  118. transform: scale(0.98);
  119. }
  120. .audio-item {
  121. transition: transform 0.1s ease;
  122. }
  123. .audio-item:hover {
  124. transform: translateY(-2px);
  125. box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  126. }
  127. `;
  128. document.head.appendChild(style);
  129.  
  130. // Create the footer with the detect button
  131. let footer = document.createElement('div');
  132. footer.style.cssText = `
  133. padding: 12px 16px;
  134. border-top: 1px solid #e0e0e0;
  135. display: flex;
  136. justify-content: space-between;
  137. background: #f8f8f8;
  138. border-bottom-left-radius: 12px;
  139. border-bottom-right-radius: 12px;
  140. `;
  141. footer.innerHTML = `
  142. <button id="detect-audio" style="
  143. padding: 9px 18px;
  144. background: #4285f4;
  145. color: white;
  146. border: none;
  147. border-radius: 6px;
  148. cursor: pointer;
  149. font-weight: bold;
  150. font-size: 13px;
  151. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  152. ">检测音频</button>
  153. <span id="status-message" style="color: #666; font-size: 12px; align-self: center; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></span>
  154. `;
  155. panel.appendChild(footer);
  156.  
  157. // Add the panel to the document
  158. document.body.appendChild(panel);
  159.  
  160. // Create the minimized panel
  161. let minimizedPanel = document.createElement('div');
  162. minimizedPanel.id = 'minimized-audio-downloader';
  163. minimizedPanel.style.cssText = `
  164. position: fixed;
  165. bottom: 20px;
  166. right: 20px;
  167. background: linear-gradient(135deg, #4285f4, #3367d6);
  168. color: white;
  169. padding: 10px 18px;
  170. border-radius: 50px;
  171. box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
  172. z-index: 9999;
  173. cursor: pointer;
  174. display: none;
  175. font-family: Arial, sans-serif;
  176. font-weight: bold;
  177. font-size: 13px;
  178. transition: all 0.3s ease;
  179. `;
  180. minimizedPanel.innerHTML = `<span>音频下载器</span>`;
  181.  
  182. // Add hover effect to minimized panel
  183. minimizedPanel.addEventListener('mouseover', function() {
  184. this.style.transform = 'translateY(-2px)';
  185. this.style.boxShadow = '0 5px 12px rgba(0, 0, 0, 0.25)';
  186. });
  187. minimizedPanel.addEventListener('mouseout', function() {
  188. this.style.transform = 'translateY(0)';
  189. this.style.boxShadow = '0 3px 10px rgba(0, 0, 0, 0.2)';
  190. });
  191.  
  192. document.body.appendChild(minimizedPanel);
  193.  
  194. // Status update function
  195. function updateStatus(message, isError = false) {
  196. const statusElement = document.getElementById('status-message');
  197. if (statusElement) {
  198. statusElement.textContent = message;
  199. statusElement.style.color = isError ? '#f44336' : '#666';
  200. statusElement.title = message; // Add title for longer messages
  201. }
  202. }
  203.  
  204. // Function to format audio file names
  205. function formatAudioName(name) {
  206. if (!name) return 'audio.mp3';
  207.  
  208. // Clean the name
  209. let cleanName = name.split('/').pop().split('?')[0];
  210.  
  211. // Add extension if missing
  212. if (!cleanName.includes('.')) {
  213. cleanName += '.mp3';
  214. }
  215.  
  216. // Try to make it more readable
  217. cleanName = cleanName
  218. .replace(/[_-]+/g, ' ')
  219. .replace(/(%20)+/g, ' ')
  220. .replace(/\s+/g, ' ');
  221.  
  222. // Capitalize first letter of each word
  223. cleanName = cleanName.split(' ')
  224. .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
  225. .join(' ');
  226.  
  227. return cleanName;
  228. }
  229.  
  230. // Add detected audio to the list
  231. function addAudioToList(name, url) {
  232. const audioList = document.getElementById('audio-list');
  233. if (!audioList) return;
  234.  
  235. // Check if the URL is already in the list
  236. const existingItems = audioList.querySelectorAll('.download-button');
  237. for (let item of existingItems) {
  238. if (item.getAttribute('data-url') === url) {
  239. // Already in the list, don't add duplicate
  240. return;
  241. }
  242. }
  243.  
  244. // Clear the "no audio detected" message if present
  245. const noAudioMessage = audioList.querySelector('p');
  246. if (noAudioMessage) {
  247. audioList.innerHTML = '';
  248. }
  249.  
  250. const audioItem = document.createElement('div');
  251. audioItem.className = 'audio-item';
  252. audioItem.style.cssText = `
  253. display: flex;
  254. justify-content: space-between;
  255. align-items: center;
  256. padding: 12px;
  257. background: #f5f5f5;
  258. border-radius: 8px;
  259. margin-bottom: 10px;
  260. border: 1px solid #e8e8e8;
  261. transition: all 0.2s ease;
  262. `;
  263.  
  264. const fileName = formatAudioName(name);
  265.  
  266. audioItem.innerHTML = `
  267. <div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 250px; color: #333; font-size: 13px;" title="${fileName}">${fileName}</div>
  268. <button class="download-button" data-url="${url}" data-filename="${fileName}" style="
  269. background: #4CAF50;
  270. color: white;
  271. border: none;
  272. border-radius: 6px;
  273. padding: 6px 12px;
  274. cursor: pointer;
  275. font-size: 12px;
  276. font-weight: bold;
  277. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  278. ">下载</button>
  279. `;
  280.  
  281. audioList.appendChild(audioItem);
  282.  
  283. // Add event listener to the download button
  284. const downloadButton = audioItem.querySelector('.download-button');
  285. downloadButton.addEventListener('click', function(e) {
  286. e.preventDefault();
  287. const url = this.getAttribute('data-url');
  288. const filename = this.getAttribute('data-filename');
  289. triggerDownload(url, filename);
  290. });
  291. }
  292.  
  293. // Make the panel draggable
  294. let isDragging = false;
  295. let offsetX, offsetY;
  296.  
  297. header.addEventListener('mousedown', function(e) {
  298. isDragging = true;
  299. offsetX = e.clientX - panel.getBoundingClientRect().left;
  300. offsetY = e.clientY - panel.getBoundingClientRect().top;
  301. });
  302.  
  303. document.addEventListener('mousemove', function(e) {
  304. if (!isDragging) return;
  305.  
  306. panel.style.left = (e.clientX - offsetX) + 'px';
  307. panel.style.top = (e.clientY - offsetY) + 'px';
  308. panel.style.right = 'auto';
  309. panel.style.bottom = 'auto';
  310. });
  311.  
  312. document.addEventListener('mouseup', function() {
  313. isDragging = false;
  314. });
  315.  
  316. // Panel control functions
  317. document.getElementById('minimize-panel').addEventListener('click', function(e) {
  318. e.stopPropagation();
  319. panel.style.display = 'none';
  320. minimizedPanel.style.display = 'block';
  321. });
  322.  
  323. document.getElementById('close-panel').addEventListener('click', function(e) {
  324. // e.stopPropagation();
  325. // panel.style.display = 'none';
  326. // minimizedPanel.style.display = 'none';
  327. });
  328.  
  329. minimizedPanel.addEventListener('click', function() {
  330. minimizedPanel.style.display = 'none';
  331. panel.style.display = 'flex';
  332. });
  333.  
  334. // ==================== Audio Detection Logic ====================
  335.  
  336. // Intercept XHR requests to capture audio URLs
  337. function setupXHRInterceptor() {
  338. const originalOpen = XMLHttpRequest.prototype.open;
  339. const originalSend = XMLHttpRequest.prototype.send;
  340.  
  341. XMLHttpRequest.prototype.open = function(method, url) {
  342. this._url = url;
  343. return originalOpen.apply(this, arguments);
  344. };
  345.  
  346. XMLHttpRequest.prototype.send = function() {
  347. this.addEventListener('load', function() {
  348. // Check if response is audio
  349. const contentType = this.getResponseHeader('Content-Type');
  350. if (contentType && (
  351. contentType.includes('audio') ||
  352. this._url.includes('.mp3') ||
  353. this._url.includes('.wav') ||
  354. this._url.includes('audio')
  355. )) {
  356. updateStatus(`检测到音频: ${formatAudioName(this._url)}`);
  357. handleAudioUrl(this._url);
  358. }
  359. });
  360. return originalSend.apply(this, arguments);
  361. };
  362.  
  363. updateStatus('已设置请求拦截器');
  364. }
  365.  
  366. // Check network requests for audio files
  367. function checkNetworkForAudio() {
  368. if (!window.performance || !window.performance.getEntries) {
  369. updateStatus('此浏览器不支持Performance API', true);
  370. return;
  371. }
  372.  
  373. const resources = window.performance.getEntries();
  374. const audioResources = resources.filter(resource => {
  375. return resource.name.includes('.mp3') ||
  376. resource.name.includes('.wav') ||
  377. resource.name.includes('audio') ||
  378. (resource.initiatorType === 'xmlhttprequest' &&
  379. resource.name.includes('blob'));
  380. });
  381.  
  382. if (audioResources.length > 0) {
  383. updateStatus(`找到 ${audioResources.length} 个可能的音频资源`);
  384. audioResources.forEach(resource => {
  385. if (!resource.name.startsWith('blob:')) {
  386. handleAudioUrl(resource.name);
  387. }
  388. });
  389. return true;
  390. } else {
  391. updateStatus('未找到网络中的音频资源');
  392. return false;
  393. }
  394. }
  395.  
  396. // Handle audio URL
  397. function handleAudioUrl(url) {
  398. if (url) {
  399. const formattedName = formatAudioName(url);
  400. addAudioToList(formattedName, url);
  401. }
  402. }
  403.  
  404. // Check audio elements on the page
  405. function checkAudioElements() {
  406. const audioElements = document.querySelectorAll('audio');
  407. if (audioElements.length > 0) {
  408. updateStatus(`找到 ${audioElements.length} 个音频元素`);
  409. audioElements.forEach(audio => {
  410. if (audio.src) {
  411. if (audio.src.startsWith('blob:')) {
  412. // Try to get a meaningful name from page context
  413. let pageName = document.title || '';
  414. pageName = pageName.replace('VocalRemover', '').trim();
  415. const audioName = pageName || 'audio.mp3';
  416. addAudioToList(audioName, audio.src);
  417. } else {
  418. handleAudioUrl(audio.src);
  419. }
  420. }
  421. });
  422. return true;
  423. }
  424. return false;
  425. }
  426.  
  427. // Check for audio in the application
  428. function detectAudio() {
  429. // Add a loading indicator to the button
  430. const detectButton = document.getElementById('detect-audio');
  431. const originalText = detectButton.textContent;
  432. detectButton.textContent = '正在检测...';
  433. detectButton.style.pointerEvents = 'none';
  434. detectButton.style.opacity = '0.7';
  435.  
  436. updateStatus('正在检测音频...');
  437.  
  438. // Use setTimeout to allow the button state to update first
  439. setTimeout(() => {
  440. // Try checking audio elements first
  441. const foundAudioElements = checkAudioElements();
  442.  
  443. // Then check network requests
  444. const foundNetworkAudio = checkNetworkForAudio();
  445.  
  446. // Setup interceptor for future requests
  447. setupXHRInterceptor();
  448.  
  449. // Try to trigger audio playback
  450. const playButton = document.querySelector('.play-button');
  451. if (playButton) {
  452. updateStatus('找到播放按钮,点击以触发音频加载...');
  453. playButton.click();
  454. }
  455.  
  456. // Check for wave elements that might contain audio data
  457. const waveElements = document.querySelectorAll('wave');
  458. if (waveElements.length > 0) {
  459. updateStatus('找到波形图元素,可能包含音频数据');
  460. }
  461.  
  462. if (!foundAudioElements && !foundNetworkAudio) {
  463. // If nothing found, give feedback
  464. updateStatus('尚未找到音频。尝试播放页面中的音频后再次检测。');
  465. }
  466.  
  467. // Reset button state
  468. detectButton.textContent = originalText;
  469. detectButton.style.pointerEvents = '';
  470. detectButton.style.opacity = '';
  471. }, 100);
  472. }
  473.  
  474. // Trigger file download using GM_download if available, or fallback to regular method
  475. function triggerDownload(url, filename) {
  476. updateStatus(`准备下载: ${filename}`);
  477.  
  478. if (typeof GM_download !== 'undefined') {
  479. // Use GM_download to download file directly
  480. GM_download({
  481. url: url,
  482. name: filename,
  483. onload: function() {
  484. updateStatus(`成功下载: ${filename}`);
  485. },
  486. onerror: function(error) {
  487. updateStatus(`下载失败: ${error}`, true);
  488. // Fall back to traditional method
  489. traditionalDownload(url, filename);
  490. }
  491. });
  492. } else {
  493. // Use traditional method
  494. traditionalDownload(url, filename);
  495. }
  496. }
  497.  
  498. // Traditional download method
  499. function traditionalDownload(url, filename) {
  500. // For blob URLs we need to fetch them first
  501. if (url.startsWith('blob:')) {
  502. fetch(url)
  503. .then(response => response.blob())
  504. .then(blob => {
  505. const blobUrl = URL.createObjectURL(blob);
  506. downloadWithLink(blobUrl, filename);
  507. URL.revokeObjectURL(blobUrl);
  508. })
  509. .catch(error => {
  510. updateStatus(`下载失败: ${error.message}`, true);
  511. });
  512. } else {
  513. downloadWithLink(url, filename);
  514. }
  515. }
  516.  
  517. // Download using a temporary anchor element
  518. function downloadWithLink(url, filename) {
  519. const a = document.createElement('a');
  520. a.href = url;
  521. a.download = filename;
  522. a.style.display = 'none';
  523. a.target = '_blank'; // Add target blank to avoid opening in the current page
  524.  
  525. document.body.appendChild(a);
  526. a.click();
  527.  
  528. setTimeout(() => {
  529. document.body.removeChild(a);
  530. updateStatus(`已启动下载: ${filename}`);
  531. }, 100);
  532. }
  533.  
  534. // Attach event listener to the detect button
  535. document.getElementById('detect-audio').addEventListener('click', function(e) {
  536. e.preventDefault(); // Prevent any default action
  537. e.stopPropagation(); // Stop propagation to prevent jitter
  538. detectAudio();
  539. });
  540. })();