IMDb List Helper

Makes creating IMDb lists more efficient and convenient

As of 2016-12-11. See the latest version.

// ==UserScript==
// @name           IMDb List Helper
// @namespace      imdb
// @description    Makes creating IMDb lists more efficient and convenient
// @version        2.3.0
// @include        http://*imdb.com/list/edit*
// @require        https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/0.71/jquery.csv-0.71.min.js
// ==/UserScript==

var jQuery = unsafeWindow.jQuery;
var $ = jQuery;

//              
// CHANGELOG
//
// 2.3.0
// bugfix: 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
// 

//
// milliseconds between each request
//
var REQUEST_DELAY = 1000;

function processImdb(file) {
    var csv_lines = file.split("\n");
    for(var i = 1; i < csv_lines.length; ++i) {
        try {
            var data = jQuery.csv.toArray(csv_lines[i]);
            var rating = data[8];
            if(rating === "") {
                continue;
            }
            
            $("#filmList").append(rating + "," + data[1] + "\n");
        }
        catch(e) {
            console.log("Exception: " + e);
            console.log("Bad line: " + csv_lines[i]);
        }
    }
}

function processRym(file) {
    var csv_lines = file.split("\n");
    for(var i = 1; i < csv_lines.length; ++i) {
        try {
            var data = jQuery.csv.toArray(csv_lines[i]);
            if(data.length < 6) {
                continue;
            }
            
            $("#filmList").append(data[4] + "," + data[1] + "\n");
        }
        catch(e) {
            console.log("Exception: " + e);
            console.log("Bad line: " + csv_lines[i]);
        }
    }
}

function processCriticker(file) {
    var lines = file.split("\n");
    for(var i = 1; i < lines.length; ++i) {
        var line = lines[i];
        var m = line.match(/([1-9]{1}|10)([0-9])?\t(.+)/);
        if(m === null) {
            continue;
        }
        
        var rating = m[1];
        var second = m[2];
        if(second === undefined) {
            // If second value is undefined the rating is < 10 (on criticker)
            // and will be changed to 1 because you can't rate 0 on IMDb
            rating = "1";
        }
        
        $("#filmList").append(rating + "," + m[3] + "\n");
    }
}

function handleImport(evt) {
    var files = evt.target.files;
    var file = files[0];
    var reader = new FileReader();
    reader.onload = function(event){
        var file = event.target.result;
        var format = $("select[name=import]").val();
        if(format === "none") {
            alert("Select importer and try again.");
            return;
        }
        else if(format === "imdb") {
            processImdb(file);
        }
        else if(format === "rym") {
            processRym(file);
        }
        else if(format === "criticker") {
            processCriticker(file);
        }
    }

    reader.readAsText(file);
}

var ListManager = {
    regex: "^(.*)$",
    processRegex: function(rx, cb) {
        var filmTitle = rx[1];        
        cb(filmTitle);
    },
    handleSelection: function(imdbId, cb) {
        cb();
    }
}; 

var RatingManager = {
    rating: 0,
    regex: "^([1-9]{1}|10),(.*)$",
    processRegex: function(rx, cb) {
        RatingManager.rating = rx[1];
        var filmTitle = rx[2];        
        cb(filmTitle);
    },
    handleSelection: function(imdbId, cb) {
        console.log("RatingManager::handleSelection: Rating " + imdbId);
        $.get("http://www.imdb.com/title/" + imdbId, function(data) {
            var authHash = $(data).find("#star-rating-widget").data("auth");
            var params = {tconst: imdbId, rating: RatingManager.rating,
                            auth: authHash, tracking_tag: "list",
                            pageId: imdbId, pageType: "title", subPageType: "main"};
            $.post("http://www.imdb.com/ratings/_ajax/title", params, function(data) {
                if(data.status !== 200) {
                    alert("Rating failed. Status code " + data.status);
                }
                else {
                    cb();
                }
            }, "json");
        });
    }
};

