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-2022, 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-2022, 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.51
// @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
//
// History:
// --------
// 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.51 (2022.05.18)'; // 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;
   }

   var WATCHLIST  = "watchlist";
   var RATINGLIST = "ratings";
   var CHECKINS   = "checkins";

   var TITLES = "Titles";
   var PEOPLE = "People";
   var IMAGES = "Images";
   // Lists can be about Titles, People & Images (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?

   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 });
      var lists = document.getElementsByClassName('user-list');
      if (!lists || lists.length < 1) {
         console.error("Error getting lists (or no lists exist)!");
         //return false;
      }
      for (var i = 0; i < lists.length; i++) {
         var listType = lists[i].getAttribute("data-list-type");
         if (listType in listTypes) {
            var tmp   = lists[i].getElementsByClassName("list-name");
            if (!tmp) {
               console.error("Error reading information from list #"+i);
               continue;
            }
            tmp = tmp[0]; // <a class="list-name" href="/list/ls003658871/">Filmes Netflix Brasil</a>
            var name  = tmp.text;
            var id    = tmp.href.match(/\/list\/([^\/\?]+)\/?/)[1];
            var colorType = listType == PEOPLE ? "DefaultPeople" : "DefaultColor";
            var color     = customColors[name] || customColors[colorType] || "";
            myLists.push({"name":name, "id":id, "color":color, "ids":{}, "type":listType });
         }
      }
      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.+?)"/>');
         if (id && id.length > 1)
            exportLink = document.location.protocol + "//www.imdb.com/list/"+id[1]+"/export";
         else {
            id = request.responseText.match('"list":{"id":"(ls.+?)"');
            if (id && id.length > 1)
               exportLink = document.location.protocol + "//www.imdb.com/list/"+id[1]+"/export";
         }
         if (exportLink)
            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;
      var isMovie = document.location.href.match(/tt[0]*(\d+)\//);
      var 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;
      var 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     = "btn";
      b.style.cssText = "margin-right: 10px; font-size: 11px;";
      b.textContent   = txt;
      b.title         = help;
      b.addEventListener('click', func, false);
      div.appendChild(b);
      return b;
   }

   function addButtons() {
      var main = document.getElementById("main");
      if (!main)
         console.error('Could not find "main <div>" to insert buttons!');
      else {
         var h1 = main.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/