youtube视频下载插件油猴版(修复第一次观看未加载下载按钮)

2026-04-29 11:132阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

此次更新修复了第一次打开youtube视频无法加载下载按钮的问题,目前软件支持视频下载和短视频下载,支持手机端和电脑端。强调:由于调用第三方解析站,下载视频需要等待加载!

// ==UserScript== // @name YouTube Downloader (loader.to) // @namespace http://tampermonkey.net/ // @version 1.0 // @description Download YouTube videos using loader.to, beautifully integrated into YouTube web and mobile UI. // @author You // @match *://*.youtube.com/* // @grant GM_xmlhttpRequest // @connect p.savenow.to // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const SELECTOR_DESKTOP = '#top-level-buttons-computed'; const SELECTOR_MOBILE = 'ytm-slim-video-action-bar-renderer .slim-video-action-bar-actions'; const SELECTOR_SHORTS_DESKTOP = 'ytd-reel-video-renderer #actions #button-bar'; const SELECTOR_SHORTS_MOBILE = 'div.ytShortsCarouselCarouselItem[aria-hidden="false"] .reel-player-overlay-actions'; // Add base styles const style = document.createElement('style'); style.textContent = ` #yt-custom-download-btn { display: flex; align-items: center; justify-content: center; padding: 0 16px; height: 36px; border-radius: 18px; background-color: var(--yt-spec-buttonchip-background-primary, rgba(0, 0, 0, 0.05)); color: var(--yt-spec-text-primary, #0f0f0f); font-size: 14px; font-weight: 500; font-family: "Roboto", "Arial", sans-serif; cursor: pointer; margin-right: 8px; border: none; outline: none; transition: background-color 0.2s; } #yt-custom-download-btn:hover { background-color: var(--yt-spec-buttonchip-background-hover, rgba(0, 0, 0, 0.1)); } .yt-is-dark-theme #yt-custom-download-btn { background-color: var(--yt-spec-buttonchip-background-primary, rgba(255, 255, 255, 0.1)); color: var(--yt-spec-text-primary, #f1f1f1); } .yt-is-dark-theme #yt-custom-download-btn:hover { background-color: var(--yt-spec-buttonchip-background-hover, rgba(255, 255, 255, 0.2)); } #yt-custom-download-btn svg { margin-right: 6px; fill: currentColor; width: 20px; height: 20px; } #yt-download-popover { position: absolute; background-color: var(--yt-spec-base-background, #ffffff); color: var(--yt-spec-text-primary, #0f0f0f); border: 1px solid var(--yt-spec-10-percent-layer, rgba(0,0,0,0.1)); border-radius: 12px; padding: 16px; box-shadow: 0 4px 24px rgba(0,0,0,0.15); z-index: 9999; display: none; flex-direction: column; gap: 12px; min-width: 250px; font-family: inherit; } .yt-is-dark-theme #yt-download-popover { background-color: var(--yt-spec-base-background, #0f0f0f); border-color: var(--yt-spec-10-percent-layer, rgba(255,255,255,0.1)); box-shadow: 0 4px 24px rgba(0,0,0,0.5); color: var(--yt-spec-text-primary, #f1f1f1); } #yt-download-popover .popover-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: bold; } #yt-dl-close { background: none; border: none; color: var(--yt-spec-text-primary, #000); cursor: pointer; font-size: 22px; line-height: 1; padding: 0 4px; } .yt-is-dark-theme #yt-dl-close { color: #f1f1f1; } #yt-dl-format { padding: 8px; border-radius: 6px; border: 1px solid var(--yt-spec-10-percent-layer, #ccc); background: var(--yt-spec-base-background, #fff); color: var(--yt-spec-text-primary, #000); width: 100%; outline: none; font-size: 14px; } .yt-is-dark-theme #yt-dl-format { border-color: rgba(255,255,255,0.2); background: #272727; color: #f1f1f1; } .yt-dl-actions { display: flex; gap: 10px; margin-top: 5px; } #yt-dl-confirm { flex: 1; background-color: #3ea6ff; color: #fff; border: none; padding: 8px; border-radius: 6px; cursor: pointer; font-weight: 500; font-size: 14px; } #yt-dl-confirm:hover { background-color: #65b8ff; } #yt-dl-iframe-container { width: 100%; text-align: center; margin-top: 5px; min-height: 65px; overflow: hidden; display: flex; justify-content: center; } /* Mobile styles overrides */ .mobile-layout #yt-custom-download-btn { margin: 0 4px; height: 32px; padding: 0 12px; border-radius: 16px; } .mobile-layout #yt-download-popover { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); width: calc(100% - 40px); max-width: 400px; } `; document.head.appendChild(style); // Context tracking let popoverElement = null; function getCleanUrl() { // Strip out list and index parameters from URL let url = window.location.href; url = url.replace(/&list=[^&]*/g, ''); url = url.replace(/\?list=[^&]*&?/g, '?'); url = url.replace(/&index=[^&]*/g, ''); url = url.replace(/\?index=[^&]*&?/g, '?'); url = url.replace(/\?$/g, ''); return url; } function isDarkTheme() { return document.documentElement.hasAttribute('dark') || document.body.hasAttribute('dark') || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); } function createPopover() { if (popoverElement) return popoverElement; popoverElement = document.createElement('div'); popoverElement.id = 'yt-download-popover'; // Replace innerHTML with programmatic nodes to satisfy TrustedHTML policy // 1. Header const headerDiv = document.createElement('div'); headerDiv.className = 'popover-header'; const headerTitle = document.createElement('span'); headerTitle.textContent = 'Download Video'; const headerClose = document.createElement('button'); headerClose.id = 'yt-dl-close'; headerClose.textContent = '×'; headerDiv.appendChild(headerTitle); headerDiv.appendChild(headerClose); // 2. Format Select const formatSelect = document.createElement('select'); formatSelect.id = 'yt-dl-format'; const formats = [ { value: 'mp3', text: 'MP3 (Audio)' }, { value: 'm4a', text: 'M4A (Audio)' }, { value: '360', text: 'MP4 360p' }, { value: '480', text: 'MP4 480p' }, { value: '720', text: 'MP4 720p', selected: true }, { value: '1080', text: 'MP4 1080p' }, { value: '4k', text: 'WEBM 4K' }, { value: '8k', text: 'WEBM 8K' } ]; formats.forEach(f => { const opt = document.createElement('option'); opt.value = f.value; opt.textContent = f.text; if (f.selected) opt.selected = true; formatSelect.appendChild(opt); }); // 3. Actions Form const actionsDiv = document.createElement('div'); actionsDiv.className = 'yt-dl-actions'; const confirmBtn = document.createElement('button'); confirmBtn.id = 'yt-dl-confirm'; confirmBtn.textContent = 'Load Download Link'; actionsDiv.appendChild(confirmBtn); // 4. Removed iframeContainer since we open in a new tab // 5. Build Popover popoverElement.appendChild(headerDiv); popoverElement.appendChild(formatSelect); popoverElement.appendChild(actionsDiv); popoverElement.appendChild(actionsDiv); document.body.appendChild(popoverElement); // Event listeners (Using the elements we just created above) headerClose.addEventListener('click', () => { popoverElement.style.display = 'none'; }); confirmBtn.addEventListener('click', () => { const selectedFormat = formatSelect.value; const cleanUrl = getCleanUrl(); // Encode the cleaned url const encodedUrl = encodeURIComponent(cleanUrl); // UI State change confirmBtn.disabled = true; confirmBtn.style.backgroundColor = '#888'; confirmBtn.textContent = 'Starting...'; const originalText = 'Load Download Link'; const originalBg = ''; // reverts to css style function resetBtn() { confirmBtn.disabled = false; confirmBtn.style.backgroundColor = originalBg; confirmBtn.textContent = originalText; } // 1. Init Download Task try { if (typeof GM_xmlhttpRequest === 'undefined') { throw new Error('GM_xmlhttpRequest not granted.'); } GM_xmlhttpRequest({ method: 'GET', url: 'https://p.savenow.to/ajax/download.php?format=' + selectedFormat + '&url=' + encodedUrl, headers: { 'Origin': 'https://en.loader.to', 'Referer': 'https://en.loader.to/' }, onload: function (response) { let data; try { data = JSON.parse(response.responseText); } catch (e) { console.error("Failed to parse init response: ", response.responseText); confirmBtn.textContent = 'Parse Error'; setTimeout(resetBtn, 2000); return; } // Extract ID. Sometimes it's directly 'id', sometimes we extract from progress_url let taskId = data.id; if (!taskId && data.progress_url) { try { const urlObj = new URL(data.progress_url); taskId = urlObj.searchParams.get('id'); } catch (e) { } } if (!data.success || !taskId || data.text === 'Video too long / Livestream') { console.error('API Error or Video too long', data); confirmBtn.textContent = data.text ? 'Error: ' + data.text : 'API Error'; setTimeout(resetBtn, 3000); return; } confirmBtn.textContent = 'Initializing (0%)...'; // 2. Poll Progress const pollInterval = setInterval(() => { GM_xmlhttpRequest({ method: 'GET', url: 'https://p.savenow.to/api/progress?id=' + taskId, headers: { 'Origin': 'https://en.loader.to', 'Referer': 'https://en.loader.to/' }, onload: function (res) { let progData; try { progData = JSON.parse(res.responseText); } catch (e) { return; // ignore parse errors on polling, try next tick } if (progData.progress !== undefined) { const rawProgress = parseInt(progData.progress, 10); const currentProgress = isNaN(rawProgress) ? 0 : rawProgress; const pct = (currentProgress / 10).toFixed(1); const statusText = progData.text || 'Downloading'; if (currentProgress < 1000) { confirmBtn.textContent = statusText + ' (' + pct + '%)...'; } else { // 100% finished clearInterval(pollInterval); confirmBtn.textContent = 'Download Ready!'; confirmBtn.style.backgroundColor = '#4caf50'; // Green // Trigger native download using window.open if (progData.download_url) { window.open(progData.download_url, '_blank'); } setTimeout(resetBtn, 3000); } } }, onerror: function (e) { console.error('Polling error:', e); clearInterval(pollInterval); confirmBtn.textContent = 'Polling Error'; setTimeout(resetBtn, 2000); } }); }, 1500); // Poll every 1.5 seconds }, onerror: function (err) { console.error('Init error:', err); confirmBtn.textContent = 'Network Error'; setTimeout(resetBtn, 2000); } }); } catch (err) { console.error('Fatal Script Error:', err); confirmBtn.textContent = 'Script Error (Update Headers)'; setTimeout(resetBtn, 3000); } }); // Close when clicking outside of the popover document.addEventListener('click', (e) => { const btn = document.getElementById('yt-custom-download-btn'); if (popoverElement.style.display === 'flex' && !popoverElement.contains(e.target) && (!btn || !btn.contains(e.target))) { popoverElement.style.display = 'none'; } }); return popoverElement; } function positionPopover(button) { const isMobile = window.location.hostname === 'm.youtube.com'; const popover = createPopover(); // removed iframe container reset // Update theme class based on YouTube's current theme if (isDarkTheme()) { document.body.classList.add('yt-is-dark-theme'); } else { document.body.classList.remove('yt-is-dark-theme'); } if (isMobile) { document.body.classList.add('mobile-layout'); // CSS handles fixed bottom position for mobile } else { document.body.classList.remove('mobile-layout'); // Calculate position for desktop (relative to clicked button) const rect = button.getBoundingClientRect(); const popoverWidth = 250; // Estimated height of the popover menu const popoverHeight = 120; // Default: position below the button let topPos = rect.bottom + window.scrollY + 10; // If it would overflow the bottom of the screen, place it ABOVE the button instead if (rect.bottom + popoverHeight + 10 > window.innerHeight) { topPos = rect.top + window.scrollY - popoverHeight - 10; } popover.style.top = topPos + 'px'; // Center popover horizontally relative to the button let leftPos = rect.left + window.scrollX - (popoverWidth / 2) + (rect.width / 2); // Ensure popover doesn't overflow viewport horizontally (especially on Shorts right-aligned UI) if (leftPos + popoverWidth > window.innerWidth) { leftPos = window.innerWidth - popoverWidth - 20; } if (leftPos < 10) { leftPos = 10; } popover.style.left = leftPos + 'px'; } popover.style.display = 'flex'; } function injectButton() { // Only run on watch or shorts pages const isWatch = window.location.pathname.startsWith('/watch'); const isShorts = window.location.pathname.startsWith('/shorts'); if (!isWatch && !isShorts) return; const isMobile = window.location.hostname === 'm.youtube.com'; let targetElement = null; // More aggressive find: Check for actual layout presence const findTarget = (selectorList) => { const selectors = selectorList.split(',').map(s => s.trim()); for (const selector of selectors) { const elms = document.querySelectorAll(selector); const found = Array.from(elms).find(el => { const rect = el.getBoundingClientRect(); // In SPA, wait for element to have at least some height, // but don't be too strict on offsetParent return rect.height > 0 || el.childNodes.length > 0; }); if (found) return found; } return null; }; if (isShorts) { targetElement = findTarget(isMobile ? SELECTOR_SHORTS_MOBILE : SELECTOR_SHORTS_DESKTOP); } else { if (isMobile) { targetElement = findTarget(SELECTOR_MOBILE); } else { // PC Desktop: Target the primary button container, with multiple fallbacks targetElement = findTarget('ytd-watch-metadata #top-level-buttons-computed, ytd-video-primary-info-renderer #top-level-buttons-computed, #top-level-buttons-computed, #actions-inner #menu #top-level-buttons-computed'); // Final fallback for PC: look for the segmented like button container and inject into its parent menu if (!targetElement) { const likeBtn = document.querySelector('ytd-segmented-like-dislike-button-renderer'); if (likeBtn) { targetElement = likeBtn.closest('#top-level-buttons-computed') || likeBtn.parentElement; } } } } if (!targetElement) return; // Prevent duplicate injection, but handle infinite scroll for Shorts const existingBtn = document.getElementById('yt-custom-download-btn'); if (existingBtn) { if (isShorts && !targetElement.contains(existingBtn)) { // Button is attached to an old/inactive Short. Remove it so we inject into the new active one. existingBtn.remove(); } else { // Button is already exactly where it should be return; } } const btn = document.createElement('button'); btn.id = 'yt-custom-download-btn'; // Construct the button safely (No innerHTML) const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); svg.setAttribute('focusable', 'false'); svg.style.pointerEvents = 'none'; svg.style.display = 'block'; const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M17 18V19H6V18H17ZM16.5 11.4L15.8 10.7L12 14.4V4H11V14.4L7.2 10.6L6.5 11.3L11.5 16.3L16.5 11.4Z'); g.appendChild(path); svg.appendChild(g); const textSpan = document.createElement('span'); textSpan.className = 'btn-text'; textSpan.textContent = 'Download'; btn.appendChild(svg); btn.appendChild(textSpan); // Add tooltip for desktop if (!isMobile) { btn.title = 'Download Video'; } btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const popover = document.getElementById('yt-download-popover'); if (popover && popover.style.display === 'flex') { popover.style.display = 'none'; // Toggle off } else { positionPopover(btn); // Show } }); // Insert into YouTube DOM if (isShorts) { if (!isMobile) { // Desktop Shorts layout needs circular button btn.style.marginRight = '0'; btn.style.marginTop = '16px'; btn.style.width = '48px'; btn.style.height = '48px'; btn.style.borderRadius = '50%'; btn.style.padding = '0'; btn.style.backgroundColor = 'var(--yt-spec-badge-chip-background, rgba(0, 0, 0, 0.05))'; // Hide text on desktop shorts (stack of round icons) textSpan.style.display = 'none'; svg.style.marginRight = '0'; svg.style.width = '24px'; svg.style.height = '24px'; targetElement.appendChild(btn); } else { // Mobile Shorts layout: transparent, vertical layout btn.style.backgroundColor = 'transparent'; btn.style.marginRight = '0'; btn.style.marginTop = '16px'; btn.style.display = 'flex'; btn.style.flexDirection = 'column'; btn.style.justifyContent = 'center'; btn.style.alignItems = 'center'; svg.style.width = '28px'; svg.style.height = '28px'; svg.style.marginRight = '0'; textSpan.style.display = 'block'; // Ensure text is visible if overridden by media query textSpan.style.fontSize = '12px'; textSpan.style.marginTop = '4px'; textSpan.style.color = '#fff'; // Add right after the other vertical buttons targetElement.appendChild(btn); } } else if (isMobile) { targetElement.appendChild(btn); } else { // Desktop standard video: Insert before the first button in the top-level-buttons container if (targetElement.firstChild) { targetElement.insertBefore(btn, targetElement.firstChild); } else { targetElement.appendChild(btn); } } } // Use MutationObserver because YouTube is an SPA and loads comments/metadata asynchronously let domObserver = new MutationObserver((mutations) => { injectButton(); }); function init() { // Start observing DOM changes to inject button when target element appears domObserver.observe(document.body, { childList: true, subtree: true }); // Handle YouTube's SPA navigation events (cleanup and re-inject) const handleNav = () => { // 1. Hide and reset popover const popover = document.getElementById('yt-download-popover'); if (popover) { popover.style.display = 'none'; } // 2. Force remove old button const oldBtn = document.getElementById('yt-custom-download-btn'); if (oldBtn) { oldBtn.remove(); } // 3. Polling retry with longer duration (10s) let retries = 0; const poller = setInterval(() => { const btn = document.getElementById('yt-custom-download-btn'); if (btn || retries > 20) { clearInterval(poller); } else { injectButton(); retries++; } }, 500); }; window.addEventListener('yt-navigate-finish', handleNav); window.addEventListener('yt-page-data-updated', handleNav); // Initial run injectButton(); } // Initialize the script init(); })(); 网友解答:


