您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download youtube videos at the comfort of your browser.
当前为
// ==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; })();