Youtube Video Downloader 2025

Download videos from youtube.com easily in various formats (mp4, webm, mp3, etc.) in 2025.

目前为 2025-04-22 提交的版本。查看 最新版本

// ==UserScript==
// @name         Youtube Video Downloader 2025
// @namespace    http://tampermonkey.net/
// @author       fb
// @version      1.2.1
// @description  Download videos from youtube.com easily in various formats (mp4, webm, mp3, etc.) in 2025.
// @match        https://www.youtube.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      p.oceansaver.in
// @license      GPL-3.0-or-later
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    let animInterval = null;
    let originalText = '';
    const STORAGE_KEY = 'selectedFormat';
    const UI_WRAPPER_ID = 'yt-downloader-wrapper';

    function isYouTubeLiveStream() {
        const ypr = window.ytInitialPlayerResponse;
        if (ypr?.videoDetails?.isLiveContent === true) {
          return true;
        }
        if (ypr?.microformat?.playerMicroformatRenderer?.liveBroadcastDetails) {
          return true;
        }
        if (document.querySelector('meta[itemprop="isLiveBroadcast"][content="True"]')) {
          return true;
        }
        if (document.querySelector('.ytp-live')) {
          return true;
        }
        return false;
      }
      

    // check if we are on a video page and inject/remove UI
    function checkPageAndInjectUI() {
        const isVideoPage = (window.location.pathname.startsWith('/watch') || window.location.pathname.startsWith('/shorts/')) && !isYouTubeLiveStream();
        const existingWrapper = document.getElementById(UI_WRAPPER_ID);

        if (isVideoPage) {
            setTimeout(() => {
                const actionsInner = document.querySelector('#end');
                if (actionsInner && !existingWrapper) {
                    console.log("Injecting Downloader UI");
                    injectUI(actionsInner);
                } else if (!actionsInner && !existingWrapper) {
                     console.log("Target container #end not found yet, will retry on next navigation event.");
                }
            }, 500);
        } else {
            if (existingWrapper) {
                console.log("Removing Downloader UI");
                removeUI();
            }
        }
    }

    document.addEventListener('yt-navigate-finish', checkPageAndInjectUI);

    checkPageAndInjectUI();

    function isDarkTheme() {
        return document.documentElement.hasAttribute('dark');
    }

    function injectUI(container) {
        if (document.getElementById(UI_WRAPPER_ID)) return;

        const wrapper = document.createElement('div');
        wrapper.id = UI_WRAPPER_ID;
        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'center';
        wrapper.style.marginRight = "10px";

        const select = document.createElement('select');
        select.id = 'format';
        select.className = 'doc';
        select.style.marginRight = '8px';

        select.addEventListener('change', (event) => {
            GM_setValue(STORAGE_KEY, event.target.value);
        });

        function makeOption(value, text, isSelected = false) {
            const opt = document.createElement('option');
            opt.value = value;
            opt.textContent = text;
            if (isSelected) opt.selected = true;
            return opt;
        }

        // default mp4 1080p
        const savedFormat = GM_getValue(STORAGE_KEY, '1080');

        const groups = [
            {
                label: 'Audio',
                options: [
                    ['mp3', 'MP3'],
                    ['m4a', 'M4A'],
                    ['webm', 'WEBM'],
                    ['aac', 'AAC'],
                    ['flac', 'FLAC'],
                    ['opus', 'OPUS'],
                    ['ogg', 'OGG'],
                    ['wav', 'WAV']
                ]
            },
            {
                label: 'Video',
                options: [
                    ['360', 'MP4 (360p)'],
                    ['480', 'MP4 (480p)'],
                    ['720', 'MP4 (720p)'],
                    ['1080', 'MP4 (1080p)'],
                    ['1440', 'MP4 (1440p)'],
                    ['4k', 'WEBM (4K)']
                ]
            }
        ];

        select.textContent = '';

        // build dropdown (trustedhtml fix)
        for (const { label, options } of groups) {
            const optgroup = document.createElement('optgroup');
            optgroup.label = label;

            for (const [value, text] of options) {
                const isSelected = (value === savedFormat);
                const option = makeOption(value, text, isSelected);
                optgroup.appendChild(option);
            }

            select.appendChild(optgroup);
        }

        const style = document.createElement('style');
        style.textContent = `
:root {
  --btn-bg: ${isDarkTheme() ? "#272727" : "#f2f2f2"};
  --btn-hover-bg: ${isDarkTheme() ? "#3f3f3f" : "#e5e5e5"};
  --btn-color: ${isDarkTheme() ? "#fff" : "#000"};
  --btn-radius: 18px;
  --btn-padding: 0 20px;
  --btn-font: 500 14px/36px "Roboto", "Arial", sans-serif;
  --btn-cursor: pointer;
  --progress-bg: ${isDarkTheme() ? "#3f3f3f" : "#e5e5e5"};
  --progress-fill-color: #2196F3;
  --progress-text-color: ${isDarkTheme() ? "#fff" : "#000"};
}

#download-button,
#format {
  display: inline-flex;
  align-items: center;
  color: var(--btn-color);
  background-color: var(--btn-bg);
  border: none;
  border-radius: var(--btn-radius);
  padding: var(--btn-padding);
  white-space: nowrap;
  text-transform: none;
  font: var(--btn-font);
  cursor: var(--btn-cursor);
  transition: background-color .2s ease;
  height: 36px;
  box-sizing: border-box;
}

#download-button:hover:not(:disabled),
#format:hover:not(:disabled) {
  background-color: var(--btn-hover-bg);
}

#download-button:disabled,
#format:disabled {
    cursor: not-allowed;
    opacity: 0.6;
}


#download-button {
  padding-left: 40px;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='${isDarkTheme() ? 'white' : 'black'}' stroke-width='0.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 4v12'/%3E%3Cpath d='M8 12l4 4 4-4'/%3E%3Cpath d='M4 18h16'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 8px center;
  background-size: 28px;
}

#format {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 40px;
  background-image:
    url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D'10'%20height%3D'7'%20xmlns%3D'http%3A//www.w3.org/2000/svg'%3E%3Cpath%20d%3D'M0%200l5%207%205-7z'%20fill%3D'%23${isDarkTheme() ? 'fff' : '000'}'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
}

#format::-ms-expand {
  display: none;
}

#download-progress-bar {
    display: inline-block;
    width: 150px;
    height: 36px;
    background-color: var(--progress-bg);
    border-radius: var(--btn-radius);
    overflow: hidden;
    position: relative;
    vertical-align: middle;
}

#download-progress-fill {
    height: 100%;
    width: 0%;
    background-color: var(--progress-fill-color);
    transition: width 0.3s ease-in-out;
}

#download-progress-text {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--progress-text-color);
    font: var(--btn-font);
    white-space: nowrap;
    z-index: 1;
}
`;
        document.head.appendChild(style);

        const btn = document.createElement('button');
        btn.id = 'download-button';
        btn.textContent = 'Download';

        btn.addEventListener('click', startDownload);

        wrapper.appendChild(select);
        wrapper.appendChild(btn);

        container.insertAdjacentElement('afterbegin', wrapper);
    }

    function removeUI() {
        const wrapper = document.getElementById(UI_WRAPPER_ID);
        if (wrapper) {
            wrapper.remove();
        }
    }

    function startDownload() {
        const btn = document.getElementById('download-button');
        const select = document.getElementById('format');
        if (!btn || !select) return;

        const fmt = select.value;
        const videoUrl = encodeURIComponent(window.location.href);
        const initUrl = `https://p.oceansaver.in/ajax/download.php?format=${fmt}&url=${videoUrl}`;

        startButtonAnimation(btn, select);

        GM_xmlhttpRequest({
            method: 'GET',
            url: initUrl,
            responseType: 'json',
            onload(res) {
                const data = res.response;
                if (!data || !data.success) {
                    stopButtonAnimation();
                    alert('❌ Failed to initialize download');
                    return;
                }
                pollProgress(data.progress_url);
            },
            onerror() {
                stopButtonAnimation();
                alert('❌ Network error while starting download');
            }
        });
    }

    function pollProgress(progressUrl) {
        const progressBarFill = document.getElementById('download-progress-fill');
        const progressText = document.getElementById('download-progress-text')

        if (!progressBarFill || !progressText) {
            console.error("Progress bar elements not found during polling.");
            stopButtonAnimation();
            return;
        }

        const intervalId = setInterval(() => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: progressUrl,
                responseType: 'json',
                onload(res) {
                    const p = res.response;
                    if (!p) {
                        console.warn('Polling received empty response.');
                        clearInterval(intervalId);
                        stopButtonAnimation();
                        return;
                    }
                    if (p.success) {
                        clearInterval(intervalId);
                        progressBarFill.style.width = '100%';
                        progressText.textContent = '100%';
                        setTimeout(() => {
                            triggerFileDownload(p.download_url);
                            stopButtonAnimation();
                        }, 300);
                    } else {
                        const progressPercent = p.progress ? Math.min(Math.round(p.progress / 10), 100) : 0;
                        console.log(`Download progress: ${progressPercent}%`);
                        progressBarFill.style.width = `${progressPercent}%`;
                        progressText.textContent = `${progressPercent}%`;
                    }
                },
                onerror() {
                    console.error('Error polling download progress');
                    clearInterval(intervalId);
                    stopButtonAnimation();
                    alert('❌ Error checking download status');
                }
            });
        }, 1500);
    }

    function triggerFileDownload(url) {
        const a = document.createElement('a');
        a.href = url;
        a.download = '';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }

    function startButtonAnimation(btn, select) {
        originalText = btn.textContent;
        btn.style.display = 'none';
        select.disabled = true;

        const progressBar = document.createElement('div');
        progressBar.id = 'download-progress-bar';

        const progressFill = document.createElement('div');
        progressFill.id = 'download-progress-fill';

        const progressText = document.createElement('div');
        progressText.id = 'download-progress-text';
        progressText.textContent = '0%';

        progressBar.appendChild(progressFill);
        progressBar.appendChild(progressText);

        select.parentNode.insertBefore(progressBar, select.nextSibling);

        if (animInterval) {
            clearInterval(animInterval);
            animInterval = null;
        }
    }

    function stopButtonAnimation() {
        const btn = document.getElementById('download-button');
        const select = document.getElementById('format');
        const progressBar = document.getElementById('download-progress-bar');

        if (progressBar && progressBar.parentNode) {
            progressBar.parentNode.removeChild(progressBar);
        }

        const wrapper = document.getElementById(UI_WRAPPER_ID);
        if (wrapper && btn) {
            btn.style.display = 'inline-flex';
            btn.disabled = false;
            btn.textContent = originalText || 'Download';
        }
        if (wrapper && select) {
            select.disabled = false;
        }

        if (animInterval) {
            clearInterval(animInterval);
            animInterval = null;
        }
    }
})();