YouTube Playlist Organizer

Organize, sort, filter, and search your YouTube playlists with powerful tools

Size

23.3 KB

Version

1.0.1

Created

Nov 21, 2025

Updated

26 days ago

1// ==UserScript==
2// @name		YouTube Playlist Organizer
3// @description		Organize, sort, filter, and search your YouTube playlists with powerful tools
4// @version		1.0.1
5// @match		https://*.youtube.com/*
6// @icon		https://www.youtube.com/s/desktop/bbe055b3/img/favicon_32x32.png
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('YouTube Playlist Organizer loaded');
12
13    // Utility function to debounce
14    function debounce(func, wait) {
15        let timeout;
16        return function executedFunction(...args) {
17            const later = () => {
18                clearTimeout(timeout);
19                func(...args);
20            };
21            clearTimeout(timeout);
22            timeout = setTimeout(later, wait);
23        };
24    }
25
26    // Main initialization
27    async function init() {
28        console.log('Initializing YouTube Playlist Organizer');
29        
30        // Wait for playlist page to load
31        if (!window.location.href.includes('/playlist?list=')) {
32            console.log('Not on a playlist page, waiting for navigation');
33            observeNavigation();
34            return;
35        }
36
37        // Wait for playlist content to be available
38        waitForPlaylistContent();
39    }
40
41    // Observe navigation changes
42    function observeNavigation() {
43        let lastUrl = location.href;
44        const observer = new MutationObserver(debounce(() => {
45            const currentUrl = location.href;
46            if (currentUrl !== lastUrl) {
47                lastUrl = currentUrl;
48                if (currentUrl.includes('/playlist?list=')) {
49                    console.log('Navigated to playlist page');
50                    waitForPlaylistContent();
51                }
52            }
53        }, 500));
54
55        observer.observe(document.body, {
56            childList: true,
57            subtree: true
58        });
59    }
60
61    // Wait for playlist content to load
62    function waitForPlaylistContent() {
63        const checkInterval = setInterval(() => {
64            const playlistVideos = document.querySelectorAll('ytd-playlist-video-renderer');
65            const playlistHeader = document.querySelector('ytd-playlist-header-renderer');
66            
67            if (playlistVideos.length > 0 && playlistHeader) {
68                clearInterval(checkInterval);
69                console.log('Playlist content loaded, creating organizer UI');
70                createOrganizerUI();
71            }
72        }, 500);
73
74        // Stop checking after 10 seconds
75        setTimeout(() => clearInterval(checkInterval), 10000);
76    }
77
78    // Create the organizer UI
79    function createOrganizerUI() {
80        // Remove existing organizer if present
81        const existingOrganizer = document.getElementById('playlist-organizer-panel');
82        if (existingOrganizer) {
83            existingOrganizer.remove();
84        }
85
86        // Find the playlist header actions area
87        const playlistHeader = document.querySelector('ytd-playlist-header-renderer .immersive-header-content');
88        if (!playlistHeader) {
89            console.error('Could not find playlist header');
90            return;
91        }
92
93        // Create organizer panel
94        const organizerPanel = document.createElement('div');
95        organizerPanel.id = 'playlist-organizer-panel';
96        organizerPanel.innerHTML = `
97            <div class="organizer-container">
98                <div class="organizer-header">
99                    <h3>šŸŽµ Playlist Organizer</h3>
100                    <button id="toggle-organizer" class="organizer-btn">ā–¼</button>
101                </div>
102                <div id="organizer-content" class="organizer-content">
103                    <div class="organizer-section">
104                        <label>šŸ” Search Videos:</label>
105                        <input type="text" id="search-videos" placeholder="Search by title, channel..." />
106                        <span id="search-results-count"></span>
107                    </div>
108                    
109                    <div class="organizer-section">
110                        <label>šŸ“Š Sort By:</label>
111                        <select id="sort-videos">
112                            <option value="default">Default Order</option>
113                            <option value="title-asc">Title (A-Z)</option>
114                            <option value="title-desc">Title (Z-A)</option>
115                            <option value="channel-asc">Channel (A-Z)</option>
116                            <option value="channel-desc">Channel (Z-A)</option>
117                            <option value="duration-asc">Duration (Shortest)</option>
118                            <option value="duration-desc">Duration (Longest)</option>
119                        </select>
120                    </div>
121                    
122                    <div class="organizer-section">
123                        <label>šŸŽ¬ Filter By:</label>
124                        <select id="filter-videos">
125                            <option value="all">Show All</option>
126                            <option value="watched">Watched Only</option>
127                            <option value="unwatched">Unwatched Only</option>
128                            <option value="short">Short Videos (&lt;5 min)</option>
129                            <option value="medium">Medium Videos (5-20 min)</option>
130                            <option value="long">Long Videos (&gt;20 min)</option>
131                        </select>
132                    </div>
133                    
134                    <div class="organizer-section">
135                        <label>⚔ Quick Actions:</label>
136                        <div class="action-buttons">
137                            <button id="show-all-btn" class="action-btn">Show All</button>
138                            <button id="highlight-duplicates-btn" class="action-btn">Find Duplicates</button>
139                            <button id="export-playlist-btn" class="action-btn">Export List</button>
140                            <button id="stats-btn" class="action-btn">View Stats</button>
141                        </div>
142                    </div>
143                    
144                    <div id="stats-display" class="stats-display" style="display: none;"></div>
145                </div>
146            </div>
147        `;
148
149        // Insert after playlist header
150        playlistHeader.parentNode.insertBefore(organizerPanel, playlistHeader.nextSibling);
151
152        // Add styles
153        addStyles();
154
155        // Attach event listeners
156        attachEventListeners();
157
158        console.log('Organizer UI created successfully');
159    }
160
161    // Add CSS styles
162    function addStyles() {
163        const styles = `
164            #playlist-organizer-panel {
165                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
166                border-radius: 12px;
167                padding: 20px;
168                margin: 20px 0;
169                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
170                font-family: 'Roboto', Arial, sans-serif;
171            }
172            
173            .organizer-container {
174                max-width: 100%;
175            }
176            
177            .organizer-header {
178                display: flex;
179                justify-content: space-between;
180                align-items: center;
181                margin-bottom: 15px;
182            }
183            
184            .organizer-header h3 {
185                color: #ffffff;
186                margin: 0;
187                font-size: 20px;
188                font-weight: 600;
189            }
190            
191            #toggle-organizer {
192                background: rgba(255, 255, 255, 0.2);
193                border: none;
194                color: #ffffff;
195                padding: 8px 12px;
196                border-radius: 6px;
197                cursor: pointer;
198                font-size: 16px;
199                transition: all 0.3s ease;
200            }
201            
202            #toggle-organizer:hover {
203                background: rgba(255, 255, 255, 0.3);
204                transform: scale(1.05);
205            }
206            
207            .organizer-content {
208                display: block;
209                animation: slideDown 0.3s ease;
210            }
211            
212            .organizer-content.collapsed {
213                display: none;
214            }
215            
216            @keyframes slideDown {
217                from {
218                    opacity: 0;
219                    transform: translateY(-10px);
220                }
221                to {
222                    opacity: 1;
223                    transform: translateY(0);
224                }
225            }
226            
227            .organizer-section {
228                background: rgba(255, 255, 255, 0.95);
229                padding: 15px;
230                border-radius: 8px;
231                margin-bottom: 12px;
232            }
233            
234            .organizer-section label {
235                display: block;
236                color: #333;
237                font-weight: 600;
238                margin-bottom: 8px;
239                font-size: 14px;
240            }
241            
242            .organizer-section input,
243            .organizer-section select {
244                width: 100%;
245                padding: 10px;
246                border: 2px solid #e0e0e0;
247                border-radius: 6px;
248                font-size: 14px;
249                transition: border-color 0.3s ease;
250                box-sizing: border-box;
251            }
252            
253            .organizer-section input:focus,
254            .organizer-section select:focus {
255                outline: none;
256                border-color: #667eea;
257            }
258            
259            #search-results-count {
260                display: block;
261                color: #666;
262                font-size: 12px;
263                margin-top: 5px;
264            }
265            
266            .action-buttons {
267                display: grid;
268                grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
269                gap: 8px;
270            }
271            
272            .action-btn {
273                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
274                color: #ffffff;
275                border: none;
276                padding: 10px 15px;
277                border-radius: 6px;
278                cursor: pointer;
279                font-size: 13px;
280                font-weight: 600;
281                transition: all 0.3s ease;
282                white-space: nowrap;
283            }
284            
285            .action-btn:hover {
286                transform: translateY(-2px);
287                box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
288            }
289            
290            .action-btn:active {
291                transform: translateY(0);
292            }
293            
294            .stats-display {
295                background: rgba(255, 255, 255, 0.95);
296                padding: 15px;
297                border-radius: 8px;
298                margin-top: 12px;
299            }
300            
301            .stats-display h4 {
302                color: #333;
303                margin: 0 0 10px 0;
304                font-size: 16px;
305            }
306            
307            .stats-display p {
308                color: #666;
309                margin: 5px 0;
310                font-size: 14px;
311            }
312            
313            .video-hidden {
314                display: none !important;
315            }
316            
317            .video-duplicate {
318                border: 3px solid #ff4444 !important;
319                background: rgba(255, 68, 68, 0.1) !important;
320            }
321        `;
322
323        TM_addStyle(styles);
324    }
325
326    // Attach event listeners
327    function attachEventListeners() {
328        // Toggle organizer
329        const toggleBtn = document.getElementById('toggle-organizer');
330        const content = document.getElementById('organizer-content');
331        
332        toggleBtn.addEventListener('click', () => {
333            content.classList.toggle('collapsed');
334            toggleBtn.textContent = content.classList.contains('collapsed') ? 'ā–¶' : 'ā–¼';
335        });
336
337        // Search functionality
338        const searchInput = document.getElementById('search-videos');
339        searchInput.addEventListener('input', debounce(() => {
340            searchVideos(searchInput.value);
341        }, 300));
342
343        // Sort functionality
344        const sortSelect = document.getElementById('sort-videos');
345        sortSelect.addEventListener('change', () => {
346            sortVideos(sortSelect.value);
347        });
348
349        // Filter functionality
350        const filterSelect = document.getElementById('filter-videos');
351        filterSelect.addEventListener('change', () => {
352            filterVideos(filterSelect.value);
353        });
354
355        // Show all button
356        document.getElementById('show-all-btn').addEventListener('click', () => {
357            showAllVideos();
358        });
359
360        // Highlight duplicates
361        document.getElementById('highlight-duplicates-btn').addEventListener('click', () => {
362            highlightDuplicates();
363        });
364
365        // Export playlist
366        document.getElementById('export-playlist-btn').addEventListener('click', () => {
367            exportPlaylist();
368        });
369
370        // View stats
371        document.getElementById('stats-btn').addEventListener('click', () => {
372            showStats();
373        });
374    }
375
376    // Search videos
377    function searchVideos(query) {
378        const videos = document.querySelectorAll('ytd-playlist-video-renderer');
379        const searchQuery = query.toLowerCase().trim();
380        let visibleCount = 0;
381
382        videos.forEach(video => {
383            const titleElement = video.querySelector('#video-title');
384            const channelElement = video.querySelector('#channel-name a');
385            
386            const title = titleElement ? titleElement.textContent.toLowerCase() : '';
387            const channel = channelElement ? channelElement.textContent.toLowerCase() : '';
388
389            if (searchQuery === '' || title.includes(searchQuery) || channel.includes(searchQuery)) {
390                video.classList.remove('video-hidden');
391                visibleCount++;
392            } else {
393                video.classList.add('video-hidden');
394            }
395        });
396
397        const resultsCount = document.getElementById('search-results-count');
398        if (searchQuery) {
399            resultsCount.textContent = `Showing ${visibleCount} of ${videos.length} videos`;
400        } else {
401            resultsCount.textContent = '';
402        }
403
404        console.log(`Search: "${query}" - ${visibleCount} results`);
405    }
406
407    // Sort videos
408    function sortVideos(sortType) {
409        const container = document.querySelector('ytd-playlist-video-list-renderer #contents');
410        if (!container) {
411            console.error('Could not find playlist container');
412            return;
413        }
414
415        const videos = Array.from(document.querySelectorAll('ytd-playlist-video-renderer'));
416        
417        if (sortType === 'default') {
418            console.log('Restoring default order');
419            return;
420        }
421
422        videos.sort((a, b) => {
423            if (sortType.startsWith('title')) {
424                const titleA = a.querySelector('#video-title')?.textContent.trim().toLowerCase() || '';
425                const titleB = b.querySelector('#video-title')?.textContent.trim().toLowerCase() || '';
426                return sortType === 'title-asc' ? titleA.localeCompare(titleB) : titleB.localeCompare(titleA);
427            } else if (sortType.startsWith('channel')) {
428                const channelA = a.querySelector('#channel-name a')?.textContent.trim().toLowerCase() || '';
429                const channelB = b.querySelector('#channel-name a')?.textContent.trim().toLowerCase() || '';
430                return sortType === 'channel-asc' ? channelA.localeCompare(channelB) : channelB.localeCompare(channelA);
431            } else if (sortType.startsWith('duration')) {
432                const durationA = parseDuration(a.querySelector('.ytd-thumbnail-overlay-time-status-renderer')?.textContent.trim() || '0:00');
433                const durationB = parseDuration(b.querySelector('.ytd-thumbnail-overlay-time-status-renderer')?.textContent.trim() || '0:00');
434                return sortType === 'duration-asc' ? durationA - durationB : durationB - durationA;
435            }
436            return 0;
437        });
438
439        // Re-append videos in sorted order
440        videos.forEach(video => container.appendChild(video));
441        
442        console.log(`Sorted by: ${sortType}`);
443    }
444
445    // Parse duration string to seconds
446    function parseDuration(durationStr) {
447        const parts = durationStr.split(':').map(p => parseInt(p) || 0);
448        if (parts.length === 3) {
449            return parts[0] * 3600 + parts[1] * 60 + parts[2];
450        } else if (parts.length === 2) {
451            return parts[0] * 60 + parts[1];
452        }
453        return 0;
454    }
455
456    // Filter videos
457    function filterVideos(filterType) {
458        const videos = document.querySelectorAll('ytd-playlist-video-renderer');
459
460        videos.forEach(video => {
461            let shouldShow = true;
462
463            if (filterType === 'all') {
464                shouldShow = true;
465            } else if (filterType === 'watched') {
466                const progressBar = video.querySelector('#progress');
467                shouldShow = progressBar && progressBar.style.width && progressBar.style.width !== '0%';
468            } else if (filterType === 'unwatched') {
469                const progressBar = video.querySelector('#progress');
470                shouldShow = !progressBar || !progressBar.style.width || progressBar.style.width === '0%';
471            } else if (filterType === 'short' || filterType === 'medium' || filterType === 'long') {
472                const durationStr = video.querySelector('.ytd-thumbnail-overlay-time-status-renderer')?.textContent.trim() || '0:00';
473                const durationSeconds = parseDuration(durationStr);
474                
475                if (filterType === 'short') {
476                    shouldShow = durationSeconds < 300; // Less than 5 minutes
477                } else if (filterType === 'medium') {
478                    shouldShow = durationSeconds >= 300 && durationSeconds <= 1200; // 5-20 minutes
479                } else if (filterType === 'long') {
480                    shouldShow = durationSeconds > 1200; // More than 20 minutes
481                }
482            }
483
484            if (shouldShow) {
485                video.classList.remove('video-hidden');
486            } else {
487                video.classList.add('video-hidden');
488            }
489        });
490
491        console.log(`Filtered by: ${filterType}`);
492    }
493
494    // Show all videos
495    function showAllVideos() {
496        const videos = document.querySelectorAll('ytd-playlist-video-renderer');
497        videos.forEach(video => {
498            video.classList.remove('video-hidden', 'video-duplicate');
499        });
500
501        // Reset controls
502        document.getElementById('search-videos').value = '';
503        document.getElementById('sort-videos').value = 'default';
504        document.getElementById('filter-videos').value = 'all';
505        document.getElementById('search-results-count').textContent = '';
506
507        console.log('Showing all videos');
508    }
509
510    // Highlight duplicate videos
511    function highlightDuplicates() {
512        const videos = document.querySelectorAll('ytd-playlist-video-renderer');
513        const videoTitles = new Map();
514        let duplicateCount = 0;
515
516        // First pass: collect all titles
517        videos.forEach(video => {
518            const titleElement = video.querySelector('#video-title');
519            if (titleElement) {
520                const title = titleElement.textContent.trim().toLowerCase();
521                if (!videoTitles.has(title)) {
522                    videoTitles.set(title, []);
523                }
524                videoTitles.get(title).push(video);
525            }
526        });
527
528        // Second pass: highlight duplicates
529        videoTitles.forEach((videoList, title) => {
530            if (videoList.length > 1) {
531                videoList.forEach(video => {
532                    video.classList.add('video-duplicate');
533                    duplicateCount++;
534                });
535            }
536        });
537
538        if (duplicateCount > 0) {
539            alert(`Found ${duplicateCount} duplicate videos! They are now highlighted in red.`);
540        } else {
541            alert('No duplicate videos found in this playlist.');
542        }
543
544        console.log(`Found ${duplicateCount} duplicate videos`);
545    }
546
547    // Export playlist
548    function exportPlaylist() {
549        const videos = document.querySelectorAll('ytd-playlist-video-renderer');
550        const playlistData = [];
551
552        videos.forEach((video, index) => {
553            const titleElement = video.querySelector('#video-title');
554            const channelElement = video.querySelector('#channel-name a');
555            const durationElement = video.querySelector('.ytd-thumbnail-overlay-time-status-renderer');
556            const linkElement = video.querySelector('#thumbnail');
557
558            playlistData.push({
559                index: index + 1,
560                title: titleElement?.textContent.trim() || 'Unknown',
561                channel: channelElement?.textContent.trim() || 'Unknown',
562                duration: durationElement?.textContent.trim() || 'Unknown',
563                url: linkElement?.href || ''
564            });
565        });
566
567        // Create CSV content
568        let csvContent = 'Index,Title,Channel,Duration,URL\n';
569        playlistData.forEach(video => {
570            csvContent += `${video.index},"${video.title.replace(/"/g, '""')}","${video.channel}","${video.duration}","${video.url}"\n`;
571        });
572
573        // Download CSV
574        const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
575        const link = document.createElement('a');
576        const url = URL.createObjectURL(blob);
577        link.setAttribute('href', url);
578        link.setAttribute('download', `youtube_playlist_${Date.now()}.csv`);
579        link.style.visibility = 'hidden';
580        document.body.appendChild(link);
581        link.click();
582        document.body.removeChild(link);
583
584        console.log('Playlist exported successfully');
585    }
586
587    // Show playlist statistics
588    function showStats() {
589        const statsDisplay = document.getElementById('stats-display');
590        const videos = document.querySelectorAll('ytd-playlist-video-renderer');
591        
592        let totalDuration = 0;
593        const channels = new Set();
594        let watchedCount = 0;
595
596        videos.forEach(video => {
597            const durationStr = video.querySelector('.ytd-thumbnail-overlay-time-status-renderer')?.textContent.trim() || '0:00';
598            totalDuration += parseDuration(durationStr);
599
600            const channelElement = video.querySelector('#channel-name a');
601            if (channelElement) {
602                channels.add(channelElement.textContent.trim());
603            }
604
605            const progressBar = video.querySelector('#progress');
606            if (progressBar && progressBar.style.width && progressBar.style.width !== '0%') {
607                watchedCount++;
608            }
609        });
610
611        const hours = Math.floor(totalDuration / 3600);
612        const minutes = Math.floor((totalDuration % 3600) / 60);
613
614        statsDisplay.innerHTML = `
615            <h4>šŸ“Š Playlist Statistics</h4>
616            <p><strong>Total Videos:</strong> ${videos.length}</p>
617            <p><strong>Total Duration:</strong> ${hours}h ${minutes}m</p>
618            <p><strong>Unique Channels:</strong> ${channels.size}</p>
619            <p><strong>Watched:</strong> ${watchedCount} (${Math.round(watchedCount / videos.length * 100)}%)</p>
620            <p><strong>Unwatched:</strong> ${videos.length - watchedCount}</p>
621        `;
622
623        statsDisplay.style.display = 'block';
624        console.log('Stats displayed');
625    }
626
627    // Start the extension
628    TM_runBody(init);
629
630})();
YouTube Playlist Organizer | Robomonkey