CSFD Movie Preview

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

// ==UserScript==
// @name        CSFD Movie Preview
// @namespace   http://csfd.cz
// @description Při najetí myší na odkaz na film se zobrazí náhled jeho profilu.
// @match       https://www.csfd.cz/*
// @match       https://www.csfd.sk/*
// @exclude     https://www.csfd.cz/uzivatel/*/editace/
// @exclude     https://www.csfd.sk/uzivatel/*/editace/
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require     https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
// @grant       GM_registerMenuCommand
// @grant       GM.registerMenuCommand
// @grant       GM_xmlhttpRequest
// @grant       GM.xmlHttpRequest
// @grant       GM_getValue
// @grant       GM.getValue
// @grant       GM_setValue
// @grant       GM.setValue
// @version     2.5
// ==/UserScript==

// CHANGES
// -------
// 2.5 - do náhledu vráceno hodnocení
// 2.4 - do náhledu vrácen název filmu
// 2.3 - opraveno načítání náhledů u epizod seriálů, úprava URL adres
// 2.2 - úpravy kvůli novému designu webu
// 2.1 - opraveno přepínání automatického nahrávání náhledů filmů
// 2.0 - GM_* funkce nahrazeny novými kvůli změně API v GreaseMonkey 4.0+
// 1.3 - upravena hlavička skriptu kvůli přechodu ČSFD na https
// 1.2 - doplněna podpora dynamicky přidávaných odkazů
// 1.1 - výměna jQuery.ajax(), který ve Firefoxu přestal fungovat, za GM_xmlhttpRequest()
// 1.0 - první verze

$ = this.jQuery = jQuery.noConflict(true);

$('<div id="movie-preview" style="display: none; z-index: 999; width: 420px; background-color: #efefef; padding: 6px; ' + 
  'border-radius: 4px; box-shadow: 0 0 10px 4px #777777"><table border="0"><tr><td id="movie-preview-poster" width="152" ' + 
  'style="text-align: center"></td><td id="movie-preview-content" style="vertical-align: top; padding-left: 7px"></td>' + 
  '</tr></table></div>').appendTo('body');

var cacheExpires = 7; // days

var movieBox = $('div#movie-preview');
var movieBoxPoster = movieBox.find('#movie-preview-poster');
var movieBoxContent = movieBox.find('#movie-preview-content');

var movieLinkSelector = 'a[href*="/film/"], a[href*="/film.php"]';

var thisPageMovieId = parseMovieId(window.location.href);
var currentMovieId = null;
var movies = [];

var timerId = -1;

// Greasmonkey-only section start

if (typeof GM.registerMenuCommand == 'function' && isStorageSupported()) {
	GM.registerMenuCommand("Přepnout automatické nahrávání náhledů filmů", function() {
        GM.getValue("doPrefetch", false).then(function(doPrefetch) {
            GM.setValue("doPrefetch", !doPrefetch);

            alert("Automatické nahrávání náhledů filmů " + (doPrefetch? "vypnuto": "zapnuto") + ".\nZměna nastavení se projeví po obnovení stránky.");
        });
    });
}

// Greasmonkey-only section end

function isStorageSupported() {
    return typeof(Storage) !== void(0);
}

function parseMovieId(movieURL) {
	var match = movieURL.match(/\/film(?:\.php\?)?(?:\/[\d]+)?.*\/([\d]+)/);
    
    return match && match.length >= 2? 'm' + match[1]: null;
}

function getDiffDays(date1, date2) {
	return Math.round(Math.abs(date1 - date2) / (1000 * 3600 * 24));
}

var storage = isStorageSupported()?
	{ // local storage
		getStoredItem: function(movieURL) {
    		return localStorage[parseMovieId(movieURL)];
		},
		setStoredItem: function(movieURL, value) {
			try {
				localStorage[parseMovieId(movieURL)] = value;
			} catch (ex) {
				// "Persistent storage maximum size reached" -> remove 10 random items
				for (var i=0; i < 10; i++) {
					var index = Math.floor(Math.random() * localStorage.length);
					var key = localStorage.key(index);

					localStorage.removeItem(key);
				}

				return this.setStoredItem(movieURL, value);
			}
		},
        cleanExpiredData: function() {
			var lastCleanup = localStorage["last-cleanup"]? Date.parse(localStorage["last-cleanup"]): new Date(0);

			// run cleanup only once per day
			if (getDiffDays(new Date(), lastCleanup) < 1) return;

            for(var key in localStorage) {
				if (key.match(/m\d+/)) {
					var cached = JSON.parse(localStorage[key]);
					
					if (getDiffDays(new Date(), Date.parse(cached.timestamp)) > cacheExpires) {
						localStorage.removeItem(key);
					}
				}
			}

			localStorage["last-cleanup"] = new Date();
        }
    }:
	{ // dummy storage
		getStoredItem: function(movieURL) {
    		return null;
		},
		setStoredItem: function(movieURL, value) {
    		// noop
		},
        cleanExpiredData: function() {
            // noop
        }
    };

