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