YouTube video downloader

A simple userscript to download YouTube videos in MAX QUALITY

目前為 2023-11-23 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name YouTube video downloader
  3. // @icon https://raw.githubusercontent.com/madkarmaa/youtube-downloader/main/images/icon.png
  4. // @namespace aGkgdGhlcmUgOik=
  5. // @source https://github.com/madkarmaa/youtube-downloader
  6. // @supportURL https://github.com/madkarmaa/youtube-downloader
  7. // @version 1.2.0
  8. // @description A simple userscript to download YouTube videos in MAX QUALITY
  9. // @author mk_
  10. // @match *://*.youtube.com/*
  11. // @connect co.wuk.sh
  12. // @connect raw.githubusercontent.com
  13. // @grant GM_addStyle
  14. // @grant GM.xmlHttpRequest
  15. // @grant GM.xmlhttpRequest
  16. // @run-at document-end
  17. // ==/UserScript==
  18.  
  19. (async () => {
  20. 'use strict';
  21.  
  22. function Cobalt(videoUrl, audioOnly = false) {
  23. // Use Promise because GM.xmlHttpRequest is async and behaves differently with different userscript managers
  24. return new Promise((resolve, reject) => {
  25. // https://github.com/wukko/cobalt/blob/current/docs/api.md
  26. GM.xmlHttpRequest({
  27. method: 'POST',
  28. url: 'https://co.wuk.sh/api/json',
  29. headers: {
  30. 'Cache-Control': 'no-cache',
  31. Accept: 'application/json',
  32. 'Content-Type': 'application/json',
  33. },
  34. data: JSON.stringify({
  35. url: encodeURI(videoUrl), // video url
  36. vQuality: 'max', // always max quality
  37. filenamePattern: 'basic', // file name = video title
  38. isAudioOnly: audioOnly,
  39. disableMetadata: true, // privacy
  40. }),
  41. onload: (response) => {
  42. const data = JSON.parse(response.responseText);
  43. if (data?.url) resolve(data.url);
  44. else reject(data);
  45. },
  46. onerror: (err) => reject(err),
  47. });
  48. });
  49. }
  50.  
  51. // https://stackoverflow.com/a/61511955
  52. function waitForElement(selector) {
  53. return new Promise((resolve) => {
  54. if (document.querySelector(selector)) return resolve(document.querySelector(selector));
  55.  
  56. const observer = new MutationObserver(() => {
  57. if (document.querySelector(selector)) {
  58. observer.disconnect();
  59. resolve(document.querySelector(selector));
  60. }
  61. });
  62.  
  63. observer.observe(document.body, { childList: true, subtree: true });
  64. });
  65. }
  66.  
  67. // wait for the share button to appear before continuing
  68. const shareButton = await waitForElement(
  69. 'div#player div.ytp-chrome-controls div.ytp-right-controls button[aria-label="Settings"]'
  70. );
  71.  
  72. const downloadButton = document.createElement('button');
  73.  
  74. const buttonId = `yt-downloader-btn-${Math.floor(Math.random() * Date.now())}`;
  75. downloadButton.id = buttonId;
  76. downloadButton.title = 'Click to download as video\nRight click to download as audio';
  77. downloadButton.innerHTML =
  78. '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="24" viewBox="0 0 24 24" width="24" focusable="false" style="pointer-events: none; display: block; width: 100%; height: 100%;"><path d="M17 18v1H6v-1h11zm-.5-6.6-.7-.7-3.8 3.7V4h-1v10.4l-3.8-3.8-.7.7 5 5 5-4.9z"></path></svg>';
  79. downloadButton.classList = shareButton.classList;
  80. downloadButton.classList.add('ytp-hd-quality-badge');
  81.  
  82. // normal click => download video
  83. downloadButton.addEventListener('click', async () => {
  84. try {
  85. window.open(await Cobalt(window.location.href), '_blank');
  86. } catch (err) {
  87. window.alert(err);
  88. }
  89. });
  90. // right click => download audio
  91. downloadButton.addEventListener('contextmenu', async (e) => {
  92. e.preventDefault();
  93. try {
  94. window.open(await Cobalt(window.location.href, true), '_blank');
  95. } catch (err) {
  96. window.alert(err);
  97. }
  98. return false;
  99. });
  100.  
  101. GM_addStyle(`
  102. #${buttonId} > svg {
  103. margin-top: 3px;
  104. margin-bottom: -3px;
  105. }
  106.  
  107. #${buttonId}:hover > svg {
  108. fill: #f00;
  109. }
  110. `);
  111.  
  112. const buttonsRow = await waitForElement('div#player div.ytp-chrome-controls div.ytp-right-controls');
  113. if (!buttonsRow.contains(downloadButton)) buttonsRow.insertBefore(downloadButton, buttonsRow.firstChild);
  114. })();