Netflix IMDb Ratings

Shows IMDb ratings on Netflix movie and TV show thumbnails

Size

9.4 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 on Netflix movie and TV show thumbnails
4// @version		1.0.1
5// @match		https://*.netflix.com/*
6// @icon		https://assets.nflxext.com/ffe/siteui/common/icons/nficon2016.ico
7// @grant		GM.xmlhttpRequest
8// @grant		GM.getValue
9// @grant		GM.setValue
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    // OMDb API key (free tier - 1000 requests/day)
15    const OMDB_API_KEY = '3e6e0c8f';
16    const CACHE_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
17
18    // Debounce function to prevent excessive calls
19    function debounce(func, wait) {
20        let timeout;
21        return function executedFunction(...args) {
22            const later = () => {
23                clearTimeout(timeout);
24                func(...args);
25            };
26            clearTimeout(timeout);
27            timeout = setTimeout(later, wait);
28        };
29    }
30
31    // Extract title from Netflix card
32    function extractTitle(card) {
33        // Try multiple selectors for title
34        const titleSelectors = [
35            '.fallback-text',
36            '.title-card-title',
37            '[class*="title"]',
38            'img[alt]'
39        ];
40
41        for (const selector of titleSelectors) {
42            const element = card.querySelector(selector);
43            if (element) {
44                const title = element.textContent || element.getAttribute('alt');
45                if (title && title.trim()) {
46                    return title.trim();
47                }
48            }
49        }
50
51        return null;
52    }
53
54    // Fetch IMDb rating from OMDb API
55    async function fetchIMDbRating(title) {
56        try {
57            // Check cache first
58            const cacheKey = `imdb_${title}`;
59            const cached = await GM.getValue(cacheKey);
60            
61            if (cached) {
62                const data = JSON.parse(cached);
63                if (Date.now() - data.timestamp < CACHE_EXPIRY) {
64                    console.log(`Using cached rating for: ${title}`);
65                    return data.rating;
66                }
67            }
68
69            // Try fetching as a series first, then as a movie
70            for (const type of ['series', 'movie']) {
71                const url = `https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&t=${encodeURIComponent(title)}&type=${type}`;
72                console.log(`Fetching IMDb rating for: ${title} (${type})`);
73
74                const response = await GM.xmlhttpRequest({
75                    method: 'GET',
76                    url: url,
77                    responseType: 'json'
78                });
79
80                if (response.status === 200) {
81                    const data = response.response;
82                    
83                    if (data.Response === 'True' && data.imdbRating && data.imdbRating !== 'N/A') {
84                        const rating = data.imdbRating;
85                        
86                        // Cache the result
87                        await GM.setValue(cacheKey, JSON.stringify({
88                            rating: rating,
89                            timestamp: Date.now()
90                        }));
91                        
92                        console.log(`Found rating for ${title}: ${rating}`);
93                        return rating;
94                    }
95                }
96            }
97
98            return null;
99        } catch (error) {
100            console.error(`Error fetching IMDb rating for ${title}:`, error);
101            return null;
102        }
103    }
104
105    // Create rating badge element
106    function createRatingBadge(rating) {
107        const badge = document.createElement('div');
108        badge.className = 'imdb-rating-badge';
109        badge.innerHTML = `
110            <div class="imdb-badge-content">
111                <span class="imdb-star"></span>
112                <span class="imdb-rating">${rating}</span>
113            </div>
114        `;
115        return badge;
116    }
117
118    // Add rating badge to Netflix card
119    async function addRatingToCard(card) {
120        // Check if badge already exists
121        if (card.querySelector('.imdb-rating-badge') || card.dataset.imdbProcessed === 'true') {
122            return;
123        }
124
125        // Mark as processed to prevent duplicates
126        card.dataset.imdbProcessed = 'true';
127
128        const title = extractTitle(card);
129        if (!title) {
130            console.log('Could not extract title from card');
131            return;
132        }
133
134        const rating = await fetchIMDbRating(title);
135        if (rating) {
136            const badge = createRatingBadge(rating);
137            
138            // Find the best place to insert the badge
139            const imageContainer = card.querySelector('.boxart-container, .title-card-image-wrapper, [class*="image"]');
140            if (imageContainer) {
141                imageContainer.style.position = 'relative';
142                imageContainer.appendChild(badge);
143            } else {
144                card.style.position = 'relative';
145                card.appendChild(badge);
146            }
147        }
148    }
149
150    // Process all Netflix cards on the page
151    async function processNetflixCards() {
152        // Netflix uses various selectors for title cards
153        const cardSelectors = [
154            '.title-card',
155            '.slider-item',
156            '[class*="title-card"]',
157            '.titleCard'
158        ];
159
160        const cards = [];
161        for (const selector of cardSelectors) {
162            const elements = document.querySelectorAll(selector);
163            elements.forEach(el => {
164                if (!cards.includes(el)) {
165                    cards.push(el);
166                }
167            });
168        }
169
170        console.log(`Found ${cards.length} Netflix cards to process`);
171
172        // Process cards with a small delay to avoid overwhelming the API
173        for (let i = 0; i < cards.length; i++) {
174            await addRatingToCard(cards[i]);
175            // Small delay between requests to be respectful to the API
176            if (i % 5 === 0 && i > 0) {
177                await new Promise(resolve => setTimeout(resolve, 100));
178            }
179        }
180    }
181
182    // Inject CSS styles
183    function injectStyles() {
184        const styles = `
185            .imdb-rating-badge {
186                position: absolute;
187                top: 8px;
188                right: 8px;
189                z-index: 10;
190                pointer-events: none;
191            }
192
193            .imdb-badge-content {
194                background: rgba(0, 0, 0, 0.85);
195                backdrop-filter: blur(4px);
196                border-radius: 6px;
197                padding: 4px 8px;
198                display: flex;
199                align-items: center;
200                gap: 4px;
201                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
202                border: 1px solid rgba(255, 255, 255, 0.1);
203            }
204
205            .imdb-star {
206                font-size: 14px;
207                line-height: 1;
208            }
209
210            .imdb-rating {
211                color: #ffffff;
212                font-size: 13px;
213                font-weight: 600;
214                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
215                line-height: 1;
216            }
217
218            /* Hover effect */
219            .title-card:hover .imdb-rating-badge .imdb-badge-content,
220            .slider-item:hover .imdb-rating-badge .imdb-badge-content {
221                background: rgba(0, 0, 0, 0.95);
222                transform: scale(1.05);
223                transition: all 0.2s ease;
224            }
225        `;
226
227        const styleElement = document.createElement('style');
228        styleElement.textContent = styles;
229        document.head.appendChild(styleElement);
230    }
231
232    // Initialize the extension
233    function init() {
234        console.log('Netflix IMDb Ratings extension initialized');
235        
236        // Inject styles
237        injectStyles();
238
239        // Process initial cards
240        setTimeout(() => {
241            processNetflixCards();
242        }, 2000);
243
244        // Watch for dynamic content changes
245        const debouncedProcess = debounce(processNetflixCards, 1000);
246        
247        const observer = new MutationObserver((mutations) => {
248            let shouldProcess = false;
249            
250            for (const mutation of mutations) {
251                if (mutation.addedNodes.length > 0) {
252                    for (const node of mutation.addedNodes) {
253                        if (node.nodeType === 1) { // Element node
254                            // Check if new cards were added
255                            if (node.classList && (
256                                node.classList.contains('title-card') ||
257                                node.classList.contains('slider-item') ||
258                                node.querySelector('.title-card, .slider-item')
259                            )) {
260                                shouldProcess = true;
261                                break;
262                            }
263                        }
264                    }
265                }
266                if (shouldProcess) break;
267            }
268
269            if (shouldProcess) {
270                console.log('New Netflix cards detected, processing...');
271                debouncedProcess();
272            }
273        });
274
275        // Start observing
276        observer.observe(document.body, {
277            childList: true,
278            subtree: true
279        });
280
281        console.log('MutationObserver started for dynamic content');
282    }
283
284    // Wait for page to be ready
285    if (document.readyState === 'loading') {
286        document.addEventListener('DOMContentLoaded', init);
287    } else {
288        init();
289    }
290})();