// ==UserScript== // @name IMDB List Importer // @namespace Neinei0k_imdb // @include https://www.imdb.com/list/* // @version 8.1 // @license GNU General Public License v3.0 or later // @description Import list of titles or people in the imdb list // @downloadURL none // ==/UserScript== let elements = createHTMLForm(); function log(level, message) { console.log("(IMDB List Importer) " + level + ": " + message); } function setStatus(message) { elements.status.textContent = message; } function createHTMLForm() { let elements = {}; try { let root = createRoot(); elements.text = createTextField(root); if (isFileAPISupported()) { elements.file = createFileInput(root); elements.isFromFile = createFromFileCheckbox(root); } else { createFileAPINotSupportedMessage(root); } elements.isCSV = createCSVCheckbox(root); elements.isUnique = createUniqueCheckbox(root); elements.status = createStatusBar(root); createImportButton(root); } catch (message) { log("Error", message); } return elements; } function isFileAPISupported() { return window.File && window.FileReader && window.FileList && window.Blob; } function createRoot() { let container = document.querySelector('.lister-search'); if (container === null) { throw ".lister-search element not found"; } let root = document.createElement('div'); root.setAttribute('class', 'search-bar'); root.style.height = 'initial'; root.style.marginBottom = '30px'; container.appendChild(root); return root; } function createTextField(root) { let text = document.createElement('textarea'); text.style = "background-color: white; width: 100%; height: 100px; overflow: initial;"; root.appendChild(text); root.appendChild(document.createElement('br')); return text; } function createFileInput(root) { let file = document.createElement('input'); file.type = 'file'; file.disabled = true; file.style.marginBottom = '10px'; root.appendChild(file); root.appendChild(document.createElement('br')); return file; } function createFromFileCheckbox(root) { let isFromFile = createCheckbox("Import from file (otherwise import from text)"); root.appendChild(isFromFile.label); root.appendChild(document.createElement('br')); isFromFile.checkbox.addEventListener('change', fromFileOrTextChangeHandler, false); return isFromFile.checkbox; } function createCheckbox(textContent) { let checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.style = 'width: initial;'; let text = document.createElement('span'); text.style = 'font-weight: normal;'; text.textContent = textContent; let label = document.createElement('label'); label.appendChild(checkbox); label.appendChild(text); return {label: label, checkbox: checkbox}; } function fromFileOrTextChangeHandler(event) { let isChecked = event.target.checked; elements.text.disabled = isChecked; elements.file.disabled = !isChecked; } function createFileAPINotSupportedMessage(root) { let notSupported = document.createElement('div'); notSupported.style = 'font-weight: normal;'; notSupported.style.marginTop = '10px'; notSupported.style.marginBottom = '10px'; notSupported.textContent = "Your browser does not support File API for reading local files."; root.appendChild(notSupported); } function createCSVCheckbox(root) { let isCSV = createCheckbox("Data from .csv file (otherwise extract ids from text)"); isCSV.checkbox.checked = true; root.appendChild(isCSV.label); root.appendChild(document.createElement('br')); return isCSV.checkbox; } function createUniqueCheckbox(root) { let isUnique = createCheckbox("Add only unique elements"); root.appendChild(isUnique.label); root.appendChild(document.createElement('br')); return isUnique.checkbox; } function createStatusBar(root) { let status = document.createElement('div'); status.textContent = "Set-up parameters. Insert text or choose file. Press 'Import List' button."; status.style.marginTop = '10px'; status.style.marginBottom = '10px'; root.appendChild(status); return status; } function createImportButton(root) { let importList = document.createElement('button'); importList.class = 'btn'; importList.textContent = "Import List"; root.appendChild(importList); importList.addEventListener('click', importListClickHandler, false); } function importListClickHandler(event) { if (elements.hasOwnProperty('isFromFile') && elements.isFromFile.checked) { readFile(); } else { importList(extractItems(elements.text.value)); } } function readFile() { let file = elements.file.files[0]; if (file !== undefined) { log("Info", "Reading file " + file.name); setStatus("Reading file " + file.name); let fileReader = new FileReader(); fileReader.onload = fileOnloadHandler; fileReader.readAsText(file); } else { setStatus("Error: File is not selected"); } } function fileOnloadHandler(event) { if (event.target.error === null) { importList(extractItems(event.target.result)); } else { log("Error", e.target.error); setStatus("Error: " + e.target.error); } } function extractItems(text) { try { let itemRegExp = getRegExpForItems(); if (elements.isCSV.checked) { return extractItemsFromCSV(itemRegExp, text); } else { return extractItemsFromText(itemRegExp, text); } } catch (message) { log("Error", message); setStatus("Error: " + message); return []; } } function getRegExpForItems() { let listType; if (isPeopleList()) { log("Info", "List type: people"); listType = "nm"; } else if (isTitlesList()) { log("Info", "List type: titles"); listType = "tt"; } else { throw "Could not determine list type"; } return listType + "[0-9]{7,8}"; } function isPeopleList() { return document.querySelector('[data-type="People"]') !== null; } function isTitlesList() { return document.querySelector('[data-type="Titles"]') !== null; } function extractItemsFromCSV(re, text) { let table = parseCSV(text); let fields = findFieldNumbers(table); if (fields.description !== -1) { log("Info", "Found csv file fields Const(" + fields.const + ") and Description(" + fields.description + ")"); } else { log("Info", "Found csv file field Const(" + fields.const + "). Description field is not found."); } re = new RegExp("^" + re + "$"); let items = []; // Add elements to the list for (let i = 1; i < table.length; i++) { let row = table[i]; if (re.exec(row[fields.const]) === null) { throw "Invalid 'const' field format on line " + (i+1); } if (elements.isUnique.checked) { let exists = items.findIndex(function(v){ return v.const === row[fields.const]; }); if (exists !== -1) continue; } items.push({const: row[fields.const], description: (fields.description == -1 ? "" : row[fields.description])}); } return items; } function parseCSV(text) { let lines = text.split('\n'); let table = []; for (let i=0; i < lines.length; i++) { if (isEmpty(lines[i])) { continue; } let isInsideString = false; let row = [""]; for (let j=0; j < lines[i].length; j++) { if (!isInsideString && lines[i][j] === ',') { row.push(""); } else if (lines[i][j] === '"') { isInsideString = !isInsideString; } else { row[row.length-1] += lines[i][j]; } } table.push(row); if (isInsideString) { throw "Wrong number of \" on line " + (i+1); } if (row.length != table[0].length) { throw "Wrong number of fields on line " + (i+1) + ". Expected " + table[0].length + " but found " + row.length + "."; } } return table; } function isEmpty(str) { return str.trim().length === 0; } function findFieldNumbers(table) { let fieldNames = table[0]; let fieldNumbers = {'const': -1, 'description': -1}; for (let i = 0; i < fieldNames.length; i++) { let fieldName = fieldNames[i].toLowerCase(); if (fieldName === 'const') { fieldNumbers.const = i; } else if (fieldName === 'description') { fieldNumbers.description = i; } } if (fieldNumbers.const === -1) { throw "Field 'const' not found." } return fieldNumbers; } function extractItemsFromText(re, text) { re = new RegExp(re); let items = []; let e; while ((e = re.exec(text)) !== null) { let flag = ''; if (elements.isUnique.checked) flag = 'g'; text = text.replace(new RegExp(e[0], flag), ''); items.push({const: e[0], description: ""}); } return items; } function importList(list) { if (list.length === 0) return; let msg = "Elements to add: "; for (let i = 0; i < list.length; i++) msg += list[i].const + ","; log("Info", msg); let l = {}; l.list = list; l.ready = 0; l.list_id = /ls[0-9]{1,}/.exec(location.href)[0]; l.hiddenElementData = getHiddenElementData(); // Data needs to be send with all requests. sendItem(l); } function getHiddenElementData() { let hiddenElement = document.querySelector('#main > input'); if (hiddenElement === null) { log("Error", "Hidden element not found. It is required to be sent with every request."); setStatus("Error: Hidden element not found. It is required to be sent with every request."); return ""; } return hiddenElement.id + "=" + hiddenElement.value; } function sendItem(l) { log("Info", 'Add element ' + l.ready + ': ' + l.list[l.ready].const); let url = 'https://www.imdb.com/list/' + l.list_id + '/' + l.list[l.ready].const + '/add'; sendRequest(sendItemHandler, l, url, l.hiddenElementData); } function sendRequest(handler, l, url, data) { var x = new XMLHttpRequest(); x.onreadystatechange = function(event) { handler(l, event); } x.open('POST', url, true); x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); x.send(data); } function sendItemHandler(l, event) { let target = event.target; log("Info", "Add element(" + l.list[l.ready].const + ") request: readyState(" + target.readyState + "), status(" + target.status + ")"); if (target.readyState == 4 && target.status == 200) { let description = l.list[l.ready].description; if (description.length !== 0) { let listItemId = JSON.parse(target.responseText).list_item_id; let url = 'https://www.imdb.com/list/' + l.list_id + '/edit/itemdescription'; let data = 'newDescription=' + description + '&listItem=' + listItemId + '&' + l.hiddenElementData sendRequest(sendItemDescriptionHandler, l, url, data); } else { showReady(l); } } } function sendItemDescriptionHandler(l, event) { let target = event.target; log("Info", "Add element(" + l.list[l.ready].const + ") description request: readyState(" + target.readyState + "), status(" + target.status + ")"); if (target.readyState == 4 && target.status == 200) { showReady(l); } } function showReady(l) { l.ready += 1; setStatus('Ready ' + l.ready + ' of ' + l.list.length + '.'); if (l.ready == l.list.length) { location.reload(); } else { sendItem(l); } }