ČSFD Extended

Extension of film profiles on ČSFD with features such as IMDb ratings or a link to justwatch.com.

// ==UserScript==
// @name         ČSFD Extended
// @name:en      ČSFD Extended
// @version      2.10.0
// @description  Rozšíření profilů filmů na ČSFD o funkce jako je hodnocení IMDB či odkaz na justwatch.com.
// @description:en Extension of film profiles on ČSFD with features such as IMDb ratings or a link to justwatch.com.
// @author       Jakub Rychecký <jakub@rychecky.cz>
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license      WTFPL 2
// @include      *csfd.cz/film/*
// @include      *csfd.sk/film/*
// @namespace CSFD-E
// ==/UserScript==
/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
var __webpack_exports__ = {};

;// CONCATENATED MODULE: ./src/classes/Csfd.js
class Csfd {

    constructor(csfdPage) {
        this.csfdPage = csfdPage
    }

    isLoggedIn() {
        return this.csfdPage.find('.my-rating').length > 0;
    }

    getImdbCode() {
        let imdbButton = this.csfdPage.find('a.button-imdb');

        return imdbButton.length > 0
            ? imdbButton.attr('href').match(/(tt\d+)/)[0]
            : null;
    }

    getCurrentUserRatingDate() {
        let ratingDateInText = this.csfdPage.find('.current-user-rating > span').attr('title');

        if (ratingDateInText === undefined) {
            return null;
        }

        return ratingDateInText.match(/.+(\d{2}\.\d{2}\.\d{4})$/)[1];
    }

    isMarkedAsWantToWatch() {
        let controlPanelText = this.csfdPage.find('.control-panel').text();

        return controlPanelText.includes('Upravit ve Chci vidět')
            || controlPanelText.includes('Upraviť v Chcem vidieť');
    }

    getLinkingDataMovieTitle() {
        let linkingDataJson = JSON.parse($('script[type="application/ld+json"]')[0].innerHTML);

        return linkingDataJson.name + ' ' + linkingDataJson.dateCreated;
    }

}

;// CONCATENATED MODULE: ./src/classes/ImdbRating.js
class ImdbRating {

    constructor(
        csfd,
        imdbRating,
        imdbVotes
    ) {
        this.csfd = csfd;

        this.initializeImdbRating(imdbRating, imdbVotes);
    }

    initializeImdbRating(
        imdbRating,
        imdbVotes
    ) {
        if (
            imdbRating === undefined
            || imdbRating === 'N/A'
            || imdbVotes === undefined
            || imdbVotes === 'N/A'
        ) {
            return;
        }

        let imdbVotesSpan = $('<span>')
            .css({
                'display': 'block',
                'font-size': '9px',
                'font-weight': 'normal',
                'line-height': '10px',
                'padding-bottom': '8px',
            })
            .html('<strong>' + imdbVotes + '</strong> hlasů');

        let imdbRatingBox = $('<a>')
            .addClass('rating-average csfd-extended-imdb-rating')
            .css({
                'display': 'block',
                'color': '#000000',
                'cursor': 'pointer',
                'line-height': '60px',
                'text-align': 'center',
                'font-size': '40px',
                'font-weight': 'bold',
            })
            .attr('href', 'https://www.imdb.com/title/' + this.csfd.getImdbCode())
            .html(imdbRating)
            .append(imdbVotesSpan);

        imdbRatingBox
            .hover(
                (e) => {
                    imdbRatingBox.css({
                        'background': '#F5BE18FF',
                    })
                },
                (e) => {
                    imdbRatingBox.css({
                        'background': '#F5C518',
                    })
                },
            )
            .trigger('mouseleave');

        imdbRatingBox.insertBefore(this.csfd.csfdPage.find('.my-rating'));
    }

}

;// CONCATENATED MODULE: ./src/classes/Omdb.js


class Omdb {

    constructor(
        csfd,
        omdbApiKey,
        cache
    ) {
        this.csfd = csfd;
        this.omdbApiKey = omdbApiKey;
        this.cache = cache;

        this.getResponse();
    }

