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.2 // @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.26 [1.2] Added Tooltip, couple other changes // 2025.05.19 [1.1] Added Fallback for GM_addStyle (Greasemonkey) // 2025.05.12 [1.0] Public Release // -------------------------------------------------------------------- (function() { 'use strict'; // === Tooltip-CSS === addStyle(` .imdb-tooltip { position: absolute; background: rgba(0,0,0,0.8); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; pointer-events: none; z-index: 10000; white-space: normal; line-height: 1.4; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif; } `); // === Tooltip-Config === let showTooltips = true; try { const v = localStorage.getItem('showTooltips'); if (v !== null) showTooltips = JSON.parse(v); } catch (e) {} function addStyle(css) { const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); } // Persistent People-Map let namesToColor = {}; try { namesToColor = JSON.parse(localStorage.getItem('namesToColor') || '{}'); } catch (e) { namesToColor = {}; } 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]; } 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})?$/i; 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/i.test(location.pathname)) { // (a) Load Database let savedListsMap = {}; if (listsLoaded) { 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; } }); // Stylesheet Lists if (/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?user\/ur\d+\/lists/.test(location.pathname)) { const cssRules = []; myLists.forEach(list => { const color = list.color; Object.keys(list.ids).forEach(code36 => { const num = parseInt(code36, 36); cssRules.push( `a[href*="/title/tt${num}/?ref_"] {` + ` color: ${color} !important; font-weight: bold !important; }` ); }); }); addStyle(cssRules.join('\n')); highlightLinks(); } } // 3) CSS and Search-Highlight const isHome = location.pathname === '/'; const isCalendar = /^\/(?:[a-z]{2}(?:-[a-z]{2})?\/)?calendar/i.test(location.pathname); // A) Stylesheet on all sites beside calendar if (!isCalendar) { let css = ''; myLists.forEach(list => { const color = list.color; Object.keys(list.ids).forEach(code36 => { const num = parseInt(code36, 36); const idPadded = String(num).padStart(7, '0'); // Title-Links css += ` a[href^="/title/tt${num}/?ref_="], a[href^="https://www.imdb.com/title/tt${num}/?ref_="] { color: ${color} !important; font-weight: bold !important; } `; // People-Links css += ` a[href^="/name/nm${idPadded}/?ref_="], a[href^="https://www.imdb.com/name/nm${idPadded}/?ref_="] { color: ${color} !important; font-weight: bold !important; } `; }); }); addStyle(css); } // B) JS-Highlight highlightTitle(); highlightLinks(); // C) Observer & Autocomplete-Dropdowns let moTimeout; const linkObserver = new MutationObserver(() => { clearTimeout(moTimeout); moTimeout = setTimeout(() => { highlightTitle(); highlightLinks(); }, 100); }); linkObserver.observe(document.body, { childList: true, subtree: true }); window.addEventListener('beforeunload', () => { linkObserver.disconnect(); }); // D) Calendar Highlighting if (isCalendar) { highlightTitle(); highlightLinks(); highlightCalendarPeople(); let calTimeout; const calObserver = new MutationObserver(() => { clearTimeout(calTimeout); calTimeout = setTimeout(() => { highlightCalendarPeople(); }, 100); }); calObserver.observe(document.body, { childList: true, subtree: true }); window.addEventListener('beforeunload', () => calObserver.disconnect()); } function collectLists() { let savedColors = {}; if (listsLoaded) { 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) => { myLists.push({ name, id, color: savedColors[id] || customColors[name] || defaultColor, ids: {}, names: {}, selected: false, csvFile: null, importMode: 'auto' }); 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 // remove all Prefix-Keywords for list names .replace(/^.*\b(?:for|für|para|pour|per|de)\b\s*/i, '') .replace(/\s*(?:के लिए सूची का पेज देखें)$/i, '') .trim(); myLists.push({ name, id, color: savedColors[id] || defaultColor, ids: {}, names: {}, selected: false, csvFile: null, importMode: 'auto' }); 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() { // 1) Toggle-Switch CSS if (!document.getElementById('imdb-toggle-style')) { const style = document.createElement('style'); style.id = 'imdb-toggle-style'; style.textContent = ` .switch { position: relative; display: inline-block; width: 50px; height: 24px; margin: 0 8px; vertical-align: middle; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; inset: 0; background-color: #ccc; transition: .4s; border-radius: 24px; } .slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } .switch input:checked + .slider { background-color: #4CAF50; } .switch input:checked + .slider:before { transform: translateX(26px); } `; document.head.appendChild(style); } // 2) load saved settings let savedListsMap = {}; if (listsLoaded) { myLists.forEach(l => { savedListsMap[l.id] = { ids: JSON.parse(JSON.stringify(l.ids)), color: l.color, selected: l.selected, importMode: l.importMode || 'auto' }; }); } // 3) load lists collectLists(); // 3) Modal & Box const modal = document.createElement('div'); modal.id = 'imdb-config-modal'; 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;' + 'min-width:600px;'; // 4) Header const header = document.createElement('div'); header.style = 'display:flex;align-items:center;justify-content:flex-start;margin-bottom:8px;font-weight:bold;'; const hChk = document.createElement('span'); hChk.style = 'width:1px;'; const hLists = document.createElement('span'); hLists.textContent = 'Select List(s):'; hLists.style = 'margin-right:80px;'; const hColor = document.createElement('span'); hColor.textContent = 'Color - HEX/Name:'; hColor.style = 'margin-right:40px;'; const hMode = document.createElement('span'); hMode.textContent = 'Import Mode:'; hMode.style = 'margin-right:130px;'; const hStatus = document.createElement('span'); hStatus.textContent = 'Status:'; hStatus.style = 'margin-right:8px;'; header.append(hChk, hLists, hColor, hMode, hStatus); box.appendChild(header); // 5) Header-Content myLists.forEach((lst, i) => { // use saved data const sav = savedListsMap[lst.id]; if (sav) { lst.ids = sav.ids; lst.color = sav.color; lst.selected = sav.selected; lst.importMode = sav.importMode; } // load importMode-Persistence const savedModes = JSON.parse(localStorage.getItem('imdbListsImportModes') || '[]'); const modesMap = savedModes.reduce((m, o) => (m[o.id] = o.importMode, m), {}); myLists.forEach(lst => { if (modesMap[lst.id] != null) { lst.importMode = modesMap[lst.id]; } }); // —– const row = document.createElement('div'); row.style = 'margin:5px 0; display:flex; align-items:center;'; // a) Lists checkboxes const chk = document.createElement('input'); chk.type = 'checkbox'; chk.className = 'list-select'; chk.style = 'margin-right:8px;'; chk.checked = lst.selected; chk.defaultChecked = lst.selected; chk.onchange = e => { lst.selected = e.target.checked; if (!lst.selected) lst.csvFile = null; }; // b) Lists label const lbl = document.createElement('span'); lbl.textContent = ' ' + lst.name + ' '; lbl.style = 'display:inline-block;width:145px;line-height:20px;margin-right:8px;cursor:default;font-weight:normal;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; if (Object.keys(lst.ids).length > 0) { lbl.style.color = lst.color; lbl.style.fontWeight = 'bold'; } // c) Hidden File-Input for CSV import const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.csv'; fileInput.style.display = 'none'; fileInput.onchange = e => { lst.csvFile = e.target.files[0]; chooseBtn.textContent = 'Ready!'; }; // d) CSV-Button const chooseBtn = document.createElement('button'); chooseBtn.type = 'button'; chooseBtn.textContent = 'Select CSV'; chooseBtn.style = 'width:90px;height:24px;margin-left:8px;margin-right:15px;background-color:#ffcc00;'; chooseBtn.addEventListener('click', () => fileInput.click()); // e) Color-Picker const col = document.createElement('input'); col.type = 'color'; col.value = nameToHex(lst.color); col.style = 'width:35px;height:25px;margin-left:2px;margin-right:20px;'; 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'; } }; // f) Text-Input for HEX/String 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;margin-right:8px;'; 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'; } }; // g) Toggle-Switch for all List (not Ratings & Check-ins) const switchLabel = document.createElement('label'); switchLabel.className = 'switch'; switchLabel.style.marginLeft = '10px'; const switchInput = document.createElement('input'); switchInput.type = 'checkbox'; // Ratings & Check-ins only manual if (lst.id === 'ratings' || lst.id === 'checkins') { switchInput.checked = false; } else { switchInput.checked = lst.importMode === 'auto'; } const slider = document.createElement('span'); slider.className = 'slider'; switchLabel.append(switchInput, slider); const modeText = document.createElement('span'); modeText.textContent = switchInput.checked ? 'Auto' : 'Manual'; modeText.style = 'margin-right:8px;'; modeText.style.display = 'inline-block'; modeText.style.width = '50px'; modeText.style.minWidth = '50px'; modeText.style.maxWidth = '50px'; modeText.style.overflow = 'hidden'; modeText.style.whiteSpace = 'nowrap'; // show CSV-Button on Manual-Mode & for Ratings/Check-ins if (lst.id === 'ratings' || lst.id === 'checkins') { chooseBtn.style.visibility = 'visible'; } else { chooseBtn.style.visibility = lst.importMode === 'manual' ? 'visible' : 'hidden'; } // Special-Handler for Ratings & Check-ins if (lst.id === 'ratings' || lst.id === 'checkins') { switchInput.addEventListener('click', e => { e.preventDefault(); alert('Only CSV Import possible for Your Ratings-List and Check-Ins-List!'); // take care Toggle is only on manual switchInput.checked = false; modeText.textContent = 'Manual'; }); } else { // Toggle behaviour for all other lists switchInput.onchange = () => { lst.importMode = switchInput.checked ? 'auto' : 'manual'; modeText.textContent = switchInput.checked ? 'Auto' : 'Manual'; modeText.style.display = 'inline-block'; modeText.style.width = '50px'; modeText.style.minWidth = '50px'; modeText.style.maxWidth = '50px'; modeText.style.overflow = 'hidden'; modeText.style.whiteSpace = 'nowrap'; chooseBtn.style.visibility = lst.importMode === 'manual' ? 'visible' : 'hidden'; }; } // h) Import-Count const countInput = document.createElement('input'); countInput.type = 'text'; countInput.readOnly = true; const count = lst.ids ? Object.keys(lst.ids).length : 0; countInput.value = count; countInput.dataset.listIdx = i; countInput.style = 'width:60px; box-sizing:border-box; text-align:right;'; const wrapper = document.createElement('div'); wrapper.style = 'position:relative; width:60px; margin-left:auto; box-sizing:border-box;'; wrapper.appendChild(countInput); row.append(chk, lbl, txt, col, switchLabel, modeText, fileInput, chooseBtn, wrapper); box.appendChild(row); }); // 6) Button Start Import, Clear Data, Close, Tooltip const imp = document.createElement('button'); imp.id = 'start-import-btn'; imp.type = 'button'; imp.textContent = 'Start Import'; imp.style = 'margin:10px;'; imp.onclick = (e) => { e.preventDefault(); // deactivate Buttons Start import, Clear Data during Import document.getElementById('start-import-btn').disabled = true; document.getElementById('start-import-btn').style.opacity = '0.5'; document.getElementById('start-import-btn').style.pointerEvents = 'none'; document.getElementById('clear-data-btn').disabled = true; document.getElementById('clear-data-btn').style.opacity = '0.5'; document.getElementById('clear-data-btn').style.pointerEvents = 'none'; // --- deactivate Lists checkboxes during Import document.querySelectorAll('input.list-select[type="checkbox"]').forEach(cb => { cb.disabled = true; cb.style.opacity = '0.5'; cb.style.pointerEvents = 'none'; }); // --- deactivate color pocker during Import document.querySelectorAll('input[type="color"]').forEach(cp => { cp.disabled = true; cp.style.opacity = '0.5'; cp.style.pointerEvents = 'none'; }); // --- deactivate toggle switch during Import document.querySelectorAll('.switch input[type="checkbox"]').forEach(sw => { sw.disabled = true; sw.style.opacity = '0.5'; sw.style.pointerEvents = 'none'; }); // --- deactivate fileinput during Import document.querySelectorAll('input[type="file"]').forEach(fi => { fi.disabled = true; fi.style.opacity = '0.5'; fi.style.pointerEvents = 'none'; }); // start Import & show progress circle startImport(); }; const clr = document.createElement('button'); clr.textContent = 'Clear Data'; clr.id = 'clear-data-btn'; clr.style = 'margin:10px;'; clr.onclick = () => { // deactive Buttons document.getElementById('start-import-btn').disabled = true; document.getElementById('start-import-btn').style.opacity = '0.5'; document.getElementById('start-import-btn').style.pointerEvents = 'none'; document.getElementById('clear-data-btn').disabled = true; document.getElementById('clear-data-btn').style.opacity = '0.5'; document.getElementById('clear-data-btn').style.pointerEvents = 'none'; eraseData(); alert('Data cleared'); document.body.removeChild(modal); }; const cxl = document.createElement('button'); cxl.textContent = 'Close'; cxl.style = 'margin:10px;'; cxl.onclick = () => { // 1) UI: unmarkd all lists checkboxes document .querySelectorAll('#imdb-config-modal input.list-select[type="checkbox"]') .forEach(cb => cb.checked = false); // 2) reset Lists-Data myLists.forEach(lst => { lst.selected = false; lst.csvFile = null; }); // 3) close config-lists-menu if (modal && modal.parentNode) { document.body.removeChild(modal); } }; const btnRow = document.createElement('div'); btnRow.style = 'margin:4px 0; display:flex; align-items:center;'; // Bottom left button "Start Import", "Clear Data", "Close" order btnRow.append(imp, clr, cxl); // Bottom right Tooltip-Checkbox const tooltipRow = document.createElement('div'); tooltipRow.style = 'display:flex; align-items:center; margin-left:auto;'; const tooltipChk = document.createElement('input'); tooltipChk.type = 'checkbox'; tooltipChk.checked = showTooltips; tooltipChk.onchange = e => { showTooltips = e.target.checked; saveTooltipSetting(e.target.checked); }; const tooltipLbl = document.createElement('label'); tooltipLbl.textContent = 'Show Tooltips'; tooltipLbl.style = 'margin-left:6px;'; tooltipRow.append(tooltipChk, tooltipLbl); btnRow.appendChild(tooltipRow); box.appendChild(btnRow); modal.appendChild(box); document.body.appendChild(modal); } // startImport: CSV import vs. automatic import function startImport() { if (!document.getElementById('spinStyleInline')) { const spinStyle = document.createElement('style'); spinStyle.id = 'spinStyleInline'; spinStyle.textContent = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(spinStyle); } const tasks = []; for (let i = 0; i < myLists.length; i++) { const l = myLists[i]; if (!l.selected) continue; // Ratings & Check-ins: only Manual-Import via CSV if (l.id === 'ratings' || l.id === 'checkins') { if (l.csvFile) { tasks.push({ type: 'csv', idx: i }); } else { alert('No CSV-File selected!'); // === re-enable UI after alert=== ['start-import-btn', 'clear-data-btn'].forEach(id => { const btn = document.getElementById(id); if (btn) { btn.disabled = false; btn.style.opacity = ''; btn.style.pointerEvents = ''; } }); document.querySelectorAll('input.list-select[type="checkbox"]').forEach(cb => { cb.disabled = false; cb.style.opacity = ''; cb.style.pointerEvents = ''; }); document.querySelectorAll('input[type="color"]').forEach(cp => { cp.disabled = false; cp.style.opacity = ''; cp.style.pointerEvents = ''; }); document.querySelectorAll('.switch input[type="checkbox"]').forEach(sw => { sw.disabled = false; sw.style.opacity = ''; sw.style.pointerEvents = ''; }); document.querySelectorAll('input[type="file"]').forEach(fi => { fi.disabled = false; fi.style.opacity = ''; fi.style.pointerEvents = ''; }); return false; } continue; } // all other lists if (l.importMode === 'manual') { if (l.csvFile) { tasks.push({ type: 'csv', idx: i }); } else { alert('No CSV-File selected!'); // === re-enable UI after alert=== ['start-import-btn', 'clear-data-btn'].forEach(id => { const btn = document.getElementById(id); if (btn) { btn.disabled = false; btn.style.opacity = ''; btn.style.pointerEvents = ''; } }); document.querySelectorAll('input.list-select[type="checkbox"]').forEach(cb => { cb.disabled = false; cb.style.opacity = ''; cb.style.pointerEvents = ''; }); document.querySelectorAll('input[type="color"]').forEach(cp => { cp.disabled = false; cp.style.opacity = ''; cp.style.pointerEvents = ''; }); document.querySelectorAll('.switch input[type="checkbox"]').forEach(sw => { sw.disabled = false; sw.style.opacity = ''; sw.style.pointerEvents = ''; }); document.querySelectorAll('input[type="file"]').forEach(fi => { fi.disabled = false; fi.style.opacity = ''; fi.style.pointerEvents = ''; }); return false; } } else { tasks.push({ type: 'auto', idx: i }); } } if (!tasks.length) { alert('No List(s) selected!'); // === re-enable UI after alert=== // Buttons ['start-import-btn', 'clear-data-btn'].forEach(id => { const btn = document.getElementById(id); if (btn) { btn.disabled = false; btn.style.opacity = ''; btn.style.pointerEvents = ''; } }); // Lists-Checkboxes document.querySelectorAll('input.list-select[type="checkbox"]').forEach(cb => { cb.disabled = false; cb.style.opacity = ''; cb.style.pointerEvents = ''; }); // Color-Picker document.querySelectorAll('input[type="color"]').forEach(cp => { cp.disabled = false; cp.style.opacity = ''; cp.style.pointerEvents = ''; }); // Toggle-Switches document.querySelectorAll('.switch input[type="checkbox"]').forEach(sw => { sw.disabled = false; sw.style.opacity = ''; sw.style.pointerEvents = ''; }); // CSV-File-Inputs document.querySelectorAll('input[type="file"]').forEach(fi => { fi.disabled = false; fi.style.opacity = ''; fi.style.pointerEvents = ''; }); return false; } // === clear all old data bevor import list tasks.forEach(({ idx }) => { myLists[idx].ids = {}; updateListProgress(idx, 0); }); // === progress circle tasks.forEach(({ idx }) => { const input = document.querySelector(`input[data-list-idx="${idx}"]`); if (input) { input.value = ''; const wrapper = input.parentNode; // create circle const spinner = document.createElement('div'); spinner.className = 'item-spinner-inline'; spinner.dataset.listIdx = idx; spinner.style = [ 'position:absolute', 'top:0', 'bottom:0', 'left:0', 'right:0', 'margin:auto', 'pointer-events:none', 'width:16px', 'height:16px', 'border:4px solid #ccc', 'border-top:4px solid #3498db', 'border-radius:50%', 'animation:spin 1s linear infinite' ].join(';'); wrapper.appendChild(spinner); } }); let rem = tasks.length; tasks.forEach(({ type, idx }) => { if (type === 'csv') { importCsv(idx, () => { updateListProgress(idx, Object.keys(myLists[idx].ids).length); if (--rem === 0) { myLists.forEach(l => l.selected = false); saveLists(); 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(); } }); } }); return true; } function importCsv(idx, cb) { const lst = myLists[idx]; lst.ids = {}; lst.names = {}; const reader = new FileReader(); reader.onload = e => { const lines = e.target.result.split(/\r?\n/); for (const line of lines) { if (!line.trim()) continue; let m; // 1a) extrac movies-IDs (tt1234567) m = line.match(/tt(\d+)/i); if (m) { const code36 = parseInt(m[1], 10).toString(36); lst.ids[code36] = {}; } // 1b) extrac people-IDs (nm1234567) + Name m = line.match(/nm(\d+).*?,\s*"([^"]+)"/i); if (m) { const code36 = parseInt(m[1], 10).toString(36); const personName = m[2].trim(); lst.ids[code36] = {}; lst.names[personName] = code36; // put in localStorage.namesToColor let ntc = {}; try { ntc = JSON.parse(localStorage.getItem('namesToColor') || '{}'); } catch (_) {} ntc[personName] = lst.color; localStorage.setItem('namesToColor', JSON.stringify(ntc)); } } saveLists(); try { namesToColor = JSON.parse(localStorage.getItem('namesToColor') || '{}'); } catch (_) {} cb(); }; reader.onerror = () => { console.error("CSV-Import-Error:", reader.error); cb(); }; reader.readAsText(lst.csvFile); } function downloadList(idx, cb) { const lst = myLists[idx]; lst.ids = lst.ids || {}; lst.names = lst.names || {}; // Watchlist-Auto-import 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 + Pageing 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)); // wait to load all entries 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; } // Custom-lists-Auto-import (async () => { const base = `https://www.imdb.com/list/${lst.id}/?mode=detail`; let page = 1; let isPeople = null; while (true) { 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) { break; } 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; 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) return; const code36 = parseInt(m[1], 10).toString(36); lst.ids[code36] = {}; if (e.item && e.item.name) { const name = e.item.name.trim(); lst.names[name] = code36; let namesToColor = {}; try { namesToColor = JSON.parse(localStorage.getItem('namesToColor') || '{}'); } catch (e) {} namesToColor[name] = lst.color; localStorage.setItem('namesToColor', JSON.stringify(namesToColor)); } }); if (items.length < 250) break; page++; } cb(); })(); } function updateListProgress(listIdx, count) { const spinner = document.querySelector(`.item-spinner-inline[data-list-idx="${listIdx}"]`); if (spinner) { spinner.remove(); } else {} const countInput = document.querySelector(`input[data-list-idx="${listIdx}"]`); if (countInput) { countInput.value = count; } else { console.error(`Count-Input für listIdx=${listIdx} nicht gefunden!`); } } function finishProgress() { // 1) close Progress-circle if (progressModal && progressModal.parentNode) { document.body.removeChild(progressModal); } // 2) UI: unmark all Lists-Checkboxes const cfg = document.getElementById('imdb-config-modal'); if (cfg) { cfg .querySelectorAll('input.list-select[type="checkbox"]') .forEach(cb => cb.checked = false); // 3) UI: default CSV-Button cfg.querySelectorAll('input[type="file"]').forEach(fileInput => { fileInput.value = null; const chooseBtn = fileInput.nextSibling; if (chooseBtn && chooseBtn.tagName === 'BUTTON') { chooseBtn.textContent = 'Select CSV'; } }); } // 4) reset-lists myLists.forEach(lst => { lst.selected = false; lst.csvFile = null; }); // 5) activate Button "Start import", "Clear Data" ['start-import-btn', 'clear-data-btn'].forEach(id => { const btn = document.getElementById(id); if (btn) { btn.disabled = false; btn.style.opacity = ''; btn.style.pointerEvents = ''; } }); // 6) unmark all Lists-Checkboxes document.querySelectorAll('input.list-select[type="checkbox"]').forEach(cb => { cb.checked = false; cb.disabled = false; cb.style.opacity = ''; cb.style.pointerEvents = ''; }); // 7) activate Color-Picke<<<<<r document.querySelectorAll('input[type="color"]').forEach(cp => { cp.disabled = false; cp.style.opacity = ''; cp.style.pointerEvents = ''; }); // 8) activate Toggle-Switch document.querySelectorAll('.switch input[type="checkbox"]').forEach(sw => { sw.disabled = false; sw.style.opacity = ''; sw.style.pointerEvents = ''; }); // 9) activate File-Inputs document.querySelectorAll('input[type="file"]').forEach(fi => { fi.disabled = false; fi.style.opacity = ''; fi.style.pointerEvents = ''; }); // 10) save importMode const modesToSave = myLists.map(lst => ({ id: lst.id, importMode: lst.importMode })); localStorage.setItem( 'imdbListsImportModes', JSON.stringify(modesToSave) ); alert('Import Done!'); } function eraseData() { localStorage.removeItem('myMovies-' + user); localStorage.removeItem('namesToColor'); localStorage.removeItem('imdbListsImportModes'); namesToColor = {}; window.location.reload(); } 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 highlightTitle() { 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); } 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() { 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_=[^/]+/i ); 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'; } }); const peopleLinkRe = /^https:\/\/www\.imdb\.com\/(?:[a-z]{2}(?:-[a-z]{2})?\/)?name\/nm(\d+)\/\?ref_=[^&#]+$/i; 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'; } }); 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_=[^/]+/i ); if (!m) return; const code36 = parseInt(m[1], 10).toString(36); const c = movieColor(code36); if (!c) return; const titleSpan = li.querySelector('.searchResult__constTitle') || li.querySelector('span'); if (titleSpan) { titleSpan.style.color = c; titleSpan.style.fontWeight = 'bold'; } }); 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_=[^/]+/i ); if (!m) return; const code36 = parseInt(m[1], 10).toString(36); const c = movieColor(code36); if (!c) return; const nameSpan = li.querySelector('.searchResult__actorName') || li.querySelector('.searchResult__constTitle') || li.querySelector('span'); if (nameSpan) { nameSpan.style.color = c; nameSpan.style.fontWeight = 'bold'; } }); if (/^\/([a-z]{2}(-[a-z]{2})?)?\/?$/i.test(location.pathname)) { document.querySelectorAll('a[href*="/name/nm"]').forEach(link => { const match = link.href.match(/\/name\/nm(\d+)\//); if (match) { const code36 = parseInt(match[1], 10).toString(36); const color = movieColor(code36); if (color) { link.style.color = color; link.style.fontWeight = 'bold'; if (link.parentElement) { link.parentElement.style.color = color; link.parentElement.style.fontWeight = 'bold'; } } } }); document.querySelectorAll('div').forEach(div => { const name = div.textContent.trim(); if (!/^[A-Za-zÄÖÜäöüß\-'. ]{2,}$/.test(name)) return; if (namesToColor[name]) { div.style.color = namesToColor[name]; div.style.fontWeight = 'bold'; } }); } // === Tooltip-Listener if (showTooltips) { document .querySelectorAll('a[href*="/title/tt"], a[href*="/name/nm"]') .forEach(a => { const m = a.href.match(/tt(\d+)\//) || a.href.match(/name\/nm(\d+)\//); if (!m) return; const code36 = parseInt(m[1], 10).toString(36); if (!movieColor(code36)) return; if (!a.dataset.tooltipListener) { a.dataset.tooltipListener = '1'; a.addEventListener('mouseenter', () => showTooltip(a, code36)); a.addEventListener('mouseleave', hideTooltip); } }); } } function highlightCalendarPeople() { document .querySelectorAll('.ipc-inline-list__item span.ipc-metadata-list-summary-item__li') .forEach(span => { const name = span.textContent.trim(); const color = namesToColor[name]; if (color) { span.style.color = color; span.style.fontWeight = 'bold'; } }); } 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; } // === Tooltip-function === let currentTooltip = null; function showTooltip(elem, code36) { const lists = myLists .filter(l => l.ids[code36]) .map(l => l.name); if (!lists.length) return; const tip = document.createElement('div'); tip.className = 'imdb-tooltip'; tip.innerHTML = '<strong>In List(s):</strong><br>' + lists.map(l => `• ${l}`).join('<br>'); document.body.appendChild(tip); const rect = elem.getBoundingClientRect(); tip.style.top = (window.scrollY + rect.bottom + 4) + 'px'; tip.style.left = (window.scrollX + rect.left) + 'px'; currentTooltip = tip; } function hideTooltip() { if (currentTooltip) { document.body.removeChild(currentTooltip); currentTooltip = null; } } })();