gazza911 / Autocomplete with Cache

// ==UserScript==
// @name         Autocomplete with Cache
// @namespace    TVMaze Autocomplete
// @version      1.02
// @description  Adds an autocomplete with local cache (that also acts an as optional search history) to TVMaze
// @author       gazza911
// @match        http://www.tvmaze.com/*
// @require      https://code.jquery.com/ui/1.11.4/jquery-ui.js
// @grant        GM_getValue
// @grant        GM_setValue
// @updateURL https://openuserjs.org/meta/gazza911/Autocomplete_with_Cache.meta.js
// @license      MIT
// ==/UserScript==

/* Instrcuctions / Read me

This autocomplete uses the TVMaze API and will not be any more accurate than if you had searched for it; like the normal search, this is limited to the top 10 results.

If a show is added/updated then you'll have to delete it from the cache in order to see the updated version - there's also a delay between the standard search and the API, this is out of my control as it is server side

The cache is local meaning that no-one else (including me, the developer) will be able to see your search history - any data that might be collected by TVMaze when using the API is also out of my control

Any images used are licensed so that they do not require a link-back and thus have been converted to base64 so that the images do not have to be stored anywhere

To delete all settings/search history: 

    1. Change line #49 - which is normally reset(false); - to reset(true);
    2. Reload any page on TVMaze once
    3. Change it back to reset(false);

To delete individual searches:

    1. Select the search you want to delete from the dropdown
    2. Click the grey 'X' to the right of the dropdown

To hide/show search history:

    1. Click the up/down arrow just below the search - when hidden, search results will still be stored for caching purposes but will not be displayed

To change the cache time (how long each search result is stored) :

    1. Change the value of 'longevity' (line #50) - this is the number of days, but any value including 0.5 for half a day would be accepted
    
To see previous search results without typing it in again:

    1. Select the show/person you want to see the search results for from the dropdown
    
*/

reset(false);
var longevity = 2;

var loadedAt = Date.now();
var cache =  GM_getValue("tvmaze_Cache") || {};
if (Object.keys(cache).length === 0)
{
    cache["shows"] = {};
    cache["people"] = {}; 
    GM_setValue("tvmaze_Cache", cache);
}
var accessTimes = GM_getValue("tvmaze_AccessTimes") || {};
if (Object.keys(accessTimes).length === 0)
{
    accessTimes["shows"] = {};
    accessTimes["people"] = {};  
    GM_setValue("tvmaze_AccessTimes", accessTimes);
}

var currentType = GM_getValue("tvmaze_Searchtype") || {};
if (currentType.length === undefined)
{
    currentType = "shows";
    GM_setValue("tvmaze_Searchtype", currentType);
}

var showRecentSearches = GM_getValue("tvmaze_ShowHistory") || {};

if (showRecentSearches["shows"] === undefined)
{
    showRecentSearches["shows"] = true;    
    GM_setValue("tvmaze_ShowHistory", showRecentSearches);
}
if (showRecentSearches["people"] === undefined)  
{
    showRecentSearches["people"] = true;
    GM_setValue("tvmaze_ShowHistory", showRecentSearches);
}

// Debug info - uncomment and view Google Chrome's console (F12) if you want to see how the data is stored

/*console.log("Cache: " + Object.keys(cache["shows"]).length + "/" + Object.keys(cache["people"]).length);
console.log(cache);
console.log("Access: " + Object.keys(accessTimes["shows"]).length + "/" + Object.keys(accessTimes["people"]).length);
console.log(accessTimes);
console.log("Show");
console.log(showRecentSearches);
console.log("Current Type: " + currentType);*/

