youtube视频下载插件油猴版(修复第一次观看未加载下载按钮)
- 内容介绍
- 文章标签
- 相关推荐
此次更新修复了第一次打开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%之后,就提示可以下载,然后无下文…是否跟调用的第三方解析接口那里出问题了?
--【玖】--:
我想问一下,就是有没有关于那些付费订阅内容也能够看的脚本嘞
--【拾】--:
这种就不清楚了,没接触过,收费内容除非别人分享,不然光靠脚本是解析不了的。

