YouTube Direct Downloader

Video/short download button next to subscribe button. Downloads MP4, WEBM, MP3 or subtitles 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, yt5s) in settings.

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name                YouTube Direct Downloader
// @version             4.3.0
// @description         Video/short download button next to subscribe button. Downloads MP4, WEBM, MP3 or subtitles 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, yt5s) in settings.
// @author              FawayTT
// @namespace           FawayTT
// @supportURL          https://github.com/FawayTT/userscripts/issues
// @icon                https://github.com/FawayTT/userscripts/blob/main/ydd-icon.png?raw=true
// @match               https://www.youtube.com/*
// @match               https://yt5s.biz/*
// @match               https://cobalt.tools/*
// @match               https://5smp3.com/*
// @connect             cobalt-api.kwiatekmiki.com
// @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==

const gmcCSS = `
 #YDD_config {
  background-color: rgba(0, 0, 0, 0.8) !important;
  backdrop-filter: blur(10px);
  color: #fff !important;
  border-radius: 30px !important;
  padding: 20px !important;
  height: fit-content !important;
  max-width: 700px !important;
  font-family: Arial, sans-serif !important;
  z-index: 9999999 !important;
  padding-bottom: 0px !important;
  width: 100% !important;
}

#YDD_config_header {
  background-color: #ff000052 !important;
  border-radius: 10px;
  padding: 10px !important;
  text-align: center !important;
  font-size: 24px !important;
  color: blob !important;
  font-weight: 600 !important;
}

.section_header_holder {
  font-weight: 600;
  margin-top: 10px !important;
}

#YDD_config_buttons_holder {
  text-align: center;
  margin-top: 20px;
}

#YDD_config_resetLink {
  color: #fff !important;
}

.config_var {
  margin: 0px !important;
  line-height: 3;
}

#YDD_config_buttons_holder button {
  background-color: #ff000052 !important;
  color: #fff;
  border: none;
  font-weight: 600;
  padding: 10px 20px !important;
  border-radius: 10px;
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.1s ease-in;
}

#YDD_config_buttons_holder button:hover {
  background-color: #ff0000 !important;
}

#YDD_config_fieldset {
  border: 1px solid #444;
  padding: 10px;
  border-radius: 5px;
  margin-top: 10px;
}

#YDD_config_fieldset legend {
  color: #ff0000;
}

 #YDD_config .section_header {
  background: none !important;
  width: fit-content;
  margin: 5px 0px !important;
  border: none !important;
  font-size: 18px !important;
  color: #ff0000 !important;
}

 #YDD_config input, select, textarea {
  cursor: pointer;
  background-color: #333;
  color: #fff;
  border: 1px solid #555;
  border-radius: 10px;
  padding: 5px;
  margin: 5px 0 !important;
}

 #YDD_config ::selection {
  color: white;
  background: #ff0000;
}

 #YDD_config input, select, textarea {
  transition: all 0.1s ease-in;
}

 #YDD_config input:focus, select:focus, textarea:focus {
  border-color: #ff0000;
}

 #YDD_config input:hover, select:hover, textarea:hover {
  opacity: 0.8;
}

 #YDD_config label {
  color: #fff;
}

#YDD_config_buttons_holder {
  position: relative;
  margin-top: 0px !important;
  display: flex;
  justify-content: center;
  align-items: baseline;
}

.reset_holder {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: 0;
  padding: 10px;
}

input[type='checkbox'] {
  appearance: none;
  position: absolute;
  width: 20px;
  transform: translateY(3px);
  height: 20px;
  border: 1px solid #555;
  border-radius: 10px;
  background-color: #333;
  cursor: pointer;
}

input[type='checkbox']:before {
  content: " ";
}

input[type='checkbox']:checked {
  background-color: #d50707;
}

input[type='checkbox']:checked::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 2px;
  width: 12px;
  height: 6px;
  border-bottom: 2px solid #ffffff;
  border-left: 2px solid #ffffff;
  transform: rotate(-45deg);
}

#YDD_config_downloadService_var:after {
  content: "▲ Use cobalt_api for direct download.";
  display: block;
  font-family: arial, tahoma, myriad pro, sans-serif;
  font-size: 10px;
  font-weight: bold;
  margin-right: 6px;
  opacity: 0.7;
}

#YDD_config_vCodec_var:after {
  content: "▲ H264 [MP4] = best compatibility. VP9 [WEBM] = better quality. AV1 = best quality but is used only by few videos.";
  display: block;
  font-family: arial, tahoma, myriad pro, sans-serif;
  font-size: 10px;
  font-weight: bold;
  margin-right: 6px;
  opacity: 0.7;
}

#YDD_config_backupService_var:after {
  content: "▲ Available via right click or used automatically when direct download is not working.";
  display: block;
  font-family: arial, tahoma, myriad pro, sans-serif;
  font-size: 10px;
  font-weight: bold;
  margin-right: 6px;
  opacity: 0.7;
}
`;

