YouTube Playlist Search Bar

Adds a search bar to YouTube playlists. Does NOT work with shorts or when playlist video filter is set to "Shorts".

< Feedback on YouTube Playlist Search Bar

Review: Good - script works

§
Posted: 2024.12.29.

Thanks for providing this script, it works great :) Also I did some tinkering to your script and added a feature where if you do @ and then the youtube channels name only videos you've liked from that channel will pop up and I improved the UI a bit.

// ==UserScript==
// @name YouTube Playlist Search Bar
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Adds a search bar to YouTube playlists. Search by title or channel name (using @). Does NOT work with shorts or when playlist video filter is set to "Shorts".
// @match https://www.youtube.com/playlist*
// @author Setnour6
// @grant none
// @license GPL-3.0
// @downloadURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.user.js
// @updateURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.meta.js
// ==/UserScript==

(function() {
'use strict';

function createSearchBar() {
// Check if custom search bar already exists
if (document.getElementById('playlist-search-bar')) return;

// Find target element to place search bar under sorting options
const target = document.querySelector('#page-manager ytd-playlist-video-list-renderer');

// Only proceed if target element exists
if (!target) return;

const container = document.createElement('div');
container.id = 'playlist-search-bar';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.marginBottom = '16px';
container.style.padding = '8px 16px';
container.style.width = '93%';
container.style.marginLeft = 'auto';
container.style.marginRight = 'auto';
container.style.transition = 'all 0.2s ease';

const searchContainer = document.createElement('div');
searchContainer.style.display = 'flex';
searchContainer.style.alignItems = 'center';
searchContainer.style.width = '100%';
searchContainer.style.maxWidth = '600px';
searchContainer.style.margin = '0 auto';
searchContainer.style.backgroundColor = 'var(--yt-spec-badge-chip-background)';
searchContainer.style.borderRadius = '24px';
searchContainer.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.1)';
searchContainer.style.transition = 'all 0.2s ease';

const input = document.createElement('input');
input.id = 'playlist-search-input';
input.placeholder = 'Filter videos by title or channel (@channel)...';
input.style.flex = '1';
input.style.padding = '12px 16px';
input.style.border = 'none';
input.style.borderRadius = '24px 0 0 24px';
input.style.color = 'var(--yt-spec-text-primary)';
input.style.backgroundColor = 'transparent';
input.style.fontFamily = 'Roboto, sans-serif';
input.style.fontSize = '14px';
input.style.height = '40px';
input.style.boxSizing = 'border-box';
input.style.outline = 'none';
input.style.transition = 'all 0.2s ease';

input.addEventListener('focus', () => {
searchContainer.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.15)';
searchContainer.style.transform = 'translateY(-1px)';
});

input.addEventListener('blur', () => {
searchContainer.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.1)';
searchContainer.style.transform = 'none';
});

input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
button.click();
}
});

const button = document.createElement('button');
button.style.padding = '0';
button.style.width = '48px';
button.style.height = '40px';
button.style.border = 'none';
button.style.borderRadius = '0 24px 24px 0';
button.style.backgroundColor = '#FF0000';
button.style.color = '#fff';
button.style.cursor = 'pointer';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.transition = 'all 0.2s ease';
button.style.outline = 'none';

// Create search icon SVG
const searchIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
searchIcon.setAttribute('viewBox', '0 0 24 24');
searchIcon.setAttribute('width', '20');
searchIcon.setAttribute('height', '20');
searchIcon.style.fill = 'currentColor';
searchIcon.style.transition = 'transform 0.2s ease';

const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
iconPath.setAttribute('d', 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z');

searchIcon.appendChild(iconPath);
button.appendChild(searchIcon);

// Enhanced hover effects
button.addEventListener('mouseover', () => {
button.style.backgroundColor = '#CC0000';
searchIcon.style.transform = 'scale(1.1)';
});

button.addEventListener('mouseout', () => {
button.style.backgroundColor = '#FF0000';
searchIcon.style.transform = 'scale(1)';
});

button.addEventListener('click', () => {
const query = input.value.trim();
if (query) {
filterVideos(query);
button.style.transform = 'scale(0.95)';
setTimeout(() => {
button.style.transform = 'scale(1)';
}, 100);
} else {
resetFilter();
}
});

searchContainer.appendChild(input);
searchContainer.appendChild(button);
container.appendChild(searchContainer);
target.parentNode.insertBefore(container, target);
}

