YouTube downloader

A simple userscript to download YouTube videos in MAX QUALITY

目前为 2023-11-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube 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.3.6
  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. const randomNumber = Math.floor(Math.random() * Date.now());
  23.  
  24. function Cobalt(videoUrl, audioOnly = false) {
  25. // Use Promise because GM.xmlHttpRequest is async and behaves differently with different userscript managers
  26. return new Promise((resolve, reject) => {
  27. // https://github.com/wukko/cobalt/blob/current/docs/api.md
  28. GM.xmlHttpRequest({
  29. method: 'POST',
  30. url: 'https://co.wuk.sh/api/json',
  31. headers: {
  32. 'Cache-Control': 'no-cache',
  33. Accept: 'application/json',
  34. 'Content-Type': 'application/json',
  35. },
  36. data: JSON.stringify({
  37. url: encodeURI(videoUrl), // video url
  38. vQuality: 'max', // always max quality
  39. filenamePattern: 'basic', // file name = video title
  40. isAudioOnly: audioOnly,
  41. disableMetadata: true, // privacy
  42. }),
  43. onload: (response) => {
  44. const data = JSON.parse(response.responseText);
  45. if (data?.url) resolve(data.url);
  46. else reject(data);
  47. },
  48. onerror: (err) => reject(err),
  49. });
  50. });
  51. }
  52.  
  53. // https://stackoverflow.com/a/61511955
  54. function waitForElement(selector) {
  55. return new Promise((resolve) => {
  56. if (document.querySelector(selector)) return resolve(document.querySelector(selector));
  57.  
  58. const observer = new MutationObserver(() => {
  59. if (document.querySelector(selector)) {
  60. observer.disconnect();
  61. resolve(document.querySelector(selector));
  62. }
  63. });
  64.  
  65. observer.observe(document.body, { childList: true, subtree: true });
  66. });
  67. }
  68.  
  69. function notify(title, message) {
  70. const notificationContainer = document.createElement('div');
  71. notificationContainer.id = `yt-downloader-notification-${randomNumber}`;
  72.  
  73. const titleElement = document.createElement('h3');
  74. titleElement.textContent = title;
  75.  
  76. const messageElement = document.createElement('span');
  77. messageElement.textContent = message;
  78.  
  79. const closeButton = document.createElement('button');
  80. closeButton.innerHTML =
  81. '<svg xmlns="http://www.w3.org/2000/svg" height="1.5rem" viewBox="0 0 384 512"><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>';
  82. closeButton.addEventListener('click', () => {
  83. notificationContainer.remove();
  84. });
  85.  
  86. notificationContainer.append(titleElement, messageElement, closeButton);
  87. document.body.appendChild(notificationContainer);
  88. }
  89.  
  90. // true if youtube, false if youtube music
  91. const YOUTUBE_SERVICE = window.location.hostname.split('.')[0] !== 'music';
  92.  
  93. // wait for the button to copy to appear before continuing
  94. const buttonToCopy = await waitForElement(
  95. YOUTUBE_SERVICE
  96. ? 'div#player div.ytp-chrome-controls div.ytp-right-controls button[aria-label="Settings"]'
  97. : '[slot="player-bar"] div.middle-controls div.middle-controls-buttons #like-button-renderer #button-shape-dislike button[aria-label="Dislike"]'
  98. );
  99.  
  100. const downloadButton = document.createElement('button');
  101.  
  102. const buttonId = `yt-downloader-btn-${randomNumber}`;
  103. downloadButton.id = buttonId;
  104. downloadButton.title = 'Click to download as video\nRight click to download as audio';
  105. downloadButton.innerHTML =
  106. '<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>';
  107. downloadButton.classList = buttonToCopy.classList;
  108.  
  109. if (YOUTUBE_SERVICE) downloadButton.classList.add('ytp-hd-quality-badge');
  110. downloadButton.classList.add(YOUTUBE_SERVICE ? 'YT' : 'YTM');
  111.  
  112. // normal click => download video
  113. downloadButton.addEventListener('click', async () => {
  114. if (!window.location.pathname.slice(1))
  115. return notify('Hey!', 'The video/song player is not open, I cannot see the link to download!'); // do nothing if video is not focused
  116.  
  117. try {
  118. window.open(await Cobalt(window.location.href), '_blank');
  119. } catch (err) {
  120. notify('An error occurred!', JSON.stringify(err));
  121. }
  122. });
  123. // right click => download audio
  124. downloadButton.addEventListener('contextmenu', async (e) => {
  125. if (!window.location.pathname.slice(1))
  126. return notify('Hey!', 'The video/song player is not open, I cannot see the link to download!'); // do nothing if video is not focused
  127.  
  128. e.preventDefault();
  129. try {
  130. window.open(await Cobalt(window.location.href, true), '_blank');
  131. } catch (err) {
  132. notify('An error occurred!', JSON.stringify(err));
  133. }
  134. return false;
  135. });
  136.  
  137. GM_addStyle(`
  138. #${buttonId}.YT > svg {
  139. margin-top: 3px;
  140. margin-bottom: -3px;
  141. }
  142.  
  143. #${buttonId}:hover > svg {
  144. fill: #f00;
  145. }
  146.  
  147. #yt-downloader-notification-${randomNumber} {
  148. background-color: #282828;
  149. color: #fff;
  150. border: 2px solid #fff;
  151. border-radius: 8px;
  152. position: fixed;
  153. top: 0;
  154. right: 0;
  155. margin-top: 10px;
  156. margin-right: 10px;
  157. padding: 15px;
  158. z-index: 999;
  159. }
  160.  
  161. #yt-downloader-notification-${randomNumber} > h3 {
  162. color: #f00;
  163. font-size: 2.5rem;
  164. }
  165.  
  166. #yt-downloader-notification-${randomNumber} > span {
  167. font-style: italic;
  168. font-size: 1.5rem;
  169. }
  170.  
  171. #yt-downloader-notification-${randomNumber} > button {
  172. position: absolute;
  173. top: 0;
  174. right: 0;
  175. background: none;
  176. border: none;
  177. outline: none;
  178. width: fit-content;
  179. height: fit-content;
  180. margin: 5px;
  181. padding: 0;
  182. }
  183.  
  184. #yt-downloader-notification-${randomNumber} > button > svg {
  185. fill: #fff;
  186. }
  187. `);
  188.  
  189. const buttonsRow = await waitForElement(
  190. YOUTUBE_SERVICE
  191. ? 'div#player div.ytp-chrome-controls div.ytp-right-controls'
  192. : '[slot="player-bar"] div.middle-controls div.middle-controls-buttons'
  193. );
  194. if (!buttonsRow.contains(downloadButton)) buttonsRow.insertBefore(downloadButton, buttonsRow.firstChild);
  195. })();