Get AI-powered music recommendations based on the current video
Size
13.9 KB
Version
1.0.1
Created
Nov 2, 2025
Updated
13 days ago
1// ==UserScript==
2// @name YouTube AI Music Recommender
3// @description Get AI-powered music recommendations based on the current video
4// @version 1.0.1
5// @match https://*.youtube.com/*
6// @icon https://www.youtube.com/s/desktop/3fd9a6f6/img/favicon_32x32.png
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('YouTube AI Music Recommender initialized');
12
13 // Debounce function to prevent multiple rapid 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 // Create the AI recommendation button
27 function createRecommendationButton() {
28 console.log('Creating recommendation button');
29
30 // Check if button already exists
31 if (document.querySelector('#ai-music-recommender-btn')) {
32 console.log('Button already exists');
33 return;
34 }
35
36 // Find the actions container
37 const actionsContainer = document.querySelector('#top-level-buttons-computed');
38 if (!actionsContainer) {
39 console.log('Actions container not found');
40 return;
41 }
42
43 // Create button container
44 const buttonContainer = document.createElement('yt-button-view-model');
45 buttonContainer.className = 'ytd-menu-renderer';
46 buttonContainer.id = 'ai-music-recommender-container';
47
48 // Create button
49 const button = document.createElement('button');
50 button.id = 'ai-music-recommender-btn';
51 button.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 yt-spec-button-shape-next--icon-leading';
52 button.setAttribute('aria-label', 'Get AI Music Recommendations');
53 button.style.marginLeft = '8px';
54
55 // Create icon
56 const iconDiv = document.createElement('div');
57 iconDiv.className = 'yt-spec-button-shape-next__icon';
58 iconDiv.setAttribute('aria-hidden', 'true');
59 iconDiv.innerHTML = '🎵';
60 iconDiv.style.fontSize = '20px';
61
62 // Create text content
63 const textDiv = document.createElement('div');
64 textDiv.className = 'yt-spec-button-shape-next__button-text-content';
65 textDiv.textContent = 'AI Recommendations';
66
67 // Assemble button
68 button.appendChild(iconDiv);
69 button.appendChild(textDiv);
70 buttonContainer.appendChild(button);
71
72 // Add click handler
73 button.addEventListener('click', handleRecommendationClick);
74
75 // Insert button after the Share button
76 actionsContainer.appendChild(buttonContainer);
77 console.log('Button created successfully');
78 }
79
80 // Handle recommendation button click
81 async function handleRecommendationClick() {
82 console.log('Recommendation button clicked');
83
84 const button = document.querySelector('#ai-music-recommender-btn');
85 if (!button) return;
86
87 // Get video information
88 const videoTitle = document.querySelector('h1.ytd-watch-metadata yt-formatted-string')?.textContent?.trim();
89 const channelName = document.querySelector('ytd-channel-name a')?.textContent?.trim();
90
91 if (!videoTitle) {
92 console.error('Could not find video title');
93 showNotification('Error: Could not find video information', 'error');
94 return;
95 }
96
97 console.log('Video title:', videoTitle);
98 console.log('Channel name:', channelName);
99
100 // Show loading state
101 const originalText = button.querySelector('.yt-spec-button-shape-next__button-text-content').textContent;
102 button.querySelector('.yt-spec-button-shape-next__button-text-content').textContent = 'Loading...';
103 button.disabled = true;
104
105 try {
106 // Call AI to get recommendations
107 const recommendations = await getAIRecommendations(videoTitle, channelName);
108
109 // Display recommendations
110 displayRecommendations(recommendations);
111
112 } catch (error) {
113 console.error('Error getting recommendations:', error);
114 showNotification('Error getting recommendations. Please try again.', 'error');
115 } finally {
116 // Restore button state
117 button.querySelector('.yt-spec-button-shape-next__button-text-content').textContent = originalText;
118 button.disabled = false;
119 }
120 }
121
122 // Get AI-powered music recommendations
123 async function getAIRecommendations(videoTitle, channelName) {
124 console.log('Getting AI recommendations for:', videoTitle);
125
126 const prompt = `Based on this music video: "${videoTitle}"${channelName ? ` by ${channelName}` : ''},
127 provide 5 similar music recommendations. For each recommendation, include the artist name and song title.
128 Focus on similar genre, mood, and style.`;
129
130 try {
131 const response = await RM.aiCall(prompt, {
132 type: "json_schema",
133 json_schema: {
134 name: "music_recommendations",
135 schema: {
136 type: "object",
137 properties: {
138 genre: { type: "string" },
139 mood: { type: "string" },
140 recommendations: {
141 type: "array",
142 items: {
143 type: "object",
144 properties: {
145 artist: { type: "string" },
146 song: { type: "string" },
147 reason: { type: "string" }
148 },
149 required: ["artist", "song", "reason"]
150 },
151 minItems: 5,
152 maxItems: 5
153 }
154 },
155 required: ["genre", "mood", "recommendations"]
156 }
157 }
158 });
159
160 console.log('AI recommendations received:', response);
161 return response;
162 } catch (error) {
163 console.error('AI call failed:', error);
164 throw error;
165 }
166 }
167
168 // Display recommendations in a panel
169 function displayRecommendations(data) {
170 console.log('Displaying recommendations');
171
172 // Remove existing panel if any
173 const existingPanel = document.querySelector('#ai-recommendations-panel');
174 if (existingPanel) {
175 existingPanel.remove();
176 }
177
178 // Create panel
179 const panel = document.createElement('div');
180 panel.id = 'ai-recommendations-panel';
181 panel.style.cssText = `
182 position: fixed;
183 top: 50%;
184 left: 50%;
185 transform: translate(-50%, -50%);
186 background: #0f0f0f;
187 border: 1px solid #3f3f3f;
188 border-radius: 12px;
189 padding: 24px;
190 max-width: 600px;
191 width: 90%;
192 max-height: 80vh;
193 overflow-y: auto;
194 z-index: 10000;
195 box-shadow: 0 4px 24px rgba(0, 0, 0, 0.8);
196 color: #f1f1f1;
197 `;
198
199 // Create header
200 const header = document.createElement('div');
201 header.style.cssText = `
202 display: flex;
203 justify-content: space-between;
204 align-items: center;
205 margin-bottom: 20px;
206 border-bottom: 1px solid #3f3f3f;
207 padding-bottom: 16px;
208 `;
209
210 const title = document.createElement('h2');
211 title.textContent = '🎵 AI Music Recommendations';
212 title.style.cssText = `
213 margin: 0;
214 font-size: 20px;
215 font-weight: 600;
216 color: #f1f1f1;
217 `;
218
219 const closeButton = document.createElement('button');
220 closeButton.textContent = '✕';
221 closeButton.style.cssText = `
222 background: transparent;
223 border: none;
224 color: #aaa;
225 font-size: 24px;
226 cursor: pointer;
227 padding: 0;
228 width: 32px;
229 height: 32px;
230 display: flex;
231 align-items: center;
232 justify-content: center;
233 border-radius: 50%;
234 transition: background 0.2s;
235 `;
236 closeButton.onmouseover = () => closeButton.style.background = '#3f3f3f';
237 closeButton.onmouseout = () => closeButton.style.background = 'transparent';
238 closeButton.onclick = () => panel.remove();
239
240 header.appendChild(title);
241 header.appendChild(closeButton);
242 panel.appendChild(header);
243
244 // Add genre and mood info
245 const infoDiv = document.createElement('div');
246 infoDiv.style.cssText = `
247 margin-bottom: 20px;
248 padding: 12px;
249 background: #1f1f1f;
250 border-radius: 8px;
251 `;
252 infoDiv.innerHTML = `
253 <div style="margin-bottom: 8px;"><strong style="color: #3ea6ff;">Genre:</strong> ${data.genre}</div>
254 <div><strong style="color: #3ea6ff;">Mood:</strong> ${data.mood}</div>
255 `;
256 panel.appendChild(infoDiv);
257
258 // Add recommendations
259 data.recommendations.forEach((rec, index) => {
260 const recDiv = document.createElement('div');
261 recDiv.style.cssText = `
262 margin-bottom: 16px;
263 padding: 16px;
264 background: #1f1f1f;
265 border-radius: 8px;
266 border-left: 3px solid #3ea6ff;
267 transition: background 0.2s;
268 cursor: pointer;
269 `;
270 recDiv.onmouseover = () => recDiv.style.background = '#2a2a2a';
271 recDiv.onmouseout = () => recDiv.style.background = '#1f1f1f';
272
273 const songTitle = document.createElement('div');
274 songTitle.style.cssText = `
275 font-size: 16px;
276 font-weight: 600;
277 margin-bottom: 4px;
278 color: #f1f1f1;
279 `;
280 songTitle.textContent = `${index + 1}. ${rec.song}`;
281
282 const artistName = document.createElement('div');
283 artistName.style.cssText = `
284 font-size: 14px;
285 color: #aaa;
286 margin-bottom: 8px;
287 `;
288 artistName.textContent = `by ${rec.artist}`;
289
290 const reason = document.createElement('div');
291 reason.style.cssText = `
292 font-size: 13px;
293 color: #ccc;
294 line-height: 1.4;
295 `;
296 reason.textContent = rec.reason;
297
298 recDiv.appendChild(songTitle);
299 recDiv.appendChild(artistName);
300 recDiv.appendChild(reason);
301
302 // Add click to search
303 recDiv.onclick = () => {
304 const searchQuery = `${rec.artist} ${rec.song}`;
305 window.open(`https://www.youtube.com/results?search_query=${encodeURIComponent(searchQuery)}`, '_blank');
306 };
307
308 panel.appendChild(recDiv);
309 });
310
311 // Add overlay
312 const overlay = document.createElement('div');
313 overlay.id = 'ai-recommendations-overlay';
314 overlay.style.cssText = `
315 position: fixed;
316 top: 0;
317 left: 0;
318 width: 100%;
319 height: 100%;
320 background: rgba(0, 0, 0, 0.7);
321 z-index: 9999;
322 `;
323 overlay.onclick = () => {
324 panel.remove();
325 overlay.remove();
326 };
327
328 document.body.appendChild(overlay);
329 document.body.appendChild(panel);
330 }
331
332 // Show notification
333 function showNotification(message, type = 'info') {
334 const notification = document.createElement('div');
335 notification.style.cssText = `
336 position: fixed;
337 top: 20px;
338 right: 20px;
339 background: ${type === 'error' ? '#cc0000' : '#065fd4'};
340 color: white;
341 padding: 16px 24px;
342 border-radius: 8px;
343 z-index: 10001;
344 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
345 font-size: 14px;
346 max-width: 300px;
347 `;
348 notification.textContent = message;
349
350 document.body.appendChild(notification);
351
352 setTimeout(() => {
353 notification.style.transition = 'opacity 0.3s';
354 notification.style.opacity = '0';
355 setTimeout(() => notification.remove(), 300);
356 }, 3000);
357 }
358
359 // Initialize the extension
360 function init() {
361 console.log('Initializing YouTube AI Music Recommender');
362
363 // Wait for the page to load
364 const checkAndInit = debounce(() => {
365 if (window.location.pathname === '/watch') {
366 createRecommendationButton();
367 }
368 }, 1000);
369
370 // Initial check
371 checkAndInit();
372
373 // Watch for navigation changes (YouTube is a SPA)
374 const observer = new MutationObserver(debounce(() => {
375 if (window.location.pathname === '/watch') {
376 createRecommendationButton();
377 }
378 }, 1000));
379
380 observer.observe(document.body, {
381 childList: true,
382 subtree: true
383 });
384
385 // Listen for URL changes
386 let lastUrl = location.href;
387 new MutationObserver(() => {
388 const url = location.href;
389 if (url !== lastUrl) {
390 lastUrl = url;
391 checkAndInit();
392 }
393 }).observe(document, { subtree: true, childList: true });
394 }
395
396 // Start the extension
397 if (document.readyState === 'loading') {
398 document.addEventListener('DOMContentLoaded', init);
399 } else {
400 init();
401 }
402
403})();