YouTube Playlist Duration Sorter

Sort YouTube playlists and Watch Later by video duration (ascending or descending)

Size

8.1 KB

Version

1.0.1

Created

Oct 30, 2025

Updated

15 days ago

1// ==UserScript==
2// @name		YouTube Playlist Duration Sorter
3// @description		Sort YouTube playlists and Watch Later by video duration (ascending or descending)
4// @version		1.0.1
5// @match		https://www.youtube.com/playlist*
6// @icon		https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('YouTube Playlist Duration Sorter loaded');
12
13    // Debounce function to prevent excessive calls
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    // Parse duration string (e.g., "11:10", "1:23:45") to seconds
27    function parseDuration(durationText) {
28        if (!durationText) return 0;
29        
30        const parts = durationText.trim().split(':').map(p => parseInt(p, 10));
31        
32        if (parts.length === 2) {
33            // MM:SS format
34            return parts[0] * 60 + parts[1];
35        } else if (parts.length === 3) {
36            // HH:MM:SS format
37            return parts[0] * 3600 + parts[1] * 60 + parts[2];
38        }
39        
40        return 0;
41    }
42
43    // Format seconds back to duration string
44    function formatDuration(seconds) {
45        const hours = Math.floor(seconds / 3600);
46        const minutes = Math.floor((seconds % 3600) / 60);
47        const secs = seconds % 60;
48        
49        if (hours > 0) {
50            return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
51        }
52        return `${minutes}:${String(secs).padStart(2, '0')}`;
53    }
54
55    // Sort playlist videos by duration
56    async function sortPlaylist(order) {
57        console.log(`Sorting playlist by duration: ${order}`);
58        
59        const playlistContainer = document.querySelector('ytd-playlist-video-list-renderer');
60        if (!playlistContainer) {
61            console.error('Playlist container not found');
62            return;
63        }
64
65        const videoRenderers = Array.from(playlistContainer.querySelectorAll('ytd-playlist-video-renderer'));
66        
67        if (videoRenderers.length === 0) {
68            console.log('No videos found in playlist');
69            return;
70        }
71
72        console.log(`Found ${videoRenderers.length} videos to sort`);
73
74        // Extract video data with duration
75        const videoData = videoRenderers.map(renderer => {
76            const durationBadge = renderer.querySelector('badge-shape .yt-badge-shape__text');
77            const durationText = durationBadge ? durationBadge.textContent.trim() : '0:00';
78            const durationSeconds = parseDuration(durationText);
79            
80            return {
81                element: renderer,
82                duration: durationSeconds,
83                durationText: durationText
84            };
85        });
86
87        // Sort by duration
88        videoData.sort((a, b) => {
89            if (order === 'ascending') {
90                return a.duration - b.duration;
91            } else {
92                return b.duration - a.duration;
93            }
94        });
95
96        console.log('Sorted videos:', videoData.map(v => `${v.durationText} (${v.duration}s)`));
97
98        // Reorder DOM elements
99        const contentsElement = playlistContainer.querySelector('#contents');
100        if (contentsElement) {
101            videoData.forEach((video, index) => {
102                contentsElement.appendChild(video.element);
103                
104                // Update the index number
105                const indexElement = video.element.querySelector('#index');
106                if (indexElement) {
107                    indexElement.textContent = String(index + 1);
108                }
109            });
110        }
111
112        // Save sort preference
113        await GM.setValue('youtube_playlist_sort_order', order);
114        console.log(`Playlist sorted successfully in ${order} order`);
115    }
116
117    // Create sort button UI
118    function createSortButton() {
119        console.log('Creating sort button');
120        
121        // Check if button already exists
122        if (document.getElementById('duration-sort-button')) {
123            console.log('Sort button already exists');
124            return;
125        }
126
127        const playlistHeader = document.querySelector('ytd-playlist-header-renderer .metadata-wrapper');
128        if (!playlistHeader) {
129            console.log('Playlist header not found, will retry');
130            return;
131        }
132
133        // Create button container
134        const buttonContainer = document.createElement('div');
135        buttonContainer.id = 'duration-sort-button';
136        buttonContainer.style.cssText = `
137            display: flex;
138            gap: 8px;
139            margin-top: 12px;
140            align-items: center;
141        `;
142
143        // Create label
144        const label = document.createElement('span');
145        label.textContent = 'Sort by duration:';
146        label.style.cssText = `
147            color: var(--yt-spec-text-secondary);
148            font-size: 14px;
149            font-weight: 500;
150        `;
151
152        // Create ascending button
153        const ascButton = document.createElement('button');
154        ascButton.textContent = '↑ Shortest First';
155        ascButton.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m';
156        ascButton.style.cssText = `
157            cursor: pointer;
158            padding: 8px 16px;
159            border-radius: 18px;
160            font-size: 14px;
161            font-weight: 500;
162        `;
163        ascButton.onclick = () => sortPlaylist('ascending');
164
165        // Create descending button
166        const descButton = document.createElement('button');
167        descButton.textContent = '↓ Longest First';
168        descButton.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m';
169        descButton.style.cssText = `
170            cursor: pointer;
171            padding: 8px 16px;
172            border-radius: 18px;
173            font-size: 14px;
174            font-weight: 500;
175        `;
176        descButton.onclick = () => sortPlaylist('descending');
177
178        buttonContainer.appendChild(label);
179        buttonContainer.appendChild(ascButton);
180        buttonContainer.appendChild(descButton);
181
182        playlistHeader.appendChild(buttonContainer);
183        console.log('Sort button created successfully');
184    }
185
186    // Initialize the extension
187    async function init() {
188        console.log('Initializing YouTube Playlist Duration Sorter');
189        
190        // Wait for playlist to load
191        const checkPlaylist = setInterval(() => {
192            const playlistHeader = document.querySelector('ytd-playlist-header-renderer');
193            const playlistVideos = document.querySelector('ytd-playlist-video-list-renderer');
194            
195            if (playlistHeader && playlistVideos) {
196                clearInterval(checkPlaylist);
197                console.log('Playlist detected, creating sort button');
198                createSortButton();
199                
200                // Auto-apply last sort preference
201                GM.getValue('youtube_playlist_sort_order', null).then(savedOrder => {
202                    if (savedOrder) {
203                        console.log(`Auto-applying saved sort order: ${savedOrder}`);
204                        setTimeout(() => sortPlaylist(savedOrder), 1000);
205                    }
206                });
207            }
208        }, 1000);
209
210        // Stop checking after 30 seconds
211        setTimeout(() => clearInterval(checkPlaylist), 30000);
212
213        // Watch for navigation changes (YouTube is a SPA)
214        const debouncedInit = debounce(() => {
215            console.log('Page navigation detected, reinitializing');
216            init();
217        }, 1000);
218
219        const observer = new MutationObserver(debouncedInit);
220        observer.observe(document.body, {
221            childList: true,
222            subtree: true
223        });
224    }
225
226    // Start when page is ready
227    if (document.readyState === 'loading') {
228        document.addEventListener('DOMContentLoaded', init);
229    } else {
230        init();
231    }
232
233})();
YouTube Playlist Duration Sorter | Robomonkey