YoutubeDL

Download youtube videos at the comfort of your browser.

目前为 2023-07-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YoutubeDL
  3. // @namespace https://www.youtube.com/
  4. // @version 1.0.1
  5. // @description Download youtube videos at the comfort of your browser.
  6. // @author realcoloride
  7. // @match https://www.youtube.com/*
  8. // @match https://www.youtube.com/watch*
  9. // @match https://www.youtube.com/shorts*
  10. // @match https://www.youtube.com/embed*
  11. // @connect savetube.io
  12. // @connect googlevideo.com
  13. // @connect aadika.xyz
  14. // @connect dlsnap11.xyz
  15. // @connect githubusercontent.com
  16. // @connect *
  17. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  18. // @license MIT
  19. // @grant GM.xmlHttpRequest
  20. // ==/UserScript==
  21.  
  22. (function() {
  23. 'use strict';
  24.  
  25. let pageInformation = {
  26. loaded : false,
  27. website : "https://savetube.io",
  28. searchEndpoint : null,
  29. convertEndpoint : null,
  30. checkingEndpoint : null,
  31. pageValues : {}
  32. }
  33.  
  34. // Process:
  35. // Search -> Checking -> Convert by -> Convert using c_server
  36.  
  37. const githubAssetEndpoint = "https://raw.githubusercontent.com/realcoloride/YoutubeDL/main/";
  38.  
  39. let videoInformation;
  40. const fetchHeaders = {
  41. 'Accept': '*/*',
  42. 'Accept-Language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
  43. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  44. 'Sec-Ch-Ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
  45. 'Sec-Ch-Ua-Mobile': '?0',
  46. 'Sec-Ch-Ua-Platform': '"Windows"',
  47. 'Sec-Fetch-Dest': 'empty',
  48. 'Sec-Fetch-Mode': 'cors',
  49. 'Sec-Fetch-Site': 'none',
  50. };
  51. const convertHeaders = {
  52. "accept": "*/*",
  53. "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
  54. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  55. "sec-ch-ua": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"",
  56. "sec-ch-ua-mobile": "?0",
  57. "sec-ch-ua-platform": "\"Windows\"",
  58. "sec-fetch-dest": "empty",
  59. "sec-fetch-mode": "cors",
  60. "sec-fetch-site": "cross-site",
  61. "x-requested-key": "de0cfuirtgf67a"
  62. };
  63. const downloadHeaders = {
  64. "accept": "*/*",
  65. "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
  66. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  67. "sec-ch-ua": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"",
  68. "sec-ch-ua-mobile": "?0",
  69. "sec-ch-ua-platform": "\"Windows\"",
  70. "sec-fetch-dest": "empty",
  71. "sec-fetch-mode": "cors",
  72. "sec-fetch-site": "same-origin",
  73. "x-requested-with": "XMLHttpRequest"
  74. };
  75.  
  76. const popupHTML = `
  77. <div id="youtubeDL-popup">
  78. <span class="youtubeDL-text bigger float">
  79. <img src="{asset}YoutubeDL.png" class="youtubeDL-logo float">
  80. YoutubeDL - Download video
  81. <button id="youtubeDL-close" class="youtubeDL-button youtubeDL-align right" aria-label="Cancel">
  82. <span>Close</span>
  83. </button>
  84. </span>
  85.  
  86. <hr style="height:3px">
  87.  
  88. <div id="youtubeDL-loading">
  89. <span class="youtubeDL-text medium center float" style="display: flex;">
  90. <img src="{asset}loading.svg" style="width:21px; padding-right: 6px;"> Loading...
  91. </span>
  92. </div>
  93.  
  94. <div id="youtubeDL-quality">
  95. <span class="youtubeDL-text medium center float" >Select a quality and click on Download.</span><br>
  96. <span class="youtubeDL-text medium center float" style="margin-bottom: 10px;">
  97. ⚠️ CLICK
  98. <a href="{asset}allow.gif" target="_blank"><strong>"ALWAYS ALLOW ALL DOMAINS"</strong></a>
  99. WHEN DOWNLOADING FOR THE FIRST TIME.
  100. <span class="youtubeDL-text center float">Some providers may have a bigger file size than estimated.</span>
  101. </span>
  102. <table id="youtubeDL-quality-table" style="width: 100%; border-spacing: 0;">
  103. <thead class="youtubeDL-row">
  104. <th class="youtubeDL-column youtubeDL-text">Format</th>
  105. <th class="youtubeDL-column youtubeDL-text">Quality</th>
  106. <th class="youtubeDL-column youtubeDL-text">Estimated Size</th>
  107. <th class="youtubeDL-column youtubeDL-text">Download</th>
  108. </thead>
  109. <tbody id="youtubeDL-quality-container">
  110. </tbody>
  111. </table>
  112. </div>
  113.  
  114. <div class="youtubeDL-credits">
  115. <span class="youtubeDL-text medium">YoutubeDL by (real)coloride - 2023</span>
  116. <br>
  117. <a class="youtubeDL-text medium" href="https://www.github.com/realcoloride/YoutubeDL">
  118. <img src="{asset}github.png" width="21px">Github</a>
  119. <a class="youtubeDL-text medium" href="https://opensource.org/license/mit/">
  120. <img src="{asset}mit.png" width="21px">MIT license
  121. </a>
  122. </div>
  123. </div>
  124. `;
  125. // Element definitions
  126. const ytdAppContainer = document.querySelector("ytd-app");
  127. let popupElement;
  128.  
  129. // Information gathering
  130. function getVideoInformation(url) {
  131. const regex = /(?:https?:\/\/(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/|embed\/)?)([\w-]+)/i;
  132. const match = regex.exec(url);
  133. const videoId = match ? match[1] : null;
  134. let type = null;
  135. if (url.includes("/shorts/")) type = "shorts";
  136. else if (url.includes("/watch?v=")) type = "video";
  137. else if (url.includes("/embed/")) type = "embed";
  138. return { type, videoId };
  139. };
  140.  
  141. // Fetching
  142. function convertSizeToBytes(size) {
  143. const units = {
  144. B: 1,
  145. KB: 1024,
  146. MB: 1024 * 1024,
  147. GB: 1024 * 1024 * 1024,
  148. };
  149. const regex = /^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i;
  150. const match = size.match(regex);
  151. if (!match) {
  152. throw new Error('Invalid size format');
  153. }
  154. const value = parseFloat(match[1]);
  155. const unit = match[2].toUpperCase();
  156. if (!units.hasOwnProperty(unit)) {
  157. throw new Error('Invalid size unit');
  158. }
  159. return value * units[unit];
  160. }
  161. function decipherVariables(variableString) {
  162. const variableDict = {};
  163. const variableAssignments = variableString.match(/var\s+(\w+)\s*=\s*(.+?);/g);
  164. variableAssignments.forEach((assignment) => {
  165. const [, variableName, variableValue] = assignment.match(/var\s+(\w+)\s*=\s*['"](.+?)['"];/);
  166. const trimmedValue = variableValue.trim().replace(/^['"]|['"]$/g, '');
  167. variableDict[variableName] = trimmedValue;
  168. });
  169. return variableDict;
  170. }
  171. function isTimestampExpired(timestamp) {
  172. const currentTimestamp = Math.floor(Date.now() / 1000);
  173. return currentTimestamp > timestamp;
  174. }
  175. async function fetchPageInformation() {
  176. // Scrapping internal values
  177. const pageRequest = await GM.xmlHttpRequest({
  178. url: `${pageInformation.website}`,
  179. method: "GET",
  180. headers: fetchHeaders,
  181. });
  182.  
  183. const parser = new DOMParser();
  184. const pageDocument = parser.parseFromString(pageRequest.responseText, "text/html");
  185.  
  186. let scrappedScriptElement;
  187.  
  188. pageDocument.querySelectorAll("script").forEach((scriptElement) => {
  189. const scriptHTML = scriptElement.innerHTML;
  190. if (scriptHTML.includes("k_time") && scriptHTML.includes("k_page")) {
  191. scrappedScriptElement = scriptElement;
  192. return;
  193. }
  194. });
  195.  
  196. const pageValues = decipherVariables(scrappedScriptElement.innerHTML);
  197. pageInformation.pageValues = pageValues;
  198.  
  199. pageInformation.searchEndpoint = pageValues['k_url_search'];
  200. pageInformation.convertEndpoint = pageValues['k_url_convert'];
  201. pageInformation.checkingEndpoint = pageValues['k_url_check_task'];
  202.  
  203. pageInformation.loaded = true;
  204. }
  205. async function startConversion(fileExtension, fileQuality, timeExpires, token, filename, button) {
  206. const videoType = videoInformation.type;
  207. const videoId = videoInformation.videoId;
  208.  
  209. if (!videoType) return;
  210.  
  211. const initialFormData = new FormData();
  212. initialFormData.append('v_id', videoId);
  213. initialFormData.append('ftype', fileExtension);
  214. initialFormData.append('fquality', fileQuality);
  215. initialFormData.append('token', token);
  216. initialFormData.append('timeExpire', timeExpires);
  217. initialFormData.append('client', 'SaveTube.io');
  218. const initialRequestBody = new URLSearchParams(initialFormData).toString();
  219.  
  220. let result = null;
  221.  
  222. try {
  223. const payload = {
  224. url: pageInformation.convertEndpoint,
  225. method: "POST",
  226. headers: convertHeaders,
  227. data: initialRequestBody,
  228. responseType: 'text',
  229. referrerPolicy: "strict-origin-when-cross-origin",
  230. mode: "cors",
  231. credentials: "omit"
  232. };
  233.  
  234. const initialRequest = await GM.xmlHttpRequest(payload);
  235. const initialResponse = JSON.parse(initialRequest.responseText);
  236.  
  237. // Needs conversion is it links to a server
  238. const downloadLink = initialResponse.d_url;
  239. const needsConversation = (downloadLink == null);
  240. if (needsConversation) {
  241. updatePopupButton(button, 'Converting...');
  242. const conversionServerEndpoint = initialResponse.c_server;
  243.  
  244. const convertFormData = new FormData();
  245. convertFormData.append('v_id', videoId);
  246. convertFormData.append('ftype', fileExtension);
  247. convertFormData.append('fquality', fileQuality);
  248. convertFormData.append('fname', filename);
  249. convertFormData.append('token', token);
  250. convertFormData.append('timeExpire', timeExpires);
  251. const convertRequestBody = new URLSearchParams(convertFormData).toString();
  252.  
  253. const convertRequest = await GM.xmlHttpRequest({
  254. url: `${conversionServerEndpoint}/api/json/convert`,
  255. method: "POST",
  256. headers: convertHeaders,
  257. data: convertRequestBody,
  258. responseType: 'text',
  259. });
  260.  
  261. let convertResponse;
  262.  
  263. let adaptedResponse = {};
  264. let result;
  265.  
  266. try {
  267. convertResponse = JSON.parse(convertRequest.responseText);
  268. result = convertResponse.result;
  269. adaptedResponse = {
  270. c_status : convertResponse.status,
  271. d_url: result
  272. }
  273. } catch (error) {
  274. alert("[YoutubeDL] Converting failed.\nYou might have been downloading too fast and have been rate limited or your antivirus may be blocking the media.\n(💡 If so, refresh the page or check your antivirus's settings.)")
  275.  
  276. result = "error";
  277. adaptedResponse = {
  278. c_status : "error"
  279. }
  280. return adaptedResponse;
  281. }
  282.  
  283. if (result == 'Converting') { // Not converted
  284. const jobId = convertResponse.jobId;
  285.  
  286. console.log(`[YoutubeDL] Download needs to be checked on, jobId: ${jobId}, waiting...`);
  287. updatePopupButton(button, 'Waiting for server...');
  288.  
  289. async function gatherResult() {
  290. return new Promise(async(resolve, reject) => {
  291. const parsedURL = new URL(conversionServerEndpoint);
  292. const protocol = parsedURL.protocol === "https:" ? "wss:" : "ws:";
  293. const websocketURL = `${protocol}//${parsedURL.host}/sub/${jobId}?fname=${pageInformation.pageValues.k_prefix_name}`;
  294. const socket = new WebSocket(websocketURL);
  295.  
  296. socket.onmessage = function(event) {
  297. const message = JSON.parse(event.data);
  298.  
  299. switch (message.action) {
  300. case "success":
  301. socket.close();
  302. resolve(message.url);
  303. case "progress":
  304. updatePopupButton(button, `Converting... ${message.value}%`)
  305. case "error":
  306. socket.close();
  307. reject("WSCheck fail");
  308. };
  309. };
  310. });
  311. };
  312.  
  313. try {
  314. const conversionUrl = await gatherResult();
  315. adaptedResponse.d_url = conversionUrl;
  316. } catch (error) {
  317. console.error("[YoutubeDL] Error while checking for job converstion: ", error);
  318. adaptedResponse.c_status = 'error';
  319. }
  320. }
  321.  
  322. return adaptedResponse;
  323. } else {
  324. result = initialResponse;
  325. }
  326. } catch (error) {
  327. console.error(error);
  328. return null;
  329. }
  330.  
  331. return result;
  332. }
  333. async function getMediaInformation() {
  334. const videoType = videoInformation.type;
  335. const videoId = videoInformation.videoId;
  336.  
  337. if (!videoType) return;
  338.  
  339. const formData = new FormData();
  340. formData.append('q', `https://www.youtube.com/watch?v=${videoId}`);
  341. formData.append('vt', 'home');
  342. const requestBody = new URLSearchParams(formData).toString();
  343.  
  344. let result = null;
  345.  
  346. try {
  347. const request = await GM.xmlHttpRequest({
  348. url: pageInformation.searchEndpoint,
  349. method: "POST",
  350. headers: fetchHeaders,
  351. data: requestBody,
  352. responseType: 'text',
  353. });
  354. result = JSON.parse(request.responseText);
  355. } catch (error) {
  356. return null;
  357. }
  358.  
  359. return result;
  360. }
  361.  
  362. // Light mode/Dark mode
  363. function isDarkMode() {
  364. if (videoInformation.type == 'embed') return true;
  365. const computedStyles = window.getComputedStyle(ytdAppContainer);
  366.  
  367. const backgroundColor = computedStyles["background-color"];
  368.  
  369. return backgroundColor.endsWith('15)');
  370. }
  371. function toggleLightClass(queryTarget) {
  372. const elements = document.querySelectorAll(queryTarget);
  373. elements.forEach((element) => {
  374. element.classList.toggle("light");
  375. toggleLightClassRecursive(element);
  376. });
  377. }
  378. function toggleLightClassRecursive(element) {
  379. const children = element.children;
  380. for (let i = 0; i < children.length; i++) {
  381. children[i].classList.toggle("light");
  382. toggleLightClassRecursive(children[i]);
  383. }
  384. }
  385.  
  386. // Popup
  387. // Links
  388. // Downloading
  389. async function downloadFile(button, url, filename) {
  390. const baseText = `Download`;
  391. button.disabled = true;
  392. updatePopupButton(button, "Downloading...");
  393. console.log(`[YoutubeDL] Downloading media URL: ${url}`);
  394. function finish() {
  395. updatePopupButton(button, baseText);
  396. if (button.disabled) button.disabled = false
  397. }
  398.  
  399. GM.xmlHttpRequest({
  400. method: 'GET',
  401. headers: downloadHeaders,
  402. url: url,
  403. responseType: 'blob',
  404. onload: async function(response) {
  405. console.log(response);
  406. if (response.status == 403) {
  407. alert("[YoutubeDL] Media expired or may be impossible to download, please retry or try with another format, sorry!");
  408. await reloadMedia();
  409. return;
  410. }
  411. const blob = response.response;
  412. const link = document.createElement('a');
  413.  
  414. link.href = URL.createObjectURL(blob);
  415. link.setAttribute('download', filename);
  416. link.click();
  417.  
  418. URL.revokeObjectURL(link.href);
  419. updatePopupButton(button, 'Downloaded!');
  420. button.disabled = false;
  421.  
  422. setTimeout(finish, 1000);
  423. },
  424. onerror: function(error) {
  425. console.error('[YoutubeDL] Download Error:', error);
  426. updatePopupButton(button, 'Download Failed');
  427. setTimeout(finish, 1000);
  428. },
  429. onprogress: function(progressEvent) {
  430. if (progressEvent.lengthComputable) {
  431. const percentComplete = Math.round((progressEvent.loaded / progressEvent.total) * 100);
  432. updatePopupButton(button, `Downloading: ${percentComplete}%`);
  433. } else
  434. updatePopupButton(button, 'Downloading...');
  435. }
  436. });
  437. }
  438. function updatePopupButton(button, text) {
  439. button.innerHTML = `<strong>${text}</strong>`;
  440. if (!isDarkMode()) button.classList.add('light');
  441. }
  442. async function createMediaFile(params) {
  443. let { format, quality, size, extension, timeExpires, videoTitle, token } = params;
  444.  
  445. const qualityContainer = getPopupElement("quality-container");
  446.  
  447. const row = document.createElement("tr");
  448. row.classList.add("youtubeDL-row");
  449.  
  450. function createRowElement() {
  451. const rowElement = document.createElement("td");
  452. rowElement.classList.add("youtubeDL-row-element");
  453.  
  454. return rowElement;
  455. }
  456. function addRowElement(rowElement) {
  457. row.appendChild(rowElement);
  458. }
  459.  
  460. function createSpanText(text, targetElement) {
  461. const spanText = document.createElement("span");
  462. spanText.classList.add("youtubeDL-text");
  463.  
  464. spanText.innerHTML = `<strong>${text}</strong>`;
  465. if (!isDarkMode()) spanText.classList.add('light');
  466.  
  467. targetElement.appendChild(spanText);
  468. }
  469.  
  470. // Format
  471. const formatRowElement = createRowElement();
  472. createSpanText(format, formatRowElement);
  473. addRowElement(formatRowElement);
  474.  
  475. // Quality
  476. const qualityRowElement = createRowElement();
  477. createSpanText(quality, qualityRowElement);
  478. addRowElement(qualityRowElement);
  479. // Size
  480. const sizeRowElement = createRowElement();
  481. createSpanText(size, sizeRowElement);
  482. addRowElement(sizeRowElement);
  483.  
  484. const downloadRowElement = createRowElement();
  485. const downloadButton = document.createElement("button");
  486. downloadButton.classList.add("youtubeDL-button");
  487. downloadButton.ariaLabel = "Download";
  488. updatePopupButton(downloadButton, "Download");
  489.  
  490. downloadButton.addEventListener("click", async(event) => {
  491. try {
  492. downloadButton.disabled = true;
  493. updatePopupButton(downloadButton, "Fetching info...");
  494.  
  495. if (isTimestampExpired(pageInformation.pageValues.k_time)) {
  496. await reloadMedia();
  497. return;
  498. }
  499.  
  500. extension = extension.replace(/ \(audio\)|kbps/g, '');
  501. quality = quality.replace(/ \(audio\)|kbps/g, '');
  502. let filename = `YoutubeDL_${videoTitle}_${quality}.${extension}`;
  503. if (extension == "mp3") filename = `YoutubeDL_${videoTitle}.${extension}`;
  504. const conversionRequest = await startConversion(extension, quality, timeExpires, token, filename, downloadButton);
  505. const conversionStatus = conversionRequest.c_status;
  506.  
  507. async function fail() {
  508. throw Error("Failed to download.");
  509. }
  510.  
  511. if (!conversionStatus) { fail(); return; }
  512. if (conversionStatus != 'ok' && conversionStatus != 'success') { fail(); return; }
  513.  
  514. const downloadLink = conversionRequest.d_url;
  515. await downloadFile(downloadButton, downloadLink, filename);
  516. } catch (error) {
  517. console.error(error);
  518.  
  519. downloadButton.disabled = true;
  520. updatePopupButton(downloadButton, '');
  521.  
  522. setTimeout(() => {
  523. downloadButton.disabled = false;
  524. updatePopupButton(downloadButton, 'Download');
  525. }, 2000);
  526. }
  527. });
  528.  
  529. downloadRowElement.appendChild(downloadButton);
  530. addRowElement(downloadRowElement);
  531.  
  532. qualityContainer.appendChild(row);
  533. }
  534. async function loadMediaFromLinks(response) {
  535. try {
  536. const links = response.links;
  537. const token = response.token;
  538. const timeExpires = response.timeExpires;
  539. const videoTitle = response.title;
  540.  
  541. const audioLinks = links.mp3;
  542. let videoLinks = links.mp4;
  543.  
  544. function addFormat(information) {
  545. const format = information.f;
  546. if (!format) return;
  547.  
  548. const quality = information.q;
  549. let size = information.size;
  550.  
  551. const regex = /\s[BKMGT]?B/;
  552. const unit = size.match(regex)[0];
  553. const sizeNoUnit = size.replace(regex, "");
  554. const roundedSize = Math.round(parseFloat(sizeNoUnit));
  555. size = `${roundedSize}${unit}`;
  556.  
  557. createMediaFile({
  558. extension: format,
  559. quality,
  560. timeExpires,
  561. videoTitle,
  562.  
  563. format: format.toUpperCase(),
  564. size,
  565. token
  566. });
  567. }
  568.  
  569. // Audio will only have this one so it doesnt matter
  570. const defaultAudioFormat = audioLinks[Object.keys(audioLinks)[0]];
  571. defaultAudioFormat.f = "mp3 (audio)";
  572.  
  573. addFormat(defaultAudioFormat);
  574.  
  575. // Format sorting first
  576. // Remove auto quality
  577. videoLinks["auto"] = null;
  578.  
  579. // Store 3gp quality if available
  580. const low3gpFormat = { ...videoLinks["3gp@144p"] };
  581. delete videoLinks["3gp@144p"];
  582.  
  583. // Sort from highest to lowest quality
  584. const qualities = {};
  585.  
  586. for (const [qualityId, information] of Object.entries(videoLinks)) {
  587. if (!information) continue;
  588.  
  589. const qualityName = information.q;
  590. const strippedQualityName = qualityName.replace('p', '');
  591. const quality = parseInt(strippedQualityName);
  592.  
  593. qualities[quality] = qualityId;
  594. }
  595.  
  596. const newOrder = Object.keys(qualities).sort((a, b) => a - b);
  597.  
  598. function swapKeys(object, victimKeys, targetKeys) {
  599. const swappedObj = {};
  600.  
  601. victimKeys.forEach((key, index) => {
  602. swappedObj[targetKeys[index]] = object[key];
  603. });
  604.  
  605. return swappedObj;
  606. }
  607. videoLinks = swapKeys(videoLinks, Object.keys(videoLinks), newOrder);
  608. // Bubble swapping estimated qualities if incorrect (by provider)
  609. function bubbleSwap() {
  610. const videoLinkIds = Object.keys(videoLinks);
  611. videoLinkIds.forEach((qualityId) => {
  612. const currentQualityInformation = videoLinks[qualityId];
  613. if (!currentQualityInformation) return;
  614.  
  615. const currentQualityIndex = videoLinkIds.findIndex((id) => id === qualityId);
  616. if (currentQualityIndex - 1 < 0) return;
  617.  
  618. const previousQualityIndex = currentQualityIndex - 1;
  619. const previousQualityId = videoLinkIds[previousQualityIndex];
  620.  
  621. if (!previousQualityId) return;
  622.  
  623. const previousQualityInformation = videoLinks[previousQualityId];
  624.  
  625. function getQualityOf(information) {
  626. const qualityName = information.q;
  627. const strippedQualityName = qualityName.replace('p', '');
  628. const quality = parseInt(strippedQualityName);
  629.  
  630. return { qualityName, strippedQualityName, quality };
  631. }
  632.  
  633. const previousQuality = getQualityOf(previousQualityInformation);
  634. const currentQuality = getQualityOf(currentQualityInformation);
  635.  
  636. function swap() {
  637. console.log(`[YoutubeDL] Swapping incorrect formats: [${previousQuality.qualityName}] ${previousQualityInformation.size} -> [${currentQuality.qualityName}] ${currentQualityInformation.size}`);
  638.  
  639. const previousClone = { ... previousQualityInformation};
  640. const currentClone = { ... currentQualityInformation};
  641.  
  642. previousQualityInformation.size = currentClone.size;
  643. currentQualityInformation.size = previousClone.size;
  644. }
  645.  
  646. const previousSize = previousQualityInformation.size;
  647. const previousSizeBytes = convertSizeToBytes(previousSize);
  648.  
  649. const currentSize = currentQualityInformation.size;
  650. const currentSizeBytes = convertSizeToBytes(currentSize);
  651.  
  652. if (previousSizeBytes < currentSizeBytes) swap();
  653. });
  654. };
  655.  
  656. for (let i = 0; i < Object.keys(videoLinks).length; i++) bubbleSwap();
  657. for (const [qualityId, information] of Object.entries(videoLinks)) {
  658. if (!information) continue;
  659.  
  660. const qualityName = information.q;
  661. const strippedQualityName = qualityName.replace('p', '');
  662. const quality = parseInt(strippedQualityName);
  663.  
  664. qualities[quality] = qualityId;
  665. addFormat(information);
  666. }
  667.  
  668. if (low3gpFormat) addFormat(low3gpFormat);
  669. } catch (error) {
  670. console.error("[YoutubeDL] Failed loading media:", error);
  671. alert("[YoutubeDL] Failed fetching media.\n" +
  672. "This could be either because:\n" +
  673. "- An unhandled error\n" +
  674. "- Your tampermonkey settings\n" +
  675. "or an issue with the API.\n\n" +
  676. "Try to refresh the page, otherwise, reinstall the plugin.")
  677.  
  678. togglePopup();
  679. popupElement.hidden = true;
  680. }
  681. }
  682. let isLoadingMedia = false;
  683. let hasLoadedMedia = false;
  684. function clearMedia() {
  685. const qualityContainer = getPopupElement("quality-container");
  686. qualityContainer.innerHTML = "";
  687.  
  688. isLoadingMedia = false;
  689. hasLoadedMedia = false;
  690. }
  691. async function reloadMedia() {
  692. console.log("[YoutubeDL] Hot reloading...");
  693.  
  694. const loadingBarSpan = getPopupElement("loading > span");
  695. loadingBarSpan.textContent = "Reloading...";
  696.  
  697. togglePopupLoading(true);
  698. clearMedia();
  699.  
  700. await fetchPageInformation();
  701. await loadMedia();
  702.  
  703. loadingBarSpan.textContent = "Loading...";
  704. }
  705. async function loadMedia() {
  706. if (isLoadingMedia || hasLoadedMedia) return;
  707. isLoadingMedia = true;
  708.  
  709. function fail() {
  710. isLoadingMedia = false;
  711. console.error("[YoutubeDL] Failed fetching media.");
  712. }
  713.  
  714. if (!isLoadingMedia) {togglePopup(); return; };
  715.  
  716. const request = await getMediaInformation();
  717. if (request.status != 'ok') { fail(); return; }
  718.  
  719. try {
  720. await loadMediaFromLinks(request);
  721.  
  722. hasLoadedMedia = true;
  723. togglePopupLoading(false);
  724. } catch (error) {
  725. console.error("[YoutubeDL] Failed fetching media content: ", error);
  726. hasLoadedMedia = false;
  727. }
  728. }
  729. // Getters
  730. function getPopupElement(element) {
  731. return document.querySelector(`#youtubeDL-${element}`);
  732. }
  733. // Loading and injection
  734. function togglePopupLoading(loading) {
  735. const loadingBar = getPopupElement("loading");
  736. const qualityContainer = getPopupElement("quality");
  737.  
  738. loadingBar.hidden = !loading;
  739. qualityContainer.hidden = loading;
  740. }
  741. function injectPopup() {
  742. /*<div id="youtubeDL-popup-bg" class="shown">
  743. </div>*/
  744. popupElement = document.createElement("div");
  745. popupElement.id = "youtubeDL-popup-bg";
  746.  
  747. const revisedHTML = popupHTML.replaceAll('{asset}', githubAssetEndpoint);
  748. popupElement.innerHTML = revisedHTML;
  749. document.body.appendChild(popupElement);
  750.  
  751. togglePopupLoading(true);
  752. createButtonConnections();
  753. popupElement.hidden = true;
  754. }
  755. let hideTimeout;
  756. let waitingReload = false;
  757. function togglePopup() {
  758. popupElement.classList.toggle("shown");
  759.  
  760. if (waitingReload) {reloadMedia(); waitingReload = false;}
  761. else loadMedia();
  762.  
  763. // Avoid overlap
  764. if (popupElement.hidden) {
  765. clearTimeout(hideTimeout);
  766.  
  767. hideTimeout = setTimeout(() => {
  768. popupElement.hidden = false;
  769. }, 200);
  770. };
  771. }
  772. // Button
  773. let injectedShorts = [];
  774. function injectDownloadButton() {
  775. let targets = [];
  776. let style;
  777.  
  778. const onShorts = (videoInformation.type == 'shorts');
  779. if (onShorts) {
  780. // Button for shorts
  781. const playerControls = document.querySelectorAll('ytd-shorts-player-controls');
  782.  
  783. targets = playerControls;
  784. style = "margin-bottom: 16px; transform: translateY(-15%); z-index: 999; pointer-events: auto;"
  785. } else {
  786. // Button for embed and normal player
  787. targets.push(document.querySelector(".ytp-left-controls"));
  788. style = "margin-top: 4px; transform: translateY(5%); padding-left: 4px;";
  789. }
  790.  
  791. targets.forEach((target) => {
  792. if (injectedShorts.includes(target)) return;
  793.  
  794. const downloadButton = document.createElement("button");
  795. downloadButton.classList.add("ytp-button");
  796. downloadButton.innerHTML = `<img src="${getAsset("YoutubeDL.png")}" style="${style}" width="36" height="36">`;
  797. downloadButton.id = 'youtubeDL-download'
  798. downloadButton.setAttribute('data-title-no-tooltip', 'YoutubeDL');
  799. downloadButton.setAttribute('aria-keyshortcuts', 'SHIFT+d');
  800. downloadButton.setAttribute('aria-label', 'Next keyboard shortcut SHIFT+d');
  801. downloadButton.setAttribute('data-duration', '');
  802. downloadButton.setAttribute('data-preview', '');
  803. downloadButton.setAttribute('data-tooltip-text', '');
  804. downloadButton.setAttribute('href', '');
  805. downloadButton.setAttribute('title', 'Download Video');
  806. downloadButton.addEventListener("click", (event) => {
  807. if (popupElement.hidden) {
  808. popupElement.hidden = false;
  809.  
  810. togglePopup();
  811. }
  812. });
  813. const chapterContainer = target.querySelector('.ytp-chapter-container');
  814.  
  815. if (onShorts) {
  816. target.insertBefore(downloadButton, target.children[1])
  817. injectedShorts.push(target);
  818. } else {
  819. if (chapterContainer) {
  820. downloadButton.style = "overflow: visible; padding-right: 6px; padding-left: 1px;";
  821. target.insertBefore(downloadButton, chapterContainer);
  822. }
  823. else target.appendChild(downloadButton);
  824. }
  825. });
  826. }
  827.  
  828. // Styles
  829. async function loadCSS(url) {
  830. return new Promise((resolve, reject) => {
  831. GM.xmlHttpRequest({
  832. method: 'GET',
  833. url: url,
  834. onload: function(response) {
  835. if (response.status === 200) {
  836. const style = document.createElement('style');
  837. style.innerHTML = response.responseText;
  838. document.head.appendChild(style);
  839. resolve();
  840. } else {
  841. reject(new Error('Failed to load CSS'));
  842. }
  843. }
  844. });
  845. });
  846. }
  847. function getAsset(filename) {
  848. return `${githubAssetEndpoint}${filename}`;
  849. }
  850. let stylesInjected = false;
  851. async function injectStyles() {
  852. if (stylesInjected) return;
  853. stylesInjected = true;
  854.  
  855. const asset = getAsset("youtubeDL.css");
  856. await loadCSS(asset);
  857. }
  858.  
  859. // Buttons
  860. function createButtonConnections() {
  861. const closeButton = popupElement.querySelector("#youtubeDL-close");
  862.  
  863. closeButton.addEventListener('click', (event) => {
  864. try {
  865. togglePopup();
  866. setTimeout(() => {
  867. popupElement.hidden = true;
  868. }, 200);
  869. } catch (error) {console.error(error);}
  870. });
  871. }
  872.  
  873. // Main page injection
  874. async function injectAll() {
  875. if (preinjected) return;
  876. preinjected = true;
  877.  
  878. console.log("[YoutubeDL] Initializing downloader...");
  879. try {
  880. await fetchPageInformation();
  881. } catch (error) {
  882. isLoadingMedia = false;
  883. console.error("[YoutubeDL] Failed fetching page information: ", error);
  884. }
  885.  
  886. console.log("[YoutubeDL] Loading custom styles...");
  887. await injectStyles();
  888.  
  889. console.log("[YoutubeDL] Loading popup...");
  890. injectPopup();
  891.  
  892. console.log("[YoutubeDL] Loading button...");
  893. injectDownloadButton();
  894.  
  895. console.log("[YoutubeDL] Setting theme... DARK:", isDarkMode());
  896. if (!isDarkMode()) toggleLightClass("#youtubeDL-popup");
  897. }
  898.  
  899. let preinjected = false;
  900. function shouldInject() {
  901. const targetElement = "#ytd-player";
  902. const videoPlayer = document.querySelector(targetElement);
  903. if (videoPlayer != null) {
  904. if (!preinjected) return true;
  905.  
  906. const popupBackgroundElement = document.querySelector("#youtubeDL-popup-bg");
  907. return popupBackgroundElement != null;
  908. }
  909. return false;
  910. }
  911.  
  912. function updateVideoInformation() {
  913. videoInformation = getVideoInformation(window.location.href);
  914. }
  915. function initialize() {
  916. updateVideoInformation();
  917. if (!videoInformation.type) return;
  918. console.log("[YoutubeDL] Loading... // (real)coloride - 2023");
  919.  
  920. // Emebds: wait for user to press play
  921. const isEmbed = (videoInformation.type == 'embed');
  922. if (isEmbed) {
  923. const player = document.querySelector("#player");
  924.  
  925. player.addEventListener("click", async(event) => {
  926. await injectAll();
  927. });
  928. } else {
  929. let injectionCheckInterval;
  930. injectionCheckInterval = setInterval(async() => {
  931. if (shouldInject())
  932. try {
  933. clearInterval(injectionCheckInterval);
  934. await injectAll();
  935. } catch (error) {
  936. console.error("[YoutubeDL] ERROR: ", error);
  937. }
  938. }, 600);
  939. }
  940. }
  941. initialize();
  942.  
  943. // Hot reswap
  944. let loadedUrl = window.location.href;
  945. async function checkUrlChange() {
  946. const currentUrl = window.location.href;
  947. if (currentUrl != loadedUrl) {
  948. console.log("[YoutubeDL] Detected URL Change");
  949.  
  950. loadedUrl = currentUrl;
  951.  
  952. updateVideoInformation();
  953.  
  954. if (!videoInformation.type) return;
  955.  
  956. waitingReload = true;
  957. await injectAll();
  958.  
  959. if (videoInformation.type == 'shorts') injectDownloadButton();
  960. }
  961. }
  962.  
  963. setInterval(checkUrlChange, 500);
  964. window.onhashchange = checkUrlChange;
  965. })();