function getHistory()
{
    var count = 0;
    if (accessTimes["shows"].length > 0) sort(accessTimes["shows"]);
    if (accessTimes["people"].length > 0) sort(accessTimes["poeple"]);
    var history;
    var deleted = 0;
    var recentSearches = '<div id="recentHistory" style="margin-top:10px;display:none"><label for="recent" style="color:white; float:left; margin-right:11px">Your recent searches:</label><select id="recent" style="max-width:27.3rem; height:22px; padding:0.1rem;float:left;"><option value="" disabled selected>Click to see</option>';
    for (var key in accessTimes[currentType]) 
    {
        if (accessTimes[currentType].hasOwnProperty(key)) 
        {
            if (loadedAt - accessTimes[currentType][key] > 1000 * 60 * 60 * 24 * longevity) // remove if over specifed amount of days
            {
                delete accessTimes[currentType][key];
                delete cache[currentType][key];
                deleted++;
            }
            else
            {
                history = '<option value="' + key + '">' + key + '</option>' + history;
                count++;
            }
        }
    }
    
    if (deleted > 0)
    {
        GM_setValue("tvmaze_AccessTimes", accessTimes);
        GM_setValue("tvmaze_Cache", cache);
    }
        
    recentSearches = recentSearches + history + '</select><img id="remove" title="This will also remove it from the cache" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAADQ0lEQVR4Xu2UXUwUVxSAz8zsLzu7q7ism4AF1qBJA2lCBBe1gOHPpAk/+wcsIBQ2qDVNjLHaNBZXjO9tt/ahBrWtiYpgLEnTlDTBhBcghEQfpBVE2TWNDw3s7szsWpyZ65m4xpQgg+lL0/RMvuTOnMn57r1zz8D/8e+jublpR6C9dbQ72PWbt9kbVBP4WtwfdHS1PWwN+Me9Ld49q/P0aoEM9LHKyoqa6urqnc78vIv+Fm/oTQKcgC/LZv+xrrbOWVtbt0/HMGFVARAqS5ZliMfjUFRUBLm5eWcUyVrF7Vm2a2VlZQzHJYBhGJBkWdiIoH9qapLX6/QvJYWFKMn9m8Tv9/iV4i6Xi+ESCTCZWJiYnJCIJJ+HjYSnxbO7O9jJXRz4lnz19Rfk8pUBcuLkcaJIlOKHP+oVlWdhJffdJaLcKyuCNYJaT2Jl2V9LS11sKiWAmbXA/dn7IAgCKdlVQvECB6zJDNPT09KfS0utQzeGbqoI1paYM0yjJSWllmQSJWYzUHjxPAcmloWZmRkptrQcGBwcHoT1Qk3S2X0wGb7wJQn1f076QqfJhW/CpPdwj6xsF6gErSagCJVv1Ov1siQBIQQTBJ6LIhgMRgpo6t1/JMAZtmZu3nS1oGAHvRxbBovFqgB4LJWTBXa7ffUR3rgAOzRg2WT9YbvTycTiMTAaMmB2dla+e+9eSqPRQCLBwTvbtoHNZltXQr+puNVi/T4vL5+JJ+Kg1xtgbn5OisUTbclnqf0oSmhRwnEc5OTkwJYtma8lavh8vgr8D4mh/j5y8tMT+GHPkA97ukSPp7Ed01pEV++u39vR2RYPne0jn50+RfrPhcihI73YJ56jqiugaPlodnY2diiHM9fB4uIj6enTPz4eHr49hmmHwsitkcXHkUhg/uE8R9MM8AIPDocDtDrdKVWBTOBJUhCUlyESiUrRaPSTn38avYOpLGTrK8bHxiO/P5gLLiws8BRFQSqVAnFFfABqUVVVtdXtaQw3eRt/qax6X9mWnch7yG6kHNmP7EV2IYXFxcUHGprqr7o9DddrasoLNtrJWsSI6NNo0zDpPEFE5DmygvyF8OnxWwWFaBADkoGYETY9NqSlFPyn4wXDv0nI59mhOQAAAABJRU5ErkJggg==" style="height:32px; padding-left:5px; padding-bottom:10px;float:left; border:1px solid #3f3f3f"></img></div>';
    $("#recentHistory").remove();
    $("#toggleRecent").remove();
    $("toggleChange").remove();    
    if (count > 0) 
    {
        $(recentSearches).insertAfter('form:first div:first');
        $('<div id="toggleRecent" style="position:relative;top:37px;height:15px;width:100%;background-color:#595959"></div>').appendTo('form:first div:first');
        $('<img id="toggleChange" style="position:absolute;left:50%;" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAGJJREFUOI3tzUEKQFAUheFvTsk2JGzVhpQpS5E1MDF4qRdPJsqpO7n3/P/lz2sp0SO70c2PbhEua6wYLiQ5RiyozsfuQhLCTexDTHILjkmS4LNkwpwKh5LtmGQ4lLRP4Y9kB5U7Fwi6X3XpAAAAAElFTkSuQmCC"></img>').appendTo("#toggleRecent");

        if (showRecentSearches[currentType] === true) 
        {
            $("#toggleChange").css(
            {
                '-webkit-transform' : 'rotate(180deg)',
                'transform' : 'rotate(180deg)'
             });
            $("#recentHistory").toggle();
        }

        $("#toggleRecent").click(function()
        {
            $("#recentHistory").slideToggle();
            showRecentSearches[currentType] = !showRecentSearches[currentType];
            GM_setValue("tvmaze_ShowHistory", showRecentSearches);
            setTimeout(flip,200);     
        });
        
        $("#recent").change(function()
        {
            $("#searchform-q").val($(this).val());
            $("#searchform-q").focus();
            $("#searchform-q").trigger("input");
        });

        $("#remove").mouseover(function(){
            $(this).css("border", "");
            $(this).css("cursor", "pointer");  
        });

        $("#remove").mouseout(function(){
            $(this).css("border", "1px solid #3f3f3f");
            $(this).css("cursor", "default");  
        });

        $("#remove").click(function()
        {
            var key = $("#recent").val();
            delete accessTimes[currentType][key];
            delete cache[currentType][key];
            GM_setValue("tvmaze_Cache", cache);
            GM_setValue("tvmaze_AccessTimes", accessTimes);
            $("#recent option:selected").remove();
        });
    }
}

