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