YouTube AI Music Recommender

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})();
YouTube AI Music Recommender | Robomonkey