var App = {
    manager: ListManager,
    films: [],
    regexObj: null,
    isEmpty: function() {
        return App.films.length === 0;
    },
    next: function() {
        return App.films.shift();
    },
    run: function() {
        var textEdit = '<div id="imdbListTextEdit" style="'
                    + 'padding: 10px; border: 1px solid #e8e8e8">'
                    + '<p><b>Import mode:</b><input type="radio" name="importmode" value="list" checked="checked">List</input>'
                    + '<input type="radio" name="importmode" value="ratings">Ratings</input></p>'
                    + '<textarea id="filmList" rows="7" cols="60" placeholder="Input titles, IMDb URLs, and IDs here and click Start"></textarea><br />'
                    + '<input type="button" id="doList" value="Start" /> '
                    + '<input type="button" id="skipFilm" value="Skip" /> '
                    + '<input type="button" id="retryPost" value="Retry" /> '
                    + '<span style="font-weight: bold">Remaining: <span id="filmsRemaining">0</span></span><br /><br />'
                    + '<span style="font-weight: bold;">Current: <input type="text" id="filmCurrent" size="65" style="font-family: monospace" /></span><br />'
                    + '<span style="font-weight: bold;">Regexp: <input type="text" value="" id="filmRegexp" size="65" style="font-family: monospace; margin-top: 4px; margin-left: 1px" /></span><br />'
                    + '<p id="importform" style="display: none"><b>Import from:</b> <select name="import"><option value="none">Select</option><option value="imdb">IMDb</option>'
                    + '<option value="rym">RateYourMusic</option><option value="criticker">Criticker</option></select><b> File: </b><input type="file" id="fileimport"></p>'
                    + '</div>';    
        $("div#main > div.list_edit > div.add").after(textEdit);
        
        $("#filmRegexp").val(App.manager.regex);
        
        $("#fileimport").on("change", handleImport);
        
        $("#imdbListTextEdit").on("change", "input[name=importmode]", function() {
            var value = $(this).val();
            if(value === "list") {
                App.manager = ListManager;
                $("#importform").hide();
            }
            else {
                App.manager = RatingManager;
                $("#importform").show();
            }
            
            $("#filmRegexp").val(App.manager.regex);
        });
        
        // When start button is clicked
        $("#imdbListTextEdit").on("click", "#doList", function(e) {                    
            $regexBox = $("#filmRegexp");    
            if($regexBox.val()) {
                App.regexObj = RegExp($regexBox.val());
            }
            else {
                App.regexObj = RegExp(App.manager.regex);
            }
            
            // Disable the text area and the button and the regexp box
            // as well as the import mode
            $filmList = $("#filmList");
            $filmList.attr("disabled", "disabled");
            $regexBox.attr("disabled", "disabled");
            $("#doList").attr("disabled", "disabled");
            $("input[name=importmode]").attr("disabled", "disabled");
                
            App.films = $filmList.val().split("\n");           
            App.handleNext();    
        });
        
        // when skip button is clicked
        $("#imdbListTextEdit").on("click", "#skipFilm", function(e) {
            App.handleNext();
        });
        
        // Sometimes the request fails forcing the user to skip an entry to continue
        $("#imdbListTextEdit").on("click", "#retryPost", function(e) {
            $("button.search").trigger("click");
        });     
    },
    reset: function() {
        App.films = [];
        App.regexObj = null;
        
        $("#filmList").removeAttr("disabled");
        $("#filmRegexp").removeAttr("disabled"); // leave regex
        $("#doList").removeAttr("disabled");
        $("input[name=importmode]").removeAttr("disabled");
        
        $("#filmCurrent").val("");
        
        $("input[name=add]", "div.add").val("");
        $("div.results", "div.add").html(""); 
    },
    search: function(filmTitle) {
        // remove unnecessary whitespace
        filmTitle = $.trim(filmTitle);
        
        // set current text to what we're searching
        $("#filmCurrent").val(filmTitle);
        
        // remove the first title from the text box and set the remaining number
        $filmList = $("#filmList");    
        var newList = $filmList.val().split("\n");
        $("#filmsRemaining").text(newList.length-1);
        $filmList.val(newList.slice(1).join("\n"));
        
        // Run regex if it matches and let the manager process the result
        var result = App.regexObj.exec(filmTitle);
        if(result !== null) {
            App.manager.processRegex(result, function(filmTitle) {
                // Set imdb search input field to film title
                $("input[name=add]", "div.add").val(filmTitle);
                
                // Trigger search button click
                $("button.search").trigger("click");
            });
        }
        else {
            App.handleNext();
        }                
    },
    handleNext: function() {
        if(!App.isEmpty()) {
            // if there's more items, search next...
            App.search(App.next());
        }
        else {
            // if last film
            App.reset();
        }
    }
};

$(document).ready(App.run);

// When a search result item is clicked by user or script
$("div.results").on("click", "li", function(e) {    
    App.manager.handleSelection($(this).attr("id"), function() {
        App.handleNext();
    });
});

// Monitors for changes to the search result box
// If it recognizes an IMDBb URL/ID, it's clicked automatically
// since there's only one result
$("div.results").bind("DOMNodeInserted", function(e) {
    if($("#filmCurrent").val().match(/([CHMNTchmnt]{2}[0-9]{7})/) !== null
            && $("div.results").find("li").length) {
        setTimeout(function() {
            $("li", "div.results").first()[0].click();
        }, REQUEST_DELAY);
    }
});