YouTube -> download MP3 or Video from YouTube.

Simple YouTube MP3 & MP4 download buttons. Simple and fast.

// ==UserScript==
// @version         3.7
// @name            YouTube -> download MP3 or Video from YouTube.
// @namespace       https://greasyfork.org/ru/scripts/386967-youtube-download-mp3-or-video-from-youtube
// @author			A.Vasin
// @description     Simple YouTube MP3 & MP4 download buttons. Simple and fast.
// @compatible      chrome
// @compatible      firefox
// @compatible      opera
// @compatible      safari
// @icon            https://avasin.herokuapp.com/UserScripts/YouTube-Saver/logo.png
// @include         http*://www.youtube.com/*
// @include      	http*://*.youtube.com/*
// @include      	http*://youtube.com/*
// @include      	http*://*.youtu.be/*
// @include      	http*://youtu.be/*
// @grant           GM_addStyle
// @grant           GM_download
// @run-at       	document-idle
// @copyright   	2019-02-11 // a.vasin
// @license         https://creativecommons.org/licenses/by-sa/4.0
// ==/UserScript==

class YouTubeSaver {
    constructor() {
        this.btnHolderSel = '#meta-contents #subscribe-button';
        this.downloadBtnClass = 'js-ytube-download';
        this.downloadAudioClass = 'js-mp3-download';
        this.language = (navigator.language || navigator.userLanguage).split('-')[0];
        this.defaultLang = 'en';
        this.baseServiceSupportedLangs = ['en', 'ru', 'sk', 'it', 'es', 'fr', 'de', 'nl', 'pt', 'tr', 'no', 'kr', 'jp', 'pl', 'cn', 'hu', 'in', 'ro', 'gr', 'cz', 'bg', 'rs', 'sa', 'id'];
        this.baseServiceLang = this.baseServiceSupportedLangs.includes(this.language) ? this.language : this.defaultLang;
        this.baseServiceUrl = `https://www.flvto.biz/${this.baseServiceLang}/convert?service=youtube&url=`; //https://y2mate.com/ru/youtube/sWgiVmcjt8c
        this.formatMap = {
            mp3: '1',
            mp4: '8',
            mp4HD: '7'
        };
        this.audioServiceBaseUrl = 'https://svr2.flvto.tv/downloader/state?id=';
        this.initInterval = 400;
        this.checkInterval = 1000;
        this.btnSize = '10px';
        this.btnPadding = '10px 5px';
        this.loaderHtml = '<div class="loader"><div class="rect1"></div><div class="rect2"></div><div class="rect3"></div><div class="rect4"></div><div class="rect5"></div></div>';
        this.langProps = {
            en: {
                'download.mp3': 'Download MP3',
                'download.video': 'Download VIDEO',
                'download': 'Download'
            },
            ru: {
                'download.mp3': 'Скачать MP3',
                'download.video': 'Скачать ВИДЕО',
                'download': 'Скачать'
            },
            sk: {
                'download.mp3': 'Stiahnuť MP3',
                'download.video': 'Stiahnuť VIDEO',
                'download': 'Stiahnuť'
            },
            ua: {
                'download.mp3': 'Скачати MP3',
                'download.video': 'Скачати VIDEO',
                'download': 'Скачати'
            }
        }
        this.currentProps = this.langProps[this.language] || this.langProps[this.defaultLang];

        this.init();
    }

    getAudioBtnHtml(link) {
        const apiDownloadUrl = this.getAudioDownloadUrl({url: link});
        const downloadUrl = this.getBaseDownloadUrl({url: link, format: this.formatMap.mp3});

        if(!apiDownloadUrl) {
            return '';
        }

        return `
            <a
                href="${downloadUrl}"
                data-href="${apiDownloadUrl}"
                target="_blank"
                class="${this.downloadBtnClass} ${this.downloadAudioClass}"
            >
                ${this.loaderHtml}
                ${this.getLangProp('download.mp3')}
            </a>
        `;
    }

    getVideoBtnHtml(link) {
        const downloadUrl = this.getBaseDownloadUrl({url: link, format: this.formatMap.mp4});

        return `
            <a
                href="${downloadUrl}"
                target="_blank"
                class="${this.downloadBtnClass}"
            >
                ${this.getLangProp('download.video')}
            </a>
        `;
    }

    init() {
        this.addStyles();
        this.initDownloadBtn();
        this.addDownloadIframe();
    }
    
    initDownloadBtn() {
        setInterval(() => {
            const appendToEl = document.querySelector(this.btnHolderSel);
            const downloadBtn = document.querySelector(`.${this.downloadBtnClass}`);
            
            // Append download buttons in case download mp3 button not available
            // && placeholder exist on page
            if(!downloadBtn && appendToEl) {
                this.appendBtns(appendToEl);
            }
        }, this.initInterval);
    }

