IMDB List Importer

Import list of titles or people in the imdb list

当前为 2024-05-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name IMDB List Importer
  3. // @namespace Neinei0k_imdb
  4. // @include https://www.imdb.com/list/ls*/edit/*
  5. // @version 10.1
  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. var request_data_add_item = {
  11. "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}",
  12. "operationName": "AddConstToList",
  13. "variables": {
  14. "listId": "",
  15. "constId": "",
  16. "includeListItemMetadata": true,
  17. "refTagQueryParam": "lsedt_add_items",
  18. "originalTitleText": false
  19. }
  20. }
  21.  
  22. var request_data_add_description = {
  23. "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}",
  24. "operationName": "EditListItemDescription",
  25. "variables": {
  26. "listId": "",
  27. "itemId": "",
  28. "itemDescription": ""
  29. }
  30. }
  31.  
  32. var request_data_reorder_item = {
  33. "query": "mutation reorderListItems($input: ReorderListInput!) {\n reorderList(input: $input) {\n listId\n }\n}",
  34. "operationName": "reorderListItems",
  35. "variables": {
  36. "input": {
  37. "newPositions": [
  38. /*{
  39. "position": -1,
  40. "itemId": ""
  41. }*/
  42. ],
  43. "listId": ""
  44. }
  45. }
  46. }
  47.  
  48. var elements = createHTMLForm();
  49.  
  50. function log(level, message) {
  51. console.log("(IMDB List Importer) " + level + ": " + message);
  52. }
  53.  
  54. function setStatus(message) {
  55. elements.status.textContent = message;
  56. }
  57.  
  58. function createHTMLForm() {
  59. let elements = {};
  60.  
  61. try {
  62. let root = createRoot();
  63. elements.text = createTextField(root);
  64.  
  65. if (isFileAPISupported()) {
  66. elements.file = createFileInput(root);
  67. elements.isFromFile = createFromFileCheckbox(root);
  68. } else {
  69. createFileAPINotSupportedMessage(root);
  70. }
  71.  
  72. elements.isCSV = createCSVCheckbox(root);
  73. elements.isUnique = createUniqueCheckbox(root);
  74. elements.isReverse = createReverseCheckbox(root);
  75. elements.insert = createInsertRadio(root);
  76. elements.insertOther = createInsertOtherInput(root);
  77. elements.status = createStatusBar(root);
  78. createImportButton(root);
  79. } catch (message) {
  80. log("Error", message);
  81. }
  82.  
  83. return elements;
  84. }
  85.  
  86. function isFileAPISupported() {
  87. return window.File && window.FileReader && window.FileList && window.Blob;
  88. }
  89.  
  90. function createRoot() {
  91. let container = document.querySelector('section.ipc-page-section--base');
  92. if (container === null) {
  93. throw "section.section.ipc-page-section--base element not found";
  94. }
  95. let root = document.createElement('div');
  96. root.setAttribute('class', 'search-bar ipc-list-card--base ipc-list-card--border-line');
  97. root.style.height = 'initial';
  98. root.style.marginTop = '30px';
  99. root.style.marginBottom = '30px';
  100. root.style.padding = '10px';
  101. container.insertBefore(root, container.children[1]);
  102.  
  103. return root;
  104. }
  105.  
  106. function createTextField(root) {
  107. let text = document.createElement('textarea');
  108. text.style = "background-color: white; width: 100%; height: 100px; overflow: initial;";
  109. root.appendChild(text);
  110. root.appendChild(document.createElement('br'));
  111.  
  112. return text;
  113. }
  114.  
  115. function createFileInput(root) {
  116. let file = document.createElement('input');
  117. file.type = 'file';
  118. file.disabled = true;
  119. file.style.marginBottom = '10px';
  120. root.appendChild(file);
  121. root.appendChild(document.createElement('br'));
  122.  
  123. return file;
  124. }
  125.  
  126. function createFromFileCheckbox(root) {
  127. let isFromFile = createCheckbox("Import from file (otherwise import from text)");
  128. root.appendChild(isFromFile.label);
  129. root.appendChild(document.createElement('br'));
  130.  
  131. isFromFile.checkbox.addEventListener('change', fromFileOrTextChangeHandler, false);
  132. return isFromFile.checkbox;
  133. }
  134.  
  135. function createCheckbox(textContent) {
  136. let checkbox = document.createElement('input');
  137. checkbox.type = 'checkbox';
  138. checkbox.style = 'width: initial;';
  139.  
  140. let text = document.createElement('span');
  141. text.style = 'font-weight: normal;';
  142. text.textContent = textContent;
  143.  
  144. let label = document.createElement('label');
  145. label.appendChild(checkbox);
  146. label.appendChild(text);
  147.  
  148. return {label: label, checkbox: checkbox};
  149. }
  150.  
  151. function createRadio(name, value, textContent) {
  152. let radio = document.createElement('input');
  153. radio.type = 'radio';
  154. radio.style = 'width: initial;';
  155. radio.name = name;
  156. radio.value = value;
  157.  
  158. let text = document.createElement('span');
  159. text.style = 'font-weight: normal;';
  160. text.textContent = textContent;
  161.  
  162. let label = document.createElement('label');
  163. label.appendChild(radio);
  164. label.appendChild(text);
  165.  
  166. return {label: label, radio: radio};
  167. }
  168.  
  169. function fromFileOrTextChangeHandler(event) {
  170. let isChecked = event.target.checked;
  171. elements.text.disabled = isChecked;
  172. elements.file.disabled = !isChecked;
  173. }
  174.  
  175. function createFileAPINotSupportedMessage(root) {
  176. let notSupported = document.createElement('div');
  177. notSupported.style = 'font-weight: normal;';
  178. notSupported.style.marginTop = '10px';
  179. notSupported.style.marginBottom = '10px';
  180. notSupported.textContent = "Your browser does not support File API for reading local files.";
  181. root.appendChild(notSupported);
  182. }
  183.  
  184. function createCSVCheckbox(root) {
  185. let isCSV = createCheckbox("Data from .csv file (otherwise extract ids from text)");
  186. isCSV.checkbox.checked = true;
  187. root.appendChild(isCSV.label);
  188. root.appendChild(document.createElement('br'));
  189.  
  190. return isCSV.checkbox;
  191. }
  192.  
  193. function createUniqueCheckbox(root) {
  194. let isUnique = createCheckbox("Add only unique elements");
  195. root.appendChild(isUnique.label);
  196. root.appendChild(document.createElement('br'));
  197.  
  198. return isUnique.checkbox;
  199. }
  200.  
  201. function createReverseCheckbox(root) {
  202. let isReverse = createCheckbox("Reverse Items on Insertion");
  203. root.appendChild(document.createElement('br'));
  204. root.appendChild(isReverse.label);
  205. root.appendChild(document.createElement('br'));
  206.  
  207. return isReverse.checkbox;
  208. }
  209.  
  210. function createInsertRadio(root) {
  211. let insertBegin = createRadio("imdb_list_importer_insert", "1", "Insert in the Beginning");
  212. let insertEnd = createRadio("imdb_list_importer_insert", "-1", "Insert in the End");
  213. let insertOther = createRadio("imdb_list_importer_insert", "0", "Insert in Other Position");
  214. insertEnd.radio.checked = true;
  215. root.appendChild(insertBegin.label);
  216. root.appendChild(document.createElement('br'));
  217. root.appendChild(insertEnd.label);
  218. root.appendChild(document.createElement('br'));
  219. root.appendChild(insertOther.label);
  220. root.appendChild(document.createElement('br'));
  221. insertBegin.radio.addEventListener('change', isOtherHandler, false);
  222. insertEnd.radio.addEventListener('change', isOtherHandler, false);
  223. insertOther.radio.addEventListener('change', isOtherHandler, false);
  224. return {'begin': insertBegin.radio, 'end': insertEnd.radio, 'other': insertOther.radio};
  225. }
  226.  
  227. function createInsertOtherInput(root) {
  228. let insertOtherInput = document.createElement('input');
  229. insertOtherInput.type = 'text';
  230. insertOtherInput.disabled = true;
  231. root.appendChild(insertOtherInput);
  232. root.appendChild(document.createElement('br'));
  233. root.appendChild(document.createElement('br'));
  234. return insertOtherInput;
  235. }
  236.  
  237. function isOtherHandler(event) {
  238. let isDisable = event.target.value != "0";
  239. elements.insertOther.disabled = isDisable;
  240. }
  241.  
  242.  
  243. function createStatusBar(root) {
  244. let status = document.createElement('div');
  245. status.textContent = "Set-up parameters. Insert text or choose file. Press 'Import List' button.";
  246. status.style.marginTop = '10px';
  247. status.style.marginBottom = '10px';
  248. root.appendChild(status);
  249.  
  250. return status;
  251. }
  252.  
  253. function createImportButton(root) {
  254. let importList = document.createElement('button');
  255. importList.class = 'btn';
  256. importList.textContent = "Import List";
  257. root.appendChild(importList);
  258.  
  259. importList.addEventListener('click', importListClickHandler, false);
  260. }
  261.  
  262. function importListClickHandler(event) {
  263. if (elements.hasOwnProperty('isFromFile') && elements.isFromFile.checked) {
  264. readFile();
  265. } else {
  266. importList(extractItems(elements.text.value));
  267. }
  268. }
  269.  
  270. function readFile() {
  271. let file = elements.file.files[0];
  272. if (file !== undefined) {
  273. log("Info", "Reading file " + file.name);
  274. setStatus("Reading file " + file.name);
  275. let fileReader = new FileReader();
  276. fileReader.onload = fileOnloadHandler;
  277. fileReader.readAsText(file);
  278. } else {
  279. setStatus("Error: File is not selected");
  280. }
  281. }
  282.  
  283. function fileOnloadHandler(event) {
  284. if (event.target.error === null) {
  285. importList(extractItems(event.target.result));
  286. } else {
  287. log("Error", e.target.error);
  288. setStatus("Error: " + e.target.error);
  289. }
  290. }
  291.  
  292. function extractItems(text) {
  293. try {
  294. let itemRegExp = getRegExpForItems();
  295.  
  296. if (elements.isCSV.checked) {
  297. return extractItemsFromCSV(itemRegExp, text);
  298. } else {
  299. return extractItemsFromText(itemRegExp, text);
  300. }
  301. } catch (message) {
  302. log("Error", message);
  303. setStatus("Error: " + message);
  304. return [];
  305. }
  306. }
  307.  
  308. function getRegExpForItems() {
  309. return "[a-z]{2}[0-9]{7,8}";
  310. }
  311.  
  312. function extractItemsFromCSV(re, text) {
  313. let table = parseCSV(text);
  314. let fields = findFieldNumbers(table);
  315.  
  316. if (fields.description !== -1) {
  317. log("Info", "Found csv file fields Const(" + fields.const + ") and Description(" + fields.description + ")");
  318. } else {
  319. log("Info", "Found csv file field Const(" + fields.const + "). Description field is not found.");
  320. }
  321.  
  322. re = new RegExp("^" + re + "$");
  323. let items = [];
  324. // Add elements to the list
  325. for (let i = 1; i < table.length; i++) {
  326. let row = table[i];
  327. if (re.exec(row[fields.const]) === null) {
  328. throw "Invalid 'const' field format on line " + (i+1);
  329. }
  330. if (elements.isUnique.checked) {
  331. let exists = items.findIndex(function(v){
  332. return v.const === row[fields.const];
  333. });
  334. if (exists !== -1) continue;
  335. }
  336. items.push({const: row[fields.const], description: (fields.description == -1 ? "" : row[fields.description])});
  337. }
  338.  
  339. return items;
  340. }
  341.  
  342. function parseCSV(text) {
  343. let lines = text.split(/\r|\n/);
  344. let table = [];
  345. for (let i=0; i < lines.length; i++) {
  346. if (isEmpty(lines[i])) {
  347. continue;
  348. }
  349. let isInsideString = false;
  350. let row = [""];
  351. for (let j=0; j < lines[i].length; j++) {
  352. if (!isInsideString && lines[i][j] === ',') {
  353. row.push("");
  354. } else if (lines[i][j] === '"') {
  355. isInsideString = !isInsideString;
  356. } else {
  357. row[row.length-1] += lines[i][j];
  358. }
  359. }
  360. table.push(row);
  361. if (isInsideString) {
  362. throw "Wrong number of \" on line " + (i+1);
  363. }
  364. if (row.length != table[0].length) {
  365. throw "Wrong number of fields on line " + (i+1) + ". Expected " + table[0].length + " but found " + row.length + ".";
  366. }
  367. }
  368.  
  369. return table;
  370. }
  371.  
  372. function isEmpty(str) {
  373. return str.trim().length === 0;
  374. }
  375.  
  376. function findFieldNumbers(table) {
  377. let fieldNames = table[0];
  378. let fieldNumbers = {'const': -1, 'description': -1};
  379.  
  380. for (let i = 0; i < fieldNames.length; i++) {
  381. let fieldName = fieldNames[i].toLowerCase().trim();
  382. if (fieldName === 'const') {
  383. fieldNumbers.const = i;
  384. } else if (fieldName === 'description') {
  385. fieldNumbers.description = i;
  386. }
  387. }
  388.  
  389. if (fieldNumbers.const === -1) {
  390. throw "Field 'const' not found."
  391. }
  392. return fieldNumbers;
  393. }
  394.  
  395. function extractItemsFromText(re, text) {
  396. re = new RegExp(re);
  397. let items = [];
  398. let e;
  399. while ((e = re.exec(text)) !== null) {
  400. let flag = '';
  401. if (elements.isUnique.checked)
  402. flag = 'g';
  403. text = text.replace(new RegExp(e[0], flag), '');
  404. items.push({const: e[0], description: ""});
  405. }
  406. return items;
  407. }
  408.  
  409. async function importList(list) {
  410. if (list.length === 0)
  411. return;
  412.  
  413. let msg = "Elements to add: ";
  414. for (let i = 0; i < list.length; i++)
  415. msg += list[i].const + ",";
  416. log("Info", msg);
  417.  
  418. let list_id = /ls[0-9]{1,}/.exec(location.href)[0];
  419. if (elements.isReverse.checked) {
  420. list.reverse();
  421. }
  422.  
  423. let items = [];
  424. for (let i = 0; i < list.length; ++i) {
  425. log("Info", `Adding element ${String(i+1)}: ${list[i].const}...`);
  426.  
  427. request_data_add_item.variables.listId = list_id;
  428. request_data_add_item.variables.constId = list[i].const;
  429. let response = await sendRequest(request_data_add_item);
  430. let listItemId = response.data.addItemToList.modifiedItem.itemId;
  431. log("Info", `${list[i].const} added as ${listItemId}`);
  432. items.push(listItemId);
  433. if (list[i].description.length !== 0) {
  434. log("Info", `Adding description to ${listItemId}...`);
  435. request_data_add_description.variables.listId = list_id;
  436. request_data_add_description.variables.itemId = listItemId;
  437. request_data_add_description.variables.itemDescription = list[i].description;
  438. await sendRequest(request_data_add_description);
  439. }
  440. setStatus(`Ready ${String(i+1)} of ${list.length}.`);
  441. }
  442. let insertPosition = -1;
  443. if (elements.insert.begin.checked) {
  444. insertPosition = 1;
  445. } else if (elements.insert.other.checked) {
  446. insertPosition = Number(elements.insertOther.value);
  447. if (isNaN(insertPosition) || insertPosition < 1) {
  448. insertPosition = -1;
  449. }
  450. }
  451. if (insertPosition != -1) {
  452. request_data_reorder_item.variables.input.newPositions = [];
  453. request_data_reorder_item.variables.input.listId = list_id;
  454.  
  455. for (let i = items.length - 1; i >= 0; i--) {
  456. request_data_reorder_item.variables.input.newPositions.push({
  457. "position": insertPosition,
  458. "itemId": items[i]
  459. });
  460. }
  461.  
  462. log("Info", `Moving items to position ${insertPosition}...`);
  463. await sendRequest(request_data_reorder_item);
  464. }
  465. location.reload();
  466. }
  467.  
  468. function sendRequest(data) {
  469. return fetch("https://api.graphql.imdb.com/", {
  470. "credentials": "include",
  471. "headers": {
  472. "Accept": "application/graphql+json, application/json",
  473. "content-type": "application/json",
  474. },
  475. "referrer": "https://www.imdb.com/",
  476. "body": JSON.stringify(data),
  477. "method": "POST",
  478. "mode": "cors"
  479. }).then((response) => {
  480. if (!response.ok) {
  481. throw new Error(`Request failed with status code ${response.status}`);
  482. }
  483.  
  484. return response.json();
  485. });
  486. }