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