    bindEvents(ctx) {
        const audioBtn = ctx.querySelector(`.${this.downloadAudioClass}`);

        audioBtn && audioBtn.addEventListener('click', this.onAudioDownload.bind(this, audioBtn));
    }

    addDownloadIframe() {
        this.downloadFrame = document.createElement('iframe');
        document.body.append(this.downloadFrame);
    }

    addStyles() {
        GM_addStyle(`
            .${this.downloadBtnClass} {
                position: relative;
                border: 2px solid #3f51b5;
                padding: ${this.btnPadding};
                font-size: ${this.btnSize};
                font-weight: 500;
                text-align: center;
                margin: 5px 4px 0;
                color: #3f51b5;
                text-decoration: none;
                flex-grow: 1;
                text-transform: uppercase;
            }

            .${this.downloadAudioClass} {
                border-color: #ff5722;
                color: #ff5722;
            }
            .loading {
                pointer-events: none;
                color: transparent;
            }
            .loading .loader {
                display: block;
            }
            .loader {
                display: none;
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                text-align: center;
                font-size: 10px;
            }
            .loader>div {
                background-color: #ff5722;
                height: 100%;
                width: 6px;
                margin: 0 1px;
                display: inline-block;
                animation: sk-stretchdelay 1.2s infinite ease-in-out;
            }
            .loader .rect2 {
                animation-delay: -1.1s;
            }
            .loader .rect3 {
                animation-delay: -1.0s;
            }
            .loader .rect4 {
                animation-delay: -0.9s;
            }
            .loader .rect5 {
                animation-delay: -0.8s;
            }
            @keyframes sk-stretchdelay {
                0%,
                40%,
                100% {
                    transform: scaleY(0.4);
                }
                20% {
                    transform: scaleY(1.0);
                }
            }

        `);
    }

    getLangProp(id) {
        return this.currentProps[id];
    }

    getAudioDownloadUrl({url} = {}) {
        const id = this.getVideoId(url);

        if(!id) {
            console.warn('Video ID not found/parsed.');
            return id;
        }

        return `${this.audioServiceBaseUrl}${id}`;
    }
    
    getBaseDownloadUrl({url, format = this.formatMap.mp3} = {}) {
        return `${this.baseServiceUrl}${encodeURIComponent(url)}&format=${format}`;
    }

    getNodeFromString(string) {
        const div = document.createElement('div');
        div.innerHTML = string.trim();

        return div.firstChild;
    }

    getVideoId(url) {
        var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
        var match = url.match(regExp);
        return (match && match[7].length==11) ? match[7] : false;
    }

    getVideoTitle() {
        return (document.querySelector('.ytp-title-link') || document.title).innerText;
    }

    appendBtns(appendToEl) {
        const url = document.location.href;
        const audioBtnHtml = this.getAudioBtnHtml(url);
        const videoBtnHtml = this.getVideoBtnHtml(url);
        const downloadWrapper = this.getNodeFromString(`
            <div style="
                display: flex;
            ">
                ${audioBtnHtml}
                ${videoBtnHtml}
            </div>
        `);

        //append buttons to the page
        appendToEl.append(downloadWrapper);

        // TODO: uncomment events for audio button
        // this.bindEvents(appendToEl);
    }

    downloadFile(url, btn) {
        this.downloadFrame.onerror = this.downloadFailed.bind(this, btn);
        /*
            TODO: Update handling error case
            this.downloadFrame.onload = () => {
            if(!this.downloadFrame.innerHTML) {
                this.downloadFailed(btn);
                this.downloadFrame.onload = null;
                this.downloadFrame.onerror = null;
            }
        }; */
        this.downloadFrame.src = url;
    }

    onAudioDownload(btn, e) {
        e.preventDefault();
        const _this = this;
        const url = btn.href;

        this.toggleLoader(btn);

        fetch(url)
            .then(resp => resp.json())
            .then(({dlMusic, status}) => {
                switch (status) {
                    case 'finished':
                        _this.downloadFile(dlMusic, btn);
                        setTimeout(_this.toggleLoader.bind(_this, btn, false), 500);
                        break;
                    case 'error':
                        throw new Error(error);
                    default:
                        setTimeout(_this.onAudioDownload.bind(_this, btn, e), _this.checkInterval);
                        break;
                }
            })
            .catch(_this.downloadFailed.bind(_this, btn));
    }

    downloadFailed(btn) {
        const alternativeUrl = this.getBaseDownloadUrl({url: window.location.href});
        this.toggleLoader(btn, false);

        window.open(alternativeUrl, '_blank');
    }

    toggleLoader(btn, isActivate = true, msg) {
        if(btn) {
            btn.classList.toggle('loading', isActivate);

            if(msg) {
                btn.innerText = msg;
            }
        }
    }
}

// Init downloader
const saver = new YouTubeSaver();