// ==UserScript==
// @name Youtube direct downloader
// @version 2.1.1
// @description Video/short download button hidden in three dots combo menu below video. Downloads MP4, WEBM or MP3 from youtube + option to redirect shorts to normal videos. Choose your preferred quality from 8k to audio only, codec (h264, vp9 or av1) or service provider (cobalt, y2mate, yt1s) in settings.
// @author FawayTT
// @namespace FawayTT
// @icon https://i.imgur.com/D57wQrY.png
// @match https://www.youtube.com/*
// @connect api.cobalt.tools
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
GM_registerMenuCommand('Settings', opencfg);
const defaults = {
downloadService: 'cobalt',
quality: 'max',
vCodec: 'vp9',
aFormat: 'mp3',
filenamePattern: 'pretty',
isAudioMuted: false,
disableMetadata: false,
audioOnly: false,
redirectShorts: false,
};
let gmc = new GM_config({
id: 'config',
title: 'Youtube direct downloader settings',
fields: {
downloadService: {
section: ['Download method (use cobalt for best quality):'],
label: 'Service',
labelPos: 'left',
type: 'select',
default: defaults.downloadService,
options: ['cobalt', 'y2mate', 'yt1s'],
},
quality: {
section: ['Cobalt-only settings'],
label: 'Quality:',
labelPos: 'left',
type: 'select',
default: defaults.quality,
options: ['max', '2160', '1440', '1080', '720', '480', '360', '240', '144'],
},
videoCodec: {
label: 'Video codec (h264 [MP4] for best compatibility, vp9 [WEBM] for better quality. AV1 = best quality but is used only by few videos):',
labelPos: 'left',
type: 'select',
default: defaults.vCodec,
options: ['h264', 'vp9', 'av1'],
},
audioFormat: {
label: 'Audio format:',
type: 'select',
default: defaults.aFormat,
options: ['best', 'mp3', 'ogg', 'wav', 'opus'],
},
audioOnly: {
label: 'Always download only audio:',
type: 'checkbox',
default: defaults.audioOnly,
},
isAudioMuted: {
label: 'Download videos without audio:',
type: 'checkbox',
default: defaults.isAudioMuted,
},
disableMetadata: {
label: 'Download videos without metadata:',
type: 'checkbox',
default: defaults.disableMetadata,
},
filenamePattern: {
label: 'Filename pattern:',
type: 'select',
default: defaults.filenamePattern,
options: ['classic', 'pretty', 'basic', 'nerdy', 'opus'],
},
redirectShorts: {
section: ['Extra features'],
label: 'Redirect shorts:',
labelPos: 'left',
type: 'checkbox',
default: defaults.redirectShorts,
},
url: {
section: ['Links'],
label: 'My other userscripts',
type: 'button',
click: () => {
GM_openInTab('https://github.com/FawayTT/userscripts');
},
},
cobaltUrl: {
label: 'Cobalt',
type: 'button',
click: () => {
GM_openInTab('https://github.com/imputnet/cobalt');
},
},
},
events: {
save: function () {
gmc.close();
},
init: onInit,
},
});
function opencfg() {
gmc.open();
config.style = `
width: 100%;
height: 100%;
max-height: 40rem;
max-width: 80rem;
border-radius: 10px;
z-index: 9999999;
position: fixed;
`;
}
let timeout;
let oldHref = document.location.href;
let menuIndex = 1;
let menuMaxTries = 10;
function getYouTubeVideoID(url) {
const urlParams = new URLSearchParams(new URL(url).search);
return urlParams.get('v');
}
function download(audioOnly) {
switch (gmc.get('downloadService')) {
case 'y2mate':
if (audioOnly) window.open(`https://www.y2mate.com/youtube-mp3/${getYouTubeVideoID(document.location.href)}`);
else window.open(`https://www.y2mate.com/download-youtube/${getYouTubeVideoID(document.location.href)}`);
break;
case 'yt1s':
if (audioOnly) window.open(`https://www.yt1s.com/en/youtube-to-mp3?q=${getYouTubeVideoID(document.location.href)}`);
else window.open(`https://www.yt1s.com/en/youtube-to-mp4?q=${getYouTubeVideoID(document.location.href)}`);
break;
default:
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.cobalt.tools/api/json',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
data: JSON.stringify({
url: encodeURI(document.location.href),
vQuality: gmc.get('quality'),
vCodec: gmc.get('videoCodec'),
aFormat: gmc.get('audioFormat'),
filenamePattern: gmc.get('filenamePattern'),
isAudioMuted: gmc.get('isAudioMuted'),
disableMetadata: gmc.get('disableMetadata'),
isAudioOnly: audioOnly || gmc.get('audioOnly'),
}),
onload: (response) => {
const data = JSON.parse(response.responseText);
if (data.url) window.open(data.url);
},
});
break;
}
}
function capitalize(s){
return s[0].toUpperCase() + s.slice(1);
}
function createButton() {
if (document.getElementsByTagName('custom-dwn-button').length !== 0) return;
const menu = document.getElementsByTagName('ytd-menu-popup-renderer')[0];
const downButtonOuter = document.createElement('custom-dwn-button');
const icon = document.createElement('div');
const text = document.createElement('div');
const downButton = document.createElement('button');
const extra = document.createElement('div');
const settings = document.createElement('div');
const downAudioOnly = document.createElement('div');
downAudioOnly.title = 'Download audio only';
settings.title = 'Settings';
menu.style.minHeight = '100px';
menu.style.minWidth = '150px';
downButtonOuter.style.cssText = `
cursor: pointer;
margin-top: 8px;
font-size: 1.4rem;
line-height: 2rem;
font-weight: 400;
position: relative;
color: var(--yt-spec-text-primary);
font-family: "Roboto","Arial",sans-serif;
white-space: nowrap;
display: flex;
margin-bottom: -10px;
padding: 10px 0 10px 21px;
gap: 23px;
align-items: center;`;
downButton.style.cssText = `
position: absolute;
left: 0;
top: 0;
width: 90%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 9999;`;
extra.style.cssText = `
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
padding: 1px;
color: var(--yt-spec-text-primary);
z-index: 9999;
right: 0;
top: 0;
width: 10%;
height: 90%;`;
icon.innerText = '⇩';
const serviceName = gmc.get('downloadService') || 'Cobalt';
text.innerText = capitalize(serviceName);
settings.innerText = '☰';
downAudioOnly.innerText = '▶';
icon.style.cssText = `
font-size: 2.1rem;`;
downButtonOuter.appendChild(icon);
downButtonOuter.appendChild(text);
downButtonOuter.appendChild(extra);
downButtonOuter.appendChild(downButton);
extra.appendChild(settings);
extra.appendChild(downAudioOnly);
downButton.addEventListener('click', () => {
download();
});
downAudioOnly.addEventListener('click', () => {
download(true);
});
settings.addEventListener('click', opencfg);
downButtonOuter.addEventListener('mouseenter', () => {
downButtonOuter.style.backgroundColor = 'var(--yt-spec-10-percent-layer)';
});
downButtonOuter.addEventListener('mouseleave', () => {
downButtonOuter.style.backgroundColor = '';
});
menu.insertBefore(downButtonOuter, menu.firstChild);
}
function watchMenu() {
menuIndex += 1;
if (menuMaxTries < menuIndex) {
menuIndex = 1;
clearTimeout(timeout);
return;
}
if (document.location.href.indexOf('youtube.com/shorts') > -1) {
const menu = document.getElementById('menu-button');
if (!menu) {
timeout = setTimeout(watchMenu, 500 * menuIndex);
return;
}
menu.addEventListener('click', createButton);
menuIndex = 1;
clearTimeout(timeout);
return;
}
const topRow = document.getElementById('top-row');
const menu = topRow.querySelector('#button-shape');
if (!topRow || !menu) {
timeout = setTimeout(() => {
watchMenu(false);
}, 500 * menuIndex);
return;
}
menu.addEventListener('click', createButton);
menuIndex = 1;
clearTimeout(timeout);
}
function modifyMenu() {
if (document.location.href.indexOf('youtube.com/watch') === -1 && document.location.href.indexOf('youtube.com/shorts') === -1) return;
if (document.hidden) {
window.addEventListener('visibilitychange', () => {
if (document.hidden) return;
timeout = setTimeout(watchMenu, 500 * menuIndex);
});
} else timeout = setTimeout(watchMenu, 500 * menuIndex);
}
function checkShort() {
if (document.location.href.indexOf('youtube.com/shorts') > -1 && gmc.get('redirectShorts')) window.location.replace(window.location.toString().replace('/shorts/', '/watch?v='));
}
function onInit() {
const bodyList = document.querySelector('body');
checkShort();
modifyMenu();
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (oldHref != document.location.href) {
checkShort();
oldHref = document.location.href;
modifyMenu();
}
});
});
observer.observe(bodyList, {
childList: true,
subtree: true,
});
}