- // ==UserScript==
- // @name IMDB List Importer
- // @namespace Neinei0k_imdb
- // @include https://www.imdb.com/list/*
- // @version 10.3
- // @license GNU General Public License v3.0 or later
- // @description Import list of titles or people in the imdb list
- // ==/UserScript==
-
- var request_data_add_item = {
- "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}",
- "operationName": "AddConstToList",
- "variables": {
- "listId": "",
- "constId": "",
- "includeListItemMetadata": true,
- "refTagQueryParam": "lsedt_add_items",
- "originalTitleText": false
- }
- }
-
- var request_data_add_description = {
- "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}",
- "operationName": "EditListItemDescription",
- "variables": {
- "listId": "",
- "itemId": "",
- "itemDescription": ""
- }
- }
-
- var request_data_reorder_item = {
- "query": "mutation reorderListItems($input: ReorderListInput!) {\n reorderList(input: $input) {\n listId\n }\n}",
- "operationName": "reorderListItems",
- "variables": {
- "input": {
- "newPositions": [
- /*{
- "position": -1,
- "itemId": ""
- }*/
- ],
- "listId": ""
- }
- }
- }
-
- if (/^https:\/\/(www.)?imdb.com\/list\/ls[0-9]+\/edit/.test(document.location)) {
- var 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('section.ipc-page-section--base');
- if (container === null) {
- throw "section.section.ipc-page-section--base element not found";
- }
- let root = document.createElement('div');
- root.setAttribute('class', 'search-bar ipc-list-card--base ipc-list-card--border-line');
- root.style.height = 'initial';
- root.style.marginTop = '30px';
- root.style.marginBottom = '30px';
- root.style.padding = '10px';
- container.insertBefore(root, container.children[1]);
-
- 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() {
- return "[a-z]{2}[0-9]{7,8}";
- }
-
- 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;
- }
-
- async 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 list_id = /ls[0-9]{1,}/.exec(location.href)[0];
-
- if (elements.isReverse.checked) {
- list.reverse();
- }
-
- let items = [];
- for (let i = 0; i < list.length; ++i) {
- log("Info", `Adding element ${String(i+1)}: ${list[i].const}...`);
-
- request_data_add_item.variables.listId = list_id;
- request_data_add_item.variables.constId = list[i].const;
- let response = await sendRequest(request_data_add_item);
-
- let listItemId = response.data.addItemToList.modifiedItem.itemId;
- log("Info", `${list[i].const} added as ${listItemId}`);
- items.push(listItemId);
-
- if (list[i].description.length !== 0) {
- log("Info", `Adding description to ${listItemId}...`);
- request_data_add_description.variables.listId = list_id;
- request_data_add_description.variables.itemId = listItemId;
- request_data_add_description.variables.itemDescription = list[i].description;
- await sendRequest(request_data_add_description);
- }
-
- setStatus(`Ready ${String(i+1)} of ${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) {
- request_data_reorder_item.variables.input.newPositions = [];
- request_data_reorder_item.variables.input.listId = list_id;
-
- for (let i = items.length - 1; i >= 0; i--) {
- request_data_reorder_item.variables.input.newPositions.push({
- "position": insertPosition,
- "itemId": items[i]
- });
- }
-
- log("Info", `Moving items to position ${insertPosition}...`);
- await sendRequest(request_data_reorder_item);
- }
-
- location.reload();
- }
-
- function sendRequest(data) {
- return fetch("https://api.graphql.imdb.com/", {
- "credentials": "include",
- "headers": {
- "Accept": "application/graphql+json, application/json",
- "content-type": "application/json",
- },
- "referrer": "https://www.imdb.com/",
- "body": JSON.stringify(data),
- "method": "POST",
- "mode": "cors"
- }).then((response) => {
- if (!response.ok) {
- throw new Error(`Request failed with status code ${response.status}`);
- }
-
- return response.json();
- });
- }