// ==UserScript==
// @name IMDb-Lists-Highlighter
// @author tronix42
// @copyright 2025, tronix42
// @copyright 2008-2024, Ricardo Mendonca Ferreira (original script - IMDb 'My Movies' enhancer)
// @namespace http://example.com/
// @version 1.1
// @description highlights movie titles, series titles, and people from your lists
// @include *://*.imdb.com/*
// @grant none
// @run-at document-idle
// @license GPL-3.0-or-later
// ==/UserScript==
//
// --------------------------------------------------------------------
//
// Thanks to AltoRetrato and his work with the great "IMDb 'My Movies' enhancer" Userscript.
// https://openuserjs.org/scripts/AltoRetrato/IMDb_My_Movies_enhancer
//
// This userscript highlights movie titles, series titles, and people from your lists. This way, you can immediately see which
// movies or series you've already seen or have on your watchlist while browsing IMDb. If you have lists of your
// favorite actors/actresses, you can see them highlighted in the calendar when they appear in a new film.
//
// This all works so far, with a small limitation. Custom lists and watchlist working fine. Unfortunately,
// the ratings list and check-in list don't work via automatic import. You have to take a detour for that.
// You can either manually import the ratings CSV file (which you downloaded previously) or create a custom list
// and add all rated films to it, which you then import via the script.
//
// A "Configure List" button will appear on the list page. All recognized lists will then be in the configuration,
// where you can assign each list a unique color. If you simply check the box next to the list WITHOUT uploading
// a CSV file, the lists will be imported automatically (as mentioned, this unfortunately doesn't work for
// ratings or check-ins). If you check the box AND upload a CSV file, the import will be done manually.
//
// As soon as you click Start Import, all lists to be imported will be displayed, along with a progress circle.
// When the import of a list is complete, the number of imported entries will be displayed next to it.
// After the import is finished, reload the page, and all imported entries should be highlighted.
// All custom colors will be saved. You don't have to import all lists at once — nothing will be lost if you import
// another list later. Clear Data deletes all data!
//
//
// History:
// --------
// 2025.05.19 [1.1] Added Fallback for GM_addStyle (Greasemonkey)
// 2025.05.12 [1.0] Public Release
// --------------------------------------------------------------------
(function() {
'use strict';
// ——— Fallback GM_addStyle (Greasemonkey 4+) ———
var GM_addStyle = (typeof GM_addStyle === 'function')
? GM_addStyle
: function(css) {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
};
// ———————————————————————————————————————————————
let myLists = [],
listOrder = [];
let progressModal = null;
let progressItems = [];
function getCurrentUser() {
const el = document.querySelector('[data-testid="user-menu-toggle-name"]') ||
document.querySelector('.navbar__user-menu-toggle__name') ||
document.querySelector('#nbpersonalize strong');
return el ? el.textContent.trim() : null;
}
function getStorageUser() {
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k.startsWith('myMovies-')) return k.slice(9);
}
return null;
}
function getUserId() {
const link = document.querySelector('a[href*="/user/ur"]');
if (link) {
const m = link.href.match(/\/user\/(ur\d+)\//);
if (m) return m[1];
}
console.error('IMDb User-ID not found');
return null;
}
const user = getCurrentUser() || getStorageUser();
// Check language Regex
const pathParts = window.location.pathname.split('/');
const langSegment = pathParts[1];
const langRegex = /^[a-z]{2}(?:-[A-Z]{2})?$/;
const countryPath = langRegex.test(langSegment) ? `/${langSegment}` : '';
if (!user) return;
// 1) Load lists from LocalStorage
const listsLoaded = loadLists();
// 2) Config-Button (Lists URL)
if (/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?user\/ur\d+\/lists/.test(location.pathname)) {
// (a) Load Database
let savedListsMap = {};
if (loadLists()) {
myLists.forEach(l => {
savedListsMap[l.id] = {
ids: JSON.parse(JSON.stringify(l.ids)),
names: l.names ? JSON.parse(JSON.stringify(l.names)) : {},
color: l.color,
selected: l.selected
};
});
}
// (b) Show all Lists
collectLists();
addConfigButton();
// (c) Overwrite Data
Object.entries(savedListsMap).forEach(([id, data]) => {
const lst = myLists.find(x => x.id === id);
if (lst) {
lst.ids = data.ids;
lst.names = data.names;
lst.color = data.color;
lst.selected = data.selected;
}
});
// (d) Highlight on lists-URL with CSS
let css = '';
myLists.forEach(list => {
Object.keys(list.ids).forEach(code36 => {
const num = parseInt(code36, 36);
css += `
a[href*="/title/tt${num}/?ref_"] {
color: ${list.color} !important;
font-weight: bold !important;
}
`;
});
});
GM_addStyle(css);
highlightLinks();
}
// 3) CSS and Search-Highlight
if (listsLoaded) {
// (a) CSS-String
let css = '';
myLists.forEach(list => {
Object.keys(list.ids).forEach(code36 => {
const num = parseInt(code36, 36);
css += `
a[href*="/title/tt${num}/?ref_"] {
color: ${list.color} !important;
font-weight: bold !important;
}
`;
});
});
GM_addStyle(css);
// (b) Search-Highlight-Function
highlightTitle();
highlightLinks();
if (/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?calendar/.test(location.pathname)) {
highlightCalendarPeople();
// Observer for Calendar URL
new MutationObserver(() => highlightCalendarPeople())
.observe(document.body, {
childList: true,
subtree: true
});
}
const observer = new MutationObserver(() => highlightLinks());
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function collectLists() {
// 1) Load savedColors
let savedColors = {};
if (loadLists()) {
savedColors = myLists.reduce((map, l) => {
map[l.id] = l.color;
return map;
}, {});
}
const customColors = {
"Your Watchlist": "DarkGoldenRod",
"Your Ratings": "Green"
};
const defaultColor = 'Red';
myLists = [];
listOrder = [];
const seen = new Set();
[
["Your Watchlist", "watchlist"],
["Your Ratings", "ratings"],
["Your check-ins", "checkins"]
].forEach(([name, id], i) => {
// 2a) check savedColors, customColors and defaultColor
myLists.push({
name,
id,
color: savedColors[id] || customColors[name] || defaultColor,
ids: {},
names: {},
selected: false,
csvFile: null
});
listOrder.push(i);
seen.add(id);
});
document.querySelectorAll('a[href*="/list/ls"]').forEach(a => {
const m = a.href.match(/\/list\/(ls\d+)/);
if (!m) return;
const id = m[1];
if (seen.has(id)) return;
seen.add(id);
const raw = a.getAttribute('aria-label') || a.title || a.textContent.trim();
const name = raw.replace(/^View list page for\s*/i, '').trim();
// 2b) check savedColors and defaultColor
myLists.push({
name,
id,
color: savedColors[id] || defaultColor,
ids: {},
names: {},
selected: false,
csvFile: null
});
listOrder.push(myLists.length - 1);
});
}
function addConfigButton() {
const h1 = document.querySelector('h1');
if (!h1) return;
const btn = document.createElement('button');
btn.textContent = 'Configure lists';
btn.style.margin = '0 10px';
btn.onclick = openConfig;
h1.parentNode.insertBefore(btn, h1.nextSibling);
}
function openConfig() {
// --- LOAD & MERGE PREVIOUS STATE ---
let savedListsMap = {};
if (loadLists()) {
myLists.forEach(l => {
savedListsMap[l.id] = {
ids: JSON.parse(JSON.stringify(l.ids)),
color: l.color,
selected: l.selected
};
});
}
collectLists();
const modal = document.createElement('div');
modal.style = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;';
const box = document.createElement('div');
box.style = 'background:#fff;padding:20px;max-height:80%;overflow:auto;';
const header = document.createElement('div');
header.style = 'display:flex;align-items:left;margin-bottom:1px;font-weight:bold;';
const hChk = document.createElement('span');
hChk.style = 'width:1px;';
const hLists = document.createElement('span');
hLists.textContent = 'Lists:';
hLists.style = 'margin-right:112px;';
const hCsv = document.createElement('span');
hCsv.textContent = 'CSV file:';
hCsv.style = 'margin-right:1px;';
const hColor = document.createElement('span');
hColor.textContent = 'Color (HEX or Name):';
hColor.style = 'margin-right:18px;';
header.append(hChk, hLists, hColor, hCsv);
box.appendChild(header);
myLists.forEach((lst, i) => {
const sav = savedListsMap[lst.id];
if (sav) {
lst.ids = sav.ids;
lst.color = sav.color;
lst.selected = sav.selected;
}
const row = document.createElement('div');
row.style = 'margin:4px 0; display:flex; align-items:center;';
// Checkbox automatic Import
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.style = 'margin-right:8px;';
chk.checked = lst.selected;
chk.onchange = e => {
lst.selected = e.target.checked;
if (lst.selected) lst.csvFile = null;
};
// Label
const lbl = document.createElement('span');
lbl.textContent = ' ' + lst.name + ' ';
lbl.style = 'margin-right:8px;';
lbl.style.fontWeight = 'normal';
// color label list, when imported
if (Object.keys(lst.ids).length > 0) {
lbl.style.color = lst.color;
lbl.style.fontWeight = 'bold';
}
// File-Input (local list CSV-file import)
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.csv';
fileInput.style = 'margin-left:8px;';
fileInput.onchange = e => {
lst.csvFile = e.target.files[0];
};
// Color-Picker & Hex
const col = document.createElement('input');
col.type = 'color';
col.value = nameToHex(lst.color);
col.style = 'margin-left:8px; margin-right:10px;';
col.oninput = e => {
lst.color = e.target.value;
txt.value = e.target.value;
if (Object.keys(lst.ids).length > 0) {
lbl.style.color = lst.color;
lbl.style.fontWeight = 'bold';
}
};
// Input Color-Textbox
const txt = document.createElement('input');
txt.type = 'text';
txt.value = lst.color.toLowerCase();
txt.placeholder = '#Hex or Name';
txt.style = 'width:100px; margin-left:auto;';
txt.oninput = e => {
const v = e.target.value.trim().toLowerCase();
lst.color = v;
if (/^#([0-9A-Fa-f]{6})$/.test(v)) {
col.value = v;
} else {
try {
col.value = nameToHex(v);
} catch {}
}
if (Object.keys(lst.ids).length > 0) {
lbl.style.color = lst.color;
lbl.style.fontWeight = 'bold';
}
};
row.append(chk, lbl, txt, col, fileInput);
box.appendChild(row);
});
const imp = document.createElement('button');
imp.textContent = 'Start Import';
imp.style.margin = '10px';
imp.onclick = () => {
startImport();
document.body.removeChild(modal);
};
const clr = document.createElement('button');
clr.textContent = 'Clear Data';
clr.style.margin = '10px';
clr.onclick = () => {
eraseData();
alert('Data cleared');
document.body.removeChild(modal);
};
const cxl = document.createElement('button');
cxl.textContent = 'Cancel';
cxl.style.margin = '10px';
cxl.onclick = () => document.body.removeChild(modal);
box.append(imp, clr, cxl);
modal.appendChild(box);
document.body.appendChild(modal);
}
// startImport: CSV import vs. automatic import
function startImport() {
const tasks = [];
myLists.forEach((l, i) => {
if (l.selected && l.csvFile) {
// CSV import, only if Checkbox is seleced
tasks.push({
type: 'csv',
idx: i
});
} else if (l.selected) {
// automatic import only if Checkbox is selected and no CSV-file loaded
tasks.push({
type: 'auto',
idx: i
});
}
});
if (!tasks.length) {
alert('No Lists selected!');
return;
}
// eraseData()
createProgressModal(tasks.map(o => o.idx));
let rem = tasks.length;
tasks.forEach(({
type,
idx
}) => {
if (type === 'csv') {
importCsv(idx, () => {
updateListProgress(idx, Object.keys(myLists[idx].ids).length);
if (--rem === 0) {
// 1) clear all Checkbox
myLists.forEach(l => l.selected = false);
// 2) save changes
saveLists();
// 3) close progress pop-up
finishProgress();
}
});
} else {
downloadList(idx, () => {
const cnt = Object.keys(myLists[idx].ids).length;
updateListProgress(idx, cnt);
if (--rem === 0) {
myLists.forEach(l => l.selected = false);
saveLists();
finishProgress();
}
});
}
});
}
// CSV-Import function
function importCsv(idx, cb) {
const lst = myLists[idx];
lst.ids = {};
const file = lst.csvFile;
const reader = new FileReader();
reader.onload = e => {
const text = e.target.result;
text.split(/\r?\n/).forEach(line => {
// search tt-Code
const match = line.match(/tt(\d+)/i);
if (!match) return;
const num = match[1]; // for example "1234567"
const id36 = parseInt(num, 10).toString(36);
lst.ids[id36] = {};
});
cb();
};
reader.onerror = () => {
console.error('CSV Import error', reader.error);
cb();
};
reader.readAsText(file);
}
function createProgressModal(selectedIndices) {
progressModal = document.createElement('div');
progressModal.style = 'position:fixed;top:0;left:0;width:100%;height:100%;' +
'background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;';
const box = document.createElement('div');
box.style = 'background:#fff;padding:20px;max-height:80%;overflow:auto;min-width:300px;';
const header = document.createElement('h2');
header.id = 'progressHeader';
header.textContent = `Import ${selectedIndices.length} Lists`;
box.appendChild(header);
box.appendChild(document.createElement('br'));
progressItems = selectedIndices.map((listIdx, idx) => {
const row = document.createElement('div');
row.style = 'display:flex;align-items:center;margin:4px 0;';
const label = document.createElement('span');
label.textContent = `${idx+1}. Import ${myLists[listIdx].name}`;
label.style = 'flex:1;';
const spinner = document.createElement('div');
spinner.className = 'item-spinner';
spinner.style = 'margin-left:8px;border:4px solid #ccc;border-top:4px solid #3498db;' +
'border-radius:50%;width:16px;height:16px;animation:spin 1s linear infinite;';
row.append(label, spinner);
box.appendChild(row);
return {
listIdx,
row,
label,
spinner
};
});
const style = document.createElement('style');
style.id = 'spinStyle';
style.textContent = '@keyframes spin {0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}';
document.head.appendChild(style);
progressModal.appendChild(box);
document.body.appendChild(progressModal);
}
function updateListProgress(listIdx, count) {
const item = progressItems.find(i => i.listIdx === listIdx);
if (!item) return;
if (item.spinner) item.row.removeChild(item.spinner);
item.label.textContent = `${item.label.textContent}: ${count} items imported`;
}
function finishProgress() {
const spinEl = document.getElementById('spinStyle');
if (spinEl) {
spinEl.parentNode.removeChild(spinEl);
}
const box = progressModal.querySelector('div');
const footer = document.createElement('div');
footer.style = 'margin-top:12px;text-align:center;font-weight:bold;';
footer.textContent = 'Import finished!';
box.appendChild(footer);
const btn = document.createElement('button');
btn.textContent = 'OK';
btn.style = 'margin-top:10px;padding:6px 12px;';
btn.onclick = () => document.body.removeChild(progressModal);
box.appendChild(btn);
}
function eraseData() {
localStorage.removeItem('myMovies-' + user);
}
function saveLists() {
localStorage.setItem('myMovies-' + user, JSON.stringify({
myLists,
listOrder
}));
}
function loadLists() {
const d = localStorage.getItem('myMovies-' + user);
if (!d) return false;
const o = JSON.parse(d);
myLists = o.myLists;
listOrder = o.listOrder;
return true;
}
function downloadList(idx, cb) {
const lst = myLists[idx];
lst.ids = {};
if (lst.id === 'watchlist') {
// Basis-URL + language regex
const BASE = `${window.location.origin}${countryPath}/user/${getUserId()}/watchlist/`;
let page = 1,
seen = new Set();
(async function fetchPage() {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
// Detail-View + Paginierung
iframe.src = `${BASE}?view=detail&page=${page}`;
document.body.appendChild(iframe);
await new Promise(r => iframe.onload = r);
await new Promise(r => setTimeout(r, 2000));
const doc = iframe.contentDocument;
const sel1 = Array.from(doc.querySelectorAll('a.ipc-title-link-wrapper[href*="/title/tt"]'));
const sel2 = Array.from(doc.querySelectorAll('.lister-item-header a[href*="/title/tt"]'));
const anchors = sel1.length ? sel1 : sel2;
let newFound = false;
anchors.forEach(a => {
const m = a.href.match(/tt(\d+)\//);
if (!m) return;
const code36 = parseInt(m[1], 10).toString(36);
if (!seen.has(code36)) {
seen.add(code36);
lst.ids[code36] = {};
newFound = true;
}
});
document.body.removeChild(iframe);
// load next page if at least one new item found
if (newFound) {
page++;
fetchPage();
} else {
cb();
}
})();
return;
}
// per JSON-LD for Custom lists, ratings... exepct Watchlist
// automatic didfference People and Titles Lists
// JSON-LD-Pagination for Custom lists (Titles and People)
(async () => {
const base = `https://www.imdb.com/list/${lst.id}/?mode=detail`;
let page = 1;
let isPeople = null;
while (true) {
// load page with &page=
const resp = await fetch(`${base}&page=${page}`, {
credentials: 'same-origin'
});
const html = await resp.text();
const d = new DOMParser().parseFromString(html, 'text/html');
const sc = d.querySelector('script[type="application/ld+json"]');
if (!sc) break;
let data;
try {
data = JSON.parse(sc.textContent);
} catch (err) {
console.error('JSON-LD parse error', err);
break;
}
// Detect list type
if (page === 1) {
const first = data.itemListElement[0];
isPeople = (first['@type'] === 'Person') ||
(first.item && first.item['@type'] === 'Person');
}
const items = data.itemListElement || [];
if (!items.length) break;
// extract ID
items.forEach(e => {
const l = e.url ||
e['@id'] ||
(e.item && (e.item.url || e.item['@id'])) ||
'';
const re = isPeople ? /name\/nm(\d+)\// : /tt(\d+)\//;
const m = l.match(re);
if (m) {
const code36 = parseInt(m[1], 10).toString(36);
lst.ids[code36] = {};
if (isPeople && e.item && e.item.name) {
lst.names[e.item.name.trim()] = code36;
}
}
});
// load 250 entries per page, if entries lower than 250 end import
if (items.length < 250) break;
page++;
}
cb();
})();
}
function highlightTitle() {
// Titels-URLs
let m = location.href.match(/tt(\d+)\//);
if (m) {
const c = movieColor(parseInt(m[1], 10).toString(36));
if (c) document.querySelectorAll('h1').forEach(h => h.style.color = c);
}
// People-URLs
m = location.href.match(/name\/nm(\d+)\//);
if (m) {
const c = movieColor(parseInt(m[1], 10).toString(36));
if (c) document.querySelectorAll('h1').forEach(h => h.style.color = c);
}
}
function highlightLinks() {
// 1) highlight standard titles-links on all URLs
document.querySelectorAll('a[href*="/title/tt"]').forEach(a => {
const m = a.href.match(
/^https?:\/\/(?:www\.)?imdb\.com\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?title\/tt(\d+)\/\?ref_=[^/]+/
);
if (!m) return;
const code36 = parseInt(m[1], 10).toString(36);
const c = movieColor(code36);
if (c) {
a.style.color = c;
a.style.fontWeight = 'bold';
}
});
// highlight standard people-links on all URLs
const peopleLinkRe = /^https:\/\/www\.imdb\.com\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?name\/nm(\d+)\/\?ref_=[^&#]+$/;
document.querySelectorAll('a[href]').forEach(a => {
const href = a.href;
const m = peopleLinkRe.exec(href);
if (!m) return;
const code36 = parseInt(m[1], 10).toString(36);
const c = movieColor(code36);
if (c) {
a.style.color = c;
a.style.fontWeight = 'bold';
}
});
// 2) highlight Suggestion-Items
document
.querySelectorAll('li[id^="react-autowhatever-navSuggestionSearch"]')
.forEach(li => {
let link = li.querySelector('a[href*="/title/tt"]');
if (!link) link = li.querySelector('[data-testid="search-result--link"]');
if (!link || !link.href) return;
const m = link.href.match(
/^https?:\/\/(?:www\.)?imdb\.com\/title\/tt(\d+)\/\?ref_=[^/]+/
);
if (!m) return;
const code36 = parseInt(m[1], 10).toString(36);
const c = movieColor(code36);
if (!c) return;
// Titles span in suggestion item
const titleSpan =
li.querySelector('.searchResult__constTitle') ||
li.querySelector('span');
if (titleSpan) {
titleSpan.style.color = c;
titleSpan.style.fontWeight = 'bold';
}
});
// 3) highlight people-Suggestions
document
.querySelectorAll('li[id^="react-autowhatever-navSuggestionSearch"]')
.forEach(li => {
let link = li.querySelector('a[href*="/name/nm"]');
if (!link) link = li.querySelector('[data-testid="search-result--link"]');
if (!link || !link.href) return;
const m = link.href.match(
/^https?:\/\/(?:www\.)?imdb\.com\/name\/nm(\d+)\/\?ref_=[^/]+/
);
if (!m) return;
const code36 = parseInt(m[1], 10).toString(36);
const c = movieColor(code36);
if (!c) return;
// Titles span in suggestion item
const nameSpan =
li.querySelector('.searchResult__actorName') ||
li.querySelector('.searchResult__constTitle') ||
li.querySelector('span');
if (nameSpan) {
nameSpan.style.color = c;
nameSpan.style.fontWeight = 'bold';
}
});
}
function highlightCalendarPeople() {
document
.querySelectorAll('ul.ipc-metadata-list-summary-item__stl span.ipc-metadata-list-summary-item__li')
.forEach(span => {
const name = span.textContent.trim();
for (const i of listOrder) {
const lst = myLists[i];
if (lst.names && lst.names[name]) {
span.style.color = lst.color;
span.style.fontWeight = 'bold';
break;
}
}
});
}
function movieColor(code) {
for (const i of listOrder)
if (myLists[i].ids[code]) return myLists[i].color;
return '';
}
function nameToHex(name) {
const ctx = document.createElement('canvas').getContext('2d');
ctx.fillStyle = name;
return ctx.fillStyle;
}
})();