/* Script to get IMDb rating next to the release year under poster on cards in Jellyfin Inspired by this post: https://www.reddit.com/r/jellyfin/comments/1qn68yy/jellyfin_ratings_setup_complete/ Disclaimer: Everything apart from code copied from the above script was written by my AI slaves. 1. Install Jellyfin JavaScript Injector https://github.com/n00bcodr/Jellyfin-JavaScript-Injector 2. Restart Jellyfin after installing the plugin. 3. Create an account on https://api.mdblist.com/ and generate an API key You will get 1K free requests per day. A $1.80 Patreon sub gives you 10K. If you show a lot of cards, 1K won't be enough, but you can set cache below. cURL command to check your rate limit info: $ curl "https://api.mdblist.com/user?apikey=YOUR_MDBLIST_API_KEY_HERE" 4. Go to: Dashboard → Plugins → JavaScript Injector 5. Paste the full JavaScript code into the large Custom JavaScript box and save. */ (function () { 'use strict'; const API_KEY = 'YOUR_MDBLIST_API_KEY_HERE'; const LOGO = 'https://cdn.jsdelivr.net/gh/Druidblack/jellyfin_ratings@main/logo/IMDb.png'; const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; const REQUEST_DELAY_MS = 300; const ratingCache = new Map(); const queue = []; const queued = new WeakSet(); let queueRunning = false; const visibleObserver = new IntersectionObserver(entries => { for (const entry of entries) { if (!entry.isIntersecting) continue; visibleObserver.unobserve(entry.target); enqueue(entry.target); } }, { root: null, rootMargin: '600px 0px', threshold: 0.01 }); function scan(root = document) { root.querySelectorAll('.card[data-id]:not([data-imdb-done])').forEach(observeCard); root.querySelectorAll('.listItem[data-id]:not([data-imdb-done])').forEach(observeListItem); } function observeCard(card) { if (!['Movie', 'Series'].includes(card.dataset.type)) return; const target = card.querySelector('.cardText-secondary') || card.querySelector('.cardText-first'); if (!target) return; visibleObserver.observe(card); } function observeListItem(item) { if (!['Movie', 'Series'].includes(item.dataset.type)) return; const target = item.querySelector('.listItemBodyText'); if (!target) return; visibleObserver.observe(item); } function enqueue(el) { if (el.dataset.imdbDone) return; if (queued.has(el)) return; queued.add(el); queue.push(el); runQueue(); } function runQueue() { if (queueRunning) return; queueRunning = true; processNext(); } function processNext() { const el = queue.shift(); if (!el) { queueRunning = false; return; } processElement(el) .finally(() => { setTimeout(processNext, REQUEST_DELAY_MS); }); } function processElement(el) { if (el.dataset.imdbDone) return Promise.resolve(); const isList = el.classList.contains('listItem'); let target; if (isList) { target = el.querySelector('.listItemBodyText'); } else { target = el.querySelector('.cardText-secondary') || el.querySelector('.cardText-first'); } if (!target) return Promise.resolve(); el.dataset.imdbDone = '1'; return injectRating(el, target); } function injectRating(el, target) { const row = document.createElement('span'); row.className = 'imdb-home-rating'; row.style.cssText = 'display:inline-flex;align-items:center;gap:0px;font-size:inherit;margin-left:8px;vertical-align:middle;color:inherit'; target.appendChild(row); return ApiClient .getItem(ApiClient.getCurrentUserId(), el.dataset.id) .then(item => { if (!item || !item.ProviderIds) return null; const type = item.Type === 'Series' ? 'show' : 'movie'; return getRating(type, item.ProviderIds); }) .then(rating => { if (!rating) { row.remove(); return; } row.innerHTML = ''; const logo = document.createElement('span'); logo.title = 'IMDb: ' + rating; logo.style.cssText = 'display:inline-block;' + 'height:1.15em;' + 'width:2.6em;' + 'background-color:currentColor;' + 'mask:url("' + LOGO + '") center / contain no-repeat;' + '-webkit-mask:url("' + LOGO + '") center / contain no-repeat;' + 'vertical-align:middle;' + 'opacity:.55'; const value = document.createElement('span'); value.textContent = rating; value.style.color = 'inherit'; row.append( logo, value ); }) .catch(() => { row.remove(); }); } function getRating(type, providerIds) { const tmdb = providerIds.Tmdb; const imdb = providerIds.Imdb; const tvdb = providerIds.Tvdb; const key = type + ':' + (tmdb || imdb || tvdb); if (ratingCache.has(key)) { return Promise.resolve( ratingCache.get(key) ); } const cached = getCachedRating(key); if (cached !== undefined) { ratingCache.set( key, cached ); return Promise.resolve( cached ); } const urls = []; if (tmdb) { urls.push( `https://api.mdblist.com/tmdb/${type}/${tmdb}?apikey=${API_KEY}` ); } if (imdb) { urls.push( `https://api.mdblist.com/imdb/${type}/${imdb}?apikey=${API_KEY}` ); } if (tvdb) { urls.push( `https://api.mdblist.com/tvdb/${type}/${tvdb}?apikey=${API_KEY}` ); } return tryUrls( urls, key ); } function tryUrls(urls, key) { if (!urls.length) { ratingCache.set( key, null ); setCachedRating( key, null ); return Promise.resolve( null ); } const url = urls.shift(); return fetch(url) .then(r => r.ok ? r.json() : null ) .then(j => { const imdb = j && Array.isArray(j.ratings) ? j.ratings.find( x => String( x.source || '' ).toLowerCase() === 'imdb' ) : null; const value = imdb && imdb.value != null ? Number( imdb.value ).toFixed(1) : null; if (value) { ratingCache.set( key, value ); setCachedRating( key, value ); return value; } return tryUrls( urls, key ); }) .catch(() => tryUrls( urls, key ) ); } function getCachedRating(key) { const raw = localStorage.getItem( 'imdb-rating:' + key ); if (!raw) return undefined; try { const cached = JSON.parse(raw); if ( !cached || typeof cached !== 'object' ) return undefined; if ( Date.now() - cached.time > CACHE_TTL_MS ) return undefined; return cached.value; } catch { return undefined; } } function setCachedRating( key, value ) { try { localStorage.setItem( 'imdb-rating:' + key, JSON.stringify({ time: Date.now(), value }) ); } catch {} } new MutationObserver( mutations => { for ( const mutation of mutations ) { for ( const node of mutation.addedNodes ) { if ( node.nodeType !== 1 ) continue; scan(node); } } } ) .observe( document.body, { childList: true, subtree: true } ); setTimeout( scan, 1000 ); setTimeout( scan, 3000 ); setTimeout( scan, 6000 ); window.addEventListener( 'hashchange', () => setTimeout( scan, 1000 ) ); window.addEventListener( 'popstate', () => setTimeout( scan, 1000 ) ); document.addEventListener( 'viewshow', () => setTimeout( scan, 1000 ) ); setInterval( scan, 3000 ); })();