const yddCSS = `
#experiment-overlay {
  overflow: visible !important;
}

#ydd-button {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.15);
  border-radius: 15px;
  margin-left: 8px;
  box-shadow: 1px 0px 7px -4px rgba(0, 0, 0, 0.8);
}

#ydd-download {
  position: relative;
  z-index: 10;
  cursor: pointer;
  font-size: 2rem;
  padding: 8px 12px;
  border: none;
  border-radius: 15px;
  line-height: 2rem;
  font-weight: 500;
  color: #0f0f0f;
  background-color: #f1f1f1;
  font-family: "Roboto","Arial",sans-serif;
  align-items: center;
  text-transform: capitalize;
}

#ydd-download:hover {
  filter: brightness(90%);
}

#ydd-options {
  line-height: 2rem;
  font-weight: 500;
  color: var(--yt-spec-text-primary);
  margin: 0px 5px;
  cursor: pointer;
  font-family: "Roboto","Arial",sans-serif;
  transition: all 0.1s ease-in;
}

#ydd-options:hover {
  scale: 1.4;
}

#ydd-options-div {
  transition: transform 0.3s ease, opacity 0.1s ease;
  position: absolute;
  top: 0px;
  transform: translateY(-70%);
  right: -24px;
  background: rgba(255, 255, 255, 0.9);
  backdrop-filter: blur(10px);
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
  border-radius: 15px;
  margin-right: 8px;
  margin-top: 6px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  z-index: 9999999;
  font-family: "Roboto","Arial",sans-serif;
  font-weight: 500;
  font-size: 1.2rem;
  line-height: 2rem;
  color: black;
  align-items: start;
  white-space: nowrap;
}

#ydd-options-div > * {
  margin: 6px;
  cursor: pointer;
  transition: all 0.1s ease-in;
}

#ydd-options-div > *:hover {
  scale: 1.1;
}

@keyframes scaleIn {
  0% {
    transform: scale(0.8) translateY(-60%);
    opacity: 0;
  }
  100% {
    transform: scale(1) translateY(-70%);
    opacity: 1;
  }
}

@keyframes scaleOut {
  0% {
    transform: scale(1) translateY(-70%);
    opacity: 1;
  }
  100% {
    transform: scale(0.8) translateY(-60%);
    opacity: 0;
  }
}

.ydd-scale-up {
  animation: scaleIn 0.3s forwards;
}

.ydd-scale-down {
  animation: scaleOut 0.3s forwards;
}
`;

GM_registerMenuCommand('Settings', opencfg);

const defaults = {
  downloadService: 'cobalt_api',
  quality: 'max',
  vCodec: 'av1',
  aFormat: 'mp3',
  filenamePattern: 'basic',
  isAudioMuted: false,
  disableMetadata: false,
  redirectShorts: false,
  backupService: 'cobalt_web',
  showCobaltError: false,
};

let frame = document.createElement('div');
document.body.appendChild(frame);
let cobaltTries = 0;

