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