- // ==UserScript==
- // @name YoutubeDL
- // @namespace https://www.youtube.com/
- // @version 1.0.1
- // @description Download youtube videos at the comfort of your browser.
- // @author realcoloride
- // @match https://www.youtube.com/*
- // @match https://www.youtube.com/watch*
- // @match https://www.youtube.com/shorts*
- // @match https://www.youtube.com/embed*
- // @connect savetube.io
- // @connect googlevideo.com
- // @connect aadika.xyz
- // @connect dlsnap11.xyz
- // @connect githubusercontent.com
- // @connect *
- // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
- // @license MIT
- // @grant GM.xmlHttpRequest
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- let pageInformation = {
- loaded : false,
- website : "https://savetube.io",
- searchEndpoint : null,
- convertEndpoint : null,
- checkingEndpoint : null,
- pageValues : {}
- }
-
- // Process:
- // Search -> Checking -> Convert by -> Convert using c_server
-
- const githubAssetEndpoint = "https://raw.githubusercontent.com/realcoloride/YoutubeDL/main/";
-
- let videoInformation;
- const fetchHeaders = {
- 'Accept': '*/*',
- 'Accept-Language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
- 'Sec-Ch-Ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
- 'Sec-Ch-Ua-Mobile': '?0',
- 'Sec-Ch-Ua-Platform': '"Windows"',
- 'Sec-Fetch-Dest': 'empty',
- 'Sec-Fetch-Mode': 'cors',
- 'Sec-Fetch-Site': 'none',
- };
- const convertHeaders = {
- "accept": "*/*",
- "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
- "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
- "sec-ch-ua": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "cross-site",
- "x-requested-key": "de0cfuirtgf67a"
- };
- const downloadHeaders = {
- "accept": "*/*",
- "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
- "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
- "sec-ch-ua": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"",
- "sec-ch-ua-mobile": "?0",
- "sec-ch-ua-platform": "\"Windows\"",
- "sec-fetch-dest": "empty",
- "sec-fetch-mode": "cors",
- "sec-fetch-site": "same-origin",
- "x-requested-with": "XMLHttpRequest"
- };
-
- const popupHTML = `
- <div id="youtubeDL-popup">
- <span class="youtubeDL-text bigger float">
- <img src="{asset}YoutubeDL.png" class="youtubeDL-logo float">
- YoutubeDL - Download video
- <button id="youtubeDL-close" class="youtubeDL-button youtubeDL-align right" aria-label="Cancel">
- <span>Close</span>
- </button>
- </span>
-
- <hr style="height:3px">
-
- <div id="youtubeDL-loading">
- <span class="youtubeDL-text medium center float" style="display: flex;">
- <img src="{asset}loading.svg" style="width:21px; padding-right: 6px;"> Loading...
- </span>
- </div>
-
- <div id="youtubeDL-quality">
- <span class="youtubeDL-text medium center float" >Select a quality and click on Download.</span><br>
- <span class="youtubeDL-text medium center float" style="margin-bottom: 10px;">
- ⚠️ CLICK
- <a href="{asset}allow.gif" target="_blank"><strong>"ALWAYS ALLOW ALL DOMAINS"</strong></a>
-
- WHEN DOWNLOADING FOR THE FIRST TIME.
-
- <span class="youtubeDL-text center float">Some providers may have a bigger file size than estimated.</span>
- </span>
-
- <table id="youtubeDL-quality-table" style="width: 100%; border-spacing: 0;">
- <thead class="youtubeDL-row">
- <th class="youtubeDL-column youtubeDL-text">Format</th>
- <th class="youtubeDL-column youtubeDL-text">Quality</th>
- <th class="youtubeDL-column youtubeDL-text">Estimated Size</th>
- <th class="youtubeDL-column youtubeDL-text">Download</th>
- </thead>
- <tbody id="youtubeDL-quality-container">
-
- </tbody>
- </table>
- </div>
-
- <div class="youtubeDL-credits">
- <span class="youtubeDL-text medium">YoutubeDL by (real)coloride - 2023</span>
- <br>
- <a class="youtubeDL-text medium" href="https://www.github.com/realcoloride/YoutubeDL">
- <img src="{asset}github.png" width="21px">Github</a>
-
- <a class="youtubeDL-text medium" href="https://opensource.org/license/mit/">
- <img src="{asset}mit.png" width="21px">MIT license
- </a>
- </div>
- </div>
- `;
-
- // Element definitions
-
- const ytdAppContainer = document.querySelector("ytd-app");
- let popupElement;
-
- // Information gathering
- function getVideoInformation(url) {
- const regex = /(?:https?:\/\/(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/|embed\/)?)([\w-]+)/i;
- const match = regex.exec(url);
- const videoId = match ? match[1] : null;
-
- let type = null;
- if (url.includes("/shorts/")) type = "shorts";
- else if (url.includes("/watch?v=")) type = "video";
- else if (url.includes("/embed/")) type = "embed";
-
- return { type, videoId };
- };
-
- // Fetching
- function convertSizeToBytes(size) {
- const units = {
- B: 1,
- KB: 1024,
- MB: 1024 * 1024,
- GB: 1024 * 1024 * 1024,
- };
-
- const regex = /^(\d+(?:\.\d+)?)\s*([A-Z]+)$/i;
- const match = size.match(regex);
-
- if (!match) {
- throw new Error('Invalid size format');
- }
-
- const value = parseFloat(match[1]);
- const unit = match[2].toUpperCase();
-
- if (!units.hasOwnProperty(unit)) {
- throw new Error('Invalid size unit');
- }
-
- return value * units[unit];
- }
- function decipherVariables(variableString) {
- const variableDict = {};
-
- const variableAssignments = variableString.match(/var\s+(\w+)\s*=\s*(.+?);/g);
-
- variableAssignments.forEach((assignment) => {
- const [, variableName, variableValue] = assignment.match(/var\s+(\w+)\s*=\s*['"](.+?)['"];/);
-
- const trimmedValue = variableValue.trim().replace(/^['"]|['"]$/g, '');
-
- variableDict[variableName] = trimmedValue;
- });
-
- return variableDict;
- }
- function isTimestampExpired(timestamp) {
- const currentTimestamp = Math.floor(Date.now() / 1000);
- return currentTimestamp > timestamp;
- }
- async function fetchPageInformation() {
- // Scrapping internal values
- const pageRequest = await GM.xmlHttpRequest({
- url: `${pageInformation.website}`,
- method: "GET",
- headers: fetchHeaders,
- });
-
- const parser = new DOMParser();
- const pageDocument = parser.parseFromString(pageRequest.responseText, "text/html");
-
- let scrappedScriptElement;
-
- pageDocument.querySelectorAll("script").forEach((scriptElement) => {
- const scriptHTML = scriptElement.innerHTML;
- if (scriptHTML.includes("k_time") && scriptHTML.includes("k_page")) {
- scrappedScriptElement = scriptElement;
- return;
- }
- });
-
- const pageValues = decipherVariables(scrappedScriptElement.innerHTML);
- pageInformation.pageValues = pageValues;
-
- pageInformation.searchEndpoint = pageValues['k_url_search'];
- pageInformation.convertEndpoint = pageValues['k_url_convert'];
- pageInformation.checkingEndpoint = pageValues['k_url_check_task'];
-
- pageInformation.loaded = true;
- }
- async function startConversion(fileExtension, fileQuality, timeExpires, token, filename, button) {
- const videoType = videoInformation.type;
- const videoId = videoInformation.videoId;
-
- if (!videoType) return;
-
- const initialFormData = new FormData();
- initialFormData.append('v_id', videoId);
- initialFormData.append('ftype', fileExtension);
- initialFormData.append('fquality', fileQuality);
- initialFormData.append('token', token);
- initialFormData.append('timeExpire', timeExpires);
- initialFormData.append('client', 'SaveTube.io');
- const initialRequestBody = new URLSearchParams(initialFormData).toString();
-
- let result = null;
-
- try {
- const payload = {
- url: pageInformation.convertEndpoint,
- method: "POST",
- headers: convertHeaders,
- data: initialRequestBody,
- responseType: 'text',
- referrerPolicy: "strict-origin-when-cross-origin",
- mode: "cors",
- credentials: "omit"
- };
-
- const initialRequest = await GM.xmlHttpRequest(payload);
- const initialResponse = JSON.parse(initialRequest.responseText);
-
- // Needs conversion is it links to a server
- const downloadLink = initialResponse.d_url;
- const needsConversation = (downloadLink == null);
-
- if (needsConversation) {
- updatePopupButton(button, 'Converting...');
- const conversionServerEndpoint = initialResponse.c_server;
-
- const convertFormData = new FormData();
- convertFormData.append('v_id', videoId);
- convertFormData.append('ftype', fileExtension);
- convertFormData.append('fquality', fileQuality);
- convertFormData.append('fname', filename);
- convertFormData.append('token', token);
- convertFormData.append('timeExpire', timeExpires);
- const convertRequestBody = new URLSearchParams(convertFormData).toString();
-
- const convertRequest = await GM.xmlHttpRequest({
- url: `${conversionServerEndpoint}/api/json/convert`,
- method: "POST",
- headers: convertHeaders,
- data: convertRequestBody,
- responseType: 'text',
- });
-
- let convertResponse;
-
- let adaptedResponse = {};
- let result;
-
- try {
- convertResponse = JSON.parse(convertRequest.responseText);
-
- result = convertResponse.result;
- adaptedResponse = {
- c_status : convertResponse.status,
- d_url: result
- }
- } catch (error) {
- 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.)")
-
- result = "error";
- adaptedResponse = {
- c_status : "error"
- }
- return adaptedResponse;
- }
-
- if (result == 'Converting') { // Not converted
- const jobId = convertResponse.jobId;
-
- console.log(`[YoutubeDL] Download needs to be checked on, jobId: ${jobId}, waiting...`);
- updatePopupButton(button, 'Waiting for server...');
-
- async function gatherResult() {
- return new Promise(async(resolve, reject) => {
- const parsedURL = new URL(conversionServerEndpoint);
- const protocol = parsedURL.protocol === "https:" ? "wss:" : "ws:";
- const websocketURL = `${protocol}//${parsedURL.host}/sub/${jobId}?fname=${pageInformation.pageValues.k_prefix_name}`;
-
- const socket = new WebSocket(websocketURL);
-
- socket.onmessage = function(event) {
- const message = JSON.parse(event.data);
-
- switch (message.action) {
- case "success":
- socket.close();
- resolve(message.url);
- case "progress":
- updatePopupButton(button, `Converting... ${message.value}%`)
- case "error":
- socket.close();
- reject("WSCheck fail");
- };
- };
- });
- };
-
- try {
- const conversionUrl = await gatherResult();
- adaptedResponse.d_url = conversionUrl;
- } catch (error) {
- console.error("[YoutubeDL] Error while checking for job converstion: ", error);
- adaptedResponse.c_status = 'error';
- }
- }
-
- return adaptedResponse;
- } else {
- result = initialResponse;
- }
- } catch (error) {
- console.error(error);
- return null;
- }
-
- return result;
- }
- async function getMediaInformation() {
- const videoType = videoInformation.type;
- const videoId = videoInformation.videoId;
-
- if (!videoType) return;
-
- const formData = new FormData();
- formData.append('q', `https://www.youtube.com/watch?v=${videoId}`);
- formData.append('vt', 'home');
- const requestBody = new URLSearchParams(formData).toString();
-
- let result = null;
-
- try {
- const request = await GM.xmlHttpRequest({
- url: pageInformation.searchEndpoint,
- method: "POST",
- headers: fetchHeaders,
- data: requestBody,
- responseType: 'text',
- });
-
- result = JSON.parse(request.responseText);
- } catch (error) {
- return null;
- }
-
- return result;
- }
-
- // Light mode/Dark mode
- function isDarkMode() {
- if (videoInformation.type == 'embed') return true;
-
- const computedStyles = window.getComputedStyle(ytdAppContainer);
-
- const backgroundColor = computedStyles["background-color"];
-
- return backgroundColor.endsWith('15)');
- }
- function toggleLightClass(queryTarget) {
- const elements = document.querySelectorAll(queryTarget);
-
- elements.forEach((element) => {
- element.classList.toggle("light");
- toggleLightClassRecursive(element);
- });
- }
- function toggleLightClassRecursive(element) {
- const children = element.children;
-
- for (let i = 0; i < children.length; i++) {
- children[i].classList.toggle("light");
- toggleLightClassRecursive(children[i]);
- }
- }
-
- // Popup
- // Links
- // Downloading
- async function downloadFile(button, url, filename) {
- const baseText = `Download`;
-
- button.disabled = true;
- updatePopupButton(button, "Downloading...");
-
- console.log(`[YoutubeDL] Downloading media URL: ${url}`);
-
- function finish() {
- updatePopupButton(button, baseText);
- if (button.disabled) button.disabled = false
- }
-
- GM.xmlHttpRequest({
- method: 'GET',
- headers: downloadHeaders,
- url: url,
- responseType: 'blob',
- onload: async function(response) {
- console.log(response);
- if (response.status == 403) {
- alert("[YoutubeDL] Media expired or may be impossible to download, please retry or try with another format, sorry!");
- await reloadMedia();
- return;
- }
-
- const blob = response.response;
- const link = document.createElement('a');
-
- link.href = URL.createObjectURL(blob);
- link.setAttribute('download', filename);
- link.click();
-
- URL.revokeObjectURL(link.href);
- updatePopupButton(button, 'Downloaded!');
- button.disabled = false;
-
- setTimeout(finish, 1000);
- },
- onerror: function(error) {
- console.error('[YoutubeDL] Download Error:', error);
- updatePopupButton(button, 'Download Failed');
-
- setTimeout(finish, 1000);
- },
- onprogress: function(progressEvent) {
- if (progressEvent.lengthComputable) {
- const percentComplete = Math.round((progressEvent.loaded / progressEvent.total) * 100);
- updatePopupButton(button, `Downloading: ${percentComplete}%`);
- } else
- updatePopupButton(button, 'Downloading...');
- }
- });
- }
- function updatePopupButton(button, text) {
- button.innerHTML = `<strong>${text}</strong>`;
- if (!isDarkMode()) button.classList.add('light');
- }
- async function createMediaFile(params) {
- let { format, quality, size, extension, timeExpires, videoTitle, token } = params;
-
- const qualityContainer = getPopupElement("quality-container");
-
- const row = document.createElement("tr");
- row.classList.add("youtubeDL-row");
-
- function createRowElement() {
- const rowElement = document.createElement("td");
- rowElement.classList.add("youtubeDL-row-element");
-
- return rowElement;
- }
- function addRowElement(rowElement) {
- row.appendChild(rowElement);
- }
-
- function createSpanText(text, targetElement) {
- const spanText = document.createElement("span");
- spanText.classList.add("youtubeDL-text");
-
- spanText.innerHTML = `<strong>${text}</strong>`;
- if (!isDarkMode()) spanText.classList.add('light');
-
- targetElement.appendChild(spanText);
- }
-
- // Format
- const formatRowElement = createRowElement();
- createSpanText(format, formatRowElement);
- addRowElement(formatRowElement);
-
- // Quality
- const qualityRowElement = createRowElement();
- createSpanText(quality, qualityRowElement);
- addRowElement(qualityRowElement);
-
- // Size
- const sizeRowElement = createRowElement();
- createSpanText(size, sizeRowElement);
- addRowElement(sizeRowElement);
-
- const downloadRowElement = createRowElement();
- const downloadButton = document.createElement("button");
- downloadButton.classList.add("youtubeDL-button");
- downloadButton.ariaLabel = "Download";
- updatePopupButton(downloadButton, "Download");
-
- downloadButton.addEventListener("click", async(event) => {
- try {
- downloadButton.disabled = true;
- updatePopupButton(downloadButton, "Fetching info...");
-
- if (isTimestampExpired(pageInformation.pageValues.k_time)) {
- await reloadMedia();
- return;
- }
-
- extension = extension.replace(/ \(audio\)|kbps/g, '');
- quality = quality.replace(/ \(audio\)|kbps/g, '');
- let filename = `YoutubeDL_${videoTitle}_${quality}.${extension}`;
- if (extension == "mp3") filename = `YoutubeDL_${videoTitle}.${extension}`;
-
- const conversionRequest = await startConversion(extension, quality, timeExpires, token, filename, downloadButton);
- const conversionStatus = conversionRequest.c_status;
-
- async function fail() {
- throw Error("Failed to download.");
- }
-
- if (!conversionStatus) { fail(); return; }
- if (conversionStatus != 'ok' && conversionStatus != 'success') { fail(); return; }
-
- const downloadLink = conversionRequest.d_url;
- await downloadFile(downloadButton, downloadLink, filename);
- } catch (error) {
- console.error(error);
-
- downloadButton.disabled = true;
- updatePopupButton(downloadButton, '');
-
- setTimeout(() => {
- downloadButton.disabled = false;
- updatePopupButton(downloadButton, 'Download');
- }, 2000);
- }
- });
-
- downloadRowElement.appendChild(downloadButton);
- addRowElement(downloadRowElement);
-
- qualityContainer.appendChild(row);
- }
- async function loadMediaFromLinks(response) {
- try {
- const links = response.links;
- const token = response.token;
- const timeExpires = response.timeExpires;
- const videoTitle = response.title;
-
- const audioLinks = links.mp3;
- let videoLinks = links.mp4;
-
- function addFormat(information) {
- const format = information.f;
- if (!format) return;
-
- const quality = information.q;
- let size = information.size;
-
- const regex = /\s[BKMGT]?B/;
- const unit = size.match(regex)[0];
- const sizeNoUnit = size.replace(regex, "");
- const roundedSize = Math.round(parseFloat(sizeNoUnit));
-
- size = `${roundedSize}${unit}`;
-
- createMediaFile({
- extension: format,
- quality,
- timeExpires,
- videoTitle,
-
- format: format.toUpperCase(),
- size,
- token
- });
- }
-
- // Audio will only have this one so it doesnt matter
- const defaultAudioFormat = audioLinks[Object.keys(audioLinks)[0]];
- defaultAudioFormat.f = "mp3 (audio)";
-
- addFormat(defaultAudioFormat);
-
- // Format sorting first
- // Remove auto quality
- videoLinks["auto"] = null;
-
- // Store 3gp quality if available
- const low3gpFormat = { ...videoLinks["3gp@144p"] };
- delete videoLinks["3gp@144p"];
-
- // Sort from highest to lowest quality
- const qualities = {};
-
- for (const [qualityId, information] of Object.entries(videoLinks)) {
- if (!information) continue;
-
- const qualityName = information.q;
- const strippedQualityName = qualityName.replace('p', '');
- const quality = parseInt(strippedQualityName);
-
- qualities[quality] = qualityId;
- }
-
- const newOrder = Object.keys(qualities).sort((a, b) => a - b);
-
- function swapKeys(object, victimKeys, targetKeys) {
- const swappedObj = {};
-
- victimKeys.forEach((key, index) => {
- swappedObj[targetKeys[index]] = object[key];
- });
-
- return swappedObj;
- }
- videoLinks = swapKeys(videoLinks, Object.keys(videoLinks), newOrder);
-
- // Bubble swapping estimated qualities if incorrect (by provider)
- function bubbleSwap() {
- const videoLinkIds = Object.keys(videoLinks);
- videoLinkIds.forEach((qualityId) => {
- const currentQualityInformation = videoLinks[qualityId];
- if (!currentQualityInformation) return;
-
- const currentQualityIndex = videoLinkIds.findIndex((id) => id === qualityId);
- if (currentQualityIndex - 1 < 0) return;
-
- const previousQualityIndex = currentQualityIndex - 1;
- const previousQualityId = videoLinkIds[previousQualityIndex];
-
- if (!previousQualityId) return;
-
- const previousQualityInformation = videoLinks[previousQualityId];
-
- function getQualityOf(information) {
- const qualityName = information.q;
- const strippedQualityName = qualityName.replace('p', '');
- const quality = parseInt(strippedQualityName);
-
- return { qualityName, strippedQualityName, quality };
- }
-
- const previousQuality = getQualityOf(previousQualityInformation);
- const currentQuality = getQualityOf(currentQualityInformation);
-
- function swap() {
- console.log(`[YoutubeDL] Swapping incorrect formats: [${previousQuality.qualityName}] ${previousQualityInformation.size} -> [${currentQuality.qualityName}] ${currentQualityInformation.size}`);
-
- const previousClone = { ... previousQualityInformation};
- const currentClone = { ... currentQualityInformation};
-
- previousQualityInformation.size = currentClone.size;
- currentQualityInformation.size = previousClone.size;
- }
-
- const previousSize = previousQualityInformation.size;
- const previousSizeBytes = convertSizeToBytes(previousSize);
-
- const currentSize = currentQualityInformation.size;
- const currentSizeBytes = convertSizeToBytes(currentSize);
-
- if (previousSizeBytes < currentSizeBytes) swap();
- });
- };
-
- for (let i = 0; i < Object.keys(videoLinks).length; i++) bubbleSwap();
-
- for (const [qualityId, information] of Object.entries(videoLinks)) {
- if (!information) continue;
-
- const qualityName = information.q;
- const strippedQualityName = qualityName.replace('p', '');
- const quality = parseInt(strippedQualityName);
-
- qualities[quality] = qualityId;
- addFormat(information);
- }
-
- if (low3gpFormat) addFormat(low3gpFormat);
- } catch (error) {
- console.error("[YoutubeDL] Failed loading media:", error);
- alert("[YoutubeDL] Failed fetching media.\n" +
- "This could be either because:\n" +
- "- An unhandled error\n" +
- "- Your tampermonkey settings\n" +
- "or an issue with the API.\n\n" +
- "Try to refresh the page, otherwise, reinstall the plugin.")
-
- togglePopup();
- popupElement.hidden = true;
- }
- }
- let isLoadingMedia = false;
- let hasLoadedMedia = false;
- function clearMedia() {
- const qualityContainer = getPopupElement("quality-container");
- qualityContainer.innerHTML = "";
-
- isLoadingMedia = false;
- hasLoadedMedia = false;
- }
- async function reloadMedia() {
- console.log("[YoutubeDL] Hot reloading...");
-
- const loadingBarSpan = getPopupElement("loading > span");
- loadingBarSpan.textContent = "Reloading...";
-
- togglePopupLoading(true);
- clearMedia();
-
- await fetchPageInformation();
- await loadMedia();
-
- loadingBarSpan.textContent = "Loading...";
- }
- async function loadMedia() {
- if (isLoadingMedia || hasLoadedMedia) return;
- isLoadingMedia = true;
-
- function fail() {
- isLoadingMedia = false;
- console.error("[YoutubeDL] Failed fetching media.");
- }
-
- if (!isLoadingMedia) {togglePopup(); return; };
-
- const request = await getMediaInformation();
- if (request.status != 'ok') { fail(); return; }
-
- try {
- await loadMediaFromLinks(request);
-
- hasLoadedMedia = true;
- togglePopupLoading(false);
- } catch (error) {
- console.error("[YoutubeDL] Failed fetching media content: ", error);
- hasLoadedMedia = false;
- }
- }
- // Getters
- function getPopupElement(element) {
- return document.querySelector(`#youtubeDL-${element}`);
- }
- // Loading and injection
- function togglePopupLoading(loading) {
- const loadingBar = getPopupElement("loading");
- const qualityContainer = getPopupElement("quality");
-
- loadingBar.hidden = !loading;
- qualityContainer.hidden = loading;
- }
- function injectPopup() {
- /*<div id="youtubeDL-popup-bg" class="shown">
-
- </div>*/
- popupElement = document.createElement("div");
- popupElement.id = "youtubeDL-popup-bg";
-
- const revisedHTML = popupHTML.replaceAll('{asset}', githubAssetEndpoint);
- popupElement.innerHTML = revisedHTML;
-
- document.body.appendChild(popupElement);
-
- togglePopupLoading(true);
- createButtonConnections();
- popupElement.hidden = true;
- }
- let hideTimeout;
- let waitingReload = false;
- function togglePopup() {
- popupElement.classList.toggle("shown");
-
- if (waitingReload) {reloadMedia(); waitingReload = false;}
- else loadMedia();
-
- // Avoid overlap
- if (popupElement.hidden) {
- clearTimeout(hideTimeout);
-
- hideTimeout = setTimeout(() => {
- popupElement.hidden = false;
- }, 200);
- };
- }
- // Button
- let injectedShorts = [];
- function injectDownloadButton() {
- let targets = [];
- let style;
-
- const onShorts = (videoInformation.type == 'shorts');
-
- if (onShorts) {
- // Button for shorts
- const playerControls = document.querySelectorAll('ytd-shorts-player-controls');
-
- targets = playerControls;
- style = "margin-bottom: 16px; transform: translateY(-15%); z-index: 999; pointer-events: auto;"
- } else {
- // Button for embed and normal player
- targets.push(document.querySelector(".ytp-left-controls"));
- style = "margin-top: 4px; transform: translateY(5%); padding-left: 4px;";
- }
-
- targets.forEach((target) => {
- if (injectedShorts.includes(target)) return;
-
- const downloadButton = document.createElement("button");
- downloadButton.classList.add("ytp-button");
- downloadButton.innerHTML = `<img src="${getAsset("YoutubeDL.png")}" style="${style}" width="36" height="36">`;
-
- downloadButton.id = 'youtubeDL-download'
- downloadButton.setAttribute('data-title-no-tooltip', 'YoutubeDL');
- downloadButton.setAttribute('aria-keyshortcuts', 'SHIFT+d');
- downloadButton.setAttribute('aria-label', 'Next keyboard shortcut SHIFT+d');
- downloadButton.setAttribute('data-duration', '');
- downloadButton.setAttribute('data-preview', '');
- downloadButton.setAttribute('data-tooltip-text', '');
- downloadButton.setAttribute('href', '');
- downloadButton.setAttribute('title', 'Download Video');
-
- downloadButton.addEventListener("click", (event) => {
- if (popupElement.hidden) {
- popupElement.hidden = false;
-
- togglePopup();
- }
- });
-
- const chapterContainer = target.querySelector('.ytp-chapter-container');
-
- if (onShorts) {
- target.insertBefore(downloadButton, target.children[1])
- injectedShorts.push(target);
- } else {
- if (chapterContainer) {
- downloadButton.style = "overflow: visible; padding-right: 6px; padding-left: 1px;";
- target.insertBefore(downloadButton, chapterContainer);
- }
- else target.appendChild(downloadButton);
- }
- });
- }
-
- // Styles
- async function loadCSS(url) {
- return new Promise((resolve, reject) => {
- GM.xmlHttpRequest({
- method: 'GET',
- url: url,
- onload: function(response) {
- if (response.status === 200) {
- const style = document.createElement('style');
- style.innerHTML = response.responseText;
- document.head.appendChild(style);
- resolve();
- } else {
- reject(new Error('Failed to load CSS'));
- }
- }
- });
- });
- }
- function getAsset(filename) {
- return `${githubAssetEndpoint}${filename}`;
- }
- let stylesInjected = false;
- async function injectStyles() {
- if (stylesInjected) return;
- stylesInjected = true;
-
- const asset = getAsset("youtubeDL.css");
- await loadCSS(asset);
- }
-
- // Buttons
- function createButtonConnections() {
- const closeButton = popupElement.querySelector("#youtubeDL-close");
-
- closeButton.addEventListener('click', (event) => {
- try {
- togglePopup();
-
- setTimeout(() => {
- popupElement.hidden = true;
- }, 200);
- } catch (error) {console.error(error);}
- });
- }
-
- // Main page injection
- async function injectAll() {
- if (preinjected) return;
- preinjected = true;
-
- console.log("[YoutubeDL] Initializing downloader...");
- try {
- await fetchPageInformation();
- } catch (error) {
- isLoadingMedia = false;
- console.error("[YoutubeDL] Failed fetching page information: ", error);
- }
-
- console.log("[YoutubeDL] Loading custom styles...");
- await injectStyles();
-
- console.log("[YoutubeDL] Loading popup...");
- injectPopup();
-
- console.log("[YoutubeDL] Loading button...");
- injectDownloadButton();
-
- console.log("[YoutubeDL] Setting theme... DARK:", isDarkMode());
- if (!isDarkMode()) toggleLightClass("#youtubeDL-popup");
- }
-
- let preinjected = false;
- function shouldInject() {
- const targetElement = "#ytd-player";
- const videoPlayer = document.querySelector(targetElement);
-
- if (videoPlayer != null) {
- if (!preinjected) return true;
-
- const popupBackgroundElement = document.querySelector("#youtubeDL-popup-bg");
- return popupBackgroundElement != null;
- }
-
- return false;
- }
-
- function updateVideoInformation() {
- videoInformation = getVideoInformation(window.location.href);
- }
- function initialize() {
- updateVideoInformation();
- if (!videoInformation.type) return;
-
- console.log("[YoutubeDL] Loading... // (real)coloride - 2023");
-
- // Emebds: wait for user to press play
- const isEmbed = (videoInformation.type == 'embed');
- if (isEmbed) {
- const player = document.querySelector("#player");
-
- player.addEventListener("click", async(event) => {
- await injectAll();
- });
- } else {
- let injectionCheckInterval;
- injectionCheckInterval = setInterval(async() => {
- if (shouldInject())
- try {
- clearInterval(injectionCheckInterval);
- await injectAll();
- } catch (error) {
- console.error("[YoutubeDL] ERROR: ", error);
- }
- }, 600);
- }
- }
-
- initialize();
-
- // Hot reswap
- let loadedUrl = window.location.href;
- async function checkUrlChange() {
- const currentUrl = window.location.href;
-
- if (currentUrl != loadedUrl) {
- console.log("[YoutubeDL] Detected URL Change");
-
- loadedUrl = currentUrl;
-
- updateVideoInformation();
-
- if (!videoInformation.type) return;
-
- waitingReload = true;
- await injectAll();
-
- if (videoInformation.type == 'shorts') injectDownloadButton();
- }
- }
-
- setInterval(checkUrlChange, 500);
- window.onhashchange = checkUrlChange;
- })();