Adds a Download button to YouTube for MP4 and DASH formats with optional stream selection.
当前为
// ==UserScript==
// @name YouSnatch
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Adds a Download button to YouTube for MP4 and DASH formats with optional stream selection.
// @author JourneysFootpath
// @match https://www.youtube.com/watch*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let advancedMode = false;
let injected = false;
document.addEventListener('DOMContentLoaded', () => {
const playerResponse = unsafeWindow.ytInitialPlayerResponse;
const actions = document.querySelector('#top-level-buttons-computed');
if (playerResponse && actions && !injected) {
injected = true;
addToggle(actions);
renderButton(playerResponse, actions);
}
});
function addToggle(container) {
const toggle = createButton('Switch to Advanced Mode', 'yt-spec-button-shape-next', () => {
advancedMode = !advancedMode;
toggle.innerText = advancedMode ? 'Switch to Basic Mode' : 'Switch to Advanced Mode';
const wrapper = document.getElementById('yt-download-wrapper');
if (wrapper) wrapper.remove();
renderButton(unsafeWindow.ytInitialPlayerResponse, container);
});
container.appendChild(toggle);
}
function createButton(text, className, onClick) {
const button = document.createElement('button');
button.innerText = text;
button.className = className;
button.style.marginLeft = '8px';
button.onclick = onClick;
return button;
}
function renderButton(playerResponse, container) {
const wrapper = document.createElement('div');
wrapper.id = 'yt-download-wrapper';
wrapper.style.marginLeft = '8px';
if (!advancedMode) {
const format = getBestFormat(playerResponse);
if (format) {
const button = createDownloadButton(format.url, `⬇️ Download (${format.qualityLabel})`);
wrapper.appendChild(button);
} else {
wrapper.innerText = 'No MP4 stream available';
}
} else {
const { videoOptions, audioOptions } = getDASHOptions(playerResponse);
if (videoOptions.length || audioOptions.length) {
const videoSelect = createSelectElement(videoOptions, '🎥 Video: ');
const audioSelect = createSelectElement(audioOptions, '🔊 Audio: ');
const videoLink = createDownloadLink('⬇️ Download Video', videoSelect);
const audioLink = createDownloadLink('⬇️ Download Audio', audioSelect);
wrapper.appendChild(videoSelect.label);
wrapper.appendChild(videoSelect.element);
wrapper.appendChild(document.createElement('br'));
wrapper.appendChild(audioSelect.label);
wrapper.appendChild(audioSelect.element);
wrapper.appendChild(document.createElement('br'));
wrapper.appendChild(videoLink);
wrapper.appendChild(audioLink);
} else {
wrapper.innerText = 'No DASH streams available';
}
}
container.appendChild(wrapper);
}
function createDownloadButton(url, text) {
const button = document.createElement('a');
button.href = url;
button.innerText = text;
button.download = '';
button.target = '_blank';
applyButtonStyles(button);
return button;
}
function createDownloadLink(text, selectElement) {
const link = document.createElement('a');
link.innerText = text;
link.download = '';
link.target = '_blank';
link.style.margin = '4px';
link.style.display = 'inline-block';
link.style.background = '#2196F3';
link.style.color = '#fff';
link.style.padding = '4px 10px';
link.style.borderRadius = '4px';
link.style.textDecoration = 'none';
selectElement.onchange = () => link.href = selectElement.value;
link.href = selectElement.value; // Set initial value
return link;
}
function applyButtonStyles(button) {
button.style.padding = '6px 12px';
button.style.background = '#4CAF50';
button.style.color = '#fff';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.cursor = 'pointer';
}
function createSelectElement(options, labelText) {
const select = document.createElement('select');
const label = document.createElement('label');
label.innerText = labelText;
options.forEach(f => {
const opt = document.createElement('option');
opt.value = f.url;
opt.text = `${f.qualityLabel || f.audioQuality}`;
select.appendChild(opt);
});
return { label, element: select };
}
function getBestFormat(playerResponse) {
return (playerResponse.streamingData?.formats || []).find(f =>
f.mimeType.includes('video/mp4') && f.qualityLabel && f.url);
}
function getDASHOptions(playerResponse) {
const adaptive = playerResponse.streamingData?.adaptiveFormats || [];
const videoOptions = adaptive.filter(f => f.mimeType.includes('video/mp4') && f.qualityLabel);
const audioOptions = adaptive.filter(f => f.mimeType.includes('audio/mp4'));
return { videoOptions, audioOptions };
}
})();