let gmc = new GM_config({
  id: 'YDD_config',
  title: 'YouTube Direct Downloader (YDD) - Settings',
  css: gmcCSS,
  frame: frame,
  fields: {
    downloadService: {
      section: ['Download method'],
      label: 'Main service:',
      labelPos: 'left',
      type: 'select',
      default: defaults.downloadService,
      options: ['cobalt_api', 'cobalt_web', 'yt5s', 'y2mate', 'yt1s'],
    },
    backupService: {
      label: 'Backup service:',
      type: 'select',
      default: defaults.backupService,
      options: ['cobalt_web', 'cobalt_api', 'y2mate', 'yt5s', 'yt1s', 'none'],
    },
    quality: {
      section: ['Cobalt API settings'],
      label: 'Quality:',
      labelPos: 'left',
      type: 'select',
      default: defaults.quality,
      options: ['max', '2160', '1440', '1080', '720', '480', '360', '240', '144'],
    },
    vCodec: {
      label: 'Video codec:',
      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'],
    },
    showCobaltError: {
      label: 'Show error messages:',
      type: 'checkbox',
      default: defaults.showCobaltError,
    },
    redirectShorts: {
      section: ['Extra features'],
      label: 'Redirect shorts:',
      labelPos: 'left',
      type: 'checkbox',
      default: defaults.redirectShorts,
    },
    url: {
      section: ['Links'],
      label: 'Creator of this script - FawayTT',
      type: 'button',
      click: () => {
        GM_openInTab('https://github.com/FawayTT/userscripts');
      },
    },
    cobaltUrl: {
      label: 'Cobalt github page',
      type: 'button',
      click: () => {
        GM_openInTab('https://github.com/imputnet/cobalt');
      },
    },
    cobaltInstance: {
      label: 'Cobalt instance provider',
      type: 'button',
      click: () => {
        GM_openInTab('kwiatekmiki.com');
      },
    },
  },
  events: {
    save: function () {
      gmc.close();
      deleteButtons();
      modify();
    },
    init: onInit,
  },
});

function opencfg() {
  gmc.open();
}

let oldHref = document.location.href;
let yddAdded = false;
let dError;
let dTimeout;

function getHeaders() {
  const userAgent = navigator.userAgent;
  const refererHeader = window.location.href;
  const originHeader = window.location.origin;
  const languages = navigator.languages;
  return {
    'User-Agent': userAgent,
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'Accept-Language': languages,
    Referer: refererHeader,
    Origin: originHeader,
  };
}

function getYouTubeVideoID(url) {
  if (url.includes('shorts')) {
    const regex = /\/shorts\/([^/?]+)/;
    const match = url.match(regex);
    const id = match ? match[1] : null;
    return id;
  }
  const urlParams = new URLSearchParams(new URL(url).search);
  return urlParams.get('v');
}

function handleCobaltError(errorMessage, isAudioOnly) {
  const backupService = gmc.get('backupService') || defaults.backupService;
  const showError = gmc.get('showCobaltError');
  if (!showError && backupService !== 'cobalt_api') {
    download(isAudioOnly, backupService);
    return;
  }
  let alertText = 'Cobalt error: ' + (errorMessage || 'Something went wrong! Try again later.');
  if (backupService !== 'none' && backupService !== 'cobalt_api') {
    alertText += '\n\nYou will be redirected to backup provider ' + backupService + '.';
    alert(alertText);
    download(isAudioOnly, backupService);
  } else alert(alertText);
}

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 'yt5s':
      if (isAudioOnly) window.open('https://5smp3.com/?ydd=' + encodeURI(document.location.href));
      else window.open('https://yt5s.biz/enxj100/?ydd=' + encodeURI(document.location.href));
      break;
    case 'cobalt_web':
      if (isAudioOnly) window.open('https://cobalt.tools/?ydd=' + encodeURI(document.location.href) + '&audioOnly=true');
      else window.open('https://cobalt.tools/?ydd=' + encodeURI(document.location.href));
      break;
    default:
      if (dError) return handleCobaltError(dError, isAudioOnly);
      GM_xmlhttpRequest({
        method: 'POST',
        url: 'https://cobalt-api.kwiatekmiki.com',
        headers: getHeaders(),
        data: JSON.stringify({
          url: encodeURI(document.location.href),
          videoQuality: gmc.get('quality'),
          youtubeVideoCodec: gmc.get('vCodec'),
          audioFormat: gmc.get('aFormat'),
          filenameStyle: gmc.get('filenamePattern'),
          disableMetadata: gmc.get('disableMetadata'),
          downloadMode: isAudioOnly ? 'audio' : `${gmc.get('isAudioMuted') ? 'muted' : 'auto'}`,
        }),
        onload: (response) => {
          const data = response.responseText && JSON.parse(response.responseText);
          if (response.status === 200) {
            if (data.url) window.open(data.url);
            else handleCobaltError('Cobalt is not sending expected response.', isAudioOnly);
          } else if (response.status === 403) {
            handleCobaltError('Cobalt is blocking your request with Bot Protection.', isAudioOnly);
          } else {
            handleCobaltError(`Something went wrong! Try again later. (${data.error.code || data.text || data.statusText || ''})`, isAudioOnly);
          }
        },
        onerror: function (error) {
          handleCobaltError(error.message || error, isAudioOnly);
        },
        ontimeout: function () {
          handleCobaltError('Cobalt is not responding. Please try again later.', isAudioOnly);
        },
      });

      clearTimeout(dTimeout);
      dError = 'Slow down.';
      dTimeout = setTimeout(() => {
        dError = null;
      }, 5000);
      break;
  }
}

