YouTube Playlist Organizer

Organize and categorize your YouTube playlists with custom tags, search, and filtering

Size

29.8 KB

Version

1.0.1

Created

Nov 21, 2025

Updated

26 days ago

1// ==UserScript==
2// @name		YouTube Playlist Organizer
3// @description		Organize and categorize your YouTube playlists with custom tags, search, and filtering
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 initialized');
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 function
27    async function init() {
28        console.log('Starting YouTube Playlist Organizer...');
29        
30        // Wait for the page to be ready
31        if (document.readyState === 'loading') {
32            document.addEventListener('DOMContentLoaded', setupOrganizer);
33        } else {
34            setupOrganizer();
35        }
36    }
37
38    async function setupOrganizer() {
39        // Check if we're on a playlist page
40        const isPlaylistPage = window.location.href.includes('/playlist?list=');
41        
42        if (!isPlaylistPage) {
43            console.log('Not on a playlist page, waiting for navigation...');
44            observeNavigation();
45            return;
46        }
47
48        console.log('On playlist page, setting up organizer...');
49        
50        // Wait for playlist content to load
51        await waitForElement('ytd-playlist-video-list-renderer');
52        
53        // Create the organizer UI
54        createOrganizerUI();
55    }
56
57    function waitForElement(selector, timeout = 10000) {
58        return new Promise((resolve, reject) => {
59            if (document.querySelector(selector)) {
60                return resolve(document.querySelector(selector));
61            }
62
63            const observer = new MutationObserver((mutations) => {
64                if (document.querySelector(selector)) {
65                    observer.disconnect();
66                    resolve(document.querySelector(selector));
67                }
68            });
69
70            observer.observe(document.body, {
71                childList: true,
72                subtree: true
73            });
74
75            setTimeout(() => {
76                observer.disconnect();
77                reject(new Error(`Element ${selector} not found within ${timeout}ms`));
78            }, timeout);
79        });
80    }
81
82    function observeNavigation() {
83        // Watch for URL changes (YouTube is a SPA)
84        let lastUrl = location.href;
85        new MutationObserver(() => {
86            const url = location.href;
87            if (url !== lastUrl) {
88                lastUrl = url;
89                console.log('URL changed to:', url);
90                setupOrganizer();
91            }
92        }).observe(document, { subtree: true, childList: true });
93    }
94
95    async function createOrganizerUI() {
96        // Check if organizer already exists
97        if (document.getElementById('yt-playlist-organizer')) {
98            console.log('Organizer already exists');
99            return;
100        }
101
102        // Find the playlist header
103        const playlistHeader = document.querySelector('ytd-playlist-header-renderer');
104        if (!playlistHeader) {
105            console.log('Playlist header not found');
106            return;
107        }
108
109        // Create organizer container
110        const organizerContainer = document.createElement('div');
111        organizerContainer.id = 'yt-playlist-organizer';
112        organizerContainer.style.cssText = `
113            background: #0f0f0f;
114            border: 1px solid #3f3f3f;
115            border-radius: 12px;
116            padding: 20px;
117            margin: 20px 0;
118            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
119        `;
120
121        // Create header
122        const header = document.createElement('div');
123        header.style.cssText = `
124            display: flex;
125            align-items: center;
126            justify-content: space-between;
127            margin-bottom: 20px;
128        `;
129        header.innerHTML = `
130            <h3 style="color: #fff; margin: 0; font-size: 18px; font-weight: 500;">
131                šŸ“‹ Playlist Organizer
132            </h3>
133            <button id="toggle-organizer" style="
134                background: #3ea6ff;
135                color: #fff;
136                border: none;
137                padding: 8px 16px;
138                border-radius: 18px;
139                cursor: pointer;
140                font-size: 14px;
141                font-weight: 500;
142                transition: background 0.2s;
143            ">
144                Collapse
145            </button>
146        `;
147
148        // Create content area
149        const contentArea = document.createElement('div');
150        contentArea.id = 'organizer-content';
151        contentArea.style.cssText = 'display: block;';
152
153        // Create search and filter section
154        const searchSection = document.createElement('div');
155        searchSection.style.cssText = `
156            display: flex;
157            gap: 12px;
158            margin-bottom: 16px;
159            flex-wrap: wrap;
160        `;
161        searchSection.innerHTML = `
162            <input 
163                type="text" 
164                id="video-search" 
165                placeholder="šŸ” Search videos..."
166                style="
167                    flex: 1;
168                    min-width: 200px;
169                    background: #272727;
170                    border: 1px solid #3f3f3f;
171                    color: #fff;
172                    padding: 10px 14px;
173                    border-radius: 8px;
174                    font-size: 14px;
175                    outline: none;
176                "
177            />
178            <select id="category-filter" style="
179                background: #272727;
180                border: 1px solid #3f3f3f;
181                color: #fff;
182                padding: 10px 14px;
183                border-radius: 8px;
184                font-size: 14px;
185                cursor: pointer;
186                outline: none;
187            ">
188                <option value="all">All Categories</option>
189            </select>
190            <select id="sort-videos" style="
191                background: #272727;
192                border: 1px solid #3f3f3f;
193                color: #fff;
194                padding: 10px 14px;
195                border-radius: 8px;
196                font-size: 14px;
197                cursor: pointer;
198                outline: none;
199            ">
200                <option value="default">Default Order</option>
201                <option value="title-asc">Title (A-Z)</option>
202                <option value="title-desc">Title (Z-A)</option>
203                <option value="duration-asc">Duration (Short)</option>
204                <option value="duration-desc">Duration (Long)</option>
205            </select>
206        `;
207
208        // Create category management section
209        const categorySection = document.createElement('div');
210        categorySection.style.cssText = `
211            background: #1a1a1a;
212            border-radius: 8px;
213            padding: 16px;
214            margin-bottom: 16px;
215        `;
216        categorySection.innerHTML = `
217            <div style="display: flex; gap: 8px; margin-bottom: 12px; align-items: center;">
218                <input 
219                    type="text" 
220                    id="new-category" 
221                    placeholder="New category name..."
222                    style="
223                        flex: 1;
224                        background: #272727;
225                        border: 1px solid #3f3f3f;
226                        color: #fff;
227                        padding: 8px 12px;
228                        border-radius: 6px;
229                        font-size: 13px;
230                        outline: none;
231                    "
232                />
233                <button id="add-category" style="
234                    background: #065fd4;
235                    color: #fff;
236                    border: none;
237                    padding: 8px 16px;
238                    border-radius: 6px;
239                    cursor: pointer;
240                    font-size: 13px;
241                    font-weight: 500;
242                    white-space: nowrap;
243                ">
244                    + Add Category
245                </button>
246            </div>
247            <div id="categories-list" style="
248                display: flex;
249                flex-wrap: wrap;
250                gap: 8px;
251            "></div>
252        `;
253
254        // Create stats section
255        const statsSection = document.createElement('div');
256        statsSection.id = 'organizer-stats';
257        statsSection.style.cssText = `
258            display: flex;
259            gap: 16px;
260            padding: 12px;
261            background: #1a1a1a;
262            border-radius: 8px;
263            margin-bottom: 16px;
264            flex-wrap: wrap;
265        `;
266        statsSection.innerHTML = `
267            <div style="flex: 1; min-width: 120px;">
268                <div style="color: #aaa; font-size: 12px; margin-bottom: 4px;">Total Videos</div>
269                <div id="total-videos" style="color: #fff; font-size: 20px; font-weight: 600;">0</div>
270            </div>
271            <div style="flex: 1; min-width: 120px;">
272                <div style="color: #aaa; font-size: 12px; margin-bottom: 4px;">Categorized</div>
273                <div id="categorized-videos" style="color: #3ea6ff; font-size: 20px; font-weight: 600;">0</div>
274            </div>
275            <div style="flex: 1; min-width: 120px;">
276                <div style="color: #aaa; font-size: 12px; margin-bottom: 4px;">Categories</div>
277                <div id="total-categories" style="color: #f1c40f; font-size: 20px; font-weight: 600;">0</div>
278            </div>
279        `;
280
281        // Create action buttons section
282        const actionsSection = document.createElement('div');
283        actionsSection.style.cssText = `
284            display: flex;
285            gap: 8px;
286            flex-wrap: wrap;
287        `;
288        actionsSection.innerHTML = `
289            <button id="export-data" style="
290                background: #2d7a2d;
291                color: #fff;
292                border: none;
293                padding: 8px 16px;
294                border-radius: 6px;
295                cursor: pointer;
296                font-size: 13px;
297                font-weight: 500;
298            ">
299                šŸ“„ Export Data
300            </button>
301            <button id="import-data" style="
302                background: #7a2d7a;
303                color: #fff;
304                border: none;
305                padding: 8px 16px;
306                border-radius: 6px;
307                cursor: pointer;
308                font-size: 13px;
309                font-weight: 500;
310            ">
311                šŸ“¤ Import Data
312            </button>
313            <button id="clear-all" style="
314                background: #c62828;
315                color: #fff;
316                border: none;
317                padding: 8px 16px;
318                border-radius: 6px;
319                cursor: pointer;
320                font-size: 13px;
321                font-weight: 500;
322            ">
323                šŸ—‘ļø Clear All
324            </button>
325        `;
326
327        // Assemble the UI
328        contentArea.appendChild(searchSection);
329        contentArea.appendChild(categorySection);
330        contentArea.appendChild(statsSection);
331        contentArea.appendChild(actionsSection);
332        organizerContainer.appendChild(header);
333        organizerContainer.appendChild(contentArea);
334
335        // Insert after playlist header
336        playlistHeader.parentNode.insertBefore(organizerContainer, playlistHeader.nextSibling);
337
338        // Initialize functionality
339        await initializeOrganizer();
340    }
341
342    async function initializeOrganizer() {
343        console.log('Initializing organizer functionality...');
344
345        // Load saved data
346        const categories = await GM.getValue('yt_organizer_categories', []);
347        const videoCategories = await GM.getValue('yt_organizer_video_categories', {});
348
349        // Populate categories
350        updateCategoriesList(categories);
351        updateCategoryFilter(categories);
352
353        // Add event listeners
354        setupEventListeners(categories, videoCategories);
355
356        // Enhance video items
357        enhanceVideoItems(categories, videoCategories);
358
359        // Update stats
360        updateStats(categories, videoCategories);
361
362        // Observe for new videos
363        observePlaylistChanges(categories, videoCategories);
364    }
365
366    function updateCategoriesList(categories) {
367        const categoriesList = document.getElementById('categories-list');
368        if (!categoriesList) return;
369
370        categoriesList.innerHTML = '';
371        
372        if (categories.length === 0) {
373            categoriesList.innerHTML = '<div style="color: #aaa; font-size: 13px;">No categories yet. Add one above!</div>';
374            return;
375        }
376
377        categories.forEach((category, index) => {
378            const categoryTag = document.createElement('div');
379            categoryTag.style.cssText = `
380                background: ${category.color || '#3ea6ff'};
381                color: #fff;
382                padding: 6px 12px;
383                border-radius: 16px;
384                font-size: 12px;
385                font-weight: 500;
386                display: flex;
387                align-items: center;
388                gap: 6px;
389            `;
390            categoryTag.innerHTML = `
391                <span>${category.name}</span>
392                <button class="delete-category" data-index="${index}" style="
393                    background: rgba(255,255,255,0.2);
394                    border: none;
395                    color: #fff;
396                    width: 18px;
397                    height: 18px;
398                    border-radius: 50%;
399                    cursor: pointer;
400                    font-size: 12px;
401                    display: flex;
402                    align-items: center;
403                    justify-content: center;
404                    padding: 0;
405                ">Ɨ</button>
406            `;
407            categoriesList.appendChild(categoryTag);
408        });
409    }
410
411    function updateCategoryFilter(categories) {
412        const categoryFilter = document.getElementById('category-filter');
413        if (!categoryFilter) return;
414
415        // Keep "All Categories" option
416        categoryFilter.innerHTML = '<option value="all">All Categories</option>';
417        
418        categories.forEach((category, index) => {
419            const option = document.createElement('option');
420            option.value = index;
421            option.textContent = category.name;
422            categoryFilter.appendChild(option);
423        });
424    }
425
426    function setupEventListeners(categories, videoCategories) {
427        // Toggle organizer
428        const toggleBtn = document.getElementById('toggle-organizer');
429        const content = document.getElementById('organizer-content');
430        if (toggleBtn && content) {
431            toggleBtn.addEventListener('click', () => {
432                const isVisible = content.style.display !== 'none';
433                content.style.display = isVisible ? 'none' : 'block';
434                toggleBtn.textContent = isVisible ? 'Expand' : 'Collapse';
435            });
436        }
437
438        // Add category
439        const addCategoryBtn = document.getElementById('add-category');
440        const newCategoryInput = document.getElementById('new-category');
441        if (addCategoryBtn && newCategoryInput) {
442            addCategoryBtn.addEventListener('click', async () => {
443                const categoryName = newCategoryInput.value.trim();
444                if (!categoryName) return;
445
446                const colors = ['#3ea6ff', '#f1c40f', '#e74c3c', '#2ecc71', '#9b59b6', '#e67e22', '#1abc9c', '#34495e'];
447                const newCategory = {
448                    name: categoryName,
449                    color: colors[Math.floor(Math.random() * colors.length)]
450                };
451
452                categories.push(newCategory);
453                await GM.setValue('yt_organizer_categories', categories);
454
455                updateCategoriesList(categories);
456                updateCategoryFilter(categories);
457                newCategoryInput.value = '';
458                
459                // Re-enhance videos with new category
460                enhanceVideoItems(categories, videoCategories);
461                updateStats(categories, videoCategories);
462            });
463
464            newCategoryInput.addEventListener('keypress', (e) => {
465                if (e.key === 'Enter') {
466                    addCategoryBtn.click();
467                }
468            });
469        }
470
471        // Delete category
472        document.addEventListener('click', async (e) => {
473            if (e.target.classList.contains('delete-category')) {
474                const index = parseInt(e.target.dataset.index);
475                if (confirm(`Delete category "${categories[index].name}"?`)) {
476                    categories.splice(index, 1);
477                    await GM.setValue('yt_organizer_categories', categories);
478                    
479                    updateCategoriesList(categories);
480                    updateCategoryFilter(categories);
481                    enhanceVideoItems(categories, videoCategories);
482                    updateStats(categories, videoCategories);
483                }
484            }
485        });
486
487        // Search videos
488        const searchInput = document.getElementById('video-search');
489        if (searchInput) {
490            searchInput.addEventListener('input', debounce((e) => {
491                filterVideos(e.target.value, categories, videoCategories);
492            }, 300));
493        }
494
495        // Filter by category
496        const categoryFilter = document.getElementById('category-filter');
497        if (categoryFilter) {
498            categoryFilter.addEventListener('change', (e) => {
499                filterByCategory(e.target.value, categories, videoCategories);
500            });
501        }
502
503        // Sort videos
504        const sortSelect = document.getElementById('sort-videos');
505        if (sortSelect) {
506            sortSelect.addEventListener('change', (e) => {
507                sortVideos(e.target.value);
508            });
509        }
510
511        // Export data
512        const exportBtn = document.getElementById('export-data');
513        if (exportBtn) {
514            exportBtn.addEventListener('click', async () => {
515                const data = {
516                    categories: await GM.getValue('yt_organizer_categories', []),
517                    videoCategories: await GM.getValue('yt_organizer_video_categories', {})
518                };
519                const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
520                const url = URL.createObjectURL(blob);
521                const a = document.createElement('a');
522                a.href = url;
523                a.download = 'youtube-organizer-data.json';
524                a.click();
525                URL.revokeObjectURL(url);
526            });
527        }
528
529        // Import data
530        const importBtn = document.getElementById('import-data');
531        if (importBtn) {
532            importBtn.addEventListener('click', () => {
533                const input = document.createElement('input');
534                input.type = 'file';
535                input.accept = 'application/json';
536                input.onchange = async (e) => {
537                    const file = e.target.files[0];
538                    const text = await file.text();
539                    const data = JSON.parse(text);
540                    
541                    await GM.setValue('yt_organizer_categories', data.categories || []);
542                    await GM.setValue('yt_organizer_video_categories', data.videoCategories || {});
543                    
544                    location.reload();
545                };
546                input.click();
547            });
548        }
549
550        // Clear all
551        const clearBtn = document.getElementById('clear-all');
552        if (clearBtn) {
553            clearBtn.addEventListener('click', async () => {
554                if (confirm('Clear all categories and data? This cannot be undone.')) {
555                    await GM.setValue('yt_organizer_categories', []);
556                    await GM.setValue('yt_organizer_video_categories', {});
557                    location.reload();
558                }
559            });
560        }
561    }
562
563    async function enhanceVideoItems(categories, videoCategories) {
564        const videoItems = document.querySelectorAll('ytd-playlist-video-renderer');
565        console.log(`Found ${videoItems.length} video items to enhance`);
566
567        videoItems.forEach((videoItem) => {
568            // Skip if already enhanced
569            if (videoItem.querySelector('.organizer-controls')) {
570                return;
571            }
572
573            const videoId = extractVideoId(videoItem);
574            if (!videoId) return;
575
576            // Create controls container
577            const controlsContainer = document.createElement('div');
578            controlsContainer.className = 'organizer-controls';
579            controlsContainer.style.cssText = `
580                display: flex;
581                gap: 6px;
582                margin-top: 8px;
583                flex-wrap: wrap;
584                align-items: center;
585            `;
586
587            // Add category selector
588            const categorySelect = document.createElement('select');
589            categorySelect.style.cssText = `
590                background: #272727;
591                border: 1px solid #3f3f3f;
592                color: #fff;
593                padding: 4px 8px;
594                border-radius: 4px;
595                font-size: 12px;
596                cursor: pointer;
597            `;
598            categorySelect.innerHTML = '<option value="">+ Add to category</option>';
599            categories.forEach((category, index) => {
600                const option = document.createElement('option');
601                option.value = index;
602                option.textContent = category.name;
603                categorySelect.appendChild(option);
604            });
605
606            categorySelect.addEventListener('change', async (e) => {
607                const categoryIndex = e.target.value;
608                if (categoryIndex === '') return;
609
610                if (!videoCategories[videoId]) {
611                    videoCategories[videoId] = [];
612                }
613                
614                if (!videoCategories[videoId].includes(parseInt(categoryIndex))) {
615                    videoCategories[videoId].push(parseInt(categoryIndex));
616                    await GM.setValue('yt_organizer_video_categories', videoCategories);
617                    
618                    // Update display
619                    updateVideoTags(videoItem, videoId, categories, videoCategories);
620                    updateStats(categories, videoCategories);
621                }
622                
623                e.target.value = '';
624            });
625
626            // Create tags container
627            const tagsContainer = document.createElement('div');
628            tagsContainer.className = 'video-tags';
629            tagsContainer.style.cssText = `
630                display: flex;
631                gap: 4px;
632                flex-wrap: wrap;
633            `;
634
635            controlsContainer.appendChild(categorySelect);
636            controlsContainer.appendChild(tagsContainer);
637
638            // Insert controls
639            const metadataLine = videoItem.querySelector('#metadata-line');
640            if (metadataLine) {
641                metadataLine.appendChild(controlsContainer);
642            }
643
644            // Update tags
645            updateVideoTags(videoItem, videoId, categories, videoCategories);
646        });
647    }
648
649    function updateVideoTags(videoItem, videoId, categories, videoCategories) {
650        const tagsContainer = videoItem.querySelector('.video-tags');
651        if (!tagsContainer) return;
652
653        tagsContainer.innerHTML = '';
654
655        const videoCats = videoCategories[videoId] || [];
656        videoCats.forEach((catIndex) => {
657            const category = categories[catIndex];
658            if (!category) return;
659
660            const tag = document.createElement('div');
661            tag.style.cssText = `
662                background: ${category.color};
663                color: #fff;
664                padding: 3px 8px;
665                border-radius: 10px;
666                font-size: 11px;
667                font-weight: 500;
668                display: flex;
669                align-items: center;
670                gap: 4px;
671            `;
672            tag.innerHTML = `
673                <span>${category.name}</span>
674                <button class="remove-tag" data-video-id="${videoId}" data-category-index="${catIndex}" style="
675                    background: rgba(255,255,255,0.3);
676                    border: none;
677                    color: #fff;
678                    width: 14px;
679                    height: 14px;
680                    border-radius: 50%;
681                    cursor: pointer;
682                    font-size: 10px;
683                    display: flex;
684                    align-items: center;
685                    justify-content: center;
686                    padding: 0;
687                ">Ɨ</button>
688            `;
689            tagsContainer.appendChild(tag);
690        });
691
692        // Add remove tag listeners
693        tagsContainer.querySelectorAll('.remove-tag').forEach(btn => {
694            btn.addEventListener('click', async (e) => {
695                e.stopPropagation();
696                const videoId = btn.dataset.videoId;
697                const catIndex = parseInt(btn.dataset.categoryIndex);
698                
699                if (videoCategories[videoId]) {
700                    videoCategories[videoId] = videoCategories[videoId].filter(c => c !== catIndex);
701                    if (videoCategories[videoId].length === 0) {
702                        delete videoCategories[videoId];
703                    }
704                    await GM.setValue('yt_organizer_video_categories', videoCategories);
705                    
706                    updateVideoTags(videoItem, videoId, categories, videoCategories);
707                    updateStats(categories, videoCategories);
708                }
709            });
710        });
711    }
712
713    function extractVideoId(videoItem) {
714        const link = videoItem.querySelector('a#thumbnail');
715        if (!link) return null;
716        
717        const href = link.getAttribute('href');
718        const match = href.match(/[?&]v=([^&]+)/);
719        return match ? match[1] : null;
720    }
721
722    function filterVideos(searchTerm, categories, videoCategories) {
723        const videoItems = document.querySelectorAll('ytd-playlist-video-renderer');
724        const term = searchTerm.toLowerCase();
725
726        videoItems.forEach((videoItem) => {
727            const title = videoItem.querySelector('#video-title')?.textContent?.toLowerCase() || '';
728            const channel = videoItem.querySelector('#channel-name')?.textContent?.toLowerCase() || '';
729            
730            const matches = title.includes(term) || channel.includes(term);
731            videoItem.style.display = matches ? '' : 'none';
732        });
733    }
734
735    function filterByCategory(categoryIndex, categories, videoCategories) {
736        const videoItems = document.querySelectorAll('ytd-playlist-video-renderer');
737
738        if (categoryIndex === 'all') {
739            videoItems.forEach(item => item.style.display = '');
740            return;
741        }
742
743        const catIndex = parseInt(categoryIndex);
744        videoItems.forEach((videoItem) => {
745            const videoId = extractVideoId(videoItem);
746            const videoCats = videoCategories[videoId] || [];
747            
748            const hasCategory = videoCats.includes(catIndex);
749            videoItem.style.display = hasCategory ? '' : 'none';
750        });
751    }
752
753    function sortVideos(sortType) {
754        const container = document.querySelector('ytd-playlist-video-list-renderer #contents');
755        if (!container) return;
756
757        const videoItems = Array.from(container.querySelectorAll('ytd-playlist-video-renderer'));
758        
759        videoItems.sort((a, b) => {
760            switch (sortType) {
761            case 'title-asc':
762                return getVideoTitle(a).localeCompare(getVideoTitle(b));
763            case 'title-desc':
764                return getVideoTitle(b).localeCompare(getVideoTitle(a));
765            case 'duration-asc':
766                return getVideoDuration(a) - getVideoDuration(b);
767            case 'duration-desc':
768                return getVideoDuration(b) - getVideoDuration(a);
769            default:
770                return 0;
771            }
772        });
773
774        videoItems.forEach(item => container.appendChild(item));
775    }
776
777    function getVideoTitle(videoItem) {
778        return videoItem.querySelector('#video-title')?.textContent?.trim() || '';
779    }
780
781    function getVideoDuration(videoItem) {
782        const durationText = videoItem.querySelector('.ytd-thumbnail-overlay-time-status-renderer')?.textContent?.trim() || '0:00';
783        const parts = durationText.split(':').map(Number);
784        if (parts.length === 2) return parts[0] * 60 + parts[1];
785        if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
786        return 0;
787    }
788
789    async function updateStats(categories, videoCategories) {
790        const totalVideos = document.querySelectorAll('ytd-playlist-video-renderer').length;
791        const categorizedVideos = Object.keys(videoCategories).length;
792        const totalCategories = categories.length;
793
794        const totalEl = document.getElementById('total-videos');
795        const categorizedEl = document.getElementById('categorized-videos');
796        const categoriesEl = document.getElementById('total-categories');
797
798        if (totalEl) totalEl.textContent = totalVideos;
799        if (categorizedEl) categorizedEl.textContent = categorizedVideos;
800        if (categoriesEl) categoriesEl.textContent = totalCategories;
801    }
802
803    function observePlaylistChanges(categories, videoCategories) {
804        const container = document.querySelector('ytd-playlist-video-list-renderer #contents');
805        if (!container) return;
806
807        const observer = new MutationObserver(debounce(() => {
808            console.log('Playlist changed, re-enhancing videos...');
809            enhanceVideoItems(categories, videoCategories);
810            updateStats(categories, videoCategories);
811        }, 1000));
812
813        observer.observe(container, {
814            childList: true,
815            subtree: true
816        });
817    }
818
819    // Start the extension
820    init();
821})();
YouTube Playlist Organizer | Robomonkey