function filterVideos(query) {
const videos = document.querySelectorAll('#contents ytd-playlist-video-renderer');

// Check if searching for channel
const isChannelSearch = query.startsWith('@');
const searchQuery = isChannelSearch ? query.slice(1).toLowerCase() : query.toLowerCase();

videos.forEach(video => {
const title = video.querySelector('#video-title').textContent.toLowerCase();
const channelName = video.querySelector('#channel-name a').textContent.toLowerCase();

let matches;
if (isChannelSearch) {
matches = channelName.includes(searchQuery);
} else {
matches = title.includes(searchQuery);
}

video.style.display = matches ? 'flex' : 'none';
});
}

function resetFilter() {
const videos = document.querySelectorAll('#contents ytd-playlist-video-renderer');
videos.forEach(video => {
video.style.display = 'flex';
});
}

const observer = new MutationObserver(() => {
createSearchBar();
});
observer.observe(document.body, { childList: true, subtree: true });

createSearchBar();
})();

§
Posted: 2024.12.29.

I've updated the UI again.

// ==UserScript==
// @name YouTube Playlist Search Bar
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Adds a search bar to YouTube playlists. Search by title or channel name (using @). Does NOT work with shorts or when playlist video filter is set to "Shorts".
// @match https://www.youtube.com/playlist*
// @author Setnour6
// @grant none
// @license GPL-3.0
// @downloadURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.user.js
// @updateURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.meta.js
// ==/UserScript==

(function() {
'use strict';

function createSearchBar() {
// Prevent duplicate search bars
if (document.getElementById('playlist-search-bar')) return;

// Find the playlist container element
const target = document.querySelector('#page-manager ytd-playlist-video-list-renderer');
if (!target) return;

// Create main container for the search feature
const container = document.createElement('div');
container.id = 'playlist-search-bar';
container.style.cssText = `
display: flex;
align-items: center;
margin: 24px auto 20px;
padding: 0 24px;
width: 100%;
max-width: 800px;
box-sizing: border-box;
transition: all 0.3s ease;
`;

// Create the search box container with modern styling
const searchContainer = document.createElement('div');
searchContainer.style.cssText = `
display: flex;
align-items: center;
width: 100%;
background-color: var(--yt-spec-badge-chip-background);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
`;

// Create the search input field
const input = document.createElement('input');
input.id = 'playlist-search-input';
input.placeholder = 'Search in playlist...';
input.style.cssText = `
flex: 1;
padding: 14px 20px;
border: none;
border-radius: 12px 0 0 12px;
color: var(--yt-spec-text-primary);
background-color: transparent;
font-family: 'YouTube Sans', Roboto, sans-serif;
font-size: 15px;
height: 48px;
box-sizing: border-box;
outline: none;
transition: all 0.3s ease;
`;

// Create the search button
const button = document.createElement('button');
button.style.cssText = `
padding: 0;
width: 56px;
height: 48px;
border: none;
border-radius: 0 12px 12px 0;
background-color: transparent;
color: var(--yt-spec-text-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
outline: none;
`;

// Create and style the search icon SVG
const searchIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
searchIcon.setAttribute('viewBox', '0 0 24 24');
searchIcon.setAttribute('width', '22');
searchIcon.setAttribute('height', '22');
searchIcon.style.cssText = `
fill: currentColor;
opacity: 0.8;
transition: transform 0.2s ease, opacity 0.2s ease;
`;

// Add the SVG path for the search icon
const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
iconPath.setAttribute('d', 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z');

searchIcon.appendChild(iconPath);
button.appendChild(searchIcon);

// Add interactive effects when focusing the search input
input.addEventListener('focus', () => {
searchContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.12)';
searchContainer.style.transform = 'translateY(-1px)';
searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.2)';
});

input.addEventListener('blur', () => {
searchContainer.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
searchContainer.style.transform = 'none';
searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.1)';
});

// Add hover effects for the search button
button.addEventListener('mouseover', () => {
searchIcon.style.opacity = '1';
searchIcon.style.transform = 'scale(1.1)';
});

button.addEventListener('mouseout', () => {
searchIcon.style.opacity = '0.8';
searchIcon.style.transform = 'scale(1)';
});

// Enable search on Enter key press
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
button.click();
}
});

