YoutubeDL

Download youtube videos at the comfort of your browser.

目前为 2024-11-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YoutubeDL
  3. // @namespace https://www.youtube.com/
  4. // @version 1.1.8
  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. // @match *://*/*
  12. // @connect savetube.io
  13. // @connect googlevideo.com
  14. // @connect aadika.xyz
  15. // @connect dlsnap11.xyz
  16. // @connect dlsnap06.xyz
  17. // @connect dlsnap02.xyz
  18. // @connect y2mate.com
  19. // @connect utomp3.com
  20. // @connect tomp3.cc
  21. // @connect snapsave.io
  22. // @connect githubusercontent.com
  23. // @connect greasyfork.org
  24. // @connect *
  25. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  26. // @license MIT
  27. // @grant GM_xmlhttpRequest
  28. // @grant GM.xmlHttpRequest
  29. // @grant GM_openInTab
  30. // @grant GM.openInTab
  31. // @grant GM.download
  32. // @grant GM_setValue
  33. // @grant GM_getValue
  34. // @grant GM_addValueChangeListener
  35. // ==/UserScript==
  36.  
  37. (function() {
  38. 'use strict';
  39.  
  40. let pageInformation = {
  41. loaded : false,
  42. ajaxLike: true,
  43. enableBubbleSwap: false,
  44. website : "https://utomp3.com/",
  45. searchEndpoint : null,
  46. convertEndpoint : null,
  47. checkingEndpoint : null,
  48. requiresQualityPatching : false,
  49. pageValues : {}
  50. }
  51.  
  52. let version = '1.1.8';
  53.  
  54. // Process:
  55. // Search -> Checking -> Convert by -> Convert using c_server
  56.  
  57. const githubAssetEndpoint = "https://raw.githubusercontent.com/realcoloride/YoutubeDL/main/";
  58. const updateGreasyUrl = "https://greasyfork.org/scripts/471103-youtubedl/versions.json";
  59.  
  60. let videoInformation;
  61. let fetchHeaders = {
  62. "accept": "*/*",
  63. "accept-language": "en-US,en;q=0.9",
  64. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  65. "priority": "u=1, i",
  66. "sec-ch-ua": "\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
  67. "sec-ch-ua-mobile": "?0",
  68. "sec-ch-ua-platform": "\"Windows\"",
  69. "sec-fetch-dest": "empty",
  70. "sec-fetch-mode": "cors",
  71. "sec-fetch-site": "same-origin",
  72. };
  73. const convertHeaders = {
  74. "accept": "*/*",
  75. "accept-language": "en-US,en;q=0.9",
  76. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  77. "sec-ch-ua": "\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
  78. "sec-ch-ua-mobile": "?0",
  79. "sec-ch-ua-platform": "\"Windows\"",
  80. "sec-fetch-dest": "empty",
  81. "sec-fetch-mode": "cors",
  82. "sec-fetch-site": "cross-site",
  83. "x-requested-key": "de0cfuirtgf67a"
  84. };
  85. const downloadHeaders = {
  86. //"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
  87. "accept-language": "en-US,en;q=0.9",
  88. "priority": "u=0, i",
  89. "sec-ch-ua": "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"",
  90. "sec-ch-ua-mobile": "?0",
  91. "sec-ch-ua-platform": "\"Windows\"",
  92. "sec-fetch-dest": "document",
  93. "sec-fetch-mode": "navigate",
  94. "sec-fetch-site": "cross-site",
  95. "sec-fetch-user": "?1",
  96. "upgrade-insecure-requests": "1"
  97. };
  98.  
  99. const popupHTML = `
  100. <div id="youtubeDL-popup">
  101. <span class="youtubeDL-text bigger float" style="display: inline-flex; align-content: center; align-items: baseline; align-content: normal;">
  102. <img src="{asset}YoutubeDL.png" class="youtubeDL-logo float">
  103. YoutubeDL - Download video
  104. <button id="youtubeDL-close" class="youtubeDL-button youtubeDL-align right" aria-label="Cancel">
  105. <span>Close</span>
  106. </button>
  107. </span>
  108.  
  109. <div id="youtubeDL-loading">
  110. <img src="{asset}loading.svg" style="width:21px; padding-right: 6px; display: flex;">
  111. <span class="youtubeDL-text medium center float" style="display: flex;">Loading...</span>
  112. </div>
  113.  
  114. <div id="youtubeDL-quality">
  115. <span class="youtubeDL-text medium center float" >Select a format and click on Download.</span><br>
  116. <span class="youtubeDL-text medium center float" style="margin-bottom: 10px;">
  117. ⚠️ CLICK
  118. <a href="{asset}allow.gif" target="_blank"><strong>"ALWAYS ALLOW ALL DOMAINS"</strong></a>
  119.  
  120. WHEN DOWNLOADING FOR THE FIRST TIME.
  121.  
  122. <span class="youtubeDL-text center float">Some providers may have a bigger file size than estimated.</span>
  123. </span>
  124.  
  125. <table id="youtubeDL-quality-table" style="width: 100%; border-spacing: 0;">
  126. <thead class="youtubeDL-row">
  127. <th class="youtubeDL-column youtubeDL-text">Format</th>
  128. <th class="youtubeDL-column youtubeDL-text">Quality</th>
  129. <th class="youtubeDL-column youtubeDL-text">Estimated Size</th>
  130. <th class="youtubeDL-column youtubeDL-text">Download</th>
  131. </thead>
  132. <tbody id="youtubeDL-quality-container">
  133.  
  134. </tbody>
  135. </table>
  136. </div>
  137.  
  138. <div class="youtubeDL-credits">
  139. <span class="youtubeDL-text medium">YoutubeDL by (real)coloride - 2023-2024</span>
  140. <br>
  141. <a class="youtubeDL-text medium" target="_blank" href="https://www.github.com/realcoloride/YoutubeDL">
  142. <img src="{asset}github.png" width="21px">Github</a>
  143.  
  144. <a class="youtubeDL-text medium" target="_blank" href="https://opensource.org/license/mit/">
  145. <img src="{asset}mit.png" width="21px">MIT license
  146. </a>
  147.  
  148. <a class="youtubeDL-text medium" target="_blank" href="https://ko-fi.com/coloride">
  149. <img src="{asset}kofi.png" width="21px">Support me on Ko-Fi
  150. </a>
  151.  
  152. <a class="youtubeDL-text medium youtubeDL-flicker" target="_blank" href="https://update.greasyfork.org/scripts/471103/YoutubeDL.user.js" style="color: yellow !important;" id="youtubeDL-update-available" hidden></a>
  153. </div>
  154. </div>
  155. `;
  156.  
  157. const pageLoadingFailedMessage =
  158. `[YoutubeDL] An error has occured while fetching data.
  159.  
  160. This can possibly mean your firewall or IP might be blocking the requests and make sure you've set up the proper permissions to the script.
  161. Please check your firewall or try using a VPN.`;
  162.  
  163. const mediaErrorMessage =
  164. `[YoutubeDL] Failed fetching media.
  165.  
  166. This could be either because:
  167. - An unhandled error
  168. - A livestream (that is still going on)
  169. - Your tampermonkey settings
  170. or an issue with the API.
  171. Try to refresh the page, otherwise, reinstall the plugin or report the issue.`;
  172.  
  173. // TrustedHTML
  174. const policy = window["trustedTypes"] != null ? trustedTypes.createPolicy("YouTubeDL_ForceInner", { createHTML: (target) => target }) : null;
  175.  
  176. // Element definitions
  177. let popupElement;
  178.  
  179. // Helper function to create safe elements in trustedHTML policy
  180. function createSafeElement(tag, content = "", attributes = {}) {
  181. const element = document.createElement(tag);
  182.  
  183. element.innerHTML = policy ? policy.createHTML(content) : content;
  184. element.editInnerHTML = (newContent) => element.innerHTML = policy ? policy.createHTML(newContent) : newContent;
  185.  
  186. // Set any additional attributes
  187. for (const [key, value] of Object.entries(attributes))
  188. element.setAttribute(key, value);
  189.  
  190. return element;
  191. }
  192.  
  193. // Information gathering
  194. function getVideoInformation(url) {
  195. const regex = /(?:https?:\/\/(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/|embed\/)?)([\w-]+)/i;
  196. const match = regex.exec(url);
  197. let videoId = match ? match[1] : null;
  198.  
  199. let type = null;
  200. if (url.includes("/shorts/")) type = "shorts";
  201. else if (url.includes("/watch?v=")) type = "video";
  202. else if (url.includes("/embed/")) type = "embed";
  203.  
  204. return { type, videoId };
  205. };
  206. function getVideoUrlFromEmbed(player) {
  207. return player.parentNode.parentNode.documentURI || window.location.href; // in case not in embed but in embed page itself
  208. }
  209.  
  210. // Fetching
  211. function convertSizeToBytes(size) {
  212. const units = {
  213. B: 1,
  214. KB: 1024,
  215. MB: 1024 * 1024,
  216. GB: 1024 * 1024 * 1024,
  217. };
  218.  
  219. const regex = /^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i;
  220. const match = size.match(regex);
  221.  
  222. if (!match) return 0;
  223.  
  224. const value = parseFloat(match[1]);
  225. const unit = match[2].toUpperCase();
  226.  
  227. if (!units.hasOwnProperty(unit)) return 0;
  228.  
  229. return value * units[unit];
  230. }
  231. function decipherVariables(variableString) {
  232. const variableDict = {};
  233.  
  234. const variableAssignments = variableString.match(/var\s+(\w+)\s*=\s*(.+?);/g);
  235.  
  236. if (variableAssignments == null) return variableDict;
  237. variableAssignments.forEach((assignment) => {
  238. const match = assignment.match(/var\s+(\w+)\s*=\s*['"](.+?)['"];/);
  239. if (match) {
  240. const [, variableName, variableValue] = match;
  241. const trimmedValue = variableValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\/g, '').split('?')[0];
  242.  
  243. variableDict[variableName] = trimmedValue;
  244. }
  245. });
  246.  
  247. return variableDict;
  248. }
  249. function isTimestampExpired(timestamp) {
  250. const currentTimestamp = Math.floor(Date.now() / 1000);
  251. return currentTimestamp > timestamp;
  252. }
  253.  
  254. // potentially adds support for violentmonkey idk
  255. async function GMxmlHttpRequest(payload, retries = 15, delay = 2000) {
  256. let solved = null;
  257.  
  258. function wait(ms) {
  259. return new Promise(resolve => setTimeout(resolve, ms));
  260. }
  261.  
  262. const onload = payload.onload;
  263. delete payload["onload"];
  264.  
  265. function returnResult(resolve) {
  266. resolve(onload == undefined ? resolve(solved) : onload(solved));
  267. }
  268.  
  269. return new Promise(async (resolve, _) => {
  270. for (let attempt = 0; attempt < retries; attempt++) {
  271. if (solved || attempt >= retries) {
  272. returnResult(resolve);
  273. return;
  274. }
  275.  
  276. const request = await GM.xmlHttpRequest(payload);
  277.  
  278. if (request.status != 429) {
  279. solved = request;
  280. returnResult(resolve);
  281. return;
  282. }
  283.  
  284. console.log(`[YouTubeDL] Request failed due to rate limit (429), retrying in ${delay}ms. [${attempt}/${retries}]`);
  285. await wait(delay * (attempt + 1));
  286. }
  287.  
  288. loop();
  289. });
  290. }
  291. async function detectCloudflare(text) {
  292. return text.startsWith(`<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title>`);
  293. }
  294.  
  295. function pageInformationIfAjaxLike(ajaxValue, notAjaxValue) {
  296. return pageInformation.ajaxLike ? ajaxValue : notAjaxValue;
  297. }
  298. let resolveCurrentMediaInformationFetch;
  299. let currentNonAjaxiFrame;
  300. async function fetchNonAjaxMediaInformation(attempts = 0) {
  301. return new Promise((resolve, reject) => {
  302. if (attempts >= 5) return reject("Too many attempts at getting the download token");
  303. resolveCurrentMediaInformationFetch = resolve;
  304.  
  305. const src = `https://download.y2api.com/api/widgetplus?url=https://www.youtube.com/watch?v=${videoInformation.videoId}`;
  306. currentNonAjaxiFrame = createSafeElement("iframe", "", { width: "0", height: "0", border: "none", src });
  307. document.body.appendChild(currentNonAjaxiFrame);
  308.  
  309. currentNonAjaxiFrame.onload = () => sendToBottomWindows("YouTubeDL_fetchMediaInformation", `${pageInformation.searchEndpoint}/api/v4/info/${videoInformation.videoId}`, currentNonAjaxiFrame);
  310. });
  311. }
  312.  
  313. function resetEndpoints() {
  314. pageInformation.searchEndpoint = null;
  315. pageInformation.convertEndpoint = null;
  316. pageInformation.checkingEndpoint = null;
  317. }
  318.  
  319. async function fetchPageInformation(needed = true) {
  320. if (needed) {
  321. if (pageInformation.searchEndpoint != null || window.self !== window.top) return;
  322.  
  323. showLoadingIcon(true);
  324. changeLoadingText("Fetching information...");
  325.  
  326. // Scrapping internal values
  327. const pageRequest = await GMxmlHttpRequest({
  328. url: pageInformation.website,
  329. method: "GET",
  330. referrerPolicy: "strict-origin-when-cross-origin",
  331. headers: fetchHeaders,
  332. credentials: "include"
  333. });
  334.  
  335. const parser = new DOMParser();
  336. const pageDocument = parser.parseFromString(
  337. policy?.createHTML(pageRequest.responseText) ?? pageRequest.responseText, "text/html");
  338.  
  339.  
  340. let scrappedScriptInnerHTML = "";
  341.  
  342. pageDocument.querySelectorAll("script").forEach((scriptElement) => {
  343. const scriptHTML = scriptElement.innerHTML;
  344.  
  345. if (pageInformation.ajaxLike) {
  346. if (scriptHTML.includes("k_url_search") || scriptHTML.includes("k_analyze_url") ||
  347. scriptHTML.includes("k_time") || scriptHTML.includes("k_page"))
  348. scrappedScriptInnerHTML += "\n" + scriptHTML;
  349. } else {
  350. if (scriptHTML.includes("window.__NUXT__"))
  351. scrappedScriptInnerHTML = scriptHTML;
  352. }
  353. });
  354.  
  355. const regex = /window\.__NUXT__\.config\s*=\s*({[\s\S]*?})\s*$/;
  356. const pageValues = pageInformation.ajaxLike
  357. ? decipherVariables(scrappedScriptInnerHTML)
  358. : JSON.parse(
  359. scrappedScriptInnerHTML.match(regex)[1]
  360. .replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":') // Add quotes around keys
  361. .replace(/'/g, '"') // Replace single quotes with double quotes
  362. .replace(/,\s*}/g, '}') // Remove trailing commas before closing curly braces
  363. .replace(/,\s*]/g, ']') // Remove trailing commas before closing square brackets
  364. )
  365. ;
  366. pageInformation.pageValues = pageValues;
  367.  
  368. let publicConfiguration = pageValues['public'];
  369. pageInformation.searchEndpoint = pageInformationIfAjaxLike(pageValues['k_url_search'] ?? pageValues['k_analyze_url'], publicConfiguration?.apiBase);
  370. pageInformation.convertEndpoint = pageInformationIfAjaxLike(pageValues['k_url_convert'] ?? pageValues['k_convert_url'], pageInformation.searchEndpoint + "/api/v4/convert");
  371. pageInformation.checkingEndpoint = pageValues['k_url_check_task'];
  372.  
  373. showLoadingIcon(false);
  374. }
  375.  
  376. pageInformation.loaded = true;
  377. }
  378. async function startConversion(fileExtension, fileQuality, timeExpires, token, filename, button) {
  379. const videoType = videoInformation.type;
  380. const videoId = videoInformation.videoId;
  381.  
  382. if (!videoType) return;
  383.  
  384. const initialFormData = new FormData();
  385. initialFormData.append('v_id', videoId);
  386. initialFormData.append('vid', videoId);
  387. initialFormData.append('ftype', fileExtension);
  388. initialFormData.append('fquality', fileQuality);
  389. initialFormData.append('fname', filename);
  390. initialFormData.append('token', token);
  391. initialFormData.append('k', token);
  392. initialFormData.append('timeExpire', timeExpires);
  393. initialFormData.append('client', 'SnapSave.io');
  394. const initialRequestBody = new URLSearchParams(initialFormData).toString();
  395.  
  396. let result = null;
  397.  
  398. try {
  399. const payload = {
  400. url: pageInformation.convertEndpoint,
  401. method: "POST",
  402. headers: convertHeaders,
  403. data: pageInformation.ajaxLike ? initialRequestBody : { token },
  404. responseType: 'text',
  405. referrerPolicy: "strict-origin-when-cross-origin",
  406. mode: "cors",
  407. credentials: "omit"
  408. };
  409.  
  410. const initialRequest = await GMxmlHttpRequest(payload);
  411. const initialResponse = JSON.parse(initialRequest.responseText);
  412.  
  413. // Needs conversion is it links to a server
  414. const downloadLink = initialResponse.d_url ?? initialResponse.dlink;
  415. const needsConversation = (downloadLink == null);
  416.  
  417. if (needsConversation) {
  418. updatePopupButton(button, 'Converting...');
  419. const conversionServerEndpoint = initialResponse.c_server;
  420.  
  421. const convertFormData = new FormData();
  422. convertFormData.append('v_id', videoId);
  423. convertFormData.append('vid', videoId);
  424. convertFormData.append('ftype', fileExtension);
  425. convertFormData.append('fquality', fileQuality);
  426. convertFormData.append('fname', filename);
  427. convertFormData.append('token', token);
  428. convertFormData.append('k', token);
  429. convertFormData.append('timeExpire', timeExpires);
  430. const convertRequestBody = new URLSearchParams(convertFormData).toString();
  431.  
  432. const convertRequest = await GMxmlHttpRequest({
  433. url: `${conversionServerEndpoint}/api/json/convert`,
  434. method: "POST",
  435. headers: convertHeaders,
  436. data: convertRequestBody,
  437. responseType: 'text',
  438. });
  439.  
  440. let convertResponse;
  441.  
  442. let adaptedResponse = {};
  443. let result;
  444.  
  445. try {
  446. convertResponse = JSON.parse(convertRequest.responseText);
  447.  
  448. result = convertResponse.result;
  449. adaptedResponse = {
  450. c_status : convertResponse.status,
  451. d_url: result
  452. }
  453. } catch (error) {
  454. 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.)")
  455.  
  456. result = "error";
  457. adaptedResponse = {
  458. c_status : "error"
  459. }
  460. console.log("[YoutubeDL] Error details: ", convertRequest.responseText);
  461. return adaptedResponse;
  462. }
  463.  
  464. if (result == 'Converting') { // Not converted
  465. const jobId = convertResponse.jobId;
  466.  
  467. console.log(`[YoutubeDL] Download needs to be checked on, jobId: ${jobId}, waiting...`);
  468. updatePopupButton(button, 'Waiting for server...');
  469.  
  470. async function gatherResult() {
  471. return new Promise(async(resolve, reject) => {
  472. const parsedURL = new URL(conversionServerEndpoint);
  473. const protocol = parsedURL.protocol === "https:" ? "wss:" : "ws:";
  474. const websocketURL = `${protocol}//${parsedURL.host}/sub/${jobId}?fname=${pageInformation.pageValues.k_prefix_name}`;
  475.  
  476. const socket = new WebSocket(websocketURL);
  477.  
  478. socket.onmessage = function(event) {
  479. const message = JSON.parse(event.data);
  480.  
  481. switch (message.action) {
  482. case "success":
  483. socket.close();
  484. resolve(message.url);
  485. break;
  486. case "progress":
  487. updatePopupButton(button, `Converting... ${message.value}%`);
  488. break;
  489. case "error":
  490. socket.close();
  491. reject("WSCheck fail: " + event.data + " - " + message.action);
  492. break;
  493. };
  494. };
  495. });
  496. };
  497.  
  498. try {
  499. const conversionUrl = await gatherResult();
  500. adaptedResponse.d_url = conversionUrl;
  501. } catch (error) {
  502. console.error("[YoutubeDL] Error while checking for job converstion:", error);
  503. adaptedResponse.c_status = 'error';
  504.  
  505. alert(
  506. `[YouTubeDL] Converting failed, but do not fret! 🚀
  507. This can happen all the time and sometimes the quality you're requesting for is not available.
  508. 👆 This can be due to all kinds of reasons, like for example an unlisted video, music, or rate limit.
  509.  
  510. In the meantime you can:
  511. * Try to download another quality
  512. * Try to refresh the page
  513. * Try later again
  514. * Use a VPN
  515.  
  516. 🫴 TIP: If this is happening frequently on other qualities; open an issue via the GitHub.
  517. `
  518. );
  519.  
  520. updatePopupButton(button, 'Converting Failed');
  521. setTimeout(() => {
  522. button.disabled = false;
  523. updatePopupButton(button, 'Download');
  524. }, 1000);
  525. }
  526. }
  527.  
  528. return adaptedResponse;
  529. } else {
  530. result = initialResponse;
  531. }
  532. } catch (error) {
  533. console.error(error);
  534. return null;
  535. }
  536.  
  537. return result;
  538. }
  539. async function getMediaInformation() {
  540. let result = { status: 'notok' };
  541.  
  542. const videoType = videoInformation.type;
  543. const videoId = videoInformation.videoId;
  544.  
  545. changeLoadingText("Loading...");
  546.  
  547. if (!videoType) return result;
  548.  
  549. let requestUrl = pageInformationIfAjaxLike(
  550. pageInformation.searchEndpoint,
  551. `${pageInformation.searchEndpoint}/api/v4/info/${videoInformation.videoId}`
  552. );
  553. let requestBody = undefined;
  554.  
  555. if (pageInformation.ajaxLike) {
  556. const link = `https://www.youtube.com/watch?v=${videoId}`;
  557. const formData = new FormData();
  558. // formData.append('q', link);
  559. formData.append('query', link);
  560. // formData.append('vt', 'downloader');
  561. formData.append('vt', 'home');
  562. // formData.append('k_query', link);
  563. // formData.append('k_page', 'home');
  564. // formData.append('k_token', pageInformation.pageValues.k__token);
  565. // formData.append('k_exp', pageInformation.pageValues.k_time);
  566. // formData.append('hl', 'en');
  567. // formData.append('q_auto', '1');
  568. requestBody = new URLSearchParams(formData).toString();
  569. } else {
  570. fetchHeaders = undefined;
  571. }
  572.  
  573. async function tryRequest() {
  574. const request = pageInformation.ajaxLike ? await GMxmlHttpRequest({
  575. url: requestUrl,
  576. method: pageInformationIfAjaxLike("POST", "GET"),
  577. headers: fetchHeaders,
  578. data: requestBody,
  579. responseType: 'text',
  580. }) : await fetchNonAjaxMediaInformation();
  581.  
  582. console.trace(`[YouTubeDL] Debug response from server [${requestUrl}] (${request.status}): ${request.responseText}`);
  583. result = JSON.parse(request.responseText);
  584. result["status"] = result["status"] ?? request.responseStatus == 200 ? 'ok' : 'notok';
  585. }
  586.  
  587. try {
  588. await tryRequest();
  589.  
  590. if (result["mess"] == "Token expired") {
  591. alert("[YouTubeDL] 🛑 The downloading token expired, but do not panic! Just try to download again. Press OK to reload.");
  592. changeLoadingText("Reloading information...");
  593.  
  594. result["status"] = "cancel";
  595. return result;
  596. }
  597.  
  598. // first retry with extra form details (sometimes some domain require it for some reason)
  599. if (result.status == 'error') {
  600. const { k__token, k_time } = pageInformation.pageValues;
  601.  
  602. formData?.append('k_exp', k_time);
  603. formData?.append('k_token', k__token);
  604. await tryRequest();
  605. }
  606.  
  607. // after that consider it as total failure
  608. if (result.status == 'error') throw new Error(result);
  609. } catch (error) {
  610. console.error(error);
  611. return error;
  612. }
  613.  
  614. return result;
  615. }
  616.  
  617. // Light mode/Dark mode
  618. function isDarkMode() {
  619. if (videoInformation.type == 'embed') return true;
  620.  
  621. const computedStyles = window.getComputedStyle(document.querySelector('ytd-app'));
  622. const backgroundColor = computedStyles["background-color"];
  623.  
  624. return backgroundColor.endsWith('15)') || backgroundColor.endsWith('0)');
  625. }
  626. function toggleLightClass(queryTarget) {
  627. const elements = document.querySelectorAll(queryTarget);
  628.  
  629. elements.forEach((element) => {
  630. element.classList.toggle("light");
  631. toggleLightClassRecursive(element);
  632. });
  633. }
  634. function toggleLightClassRecursive(element) {
  635. const children = element.children;
  636.  
  637. for (let i = 0; i < children.length; i++) {
  638. children[i].classList.toggle("light");
  639. toggleLightClassRecursive(children[i]);
  640. }
  641. }
  642.  
  643. function parseHeaders(headersString) {
  644. const headers = {};
  645. const lines = headersString.trim().split(/[\r\n]+/);
  646.  
  647. lines.forEach((line) => {
  648. const parts = line.split(': ');
  649. const header = parts.shift().toLowerCase();
  650. const value = parts.join(': ');
  651. headers[header] = value;
  652. });
  653.  
  654. return headers;
  655. }
  656.  
  657. // Popup
  658. // Links
  659. // Downloading
  660. async function downloadFile(button, url, filename) {
  661. const baseText = "Download";
  662.  
  663. button.disabled = true;
  664. updatePopupButton(button, "Downloading...");
  665.  
  666. console.trace(`[YoutubeDL] Downloading media URL: ${url}`);
  667.  
  668. function finish() {
  669. updatePopupButton(button, baseText);
  670. if (button.disabled) button.disabled = false
  671. }
  672.  
  673. async function retryWith(url, isCloudflare = false) {
  674. if (isCloudflare) alert("[YouTubeDL] 👋 Before you continue downloading...\n\n👉 A cloudflare protection page might open in a new tab and require you to click on a ✅ checkbox to download the file.\n\nClick OK when you read and understood.");
  675. GM.openInTab(url);
  676.  
  677. updatePopupButton(button, 'Downloaded!');
  678. button.disabled = false;
  679.  
  680. setTimeout(finish, 1000);
  681. }
  682.  
  683. GMxmlHttpRequest({
  684. method: 'GET',
  685. headers: downloadHeaders,
  686. url: url,
  687. responseType: 'blob',
  688. onload: async function(response) {
  689. if (response.status == 403) {
  690. if (detectCloudflare(response.responseText)) {
  691. await retryWith(response.finalUrl, true);
  692. return;
  693. }
  694.  
  695. alert("[YoutubeDL] Media expired or may be impossible to download (due to a server fail or copyrighted content), please retry or try with another format/quality, sorry!");
  696. console.log("[YoutubeDL] Download Error:", response.finalUrl, url);
  697. await reloadMedia();
  698. return;
  699. }
  700.  
  701. if (response.response == undefined) {
  702. await retryWith(response.finalUrl);
  703. return;
  704. }
  705.  
  706. const blob = response.response;
  707. const link = createSafeElement('a', "", {
  708. href: URL.createObjectURL(blob),
  709. 'download': filename,
  710. 'target': '_blank'
  711. });
  712.  
  713. document.body.appendChild(link); // firefox compatibility
  714. link.click();
  715. link.remove();
  716.  
  717. URL.revokeObjectURL(link.href);
  718. updatePopupButton(button, 'Downloaded!');
  719. button.disabled = false;
  720.  
  721. setTimeout(finish, 1000);
  722. },
  723. onerror: function(error) {
  724. console.error('[YoutubeDL] Download Error:', error);
  725. updatePopupButton(button, 'Download Failed');
  726. setTimeout(finish, 1000);
  727. },
  728. onprogress: function(progressEvent) {
  729. if (progressEvent.lengthComputable) {
  730. const percentComplete = Math.round((progressEvent.loaded / progressEvent.total) * 100);
  731. updatePopupButton(button, `Downloading: ${percentComplete}%`);
  732. } else {
  733. updatePopupButton(button, 'Downloading...');
  734. }
  735. }
  736. });
  737. }
  738. function updatePopupButton(button, text) {
  739. button.editInnerHTML(`<strong>${text}</strong>`);
  740. if (!isDarkMode()) button.classList.add('light');
  741. }
  742. async function createMediaFile(params) {
  743. let { format, quality, size, extension, timeExpires, videoTitle, token } = params;
  744.  
  745. const qualityContainer = getPopupElement("quality-container");
  746.  
  747. const row = createSafeElement("tr");
  748. row.classList.add("youtubeDL-row");
  749.  
  750. function createRowElement() {
  751. const rowElement = createSafeElement("td");
  752. rowElement.classList.add("youtubeDL-row-element");
  753.  
  754. return rowElement;
  755. }
  756. function addRowElement(rowElement) {
  757. row.appendChild(rowElement);
  758. }
  759.  
  760. function createSpanText(text, targetElement) {
  761. const spanText = createSafeElement("span", `<strong>${text}</strong>`);
  762. spanText.classList.add("youtubeDL-text");
  763.  
  764. if (!isDarkMode()) spanText.classList.add('light');
  765.  
  766. targetElement.appendChild(spanText);
  767. }
  768.  
  769. // Format
  770. const formatRowElement = createRowElement();
  771. createSpanText(format, formatRowElement);
  772. addRowElement(formatRowElement);
  773.  
  774. // Quality
  775. const qualityRowElement = createRowElement();
  776. createSpanText(quality, qualityRowElement);
  777. addRowElement(qualityRowElement);
  778.  
  779. // Size
  780. const sizeRowElement = createRowElement();
  781. createSpanText(size, sizeRowElement);
  782. addRowElement(sizeRowElement);
  783.  
  784. const downloadRowElement = createRowElement();
  785. const downloadButton = createSafeElement("button", "", { ariaLabel: "Download" });
  786. downloadButton.classList.add("youtubeDL-button");
  787. updatePopupButton(downloadButton, "Download");
  788.  
  789. downloadButton.addEventListener("click", async(_) => {
  790. try {
  791. downloadButton.disabled = true;
  792. updatePopupButton(downloadButton, "Fetching info...");
  793.  
  794. if (isTimestampExpired(pageInformation.pageValues.k_time)) {
  795. await reloadMedia();
  796. return;
  797. }
  798.  
  799. extension = extension.replace(/ \(audio\)|kbps/g, '');
  800. quality = quality.replace(/ \(audio\)|kbps/g, '');
  801. let filename = `YoutubeDL_${videoTitle}_${quality}.${extension}`;
  802. if (extension == "mp3") filename = `YoutubeDL_${videoTitle}.${extension}`;
  803.  
  804. const conversionRequest = await startConversion(extension, quality, timeExpires, token, filename, downloadButton);
  805. let conversionStatus = conversionRequest.c_status;
  806. if (conversionStatus == "CONVERTED") conversionStatus = 'ok';
  807.  
  808. async function fail(status) {
  809. throw Error("Failed to download: " + status);
  810. }
  811.  
  812. if (!conversionStatus) { fail(conversionStatus ?? "unknown"); return; }
  813. if (conversionStatus != 'ok' && conversionStatus != 'success') { fail(conversionStatus); return; }
  814.  
  815. const downloadLink = conversionRequest.d_url ?? conversionRequest.dlink;
  816. await downloadFile(downloadButton, downloadLink, filename);
  817. } catch (error) {
  818. console.error(error);
  819.  
  820. downloadButton.disabled = true;
  821. updatePopupButton(downloadButton, '');
  822.  
  823. setTimeout(() => {
  824. downloadButton.disabled = false;
  825. updatePopupButton(downloadButton, 'Download');
  826. }, 2000);
  827. }
  828. });
  829.  
  830. downloadRowElement.appendChild(downloadButton);
  831. addRowElement(downloadRowElement);
  832.  
  833. qualityContainer.appendChild(row);
  834. }
  835. let hasMediaError = false;
  836. async function loadMediaFromLinks(response) {
  837. try {
  838. const links = response.links ?? response.formats;
  839. const token = response.token;
  840. const timeExpires = response.timeExpires;
  841. const videoTitle = response.title;
  842.  
  843. let audioLinks = links.mp3 ?? links.audio.mp3;
  844. let videoLinks = links.mp4 ?? links.video.mp4;
  845.  
  846. function addFormat(information) {
  847. const format = information.f ?? information.ext;
  848. if (!format) return;
  849.  
  850. const quality = information.q ?? information.quality;
  851. let size = information.size ?? 'MB';
  852.  
  853. if (size == 'MB') size = '0 MB';
  854.  
  855. const regex = /\s[BKMGT]?B/;
  856. const regexMatch = size.match(regex);
  857. const unit = regexMatch != null ? regexMatch[0] : " MB";
  858. const sizeNoUnit = size.replace(regex, "");
  859. const roundedSize = parseFloat(sizeNoUnit).toFixed(1);
  860.  
  861. size = `${roundedSize}${unit}`;
  862. if (Math.round(roundedSize) == 0 && unit == ' B') size = "Unavailable :/";
  863. if (roundedSize == '0.0') size = "Unavailable :/";
  864.  
  865. createMediaFile({
  866. extension: format,
  867. quality,
  868. timeExpires,
  869. videoTitle,
  870.  
  871. format: format.toUpperCase(),
  872. size,
  873. token: token ?? information.token ?? information.k
  874. });
  875. }
  876.  
  877. // keep only the HDR qualities higher than 1080p
  878. for (const [key, value] of Object.entries(videoLinks)) {
  879. const qualityName = value.k ?? value.quality;
  880. if (qualityName.endsWith("HDR") && parseInt(qualityName.substr(0, 4)) <= 1080)
  881. delete videoLinks[key];
  882. }
  883.  
  884. // Format sorting first
  885. // Remove auto quality
  886. videoLinks["auto"] = null;
  887.  
  888. // Sort from highest to lowest quality
  889. let qualities = {};
  890.  
  891. for (const [qualityId, information] of Object.entries(videoLinks)) {
  892. if (!information) continue;
  893.  
  894. const qualityName = information.q ?? information.quality;
  895. const strippedQualityName = qualityName.replace('p', '');
  896. const quality = parseInt(strippedQualityName);
  897.  
  898. qualities[quality] = qualityId;
  899. }
  900.  
  901. const newOrder = Object.keys(qualities).sort((a, b) => a - b);
  902.  
  903. function swapKeys(object, victimKeys, targetKeys) {
  904. const swappedObj = {};
  905.  
  906. victimKeys.forEach((key, index) => {
  907. swappedObj[targetKeys[index]] = object[key];
  908. });
  909.  
  910. return swappedObj;
  911. }
  912. videoLinks = swapKeys(videoLinks, Object.keys(videoLinks), newOrder);
  913.  
  914. // Bubble swapping estimated qualities if incorrect (by provider)
  915. function bubbleSwap() {
  916. const videoLinkIds = Object.keys(videoLinks);
  917. videoLinkIds.forEach((qualityId) => {
  918. const currentQualityInformation = videoLinks[qualityId];
  919. if (!currentQualityInformation) return;
  920.  
  921. const currentQualityIndex = videoLinkIds.findIndex((id) => id === qualityId);
  922. if (currentQualityIndex - 1 < 0) return;
  923.  
  924. const previousQualityIndex = currentQualityIndex - 1;
  925. const previousQualityId = videoLinkIds[previousQualityIndex];
  926.  
  927. if (!previousQualityId) return;
  928.  
  929. const previousQualityInformation = videoLinks[previousQualityId];
  930.  
  931. function getQualityOf(information) {
  932. const qualityName = information.q ?? information.quality;
  933. const strippedQualityName = qualityName.replace('p', '');
  934. const quality = parseInt(strippedQualityName);
  935.  
  936. return { qualityName, strippedQualityName, quality };
  937. }
  938.  
  939. const previousQuality = getQualityOf(previousQualityInformation);
  940. const currentQuality = getQualityOf(currentQualityInformation);
  941.  
  942. function swap() {
  943. console.log(`[YoutubeDL] Swapping incorrect formats: [${previousQuality.qualityName}] ${previousQualityInformation.size} -> [${currentQuality.qualityName}] ${currentQualityInformation.size}`);
  944.  
  945. const previousClone = { ... previousQualityInformation};
  946. const currentClone = { ... currentQualityInformation};
  947.  
  948. previousQualityInformation.size = currentClone.size;
  949. currentQualityInformation.size = previousClone.size;
  950. }
  951.  
  952. const previousSize = previousQualityInformation.size;
  953. const previousSizeBytes = convertSizeToBytes(previousSize);
  954.  
  955. const currentSize = currentQualityInformation.size;
  956. const currentSizeBytes = convertSizeToBytes(currentSize);
  957.  
  958. if (previousSizeBytes > currentSizeBytes) swap();
  959. });
  960. };
  961.  
  962. // sort quality if needed one more time
  963. for (const information of Object.values(videoLinks)) {
  964. if (!information) continue;
  965.  
  966. const qualityName = information.q ?? information.quality;
  967. const strippedQualityName = qualityName.replace('p', '');
  968. const quality = parseInt(strippedQualityName);
  969.  
  970. videoLinks[quality] = information;
  971. }
  972.  
  973. if (pageInformation.enableBubbleSwap)
  974. for (let i = 0; i < Object.keys(videoLinks).length; i++) bubbleSwap();
  975.  
  976. // and then finally ensure the order is descending
  977. let sortedKeys = Object.keys(videoLinks).sort((a, b) => b - a);
  978.  
  979. audioLinks = Object.values(audioLinks);
  980.  
  981. // Add 128 kbps (or lowest) and the best audio quality available
  982. let bestQualityFormat = audioLinks[0];
  983. let lowestQualityFormat = audioLinks[0];
  984.  
  985. // Find the best and the lowest quality
  986. audioLinks.forEach(item => {
  987. if (item.quality > bestQualityFormat.quality)
  988. bestQualityFormat = item;
  989. if (item.quality < lowestQualityFormat.quality)
  990. lowestQualityFormat = item;
  991. });
  992.  
  993. function patchAudioQualityFormat(format) {
  994. if (!pageInformation.requiresQualityPatching) return;
  995. format.quality += 'kbps';
  996. return format;
  997. }
  998.  
  999.  
  1000. // Add both best quality and 128 kbps (or lowest)
  1001. if (bestQualityFormat == lowestQualityFormat) {
  1002. addFormat(bestQualityFormat);
  1003. } else {
  1004. addFormat(patchAudioQualityFormat(bestQualityFormat));
  1005. addFormat(patchAudioQualityFormat(lowestQualityFormat.quality === 128 ? lowestQualityFormat : audioLinks.find(item => item.quality === 128)));
  1006. }
  1007.  
  1008. // Add video qualities
  1009. sortedKeys.forEach(qualityId => {
  1010. if (qualityId == "undefined") return;
  1011. const information = videoLinks[parseInt(qualityId)];
  1012.  
  1013. if (!information) return;
  1014.  
  1015. const qualityName = information.q ?? information.quality;
  1016. const strippedQualityName = qualityName.replace('p', '');
  1017. const quality = parseInt(strippedQualityName);
  1018.  
  1019. qualities[quality] = qualityId;
  1020. addFormat(information);
  1021. });
  1022. } catch (error) {
  1023. console.error("[YoutubeDL] Failed loading media:", error);
  1024. alert(mediaErrorMessage);
  1025. hasMediaError = true;
  1026.  
  1027. showErrorOnDownloadButtons();
  1028.  
  1029. togglePopup();
  1030. popupElement.hidden = true;
  1031. }
  1032. }
  1033. let isLoadingMedia = false;
  1034. let hasLoadedMedia = false;
  1035. function clearMedia() {
  1036. const qualityContainer = getPopupElement("quality-container");
  1037. qualityContainer.innerHTML = policy ? policy.createHTML("") : "";
  1038.  
  1039. isLoadingMedia = false;
  1040. hasLoadedMedia = false;
  1041. }
  1042. function changeLoadingText(text) {
  1043. const loadingBarSpan = getPopupElement("loading > span");
  1044. if (!loadingBarSpan) return;
  1045. loadingBarSpan.textContent = text;
  1046. }
  1047.  
  1048. async function reloadMedia() {
  1049. console.trace("[YoutubeDL] Hot reloading...");
  1050.  
  1051. changeLoadingText("Reloading...");
  1052. isLoadingMedia = false;
  1053. hasLoadedMedia = false;
  1054.  
  1055. togglePopupLoading(true);
  1056. clearMedia();
  1057.  
  1058. await fetchPageInformation();
  1059. await loadMedia();
  1060.  
  1061. changeLoadingText("Loading...");
  1062. }
  1063. async function loadMedia() {
  1064. if (isLoadingMedia || hasLoadedMedia) return;
  1065. isLoadingMedia = true;
  1066.  
  1067. function fail(reason) {
  1068. isLoadingMedia = false;
  1069. console.error("[YoutubeDL] Failed fetching media. Extra details: ", reason);
  1070. }
  1071.  
  1072. if (!isLoadingMedia) { togglePopup(); return; };
  1073.  
  1074. const request = await getMediaInformation();
  1075. if (request.status != 'ok') { fail(request); return; }
  1076. if (request.status == 'cancel') {
  1077. resetEndpoints();
  1078. await reloadMedia();
  1079. return;
  1080. }
  1081.  
  1082. try {
  1083. if (hasLoadedMedia) return;
  1084.  
  1085. hasLoadedMedia = true;
  1086. changeLoadingText("Loading medias...");
  1087. await loadMediaFromLinks(request);
  1088.  
  1089. togglePopupLoading(false);
  1090. } catch (error) {
  1091. console.error("[YoutubeDL] Failed fetching media content: ", error);
  1092. hasLoadedMedia = false;
  1093. }
  1094. }
  1095. // Getters
  1096. function getPopupElement(element) {
  1097. return document.querySelector(`#youtubeDL-${element}`);
  1098. }
  1099. // Loading and injection
  1100. function togglePopupLoading(loading) {
  1101. const loadingBar = getPopupElement("loading");
  1102. const qualityContainer = getPopupElement("quality");
  1103.  
  1104. loadingBar.hidden = !loading;
  1105. qualityContainer.hidden = loading;
  1106. loadingBar.style = loading ? "" : "display: none;"
  1107.  
  1108. // cool slide animation
  1109. const popup = getPopupElement("popup");
  1110. popup.style.maxHeight = loading ? "200px" : popup.scrollHeight + "px";
  1111. }
  1112.  
  1113. let hasPreparedForOuterInjection = false;
  1114. let hasOuterInjectedFromTop = false;
  1115. function prepareOuterInjection() {
  1116. // check if in top window or already prepared
  1117. if (window.self !== window.top || hasPreparedForOuterInjection) return;
  1118.  
  1119. // check if link is different (other pages than youtube's)
  1120. // const youtubeRegex = /(?:https?:\/\/(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/|embed\/)?)([\w-]+)/i;
  1121. // const href = window.location.href;
  1122. // if (youtubeRegex.test(href) && href != "https://download.y2api.com") return;
  1123.  
  1124. window.top.addEventListener('message', async (event) => {
  1125. if (typeof(event.data) != 'object' || !event.isTrusted) return;
  1126. const data = event.data;
  1127.  
  1128. const title = data["title"];
  1129. const object = data["object"];
  1130. if (title == null) return;
  1131.  
  1132. // check if in youtube no cookie domain (with extra acceptance for regular youtube embed)
  1133. if (event.origin !== "https://www.youtube.com" &&
  1134. event.origin !== "https://www.youtube-nocookie.com" &&
  1135. event.origin !== "https://download.y2api.com") return;
  1136.  
  1137. switch (title) {
  1138. case "YoutubeDL_outerInject":
  1139. if (hasOuterInjectedFromTop) break;
  1140. // cross window communication for proxy windows to have interactivity
  1141. try {
  1142. // show flickering and loading
  1143. sendToBottomWindows("YoutubeDL_topLoadingShow", true);
  1144.  
  1145. // load everything needed on top window if not done
  1146. console.log("[YoutubeDL/Proxy] Fetching page information...");
  1147. await fetchPageInformation();
  1148.  
  1149. console.log("[YoutubeDL/Proxy] Loading custom styles...");
  1150. await injectStyles();
  1151.  
  1152. console.log("[YoutubeDL/Proxy] Loading popup...");
  1153. injectPopup();
  1154.  
  1155. hasOuterInjectedFromTop = true;
  1156.  
  1157. sendToBottomWindows("YoutubeDL_topLoadingShow", false);
  1158. } catch (error) {
  1159. sendToBottomWindows("YoutubeDL_topLoadingShow", false);
  1160. sendToBottomWindows("YoutubeDL_showError", error);
  1161. }
  1162.  
  1163. break;
  1164. case "YoutubeDL_togglePopup":
  1165. togglePopup();
  1166. break;
  1167. case "YoutubeDL_togglePopupElement":
  1168. await togglePopupElement(object);
  1169. break;
  1170. case "YouTubeDL_fetchMediaInformation":
  1171. resolveCurrentMediaInformationFetch(object);
  1172. break;
  1173. }
  1174. });
  1175.  
  1176. hasPreparedForOuterInjection = true;
  1177. console.log("[YoutubeDL] Has prepared for outer injection.");
  1178. }
  1179. let hasPreparedForInnerProxyInjection = false;
  1180. let outerProxyLoading = false;
  1181. function prepareOuterInjectionForProxy() {
  1182. // check if in top window or already prepared
  1183. if (window.self === window.top || hasPreparedForInnerProxyInjection) return;
  1184.  
  1185. window.addEventListener('message', async(event) => {
  1186. if (typeof(event.data) != 'object' || !event.isTrusted) return;
  1187.  
  1188. const data = event.data;
  1189.  
  1190. const title = data["title"];
  1191. const object = data["object"];
  1192. const passcode = data["passcode"];
  1193. if (title == null || passcode != "spaghetti") return;
  1194.  
  1195. switch (title) {
  1196. case "YoutubeDL_topLoadingShow":
  1197. outerProxyLoading = object;
  1198. showLoadingIcon(object);
  1199. break;
  1200. case "YoutubeDL_showError":
  1201. console.error("[YoutubeDL] Error coming from proxy window:", object);
  1202. break;
  1203. case "YouTubeDL_fetchMediaInformation":
  1204. fetch(object).then(async(response) => sendToTopWindow("YouTubeDL_fetchMediaInformation", {
  1205. responseStatus: response.status,
  1206. responseText: await response.text(),
  1207. responseHeaders: JSON.stringify(response.headers)
  1208. }));
  1209. break;
  1210. }
  1211. });
  1212.  
  1213. hasPreparedForInnerProxyInjection = true;
  1214. }
  1215. function showLoadingIcon(shown) {
  1216. // object is now a boolean
  1217. const downloadButtonImage = document.querySelector("#youtubeDL-download > img");
  1218. if (downloadButtonImage == null) return;
  1219.  
  1220. // set loading icon and flicker if loading else reset
  1221. downloadButtonImage.src = getAsset(shown == true ? "YoutubeDL-loading.png" : "YoutubeDL.png");
  1222.  
  1223. if (shown == true) downloadButtonImage.classList.add("youtubeDL-flicker");
  1224. else downloadButtonImage.classList.remove("youtubeDL-flicker");
  1225. }
  1226. function injectPopup() {
  1227. /*<div id="youtubeDL-popup-bg" class="shown">
  1228.  
  1229. </div>*/
  1230. // if in proxy window/embed
  1231. if (window.self !== window.top) {
  1232. console.log("[YoutubeDL] Embed or internal window detected. Outer-injecting the popup of the iframe.");
  1233.  
  1234. // outer injection
  1235. sendToTopWindow("YoutubeDL_outerInject", null);
  1236. return;
  1237. }
  1238.  
  1239. // check if existing already then set
  1240. const existingElement = window.top.document.querySelector("#youtubeDL-popup-bg");
  1241. if (existingElement) {
  1242. popupElement = existingElement;
  1243. return;
  1244. }
  1245.  
  1246. const revisedHTML = popupHTML.replaceAll('{asset}', githubAssetEndpoint);
  1247.  
  1248. popupElement = createSafeElement("div", revisedHTML, {
  1249. id: "youtubeDL-popup-bg",
  1250. style: `line-height: initial; font-size: initial; z-index: ${Number.MAX_SAFE_INTEGER}`
  1251. });
  1252.  
  1253. document.body.appendChild(popupElement);
  1254.  
  1255. togglePopupLoading(true);
  1256. createButtonConnections();
  1257. popupElement.hidden = true;
  1258. }
  1259. function sendToTopWindow(title, object) {
  1260. window.top.postMessage({ title, object }, '*');
  1261. }
  1262. function sendToBottomWindows(title, object, specifiediFrame = null) {
  1263. const iframes = specifiediFrame == null ? [specifiediFrame] : document.querySelectorAll('iframe');
  1264. iframes.forEach((iframe) => iframe.contentWindow.postMessage({ title, object, passcode: "spaghetti" }, '*'));
  1265. }
  1266.  
  1267. let hideTimeout;
  1268. let waitingReload = false;
  1269. function togglePopup() {
  1270. // if proxy window, send message via outer injection
  1271. if (window.self !== window.top) {
  1272. // outer injection
  1273. sendToTopWindow("YoutubeDL_togglePopup", null);
  1274. return;
  1275. }
  1276.  
  1277. checkUrlChange();
  1278.  
  1279. if (needsUpdate) showNewUpdateText(needsUpdate);
  1280. popupElement.classList.toggle("shown");
  1281.  
  1282. if (waitingReload) { reloadMedia(); waitingReload = false;}
  1283. else loadMedia();
  1284.  
  1285. // Avoid overlap
  1286. if (popupElement.hidden) {
  1287. clearTimeout(hideTimeout);
  1288.  
  1289. hideTimeout = setTimeout(() => popupElement.hidden = false, 200);
  1290. };
  1291. }
  1292. async function togglePopupElement(embedLink) {
  1293. if (popupElement.hidden == false) return;
  1294. popupElement.hidden = false;
  1295.  
  1296. const oldId = videoInformation.videoId;
  1297.  
  1298. if (embedLink != null) {
  1299. // reset video information
  1300. videoInformation = getVideoInformation(embedLink);
  1301.  
  1302. // youtube & no cookie support
  1303. // replace embed via normal page for api support
  1304. if (embedLink.includes('youtube.com/embed/') ||
  1305. embedLink.includes('youtube-nocookie.com/embed/')) {
  1306. const videoId = embedLink.split('/embed/')[1].split('?')[0];
  1307. videoInformation.type = 'embed';
  1308. videoInformation.videoId = videoId;
  1309. }
  1310. }
  1311.  
  1312. togglePopup();
  1313.  
  1314. // if changed from embed to another, reload media
  1315. if (oldId != videoInformation.videoId)
  1316. await reloadMedia();
  1317. }
  1318. // Button
  1319. let injectedShorts = [];
  1320. function injectDownloadButton() {
  1321. let targets = [];
  1322. let style;
  1323.  
  1324. const onShorts = (videoInformation.type == 'shorts');
  1325. const onEmbed = (videoInformation.type == 'embed');
  1326. if (onShorts) {
  1327. // Button for shorts
  1328. const playerControls = document.querySelectorAll('ytd-shorts-player-controls');
  1329. targets = playerControls;
  1330. style = "margin-bottom: 16px; transform: translate(36%, 10%); pointer-events: auto;";
  1331. } else if (onEmbed) {
  1332. // Get all embeds on the page
  1333. const controls = document.querySelectorAll(".ytp-left-controls");
  1334. for (let i = 0; i < controls.length; i++) {
  1335. const control = controls[i];
  1336. const player = control.parentNode.parentNode.parentNode.parentNode;
  1337.  
  1338. control.setAttribute(
  1339. "embedLink",
  1340. // if on top window, you can directly fetch from the iframe
  1341. // or else if in a proxy window, fetch directly from the location href
  1342. window.self === window.top ? getVideoUrlFromEmbed(player) : window.self.location.href
  1343. );
  1344.  
  1345. targets.push(control);
  1346. }
  1347.  
  1348. style = "margin-top: 4px; transform: translateY(-7%); display: flex;";
  1349. } else {
  1350. // Button for normal player
  1351. targets.push(document.querySelector(".ytp-left-controls"));
  1352. style = "margin-top: 4px; transform: translateY(-10%); padding-left: 4px; display: flex;";
  1353. }
  1354.  
  1355. targets.forEach((target) => {
  1356. if (injectedShorts.includes(target)) return;
  1357.  
  1358. const downloadButton = createSafeElement(
  1359. "button",
  1360. `<img src="${getAsset(hasFailedLoadingPageInformation ? "YoutubeDL-warning.png" : "YoutubeDL.png")}" style="${style}" width="36" height="36">`,
  1361. {
  1362. id: 'youtubeDL-download',
  1363. 'data-title-no-tooltip': 'YoutubeDL',
  1364. 'aria-keyshortcuts': 'SHIFT+d',
  1365. 'aria-label': 'Next keyboard shortcut SHIFT+d',
  1366. 'data-duration': '',
  1367. 'data-preview': '',
  1368. 'data-tooltip-text': '',
  1369. href: '',
  1370. title: 'Download Video'
  1371. }
  1372. );
  1373. downloadButton.classList.add("ytp-button");
  1374.  
  1375. downloadButton.addEventListener("click", async(_) => {
  1376. if (hasFailedLoadingPageInformation) {
  1377. alert(pageLoadingFailedMessage);
  1378. return;
  1379. }
  1380. if (hasMediaError) {
  1381. alert(mediaErrorMessage);
  1382. return;
  1383. }
  1384.  
  1385. // left controls
  1386. const embedLink = downloadButton.parentNode.getAttribute("embedLink");
  1387.  
  1388. // if we're in a proxy window/embed
  1389. if (window.self !== window.top) {
  1390. console.log(`[YoutubeDL] Communicating to toggle popup (outer-proxy) | Linked iframe link: ${embedLink}`);
  1391.  
  1392. if (outerProxyLoading) return;
  1393.  
  1394. // outer injection
  1395. sendToTopWindow("YoutubeDL_togglePopupElement", embedLink);
  1396. return;
  1397. }
  1398.  
  1399. // else do regularly on top window
  1400. await togglePopupElement(embedLink);
  1401. });
  1402.  
  1403. if (target.querySelector("#youtubeDL-download")) return;
  1404.  
  1405. const chapterContainer = target.querySelector('.ytp-chapter-container');
  1406.  
  1407. if (onShorts) {
  1408. target.insertBefore(downloadButton, target.children[target.children.length]);
  1409. injectedShorts.push(target);
  1410. } else {
  1411. if (chapterContainer) {
  1412. downloadButton.style = "overflow: visible; padding-right: 6px; padding-left: 1px;";
  1413. target.insertBefore(downloadButton, chapterContainer);
  1414. }
  1415. else target.appendChild(downloadButton);
  1416. }
  1417. });
  1418. }
  1419.  
  1420. // Styles
  1421. async function loadCSS(url) {
  1422. return new Promise((resolve, reject) => {
  1423. GMxmlHttpRequest({
  1424. method: 'GET',
  1425. url: url,
  1426. onload: function(response) {
  1427. if (response.status === 200) {
  1428. const style = createSafeElement('style', response.responseText);
  1429. document.head.appendChild(style);
  1430. resolve();
  1431. } else reject(new Error('Failed to load CSS'));
  1432. }
  1433. });
  1434. });
  1435. }
  1436. function getAsset(filename) {
  1437. return `${githubAssetEndpoint}${filename}`;
  1438. }
  1439. let stylesInjected = false;
  1440. async function injectStyles() {
  1441. if (stylesInjected) return;
  1442. stylesInjected = true;
  1443.  
  1444. const asset = getAsset("youtubeDL.css");
  1445. await loadCSS(asset);
  1446. }
  1447.  
  1448. // Buttons
  1449. function createButtonConnections() {
  1450. const closeButton = popupElement.querySelector("#youtubeDL-close");
  1451.  
  1452. closeButton.addEventListener('click', (_) => {
  1453. try {
  1454. togglePopup();
  1455.  
  1456. setTimeout(() => popupElement.hidden = true, 200);
  1457. } catch (error) {console.error(error);}
  1458. });
  1459. }
  1460.  
  1461. function showErrorOnDownloadButtons() {
  1462. const downloadButtonsImages = document.querySelectorAll("#youtubeDL-download > img");
  1463.  
  1464. for (let i = 0; i < downloadButtonsImages.length; i++) {
  1465. const downloadButtonImage = downloadButtonsImages[i];
  1466. downloadButtonImage.src = getAsset("YoutubeDL-warning.png");
  1467. }
  1468. }
  1469.  
  1470. // Main page injection
  1471. let hasFailedLoadingPageInformation = false;
  1472. let didFirstShortsInjection = false;
  1473. async function injectAll() {
  1474. // double check
  1475. if (videoInformation.type == 'shorts' && !didFirstShortsInjection) {
  1476. injectDownloadButton();
  1477. didFirstShortsInjection = true;
  1478. }
  1479.  
  1480. if (preinjected) return;
  1481. preinjected = true;
  1482.  
  1483. console.log("[YoutubeDL] Initializing downloader...");
  1484. try {
  1485. await fetchPageInformation();
  1486. } catch (error) {
  1487. isLoadingMedia = false;
  1488. console.error("[YoutubeDL] Failed fetching page information: ", error);
  1489. hasFailedLoadingPageInformation = true;
  1490.  
  1491. showErrorOnDownloadButtons();
  1492. }
  1493.  
  1494. console.log("[YoutubeDL] Loading custom styles...");
  1495. await injectStyles();
  1496.  
  1497. console.log("[YoutubeDL] Loading popup...");
  1498. injectPopup();
  1499.  
  1500. console.log("[YoutubeDL] Loading button...");
  1501. injectDownloadButton();
  1502.  
  1503. console.log("[YoutubeDL] Setting theme... DARK:", isDarkMode());
  1504. if (!isDarkMode()) toggleLightClass("#youtubeDL-popup");
  1505. }
  1506.  
  1507. let preinjected = false;
  1508. function shouldInject() {
  1509. const targetElement = "#ytd-player";
  1510. const videoPlayer = document.querySelector(targetElement);
  1511.  
  1512. if (videoPlayer != null) {
  1513. if (!preinjected) return true;
  1514.  
  1515. const popupBackgroundElement = document.querySelector("#youtubeDL-popup-bg");
  1516. return popupBackgroundElement != null;
  1517. }
  1518.  
  1519. return false;
  1520. }
  1521.  
  1522. function updateVideoInformation() {
  1523. videoInformation = getVideoInformation(window.location.href);
  1524. }
  1525. let embedRefreshInterval;
  1526. function initialize() {
  1527. prepareOuterInjection();
  1528. prepareOuterInjectionForProxy();
  1529. updateVideoInformation();
  1530. if (!videoInformation.type) return;
  1531.  
  1532. console.log("[YoutubeDL] Loading... // (real)coloride - 2023-2024");
  1533.  
  1534. if (window.self === window.top) prepareOuterInjection();
  1535.  
  1536. // Emebds: wait for user to press play
  1537. const isEmbed = (videoInformation.type == 'embed');
  1538. if (isEmbed) {
  1539. // if embed keep going until url changes
  1540. if (embedRefreshInterval != null) return;
  1541.  
  1542. // we have to handle when its executed in side of the embed and when outside (proxied windows)
  1543. if (window.self !== window.top) {
  1544. // if in proxy window, directly inject because the user would have already clicked
  1545. (async() => await injectAll())();
  1546. return;
  1547. }
  1548.  
  1549. // wait for click
  1550. function injectTo(player) {
  1551. player.addEventListener("click", async(_) => await injectAll());
  1552. }
  1553.  
  1554. // check if page is actual embed, get first player & inject (NOT autoplay)
  1555. const regex = /^(?!.*youtube-nocookie\.com).*youtube\.com\/embed\/\w+/;
  1556. if (regex.test(window.location.href)) {
  1557. injectTo(document.querySelector("#player"));
  1558. return;
  1559. }
  1560.  
  1561. // else if in global window
  1562. const embeds = window.self.document.querySelectorAll('iframe[data-player="youtube"]');
  1563. if (embeds.length == 0) return;
  1564.  
  1565. // wait for click because of the youtube icon embed
  1566. for (let i = 0; i < players.length; i++) {
  1567. const embed = embeds[i];
  1568.  
  1569. const allowAttributes = embed.getAttribute("allow");
  1570.  
  1571. // check if on autoplay & if not inject only on click
  1572. if (!allowAttributes.includes('autoplay'))
  1573. injectTo(embed);
  1574. else (async() => await injectAll())();
  1575. }
  1576. } else {
  1577. let injectionCheckInterval;
  1578. injectionCheckInterval = setInterval(async() => {
  1579. if (shouldInject())
  1580. try {
  1581. clearInterval(injectionCheckInterval);
  1582. await injectAll();
  1583. } catch (error) {
  1584. console.error("[YoutubeDL] ERROR: ", error);
  1585. }
  1586. }, 600);
  1587. }
  1588. }
  1589.  
  1590. // Checking for updates
  1591. let needsUpdate = null;
  1592. function showNewUpdateText(version) {
  1593. needsUpdate = version;
  1594.  
  1595. const element = document.querySelector("#youtubeDL-update-available"); if (!element) return;
  1596. element.hidden = false;
  1597. element.innerText = `An update (${version}) is available! Click here to update.`;
  1598. }
  1599. function checkForUpdates() {
  1600. (async() => {
  1601. const payload = {
  1602. url: updateGreasyUrl,
  1603. method: "GET",
  1604. responseType: 'text',
  1605. referrerPolicy: "strict-origin-when-cross-origin",
  1606. mode: "cors",
  1607. credentials: "omit"
  1608. };
  1609.  
  1610. const request = await GMxmlHttpRequest(payload);
  1611. const response = JSON.parse(request.responseText);
  1612.  
  1613. const currentVersion = response[0]["version"];
  1614. const requiresUpdate = currentVersion != version;
  1615.  
  1616. if (requiresUpdate) showNewUpdateText(currentVersion);
  1617. })();
  1618. }
  1619.  
  1620. // Hot reswap
  1621. let loadedUrl = window.location.href;
  1622. async function checkUrlChange() {
  1623. const currentUrl = window.location.href;
  1624.  
  1625. if (currentUrl != loadedUrl) {
  1626. console.log("[YoutubeDL] Detected URL Change");
  1627.  
  1628. loadedUrl = currentUrl;
  1629. clearInterval(embedRefreshInterval);
  1630.  
  1631. didFirstShortsInjection = false;
  1632.  
  1633. updateVideoInformation();
  1634.  
  1635. console.log(`[YoutubeDL] Detected video type: ${videoInformation.type}`);
  1636.  
  1637. if (!videoInformation.type) return;
  1638.  
  1639. waitingReload = true;
  1640. await injectAll();
  1641.  
  1642. if (videoInformation.type == 'shorts') injectDownloadButton();
  1643. }
  1644. }
  1645.  
  1646. initialize();
  1647. checkForUpdates();
  1648.  
  1649. setInterval(checkUrlChange, 500);
  1650. window.onhashchange = checkUrlChange;
  1651. })();