IMDB List Importer

Import list of titles or people in the imdb list

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址