YouTube Playlist Duration Sorter

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

Size

9.8 KB

Version

1.1.1

Created

Oct 30, 2025

Updated

6 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.1.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        // Enable the Play All (Sorted) button
117        const playAllButton = document.getElementById('play-all-sorted-button');
118        if (playAllButton) {
119            playAllButton.disabled = false;
120            playAllButton.style.opacity = '1';
121        }
122    }
123
124    // Play all videos in sorted order
125    function playAllSorted() {
126        console.log('Playing all videos in sorted order');
127        
128        const playlistContainer = document.querySelector('ytd-playlist-video-list-renderer');
129        if (!playlistContainer) {
130            console.error('Playlist container not found');
131            return;
132        }
133
134        const videoRenderers = Array.from(playlistContainer.querySelectorAll('ytd-playlist-video-renderer'));
135        
136        if (videoRenderers.length === 0) {
137            console.log('No videos found in playlist');
138            return;
139        }
140
141        // Get the first video's link
142        const firstVideo = videoRenderers[0];
143        const videoLink = firstVideo.querySelector('a#thumbnail');
144        
145        if (videoLink && videoLink.href) {
146            console.log('Starting playback with first video:', videoLink.href);
147            window.location.href = videoLink.href;
148        } else {
149            console.error('Could not find video link');
150        }
151    }
152
153    // Create sort button UI
154    function createSortButton() {
155        console.log('Creating sort button');
156        
157        // Check if button already exists
158        if (document.getElementById('duration-sort-button')) {
159            console.log('Sort button already exists');
160            return;
161        }
162
163        const playlistHeader = document.querySelector('ytd-playlist-header-renderer .metadata-wrapper');
164        if (!playlistHeader) {
165            console.log('Playlist header not found, will retry');
166            return;
167        }
168
169        // Create button container
170        const buttonContainer = document.createElement('div');
171        buttonContainer.id = 'duration-sort-button';
172        buttonContainer.style.cssText = `
173            display: flex;
174            gap: 8px;
175            margin-top: 12px;
176            align-items: center;
177            flex-wrap: wrap;
178        `;
179
180        // Create label
181        const label = document.createElement('span');
182        label.textContent = 'Sort by duration:';
183        label.style.cssText = `
184            color: var(--yt-spec-text-secondary);
185            font-size: 14px;
186            font-weight: 500;
187        `;
188
189        // Create ascending button
190        const ascButton = document.createElement('button');
191        ascButton.textContent = '↑ Shortest First';
192        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';
193        ascButton.style.cssText = `
194            cursor: pointer;
195            padding: 8px 16px;
196            border-radius: 18px;
197            font-size: 14px;
198            font-weight: 500;
199        `;
200        ascButton.onclick = () => sortPlaylist('ascending');
201
202        // Create descending button
203        const descButton = document.createElement('button');
204        descButton.textContent = '↓ Longest First';
205        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';
206        descButton.style.cssText = `
207            cursor: pointer;
208            padding: 8px 16px;
209            border-radius: 18px;
210            font-size: 14px;
211            font-weight: 500;
212        `;
213        descButton.onclick = () => sortPlaylist('descending');
214
215        // Create Play All (Sorted) button
216        const playAllButton = document.createElement('button');
217        playAllButton.id = 'play-all-sorted-button';
218        playAllButton.textContent = '▶ Play All (Sorted)';
219        playAllButton.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--filled yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m';
220        playAllButton.style.cssText = `
221            cursor: pointer;
222            padding: 8px 16px;
223            border-radius: 18px;
224            font-size: 14px;
225            font-weight: 500;
226            opacity: 0.5;
227            margin-left: 8px;
228        `;
229        playAllButton.disabled = true;
230        playAllButton.onclick = playAllSorted;
231
232        buttonContainer.appendChild(label);
233        buttonContainer.appendChild(ascButton);
234        buttonContainer.appendChild(descButton);
235        buttonContainer.appendChild(playAllButton);
236
237        playlistHeader.appendChild(buttonContainer);
238        console.log('Sort button created successfully');
239    }
240
241    // Initialize the extension
242    async function init() {
243        console.log('Initializing YouTube Playlist Duration Sorter');
244        
245        // Wait for playlist to load
246        const checkPlaylist = setInterval(() => {
247            const playlistHeader = document.querySelector('ytd-playlist-header-renderer');
248            const playlistVideos = document.querySelector('ytd-playlist-video-list-renderer');
249            
250            if (playlistHeader && playlistVideos) {
251                clearInterval(checkPlaylist);
252                console.log('Playlist detected, creating sort button');
253                createSortButton();
254            }
255        }, 1000);
256
257        // Stop checking after 30 seconds
258        setTimeout(() => clearInterval(checkPlaylist), 30000);
259
260        // Watch for navigation changes (YouTube is a SPA)
261        const debouncedInit = debounce(() => {
262            console.log('Page navigation detected, reinitializing');
263            init();
264        }, 1000);
265
266        const observer = new MutationObserver(debouncedInit);
267        observer.observe(document.body, {
268            childList: true,
269            subtree: true
270        });
271    }
272
273    // Start when page is ready
274    if (document.readyState === 'loading') {
275        document.addEventListener('DOMContentLoaded', init);
276    } else {
277        init();
278    }
279
280})();
YouTube Playlist Duration Sorter | Robomonkey