JohannesSoderberg icon

Jellyfin IMDb ratings on homescreen

JohannesSoderberg | PRO | 06/10/26 10:53:06 PM UTC | 0 ⭐ | 25746 👁️ | Never ⏰ | [jellyfin, imdb]
JavaScript |

9.58 KB

|

Source Code

|

0 👍

/

0 👎

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

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