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".

< Opinie na YouTube Playlist Search Bar

Ocena: Dobry - skrypt działa

§
Napisano: 29-12-2024

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();
})();

§
Napisano: 29-12-2024

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();
})();

§
Napisano: 29-12-2024

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();
})();

§
Napisano: 29-12-2024

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();
})();

§
Napisano: 29-12-2024

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();
})();

Odpowiedz

Zaloguj się, by odpowiedzieć.