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