Greasy Fork

Greasy Fork is available in English.

IMDB List Importer

Import list of titles or people in the imdb list

目前为 2023-11-23 提交的版本,查看 最新版本

// ==UserScript==
// @name            IMDB List Importer
// @namespace       Neinei0k_imdb
// @include         https://www.imdb.com/list/*
// @version         9.0
// @license         GNU General Public License v3.0 or later
// @description	Import list of titles or people in the imdb list
// ==/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.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('.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 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() {
	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(/\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;
}

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.
  l.items = [];
  
  if (elements.isReverse.checked) {
   	l.list.reverse(); 
  }

	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 listItemId = JSON.parse(target.responseText).list_item_id;
    l.items.push(listItemId);
    
		let description = l.list[l.ready].description;
		if (description.length !== 0) {
			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) {
    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) {
      reorder(l, insertPosition);
    } else {
			location.reload();
    }
	} else {
		sendItem(l);
	}
}

function reorder(l, insertPosition) {
  let order = [];
  for (let i = l.items.length - 1; i >= 0; i--) {
    order.push('"' + l.items[i] + '":' + insertPosition);
  }
  order = '{' + order.join(',') + '}';
  
	let url = 'https://www.imdb.com/list/' + l.list_id + '/edit/reorderitems';
	let data = 'newListOrder=' + order + '&' + l.hiddenElementData;
  log("Info", "Reoder items: " + order);
  sendRequest(sendReorderHandler, l, url, data);
}

function sendReorderHandler(l, event) {
  let target = event.target;
 	if (target.readyState == 4 && target.status == 200) {
		location.reload();
	}
}