JohannesSoderberg

Jellyfin IMDb ratings on homescreen

Jun 10th, 2026
4,463
0
Never
2
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 9.58 KB | Source Code | 0 0
  1. /*
  2.  
  3. Script to get IMDb rating next to the release year under poster on cards in Jellyfin
  4. Inspired by this post: https://www.reddit.com/r/jellyfin/comments/1qn68yy/jellyfin_ratings_setup_complete/
  5. Disclaimer: Everything apart from code copied from the above script was written by my AI slaves.
  6.  
  7. 1. Install Jellyfin JavaScript Injector
  8.    https://github.com/n00bcodr/Jellyfin-JavaScript-Injector
  9.  
  10. 2. Restart Jellyfin after installing the plugin.
  11.  
  12. 3. Create an account on https://api.mdblist.com/ and generate an API key
  13.    You will get 1K free requests per day. A $1.80 Patreon sub gives you 10K.
  14.    If you show a lot of cards, 1K won't be enough, but you can set cache below.
  15.    cURL command to check your rate limit info:
  16.    $ curl "https://api.mdblist.com/user?apikey=YOUR_MDBLIST_API_KEY_HERE"
  17.  
  18. 4. Go to: Dashboard → Plugins → JavaScript Injector
  19.  
  20. 5. Paste the full JavaScript code into the large Custom JavaScript box and save.
  21.  
  22. */
  23.  
  24.  
  25. (function () {
  26. 'use strict';
  27.  
  28. const API_KEY = 'YOUR_MDBLIST_API_KEY_HERE';
  29. const LOGO = 'https://cdn.jsdelivr.net/gh/Druidblack/jellyfin_ratings@main/logo/IMDb.png';
  30.  
  31. const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
  32. const REQUEST_DELAY_MS = 300;
  33.  
  34. const ratingCache = new Map();
  35. const queue = [];
  36. const queued = new WeakSet();
  37.  
  38. let queueRunning = false;
  39.  
  40. const visibleObserver = new IntersectionObserver(entries => {
  41.     for (const entry of entries) {
  42.         if (!entry.isIntersecting) continue;
  43.  
  44.         visibleObserver.unobserve(entry.target);
  45.         enqueue(entry.target);
  46.     }
  47. }, {
  48.     root: null,
  49.     rootMargin: '600px 0px',
  50.     threshold: 0.01
  51. });
  52.  
  53. function scan(root = document) {
  54.     root.querySelectorAll('.card[data-id]:not([data-imdb-done])').forEach(observeCard);
  55.     root.querySelectorAll('.listItem[data-id]:not([data-imdb-done])').forEach(observeListItem);
  56. }
  57.  
  58. function observeCard(card) {
  59.     if (!['Movie', 'Series'].includes(card.dataset.type)) return;
  60.  
  61.     const target =
  62.         card.querySelector('.cardText-secondary') ||
  63.         card.querySelector('.cardText-first');
  64.  
  65.     if (!target) return;
  66.  
  67.     visibleObserver.observe(card);
  68. }
  69.  
  70. function observeListItem(item) {
  71.     if (!['Movie', 'Series'].includes(item.dataset.type)) return;
  72.  
  73.     const target = item.querySelector('.listItemBodyText');
  74.  
  75.     if (!target) return;
  76.  
  77.     visibleObserver.observe(item);
  78. }
  79.  
  80. function enqueue(el) {
  81.     if (el.dataset.imdbDone) return;
  82.     if (queued.has(el)) return;
  83.  
  84.     queued.add(el);
  85.     queue.push(el);
  86.  
  87.     runQueue();
  88. }
  89.  
  90. function runQueue() {
  91.     if (queueRunning) return;
  92.  
  93.     queueRunning = true;
  94.     processNext();
  95. }
  96.  
  97. function processNext() {
  98.     const el = queue.shift();
  99.  
  100.     if (!el) {
  101.         queueRunning = false;
  102.         return;
  103.     }
  104.  
  105.     processElement(el)
  106.         .finally(() => {
  107.             setTimeout(processNext, REQUEST_DELAY_MS);
  108.         });
  109. }
  110.  
  111. function processElement(el) {
  112.     if (el.dataset.imdbDone) return Promise.resolve();
  113.  
  114.     const isList = el.classList.contains('listItem');
  115.  
  116.     let target;
  117.  
  118.     if (isList) {
  119.         target = el.querySelector('.listItemBodyText');
  120.     } else {
  121.         target =
  122.             el.querySelector('.cardText-secondary') ||
  123.             el.querySelector('.cardText-first');
  124.     }
  125.  
  126.     if (!target) return Promise.resolve();
  127.  
  128.     el.dataset.imdbDone = '1';
  129.  
  130.     return injectRating(el, target);
  131. }
  132.  
  133. function injectRating(el, target) {
  134.  
  135.     const row = document.createElement('span');
  136.  
  137.     row.className = 'imdb-home-rating';
  138.  
  139.     row.style.cssText =
  140.         'display:inline-flex;align-items:center;gap:0px;font-size:inherit;margin-left:8px;vertical-align:middle;color:inherit';
  141.  
  142.     target.appendChild(row);
  143.  
  144.     return ApiClient
  145.         .getItem(ApiClient.getCurrentUserId(), el.dataset.id)
  146.  
  147.         .then(item => {
  148.  
  149.             if (!item || !item.ProviderIds) return null;
  150.  
  151.             const type =
  152.                 item.Type === 'Series'
  153.                     ? 'show'
  154.                     : 'movie';
  155.  
  156.             return getRating(type, item.ProviderIds);
  157.         })
  158.  
  159.         .then(rating => {
  160.  
  161.             if (!rating) {
  162.                 row.remove();
  163.                 return;
  164.             }
  165.  
  166.             row.innerHTML = '';
  167.  
  168.             const logo = document.createElement('span');
  169.  
  170.             logo.title = 'IMDb: ' + rating;
  171.  
  172.             logo.style.cssText =
  173.                 'display:inline-block;' +
  174.                 'height:1.15em;' +
  175.                 'width:2.6em;' +
  176.                 'background-color:currentColor;' +
  177.                 'mask:url("' + LOGO + '") center / contain no-repeat;' +
  178.                 '-webkit-mask:url("' + LOGO + '") center / contain no-repeat;' +
  179.                 'vertical-align:middle;' +
  180.                 'opacity:.55';
  181.  
  182.             const value = document.createElement('span');
  183.  
  184.             value.textContent = rating;
  185.             value.style.color = 'inherit';
  186.  
  187.             row.append(
  188.                 logo,
  189.                 value
  190.             );
  191.         })
  192.  
  193.         .catch(() => {
  194.             row.remove();
  195.         });
  196. }
  197.  
  198. function getRating(type, providerIds) {
  199.  
  200.     const tmdb = providerIds.Tmdb;
  201.     const imdb = providerIds.Imdb;
  202.     const tvdb = providerIds.Tvdb;
  203.  
  204.     const key =
  205.         type +
  206.         ':' +
  207.         (tmdb || imdb || tvdb);
  208.  
  209.     if (ratingCache.has(key)) {
  210.         return Promise.resolve(
  211.             ratingCache.get(key)
  212.         );
  213.     }
  214.  
  215.     const cached =
  216.         getCachedRating(key);
  217.  
  218.     if (cached !== undefined) {
  219.  
  220.         ratingCache.set(
  221.             key,
  222.             cached
  223.         );
  224.  
  225.         return Promise.resolve(
  226.             cached
  227.         );
  228.     }
  229.  
  230.     const urls = [];
  231.  
  232.     if (tmdb) {
  233.         urls.push(
  234.             `https://api.mdblist.com/tmdb/${type}/${tmdb}?apikey=${API_KEY}`
  235.         );
  236.     }
  237.  
  238.     if (imdb) {
  239.         urls.push(
  240.             `https://api.mdblist.com/imdb/${type}/${imdb}?apikey=${API_KEY}`
  241.         );
  242.     }
  243.  
  244.     if (tvdb) {
  245.         urls.push(
  246.             `https://api.mdblist.com/tvdb/${type}/${tvdb}?apikey=${API_KEY}`
  247.         );
  248.     }
  249.  
  250.     return tryUrls(
  251.         urls,
  252.         key
  253.     );
  254. }
  255.  
  256. function tryUrls(urls, key) {
  257.  
  258.     if (!urls.length) {
  259.  
  260.         ratingCache.set(
  261.             key,
  262.             null
  263.         );
  264.  
  265.         setCachedRating(
  266.             key,
  267.             null
  268.         );
  269.  
  270.         return Promise.resolve(
  271.             null
  272.         );
  273.     }
  274.  
  275.     const url =
  276.         urls.shift();
  277.  
  278.     return fetch(url)
  279.  
  280.         .then(r =>
  281.             r.ok
  282.                 ? r.json()
  283.                 : null
  284.         )
  285.  
  286.         .then(j => {
  287.  
  288.             const imdb =
  289.                 j &&
  290.                 Array.isArray(j.ratings)
  291.  
  292.                     ? j.ratings.find(
  293.                         x =>
  294.                             String(
  295.                                 x.source || ''
  296.                             ).toLowerCase()
  297.                             === 'imdb'
  298.                     )
  299.  
  300.                     : null;
  301.  
  302.             const value =
  303.                 imdb &&
  304.                 imdb.value != null
  305.  
  306.                     ? Number(
  307.                         imdb.value
  308.                     ).toFixed(1)
  309.  
  310.                     : null;
  311.  
  312.             if (value) {
  313.  
  314.                 ratingCache.set(
  315.                     key,
  316.                     value
  317.                 );
  318.  
  319.                 setCachedRating(
  320.                     key,
  321.                     value
  322.                 );
  323.  
  324.                 return value;
  325.             }
  326.  
  327.             return tryUrls(
  328.                 urls,
  329.                 key
  330.             );
  331.         })
  332.  
  333.         .catch(() =>
  334.             tryUrls(
  335.                 urls,
  336.                 key
  337.             )
  338.         );
  339. }
  340.  
  341. function getCachedRating(key) {
  342.  
  343.     const raw =
  344.         localStorage.getItem(
  345.             'imdb-rating:' + key
  346.         );
  347.  
  348.     if (!raw)
  349.         return undefined;
  350.  
  351.     try {
  352.  
  353.         const cached =
  354.             JSON.parse(raw);
  355.  
  356.         if (
  357.             !cached ||
  358.             typeof cached !== 'object'
  359.         )
  360.             return undefined;
  361.  
  362.         if (
  363.             Date.now()
  364.             - cached.time
  365.             > CACHE_TTL_MS
  366.         )
  367.             return undefined;
  368.  
  369.         return cached.value;
  370.  
  371.     } catch {
  372.  
  373.         return undefined;
  374.     }
  375. }
  376.  
  377. function setCachedRating(
  378.     key,
  379.     value
  380. ) {
  381.     try {
  382.  
  383.         localStorage.setItem(
  384.             'imdb-rating:' + key,
  385.  
  386.             JSON.stringify({
  387.                 time:
  388.                     Date.now(),
  389.  
  390.                 value
  391.             })
  392.         );
  393.  
  394.     } catch {}
  395. }
  396.  
  397. new MutationObserver(
  398.     mutations => {
  399.  
  400.         for (
  401.             const mutation
  402.             of mutations
  403.         ) {
  404.  
  405.             for (
  406.                 const node
  407.                 of mutation.addedNodes
  408.             ) {
  409.  
  410.                 if (
  411.                     node.nodeType !== 1
  412.                 )
  413.                     continue;
  414.  
  415.                 scan(node);
  416.             }
  417.         }
  418.     }
  419. )
  420.  
  421. .observe(
  422.     document.body,
  423.     {
  424.         childList: true,
  425.         subtree: true
  426.     }
  427. );
  428.  
  429. setTimeout(
  430.     scan,
  431.     1000
  432. );
  433.  
  434. setTimeout(
  435.     scan,
  436.     3000
  437. );
  438.  
  439. setTimeout(
  440.     scan,
  441.     6000
  442. );
  443.  
  444. window.addEventListener(
  445.     'hashchange',
  446.     () =>
  447.         setTimeout(
  448.             scan,
  449.             1000
  450.         )
  451. );
  452.  
  453. window.addEventListener(
  454.     'popstate',
  455.     () =>
  456.         setTimeout(
  457.             scan,
  458.             1000
  459.         )
  460. );
  461.  
  462. document.addEventListener(
  463.     'viewshow',
  464.     () =>
  465.         setTimeout(
  466.             scan,
  467.             1000
  468.         )
  469. );
  470.  
  471. setInterval(
  472.     scan,
  473.     3000
  474. );
  475.  
  476. })();
Tags: jellyfin imdb
Advertisement