// ==UserScript==
// @name YouTube Hotkeys
// @namespace Violentmonkey Scripts
// @version 1.0
// @description Custom hotkeys for YouTube
// @author dpi0
// @include *://*.youtube.com/*
// @match https://www.youtube.com/*
// @match https://youtube.com/*
// @grant none
// @homepageURL https://github.com/dpi0/scripts/blob/main/greasyfork/youtube-hotkeys.js
// @supportURL https://github.com/dpi0/scripts/issues
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Default key bindings - these can be customized below
const DEFAULT_KEY_BINDINGS = {
'a': navigateToChannelVideos, // Navigate to channel videos
'q': navigateToChannelPlaylists, // Navigate to channel playlists
'E': navigateToHistory, // Navigate to history
'w': navigateToWatchLater, // Navigate to watch later
'A': navigateToLikedVideos, // Navigate to liked videos
// 'C': navigateToHome is removed as requested
'S': navigateToSubscriptions, // Navigate to subscriptions
'T': navigateToTrending, // Navigate to trending
'/': focusSearchBar, // Focus search bar
'H': clickNextButton, // Click next video button
'G': clickPrevButton, // Click previous video button
'e': saveToPlaylist, // Save current video to playlist
'u': saveToWatchLater, // Save current video to watch later
'y': copyVideoUrlWithTimestamp, // Copy video URL with timestamp
'Tab': toggleSidebar, // Toggle sidebar
};
// CONFIGURATION - Edit this object to customize your key bindings
const KEY_BINDINGS = DEFAULT_KEY_BINDINGS;
// Utility Functions
function getChannelInfo() {
// First try: Check if we're on a watch page
if (window.location.pathname.startsWith('/watch')) {
// Try to get channel link from video owner
const ownerLinks = document.querySelectorAll('#owner #channel-name a, #top-row ytd-channel-name a');
if (ownerLinks.length > 0) {
return ownerLinks[0].href;
}
}
// Second try: Check if we're already on a channel page
if (window.location.pathname.includes('/channel/') || window.location.pathname.includes('/@')) {
// Get the base channel URL without /videos or /playlists
const baseUrl = window.location.href.split('/').slice(0, -1).join('/');
if (baseUrl.includes('/channel/') || baseUrl.includes('/@')) {
return baseUrl;
}
return window.location.href;
}
// Third try: Find any channel link on the page
const channelLinks = document.querySelectorAll('a[href*="/channel/"], a[href*="/@"]');
if (channelLinks.length > 0) {
return channelLinks[0].href;
}
return null;
}
function isInputFocused() {
const activeElement = document.activeElement;
const tagName = activeElement.tagName.toLowerCase();
return tagName === 'input' || tagName === 'textarea' || activeElement.isContentEditable;
}
// Navigation Functions
function navigateToChannelVideos() {
const channelUrl = getChannelInfo();
if (channelUrl) {
console.log("Channel URL detected:", channelUrl);
// Construct the videos URL
let videosUrl;
// Handle different URL formats
if (channelUrl.includes('/videos') || channelUrl.includes('/playlists')) {
// Already has a section, replace it with /videos
videosUrl = channelUrl.replace(/\/(videos|playlists|featured|about|community|shorts|streams).*$/, '/videos');
} else {
// Doesn't have a section yet, add /videos
videosUrl = channelUrl + '/videos';
}
console.log("Opening videos URL:", videosUrl);
window.open(videosUrl, '_blank');
return true;
} else {
console.log("Could not detect channel URL");
}
return false;
}
function navigateToChannelPlaylists() {
const channelUrl = getChannelInfo();
if (channelUrl) {
console.log("Channel URL detected:", channelUrl);
// Construct the playlists URL
let playlistsUrl;
// Handle different URL formats
if (channelUrl.includes('/videos') || channelUrl.includes('/playlists')) {
// Already has a section, replace it with /playlists
playlistsUrl = channelUrl.replace(/\/(videos|playlists|featured|about|community|shorts|streams).*$/, '/playlists');
} else {
// Doesn't have a section yet, add /playlists
playlistsUrl = channelUrl + '/playlists';
}
console.log("Opening playlists URL:", playlistsUrl);
window.open(playlistsUrl, '_blank');
return true;
} else {
console.log("Could not detect channel URL");
}
return false;
}
function navigateToHistory() {
window.open('https://www.youtube.com/feed/history', '_blank');
return true;
}
function navigateToWatchLater() {
window.open('https://www.youtube.com/playlist?list=WL', '_blank');
return true;
}
function navigateToLikedVideos() {
window.open('https://www.youtube.com/playlist?list=LL', '_blank');
return true;
}
function navigateToSubscriptions() {
window.open('https://youtube.com/feed/subscriptions', '_blank');
return true;
}
function navigateToTrending() {
window.open('https://youtube.com/feed/trending', '_blank');
return true;
}
function focusSearchBar() {
const searchBar = document.querySelector('input#search');
if (searchBar) {
searchBar.focus();
return true; // Prevent default '/' behavior
}
return false;
}
// Video Control Functions
function clickNextButton() {
const nextButton = document.querySelector('.ytp-next-button');
if (nextButton) {
nextButton.click();
return true;
}
return false;
}
function clickPrevButton() {
const prevButton = document.querySelector('.ytp-prev-button');
if (prevButton) {
prevButton.click();
return true;
}
return false;
}
function saveToPlaylist() {
// Comprehensive approach to find the save button
// 1. Try modern YouTube UI (desktop)
let saveButton = document.querySelector('ytd-menu-renderer yt-button-shape button[aria-label*="Save"]');
// 2. Try expanded action buttons
if (!saveButton) {
saveButton = document.querySelector('ytd-watch-metadata button[aria-label*="Save"]');
}
// 3. Try the "More actions" menu if it exists and need to open it first
if (!saveButton) {
const moreButton = document.querySelector('ytd-menu-renderer button[aria-label="More actions"], ytd-menu-renderer button[aria-label="More"]');
if (moreButton) {
moreButton.click();
// Wait for menu to appear
setTimeout(() => {
const saveOptionInMenu = document.querySelector('ytd-menu-service-item-renderer[aria-label*="Save"]');
if (saveOptionInMenu) {
saveOptionInMenu.click();
}
}, 100);
return true;
}
}
// 4. Try old player UI
if (!saveButton) {
saveButton = document.querySelector('.ytp-save-button');
}
// 5. Fallback to text search
if (!saveButton) {
const allButtons = Array.from(document.querySelectorAll('button, ytd-button-renderer, yt-button-renderer'));
saveButton = allButtons.find(btn => {
const text = btn.textContent || '';
return text.includes('Save');
});
}
if (saveButton) {
saveButton.click();
console.log("Save button clicked");
return true;
}
console.log("Save button not found");
return false;
}
function saveToWatchLater() {
// First, call saveToPlaylist to open the menu
if (saveToPlaylist()) {
// Wait for the save dialog to appear
setTimeout(() => {
// Try all possible selectors for watch later option
const watchLaterSelectors = [
'ytd-playlist-add-to-option-renderer[aria-label="Watch later"]',
'tp-yt-paper-item:has(yt-formatted-string:contains("Watch later"))',
'.ytd-menu-service-item-renderer:contains("Watch later")',
'ytd-menu-service-item-renderer[aria-label*="Watch later"]',
'yt-formatted-string:contains("Watch later")',
'span:contains("Watch later")'
];
// Try each selector
for (const selector of watchLaterSelectors) {
try {
const watchLaterOption = document.querySelector(selector);
if (watchLaterOption) {
watchLaterOption.click();
console.log("Watch Later option clicked");
// Close the dialog if still open
setTimeout(() => {
const closeButtons = document.querySelectorAll('.ytd-add-to-playlist-renderer #close-button button, button[aria-label="Close"], tp-yt-paper-dialog button[aria-label="Cancel"]');
if (closeButtons.length) {
closeButtons[0].click();
console.log("Dialog closed");
}
}, 500);
return;
}
} catch (e) {
// Ignore selector errors
}
}
// Alternative: find by text content using Array.from and find
const allElements = Array.from(document.querySelectorAll('ytd-playlist-add-to-option-renderer, yt-formatted-string, span, div'));
const watchLaterOption = allElements.find(el => {
const text = el.textContent || '';
return text.trim() === 'Watch later';
});
if (watchLaterOption) {
watchLaterOption.click();
console.log("Watch Later option found and clicked via text search");
} else {
console.log("Watch Later option not found");
}
}, 300);
return true;
}
return false;
}
function copyVideoUrlWithTimestamp() {
const video = document.querySelector('video');
if (video) {
const currentTime = Math.floor(video.currentTime);
const url = new URL(window.location.href);
// Remove any existing t parameter
url.searchParams.delete('t');
// Add the current timestamp
if (currentTime > 0) {
url.searchParams.set('t', currentTime + 's');
}
// Copy to clipboard
navigator.clipboard.writeText(url.toString())
.then(() => {
// Show a brief notification
const notification = document.createElement('div');
notification.textContent = 'URL copied to clipboard!';
notification.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 15px;
border-radius: 4px;
z-index: 9999;
`;
document.body.appendChild(notification);
// Remove notification after 2 seconds
setTimeout(() => {
notification.remove();
}, 2000);
})
.catch(err => {
console.error('Could not copy URL: ', err);
});
return true;
}
return false;
}
function toggleSidebar() {
// Find and click the guide button (hamburger menu) to toggle sidebar
const guideButton = document.querySelector('button#guide-button, yt-icon-button#guide-button, ytd-topbar-menu-button-renderer button');
if (guideButton) {
guideButton.click();
return true; // Prevent default Tab behavior
}
return false;
}
// Main event handler
function handleKeyDown(event) {
// Skip if an input or contenteditable is focused
if (isInputFocused()) {
return;
}
// Handle Tab key specially
if (event.key === 'Tab' && !event.shiftKey && !event.ctrlKey && !event.altKey) {
toggleSidebar();
event.preventDefault();
return;
}
// Get the key pressed
const key = event.key;
// Check if the key is in our bindings
if (key in KEY_BINDINGS) {
console.log(`Key '${key}' pressed, executing function:`, KEY_BINDINGS[key].name);
// Execute the function
const result = KEY_BINDINGS[key]();
// If the function returns true, prevent default behavior
if (result === true) {
event.preventDefault();
}
}
}
// Add event listener for keydown
document.addEventListener('keydown', handleKeyDown);
// Log that the script is running
console.log('YouTube Custom Hotkeys script loaded with bindings:', Object.keys(KEY_BINDINGS));
})();