function getMovieBoxPosition(event) {
	var boxWidth = movieBox.width() + 10;
	var tPosX = boxWidth - event.clientX + 30 > 0? event.pageX + 30: event.pageX - boxWidth - 30;
	var tPosY = event.pageY + event.clientY;

	if (event.clientY > 30) {
		var winHeight = $(window).height();
		var boxHeight = movieBox.height() > winHeight? winHeight - 60: movieBox.height();
		var overflowY = event.clientY + boxHeight - winHeight;
		tPosY = overflowY > 0? event.pageY - overflowY - 50: event.pageY - 30;
	}

    return { X: tPosX, Y: tPosY };
}
    
function showMovieBox(event, profile, rating) {
    var poster   = profile.find(".film-posters img");
    var title    = "<h1 style='font-size: 22px; padding-bottom: 12px'>" + profile.find(".film-header-name h1").text().trim() + "</h1>";
    var genre    = profile.find(".genres");
    var origin   = profile.find(".origin");
    var creators = profile.find(".creators");

    movieBoxPoster.html('');
    movieBoxPoster.append(poster.css('width', 140));
    movieBoxPoster.append('<h1 style="font-size: 32px; margin-top: 12px">' + rating + '</h1>');

    movieBoxContent.html('');
    movieBoxContent.append(title);
    movieBoxContent.append(genre.css('font-weight', 'bold'));
    movieBoxContent.append(origin.css('font-weight', 'bold'));
    movieBoxContent.append('<br>');
    movieBoxContent.append(creators);

    var pos = getMovieBoxPosition(event);
    
    movieBox.css({ 'position': 'absolute', 'top': pos.Y, 'left': pos.X }).show();
}

function getCachedData(movieURL) {
    var cached = storage.getStoredItem(movieURL);

	if (cached) {
		cached = JSON.parse(cached);

		if (getDiffDays(new Date(), Date.parse(cached.timestamp)) <= cacheExpires)
			return { "profile": $(cached.profile), "rating": cached.rating };
	}

	return null;
}
    
function loadMovieBox(movieURL, doneCallback, errorCallback, redirectMovieURL) {
	if (!redirectMovieURL) redirectMovieURL = movieURL;

	console.log("[CSFD Movie Preview] Loading movie page: " + redirectMovieURL);

	GM.xmlHttpRequest({
		method: "GET",
		url: redirectMovieURL,
		onload: function(response) {
			try {
				if (false /* TODO: handle redirect */) {
					loadMovieBox(movieURL, doneCallback, errorCallback, response.redirect);
				} else {
					response = $(response.responseText);

					var profile = response.find(".film-info").html().replace(/[\t\n]+/mg, ' ');
					var rating  = response.find(".film-info .film-rating-average").text().trim();
					
					storage.setStoredItem(movieURL, JSON.stringify({ "profile": profile, "rating": rating, "timestamp": new Date() }));
					
					if (doneCallback) doneCallback($(profile), rating);
				}
			} catch(ex) {
				console.log("[CSFD Movie Preview] Error in AJAX handler: " + ex.message);

				if (errorCallback) errorCallback();
			}
		},
		onerror: function(response) {
			if (errorCallback) errorCallback();
		}
	});
}

function prefetchMovies() {
	if (!isStorageSupported()) return;
	
    GM.getValue("doPrefetch", false).then(function(doPrefetch) {
		var movieURL;

		if (doPrefetch && (movieURL = movies.shift())) {
			setTimeout(function() {
				if (!getCachedData(movieURL)) {
					loadMovieBox(movieURL, prefetchMovies, prefetchMovies);
				} else {
					prefetchMovies();
				}
			}, 300);
		}
	});
}

function addHoverHandler(element) {
	element.hover(function(event) {
		var movieURL = $(this).attr("href").trim();
		var movieId  = parseMovieId(movieURL);

		// prevent previews of the movie on its page
		if (thisPageMovieId == movieId) return;

		currentMovieId = movieId;

		var cached = getCachedData(movieURL);
	  
		if (cached) {
			showMovieBox(event, cached.profile, cached.rating);
		} else {
			clearTimeout(timerId);

			timerId = setTimeout(function() {
				loadMovieBox(movieURL, function(profile, rating) {
					if (currentMovieId == movieId) showMovieBox(event, profile, rating);
				});
			}, 30);
		}
	}, function() {
		clearTimeout(timerId);
		timerId = -1;
		
		currentMovieId = null;

		movieBox.hide();
	});
}

function setupMutationObserver() {
	var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

	var observer = new MutationObserver(function(mutations) {
		mutations.forEach(function(mutation) {
			for (var i=0; i < mutation.addedNodes.length; i++) {
				$(mutation.addedNodes[i]).find("a").each(function() {
					if (this.href && this.href.match(/\/film/)) {
						addHoverHandler($(this));

						var movieURL = this.href.trim();
						movies.push(movieURL);
					}
				});
			}
		});

		prefetchMovies();
	});

	observer.observe(document.querySelector("body"), {
		childList: true,
		subtree: true
	});
}

// program start

storage.cleanExpiredData();

$(movieLinkSelector).each(function() {
	addHoverHandler($(this));

	var movieURL = $(this).attr("href").trim();
	movies.push(movieURL);
});

setupMutationObserver();

prefetchMovies();

// program end