// Handle search button clicks
button.addEventListener('click', () => {
const query = input.value.trim();
if (query) {
filterVideos(query);
// Add click animation
button.style.transform = 'scale(0.92)';
setTimeout(() => button.style.transform = 'scale(1)', 100);
} else {
resetFilter();
}
});

// Assemble all components and insert into the page
searchContainer.appendChild(input);
searchContainer.appendChild(button);
container.appendChild(searchContainer);
target.parentNode.insertBefore(container, target);
}

function filterVideos(query) {
// Get all video elements in the playlist
const videos = document.querySelectorAll('#contents ytd-playlist-video-renderer');

// Check if we're searching for a channel (starts with @)
const isChannelSearch = query.startsWith('@');
const searchQuery = isChannelSearch ? query.slice(1).toLowerCase() : query.toLowerCase();

// Filter videos based on search query
videos.forEach(video => {
const title = video.querySelector('#video-title').textContent.toLowerCase();
const channelName = video.querySelector('#channel-name a').textContent.toLowerCase();

let matches;
if (isChannelSearch) {
matches = channelName.includes(searchQuery);
} else {
matches = title.includes(searchQuery);
}

video.style.display = matches ? 'flex' : 'none';
});
}

function resetFilter() {
// Show all videos when search is cleared
const videos = document.querySelectorAll('#contents ytd-playlist-video-renderer');
videos.forEach(video => {
video.style.display = 'flex';
});
}

// Create a mutation observer to handle YouTube's dynamic page loading
const observer = new MutationObserver(() => {
createSearchBar();
});
observer.observe(document.body, { childList: true, subtree: true });

// Initial creation of search bar
createSearchBar();
})();

§
Posted: 2024.12.29.

I've now updated it again so you don't need to press the search icon or press enter to search, it will do it automatically after you type something new in.

// ==UserScript==
// @name YouTube Playlist Search Bar
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Adds a search bar to YouTube playlists. Search by title or channel name (using @). Does NOT work with shorts or when playlist video filter is set to "Shorts".
// @match https://www.youtube.com/playlist*
// @author Setnour6
// @grant none
// @license GPL-3.0
// @downloadURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.user.js
// @updateURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.meta.js
// ==/UserScript==

(function() {
'use strict';

function createSearchBar() {
// Prevent duplicate search bars
if (document.getElementById('playlist-search-bar')) return;

// Find the playlist container element
const target = document.querySelector('#page-manager ytd-playlist-video-list-renderer');
if (!target) return;

// Create main container for the search feature
const container = document.createElement('div');
container.id = 'playlist-search-bar';
container.style.cssText = `
display: flex;
align-items: center;
margin: 24px auto 20px;
padding: 0 24px;
width: 100%;
max-width: 800px;
box-sizing: border-box;
transition: all 0.3s ease;
`;

// Create the search box container with modern styling
const searchContainer = document.createElement('div');
searchContainer.style.cssText = `
display: flex;
align-items: center;
width: 100%;
background-color: var(--yt-spec-badge-chip-background);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
`;

// Create the search input field with updated styling
const input = document.createElement('input');
input.id = 'playlist-search-input';
input.placeholder = 'Search in playlist...';
input.style.cssText = `
flex: 1;
padding: 14px 20px;
border: none;
border-radius: 12px;
color: var(--yt-spec-text-primary);
background-color: transparent;
font-family: 'YouTube Sans', Roboto, sans-serif;
font-size: 15px;
height: 48px;
box-sizing: border-box;
outline: none;
transition: all 0.3s ease;
`;

// Add interactive effects when focusing the search input
input.addEventListener('focus', () => {
searchContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.12)';
searchContainer.style.transform = 'translateY(-1px)';
searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.2)';
});

input.addEventListener('blur', () => {
searchContainer.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
searchContainer.style.transform = 'none';
searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.1)';
});

