NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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 // @updateURL https://openuserjs.org/install/tronix42/IMDb-Lists-Highlighter.meta.js // ==/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; } })();