IMDB List Importer

Import list of titles or people in the imdb list

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name            IMDB List Importer
// @namespace       Neinei0k_imdb
// @include         https://www.imdb.com/list/*
// @version         10.3
// @license         GNU General Public License v3.0 or later
// @description	Import list of titles or people in the imdb list
// ==/UserScript==

var request_data_add_item = {
    "query": "mutation AddConstToList($listId: ID!, $constId: ID!, $includeListItemMetadata: Boolean!, $refTagQueryParam: String, $originalTitleText: Boolean) {\n  addItemToList(input: {listId: $listId, item: {itemElementId: $constId}}) {\n    listId\n    modifiedItem {\n      ...ListItemMetadata\n      listItem @include(if: $includeListItemMetadata) {\n        ... on Title {\n          ...TitleListItemMetadata\n        }\n        ... on Name {\n          ...NameListItemMetadata\n        }\n        ... on Image {\n          ...ImageListItemMetadata\n        }\n        ... on Video {\n          ...VideoListItemMetadata\n        }\n      }\n    }\n  }\n}\n\nfragment ListItemMetadata on ListItemNode {\n  itemId\n  createdDate\n  description {\n    originalText {\n      markdown\n      plaidHtml(showLineBreak: true)\n      plainText\n    }\n  }\n}\n\nfragment TitleListItemMetadata on Title {\n  ...BaseTitleCard\n  plot {\n    plotText {\n      plainText\n    }\n  }\n  latestTrailer {\n    id\n  }\n  series {\n    series {\n      id\n      originalTitleText {\n        text\n      }\n      releaseYear {\n        endYear\n        year\n      }\n      titleText {\n        text\n      }\n    }\n  }\n}\n\nfragment BaseTitleCard on Title {\n  id\n  titleText {\n    text\n  }\n  titleType {\n    id\n    text\n    canHaveEpisodes\n    displayableProperty {\n      value {\n        plainText\n      }\n    }\n  }\n  originalTitleText {\n    text\n  }\n  primaryImage {\n    id\n    width\n    height\n    url\n    caption {\n      plainText\n    }\n  }\n  releaseYear {\n    year\n    endYear\n  }\n  ratingsSummary {\n    aggregateRating\n    voteCount\n  }\n  runtime {\n    seconds\n  }\n  certificate {\n    rating\n  }\n  canRate {\n    isRatable\n  }\n  titleGenres {\n    genres(limit: 3) {\n      genre {\n        text\n      }\n    }\n  }\n  canHaveEpisodes\n}\n\nfragment NameListItemMetadata on Name {\n  id\n  primaryImage {\n    url\n    caption {\n      plainText\n    }\n    width\n    height\n  }\n  nameText {\n    text\n  }\n  primaryProfessions {\n    category {\n      text\n    }\n  }\n  knownFor(first: 1) {\n    edges {\n      node {\n        summary {\n          yearRange {\n            year\n            endYear\n          }\n        }\n        title {\n          id\n          originalTitleText {\n            text\n          }\n          titleText {\n            text\n          }\n          titleType {\n            canHaveEpisodes\n          }\n        }\n      }\n    }\n  }\n  bio {\n    displayableArticle {\n      body {\n        plaidHtml(\n          queryParams: $refTagQueryParam\n          showOriginalTitleText: $originalTitleText\n        )\n      }\n    }\n  }\n}\n\nfragment ImageListItemMetadata on Image {\n  id\n  url\n  height\n  width\n  caption {\n    plainText\n  }\n  names(limit: 4) {\n    id\n    nameText {\n      text\n    }\n  }\n  titles(limit: 1) {\n    id\n    titleText {\n      text\n    }\n    originalTitleText {\n      text\n    }\n    releaseYear {\n      year\n      endYear\n    }\n  }\n}\n\nfragment VideoListItemMetadata on Video {\n  id\n  thumbnail {\n    url\n    width\n    height\n  }\n  name {\n    value\n    language\n  }\n  description {\n    value\n    language\n  }\n  runtime {\n    unit\n    value\n  }\n  primaryTitle {\n    id\n    originalTitleText {\n      text\n    }\n    titleText {\n      text\n    }\n    titleType {\n      canHaveEpisodes\n    }\n    releaseYear {\n      year\n      endYear\n    }\n  }\n}",
    "operationName": "AddConstToList",
    "variables": {
        "listId": "",
        "constId": "",
        "includeListItemMetadata": true,
        "refTagQueryParam": "lsedt_add_items",
        "originalTitleText": false
    }
}

var request_data_add_description = {
    "query": "mutation EditListItemDescription($listId: ID!, $itemId: ID!, $itemDescription: String!) {\n  editListItemDescription(\n    input: {listId: $listId, itemId: $itemId, itemDescription: $itemDescription}\n  ) {\n    formattedItemDescription {\n      originalText {\n        markdown\n        plaidHtml(showLineBreak: true)\n        plainText\n      }\n    }\n  }\n}",
    "operationName": "EditListItemDescription",
    "variables": {
        "listId": "",
        "itemId": "",
        "itemDescription": ""
    }
}

var request_data_reorder_item = {
    "query": "mutation reorderListItems($input: ReorderListInput!) {\n  reorderList(input: $input) {\n    listId\n  }\n}",
    "operationName": "reorderListItems",
    "variables": {
        "input": {
            "newPositions": [
                /*{
                    "position": -1,
                    "itemId": ""
                }*/
            ],
            "listId": ""
        }
    }
}

