AltoRetrato / IMDb 'My Movies' enhancer

// File encoding: UTF-8
//{
// This is a script for the IMDb site. It emphasize links to movies in your
// "My Movies" and "Vote History" lists. For instance, on an actor's page,
// you'll easily notice which of his/her movies you've already seen/voted.
//
// Copyright (c) 2008-2024, Ricardo Mendonça Ferreira (ric@mpcnet.com.br)
// Released under the GPL license - http://www.gnu.org/copyleft/gpl.html
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name          IMDb 'My Movies' enhancer
// @description   Emphasize the links for movies and people in your IMDb lists
// @namespace     http://www.flickr.com/photos/ricardo_ferreira/2502798105/
// @homepageURL   https://openuserjs.org/scripts/AltoRetrato/IMDb_My_Movies_enhancer
// @copyright     2008-2024, Ricardo Mendonca Ferreira
// @license       GPL-3.0-or-later
// @oujs:author   Ricardo
// @include       http://*.imdb.com/*
// @include       https://*.imdb.com/*
// @match         http://*.imdb.com/*
// @match         https://*.imdb.com/*
// @exclude       http://i.imdb.com/*
// @exclude       http://*imdb.com/images/*
// @exclude       http://*imdb.com/list/export*
// @exclude       http://*imdb.com/eyeblaster/*
// @exclude       https://i.imdb.com/*
// @exclude       https://*imdb.com/images/*
// @exclude       https://*imdb.com/list/export*
// @exclude       https://*imdb.com/eyeblaster/*
// @version       1.53
// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_addStyle
// @updateURL     https://openuserjs.org/meta/AltoRetrato/IMDb_My_Movies_enhancer.meta.js
// ==/UserScript==
//
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you either need Google Chrome (www.google.com/chrome)
// or Firefox (www.firefox.com) with Greasemonkey (www.greasespot.net).
// Install Greasemonkey, then restart Firefox and revisit this script.
//
// To uninstall, go to Tools/Greasemonkey/Manage User Scripts,
// select this script and click Uninstall.
//
// --------------------------------------------------------------------
//
// To-do:
//   - Add support for other types of lists
//   - Allow script configuration without source code changes
//   - Make it a Chrome / Firefox plugin
//   - Add more highlighting options
//
// History:
// --------
// 2024.06.04  [1.53] Fixed issue loading Watchlist and Check-Ins lists.
// 2024.06.03  [1.52] New IMDb layout, new script update.
// 2022.05.18  [1.51] New IMDb layout, new highlightTitle() & highlightLinks()
// 2021.06.18  [1.50] New release, just to fix auto update issue I might have caused in the previous version. Thanks, Martin! See https://openuserjs.org/scripts/AltoRetrato/IMDb_My_Movies_enhancer/issues/script_stopped_working_since_IMDB_new_Update
// 2021.06.09  [1.49] New IMDb layout, new highlightTitle()
// 2020.02.28  [1.48] Fixed bug where script won't work unless user has custom lists. Thanks, alienfoil!
// 2019.11.22  [1.47] New IMDb layout, new getCurrentUser()
// 2018.09.03  [1.46] New IMDb layout, new highlightTitle()
// 2018.07.10  [1.45] Better error messages & progress bar handling during errors, specially for those who never used check-ins.
// 2018.07.05  [1.44] 10 year anniversary edition! :D
//                    Fixed bug from v. 1.43 where movie titles and people names were getting mixed.
//                    New default highlight color for lists of people.
//                    New & improved way to set color priority (search for listOrder).
//                    Now supporting check-ins list.
// 2018.07.03  [1.43] Added support for lists of people - but since I don't use this feature, I'll only see problems via bug reports.
// 2018.02.16  [1.42] Included support for "https" protocol. There is one local storage for each scheme,
//                    so stick to either http or https browsing to avoid having to download your data twice.
// 2018.01.19  [1.41] New IMDb layout "removed" script buttons, this update fixes it.
// 2017.12.19  [1.40] IMDb FINALLY produces correct CSV files (it took them only a few years to fix this)!
//                    There's also a new "Your Lists" page layout, so a new version of the script is required.
//                    Now two steps are required to fetch the watchlist.
// 2017.03.16  [1.39] New IMDb layout, new highlightTitle()
// 2016.01.27  [1.38] New IMDb layout, new highlightTitle()
// 2014.12.19  [1.37] Work around IMDb CSV bug: https://openuserjs.org/scripts/Ricardo/IMDb_My_Movies_enhancer/issues/Script_doesnt_parse_movies_with_character_in_the_title
// 2014.10.31  [1.36] New script hosting (OpenUserJs.org); small fixes
// 2013.10.05  [1.35] Fixed bug where script buttons might not show up sometimes
// 2013.09.21  [1.33] Experimental: downloading all lists at once! (http://userscripts.org/topics/131873)
// 2013.09.20  [1.32] Fixed another change in the IMDb site (http://userscripts.org/topics/126010?page=2)
// 2013.05.31  [1.31] Adding some more error checking
// 2013.05.29  [1.30] Fixed bug in the "tabs" sections (trivia, connections, etc.)
// 2013.05.27  [1.29] Working again after changes on IMDb site; fixed tooltip bug
//                    that could affect navigation (thanks, somini!); fixed tooltip bug where
//                    the tooltip would appear behind another element; highlighting works
//                    on the main page of a title
// 2012.12.06  [1.28] Sorry... fixed a bug introduced in the previous bugfix. :P
// 2012.12.06  [1.27] Changed versioning model (since it wasn't working correctly on Chrome),
//                    fixed small bug where search results were not being highlighted.
// 2012.12.06  [1.26] Fix for IMDb site change, correctly shows how many lists it will load,
//                    should also work with dynamic loaded links and images
// 2011.10.06  [1.25] Workaround fixed Opera regex memory leak; Added code to give
//                    "color priority" for a specific list (see movieColor function)
// 2011.10.05  [1.24] Fixed bug where movie data was not captured if there were no votes for it;
//                    Small changes to try to fix possible memory leaks on Opera
// 2011.09.19  [1.23] Fixed & improved code to handle ratings, now should always show the
//                    correct rating for a movie in the tooltips
// 2011.09.17  [1.22] Small bugfixes; made it work again on Google Chrome
// 2011.09.14  [1.21] Works on xx.imdb.com; made it easier to support movies lists in
//                    languages other than English (if/when they are available)
// 2011.09.09  [1.20] To disable color highlighting for a list just remove its customColors
//                    entry, or make the color = ""; Now compatible with N900 again; Shows
//                    both your rating & IMDb rating in the tooltip
// 2011.09.07  [1.19] Fix for IMDb change in the format of the export link
// 2011.09.06  [1.18] Fixed bug where movies were still considered in a list when they were not
// 2011.09.04  [1.17] Using dhtml tooltips; added tooltip to movie titles;
// 2011.09.02  [1.16] Get lists id with new regex to avoid conflict with other scripts;
//                    Show in the link title all lists a movie is in;
//                    Enable custom lists colors by changing the script code (look for customColors)
// 2011.08.27c [1.15] Automatically reload page when changed sort/view
// 2011.08.27b [1.14] Don't stop downloads if can't find movies in a list; ignore lists not about titles
// 2011.08.27  [1.13] Slightly better handling of download errors
// 2011.08.26d [1.12] Fourth updade in a day! Now sorting option is user selectable
// 2011.08.26c [1.11] Guess what? Now IMDb enabled list configuration... it's not working on all lists, though... XP
// 2011.08.26b [1.10] Less than one hour after uploading this script IMDB changed a few features again! :P
// 2011.08.26  [ 1.9] Changed how lists are displayed by default; allows manual update of information
// 2011.08.13  [ 1.8] Working with new list design, using localStorage instead of GM_*Value
// 2010.06.17  [ 1.7] Added functions "missing" from Chrome; thanks, ode!
// 2009.09.23  [ 1.6] Fix for another site redesign
// 2009.08.12  [ 1.5] Restored code to deal with links like those on http://www.imdb.com/Sections/Genres/Sci-Fi/average-vote
// 2009.07.28  [ 1.4] Fix for IMDb site change, added debug information, exclude running on image URLs
// 2008.08.27  [ 1.3] Explicitly send cookies (FF3 compatibility fix)
// 2008.07.27  [ 1.2] Fixed bug where removed movies where not actually removed;
//                    now also highlight the title of the movies
// 2008.06.11  [ 1.1] Fixed bug that ketp growing the movie data in Firefox;
//                    now also get the vote history
// 2008.05.18  [ 1.0] First public release
// 2008.05.12  [ 0.1] First test version, private use only
//
//}


