/*
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
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎
0 B
|👍
/👎