您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Makes creating IMDb lists more efficient and convenient
当前为
- // ==UserScript==
- // @name IMDb - List Helper
- // @description Makes creating IMDb lists more efficient and convenient
- // @namespace imdb
- // @author themagician, monk-time
- // @include http://*imdb.com/list/*/edit
- // @include http://*imdb.com/list/*/edit?*
- // @require https://cdnjs.cloudflare.com/ajax/libs/d3-dsv/1.0.8/d3-dsv.min.js
- // @icon http://www.imdb.com/favicon.ico
- // @version 3.0.1
- // ==/UserScript==
- //
- // CHANGELOG
- //
- // 3.0.1
- // fixed: New IMDb search results layout
- // changed: Search only for regex matches by default
- // added: A checkbox to toggle search mode (matches only vs. matches or full string)
- //
- // 3.0
- // fixed: Search by movie title
- // fixed: No longer requires jQuery; jquery-csv is replaced with d3-dsv
- // fixed: Remove delay before auto-clicking on a search result
- // changed: Criticker score conversion: 0..10 -> 1, 11..20 -> 2, 91..100 -> 10
- // changed: Criticker importer requires .csv
- // changed: If the regex fails, try searching for the whole string
- //
- // 2.4.1
- // fixed: In some instances the script wasn't loaded (bad @include)
- // changed: Limit number of setTimeOut calls
- //
- // 2.4.0
- // fixed: IMDb changed layout
- //
- // 2.3.0
- // fixed: importing ratings works again
- //
- // 2.2.0
- // added: support for people
- //
- // 2.1.1
- // added: only show import form if ratings is selected
- //
- // 2.1
- // added: importers for imdb, rateyourmusic, criticker
- //
- // 2.0
- // added: import ratings
- // added: if regex doesn't match, skip entry
- //
- // 1.6.1.2
- // added: input text suggestion as a placeholder
- //
- // 1.6.1.1
- // fixed: some entries are skipped when adding imdb ids/urls
- //
- /* global d3 */
- 'use strict';
- // milliseconds between each request
- const REQUEST_DELAY = 1000;
- // ----- DOM ELEMENTS: STYLING, CREATION AND TRACKING -----
- document.head.insertAdjacentHTML('beforeend', `<style>
- #ilh-ui {
- margin: 0 5% 5% 5%;
- padding: 10px;
- border: 1px solid #e8e8e8;
- }
- #ilh-ui div {
- margin: 0.5em 0 0.75em
- }
- #ilh-ui label {
- font-weight: normal;
- }
- #ilh-ui input[type=text] {
- width: 100%;
- font-family: monospace;
- }
- #ilh-ui #ilh-regexp {
- margin-top: 4px;
- margin-left: 1px;
- }
- #ilh-ui textarea {
- width: 100%;
- background-color: lightyellow;
- overflow: auto;
- }
- #ilh-ui span {
- font-weight: bold;
- }
- </style>`);
- const uiHTML = `
- <div id="ilh-ui">
- <div>
- <span>Import mode:</span>
- <label>
- <input type="radio" name="importmode" value="list" checked>List</input>
- </label>
- <label>
- <input type="radio" name="importmode" value="ratings">Ratings</input>
- </label>
- </div>
- <textarea id="ilh-film-list" rows="7" cols="60" placeholder="Input titles or IMDb IDs and click Start"></textarea>
- <div>
- <input type="button" value="Start" id="ilh-start">
- <input type="button" value="Skip" id="ilh-skip">
- <input type="button" value="Retry" id="ilh-retry">
- <span>Remaining: <span id="ilh-films-remaining">0</span></span>
- </div>
- <div>
- <span>Current:</span>
- <input type="text" id="ilh-current-film" size="65"">
- </div>
- <div>
- <span>Regexp:</span>
- <input type="checkbox" id="ilh-matches-only" checked>
- <label for="ilh-matches-only">matches only (disable to search for film titles)</label>
- <input type="text" id="ilh-regexp" size="65">
- </div>
- <div id="ilh-import" style="display: none">
- <b>Import .csv from:</b>
- <select name="import" id="ilh-import-sel">
- <option value="" selected disabled hidden>Select</option>
- <option value="imdb">IMDb</option>
- <option value="rym">RateYourMusic</option>
- <option value="criticker">Criticker</option>
- </select>
- <span>File:</span>
- <input type="file" id="ilh-file-import" disabled>
- </div>
- </div>`;
- document.querySelector('div.lister-search').insertAdjacentHTML('afterend', uiHTML);
- const innerIDs = [
- 'film-list',
- 'start',
- 'skip',
- 'retry',
- 'films-remaining',
- 'current-film',
- 'regexp',
- 'matches-only',
- 'import',
- 'import-sel',
- 'file-import',
- ];
- const camelCase = s => s.replace(/-[a-z]/g, m => m[1].toUpperCase());
- // Main object for interacting with the script's UI; keys match element ids
- const ui = Object.assign(...innerIDs.map(id => ({
- [camelCase(id)]: document.getElementById(`ilh-${id}`),
- })));
- [ui.radioList, ui.radioRatings] = document.querySelectorAll('#ilh-ui input[name=importmode]');
- const elIMDbSearch = document.getElementById('add-to-list-search');
- const elIMDbResults = document.getElementById('add-to-list-search-results');
- // ----- HANDLERS AND ACTIONS -----
- const convertRating = n => Math.ceil(n / 10) || 1; // 0..100 -> 1..10
- // d3 skips a row if a row conversion function returns null
- const joinOrSkip = (...fields) => (fields.includes(undefined) ? null : fields.join(','));
- const rowConverters = {
- imdb: row => joinOrSkip(row['Your Rating'], row.Const),
- rym: row => joinOrSkip(row.Rating, row.Title),
- // .csv exported from Criticker have spaces between column names
- criticker: row => joinOrSkip(convertRating(+row.Score), row[' IMDB ID']),
- };
- const prepareImport = e => {
- const isLegalParser = Boolean(rowConverters[e.target.value]); // in case of html-js mismatch
- ui.fileImport.disabled = !isLegalParser;
- };
- const handleImport = e => {
- const format = ui.importSel.value;
- const reader = new FileReader();
- reader.onload = event => {
- const fileStr = event.target.result;
- ui.filmList.value = d3.csvParse(fileStr, rowConverters[format]).join('\n');
- };
- const [file] = e.target.files;
- reader.readAsText(file);
- };
- const RatingManager = {
- rating: 0,
- regex: '^([1-9]|10),(.*)$',
- processRegexMatch: ([, rating, filmTitle], callback) => {
- RatingManager.rating = rating;
- callback(filmTitle);
- },
- handleSelection: async (imdbID, callback) => {
- console.log(`RatingManager::handleSelection: Rating ${imdbID}`);
- const moviePage = await fetch(
- `http://www.imdb.com/title/${imdbID}/`,
- { credentials: 'same-origin' },
- );
- const authHash = new DOMParser()
- .parseFromString(await moviePage.text(), 'text/html')
- .getElementById('star-rating-widget')
- .dataset.auth;
- const params = {
- tconst: imdbID,
- rating: RatingManager.rating,
- auth: authHash,
- tracking_tag: 'list',
- };
- const postResp = await fetch('http://www.imdb.com/ratings/_ajax/title', {
- method: 'POST',
- body: new URLSearchParams(params),
- credentials: 'same-origin',
- });
- if (postResp.ok) {
- callback();
- } else {
- alert(`Rating failed. Status code ${postResp.status}`);
- }
- },
- };
- const ListManager = {
- regex: '((?:tt|nm)\\d+)',
- processRegexMatch: ([, filmTitle], callback) => callback(filmTitle),
- handleSelection: (imdbID, callback) => callback(),
- };
- const App = {
- manager: ListManager,
- films: [],
- regexObj: null,
- run: () => {
- ui.regexp.value = App.manager.regex;
- ui.importSel.addEventListener('change', prepareImport);
- ui.fileImport.addEventListener('change', handleImport);
- ui.radioList.addEventListener('change', () => {
- App.manager = ListManager;
- ui.import.style.display = 'none';
- ui.regexp.value = App.manager.regex;
- });
- ui.radioRatings.addEventListener('change', () => {
- App.manager = RatingManager;
- ui.import.style.display = 'block';
- ui.regexp.value = App.manager.regex;
- });
- // When start button is clicked
- ui.start.addEventListener('click', () => {
- App.regexObj = RegExp(ui.regexp.value || App.manager.regex, 'i');
- // Disable relevant UI elements
- [ui.filmList, ui.start, ui.regexp, ui.radioList, ui.radioRatings]
- .forEach(el => { el.disabled = true; });
- App.films = ui.filmList.value.trim().split('\n');
- App.handleNext();
- });
- // when skip button is clicked
- ui.skip.addEventListener('click', () => App.handleNext());
- // Sometimes the request fails forcing the user to skip an entry to continue
- ui.retry.addEventListener('click', () => {
- elIMDbSearch.dispatchEvent(new Event('keydown'));
- });
- },
- handleNext: () => {
- if (App.films.length) {
- App.search(App.films.shift());
- } else { // if last film
- App.reset();
- }
- },
- reset: () => {
- App.films = [];
- App.regexObj = null;
- [ui.filmList, ui.start, ui.regexp, ui.radioList, ui.radioRatings]
- .forEach(el => { el.disabled = false; });
- ui.currentFilm.value = '';
- elIMDbSearch.value = '';
- },
- search: filmTitle => {
- // Remove unnecessary whitespace
- filmTitle = filmTitle.trim();
- // Set current text to what we're searching
- ui.currentFilm.value = filmTitle;
- // Remove the first title from the text box and set the remaining number
- ui.filmsRemaining.textContent = App.films.length;
- ui.filmList.value = App.films.join('\n');
- // Run regex if it matches and let the manager process the result.
- // Otherwise, if "matches only" is unchecked,
- // try searching for the whole line (if it's not empty) before skipping it.
- const result = App.regexObj.exec(filmTitle) ||
- // eslint-disable-next-line no-sparse-arrays
- (!ui.matchesOnly.checked && filmTitle && [, filmTitle]);
- if (result) {
- App.manager.processRegexMatch(result, filmTitle2 => {
- // Set imdb search input field to film title and trigger search
- elIMDbSearch.value = filmTitle2;
- elIMDbSearch.dispatchEvent(new Event('keydown'));
- });
- } else {
- App.handleNext();
- }
- },
- };
- // Handle clicks on search results by a user or the script
- elIMDbResults.addEventListener('click', e => {
- const imdbID = e.target.closest('a').dataset.const;
- if (!imdbID || !imdbID.startsWith('tt')) return;
- App.manager.handleSelection(imdbID, () => {
- setTimeout(() => App.handleNext(), REQUEST_DELAY);
- });
- });
- // Monitor for changes to the search result box.
- // If the search was for IMDb URL/ID, the only result is clicked automatically
- const mut = new MutationObserver(mutList => mutList.forEach(({ addedNodes }) => {
- if (!addedNodes.length || !/(nm|tt)\d{7}/i.test(ui.currentFilm.value)) return;
- addedNodes[0].click();
- }));
- mut.observe(elIMDbResults, { childList: true });
- App.run();