function addStyles() {
  const style = document.createElement('style');
  style.innerHTML = yddCSS;
  document.head.appendChild(style);
}

function deleteButtons() {
  const buttons = document.querySelectorAll('#ydd-button');
  if (buttons.length === 0) return;
  buttons.forEach((button) => {
    button.remove();
  });
}

function closeOptions(optionsDiv) {
  optionsDiv.classList.remove('ydd-scale-up');
  optionsDiv.classList.add('ydd-scale-down');
  setTimeout(() => {
    optionsDiv.remove();
  }, 400);
}

function showOptions(div) {
  let optionsDiv = document.getElementById('ydd-options-div');
  if (optionsDiv) {
    closeOptions(optionsDiv);
    return;
  }
  optionsDiv = document.createElement('div');
  const audio = document.createElement('div');
  const subtitles = document.createElement('div');
  const settings = document.createElement('div');
  audio.innerText = '🔊 Audio';
  subtitles.innerText = '🖹 Subtitles';
  settings.innerText = '⛭ Settings';
  optionsDiv.id = 'ydd-options-div';
  optionsDiv.appendChild(audio);
  optionsDiv.appendChild(subtitles);
  optionsDiv.appendChild(settings);
  div.appendChild(optionsDiv);
  optionsDiv.style.opacity = undefined;
  optionsDiv.classList.add('ydd-scale-up');
  settings.addEventListener('click', () => {
    opencfg();
    closeOptions(optionsDiv);
  });
  audio.addEventListener('click', () => {
    download(true);
    closeOptions(optionsDiv);
  });
  subtitles.addEventListener('click', () => {
    window.open(`https://downsub.com/?url=${document.location.href}`);
    closeOptions(optionsDiv);
  });
  window.addEventListener('click', (e) => {
    if (!div.contains(e.target)) {
      closeOptions(optionsDiv);
    }
  });
}

