// ==UserScript==
// @name YouTube downloader
// @icon https://raw.githubusercontent.com/madkarmaa/youtube-downloader/main/images/icon.png
// @namespace aGkgdGhlcmUgOik=
// @source https://github.com/madkarmaa/youtube-downloader
// @supportURL https://github.com/madkarmaa/youtube-downloader
// @version 3.4.0
// @description A simple userscript to download YouTube videos in MAX QUALITY
// @author mk_
// @match *://*.youtube.com/*
// @connect api.cobalt.tools
// @connect raw.githubusercontent.com
// @grant GM_info
// @grant GM_addStyle
// @grant GM_xmlHttpRequest
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==
(async () => {
'use strict'; // prettier-ignore
// abort if not on youtube or youtube music or if in an iframe
if (!detectYoutubeService() || window !== window.parent) return;
// ===== VARIABLES =====
let ADVANCED_SETTINGS = localStorage.getItem('ytdl-advanced-settings')
? JSON.parse(localStorage.getItem('ytdl-advanced-settings'))
: {
enabled: false,
openUrl: '',
};
localStorage.setItem('ytdl-advanced-settings', JSON.stringify(ADVANCED_SETTINGS));
let DEV_MODE = String(localStorage.getItem('ytdl-dev-mode')).toLowerCase() === 'true';
let SHOW_NOTIFICATIONS =
localStorage.getItem('ytdl-notif-enabled') === null
? true
: String(localStorage.getItem('ytdl-notif-enabled')).toLowerCase() === 'true';
let oldILog = console.log;
let oldWLog = console.warn;
let oldELog = console.error;
let VIDEO_DATA = {
video_duration: null,
video_url: null,
video_author: null,
video_title: null,
video_id: null,
};
let videoDataReady = false;
// https://github.com/imputnet/cobalt/blob/current/docs/api.md#request-body-variables
const QUALITIES = {
MAX: 'max',
'2160p': '2160',
'1440p': '1440',
'1080p': '1080',
'720p': '720',
'480p': '480',
'360p': '360',
'240p': '240',
'144p': '144',
};
// ===== END VARIABLES =====
// ===== METHODS =====
function logger(level, ...args) {
if (DEV_MODE && level.toLowerCase() === 'info') oldILog.apply(console, ['%c[YTDL]', 'color: #f00;', ...args]);
else if (DEV_MODE && level.toLowerCase() === 'warn')
oldWLog.apply(console, ['%c[YTDL]', 'color: #f00;', ...args]);
else if (level.toLowerCase() === 'error') oldELog.apply(console, ['%c[YTDL]', 'color: #f00;', ...args]);
}
function Cobalt(videoUrl, audioOnly = false) {
// Use Promise because GM.xmlHttpRequest behaves differently with different userscript managers
return new Promise((resolve, reject) => {
// https://github.com/imputnet/cobalt/blob/current/docs/api.md
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.cobalt.tools/api/json',
headers: {
'Cache-Control': 'no-cache',
Accept: 'application/json',
'Content-Type': 'application/json',
},
data: JSON.stringify({
url: encodeURI(videoUrl),
vQuality: localStorage.getItem('ytdl-quality') ?? 'max',
filenamePattern: 'basic', // file name = video title
isAudioOnly: audioOnly,
disableMetadata: true, // privacy
}),
onload: (response) => {
const data = JSON.parse(response.responseText);
if (data?.url) resolve(data.url);
else reject(data);
},
onerror: (err) => reject(err),
});
});
}
// https://stackoverflow.com/a/61511955
function waitForElement(selector) {
return new Promise((resolve) => {
if (document.querySelector(selector)) return resolve(document.querySelector(selector));
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
function fetchNotifications() {
// Use Promise because GM.xmlHttpRequest behaves differently with different userscript managers
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://raw.githubusercontent.com/madkarmaa/youtube-downloader/main/notifications.json',
headers: {
'Cache-Control': 'no-cache',
Accept: 'application/json',
'Content-Type': 'application/json',
},
onload: (response) => {
const data = JSON.parse(response.responseText);
if (data?.length) resolve(data);
else reject(data);
},
onerror: (err) => reject(err),
});
});
}
class Notification {
constructor(title, body, uuid, storeUUID = true) {
const notification = document.createElement('div');
notification.classList.add('ytdl-notification', 'opened', uuid);
hideOnAnimationEnd(notification, 'closeNotif', true);
const nTitle = document.createElement('h2');
nTitle.textContent = title;
notification.appendChild(nTitle);
const nBody = document.createElement('div');
body.split('\n').forEach((text) => {
const paragraph = document.createElement('p');
paragraph.textContent = text;
nBody.appendChild(paragraph);
});
notification.appendChild(nBody);
const nDismissButton = document.createElement('button');
nDismissButton.textContent = 'Dismiss';
nDismissButton.addEventListener('click', () => {
if (storeUUID) {
const localNotificationsHashes = JSON.parse(localStorage.getItem('ytdl-notifications') ?? '[]');
localNotificationsHashes.push(uuid);
localStorage.setItem('ytdl-notifications', JSON.stringify(localNotificationsHashes));
logger('info', `Notification ${uuid} set as read`);
}
notification.classList.remove('opened');
notification.classList.add('closed');
});
notification.appendChild(nDismissButton);
document.body.appendChild(notification);
logger('info', 'New notification displayed', notification);
}
}
async function manageNotifications() {
if (!SHOW_NOTIFICATIONS) {
logger('info', 'Notifications disabled by the user');
return;
}
const localNotificationsHashes = JSON.parse(localStorage.getItem('ytdl-notifications')) ?? [];
logger('info', 'Local read notifications hashes\n\n', localNotificationsHashes);
const onlineNotifications = await fetchNotifications();
logger(
'info',
'Online notifications hashes\n\n',
onlineNotifications.map((n) => n.uuid)
);
const unreadNotifications = onlineNotifications.filter((n) => !localNotificationsHashes.includes(n.uuid));
logger(
'info',
'Unread notifications hashes\n\n',
unreadNotifications.map((n) => n.uuid)
);
unreadNotifications.reverse().forEach((n) => {
new Notification(n.title, n.body, n.uuid);
});
}
async function updateVideoData(e) {
videoDataReady = false;
const temp_video_data = e.detail?.getVideoData();
VIDEO_DATA.video_duration = e.detail?.getDuration();
VIDEO_DATA.video_url = e.detail?.getVideoUrl();
VIDEO_DATA.video_author = temp_video_data?.author;
VIDEO_DATA.video_title = temp_video_data?.title;
VIDEO_DATA.video_id = temp_video_data?.video_id;
videoDataReady = true;
logger('info', 'Video data updated\n\n', VIDEO_DATA);
}
async function hookPlayerEvent(...fns) {
document.addEventListener('yt-player-updated', (e) => {
for (let i = 0; i < fns.length; i++) fns[i](e);
});
logger(
'info',
'Video player event hooked. Callbacks:\n\n',
fns.map((f) => f.name)
);
}
async function hookNavigationEvents(...fns) {
['yt-navigate', 'yt-navigate-finish', 'yt-navigate-finish', 'yt-page-data-updated'].forEach((evName) => {
document.addEventListener(evName, (e) => {
for (let i = 0; i < fns.length; i++) fns[i](e);
});
});
logger(
'info',
'Navigation events hooked. Callbacks:\n\n',
fns.map((f) => f.name)
);
}
function hideOnAnimationEnd(target, animationName, alsoRemove = false) {
target.addEventListener('animationend', (e) => {
if (e.animationName === animationName) {
if (alsoRemove) e.target.remove();
else e.target.style.display = 'none';
}
});
}
// https://stackoverflow.com/a/10344293
function isTyping() {
const el = document.activeElement;
return (
el &&
(el.tagName.toLowerCase() === 'input' ||
el.tagName.toLowerCase() === 'textarea' ||
String(el.getAttribute('contenteditable')).toLowerCase() === 'true')
);
}
function replacePlaceholders(inputString) {
return inputString.replace(/{{\s*([^}\s]+)\s*}}/g, (match, placeholder) => VIDEO_DATA[placeholder] || match);
}
async function appendSideMenu() {
const sideMenu = document.createElement('div');
sideMenu.id = 'ytdl-sideMenu';
sideMenu.classList.add('closed');
sideMenu.style.display = 'none';
hideOnAnimationEnd(sideMenu, 'closeMenu');
const sideMenuHeader = document.createElement('h2');
sideMenuHeader.textContent = 'Youtube downloader settings';
sideMenuHeader.classList.add('header');
sideMenu.appendChild(sideMenuHeader);
// ===== templates, don't use, just clone the node =====
const sideMenuSettingContainer = document.createElement('div');
sideMenuSettingContainer.classList.add('setting-row');
const sideMenuSettingLabel = document.createElement('h3');
sideMenuSettingLabel.classList.add('setting-label');
const sideMenuSettingDescription = document.createElement('p');
sideMenuSettingDescription.classList.add('setting-description');
sideMenuSettingContainer.append(sideMenuSettingLabel, sideMenuSettingDescription);
const switchContainer = document.createElement('span');
switchContainer.classList.add('ytdl-switch');
const switchCheckbox = document.createElement('input');
switchCheckbox.type = 'checkbox';
const switchLabel = document.createElement('label');
switchContainer.append(switchCheckbox, switchLabel);
// ===== end templates =====
// NOTIFICATIONS
const notifContainer = sideMenuSettingContainer.cloneNode(true);
notifContainer.querySelector('.setting-label').textContent = 'Notifications';
notifContainer.querySelector('.setting-description').textContent =
"Disable if you don't want to receive notifications from the developer.";
const notifSwitch = switchContainer.cloneNode(true);
notifSwitch.querySelector('input').checked = SHOW_NOTIFICATIONS;
notifSwitch.querySelector('input').id = 'ytdl-notif-switch';
notifSwitch.querySelector('label').setAttribute('for', 'ytdl-notif-switch');
notifSwitch.querySelector('input').addEventListener('change', (e) => {
SHOW_NOTIFICATIONS = e.target.checked;
localStorage.setItem('ytdl-notif-enabled', SHOW_NOTIFICATIONS);
logger('info', `Notifications ${SHOW_NOTIFICATIONS ? 'enabled' : 'disabled'}`);
});
notifContainer.appendChild(notifSwitch);
sideMenu.appendChild(notifContainer);
// VIDEO QUALITY CONTROL
const qualityContainer = sideMenuSettingContainer.cloneNode(true);
qualityContainer.querySelector('.setting-label').textContent = 'Video download quality';
qualityContainer.querySelector('.setting-description').textContent =
'Control the resolution of the downloaded videos. Not all the resolutions are supported by some videos.';
const qualitySelect = document.createElement('select');
qualitySelect.name = 'dl-quality';
qualitySelect.id = 'ytdl-dl-quality-select';
qualitySelect.disabled = ADVANCED_SETTINGS.enabled;
Object.entries(QUALITIES).forEach(([name, value]) => {
const qualityOption = document.createElement('option');
qualityOption.textContent = name;
qualityOption.value = value;
qualitySelect.appendChild(qualityOption);
});
qualitySelect.value = localStorage.getItem('ytdl-quality') ?? 'max';
qualitySelect.addEventListener('change', (e) => {
localStorage.setItem('ytdl-quality', String(e.target.value));
logger('info', `Download quality set to ${e.target.value}`);
});
qualityContainer.appendChild(qualitySelect);
sideMenu.appendChild(qualityContainer);
// DEVELOPER MODE
const devModeContainer = sideMenuSettingContainer.cloneNode(true);
devModeContainer.querySelector('.setting-label').textContent = 'Developer mode';
devModeContainer.querySelector('.setting-description').textContent =
"Show a detailed output of what's happening under the hood in the console.";
const devModeSwitch = switchContainer.cloneNode(true);
devModeSwitch.querySelector('input').checked = DEV_MODE;
devModeSwitch.querySelector('input').id = 'ytdl-dev-mode-switch';
devModeSwitch.querySelector('label').setAttribute('for', 'ytdl-dev-mode-switch');
devModeSwitch.querySelector('input').addEventListener('change', (e) => {
DEV_MODE = e.target.checked;
localStorage.setItem('ytdl-dev-mode', DEV_MODE);
// always use console.log here to show output
console.log(`\x1b[31m[YTDL]\x1b[0m Developer mode ${DEV_MODE ? 'enabled' : 'disabled'}`);
});
devModeContainer.appendChild(devModeSwitch);
sideMenu.appendChild(devModeContainer);
// ADVANCED SETTINGS
const advancedSettingsContainer = sideMenuSettingContainer.cloneNode(true);
advancedSettingsContainer.querySelector('.setting-label').textContent = 'Advanced settings';
advancedSettingsContainer.querySelector('.setting-description').textContent =
'FOR EXPERIENCED USERS ONLY. Modify the behaviour of the download button.';
const advancedOptionsContainer = document.createElement('div');
advancedOptionsContainer.classList.add('advanced-options', ADVANCED_SETTINGS.enabled ? 'opened' : 'closed');
advancedOptionsContainer.style.display = ADVANCED_SETTINGS.enabled ? 'flex' : 'none';
hideOnAnimationEnd(advancedOptionsContainer, 'closeNotif');
const advancedSwitch = switchContainer.cloneNode(true);
advancedSwitch.querySelector('input').checked = ADVANCED_SETTINGS.enabled;
advancedSwitch.querySelector('input').id = 'ytdl-advanced-switch';
advancedSwitch.querySelector('label').setAttribute('for', 'ytdl-advanced-switch');
advancedSwitch.querySelector('input').addEventListener('change', (e) => {
ADVANCED_SETTINGS.enabled = e.target.checked;
localStorage.setItem('ytdl-advanced-settings', JSON.stringify(ADVANCED_SETTINGS));
qualitySelect.disabled = e.target.checked;
if (e.target.checked) {
advancedOptionsContainer.style.display = 'flex';
advancedOptionsContainer.classList.remove('closed');
advancedOptionsContainer.classList.add('opened');
} else {
advancedOptionsContainer.classList.remove('opened');
advancedOptionsContainer.classList.add('closed');
}
logger('info', `Advanced settings ${ADVANCED_SETTINGS.enabled ? 'enabled' : 'disabled'}`);
});
advancedSettingsContainer.appendChild(advancedSwitch);
const openUrlLabel = document.createElement('label');
openUrlLabel.setAttribute('for', 'advanced-settings-open-url');
openUrlLabel.textContent = 'Open the given URL in a new window. GET request only.';
const placeholdersLink = document.createElement('a');
placeholdersLink.href = 'https://github.com/madkarmaa/youtube-downloader/blob/main/docs/PLACEHOLDERS.md';
placeholdersLink.target = '_blank';
placeholdersLink.textContent = 'Use placeholders to access video data. Click to know about placeholders';
openUrlLabel.appendChild(placeholdersLink);
const openUrlInput = document.createElement('input');
openUrlInput.id = 'advanced-settings-open-url';
openUrlInput.type = 'url';
openUrlInput.placeholder = 'URL to open';
openUrlInput.value = ADVANCED_SETTINGS.openUrl ?? null;
openUrlInput.addEventListener('focusout', (e) => {
if (e.target.checkValidity()) {
ADVANCED_SETTINGS.openUrl = e.target.value;
localStorage.setItem('ytdl-advanced-settings', JSON.stringify(ADVANCED_SETTINGS));
logger('info', `Advanced settings: URL to open set to "${e.target.value}"`);
} else {
logger('error', `Invalid URL to open: "${e.target.value}"`);
alert(e.target.validationMessage);
e.target.value = '';
}
});
advancedOptionsContainer.append(openUrlLabel, openUrlInput);
advancedSettingsContainer.appendChild(advancedOptionsContainer);
sideMenu.appendChild(advancedSettingsContainer);
// SIDE MENU EVENTS
document.addEventListener('mousedown', (e) => {
if (sideMenu.style.display !== 'none' && !sideMenu.contains(e.target)) {
sideMenu.classList.remove('opened');
sideMenu.classList.add('closed');
logger('info', 'Side menu closed');
}
});
document.addEventListener('keydown', (e) => {
if (e.key !== 'p') return;
if (isTyping()) return;
if (sideMenu.style.display === 'none') {
sideMenu.style.top = window.scrollY + 'px';
sideMenu.style.display = 'flex';
sideMenu.classList.remove('closed');
sideMenu.classList.add('opened');
logger('info', 'Side menu opened');
} else {
sideMenu.classList.remove('opened');
sideMenu.classList.add('closed');
logger('info', 'Side menu closed');
}
});
window.addEventListener('scroll', () => {
if (sideMenu.classList.contains('closed')) return;
sideMenu.classList.remove('opened');
sideMenu.classList.add('closed');
logger('info', 'Side menu closed');
});
document.body.appendChild(sideMenu);
logger('info', 'Side menu created\n\n', sideMenu);
}
function detectYoutubeService() {
if (window.location.hostname === 'www.youtube.com' && window.location.pathname.startsWith('/shorts'))
return 'SHORTS';
if (window.location.hostname === 'www.youtube.com' && window.location.pathname.startsWith('/watch'))
return 'WATCH';
else if (window.location.hostname === 'music.youtube.com') return 'MUSIC';
else if (window.location.hostname === 'www.youtube.com') return 'YOUTUBE';
else return null;
}
function elementInContainer(container, element) {
return container.contains(element);
}
async function leftClick() {
const isYtMusic = detectYoutubeService() === 'MUSIC';
if (!isYtMusic && !videoDataReady) {
logger('warn', 'Video data not ready');
new Notification('Wait!', 'The video data is not ready yet, try again in a few seconds.', 'popup', false);
return;
} else if (isYtMusic && !window.location.pathname.startsWith('/watch')) {
logger('warn', 'Video URL not avaiable');
new Notification(
'Wait!',
'Open the music player so the song link is visible, then try again.',
'popup',
false
);
return;
}
try {
logger('info', 'Download started');
if (!ADVANCED_SETTINGS.enabled)
window.open(
await Cobalt(
isYtMusic
? window.location.href.replace('music.youtube.com', 'www.youtube.com')
: VIDEO_DATA.video_url
),
'_blank'
);
else if (ADVANCED_SETTINGS.openUrl) window.open(replacePlaceholders(ADVANCED_SETTINGS.openUrl));
logger('info', 'Download completed');
} catch (err) {
logger('error', JSON.parse(JSON.stringify(err)));
new Notification('Error', JSON.stringify(err), 'error', false);
}
}
async function rightClick(e) {
const isYtMusic = detectYoutubeService() === 'MUSIC';
e.preventDefault();
if (!isYtMusic && !videoDataReady) {
logger('warn', 'Video data not ready');
new Notification('Wait!', 'The video data is not ready yet, try again in a few seconds.', 'popup', false);
return false;
} else if (isYtMusic && !window.location.pathname.startsWith('/watch')) {
logger('warn', 'Video URL not avaiable');
new Notification(
'Wait!',
'Open the music player so the song link is visible, then try again.',
'popup',
false
);
return;
}
try {
logger('info', 'Download started');
if (!ADVANCED_SETTINGS.enabled)
window.open(
await Cobalt(
isYtMusic
? window.location.href.replace('music.youtube.com', 'www.youtube.com')
: VIDEO_DATA.video_url,
true
),
'_blank'
);
else if (ADVANCED_SETTINGS.openUrl) window.open(replacePlaceholders(ADVANCED_SETTINGS.openUrl));
logger('info', 'Download completed');
} catch (err) {
logger('error', JSON.parse(JSON.stringify(err)));
new Notification('Error', JSON.stringify(err), 'error', false);
}
return false;
}
// https://www.30secondsofcode.org/js/s/element-is-visible-in-viewport/
function elementIsVisibleInViewport(el, partiallyVisible = false) {
const { top, left, bottom, right } = el.getBoundingClientRect();
const { innerHeight, innerWidth } = window;
return partiallyVisible
? ((top > 0 && top < innerHeight) || (bottom > 0 && bottom < innerHeight)) &&
((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
: top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth;
}
async function appendDownloadButton(e) {
const ytContainerSelector =
'#movie_player > div.ytp-chrome-bottom > div.ytp-chrome-controls > div.ytp-right-controls';
const ytmContainerSelector =
'#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > div.middle-controls-buttons.style-scope.ytmusic-player-bar';
const ytsContainerSelector = '#actions.style-scope.ytd-reel-player-overlay-renderer';
// ===== templates, don't use, just clone the node =====
const downloadIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
downloadIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
downloadIcon.setAttribute('fill', 'currentColor');
downloadIcon.setAttribute('height', '24');
downloadIcon.setAttribute('viewBox', '0 0 24 24');
downloadIcon.setAttribute('width', '24');
downloadIcon.setAttribute('focusable', 'false');
downloadIcon.style.pointerEvents = 'none';
downloadIcon.style.display = 'block';
downloadIcon.style.width = '100%';
downloadIcon.style.height = '100%';
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M17 18v1H6v-1h11zm-.5-6.6-.7-.7-3.8 3.7V4h-1v10.4l-3.8-3.8-.7.7 5 5 5-4.9z');
downloadIcon.appendChild(path);
const downloadButton = document.createElement('button');
downloadButton.id = 'ytdl-download-button';
downloadButton.classList.add('ytp-button');
downloadButton.title = 'Left click to download as video, right click as audio only';
downloadButton.appendChild(downloadIcon);
// ===== end templates =====
switch (detectYoutubeService()) {
case 'WATCH':
const ytCont = await waitForElement(ytContainerSelector);
logger('info', 'Download button container found\n\n', ytCont);
if (elementInContainer(ytCont, ytCont.querySelector('#ytdl-download-button'))) {
logger('warn', 'Download button already in container');
break;
}
const ytDlBtnClone = downloadButton.cloneNode(true);
ytDlBtnClone.classList.add('YT');
ytDlBtnClone.addEventListener('click', leftClick);
ytDlBtnClone.addEventListener('contextmenu', rightClick);
logger('info', 'Download button created\n\n', ytDlBtnClone);
ytCont.insertBefore(ytDlBtnClone, ytCont.firstChild);
logger('info', 'Download button inserted in container');
break;
case 'MUSIC':
const ytmCont = await waitForElement(ytmContainerSelector);
logger('info', 'Download button container found\n\n', ytmCont);
if (elementInContainer(ytmCont, ytmCont.querySelector('#ytdl-download-button'))) {
logger('warn', 'Download button already in container');
break;
}
const ytmDlBtnClone = downloadButton.cloneNode(true);
ytmDlBtnClone.classList.add('YTM');
ytmDlBtnClone.addEventListener('click', leftClick);
ytmDlBtnClone.addEventListener('contextmenu', rightClick);
logger('info', 'Download button created\n\n', ytmDlBtnClone);
ytmCont.insertBefore(ytmDlBtnClone, ytmCont.firstChild);
logger('info', 'Download button inserted in container');
break;
case 'SHORTS':
if (e.type !== 'yt-navigate-finish') return;
await waitForElement(ytsContainerSelector); // wait for the UI to finish loading
const visibleYtsConts = Array.from(document.querySelectorAll(ytsContainerSelector)).filter((el) =>
elementIsVisibleInViewport(el)
);
logger('info', 'Download button containers found\n\n', visibleYtsConts);
visibleYtsConts.forEach((ytsCont) => {
if (elementInContainer(ytsCont, ytsCont.querySelector('#ytdl-download-button'))) {
logger('warn', 'Download button already in container');
return;
}
const ytsDlBtnClone = downloadButton.cloneNode(true);
ytsDlBtnClone.classList.add(
'YTS',
'yt-spec-button-shape-next',
'yt-spec-button-shape-next--tonal',
'yt-spec-button-shape-next--mono',
'yt-spec-button-shape-next--size-l',
'yt-spec-button-shape-next--icon-button'
);
ytsDlBtnClone.addEventListener('click', leftClick);
ytsDlBtnClone.addEventListener('contextmenu', rightClick);
logger('info', 'Download button created\n\n', ytsDlBtnClone);
ytsCont.insertBefore(ytsDlBtnClone, ytsCont.firstChild);
logger('info', 'Download button inserted in container');
});
break;
default:
return;
}
}
async function devStuff() {
if (!DEV_MODE) return;
logger('info', 'Current service is: ' + detectYoutubeService());
}
// ===== END METHODS =====
GM_addStyle(`
#ytdl-sideMenu {
min-height: 100vh;
z-index: 9998;
position: absolute;
top: 0;
left: -100vw;
width: 50vw;
background-color: var(--yt-spec-base-background);
border-right: 2px solid var(--yt-spec-static-grey);
display: flex;
flex-direction: column;
gap: 2rem;
padding: 2rem 2.5rem;
font-family: "Roboto", "Arial", sans-serif;
}
#ytdl-sideMenu.opened {
animation: openMenu .3s linear forwards;
}
#ytdl-sideMenu.closed {
animation: closeMenu .3s linear forwards;
}
#ytdl-sideMenu a {
color: var(--yt-brand-youtube-red);
text-decoration: none;
font-weight: 600;
}
#ytdl-sideMenu a:hover {
text-decoration: underline;
}
#ytdl-sideMenu label {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 1.4rem;
color: var(--yt-spec-text-primary);
}
#ytdl-sideMenu .header {
text-align: center;
font-size: 2.5rem;
color: var(--yt-brand-youtube-red);
}
#ytdl-sideMenu .setting-row {
display: flex;
flex-direction: column;
gap: 1rem;
transition: all 0.2s ease-in-out;
}
#ytdl-sideMenu .setting-label {
font-size: 1.8rem;
color: var(--yt-brand-youtube-red);
}
#ytdl-sideMenu .setting-description {
font-size: 1.4rem;
color: var(--yt-spec-text-primary);
}
.ytdl-switch {
display: inline-block;
}
.ytdl-switch input {
display: none;
}
.ytdl-switch label {
display: block;
width: 50px;
height: 19.5px;
padding: 3px;
border-radius: 15px;
border: 2px solid var(--yt-brand-medium-red);
cursor: pointer;
transition: 0.3s;
}
.ytdl-switch label::after {
content: "";
display: inherit;
width: 20px;
height: 20px;
border-radius: 12px;
background: var(--yt-brand-medium-red);
transition: 0.3s;
}
.ytdl-switch input:checked ~ label {
border-color: var(--yt-spec-light-green);
}
.ytdl-switch input:checked ~ label::after {
translate: 30px 0;
background: var(--yt-spec-light-green);
}
.ytdl-switch input:disabled ~ label {
opacity: 0.5;
cursor: not-allowed;
}
#ytdl-sideMenu .advanced-options {
display: flex;
flex-direction: column;
gap: 0.7rem;
margin: 1rem 0;
}
#ytdl-sideMenu .advanced-options.opened {
animation: openNotif 0.3s linear forwards;
}
#ytdl-sideMenu .advanced-options.closed {
animation: closeNotif .3s linear forwards;
}
#ytdl-sideMenu input[type="url"] {
background: none;
padding: 0.7rem 1rem;
border: none;
outline: none;
border-bottom: 2px solid var(--yt-spec-red-70);
color: var(--yt-spec-text-primary);
font-family: monospace;
transition: border-bottom-color 0.2s ease-in-out;
}
#ytdl-sideMenu input[type="url"]:focus {
border-bottom-color: var(--yt-brand-youtube-red);
}
.ytdl-notification {
display: flex;
flex-direction: column;
gap: 2rem;
position: fixed;
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
background-color: var(--yt-spec-base-background);
border: 2px solid var(--yt-spec-static-grey);
border-radius: 8px;
color: var(--yt-spec-text-primary);
z-index: 9999;
padding: 1.5rem 1.6rem;
font-family: "Roboto", "Arial", sans-serif;
font-size: 1.4rem;
width: fit-content;
height: fit-content;
max-width: 40vw;
max-height: 50vh;
word-wrap: break-word;
line-height: var(--yt-caption-line-height);
}
.ytdl-notification.opened {
animation: openNotif 0.3s linear forwards;
}
.ytdl-notification.closed {
animation: closeNotif 0.3s linear forwards;
}
.ytdl-notification h2 {
color: var(--yt-brand-youtube-red);
}
.ytdl-notification > div {
display: flex;
flex-direction: column;
gap: 1rem;
}
.ytdl-notification > button {
transition: all 0.2s ease-in-out;
cursor: pointer;
border: 2px solid var(--yt-spec-static-grey);
border-radius: 8px;
background-color: var(--yt-brand-medium-red);
padding: 0.7rem 0.8rem;
color: #fff;
font-weight: 600;
}
.ytdl-notification button:hover {
background-color: var(--yt-spec-red-70);
}
#ytdl-download-button {
background: none;
border: none;
outline: none;
color: var(--yt-spec-text-primary);
cursor: pointer;
transition: color 0.2s ease-in-out;
display: inline-flex;
justify-content: center;
align-items: center;
}
#ytdl-download-button:hover {
color: var(--yt-brand-youtube-red);
}
#ytdl-download-button.YTM {
transform: scale(1.5);
margin: 0 1rem;
}
#ytdl-download-button > svg {
transform: translateX(3.35%);
}
#ytdl-dl-quality-select {
background-color: var(--yt-spec-base-background);
color: var(--yt-spec-text-primary);
padding: 0.7rem 1rem;
border: none;
outline: none;
border-bottom: 2px solid var(--yt-spec-red-70);
border-left: 2px solid var(--yt-spec-red-70);
transition: all 0.2s ease-in-out;
font-family: "Roboto", "Arial", sans-serif;
font-size: 1.4rem;
}
#ytdl-dl-quality-select:focus {
border-bottom-color: var(--yt-brand-youtube-red);
border-left-color: var(--yt-brand-youtube-red);
}
#ytdl-sideMenu > div:has(> #ytdl-dl-quality-select:disabled) {
filter: grayscale(0.8);
}
#ytdl-dl-quality-select:disabled {
cursor: not-allowed;
}
@keyframes openMenu {
0% {
left: -100vw;
}
100% {
left: 0;
}
}
@keyframes closeMenu {
0% {
left: 0;
}
100% {
left: -100vw;
}
}
@keyframes openNotif {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes closeNotif {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
`);
logger('info', 'Custom styles added');
hookPlayerEvent(updateVideoData);
hookNavigationEvents(appendDownloadButton, devStuff);
// functions that require the DOM to exist
window.addEventListener('DOMContentLoaded', () => {
appendSideMenu();
appendDownloadButton();
manageNotifications();
});
})();