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

  1. // ==UserScript==
  2. // @name Torrentz : The Bobcat add-on
  3. // @namespace http://torrentzBobCat
  4. // @homepage http://www.youtube.com/watch?v=1QyuIDw0CIw&feature=youtu.be
  5. // @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
  6. // @author CoolMatt
  7. // @version 1.6.1
  8. // @grant GM_xmlhttpRequest
  9. // @include *://torrentz.*
  10. // @match *://torrentz.com/*
  11. // @match *://torrentz.eu/*
  12. // @match *://torcache.net/*
  13.  
  14. // ==/UserScript==
  15. // @date 19 Jun 2013
  16. // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
  17.  
  18.  
  19. //Define the namespace
  20. var Torrentz = Torrentz || {};
  21. Torrentz.GM = {};
  22. Torrentz.GM.BobCatTorrentz = {};
  23.  
  24. Torrentz.GM.BobCatTorrentz = {
  25.  
  26. //Store info about movies of the page
  27. PageCache_movieInfo: {},
  28.  
  29. //Lookup table [torrentId -> movieKey] - as several torrents can point at the same movie info
  30. PageCache_lk_id_info: {},
  31.  
  32. start: function () {
  33. // Load and inject css
  34. initCss();
  35.  
  36. // Add the 'bobcat touch'
  37. this.addBadgeAndButtons();
  38.  
  39. //// Prevent scripts to bounce you to another page
  40. //window.onbeforeunload = function () {
  41. // return "Exit this page?";
  42. //};
  43.  
  44. // Hide visual spam
  45. $("div.cloud").hide();
  46.  
  47. // Load store
  48. var moviesStore = Enbalaba.GetLocalStore("moviesInfo"),
  49. moviesData = moviesStore.get(),
  50. that = this,
  51. results;
  52.  
  53. //Calculate cache size and clear it if too big
  54. this.checkCacheSize(moviesStore);
  55.  
  56. //Get rid of this incredibly annoying & ridiculous advertising banner
  57. $("body>iframe:first").hide();
  58.  
  59. // Append download modal
  60. $("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");
  61. $("#downloadModal,#downloadModalOverlay").click(function (e) { $("#downloadModal,#downloadModalOverlay").hide(); });
  62.  
  63. //Start processing of the rows
  64. results = $(".results");
  65.  
  66. results.find("h3:first").append("<span>|&nbsp</span><b title='IMDB Rating. Brought to you by the Torrentz Dominion Plugin'>Rating</b>");
  67. results.children("dl").each(function (index) {
  68. that.processRow($(this), moviesData, false);
  69. });
  70.  
  71. //Add events for when the row is clicked
  72. results.find("dt").click(function () {
  73. var dt = $(this),
  74. divDesc = dt.find(".movieDesc"),
  75. info = null,
  76. text, id, divQuality, lk, aElement;
  77.  
  78. if (divDesc.length == 0) {
  79. // First time the user clicks here
  80. // Init the description container
  81. aElement = dt.children("a:first");
  82. if (aElement.length == 0) return;
  83. id = aElement.attr("href").substr(1).toUpperCase();
  84. if (!id) return;
  85.  
  86. //Query cache to retrieve movie info
  87. if (that.PageCache_lk_id_info[id]) {
  88. info = that.PageCache_movieInfo[that.PageCache_lk_id_info[id]];
  89. }
  90.  
  91. if (info) {
  92. // Info found in cache
  93. text = "<div class='plot'><b>Plot</b>: " + info.Plot + "</div><div class='actorsInfo'> <b>Actors</b>: " + info.Actors + "</div>";
  94. }
  95. else {
  96. //No info > display the title
  97. text = aElement.attr("title");
  98. }
  99.  
  100. divDesc = $("<div class='movieDesc'>" + text + "</div>");
  101.  
  102. divQuality = $("<div class='divQuality'></div>");
  103. lk = $("<div class='hyperlink fleft'>See user comments</div>");
  104. lk.hover(function (e) {
  105. if ($(this).data('processed')) return;
  106. that.getQuality($(this.parentNode), aElement.attr("href")); e.stopPropagation(); $(this).hide();
  107. $(this).data('processed', true);
  108. });
  109.  
  110. divQuality.append(lk).append($("<img class='spinner fleft'></div><div class='qualityComments'><div/>").hide());
  111. divDesc.append(divQuality).hide();
  112. dt.append(divDesc);
  113. }
  114.  
  115. // Animate the pane
  116. if (divDesc.is(":hidden")) {
  117. divDesc.children().hide();
  118. divDesc.slideDown(200, function () { divDesc.children().show(); });
  119. dt.children(".expand").addClass("collapse");
  120. }
  121. else {
  122. divDesc.slideUp();
  123. dt.children(".expand").removeClass("collapse");
  124. }
  125. });
  126.  
  127. // Once a day, the serie tracker check if there are new episodes available
  128. this.checkForNewEpisodes();
  129. }
  130.  
  131. , processRow: function (row, moviesData, isIFrameDownload) {
  132. if (!row) return;
  133. var tags = null,
  134. lk = row.find("dt>a"),
  135. name, torrentId;
  136.  
  137. if (lk.length > 0) {
  138. var id = lk.attr("href").substr(1).toUpperCase(), //Get id from href
  139. info = lk.parent().text(),
  140. index = info.indexOf('\u00BB'), //Look for the utf-8 character >> in the row
  141. rightCol = row.find("dd"),
  142. torrentId = lk.attr("href").substr(1).toUpperCase(),
  143. type, year;
  144.  
  145. rightCol.addClass("actionAndStatsColumns");
  146. row.find("dt").css("width", "600px");
  147.  
  148. if (index > -1) {
  149. tags = info.substr(index + 1);
  150. name = info.substr(0, index);
  151. }
  152. lk.attr("title", tags ? "Tags: " + tags : info).parent().html(lk); //Remove info to make some room
  153.  
  154. type = this.getType(name, tags);
  155. if (this.isTVSerie(name)) {
  156. type = "tv"; //extra verification as sometimes a tv serie is not tagged as such
  157. }
  158. if (type == "movie") {
  159. if (!moviesData) return;
  160. lk.css('color', '#3F14FF');
  161.  
  162. var yearIndex = name.search(/\s[0-9]{4}\s/); //Year is mandatory
  163. if (yearIndex != -1) {
  164. year = name.substr(yearIndex + 1, 4);
  165. name = name.substr(0, yearIndex);
  166. //console.log(name + ":" + year);
  167.  
  168. info = moviesData[(name + year).toLowerCase()]; //Search in cache
  169. if (info) {
  170. this.PageCache_movieInfo[(name + year).toLowerCase()] = info;
  171. this.PageCache_lk_id_info[id] = (name + year).toLowerCase();
  172.  
  173. lk.text(name + " " + year);
  174. rightCol.append($("<a class='rateBox' " + (info.ImdbID && info.ImdbID != "" ? "target='_blank' href='http://www.imdb.com/title/" + info.ImdbID + "'" : "") + " >" + info.ImdbRating + "</a>"));
  175. }
  176. else {
  177. this.searchIMDBinfo(
  178. name,
  179. year,
  180. torrentId,
  181. function (movieData) {
  182. // Add rating link in the DOM
  183. rightCol.append($("<a class='rateBox' " +
  184. (movieData.ImdbID ? "target='_blank' href='http://www.imdb.com/title/" + movieData.ImdbID + "'" : "") +
  185. " >" + movieData.ImdbRating + "</a>"));
  186. });
  187. }
  188. }
  189. }
  190.  
  191. else if (type == "tv") {
  192. lk.css('color', 'black' /*'#47D4FF'*/);
  193. }
  194. else {
  195. lk.css('color', '#555');
  196. }
  197. //Add download link
  198. rightCol.prepend('<a class="downloadLink hyperlink" style="float:left" href="http://torcache.net/torrent/' + id + '.torrent" target="_blank" rel="nofollow">Download</a>');
  199. //
  200. lk.parent().prepend("<div class='expand fleft'></div>");
  201. }
  202. }
  203.  
  204. , getType: function (name, tags) {
  205. if (tags.indexOf("movies") > -1) return "movie";
  206. else if (tags.indexOf("tv") > -1) return "tv";
  207. else if (tags.indexOf("games") > -1) return "game";
  208. }
  209. /*As sometimes the tv serie is not tagged as such. This test will help catch those ones*/
  210. , isTVSerie: function (fullName) {
  211. return (
  212. new RegExp(/[sS][0-9]+[eE][0-9]+/).test(fullName)
  213. || new RegExp(/[0-9]+[x][0-9]+/).test(fullName)
  214. || new RegExp(/season[\s]?[0-9]{1,2}[\s]/i).test(fullName)
  215. );
  216. }
  217.  
  218. /* Make a query to the IMDB database to get data for the specified movie*/
  219. , searchIMDBinfo: function (name, year, torrentId, successCallback, isRetry) {
  220. var url = encodeURI("http://www.omdbapi.com/?t=" + name + "&y=" + year + "&plot=full&r=json"),
  221. that = this;
  222. // Cross the same origin policy boundaries: Torrentz (https) to IMDB (http)
  223. GM_xmlhttpRequest({
  224. method: "GET",
  225. url: url,
  226. onload: function (response) {
  227. var obj = $.parseJSON(response.responseText),
  228. moviesStore, moviesData, movieData;
  229.  
  230. if (obj) {
  231. if (obj.imdbRating) {
  232. // That's a legit object
  233. moviesStore = Enbalaba.GetLocalStore("moviesInfo");
  234. moviesData = moviesStore.get();
  235. if (moviesData) {
  236. refName = (name + "&y=" + year).toLowerCase();
  237. movieData = {
  238. ImdbRating: obj.imdbRating,
  239. Plot: obj.Plot,
  240. Actors: obj.Actors,
  241. ImdbID: obj.imdbID,
  242. Poster: obj.Poster,
  243. Genre: obj.Genre,
  244. Runtime: obj.Runtime,
  245. Metascore: obj.Metascore
  246. };
  247.  
  248. // Store in cache
  249. that.PageCache_movieInfo[refName] = movieData;
  250. // Link the torrent id to a movie data key in the movie cache (several torrent can point at the same key)
  251. that.PageCache_lk_id_info[torrentId] = refName;
  252.  
  253. // Save in store
  254. moviesData[refName] = movieData;
  255. moviesStore.set(moviesData);
  256. }
  257. if (typeof successCallback === "function") {
  258. successCallback(movieData);
  259. }
  260. } else if (obj.Response == "False") {
  261. if (isRetry != true) {
  262. //Tries a second search, if applicable
  263. var name2 = name.replace(/thats/gi, "that's")
  264. .replace(/it's/gi, "its")
  265. .replace(/spiderman/i, "spider man")
  266. .replace(/extended$/i, "");
  267. if (name2 != name) {
  268. that.searchIMDBinfo(name2, year, torrentId, successCallback, true);
  269. return;
  270. }
  271. }
  272. // Display error in console
  273. console.info(name + ": " + obj.Error);
  274. }
  275. }
  276. }
  277. });
  278.  
  279.  
  280. return;
  281. }
  282.  
  283. , tempID: 0
  284. , getQuality: function ($qualityDiv, url) {
  285.  
  286. url = "https://torrentz.eu" + url;
  287. //console.info(url);
  288. $qualityDiv.find(".spinner").show();
  289. var id = "divComment" + (this.tempID++);
  290. $("<div style='display:none' id='" + id + "'></div>").load(url, function (data) {
  291. var comments = $(data).find("div.comment .com"),
  292. qualityComments = [];
  293.  
  294. for (var i = 0, comment; i < comments.length; i++) {
  295. comment = $(comments[i]).text();
  296. if (comment.length > 400) comment = comment.substr(0, 400) + " (...)";
  297. qualityComments.push(comment);
  298. }
  299. $qualityDiv.find(".spinner").hide();
  300. $qualityDiv.find(".qualityComments").show().html("<b>User comment:</b><br/>" + qualityComments.join("<br/>"));
  301. $(id).empty(); //free memory of the temporary div
  302. });
  303. }
  304.  
  305. /*
  306. * Calculate cache size and clear it if too big
  307. */
  308. , checkCacheSize: function (moviesStore) {
  309. var cacheSize,
  310. k,
  311. that = this,
  312. moviesData = moviesStore.get();
  313.  
  314. try {
  315. //Works in all recent browsers
  316. cacheSize = Object.keys(moviesData).length;
  317. }
  318. catch (err) {
  319.  
  320. cacheSize = 0;
  321. for (k in moviesData) {
  322. if (moviesData.hasOwnProperty(k)) cacheSize++;
  323. }
  324. }
  325. console.log("Bobcat - Cache size:" + cacheSize);
  326. if (cacheSize > 150) {
  327. //Clear the cache from time to time
  328. moviesStore.set({});
  329. console.info("Bobcat - Movie cache cleared");
  330. }
  331. }
  332.  
  333. /*
  334. * Add badges and buttons
  335. */
  336. , addBadgeAndButtons: function () {
  337. //Add bobcat badge in the top banner
  338. $("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>");
  339.  
  340. //Add serie tracker button
  341. var btST = $("<button type='button' id='btSerieTracker' class='bcButton bobcatStamp'>Serie Tracker</button>"),
  342. that = this;
  343. btST.click(function () { that.onclick_btSerieTracker(); });
  344. $("div.results h2").append(btST);
  345. //this.onclick_btSerieTracker(); //~~ Uncomment when developping serie tracker
  346. }
  347.  
  348. //---------------------
  349. //-----SERIE TRACKER---
  350. //---------------------
  351. , _ddSeasonHTML: ""
  352. , _ddEpisodeHTML: ""
  353. , onclick_btSerieTracker: function () {
  354. if (!this.SerieTrackerMode) {
  355. $("div.results h3").nextAll().hide();
  356. $("div.recent").hide();
  357. $("#serieContainer").show();
  358. $("#btSerieTracker").text("Return to List");
  359. if (this.SerieTrackerMode == null) { //init serie tracker
  360.  
  361. var serieTrakerLastCheckedStore = Enbalaba.GetLocalStore("serieTrackerLastCheck")
  362. , serieTrakerLastChecked = serieTrakerLastCheckedStore.get();
  363. serieTrakerLastCheckedStore.set({ FoundNewEpisodes: false, LastChecked: encodeDate(new Date()) });
  364. delete serieTrakerLastCheckedStore;
  365.  
  366. var serieStore = Enbalaba.GetLocalStore("trackedSeriesInfo"), serieInfo = serieStore.get();
  367. if (!serieInfo.Ids) serieStore.set({ Ids: [], CurrentId: 1 });
  368.  
  369. $("div.results").after("<div class='results' id='serieContainer'><dl></dl><div id='addSerieContainer' style='float:left;position:relative;'> <h2>Track a new Serie</h2>"
  370. + "<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>"
  371. + "<div class='row'><div class='col1'><label>Season</label></div><div class='col2'><select id='st_ddSeasonNew' class='bcSelect'><option></option></select></div></div>"
  372. + "<div class='row'><div class='col1'><label>Episode</label></div><div class='col2'><select id='st_ddEpisodeNew' class='bcSelect'><option></option></select></div></div>"
  373. + "<button type='button' id='btAddSerie' class='bcButton'>Add This Serie</button><span id='st_lblOutput' style='color:red'></span><br/>"
  374. + "<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>"
  375. //+ "<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/>"
  376. + "</div>"
  377. + "<div style='clear:both;cursor:pointer' id='st_btDeleteAll' >Delete All Tracked Series<div/>"
  378. + "<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>");
  379. //$("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");
  380.  
  381. /*Populate dropdowns*/
  382. var i, htmlddSeasons = "", htmlddEpisodes = "";
  383. for (i = 1; i < 16; i++) htmlddSeasons += "<option value='" + i + "'>" + i + "</option>";
  384. for (i = 1; i < 31; i++) htmlddEpisodes += "<option value='" + i + "'>" + i + "</option>";
  385. $("#st_ddSeasonNew").html(htmlddSeasons);
  386. $("#st_ddEpisodeNew").html(htmlddEpisodes);
  387.  
  388.  
  389. htmlddSeasons = ""; //blank variable because of closures
  390. htmlddEpisodes = "";
  391.  
  392. /*Display Series*/
  393. this.displayTrackedSeries();
  394.  
  395. /*Search for new*/
  396. this.searchForNewEpisodes(this.episodeFoundCallback);
  397.  
  398. /*Add Serie event*/
  399. var that = this;
  400. $("#btAddSerie").click(function () {
  401. var name = $("#st_tbNameNew").val();
  402. if ($.trim(name) == "") {
  403. $("#st_lblOutput").text("Enter a Name");
  404. }
  405. else { //---ADDITION
  406. var store = Enbalaba.GetLocalStore("trackedSeriesInfo"), serieInfo = store.get()
  407. , id = serieInfo.CurrentId;
  408. serieInfo.CurrentId = id + 1;
  409. serieInfo.Ids.push(id);
  410. store.set(serieInfo);
  411.  
  412. var store = Enbalaba.GetLocalStore("ts_" + id)
  413. , isFinished = $("#cbIsFinishedSeason").is(":checked")
  414. , serie = { Name: name, Season: parseInt($("#st_ddSeasonNew").val(), 10), Current: { e: parseInt($("#st_ddEpisodeNew").val(), 10) }, History: [], id: id };
  415. if (isFinished) {
  416. serie.isFinished = true;
  417. serie.NbTotEpisodes = parseInt($("#tbSeasonNbEpisodes").val(), 10);
  418. if (isNaN(serie.NbTotEpisodes)) { alert("Enter a valid number of episodes"); return; }
  419. }
  420. store.set(serie);
  421.  
  422. that.displayTrackedSeries();
  423. that.searchForNewEpisodes(that.episodeFoundCallback);
  424. //that.displayTrackedSeries(serieStore);
  425. $("#st_lblOutput").text("");
  426. }
  427. });
  428.  
  429. $("#st_lblSuggestion").click(function () {
  430. $("#st_tbNameNew").val($(this).text());
  431. });
  432. // Logic serie name live suggestions
  433. $("#st_tbNameNew").keypress(function (e) {
  434. 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'
  435. var txt = $(this).val();
  436. if (txt.length >= 3) {
  437. var url = encodeURI("https://torrentz.eu/suggestions.php?q=" + $.trim(txt));
  438. $("<span></span>").css("display", "none").load(url, function (data) {
  439. var res = $.parseJSON(data);
  440.  
  441. if (res && res.length == 2 && res[1] != null && res[1].length > 0) {
  442. console.log(res[1][0]);
  443. $("#st_lblSuggestion").text(res[1][0]);
  444. }
  445. else $("#st_lblSuggestion").val("-");
  446. });
  447. }
  448. });
  449. $("#st_btDeleteAll").click(function () {
  450. if (confirm("Do you want to delete all the currently tracked series ?")) {
  451. Enbalaba.GetLocalStore("trackedSeriesInfo").set({ Ids: [], CurrentId: 1 });
  452. that.displayTrackedSeries();
  453. }
  454.  
  455. });
  456.  
  457. }
  458. this.SerieTrackerMode = true;
  459.  
  460. }
  461. else {
  462. // Back to the list of torrents
  463. this.SerieTrackerMode = false;
  464. $("div.results h3,div.recent").nextAll().not("#downloadModal,#downloadModalOverlay").show();
  465. $("#serieContainer").hide();
  466. $("#btSerieTracker").text("Serie Tracker");
  467. }
  468. }
  469.  
  470. , displayTrackedSeries: function () {
  471. var serieIds = Enbalaba.GetLocalStore("trackedSeriesInfo").get().Ids;
  472. var dl = $("#serieContainer dl");
  473.  
  474. dl.empty();
  475. for (var i = 0, serie, id; i < serieIds.length; i++) {
  476. this.displaySerie(serieIds[i]);
  477. }
  478. }
  479.  
  480. , displaySerie: function (serieId) {
  481. var serie = Enbalaba.GetLocalStore("ts_" + serieId).get(), time
  482. , hasNew = false;
  483.  
  484. if (!serie.History) serie.History = [];
  485.  
  486. var html = "<div class='trackedSerieContainer' data-id='" + serie.id + "'>"
  487. + "<div class='trackedSerieHeader'>"
  488. + "<div class='st_name st-col1'>"
  489. + "<div class='deleteIcon fleft' data-id='" + serie.id + "' title='delete' style='margin-right:2px'></div>"
  490. + serie.Name + "</div>"
  491. + "<div class='st-col2'><b>Season " + serie.Season + "</b></div>"
  492. + "<div class='episode st-col3'><b>" + (serie.History.length > 0 ? "Episode " + serie.History[0].e : " - ") + "</b></div>"
  493. + "<div class='st-col4'>" + (serie.isFinished ? "" : "Tracking: On") + "</div>"
  494. + "</div>"
  495. + "<div class='trackerSerieBody' style='display:none'>";
  496.  
  497. //History
  498.  
  499. if (serie.History.length == 0) {
  500. html += "<div style='margin-left:50px'>No results found</div>";
  501. }
  502. else {
  503. for (var j = 0, h, d, l = serie.History.length; j < l; j++) {
  504. h = serie.History[j];
  505. d = (h.d ? new Date(new Date() - getDateFromDateString(h.d)) : null);
  506.  
  507. if (d) {
  508. if (d.getMonth() > 0) {
  509. dif = d.getMonth() + " month" + (d.getMonth() == 1 ? "" : "s") + " ago";
  510. }
  511. else {
  512. time = d.getDate() - 1;
  513. if (time == 0) {
  514. time = "today";
  515. hasNew = true;
  516. }
  517. else time += " days ago";
  518. }
  519. }
  520. 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'>"
  521. + (h.f ? "<span class='st-btShowLk hyperlink'>Show Links<span>" : "<span>Not found</span>") + "</div>"
  522. + "<div class='st-col4'>" + time + "</div>"
  523. + "</div> ";
  524. }
  525. }
  526. html += "</div></div>";
  527. var el = $("#serieContainer .trackedSerieContainer").filter(function () { return $(this).data("id") == serieId; }), newEl = $(html);
  528. delete html; //for the closure
  529.  
  530. if (el.length > 0) el.empty().replaceWith(newEl);
  531. else $("#serieContainer dl").append(newEl);
  532.  
  533. var that = this;
  534.  
  535. newEl.find("div.deleteIcon").click(function (e) { e.stopImmediatePropagation(); that.onclick_deleteTrackedSerie(this); });
  536. newEl.find("span.st-btShowLk").click(function (e) { that.onclick_showLinks(this); });
  537. newEl.find("div.trackedSerieHeader").addClass(hasNew ? "hasNew" : "").click(function (e) { that.onclick_serieHeader(this); });
  538.  
  539. }
  540.  
  541. // Once a day, the serie tracker check if there are new episodes available.
  542. , checkForNewEpisodes: function () {
  543. var serieTrakerLastCheckedStore = Enbalaba.GetLocalStore("serieTrackerLastCheck"),
  544. serieTrakerLastChecked = serieTrakerLastCheckedStore.get(),
  545. today = encodeDate(new Date());
  546.  
  547. if (!serieTrakerLastChecked || serieTrakerLastChecked.LastChecked != today) {
  548. //Start daily search
  549. serieTrakerLastCheckedStore.set({ FoundNewEpisodes: false, LastChecked: today });
  550. this.searchForNewEpisodes(this.episodeFoundCallback_2);
  551. }
  552. else if (serieTrakerLastChecked.FoundNewEpisodes) {
  553. //A search was previously done, but the user didn't go to the serie tracker. Add a specific icon to signal new episode available.
  554. $("#btSerieTracker").css("color", "Blue").removeClass("bobcatStamp").addClass("bobcatStamp2");
  555. }
  556. }
  557.  
  558. , searchForNewEpisodes: function (callback) {
  559.  
  560. var serieInfo = Enbalaba.GetLocalStore("trackedSeriesInfo").get()
  561. , that = this
  562. , today = encodeDate(new Date());
  563.  
  564. if (serieInfo && serieInfo.Ids) {
  565. for (var i = 0, store, serie, search, ids = serieInfo.Ids; i < ids.length; i++) {
  566. //serie = series[i];
  567. store = Enbalaba.GetLocalStore("ts_" + ids[i]);
  568. serie = store.get();
  569. if (!serie.isFinished || serie.History.length == 0) {
  570.  
  571. this.lookForEpisode(serie, serie.Current.e, callback, store);
  572. }
  573. }
  574. }
  575. }
  576.  
  577. , lookForEpisode: function (serie, episode, callback, store) {
  578. var search = serie.Name + " S" + (serie.Season < 10 ? "0" : "") + serie.Season + "E" + (episode < 10 ? "0" : "") + episode
  579. , url = encodeURI("https://torrentz.eu/search?f=" + search)
  580. , that = this;
  581. //console.info(url);
  582. search = search.toLowerCase();
  583. $("<span></span>").css("display", "none").load(url, function (data) {
  584. var rows = $(this).find("div.results dl"), results = [];
  585. //console.info(rows.length + " results");
  586.  
  587. for (var i = 0, $row, txt; i < rows.length; i++) {
  588. $row = $(rows[i]);
  589. txt = $row.find("dt").text().toLowerCase();
  590. if (txt.indexOf(search) > -1) { //we need to be sure the results returned are related to the search
  591. results.push($row);
  592. }
  593. }
  594. if (callback) callback(serie, episode, results, store, that);
  595. $(this).empty(); //free memory of the temporary div
  596. });
  597. }
  598.  
  599. , episodeFoundCallback: function (s, e, results, store, context) {
  600.  
  601. if (results.length < 1) { //NO EPISODE FOUND
  602. if (s.isFinished && e < s.NbTotEpisodes) {
  603. s.History.splice(0, 0, { e: e, f: false, d: encodeDate(new Date()) }); //insert new entry in history - at the begining
  604. s.Current.e = parseInt(s.Current.e, 10) + 1;
  605. store.set(s);
  606. context.displaySerie(s.id); //redisplay the result for the serie
  607. context.lookForEpisode(s, e + 1, context.episodeFoundCallback, store); //rec
  608. }
  609. }
  610. else { //EPISODE FOUND
  611. //console.info("New episode Found for " + s.Name);
  612. //New episode found
  613. s.History.splice(0, 0, { e: e, f: true, d: encodeDate(new Date()) }); //insert new entry in history - at the begining
  614. s.Current.e = parseInt(s.Current.e, 10) + 1;
  615. store.set(s);
  616. context.displaySerie(s.id); //redisplay the result for the serie
  617. var el = $("#serieContainer .trackedSerieContainer").filter(function () { return $(this).data("id") == s.id; });
  618. el.find(".trackedSerieHeader").css("color", "Blue");
  619.  
  620. context.lookForEpisode(s, s.Current.e, context.episodeFoundCallback, store); //rec
  621. }
  622. }
  623.  
  624. // Function called the first time the user visit torrentz in the day,
  625. // even if he hasn't entered the Section Tracker section
  626. , episodeFoundCallback_2: function (s, e, results) {
  627. if (results.length > 0) {
  628. var serieTrakerLastCheckedStore = Enbalaba.GetLocalStore("serieTrackerLastCheck")
  629. , serieTrakerLastChecked = serieTrakerLastCheckedStore.get();
  630.  
  631. $("#btSerieTracker").css("color", "Blue"); //.text("Serie Tracker ( New Episodes! )");
  632. serieTrakerLastCheckedStore.set({ FoundNewEpisodes: true, LastChecked: encodeDate(new Date()) });
  633. }
  634. }
  635.  
  636. // Handler for when a followed-serie header is clicked
  637. , onclick_serieHeader: function (headerEl) {
  638. headerEl = $(headerEl);
  639. var body = headerEl.parent().children(".trackerSerieBody");
  640. if (body.is(":visible")) {
  641. body.slideUp(200, function () { });
  642. }
  643. else body.slideDown(200, function () { });
  644. }
  645. // Handler for when the user clicks on "Show Links"
  646. , onclick_showLinks: function (lk) {
  647.  
  648. lk = $(lk);
  649. var row = lk.parent().parent()
  650. , that = this
  651. , data = row.data("seriedata"), dataParts; //row store some data : serieId_episodeNumber
  652. var existingBox = row.parent().children(".st-link-container").filter(function () { return $(this).data("seriedata") == data; });
  653. if (existingBox.length != 0) {
  654. existingBox.remove();
  655. }
  656. else {
  657. if (data) {
  658. dataParts = data.split('_'); //format: "serieId _episodeNumber"
  659. var serie = this.getSerieFromStore(dataParts[0]);
  660.  
  661. this.lookForEpisode(serie, dataParts[1], function (s, e, results) {
  662.  
  663. var html = "<div class='st-link-container' data-seriedata='" + data + "'>",
  664. max = (results.length < 3 ? results.length : 3);
  665. for (var i = 0; i < max; i++) { //show only 3 first
  666. r = results[i];
  667. that.processRow(r, null, true);
  668. html += "<div class='st-row'><div class='st-link-col1'>" + r.find("dt").html() + "</div>";
  669. html += "<div class='st-link-col2'>" + r.find("dd").html() + "</div> ";
  670. html += "</div>";
  671. }
  672. var linkContainer = $(html);
  673. row.after(linkContainer);
  674. });
  675. }
  676. }
  677. }
  678. , onclick_deleteTrackedSerie: function (element) {
  679. //serie deletion
  680.  
  681. var name = $(element).parent().text(), id = $(element).data('id');
  682. if (confirm("Are you sure you want to delete the entry for '" + name + "'")) {
  683.  
  684. var store = Enbalaba.GetLocalStore("trackedSeriesInfo"), serieInfo = store.get();
  685. serieInfo.Ids = $.grep(serieInfo.Ids, function (value) { return value != id; });
  686. store.set(serieInfo);
  687.  
  688. store = Enbalaba.GetLocalStore("ts_" + id); //.get();
  689. store.set({}); //TODO : real deletion
  690. this.displayTrackedSeries();
  691. //console.info("deleted");
  692. }
  693. //else console.info("Not deleted");
  694. }
  695.  
  696. , getSerieFromStore: function (id) {
  697. var serieStore = Enbalaba.GetLocalStore("ts_" + id), serie = serieStore.get();
  698. return serie;
  699. }
  700.  
  701. }
  702.  
  703. //Basic Class to deal with the localstorage
  704. Enbalaba = {};
  705.  
  706. //Add CSS
  707. function initCss() {
  708. var css = [
  709. " .rateBox{ margin-left:10px;position:relative;bottom:0; cursor: pointer; padding:1px; background-color:#EEE; border:#AAA solid 1px; border-radius:4px}"
  710. , ".bobcatLogo{background:transparent url(http://i.imgur.com/MlVVyzX.png) no-repeat scroll 0 0}"
  711. , "#bobcatLogoContainer a{color:White}"
  712. , "#bobcatLogoContainer a:hover{color:White}"
  713. , ".bobcatStamp{background:transparent url(http://i.imgur.com/tDWKswF.png) no-repeat scroll 0 0}"
  714. , ".bobcatStamp2{background:transparent url(http://i.imgur.com/n7tvk8I.png) no-repeat scroll 0 0}"
  715. , "#bobcatLogoContainer{color:White;height:30px;width:200px;float:left;padding-left:50px;padding-top:5px; margin-top:10px;font-size:12px}"
  716. , ".downloadLink{margin-right:20px}"
  717. , ".moreLk{padding-left:30px;cursor:pointer}"
  718. , ".movieDesc{width:530px;margin:10px 0px 40px 0px;color:Black;white-space:normal}"
  719. , ".fleft{ float:left}"
  720. , "dd.actionAndStatsColumns{ width:480px !important; overflow:hidden;}"
  721. , "dt:hover{ background-color:#EEE}"
  722. , ".qualityComments{float:clear}"
  723. , ".spinner{ background:url(http://www.andrewdavidson.com/articles/spinning-wait-icons/wait16trans.gif) no-repeat left center;width: 16px;height: 16px}"
  724. , ".actorsInfo,.qualityComments,.divQuality,.plot{ margin-top:11px;margin-bottom:5px; font-size:12px;font-family:Verdana,Tahoma,sans-serif}"
  725. , "#pluginZoneContainer{ position:absolute; left: 210px; top:10px; width: 200px; height: 200px;background-color:Gray}"
  726. , ".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
  727. , ".deleteIcon{ background:transparent url(http://i.imgur.com/4RjuUFU.png) no-repeat scroll 0 0; width:15px; height: 13px; position: relative; top:3px}"
  728. , ".downloadIcon{ background:transparent url(http://i.imgur.com/7Jkx1N9.png) no-repeat scroll 0 0; width:17px; height: 18px; position: relative; top:3px}"
  729. , ".deleteIcon:hover{ background-color:#CCC}"
  730. , ".collapse{ background-image:url(http://i.imgur.com/apcKFJ5.png)}"//arrow2.png
  731. , "#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}",
  732. , "#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}",
  733. , ".torrentContainer{white-space: nowrap; overflow: hidden; margin: 20px 0}"
  734. , ".torrentContainer li{ margin-bottom: 15px}"
  735.  
  736. , ".btSerieTrackerHighlight{color:Yellow !important}"
  737. , "#btSerieTracker{ padding-left:40px;margin-left:20px; background-color:White;background-position:3px 3px}"
  738. , "#addSerieContainer{ width:50%; border: 1px solid #B5B8C8; margin: 30px 0px; padding:20px; border-radius: 15px}"
  739. , "#st_tbNameNew{ width : 150px}"
  740. , "#btAddSerie{ margin: 10px 0px}"
  741. , "#cbIsFinishedSeason{margin-right:7px}"
  742. , "#st_lblSuggestion{ color:Grey; font-size:11px;cursor:pointer}"
  743. , ".trackedSerieHeader{ margin: 15px 0px}"
  744. , ".trackedSerieHeader,.st-row{clear:both;width:100%;font-size:12px;height:15px}"
  745. , ".trackedSerieHeader>div,.st-row>div{ margin-right:30px;float:left}"
  746. , ".st-col1{ width:200px}"
  747. , ".st-col2,.st-col3{ width:100px}"
  748. , ".st-link-col1{width:500px}"
  749. , ".st_name{font-weight:bold}"
  750. , " .st-link-container{margin:10px; border:1px solid #AAA;padding:20px}"
  751. , ".st-link-col2 span{ margin-right:10px}"
  752. , ".st-link-col2 .u{ font-weight:bold}"
  753. , "div.trackedSerieHeader{cursor:pointer}"
  754. , "div.trackedSerieHeader:hover{ background-color:#EEE}"
  755. , ".hasNew{color:Blue}"
  756.  
  757. //Generic
  758. , ".hyperlink{color:#0066EE;text-decoration:none;cursor:pointer;text-decoration:underline}"
  759. , ".bcButton{color:#6B3F2E; border-radius: 6px; border: 1px solid #6B3F2E; height:25px; padding-bottom:1px; min-width:80px; font-weight:bold;cursor:pointer}"
  760. , ".bcButton:hover{color:#AA3F2E; }"
  761. , ".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}"
  762. , ".bcSelect{ background-color:#FFFFFF;height:26px;line-height:26px;border:1px solid #CCCCCC;color:Black;font-size:16px; padding:4px;border-radius:5px}"
  763. , " .col1{ float:left; width:100px; }"
  764. , ".col2{ float:left; width:200px}"
  765. , ".col3{ float:left; width:200px}"
  766. , ".row{ clear:both; width : 500px; margin:10px 0px; padding-bottom:20px}"
  767. ];
  768. css = css.join("\n");
  769. if (typeof GM_addStyle != "undefined") GM_addStyle(css);
  770. else if (typeof PRO_addStyle != "undefined") PRO_addStyle(css);
  771. else if (typeof addStyle != "undefined") addStyle(css);
  772. else {
  773. var heads = document.getElementsByTagName("head");
  774. if (heads.length > 0) {
  775. var node = document.createElement("style");
  776. node.type = "text/css";
  777. node.appendChild(document.createTextNode(css));
  778. heads[0].appendChild(node);
  779. }
  780. }
  781. }
  782.  
  783. /* Parse a string with a basic format (yyyyMMdd HHmmss) to a date object */
  784. function getDateFromDateString(dateString, isUTCDate) {
  785. try {
  786. //This is for Javascript to understand format 'yymmdd hhmmmss'
  787. var year = dateString.substring(0, 4),
  788. month = dateString.substring(4, 6),
  789. day = dateString.substring(6, 8),
  790. hours = dateString.substring(9, 11),
  791. minutes = dateString.substring(11, 13),
  792. seconds = dateString.substring(13, 15);
  793. var date = new Date(year, month - 1, day, hours, minutes, seconds, "00"); // months are 0-based
  794. if (isUTCDate == true) { //Must convert the date from UTC/GMT to local time
  795. var n = date.getTimezoneOffset();
  796. date.setMinutes(date.getMinutes() - n);
  797. }
  798. return date;
  799. }
  800. catch (err) {
  801. return new Date(dateString);
  802. }
  803. }
  804.  
  805. /* Encode a date : "yyyyMMdd" */
  806. function encodeDate(d) {
  807. var twoDigit = function (val) { if (val < 10) return "0" + val; else return val; };
  808. if (d && d.getMonth) return d.getFullYear().toString() + twoDigit((d.getMonth() + 1)) + twoDigit(d.getDate()); // + " " + twoDigit(d.getHours()) + twoDigit(d.getMinutes()) + twoDigit(d.getSeconds());
  809. else return null;
  810. }
  811.  
  812. function showDownloadDialog(id) {
  813. if (!id) return;
  814.  
  815. var urls = ["http://torcache.net/torrent/", "http://torrage.com/torrent/", "http://zoink.it/torrent/"],
  816. html = "<ul>",
  817. fileName = id + ".torrent",
  818. template = "<li><a href=\"{url}\" target=\"_blank\" rel=\"nofollow\">{url}</a></li>";
  819.  
  820. $.each(urls, function (i, x) {
  821. html += template.replace(/{url}/g, x + fileName);
  822. });
  823. html += "</ul>";
  824. // Show modal and overlay
  825. $('#downloadModal,#downloadModalOverlay').show();
  826. // Empty and populate modal
  827. $('#downloadModal .torrentContainer').empty().html(html);
  828. }
  829.  
  830.  
  831. //--Application specific. Ensure Singleton, single location to set up the specific configs. Better to use that than using new Enbalaba.LocalStore()
  832. Enbalaba.GetLocalStore = (function () {
  833. var _stores = []; //*Private*
  834. return function (name) {
  835. if (!_stores[name]) {
  836. var config = {};
  837. switch (name) {
  838. case "moviesInfo": config = { MaxProperties: 100 }; break;
  839. case "trackedSeries": config = { IsArray: true }; break;
  840. }
  841. _stores[name] = new Enbalaba.LocalStore(name, config);
  842. }
  843. return _stores[name];
  844. }
  845. })();
  846. //--------------
  847. Enbalaba.LocalStore = function (name, config) {
  848. this.Name = name;
  849. var defaultConfig = { EmptyValue: {}, MaxTotalSize: 250000 };
  850. //Notes : 1 char = 2octets (Strings in JavaScript are UTF-16, so each character requires two bytes of memory)
  851. if (!config) config = {};
  852. else {
  853. if (config.IsArray == true) defaultConfig = { EmptyValue: [], MaxItems: 100, MaxTotalSize: 250000 }; //MaxTotalSize for arrays (usually used for MRU)== 500Ko
  854. }
  855. this.Config = $.extend(defaultConfig, config);
  856. };
  857.  
  858. Enbalaba.LocalStore.prototype = {
  859.  
  860. _isSupported: !(typeof localStorage == 'undefined' || typeof JSON == 'undefined'),
  861.  
  862. set: function (val) {
  863. if (this._isSupported) {
  864. if ($.isArray(val) && val.length > this.Config.MaxItems) {
  865. for (var i = 0, dif = val.length - this.Config.MaxItems; i < dif; i++) val.shift(); //remove X first elements
  866. }
  867.  
  868. var s = JSON.stringify(val);
  869.  
  870. if (s.length > this.Config.MaxTotalSize) return false; //todo: something more significant
  871. localStorage.setItem(this.Name, s);
  872. return true;
  873. }
  874. }
  875.  
  876. /*Get the value associated with the store are. Can return null, except if Config.EmptyValue has been defined */
  877. , get: function () {
  878. if (this._isSupported) {
  879. var s = localStorage.getItem(this.Name);
  880. if (s != null && s != "") {
  881. return JSON.parse(s);
  882. }
  883. else if (this.Config.EmptyValue) return this.Config.EmptyValue;
  884. }
  885. if (this.Config.EmptyValue) return this.Config.EmptyValue;
  886. return null;
  887. }
  888. };
  889.  
  890. Torrentz.GM.BobCatTorcache = {
  891. start: function () {
  892. console.log('TORCACHE');
  893. var url = window.location.href,
  894. ok = true;
  895. $('center').hide();
  896.  
  897. // The process is a bit tricky because of some proxy rules
  898. // When we hit the page from outside, the webpage is rendered, when the same url is hit from inside Torcache,
  899. // the file is actually downloaded, and no page redirection is triggered, except if the torrent file isn't found.
  900. //
  901.  
  902. if (url.indexOf('?ok') === -1) {
  903. // First time we hit the page
  904. // Try download the file. If file not found, an immediate redirection will occur. If the file is found, no redirection will be done
  905. window.location = window.location.href + '?ok';
  906.  
  907. // For successes. Let's go away now
  908. setTimeout(function () { window.location = 'http://torcache.net/?ok=1'; }, 1000);
  909.  
  910. } else if (url.indexOf('ok=') === -1) {
  911. // Most likely a 404 if we end up here
  912. ok = $('h1').text().indexOf('404') === -1;
  913. if (!ok) {
  914. // For failures
  915. window.location = 'http://torcache.net/?ok=' + (ok ? 1 : 0);
  916. }
  917. } else {
  918. $('.container-fluid').hide();
  919. if (url.indexOf('ok=1') !== -1){
  920. $('body').append('<h2>Bobcat: Download successful. You can now close this window</h2>');
  921. } else if(url.indexOf('ok=0') !== -1){
  922. $('body').append('<h2>Bobcat: 404 Torrent not found. You can now close this window</h2>');
  923. }
  924. }
  925.  
  926. }
  927. };
  928. if (window.location.href.indexOf('torcache') !== -1) {
  929. Torrentz.GM.BobCatTorcache.start();
  930. }
  931. else {
  932. Torrentz.GM.BobCatTorrentz.start();
  933. }