YouTube Direct Downloader

Add a custom download button and provide options to download the video or audio directly from the YouTube page.

  1. // ==UserScript==
  2. // @name YouTube Direct Downloader
  3. // @description Add a custom download button and provide options to download the video or audio directly from the YouTube page.
  4. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  5. // @version 1.5
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://www.youtube.com/*
  11. // @match https://youtube.com/*
  12. // @grant GM.xmlHttpRequest
  13. // @grant GM_download
  14. // @grant GM.download
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @connect api.mp3youtube.cc
  18. // @connect iframe.y2meta-uk.com
  19. // @connect *
  20. // @run-at document-end
  21. // ==/UserScript==
  22.  
  23. (function() {
  24. 'use strict';
  25.  
  26. let lastSelectedFormat = GM_getValue('lastSelectedFormat', 'video');
  27. let lastSelectedVideoQuality = GM_getValue('lastSelectedVideoQuality', '1080');
  28. let lastSelectedAudioBitrate = GM_getValue('lastSelectedAudioBitrate', '320');
  29.  
  30. const API_KEY_URL = 'https://api.mp3youtube.cc/v2/sanity/key';
  31. const API_CONVERT_URL = 'https://api.mp3youtube.cc/v2/converter';
  32. const REQUEST_HEADERS = {
  33. "Content-Type": "application/json",
  34. "Origin": "https://iframe.y2meta-uk.com",
  35. "Accept": "*/*",
  36. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
  37. };
  38. const style = document.createElement('style');
  39. style.textContent = `
  40. .ytddl-download-btn {
  41. width: 36px;
  42. height: 36px;
  43. border-radius: 50%;
  44. display: flex;
  45. align-items: center;
  46. justify-content: center;
  47. cursor: pointer;
  48. margin-left: 8px;
  49. transition: background-color 0.2s;
  50. }
  51. html[dark] .ytddl-download-btn {
  52. background-color: #ffffff1a;
  53. }
  54. html:not([dark]) .ytddl-download-btn {
  55. background-color: #0000000d;
  56. }
  57. html[dark] .ytddl-download-btn:hover {
  58. background-color: #ffffff33;
  59. }
  60. html:not([dark]) .ytddl-download-btn:hover {
  61. background-color: #00000014;
  62. }
  63. .ytddl-download-btn svg {
  64. width: 18px;
  65. height: 18px;
  66. }
  67. html[dark] .ytddl-download-btn svg {
  68. fill: var(--yt-spec-text-primary, #fff);
  69. }
  70. html:not([dark]) .ytddl-download-btn svg {
  71. fill: var(--yt-spec-text-primary, #030303);
  72. }
  73. .ytddl-dialog {
  74. position: fixed;
  75. top: 50%;
  76. left: 50%;
  77. transform: translate(-50%, -50%);
  78. background: #000000;
  79. color: #e1e1e1;
  80. border-radius: 12px;
  81. box-shadow: 0 0 0 1px rgba(225,225,225,.1), 0 2px 4px 1px rgba(225,225,225,.18);
  82. font-family: 'IBM Plex Mono', 'Noto Sans Mono Variable', 'Noto Sans Mono', monospace;
  83. width: 400px;
  84. z-index: 9999;
  85. padding: 16px;
  86. }
  87. .ytddl-backdrop {
  88. position: fixed;
  89. top: 0;
  90. left: 0;
  91. width: 100%;
  92. height: 100%;
  93. background: rgba(0, 0, 0, 0.5);
  94. z-index: 9998;
  95. }
  96. .ytddl-dialog h3 {
  97. margin: 0 0 16px 0;
  98. font-size: 18px;
  99. font-weight: 700;
  100. }
  101. .quality-options {
  102. display: grid;
  103. grid-template-columns: repeat(3, 1fr);
  104. gap: 8px;
  105. margin-bottom: 16px;
  106. }
  107. .quality-option {
  108. display: flex;
  109. align-items: center;
  110. padding: 8px;
  111. cursor: pointer;
  112. border-radius: 6px;
  113. }
  114. .quality-option:hover {
  115. background: #191919;
  116. }
  117. .quality-option input[type="radio"] {
  118. margin-right: 8px;
  119. }
  120. .download-status {
  121. text-align: center;
  122. margin: 16px 0;
  123. font-size: 12px;
  124. display: none;
  125. color: #1ed760;
  126. }
  127. .button-container {
  128. display: flex;
  129. justify-content: center;
  130. gap: 8px;
  131. margin-top: 16px;
  132. }
  133. .ytddl-button {
  134. background: transparent;
  135. border: 1px solid #e1e1e1;
  136. color: #e1e1e1;
  137. font-size: 14px;
  138. font-weight: 500;
  139. padding: 8px 16px;
  140. border-radius: 18px;
  141. cursor: pointer;
  142. font-family: inherit;
  143. transition: all 0.2s;
  144. }
  145. .ytddl-button:hover {
  146. background: #1ed760;
  147. border-color: #1ed760;
  148. color: #000000;
  149. }
  150. .ytddl-button.cancel:hover {
  151. background: #f3727f;
  152. border-color: #f3727f;
  153. color: #000000;
  154. }
  155. .format-selector {
  156. margin-bottom: 16px;
  157. display: flex;
  158. gap: 8px;
  159. justify-content: center;
  160. }
  161. .format-button {
  162. background: transparent;
  163. border: 1px solid #e1e1e1;
  164. color: #e1e1e1;
  165. padding: 6px 12px;
  166. border-radius: 14px;
  167. cursor: pointer;
  168. font-family: inherit;
  169. font-size: 12px;
  170. transition: all 0.2s ease;
  171. }
  172. .format-button:hover {
  173. background: #808080;
  174. color: #000000;
  175. }
  176. .format-button.selected {
  177. background: #1ed760;
  178. border-color: #1ed760;
  179. color: #000000;
  180. }
  181. .ytddl-overlay {
  182. position: fixed;
  183. top: 20px;
  184. right: 20px;
  185. background: rgba(0, 0, 0, 0.9);
  186. color: #e1e1e1;
  187. border-radius: 8px;
  188. padding: 16px;
  189. width: 350px;
  190. max-width: 350px;
  191. z-index: 10000;
  192. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  193. font-size: 14px;
  194. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  195. border: 1px solid rgba(255, 255, 255, 0.1);
  196. backdrop-filter: blur(10px);
  197. opacity: 0;
  198. transform: translateX(100%);
  199. transition: all 0.3s ease;
  200. }
  201. .ytddl-overlay.show {
  202. opacity: 1;
  203. transform: translateX(0);
  204. }
  205. .ytddl-overlay-content {
  206. line-height: 1.5;
  207. }
  208. .ytddl-overlay-status {
  209. margin-bottom: 8px;
  210. color: #1ed760;
  211. font-weight: 500;
  212. }
  213. .ytddl-overlay-details {
  214. color: #ccc;
  215. font-size: 13px;
  216. margin-bottom: 12px;
  217. }
  218. .ytddl-overlay-file-info {
  219. display: flex;
  220. justify-content: space-between;
  221. margin-bottom: 8px;
  222. font-size: 12px;
  223. }
  224. .ytddl-overlay-size {
  225. color: #1ed760;
  226. font-weight: 500;
  227. }
  228. .ytddl-overlay-speed {
  229. color: #ffa500;
  230. font-weight: 500;
  231. }
  232. .ytddl-overlay-error {
  233. color: #ff6b6b;
  234. }
  235. .ytddl-overlay-success {
  236. color: #1ed760;
  237. }
  238. `;
  239. document.head.appendChild(style);
  240.  
  241. let currentOverlay = null;
  242.  
  243. function createOverlay() {
  244. if (currentOverlay) {
  245. removeOverlay();
  246. } const overlay = document.createElement('div');
  247. overlay.className = 'ytddl-overlay';
  248. const content = document.createElement('div');
  249. content.className = 'ytddl-overlay-content';
  250. const status = document.createElement('div');
  251. status.className = 'ytddl-overlay-status';
  252. status.textContent = 'Initializing...';
  253. const details = document.createElement('div');
  254. details.className = 'ytddl-overlay-details';
  255. details.textContent = 'Preparing download request';
  256. const fileInfoContainer = document.createElement('div');
  257. fileInfoContainer.className = 'ytddl-overlay-file-info';
  258. const sizeElement = document.createElement('div');
  259. sizeElement.className = 'ytddl-overlay-size';
  260. sizeElement.textContent = 'Size: Calculating...';
  261. const speedElement = document.createElement('div');
  262. speedElement.className = 'ytddl-overlay-speed';
  263. speedElement.textContent = 'Speed: -';
  264. fileInfoContainer.appendChild(sizeElement);
  265. fileInfoContainer.appendChild(speedElement);
  266. content.appendChild(status);
  267. content.appendChild(details);
  268. content.appendChild(fileInfoContainer);
  269. overlay.appendChild(content);
  270. overlay.addEventListener('click', function(e) {
  271. if (e.target === overlay) {
  272. removeOverlay();
  273. }
  274. });
  275. document.body.appendChild(overlay);
  276. setTimeout(() => {
  277. overlay.classList.add('show');
  278. }, 100);
  279. currentOverlay = overlay;
  280. return overlay;
  281. }
  282.  
  283. function updateOverlay(status, details, fileSize = null, downloadSpeed = null, isError = false, isSuccess = false) {
  284. if (!currentOverlay) return;
  285. const statusEl = currentOverlay.querySelector('.ytddl-overlay-status');
  286. const detailsEl = currentOverlay.querySelector('.ytddl-overlay-details');
  287. const sizeEl = currentOverlay.querySelector('.ytddl-overlay-size');
  288. const speedEl = currentOverlay.querySelector('.ytddl-overlay-speed');
  289. if (statusEl) {
  290. statusEl.textContent = status;
  291. statusEl.className = 'ytddl-overlay-status';
  292. if (isError) statusEl.classList.add('ytddl-overlay-error');
  293. if (isSuccess) statusEl.classList.add('ytddl-overlay-success');
  294. }
  295. if (detailsEl) {
  296. detailsEl.textContent = details;
  297. }
  298. if (sizeEl) {
  299. if (fileSize !== null) {
  300. sizeEl.textContent = `Size: ${fileSize}`;
  301. sizeEl.style.display = 'block';
  302. } else {
  303. sizeEl.style.display = 'none';
  304. }
  305. }
  306. if (speedEl) {
  307. if (downloadSpeed !== null) {
  308. speedEl.textContent = `Speed: ${downloadSpeed}`;
  309. speedEl.style.display = 'block';
  310. } else {
  311. speedEl.style.display = 'none';
  312. }
  313. }
  314. currentOverlay.offsetHeight;
  315. }
  316.  
  317. function removeOverlay() {
  318. if (currentOverlay) {
  319. currentOverlay.classList.remove('show');
  320. setTimeout(() => {
  321. if (currentOverlay && currentOverlay.parentNode) {
  322. currentOverlay.parentNode.removeChild(currentOverlay);
  323. }
  324. currentOverlay = null;
  325. }, 300);
  326. }
  327. }
  328. function formatBytes(bytes) {
  329. if (bytes === 0) return '0 Bytes';
  330. const k = 1024;
  331. const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  332. const i = Math.floor(Math.log(bytes) / Math.log(k));
  333. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  334. }
  335. function truncateTitle(title, maxLength = 50) {
  336. if (!title || title.length <= maxLength) return title;
  337. return title.substring(0, maxLength - 3) + '...';
  338. }
  339.  
  340. function triggerDirectDownload(url, filename) {
  341. let downloadStartTime = Date.now();
  342. updateOverlay('Validating download URL', 'Testing download link...', null, null);
  343. GM.xmlHttpRequest({
  344. method: 'HEAD',
  345. url: url,
  346. headers: {
  347. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
  348. },
  349. onload: function(response) {
  350. console.log('URL Test Response:', response.status, response.statusText);
  351. console.log('Content-Length:', response.responseHeaders.match(/content-length:\s*(\d+)/i));
  352. if (response.status === 200 || response.status === 206) {
  353. fetchAndDownload(url, filename, downloadStartTime);
  354. } else {
  355. updateOverlay(
  356. 'Download failed',
  357. `Invalid download URL (Status: ${response.status})`,
  358. null,
  359. null, true
  360. );
  361. setTimeout(removeOverlay, 2500);
  362. }
  363. },
  364. onerror: function(error) {
  365. console.error('URL validation failed:', error);
  366. updateOverlay(
  367. 'Download failed',
  368. 'Cannot access download URL - may be expired or invalid',
  369. null,
  370. null, true
  371. );
  372. setTimeout(removeOverlay, 2500);
  373. }
  374. });
  375. }
  376. function fetchAndDownload(url, filename, downloadStartTime) {
  377. updateOverlay('Starting download', 'Connecting to server...', '0 B', '0 B/s');
  378. console.log('=== FETCH AND DOWNLOAD ===');
  379. console.log('URL:', url);
  380. console.log('Filename:', filename);
  381. console.log('Method: GM.xmlHttpRequest with responseType blob');
  382. console.log('Start time:', new Date(downloadStartTime).toISOString());
  383. console.log('==========================');
  384. let totalSize = 0;
  385. let downloadedSize = 0;
  386. let lastUpdateTime = 0;
  387. const UPDATE_INTERVAL = 250;
  388. GM.xmlHttpRequest({
  389. method: 'GET',
  390. url: url,
  391. responseType: 'blob',
  392. headers: {
  393. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  394. "Referer": "https://iframe.y2meta-uk.com/",
  395. "Accept": "*/*"
  396. }, onprogress: function(progressEvent) {
  397. const currentTime = Date.now();
  398. const elapsed = (currentTime - downloadStartTime) / 1000;
  399. const shouldUpdate = (currentTime - lastUpdateTime) >= UPDATE_INTERVAL ||
  400. (progressEvent.lengthComputable && progressEvent.loaded === progressEvent.total);
  401. if (progressEvent.lengthComputable) {
  402. totalSize = progressEvent.total;
  403. downloadedSize = progressEvent.loaded;
  404. const percentage = Math.round((downloadedSize / totalSize) * 100);
  405. const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
  406. if (shouldUpdate) {
  407. const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
  408. const speedText = `${formatBytes(speed)}/s`;
  409. const percentText = `${percentage}%`;
  410. updateOverlay(
  411. `Downloading ${percentText}`,
  412. `${filename || 'video.mp4'}`,
  413. sizeText,
  414. speedText
  415. );
  416. lastUpdateTime = currentTime;
  417. }
  418. if ((currentTime - lastUpdateTime) >= 1000 || percentage === 100) {
  419. console.log(`[${elapsed.toFixed(1)}s] Progress: ${percentage}% | Downloaded: ${formatBytes(downloadedSize)}/${formatBytes(totalSize)} | Speed: ${formatBytes(speed)}/s`);
  420. }
  421. } else {
  422. downloadedSize = progressEvent.loaded || 0;
  423. const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
  424. if (shouldUpdate) {
  425. const sizeText = `${formatBytes(downloadedSize)}`;
  426. const speedText = `${formatBytes(speed)}/s`;
  427. const timeText = `${elapsed.toFixed(1)}s`;
  428. updateOverlay(
  429. `Downloading...`,
  430. `${filename || 'video.mp4'} - ${timeText}`,
  431. sizeText,
  432. speedText
  433. );
  434. lastUpdateTime = currentTime;
  435. }
  436. if ((currentTime - lastUpdateTime) >= 1000) {
  437. console.log(`[${elapsed.toFixed(1)}s] Downloaded: ${formatBytes(downloadedSize)} | Speed: ${formatBytes(speed)}/s`);
  438. }
  439. }
  440. },
  441. onload: function(response) {
  442. console.log('Fetch completed. Response status:', response.status);
  443. console.log('Response type:', typeof response.response);
  444. console.log('Response size:', response.response?.size || 'unknown');
  445. if (response.status === 200 && response.response) {
  446. updateOverlay('Creating download file', 'Converting to downloadable file...', formatBytes(response.response.size || 0), 'Processing');
  447. try {
  448. const blob = response.response;
  449. const blobUrl = URL.createObjectURL(blob);
  450. console.log('Blob created:', blob.size, 'bytes');
  451. console.log('Blob URL:', blobUrl);
  452. const a = document.createElement('a');
  453. a.style.display = 'none';
  454. a.href = blobUrl;
  455. a.download = filename || 'video.mp4';
  456. document.body.appendChild(a);
  457. a.click();
  458. setTimeout(() => {
  459. document.body.removeChild(a);
  460. URL.revokeObjectURL(blobUrl);
  461. }, 1000);
  462. updateOverlay(
  463. 'Download completed successfully!',
  464. `${filename || 'video.mp4'}`,
  465. formatBytes(blob.size),
  466. 'Complete',
  467. false,
  468. true
  469. );
  470. console.log('✅ Download successful via blob method');
  471. setTimeout(() => {
  472. removeOverlay();
  473. }, 2500);
  474. } catch (blobError) {
  475. console.error('Blob download failed:', blobError);
  476. updateOverlay(
  477. 'Blob conversion failed',
  478. 'Trying alternative download methods...',
  479. null,
  480. null,
  481. true
  482. );
  483. setTimeout(() => {
  484. proceedWithDownload(url, filename, downloadStartTime);
  485. }, 2000);
  486. }
  487. } else {
  488. console.error('Fetch failed with status:', response.status);
  489. updateOverlay(
  490. 'Data fetch failed',
  491. `Server returned status ${response.status}`,
  492. null,
  493. null,
  494. true
  495. );
  496. setTimeout(() => {
  497. proceedWithDownload(url, filename, downloadStartTime);
  498. }, 2000);
  499. }
  500. }, onerror: function(error) {
  501. console.error('GM.xmlHttpRequest fetch failed:', error);
  502. updateOverlay(
  503. 'Data fetch failed',
  504. 'Trying native fetch method...',
  505. null,
  506. null,
  507. true
  508. );
  509. setTimeout(() => {
  510. nativeFetchDownload(url, filename, downloadStartTime);
  511. }, 2000);
  512. },
  513. ontimeout: function() {
  514. console.error('GM.xmlHttpRequest fetch timeout');
  515. updateOverlay(
  516. 'Download timeout',
  517. 'Trying native fetch method...',
  518. null,
  519. null,
  520. true
  521. );
  522. setTimeout(() => {
  523. nativeFetchDownload(url, filename, downloadStartTime);
  524. }, 2000);
  525. }
  526. });
  527. }
  528. async function nativeFetchDownload(url, filename, downloadStartTime) {
  529. updateOverlay('Trying native fetch', 'Using browser fetch API...', 'Starting...', 'Native method');
  530. console.log('=== NATIVE FETCH DOWNLOAD ===');
  531. console.log('URL:', url);
  532. console.log('Filename:', filename);
  533. console.log('Method: Native fetch API with ReadableStream');
  534. console.log('=============================');
  535. try {
  536. const response = await fetch(url, {
  537. method: 'GET',
  538. headers: {
  539. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  540. "Referer": "https://iframe.y2meta-uk.com/",
  541. "Accept": "*/*"
  542. }
  543. });
  544. if (!response.ok) {
  545. throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  546. }
  547. const contentLength = response.headers.get('content-length');
  548. const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
  549. console.log('Native fetch response OK. Content-Length:', totalSize);
  550. if (response.body && totalSize > 0) {
  551. const reader = response.body.getReader();
  552. const chunks = [];
  553. let downloadedSize = 0;
  554. updateOverlay(
  555. 'Downloading with native fetch',
  556. `Total size: ${formatBytes(totalSize)}`,
  557. '0%',
  558. 'Starting...'
  559. );
  560. while (true) {
  561. const { done, value } = await reader.read();
  562. if (done) break;
  563. chunks.push(value);
  564. downloadedSize += value.length;
  565. const percentage = (downloadedSize / totalSize) * 100;
  566. const elapsed = (Date.now() - downloadStartTime) / 1000;
  567. const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
  568. const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
  569. const speedText = `${formatBytes(speed)}/s`;
  570. updateOverlay(
  571. 'Downloading with native fetch',
  572. `${Math.round(percentage)}% - ${filename || 'video.mp4'}`,
  573. sizeText,
  574. speedText
  575. );
  576. console.log(`Native fetch progress: ${Math.round(percentage)}% | ${sizeText} | ${speedText}`);
  577. }
  578. const blob = new Blob(chunks);
  579. console.log('Native fetch blob created from chunks:', blob.size, 'bytes');
  580. const blobUrl = URL.createObjectURL(blob);
  581. const a = document.createElement('a');
  582. a.style.display = 'none';
  583. a.href = blobUrl;
  584. a.download = filename || 'video.mp4';
  585. document.body.appendChild(a);
  586. a.click();
  587. setTimeout(() => {
  588. document.body.removeChild(a);
  589. URL.revokeObjectURL(blobUrl);
  590. }, 1000);
  591. updateOverlay(
  592. 'Native fetch download completed!',
  593. `${filename || 'video.mp4'}`,
  594. formatBytes(blob.size),
  595. 'Complete',
  596. false,
  597. true
  598. );
  599. } else {
  600. updateOverlay('Downloading with native fetch', 'Size unknown, downloading...', 'Unknown size', 'Downloading...');
  601. const blob = await response.blob();
  602. console.log('Native fetch blob created (no progress):', blob.size, 'bytes');
  603. const blobUrl = URL.createObjectURL(blob);
  604. const a = document.createElement('a');
  605. a.style.display = 'none';
  606. a.href = blobUrl;
  607. a.download = filename || 'video.mp4';
  608. document.body.appendChild(a);
  609. a.click();
  610. setTimeout(() => {
  611. document.body.removeChild(a);
  612. URL.revokeObjectURL(blobUrl);
  613. }, 1000);
  614. updateOverlay(
  615. 'Native fetch download completed!',
  616. `${filename || 'video.mp4'}`,
  617. formatBytes(blob.size),
  618. 'Complete',
  619. false,
  620. true
  621. );
  622. }
  623. console.log('✅ Download successful via native fetch');
  624. setTimeout(() => {
  625. removeOverlay();
  626. }, 2500);
  627. } catch (fetchError) {
  628. console.error('Native fetch failed:', fetchError);
  629. updateOverlay(
  630. 'Native fetch failed',
  631. `Error: ${fetchError.message}`,
  632. null,
  633. null,
  634. true
  635. );
  636. setTimeout(() => {
  637. proceedWithDownload(url, filename, downloadStartTime);
  638. }, 2000);
  639. }
  640. }function proceedWithDownload(url, filename, downloadStartTime) {
  641. console.log('=== FALLBACK DOWNLOAD METHODS ===');
  642. console.log('GM_download (legacy):', typeof GM_download, GM_download);
  643. console.log('GM.download (new):', typeof GM?.download, GM?.download);
  644. console.log('Download URL:', url);
  645. console.log('Filename:', filename);
  646. console.log('==================================');
  647. updateOverlay('Opening download in new tab', 'Most reliable method for video downloads', null, null);
  648. try {
  649. const downloadWindow = window.open(url, '_blank', 'noopener,noreferrer');
  650. if (downloadWindow) {
  651. console.log('New tab opened successfully');
  652. updateOverlay(
  653. 'Download opened in new tab',
  654. `${truncateTitle(filename || 'video.mp4')} - Check Downloads folder`,
  655. 'Via new tab',
  656. 'Browser handling',
  657. false,
  658. true
  659. );
  660. setTimeout(() => {
  661. removeOverlay();
  662. }, 10000);
  663. return;
  664. } else {
  665. console.log('New tab blocked, trying GM_download');
  666. attemptGMDownload(url, filename, downloadStartTime);
  667. }
  668. } catch (error) {
  669. console.error('New tab method failed:', error);
  670. attemptGMDownload(url, filename, downloadStartTime);
  671. }
  672. }
  673. function attemptGMDownload(url, filename, downloadStartTime) {
  674. const gmDownloadAvailable = typeof GM_download !== 'undefined' && GM_download;
  675. const gmDownloadNewSyntax = typeof GM !== 'undefined' && GM.download;
  676. if (gmDownloadAvailable) {
  677. try {
  678. updateOverlay('Trying GM_download method', `File: ${truncateTitle(filename || 'video.mp4')}`, 'Initializing...', '-');
  679. console.log('Using GM_download (legacy API)');
  680. const downloadId = GM_download(url, filename || 'video.mp4', {
  681. headers: {
  682. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  683. "Referer": "https://iframe.y2meta-uk.com/",
  684. "Accept": "*/*"
  685. },
  686. onprogress: function(progressEvent) {
  687. console.log('Download progress (legacy):', progressEvent);
  688. if (progressEvent.lengthComputable) {
  689. const totalSize = progressEvent.total;
  690. const downloadedSize = progressEvent.loaded;
  691. const percentage = (downloadedSize / totalSize) * 100;
  692. const elapsed = (Date.now() - downloadStartTime) / 1000;
  693. const speed = downloadedSize / elapsed;
  694. const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
  695. const speedText = `${formatBytes(speed)}/s`;
  696. updateOverlay(
  697. 'Downloading via GM_download',
  698. `${Math.round(percentage)}% - ${truncateTitle(filename || 'video.mp4')}`,
  699. sizeText,
  700. speedText
  701. );
  702. } else {
  703. const elapsed = (Date.now() - downloadStartTime) / 1000;
  704. updateOverlay(
  705. 'Downloading via GM_download',
  706. `${truncateTitle(filename || 'video.mp4')} - ${elapsed.toFixed(1)}s elapsed`,
  707. 'Size unknown',
  708. 'Progress...'
  709. );
  710. }
  711. },
  712. onload: function() {
  713. console.log('Download completed successfully (legacy)');
  714. updateOverlay(
  715. 'Download completed successfully',
  716. `${truncateTitle(filename || 'video.mp4')}`,
  717. 'Complete',
  718. 'Done',
  719. false,
  720. true
  721. );
  722. setTimeout(() => {
  723. removeOverlay();
  724. }, 2500);
  725. },
  726. onerror: function(error) {
  727. console.error('GM_download error (legacy):', error);
  728. updateOverlay(
  729. 'GM_download failed',
  730. 'Trying alternative download methods...',
  731. null,
  732. null,
  733. true
  734. );
  735. setTimeout(() => {
  736. fallbackDownload(url, filename);
  737. }, 2000);
  738. }
  739. });
  740. console.log('Download ID (legacy):', downloadId);
  741. setTimeout(() => {
  742. console.log('Checking if GM_download callbacks fired...');
  743. updateOverlay(
  744. 'GM_download may have CORS issues',
  745. 'Switching to fallback methods...',
  746. null,
  747. null,
  748. true
  749. );
  750. setTimeout(() => {
  751. fallbackDownload(url, filename);
  752. }, 2000);
  753. }, 2500);
  754. } catch (downloadError) {
  755. console.error('GM_download exception:', downloadError);
  756. tryNewGMDownload(url, filename, downloadStartTime);
  757. }
  758. } else if (gmDownloadNewSyntax) {
  759. tryNewGMDownload(url, filename, downloadStartTime);
  760. } else {
  761. console.log('No GM download APIs available, using fallback');
  762. fallbackDownload(url, filename);
  763. }
  764. }
  765. function tryNewGMDownload(url, filename, downloadStartTime) {
  766. try {
  767. updateOverlay('Trying GM.download (new API)', `File: ${filename || 'video.mp4'}`, 'Initializing...', '-');
  768. console.log('Using GM.download (new API)');
  769. GM.download(url, filename || 'video.mp4', {
  770. headers: {
  771. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  772. "Referer": "https://iframe.y2meta-uk.com/",
  773. "Accept": "*/*"
  774. },
  775. onprogress: function(progressEvent) {
  776. console.log('Download progress (new):', progressEvent);
  777. if (progressEvent.lengthComputable) {
  778. const totalSize = progressEvent.total;
  779. const downloadedSize = progressEvent.loaded;
  780. const percentage = (downloadedSize / totalSize) * 100;
  781. const elapsed = (Date.now() - downloadStartTime) / 1000;
  782. const speed = downloadedSize / elapsed;
  783. const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
  784. const speedText = `${formatBytes(speed)}/s`;
  785. updateOverlay(
  786. 'Downloading via GM.download',
  787. `${Math.round(percentage)}% - ${filename || 'video.mp4'}`,
  788. sizeText,
  789. speedText
  790. );
  791. } else {
  792. const elapsed = (Date.now() - downloadStartTime) / 1000;
  793. updateOverlay(
  794. 'Downloading via GM.download',
  795. `${filename || 'video.mp4'} - ${elapsed.toFixed(1)}s elapsed`,
  796. 'Size unknown',
  797. 'Progress...'
  798. );
  799. }
  800. },
  801. onload: function() {
  802. console.log('Download completed successfully (new)');
  803. updateOverlay(
  804. 'Download completed successfully',
  805. `${filename || 'video.mp4'}`,
  806. 'Complete',
  807. 'Done',
  808. false,
  809. true
  810. );
  811. setTimeout(() => {
  812. removeOverlay();
  813. }, 2500);
  814. },
  815. onerror: function(error) {
  816. console.error('GM.download error (new):', error);
  817. updateOverlay(
  818. 'GM.download failed',
  819. 'Trying alternative download methods...',
  820. null,
  821. null,
  822. true
  823. );
  824. setTimeout(() => {
  825. fallbackDownload(url, filename);
  826. }, 2000);
  827. }
  828. }).then(downloadId => {
  829. console.log('Download ID (new):', downloadId);
  830. setTimeout(() => {
  831. console.log('Checking if GM.download callbacks fired...');
  832. updateOverlay(
  833. 'GM.download may have CORS issues',
  834. 'Switching to fallback methods...',
  835. null,
  836. null,
  837. true
  838. );
  839. setTimeout(() => {
  840. fallbackDownload(url, filename);
  841. }, 2000);
  842. }, 2500);
  843. }).catch(error => {
  844. console.error('GM.download promise error:', error);
  845. fallbackDownload(url, filename);
  846. });
  847. } catch (downloadError) {
  848. console.error('GM.download exception:', downloadError);
  849. fallbackDownload(url, filename);
  850. }
  851. }function fallbackDownload(url, filename) {
  852. updateOverlay('Using direct download methods', 'Testing browser download capabilities...', null, null);
  853. console.log('=== FALLBACK DOWNLOAD ===');
  854. console.log('URL:', url);
  855. console.log('Filename:', filename);
  856. console.log('=========================');
  857. try {
  858. const a = document.createElement('a');
  859. a.style.display = 'none';
  860. a.href = url;
  861. a.download = filename || 'video.mp4';
  862. a.target = '_blank';
  863. a.rel = 'noopener noreferrer';
  864. document.body.appendChild(a);
  865. updateOverlay('Method 1: Force download link', 'Creating download trigger...', null, null);
  866. const clickEvent = new MouseEvent('click', {
  867. bubbles: true,
  868. cancelable: true,
  869. view: window
  870. });
  871. a.dispatchEvent(clickEvent);
  872. a.click();
  873. setTimeout(() => {
  874. document.body.removeChild(a);
  875. updateOverlay(
  876. 'Download link triggered',
  877. `${filename || 'video.mp4'} - Check Downloads folder`,
  878. 'Via download link',
  879. 'Browser handling',
  880. false,
  881. true
  882. );
  883. setTimeout(() => {
  884. trySecondaryMethod(url, filename);
  885. }, 3000);
  886. }, 1000);
  887. } catch (error) {
  888. console.error('Direct link method failed:', error);
  889. trySecondaryMethod(url, filename);
  890. }
  891. }
  892. function trySecondaryMethod(url, filename) {
  893. updateOverlay('Method 2: Location redirect', 'Attempting direct navigation...', null, null);
  894. try {
  895. const downloadWindow = window.open('', '_blank');
  896. if (downloadWindow) {
  897. downloadWindow.location.href = url;
  898. updateOverlay(
  899. 'Download redirected to new tab',
  900. `${filename || 'video.mp4'} - Check new tab`,
  901. 'Via location redirect',
  902. 'Tab navigation',
  903. false,
  904. true
  905. );
  906. setTimeout(() => {
  907. tryFinalMethod(url, filename);
  908. }, 2500);
  909. } else {
  910. tryFinalMethod(url, filename);
  911. }
  912. } catch (error) {
  913. console.error('Secondary method failed:', error);
  914. tryFinalMethod(url, filename);
  915. }
  916. }
  917. function tryFinalMethod(url, filename) {
  918. updateOverlay('Method 3: Manual URL access', 'Preparing manual download option...', null, null);
  919. try {
  920. navigator.clipboard.writeText(url).then(() => {
  921. updateOverlay(
  922. 'URL copied to clipboard!',
  923. 'Open new tab and paste (Ctrl+L, Ctrl+V, Enter)',
  924. 'Clipboard ready',
  925. 'Manual paste',
  926. false,
  927. true
  928. );
  929. console.log('=== MANUAL DOWNLOAD URL ===');
  930. console.log('URL copied to clipboard. Paste in new tab:');
  931. console.log(url);
  932. console.log('Filename:', filename);
  933. console.log('===========================');
  934. }).catch(() => {
  935. showConsoleMethod(url, filename);
  936. });
  937. } catch (error) {
  938. showConsoleMethod(url, filename);
  939. }
  940. setTimeout(removeOverlay, 10000);
  941. }
  942. function showConsoleMethod(url, filename) {
  943. console.log('=== MANUAL DOWNLOAD URL ===');
  944. console.log('Copy this URL and paste in new browser tab:');
  945. console.log(url);
  946. console.log('Filename:', filename);
  947. console.log('===========================');
  948. updateOverlay(
  949. 'Check browser console (F12)',
  950. 'URL available in console for manual copy',
  951. 'Console method',
  952. 'Manual copy/paste',
  953. false,
  954. false
  955. );
  956. }
  957.  
  958. function createDownloadDialog() {
  959. const dialog = document.createElement('div');
  960. dialog.className = 'ytddl-dialog';
  961. const title = document.createElement('h3');
  962. title.textContent = '';
  963.  
  964. const formatSelector = document.createElement('div');
  965. formatSelector.className = 'format-selector';
  966. const videoBtn = document.createElement('button');
  967. videoBtn.className = `format-button ${lastSelectedFormat === 'video' ? 'selected' : ''}`;
  968. videoBtn.setAttribute('data-format', 'video');
  969. videoBtn.textContent = 'VIDEO (MP4)';
  970.  
  971. const audioBtn = document.createElement('button');
  972. audioBtn.className = `format-button ${lastSelectedFormat === 'audio' ? 'selected' : ''}`;
  973. audioBtn.setAttribute('data-format', 'audio');
  974. audioBtn.textContent = 'AUDIO (MP3)';
  975.  
  976. formatSelector.appendChild(videoBtn);
  977. formatSelector.appendChild(audioBtn);
  978.  
  979. const qualityContainer = document.createElement('div');
  980. qualityContainer.id = 'quality-container';
  981. const videoQualities = document.createElement('div');
  982. videoQualities.className = 'quality-options';
  983. videoQualities.id = 'video-qualities';
  984. videoQualities.style.display = lastSelectedFormat === 'video' ? 'grid' : 'none';
  985. ['144p', '240p', '360p', '480p', '720p', '1080p'].forEach((quality, index) => {
  986. const option = document.createElement('div');
  987. option.className = 'quality-option';
  988.  
  989. const input = document.createElement('input');
  990. input.type = 'radio';
  991. input.id = `quality-${index}`;
  992. input.name = 'quality';
  993. input.value = quality.replace('p', '');
  994.  
  995. const label = document.createElement('label');
  996. label.setAttribute('for', `quality-${index}`);
  997. label.textContent = quality;
  998. label.style.fontSize = '14px';
  999. label.style.cursor = 'pointer';
  1000.  
  1001. option.appendChild(input);
  1002. option.appendChild(label);
  1003. videoQualities.appendChild(option);
  1004.  
  1005. option.addEventListener('click', function() {
  1006. input.checked = true;
  1007. GM_setValue('lastSelectedVideoQuality', input.value);
  1008. lastSelectedVideoQuality = input.value;
  1009. });
  1010. });
  1011.  
  1012. const defaultQuality = videoQualities.querySelector(`input[value="${lastSelectedVideoQuality}"]`);
  1013. if (defaultQuality) {
  1014. defaultQuality.checked = true;
  1015. }
  1016. const audioQualities = document.createElement('div');
  1017. audioQualities.className = 'quality-options';
  1018. audioQualities.id = 'audio-qualities';
  1019. audioQualities.style.display = lastSelectedFormat === 'audio' ? 'grid' : 'none';
  1020. ['128', '256', '320'].forEach((bitrate, index) => {
  1021. const option = document.createElement('div');
  1022. option.className = 'quality-option';
  1023.  
  1024. const input = document.createElement('input');
  1025. input.type = 'radio';
  1026. input.id = `bitrate-${index}`;
  1027. input.name = 'bitrate';
  1028. input.value = bitrate;
  1029.  
  1030. const label = document.createElement('label');
  1031. label.setAttribute('for', `bitrate-${index}`);
  1032. label.textContent = `${bitrate} kbps`;
  1033. label.style.fontSize = '14px';
  1034. label.style.cursor = 'pointer';
  1035.  
  1036. option.appendChild(input);
  1037. option.appendChild(label);
  1038. audioQualities.appendChild(option);
  1039.  
  1040. option.addEventListener('click', function() {
  1041. input.checked = true;
  1042. GM_setValue('lastSelectedAudioBitrate', input.value);
  1043. lastSelectedAudioBitrate = input.value;
  1044. });
  1045. });
  1046.  
  1047. const defaultBitrate = audioQualities.querySelector(`input[value="${lastSelectedAudioBitrate}"]`);
  1048. if (defaultBitrate) {
  1049. defaultBitrate.checked = true;
  1050. }
  1051.  
  1052. qualityContainer.appendChild(videoQualities);
  1053. qualityContainer.appendChild(audioQualities);
  1054.  
  1055. const downloadStatus = document.createElement('div');
  1056. downloadStatus.className = 'download-status';
  1057. downloadStatus.id = 'download-status';
  1058.  
  1059. const buttonContainer = document.createElement('div');
  1060. buttonContainer.className = 'button-container';
  1061.  
  1062. const cancelButton = document.createElement('button');
  1063. cancelButton.className = 'ytddl-button cancel';
  1064. cancelButton.textContent = 'Cancel';
  1065.  
  1066. const downloadButton = document.createElement('button');
  1067. downloadButton.className = 'ytddl-button';
  1068. downloadButton.textContent = 'Download';
  1069.  
  1070. buttonContainer.appendChild(cancelButton);
  1071. buttonContainer.appendChild(downloadButton);
  1072.  
  1073. dialog.appendChild(title);
  1074. dialog.appendChild(formatSelector);
  1075. dialog.appendChild(qualityContainer);
  1076. dialog.appendChild(downloadStatus);
  1077. dialog.appendChild(buttonContainer);
  1078.  
  1079. formatSelector.addEventListener('click', (e) => {
  1080. if (e.target.classList.contains('format-button')) {
  1081. formatSelector.querySelectorAll('.format-button').forEach(btn => {
  1082. btn.classList.remove('selected');
  1083. });
  1084. e.target.classList.add('selected');
  1085. const format = e.target.getAttribute('data-format');
  1086. if (format === 'video') {
  1087. videoQualities.style.display = 'grid';
  1088. audioQualities.style.display = 'none';
  1089. lastSelectedFormat = 'video';
  1090. GM_setValue('lastSelectedFormat', 'video');
  1091. } else {
  1092. videoQualities.style.display = 'none';
  1093. audioQualities.style.display = 'grid';
  1094. lastSelectedFormat = 'audio';
  1095. GM_setValue('lastSelectedFormat', 'audio');
  1096. }
  1097. }
  1098. });
  1099.  
  1100. const backdrop = document.createElement('div');
  1101. backdrop.className = 'ytddl-backdrop';
  1102.  
  1103. return { dialog, backdrop, cancelButton, downloadButton };
  1104. }
  1105.  
  1106. function closeDialog(dialog, backdrop) {
  1107. if (dialog && dialog.parentNode) {
  1108. dialog.parentNode.removeChild(dialog);
  1109. }
  1110. if (backdrop && backdrop.parentNode) {
  1111. backdrop.parentNode.removeChild(backdrop);
  1112. }
  1113. }
  1114.  
  1115. function extractVideoId(url) {
  1116. const urlObj = new URL(url);
  1117. const searchParams = new URLSearchParams(urlObj.search);
  1118. return searchParams.get('v');
  1119. } async function downloadWithMP3YouTube(videoUrl, format, quality) {
  1120. const statusElement = document.getElementById('download-status');
  1121. createOverlay();
  1122. if (statusElement) {
  1123. statusElement.style.display = 'block';
  1124. statusElement.textContent = 'Getting API key...';
  1125. }
  1126.  
  1127. try {
  1128. updateOverlay('Getting API key', 'Connecting to MP3YouTube API...');
  1129.  
  1130. const keyResponse = await new Promise((resolve, reject) => {
  1131. GM.xmlHttpRequest({
  1132. method: 'GET',
  1133. url: API_KEY_URL,
  1134. headers: REQUEST_HEADERS,
  1135. onload: resolve,
  1136. onerror: reject,
  1137. ontimeout: reject
  1138. });
  1139. });
  1140.  
  1141. const keyData = JSON.parse(keyResponse.responseText);
  1142. if (!keyData || !keyData.key) {
  1143. throw new Error('Failed to get API key');
  1144. }
  1145.  
  1146. const key = keyData.key;
  1147. updateOverlay('Processing request', `${format} (${format === 'video' ? quality + 'p' : quality + ' kbps'})`);
  1148. if (statusElement) {
  1149. statusElement.textContent = 'Processing download...';
  1150. }
  1151.  
  1152. let payload;
  1153. if (format === 'video') {
  1154. payload = {
  1155. "link": videoUrl,
  1156. "format": "mp4",
  1157. "audioBitrate": "128",
  1158. "videoQuality": quality,
  1159. "filenameStyle": "pretty",
  1160. "vCodec": "h264"
  1161. };
  1162. } else {
  1163. payload = {
  1164. "link": videoUrl,
  1165. "format": "mp3",
  1166. "audioBitrate": quality,
  1167. "filenameStyle": "pretty"
  1168. };
  1169. }
  1170.  
  1171. const customHeaders = {
  1172. ...REQUEST_HEADERS,
  1173. "key": key
  1174. };
  1175.  
  1176. updateOverlay('Converting media', 'Processing video/audio conversion...');
  1177.  
  1178. const downloadResponse = await new Promise((resolve, reject) => {
  1179. GM.xmlHttpRequest({
  1180. method: 'POST',
  1181. url: API_CONVERT_URL,
  1182. headers: customHeaders,
  1183. data: JSON.stringify(payload),
  1184. onload: resolve,
  1185. onerror: reject,
  1186. ontimeout: reject
  1187. });
  1188. });
  1189.  
  1190. const downloadInfo = JSON.parse(downloadResponse.responseText);
  1191. if (downloadInfo.url) {
  1192. updateOverlay('Starting download', `File: ${truncateTitle(downloadInfo.filename || `video.${format === 'video' ? 'mp4' : 'mp3'}`)}`);
  1193. if (statusElement) {
  1194. statusElement.textContent = 'Starting download...';
  1195. }
  1196. triggerDirectDownload(downloadInfo.url, downloadInfo.filename);
  1197. return downloadInfo;
  1198. } else {
  1199. throw new Error('No download URL received from API');
  1200. }} catch (error) {
  1201. updateOverlay('Download failed', `Error: ${error.message}`, null, null, true);
  1202. setTimeout(() => {
  1203. removeOverlay();
  1204. }, 4000);
  1205. throw error;
  1206. }
  1207. }
  1208.  
  1209. function createDownloadButton() {
  1210. const downloadButton = document.createElement('div');
  1211. downloadButton.className = 'ytddl-download-btn';
  1212. const svgNS = "http://www.w3.org/2000/svg";
  1213. const svg = document.createElementNS(svgNS, "svg");
  1214. svg.setAttribute("viewBox", "0 0 512 512");
  1215. const path = document.createElementNS(svgNS, "path");
  1216. path.setAttribute("d", "M256 464c114.9 0 208-93.1 208-208c0-13.3 10.7-24 24-24s24 10.7 24 24c0 141.4-114.6 256-256 256S0 397.4 0 256c0-13.3 10.7-24 24-24s24 10.7 24 24c0 114.9 93.1 208 208 208zM377.6 232.3l-104 112c-4.5 4.9-10.9 7.7-17.6 7.7s-13-2.8-17.6-7.7l-104-112c-9-9.7-8.5-24.9 1.3-33.9s24.9-8.5 33.9 1.3L232 266.9 232 24c0-13.3 10.7-24 24-24s24 10.7 24 24l0 242.9 62.4-67.2c9-9.7 24.2-10.3 33.9-1.3s10.3 24.2 1.3 33.9z");
  1217. svg.appendChild(path);
  1218. downloadButton.appendChild(svg);
  1219. downloadButton.addEventListener('click', function() {
  1220. showDownloadDialog();
  1221. });
  1222. return downloadButton;
  1223. }
  1224.  
  1225. function showDownloadDialog() {
  1226. const videoUrl = window.location.href;
  1227. const videoId = extractVideoId(videoUrl);
  1228. if (!videoId) {
  1229. alert('Could not extract video ID from URL');
  1230. return;
  1231. }
  1232.  
  1233. const { dialog, backdrop, cancelButton, downloadButton } = createDownloadDialog();
  1234. document.body.appendChild(backdrop);
  1235. document.body.appendChild(dialog);
  1236.  
  1237. backdrop.addEventListener('click', () => {
  1238. closeDialog(dialog, backdrop);
  1239. });
  1240.  
  1241. cancelButton.addEventListener('click', () => {
  1242. closeDialog(dialog, backdrop);
  1243. }); downloadButton.addEventListener('click', async () => {
  1244. const selectedFormat = dialog.querySelector('.format-button.selected').getAttribute('data-format');
  1245. let quality;
  1246. if (selectedFormat === 'video') {
  1247. const selectedQuality = dialog.querySelector('input[name="quality"]:checked');
  1248. if (!selectedQuality) {
  1249. alert('Please select a video quality');
  1250. return;
  1251. }
  1252. quality = selectedQuality.value;
  1253. } else {
  1254. const selectedBitrate = dialog.querySelector('input[name="bitrate"]:checked');
  1255. if (!selectedBitrate) {
  1256. alert('Please select an audio bitrate');
  1257. return;
  1258. }
  1259. quality = selectedBitrate.value;
  1260. }
  1261.  
  1262. GM_setValue('lastSelectedFormat', selectedFormat);
  1263. closeDialog(dialog, backdrop);
  1264. try {
  1265. await downloadWithMP3YouTube(videoUrl, selectedFormat, quality);
  1266. } catch (error) {
  1267. console.error('Download error:', error);
  1268. updateOverlay('Download Failed', `Error: ${error.message}`, null, null, true);
  1269. setTimeout(removeOverlay, 2500);
  1270. }
  1271. });
  1272. }
  1273. function insertDownloadButton() {
  1274. const targetSelector = '#owner';
  1275. const target = document.querySelector(targetSelector);
  1276. if (target && !document.querySelector('.ytddl-download-btn')) {
  1277. const downloadButton = createDownloadButton();
  1278. target.appendChild(downloadButton);
  1279. }
  1280. }
  1281. const observer = new MutationObserver(() => {
  1282. if (window.location.pathname.includes('/watch')) {
  1283. insertDownloadButton();
  1284. }
  1285. });
  1286. observer.observe(document.body, { childList: true, subtree: true });
  1287. insertDownloadButton();
  1288. window.addEventListener('yt-navigate-finish', () => {
  1289. insertDownloadButton();
  1290. });
  1291. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址