您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Import ratings for movies, TV series and episodes from a csv file.
// ==UserScript== // @name IMDB Ratings Importer // @namespace Neinei0k_imdb // @include https://www.imdb.com/user/ur*/ratings* // @grant none // @version 2.01 // @license GNU General Public License v3.0 or later // @description Import ratings for movies, TV series and episodes from a csv file. // ==/UserScript== let request_data_add_rating = { "query": "mutation UpdateTitleRating($rating: Int!, $titleId: ID!) {\n rateTitle(input: {rating: $rating, titleId: $titleId}) {\n rating {\n value\n }\n }\n}", "operationName": "UpdateTitleRating", "variables": { "rating": 0, "titleId": "" } } let elements = createHTMLForm(); function log(level, message) { console.log("(IMDB Ratings 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.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('.ipc-page-section--base'); if (container === null) { throw ".ipc-page-section--base element not found"; } let nextChild = container.children[0]; let root = document.createElement('div'); root.setAttribute('class', 'aux-content-widget-2 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, nextChild); 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 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 createStatusBar(root) { let status = document.createElement('div'); status.textContent = "Insert text or choose file. Press 'Import Ratings' 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 Ratings"; root.appendChild(importList); importList.addEventListener('click', importRatingsClickHandler, false); } function importRatingsClickHandler(event) { if (elements.hasOwnProperty('isFromFile') && elements.isFromFile.checked) { readFile(); } else { importRatings(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) { importRatings(extractItems(event.target.result)); } else { log("Error", e.target.error); setStatus("Error: " + e.target.error); } } function extractItems(text) { try { return extractItemsFromCSV(text); } catch (message) { log("Error", message); setStatus("Error: " + message); return []; } } function extractItemsFromCSV(text) { let table = parseCSV(text); let fields = findFieldNumbers(table); log("Info", "Found csv file fields Const(" + fields.const + ") and Your Rating(" + fields.rating + ")"); re = new RegExp("^tt[0-9]{7,8}$"); let items = []; // Add elements to the list for (let i = 1; i < table.length; i++) { let fconst = table[i][fields.const]; let frating = table[i][fields.rating]; if (re.exec(fconst) === null) { throw "Invalid 'const' field format on line " + (i+1); } if (frating === "") { continue; } frating = parseInt(frating); if (isNaN(frating)) { throw "Invalid 'your rating' field format on line " + (i+1); } items.push({const: fconst, rating: frating}); } 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, 'rating': -1}; for (let i = 0; i < fieldNames.length; i++) { let fieldName = fieldNames[i].toLowerCase().trim(); if (fieldName === 'const') { fieldNumbers.const = i; } else if (fieldName === 'your rating') { fieldNumbers.rating = i; } } if (fieldNumbers.const === -1) { throw "Field 'const' not found."; } else if (fieldNumbers.rating === -1) { throw "Field 'your rating' not found."; } return fieldNumbers; } async function importRatings(list) { if (list.length === 0) return; let l = {}; l.list = list; l.ready = 0; for (let i = 0; i < list.length; ++i) { log("Info", `Setting rating ${list[i].rating} for ${list[i].const}...`); request_data_add_rating.variables.rating = list[i].rating; request_data_add_rating.variables.titleId = list[i].const; await sendRequest(request_data_add_rating); setStatus(`Ready ${i+1} of ${list.length}.`); } } 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(); }); }