function flip()
{
    if (showRecentSearches[currentType] === true)
    {
        $("#toggleChange").css(
        {
            '-webkit-transform' : 'rotate(180deg)',
            'transform' : 'rotate(180deg)'
        });
    }
    else
    {
        $("#toggleChange").css(
        {
            '-webkit-transform' : 'rotate(360deg)',
            'transform' : 'rotate(360deg)'
        });
    }
}

$('<div style="margin-top:10px;margin-left:20px;color:white">Autocomplete for<input type="radio" value="shows" name="type" style="margin-left:10px;"> Shows <input type="radio" value="people" name="type" style="margin-left:15px;"> People</div>').appendTo('form:first');
$('input:radio[value="' + currentType + '"]').prop("checked", true );

getHistory();

$('input:radio').change(function()
{
    currentType = $("input:radio:checked").val();
    GM_setValue("tvmaze_Searchtype", currentType);
    $("#searchform-q").focus();
    $("#searchform-q").trigger("input");
    getHistory();
});

$('head').append('<link rel="stylesheet" href="//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">');

$("#searchform-q").autocomplete({
    source: function (request, response) 
    {
        cache =  GM_getValue("tvmaze_Cache") || {};
        accessTimes = GM_getValue("tvmaze_AccessTimes") || {};
        var term = request.term;
        accessTimes[currentType][term] = Date.now();
        GM_setValue("tvmaze_AccessTimes", accessTimes);
        if (cache != undefined && term in cache[currentType] ) 
        {
            response(cache[currentType][ term ]);
            console.log("in cache");
            checkWidth();
            return;
        }
        console.log("not in cache");
		$.ajax(
		{
			url: "http://api.tvmaze.com/search/" + currentType + "?q=" + request.term,
			method: "GET",
			success: function (data) 
			{
				input = request.term;
                cache[currentType][ term ] = transform(data);
                GM_setValue("tvmaze_Cache", cache);
                response(cache[currentType][ term ]);
                return;
			},
			error: function () 
			{
				response([]);
			},
            complete: checkWidth()
		});
	},
    minLength: 4,
    delay:500, 
    select: function( event, ui ) {
          window.location.assign(ui.item.link);
    }
})
.autocomplete( "instance" )._renderItem = function( ul, item ) {
    if (item != undefined)
    {
        var element = $( '<li>' );        
        $(element).append( '<span class="ui-autocomplete-label">' + item.label + '</span>');
        if (currentType == "shows")
        {
            var showDetails = '<span style="position:absolute; right:32px; top:8px; font-size:10px; color:grey;">';
            if (item.year != null) 
            {
                showDetails += item.year;
            }
            showDetails += '</span>';
            if (item.country != null) showDetails += " <img style='position:absolute; right:10px; top:11px;' src='http://tvmazecdn.com/intvendor/flags/" + item.country.toLowerCase() + ".png'></img>";
            $(showDetails).appendTo( element );  
        }
        $(element).appendTo( ul );
        return element;
    }
};

function checkWidth()
{
    var menu = $(".ui-autocomplete").width();
    var max = Math.max.apply(null, 
              $(".ui-autocomplete-label").map(function ()
              {
                  return $(this).width();
              }).get());
    if (menu - max <= 80) $(".ui-autocomplete").width(max + 80 + "px");
}

function transform(data, type)
{
    var transformed = $.map(data, function (el) 
    {
        if (currentType == "shows") 
        {
            return {
                label: el.show.name,
                id: el.score,
                link: el.show.url,
                year: (el.show.premiered != null) ? el.show.premiered.split("-")[0] : null,
                country: (el.show.network != undefined) ? el.show.network.country.code : el.show.webChannel.country.code
            };
        }
        else if (currentType == "people") 
        {
            return {
                label: el.person.name,
                id: el.score,
                link: el.person.url                      
            };
        }
    });
    return transformed;
}

function sort(array){ 
  Object.keys(array).sort( function(keyA, keyB) {
  return array[keyA] - array[keyB];
  });
}

function reset(input)
{
   if (input == true) 
   {
       GM_setValue("tvmaze_Cache", {});
       GM_setValue("tvmaze_AccessTimes", {});
       GM_setValue("tvmaze_ShowHistory", {});
       GM_setValue("tvmaze_Searchtype", {}); 
   }
}