- // ==UserScript==
- // @name Youtube Direct Downloader
- // @version 2.1.6
- // @description Video/short download button hidden in three dots combo menu below video or next to subscribe button. 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
- // @supportURL https://github.com/FawayTT/userscripts/issues
- // @icon https://github.com/FawayTT/userscripts/blob/main/youtube-downloader-icon.png?raw=true
- // @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',
- buttonDownloadInfo: 'onchange',
- isAudioMuted: false,
- disableMetadata: false,
- redirectShorts: false,
- backupProvider: 'y2mate',
- subscribeButton: true,
- };
-
- const providers = ['cobalt', 'y2mate', 'yt1s'];
-
- let gmc = new GM_config({
- id: 'config',
- title: 'Youtube direct downloader settings',
- fields: {
- subscribeButton: {
- section: ['Position of download button:'],
- label: 'Show download button next to subscribe button:',
- labelPos: 'left',
- type: 'checkbox',
- default: defaults.subscribeButton,
- },
- 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'],
- },
- vCodec: {
- 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'],
- },
- aFormat: {
- label: 'Audio format:',
- type: 'select',
- default: defaults.aFormat,
- options: ['best', 'mp3', 'ogg', 'wav', 'opus'],
- },
- 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'],
- },
- buttonDownloadInfo: {
- label: 'Show quality info below button:',
- type: 'select',
- default: defaults.buttonDownloadInfo,
- options: ['always', 'onchange', 'never'],
- },
- backupProvider: {
- label: 'Backup provider in case Cobalt is not responding:',
- type: 'select',
- default: defaults.backupProvider,
- options: ['y2mate', 'yt1s', 'none'],
- },
- 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();
- deleteButton();
- createButton();
- deleteSubscribeButton();
- createSubscribeButton();
- },
- 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 menuOuter;
- let menuParent;
- let nextSibling;
- let oldHref = document.location.href;
- let menuIndex = 1;
- const menuMaxTries = 10;
-
- function getYouTubeVideoID(url) {
- const urlParams = new URLSearchParams(new URL(url).search);
- return urlParams.get('v');
- }
-
- function download(isAudioOnly, downloadService) {
- if (!downloadService) downloadService = gmc.get('downloadService');
- switch (downloadService) {
- case 'y2mate':
- if (isAudioOnly) 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 (isAudioOnly) 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;
- case 'cobalt':
- 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('vCodec'),
- aFormat: gmc.get('aFormat'),
- filenamePattern: gmc.get('filenamePattern'),
- isAudioMuted: gmc.get('isAudioMuted'),
- disableMetadata: gmc.get('disableMetadata'),
- isAudioOnly: isAudioOnly,
- }),
- onload: (response) => {
- const data = JSON.parse(response.responseText);
- if (data.url) window.open(data.url);
- else {
- let alertText = 'Cobalt error: ' + data.text || 'Something went wrong! Try again later.';
- const backupProvider = gmc.get('backupProvider');
- if (backupProvider !== 'none') {
- alertText += '\n\nYou will be redirected to backup provider ' + backupProvider + '.';
- alert(alertText);
- download(isAudioOnly, backupProvider);
- } else alert(alertText);
- }
- },
- onerror: function (error) {
- const errorMessage = error.message || error;
- let alertText = 'Cobalt error occurred: ' + errorMessage;
- const backupProvider = gmc.get('backupProvider');
- if (backupProvider !== 'none') {
- alertText += '\n\nYou will be redirected to backup provider ' + backupProvider + '.';
- alert(alertText);
- download(isAudioOnly, backupProvider);
- } else alert(alertText);
- },
- ontimeout: function () {
- let alertText = 'Cobalt is not responding. Please try again later.';
- const backupProvider = gmc.get('backupProvider');
- if (backupProvider !== 'none') {
- alertText += '\n\nYou will be redirected to backup provider ' + backupProvider + '.';
- alert(alertText);
- download(isAudioOnly, backupProvider);
- } else alert(alertText);
- },
- });
- break;
- default:
- break;
- }
- hideMenu();
- }
-
- function addButtonDownloadInfo(serviceName, div) {
- if (serviceName === 'cobalt') {
- const option = gmc.get('buttonDownloadInfo');
- if (option === 'never') return;
- const quality = gmc.get('quality') || defaults.quality;
- const vCodec = gmc.get('vCodec') || defaults.vCodec;
- if (option === 'onchange' && quality === defaults.quality && vCodec === defaults.vCodec) return;
- const qualityText = `${quality}, ${vCodec}`;
- const downloadInfo = document.createElement('custom-dwn-button-download-info');
- downloadInfo.style.cssText = `
- position: absolute;
- left: 0;
- bottom: -10px;
- font-size: 0.8rem;
- color: var(--yt-spec-text-primary);
- opacity: 0.6;`;
- downloadInfo.innerText = qualityText;
- div.appendChild(downloadInfo);
- }
- }
-
- function deleteButton() {
- const buttons = document.getElementsByTagName('custom-dwn-button');
- if (buttons.length === 0) return;
- const button = buttons[0];
- button.remove();
- }
-
- function hideMenu() {
- const menu = document.getElementsByTagName('ytd-menu-popup-renderer')[0];
- if (!menu) return;
- menuOuter = menu.parentElement.parentElement;
- if (!menuOuter) return;
- menuParent = menuOuter.parentNode;
- nextSibling = menuOuter.nextSibling;
- menuOuter.remove();
- }
-
- function addMenu(hidden) {
- if (menuOuter && menuParent) {
- if (nextSibling) {
- menuParent.insertBefore(menuOuter, nextSibling);
- } else {
- menuParent.appendChild(menuOuter);
- }
- if (hidden) menuOuter.style.display = 'none';
- menuOuter = null;
- menuParent = null;
- nextSibling = null;
- }
- }
-
- function createButton() {
- addMenu();
- if (document.getElementsByTagName('custom-dwn-button').length !== 0) return;
- const serviceName = gmc.get('downloadService') || defaults.downloadService;
- 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('custom-dwn-button-text');
- const downButton = document.createElement('button');
- const extra = document.createElement('div');
- const settings = document.createElement('div');
- const downAudioOnly = document.createElement('div');
- menu.style.minHeight = '100px';
- menu.style.minWidth = '133px';
- text.style.position = 'relative';
- 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;
- text-transform: capitalize`;
- 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.style.cssText = `
- font-size: 2.1rem;`;
- icon.innerText = '⇩';
- text.innerText = serviceName;
- settings.innerText = '☰';
- downAudioOnly.innerText = '▶';
- addButtonDownloadInfo(serviceName, text);
- downAudioOnly.title = `Download audio only`;
- settings.title = 'Settings';
- downButtonOuter.appendChild(icon);
- downButtonOuter.appendChild(text);
- downButtonOuter.appendChild(extra);
- downButtonOuter.appendChild(downButton);
- extra.appendChild(settings);
- extra.appendChild(downAudioOnly);
- downButton.addEventListener('click', () => {
- download();
- downButtonOuter.style.backgroundColor = '';
- });
-
- downAudioOnly.addEventListener('click', () => {
- download(true);
- downButtonOuter.style.backgroundColor = '';
- });
- 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 deleteSubscribeButton() {
- const button = document.getElementById('custom-dwn-button-sub');
- if (!button) return;
- button.remove();
- }
-
- function createSubscribeButton() {
- if (!gmc.get('subscribeButton')) return;
- const ownerBar = document.getElementById('owner');
- if (!ownerBar) return;
- const downButton = document.createElement('button');
- downButton.id = 'custom-dwn-button-sub';
- downButton.style.cssText = `
- cursor: pointer;
- font-size: 2rem;
- padding: 8px 12px;
- border: none;
- border-radius: 15px;
- margin-left: 8px;
- line-height: 2rem;
- font-weight: 500;
- color: #0f0f0f;
- backgroundColor: #f1f1f1;
- font-family: "Roboto","Arial",sans-serif;
- align-items: center;
- text-transform: capitalize;`;
- ownerBar.style.position = 'relative';
- ownerBar.appendChild(downButton);
- downButton.title = 'Download via ' + gmc.get('downloadService');
- downButton.innerText = '⇩';
- downButton.addEventListener('click', () => {
- download();
- });
- }
-
- function watchMenu() {
- menuIndex += 1;
- menuOuter = null;
- menuParent = null;
- nextSibling = null;
- if (menuMaxTries < menuIndex) {
- menuIndex = 1;
- clearTimeout(timeout);
- return;
- }
- if (document.location.href.indexOf('youtube.com/shorts') > -1) {
- const menuBtn = document.getElementById('menu-button');
- if (!menuBtn) {
- timeout = setTimeout(watchMenu, 500 * menuIndex);
- return;
- }
- menuBtn.addEventListener('click', createButton);
- menuIndex = 1;
- clearTimeout(timeout);
- return;
- }
- const topRow = document.getElementById('top-row');
- const menuBtn = topRow.querySelector('#button-shape');
- createSubscribeButton();
- if (!topRow || !menuBtn) {
- timeout = setTimeout(() => {
- watchMenu();
- }, 500 * menuIndex);
- return;
- }
- menuBtn.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;
- addMenu(true);
- modifyMenu();
- }
- });
- });
- observer.observe(bodyList, {
- childList: true,
- subtree: true,
- });
- }