Torrentz : The Bobcat add-on

Torrentz.eu: Add IMDB ratings, download links, movie plot/actors, and other goodies. Also features an light built-in serie tracker. Torrentz gets so much simpler and efficient! Demo video here: http://www.youtube.com/watch?v=1QyuIDw0CIw&feature=youtu.be

Mint 2015.04.16.. Lásd a legutóbbi verzió

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name          Torrentz : The Bobcat add-on
// @namespace     http://torrentzBobCat
// @homepage      http://www.youtube.com/watch?v=1QyuIDw0CIw&feature=youtu.be
// @description   Torrentz.eu: Add IMDB ratings, download links, movie plot/actors, and other goodies. Also features an light built-in serie tracker. Torrentz gets so much simpler and efficient! Demo video here: http://www.youtube.com/watch?v=1QyuIDw0CIw&feature=youtu.be
// @author        CoolMatt
// @version        1.5.1
// @grant none
// @include       *://torrentz.*
// @match         *://torrentz.com/*
// @match         *://torrentz.eu/*

// ==/UserScript==
// @date    19 Jun 2013
// @license    GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html

//Define the namespace
var Torrentz = Torrentz || {};
Torrentz.GM = {};
Torrentz.GM.BobCatTorrentz = {}; 

Torrentz.GM.BobCatTorrentz = {

    PageCache_movieInfo: {},  //Store info about movies of the page
    PageCache_lk_id_info: {}, //Lookup table - as several torrentz can point at the same movie info

    start: function () {
        
        initCss();
        this.addBadgeAndButtons();

        $("div.cloud").hide();

        var loginStore = Enbalaba.GetLocalStore("moviesInfo"),
            loginData = loginStore.get(),
            that = this,
            results;

        //Calculate cache size and clear it if too big
        this.checkCacheSize(loginStore);

        //Get rid of this incredibly annoying & ridiculous advertising banner
        $("body>iframe:first").hide();

        // Append download modal
        $("body").append("<div id=\"downloadModalOverlay\"></div><div id=\"downloadModal\"><p>Click a link below in order to download your torrent</p><div class=\"torrentContainer\"></div></div");
        $("#downloadModal,#downloadModalOverlay").click( function(e){ $("#downloadModal,#downloadModalOverlay").hide();});
        
        //Start processing of the rows
        results = $(".results");

        results.find("h3:first").append("<span>|&nbsp</span><b title='IMDB Rating. Brought to you by the Torrentz Dominion Plugin'>Rating</b>");
        results.children("dl").each(function (index) {
            that.processRow($(this), loginData, false);
        });

        results.find("span.downloadLink").click(function () {
            //$(this).replaceWith("<div class='fleft bobcatStamp' style='width:80px;height:25px;position:relative;top:-4px'></div>");
            downloadTorrent($(this).attr("data-torrentid"));
        });
        
        //Add events for when the row is clicked
        results.find("dt").click(function () {
            var dt = $(this),
                text,
                divDesc = dt.find(".movieDesc"),
                div, lk, aElement;

            if (divDesc.length == 0) { //First time the user clicks here
                aElement = dt.children("a:first");
                if (aElement.length == 0) return;
                var id = aElement.attr("href").substr(1).toUpperCase(), info = null;
                if (!id) return;

                //Retrieve info from cache
                if (that.PageCache_lk_id_info[id]) {
                    info = that.PageCache_movieInfo[that.PageCache_lk_id_info[id]];
                }

                if (info) {
                    // 
                    text = "<div class='plot'><b>Plot</b>: " + info.Plot + "</div><div class='actorsInfo'> <b>Actors</b>: " + info.Actors + "</div>";
                }
                else {
                    //No info > display the title
                    text = aElement.attr("title");
                }

                divDesc = $("<div class='movieDesc'>" + text + "</div>");

                div = $("<div class='divQuality'></div>");
                lk = $("<div class='hyperlink fleft'>See user comments</div>");
                lk.hover(function (e) {
                    if ($(this).data('processed')) return;
                    that.getQuality($(this.parentNode), aElement.attr("href")); e.stopPropagation(); $(this).hide();
                    $(this).data('processed', true);
                });

                div.append(lk).append($("<img class='spinner fleft'></div><div class='qualityComments'><div/>").hide()); //Add get Quality link and spinner image
                divDesc.append(div).hide();
                dt.append(divDesc);
            }
            if (divDesc.is(":hidden")) {
                divDesc.children().hide();
                divDesc.slideDown(200, function () { divDesc.children().show(); });
                //dt.children(".expandCollapse").removeClass("expand").addClass("collapse");
                dt.children(".expand").addClass("collapse");
            }
            else {
                divDesc.slideUp();
                dt.children(".expand").removeClass("collapse");
            }
        });

        // Once a day, the serie tracker check if there are new episodes available
        this.checkForNewEpisodes();
    }

    , processRow: function (row, loginData, isIFrameDownload) {
        if (!row) return;
        var tags = null,
            name,
            lk = row.find("dt>a");

        if (lk.length > 0) {
            var id = lk.attr("href").substr(1).toUpperCase(),   //Get id from href
                info = lk.parent().text(),
                index = info.indexOf('\u00BB'), //Look for the utf-8 character >> in the row
                rightCol = row.find("dd");

            rightCol.css("width", "480px");
            row.find("dt").css("width", "540px");

            if (index > -1) {
                tags = info.substr(index + 1);
                name = info.substr(0, index);
            }
            lk.attr("title", tags ? "Tags: " + tags : info).parent().html(lk); //Remove info to make some room

            //lk.after("<span class='moreLk'>more</span>");
            var type = this.getType(name, tags);
            if (this.isTVSerie(name)) {
                type = "tv"; //extra verification as sometimes a tv serie is not tagged as such
            }
            if (type == "movie") {
                if (!loginData) return;
                lk.css('color', '#3F14FF');

                var yearIndex = name.search(/\s[0-9]{4}\s/); //Year is mandatory
                if (yearIndex != -1) {
                    var year = name.substr(yearIndex + 1, 4);
                    name = name.substr(0, yearIndex);
                    //console.log(name + ":" + year);

                    info = loginData[(name + year).toLowerCase()]; //Search in cache
                    if (info) {
                        this.PageCache_movieInfo[(name + year).toLowerCase()] = info;
                        this.PageCache_lk_id_info[id] = (name + year).toLowerCase();

                        lk.text(name + " " + year);
                        rightCol.append($("<a class='rateBox' " + (info.ImdbID && info.ImdbID != "" ? "target='_blank' href='http://www.imdb.com/title/" + info.ImdbID + "'" : "") + " >" + info.imdbRating + "</a>"));
                    }
                    else {
                        //this.searchIMDBinfo(name, year, lk, rightCol);
                    }
                }
            }

            else if (type == "tv") {
                lk.css('color', 'Black' /*'#47D4FF'*/);
            }
            else {
                lk.css('color', '#555');
            }
            //Add link
            //if (isIFrameDownload == true) {
            rightCol.prepend("<span class='downloadLink hyperlink' data-torrentid='" + id + "'>Download</span>");
            //rightCol.prepend("<div class='downloadIcon fleft' data-torrentid='" + id + "'></div>");

            lk.parent().prepend("<div class='expand fleft'></div>");
        }
        //this.attachRowEvent(row);

        //row.find("span.downloadLink").text("download");
    }

    , getType: function (name, tags) {
        if (tags.indexOf("movies") > -1) return "movie";
        else if (tags.indexOf("tv") > -1) return "tv";
        else if (tags.indexOf("games") > -1) return "game";
    }
    /*As sometimes the tv serie is not tagged as such. This test will help catch those ones*/
    , isTVSerie: function (fullName) {
        return (
            new RegExp(/[sS][0-9]+[eE][0-9]+/).test(fullName)
        || new RegExp(/[0-9]+[x][0-9]+/).test(fullName)
        || new RegExp(/season[\s]?[0-9]{1,2}[\s]/i).test(fullName)
        );
    }

    /*
     *
     */
 , searchIMDBinfo: function (name, year, link, rightCol, isRetry) {
     //console.log("encodeURI('http://www.imdbapi.com/?t=" + name + "')");
     var url = encodeURI("http://www.imdbapi.com/?t=" + name), that = this;
     $("<span></span>").css("display", "none").load(url, function (data) {
         var obj = $.parseJSON(data);
         if (obj) {
             if (obj.imdbRating) {
                 var loginStore = Enbalaba.GetLocalStore("moviesInfo")
               , loginData = loginStore.get();
                 if (loginData) {
                     var refName = (name + year).toLowerCase();
                     that.PageCache_movieInfo[refName] = { imdbRating: obj.imdbRating, Plot: obj.Plot, Actors: obj.Actors, ImdbID: obj.imdbID };
                     that.PageCache_lk_id_info[link.attr("href").substr(1).toUpperCase()] = refName;

                     loginData[refName] = { imdbRating: obj.imdbRating, Plot: obj.Plot, Actors: obj.Actors, ImdbID: obj.imdbID };
                     loginStore.set(loginData);
                 }
                 rightCol.append($("<a class='rateBox' " + (obj.imdbID && obj.imdbID != "" ? "target='_blank' href='http://www.imdb.com/title/" + obj.imdbID + "'" : "") + " >" + obj.imdbRating + "</a>"));
                 //rightCol.prepend($("<input type='text' disabled='disabled' class='rateBox' value='" + obj.imdbRating + "'></input>")); //tbMark.val(obj.imdbRating);
                 //if (obj.Plot) link.attr("title", obj.Plot).data("MovieInfo", { imdbRating: obj.imdbRating, Plot: obj.Plot, Actors: obj.Actors });
             }
             else if (obj.Response == "False"/* && obj.Error.indexOf("Movie not found") != -1*/) {
                 if (isRetry != true) {  //Tries a second search
                     var name2 = name.replace(/thats/gi, "that's").replace(/it's/gi, "its").replace(/spiderman/i, "spider man").replace(/extended$/i, "");
                     if (name2 != name) { that.searchIMDBinfo(name2, year, link, rightCol, true); return; }
                 }
                 console.info(name + ": " + obj.Error);
                 //rightCol.prepend($("<input type='text' disabled='disabled' class='rateBox' value='?'></input>"));
             }
         }
     });
     return;
 }

     , tempID: 0
     , getQuality: function ($qualityDiv, url) {

         url = "https://torrentz.eu" + url;
         //console.info(url);
         $qualityDiv.find(".spinner").show();
         var id = "divComment" + (this.tempID++);
         $("<div style='display:none' id='" + id + "'></div>").load(url, function (data) {
             var comments = $(data).find("div.comment .com"),
                 qualityComments = [];

             for (var i = 0, comment; i < comments.length; i++) {
                 comment = $(comments[i]).text();
                 if (comment.length > 400) comment = comment.substr(0, 400) + " (...)";
                 qualityComments.push(comment);
             }
             $qualityDiv.find(".spinner").hide();
             $qualityDiv.find(".qualityComments").show().html("<b>User comment:</b><br/>" + qualityComments.join("<br/>"));
             $(id).empty(); //free memory of the temporary div
         });
     }

/*
 * Calculate cache size and clear it if too big
 */
 , checkCacheSize: function (loginStore) {
     var cacheSize,
         k,
         that = this,
         loginData = loginStore.get();
     
     try {
         //Works in all recent browsers
         cacheSize = Object.keys(loginData).length; 
     }
     catch (err) {
         
         cacheSize = 0;
         for (k in loginData) {
             if (loginData.hasOwnProperty(k)) cacheSize++;
         }
     }
     console.log("Bobcat - Cache size:" + cacheSize);
     if (cacheSize > 150) {
         //Clear the cache from time to time
         loginStore.set({});
         console.info("Bobcat - Movie cache cleared");
     }
 }

 /*
  * Add badges and buttons
  */
  , addBadgeAndButtons: function () {
      //Add bobcat badge in the top banner
      $("div.top").append("<div id='bobcatLogoContainer' class='bobcatLogo'><a href='http://www.youtube.com/watch?v=1QyuIDw0CIw&feature=youtu.be'>with the Bobcat add-on</a></div>");

      //Add serie tracker button
      var btST = $("<button type='button' id='btSerieTracker' class='bcButton bobcatStamp'>Serie Tracker</button>"),
          that = this;
      btST.click(function () { that.onclick_btSerieTracker(); });
      $("div.results h2").append(btST);
      //this.onclick_btSerieTracker(); //~~ Uncomment when developping serie tracker
  }

    //---------------------
    //-----SERIE TRACKER---
    //---------------------
  , _ddSeasonHTML: ""
  , _ddEpisodeHTML: ""
  , onclick_btSerieTracker: function () {

      if (!this.SerieTrackerMode) {
          $("div.results h3").nextAll().hide();
          $("div.recent").hide();
          $("#serieContainer").show();
          $("#btSerieTracker").text("Return to List");
          if (this.SerieTrackerMode == null) { //init serie tracker

              var serieTrakerLastCheckedStore = Enbalaba.GetLocalStore("serieTrackerLastCheck")
            , serieTrakerLastChecked = serieTrakerLastCheckedStore.get();
              serieTrakerLastCheckedStore.set({ FoundNewEpisodes: false, LastChecked: encodeDate(new Date()) });
              delete serieTrakerLastCheckedStore;

              var serieStore = Enbalaba.GetLocalStore("trackedSeriesInfo"), serieInfo = serieStore.get();
              if (!serieInfo.Ids) serieStore.set({ Ids: [], CurrentId: 1 });

              $("div.results").after("<div class='results' id='serieContainer'><dl></dl><div id='addSerieContainer' style='float:left;position:relative;'> <h2>Track a new Serie</h2>"
              + "<div class='row'><div class='col1'><label>Name</label></div><div  class='col2'><input type='text' id='st_tbNameNew' class='bcTextbox'/></div> <div class='col3'><span id='st_lblSuggestion'></span></div></div>"
              + "<div class='row'><div class='col1'><label>Season</label></div><div  class='col2'><select id='st_ddSeasonNew' class='bcSelect'><option></option></select></div></div>"
              + "<div class='row'><div class='col1'><label>Episode</label></div><div  class='col2'><select id='st_ddEpisodeNew' class='bcSelect'><option></option></select></div></div>"
              + "<button type='button' id='btAddSerie' class='bcButton'>Add This Serie</button><span id='st_lblOutput' style='color:red'></span><br/>"
              + "<input type='checkbox' id='cbIsFinishedSeason'/><label for='cbIsFinishedSeason'>I know this season is finished and has </label><input type='input' id='tbSeasonNbEpisodes' style='width:20px' maxlength=2 value='20'/> episodes</div>"
              //+ "<div id='st_lblNotes' style='position:absolute;top:30px;left:30px' >Bobcat-Torrentz will check once a day for new episodes of tracked series<div/>"
              + "</div>"
              + "<div style='clear:both;cursor:pointer' id='st_btDeleteAll' >Delete All Tracked Series<div/>"
              + "<div style='width:500px;border-radius:6px; border-size:1px;margin-top:25px'>The Bobcat addon will check once a day your tracked series for new episodes.<br/><img src='http://i.imgur.com/n7tvk8I.png'/>: New episode(s)<br/><img src='http://i.imgur.com/tDWKswF.png'/>: No new episodes</div>");
              //$("div.note").css("width", "400px").html("The Bobcat addon will check once a day your tracked series for new episodes.<br/><img src='http://i.imgur.com/n7tvk8I.png'/>:New episode(s)<br/><img src='http://i.imgur.com/tDWKswF.png'/>: No new episodes");

              /*Populate dropdowns*/
              var i, htmlddSeasons = "", htmlddEpisodes = "";
              for (i = 1; i < 16; i++) htmlddSeasons += "<option value='" + i + "'>" + i + "</option>";
              for (i = 1; i < 31; i++) htmlddEpisodes += "<option value='" + i + "'>" + i + "</option>";
              $("#st_ddSeasonNew").html(htmlddSeasons);
              $("#st_ddEpisodeNew").html(htmlddEpisodes);


              htmlddSeasons = ""; //blank variable because of closures
              htmlddEpisodes = "";

              /*Display Series*/
              this.displayTrackedSeries();

              /*Search for new*/
              this.searchForNewEpisodes(this.episodeFoundCallback);

              /*Add Serie event*/
              var that = this;
              $("#btAddSerie").click(function () {
                  var name = $("#st_tbNameNew").val();
                  if ($.trim(name) == "") {
                      $("#st_lblOutput").text("Enter a Name");
                      //$("#st_tbNameNew").css("border-color", "red");
                  }
                  else { //---ADDITION
                      var store = Enbalaba.GetLocalStore("trackedSeriesInfo"), serieInfo = store.get()
                      , id = serieInfo.CurrentId;
                      serieInfo.CurrentId = id + 1;
                      serieInfo.Ids.push(id);
                      store.set(serieInfo);

                      var store = Enbalaba.GetLocalStore("ts_" + id)
                      , isFinished = $("#cbIsFinishedSeason").is(":checked")
                      , serie = { Name: name, Season: parseInt($("#st_ddSeasonNew").val(), 10), Current: { e: parseInt($("#st_ddEpisodeNew").val(), 10) }, History: [], id: id };
                      if (isFinished) {
                          serie.isFinished = true;
                          serie.NbTotEpisodes = parseInt($("#tbSeasonNbEpisodes").val(), 10);
                          if (isNaN(serie.NbTotEpisodes)) { alert("Enter a valid number of episodes"); return; }
                      }
                      store.set(serie);

                      that.displayTrackedSeries();
                      that.searchForNewEpisodes(that.episodeFoundCallback);
                      //that.displayTrackedSeries(serieStore);
                      $("#st_lblOutput").text("");
                  }
              });

              $("#st_lblSuggestion").click(function () {
                  $("#st_tbNameNew").val($(this).text());
              });
              $("#st_tbNameNew").keypress(function (e) {
                  if (e.keyCode >= 20 && e.keyCode <= 40 && e.keyCode != 32) return true; //arrows, shift, and other keys that don't change the input. 32 is 'space'
                  var txt = $(this).val();
                  if (txt.length >= 3) {
                      var url = encodeURI("http://torrentz.eu/suggestions.php?q=" + $.trim(txt)/*.replace(' ', '+')*/);
                      $("<span></span>").css("display", "none").load(url, function (data) {
                          var res = $.parseJSON(data);

                          if (res && res.length == 2 && res[1] != null && res[1].length > 0) {
                              console.log(res[1][0]);
                              $("#st_lblSuggestion").text(res[1][0]);
                          }
                          else $("#st_lblSuggestion").val("-");
                      });
                  }
              });
              $("#st_btDeleteAll").click(function () {
                  if (confirm("Do you want to delete all the currently tracked series ?")) {
                      Enbalaba.GetLocalStore("trackedSeriesInfo").set({ Ids: [], CurrentId: 1 });
                      that.displayTrackedSeries();
                  }

              });

          }
          this.SerieTrackerMode = true;

      }
      else {
          this.SerieTrackerMode = false;
          $("div.results h3,div.recent").nextAll().show();
          $("#serieContainer").hide();
          $("#btSerieTracker").text("Serie Tracker");
      }
  }

  , displayTrackedSeries: function () {
      var serieIds = Enbalaba.GetLocalStore("trackedSeriesInfo").get().Ids;
      var dl = $("#serieContainer dl");

      dl.empty();
      for (var i = 0, serie, id; i < serieIds.length; i++) {
          this.displaySerie(serieIds[i]);
      }
  }

  , displaySerie: function (serieId) {
      var serie = Enbalaba.GetLocalStore("ts_" + serieId).get(), time
      , hasNew = false;

      if (!serie.History) serie.History = [];

      var html = "<div class='trackedSerieContainer'  data-id='" + serie.id + "'>"
        + "<div class='trackedSerieHeader'>"
        + "<div class='st_name st-col1'>"
        + "<div class='deleteIcon fleft' data-id='" + serie.id + "' title='delete' style='margin-right:2px'></div>"
        + serie.Name + "</div>"
        + "<div class='st-col2'><b>Season " + serie.Season + "</b></div>"
        + "<div class='episode st-col3'><b>" + (serie.History.length > 0 ? "Episode " + serie.History[0].e : " - ") + "</b></div>"
        + "<div class='st-col4'>" + (serie.isFinished ? "" : "Tracking: On") + "</div>"
        + "</div>"
        + "<div class='trackerSerieBody' style='display:none'>";

      //History

      if (serie.History.length == 0) {
          html += "<div style='margin-left:50px'>No results found</div>";
      }
      else {
          for (var j = 0, h, d, l = serie.History.length; j < l; j++) {
              h = serie.History[j];
              d = (h.d ? new Date(new Date() - getDateFromDateString(h.d)) : null);

              if (d) {
                  if (d.getMonth() > 0) {
                      dif = d.getMonth() + " month" + (d.getMonth() == 1 ? "" : "s") + " ago";
                  }
                  else {
                      time = d.getDate() - 1;
                      if (time == 0) {
                          time = "today";
                          hasNew = true;
                      }
                      else time += " days ago";
                  }
              }
              html += "<div class='st-row' data-serieData='" + (serie.id + "_" + h.e) + "'><div class='st-col1'>&nbsp</div> <div class='st-col2'>Episode " + h.e + "</div><div class='st-col3'>"
              + (h.f ? "<span class='st-btShowLk hyperlink'>Show Links<span>" : "<span>Not found</span>") + "</div>"
              + "<div class='st-col4'>" + time + "</div>"
              + "</div> ";
          }
      }
      html += "</div></div>";
      var el = $("#serieContainer .trackedSerieContainer").filter(function () { return $(this).data("id") == serieId; }), newEl = $(html);
      delete html; //for the closure

      if (el.length > 0) el.empty().replaceWith(newEl);
      else $("#serieContainer dl").append(newEl);

      var that = this;

      newEl.find("div.deleteIcon").click(function (e) { e.stopImmediatePropagation(); that.onclick_deleteTrackedSerie(this); });
      newEl.find("span.st-btShowLk").click(function (e) { that.onclick_showLinks(this); });
      newEl.find("div.trackedSerieHeader").addClass(hasNew ? "hasNew" : "").click(function (e) { that.onclick_serieHeader(this); });

  }

    // Once a day, the serie tracker check if there are new episodes available.
    , checkForNewEpisodes: function () {
        var serieTrakerLastCheckedStore = Enbalaba.GetLocalStore("serieTrackerLastCheck"),
            serieTrakerLastChecked = serieTrakerLastCheckedStore.get(),
            today = encodeDate(new Date());

        if (!serieTrakerLastChecked || serieTrakerLastChecked.LastChecked != today) {
            //Start daily search
            serieTrakerLastCheckedStore.set({ FoundNewEpisodes: false, LastChecked: today });
            this.searchForNewEpisodes(this.episodeFoundCallback_2);
        }
        else if (serieTrakerLastChecked.FoundNewEpisodes) {
            //A search was previously done, but the user didn't go to the serie tracker. Add a specific icon to signal new episode available.
            $("#btSerieTracker").css("color", "Blue").removeClass("bobcatStamp").addClass("bobcatStamp2");
        }
    }

  , searchForNewEpisodes: function (callback) {

      var serieInfo = Enbalaba.GetLocalStore("trackedSeriesInfo").get()
      , that = this
      , today = encodeDate(new Date());

      for (var i = 0, store, serie, search, ids = serieInfo.Ids; i < ids.length; i++) {
          //serie = series[i];
          store = Enbalaba.GetLocalStore("ts_" + ids[i]);
          serie = store.get();
          if (!serie.isFinished || serie.History.length == 0) {

              this.lookForEpisode(serie, serie.Current.e, callback, store);
          }
      }

  }

   , lookForEpisode: function (serie, episode, callback, store) {
       var search = serie.Name + " S" + (serie.Season < 10 ? "0" : "") + serie.Season + "E" + (episode < 10 ? "0" : "") + episode
      , url = encodeURI("http://torrentz.eu/search?f=" + search)
      , that = this;
       //console.info(url);
       search = search.toLowerCase();
       $("<span></span>").css("display", "none").load(url, function (data) {
           var rows = $(this).find("div.results dl"), results = [];
           //console.info(rows.length + " results");

           for (var i = 0, $row, txt; i < rows.length; i++) {
               $row = $(rows[i]);
               txt = $row.find("dt").text().toLowerCase();
               if (txt.indexOf(search) > -1) { //we need to be sure the results returned are related to the search
                   results.push($row);
               }
           }
           if (callback) callback(serie, episode, results, store, that);
           $(this).empty(); //free memory of the temporary div
       });
   }

  , episodeFoundCallback: function (s, e, results, store, context) {

      if (results.length < 1) { //NO EPISODE FOUND
          if (s.isFinished && e < s.NbTotEpisodes) {
              s.History.splice(0, 0, { e: e, f: false, d: encodeDate(new Date()) }); //insert new entry in history - at the begining
              s.Current.e = parseInt(s.Current.e, 10) + 1;
              store.set(s);
              context.displaySerie(s.id); //redisplay the result for the serie
              context.lookForEpisode(s, e + 1, context.episodeFoundCallback, store); //rec
          }
      }
      else { //EPISODE FOUND
          //console.info("New episode Found for " + s.Name);
          //New episode found
          s.History.splice(0, 0, { e: e, f: true, d: encodeDate(new Date()) }); //insert new entry in history - at the begining
          s.Current.e = parseInt(s.Current.e, 10) + 1;
          store.set(s);
          context.displaySerie(s.id); //redisplay the result for the serie
          var el = $("#serieContainer .trackedSerieContainer").filter(function () { return $(this).data("id") == s.id; });
          el.find(".trackedSerieHeader").css("color", "Blue");

          context.lookForEpisode(s, s.Current.e, context.episodeFoundCallback, store); //rec
      }
  }

    /*Function called the first time the user visit torrentz in the day, even if he hasn't entered the Section Tracker section*/
  , episodeFoundCallback_2: function (s, e, results) {
      if (results.length < 1) return;
      $("#btSerieTracker").css("color", "Blue"); //.text("Serie Tracker ( New Episodes! )");

      var serieTrakerLastCheckedStore = Enbalaba.GetLocalStore("serieTrackerLastCheck")
        , serieTrakerLastChecked = serieTrakerLastCheckedStore.get();

      serieTrakerLastCheckedStore.set({ FoundNewEpisodes: true, LastChecked: encodeDate(new Date()) });
  }



  , onclick_serieHeader: function (headerEl) {

      headerEl = $(headerEl);
      var body = headerEl.parent().children(".trackerSerieBody");
      if (body.is(":visible")) {
          //console.info("up");
          body.slideUp(200, function () { });
      }
      else body.slideDown(200, function () { });

      //$(this).parent().data("id"));
  }

  , onclick_showLinks: function (lk) {

      lk = $(lk);
      var row = lk.parent().parent()
      , that = this
      , data = row.data("seriedata"), dataParts; //row store some data : serieId_episodeNumber
      var existingBox = row.parent().children(".st-link-container").filter(function () { return $(this).data("seriedata") == data; });
      if (existingBox.length != 0) {
          existingBox.remove();
          //row.parent().remove(existingBox);
      }
      else {
          if (data) {
              dataParts = data.split('_'); //"serieId _episodeNumber"
              var serie = this.getSerieFromStore(dataParts[0]);

              this.lookForEpisode(serie, dataParts[1], function (s, e, results) {


                  var html = "<div class='st-link-container' data-seriedata='" + data + "'>"
                  , max = (results.length < 3 ? results.length : 3);
                  for (var i = 0; i < max; i++) { //show only 3 first
                      r = results[i];
                      that.processRow(r, null, true);
                      html += "<div class='st-row'><div class='st-link-col1'>" + r.find("dt").html() + "</div>";
                      html += "<div class='st-link-col2'>" + r.find("dd").html() + "</div> ";
                      html += "</div>";
                  }
                  var linkContainer = $(html);
                  linkContainer.find("span.downloadLink").click(function () {
                      downloadTorrent($(this).attr("data-torrentid"));
                  });
                  row.after(linkContainer);

              });
          }
      }
  }
   , onclick_deleteTrackedSerie: function (element) {
       //serie deletion

       var name = $(element).parent().text(), id = $(element).data('id');
       if (confirm("Are you sure you want to delete the entry for '" + name + "'")) {

           var store = Enbalaba.GetLocalStore("trackedSeriesInfo"), serieInfo = store.get();
           serieInfo.Ids = $.grep(serieInfo.Ids, function (value) { return value != id; });
           store.set(serieInfo);

           store = Enbalaba.GetLocalStore("ts_" + id); //.get();
           store.set({}); //TODO : real deletion
           this.displayTrackedSeries();
           //console.info("deleted");
       }
       //else console.info("Not deleted");

   }

  , getSerieFromStore: function (id) {
      var serieStore = Enbalaba.GetLocalStore("ts_" + id), serie = serieStore.get();
      return serie;
  }

}

//Basic Class to deal with the localstorage
Enbalaba = {};

//Add CSS
function initCss() {
    var css = [
        " .rateBox{ margin-left:10px;position:relative;bottom:3px; cursor: pointer; padding:1px; background-color:#EEE; border:#AAA solid 1px; border-radius:4px}"
        , ".bobcatLogo{background:transparent url(http://i.imgur.com/MlVVyzX.png) no-repeat scroll 0 0}"
        , "#bobcatLogoContainer a{color:White}"
        , "#bobcatLogoContainer a:hover{color:White}"
        , ".bobcatStamp{background:transparent url(http://i.imgur.com/tDWKswF.png) no-repeat scroll 0 0}"
        , ".bobcatStamp2{background:transparent url(http://i.imgur.com/n7tvk8I.png) no-repeat scroll 0 0}"
        , "#bobcatLogoContainer{color:White;height:30px;width:200px;float:left;padding-left:50px;padding-top:5px; margin-top:10px}"
        , ".downloadLink{;margin-right:20px}"
        , ".moreLk{padding-left:30px;cursor:pointer}"
        , ".movieDesc{width:530px;margin:10px 0px 40px 0px;color:Black;white-space:normal}"
        , ".fleft{ float:left}"
        , "dt:hover{ background-color:#EEE}"
        , ".qualityComments{float:clear}"
        , ".spinner{ background:url(http://www.andrewdavidson.com/articles/spinning-wait-icons/wait16trans.gif) no-repeat left center;width: 16px;height: 16px}"
        , ".actorsInfo,.qualityComments,.divQuality,.plot{ margin-top:11px;margin-bottom:5px; font-size:12px;font-family:Verdana,Tahoma,sans-serif}"
        , "#pluginZoneContainer{ position:absolute; left: 210px; top:10px; width: 200px; height: 200px;background-color:Gray}"
        , ".expand{  background:transparent url(http://i.imgur.com/mIIop2R.png) no-repeat scroll 0 0; width:15px; height: 9px; position: relative; top:3px}" //arrow1.png
        , ".deleteIcon{  background:transparent url(http://i.imgur.com/4RjuUFU.png) no-repeat scroll 0 0; width:15px; height: 13px; position: relative; top:3px}"
        , ".downloadIcon{  background:transparent url(http://i.imgur.com/7Jkx1N9.png) no-repeat scroll 0 0; width:17px; height: 18px; position: relative; top:3px}"
        , ".deleteIcon:hover{ background-color:#CCC}"
        , ".collapse{  background-image:url(http://i.imgur.com/apcKFJ5.png)}"//arrow2.png
        , "#downloadModal{ position: fixed; left: 20%; top:200px; width: 720px; height: 160px; background-color: white; border:8px solid #DDD; border-radius: 6px;padding: 15px; display:none}",
        , "#downloadModalOverlay{ position: fixed;top:0;left:0;width:100%;height:100%;background-color: rgb(100,100,100);filter: alpha(opacity=80);opacity:0.8; display:none}",
        , ".torrentContainer{white-space: nowrap; overflow: hidden; margin: 20px 0}"
        , ".torrentContainer li{ margin-bottom: 15px}"
        
        , ".btSerieTrackerHighlight{color:Yellow !important}"
        , "#btSerieTracker{ padding-left:40px;margin-left:20px; background-color:White;background-position:3px 3px}"
        , "#addSerieContainer{ width:50%;   border: 1px solid #B5B8C8; margin: 30px 0px; padding:20px; border-radius: 15px}"
        , "#st_tbNameNew{ width : 150px}"
        , "#btAddSerie{ margin: 10px 0px}"
        , "#cbIsFinishedSeason{margin-right:7px}"
        , "#st_lblSuggestion{ color:Grey; font-size:11px;cursor:pointer}"
        , ".trackedSerieHeader{ margin: 15px 0px}"
        , ".trackedSerieHeader,.st-row{clear:both;width:100%;font-size:12px;height:15px}"
        , ".trackedSerieHeader>div,.st-row>div{ margin-right:30px;float:left}"
        , ".st-col1{ width:200px}"
        , ".st-col2,.st-col3{ width:100px}"
        , ".st-link-col1{width:500px}"
        , ".st_name{font-weight:bold}"
        , " .st-link-container{margin:10px; border:1px solid #AAA;padding:20px}"
        , ".st-link-col2 span{ margin-right:10px}"
        , ".st-link-col2 .u{ font-weight:bold}"
        , "div.trackedSerieHeader{cursor:pointer}"
        , "div.trackedSerieHeader:hover{ background-color:#EEE}"
        , ".hasNew{color:Blue}"

    //Generic
        , ".hyperlink{color:#0066EE;text-decoration:none;cursor:pointer;text-decoration:underline}"
        , ".bcButton{color:#6B3F2E; border-radius: 6px; border: 1px solid #6B3F2E; height:25px; padding-bottom:1px; min-width:80px; font-weight:bold;cursor:pointer}"
        , ".bcButton:hover{color:#AA3F2E; }"
        , ".bcTextbox{background-color:#FFF;border: 1px solid #B5B8C8; font-size: 14px; height: 16px;  line-height: 14px; padding: 2px; vertical-align: middle;border-radius: 5px; color:color:#6B3F2E}"
        , ".bcSelect{ background-color:#FFFFFF;height:26px;line-height:26px;border:1px solid #CCCCCC;color:Black;font-size:16px;    padding:4px;border-radius:5px}"
        , " .col1{ float:left; width:100px; }"
        , ".col2{ float:left; width:200px}"
        , ".col3{ float:left; width:200px}"
        , ".row{ clear:both; width : 500px; margin:10px 0px; padding-bottom:20px}"
    ];
    css = css.join("\n");
    if (typeof GM_addStyle != "undefined") GM_addStyle(css);
    else if (typeof PRO_addStyle != "undefined") PRO_addStyle(css);
    else if (typeof addStyle != "undefined") addStyle(css);
    else {
        var heads = document.getElementsByTagName("head");
        if (heads.length > 0) {
            var node = document.createElement("style");
            node.type = "text/css";
            node.appendChild(document.createTextNode(css));
            heads[0].appendChild(node);
        }
    }
}

/*Parse a string with a basic format (yyyyMMdd HHmmss) to a date object */
function getDateFromDateString(dateString, isUTCDate) {
    try {
        //This is for Javascript to understand format 'yymmdd hhmmmss'
        var year = dateString.substring(0, 4),
                month = dateString.substring(4, 6),
                day = dateString.substring(6, 8),
                hours = dateString.substring(9, 11),
                minutes = dateString.substring(11, 13),
                seconds = dateString.substring(13, 15);
        var date = new Date(year, month - 1, day, hours, minutes, seconds, "00"); // months are 0-based
        if (isUTCDate == true) { //Must convert the date from UTC/GMT to local time
            var n = date.getTimezoneOffset();
            date.setMinutes(date.getMinutes() - n);
        }
        return date;
    }
    catch (err) {
        return new Date(dateString);
    }
}

/** Encode a date : "yyyyMMdd"
*/
function encodeDate(d) {
    //debugger;
    var twoDigit = function (val) { if (val < 10) return "0" + val; else return val; };
    if (d && d.getMonth) return d.getFullYear().toString() + twoDigit((d.getMonth() + 1)) + twoDigit(d.getDate()); // + " " + twoDigit(d.getHours()) + twoDigit(d.getMinutes()) + twoDigit(d.getSeconds());
    else return null;
}


function downloadTorrent(id){
    if (!id) return;

    var urls = ["http://torcache.net/torrent/","http://torrage.com/torrent/","http://zoink.it/torrent/"],
        html = "<ul>",
        fileName = id+ ".torrent";
    $.each(urls,function(i,x){ 
        html += "<li><a href=\""+x+fileName+"\" target=\"_blank\" rel=\"nofollow\">"+x+fileName+"</a></li>";
    });
    html += "</ul>";
    
    $('#downloadModal,#downloadModalOverlay').show();
    $('#downloadModal .torrentContainer').empty().html(html);
  }


//--Application specific. Ensure Singleton, single location to set up the specific configs. Better to use that than using new Enbalaba.LocalStore()
Enbalaba.GetLocalStore = (function () {
    var _stores = []; //*Private*
    return function (name) {
        if (!_stores[name]) {
            var config = {};
            switch (name) {
                case "moviesInfo": config = { MaxProperties: 100 }; break;
                case "trackedSeries": config = { IsArray: true }; break;
            }
            _stores[name] = new Enbalaba.LocalStore(name, config);
        }
        return _stores[name];
    }
})();
//--------------
Enbalaba.LocalStore = function (name, config) {
    this.Name = name;
    var defaultConfig = { EmptyValue: {}, MaxTotalSize: 250000 };
    //Notes : 1 char = 2octets (Strings in JavaScript are UTF-16, so each character requires two bytes of memory)
    if (!config) config = {};
    else {
        if (config.IsArray == true) defaultConfig = { EmptyValue: [], MaxItems: 100, MaxTotalSize: 250000 }; //MaxTotalSize for arrays (usually used for MRU)== 500Ko        
    }
    this.Config = $.extend(defaultConfig, config);
};

Enbalaba.LocalStore.prototype = {

    _isSupported: !(typeof localStorage == 'undefined' || typeof JSON == 'undefined'),

    set: function (val) {
        if (this._isSupported) {
            if ($.isArray(val) && val.length > this.Config.MaxItems) {
                for (var i = 0, dif = val.length - this.Config.MaxItems; i < dif; i++) val.shift(); //remove X first elements
            }

            var s = JSON.stringify(val);

            if (s.length > this.Config.MaxTotalSize) return false; //todo: something more significant
            localStorage.setItem(this.Name, s);
            return true;
        }
    }

    /*Get the value associated with the store are. Can return null, except if Config.EmptyValue has been defined */
    , get: function () {
        if (this._isSupported) {
            var s = localStorage.getItem(this.Name);
            if (s != null && s != "") {
                return JSON.parse(s);
            }
            else if (this.Config.EmptyValue) return this.Config.EmptyValue;
        }
        if (this.Config.EmptyValue) return this.Config.EmptyValue;
        return null;
    }
};


Torrentz.GM.BobCatTorrentz.start();