    getResponse() {
        let imdbCode = this.csfd.getImdbCode();

        if (imdbCode === null) {
            return;
        }

        let cacheItem = this.cache.getItem(imdbCode);

        if (cacheItem !== null && !this.cache.isItemExpired(cacheItem)) {
            let responseFromCache = cacheItem.value;

            new ImdbRating(
                this.csfd,
                responseFromCache.imdbRating,
                responseFromCache.imdbVotes
            );

            return;
        }

        let request = $.ajax({
            method: 'GET',
            url: 'https://omdbapi.com/',
            data: {
                apikey: this.omdbApiKey,
                i: imdbCode,
                r: 'json'
            },
        });

        request.done((response) => {
            if (
                response.imdbRating !== undefined
                && response.imdbRating !== 'N/A'
            ) {
                this.cache.saveItem(imdbCode, response);
            }

            new ImdbRating(
                this.csfd,
                response.imdbRating,
                response.imdbVotes
            )

            this.response = response;
        });
    }

}

;// CONCATENATED MODULE: ./src/classes/Toolbar.js
class Toolbar {

    constructor(
        csfd
    ) {
        this.csfd = csfd;

        this.initializeToolbar();
    }

    initializeToolbar() {
        let boxButtons = this.csfd.csfdPage.find('.box-rating-container .box-buttons');

        let imdbCode = this.csfd.getImdbCode();
        let encodedLinkingDataMovieTitle = encodeURIComponent(this.csfd.getLinkingDataMovieTitle());

        boxButtons.prepend(
            this.createButton(
                'Titulky.com',
                null,
                'http://www.titulky.com/?Fulltext=' + encodedLinkingDataMovieTitle
            ),
            this.createButton(
                'Trakt.TV',
                null,
                'https://trakt.tv/search/imdb?q=' + imdbCode
            ),
            this.createButton(
                'JustWatch',
                null,
                'https://www.justwatch.com/cz/vyhled%C3%A1n%C3%AD?q=' + encodedLinkingDataMovieTitle
            ),
            this.createButton(
                'Google',
                null,
                'https://www.google.cz/search?q=' + encodedLinkingDataMovieTitle
            ),
            this.createButton(
                'YouTube',
                null,
                'https://www.youtube.com/results?search_query=' + encodedLinkingDataMovieTitle
            ),
            this.createButton(
                'BoxOffice',
                null,
                'http://www.boxofficemojo.com/search/?q=' + encodedLinkingDataMovieTitle
            ),
            this.createButton(
                'Webshare.cz',
                'pirate',
                'https://webshare.cz/#/search?what=' + encodedLinkingDataMovieTitle
            ),
            this.createButton(
                'YIFY',
                'pirate',
                'https://www.google.cz/search?q=' + encodedLinkingDataMovieTitle + ' site:yts.mx OR site:yify-movies.net'
            ),
            this.createButton(
                'Torrent',
                'pirate',
                'http://www.aiosearch.com/search/4/Torrents/' + encodedLinkingDataMovieTitle
            ),
        );
    }

    createButton(
        name,
        style,
        url,
    ) {
        let backgroundColor = '#DE5254';
        let fontColor = '#FFF';
        let iconClass = 'icon-globe-circle';

        if (style === 'pirate') {
            backgroundColor = '#A2A2A2';
            iconClass = 'icon-folder';
        }

        let button = $('<a>')
            .attr('href', url)
            .addClass('button button-big')
            .css({
                'background-color': backgroundColor,
                'color': fontColor,
                'padding-left': '6px',
            })
            .html('<i class="icon ' + iconClass + '"></i>' + name);

        button.hover(
            (e) => {
                $(e.target).css({
                    'opacity': 1.0,
                });
            },
            (e) => {
                $(e.target).css({
                    'opacity': 0.95,
                });
            },
        );

        button.trigger('mouseleave');

        return button;
    }

}

;// CONCATENATED MODULE: ./src/classes/UserRating.js
class UserRating {

    constructor(
        csfd
    ) {
        this.csfd = csfd;

        this.initializeUserRating();
    }

    initializeUserRating() {
        let currentUserRatingDate = this.csfd.getCurrentUserRatingDate();

        if (currentUserRatingDate === null) {
            return;
        }

        let currentUserRatingBoxTitle = this.csfd.csfdPage.find('.my-rating h3');

        if (currentUserRatingBoxTitle.length === 0) {
            return;
        }

        currentUserRatingBoxTitle.text('Hodnoceno ' + currentUserRatingDate);
    }

}

;// CONCATENATED MODULE: ./src/classes/WantToWatch.js
class WantToWatch {

    constructor(
        csfd
    ) {
        this.csfd = csfd;

        this.initializeWantToWatch();
    }

