Youtube Video Downloader 2025

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

目前為 2025-04-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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;
        }
    }
})();