// Add real-time search
input.addEventListener('input', (e) => {
const query = e.target.value.trim();
if (query) {
filterVideos(query);
} else {
resetFilter();
}
});

// Assemble components
searchContainer.appendChild(input);
container.appendChild(searchContainer);
target.parentNode.insertBefore(container, target);
}

let originalVideos = []; // Store original video elements

function filterVideos(query) {
// Get all video elements in the playlist
const videos = document.querySelectorAll('#contents ytd-playlist-video-renderer');
const playlistContents = document.querySelector('#contents.ytd-playlist-video-list-renderer');

// Store original videos if not already stored
if (originalVideos.length === 0) {
originalVideos = Array.from(videos);
}

// Check if we're searching for a channel (starts with @)
const isChannelSearch = query.startsWith('@');
const searchQuery = isChannelSearch ? query.slice(1).toLowerCase() : query.toLowerCase();

// Store matching videos
const matchingVideos = originalVideos.filter(video => {
const title = video.querySelector('#video-title').textContent.toLowerCase();
const channelName = video.querySelector('#channel-name a').textContent.toLowerCase();

return isChannelSearch ?
channelName.includes(searchQuery) :
title.includes(searchQuery);
});

// Remove all videos from the playlist
while (playlistContents.firstChild) {
playlistContents.removeChild(playlistContents.firstChild);
}

// Add only matching videos back
matchingVideos.forEach(video => {
playlistContents.appendChild(video);
});
}

function resetFilter() {
const playlistContents = document.querySelector('#contents.ytd-playlist-video-list-renderer');

// Remove current videos
while (playlistContents.firstChild) {
playlistContents.removeChild(playlistContents.firstChild);
}

// Restore original videos
originalVideos.forEach(video => {
playlistContents.appendChild(video);
});
}

// Create a mutation observer to handle YouTube's dynamic page loading
const observer = new MutationObserver(() => {
createSearchBar();
});
observer.observe(document.body, { childList: true, subtree: true });

// Initial creation of search bar
createSearchBar();
})();

§
Posted: 2024.12.29.

Here is the code improved again with typed.js in the input box and the code has been cleaned up.

// ==UserScript==
// @name YouTube Playlist Search Bar
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Adds a search bar to YouTube playlists. Search by title or channel name (using @). Does NOT work with shorts or when playlist video filter is set to "Shorts".
// @match https://www.youtube.com/playlist*
// @author Setnour6
// @grant none
// @license GPL-3.0
// @downloadURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.user.js
// @updateURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.meta.js
// ==/UserScript==

