Greasy Fork is available in English.

IMDB List Importer

Import list of titles or people in the imdb list

// ==UserScript==
// @name            IMDB List Importer
// @namespace       Neinei0k_imdb
// @include         https://www.imdb.com/list/ls*/edit/*
// @version         10.1
// @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": ""
        }
    }
}

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();
  });
}