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