Cobalt Tools (Direct YouTube Audio Downloader)

Integrate a high-quality 320kbps audio download button before the video title.

// ==UserScript==
// @name         Cobalt Tools (Direct YouTube Audio Downloader)
// @description  Integrate a high-quality 320kbps audio download button before the video title.
// @icon         https://raw.githubusercontent.com/exyezed/cobalt-tools/refs/heads/main/extras/cobalt-tools.png
// @version      1.2
// @author       exyezed
// @namespace    https://github.com/exyezed/cobalt-tools/
// @supportURL   https://github.com/exyezed/cobalt-tools/issues
// @license      MIT
// @match        https://www.youtube.com/*
// @match        https://youtube.com/*
// @grant        GM.xmlHttpRequest
// @connect      exyezed.vercel.app
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    function addMusicIconToTitle() {
        const titleContainer = document.querySelector('h1.style-scope.ytd-watch-metadata');
        const titleElement = titleContainer?.querySelector('yt-formatted-string');
        
        if (!titleContainer || !titleElement) return;
        
        if (titleContainer.querySelector('svg')) return;
        
        const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svgElement.setAttribute("viewBox", "0 0 384 512");
        svgElement.setAttribute("width", "16");
        svgElement.setAttribute("height", "16");
        svgElement.style.marginRight = "8px";
        svgElement.style.transition = "all 0.2s ease";
        svgElement.style.fill = "white";
        svgElement.style.flexShrink = "0";
        svgElement.style.cursor = "pointer";
        
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("d", "M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8l0 128c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376 80 376c-8.8 0-16-7.2-16-16l0-48c0-8.8 7.2-16 16-16l33.4 0 35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6");
        svgElement.appendChild(path);

        const successSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        successSvg.setAttribute("viewBox", "0 0 512 512");
        successSvg.setAttribute("width", "16");
        successSvg.setAttribute("height", "16");
        successSvg.style.display = "none";
        successSvg.style.fill = "#1ed760";
        successSvg.style.marginRight = "8px";
        
        const successPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
        successPath.setAttribute("d", "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z");
        successSvg.appendChild(successPath);

        const errorSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        errorSvg.setAttribute("viewBox", "0 0 512 512");
        errorSvg.setAttribute("width", "16");
        errorSvg.setAttribute("height", "16");
        errorSvg.style.display = "none";
        errorSvg.style.fill = "#f3727f";
        errorSvg.style.marginRight = "8px";
        
        const errorPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
        errorPath.setAttribute("d", "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z");
        errorSvg.appendChild(errorPath);

        const loadingSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        loadingSvg.setAttribute("viewBox", "0 0 512 512");
        loadingSvg.setAttribute("width", "16");
        loadingSvg.setAttribute("height", "16");
        loadingSvg.style.display = "none";
        loadingSvg.style.fill = "white";
        loadingSvg.style.marginRight = "8px";

        const style2 = document.createElementNS("http://www.w3.org/2000/svg", "style");
        style2.textContent = ".fa-secondary{opacity:.4}";
        loadingSvg.appendChild(style2);

        const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
        secondaryPath.setAttribute("class", "fa-secondary");
        secondaryPath.setAttribute("d", "M0 256C0 114.9 114.1 .5 255.1 0C237.9 .5 224 14.6 224 32c0 17.7 14.3 32 32 32C150 64 64 150 64 256s86 192 192 192c69.7 0 130.7-37.1 164.5-92.6c-3 6.6-3.3 14.8-1 22.2c1.2 3.7 3 7.2 5.4 10.3c1.2 1.5 2.6 3 4.1 4.3c.8 .7 1.6 1.3 2.4 1.9c.4 .3 .8 .6 1.3 .9s.9 .6 1.3 .8c5 2.9 10.6 4.3 16 4.3c11 0 21.8-5.7 27.7-16c-44.3 76.5-127 128-221.7 128C114.6 512 0 397.4 0 256z");
        loadingSvg.appendChild(secondaryPath);

        const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
        primaryPath.setAttribute("class", "fa-primary");
        primaryPath.setAttribute("d", "M224 32c0-17.7 14.3-32 32-32C397.4 0 512 114.6 512 256c0 46.6-12.5 90.4-34.3 128c-8.8 15.3-28.4 20.5-43.7 11.7s-20.5-28.4-11.7-43.7c16.3-28.2 25.7-61 25.7-96c0-106-86-192-192-192c-17.7 0-32-14.3-32-32z");
        loadingSvg.appendChild(primaryPath);

        titleContainer.insertBefore(svgElement, titleContainer.firstChild);
        titleContainer.insertBefore(loadingSvg, svgElement.nextSibling);
        titleContainer.insertBefore(successSvg, loadingSvg.nextSibling);
        titleContainer.insertBefore(errorSvg, successSvg.nextSibling);

        function getYouTubeVideoID(url) {
            const urlParams = new URLSearchParams(new URL(url).search);
            return urlParams.get('v');
        }

        function makeRequest(videoId) {
            return new Promise((resolve, reject) => {
                GM.xmlHttpRequest({
                    method: "GET",
                    url: `https://exyezed.vercel.app/api/cobalt/audio/${videoId}`,
                    headers: {
                        "Content-Type": "application/json"
                    },
                    onload: function(response) {
                        try {
                            const data = JSON.parse(response.responseText);
                            resolve(data);
                        } catch (e) {
                            reject(e);
                        }
                    },
                    onerror: function(error) {
                        reject(error);
                    }
                });
            });
        }

        async function cobaltRequest(videoUrl) {
            const videoId = getYouTubeVideoID(videoUrl);

            if (!videoId) {
                showError();
                return;
            }

            try {
                svgElement.style.display = "none";
                loadingSvg.style.display = "block";
                loadingSvg.style.animation = "spin 1s linear infinite";
                
                const data = await makeRequest(videoId);
                
                loadingSvg.style.display = "none";
                loadingSvg.style.animation = "";
                
                if (data.url) {
                    const downloadLink = document.createElement('a');
                    downloadLink.href = data.url;
                    downloadLink.download = titleElement.textContent;
                    document.body.appendChild(downloadLink);
                    downloadLink.click();
                    document.body.removeChild(downloadLink);
                    
                    successSvg.style.display = "block";
                    setTimeout(() => {
                        successSvg.style.display = "none";
                        svgElement.style.display = "block";
                        svgElement.style.fill = "white";
                    }, 2000);
                } else {
                    showError();
                }
            } catch (error) {
                showError();
            }
        }

        function showError() {
            loadingSvg.style.display = "none";
            loadingSvg.style.animation = "";
            errorSvg.style.display = "block";
            setTimeout(() => {
                errorSvg.style.display = "none";
                svgElement.style.display = "block";
                svgElement.style.fill = "white";
            }, 2000);
        }
        
        svgElement.addEventListener('click', () => {
            const currentVideoUrl = window.location.href;
            cobaltRequest(currentVideoUrl);
        });
        
        const style = document.createElement('style');
        style.textContent = `
            @keyframes spin {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }
        `;
        document.head.appendChild(style);
        
        svgElement.addEventListener('mouseenter', () => {
            if (loadingSvg.style.display !== "block" && 
                successSvg.style.display !== "block" && 
                errorSvg.style.display !== "block") {
                svgElement.style.fill = "#1ed760";
            }
        });
        
        svgElement.addEventListener('mouseleave', () => {
            if (loadingSvg.style.display !== "block" && 
                successSvg.style.display !== "block" && 
                errorSvg.style.display !== "block") {
                svgElement.style.fill = "white";
            }
        });
        
        titleContainer.style.display = 'flex';
        titleContainer.style.alignItems = 'center';
    }

    if (window.location.pathname === '/watch') {
        const checkExist = setInterval(function() {
            const titleContainer = document.querySelector('h1.style-scope.ytd-watch-metadata');
            if (titleContainer) {
                clearInterval(checkExist);
                addMusicIconToTitle();
            }
        }, 100);
    }

    const observer = new MutationObserver((mutations) => {
        if (window.location.pathname === '/watch') {
            addMusicIconToTitle();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
    console.log('Cobalt Tools (Direct YouTube Audio Downloader) is running');
})();