YouTube Playlist Duration Sorter

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

Size

7.7 KB

Version

1.1.1

Created

Dec 7, 2025

Updated

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