(function() {
'use strict';

// Array of placeholder texts that will be animated in the search input
const PLACEHOLDERS = [
'Search in playlist...',
'Type @ to search by channel...',
'Find your favorite videos...',
'Search through your collection...',
'Looking for something specific?',
'Filter playlist content...'
];

// CSS styles for the search bar components
const STYLES = {
container: `
display: flex;
align-items: center;
margin: 24px auto 20px;
padding: 0 24px;
width: 100%;
max-width: 800px;
box-sizing: border-box;
transition: all 0.3s ease;
`,
searchContainer: `
display: flex;
align-items: center;
width: 100%;
background-color: var(--yt-spec-badge-chip-background);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
`,
input: `
flex: 1;
padding: 14px 20px;
border: none;
border-radius: 12px;
color: var(--yt-spec-text-primary);
background-color: transparent;
font-family: 'YouTube Sans', Roboto, sans-serif;
font-size: 15px;
height: 48px;
box-sizing: border-box;
outline: none;
transition: all 0.3s ease;
`
};

// Global variables to track state
let originalVideos = []; // Stores the original list of videos before filtering
let isTyping = true; // Controls typing animation direction (typing vs deleting)
let isAnimating = false; // Prevents multiple animations from running simultaneously
let currentPlaceholderIndex = 0; // Tracks which placeholder text is currently being shown

// Handles the animated typing effect for placeholder text
async function animatePlaceholder(input) {
if (isAnimating) return;
isAnimating = true;

while (document.getElementById('playlist-search-input') && !input.value && !input.matches(':focus')) {
const placeholder = PLACEHOLDERS[currentPlaceholderIndex];

if (isTyping) {
// Typing effect
input.placeholder = ''; // Clear before starting
for (let i = 0; i <= placeholder.length; i++) {
if (!document.getElementById('playlist-search-input') || input.value || input.matches(':focus')) {
isAnimating = false;
return;
}
input.placeholder = placeholder.slice(0, i);
await new Promise(resolve => setTimeout(resolve, 50));
}
await new Promise(resolve => setTimeout(resolve, 2000));
isTyping = false;
} else {
// Deleting effect
for (let i = placeholder.length; i >= 0; i--) {
if (!document.getElementById('playlist-search-input') || input.value || input.matches(':focus')) {
isAnimating = false;
return;
}
input.placeholder = placeholder.slice(0, i);
await new Promise(resolve => setTimeout(resolve, 30));
}
currentPlaceholderIndex = (currentPlaceholderIndex + 1) % PLACEHOLDERS.length;
isTyping = true;
await new Promise(resolve => setTimeout(resolve, 500));
}
}

isAnimating = false;
}

// Handles visual changes when the search input is focused
function handleFocus(searchContainer, input) {
searchContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.12)';
searchContainer.style.transform = 'translateY(-1px)';
searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.2)';
input.placeholder = '';
}

// Handles visual changes when the search input loses focus
function handleBlur(searchContainer, input) {
searchContainer.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
searchContainer.style.transform = 'none';
searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.1)';

// Only restart animation if the input is empty
if (!input.value) {
// Reset all animation states
isAnimating = false;
isTyping = true;
currentPlaceholderIndex = 0;
input.placeholder = '';
setTimeout(() => {
if (!input.value && !input.matches(':focus')) {
animatePlaceholder(input);
}
}, 500); // Delay restart of animation
}
}

// Filters the playlist videos based on search query
function filterVideos(query) {
const playlistContents = document.querySelector('#contents.ytd-playlist-video-list-renderer');
if (!playlistContents) return;

// Store original videos on first search
if (originalVideos.length === 0) {
originalVideos = Array.from(document.querySelectorAll('#contents ytd-playlist-video-renderer'));
}

// Check if searching by channel name (using @) or video title
const isChannelSearch = query.startsWith('@');
const searchQuery = isChannelSearch ? query.slice(1).toLowerCase() : query.toLowerCase();

// Filter videos based on search criteria
const matchingVideos = originalVideos.filter(video => {
const title = video.querySelector('#video-title').textContent.toLowerCase();
const channelName = video.querySelector('#channel-name a').textContent.toLowerCase();
return isChannelSearch ? channelName.includes(searchQuery) : title.includes(searchQuery);
});

updatePlaylistContents(playlistContents, matchingVideos);
}

// Resets the playlist to show all videos
function resetFilter() {
const playlistContents = document.querySelector('#contents.ytd-playlist-video-list-renderer');
if (!playlistContents) return;
updatePlaylistContents(playlistContents, originalVideos);
}

// Updates the playlist container with filtered videos
function updatePlaylistContents(container, videos) {
container.replaceChildren(...videos);
}