(function() {

    var myName = 'IMDb "My Movies" Enhancer v. 1.53 (2024.06.04)'; // Name & version of this script
    var myHome = 'https://openuserjs.org/scripts/AltoRetrato/IMDb_My_Movies_enhancer';
 
    // Greasemonkey "clone functions" for Chrome (& others?), from http://userscripts.org/topics/41177
    // (thanks to ode: http://userscripts.org/topics/52056?page=1#posts-261313)
    // @copyright 2009, 2010 James Campos
    // @license cc-by-3.0; http://creativecommons.org/licenses/by/3.0/
    //
    // See also:
    //
    // https://userscripts.org/scripts/source/145813.user.js
    if (typeof GM_getValue == 'undefined') {
       GM_addStyle = function(css) {
          var style = document.createElement('style');
          style.textContent = css;
          document.getElementsByTagName('head')[0].appendChild(style);
       };
 
       GM_getValue = function(name, defaultValue) {
          var value = localStorage.getItem(name);
          if (!value)
             return defaultValue;
          var type = value[0];
          value = value.substring(1);
          switch (type) {
             case 'b':
                return value == 'true';
             case 'n':
                return Number(value);
             default:
                return value;
          }
       };
 
       GM_setValue = function(name, value) {
          value = (typeof value)[0] + value;
          localStorage.setItem(name, value);
       };
    }
    //----------------- End of Greasemonkey clone functions -----------------
 
    // Modified version of Michael Leigeber's code, from:
    // http://sixrevisions.com/tutorials/javascript_tutorial/create_lightweight_javascript_tooltip/
    // http://userscripts.org/scripts/review/91851 & others
    var injectJs = 'function tooltipClass(msg) {this.msg = msg;this.id = "tt";this.top = 3;this.left = 15;this.maxw = 500;this.speed = 10;this.timer = 20;this.endalpha = 95;this.alpha = 0;this.tt == null;this.c;this.h = 0;this.moveFunc = null;this.fade = function (d) {var a = this.alpha;if (a != this.endalpha && d == 1 || a != 0 && d == -1) {var i = this.speed;if (this.endalpha - a < this.speed && d == 1) {i = this.endalpha - a;} else if (this.alpha < this.speed && d == -1) {i = a;}this.alpha = a + i * d;this.tt.style.opacity = this.alpha * 0.01;} else {clearInterval(this.tt.timer);if (d == -1) {this.tt.style.display="none";document.removeEventListener("mousemove", this.moveFunc, false);this.tt = null;}}};this.pos = function (e, inst) {inst.tt.style.top = e.pageY - inst.h + "px";inst.tt.style.left = e.pageX + inst.left + "px";};this.show = function (msg) {if (this.tt == null) {this.tt = document.createElement("div");this.tt.setAttribute("id", this.id);c = document.createElement("div");c.setAttribute("id", this.id + "cont");this.tt.appendChild(c);document.body.appendChild(this.tt);this.tt.style.opacity = 0; this.tt.style.zIndex=100000; var inst = this;this.moveFunc = function (e) {inst.pos(e, inst);};document.addEventListener("mousemove", this.moveFunc, false);}this.tt.style.display = "block";c.innerHTML = msg || this.msg;this.tt.style.width = "auto";if (this.tt.offsetWidth > this.maxw) {this.tt.style.width = this.maxw + "px";}h = parseInt(this.tt.offsetHeight) + this.top;clearInterval(this.tt.timer);var inst = this;this.tt.timer = setInterval(function () {inst.fade(1);}, this.timer);};this.hide = function () {if (this.tt) {clearInterval(this.tt.timer);var inst = this;this.tt.timer = setInterval(function () {inst.fade(-1);}, this.timer);}};} tooltip = new tooltipClass("default txt");';
 
    var newJs = document.createElement('script');
    newJs.setAttribute('type', 'text/javascript');
    newJs.innerHTML = injectJs;
    document.getElementsByTagName('head')[0].appendChild(newJs);
 
    var user   = '';      // Current user name/alias
    var interval = 1000;  // Interval (in ms, >= 100) to re-scan links in the DOM
                          // Won't re-scan if < 100
                          // (I might consider using MutationObserver in the future, instead)
 
    function getCurrentUser() {
       //
       // Return name of user currently logged on IMDb (log on console if failed)
       //
       var loggedIn = '';
       var account = document.getElementById('consumer_user_nav') ||
                     document.getElementById('nbpersonalize');
       if (account) {
          var                 result = account.getElementsByTagName('strong');
          if (!result.length) result = account.getElementsByClassName("navCategory");
          if (!result.length) result = account.getElementsByClassName("singleLine");
          if (!result.length) result = account.getElementsByTagName("p");
          if (result)
             loggedIn = result[0].textContent.trim();
       } else {
          var result = document.getElementsByClassName("navbar__user-menu-toggle__name");
          if (result)
             loggedIn = result[0].textContent.trim();
       }
       if (!loggedIn)
          console.error(document.URL + "\nUser not logged in (or couldn't get user info)"); // responseDetails.responseText
       return loggedIn;
    }
 
    const WATCHLIST  = "watchlist";
    const RATINGLIST = "ratings";
    const CHECKINS   = "checkins";
 
    const TITLES = "Titles";
    const PEOPLE = "People";
    const IMAGES = "Images";
    const VIDEOS = "Videos";
    // Lists can be about Titles, People, Images & Videos (no Characters lists anymore?)
    // Comment out a list type to disable highlighting for it.
    var listTypes = {};
    listTypes[TITLES] = true;
    listTypes[PEOPLE] = true;
    //listTypes[IMAGES] = true; // To-do: highlight images using colored borders?
    //listTypes[VIDEOS] = true; // To-do: highlight videos using colored borders?
 
    var listOrderIdx = [];
 
    var myLists = [];
 
    // myLists is our main data structure. It's a list, and here's the description of the 1st entry:
    // myLists[0].name  == "Your Watchlist" -> Name of the list
    // myLists[0].id    == "watchlist"      -> "id" of the list
    // myLists[0].color == "DarkGoldenRod"  -> color used to highlight movies in this list
    // myLists[0].type  == TITLES           -> type of entries in this list
    // myLists[0].ids["1yjf"].m == "10"     -> my rating
    // myLists[0].ids["1yjf"].i == "6.6"    -> IMDB rating
    // "1yjf" = movie number (e.g., "tt0091419") "encoded in base 36"
 
                // To see some color names and codes, visit:
                //    https://www.w3schools.com/colors/colors_hex.asp
 
    function getMyLists() {
       //
       // Get all lists (name & id) for current user into myLists array
       // and set default colors for them (if not previously defined)
       //
 
       // You can customize your lists colors.
       // See also the listOrder variable below.
       // After any change in the code: save the script, reload the lists page,
       // clear the highlight data and refresh the highlight data!
       var customColors = [];
       customColors["Your Watchlist"] = "DarkGoldenRod";
       customColors["Your ratings"  ] = "Green";
       customColors["Your check-ins"] = "DarkGreen";
       customColors["DefaultColor"  ] = "DarkCyan";
       customColors["DefaultPeople" ] = "DarkMagenta";
       customColors["Filmes Netflix Brasil"] = "Red";
 
       // You can set the search order for the highlight color when a title is in multiple lists.
       // The script will choose the color of the the first list found in the variable below.
       // Uncomment the line below and enter the names of any lists you want to give preference over the others.
       var listOrder = ["Your Watchlist", "Your ratings"];
 
       myLists.length = 0; // Clear arrays and insert the two defaults
       myLists.push({"name":"Your Watchlist", "id":WATCHLIST,  "color":customColors["Your Watchlist"] || "", "ids":{}, "type":TITLES });
       myLists.push({"name":"Your ratings",   "id":RATINGLIST, "color":customColors["Your ratings"]   || "", "ids":{}, "type":TITLES });
       myLists.push({"name":"Your check-ins", "id":CHECKINS,   "color":customColors["Your check-ins"] || "", "ids":{}, "type":TITLES });
       
       /*
         Try to fetch user's custom lists. This code can easily break when IMDb changes its page layout / format...
         Data I want to get from HTML: list id, list name, list size, list type, public/private, modified date.
         raw HTML = <a class="ipc-metadata-list-summary-item__t" role="button" tabindex="0" aria-disabled="false" href="/list/ls003658871/?ref_=uspf_t_3">Filmes Netflix Brasil</a><ul class="ipc-inline-list ipc-inline-list--show-dividers ipc-inline-list--no-wrap ipc-inline-list--inline ipc-metadata-list-summary-item__tl base" role="presentation"><li role="presentation" class="ipc-inline-list__item"><span class="ipc-metadata-list-summary-item__li" aria-disabled="false">4314 titles</span></li></ul><ul class="ipc-inline-list ipc-inline-list--show-dividers ipc-inline-list--no-wrap ipc-inline-list--inline ipc-metadata-list-summary-item__stl base" role="presentation"><li role="presentation" class="ipc-inline-list__item"><span class="ipc-metadata-list-summary-item__li" aria-disabled="false">Public</span></li><li role="presentation" class="ipc-inline-list__item"><span class="ipc-metadata-list-summary-item__li" aria-disabled="false">Modified Dec 17, 2020</
         reduced  = "/list/ls003658871/?ref_=uspf_t_3">Filmes Netflix Brasil<…<span…>4314 titles<…<ul…><li…><span…>Public</…<li…><span…>Modified Dec 17, 2020</
       */
       const regex = /"\/list\/([^"]+)\/[^>]*>([^<]+)<.{0,500}?>(\d+) (people|person|title|image|video).{0,500}?(Public|Private).{0,500}?Modified (.+?)</g;
       const matches = document.body.innerHTML.matchAll(regex);
       var numListsFound = 0;
       const listTypeMap = {
         "person": PEOPLE,
         "people": PEOPLE,
         "title": TITLES,
         "image": IMAGES,
         "video": VIDEOS
       };
       for (const match of matches) {
         numListsFound += 1;
         const listId     = match[1]; // "ls003658871"
         const listName   = match[2]; // "Filmes Netflix Brasil"
         const listSize   = match[3]; // "4314"
         var   listType   = match[4]; // "title" (must convert to "Titles")
         const listPublic = match[5]; // "Public"
         const listDate   = match[6]; // "Dec 17, 2020"
         if (listType in listTypeMap)
             listType = listTypeMap[listType];
         //console.log({ listId, listName, listSize, listType, listPublic, listDate });
         const colorType = listType == PEOPLE ? "DefaultPeople" : "DefaultColor";
         const color     = customColors[listName] || customColors[colorType] || "";
         myLists.push({"name":listName, "id":listId, "color":color, "ids":{}, "type":listType });
       }
       if (numListsFound == 0) {
          console.error("Error getting lists (or no lists exist)!");
          //return false;
       }
       setListOrder(listOrder);
       return true;
    }
 
    function loadMyLists() {
       //
       // Load data for the current user
       //
       var userData = localStorage.getItem("myMovies-"+user);
       if (userData) {
          try {
             myLists = JSON.parse(userData);
             if ("myLists" in myLists) {
                listOrderIdx = myLists["listOrder"];
                myLists      = myLists["myLists"  ];
             }
             return true;
          } catch(err) {
             alert("Error loading previous data!\n" + err.message);
          }
       }
       return false;
    }
 
    function saveMyLists() {
       //
       // Save data for the current user
       //
       var userData = {"listOrder": listOrderIdx, "myLists": myLists};
           userData = JSON.stringify(userData);
       localStorage.setItem("myMovies-"+user, userData);
    }
 
    function eraseMyData() {
       //
       // Erase just the movies and lists information for the user
       //
       localStorage.removeItem("myMovies-"+user);
       for (var i = 0; i < myLists.length; i++)
          myLists[i].ids = {};
    }
 
    function parseCSV(str) {
       // Simple CSV parsing function, by Trevor Dixon:
       // https://stackoverflow.com/questions/1293147/javascript-code-to-parse-csv-data
       var arr = [];
       var quote = false;  // true means we're inside a quoted field
 
       // iterate over each character, keep track of current row and column (of the returned array)
       var row, col, c;
       for (row = col = c = 0; c < str.length; c++) {
          var cc = str[c], nc = str[c+1];        // current character, next character
          arr[row] = arr[row] || [];             // create a new row if necessary
          arr[row][col] = arr[row][col] || '';   // create a new column (start with empty string) if necessary
 
          // If the current character is a quotation mark, and we're inside a
          // quoted field, and the next character is also a quotation mark,
          // add a quotation mark to the current column and skip the next character
          if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
 
          // If it's just one quotation mark, begin/end quoted field
          if (cc == '"') { quote = !quote; continue; }
 
          // If it's a comma and we're not in a quoted field, move on to the next column
          if (cc == ',' && !quote) { ++col; continue; }
 
          // If it's a newline (CRLF) and we're not in a quoted field, skip the next character
          // and move on to the next row and move to column 0 of that new row
          if (cc == '\r' && nc == '\n' && !quote) { ++row; col = 0; ++c; continue; }
 
          // If it's a newline (LF or CR) and we're not in a quoted field,
          // move on to the next row and move to column 0 of that new row
          if (cc == '\n' && !quote) { ++row; col = 0; continue; }
          if (cc == '\r' && !quote) { ++row; col = 0; continue; }
 
          // Otherwise, append the current character to the current column
          arr[row][col] += cc;
       }
       return arr;
    }
 
    var downloadedLists = 0;
    var listsNotDownloaded = [];
 
 
    function advanceProgressBar() {
       //
       // Update progress bar
       //
       downloadedLists += 1;
       var total = myLists.length;
       var p = Math.round(downloadedLists*(100/total));
       updateProgressBar(p, "Loaded "+downloadedLists+"/"+total);
       if (downloadedLists >= total) {
          updateProgressBar(0, "");
          if (listsNotDownloaded.length > 0) {
             var msg = "Done, but could not load list(s):";
             listsNotDownloaded.forEach(function(l) { msg += "\n * " + l;} );
             msg += "\n\nThis script can only read public lists.";
             alert(msg);
          } else
             alert("OK, we're done!");
       }
    }
 
    function downloadOK(idx, request, link) {
       //
       // Process a downloaded list
       //
       if (request.status != 200) {
           console.error("Error "+request.status+" downloading "+link+": " + request.statusText);
       } else
       if (request.responseText.indexOf("<!DOCTYPE html") >= 0) {
          console.error("Received HTML instead of CSV file from "+link);
       } else {
          var data = parseCSV(request.responseText);
          var res, entryCode;
          var fields = {};
          var type   = myLists[idx].type;
          for (var i=1; i < data.length; i++) {
             if (type == TITLES) {
                //            ___0___   _____1_____  ____2_____  ___3____  _____4_____  ____5_____  _____6_____  ______7_______  _____8_____  _______9______  ____10___  _____11_____  ___12____   _____13_____  ____14____
                // ratings  : Const,    Your Rating, Date Added, Title,    URL,         Title Type, IMDb Rating, Runtime (mins), Year,        Genres,         Num Votes, Release Date, Directors
                // others   : Position, Const,       Created,    Modified, Description, Title,      URL,         Title Type,     IMDb Rating, Runtime (mins), Year,      Genres,       Num Votes,  Release Date,  Directors
                for (var f=0; f < data[0].length; f++)
                   { fields[data[0][f]] = data[i][f]; }
                var tt = fields["Const"];
                var ratingMine = fields["Your Rating"];
                var ratingIMDb = fields["IMDb Rating"];
                if (typeof tt === "undefined")   console.error("Error processing line "+i+" of "+idx);
                else if (tt.substr(0,2) != 'tt') console.error("Error getting IMDb const from: "+data[i]);
                else {
                   var ttNum = parseInt(tt.substr(2));
                   // Encode the movie number with "base 36" to save memory
                   entryCode = ttNum.toString(36);
                   myLists[idx].ids[entryCode] = {m:ratingMine, i:ratingIMDb};
                }
             } else if (type == PEOPLE) {
                // ___0___   __1__  ___2___  ___3____  _____4_____  __5__  ____6____  ____7_____
                // Position, Const, Created, Modified, Description, Name,  Known For, Birth Date
                for (var f=0; f < data[0].length; f++)
                   { fields[data[0][f]] = data[i][f]; }
                var nm   = fields["Const"];
              //var name = fields["Name"];
                if (typeof nm === "undefined")   console.error("Error processing line "+i+" of "+idx);
                else if (nm.substr(0,2) != 'nm') console.error("Error getting IMDb const from: "+data[i]);
                else {
                   var nmNum = parseInt(nm.substr(2));
                   // Encode the entry with "base 36" to save memory
                   entryCode = nmNum.toString(36);
                 //myLists[idx].ids[entryCode] = {n: name};
                   myLists[idx].ids[entryCode] = {};
                }
             } else if (type == IMAGES) {
                // Do nothing for now
             }
          }
          // Save data into browser
          saveMyLists();
       }
 
       advanceProgressBar() ;
 
       // Try to free some memory
       delete request.responseText;
    }
 
    var createFunction = function( func, p1, p2, p3 ) {
       return function() {
          func(p1, p2, p3);
       };
    };
 
    function downloadError(name, request, link) {
       //
       // Alert user about a download error
       //
       var msg = "Error downloading your list "+name+":\n"+
                 "Status: "  +request.status + " - " + request.statusText +":\n"+
                 "Source: "  +link +"\n" +
                 "Headers: " +request.getAllResponseHeaders();
       alert(msg);
       console.error(msg);
       updateProgressBar(0, "");
    }
 
    function downloadAsync(name, idx, exportLink) {
       var request = new XMLHttpRequest();
       request.onload  = createFunction(downloadOK,     idx, request, exportLink);
       request.onerror = createFunction(downloadError, name, request, exportLink);
       request.open("GET", exportLink, true);
     //request.setRequestHeader("Accept-Encoding","gzip"); // Browser does this already? (I get 'Refused to set unsafe header "Accept-Encoding"')...
       request.send();
    }
 
    function downloadAsyncWatchlist(name, idx, url) {
       var request = new XMLHttpRequest();
       request.onload  = function() {
          var exportLink;
          var id = request.responseText.match('<meta property="pageId" content="(ls.+?)"/>') ||
                   request.responseText.match('"list":{"id":"(ls.+?)"') ||
                   request.responseText.match('"listId":"(ls.+?)"');
          if (id) {
             exportLink = document.location.protocol + "//www.imdb.com/list/"+id[1]+"/export";
             downloadAsync(name, idx, exportLink);
          } else {
             console.error("Could not find id of the '"+name+"' list! Try to make it public (you can make it private again right after).");
             listsNotDownloaded.push(name);
             advanceProgressBar();
          }
       };
       request.onerror = createFunction(downloadError, name, request, url);
       request.open("GET", url, true);
       request.send();
    }
 
    function downloadList(idx) {
       //
       // Download a list
       //
       var ur = document.location.pathname.match(/\/(ur\d+)/);
       if (ur && ur[1])
          ur = ur[1];
       else {
          alert("Sorry, but I could not find your user ID (required to download your lists). :(");
          return;
       }
 
       var name = myLists[idx].name;
       var id   = myLists[idx].id;
       // Watchlist & check-ins are not easily available (requires another fetch to find export link)
       // http://www.imdb.com/user/ur???????/watchlist/export                   | shows old HTML format
       // http://www.imdb.com/list/export?list_id=watchlist&author_id=ur??????? | 404 error
       // http://www.imdb.com/user/ur???????/watchlist                          | HTML page w/ "export link" at the bottom
       if (id == WATCHLIST || id == CHECKINS) {
          var url = document.location.protocol + "//www.imdb.com/user/"+ur+"/"+id;
          downloadAsyncWatchlist(name, idx, url);
       } else {
          var exportLink;
          if (id == RATINGLIST)
               exportLink = document.location.protocol + "//www.imdb.com/user/"+ur+"/"+id+"/export";
          else exportLink = document.location.protocol + "//www.imdb.com/list/"+id+"/export";
          downloadAsync(name, idx, exportLink);
       }
    }
 
    function downloadLists() {
       //
       // Begin to download all user lists at once (asynchronously)
       //
       downloadedLists = 0;
       for (var idx=0; idx < myLists.length; idx++)
          downloadList(idx);
       // With 10.000 items in 5 lists, the approx. time to download them (on Chrome 29) was:
       //  -  synchronously: 1:50s
       //  - asynchronously:   30s
       // Results might vary - a lot! - depending on number of lists and browser
       // Connections per hostname seems to be around 6: http://www.browserscope.org/?category=network&v=top
    }
 
    // Really simple progress bar...
    var pb;
    var pbBox;
    var pbTxt;
 
    function createProgressBar(p, msg) {
       var top_  = Math.round(window.innerHeight / 2)  -15;
       var left  = Math.round(window.innerWidth  / 2) -100;
       pbBox = document.createElement('div');
       pbBox.style.cssText  = "background-color: white; border: 2px solid black; "+
          "position: fixed; height: 30px; width: 200px; top: "+top_+"px; left: "+left+"px;";
       document.body.appendChild(pbBox);
 
       pb = document.createElement('div');
       pb.style.cssText = "background-color: green; border: none; height: 100%; width: "+p+"%;";
       pbBox.appendChild(pb);
 
       pbTxt = document.createElement('div');
       pbTxt.textContent   = msg;
       pbTxt.style.cssText = "text-align: center; margin-top: -25px; font-family: verdana,sans-serif;";
       pbBox.appendChild(pbTxt);
    }
 
    function updateProgressBar(p, msg) {
       if (p <= 0) {
          pbBox.style.display = "none";
          return;
       }
       pbTxt.textContent = msg;
       pb.style.width    = p+"%";
    }
 
 
    function setListOrder(listOrder) {
       //
       // Set color highlight order using lists indices, after variable listOrder (containing lists names).
       //
       if (typeof listOrder == "undefined")
          listOrder = []; // array of lists names
 
       listOrderIdx = []; // array of lists indices
 
       // First add indices set by user in listOrder
       for (var j = 0; j < listOrder.length; j++)
          for (var i = 0; i < myLists.length; i++)
             if (myLists[i].name == listOrder[j]) {
                listOrderIdx.push(i);
                break;
             }
       // Add remaining indices
       for (var i = 0; i < myLists.length; i++)
          if (!listOrderIdx.includes(i))
             listOrderIdx.push(i);
    }
 
 
    function movieColor(num, type) {
       //
       // Receives an IMDb movie code & list type, return the highlight color (if any).
       // It will return the color for the first list where the file is found.
       // Argument "num": movie number encoded in base 36
       //
 
       for (var j = 0; j < listOrderIdx.length; j++) {
          var i = listOrderIdx[j];
          if (myLists[i].type == type && myLists[i].ids[num])
             if (myLists[i].color)
                return myLists[i].color;
       }
 
       return "";
    }
    function inLists(num, type) {
       //
       // Receives an IMDb code and return the names of lists containing it.
       // Argument "num" : entry number encoded in base 36
       // Argument "type": optional, if set, limits search to a specific type of list
       //
       var num_l = 0;
       var lists = "";
       var pos   = -1;
       var rated = false;
       var imdbRating = "";
       var header     = "";
       var movie, name;
       for (var i = 0; i < myLists.length; i++) {
          if (type && myLists[i].type != type)
             continue;
          movie = myLists[i].ids[num];
          if (movie) {
             if (num_l)
                lists += "<br>";
             name = myLists[i].name;
             imdbRating = movie.i;
             if (imdbRating && name == "Your ratings") {
                name = "Your ratings: " + movie.m + " (IMDb: " + imdbRating + ")";
                rated = true;
             }
             lists += name;
             num_l += 1;
          }
       }
       if (imdbRating && !rated)
            imdbRating = "IMDb rating: " + imdbRating + "<br>";
       else imdbRating = "";
       if (num_l == 1)
            header = "<b>In your list:</b><br>";
       else header = "<b>In "+num_l+" of your lists:</b><br>";
 
       return imdbRating + header + '<div style="margin-left: 15px">' + lists + '</div>';
    }
 
    function addTooltip(obj, txt) {
       txt = txt.replace(/'/g, '"');
       obj.setAttribute("onmouseover", "tooltip.show('"+txt+"');");
         obj.setAttribute("onmouseout",  "tooltip.hide();");
    }
 
    function addTooltipStyle() {
       // Tooltips stuff
       GM_addStyle("#tt {position:absolute; display:block;} " +
                   "#ttcont {display:block; padding:2px 12px 3px 7px; margin-left:5px; background:#666; color:#FFF; font:11px/1.5 Verdana, Arial, Helvetica, sans-serif;}");
    }
 
    function highlightTitle() {
       //
       // Highlight title in the current page
       //
       var entry;
       var type;
       const isMovie = document.location.href.match(/tt[0]*(\d+)\//);
       const isName  = document.location.href.match(/nm[0]*(\d+)\//);
       if (isMovie) {
          entry = isMovie;
          type  = TITLES;
       } else if (isName) {
          entry = isName;
          type  = PEOPLE;
       }
       if (entry && type in listTypes) {
          var num = parseInt(entry[1]).toString(36);
          var color = movieColor(num, type);
          var lists = inLists(num, type);
          if (color) {
             var title = document.querySelector("div.title_wrapper > h1");
             if (!title)
                title = document.querySelector("div[class*=TitleBlock] > h1");
             if (!title)
                title = document.querySelector("h1 > span");
             if (!title)
                title = document.querySelector("h1");
             if (title) {
                title.style.color = color;
                addTooltip(title, lists);
             }
          }
       }
    }
 
    var lastAnchors;
 
    function highlightLinks() {
       //
       // Highlight all links in the current page for an IMDb movie page
       //
 
       var m, num, color, lists, movie, name, type;
       const anchors = document.getElementsByTagName('a');
       //if (anchors.length == lastAnchors) return;
 
       for (var i=0; i < anchors.length; i++) {
          var a = anchors[i];
          if (a.imdbE === undefined) {
             a.imdbE = false; // set to "true" when "enhanced" (so we skip it on next pass)
             movie = (a.href.indexOf("/tt") >= 0) || (a.href.indexOf("/Title") >= 0) &&
                     a.href.indexOf("tt_moviemeter_why") == -1;
             name  = a.href.indexOf("/name/nm") >= 0;
             type  = false;
             if (movie) {
                type = TITLES;
                //m = a.href.match(/tt[0]*(\d+)\/(.?)/);
                m = a.href.match(/tt[0]*(\d+)\/?(.?)/);
                if (!m) {
                   m = a.href.match(/imdb\..{2,3}\/Title\?[0]*(\d+)$/);
                   // http://www.imdb.com/Title?0266543
                   if (!m) continue;
                }
                if (m.length >= 3 && m[2] !== undefined && m[2] != "?" && m[2] !== "")
                   continue;
             } else if (name) {
                type = PEOPLE;
                // To-do (maybe): skip some links (e.g., quotes, trivia, ...)
                // if (a.href.indexOf("/#writer"  )) >= 0) continue;
                // if (a.href.indexOf("/#director")) >= 0) continue;
                // if (a.href.indexOf("/publicity")) >= 0) continue;
                m = a.href.match(/nm[0]*(\d+)\/?(.?)/);
             }
             if (type != false) {
                // I "encode" the movie number with "base 36" to save memory
                num   = parseInt(m[1]).toString(36);
                color = movieColor(num, type);
                lists = inLists(num, type);
                if (color) {
                   a.style.fontWeight = "bold";
                   a.style.color      = color;
                 //a.style.fontStyle  = "italic";
                   addTooltip(a, lists);
                   a.imdbE = true;
                }
                // Highlight titles & names in search results preview
                searchResultTitle = a.querySelector("div[class*=searchResults]");
                if (!searchResultTitle)
                   searchResultTitle = a.querySelector("div[class*=searchResult__constTitle]");
                if (searchResultTitle) {
                   searchResultTitle.style.color = color;
                }
             }
          }
       }
       lastAnchors = anchors.length;
    }
 
    function refreshMovieData() {
       alert(myName+"\n\n"+user+", I'll get some info from IMDb to be able to highlight your movies,\nplease click [OK] and wait a bit...");
       eraseMyData();
       createProgressBar(0, "Loading 1/"+myLists.length+"...");
       downloadLists();
    }
 
    var btn1; // refresh
    var btn2; // clear
    var btn4; // help
 
    function btnRefresh() {
       refreshMovieData();
    }
 
    function btnClear() {
       eraseMyData();
       alert(myName+"\n\nDone! Information cleared, so highlighting is now disabled.");
       window.location.reload();
    }
 
    function btnHelp () {
       alert(myName+"\n\nThis is a user script that:\n"+
             " • highlights links for entries in your lists (e.g., movies, series & people)\n"+
             " • shows in which of your lists an entry is (in a tooltip)\n"+
             "\nIn order to highlight the entries "+
             "in all IMDb pages as fast as possible, we need to download "+
             "the data from your lists into your browser. Unfortunately " +
             "this can be slow, so it is not done automatically. I suggest "+
             "you to update this information at most once a day.\n\n" +
             "[Refresh highlight data] updates the data in your browser.\n" +
             "[Clear highlight data] disables color highlighting.\n" +
             "\nFor more information and updates, visit " + myHome
       );
    }
 
    function addBtn(div, func, txt, help) {
       var b = document.createElement('button');
       b.className     = "ipc-btn ipc-btn--core-accent1 ipc-btn--theme-baseAlt"; //"btn";
       b.style.cssText = "margin-right: 10px; margin-bottom: 10px; font-size: 11px;";
       b.textContent   = txt;
       b.title         = help;
       b.addEventListener('click', func, false);
       div.appendChild(b);
       return b;
    }
 
    function addButtons() {
     const h1 = document.body.getElementsByTagName("h1");
     if (h1) {
         var div  = document.createElement('div');
         div.className      = "aux-content-widget-2";
         div.style.cssText  = "margin-top: 10px;";
         btn1 = addBtn(div, btnRefresh, "Refresh highlight data", "Reload information from your lists - might take a few seconds");
         btn2 = addBtn(div, btnClear,   "Clear highlight data",   "Disable color highlighting of your lists");
         btn4 = addBtn(div, btnHelp,    "What's this?",           "Click for help on these buttons");
         h1[0].appendChild(div);
     } else console.error('Could not find "<h1>Your Lists</h1>" to insert buttons!');
    }
 
    //-------- "main" --------
 
    var we_are_in_the_lists_page = false;
    if (document.location.href.match(/\.imdb\..{2,3}\/user\/[^\/]+\/lists/)) {
       we_are_in_the_lists_page = true;
       getMyLists();
    }
 
    // Find current logged in user, or quit script
    user = getCurrentUser();
    if (!user) return;  // FIX-ME: to support external sites: set/get LAST user to/from browser storage
 
    // Allow user to manually update his/her lists
    if (we_are_in_the_lists_page) {
       addButtons();
       return; // Nothing else to do on the lists page - goodbye!
    }
 
    // Load lists data for this user from localStorage
    loadMyLists();
 
    // highlight movie links
    if (myLists.length) {
       addTooltipStyle();
       highlightTitle();
       highlightLinks();
       if (interval >= 100)
          setInterval(highlightLinks, interval);
    }
 
 })();
 
 // Test URLs:
 //    http://www.imdb.com/mymovies/list
 //    http://www.imdb.com/title/tt0110912/movieconnections/
 //    http://www.imdb.com/chart/top
 //   https://www.imdb.com/chart/top
 //    http://www.imdb.com/genre/sci_fi
 //    http://www.imdb.com/search/title?genres=sci_fi&title_type=feature&num_votes=1000,&sort=user_rating,desc
 //    http://www.imdb.com/event/ev0000003/2011
 //    http://www.imdb.com/year/2004
 //    Over the "instantaneous results" below the search box
 //       Funny... Shark Tale on the page above points to http://www.imdb.com/title/tt0384531/,
 //       but when opened it redirects to ............... http://www.imdb.com/title/tt0307453/
 //    Titles producing invalid CSV files:
 //		http://www.imdb.com/title/tt0095675/
 //		http://www.imdb.com/title/tt0365748/
 // Test people vs movies:
 //    https://www.imdb.com/title/tt0000697/
 //    https://www.imdb.com/name/nm0000697/
 //    https://www.imdb.com/title/tt0053291/