CSFD Movie Preview

Při najetí myší na odkaz na film se zobrazí náhled jeho profilu.

  1. // ==UserScript==
  2. // @name CSFD Movie Preview
  3. // @namespace http://csfd.cz
  4. // @description Při najetí myší na odkaz na film se zobrazí náhled jeho profilu.
  5. // @match https://www.csfd.cz/*
  6. // @match https://www.csfd.sk/*
  7. // @exclude https://www.csfd.cz/uzivatel/*/editace/
  8. // @exclude https://www.csfd.sk/uzivatel/*/editace/
  9. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
  11. // @grant GM_registerMenuCommand
  12. // @grant GM.registerMenuCommand
  13. // @grant GM_xmlhttpRequest
  14. // @grant GM.xmlHttpRequest
  15. // @grant GM_getValue
  16. // @grant GM.getValue
  17. // @grant GM_setValue
  18. // @grant GM.setValue
  19. // @version 2.5
  20. // ==/UserScript==
  21.  
  22. // CHANGES
  23. // -------
  24. // 2.5 - do náhledu vráceno hodnocení
  25. // 2.4 - do náhledu vrácen název filmu
  26. // 2.3 - opraveno načítání náhledů u epizod seriálů, úprava URL adres
  27. // 2.2 - úpravy kvůli novému designu webu
  28. // 2.1 - opraveno přepínání automatického nahrávání náhledů filmů
  29. // 2.0 - GM_* funkce nahrazeny novými kvůli změně API v GreaseMonkey 4.0+
  30. // 1.3 - upravena hlavička skriptu kvůli přechodu ČSFD na https
  31. // 1.2 - doplněna podpora dynamicky přidávaných odkazů
  32. // 1.1 - výměna jQuery.ajax(), který ve Firefoxu přestal fungovat, za GM_xmlhttpRequest()
  33. // 1.0 - první verze
  34.  
  35. $ = this.jQuery = jQuery.noConflict(true);
  36.  
  37. $('<div id="movie-preview" style="display: none; z-index: 999; width: 420px; background-color: #efefef; padding: 6px; ' +
  38. 'border-radius: 4px; box-shadow: 0 0 10px 4px #777777"><table border="0"><tr><td id="movie-preview-poster" width="152" ' +
  39. 'style="text-align: center"></td><td id="movie-preview-content" style="vertical-align: top; padding-left: 7px"></td>' +
  40. '</tr></table></div>').appendTo('body');
  41.  
  42. var cacheExpires = 7; // days
  43.  
  44. var movieBox = $('div#movie-preview');
  45. var movieBoxPoster = movieBox.find('#movie-preview-poster');
  46. var movieBoxContent = movieBox.find('#movie-preview-content');
  47.  
  48. var movieLinkSelector = 'a[href*="/film/"], a[href*="/film.php"]';
  49.  
  50. var thisPageMovieId = parseMovieId(window.location.href);
  51. var currentMovieId = null;
  52. var movies = [];
  53.  
  54. var timerId = -1;
  55.  
  56. // Greasmonkey-only section start
  57.  
  58. if (typeof GM.registerMenuCommand == 'function' && isStorageSupported()) {
  59. GM.registerMenuCommand("Přepnout automatické nahrávání náhledů filmů", function() {
  60. GM.getValue("doPrefetch", false).then(function(doPrefetch) {
  61. GM.setValue("doPrefetch", !doPrefetch);
  62.  
  63. alert("Automatické nahrávání náhledů filmů " + (doPrefetch? "vypnuto": "zapnuto") + ".\nZměna nastavení se projeví po obnovení stránky.");
  64. });
  65. });
  66. }
  67.  
  68. // Greasmonkey-only section end
  69.  
  70. function isStorageSupported() {
  71. return typeof(Storage) !== void(0);
  72. }
  73.  
  74. function parseMovieId(movieURL) {
  75. var match = movieURL.match(/\/film(?:\.php\?)?(?:\/[\d]+)?.*\/([\d]+)/);
  76. return match && match.length >= 2? 'm' + match[1]: null;
  77. }
  78.  
  79. function getDiffDays(date1, date2) {
  80. return Math.round(Math.abs(date1 - date2) / (1000 * 3600 * 24));
  81. }
  82.  
  83. var storage = isStorageSupported()?
  84. { // local storage
  85. getStoredItem: function(movieURL) {
  86. return localStorage[parseMovieId(movieURL)];
  87. },
  88. setStoredItem: function(movieURL, value) {
  89. try {
  90. localStorage[parseMovieId(movieURL)] = value;
  91. } catch (ex) {
  92. // "Persistent storage maximum size reached" -> remove 10 random items
  93. for (var i=0; i < 10; i++) {
  94. var index = Math.floor(Math.random() * localStorage.length);
  95. var key = localStorage.key(index);
  96.  
  97. localStorage.removeItem(key);
  98. }
  99.  
  100. return this.setStoredItem(movieURL, value);
  101. }
  102. },
  103. cleanExpiredData: function() {
  104. var lastCleanup = localStorage["last-cleanup"]? Date.parse(localStorage["last-cleanup"]): new Date(0);
  105.  
  106. // run cleanup only once per day
  107. if (getDiffDays(new Date(), lastCleanup) < 1) return;
  108.  
  109. for(var key in localStorage) {
  110. if (key.match(/m\d+/)) {
  111. var cached = JSON.parse(localStorage[key]);
  112. if (getDiffDays(new Date(), Date.parse(cached.timestamp)) > cacheExpires) {
  113. localStorage.removeItem(key);
  114. }
  115. }
  116. }
  117.  
  118. localStorage["last-cleanup"] = new Date();
  119. }
  120. }:
  121. { // dummy storage
  122. getStoredItem: function(movieURL) {
  123. return null;
  124. },
  125. setStoredItem: function(movieURL, value) {
  126. // noop
  127. },
  128. cleanExpiredData: function() {
  129. // noop
  130. }
  131. };
  132.  
  133. function getMovieBoxPosition(event) {
  134. var boxWidth = movieBox.width() + 10;
  135. var tPosX = boxWidth - event.clientX + 30 > 0? event.pageX + 30: event.pageX - boxWidth - 30;
  136. var tPosY = event.pageY + event.clientY;
  137.  
  138. if (event.clientY > 30) {
  139. var winHeight = $(window).height();
  140. var boxHeight = movieBox.height() > winHeight? winHeight - 60: movieBox.height();
  141. var overflowY = event.clientY + boxHeight - winHeight;
  142. tPosY = overflowY > 0? event.pageY - overflowY - 50: event.pageY - 30;
  143. }
  144.  
  145. return { X: tPosX, Y: tPosY };
  146. }
  147. function showMovieBox(event, profile, rating) {
  148. var poster = profile.find(".film-posters img");
  149. var title = "<h1 style='font-size: 22px; padding-bottom: 12px'>" + profile.find(".film-header-name h1").text().trim() + "</h1>";
  150. var genre = profile.find(".genres");
  151. var origin = profile.find(".origin");
  152. var creators = profile.find(".creators");
  153.  
  154. movieBoxPoster.html('');
  155. movieBoxPoster.append(poster.css('width', 140));
  156. movieBoxPoster.append('<h1 style="font-size: 32px; margin-top: 12px">' + rating + '</h1>');
  157.  
  158. movieBoxContent.html('');
  159. movieBoxContent.append(title);
  160. movieBoxContent.append(genre.css('font-weight', 'bold'));
  161. movieBoxContent.append(origin.css('font-weight', 'bold'));
  162. movieBoxContent.append('<br>');
  163. movieBoxContent.append(creators);
  164.  
  165. var pos = getMovieBoxPosition(event);
  166. movieBox.css({ 'position': 'absolute', 'top': pos.Y, 'left': pos.X }).show();
  167. }
  168.  
  169. function getCachedData(movieURL) {
  170. var cached = storage.getStoredItem(movieURL);
  171.  
  172. if (cached) {
  173. cached = JSON.parse(cached);
  174.  
  175. if (getDiffDays(new Date(), Date.parse(cached.timestamp)) <= cacheExpires)
  176. return { "profile": $(cached.profile), "rating": cached.rating };
  177. }
  178.  
  179. return null;
  180. }
  181. function loadMovieBox(movieURL, doneCallback, errorCallback, redirectMovieURL) {
  182. if (!redirectMovieURL) redirectMovieURL = movieURL;
  183.  
  184. console.log("[CSFD Movie Preview] Loading movie page: " + redirectMovieURL);
  185.  
  186. GM.xmlHttpRequest({
  187. method: "GET",
  188. url: redirectMovieURL,
  189. onload: function(response) {
  190. try {
  191. if (false /* TODO: handle redirect */) {
  192. loadMovieBox(movieURL, doneCallback, errorCallback, response.redirect);
  193. } else {
  194. response = $(response.responseText);
  195.  
  196. var profile = response.find(".film-info").html().replace(/[\t\n]+/mg, ' ');
  197. var rating = response.find(".film-info .film-rating-average").text().trim();
  198. storage.setStoredItem(movieURL, JSON.stringify({ "profile": profile, "rating": rating, "timestamp": new Date() }));
  199. if (doneCallback) doneCallback($(profile), rating);
  200. }
  201. } catch(ex) {
  202. console.log("[CSFD Movie Preview] Error in AJAX handler: " + ex.message);
  203.  
  204. if (errorCallback) errorCallback();
  205. }
  206. },
  207. onerror: function(response) {
  208. if (errorCallback) errorCallback();
  209. }
  210. });
  211. }
  212.  
  213. function prefetchMovies() {
  214. if (!isStorageSupported()) return;
  215. GM.getValue("doPrefetch", false).then(function(doPrefetch) {
  216. var movieURL;
  217.  
  218. if (doPrefetch && (movieURL = movies.shift())) {
  219. setTimeout(function() {
  220. if (!getCachedData(movieURL)) {
  221. loadMovieBox(movieURL, prefetchMovies, prefetchMovies);
  222. } else {
  223. prefetchMovies();
  224. }
  225. }, 300);
  226. }
  227. });
  228. }
  229.  
  230. function addHoverHandler(element) {
  231. element.hover(function(event) {
  232. var movieURL = $(this).attr("href").trim();
  233. var movieId = parseMovieId(movieURL);
  234.  
  235. // prevent previews of the movie on its page
  236. if (thisPageMovieId == movieId) return;
  237.  
  238. currentMovieId = movieId;
  239.  
  240. var cached = getCachedData(movieURL);
  241. if (cached) {
  242. showMovieBox(event, cached.profile, cached.rating);
  243. } else {
  244. clearTimeout(timerId);
  245.  
  246. timerId = setTimeout(function() {
  247. loadMovieBox(movieURL, function(profile, rating) {
  248. if (currentMovieId == movieId) showMovieBox(event, profile, rating);
  249. });
  250. }, 30);
  251. }
  252. }, function() {
  253. clearTimeout(timerId);
  254. timerId = -1;
  255. currentMovieId = null;
  256.  
  257. movieBox.hide();
  258. });
  259. }
  260.  
  261. function setupMutationObserver() {
  262. var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  263.  
  264. var observer = new MutationObserver(function(mutations) {
  265. mutations.forEach(function(mutation) {
  266. for (var i=0; i < mutation.addedNodes.length; i++) {
  267. $(mutation.addedNodes[i]).find("a").each(function() {
  268. if (this.href && this.href.match(/\/film/)) {
  269. addHoverHandler($(this));
  270.  
  271. var movieURL = this.href.trim();
  272. movies.push(movieURL);
  273. }
  274. });
  275. }
  276. });
  277.  
  278. prefetchMovies();
  279. });
  280.  
  281. observer.observe(document.querySelector("body"), {
  282. childList: true,
  283. subtree: true
  284. });
  285. }
  286.  
  287. // program start
  288.  
  289. storage.cleanExpiredData();
  290.  
  291. $(movieLinkSelector).each(function() {
  292. addHoverHandler($(this));
  293.  
  294. var movieURL = $(this).attr("href").trim();
  295. movies.push(movieURL);
  296. });
  297.  
  298. setupMutationObserver();
  299.  
  300. prefetchMovies();
  301.  
  302. // program end