IMDB Ratings Importer

Import ratings for movies, TV series and episodes from a csv file.

  1. // ==UserScript==
  2. // @name IMDB Ratings Importer
  3. // @namespace Neinei0k_imdb
  4. // @include https://www.imdb.com/user/ur*/ratings*
  5. // @grant none
  6. // @version 2.01
  7. // @license GNU General Public License v3.0 or later
  8. // @description Import ratings for movies, TV series and episodes from a csv file.
  9. // ==/UserScript==
  10.  
  11. let request_data_add_rating = {
  12. "query": "mutation UpdateTitleRating($rating: Int!, $titleId: ID!) {\n rateTitle(input: {rating: $rating, titleId: $titleId}) {\n rating {\n value\n }\n }\n}",
  13. "operationName": "UpdateTitleRating",
  14. "variables": {
  15. "rating": 0,
  16. "titleId": ""
  17. }
  18. }
  19.  
  20. let elements = createHTMLForm();
  21. function log(level, message) {
  22. console.log("(IMDB Ratings Importer) " + level + ": " + message);
  23. }
  24. function setStatus(message) {
  25. elements.status.textContent = message;
  26. }
  27. function createHTMLForm() {
  28. let elements = {};
  29. try {
  30. let root = createRoot();
  31. elements.text = createTextField(root);
  32. if (isFileAPISupported()) {
  33. elements.file = createFileInput(root);
  34. elements.isFromFile = createFromFileCheckbox(root);
  35. } else {
  36. createFileAPINotSupportedMessage(root);
  37. }
  38. elements.status = createStatusBar(root);
  39. createImportButton(root);
  40. } catch (message) {
  41. log("Error", message);
  42. }
  43. return elements;
  44. }
  45. function isFileAPISupported() {
  46. return window.File && window.FileReader && window.FileList && window.Blob;
  47. }
  48. function createRoot() {
  49. let container = document.querySelector('.ipc-page-section--base');
  50. if (container === null) {
  51. throw ".ipc-page-section--base element not found";
  52. }
  53. let nextChild = container.children[0];
  54. let root = document.createElement('div');
  55. root.setAttribute('class', 'aux-content-widget-2 ipc-list-card--base ipc-list-card--border-line');
  56. root.style.height = 'initial';
  57. root.style.marginTop = '30px';
  58. root.style.marginBottom = '30px';
  59. root.style.padding = '10px';
  60. container.insertBefore(root, nextChild);
  61. return root;
  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. return text;
  69. }
  70. function createFileInput(root) {
  71. let file = document.createElement('input');
  72. file.type = 'file';
  73. file.disabled = true;
  74. file.style.marginBottom = '10px';
  75. root.appendChild(file);
  76. root.appendChild(document.createElement('br'));
  77. return file;
  78. }
  79. function createFromFileCheckbox(root) {
  80. let isFromFile = createCheckbox("Import from file (otherwise import from text)");
  81. root.appendChild(isFromFile.label);
  82. root.appendChild(document.createElement('br'));
  83. isFromFile.checkbox.addEventListener('change', fromFileOrTextChangeHandler, false);
  84. return isFromFile.checkbox;
  85. }
  86. function createCheckbox(textContent) {
  87. let checkbox = document.createElement('input');
  88. checkbox.type = 'checkbox';
  89. checkbox.style = 'width: initial;';
  90. let text = document.createElement('span');
  91. text.style = 'font-weight: normal;';
  92. text.textContent = textContent;
  93. let label = document.createElement('label');
  94. label.appendChild(checkbox);
  95. label.appendChild(text);
  96. return {label: label, checkbox: checkbox};
  97. }
  98. function fromFileOrTextChangeHandler(event) {
  99. let isChecked = event.target.checked;
  100. elements.text.disabled = isChecked;
  101. elements.file.disabled = !isChecked;
  102. }
  103. function createFileAPINotSupportedMessage(root) {
  104. let notSupported = document.createElement('div');
  105. notSupported.style = 'font-weight: normal;';
  106. notSupported.style.marginTop = '10px';
  107. notSupported.style.marginBottom = '10px';
  108. notSupported.textContent = "Your browser does not support File API for reading local files.";
  109. root.appendChild(notSupported);
  110. }
  111. function createStatusBar(root) {
  112. let status = document.createElement('div');
  113. status.textContent = "Insert text or choose file. Press 'Import Ratings' button.";
  114. status.style.marginTop = '10px';
  115. status.style.marginBottom = '10px';
  116. root.appendChild(status);
  117. return status;
  118. }
  119. function createImportButton(root) {
  120. let importList = document.createElement('button');
  121. importList.class = 'btn';
  122. importList.textContent = "Import Ratings";
  123. root.appendChild(importList);
  124. importList.addEventListener('click', importRatingsClickHandler, false);
  125. }
  126. function importRatingsClickHandler(event) {
  127. if (elements.hasOwnProperty('isFromFile') && elements.isFromFile.checked) {
  128. readFile();
  129. } else {
  130. importRatings(extractItems(elements.text.value));
  131. }
  132. }
  133. function readFile() {
  134. let file = elements.file.files[0];
  135. if (file !== undefined) {
  136. log("Info", "Reading file " + file.name);
  137. setStatus("Reading file " + file.name);
  138. let fileReader = new FileReader();
  139. fileReader.onload = fileOnloadHandler;
  140. fileReader.readAsText(file);
  141. } else {
  142. setStatus("Error: File is not selected");
  143. }
  144. }
  145. function fileOnloadHandler(event) {
  146. if (event.target.error === null) {
  147. importRatings(extractItems(event.target.result));
  148. } else {
  149. log("Error", e.target.error);
  150. setStatus("Error: " + e.target.error);
  151. }
  152. }
  153. function extractItems(text) {
  154. try {
  155. return extractItemsFromCSV(text);
  156. } catch (message) {
  157. log("Error", message);
  158. setStatus("Error: " + message);
  159. return [];
  160. }
  161. }
  162. function extractItemsFromCSV(text) {
  163. let table = parseCSV(text);
  164. let fields = findFieldNumbers(table);
  165. log("Info", "Found csv file fields Const(" + fields.const + ") and Your Rating(" + fields.rating + ")");
  166. re = new RegExp("^tt[0-9]{7,8}$");
  167. let items = [];
  168. // Add elements to the list
  169. for (let i = 1; i < table.length; i++) {
  170. let fconst = table[i][fields.const];
  171. let frating = table[i][fields.rating];
  172. if (re.exec(fconst) === null) {
  173. throw "Invalid 'const' field format on line " + (i+1);
  174. }
  175. if (frating === "") {
  176. continue;
  177. }
  178. frating = parseInt(frating);
  179. if (isNaN(frating)) {
  180. throw "Invalid 'your rating' field format on line " + (i+1);
  181. }
  182. items.push({const: fconst, rating: frating});
  183. }
  184. return items;
  185. }
  186. function parseCSV(text) {
  187. let lines = text.split(/\r|\n/);
  188. let table = [];
  189. for (let i=0; i < lines.length; i++) {
  190. if (isEmpty(lines[i])) {
  191. continue;
  192. }
  193. let isInsideString = false;
  194. let row = [""];
  195. for (let j=0; j < lines[i].length; j++) {
  196. if (!isInsideString && lines[i][j] === ',') {
  197. row.push("");
  198. } else if (lines[i][j] === '"') {
  199. isInsideString = !isInsideString;
  200. } else {
  201. row[row.length-1] += lines[i][j];
  202. }
  203. }
  204. table.push(row);
  205. if (isInsideString) {
  206. throw "Wrong number of \" on line " + (i+1);
  207. }
  208. if (row.length != table[0].length) {
  209. throw "Wrong number of fields on line " + (i+1) + ". Expected " + table[0].length + " but found " + row.length + ".";
  210. }
  211. }
  212. return table;
  213. }
  214. function isEmpty(str) {
  215. return str.trim().length === 0;
  216. }
  217.  
  218. function findFieldNumbers(table) {
  219. let fieldNames = table[0];
  220. let fieldNumbers = {'const': -1, 'rating': -1};
  221. for (let i = 0; i < fieldNames.length; i++) {
  222. let fieldName = fieldNames[i].toLowerCase().trim();
  223. if (fieldName === 'const') {
  224. fieldNumbers.const = i;
  225. } else if (fieldName === 'your rating') {
  226. fieldNumbers.rating = i;
  227. }
  228. }
  229. if (fieldNumbers.const === -1) {
  230. throw "Field 'const' not found.";
  231. } else if (fieldNumbers.rating === -1) {
  232. throw "Field 'your rating' not found.";
  233. }
  234. return fieldNumbers;
  235. }
  236. async function importRatings(list) {
  237. if (list.length === 0)
  238. return;
  239. let l = {};
  240. l.list = list;
  241. l.ready = 0;
  242. for (let i = 0; i < list.length; ++i) {
  243. log("Info", `Setting rating ${list[i].rating} for ${list[i].const}...`);
  244. request_data_add_rating.variables.rating = list[i].rating;
  245. request_data_add_rating.variables.titleId = list[i].const;
  246. await sendRequest(request_data_add_rating);
  247. setStatus(`Ready ${i+1} of ${list.length}.`);
  248. }
  249. }
  250. function sendRequest(data) {
  251. return fetch("https://api.graphql.imdb.com/", {
  252. "credentials": "include",
  253. "headers": {
  254. "Accept": "application/graphql+json, application/json",
  255. "content-type": "application/json",
  256. },
  257. "referrer": "https://www.imdb.com/",
  258. "body": JSON.stringify(data),
  259. "method": "POST",
  260. "mode": "cors"
  261. }).then((response) => {
  262. if (!response.ok) {
  263. throw new Error(`Request failed with status code ${response.status}`);
  264. }
  265.  
  266. return response.json();
  267. });
  268. }