Netflix IMDb Ratings

Shows IMDb ratings directly on Netflix thumbnails

Size

9.8 KB

Version

1.0.1

Created

Feb 24, 2026

Updated

26 days ago

1// ==UserScript==
2// @name		Netflix IMDb Ratings
3// @description		Shows IMDb ratings directly on Netflix thumbnails
4// @version		1.0.1
5// @match		https://www.netflix.com/*
6// @icon		https://ssl.gstatic.com/docs/spreadsheets/spreadsheets_2023q4.ico
7// @grant		GM.xmlhttpRequest
8// @connect		www.omdbapi.com
9// ==/UserScript==
10(function() {
11    'use strict';
12
13    // OMDb API key (free tier - 1000 requests/day)
14    const OMDB_API_KEY = '3e13c010';
15    const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
16    
17    // Track processed titles to avoid duplicates
18    const processedTitles = new Set();
19    
20    // Debounce function to limit API calls
21    function debounce(func, wait) {
22        let timeout;
23        return function executedFunction(...args) {
24            const later = () => {
25                clearTimeout(timeout);
26                func(...args);
27            };
28            clearTimeout(timeout);
29            timeout = setTimeout(later, wait);
30        };
31    }
32
33    // Extract title name from Netflix card
34    function extractTitleName(titleCard) {
35        // Try multiple selectors for title text
36        const selectors = [
37            '.fallback-text',
38            '.title-card-title',
39            'img[alt]',
40            '[aria-label]',
41            '.bob-title'
42        ];
43        
44        for (const selector of selectors) {
45            const element = titleCard.querySelector(selector);
46            if (element) {
47                const title = element.getAttribute('alt') || 
48                             element.getAttribute('aria-label') || 
49                             element.textContent;
50                if (title && title.trim()) {
51                    return title.trim();
52                }
53            }
54        }
55        
56        return null;
57    }
58
59    // Fetch IMDb rating from OMDb API with caching
60    async function fetchIMDbRating(title) {
61        try {
62            // Check cache first
63            const cacheKey = `imdb_rating_${title}`;
64            const cached = await GM.getValue(cacheKey);
65            
66            if (cached) {
67                const data = JSON.parse(cached);
68                // Check if cache is still valid
69                if (Date.now() - data.timestamp < CACHE_DURATION) {
70                    console.log(`Using cached rating for: ${title}`);
71                    return data.rating;
72                }
73            }
74
75            // Fetch from API
76            console.log(`Fetching IMDb rating for: ${title}`);
77            const response = await GM.xmlhttpRequest({
78                method: 'GET',
79                url: `https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&t=${encodeURIComponent(title)}&type=movie`,
80                responseType: 'json'
81            });
82
83            if (response.response && response.response.Response === 'True') {
84                const rating = response.response.imdbRating;
85                
86                if (rating && rating !== 'N/A') {
87                    // Cache the result
88                    await GM.setValue(cacheKey, JSON.stringify({
89                        rating: rating,
90                        timestamp: Date.now()
91                    }));
92                    
93                    return rating;
94                }
95            }
96            
97            return null;
98        } catch (error) {
99            console.error(`Error fetching IMDb rating for ${title}:`, error);
100            return null;
101        }
102    }
103
104    // Create and add rating badge to title card
105    function addRatingBadge(titleCard, rating) {
106        // Check if badge already exists
107        if (titleCard.querySelector('.imdb-rating-badge')) {
108            return;
109        }
110
111        const badge = document.createElement('div');
112        badge.className = 'imdb-rating-badge';
113        badge.innerHTML = `
114            <span class="imdb-star"></span>
115            <span class="imdb-rating-value">${rating}</span>
116        `;
117
118        // Find the best container to append the badge
119        const container = titleCard.querySelector('.boxart-container') || 
120                         titleCard.querySelector('.slider-refocus') ||
121                         titleCard;
122        
123        container.style.position = 'relative';
124        container.appendChild(badge);
125        
126        console.log(`Added IMDb rating badge: ${rating}`);
127    }
128
129    // Process a single title card
130    async function processTitleCard(titleCard) {
131        // Create unique identifier for this card
132        const cardId = titleCard.getAttribute('data-list-context') || 
133                      titleCard.querySelector('img')?.src || 
134                      Math.random().toString();
135        
136        // Skip if already processed
137        if (processedTitles.has(cardId)) {
138            return;
139        }
140        
141        processedTitles.add(cardId);
142        
143        const titleName = extractTitleName(titleCard);
144        if (!titleName) {
145            console.log('Could not extract title name from card');
146            return;
147        }
148
149        const rating = await fetchIMDbRating(titleName);
150        if (rating) {
151            addRatingBadge(titleCard, rating);
152        }
153    }
154
155    // Process all visible title cards
156    async function processAllTitleCards() {
157        // Netflix uses various selectors for title cards
158        const selectors = [
159            '.title-card',
160            '.slider-item',
161            '[data-list-context]',
162            '.titleCard'
163        ];
164        
165        const titleCards = new Set();
166        
167        for (const selector of selectors) {
168            document.querySelectorAll(selector).forEach(card => titleCards.add(card));
169        }
170        
171        console.log(`Found ${titleCards.size} title cards to process`);
172        
173        // Process cards in batches to avoid overwhelming the API
174        const cardsArray = Array.from(titleCards);
175        for (let i = 0; i < cardsArray.length; i += 3) {
176            const batch = cardsArray.slice(i, i + 3);
177            await Promise.all(batch.map(card => processTitleCard(card)));
178            // Small delay between batches
179            await new Promise(resolve => setTimeout(resolve, 100));
180        }
181    }
182
183    // Add CSS styles for the rating badge
184    function addStyles() {
185        const styles = `
186            .imdb-rating-badge {
187                position: absolute;
188                top: 8px;
189                right: 8px;
190                background: rgba(0, 0, 0, 0.85);
191                color: #f5c518;
192                padding: 4px 8px;
193                border-radius: 4px;
194                font-size: 14px;
195                font-weight: bold;
196                font-family: Arial, sans-serif;
197                display: flex;
198                align-items: center;
199                gap: 4px;
200                z-index: 10;
201                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
202                backdrop-filter: blur(4px);
203                transition: transform 0.2s ease;
204            }
205            
206            .imdb-rating-badge:hover {
207                transform: scale(1.05);
208            }
209            
210            .imdb-star {
211                color: #f5c518;
212                font-size: 16px;
213            }
214            
215            .imdb-rating-value {
216                color: #ffffff;
217                font-size: 13px;
218            }
219        `;
220        
221        TM_addStyle(styles);
222    }
223
224    // Set up MutationObserver to watch for new content
225    function setupObserver() {
226        const debouncedProcess = debounce(processAllTitleCards, 1000);
227        
228        const observer = new MutationObserver((mutations) => {
229            // Check if new title cards were added
230            let shouldProcess = false;
231            
232            for (const mutation of mutations) {
233                if (mutation.addedNodes.length > 0) {
234                    for (const node of mutation.addedNodes) {
235                        if (node.nodeType === 1) { // Element node
236                            if (node.classList?.contains('title-card') ||
237                                node.classList?.contains('slider-item') ||
238                                node.querySelector?.('.title-card') ||
239                                node.querySelector?.('.slider-item')) {
240                                shouldProcess = true;
241                                break;
242                            }
243                        }
244                    }
245                }
246                if (shouldProcess) break;
247            }
248            
249            if (shouldProcess) {
250                console.log('New content detected, processing title cards...');
251                debouncedProcess();
252            }
253        });
254
255        observer.observe(document.body, {
256            childList: true,
257            subtree: true
258        });
259        
260        console.log('MutationObserver set up successfully');
261    }
262
263    // Initialize the extension
264    async function init() {
265        console.log('Netflix IMDb Ratings extension initialized');
266        
267        // Add styles
268        addStyles();
269        
270        // Wait for Netflix to load
271        await new Promise(resolve => setTimeout(resolve, 2000));
272        
273        // Process initial content
274        await processAllTitleCards();
275        
276        // Set up observer for dynamic content
277        setupObserver();
278        
279        // Re-process on navigation (Netflix is a SPA)
280        let lastUrl = location.href;
281        new MutationObserver(() => {
282            const currentUrl = location.href;
283            if (currentUrl !== lastUrl) {
284                lastUrl = currentUrl;
285                console.log('Navigation detected, reprocessing...');
286                processedTitles.clear();
287                setTimeout(processAllTitleCards, 2000);
288            }
289        }).observe(document.querySelector('title'), {
290            childList: true,
291            subtree: true
292        });
293    }
294
295    // Start when DOM is ready
296    if (document.readyState === 'loading') {
297        document.addEventListener('DOMContentLoaded', init);
298    } else {
299        init();
300    }
301})();