// Creates and injects the search bar into the YouTube playlist page
function createSearchBar() {
// Prevent duplicate search bars
if (document.getElementById('playlist-search-bar')) return;

const target = document.querySelector('#page-manager ytd-playlist-video-list-renderer');
if (!target) return;

const container = document.createElement('div');
container.id = 'playlist-search-bar';
container.style.cssText = STYLES.container;

const searchContainer = document.createElement('div');
searchContainer.style.cssText = STYLES.searchContainer;

const input = document.createElement('input');
input.id = 'playlist-search-input';
input.placeholder = '';
input.style.cssText = STYLES.input;

input.addEventListener('focus', () => handleFocus(searchContainer, input));
input.addEventListener('blur', () => handleBlur(searchContainer, input));
input.addEventListener('input', (e) => {
const query = e.target.value.trim();
query ? filterVideos(query) : resetFilter();
});

searchContainer.appendChild(input);
container.appendChild(searchContainer);
target.parentNode.insertBefore(container, target);

requestAnimationFrame(() => animatePlaceholder(input));
}

// Initialize the script
// Use MutationObserver to handle YouTube's dynamic page loading
const observer = new MutationObserver(createSearchBar);
observer.observe(document.body, { childList: true, subtree: true });
createSearchBar();
})();

§
Posted: 2024.12.29.

Sorry for all the replies but I'm just doing them as Im adding stuff😂 This time I've made it so the search works smoother and is not as stuttery.

// ==UserScript==
// @name YouTube Playlist Search Bar
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Adds a search bar to YouTube playlists. Search by title or channel name (using @). Does NOT work with shorts or when playlist video filter is set to "Shorts".
// @match https://www.youtube.com/playlist*
// @author Setnour6
// @grant none
// @license GPL-3.0
// @downloadURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.user.js
// @updateURL https://update.greasyfork.org/scripts/517253/YouTube%20Playlist%20Search%20Bar.meta.js
// ==/UserScript==

(function() {
'use strict';

// Array of placeholder texts that will be animated in the search input
const PLACEHOLDERS = [
'Search in playlist...',
'Type @ to search by channel...',
'Find your favorite videos...',
'Search through your collection...',
'Looking for something specific?',
'Filter playlist content...'
];

// CSS styles for the search bar components
const STYLES = {
container: `
display: flex;
align-items: center;
margin: 24px auto 20px;
padding: 0 24px;
width: 100%;
max-width: 800px;
box-sizing: border-box;
transition: all 0.3s ease;
`,
searchContainer: `
display: flex;
align-items: center;
width: 100%;
background-color: var(--yt-spec-badge-chip-background);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
`,
input: `
flex: 1;
padding: 14px 20px;
border: none;
border-radius: 12px;
color: var(--yt-spec-text-primary);
background-color: transparent;
font-family: 'YouTube Sans', Roboto, sans-serif;
font-size: 15px;
height: 48px;
box-sizing: border-box;
outline: none;
transition: all 0.3s ease;
`
};

// Global variables to track state
let originalVideos = []; // Stores the original list of videos before filtering
let isTyping = true; // Controls typing animation direction (typing vs deleting)
let isAnimating = false; // Prevents multiple animations from running simultaneously
let currentPlaceholderIndex = 0; // Tracks which placeholder text is currently being shown

// Handles the animated typing effect for placeholder text
async function animatePlaceholder(input) {
if (isAnimating) return;
isAnimating = true;

while (document.getElementById('playlist-search-input') && !input.value && !input.matches(':focus')) {
const placeholder = PLACEHOLDERS[currentPlaceholderIndex];

if (isTyping) {
// Typing effect
input.placeholder = ''; // Clear before starting
for (let i = 0; i <= placeholder.length; i++) {
if (!document.getElementById('playlist-search-input') || input.value || input.matches(':focus')) {
isAnimating = false;
return;
}
input.placeholder = placeholder.slice(0, i);
await new Promise(resolve => setTimeout(resolve, 50));
}
await new Promise(resolve => setTimeout(resolve, 2000));
isTyping = false;
} else {
// Deleting effect
for (let i = placeholder.length; i >= 0; i--) {
if (!document.getElementById('playlist-search-input') || input.value || input.matches(':focus')) {
isAnimating = false;
return;
}
input.placeholder = placeholder.slice(0, i);
await new Promise(resolve => setTimeout(resolve, 30));
}
currentPlaceholderIndex = (currentPlaceholderIndex + 1) % PLACEHOLDERS.length;
isTyping = true;
await new Promise(resolve => setTimeout(resolve, 500));
}
}

isAnimating = false;
}

// Handles visual changes when the search input is focused
function handleFocus(searchContainer, input) {
searchContainer.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.12)';
searchContainer.style.transform = 'translateY(-1px)';
searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.2)';
input.placeholder = '';
}

