CSFD Movie Preview

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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