if (/^https:\/\/(www.)?imdb.com\/list\/ls[0-9]+\/edit/.test(document.location)) {
    var 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.isReverse = createReverseCheckbox(root);
    elements.insert = createInsertRadio(root);
    elements.insertOther = createInsertOtherInput(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('section.ipc-page-section--base');
	if (container === null) {
		throw "section.section.ipc-page-section--base element not found";
	}
	let root = document.createElement('div');
	root.setAttribute('class', 'search-bar ipc-list-card--base ipc-list-card--border-line');
	root.style.height = 'initial';
  root.style.marginTop = '30px';
  root.style.marginBottom = '30px';
  root.style.padding = '10px';
	container.insertBefore(root, container.children[1]);

	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 createRadio(name, value, textContent) {
  let radio = document.createElement('input');
	radio.type = 'radio';
	radio.style = 'width: initial;';
  radio.name = name;
  radio.value = value;

	let text = document.createElement('span');
	text.style = 'font-weight: normal;';
	text.textContent = textContent;

	let label = document.createElement('label');
	label.appendChild(radio);
	label.appendChild(text);

	return {label: label, radio: radio};
}

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 createReverseCheckbox(root) {
  let isReverse = createCheckbox("Reverse Items on Insertion");
  root.appendChild(document.createElement('br'));
	root.appendChild(isReverse.label);
	root.appendChild(document.createElement('br'));

	return isReverse.checkbox;
}

function createInsertRadio(root) {
  let insertBegin = createRadio("imdb_list_importer_insert", "1", "Insert in the Beginning");
  let insertEnd = createRadio("imdb_list_importer_insert", "-1", "Insert in the End");
  let insertOther = createRadio("imdb_list_importer_insert", "0", "Insert in Other Position");
  
  insertEnd.radio.checked = true;
  
  root.appendChild(insertBegin.label);
  root.appendChild(document.createElement('br'));
  root.appendChild(insertEnd.label);
  root.appendChild(document.createElement('br'));
  root.appendChild(insertOther.label);
  root.appendChild(document.createElement('br'));
  
  insertBegin.radio.addEventListener('change', isOtherHandler, false);
  insertEnd.radio.addEventListener('change', isOtherHandler, false);
  insertOther.radio.addEventListener('change', isOtherHandler, false);
  
  return {'begin': insertBegin.radio, 'end': insertEnd.radio, 'other': insertOther.radio};
}

function createInsertOtherInput(root) {
  let insertOtherInput = document.createElement('input');
  insertOtherInput.type = 'text';
  insertOtherInput.disabled = true;
  root.appendChild(insertOtherInput);
  root.appendChild(document.createElement('br'));
  root.appendChild(document.createElement('br'));
  
  return insertOtherInput;
}

function isOtherHandler(event) {
  let isDisable = event.target.value != "0";
  elements.insertOther.disabled = isDisable;
}


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() {
	return "[a-z]{2}[0-9]{7,8}";
}

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(/\r|\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().trim();
		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;
}

async 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 list_id = /ls[0-9]{1,}/.exec(location.href)[0];
  
  if (elements.isReverse.checked) {
   	list.reverse();
  }

  let items = [];
  for (let i = 0; i < list.length; ++i) {
    log("Info", `Adding element ${String(i+1)}: ${list[i].const}...`);

    request_data_add_item.variables.listId = list_id;
  	request_data_add_item.variables.constId = list[i].const;
    let response = await sendRequest(request_data_add_item);
    
    let listItemId = response.data.addItemToList.modifiedItem.itemId;
    log("Info", `${list[i].const} added as ${listItemId}`);
    items.push(listItemId);
      
		if (list[i].description.length !== 0) {
      log("Info", `Adding description to ${listItemId}...`);
      request_data_add_description.variables.listId = list_id;
    	request_data_add_description.variables.itemId = listItemId;
    	request_data_add_description.variables.itemDescription = list[i].description;
      await sendRequest(request_data_add_description);
    }
    
    setStatus(`Ready ${String(i+1)} of ${list.length}.`);  
  }
  
  let insertPosition = -1;
  if (elements.insert.begin.checked) {
   	insertPosition = 1;
  } else if (elements.insert.other.checked) {
   	insertPosition = Number(elements.insertOther.value);
    if (isNaN(insertPosition) || insertPosition < 1) {
     	insertPosition = -1; 
    }
  }
  if (insertPosition != -1) {
    request_data_reorder_item.variables.input.newPositions = [];
    request_data_reorder_item.variables.input.listId = list_id;

    for (let i = items.length - 1; i >= 0; i--) {
      request_data_reorder_item.variables.input.newPositions.push({
        "position": insertPosition,
        "itemId": items[i]
      });
    }

    log("Info", `Moving items to position ${insertPosition}...`);
    await sendRequest(request_data_reorder_item);
  }
  
  location.reload();
}

function sendRequest(data) {
  return fetch("https://api.graphql.imdb.com/", {
    "credentials": "include",
    "headers": {
        "Accept": "application/graphql+json, application/json",
        "content-type": "application/json",
    },
    "referrer": "https://www.imdb.com/",
    "body": JSON.stringify(data),
    "method": "POST",
    "mode": "cors"
	}).then((response) => {
  	if (!response.ok) {
      throw new Error(`Request failed with status code ${response.status}`);
    }

    return response.json();
  });
}