IMDB List Importer

Import list of titles or people in the imdb list

当前为 2023-05-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name IMDB List Importer
  3. // @namespace Neinei0k_imdb
  4. // @include https://www.imdb.com/list/*
  5. // @version 8.2
  6. // @license GNU General Public License v3.0 or later
  7. // @description Import list of titles or people in the imdb list
  8. // ==/UserScript==
  9.  
  10. let elements = createHTMLForm();
  11.  
  12. function log(level, message) {
  13. console.log("(IMDB List Importer) " + level + ": " + message);
  14. }
  15.  
  16. function setStatus(message) {
  17. elements.status.textContent = message;
  18. }
  19.  
  20. function createHTMLForm() {
  21. let elements = {};
  22.  
  23. try {
  24. let root = createRoot();
  25. elements.text = createTextField(root);
  26.  
  27. if (isFileAPISupported()) {
  28. elements.file = createFileInput(root);
  29. elements.isFromFile = createFromFileCheckbox(root);
  30. } else {
  31. createFileAPINotSupportedMessage(root);
  32. }
  33.  
  34. elements.isCSV = createCSVCheckbox(root);
  35. elements.isUnique = createUniqueCheckbox(root);
  36. elements.status = createStatusBar(root);
  37. createImportButton(root);
  38. } catch (message) {
  39. log("Error", message);
  40. }
  41.  
  42. return elements;
  43. }
  44.  
  45. function isFileAPISupported() {
  46. return window.File && window.FileReader && window.FileList && window.Blob;
  47. }
  48.  
  49. function createRoot() {
  50. let container = document.querySelector('.lister-search');
  51. if (container === null) {
  52. throw ".lister-search element not found";
  53. }
  54. let root = document.createElement('div');
  55. root.setAttribute('class', 'search-bar');
  56. root.style.height = 'initial';
  57. root.style.marginBottom = '30px';
  58. container.appendChild(root);
  59.  
  60. return root;
  61. }
  62.  
  63. function createTextField(root) {
  64. let text = document.createElement('textarea');
  65. text.style = "background-color: white; width: 100%; height: 100px; overflow: initial;";
  66. root.appendChild(text);
  67. root.appendChild(document.createElement('br'));
  68.  
  69. return text;
  70. }
  71.  
  72. function createFileInput(root) {
  73. let file = document.createElement('input');
  74. file.type = 'file';
  75. file.disabled = true;
  76. file.style.marginBottom = '10px';
  77. root.appendChild(file);
  78. root.appendChild(document.createElement('br'));
  79.  
  80. return file;
  81. }
  82.  
  83. function createFromFileCheckbox(root) {
  84. let isFromFile = createCheckbox("Import from file (otherwise import from text)");
  85. root.appendChild(isFromFile.label);
  86. root.appendChild(document.createElement('br'));
  87.  
  88. isFromFile.checkbox.addEventListener('change', fromFileOrTextChangeHandler, false);
  89. return isFromFile.checkbox;
  90. }
  91.  
  92. function createCheckbox(textContent) {
  93. let checkbox = document.createElement('input');
  94. checkbox.type = 'checkbox';
  95. checkbox.style = 'width: initial;';
  96.  
  97. let text = document.createElement('span');
  98. text.style = 'font-weight: normal;';
  99. text.textContent = textContent;
  100.  
  101. let label = document.createElement('label');
  102. label.appendChild(checkbox);
  103. label.appendChild(text);
  104.  
  105. return {label: label, checkbox: checkbox};
  106. }
  107.  
  108. function fromFileOrTextChangeHandler(event) {
  109. let isChecked = event.target.checked;
  110. elements.text.disabled = isChecked;
  111. elements.file.disabled = !isChecked;
  112. }
  113.  
  114. function createFileAPINotSupportedMessage(root) {
  115. let notSupported = document.createElement('div');
  116. notSupported.style = 'font-weight: normal;';
  117. notSupported.style.marginTop = '10px';
  118. notSupported.style.marginBottom = '10px';
  119. notSupported.textContent = "Your browser does not support File API for reading local files.";
  120. root.appendChild(notSupported);
  121. }
  122.  
  123. function createCSVCheckbox(root) {
  124. let isCSV = createCheckbox("Data from .csv file (otherwise extract ids from text)");
  125. isCSV.checkbox.checked = true;
  126. root.appendChild(isCSV.label);
  127. root.appendChild(document.createElement('br'));
  128.  
  129. return isCSV.checkbox;
  130. }
  131.  
  132. function createUniqueCheckbox(root) {
  133. let isUnique = createCheckbox("Add only unique elements");
  134. root.appendChild(isUnique.label);
  135. root.appendChild(document.createElement('br'));
  136.  
  137. return isUnique.checkbox;
  138. }
  139.  
  140. function createStatusBar(root) {
  141. let status = document.createElement('div');
  142. status.textContent = "Set-up parameters. Insert text or choose file. Press 'Import List' button.";
  143. status.style.marginTop = '10px';
  144. status.style.marginBottom = '10px';
  145. root.appendChild(status);
  146.  
  147. return status;
  148. }
  149.  
  150. function createImportButton(root) {
  151. let importList = document.createElement('button');
  152. importList.class = 'btn';
  153. importList.textContent = "Import List";
  154. root.appendChild(importList);
  155.  
  156. importList.addEventListener('click', importListClickHandler, false);
  157. }
  158.  
  159. function importListClickHandler(event) {
  160. if (elements.hasOwnProperty('isFromFile') && elements.isFromFile.checked) {
  161. readFile();
  162. } else {
  163. importList(extractItems(elements.text.value));
  164. }
  165. }
  166.  
  167. function readFile() {
  168. let file = elements.file.files[0];
  169. if (file !== undefined) {
  170. log("Info", "Reading file " + file.name);
  171. setStatus("Reading file " + file.name);
  172. let fileReader = new FileReader();
  173. fileReader.onload = fileOnloadHandler;
  174. fileReader.readAsText(file);
  175. } else {
  176. setStatus("Error: File is not selected");
  177. }
  178. }
  179.  
  180. function fileOnloadHandler(event) {
  181. if (event.target.error === null) {
  182. importList(extractItems(event.target.result));
  183. } else {
  184. log("Error", e.target.error);
  185. setStatus("Error: " + e.target.error);
  186. }
  187. }
  188.  
  189. function extractItems(text) {
  190. try {
  191. let itemRegExp = getRegExpForItems();
  192.  
  193. if (elements.isCSV.checked) {
  194. return extractItemsFromCSV(itemRegExp, text);
  195. } else {
  196. return extractItemsFromText(itemRegExp, text);
  197. }
  198. } catch (message) {
  199. log("Error", message);
  200. setStatus("Error: " + message);
  201. return [];
  202. }
  203. }
  204.  
  205. function getRegExpForItems() {
  206. let listType;
  207. if (isPeopleList()) {
  208. log("Info", "List type: people");
  209. listType = "nm";
  210. } else if (isTitlesList()) {
  211. log("Info", "List type: titles");
  212. listType = "tt";
  213. } else {
  214. throw "Could not determine list type";
  215. }
  216. return listType + "[0-9]{7,8}";
  217. }
  218.  
  219. function isPeopleList() {
  220. return document.querySelector('[data-type="People"]') !== null;
  221. }
  222.  
  223. function isTitlesList() {
  224. return document.querySelector('[data-type="Titles"]') !== null;
  225. }
  226.  
  227. function extractItemsFromCSV(re, text) {
  228. let table = parseCSV(text);
  229. let fields = findFieldNumbers(table);
  230.  
  231. if (fields.description !== -1) {
  232. log("Info", "Found csv file fields Const(" + fields.const + ") and Description(" + fields.description + ")");
  233. } else {
  234. log("Info", "Found csv file field Const(" + fields.const + "). Description field is not found.");
  235. }
  236.  
  237. re = new RegExp("^" + re + "$");
  238. let items = [];
  239. // Add elements to the list
  240. for (let i = 1; i < table.length; i++) {
  241. let row = table[i];
  242. if (re.exec(row[fields.const]) === null) {
  243. throw "Invalid 'const' field format on line " + (i+1);
  244. }
  245. if (elements.isUnique.checked) {
  246. let exists = items.findIndex(function(v){
  247. return v.const === row[fields.const];
  248. });
  249. if (exists !== -1) continue;
  250. }
  251. items.push({const: row[fields.const], description: (fields.description == -1 ? "" : row[fields.description])});
  252. }
  253.  
  254. return items;
  255. }
  256.  
  257. function parseCSV(text) {
  258. let lines = text.split(/\r|\n/);
  259. let table = [];
  260. for (let i=0; i < lines.length; i++) {
  261. if (isEmpty(lines[i])) {
  262. continue;
  263. }
  264. let isInsideString = false;
  265. let row = [""];
  266. for (let j=0; j < lines[i].length; j++) {
  267. if (!isInsideString && lines[i][j] === ',') {
  268. row.push("");
  269. } else if (lines[i][j] === '"') {
  270. isInsideString = !isInsideString;
  271. } else {
  272. row[row.length-1] += lines[i][j];
  273. }
  274. }
  275. table.push(row);
  276. if (isInsideString) {
  277. throw "Wrong number of \" on line " + (i+1);
  278. }
  279. if (row.length != table[0].length) {
  280. throw "Wrong number of fields on line " + (i+1) + ". Expected " + table[0].length + " but found " + row.length + ".";
  281. }
  282. }
  283.  
  284. return table;
  285. }
  286.  
  287. function isEmpty(str) {
  288. return str.trim().length === 0;
  289. }
  290.  
  291. function findFieldNumbers(table) {
  292. let fieldNames = table[0];
  293. let fieldNumbers = {'const': -1, 'description': -1};
  294.  
  295. for (let i = 0; i < fieldNames.length; i++) {
  296. let fieldName = fieldNames[i].toLowerCase().trim();
  297. if (fieldName === 'const') {
  298. fieldNumbers.const = i;
  299. } else if (fieldName === 'description') {
  300. fieldNumbers.description = i;
  301. }
  302. }
  303.  
  304. if (fieldNumbers.const === -1) {
  305. throw "Field 'const' not found."
  306. }
  307. return fieldNumbers;
  308. }
  309.  
  310. function extractItemsFromText(re, text) {
  311. re = new RegExp(re);
  312. let items = [];
  313. let e;
  314. while ((e = re.exec(text)) !== null) {
  315. let flag = '';
  316. if (elements.isUnique.checked)
  317. flag = 'g';
  318. text = text.replace(new RegExp(e[0], flag), '');
  319. items.push({const: e[0], description: ""});
  320. }
  321. return items;
  322. }
  323.  
  324. function importList(list) {
  325. if (list.length === 0)
  326. return;
  327.  
  328. let msg = "Elements to add: ";
  329. for (let i = 0; i < list.length; i++)
  330. msg += list[i].const + ",";
  331. log("Info", msg);
  332.  
  333. let l = {};
  334. l.list = list;
  335. l.ready = 0;
  336. l.list_id = /ls[0-9]{1,}/.exec(location.href)[0];
  337. l.hiddenElementData = getHiddenElementData(); // Data needs to be send with all requests.
  338.  
  339. sendItem(l);
  340. }
  341.  
  342. function getHiddenElementData() {
  343. let hiddenElement = document.querySelector('#main > input');
  344. if (hiddenElement === null) {
  345. log("Error", "Hidden element not found. It is required to be sent with every request.");
  346. setStatus("Error: Hidden element not found. It is required to be sent with every request.");
  347. return "";
  348. }
  349. return hiddenElement.id + "=" + hiddenElement.value;
  350. }
  351.  
  352. function sendItem(l) {
  353. log("Info", 'Add element ' + l.ready + ': ' + l.list[l.ready].const);
  354. let url = 'https://www.imdb.com/list/' + l.list_id + '/' + l.list[l.ready].const + '/add';
  355. sendRequest(sendItemHandler, l, url, l.hiddenElementData);
  356. }
  357.  
  358. function sendRequest(handler, l, url, data) {
  359. var x = new XMLHttpRequest();
  360. x.onreadystatechange = function(event) {
  361. handler(l, event);
  362. }
  363. x.open('POST', url, true);
  364. x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  365. x.send(data);
  366. }
  367.  
  368. function sendItemHandler(l, event) {
  369. let target = event.target;
  370. log("Info", "Add element(" + l.list[l.ready].const + ") request: readyState(" + target.readyState + "), status(" + target.status + ")");
  371. if (target.readyState == 4 && target.status == 200) {
  372. let description = l.list[l.ready].description;
  373. if (description.length !== 0) {
  374. let listItemId = JSON.parse(target.responseText).list_item_id;
  375. let url = 'https://www.imdb.com/list/' + l.list_id + '/edit/itemdescription';
  376. let data = 'newDescription=' + description + '&listItem=' + listItemId + '&' + l.hiddenElementData
  377. sendRequest(sendItemDescriptionHandler, l, url, data);
  378. } else {
  379. showReady(l);
  380. }
  381. }
  382. }
  383.  
  384. function sendItemDescriptionHandler(l, event) {
  385. let target = event.target;
  386. log("Info", "Add element(" + l.list[l.ready].const + ") description request: readyState(" + target.readyState + "), status(" + target.status + ")");
  387. if (target.readyState == 4 && target.status == 200) {
  388. showReady(l);
  389. }
  390. }
  391.  
  392. function showReady(l) {
  393. l.ready += 1;
  394. setStatus('Ready ' + l.ready + ' of ' + l.list.length + '.');
  395. if (l.ready == l.list.length) {
  396. location.reload();
  397. } else {
  398. sendItem(l);
  399. }
  400. }