YouTube Direct Downloader (Cobalt)

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

  1. // ==UserScript==
  2. // @name YouTube Direct Downloader (Cobalt)
  3. // @description Add a custom download button and provide options to download the video, video dubs, or audio directly from the YouTube page.
  4. // @icon https://www.google.com/s2/favicons?sz=64&domain=cobalt.tools
  5. // @version 1.4
  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. // @connect c.blahaj.ca
  14. // @connect dwnld.nichind.dev
  15. // @run-at document-end
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. const PRIMARY_API_URL = 'https://c.blahaj.ca/';
  22. const FALLBACK_API_URL = 'https://dwnld.nichind.dev/';
  23. let currentApiUrl = PRIMARY_API_URL;
  24.  
  25. const LANGUAGE_MAP = {
  26. "af": "Afrikaans",
  27. "am": "አማርኛ",
  28. "ar": "العربية",
  29. "as": "Assamese",
  30. "az": "azərbaycan",
  31. "be": "Belarusian",
  32. "bg": "български",
  33. "bn": "বাংলা",
  34. "bs": "bosanski",
  35. "ca": "català",
  36. "cs": "čeština",
  37. "da": "dansk",
  38. "de": "Deutsch",
  39. "el": "Ελληνικά",
  40. "en": "English",
  41. "es": "español",
  42. "et": "eesti",
  43. "eu": "Basque",
  44. "fa": "فارسی",
  45. "fi": "suomi",
  46. "fil": "Filipino",
  47. "fr": "français",
  48. "gl": "Galician",
  49. "gu": "ગુજરાતી",
  50. "hi": "हिन्दी",
  51. "hr": "hrvatski",
  52. "hu": "magyar",
  53. "hy": "Armenian",
  54. "id": "Indonesia",
  55. "is": "Icelandic",
  56. "it": "italiano",
  57. "iw": "עברית",
  58. "ja": "日本語",
  59. "ka": "Georgian",
  60. "kk": "Kazakh",
  61. "km": "Khmer",
  62. "kn": "ಕನ್ನಡ",
  63. "ko": "한국어",
  64. "ky": "Kyrgyz",
  65. "lo": "Lao",
  66. "lt": "lietuvių",
  67. "lv": "latviešu",
  68. "mk": "Macedonian",
  69. "ml": "മലയാളം",
  70. "mn": "Mongolian",
  71. "mr": "मराठी",
  72. "ms": "Melayu",
  73. "my": "Burmese",
  74. "ne": "Nepali",
  75. "nl": "Nederlands",
  76. "no": "norsk",
  77. "or": "Odia",
  78. "pa": "ਪੰਜਾਬੀ",
  79. "pl": "polski",
  80. "pt": "português",
  81. "ro": "română",
  82. "ru": "русский",
  83. "si": "Sinhala",
  84. "sk": "slovenčina",
  85. "sl": "slovenščina",
  86. "sq": "Albanian",
  87. "sr": "српски",
  88. "sv": "svenska",
  89. "sw": "Kiswahili",
  90. "ta": "தமிழ்",
  91. "te": "తెలుగు",
  92. "th": "ไทย",
  93. "tr": "Türkçe",
  94. "uk": "українська",
  95. "ur": "اردو",
  96. "uz": "o'zbek",
  97. "vi": "Tiếng Việt",
  98. "zh-CN": "中文(中国)",
  99. "zh-HK": "中文(香港)",
  100. "zh-TW": "中文(台灣)",
  101. "zu": "Zulu"
  102. };
  103.  
  104. const style = document.createElement('style');
  105. style.textContent = `
  106. .cobalt-download-btn {
  107. width: 36px;
  108. height: 36px;
  109. border-radius: 50%;
  110. display: flex;
  111. align-items: center;
  112. justify-content: center;
  113. cursor: pointer;
  114. margin-left: 8px;
  115. transition: background-color 0.2s;
  116. }
  117. html[dark] .cobalt-download-btn {
  118. background-color: #ffffff1a;
  119. }
  120. html:not([dark]) .cobalt-download-btn {
  121. background-color: #0000000d;
  122. }
  123. html[dark] .cobalt-download-btn:hover {
  124. background-color: #ffffff33;
  125. }
  126. html:not([dark]) .cobalt-download-btn:hover {
  127. background-color: #00000014;
  128. }
  129. .cobalt-download-btn svg {
  130. width: 18px;
  131. height: 18px;
  132. }
  133. html[dark] .cobalt-download-btn svg {
  134. fill: var(--yt-spec-text-primary, #fff);
  135. }
  136. html:not([dark]) .cobalt-download-btn svg {
  137. fill: var(--yt-spec-text-primary, #030303);
  138. }
  139. `;
  140. document.head.appendChild(style);
  141.  
  142. function triggerDirectDownload(url) {
  143. const a = document.createElement('a');
  144. a.style.display = 'none';
  145. a.href = url;
  146. document.body.appendChild(a);
  147. a.click();
  148. setTimeout(() => {
  149. document.body.removeChild(a);
  150. URL.revokeObjectURL(url);
  151. }, 100);
  152. }
  153.  
  154. function createDownloadDialog() {
  155. const dialog = document.createElement('div');
  156. dialog.className = 'yt-download-dialog';
  157. dialog.style.cssText = `
  158. position: fixed;
  159. top: 50%;
  160. left: 50%;
  161. transform: translate(-50%, -50%);
  162. background: #000000;
  163. color: #e1e1e1;
  164. border-radius: 12px;
  165. box-shadow: 0 0 0 1px rgba(225,225,225,.1), 0 2px 4px 1px rgba(225,225,225,.18);
  166. font-family: 'IBM Plex Mono', 'Noto Sans Mono Variable', 'Noto Sans Mono', monospace;
  167. width: 400px;
  168. z-index: 9999;
  169. `;
  170.  
  171. const dialogContent = document.createElement('div');
  172. dialogContent.style.padding = '16px';
  173.  
  174. const styleElement = document.createElement('style');
  175. styleElement.textContent = `
  176. @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&display=swap');
  177.  
  178. .quality-grid {
  179. display: grid;
  180. grid-template-columns: repeat(3, 1fr);
  181. gap: 8px;
  182. margin-bottom: 16px;
  183. }
  184.  
  185. .quality-option {
  186. display: flex;
  187. align-items: center;
  188. padding: 8px;
  189. cursor: pointer;
  190. }
  191.  
  192. .quality-option:hover {
  193. background: #191919;
  194. border-radius: 6px;
  195. }
  196.  
  197. .logo-container {
  198. display: flex;
  199. align-items: center;
  200. gap: 8px;
  201. margin-bottom: 16px;
  202. }
  203.  
  204. .subtitle {
  205. color: #e1e1e1;
  206. opacity: 0.7;
  207. font-size: 12px;
  208. margin-top: 4px;
  209. }
  210.  
  211. .title {
  212. font-size: 18px;
  213. font-weight: 700;
  214. }
  215.  
  216. .title-link {
  217. text-decoration: none;
  218. color: inherit;
  219. cursor: pointer;
  220. transition: opacity 0.2s ease;
  221. }
  222.  
  223. .title-link:hover {
  224. opacity: 0.8;
  225. }
  226.  
  227. .codec-selector {
  228. margin-bottom: 16px;
  229. display: flex;
  230. gap: 8px;
  231. justify-content: center;
  232. }
  233.  
  234. .codec-button {
  235. background: transparent;
  236. border: 1px solid #e1e1e1;
  237. color: #e1e1e1;
  238. padding: 6px 12px;
  239. border-radius: 14px;
  240. cursor: pointer;
  241. font-family: inherit;
  242. font-size: 12px;
  243. transition: all 0.2s ease;
  244. }
  245.  
  246. .codec-button:hover {
  247. background: #808080;
  248. color: #000000;
  249. }
  250.  
  251. .codec-button.selected {
  252. background: #1ed760;
  253. border-color: #1ed760;
  254. color: #000000;
  255. }
  256.  
  257. .download-status {
  258. text-align: center;
  259. margin: 16px 0;
  260. font-size: 12px;
  261. display: none;
  262. }
  263.  
  264. .button-container {
  265. display: flex;
  266. justify-content: center;
  267. gap: 8px;
  268. }
  269.  
  270. .switch-container {
  271. position: absolute;
  272. top: 16px;
  273. right: 16px;
  274. display: flex;
  275. align-items: center;
  276. }
  277. .switch-button {
  278. background: transparent;
  279. border: none;
  280. cursor: pointer;
  281. padding: 4px;
  282. transition: all 0.2s ease;
  283. }
  284. .switch-button svg {
  285. width: 20px;
  286. height: 20px;
  287. fill: #e1e1e1;
  288. transition: all 0.2s ease;
  289. }
  290. .switch-button:hover svg {
  291. fill: #1ed760;
  292. }
  293. .audio-options {
  294. display: none;
  295. margin-bottom: 16px;
  296. }
  297. .audio-options.active {
  298. display: block;
  299. }
  300. .dub-selector {
  301. margin-top: 16px;
  302. margin-bottom: 16px;
  303. display: none;
  304. }
  305. .dub-select {
  306. width: 80%;
  307. margin: 0 auto;
  308. display: block;
  309. }
  310. .dub-button {
  311. background: transparent;
  312. border: 1px solid #39a9db;
  313. color: #39a9db;
  314. }
  315. .dub-button:hover {
  316. background: #39a9db;
  317. color: #000000;
  318. }
  319. .dub-button.selected {
  320. background: #39a9db;
  321. border-color: #39a9db;
  322. color: #000000;
  323. }
  324. `;
  325. dialog.appendChild(styleElement);
  326.  
  327. const logoContainer = document.createElement('div');
  328. logoContainer.className = 'logo-container';
  329.  
  330. const logoSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  331. logoSvg.setAttribute('width', '24');
  332. logoSvg.setAttribute('height', '16');
  333. logoSvg.setAttribute('viewBox', '0 0 24 16');
  334. logoSvg.setAttribute('fill', 'none');
  335.  
  336. const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  337. path1.setAttribute('d', 'M0 15.6363L0 12.8594L9.47552 8.293L0 3.14038L0 0.363525L12.8575 7.4908V9.21862L0 15.6363Z');
  338. path1.setAttribute('fill', 'white');
  339.  
  340. const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  341. path2.setAttribute('d', 'M11.1425 15.6363V12.8594L20.6181 8.293L11.1425 3.14038V0.363525L24 7.4908V9.21862L11.1425 15.6363Z');
  342. path2.setAttribute('fill', 'white');
  343.  
  344. logoSvg.appendChild(path1);
  345. logoSvg.appendChild(path2);
  346.  
  347. const logoDiv = document.createElement('div');
  348. logoDiv.id = 'cobalt-logo';
  349. logoDiv.appendChild(logoSvg);
  350.  
  351. logoContainer.appendChild(logoDiv);
  352.  
  353. const titleContainer = document.createElement('div');
  354. const titleLink = document.createElement('a');
  355. titleLink.href = 'https://gf.qytechs.cn/en/users/1376410';
  356. titleLink.target = '_blank';
  357. titleLink.rel = 'noopener noreferrer';
  358. titleLink.className = 'title-link';
  359.  
  360. const title = document.createElement('div');
  361. title.className = 'title';
  362. title.textContent = 'cobalt';
  363. const statusSpan = document.createElement('span');
  364. statusSpan.id = 'api-status';
  365. statusSpan.style.marginLeft = '8px';
  366. statusSpan.style.fontSize = '14px';
  367. statusSpan.style.fontWeight = 'normal';
  368. statusSpan.textContent = 'checking...';
  369. title.appendChild(statusSpan);
  370.  
  371. titleLink.appendChild(title);
  372.  
  373. checkApiStatus(function(isOnline, apiType) {
  374. const statusSpan = document.getElementById('api-status');
  375. if (statusSpan) {
  376. statusSpan.textContent = isOnline ? apiType : 'Offline';
  377. statusSpan.style.color = isOnline ? '#1ed760' : '#f3727f';
  378. }
  379. });
  380.  
  381. titleContainer.appendChild(titleLink);
  382.  
  383. const subtitle = document.createElement('div');
  384. subtitle.className = 'subtitle';
  385. subtitle.textContent = 'YouTube Direct Downloader';
  386.  
  387. titleContainer.appendChild(subtitle);
  388. logoContainer.appendChild(titleContainer);
  389.  
  390. dialogContent.appendChild(logoContainer);
  391.  
  392. const switchContainer = document.createElement('div');
  393. switchContainer.className = 'switch-container';
  394.  
  395. const switchButton = document.createElement('button');
  396. switchButton.className = 'switch-button';
  397. switchButton.id = 'mode-switch';
  398.  
  399. const switchSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  400. switchSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  401. switchSvg.setAttribute('viewBox', '0 0 384 512');
  402.  
  403. const switchPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  404. switchPath.setAttribute('d', 'M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM64 288c0-17.7 14.3-32 32-32l96 0c17.7 0 32 14.3 32 32l0 96c0 17.7-14.3 32-32 32l-96 0c-17.7 0-32-14.3-32-32l0-96zM300.9 397.9L256 368l0-64 44.9-29.9c2-1.3 4.4-2.1 6.8-2.1c6.8 0 12.3 5.5 12.3 12.3l0 103.4c0 6.8-5.5 12.3-12.3 12.3c-2.4 0-4.8-.7-6.8-2.1z');
  405.  
  406. switchSvg.appendChild(switchPath);
  407. switchButton.appendChild(switchSvg);
  408. switchContainer.appendChild(switchButton);
  409.  
  410. dialogContent.appendChild(switchContainer);
  411.  
  412. const videoOptions = document.createElement('div');
  413. videoOptions.id = 'video-options';
  414.  
  415. const videoCodecSelector = document.createElement('div');
  416. videoCodecSelector.className = 'codec-selector';
  417.  
  418. ['h264', 'vp9', 'av1'].forEach(codec => {
  419. const button = document.createElement('button');
  420. button.className = 'codec-button';
  421. button.dataset.codec = codec;
  422. button.textContent = codec.toUpperCase();
  423. videoCodecSelector.appendChild(button);
  424. });
  425.  
  426. const dubButton = document.createElement('button');
  427. dubButton.className = 'codec-button dub-button';
  428. dubButton.dataset.codec = 'dub';
  429. dubButton.textContent = 'DUB';
  430. videoCodecSelector.appendChild(dubButton);
  431.  
  432. videoOptions.appendChild(videoCodecSelector);
  433.  
  434. const qualityOptions = document.createElement('div');
  435. qualityOptions.id = 'quality-options';
  436. qualityOptions.className = 'quality-grid';
  437. videoOptions.appendChild(qualityOptions);
  438.  
  439. const dubSelector = document.createElement('div');
  440. dubSelector.className = 'dub-selector';
  441. dubSelector.style.display = 'none';
  442.  
  443. const dubSelect = document.createElement('select');
  444. dubSelect.className = 'dub-select';
  445. dubSelect.style.cssText = `
  446. padding: 8px;
  447. background: #191919;
  448. color: #e1e1e1;
  449. border: 1px solid #e1e1e1;
  450. border-radius: 6px;
  451. font-family: inherit;
  452. cursor: pointer;
  453. `;
  454.  
  455. const defaultOption = document.createElement('option');
  456. defaultOption.value = '';
  457. defaultOption.textContent = 'Original Audio';
  458. dubSelect.appendChild(defaultOption);
  459.  
  460. Object.entries(LANGUAGE_MAP).forEach(([code, name]) => {
  461. const option = document.createElement('option');
  462. option.value = code;
  463. option.textContent = `${name} (${code})`;
  464. dubSelect.appendChild(option);
  465. });
  466.  
  467. dubSelector.appendChild(dubSelect);
  468. videoOptions.appendChild(dubSelector);
  469.  
  470. dialogContent.appendChild(videoOptions);
  471.  
  472. const audioOptions = document.createElement('div');
  473. audioOptions.id = 'audio-options';
  474. audioOptions.className = 'audio-options';
  475.  
  476. const audioCodecSelector = document.createElement('div');
  477. audioCodecSelector.className = 'codec-selector';
  478.  
  479. ['mp3', 'ogg', 'opus', 'wav'].forEach(codec => {
  480. const button = document.createElement('button');
  481. button.className = 'codec-button';
  482. button.dataset.codec = codec;
  483. button.textContent = codec.toUpperCase();
  484. audioCodecSelector.appendChild(button);
  485. });
  486.  
  487. audioOptions.appendChild(audioCodecSelector);
  488.  
  489. const bitrateOptions = document.createElement('div');
  490. bitrateOptions.id = 'bitrate-options';
  491. bitrateOptions.className = 'quality-grid';
  492. audioOptions.appendChild(bitrateOptions);
  493.  
  494. dialogContent.appendChild(audioOptions);
  495.  
  496. const downloadStatus = document.createElement('div');
  497. downloadStatus.className = 'download-status';
  498. downloadStatus.id = 'download-status';
  499. dialogContent.appendChild(downloadStatus);
  500.  
  501. const buttonContainer = document.createElement('div');
  502. buttonContainer.className = 'button-container';
  503.  
  504. const cancelButton = document.createElement('button');
  505. cancelButton.id = 'cancel-button';
  506. cancelButton.textContent = 'Cancel';
  507. cancelButton.style.cssText = `
  508. background: transparent;
  509. border: 1px solid #e1e1e1;
  510. color: #e1e1e1;
  511. font-size: 14px;
  512. font-weight: 500;
  513. padding: 8px 16px;
  514. cursor: pointer;
  515. font-family: inherit;
  516. border-radius: 18px;
  517. `;
  518.  
  519. const downloadButton = document.createElement('button');
  520. downloadButton.id = 'download-button';
  521. downloadButton.textContent = 'Download';
  522. downloadButton.style.cssText = `
  523. background: transparent;
  524. border: 1px solid #e1e1e1;
  525. color: #e1e1e1;
  526. font-size: 14px;
  527. font-weight: 500;
  528. padding: 8px 16px;
  529. border-radius: 18px;
  530. cursor: pointer;
  531. font-family: inherit;
  532. `;
  533.  
  534. buttonContainer.appendChild(cancelButton);
  535. buttonContainer.appendChild(downloadButton);
  536.  
  537. dialogContent.appendChild(buttonContainer);
  538.  
  539. dialog.appendChild(dialogContent);
  540.  
  541. const backdrop = document.createElement('div');
  542. backdrop.style.cssText = `
  543. position: fixed;
  544. top: 0;
  545. left: 0;
  546. width: 100%;
  547. height: 100%;
  548. background: rgba(0, 0, 0, 0.5);
  549. z-index: 9998;
  550. `;
  551. document.body.appendChild(backdrop);
  552.  
  553. backdrop.addEventListener('click', () => {
  554. closeDialog(dialog, backdrop);
  555. });
  556.  
  557. const savedCodec = localStorage.getItem('cobaltToolsCodec') || 'h264';
  558. const savedQuality = localStorage.getItem('cobaltToolsQuality') || '1080p';
  559. const savedMode = localStorage.getItem('cobaltToolsMode') || 'video';
  560. const savedAudioCodec = localStorage.getItem('cobaltToolsAudioCodec') || 'mp3';
  561. const savedDub = localStorage.getItem('cobaltToolsDub') || '';
  562.  
  563. return { dialog, backdrop, savedCodec, savedQuality, savedMode, savedAudioCodec, savedDub };
  564. }
  565.  
  566. function closeDialog(dialog, backdrop) {
  567. dialog.remove();
  568. backdrop.remove();
  569. }
  570.  
  571. function extractVideoId(url) {
  572. const urlObj = new URL(url);
  573. const searchParams = new URLSearchParams(urlObj.search);
  574. return searchParams.get('v');
  575. }
  576.  
  577. function checkApiStatus(callback) {
  578. checkSingleApiStatus(PRIMARY_API_URL, function(isOnline) {
  579. if (isOnline) {
  580. currentApiUrl = PRIMARY_API_URL;
  581. callback(true, 'Primary');
  582. } else {
  583. checkSingleApiStatus(FALLBACK_API_URL, function(fallbackIsOnline) {
  584. if (fallbackIsOnline) {
  585. currentApiUrl = FALLBACK_API_URL;
  586. callback(true, 'Fallback');
  587. } else {
  588. currentApiUrl = PRIMARY_API_URL;
  589. callback(false, 'Offline');
  590. }
  591. });
  592. }
  593. });
  594. }
  595.  
  596. function checkSingleApiStatus(apiUrl, callback) {
  597. GM.xmlHttpRequest({
  598. method: 'GET',
  599. url: apiUrl,
  600. timeout: 5000,
  601. onload: function(response) {
  602. callback(response.status >= 200 && response.status < 300);
  603. },
  604. onerror: function() {
  605. callback(false);
  606. },
  607. ontimeout: function() {
  608. callback(false);
  609. }
  610. });
  611. }
  612.  
  613. function downloadVideo(quality, videoId, codec, dialog, backdrop) {
  614. const statusElement = dialog.querySelector('#download-status');
  615. statusElement.style.display = 'block';
  616. statusElement.textContent = 'Preparing download...';
  617. const dubSelect = dialog.querySelector('.dub-select');
  618. const selectedDub = dubSelect ? dubSelect.value : '';
  619. const payload = {
  620. url: `https://www.youtube.com/watch?v=${videoId}`,
  621. downloadMode: "auto",
  622. filenameStyle: "basic",
  623. videoQuality: quality.replace('p', ''),
  624. youtubeVideoCodec: codec,
  625. youtubeDubLang: selectedDub ? selectedDub : 'original'
  626. };
  627. function attemptDownload(apiUrl, isRetry = false) {
  628. statusElement.textContent = isRetry ? 'Trying fallback API...' : 'Preparing download...';
  629. GM.xmlHttpRequest({
  630. method: 'POST',
  631. url: apiUrl,
  632. headers: {
  633. 'accept': 'application/json',
  634. 'content-type': 'application/json'
  635. },
  636. data: JSON.stringify(payload),
  637. responseType: 'json',
  638. onload: function(response) {
  639. try {
  640. const data = JSON.parse(response.responseText);
  641. if (data.url) {
  642. statusElement.textContent = 'Starting download...';
  643. triggerDirectDownload(data.url);
  644. setTimeout(() => {
  645. closeDialog(dialog, backdrop);
  646. }, 1000);
  647. } else {
  648. if (!isRetry && apiUrl === PRIMARY_API_URL) {
  649. attemptDownload(FALLBACK_API_URL, true);
  650. } else {
  651. statusElement.textContent = 'Error: No download URL found';
  652. console.error('No URL in response:', data);
  653. }
  654. }
  655. } catch (error) {
  656. if (!isRetry && apiUrl === PRIMARY_API_URL) {
  657. attemptDownload(FALLBACK_API_URL, true);
  658. } else {
  659. statusElement.textContent = 'Error: API service might be temporarily unavailable';
  660. console.error('Error processing response:', error);
  661. }
  662. }
  663. },
  664. onerror: function(error) {
  665. if (!isRetry && apiUrl === PRIMARY_API_URL) {
  666. attemptDownload(FALLBACK_API_URL, true);
  667. } else {
  668. statusElement.textContent = 'Network error. Please check your connection.';
  669. console.error('Network error:', error);
  670. }
  671. }
  672. });
  673. }
  674. attemptDownload(currentApiUrl);
  675. }
  676.  
  677. function updateQualityOptions(dialog, codec, savedQuality) {
  678. const qualityOptions = dialog.querySelector('#quality-options');
  679. while (qualityOptions.firstChild) {
  680. qualityOptions.removeChild(qualityOptions.firstChild);
  681. }
  682.  
  683. let qualities;
  684. if (codec === 'h264') {
  685. qualities = ['144p', '240p', '360p', '480p', '720p', '1080p'];
  686. } else if (codec === 'vp9') {
  687. qualities = ['144p', '240p', '360p', '480p', '720p', '1080p', '1440p', '4k'];
  688. } else {
  689. qualities = ['144p', '240p', '360p', '480p', '720p', '1080p', '1440p', '4k', '8k+'];
  690. }
  691.  
  692. qualities.forEach((quality, index) => {
  693. const option = document.createElement('div');
  694. option.className = 'quality-option';
  695.  
  696. const input = document.createElement('input');
  697. input.type = 'radio';
  698. input.id = `quality-${index}`;
  699. input.name = 'quality';
  700. input.value = quality;
  701. input.style.marginRight = '8px';
  702.  
  703. const label = document.createElement('label');
  704. label.htmlFor = `quality-${index}`;
  705. label.style.fontSize = '14px';
  706. label.style.cursor = 'pointer';
  707. label.textContent = quality;
  708.  
  709. option.appendChild(input);
  710. option.appendChild(label);
  711. qualityOptions.appendChild(option);
  712.  
  713. option.addEventListener('click', function() {
  714. const radioButton = this.querySelector('input[type="radio"]');
  715. qualityOptions.querySelectorAll('input[type="radio"]').forEach(rb => {
  716. rb.checked = false;
  717. });
  718. radioButton.checked = true;
  719.  
  720. localStorage.setItem('cobaltToolsQuality', quality);
  721. });
  722. });
  723.  
  724. const defaultQuality = qualities.includes(savedQuality) ? savedQuality : qualities[qualities.length - 1];
  725. const defaultRadio = dialog.querySelector(`input[name="quality"][value="${defaultQuality}"]`);
  726. if (defaultRadio) {
  727. defaultRadio.checked = true;
  728. }
  729. }
  730.  
  731. function updateAudioOptions(dialog, codec, savedBitrate) {
  732. const bitrateOptions = dialog.querySelector('#bitrate-options');
  733. while (bitrateOptions.firstChild) {
  734. bitrateOptions.removeChild(bitrateOptions.firstChild);
  735. }
  736.  
  737. if (codec === 'wav') {
  738. return;
  739. }
  740.  
  741. const bitrates = ['8', '64', '96', '128', '256', '320'];
  742.  
  743. bitrates.forEach((bitrate, index) => {
  744. const option = document.createElement('div');
  745. option.className = 'quality-option';
  746.  
  747. const input = document.createElement('input');
  748. input.type = 'radio';
  749. input.id = `bitrate-${index}`;
  750. input.name = 'bitrate';
  751. input.value = bitrate;
  752. input.style.marginRight = '8px';
  753.  
  754. const label = document.createElement('label');
  755. label.htmlFor = `bitrate-${index}`;
  756. label.style.fontSize = '14px';
  757. label.style.cursor = 'pointer';
  758. label.textContent = `${bitrate} kb/s`;
  759.  
  760. option.appendChild(input);
  761. option.appendChild(label);
  762. bitrateOptions.appendChild(option);
  763.  
  764. option.addEventListener('click', function() {
  765. const radioButton = this.querySelector('input[type="radio"]');
  766. bitrateOptions.querySelectorAll('input[type="radio"]').forEach(rb => {
  767. rb.checked = false;
  768. });
  769. radioButton.checked = true;
  770.  
  771. localStorage.setItem('cobaltToolsBitrate', bitrate);
  772. });
  773. });
  774.  
  775. const defaultBitrate = bitrates.includes(savedBitrate) ? savedBitrate : bitrates[bitrates.length - 1];
  776. const defaultRadio = dialog.querySelector(`input[name="bitrate"][value="${defaultBitrate}"]`);
  777. if (defaultRadio) {
  778. defaultRadio.checked = true;
  779. }
  780. }
  781.  
  782. function downloadAudio(format, bitrate, videoId, dialog, backdrop) {
  783. const statusElement = dialog.querySelector('#download-status');
  784. statusElement.style.display = 'block';
  785. statusElement.textContent = 'Preparing audio download...';
  786. let payload;
  787. if (format === 'wav') {
  788. payload = {
  789. url: `https://www.youtube.com/watch?v=${videoId}`,
  790. downloadMode: "audio",
  791. filenameStyle: "basic",
  792. audioFormat: "wav"
  793. };
  794. } else {
  795. payload = {
  796. url: `https://www.youtube.com/watch?v=${videoId}`,
  797. downloadMode: "audio",
  798. filenameStyle: "basic",
  799. audioFormat: format,
  800. audioBitrate: bitrate
  801. };
  802. }
  803. function attemptDownload(apiUrl, isRetry = false) {
  804. statusElement.textContent = isRetry ? 'Trying fallback API...' : 'Preparing audio download...';
  805. GM.xmlHttpRequest({
  806. method: 'POST',
  807. url: apiUrl,
  808. headers: {
  809. 'accept': 'application/json',
  810. 'content-type': 'application/json'
  811. },
  812. data: JSON.stringify(payload),
  813. responseType: 'json',
  814. onload: function(response) {
  815. try {
  816. const data = JSON.parse(response.responseText);
  817. if (data.url) {
  818. statusElement.textContent = 'Starting audio download...';
  819. triggerDirectDownload(data.url);
  820. setTimeout(() => {
  821. closeDialog(dialog, backdrop);
  822. }, 1000);
  823. } else {
  824. if (!isRetry && apiUrl === PRIMARY_API_URL) {
  825. attemptDownload(FALLBACK_API_URL, true);
  826. } else {
  827. statusElement.textContent = 'Error: No download URL found';
  828. console.error('No URL in response:', data);
  829. }
  830. }
  831. } catch (error) {
  832. if (!isRetry && apiUrl === PRIMARY_API_URL) {
  833. attemptDownload(FALLBACK_API_URL, true);
  834. } else {
  835. statusElement.textContent = 'Error: API service might be temporarily unavailable';
  836. console.error('Error processing response:', error);
  837. }
  838. }
  839. },
  840. onerror: function(error) {
  841. if (!isRetry && apiUrl === PRIMARY_API_URL) {
  842. attemptDownload(FALLBACK_API_URL, true);
  843. } else {
  844. statusElement.textContent = 'Network error. Please check your connection.';
  845. console.error('Network error:', error);
  846. }
  847. }
  848. });
  849. }
  850. attemptDownload(currentApiUrl);
  851. }
  852.  
  853. function updateModeSwitch(modeSwitch, isAudioMode) {
  854. while (modeSwitch.firstChild) {
  855. modeSwitch.removeChild(modeSwitch.firstChild);
  856. }
  857.  
  858. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  859. svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  860. svg.setAttribute('viewBox', '0 0 384 512');
  861.  
  862. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  863.  
  864. if (isAudioMode) {
  865. path.setAttribute('d', 'M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8l0 128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376 80 376c-8.8 0-16-7.2-16-16l0-48c0-8.8 7.2-16 16-16l33.4 0 35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z');
  866. } else {
  867. path.setAttribute('d', 'M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM64 288c0-17.7 14.3-32 32-32l96 0c17.7 0 32 14.3 32 32l0 96c0 17.7-14.3 32-32 32l-96 0c-17.7 0-32-14.3-32-32l0-96zM300.9 397.9L256 368l0-64 44.9-29.9c2-1.3 4.4-2.1 6.8-2.1c6.8 0 12.3 5.5 12.3 12.3l0 103.4c0 6.8-5.5 12.3-12.3 12.3c-2.4 0-4.8-.7-6.8-2.1z');
  868. }
  869.  
  870. svg.appendChild(path);
  871. modeSwitch.appendChild(svg);
  872. }
  873.  
  874. function createDownloadButton() {
  875. const downloadButton = document.createElement('div');
  876. downloadButton.className = 'cobalt-download-btn';
  877. const svgNS = "http://www.w3.org/2000/svg";
  878. const svg = document.createElementNS(svgNS, "svg");
  879. svg.setAttribute("viewBox", "0 0 512 512");
  880. const path = document.createElementNS(svgNS, "path");
  881. 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");
  882. svg.appendChild(path);
  883. downloadButton.appendChild(svg);
  884. downloadButton.addEventListener('click', function() {
  885. const customDialog = modifyQualityOptionsAndRemoveElements();
  886. document.body.appendChild(customDialog);
  887. });
  888. return downloadButton;
  889. }
  890. function insertDownloadButton() {
  891. const targetSelector = '#owner';
  892. const target = document.querySelector(targetSelector);
  893. if (target && !document.querySelector('.cobalt-download-btn')) {
  894. const downloadButton = createDownloadButton();
  895. target.appendChild(downloadButton);
  896. }
  897. }
  898. function modifyQualityOptionsAndRemoveElements() {
  899. const { dialog, backdrop, savedCodec, savedMode, savedAudioCodec, savedDub } = createDownloadDialog();
  900. let currentVideoId = null;
  901. let selectedVideoCodec = savedCodec;
  902. let selectedAudioCodec = savedAudioCodec;
  903. let isAudioMode = savedMode === 'audio';
  904.  
  905. try {
  906. const url = window.location.href;
  907. currentVideoId = extractVideoId(url);
  908. } catch (error) {
  909. console.error('Error extracting video ID:', error);
  910. return;
  911. }
  912.  
  913. const modeSwitch = dialog.querySelector('#mode-switch');
  914. const videoOptions = dialog.querySelector('#video-options');
  915. const audioOptions = dialog.querySelector('#audio-options');
  916. const dubSelector = dialog.querySelector('.dub-selector');
  917.  
  918. function updateModeSwitchAndOptions() {
  919. updateModeSwitch(modeSwitch, isAudioMode);
  920. if (isAudioMode) {
  921. audioOptions.style.display = 'block';
  922. videoOptions.style.display = 'none';
  923. } else {
  924. videoOptions.style.display = 'block';
  925. audioOptions.style.display = 'none';
  926. }
  927. }
  928.  
  929. updateModeSwitchAndOptions();
  930.  
  931. modeSwitch.addEventListener('click', () => {
  932. isAudioMode = !isAudioMode;
  933. updateModeSwitchAndOptions();
  934. localStorage.setItem('cobaltToolsMode', isAudioMode ? 'audio' : 'video');
  935. updateCodecButtons();
  936. });
  937.  
  938. function updateCodecButtons() {
  939. const videoCodecButtons = videoOptions.querySelectorAll('.codec-button');
  940. const audioCodecButtons = audioOptions.querySelectorAll('.codec-button');
  941.  
  942. videoCodecButtons.forEach(button => {
  943. button.classList.remove('selected');
  944. if (button.dataset.codec === selectedVideoCodec) {
  945. button.classList.add('selected');
  946. }
  947. });
  948.  
  949. audioCodecButtons.forEach(button => {
  950. button.classList.remove('selected');
  951. if (button.dataset.codec === selectedAudioCodec) {
  952. button.classList.add('selected');
  953. }
  954. });
  955.  
  956. if (isAudioMode) {
  957. updateAudioOptions(dialog, selectedAudioCodec, localStorage.getItem('cobaltToolsBitrate') || '320');
  958. } else {
  959. updateQualityOptions(dialog, selectedVideoCodec, localStorage.getItem('cobaltToolsQuality') || '1080p');
  960. }
  961.  
  962. if (selectedVideoCodec === 'dub') {
  963. dubSelector.style.display = 'block';
  964. dialog.querySelector('#quality-options').style.display = 'none';
  965. } else {
  966. dubSelector.style.display = 'none';
  967. dialog.querySelector('#quality-options').style.display = 'grid';
  968. }
  969. }
  970.  
  971. const codecButtons = dialog.querySelectorAll('.codec-button');
  972. codecButtons.forEach(button => {
  973. button.addEventListener('click', () => {
  974. if (isAudioMode) {
  975. selectedAudioCodec = button.dataset.codec;
  976. localStorage.setItem('cobaltToolsAudioCodec', selectedAudioCodec);
  977. } else {
  978. selectedVideoCodec = button.dataset.codec;
  979. localStorage.setItem('cobaltToolsCodec', selectedVideoCodec);
  980. }
  981. updateCodecButtons();
  982. });
  983. });
  984.  
  985. updateCodecButtons();
  986.  
  987. const dubSelect = dialog.querySelector('.dub-select');
  988. if (dubSelect) {
  989. dubSelect.value = savedDub;
  990. dubSelect.addEventListener('change', () => {
  991. localStorage.setItem('cobaltToolsDub', dubSelect.value);
  992. });
  993. }
  994.  
  995. const cancelButton = dialog.querySelector('#cancel-button');
  996. const downloadButton = dialog.querySelector('#download-button');
  997.  
  998. if (cancelButton) {
  999. cancelButton.addEventListener('click', () => closeDialog(dialog, backdrop));
  1000. cancelButton.addEventListener('mouseover', () => {
  1001. cancelButton.style.background = '#f3727f';
  1002. cancelButton.style.borderColor = '#f3727f';
  1003. cancelButton.style.color = '#000000';
  1004. });
  1005. cancelButton.addEventListener('mouseout', () => {
  1006. cancelButton.style.background = 'transparent';
  1007. cancelButton.style.borderColor = '#e1e1e1';
  1008. cancelButton.style.color = '#e1e1e1';
  1009. });
  1010. }
  1011.  
  1012. if (downloadButton) {
  1013. downloadButton.addEventListener('click', () => {
  1014. if (isAudioMode) {
  1015. const selectedFormat = selectedAudioCodec;
  1016. const selectedBitrate = selectedFormat === 'wav' ? 'WAV' : dialog.querySelector('input[name="bitrate"]:checked')?.value || '320';
  1017. if (selectedFormat && currentVideoId) {
  1018. downloadAudio(selectedFormat, selectedBitrate, currentVideoId, dialog, backdrop);
  1019. }
  1020. } else {
  1021. if (selectedVideoCodec === 'dub') {
  1022. downloadVideo('dub', currentVideoId, 'dub', dialog, backdrop);
  1023. } else {
  1024. const selectedQuality = dialog.querySelector('input[name="quality"]:checked');
  1025. if (selectedQuality && currentVideoId) {
  1026. downloadVideo(selectedQuality.value, currentVideoId, selectedVideoCodec, dialog, backdrop);
  1027. }
  1028. }
  1029. }
  1030. });
  1031. downloadButton.addEventListener('mouseover', () => {
  1032. downloadButton.style.background = '#1ed760';
  1033. downloadButton.style.borderColor = '#1ed760';
  1034. downloadButton.style.color = '#000000';
  1035. });
  1036. downloadButton.addEventListener('mouseout', () => {
  1037. downloadButton.style.background = 'transparent';
  1038. downloadButton.style.borderColor = '#e1e1e1';
  1039. downloadButton.style.color = '#e1e1e1';
  1040. });
  1041. }
  1042.  
  1043. return dialog;
  1044. }
  1045. const observer = new MutationObserver(() => {
  1046. if (window.location.pathname.includes('/watch')) {
  1047. insertDownloadButton();
  1048. }
  1049. });
  1050. observer.observe(document.body, { childList: true, subtree: true });
  1051. insertDownloadButton();
  1052. window.addEventListener('yt-navigate-finish', insertDownloadButton);
  1053. })();

QingJ © 2025

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