function createButton(bar, short) {
  if (!bar) return;
  const div = document.createElement('div');
  const button = document.createElement('button');
  const options = document.createElement('div');
  div.id = 'ydd-button';
  button.id = 'ydd-download';
  options.id = 'ydd-options';
  div.appendChild(button);
  div.appendChild(options);
  if (short) {
    div.style.marginTop = '10px';
    div.style.marginLeft = '0px';
    bar.insertBefore(div, bar.firstChild);
  } else bar.appendChild(div);

  let downloadService = gmc.get('downloadService') || defaults.downloadService;
  switch (downloadService) {
    case 'y2mate':
      button.title = 'Y2Mate';
      break;
    case 'yt1s':
      button.title = 'YT1S';
      break;
    case 'yt5s':
      button.title = 'YT5S';
      break;
    case 'cobalt_web':
      button.title = 'Cobalt';
      break;
    case 'cobalt_api':
      const quality = gmc.get('quality') || defaults.quality;
      const vCodec = gmc.get('vCodec') || defaults.vCodec;
      const info = `${quality}, ${vCodec}`;
      button.title = 'Cobalt: ' + info.toUpperCase();
      break;
    default:
      button.title = 'YDD';
      break;
  }
  button.innerText = '⇩';
  options.innerText = '☰';
  button.addEventListener('click', () => {
    download();
  });
  button.addEventListener('contextmenu', (e) => {
    const downloadService = gmc.get('downloadService') || defaults.downloadService;
    const backupService = gmc.get('backupService') || defaults.backupService;
    if (downloadService !== backupService && backupService !== 'none') download(false, backupService);
  });
  options.addEventListener('click', () => {
    showOptions(div);
  });
}

function checkShort(replace = true) {
  if (document.location.href.indexOf('youtube.com/shorts') > -1) {
    if (gmc.get('redirectShorts') && replace) window.location.replace(window.location.toString().replace('/shorts/', '/watch?v='));
    return true;
  } else return false;
}

function checkPage(alternative) {
  const service = alternative ? gmc.get('backupService') : gmc.get('downloadService');
  switch (service) {
    case 'cobalt_web':
      if (document.location.href.indexOf('cobalt.tools') > -1) {
        const url = new URL(document.location.href);
        const site = url.searchParams.get('ydd');
        if (site) {
          const audioOnly = url.searchParams.get('audioOnly') === 'true';
          const input = document.querySelector('#link-area');
          const button = audioOnly ? document.querySelector('#setting-button-save-downloadMode-audio') : document.querySelector('#setting-button-save-downloadMode-auto');
          const loadingIcon = document.querySelector('#input-icons');
          if (!input || !button || !loadingIcon) {
            cobaltTries++;
            if (cobaltTries > 10) yddAdded = true;
            else yddAdded = false;
          } else {
            yddAdded = true;
            input.value = site;
            input.dispatchEvent(new Event('input', { bubbles: true }));
            input.dispatchEvent(new Event('change', { bubbles: true }));
            button.click();
            const observer = new MutationObserver(function () {
              if (loadingIcon.classList.contains('loading') || !loadingIcon) return;
              const dwnButton = document.querySelector('#download-button');
              dwnButton.click();
              observer.disconnect();
            });
            observer.observe(loadingIcon, { attributes: true, attributeFilter: ['class'] });
          }
        }
        return true;
      }
      return false;
    case 'yt5s':
      if (document.location.href.indexOf('yt5s.biz/enxj100') > -1 || document.location.href.indexOf('5smp3.com') > -1) {
        const url = new URL(document.location.href);
        const site = url.searchParams.get('ydd');
        if (site) {
          const input = document.querySelector('#txt-url');
          const button = document.querySelector('#btn-submit');
          if (!input || !button) {
            yddAdded = false;
          } else {
            yddAdded = true;
            input.value = site;
            button.click();
          }
        }
        return true;
      }
      return false;
    default:
      return false;
  }
}

function modify() {
  const short = checkShort();
  if (checkPage() || checkPage(true)) return;
  if (document.location.href.indexOf('youtube.com/watch') === -1 && !short) {
    yddAdded = true;
    return;
  }
  if (short) {
    const bars = document.querySelectorAll('#actions');
    if (bars.length <= 1) {
      yddAdded = false;
      return;
    }
    deleteButtons();
    bars.forEach((bar) => {
      createButton(bar, true);
    });
    yddAdded = true;
  } else {
    const bar = document.getElementById('owner');
    if (!bar) {
      yddAdded = false;
      return;
    }
    deleteButtons();
    createButton(bar, false);
    yddAdded = true;
  }
}

function onInit() {
  addStyles();
  const observer = new MutationObserver(function () {
    if (!yddAdded) return modify();
    if (oldHref != document.location.href) {
      oldHref = document.location.href;
      yddAdded = false;
    }
  });
  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });
}