--【壹】--:

谢谢!已私信给你。

感觉大概率是三方解析的问题。跟第一天下载成功的状态完全不同呢。


--【贰】--:

啥原理?第三方解析没有遥测的?代码太长了。。


--【叁】--:

感谢佬友分享油管下载油猴脚本,已经安装正在测试,就是第三方解析这里,时间的确有点久


--【肆】--:

这种没办法的,原站解析还有广告,除非找到更好的解析网站,目前为止这个应该是最稳定的。


--【伍】--:

有没有具体的视频链接呢?我试试,之前测试没问题,如果出现这种,可能是解析站故障


--【陆】--:

调用第三方解析网站,相当于去广告集成油管。


--【柒】--:

大佬厉害,可以使用了,虽然解析有点慢,但是能用就行,终于可以卸载idm了


--【捌】--:

佬友好,今天似乎一直不成功,读条进度到2.5%之后,就提示可以下载,然后无下文…是否跟调用的第三方解析接口那里出问题了?


--【玖】--:

我想问一下,就是有没有关于那些付费订阅内容也能够看的脚本嘞


--【拾】--:

这种就不清楚了,没接触过,收费内容除非别人分享,不然光靠脚本是解析不了的。

问题描述:

此次更新修复了第一次打开youtube视频无法加载下载按钮的问题,目前软件支持视频下载和短视频下载,支持手机端和电脑端。强调:由于调用第三方解析站,下载视频需要等待加载!

