YoutubeDL

Download youtube videos at the comfort of your browser.

// ==UserScript==
// @name         YoutubeDL
// @namespace    https://www.youtube.com/
// @version      1.1.8
// @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*
// @match        *://*/*
// @connect      savetube.io
// @connect      googlevideo.com
// @connect      aadika.xyz
// @connect      dlsnap11.xyz
// @connect      dlsnap06.xyz
// @connect      dlsnap02.xyz
// @connect      y2mate.com
// @connect      utomp3.com
// @connect      tomp3.cc
// @connect      snapsave.io
// @connect      githubusercontent.com
// @connect      greasyfork.org
// @connect      *
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @license      MIT
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_openInTab
// @grant        GM.openInTab
// @grant        GM.download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// ==/UserScript==

(function() {
    'use strict';

    let pageInformation = {
        loaded : false,
        ajaxLike: true,
        enableBubbleSwap: false,
        website : "https://utomp3.com/",
        searchEndpoint : null,
        convertEndpoint : null,
        checkingEndpoint : null,
        requiresQualityPatching : false,
        pageValues : {}
    }

    let version = '1.1.8';

    // Process:
    // Search -> Checking -> Convert by -> Convert using c_server

    const githubAssetEndpoint = "https://raw.githubusercontent.com/realcoloride/YoutubeDL/main/";
    const updateGreasyUrl = "https://greasyfork.org/scripts/471103-youtubedl/versions.json";

    let videoInformation;
    let fetchHeaders = {
        "accept": "*/*",
        "accept-language": "en-US,en;q=0.9",
        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
        "priority": "u=1, i",
        "sec-ch-ua": "\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
    };
    const convertHeaders = {
        "accept": "*/*",
        "accept-language": "en-US,en;q=0.9",
        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
        "sec-ch-ua": "\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
        "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": "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",
        "accept-language": "en-US,en;q=0.9",
        "priority": "u=0, i",
        "sec-ch-ua": "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\"",
        "sec-fetch-dest": "document",
        "sec-fetch-mode": "navigate",
        "sec-fetch-site": "cross-site",
        "sec-fetch-user": "?1",
        "upgrade-insecure-requests": "1"
    };

    const popupHTML = `
        <div id="youtubeDL-popup">
                <span class="youtubeDL-text bigger float" style="display: inline-flex; align-content: center; align-items: baseline; align-content: normal;">
                    <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>

                <div id="youtubeDL-loading">
                    <img src="{asset}loading.svg" style="width:21px; padding-right: 6px; display: flex;">
                    <span class="youtubeDL-text medium center float" style="display: flex;">Loading...</span>
                </div>

                <div id="youtubeDL-quality">
                    <span class="youtubeDL-text medium center float" >Select a format 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-2024</span>
                    <br>
                    <a class="youtubeDL-text medium" target="_blank" href="https://www.github.com/realcoloride/YoutubeDL">
                        <img src="{asset}github.png" width="21px">Github</a>

                    <a class="youtubeDL-text medium" target="_blank" href="https://opensource.org/license/mit/">
                        <img src="{asset}mit.png" width="21px">MIT license
                    </a>

                    <a class="youtubeDL-text medium" target="_blank" href="https://ko-fi.com/coloride">
                        <img src="{asset}kofi.png" width="21px">Support me on Ko-Fi
                    </a>

                    <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>
                </div>
            </div>
    `;

    const pageLoadingFailedMessage =
`[YoutubeDL] An error has occured while fetching data.

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.
Please check your firewall or try using a VPN.`;

    const mediaErrorMessage =
`[YoutubeDL] Failed fetching media.

This could be either because:
- An unhandled error
- A livestream (that is still going on)
- Your tampermonkey settings
or an issue with the API.
Try to refresh the page, otherwise, reinstall the plugin or report the issue.`;

    // TrustedHTML
    const policy = window["trustedTypes"] != null ? trustedTypes.createPolicy("YouTubeDL_ForceInner", { createHTML: (target) => target }) : null;

    // Element definitions
    let popupElement;

    // Helper function to create safe elements in trustedHTML policy
    function createSafeElement(tag, content = "", attributes = {}) {
        const element = document.createElement(tag);

        element.innerHTML = policy ? policy.createHTML(content) : content;
        element.editInnerHTML = (newContent) => element.innerHTML = policy ? policy.createHTML(newContent) : newContent;

        // Set any additional attributes
        for (const [key, value] of Object.entries(attributes))
            element.setAttribute(key, value);

        return element;
    }

    // Information gathering
    function getVideoInformation(url) {
        const regex = /(?:https?:\/\/(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/|embed\/)?)([\w-]+)/i;
        const match = regex.exec(url);
        let 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 };
    };
    function getVideoUrlFromEmbed(player) {
        return player.parentNode.parentNode.documentURI || window.location.href; // in case not in embed but in embed page itself
    }

    // 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) return 0;

        const value = parseFloat(match[1]);
        const unit = match[2].toUpperCase();

        if (!units.hasOwnProperty(unit)) return 0;

        return value * units[unit];
    }
    function decipherVariables(variableString) {
        const variableDict = {};

        const variableAssignments = variableString.match(/var\s+(\w+)\s*=\s*(.+?);/g);

        if (variableAssignments == null) return variableDict;
        variableAssignments.forEach((assignment) => {
            const match = assignment.match(/var\s+(\w+)\s*=\s*['"](.+?)['"];/);
            if (match) {
                const [, variableName, variableValue] = match;
                const trimmedValue = variableValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\/g, '').split('?')[0];

                variableDict[variableName] = trimmedValue;
            }
        });

        return variableDict;
    }
    function isTimestampExpired(timestamp) {
        const currentTimestamp = Math.floor(Date.now() / 1000);
        return currentTimestamp > timestamp;
    }

    // potentially adds support for violentmonkey idk
    async function GMxmlHttpRequest(payload, retries = 15, delay = 2000) {
        let solved = null;

        function wait(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }

        const onload = payload.onload;
        delete payload["onload"];

        function returnResult(resolve) {
            resolve(onload == undefined ? resolve(solved) : onload(solved));
        }

        return new Promise(async (resolve, _) => {
            for (let attempt = 0; attempt < retries; attempt++) {
                if (solved || attempt >= retries) {
                    returnResult(resolve);
                    return;
                }

                const request = await GM.xmlHttpRequest(payload);

                if (request.status != 429) {
                    solved = request;
                    returnResult(resolve);
                    return;
                }

                console.log(`[YouTubeDL] Request failed due to rate limit (429), retrying in ${delay}ms. [${attempt}/${retries}]`);
                await wait(delay * (attempt + 1));
            }

            loop();
        });
    }
    async function detectCloudflare(text) {
        return text.startsWith(`<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title>`);
    }

    function pageInformationIfAjaxLike(ajaxValue, notAjaxValue) {
        return pageInformation.ajaxLike ? ajaxValue : notAjaxValue;
    }
    let resolveCurrentMediaInformationFetch;
    let currentNonAjaxiFrame;
    async function fetchNonAjaxMediaInformation(attempts = 0) {
        return new Promise((resolve, reject) => {
            if (attempts >= 5) return reject("Too many attempts at getting the download token");
            resolveCurrentMediaInformationFetch = resolve;

            const src = `https://download.y2api.com/api/widgetplus?url=https://www.youtube.com/watch?v=${videoInformation.videoId}`;
            currentNonAjaxiFrame = createSafeElement("iframe", "", { width: "0", height: "0", border: "none", src });
            document.body.appendChild(currentNonAjaxiFrame);

            currentNonAjaxiFrame.onload = () => sendToBottomWindows("YouTubeDL_fetchMediaInformation", `${pageInformation.searchEndpoint}/api/v4/info/${videoInformation.videoId}`, currentNonAjaxiFrame);
        });
    }

    function resetEndpoints() {
        pageInformation.searchEndpoint = null;
        pageInformation.convertEndpoint = null;
        pageInformation.checkingEndpoint = null;
    }

    async function fetchPageInformation(needed = true) {
        if (needed) {
            if (pageInformation.searchEndpoint != null || window.self !== window.top) return;

            showLoadingIcon(true);
            changeLoadingText("Fetching information...");

            // Scrapping internal values
            const pageRequest = await GMxmlHttpRequest({
                url: pageInformation.website,
                method: "GET",
                referrerPolicy: "strict-origin-when-cross-origin",
                headers: fetchHeaders,
                credentials: "include"
            });

            const parser = new DOMParser();
            const pageDocument = parser.parseFromString(
                policy?.createHTML(pageRequest.responseText) ?? pageRequest.responseText, "text/html");


            let scrappedScriptInnerHTML = "";

            pageDocument.querySelectorAll("script").forEach((scriptElement) => {
                const scriptHTML = scriptElement.innerHTML;

                if (pageInformation.ajaxLike) {
                    if (scriptHTML.includes("k_url_search") || scriptHTML.includes("k_analyze_url") ||
                        scriptHTML.includes("k_time") || scriptHTML.includes("k_page"))
                            scrappedScriptInnerHTML += "\n" + scriptHTML;
                } else {
                    if (scriptHTML.includes("window.__NUXT__"))
                        scrappedScriptInnerHTML = scriptHTML;
                }
            });

            const regex = /window\.__NUXT__\.config\s*=\s*({[\s\S]*?})\s*$/;
            const pageValues = pageInformation.ajaxLike
                ? decipherVariables(scrappedScriptInnerHTML)
                : JSON.parse(
                    scrappedScriptInnerHTML.match(regex)[1]
                    .replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":') // Add quotes around keys
                    .replace(/'/g, '"') // Replace single quotes with double quotes
                    .replace(/,\s*}/g, '}') // Remove trailing commas before closing curly braces
                    .replace(/,\s*]/g, ']') // Remove trailing commas before closing square brackets
                )
            ;
            pageInformation.pageValues = pageValues;

            let publicConfiguration = pageValues['public'];
            pageInformation.searchEndpoint = pageInformationIfAjaxLike(pageValues['k_url_search'] ?? pageValues['k_analyze_url'], publicConfiguration?.apiBase);
            pageInformation.convertEndpoint = pageInformationIfAjaxLike(pageValues['k_url_convert'] ?? pageValues['k_convert_url'], pageInformation.searchEndpoint + "/api/v4/convert");
            pageInformation.checkingEndpoint = pageValues['k_url_check_task'];

            showLoadingIcon(false);
        }

        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('vid', videoId);
        initialFormData.append('ftype', fileExtension);
        initialFormData.append('fquality', fileQuality);
        initialFormData.append('fname', filename);
        initialFormData.append('token', token);
        initialFormData.append('k', token);
        initialFormData.append('timeExpire', timeExpires);
        initialFormData.append('client', 'SnapSave.io');
        const initialRequestBody = new URLSearchParams(initialFormData).toString();

        let result = null;

        try {
            const payload = {
                url: pageInformation.convertEndpoint,
                method: "POST",
                headers: convertHeaders,
                data: pageInformation.ajaxLike ? initialRequestBody : { token },
                responseType: 'text',
                referrerPolicy: "strict-origin-when-cross-origin",
                mode: "cors",
                credentials: "omit"
            };

            const initialRequest = await GMxmlHttpRequest(payload);
            const initialResponse = JSON.parse(initialRequest.responseText);

            // Needs conversion is it links to a server
            const downloadLink = initialResponse.d_url ?? initialResponse.dlink;
            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('vid', videoId);
                convertFormData.append('ftype', fileExtension);
                convertFormData.append('fquality', fileQuality);
                convertFormData.append('fname', filename);
                convertFormData.append('token', token);
                convertFormData.append('k', token);
                convertFormData.append('timeExpire', timeExpires);
                const convertRequestBody = new URLSearchParams(convertFormData).toString();

                const convertRequest = await GMxmlHttpRequest({
                    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"
                    }
                    console.log("[YoutubeDL] Error details: ", convertRequest.responseText);
                    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);
                                        break;
                                    case "progress":
                                        updatePopupButton(button, `Converting... ${message.value}%`);
                                        break;
                                    case "error":
                                        socket.close();
                                        reject("WSCheck fail: " + event.data + " - " + message.action);
                                        break;
                                };
                            };
                        });
                    };

                    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';

                        alert(
`[YouTubeDL] Converting failed, but do not fret! 🚀
⭐ This can happen all the time and sometimes the quality you're requesting for is not available.
👆 This can be due to all kinds of reasons, like for example an unlisted video, music, or rate limit.

In the meantime you can:
* Try to download another quality
* Try to refresh the page
* Try later again
* Use a VPN

🫴 TIP: If this is happening frequently on other qualities; open an issue via the GitHub.
`
                        );

                        updatePopupButton(button, 'Converting Failed');
                        setTimeout(() => {
                            button.disabled = false;
                            updatePopupButton(button, 'Download');
                        }, 1000);
                    }
                }

                return adaptedResponse;
            } else {
                result = initialResponse;
            }
        } catch (error) {
            console.error(error);
            return null;
        }

        return result;
    }
    async function getMediaInformation() {
        let result = { status: 'notok' };

        const videoType = videoInformation.type;
        const videoId = videoInformation.videoId;

        changeLoadingText("Loading...");

        if (!videoType) return result;

        let requestUrl = pageInformationIfAjaxLike(
            pageInformation.searchEndpoint,
            `${pageInformation.searchEndpoint}/api/v4/info/${videoInformation.videoId}`
        );
        let requestBody = undefined;

        if (pageInformation.ajaxLike) {
            const link = `https://www.youtube.com/watch?v=${videoId}`;
            const formData = new FormData();
            // formData.append('q', link);
            formData.append('query', link);
            // formData.append('vt', 'downloader');
            formData.append('vt', 'home');
            // formData.append('k_query', link);
            // formData.append('k_page', 'home');
            // formData.append('k_token', pageInformation.pageValues.k__token);
            // formData.append('k_exp', pageInformation.pageValues.k_time);
            // formData.append('hl', 'en');
            // formData.append('q_auto', '1');
            requestBody = new URLSearchParams(formData).toString();
        } else {
            fetchHeaders = undefined;
        }

        async function tryRequest() {
            const request = pageInformation.ajaxLike ? await GMxmlHttpRequest({
                url: requestUrl,
                method: pageInformationIfAjaxLike("POST", "GET"),
                headers: fetchHeaders,
                data: requestBody,
                responseType: 'text',
            }) : await fetchNonAjaxMediaInformation();

            console.trace(`[YouTubeDL] Debug response from server [${requestUrl}] (${request.status}): ${request.responseText}`);
            result = JSON.parse(request.responseText);
            result["status"] = result["status"] ?? request.responseStatus == 200 ? 'ok' : 'notok';
        }

        try {
            await tryRequest();

            if (result["mess"] == "Token expired") {
                alert("[YouTubeDL] 🛑 The downloading token expired, but do not panic! Just try to download again. Press OK to reload.");
                changeLoadingText("Reloading information...");

                result["status"] = "cancel";
                return result;
            }

            // first retry with extra form details (sometimes some domain require it for some reason)
            if (result.status == 'error') {
                const { k__token, k_time } = pageInformation.pageValues;

                formData?.append('k_exp', k_time);
                formData?.append('k_token', k__token);
                await tryRequest();
            }

            // after that consider it as total failure
            if (result.status == 'error') throw new Error(result);
        } catch (error) {
            console.error(error);
            return error;
        }

        return result;
    }

    // Light mode/Dark mode
    function isDarkMode() {
        if (videoInformation.type == 'embed') return true;

        const computedStyles = window.getComputedStyle(document.querySelector('ytd-app'));
        const backgroundColor = computedStyles["background-color"];

        return backgroundColor.endsWith('15)') || backgroundColor.endsWith('0)');
    }
    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]);
        }
    }

    function parseHeaders(headersString) {
        const headers = {};
        const lines = headersString.trim().split(/[\r\n]+/);

        lines.forEach((line) => {
            const parts = line.split(': ');
            const header = parts.shift().toLowerCase();
            const value = parts.join(': ');
            headers[header] = value;
        });

        return headers;
    }

    // Popup
    // Links
    // Downloading
    async function downloadFile(button, url, filename) {
        const baseText = "Download";

        button.disabled = true;
        updatePopupButton(button, "Downloading...");

        console.trace(`[YoutubeDL] Downloading media URL: ${url}`);

        function finish() {
            updatePopupButton(button, baseText);
            if (button.disabled) button.disabled = false
        }

        async function retryWith(url, isCloudflare = false) {
            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.");
            GM.openInTab(url);

            updatePopupButton(button, 'Downloaded!');
            button.disabled = false;

            setTimeout(finish, 1000);
        }

        GMxmlHttpRequest({
            method: 'GET',
            headers: downloadHeaders,
            url: url,
            responseType: 'blob',
            onload: async function(response) {
                if (response.status == 403) {
                    if (detectCloudflare(response.responseText)) {
                        await retryWith(response.finalUrl, true);
                        return;
                    }

                    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!");
                    console.log("[YoutubeDL] Download Error:", response.finalUrl, url);
                    await reloadMedia();
                    return;
                }

                if (response.response == undefined) {
                    await retryWith(response.finalUrl);
                    return;
                }

                const blob = response.response;
                const link = createSafeElement('a', "", {
                    href: URL.createObjectURL(blob),
                    'download': filename,
                    'target': '_blank'
                });

                document.body.appendChild(link); // firefox compatibility
                link.click();
                link.remove();

                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.editInnerHTML(`<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 = createSafeElement("tr");
        row.classList.add("youtubeDL-row");

        function createRowElement() {
            const rowElement = createSafeElement("td");
            rowElement.classList.add("youtubeDL-row-element");

            return rowElement;
        }
        function addRowElement(rowElement) {
            row.appendChild(rowElement);
        }

        function createSpanText(text, targetElement) {
            const spanText = createSafeElement("span", `<strong>${text}</strong>`);
            spanText.classList.add("youtubeDL-text");

            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 = createSafeElement("button", "", { ariaLabel: "Download" });
        downloadButton.classList.add("youtubeDL-button");
        updatePopupButton(downloadButton, "Download");

        downloadButton.addEventListener("click", async(_) => {
            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);
                let conversionStatus = conversionRequest.c_status;
                if (conversionStatus == "CONVERTED") conversionStatus = 'ok';

                async function fail(status) {
                    throw Error("Failed to download: " + status);
                }

                if (!conversionStatus) { fail(conversionStatus ?? "unknown"); return; }
                if (conversionStatus != 'ok' && conversionStatus != 'success') { fail(conversionStatus); return; }

                const downloadLink = conversionRequest.d_url ?? conversionRequest.dlink;
                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);
    }
    let hasMediaError = false;
    async function loadMediaFromLinks(response) {
        try {
            const links = response.links ?? response.formats;
            const token = response.token;
            const timeExpires = response.timeExpires;
            const videoTitle = response.title;

            let audioLinks = links.mp3 ?? links.audio.mp3;
            let videoLinks = links.mp4 ?? links.video.mp4;

            function addFormat(information) {
                const format = information.f ?? information.ext;
                if (!format) return;

                const quality = information.q ?? information.quality;
                let size = information.size ?? 'MB';

                if (size == 'MB') size = '0 MB';

                const regex = /\s[BKMGT]?B/;
                const regexMatch = size.match(regex);
                const unit = regexMatch != null ? regexMatch[0] : " MB";
                const sizeNoUnit = size.replace(regex, "");
                const roundedSize = parseFloat(sizeNoUnit).toFixed(1);

                size = `${roundedSize}${unit}`;
                if (Math.round(roundedSize) == 0 && unit == ' B') size = "Unavailable :/";
                if (roundedSize == '0.0') size = "Unavailable :/";

                createMediaFile({
                    extension: format,
                    quality,
                    timeExpires,
                    videoTitle,

                    format: format.toUpperCase(),
                    size,
                    token: token ?? information.token ?? information.k
                });
            }

            // keep only the HDR qualities higher than 1080p
            for (const [key, value] of Object.entries(videoLinks)) {
                const qualityName = value.k ?? value.quality;
                if (qualityName.endsWith("HDR") && parseInt(qualityName.substr(0, 4)) <= 1080)
                    delete videoLinks[key];
            }

            // Format sorting first
            // Remove auto quality
            videoLinks["auto"] = null;

            // Sort from highest to lowest quality
            let qualities = {};

            for (const [qualityId, information] of Object.entries(videoLinks)) {
                if (!information) continue;

                const qualityName = information.q ?? information.quality;
                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 ?? information.quality;
                        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();
                });
            };

            // sort quality if needed one more time
            for (const information of Object.values(videoLinks)) {
                if (!information) continue;

                const qualityName = information.q ?? information.quality;
                const strippedQualityName = qualityName.replace('p', '');
                const quality = parseInt(strippedQualityName);

                videoLinks[quality] = information;
            }

            if (pageInformation.enableBubbleSwap)
                for (let i = 0; i < Object.keys(videoLinks).length; i++) bubbleSwap();

            // and then finally ensure the order is descending
            let sortedKeys = Object.keys(videoLinks).sort((a, b) => b - a);

            audioLinks = Object.values(audioLinks);

            // Add 128 kbps (or lowest) and the best audio quality available
            let bestQualityFormat = audioLinks[0];
            let lowestQualityFormat = audioLinks[0];

            // Find the best and the lowest quality
            audioLinks.forEach(item => {
                if (item.quality > bestQualityFormat.quality)
                    bestQualityFormat = item;
                if (item.quality < lowestQualityFormat.quality)
                    lowestQualityFormat = item;
            });

            function patchAudioQualityFormat(format) {
                if (!pageInformation.requiresQualityPatching) return;
                format.quality += 'kbps';
                return format;
            }


            // Add both best quality and 128 kbps (or lowest)
            if (bestQualityFormat == lowestQualityFormat) {
                addFormat(bestQualityFormat);
            } else {
                addFormat(patchAudioQualityFormat(bestQualityFormat));
                addFormat(patchAudioQualityFormat(lowestQualityFormat.quality === 128 ? lowestQualityFormat : audioLinks.find(item => item.quality === 128)));
            }

            // Add video qualities
            sortedKeys.forEach(qualityId => {
                if (qualityId == "undefined") return;
                const information = videoLinks[parseInt(qualityId)];

                if (!information) return;

                const qualityName = information.q ?? information.quality;
                const strippedQualityName = qualityName.replace('p', '');
                const quality = parseInt(strippedQualityName);

                qualities[quality] = qualityId;
                addFormat(information);
            });
        } catch (error) {
            console.error("[YoutubeDL] Failed loading media:", error);
            alert(mediaErrorMessage);
            hasMediaError = true;

            showErrorOnDownloadButtons();

            togglePopup();
            popupElement.hidden = true;
        }
    }
    let isLoadingMedia = false;
    let hasLoadedMedia = false;
    function clearMedia() {
        const qualityContainer = getPopupElement("quality-container");
        qualityContainer.innerHTML = policy ? policy.createHTML("") : "";

        isLoadingMedia = false;
        hasLoadedMedia = false;
    }
    function changeLoadingText(text) {
        const loadingBarSpan = getPopupElement("loading > span");
        if (!loadingBarSpan) return;
        loadingBarSpan.textContent = text;
    }

    async function reloadMedia() {
        console.trace("[YoutubeDL] Hot reloading...");

        changeLoadingText("Reloading...");
        isLoadingMedia = false;
        hasLoadedMedia = false;

        togglePopupLoading(true);
        clearMedia();

        await fetchPageInformation();
        await loadMedia();

        changeLoadingText("Loading...");
    }
    async function loadMedia() {
        if (isLoadingMedia || hasLoadedMedia) return;
        isLoadingMedia = true;

        function fail(reason) {
            isLoadingMedia = false;
            console.error("[YoutubeDL] Failed fetching media. Extra details: ", reason);
        }

        if (!isLoadingMedia) { togglePopup(); return; };

        const request = await getMediaInformation();
        if (request.status != 'ok') { fail(request); return; }
        if (request.status == 'cancel') { 
            resetEndpoints();
            await reloadMedia(); 
            return; 
        }

        try {
            if (hasLoadedMedia) return;

            hasLoadedMedia = true;
            changeLoadingText("Loading medias...");
            await loadMediaFromLinks(request);

            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;
        loadingBar.style = loading ? "" : "display: none;"

        // cool slide animation
        const popup = getPopupElement("popup");
        popup.style.maxHeight = loading ? "200px" : popup.scrollHeight + "px";
    }

    let hasPreparedForOuterInjection = false;
    let hasOuterInjectedFromTop = false;
    function prepareOuterInjection() {
        // check if in top window or already prepared
        if (window.self !== window.top || hasPreparedForOuterInjection) return;

        // check if link is different (other pages than youtube's)
        // const youtubeRegex = /(?:https?:\/\/(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/|embed\/)?)([\w-]+)/i;
        // const href = window.location.href;
        // if (youtubeRegex.test(href) && href != "https://download.y2api.com") return;

        window.top.addEventListener('message', async (event) => {
            if (typeof(event.data) != 'object' || !event.isTrusted) return;
            const data = event.data;

            const title = data["title"];
            const object = data["object"];
            if (title == null) return;

            // check if in youtube no cookie domain (with extra acceptance for regular youtube embed)
            if (event.origin !== "https://www.youtube.com" &&
                event.origin !== "https://www.youtube-nocookie.com" &&
                event.origin !== "https://download.y2api.com") return;

            switch (title) {
                case "YoutubeDL_outerInject":
                    if (hasOuterInjectedFromTop) break;
                    // cross window communication for proxy windows to have interactivity
                    try {
                        // show flickering and loading
                        sendToBottomWindows("YoutubeDL_topLoadingShow", true);

                        // load everything needed on top window if not done
                        console.log("[YoutubeDL/Proxy] Fetching page information...");
                        await fetchPageInformation();

                        console.log("[YoutubeDL/Proxy] Loading custom styles...");
                        await injectStyles();

                        console.log("[YoutubeDL/Proxy] Loading popup...");
                        injectPopup();

                        hasOuterInjectedFromTop = true;

                        sendToBottomWindows("YoutubeDL_topLoadingShow", false);
                    } catch (error) {
                        sendToBottomWindows("YoutubeDL_topLoadingShow", false);
                        sendToBottomWindows("YoutubeDL_showError", error);
                    }

                    break;
                case "YoutubeDL_togglePopup":
                    togglePopup();
                    break;
                case "YoutubeDL_togglePopupElement":
                    await togglePopupElement(object);
                    break;
                case "YouTubeDL_fetchMediaInformation":
                    resolveCurrentMediaInformationFetch(object);
                    break;
            }
        });

        hasPreparedForOuterInjection = true;
        console.log("[YoutubeDL] Has prepared for outer injection.");
    }
    let hasPreparedForInnerProxyInjection = false;
    let outerProxyLoading = false;
    function prepareOuterInjectionForProxy() {
        // check if in top window or already prepared
        if (window.self === window.top || hasPreparedForInnerProxyInjection) return;

        window.addEventListener('message', async(event) => {
            if (typeof(event.data) != 'object' || !event.isTrusted) return;

            const data = event.data;

            const title = data["title"];
            const object = data["object"];
            const passcode = data["passcode"];
            if (title == null || passcode != "spaghetti") return;

            switch (title) {
                case "YoutubeDL_topLoadingShow":
                    outerProxyLoading = object;
                    showLoadingIcon(object);
                    break;
                case "YoutubeDL_showError":
                    console.error("[YoutubeDL] Error coming from proxy window:", object);
                    break;
                case "YouTubeDL_fetchMediaInformation":
                    fetch(object).then(async(response) => sendToTopWindow("YouTubeDL_fetchMediaInformation", {
                        responseStatus: response.status,
                        responseText: await response.text(),
                        responseHeaders: JSON.stringify(response.headers)
                    }));
                    break;
            }
        });

        hasPreparedForInnerProxyInjection = true;
    }
    function showLoadingIcon(shown) {
        // object is now a boolean
        const downloadButtonImage = document.querySelector("#youtubeDL-download > img");
        if (downloadButtonImage == null) return;

        // set loading icon and flicker if loading else reset
        downloadButtonImage.src = getAsset(shown == true ? "YoutubeDL-loading.png" : "YoutubeDL.png");

        if (shown == true) downloadButtonImage.classList.add("youtubeDL-flicker");
        else               downloadButtonImage.classList.remove("youtubeDL-flicker");
    }
    function injectPopup() {
        /*<div id="youtubeDL-popup-bg" class="shown">

        </div>*/
        // if in proxy window/embed
        if (window.self !== window.top) {
            console.log("[YoutubeDL] Embed or internal window detected. Outer-injecting the popup of the iframe.");

            // outer injection
            sendToTopWindow("YoutubeDL_outerInject", null);
            return;
        }

        // check if existing already then set
        const existingElement = window.top.document.querySelector("#youtubeDL-popup-bg");
        if (existingElement) {
            popupElement = existingElement;
            return;
        }

        const revisedHTML = popupHTML.replaceAll('{asset}', githubAssetEndpoint);

        popupElement = createSafeElement("div", revisedHTML, {
            id: "youtubeDL-popup-bg",
            style: `line-height: initial; font-size: initial; z-index: ${Number.MAX_SAFE_INTEGER}`
        });

        document.body.appendChild(popupElement);

        togglePopupLoading(true);
        createButtonConnections();
        popupElement.hidden = true;
    }
    function sendToTopWindow(title, object) {
        window.top.postMessage({ title, object }, '*');
    }
    function sendToBottomWindows(title, object, specifiediFrame = null) {
        const iframes = specifiediFrame == null ? [specifiediFrame] : document.querySelectorAll('iframe');
        iframes.forEach((iframe) => iframe.contentWindow.postMessage({ title, object, passcode: "spaghetti" }, '*'));
    }

    let hideTimeout;
    let waitingReload = false;
    function togglePopup() {
        // if proxy window, send message via outer injection
        if (window.self !== window.top) {
            // outer injection
            sendToTopWindow("YoutubeDL_togglePopup", null);
            return;
        }

        checkUrlChange();

        if (needsUpdate) showNewUpdateText(needsUpdate);
        popupElement.classList.toggle("shown");

        if (waitingReload) { reloadMedia(); waitingReload = false;}
        else loadMedia();

        // Avoid overlap
        if (popupElement.hidden) {
            clearTimeout(hideTimeout);

            hideTimeout = setTimeout(() => popupElement.hidden = false, 200);
        };
    }
    async function togglePopupElement(embedLink) {
        if (popupElement.hidden == false) return;
        popupElement.hidden = false;

        const oldId = videoInformation.videoId;

        if (embedLink != null) {
            // reset video information
            videoInformation = getVideoInformation(embedLink);

            // youtube & no cookie support
            // replace embed via normal page for api support
            if (embedLink.includes('youtube.com/embed/') ||
                embedLink.includes('youtube-nocookie.com/embed/')) {
                const videoId = embedLink.split('/embed/')[1].split('?')[0];
                videoInformation.type = 'embed';
                videoInformation.videoId = videoId;
            }
        }

        togglePopup();

        // if changed from embed to another, reload media
        if (oldId != videoInformation.videoId)
            await reloadMedia();
    }
    // Button
    let injectedShorts = [];
    function injectDownloadButton() {
        let targets = [];
        let style;

        const onShorts = (videoInformation.type == 'shorts');
        const onEmbed = (videoInformation.type == 'embed');
        if (onShorts) {
            // Button for shorts
            const playerControls = document.querySelectorAll('ytd-shorts-player-controls');
            targets = playerControls;
            style = "margin-bottom: 16px; transform: translate(36%, 10%); pointer-events: auto;";
        } else if (onEmbed) {
            // Get all embeds on the page
            const controls = document.querySelectorAll(".ytp-left-controls");
            for (let i = 0; i < controls.length; i++) {
                const control = controls[i];
                const player = control.parentNode.parentNode.parentNode.parentNode;

                control.setAttribute(
                    "embedLink",
                    // if on top window, you can directly fetch from the iframe
                    // or else if in a proxy window, fetch directly from the location href
                    window.self === window.top ? getVideoUrlFromEmbed(player) : window.self.location.href
                );

                targets.push(control);
            }

            style = "margin-top: 4px; transform: translateY(-7%); display: flex;";
        } else {
            // Button for normal player
            targets.push(document.querySelector(".ytp-left-controls"));
            style = "margin-top: 4px; transform: translateY(-10%); padding-left: 4px; display: flex;";
        }

        targets.forEach((target) => {
            if (injectedShorts.includes(target)) return;

            const downloadButton = createSafeElement(
                "button",
                `<img src="${getAsset(hasFailedLoadingPageInformation ? "YoutubeDL-warning.png" : "YoutubeDL.png")}" style="${style}" width="36" height="36">`,
                {
                    id: 'youtubeDL-download',
                    'data-title-no-tooltip': 'YoutubeDL',
                    'aria-keyshortcuts': 'SHIFT+d',
                    'aria-label': 'Next keyboard shortcut SHIFT+d',
                    'data-duration': '',
                    'data-preview': '',
                    'data-tooltip-text': '',
                    href: '',
                    title: 'Download Video'
                }
            );
            downloadButton.classList.add("ytp-button");

            downloadButton.addEventListener("click", async(_) => {
                if (hasFailedLoadingPageInformation) {
                    alert(pageLoadingFailedMessage);
                    return;
                }
                if (hasMediaError) {
                    alert(mediaErrorMessage);
                    return;
                }

                // left controls
                const embedLink = downloadButton.parentNode.getAttribute("embedLink");

                // if we're in a proxy window/embed
                if (window.self !== window.top) {
                    console.log(`[YoutubeDL] Communicating to toggle popup (outer-proxy) | Linked iframe link: ${embedLink}`);

                    if (outerProxyLoading) return;

                    // outer injection
                    sendToTopWindow("YoutubeDL_togglePopupElement", embedLink);
                    return;
                }

                // else do regularly on top window
                await togglePopupElement(embedLink);
            });

            if (target.querySelector("#youtubeDL-download")) return;

            const chapterContainer = target.querySelector('.ytp-chapter-container');

            if (onShorts) {
                target.insertBefore(downloadButton, target.children[target.children.length]);
                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) => {
            GMxmlHttpRequest({
                method: 'GET',
                url: url,
                onload: function(response) {
                    if (response.status === 200) {
                        const style = createSafeElement('style', 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', (_) => {
            try {
                togglePopup();

                setTimeout(() => popupElement.hidden = true, 200);
            } catch (error) {console.error(error);}
        });
    }

    function showErrorOnDownloadButtons() {
        const downloadButtonsImages = document.querySelectorAll("#youtubeDL-download > img");

        for (let i = 0; i < downloadButtonsImages.length; i++) {
            const downloadButtonImage = downloadButtonsImages[i];
            downloadButtonImage.src = getAsset("YoutubeDL-warning.png");
        }
    }

    // Main page injection
    let hasFailedLoadingPageInformation = false;
    let didFirstShortsInjection = false;
    async function injectAll() {
        // double check
        if (videoInformation.type == 'shorts' && !didFirstShortsInjection) {
            injectDownloadButton();
            didFirstShortsInjection = true;
        }

        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);
            hasFailedLoadingPageInformation = true;

            showErrorOnDownloadButtons();
        }

        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);
    }
    let embedRefreshInterval;
    function initialize() {
        prepareOuterInjection();
        prepareOuterInjectionForProxy();
        updateVideoInformation();
        if (!videoInformation.type) return;

        console.log("[YoutubeDL] Loading... // (real)coloride - 2023-2024");

        if (window.self === window.top) prepareOuterInjection();

        // Emebds: wait for user to press play
        const isEmbed = (videoInformation.type == 'embed');
        if (isEmbed) {
            // if embed keep going until url changes
            if (embedRefreshInterval != null) return;

            // we have to handle when its executed in side of the embed and when outside (proxied windows)
            if (window.self !== window.top) {
                // if in proxy window, directly inject because the user would have already clicked
                (async() => await injectAll())();
                return;
            }

            // wait for click
            function injectTo(player) {
                player.addEventListener("click", async(_) => await injectAll());
            }

            // check if page is actual embed, get first player & inject (NOT autoplay)
            const regex = /^(?!.*youtube-nocookie\.com).*youtube\.com\/embed\/\w+/;
            if (regex.test(window.location.href)) {
                injectTo(document.querySelector("#player"));
                return;
            }

            // else if in global window
            const embeds = window.self.document.querySelectorAll('iframe[data-player="youtube"]');
            if (embeds.length == 0) return;

            // wait for click because of the youtube icon embed
            for (let i = 0; i < players.length; i++) {
                const embed = embeds[i];

                const allowAttributes = embed.getAttribute("allow");

                // check if on autoplay & if not inject only on click
                if (!allowAttributes.includes('autoplay'))
                    injectTo(embed);
                else (async() => await injectAll())();
            }
        } else {
            let injectionCheckInterval;
            injectionCheckInterval = setInterval(async() => {
                if (shouldInject())
                    try {
                        clearInterval(injectionCheckInterval);
                        await injectAll();
                    } catch (error) {
                        console.error("[YoutubeDL] ERROR: ", error);
                    }
            }, 600);
        }
    }

    // Checking for updates
    let needsUpdate = null;
    function showNewUpdateText(version) {
        needsUpdate = version;

        const element = document.querySelector("#youtubeDL-update-available"); if (!element) return;
        element.hidden = false;
        element.innerText = `An update (${version}) is available! Click here to update.`;
    }
    function checkForUpdates() {
        (async() => {
            const payload = {
                url: updateGreasyUrl,
                method: "GET",
                responseType: 'text',
                referrerPolicy: "strict-origin-when-cross-origin",
                mode: "cors",
                credentials: "omit"
            };

            const request = await GMxmlHttpRequest(payload);
            const response = JSON.parse(request.responseText);

            const currentVersion = response[0]["version"];
            const requiresUpdate = currentVersion != version;

            if (requiresUpdate) showNewUpdateText(currentVersion);
        })();
    }

    // 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;
            clearInterval(embedRefreshInterval);

            didFirstShortsInjection = false;

            updateVideoInformation();

            console.log(`[YoutubeDL] Detected video type: ${videoInformation.type}`);

            if (!videoInformation.type) return;

            waitingReload = true;
            await injectAll();

            if (videoInformation.type == 'shorts') injectDownloadButton();
        }
    }

    initialize();
    checkForUpdates();

    setInterval(checkUrlChange, 500);
    window.onhashchange = checkUrlChange;
})();