    initializeWantToWatch() {
        if (!this.csfd.isMarkedAsWantToWatch()) {
            return;
        }

        let wantToWatch = $('<a>')
            .attr('href', '?name=watchlist&do=modalWindow')
            .css({
                'background': '#BA034F',
                'border-top': '1px solid #D2D2D2',
                'color': '#FFFFFF',
                'display': 'block',
                'opacity': 0.8,
                'padding': '5px',
                'text-align': 'center',
            })
            .html('👁️ Chci vidět');

        wantToWatch.hover(
            (e) => {
                $(e.target).animate({
                    'opacity': 1.0,
                });
            },
            (e) => {
                $(e.target).animate({
                    'opacity': 0.8,
                });
            },
        );

        this.csfd.csfdPage.find('.tabs.tabs-rating.rating-fan-switch').prepend(wantToWatch);
    }

}

;// CONCATENATED MODULE: ./src/classes/ImageFloatingPreview.js


class ImageFloatingPreview {

    constructor(
        csfd
    ) {
        this.csfd = csfd;

        this.initializeImageFloatingPreview();
    }

    initializeImageFloatingPreview() {
        this.popup = $('<img>')
            .css({
                'box-shadow': '5px 5px 14px 8px rgba(0,0,0,0.75)',
                'z-index': 999,
            });
        $('body').append(this.popup);

        $('.creators a')
            .bind('mouseenter', (e) => {
                let creatorUrl = $(e.target).attr('href');

                this.hoverCreatorLink(creatorUrl);
                this.refreshPopupPosition(e.pageX, e.pageY);
            })
            .bind('mousemove', (e) => this.refreshPopupPosition(e.pageX, e.pageY))
            .bind('mouseleave', () => this.abort());
    }

    showPopup(
        imageUrl
    ) {
        this.popup.attr('src', imageUrl);
        this.popup.show();
    }

    hidePopup() {
        this.popup.attr('src', '');
        this.popup.hide();
    }

    refreshPopupPosition(
        x,
        y
    ) {
        this.popup.css({
            'position': 'absolute',
            'left': x + 15,
            'top': y + 15,
        })
    }

    abort() {
        this.currentRequest.abort();
        this.hidePopup();
    }

    hoverCreatorLink(
        url
    ) {
        this.currentRequest = $.ajax({
            method: 'GET',
            url: url,
        });

        this.currentRequest.done((response) => {
            if (
                typeof response === 'object'
                && 'redirect' in response
            ) {
                this.hoverCreatorLink(response.redirect);
                return;
            }

            let creatorImageUrl = $(response).find('.creator-profile-content>figure img').attr('src');

            if (creatorImageUrl !== undefined) {
                this.showPopup(creatorImageUrl);
            }
        });
    }

}

;// CONCATENATED MODULE: ./src/classes/CacheItem.js
class CacheItem {

    constructor(
        name,
        value,
        expireAt
    ) {
        this.name = name;
        this.expireAt = expireAt;
        this.value = value;
    }

}

;// CONCATENATED MODULE: ./src/classes/Cache.js


class Cache {

    constructor(
        expirationInSeconds
    ) {
        this.expirationInSeconds = 600;
        this.namespace = 'csfd-extended';
    }

    saveItem(
        key,
        value
    ) {
        let cacheItem = new CacheItem(
            this.addNamespaceToName(key),
            value,
            Math.floor(Date.now() / 1000) + this.expirationInSeconds
        )

        localStorage.setItem(
            this.addNamespaceToName(key),
            JSON.stringify(cacheItem)
        )
    }

    getItem(
        key
    ) {
        let cacheItem = localStorage.getItem(
            this.addNamespaceToName(key)
        );

        return cacheItem !== null
            ? JSON.parse(cacheItem)
            : null;
    }

    isItemExpired(
        caheItem
    ) {
        return caheItem.expireAt < Math.floor(Date.now() / 1000);
    }

    addNamespaceToName(name) {
        return this.namespace + '.' + name;
    }

}

;// CONCATENATED MODULE: ./src/index.js








let cache = new Cache(7 * 24 * 3600);

let csfd = new Csfd($('div.page-content'));
let omdb = new Omdb(csfd, 'ee2fe641', cache);
let userRating = new UserRating(csfd);
let wantToWatch = new WantToWatch(csfd);
let toolbar = new Toolbar(csfd);
let imageFloatingPreview = new ImageFloatingPreview(csfd);

/******/ })()
;