// ==UserScript== // @name YouTube Downloader (loader.to) // @namespace http://tampermonkey.net/ // @version 1.0 // @description Download YouTube videos using loader.to, beautifully integrated into YouTube web and mobile UI. // @author You // @match *://*.youtube.com/* // @grant GM_xmlhttpRequest // @connect p.savenow.to // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const SELECTOR_DESKTOP = '#top-level-buttons-computed'; const SELECTOR_MOBILE = 'ytm-slim-video-action-bar-renderer .slim-video-action-bar-actions'; const SELECTOR_SHORTS_DESKTOP = 'ytd-reel-video-renderer #actions #button-bar'; const SELECTOR_SHORTS_MOBILE = 'div.ytShortsCarouselCarouselItem[aria-hidden="false"] .reel-player-overlay-actions'; // Add base styles const style = document.createElement('style'); style.textContent = ` #yt-custom-download-btn { display: flex; align-items: center; justify-content: center; padding: 0 16px; height: 36px; border-radius: 18px; background-color: var(--yt-spec-buttonchip-background-primary, rgba(0, 0, 0, 0.05)); color: var(--yt-spec-text-primary, #0f0f0f); font-size: 14px; font-weight: 500; font-family: "Roboto", "Arial", sans-serif; cursor: pointer; margin-right: 8px; border: none; outline: none; transition: background-color 0.2s; } #yt-custom-download-btn:hover { background-color: var(--yt-spec-buttonchip-background-hover, rgba(0, 0, 0, 0.1)); } .yt-is-dark-theme #yt-custom-download-btn { background-color: var(--yt-spec-buttonchip-background-primary, rgba(255, 255, 255, 0.1)); color: var(--yt-spec-text-primary, #f1f1f1); } .yt-is-dark-theme #yt-custom-download-btn:hover { background-color: var(--yt-spec-buttonchip-background-hover, rgba(255, 255, 255, 0.2)); } #yt-custom-download-btn svg { margin-right: 6px; fill: currentColor; width: 20px; height: 20px; } #yt-download-popover { position: absolute; background-color: var(--yt-spec-base-background, #ffffff); color: var(--yt-spec-text-primary, #0f0f0f); border: 1px solid var(--yt-spec-10-percent-layer, rgba(0,0,0,0.1)); border-radius: 12px; padding: 16px; box-shadow: 0 4px 24px rgba(0,0,0,0.15); z-index: 9999; display: none; flex-direction: column; gap: 12px; min-width: 250px; font-family: inherit; } .yt-is-dark-theme #yt-download-popover { background-color: var(--yt-spec-base-background, #0f0f0f); border-color: var(--yt-spec-10-percent-layer, rgba(255,255,255,0.1)); box-shadow: 0 4px 24px rgba(0,0,0,0.5); color: var(--yt-spec-text-primary, #f1f1f1); } #yt-download-popover .popover-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: bold; } #yt-dl-close { background: none; border: none; color: var(--yt-spec-text-primary, #000); cursor: pointer; font-size: 22px; line-height: 1; padding: 0 4px; } .yt-is-dark-theme #yt-dl-close { color: #f1f1f1; } #yt-dl-format { padding: 8px; border-radius: 6px; border: 1px solid var(--yt-spec-10-percent-layer, #ccc); background: var(--yt-spec-base-background, #fff); color: var(--yt-spec-text-primary, #000); width: 100%; outline: none; font-size: 14px; } .yt-is-dark-theme #yt-dl-format { border-color: rgba(255,255,255,0.2); background: #272727; color: #f1f1f1; } .yt-dl-actions { display: flex; gap: 10px; margin-top: 5px; } #yt-dl-confirm { flex: 1; background-color: #3ea6ff; color: #fff; border: none; padding: 8px; border-radius: 6px; cursor: pointer; font-weight: 500; font-size: 14px; } #yt-dl-confirm:hover { background-color: #65b8ff; } #yt-dl-iframe-container { width: 100%; text-align: center; margin-top: 5px; min-height: 65px; overflow: hidden; display: flex; justify-content: center; } /* Mobile styles overrides */ .mobile-layout #yt-custom-download-btn { margin: 0 4px; height: 32px; padding: 0 12px; border-radius: 16px; } .mobile-layout #yt-download-popover { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); width: calc(100% - 40px); max-width: 400px; } `; document.head.appendChild(style); // Context tracking let popoverElement = null; function getCleanUrl() { // Strip out list and index parameters from URL let url = window.location.href; url = url.replace(/&list=[^&]*/g, ''); url = url.replace(/\?list=[^&]*&?/g, '?'); url = url.replace(/&index=[^&]*/g, ''); url = url.replace(/\?index=[^&]*&?/g, '?'); url = url.replace(/\?$/g, ''); return url; } function isDarkTheme() { return document.documentElement.hasAttribute('dark') || document.body.hasAttribute('dark') || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); } function createPopover() { if (popoverElement) return popoverElement; popoverElement = document.createElement('div'); popoverElement.id = 'yt-download-popover'; // Replace innerHTML with programmatic nodes to satisfy TrustedHTML policy // 1. Header const headerDiv = document.createElement('div'); headerDiv.className = 'popover-header'; const headerTitle = document.createElement('span'); headerTitle.textContent = 'Download Video'; const headerClose = document.createElement('button'); headerClose.id = 'yt-dl-close'; headerClose.textContent = '×'; headerDiv.appendChild(headerTitle); headerDiv.appendChild(headerClose); // 2. Format Select const formatSelect = document.createElement('select'); formatSelect.id = 'yt-dl-format'; const formats = [ { value: 'mp3', text: 'MP3 (Audio)' }, { value: 'm4a', text: 'M4A (Audio)' }, { value: '360', text: 'MP4 360p' }, { value: '480', text: 'MP4 480p' }, { value: '720', text: 'MP4 720p', selected: true }, { value: '1080', text: 'MP4 1080p' }, { value: '4k', text: 'WEBM 4K' }, { value: '8k', text: 'WEBM 8K' } ]; formats.forEach(f => { const opt = document.createElement('option'); opt.value = f.value; opt.textContent = f.text; if (f.selected) opt.selected = true; formatSelect.appendChild(opt); }); // 3. Actions Form const actionsDiv = document.createElement('div'); actionsDiv.className = 'yt-dl-actions'; const confirmBtn = document.createElement('button'); confirmBtn.id = 'yt-dl-confirm'; confirmBtn.textContent = 'Load Download Link'; actionsDiv.appendChild(confirmBtn); // 4. Removed iframeContainer since we open in a new tab // 5. Build Popover popoverElement.appendChild(headerDiv); popoverElement.appendChild(formatSelect); popoverElement.appendChild(actionsDiv); popoverElement.appendChild(actionsDiv); document.body.appendChild(popoverElement); // Event listeners (Using the elements we just created above) headerClose.addEventListener('click', () => { popoverElement.style.display = 'none'; }); confirmBtn.addEventListener('click', () => { const selectedFormat = formatSelect.value; const cleanUrl = getCleanUrl(); // Encode the cleaned url const encodedUrl = encodeURIComponent(cleanUrl); // UI State change confirmBtn.disabled = true; confirmBtn.style.backgroundColor = '#888'; confirmBtn.textContent = 'Starting...'; const originalText = 'Load Download Link'; const originalBg = ''; // reverts to css style function resetBtn() { confirmBtn.disabled = false; confirmBtn.style.backgroundColor = originalBg; confirmBtn.textContent = originalText; } // 1. Init Download Task try { if (typeof GM_xmlhttpRequest === 'undefined') { throw new Error('GM_xmlhttpRequest not granted.'); } GM_xmlhttpRequest({ method: 'GET', url: 'https://p.savenow.to/ajax/download.php?format=' + selectedFormat + '&url=' + encodedUrl, headers: { 'Origin': 'https://en.loader.to', 'Referer': 'https://en.loader.to/' }, onload: function (response) { let data; try { data = JSON.parse(response.responseText); } catch (e) { console.error("Failed to parse init response: ", response.responseText); confirmBtn.textContent = 'Parse Error'; setTimeout(resetBtn, 2000); return; } // Extract ID. Sometimes it's directly 'id', sometimes we extract from progress_url let taskId = data.id; if (!taskId && data.progress_url) { try { const urlObj = new URL(data.progress_url); taskId = urlObj.searchParams.get('id'); } catch (e) { } } if (!data.success || !taskId || data.text === 'Video too long / Livestream') { console.error('API Error or Video too long', data); confirmBtn.textContent = data.text ? 'Error: ' + data.text : 'API Error'; setTimeout(resetBtn, 3000); return; } confirmBtn.textContent = 'Initializing (0%)...'; // 2. Poll Progress const pollInterval = setInterval(() => { GM_xmlhttpRequest({ method: 'GET', url: 'https://p.savenow.to/api/progress?id=' + taskId, headers: { 'Origin': 'https://en.loader.to', 'Referer': 'https://en.loader.to/' }, onload: function (res) { let progData; try { progData = JSON.parse(res.responseText); } catch (e) { return; // ignore parse errors on polling, try next tick } if (progData.progress !== undefined) { const rawProgress = parseInt(progData.progress, 10); const currentProgress = isNaN(rawProgress) ? 0 : rawProgress; const pct = (currentProgress / 10).toFixed(1); const statusText = progData.text || 'Downloading'; if (currentProgress < 1000) { confirmBtn.textContent = statusText + ' (' + pct + '%)...'; } else { // 100% finished clearInterval(pollInterval); confirmBtn.textContent = 'Download Ready!'; confirmBtn.style.backgroundColor = '#4caf50'; // Green // Trigger native download using window.open if (progData.download_url) { window.open(progData.download_url, '_blank'); } setTimeout(resetBtn, 3000); } } }, onerror: function (e) { console.error('Polling error:', e); clearInterval(pollInterval); confirmBtn.textContent = 'Polling Error'; setTimeout(resetBtn, 2000); } }); }, 1500); // Poll every 1.5 seconds }, onerror: function (err) { console.error('Init error:', err); confirmBtn.textContent = 'Network Error'; setTimeout(resetBtn, 2000); } }); } catch (err) { console.error('Fatal Script Error:', err); confirmBtn.textContent = 'Script Error (Update Headers)'; setTimeout(resetBtn, 3000); } }); // Close when clicking outside of the popover document.addEventListener('click', (e) => { const btn = document.getElementById('yt-custom-download-btn'); if (popoverElement.style.display === 'flex' && !popoverElement.contains(e.target) && (!btn || !btn.contains(e.target))) { popoverElement.style.display = 'none'; } }); return popoverElement; } function positionPopover(button) { const isMobile = window.location.hostname === 'm.youtube.com'; const popover = createPopover(); // removed iframe container reset // Update theme class based on YouTube's current theme if (isDarkTheme()) { document.body.classList.add('yt-is-dark-theme'); } else { document.body.classList.remove('yt-is-dark-theme'); } if (isMobile) { document.body.classList.add('mobile-layout'); // CSS handles fixed bottom position for mobile } else { document.body.classList.remove('mobile-layout'); // Calculate position for desktop (relative to clicked button) const rect = button.getBoundingClientRect(); const popoverWidth = 250; // Estimated height of the popover menu const popoverHeight = 120; // Default: position below the button let topPos = rect.bottom + window.scrollY + 10; // If it would overflow the bottom of the screen, place it ABOVE the button instead if (rect.bottom + popoverHeight + 10 > window.innerHeight) { topPos = rect.top + window.scrollY - popoverHeight - 10; } popover.style.top = topPos + 'px'; // Center popover horizontally relative to the button let leftPos = rect.left + window.scrollX - (popoverWidth / 2) + (rect.width / 2); // Ensure popover doesn't overflow viewport horizontally (especially on Shorts right-aligned UI) if (leftPos + popoverWidth > window.innerWidth) { leftPos = window.innerWidth - popoverWidth - 20; } if (leftPos < 10) { leftPos = 10; } popover.style.left = leftPos + 'px'; } popover.style.display = 'flex'; } function injectButton() { // Only run on watch or shorts pages const isWatch = window.location.pathname.startsWith('/watch'); const isShorts = window.location.pathname.startsWith('/shorts'); if (!isWatch && !isShorts) return; const isMobile = window.location.hostname === 'm.youtube.com'; let targetElement = null; // More aggressive find: Check for actual layout presence const findTarget = (selectorList) => { const selectors = selectorList.split(',').map(s => s.trim()); for (const selector of selectors) { const elms = document.querySelectorAll(selector); const found = Array.from(elms).find(el => { const rect = el.getBoundingClientRect(); // In SPA, wait for element to have at least some height, // but don't be too strict on offsetParent return rect.height > 0 || el.childNodes.length > 0; }); if (found) return found; } return null; }; if (isShorts) { targetElement = findTarget(isMobile ? SELECTOR_SHORTS_MOBILE : SELECTOR_SHORTS_DESKTOP); } else { if (isMobile) { targetElement = findTarget(SELECTOR_MOBILE); } else { // PC Desktop: Target the primary button container, with multiple fallbacks targetElement = findTarget('ytd-watch-metadata #top-level-buttons-computed, ytd-video-primary-info-renderer #top-level-buttons-computed, #top-level-buttons-computed, #actions-inner #menu #top-level-buttons-computed'); // Final fallback for PC: look for the segmented like button container and inject into its parent menu if (!targetElement) { const likeBtn = document.querySelector('ytd-segmented-like-dislike-button-renderer'); if (likeBtn) { targetElement = likeBtn.closest('#top-level-buttons-computed') || likeBtn.parentElement; } } } } if (!targetElement) return; // Prevent duplicate injection, but handle infinite scroll for Shorts const existingBtn = document.getElementById('yt-custom-download-btn'); if (existingBtn) { if (isShorts && !targetElement.contains(existingBtn)) { // Button is attached to an old/inactive Short. Remove it so we inject into the new active one. existingBtn.remove(); } else { // Button is already exactly where it should be return; } } const btn = document.createElement('button'); btn.id = 'yt-custom-download-btn'; // Construct the button safely (No innerHTML) const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); svg.setAttribute('focusable', 'false'); svg.style.pointerEvents = 'none'; svg.style.display = 'block'; const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M17 18V19H6V18H17ZM16.5 11.4L15.8 10.7L12 14.4V4H11V14.4L7.2 10.6L6.5 11.3L11.5 16.3L16.5 11.4Z'); g.appendChild(path); svg.appendChild(g); const textSpan = document.createElement('span'); textSpan.className = 'btn-text'; textSpan.textContent = 'Download'; btn.appendChild(svg); btn.appendChild(textSpan); // Add tooltip for desktop if (!isMobile) { btn.title = 'Download Video'; } btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const popover = document.getElementById('yt-download-popover'); if (popover && popover.style.display === 'flex') { popover.style.display = 'none'; // Toggle off } else { positionPopover(btn); // Show } }); // Insert into YouTube DOM if (isShorts) { if (!isMobile) { // Desktop Shorts layout needs circular button btn.style.marginRight = '0'; btn.style.marginTop = '16px'; btn.style.width = '48px'; btn.style.height = '48px'; btn.style.borderRadius = '50%'; btn.style.padding = '0'; btn.style.backgroundColor = 'var(--yt-spec-badge-chip-background, rgba(0, 0, 0, 0.05))'; // Hide text on desktop shorts (stack of round icons) textSpan.style.display = 'none'; svg.style.marginRight = '0'; svg.style.width = '24px'; svg.style.height = '24px'; targetElement.appendChild(btn); } else { // Mobile Shorts layout: transparent, vertical layout btn.style.backgroundColor = 'transparent'; btn.style.marginRight = '0'; btn.style.marginTop = '16px'; btn.style.display = 'flex'; btn.style.flexDirection = 'column'; btn.style.justifyContent = 'center'; btn.style.alignItems = 'center'; svg.style.width = '28px'; svg.style.height = '28px'; svg.style.marginRight = '0'; textSpan.style.display = 'block'; // Ensure text is visible if overridden by media query textSpan.style.fontSize = '12px'; textSpan.style.marginTop = '4px'; textSpan.style.color = '#fff'; // Add right after the other vertical buttons targetElement.appendChild(btn); } } else if (isMobile) { targetElement.appendChild(btn); } else { // Desktop standard video: Insert before the first button in the top-level-buttons container if (targetElement.firstChild) { targetElement.insertBefore(btn, targetElement.firstChild); } else { targetElement.appendChild(btn); } } } // Use MutationObserver because YouTube is an SPA and loads comments/metadata asynchronously let domObserver = new MutationObserver((mutations) => { injectButton(); }); function init() { // Start observing DOM changes to inject button when target element appears domObserver.observe(document.body, { childList: true, subtree: true }); // Handle YouTube's SPA navigation events (cleanup and re-inject) const handleNav = () => { // 1. Hide and reset popover const popover = document.getElementById('yt-download-popover'); if (popover) { popover.style.display = 'none'; } // 2. Force remove old button const oldBtn = document.getElementById('yt-custom-download-btn'); if (oldBtn) { oldBtn.remove(); } // 3. Polling retry with longer duration (10s) let retries = 0; const poller = setInterval(() => { const btn = document.getElementById('yt-custom-download-btn'); if (btn || retries > 20) { clearInterval(poller); } else { injectButton(); retries++; } }, 500); }; window.addEventListener('yt-navigate-finish', handleNav); window.addEventListener('yt-page-data-updated', handleNav); // Initial run injectButton(); } // Initialize the script init(); })(); 网友解答:


--【壹】--:

谢谢!已私信给你。

感觉大概率是三方解析的问题。跟第一天下载成功的状态完全不同呢。


--【贰】--:

啥原理?第三方解析没有遥测的?代码太长了。。


--【叁】--:

感谢佬友分享油管下载油猴脚本,已经安装正在测试,就是第三方解析这里,时间的确有点久


--【肆】--:

这种没办法的,原站解析还有广告,除非找到更好的解析网站,目前为止这个应该是最稳定的。


--【伍】--:

有没有具体的视频链接呢?我试试,之前测试没问题,如果出现这种,可能是解析站故障


--【陆】--:

调用第三方解析网站,相当于去广告集成油管。


--【柒】--:

大佬厉害,可以使用了,虽然解析有点慢,但是能用就行,终于可以卸载idm了


--【捌】--:

佬友好,今天似乎一直不成功,读条进度到2.5%之后,就提示可以下载,然后无下文…是否跟调用的第三方解析接口那里出问题了?


--【玖】--:

我想问一下,就是有没有关于那些付费订阅内容也能够看的脚本嘞


--【拾】--:

这种就不清楚了,没接触过,收费内容除非别人分享,不然光靠脚本是解析不了的。