// Handles visual changes when the search input loses focus
function handleBlur(searchContainer, input) {
searchContainer.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
searchContainer.style.transform = 'none';
searchContainer.style.borderColor = 'rgba(255, 255, 255, 0.1)';

// Only restart animation if the input is empty
if (!input.value) {
// Reset all animation states
isAnimating = false;
isTyping = true;
currentPlaceholderIndex = 0;
input.placeholder = '';
setTimeout(() => {
if (!input.value && !input.matches(':focus')) {
animatePlaceholder(input);
}
}, 500); // Delay restart of animation
}
}

// Filters the playlist videos based on search query
function filterVideos(query) {
const playlistContents = document.querySelector('#contents.ytd-playlist-video-list-renderer');
if (!playlistContents) return;

// Store original videos on first search
if (originalVideos.length === 0) {
originalVideos = Array.from(document.querySelectorAll('#contents ytd-playlist-video-renderer'));
}

// Check if searching by channel name (using @) or video title
const isChannelSearch = query.startsWith('@');
const searchQuery = isChannelSearch ? query.slice(1).toLowerCase() : query.toLowerCase();

// Filter videos based on search criteria
const matchingVideos = originalVideos.filter(video => {
const title = video.querySelector('#video-title').textContent.toLowerCase();
const channelName = video.querySelector('#channel-name a').textContent.toLowerCase();
return isChannelSearch ? channelName.includes(searchQuery) : title.includes(searchQuery);
});

updatePlaylistContents(playlistContents, matchingVideos);
}

// Resets the playlist to show all videos
function resetFilter() {
const playlistContents = document.querySelector('#contents.ytd-playlist-video-list-renderer');
if (!playlistContents) return;
updatePlaylistContents(playlistContents, originalVideos);
}

// Updates the playlist container with filtered videos
function updatePlaylistContents(container, videos) {
// Clear the container first
container.textContent = '';

// Process videos in batches
const BATCH_SIZE = 10;
let currentIndex = 0;

function processBatch() {
const batch = videos.slice(currentIndex, currentIndex + BATCH_SIZE);
if (batch.length === 0) return;

requestAnimationFrame(() => {
batch.forEach(video => container.appendChild(video));
currentIndex += BATCH_SIZE;

// Schedule next batch
if (currentIndex < videos.length) {
setTimeout(processBatch, 16); // Roughly aims for 60fps
}
});
}

processBatch();
}

// Creates and injects the search bar into the YouTube playlist page
function createSearchBar() {
// Prevent duplicate search bars
if (document.getElementById('playlist-search-bar')) return;

const target = document.querySelector('#page-manager ytd-playlist-video-list-renderer');
if (!target) return;

const container = document.createElement('div');
container.id = 'playlist-search-bar';
container.style.cssText = STYLES.container;

const searchContainer = document.createElement('div');
searchContainer.style.cssText = STYLES.searchContainer;

const input = document.createElement('input');
input.id = 'playlist-search-input';
input.placeholder = '';
input.style.cssText = STYLES.input;

input.addEventListener('focus', () => handleFocus(searchContainer, input));
input.addEventListener('blur', () => handleBlur(searchContainer, input));
input.addEventListener('input', (e) => {
const query = e.target.value.trim();
query ? filterVideos(query) : resetFilter();
});

searchContainer.appendChild(input);
container.appendChild(searchContainer);
target.parentNode.insertBefore(container, target);

requestAnimationFrame(() => animatePlaceholder(input));
}

// Initialize the script
// Use MutationObserver to handle YouTube's dynamic page loading
const observer = new MutationObserver(createSearchBar);
observer.observe(document.body, { childList: true, subtree: true });
createSearchBar();
})();

Post reply

Sign in to post a reply.